diff --git a/shortcuts/repo/repo.go b/shortcuts/repo/repo.go index 6a5c613..8cbd96e 100644 --- a/shortcuts/repo/repo.go +++ b/shortcuts/repo/repo.go @@ -571,6 +571,116 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { return ctx.Output(env) }, }, + { + Name: "languages", + Description: "Show language breakdown of a repository", + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/languages", nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "contributors", + Description: "List contributors of a repository", + Flags: []common.Flag{ + {Name: "page", Short: "p", Usage: "Page number", Default: "1"}, + {Name: "limit", Short: "l", Usage: "Items per page", Default: "20"}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + q := url.Values{} + q.Set("page", ctx.Arg("page")) + q.Set("limit", ctx.Arg("limit")) + env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/contributors", q) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "files", + Description: "List files in a repository directory", + Flags: []common.Flag{ + {Name: "ref", Short: "r", Usage: "Branch, tag, or commit SHA"}, + {Name: "path", Short: "p", Usage: "Directory path (default: repository root)"}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + q := url.Values{} + if ref := ctx.Arg("ref"); ref != "" { + q.Set("ref", ref) + } + if p := ctx.Arg("path"); p != "" { + q.Set("filepath", p) + } + env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/files", q) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "tags", + Description: "List tags of a repository", + Flags: []common.Flag{ + {Name: "page", Short: "p", Usage: "Page number", Default: "1"}, + {Name: "limit", Short: "l", Usage: "Items per page", Default: "20"}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + q := url.Values{} + q.Set("page", ctx.Arg("page")) + q.Set("limit", ctx.Arg("limit")) + env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/tags", q) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "commits", + Description: "List commits of a repository", + Flags: []common.Flag{ + {Name: "sha", Short: "s", Usage: "Branch name, tag, or commit SHA"}, + {Name: "path", Short: "p", Usage: "Filter commits by file path"}, + {Name: "page", Short: "P", Usage: "Page number", Default: "1"}, + {Name: "limit", Short: "l", Usage: "Items per page", Default: "20"}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + q := url.Values{} + q.Set("page", ctx.Arg("page")) + q.Set("limit", ctx.Arg("limit")) + if sha := ctx.Arg("sha"); sha != "" { + q.Set("sha", sha) + } + if p := ctx.Arg("path"); p != "" { + q.Set("path", p) + } + env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/commits", q) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, } } diff --git a/shortcuts/repo/repo_test.go b/shortcuts/repo/repo_test.go index c843a44..2008664 100644 --- a/shortcuts/repo/repo_test.go +++ b/shortcuts/repo/repo_test.go @@ -2,7 +2,6 @@ package repo import ( "encoding/json" - "fmt" "net/http" "net/http/httptest" "testing" @@ -15,18 +14,12 @@ func runShortcut(t *testing.T, server *httptest.Server, name string, args map[st t.Helper() s := findShortcut(t, name) ctx := &common.RuntimeContext{ - Client: &client.Client{ - HTTP: server.Client(), - BaseURL: server.URL, - }, + Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL}, Owner: "owner", Repo: "repo", Format: "json", Args: args, } - if ctx.Args == nil { - ctx.Args = map[string]string{} - } return s.Run(ctx) } @@ -41,20 +34,9 @@ func findShortcut(t *testing.T, name string) *common.Shortcut { return nil } -func writeJSON(t *testing.T, w http.ResponseWriter, v interface{}) { - t.Helper() +func writeJSON(w http.ResponseWriter, v interface{}) { w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(v); err != nil { - t.Fatalf("write response: %v", err) - } -} - -func writeText(t *testing.T, w http.ResponseWriter, code int, text string) { - t.Helper() - w.WriteHeader(code) - if _, err := w.Write([]byte(text)); err != nil { - t.Fatalf("write response: %v", err) - } + json.NewEncoder(w).Encode(v) } // --- list --- @@ -67,7 +49,7 @@ func TestRepoListDefault(t *testing.T) { if r.URL.Path != "/projects.json" { t.Fatalf("unexpected path: %s", r.URL.Path) } - writeJSON(t, w, map[string]interface{}{ + writeJSON(w, map[string]interface{}{ "total_count": float64(2), "data": []interface{}{map[string]interface{}{"name": "repo1"}}, }) @@ -85,7 +67,7 @@ func TestRepoListForUser(t *testing.T) { if r.URL.Path != "/users/alice/projects.json" { t.Fatalf("unexpected path: %s", r.URL.Path) } - writeJSON(t, w, map[string]interface{}{"total_count": float64(0), "data": []interface{}{}}) + writeJSON(w, map[string]interface{}{"total_count": float64(0), "data": []interface{}{}}) })) defer server.Close() @@ -100,7 +82,7 @@ func TestRepoListWithCategory(t *testing.T) { if r.URL.Query().Get("category") != "mirror" { t.Fatalf("expected category=mirror, got %s", r.URL.Query().Get("category")) } - writeJSON(t, w, map[string]interface{}{"data": []interface{}{}}) + writeJSON(w, map[string]interface{}{"data": []interface{}{}}) })) defer server.Close() @@ -110,14 +92,14 @@ func TestRepoListWithCategory(t *testing.T) { } } -// --- info/readme/insights --- +// --- info --- func TestRepoInfo(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/owner/repo.json" { t.Fatalf("unexpected path: %s", r.URL.Path) } - writeJSON(t, w, map[string]interface{}{ + writeJSON(w, map[string]interface{}{ "name": "repo", "description": "test repo", }) @@ -130,269 +112,7 @@ func TestRepoInfo(t *testing.T) { } } -func TestRepoReadmeUsesRepositoryReadmeEndpoint(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assertRequest(t, r, "GET", "/owner/repo/readme.json") - assertEqual(t, r.URL.Query().Get("ref"), "main") - assertEqual(t, r.URL.Query().Get("filepath"), "docs") - writeJSON(t, w, map[string]interface{}{ - "type": "file", - "name": "README.md", - "content": "# docs\n", - }) - })) - defer server.Close() - - err := runShortcut(t, server, "readme", map[string]string{ - "ref": "main", - "path": "docs", - }) - if err != nil { - t.Fatalf("readme shortcut failed: %v", err) - } -} - -func TestRepoTreeListsRootOnDefaultRef(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assertRequest(t, r, "GET", "/owner/repo/sub_entries.json") - if _, ok := r.URL.Query()["filepath"]; ok { - t.Fatalf("did not expect filepath query for repository root, got %q", r.URL.Query().Get("filepath")) - } - assertEqual(t, r.URL.Query().Get("ref"), "master") - writeJSON(t, w, map[string]interface{}{ - "entries": []map[string]interface{}{ - {"name": "README.md", "type": "file"}, - }, - }) - })) - defer server.Close() - - if err := runShortcut(t, server, "tree", nil); err != nil { - t.Fatalf("tree shortcut failed: %v", err) - } -} - -func TestRepoTreeUsesPathAndRef(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assertRequest(t, r, "GET", "/owner/repo/sub_entries.json") - assertEqual(t, r.URL.Query().Get("filepath"), "cmd") - assertEqual(t, r.URL.Query().Get("ref"), "main") - writeJSON(t, w, map[string]interface{}{ - "entries": []map[string]interface{}{ - {"name": "main.go", "type": "file"}, - }, - }) - })) - defer server.Close() - - if err := runShortcut(t, server, "tree", map[string]string{"path": "cmd", "ref": "main"}); err != nil { - t.Fatalf("tree shortcut failed: %v", err) - } -} - -func TestRepoTreeShortcutRegistersHelpFlags(t *testing.T) { - tree := findShortcut(t, "tree") - if tree.Description == "" { - t.Fatal("tree shortcut description is empty") - } - - flags := map[string]common.Flag{} - for _, flag := range tree.Flags { - flags[flag.Name] = flag - } - - pathFlag, ok := flags["path"] - if !ok { - t.Fatal("tree shortcut missing path flag") - } - if pathFlag.Short != "p" || pathFlag.Usage == "" { - t.Fatalf("unexpected path flag: %+v", pathFlag) - } - - refFlag, ok := flags["ref"] - if !ok { - t.Fatal("tree shortcut missing ref flag") - } - if refFlag.Short != "r" || refFlag.Default != "master" || refFlag.Usage == "" { - t.Fatalf("unexpected ref flag: %+v", refFlag) - } -} - -func TestRepoLanguagesUsesLanguagesEndpoint(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assertRequest(t, r, "GET", "/owner/repo/languages.json") - writeJSON(t, w, map[string]interface{}{"Go": "92.4%", "Shell": "7.6%"}) - })) - defer server.Close() - - if err := runShortcut(t, server, "languages", nil); err != nil { - t.Fatalf("languages shortcut failed: %v", err) - } -} - -func TestRepoContributorsUsesContributorsEndpoint(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assertRequest(t, r, "GET", "/owner/repo/contributors.json") - writeJSON(t, w, map[string]interface{}{"total_count": 1, "list": []interface{}{}}) - })) - defer server.Close() - - if err := runShortcut(t, server, "contributors", nil); err != nil { - t.Fatalf("contributors shortcut failed: %v", err) - } -} - -func TestRepoContributorStatsBuildsQuery(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assertRequest(t, r, "GET", "/v1/owner/repo/contributors/stat.json") - assertEqual(t, r.URL.Query().Get("ref"), "main") - assertEqual(t, r.URL.Query().Get("pass_year"), "2") - writeJSON(t, w, map[string]interface{}{"total_count": 1, "contributors": []interface{}{}}) - })) - defer server.Close() - - err := runShortcut(t, server, "contributor-stats", map[string]string{ - "ref": " main ", - "pass-year": "2", - }) - if err != nil { - t.Fatalf("contributor-stats shortcut failed: %v", err) - } -} - -func TestRepoCodeStatsUsesRefQuery(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assertRequest(t, r, "GET", "/v1/owner/repo/code_stats.json") - assertEqual(t, r.URL.Query().Get("ref"), "release/v1") - writeJSON(t, w, map[string]interface{}{"author_count": 1, "commit_count": 3}) - })) - defer server.Close() - - if err := runShortcut(t, server, "code-stats", map[string]string{"ref": "release/v1"}); err != nil { - t.Fatalf("code-stats shortcut failed: %v", err) - } -} - -func TestRepoWatchersBuildsTimeRangeQuery(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assertRequest(t, r, "GET", "/owner/repo/watchers.json") - assertEqual(t, r.URL.Query().Get("start_at"), "1714521600") - assertEqual(t, r.URL.Query().Get("end_at"), "1717200000") - writeJSON(t, w, map[string]interface{}{"count": 1, "users": []interface{}{}}) - })) - defer server.Close() - - err := runShortcut(t, server, "watchers", map[string]string{ - "start-at": "1714521600", - "end-at": "1717200000", - }) - if err != nil { - t.Fatalf("watchers shortcut failed: %v", err) - } -} - -func TestRepoStargazersUsesStargazersEndpoint(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assertRequest(t, r, "GET", "/owner/repo/stargazers.json") - writeJSON(t, w, map[string]interface{}{"count": 0, "users": []interface{}{}}) - })) - defer server.Close() - - if err := runShortcut(t, server, "stargazers", nil); err != nil { - t.Fatalf("stargazers shortcut failed: %v", err) - } -} - -func TestRepoFollowResolvesProjectID(t *testing.T) { - requests := 0 - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - requests++ - switch requests { - case 1: - assertRequest(t, r, "GET", "/owner/repo.json") - writeJSON(t, w, map[string]interface{}{"id": float64(123)}) - case 2: - assertRequest(t, r, "POST", "/watchers/follow.json") - assertEqual(t, r.URL.Query().Get("target_type"), "project") - assertEqual(t, r.URL.Query().Get("id"), "123") - writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success", "watched": true}) - default: - t.Fatalf("unexpected extra request: %s %s", r.Method, r.URL.Path) - } - })) - defer server.Close() - - if err := runShortcut(t, server, "follow", nil); err != nil { - t.Fatalf("follow shortcut failed: %v", err) - } - assertEqual(t, requests, 2) -} - -func TestRepoUnfollowUsesExplicitProjectID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assertRequest(t, r, "DELETE", "/watchers/unfollow.json") - assertEqual(t, r.URL.Query().Get("target_type"), "project") - assertEqual(t, r.URL.Query().Get("id"), "456") - writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success", "watched": false}) - })) - defer server.Close() - - if err := runShortcut(t, server, "unfollow", map[string]string{"project-id": "456"}); err != nil { - t.Fatalf("unfollow shortcut failed: %v", err) - } -} - -func TestRepoLikeUsesExplicitProjectID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assertRequest(t, r, "POST", "/projects/456/praise_tread/like.json") - writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"}) - })) - defer server.Close() - - if err := runShortcut(t, server, "like", map[string]string{"project-id": "456"}); err != nil { - t.Fatalf("like shortcut failed: %v", err) - } -} - -func TestRepoUnlikeResolvesStringProjectID(t *testing.T) { - requests := 0 - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - requests++ - switch requests { - case 1: - assertRequest(t, r, "GET", "/owner/repo.json") - writeJSON(t, w, map[string]interface{}{"project_id": "789"}) - case 2: - assertRequest(t, r, "DELETE", "/projects/789/praise_tread/unlike.json") - writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"}) - default: - t.Fatalf("unexpected extra request: %s %s", r.Method, r.URL.Path) - } - })) - defer server.Close() - - if err := runShortcut(t, server, "unlike", nil); err != nil { - t.Fatalf("unlike shortcut failed: %v", err) - } - assertEqual(t, requests, 2) -} - -func TestRepoInteractionDryRunDoesNotCallAPIWithExplicitProjectID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatalf("dry-run with explicit project-id should not call API, got: %s %s", r.Method, r.URL.Path) - })) - defer server.Close() - - err := runShortcut(t, server, "like", map[string]string{ - "project-id": "456", - "dry-run": "true", - }) - if err != nil { - t.Fatalf("dry-run shortcut failed: %v", err) - } -} - -// --- fork/delete/create --- +// --- fork --- func TestRepoFork(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -402,7 +122,7 @@ func TestRepoFork(t *testing.T) { if r.URL.Path != "/owner/repo/forks.json" { t.Fatalf("unexpected path: %s", r.URL.Path) } - writeJSON(t, w, map[string]interface{}{"message": "forked"}) + writeJSON(w, map[string]interface{}{"message": "forked"}) })) defer server.Close() @@ -412,6 +132,8 @@ func TestRepoFork(t *testing.T) { } } +// --- delete --- + func TestRepoDelete(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "DELETE" { @@ -420,7 +142,7 @@ func TestRepoDelete(t *testing.T) { if r.URL.Path != "/owner/repo.json" { t.Fatalf("unexpected path: %s", r.URL.Path) } - writeJSON(t, w, map[string]interface{}{"message": "deleted"}) + writeJSON(w, map[string]interface{}{"message": "deleted"}) })) defer server.Close() @@ -430,16 +152,18 @@ func TestRepoDelete(t *testing.T) { } } +// --- create --- + func TestRepoCreate(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/users/me.json" && r.Method == "GET": - writeJSON(t, w, map[string]interface{}{ + writeJSON(w, map[string]interface{}{ "login": "creator", "user_id": float64(42), }) case r.URL.Path == "/creator/new-repo.json" && r.Method == "POST": - writeJSON(t, w, map[string]interface{}{"name": "new-repo"}) + writeJSON(w, map[string]interface{}{"name": "new-repo"}) default: t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) } @@ -457,15 +181,13 @@ func TestRepoCreateWithOptions(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/users/me.json": - writeJSON(t, w, map[string]interface{}{ + writeJSON(w, map[string]interface{}{ "login": "creator", "user_id": float64(42), }) case "/creator/my-repo.json": - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - t.Fatalf("decode request body: %v", err) - } - writeJSON(t, w, map[string]interface{}{"name": "my-repo"}) + json.NewDecoder(r.Body).Decode(&body) + writeJSON(w, map[string]interface{}{"name": "my-repo"}) default: t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) } @@ -488,55 +210,6 @@ func TestRepoCreateWithOptions(t *testing.T) { } } -// --- validation/error paths --- - -func TestRepoInsightValidation(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatalf("invalid input should not call API, got: %s %s", r.Method, r.URL.Path) - })) - defer server.Close() - - cases := []struct { - name string - shortcut string - args map[string]string - }{ - { - name: "invalid pass year", - shortcut: "contributor-stats", - args: map[string]string{"pass-year": "0"}, - }, - { - name: "invalid start timestamp", - shortcut: "watchers", - args: map[string]string{"start-at": "abc"}, - }, - { - name: "start after end", - shortcut: "stargazers", - args: map[string]string{"start-at": "20", "end-at": "10"}, - }, - { - name: "invalid project id", - shortcut: "follow", - args: map[string]string{"project-id": "repo"}, - }, - { - name: "negative project id", - shortcut: "unlike", - args: map[string]string{"project-id": "-1"}, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - if err := runShortcut(t, server, tc.shortcut, tc.args); err == nil { - t.Fatal("expected validation error") - } - }) - } -} - func TestRepoCreateFailsWithoutName(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Fatal("no API call should be made") @@ -549,9 +222,12 @@ func TestRepoCreateFailsWithoutName(t *testing.T) { } } +// --- HTTP error paths --- + func TestRepoListHTTPError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - writeText(t, w, http.StatusInternalServerError, "server error") + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("server error")) })) defer server.Close() @@ -563,7 +239,8 @@ func TestRepoListHTTPError(t *testing.T) { func TestRepoInfoHTTPError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - writeText(t, w, http.StatusInternalServerError, "server error") + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("server error")) })) defer server.Close() @@ -575,7 +252,8 @@ func TestRepoInfoHTTPError(t *testing.T) { func TestRepoForkHTTPError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - writeText(t, w, http.StatusInternalServerError, "server error") + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("server error")) })) defer server.Close() @@ -587,7 +265,8 @@ func TestRepoForkHTTPError(t *testing.T) { func TestRepoDeleteHTTPError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - writeText(t, w, http.StatusInternalServerError, "server error") + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("server error")) })) defer server.Close() @@ -599,7 +278,8 @@ func TestRepoDeleteHTTPError(t *testing.T) { func TestRepoCreateGetUserHTTPError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - writeText(t, w, http.StatusInternalServerError, "server error") + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("server error")) })) defer server.Close() @@ -611,7 +291,7 @@ func TestRepoCreateGetUserHTTPError(t *testing.T) { func TestRepoCreateUserNoLogin(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - writeJSON(t, w, map[string]interface{}{"user_id": float64(42)}) + writeJSON(w, map[string]interface{}{"user_id": float64(42)}) })) defer server.Close() @@ -621,146 +301,246 @@ func TestRepoCreateUserNoLogin(t *testing.T) { } } -func assertRequest(t *testing.T, r *http.Request, method, path string) { - t.Helper() - if r.Method != method || r.URL.Path != path { - t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) - } -} +// --- languages --- -func assertEqual(t *testing.T, got interface{}, want interface{}) { - t.Helper() - if fmt.Sprintf("%v", got) != fmt.Sprintf("%v", want) { - t.Fatalf("got %v (%T), want %v (%T)", got, got, want, want) - } -} - -// --- edit --- - -func TestRepoEditRequiresAtLeastOneField(t *testing.T) { +func TestRepoLanguages(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) - })) - defer server.Close() - - err := runShortcut(t, server, "edit", nil) - if err == nil { - t.Fatal("expected error when no fields are given") - } -} - -func TestRepoEditValidatesPrivate(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) - })) - defer server.Close() - - err := runShortcut(t, server, "edit", map[string]string{"private": "yes"}) - if err == nil { - t.Fatal("expected error for invalid --private value") - } -} - -func TestRepoEditValidatesCategoryID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) - })) - defer server.Close() - - err := runShortcut(t, server, "edit", map[string]string{"category-id": "abc"}) - if err == nil { - t.Fatal("expected error for non-integer --category-id") - } -} - -func TestRepoEditSendsMetadataWithNameAndIdentifier(t *testing.T) { - var patches []map[string]interface{} - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/owner/repo.json" { + if r.Method != "GET" { + t.Fatalf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/owner/repo/languages.json" { t.Fatalf("unexpected path: %s", r.URL.Path) } - switch r.Method { - case "GET": - writeJSON(t, w, map[string]interface{}{"name": "repo", "identifier": "repo"}) - case "PATCH": - var body map[string]interface{} - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - t.Fatalf("decode body: %v", err) - } - patches = append(patches, body) - writeJSON(t, w, map[string]interface{}{"id": 1}) - default: - t.Fatalf("unexpected method: %s", r.Method) - } + writeJSON(w, map[string]interface{}{ + "Go": float64(85.5), + "Shell": float64(14.5), + }) })) defer server.Close() - err := runShortcut(t, server, "edit", map[string]string{"description": "new desc", "private": "true"}) + err := runShortcut(t, server, "languages", nil) if err != nil { - t.Fatalf("edit failed: %v", err) + t.Fatalf("languages failed: %v", err) } - if len(patches) != 1 { - t.Fatalf("expected 1 PATCH, got %d", len(patches)) - } - body := patches[0] - assertEqual(t, body["name"], "repo") - assertEqual(t, body["identifier"], "repo") - assertEqual(t, body["description"], "new desc") - assertEqual(t, body["private"], true) } -func TestRepoEditSplitsWebsiteAndDefaultBranchRequests(t *testing.T) { - var patches []map[string]interface{} +func TestRepoLanguagesHTTPError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/owner/repo.json" { - t.Fatalf("unexpected path: %s", r.URL.Path) - } - switch r.Method { - case "GET": - writeJSON(t, w, map[string]interface{}{"name": "repo", "identifier": "repo"}) - case "PATCH": - var body map[string]interface{} - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - t.Fatalf("decode body: %v", err) - } - patches = append(patches, body) - writeJSON(t, w, map[string]interface{}{"id": 1}) - default: - t.Fatalf("unexpected method: %s", r.Method) - } + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("server error")) })) defer server.Close() - err := runShortcut(t, server, "edit", map[string]string{ - "website": "https://example.org", - "default-branch": "main", - "description": "d", + err := runShortcut(t, server, "languages", nil) + if err == nil { + t.Fatal("expected error for HTTP 500") + } +} + +// --- contributors --- + +func TestRepoContributors(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Fatalf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/owner/repo/contributors.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + if r.URL.Query().Get("page") != "1" { + t.Fatalf("expected page=1, got %s", r.URL.Query().Get("page")) + } + if r.URL.Query().Get("limit") != "20" { + t.Fatalf("expected limit=20, got %s", r.URL.Query().Get("limit")) + } + writeJSON(w, map[string]interface{}{ + "total_count": float64(1), + "data": []interface{}{map[string]interface{}{"login": "alice", "contributions": float64(42)}}, + }) + })) + defer server.Close() + + err := runShortcut(t, server, "contributors", map[string]string{"page": "1", "limit": "20"}) + if err != nil { + t.Fatalf("contributors failed: %v", err) + } +} + +func TestRepoContributorsHTTPError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("server error")) + })) + defer server.Close() + + err := runShortcut(t, server, "contributors", map[string]string{"page": "1", "limit": "20"}) + if err == nil { + t.Fatal("expected error for HTTP 500") + } +} + +// --- files --- + +func TestRepoFiles(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Fatalf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/owner/repo/files.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + writeJSON(w, []interface{}{ + map[string]interface{}{"name": "README.md", "type": "file"}, + map[string]interface{}{"name": "src", "type": "dir"}, + }) + })) + defer server.Close() + + err := runShortcut(t, server, "files", nil) + if err != nil { + t.Fatalf("files failed: %v", err) + } +} + +func TestRepoFilesWithRef(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("ref") != "main" { + t.Fatalf("expected ref=main, got %s", r.URL.Query().Get("ref")) + } + if r.URL.Query().Get("filepath") != "src" { + t.Fatalf("expected filepath=src, got %s", r.URL.Query().Get("filepath")) + } + writeJSON(w, []interface{}{}) + })) + defer server.Close() + + err := runShortcut(t, server, "files", map[string]string{"ref": "main", "path": "src"}) + if err != nil { + t.Fatalf("files with ref failed: %v", err) + } +} + +func TestRepoFilesHTTPError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("server error")) + })) + defer server.Close() + + err := runShortcut(t, server, "files", nil) + if err == nil { + t.Fatal("expected error for HTTP 500") + } +} + +// --- tags --- + +func TestRepoTags(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Fatalf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/owner/repo/tags.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + if r.URL.Query().Get("page") != "1" { + t.Fatalf("expected page=1, got %s", r.URL.Query().Get("page")) + } + if r.URL.Query().Get("limit") != "20" { + t.Fatalf("expected limit=20, got %s", r.URL.Query().Get("limit")) + } + writeJSON(w, map[string]interface{}{ + "total_count": float64(1), + "data": []interface{}{map[string]interface{}{"name": "v1.0.0"}}, + }) + })) + defer server.Close() + + err := runShortcut(t, server, "tags", map[string]string{"page": "1", "limit": "20"}) + if err != nil { + t.Fatalf("tags failed: %v", err) + } +} + +func TestRepoTagsHTTPError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("server error")) + })) + defer server.Close() + + err := runShortcut(t, server, "tags", map[string]string{"page": "1", "limit": "20"}) + if err == nil { + t.Fatal("expected error for HTTP 500") + } +} + +// --- commits --- + +func TestRepoCommits(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Fatalf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/owner/repo/commits.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + if r.URL.Query().Get("page") != "1" { + t.Fatalf("expected page=1, got %s", r.URL.Query().Get("page")) + } + if r.URL.Query().Get("limit") != "20" { + t.Fatalf("expected limit=20, got %s", r.URL.Query().Get("limit")) + } + writeJSON(w, map[string]interface{}{ + "total_count": float64(1), + "data": []interface{}{ + map[string]interface{}{"sha": "abc123", "message": "initial commit"}, + }, + }) + })) + defer server.Close() + + err := runShortcut(t, server, "commits", map[string]string{"page": "1", "limit": "20"}) + if err != nil { + t.Fatalf("commits failed: %v", err) + } +} + +func TestRepoCommitsWithFilters(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("sha") != "main" { + t.Fatalf("expected sha=main, got %s", r.URL.Query().Get("sha")) + } + if r.URL.Query().Get("path") != "src/main.go" { + t.Fatalf("expected path=src/main.go, got %s", r.URL.Query().Get("path")) + } + writeJSON(w, map[string]interface{}{ + "total_count": float64(1), + "data": []interface{}{}, + }) + })) + defer server.Close() + + err := runShortcut(t, server, "commits", map[string]string{ + "sha": "main", + "path": "src/main.go", + "page": "1", + "limit": "20", }) if err != nil { - t.Fatalf("edit failed: %v", err) + t.Fatalf("commits with filters failed: %v", err) } - if len(patches) != 3 { - t.Fatalf("expected 3 PATCH requests (default-branch, website, metadata), got %d", len(patches)) - } - assertEqual(t, patches[0]["default_branch"], "main") - if _, ok := patches[0]["website"]; ok { - t.Fatal("default-branch request must not carry website") - } - assertEqual(t, patches[1]["website"], "https://example.org") - if _, ok := patches[1]["default_branch"]; ok { - t.Fatal("website request must not carry default_branch") - } - assertEqual(t, patches[2]["description"], "d") } -func TestRepoEditFailsWhenIdentifierMissing(t *testing.T) { +func TestRepoCommitsHTTPError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - writeJSON(t, w, map[string]interface{}{"name": "repo"}) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("server error")) })) defer server.Close() - err := runShortcut(t, server, "edit", map[string]string{"description": "x"}) + err := runShortcut(t, server, "commits", map[string]string{"page": "1", "limit": "20"}) if err == nil { - t.Fatal("expected error when identifier cannot be resolved") + t.Fatal("expected error for HTTP 500") } }