package forge import ( "context" json "github.com/goccy/go-json" "net/http" "net/http/httptest" "testing" "dappco.re/go/core/forge/types" ) func TestAdminService_ListUsers_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { t.Errorf("expected GET, got %s", r.Method) } if r.URL.Path != "/api/v1/admin/users" { t.Errorf("wrong path: %s", r.URL.Path) } w.Header().Set("X-Total-Count", "2") json.NewEncoder(w).Encode([]types.User{ {ID: 1, UserName: "alice"}, {ID: 2, UserName: "bob"}, }) })) defer srv.Close() f := NewForge(srv.URL, "tok") users, err := f.Admin.ListUsers(context.Background()) if err != nil { t.Fatal(err) } if len(users) != 2 { t.Errorf("got %d users, want 2", len(users)) } if users[0].UserName != "alice" { t.Errorf("got username=%q, want %q", users[0].UserName, "alice") } } func TestAdminService_CreateUser_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("expected POST, got %s", r.Method) } if r.URL.Path != "/api/v1/admin/users" { t.Errorf("wrong path: %s", r.URL.Path) } var opts types.CreateUserOption if err := json.NewDecoder(r.Body).Decode(&opts); err != nil { t.Fatal(err) } if opts.Username != "newuser" { t.Errorf("got username=%q, want %q", opts.Username, "newuser") } if opts.Email != "new@example.com" { t.Errorf("got email=%q, want %q", opts.Email, "new@example.com") } json.NewEncoder(w).Encode(types.User{ID: 42, UserName: "newuser", Email: "new@example.com"}) })) defer srv.Close() f := NewForge(srv.URL, "tok") user, err := f.Admin.CreateUser(context.Background(), &types.CreateUserOption{ Username: "newuser", Email: "new@example.com", Password: "secret123", }) if err != nil { t.Fatal(err) } if user.ID != 42 { t.Errorf("got id=%d, want 42", user.ID) } if user.UserName != "newuser" { t.Errorf("got username=%q, want %q", user.UserName, "newuser") } } func TestAdminService_DeleteUser_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete { t.Errorf("expected DELETE, got %s", r.Method) } if r.URL.Path != "/api/v1/admin/users/alice" { t.Errorf("wrong path: %s", r.URL.Path) } w.WriteHeader(http.StatusNoContent) })) defer srv.Close() f := NewForge(srv.URL, "tok") if err := f.Admin.DeleteUser(context.Background(), "alice"); err != nil { t.Fatal(err) } } func TestAdminService_RunCron_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("expected POST, got %s", r.Method) } if r.URL.Path != "/api/v1/admin/cron/repo_health_check" { t.Errorf("wrong path: %s", r.URL.Path) } w.WriteHeader(http.StatusNoContent) })) defer srv.Close() f := NewForge(srv.URL, "tok") if err := f.Admin.RunCron(context.Background(), "repo_health_check"); err != nil { t.Fatal(err) } } func TestAdminService_EditUser_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPatch { t.Errorf("expected PATCH, got %s", r.Method) } if r.URL.Path != "/api/v1/admin/users/alice" { t.Errorf("wrong path: %s", r.URL.Path) } var body map[string]any if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Fatal(err) } if body["email"] != "alice@new.com" { t.Errorf("got email=%v, want %q", body["email"], "alice@new.com") } w.WriteHeader(http.StatusOK) })) defer srv.Close() f := NewForge(srv.URL, "tok") err := f.Admin.EditUser(context.Background(), "alice", map[string]any{ "email": "alice@new.com", }) if err != nil { t.Fatal(err) } } func TestAdminService_RenameUser_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("expected POST, got %s", r.Method) } if r.URL.Path != "/api/v1/admin/users/alice/rename" { t.Errorf("wrong path: %s", r.URL.Path) } var opts types.RenameUserOption if err := json.NewDecoder(r.Body).Decode(&opts); err != nil { t.Fatal(err) } if opts.NewName != "alice2" { t.Errorf("got new_username=%q, want %q", opts.NewName, "alice2") } w.WriteHeader(http.StatusNoContent) })) defer srv.Close() f := NewForge(srv.URL, "tok") if err := f.Admin.RenameUser(context.Background(), "alice", "alice2"); err != nil { t.Fatal(err) } } func TestAdminService_ListOrgs_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { t.Errorf("expected GET, got %s", r.Method) } if r.URL.Path != "/api/v1/admin/orgs" { t.Errorf("wrong path: %s", r.URL.Path) } w.Header().Set("X-Total-Count", "1") json.NewEncoder(w).Encode([]types.Organization{ {ID: 10, Name: "myorg"}, }) })) defer srv.Close() f := NewForge(srv.URL, "tok") orgs, err := f.Admin.ListOrgs(context.Background()) if err != nil { t.Fatal(err) } if len(orgs) != 1 { t.Errorf("got %d orgs, want 1", len(orgs)) } if orgs[0].Name != "myorg" { t.Errorf("got name=%q, want %q", orgs[0].Name, "myorg") } } func TestAdminService_ListEmails_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { t.Errorf("expected GET, got %s", r.Method) } if r.URL.Path != "/api/v1/admin/emails" { t.Errorf("wrong path: %s", r.URL.Path) } w.Header().Set("X-Total-Count", "2") json.NewEncoder(w).Encode([]types.Email{ {Email: "alice@example.com", Primary: true}, {Email: "bob@example.com", Verified: true}, }) })) defer srv.Close() f := NewForge(srv.URL, "tok") emails, err := f.Admin.ListEmails(context.Background()) if err != nil { t.Fatal(err) } if len(emails) != 2 { t.Errorf("got %d emails, want 2", len(emails)) } if emails[0].Email != "alice@example.com" || !emails[0].Primary { t.Errorf("got first email=%+v, want primary alice@example.com", emails[0]) } } func TestAdminService_ListHooks_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { t.Errorf("expected GET, got %s", r.Method) } if r.URL.Path != "/api/v1/admin/hooks" { t.Errorf("wrong path: %s", r.URL.Path) } w.Header().Set("X-Total-Count", "1") json.NewEncoder(w).Encode([]types.Hook{ {ID: 7, Type: "forgejo", URL: "https://example.com/admin-hook", Active: true}, }) })) defer srv.Close() f := NewForge(srv.URL, "tok") hooks, err := f.Admin.ListHooks(context.Background()) if err != nil { t.Fatal(err) } if len(hooks) != 1 { t.Fatalf("got %d hooks, want 1", len(hooks)) } if hooks[0].ID != 7 || hooks[0].URL != "https://example.com/admin-hook" { t.Errorf("unexpected hook: %+v", hooks[0]) } } func TestAdminService_CreateHook_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("expected POST, got %s", r.Method) } if r.URL.Path != "/api/v1/admin/hooks" { t.Errorf("wrong path: %s", r.URL.Path) } var opts types.CreateHookOption if err := json.NewDecoder(r.Body).Decode(&opts); err != nil { t.Fatal(err) } if opts.Type != "forgejo" { t.Errorf("got type=%q, want %q", opts.Type, "forgejo") } json.NewEncoder(w).Encode(types.Hook{ ID: 12, Type: opts.Type, Active: opts.Active, Events: opts.Events, URL: "https://example.com/admin-hook", }) })) defer srv.Close() f := NewForge(srv.URL, "tok") hook, err := f.Admin.CreateHook(context.Background(), &types.CreateHookOption{ Type: "forgejo", Active: true, Events: []string{"push"}, }) if err != nil { t.Fatal(err) } if hook.ID != 12 { t.Errorf("got id=%d, want 12", hook.ID) } if hook.Type != "forgejo" { t.Errorf("got type=%q, want %q", hook.Type, "forgejo") } } func TestAdminService_GetHook_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { t.Errorf("expected GET, got %s", r.Method) } if r.URL.Path != "/api/v1/admin/hooks/7" { t.Errorf("wrong path: %s", r.URL.Path) } json.NewEncoder(w).Encode(types.Hook{ ID: 7, Type: "forgejo", Active: true, URL: "https://example.com/admin-hook", }) })) defer srv.Close() f := NewForge(srv.URL, "tok") hook, err := f.Admin.GetHook(context.Background(), 7) if err != nil { t.Fatal(err) } if hook.ID != 7 { t.Errorf("got id=%d, want 7", hook.ID) } } func TestAdminService_EditHook_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPatch { t.Errorf("expected PATCH, got %s", r.Method) } if r.URL.Path != "/api/v1/admin/hooks/7" { t.Errorf("wrong path: %s", r.URL.Path) } var opts types.EditHookOption if err := json.NewDecoder(r.Body).Decode(&opts); err != nil { t.Fatal(err) } if !opts.Active { t.Error("expected active=true") } json.NewEncoder(w).Encode(types.Hook{ ID: 7, Type: "forgejo", Active: opts.Active, URL: "https://example.com/admin-hook", }) })) defer srv.Close() f := NewForge(srv.URL, "tok") hook, err := f.Admin.EditHook(context.Background(), 7, &types.EditHookOption{Active: true}) if err != nil { t.Fatal(err) } if hook.ID != 7 || !hook.Active { t.Errorf("unexpected hook: %+v", hook) } } func TestAdminService_DeleteHook_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete { t.Errorf("expected DELETE, got %s", r.Method) } if r.URL.Path != "/api/v1/admin/hooks/7" { t.Errorf("wrong path: %s", r.URL.Path) } w.WriteHeader(http.StatusNoContent) })) defer srv.Close() f := NewForge(srv.URL, "tok") if err := f.Admin.DeleteHook(context.Background(), 7); err != nil { t.Fatal(err) } } func TestAdminService_ListQuotaGroups_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { t.Errorf("expected GET, got %s", r.Method) } if r.URL.Path != "/api/v1/admin/quota/groups" { t.Errorf("wrong path: %s", r.URL.Path) } json.NewEncoder(w).Encode([]types.QuotaGroup{ { Name: "default", Rules: []*types.QuotaRuleInfo{ {Name: "git", Limit: 200000000, Subjects: []string{"size:repos:all"}}, }, }, { Name: "premium", }, }) })) defer srv.Close() f := NewForge(srv.URL, "tok") groups, err := f.Admin.ListQuotaGroups(context.Background()) if err != nil { t.Fatal(err) } if len(groups) != 2 { t.Fatalf("got %d groups, want 2", len(groups)) } if groups[0].Name != "default" { t.Errorf("got name=%q, want %q", groups[0].Name, "default") } if len(groups[0].Rules) != 1 || groups[0].Rules[0].Name != "git" { t.Errorf("unexpected rules: %+v", groups[0].Rules) } } func TestAdminService_CreateQuotaGroup_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("expected POST, got %s", r.Method) } if r.URL.Path != "/api/v1/admin/quota/groups" { t.Errorf("wrong path: %s", r.URL.Path) } var opts types.CreateQuotaGroupOptions if err := json.NewDecoder(r.Body).Decode(&opts); err != nil { t.Fatal(err) } if opts.Name != "newgroup" { t.Errorf("got name=%q, want %q", opts.Name, "newgroup") } if len(opts.Rules) != 1 || opts.Rules[0].Name != "git" { t.Fatalf("unexpected rules: %+v", opts.Rules) } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(types.QuotaGroup{ Name: opts.Name, Rules: []*types.QuotaRuleInfo{ { Name: opts.Rules[0].Name, Limit: opts.Rules[0].Limit, Subjects: opts.Rules[0].Subjects, }, }, }) })) defer srv.Close() f := NewForge(srv.URL, "tok") group, err := f.Admin.CreateQuotaGroup(context.Background(), &types.CreateQuotaGroupOptions{ Name: "newgroup", Rules: []*types.CreateQuotaRuleOptions{ { Name: "git", Limit: 200000000, Subjects: []string{"size:repos:all"}, }, }, }) if err != nil { t.Fatal(err) } if group.Name != "newgroup" { t.Errorf("got name=%q, want %q", group.Name, "newgroup") } if len(group.Rules) != 1 || group.Rules[0].Limit != 200000000 { t.Errorf("unexpected rules: %+v", group.Rules) } } func TestAdminService_GetQuotaGroup_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { t.Errorf("expected GET, got %s", r.Method) } if r.URL.Path != "/api/v1/admin/quota/groups/default" { t.Errorf("wrong path: %s", r.URL.Path) } json.NewEncoder(w).Encode(types.QuotaGroup{ Name: "default", Rules: []*types.QuotaRuleInfo{ {Name: "git", Limit: 200000000, Subjects: []string{"size:repos:all"}}, }, }) })) defer srv.Close() f := NewForge(srv.URL, "tok") group, err := f.Admin.GetQuotaGroup(context.Background(), "default") if err != nil { t.Fatal(err) } if group.Name != "default" { t.Errorf("got name=%q, want %q", group.Name, "default") } if len(group.Rules) != 1 || group.Rules[0].Name != "git" { t.Fatalf("unexpected rules: %+v", group.Rules) } } func TestAdminService_DeleteQuotaGroup_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete { t.Errorf("expected DELETE, got %s", r.Method) } if r.URL.Path != "/api/v1/admin/quota/groups/default" { t.Errorf("wrong path: %s", r.URL.Path) } w.WriteHeader(http.StatusNoContent) })) defer srv.Close() f := NewForge(srv.URL, "tok") if err := f.Admin.DeleteQuotaGroup(context.Background(), "default"); err != nil { t.Fatal(err) } } func TestAdminService_SearchEmails_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { t.Errorf("expected GET, got %s", r.Method) } if r.URL.Path != "/api/v1/admin/emails/search" { t.Errorf("wrong path: %s", r.URL.Path) } if got := r.URL.Query().Get("q"); got != "alice" { t.Errorf("got q=%q, want %q", got, "alice") } w.Header().Set("X-Total-Count", "1") json.NewEncoder(w).Encode([]types.Email{ {Email: "alice@example.com", Primary: true}, }) })) defer srv.Close() f := NewForge(srv.URL, "tok") emails, err := f.Admin.SearchEmails(context.Background(), "alice") if err != nil { t.Fatal(err) } if len(emails) != 1 { t.Errorf("got %d emails, want 1", len(emails)) } if emails[0].Email != "alice@example.com" { t.Errorf("got email=%q, want %q", emails[0].Email, "alice@example.com") } } func TestAdminService_ListCron_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { t.Errorf("expected GET, got %s", r.Method) } if r.URL.Path != "/api/v1/admin/cron" { t.Errorf("wrong path: %s", r.URL.Path) } w.Header().Set("X-Total-Count", "1") json.NewEncoder(w).Encode([]types.Cron{ {Name: "repo_health_check", Schedule: "@every 24h"}, }) })) defer srv.Close() f := NewForge(srv.URL, "tok") crons, err := f.Admin.ListCron(context.Background()) if err != nil { t.Fatal(err) } if len(crons) != 1 { t.Errorf("got %d crons, want 1", len(crons)) } if crons[0].Name != "repo_health_check" { t.Errorf("got name=%q, want %q", crons[0].Name, "repo_health_check") } } func TestAdminService_AdoptRepo_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("expected POST, got %s", r.Method) } if r.URL.Path != "/api/v1/admin/unadopted/alice/myrepo" { t.Errorf("wrong path: %s", r.URL.Path) } w.WriteHeader(http.StatusNoContent) })) defer srv.Close() f := NewForge(srv.URL, "tok") if err := f.Admin.AdoptRepo(context.Background(), "alice", "myrepo"); err != nil { t.Fatal(err) } } func TestAdminService_GenerateRunnerToken_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { t.Errorf("expected GET, got %s", r.Method) } if r.URL.Path != "/api/v1/admin/runners/registration-token" { t.Errorf("wrong path: %s", r.URL.Path) } json.NewEncoder(w).Encode(map[string]string{"token": "abc123"}) })) defer srv.Close() f := NewForge(srv.URL, "tok") token, err := f.Admin.GenerateRunnerToken(context.Background()) if err != nil { t.Fatal(err) } if token != "abc123" { t.Errorf("got token=%q, want %q", token, "abc123") } } func TestAdminService_DeleteUser_NotFound_Bad(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(map[string]string{"message": "user not found"}) })) defer srv.Close() f := NewForge(srv.URL, "tok") err := f.Admin.DeleteUser(context.Background(), "nonexistent") if !IsNotFound(err) { t.Errorf("expected not found error, got %v", err) } } func TestAdminService_CreateUser_Forbidden_Bad(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusForbidden) json.NewEncoder(w).Encode(map[string]string{"message": "only admins can create users"}) })) defer srv.Close() f := NewForge(srv.URL, "tok") _, err := f.Admin.CreateUser(context.Background(), &types.CreateUserOption{ Username: "newuser", Email: "new@example.com", }) if !IsForbidden(err) { t.Errorf("expected forbidden error, got %v", err) } }