package app import ( "fmt" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" ) func TestCLIRepoMutatingHTTPCommands(t *testing.T) { t.Setenv("GITOOCEAN_CONFIG", filepath.Join(t.TempDir(), "client.json")) seen := map[string]int{} ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { key := r.Method + " " + r.URL.Path seen[key]++ if r.Header.Get("Authorization") != "Bearer tok" { t.Fatalf("missing auth for %s: %q", key, r.Header.Get("Authorization")) } switch key { case "POST /api/repos": writeJSON(w, http.StatusCreated, Repository{Owner: "alice", Name: "demo", Visibility: "public", Description: "desc", DefaultBranch: "main"}) case "PATCH /api/repos/alice/demo": writeJSON(w, http.StatusOK, Repository{Owner: "alice", Name: "demo", Visibility: "private", Description: "new", DefaultBranch: "main", Archived: seen[key] > 1}) case "DELETE /api/repos/alice/demo": if r.URL.Query().Get("force") != "true" { t.Fatalf("expected force=true, got %s", r.URL.RawQuery) } writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) case "POST /api/repos/bob/demo/fork": writeJSON(w, http.StatusCreated, Repository{Owner: "alice", Name: "fork", Visibility: "public", DefaultBranch: "main"}) case "GET /api/repos/alice/demo/collaborators": writeJSON(w, http.StatusOK, []Collaborator{{Username: "bob", Role: "write"}}) case "POST /api/repos/alice/demo/collaborators": writeJSON(w, http.StatusOK, map[string]string{"username": "bob", "role": "read"}) case "DELETE /api/repos/alice/demo/collaborators/bob": writeJSON(w, http.StatusOK, map[string]string{"status": "removed"}) default: t.Fatalf("unexpected request %s", key) } })) defer ts.Close() if err := saveConfig(Config{Server: ts.URL, Token: "tok", Username: "alice"}); err != nil { t.Fatal(err) } checks := []struct { name string fn func() error want string }{ {"create", func() error { return cliRepoCreate([]string{"demo", "--public", "--description", "desc"}) }, "Created public repository"}, {"create json", func() error { return cliRepoCreate([]string{"demo", "--public", "--json"}) }, `"owner": "alice"`}, {"set", func() error { return cliRepoSet([]string{"alice/demo", "--description", "new", "--visibility", "private"}) }, "Visibility: private"}, {"archive", func() error { return cliRepoArchive([]string{"alice/demo"}, true) }, "Archived: true"}, {"delete", func() error { return cliRepoDelete([]string{"alice/demo", "--force"}) }, "Deleted alice/demo"}, {"fork", func() error { return cliRepoFork([]string{"bob/demo", "--name", "fork"}) }, "Forked bob/demo to alice/fork"}, {"collaborators", func() error { return cliRepoCollaborators([]string{"alice/demo"}) }, "bob"}, {"collaborator add", func() error { return cliRepoCollaborator([]string{"add", "alice/demo", "bob", "--role", "read"}) }, "Added bob as read"}, {"collaborator remove", func() error { return cliRepoCollaborator([]string{"remove", "alice/demo", "bob"}) }, ""}, } for _, tc := range checks { t.Run(tc.name, func(t *testing.T) { out := captureStdout(t, func() { if err := tc.fn(); err != nil { t.Fatal(err) } }) if !strings.Contains(out, tc.want) { t.Fatalf("output missing %q: %s", tc.want, out) } }) } } func TestCLIPRMutatingHTTPCommands(t *testing.T) { t.Setenv("GITOOCEAN_CONFIG", filepath.Join(t.TempDir(), "client.json")) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { key := r.Method + " " + r.URL.Path switch key { case "POST /api/repos/alice/demo/pulls": writeJSON(w, http.StatusCreated, PullRequest{Number: 1, Status: "open", Title: "Fix", Author: "alice", SourceOwner: "alice", SourceRepo: "demo", SourceBranch: "feature", TargetOwner: "alice", TargetRepo: "demo", TargetBranch: "main"}) case "GET /api/repos/alice/demo/pulls/1/diff": w.Header().Set("Content-Type", "text/plain") _, _ = fmt.Fprint(w, "diff --git a/file b/file\n") case "POST /api/repos/alice/demo/pulls/1/comments": writeJSON(w, http.StatusCreated, PRComment{Author: "alice", Body: "hello"}) case "POST /api/repos/alice/demo/pulls/1/close", "POST /api/repos/alice/demo/pulls/1/merge": writeJSON(w, http.StatusOK, PullRequest{Number: 1, Status: strings.TrimPrefix(r.URL.Path, "/api/repos/alice/demo/pulls/1/"), Title: "Fix", Author: "alice", SourceOwner: "alice", SourceRepo: "demo", SourceBranch: "feature", TargetOwner: "alice", TargetRepo: "demo", TargetBranch: "main"}) default: t.Fatalf("unexpected request %s", key) } })) defer ts.Close() if err := saveConfig(Config{Server: ts.URL, Token: "tok", Username: "alice"}); err != nil { t.Fatal(err) } checks := []struct { name string fn func() error want string }{ {"create same", func() error { return cliPRCreate([]string{"--repo", "alice/demo", "--from", "feature", "--to", "main", "--title", "Fix"}) }, "#1 open: Fix"}, {"create cross", func() error { return cliPRCreate([]string{"--from", "alice/demo:feature", "--to", "alice/demo:main", "--title", "Fix"}) }, "#1 open: Fix"}, {"diff", func() error { return cliPRDiff([]string{"alice/demo", "1"}) }, "diff --git"}, {"comment", func() error { return cliPRComment([]string{"alice/demo", "1", "hello"}) }, "Commented as alice"}, {"close", func() error { return cliPRAction([]string{"alice/demo", "1"}, "close") }, "#1 close: Fix"}, {"merge", func() error { return cliPRAction([]string{"alice/demo", "1"}, "merge") }, "#1 merge: Fix"}, } for _, tc := range checks { t.Run(tc.name, func(t *testing.T) { out := captureStdout(t, func() { if err := tc.fn(); err != nil { t.Fatal(err) } }) if !strings.Contains(out, tc.want) { t.Fatalf("output missing %q: %s", tc.want, out) } }) } } func TestCLITokenAdminAndBackupCommands(t *testing.T) { t.Setenv("GITOOCEAN_CONFIG", filepath.Join(t.TempDir(), "client.json")) backupCfgPath := filepath.Join(t.TempDir(), "server.json") t.Setenv("GITOOCEAN_SERVER_CONFIG", backupCfgPath) storage := t.TempDir() if err := os.MkdirAll(filepath.Join(storage, "repos", "alice", "demo.git"), 0755); err != nil { t.Fatal(err) } if err := saveServerConfig(backupCfgPath, ServerConfig{Storage: storage, MySQLDSN: "u:p@tcp(127.0.0.1:3306)/gitocean"}); err != nil { t.Fatal(err) } ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method + " " + r.URL.Path { case "GET /api/tokens": writeJSON(w, http.StatusOK, []TokenInfo{{ID: 1, Active: true}}) case "DELETE /api/tokens/1", "POST /api/tokens/prune", "DELETE /api/admin/users/bob", "POST /api/admin/tokens/prune": writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) case "GET /api/admin/users": writeJSON(w, http.StatusOK, []User{{Username: "alice", Email: "alice@example.com", IsAdmin: true}}) case "GET /api/admin/repos": writeJSON(w, http.StatusOK, []Repository{{Owner: "alice", Name: "demo", Visibility: "public"}}) default: t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path) } })) defer ts.Close() if err := saveConfig(Config{Server: ts.URL, Token: "tok", Username: "alice"}); err != nil { t.Fatal(err) } checks := []struct { name string fn func() error want string }{ {"token list", func() error { return cliToken([]string{"list"}) }, "active"}, {"token revoke", func() error { return cliToken([]string{"revoke", "1"}) }, ""}, {"token prune", func() error { return cliToken([]string{"prune"}) }, ""}, {"admin users", func() error { return cliAdmin([]string{"users", "list"}) }, "alice@example.com"}, {"admin users delete", func() error { return cliAdmin([]string{"users", "delete", "bob"}) }, ""}, {"admin repos", func() error { return cliAdmin([]string{"repos", "list"}) }, "alice/demo"}, {"admin storage", func() error { return cliAdmin([]string{"storage", "check"}) }, "demo.git"}, } for _, tc := range checks { t.Run(tc.name, func(t *testing.T) { out := captureStdout(t, func() { if err := tc.fn(); err != nil { t.Fatal(err) } }) if !strings.Contains(out, tc.want) { t.Fatalf("output missing %q: %s", tc.want, out) } }) } }