from __future__ import annotations import asyncio from pathlib import Path import pytest from codexpi.commands import CommandResult from codexpi.gitlab_service import ( GitLabError, connect, connect_repository, create_repository, namespaces, normalize_server_url, status, ) def test_gitlab_server_url_requires_safe_explicit_transport(): assert normalize_server_url("https://gitlab.local/") == "https://gitlab.local" assert normalize_server_url("http://gitlab.local", True) == "http://gitlab.local" with pytest.raises(GitLabError, match="HTTP"): normalize_server_url("http://gitlab.local") for value in ("gitlab.local", "https://user:password@gitlab.local", "https://gitlab.local/?token=secret"): with pytest.raises(GitLabError): normalize_server_url(value) def test_gitlab_connection_adds_ssh_key_and_encrypts_token(tmp_path: Path, monkeypatch): calls: list[tuple[str, str, dict | None]] = [] def fake_request(_config, path, method="GET", payload=None): calls.append((path, method, payload)) if path == "user": return {"username": "alice", "name": "Alice"} if path.startswith("user/keys?"): return [] return {"id": 42} monkeypatch.setattr("codexpi.gitlab_service._request", fake_request) monkeypatch.setattr("codexpi.gitlab_service.generate_key", lambda _data: {"public_key": "ssh-ed25519 AAAATEST codexpi"}) value = connect(tmp_path, "application-secret", "https://gitlab.local", "glpat-top-secret") encrypted = (tmp_path / "gitlab" / "connection.json").read_bytes() assert value["configured"] is True assert value["account"] == "alice" assert "token" not in value assert b"glpat-top-secret" not in encrypted assert any(path == "user/keys" and method == "POST" for path, method, _ in calls) def test_gitlab_does_not_add_an_existing_ssh_key(tmp_path: Path, monkeypatch): calls: list[str] = [] def fake_request(_config, path, method="GET", payload=None): calls.append(f"{method} {path}") if path == "user": return {"username": "alice"} return [{"key": "ssh-ed25519 AAAATEST another-comment"}] monkeypatch.setattr("codexpi.gitlab_service._request", fake_request) monkeypatch.setattr("codexpi.gitlab_service.generate_key", lambda _data: {"public_key": "ssh-ed25519 AAAATEST codexpi"}) connect(tmp_path, "secret", "https://gitlab.local", "glpat-example") assert "POST user/keys" not in calls def test_gitlab_namespaces_and_repository_creation_use_saved_connection(tmp_path: Path, monkeypatch): def connect_request(_config, path, method="GET", payload=None): if path == "user": return {"username": "alice"} if path.startswith("user/keys?"): return [{"key": "ssh-ed25519 AAAATEST"}] raise AssertionError(path) monkeypatch.setattr("codexpi.gitlab_service._request", connect_request) monkeypatch.setattr("codexpi.gitlab_service.generate_key", lambda _data: {"public_key": "ssh-ed25519 AAAATEST"}) connect(tmp_path, "secret", "https://gitlab.local", "glpat-example") def api_request(config, path, method="GET", payload=None): assert config["token"] == "glpat-example" if path.startswith("namespaces?"): return [{"id": 7, "name": "Alice", "full_path": "alice", "kind": "user"}] assert method == "POST" and payload["namespace_id"] == 7 return {"id": 9, "name": "Demo", "path_with_namespace": "alice/demo", "visibility": "private", "web_url": "https://gitlab.local/alice/demo", "ssh_url_to_repo": "git@gitlab.local:alice/demo.git"} monkeypatch.setattr("codexpi.gitlab_service._request", api_request) assert namespaces(tmp_path, "secret")[0]["full_path"] == "alice" repository = create_repository(tmp_path, "secret", "Demo", "demo", 7, "private") assert repository["ssh_url_to_repo"] == "git@gitlab.local:alice/demo.git" def test_gitlab_repository_is_initialized_and_linked(tmp_path: Path, monkeypatch): data = tmp_path / "data" key = data / "github" / "id_ed25519" key.parent.mkdir(parents=True) key.write_text("private", encoding="utf-8") root = tmp_path / "project" root.mkdir() async def fake_command(*_args, **_kwargs): return CommandResult([], 0, "", "") monkeypatch.setattr("codexpi.gitlab_service.run_command", fake_command) value = asyncio.run(connect_repository(root, data, "git@gitlab.local:alice/demo.git")) assert value["is_repository"] is True assert value["remote_url"] == "git@gitlab.local:alice/demo.git" def test_gitlab_api_connects_and_creates_repository(logged_in, monkeypatch): client, _app, projects, _, headers = logged_in root = projects / "gitlab-project" root.mkdir() project = client.post("/api/projects", headers=headers, json={"name": "GitLab", "path": str(root)}).json() monkeypatch.setattr("codexpi.main.connect_gitlab", lambda *_args: {"configured": True, "server_url": "https://gitlab.local", "account": "alice", "name": "Alice", "verified_at": "now", "verify_tls": True}) monkeypatch.setattr("codexpi.main.create_gitlab_repository", lambda *_args: {"name": "Demo", "path_with_namespace": "alice/demo", "web_url": "https://gitlab.local/alice/demo", "ssh_url_to_repo": "git@gitlab.local:alice/demo.git"}) async def fake_link(*_args): return {"is_repository": True, "remote_url": "git@gitlab.local:alice/demo.git"} monkeypatch.setattr("codexpi.main.connect_gitlab_repository", fake_link) assert client.post("/api/gitlab/connect", json={"server_url": "https://gitlab.local", "token": "secret"}).status_code == 403 connected = client.post("/api/gitlab/connect", headers=headers, json={"server_url": "https://gitlab.local", "token": "secret"}) assert connected.status_code == 200 created = client.post(f"/api/projects/{project['id']}/gitlab/repositories", headers=headers, json={"name": "Demo", "path": "demo", "namespace_id": 7, "visibility": "private"}) assert created.status_code == 201 assert created.json()["git"]["remote_url"] == "git@gitlab.local:alice/demo.git"