gitlink-cli/shortcuts/user/user_test.go

654 lines
20 KiB
Go

package user
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gitlink-org/gitlink-cli/internal/client"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
func runUserShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error {
t.Helper()
shortcut := findUserShortcut(t, name)
ctx := &common.RuntimeContext{
Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
Format: "json",
Args: args,
}
return shortcut.Run(ctx)
}
func findUserShortcut(t *testing.T, name string) *common.Shortcut {
t.Helper()
for _, shortcut := range Shortcuts() {
if shortcut.Name == name {
return shortcut
}
}
t.Fatalf("shortcut %q not found", name)
return nil
}
func writeUserJSON(w http.ResponseWriter, v interface{}) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(v)
}
func TestUserMe(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/users/me.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
writeUserJSON(w, map[string]interface{}{
"login": "currentuser",
"name": "Current User",
"id": 1,
})
}))
defer server.Close()
if err := runUserShortcut(t, server, "me", nil); err != nil {
t.Fatalf("me failed: %v", err)
}
}
func TestUserInfo(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/users/alice.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
writeUserJSON(w, map[string]interface{}{
"login": "alice",
"name": "Alice",
})
}))
defer server.Close()
if err := runUserShortcut(t, server, "info", map[string]string{"login": "alice"}); err != nil {
t.Fatalf("info failed: %v", err)
}
}
func TestUserInfoMissingLogin(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no API call expected")
}))
defer server.Close()
if err := runUserShortcut(t, server, "info", map[string]string{}); err == nil {
t.Fatal("expected error for missing login")
}
}
func TestUserHeadmapShortcutBuildsYearQuery(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/users/alice/headmaps.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
if r.URL.Query().Get("year") != "2026" {
t.Fatalf("unexpected year query: %q", r.URL.Query().Get("year"))
}
writeUserJSON(w, map[string]interface{}{
"headmaps": []interface{}{
map[string]interface{}{"date": "2026-06-01", "contributions": 5},
},
"total_contributions": 5,
})
}))
defer server.Close()
if err := runUserShortcut(t, server, "headmap", map[string]string{"login": "alice", "year": "2026"}); err != nil {
t.Fatalf("headmap failed: %v", err)
}
}
func TestUserDevelopShortcutBuildsTimeRangeQuery(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/users/alice/statistics/develop.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
if r.URL.Query().Get("start_time") != "100" || r.URL.Query().Get("end_time") != "200" {
t.Fatalf("unexpected time range query: %s", r.URL.RawQuery)
}
writeUserJSON(w, map[string]interface{}{
"platform": map[string]interface{}{"activity": 90},
"user": map[string]interface{}{
"activity": 70,
"languages_percent": map[string]interface{}{"Go": 0.8},
"each_language_score": map[string]interface{}{"Go": 88},
},
})
}))
defer server.Close()
if err := runUserShortcut(t, server, "develop", map[string]string{
"login": "alice",
"start-time": "100",
"end-time": "200",
}); err != nil {
t.Fatalf("develop failed: %v", err)
}
}
func TestUserTrendsFetchesAllPagesWhenFiltersPresent(t *testing.T) {
requests := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
page := r.URL.Query().Get("page")
switch page {
case "1":
writeUserJSON(w, map[string]interface{}{
"project_trends": []interface{}{
map[string]interface{}{
"id": 1,
"trend_id": 11,
"trend_type": "CommitLog",
"name": "initial commit",
"action_type": "创建了代码提交(Commit)",
"user_login": "alice",
"user_name": "Alice",
"project": map[string]interface{}{
"identifier": "repo-a",
"owner": map[string]interface{}{"login": "Gitlink"},
},
},
},
"total_count": 2,
})
case "2":
writeUserJSON(w, map[string]interface{}{
"project_trends": []interface{}{
map[string]interface{}{
"id": 2,
"trend_id": 12,
"trend_type": "PullRequest",
"name": "improve docs",
"action_type": "创建了合并请求(PR)",
"user_login": "alice",
"user_name": "Alice",
"project": map[string]interface{}{
"identifier": "repo-b",
"owner": map[string]interface{}{"login": "Gitlink"},
},
},
},
"total_count": 2,
})
default:
t.Fatalf("unexpected page query: %s", page)
}
}))
defer server.Close()
if err := runUserShortcut(t, server, "trends", map[string]string{
"login": "alice",
"page": "1",
"limit": "1",
"trend-type": "PullRequest",
}); err != nil {
t.Fatalf("trends failed: %v", err)
}
if requests != 2 {
t.Fatalf("expected 2 requests for filtered trends, got %d", requests)
}
}
func TestBuildTimeRangeQueryRejectsInvalidOrder(t *testing.T) {
if _, _, err := buildTimeRangeQuery("200", "100"); err == nil {
t.Fatal("expected invalid time range error")
}
}
func TestNormalizeActivityDataBuildsTimelineSummary(t *testing.T) {
result, err := normalizeActivityData("alice", map[string]interface{}{
"dates": []interface{}{"2026.06.01", "2026.06.02"},
"commits_count": []interface{}{3, 5},
"issues_count": []interface{}{1, 0},
"pull_requests_count": []interface{}{0, 2},
})
if err != nil {
t.Fatalf("normalizeActivityData failed: %v", err)
}
totals := result["totals"].(map[string]interface{})
if totals["all"].(int) != 11 {
t.Fatalf("unexpected total activity count: %#v", totals)
}
peakDay := result["peak_day"].(map[string]interface{})
if peakDay["date"].(string) != "2026.06.02" {
t.Fatalf("unexpected peak day: %#v", peakDay)
}
}
func TestNormalizeDevelopDataSortsLanguages(t *testing.T) {
result, err := normalizeDevelopData("alice", map[string]interface{}{"start_time": 100}, map[string]interface{}{
"platform": map[string]interface{}{
"activity": 90,
},
"user": map[string]interface{}{
"activity": 70,
"languages_percent": map[string]interface{}{"Go": 0.4, "Python": 0.6},
"each_language_score": map[string]interface{}{"Go": 80, "Python": 95},
},
})
if err != nil {
t.Fatalf("normalizeDevelopData failed: %v", err)
}
languages := result["languages"].([]map[string]interface{})
if len(languages) != 2 || languages[0]["name"].(string) != "Python" {
t.Fatalf("languages not sorted by percent: %#v", languages)
}
if result["period"].(map[string]interface{})["start_time"].(int) != 100 {
t.Fatalf("unexpected period: %#v", result["period"])
}
}
func TestNormalizeRoleDataSortsRoleCounts(t *testing.T) {
result, err := normalizeRoleData("alice", nil, map[string]interface{}{
"role": map[string]interface{}{
"owner": map[string]interface{}{"count": 3, "percent": 0.6},
"developer": map[string]interface{}{"count": 2, "percent": 0.4},
},
"total_projects_count": 5,
})
if err != nil {
t.Fatalf("normalizeRoleData failed: %v", err)
}
roles := result["roles"].([]map[string]interface{})
if roles[0]["name"].(string) != "owner" {
t.Fatalf("expected owner to be primary role: %#v", roles)
}
}
func TestNormalizeTrendDataAppliesFilters(t *testing.T) {
result, err := normalizeTrendData("alice", 1, 20, 2, true, trendFilters{TrendType: "PullRequest"}, []interface{}{
map[string]interface{}{
"id": 1,
"trend_id": 11,
"trend_type": "CommitLog",
"name": "initial commit",
"action_type": "创建了代码提交(Commit)",
"user_login": "alice",
"user_name": "Alice",
"project": map[string]interface{}{
"identifier": "repo-a",
"owner": map[string]interface{}{"login": "Gitlink"},
},
},
map[string]interface{}{
"id": 2,
"trend_id": 12,
"trend_type": "PullRequest",
"name": "improve docs",
"action_type": "创建了合并请求(PR)",
"user_login": "alice",
"user_name": "Alice",
"project": map[string]interface{}{
"identifier": "repo-b",
"owner": map[string]interface{}{"login": "Gitlink"},
},
},
})
if err != nil {
t.Fatalf("normalizeTrendData failed: %v", err)
}
items := result["items"].([]map[string]interface{})
if len(items) != 1 || items[0]["trend_type"].(string) != "PullRequest" {
t.Fatalf("unexpected filtered trend items: %#v", items)
}
}
func TestUserInfoHTTPError(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()
if err := runUserShortcut(t, server, "info", map[string]string{"login": "alice"}); err == nil {
t.Fatal("expected error for HTTP 500")
}
}
func TestUserStatisticsHTTPError(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()
for _, shortcut := range []string{"activity", "headmap", "develop", "role", "major"} {
t.Run(shortcut, func(t *testing.T) {
err := runShortcut(t, server, shortcut, map[string]string{"login": "alice"})
if err == nil {
t.Fatal("expected error for HTTP 500")
}
})
}
}
func TestUserActivityUsesLogin(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" || r.URL.Path != "/users/alice/statistics/activity.json" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
writeJSON(w, map[string]interface{}{"dates": []string{"2026.06.13"}, "issues_count": []int{1}})
}))
defer server.Close()
if err := runShortcut(t, server, "activity", map[string]string{"login": "alice"}); err != nil {
t.Fatalf("activity failed: %v", err)
}
}
func TestUserHeadmapBuildsYearQuery(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" || r.URL.Path != "/users/alice/headmaps.json" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
if got := r.URL.Query().Get("year"); got != "2026" {
t.Fatalf("year = %q", got)
}
writeJSON(w, map[string]interface{}{"total_contributions": 1, "headmaps": []interface{}{}})
}))
defer server.Close()
if err := runShortcut(t, server, "headmap", map[string]string{"login": "alice", "year": "2026"}); err != nil {
t.Fatalf("headmap failed: %v", err)
}
}
func TestUserDevelopBuildsTimeRangeQuery(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" || r.URL.Path != "/users/alice/statistics/develop.json" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
if got := r.URL.Query().Get("start_time"); got != "1717200000" {
t.Fatalf("start_time = %q", got)
}
if got := r.URL.Query().Get("end_time"); got != "1719800000" {
t.Fatalf("end_time = %q", got)
}
writeJSON(w, map[string]interface{}{"user": map[string]interface{}{"activity": 90}})
}))
defer server.Close()
err := runShortcut(t, server, "develop", map[string]string{"login": "alice", "start-time": "1717200000", "end-time": "1719800000"})
if err != nil {
t.Fatalf("develop failed: %v", err)
}
}
func TestUserRoleAndMajorUseStatisticsEndpoints(t *testing.T) {
calls := []string{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls = append(calls, r.URL.Path)
writeJSON(w, map[string]interface{}{"ok": true})
}))
defer server.Close()
if err := runShortcut(t, server, "role", map[string]string{"login": "alice"}); err != nil {
t.Fatalf("role failed: %v", err)
}
if err := runShortcut(t, server, "major", map[string]string{"login": "alice"}); err != nil {
t.Fatalf("major failed: %v", err)
}
want := []string{"/users/alice/statistics/role.json", "/users/alice/statistics/major.json"}
if len(calls) != len(want) {
t.Fatalf("calls = %#v", calls)
}
for i := range want {
if calls[i] != want[i] {
t.Fatalf("calls = %#v, want %#v", calls, want)
}
}
}
func TestUserStatisticsResolveCurrentUser(t *testing.T) {
requests := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
switch requests {
case 1:
if r.URL.Path != "/users/me.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
writeJSON(w, map[string]interface{}{"login": "current"})
case 2:
if r.URL.Path != "/users/current/statistics/activity.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
writeJSON(w, map[string]interface{}{"dates": []interface{}{}})
default:
t.Fatalf("unexpected extra request: %s", r.URL.Path)
}
}))
defer server.Close()
shortcut := findShortcut(t, "activity")
ctx := &common.RuntimeContext{Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL}, Format: "json", Args: map[string]string{}}
if err := shortcut.Run(ctx); err != nil {
t.Fatalf("activity failed: %v", err)
}
}
func TestUserStatisticsValidation(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: "bad year", shortcut: "headmap", args: map[string]string{"login": "alice", "year": "abcd"}},
{name: "negative start", shortcut: "develop", args: map[string]string{"login": "alice", "start-time": "-1"}},
{name: "start after end", shortcut: "role", args: map[string]string{"login": "alice", "start-time": "20", "end-time": "10"}},
}
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")
}
})
}
}
// --- heatmap ---
func TestUserHeatmap(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/users/alice/headmaps.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
if r.URL.Query().Get("year") != "" {
t.Fatalf("expected no year query param, got %s", r.URL.Query().Get("year"))
}
writeJSON(w, map[string]interface{}{
"contributions": []interface{}{
map[string]interface{}{"date": "2026-01-01", "count": float64(5)},
},
})
}))
defer server.Close()
err := runShortcut(t, server, "heatmap", map[string]string{"login": "alice"})
if err != nil {
t.Fatalf("heatmap failed: %v", err)
}
}
func TestUserHeatmapWithYear(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/users/alice/headmaps.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
if r.URL.Query().Get("year") != "2025" {
t.Fatalf("expected year=2025, got %s", r.URL.Query().Get("year"))
}
writeJSON(w, map[string]interface{}{
"contributions": []interface{}{},
})
}))
defer server.Close()
err := runShortcut(t, server, "heatmap", map[string]string{"login": "alice", "year": "2025"})
if err != nil {
t.Fatalf("heatmap with year failed: %v", err)
}
}
func TestUserHeatmapMissingLogin(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no API call expected")
}))
defer server.Close()
err := runShortcut(t, server, "heatmap", map[string]string{})
if err == nil {
t.Fatal("expected error for missing login")
}
}
func TestUserHeatmapHTTPError(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, "heatmap", map[string]string{"login": "alice"})
if err == nil {
t.Fatal("expected error for HTTP 500")
}
}
// --- stats ---
func TestUserStats(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/users/alice/statistics/develop.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
if r.URL.Query().Get("start_time") != "" || r.URL.Query().Get("end_time") != "" {
t.Fatal("expected no time query params")
}
writeJSON(w, map[string]interface{}{
"pull_request_count": float64(10),
"commit_count": float64(42),
})
}))
defer server.Close()
err := runShortcut(t, server, "stats", map[string]string{"login": "alice"})
if err != nil {
t.Fatalf("stats failed: %v", err)
}
}
func TestUserStatsWithTimeRange(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/users/alice/statistics/develop.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
if r.URL.Query().Get("start_time") != "2026-01-01" {
t.Fatalf("expected start_time=2026-01-01, got %s", r.URL.Query().Get("start_time"))
}
if r.URL.Query().Get("end_time") != "2026-03-31" {
t.Fatalf("expected end_time=2026-03-31, got %s", r.URL.Query().Get("end_time"))
}
writeJSON(w, map[string]interface{}{
"pull_request_count": float64(5),
"commit_count": float64(20),
})
}))
defer server.Close()
err := runShortcut(t, server, "stats", map[string]string{
"login": "alice",
"start-time": "2026-01-01",
"end-time": "2026-03-31",
})
if err != nil {
t.Fatalf("stats with time range failed: %v", err)
}
}
func TestUserStatsMissingLogin(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no API call expected")
}))
defer server.Close()
err := runShortcut(t, server, "stats", map[string]string{})
if err == nil {
t.Fatal("expected error for missing login")
}
}
func TestUserStatsHTTPError(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, "stats", map[string]string{"login": "alice"})
if err == nil {
t.Fatal("expected error for HTTP 500")
}
}
// --- trends ---
func TestUserTrends(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/users/alice/project_trends.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
writeJSON(w, []interface{}{
map[string]interface{}{"id": float64(1), "name": "created project"},
})
}))
defer server.Close()
err := runShortcut(t, server, "trends", map[string]string{"login": "alice"})
if err != nil {
t.Fatalf("trends failed: %v", err)
}
}
func TestUserTrendsMissingLogin(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no API call expected")
}))
defer server.Close()
err := runShortcut(t, server, "trends", map[string]string{})
if err == nil {
t.Fatal("expected error for missing login")
}
}
func TestUserTrendsHTTPError(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, "trends", map[string]string{"login": "alice"})
if err == nil {
t.Fatal("expected error for HTTP 500")
}
}