package main // gitlab_extra_test.go — error taxonomy, insecure client seam, decode failures. import ( "context" "fmt" "net/http" "net/http/httptest" "strconv" "strings" "testing" ) func TestGitLabErrorType(t *testing.T) { err := &GitLabError{msg: "upstream sad"} if err.Error() != "upstream sad" { t.Fatalf("GitLabError.Error = %q", err.Error()) } } func TestNewGitLabWithClientInsecure(t *testing.T) { store := openTestStore(t) gl := NewGitLabWithClient(store, "https://gl.example/", true) if !gl.insecure { t.Fatal("insecure flag not set") } hc := gl.httpClient() if hc.Transport == nil { t.Fatal("insecure client must install a custom transport") } plain := NewGitLab(store, "https://gl.example/") if plain.httpClient().Transport != nil { t.Fatal("default client must not override the transport") } if !strings.HasSuffix(gl.baseURL, "example") || strings.HasSuffix(gl.baseURL, "/") { t.Fatalf("baseURL trailing slash not trimmed: %q", gl.baseURL) } } func TestGitLabConnectDecodeAndEmptyUser(t *testing.T) { cases := []struct { name string body string want string }{ {"decode-failure", "not json", "decode"}, {"empty-login", `{}`, "no login"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(tc.body)) })) t.Cleanup(up.Close) gl := NewGitLab(openTestStore(t), up.URL) _, err := gl.Connect(context.Background(), "pat") if err == nil || !strings.Contains(err.Error(), tc.want) { t.Fatalf("Connect = %v, want containing %q", err, tc.want) } }) } } func TestGitLabRequestFailures(t *testing.T) { store := openTestStore(t) // unparseable base URL → request construction error gl := NewGitLab(store, "https://gl.example/\n") if _, err := gl.Connect(context.Background(), "pat"); err == nil { t.Fatal("connect against unparseable URL must fail") } // transport failure (cancelled context) wraps into GitLabError gl2 := NewGitLab(store, "https://gl.example") ctx, cancel := context.WithCancel(context.Background()) cancel() _, err := gl2.Connect(ctx, "pat") if err == nil || !strings.Contains(err.Error(), "gitlab request failed") { t.Fatalf("connect with dead context = %v, want GitLabError", err) } // store failures surface from token() and Disconnect dead := openTestStore(t) _ = dead.Close() gl3 := NewGitLab(dead, "https://gl.example") if _, err := gl3.Repos(context.Background()); err == nil || !strings.Contains(err.Error(), "closed") { t.Fatalf("repos with closed store = %v", err) } if err := gl3.Disconnect(); err == nil { t.Fatal("disconnect with closed store must fail") } } func TestGitLabStatusStoreFailure(t *testing.T) { dead := openTestStore(t) _ = dead.Close() gl := NewGitLab(dead, "https://gl.example") status := gl.Status() if status["connected"] != false { t.Fatalf("status with broken store = %v, want connected=false", status) } } func TestGitLabConnectStoreFailures(t *testing.T) { up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"login":"alice"}`)) })) t.Cleanup(up.Close) dead := openTestStore(t) _ = dead.Close() gl := NewGitLab(dead, up.URL) if _, err := gl.Connect(context.Background(), "pat"); err == nil { t.Fatal("Connect with closed store must fail at SetSetting") } } func TestGitLabReposSkipsEmptyPaths(t *testing.T) { up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`[{"full_name":"","name":"X"}]`)) })) t.Cleanup(up.Close) store := openTestStore(t) gl := NewGitLab(store, up.URL) if err := store.SetSetting(settingGitLabToken, "pat"); err != nil { t.Fatalf("set token: %v", err) } repos, err := gl.Repos(context.Background()) if err != nil { t.Fatalf("repos: %v", err) } if len(repos) != 0 { t.Fatalf("repos = %+v, want empty (blank path skipped)", repos) } } // newPagedGitLab serves full pages until pagesToFull+1, then a short page. func newPagedGitLab(t *testing.T, alwaysFull bool) *GitLab { t.Helper() mux := http.NewServeMux() mux.HandleFunc("/api/v1/user/repos", func(w http.ResponseWriter, r *http.Request) { page, _ := strconv.Atoi(r.URL.Query().Get("page")) if page < 1 { page = 1 } count := projectsPerPage if !alwaysFull && page > 2 { count = 1 } var b strings.Builder b.WriteString("[") for i := 0; i < count; i++ { if i > 0 { b.WriteString(",") } fmt.Fprintf(&b, `{"full_name":"team/p%d-%d","name":"P","owner":{"login":"team"},`+ `"updated_at":"2024-01-01T00:00:00Z","html_url":"https://gl","default_branch":"main"}`, page, i) } b.WriteString("]") w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(b.String())) }) up := httptest.NewServer(mux) t.Cleanup(up.Close) store := openTestStore(t) if err := store.SetSetting(settingGitLabToken, "pat"); err != nil { t.Fatalf("set token: %v", err) } return NewGitLab(store, up.URL) } func TestGitLabReposPaginatesUntilShortPage(t *testing.T) { gl := newPagedGitLab(t, false) // 50 + 50 + 1 (short) → stops after page 3 repos, err := gl.Repos(context.Background()) if err != nil { t.Fatalf("repos: %v", err) } if len(repos) != 2*projectsPerPage+1 { t.Fatalf("repos = %d, want %d (pages 1-2 full + short page 3)", len(repos), 2*projectsPerPage+1) } if repos[0].Path != "team/p1-0" || repos[len(repos)-1].Path != "team/p3-0" { t.Fatalf("first/last = %q..%q, want page1..page3", repos[0].Path, repos[len(repos)-1].Path) } } func TestGitLabReposPaginationCapped(t *testing.T) { gl := newPagedGitLab(t, true) // upstream always returns full pages repos, err := gl.Repos(context.Background()) if err != nil { t.Fatalf("repos: %v", err) } if len(repos) != maxRepoPages*projectsPerPage { t.Fatalf("repos = %d, want capped at %d", len(repos), maxRepoPages*projectsPerPage) } }