package main // gitlab_test.go — PAT connect + project mapping against httptest upstream. import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" ) func newGitLabUpstream(t *testing.T) (*httptest.Server, *GitLab) { t.Helper() mux := http.NewServeMux() mux.HandleFunc("/api/v4/user", func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Private-Token") != "pat-good" { w.WriteHeader(http.StatusUnauthorized) return } w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"username":"alice"}`)) }) mux.HandleFunc("/api/v4/projects", func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Private-Token") != "pat-good" { w.WriteHeader(http.StatusUnauthorized) return } q := r.URL.Query() if q.Get("membership") != "true" || q.Get("order_by") != "last_activity_at" { w.WriteHeader(http.StatusBadRequest) return } w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`[ {"path_with_namespace":"team/b","name":"B","namespace":{"path":"team"}, "last_activity_at":"2024-02-02T00:00:00Z","web_url":"https://gl/team/b","default_branch":"main"}, {"path_with_namespace":"team/a","name":"A","namespace":{"path":"team"}, "last_activity_at":"2024-03-03T00:00:00Z","web_url":"https://gl/team/a","default_branch":"trunk"} ]`)) }) ts := httptest.NewServer(mux) t.Cleanup(ts.Close) store := openTestStore(t) return ts, NewGitLab(store, ts.URL) } func TestGitLabConnectValidatesAndStores(t *testing.T) { _, gl := newGitLabUpstream(t) if status := gl.Status(); status["connected"] != false { t.Fatalf("status before connect = %v", status) } if _, err := gl.Connect(context.Background(), "pat-bad"); err == nil { t.Fatal("bad PAT should fail validation") } username, err := gl.Connect(context.Background(), "pat-good") if err != nil { t.Fatalf("connect: %v", err) } if username != "alice" { t.Fatalf("username = %q", username) } status := gl.Status() if status["connected"] != true || status["username"] != "alice" { t.Fatalf("status after connect = %v", status) } if strings.Contains(mustJSON(t, status), "pat-good") { t.Fatal("status must never expose the PAT") } if err := gl.Disconnect(); err != nil { t.Fatalf("disconnect: %v", err) } if status := gl.Status(); status["connected"] != false { t.Fatalf("status after disconnect = %v", status) } } func mustJSON(t *testing.T, v any) string { t.Helper() b, err := json.Marshal(v) if err != nil { t.Fatalf("marshal: %v", err) } return string(b) } func TestGitLabReposMappingAndSorting(t *testing.T) { _, gl := newGitLabUpstream(t) if _, err := gl.Repos(context.Background()); err == nil || !strings.Contains(err.Error(), "not connected") { t.Fatalf("repos without connect = %v, want not-connected error", err) } if _, err := gl.Connect(context.Background(), "pat-good"); err != nil { t.Fatalf("connect: %v", err) } repos, err := gl.Repos(context.Background()) if err != nil { t.Fatalf("repos: %v", err) } if len(repos) != 2 { t.Fatalf("repos = %d, want 2", len(repos)) } want := GitLabRepo{ Path: "team/a", Name: "A", Namespace: "team", LastActivityAt: "2024-03-03T00:00:00Z", WebURL: "https://gl/team/a", DefaultBranch: "trunk", } if repos[0] != want { t.Fatalf("first repo = %+v, want %+v (sorted by activity desc)", repos[0], want) } encoded := mustJSON(t, repos[0]) for _, key := range []string{`"path":"team/a"`, `"namespace":"team"`, `"webUrl":"https://gl/team/a"`, `"defaultBranch":"trunk"`} { if !strings.Contains(encoded, key) { t.Fatalf("repo JSON %s missing %s", encoded, key) } } }