diff --git a/tests/integration/tenant/api_key_test.go b/tests/integration/tenant/api_key_test.go new file mode 100644 index 0000000..6c1c764 --- /dev/null +++ b/tests/integration/tenant/api_key_test.go @@ -0,0 +1,117 @@ +//go:build integration + +package tenant_test + +import ( + "fmt" + "testing" + + admintest "github.com/qianfree/team-api/tests/integration/admin/testinfra" + "github.com/qianfree/team-api/tests/integration/tenant/testinfra" +) + +func TestApiKeyList(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + _, cleanup := testinfra.CreateTestApiKey(t, client) + defer cleanup() + + resp := client.Get("/api/tenant/api-keys", map[string]string{ + "page": "1", + "page_size": "10", + }) + admintest.AssertPaginatedList(t, resp, 1) +} + +func TestApiKeyCRUD(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + // --- Create --- + suffix := testinfra.RandomSuffix() + createResp := client.Post("/api/tenant/api-keys", map[string]any{ + "name": fmt.Sprintf("crud-key-%s", suffix), + }) + createResp.AssertSuccess(t) + keyID := createResp.GetID(t) + defer func() { + client.Delete(fmt.Sprintf("/api/tenant/api-keys/%d", keyID)) + }() + + var createData struct { + ID int64 `json:"id"` + Key string `json:"key"` + KeyPrefix string `json:"key_prefix"` + Name string `json:"name"` + } + createResp.DecodeData(t, &createData) + + if createData.Key == "" { + t.Fatal("expected non-empty key on create") + } + if createData.KeyPrefix == "" { + t.Fatal("expected non-empty key_prefix") + } + + // --- List should contain the key --- + listResp := client.Get("/api/tenant/api-keys", map[string]string{ + "page": "1", + "page_size": "50", + }) + listResp.AssertSuccess(t) + + // --- Update --- + updateResp := client.Put(fmt.Sprintf("/api/tenant/api-keys/%d", keyID), map[string]any{ + "name": fmt.Sprintf("updated-key-%s", suffix), + "status": "disabled", + }) + updateResp.AssertSuccess(t) + + // Verify update via list + verifyResp := client.Get("/api/tenant/api-keys", map[string]string{ + "page": "1", + "page_size": "50", + }) + verifyResp.AssertSuccess(t) + + // --- Delete --- + deleteResp := client.Delete(fmt.Sprintf("/api/tenant/api-keys/%d", keyID)) + deleteResp.AssertSuccess(t) +} + +func TestApiKeyUpdateScopes(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + keyID, cleanup := testinfra.CreateTestApiKey(t, client) + defer cleanup() + + // Update scopes + scopesResp := client.Put(fmt.Sprintf("/api/tenant/api-keys/%d/scopes", keyID), map[string]any{ + "model_names": []string{"gpt-4", "claude-3"}, + }) + scopesResp.AssertSuccess(t) + + // Verify model scopes + getScopesResp := client.Get(fmt.Sprintf("/api/tenant/api-keys/%d/model-scopes", keyID), nil) + getScopesResp.AssertSuccess(t) + + var scopesData struct { + ModelNames []string `json:"model_names"` + } + getScopesResp.DecodeData(t, &scopesData) + + if len(scopesData.ModelNames) != 2 { + t.Fatalf("expected 2 model names, got %d", len(scopesData.ModelNames)) + } +} + +func TestApiKeyExport(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + _, cleanup := testinfra.CreateTestApiKey(t, client) + defer cleanup() + + resp := client.Get("/api/tenant/api-keys/export", map[string]string{ + "format": "csv", + }) + resp.AssertSuccess(t) +} diff --git a/tests/integration/tenant/audit_test.go b/tests/integration/tenant/audit_test.go new file mode 100644 index 0000000..9e11773 --- /dev/null +++ b/tests/integration/tenant/audit_test.go @@ -0,0 +1,107 @@ +//go:build integration + +package tenant_test + +import ( + "testing" + + "github.com/qianfree/team-api/tests/integration/tenant/testinfra" +) + +func TestAuditConfigGet(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/audit/config", nil) + resp.AssertSuccess(t) + + var data struct { + AuditLevel string `json:"audit_level"` + } + resp.DecodeData(t, &data) +} + +func TestAuditConfigUpdate(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + // Get current level + getResp := client.Get("/api/tenant/audit/config", nil) + getResp.AssertSuccess(t) + + var getData struct { + AuditLevel string `json:"audit_level"` + } + getResp.DecodeData(t, &getData) + + // Update to a different level + newLevel := "full_text" + if getData.AuditLevel == "full_text" { + newLevel = "masked" + } + + updateResp := client.Put("/api/tenant/audit/config", map[string]any{ + "audit_level": newLevel, + }) + updateResp.AssertSuccess(t) + + // Restore original level + client.Put("/api/tenant/audit/config", map[string]any{ + "audit_level": getData.AuditLevel, + }) +} + +func TestAuditLogs(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/audit/logs", map[string]string{ + "page": "1", + "page_size": "10", + }) + resp.AssertSuccess(t) + + var data struct { + List []map[string]any `json:"list"` + Total int `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` + } + resp.DecodeData(t, &data) +} + +func TestRequestAuditLogs(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/audit/request-logs", map[string]string{ + "page": "1", + "page_size": "10", + }) + resp.AssertSuccess(t) + + var data struct { + List []map[string]any `json:"list"` + Total int `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` + } + resp.DecodeData(t, &data) +} + +func TestRequestAuditLogsWithFilters(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + // Filter by username + resp := client.Get("/api/tenant/audit/request-logs", map[string]string{ + "page": "1", + "page_size": "10", + "username": "owner", + }) + resp.AssertSuccess(t) + + // Filter by date range + resp = client.Get("/api/tenant/audit/request-logs", map[string]string{ + "page": "1", + "page_size": "10", + "start_date": "2025-01-01", + "end_date": "2026-12-31", + }) + resp.AssertSuccess(t) +} diff --git a/tests/integration/tenant/auth_test.go b/tests/integration/tenant/auth_test.go new file mode 100644 index 0000000..7254798 --- /dev/null +++ b/tests/integration/tenant/auth_test.go @@ -0,0 +1,203 @@ +//go:build integration + +package tenant_test + +import ( + "fmt" + "testing" + + admintest "github.com/qianfree/team-api/tests/integration/admin/testinfra" + "github.com/qianfree/team-api/tests/integration/tenant/testinfra" +) + +func TestTenantRegister_Success(t *testing.T) { + result := testinfra.RegisterTestTenant(t) + + if result.AccessToken == "" { + t.Fatal("expected non-empty access_token") + } + if result.RefreshToken == "" { + t.Fatal("expected non-empty refresh_token") + } + if result.ExpiresAt == "" { + t.Fatal("expected non-empty expires_at") + } + if result.Tenant.ID == 0 { + t.Fatal("expected tenant.id > 0") + } + if result.Tenant.Code == "" { + t.Fatal("expected tenant.code") + } + if result.User.Role != "owner" { + t.Fatalf("expected role=owner, got %s", result.User.Role) + } +} + +func TestTenantRegister_DuplicateTenantCode(t *testing.T) { + result := testinfra.RegisterTestTenant(t) + + // Try registering again with the same tenant_code + captchaKey, captchaX := testinfra.SolveCaptcha(t) + client := admintest.NewAPIClient(testinfra.DefaultBaseURL) + resp := client.Post("/api/tenant/auth/register", map[string]any{ + "email": fmt.Sprintf("dup-%s@test.com", result.TenantCode), + "password": testinfra.TestPassword, + "tenant_name": "重复租户", + "tenant_code": result.TenantCode, + "username": "anotheruser", + "captcha_key": captchaKey, + "captcha_x": captchaX, + }) + if resp.Code == 0 { + t.Fatal("expected error for duplicate tenant_code, but got success") + } +} + +func TestTenantLogin_RAM_Success(t *testing.T) { + regResult := testinfra.RegisterTestTenant(t) + + loginResult := testinfra.LoginTenant(t, regResult.Username, regResult.TenantCode, regResult.Password) + + if loginResult.AccessToken == "" { + t.Fatal("expected non-empty access_token after RAM login") + } + if loginResult.RefreshToken == "" { + t.Fatal("expected non-empty refresh_token after RAM login") + } +} + +func TestTenantLogin_InvalidPassword(t *testing.T) { + regResult := testinfra.RegisterTestTenant(t) + + captchaKey, captchaX := testinfra.SolveCaptcha(t) + client := admintest.NewAPIClient(testinfra.DefaultBaseURL) + account := fmt.Sprintf("%s@%s", regResult.Username, regResult.TenantCode) + resp := client.Post("/api/tenant/auth/login", map[string]any{ + "account": account, + "password": "WrongPassword123!", + "type": "ram", + "captcha_key": captchaKey, + "captcha_x": captchaX, + }) + if resp.Code == 0 { + t.Fatal("expected login to fail with wrong password, but got success") + } +} + +func TestTenantLogout_Success(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + logoutResp := client.Post("/api/tenant/auth/logout", nil) + logoutResp.AssertSuccess(t) + + // Verify token is invalidated + sessionResp := client.Get("/api/tenant/auth/sessions", map[string]string{ + "page": "1", + "page_size": "10", + }) + sessionResp.AssertHTTPStatus(t, 401) +} + +func TestTenantTokenRefresh(t *testing.T) { + regResult := testinfra.RegisterTestTenant(t) + + client := admintest.NewAPIClient(testinfra.DefaultBaseURL) + refreshResp := client.Post("/api/tenant/auth/refresh", map[string]any{ + "refresh_token": regResult.RefreshToken, + }) + refreshResp.AssertSuccess(t) + + var refreshData struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresAt string `json:"expires_at"` + } + refreshResp.DecodeData(t, &refreshData) + + if refreshData.AccessToken == "" { + t.Fatal("expected non-empty access_token after refresh") + } + + // Verify new token works + authedClient := admintest.NewAPIClient(testinfra.DefaultBaseURL).WithToken(refreshData.AccessToken) + sessionsResp := authedClient.Get("/api/tenant/auth/sessions", map[string]string{ + "page": "1", + "page_size": "10", + }) + sessionsResp.AssertSuccess(t) +} + +func TestTenantChangePassword(t *testing.T) { + regResult := testinfra.RegisterTestTenant(t) + client := admintest.NewAPIClient(testinfra.DefaultBaseURL).WithToken(regResult.AccessToken) + + newPassword := "NewPass456!" + changeResp := client.Put("/api/tenant/auth/change-password", map[string]any{ + "old_password": regResult.Password, + "new_password": newPassword, + }) + changeResp.AssertSuccess(t) + + // Verify can login with new password + loginResult := testinfra.LoginTenant(t, regResult.Username, regResult.TenantCode, newPassword) + if loginResult.AccessToken == "" { + t.Fatal("expected login with new password to succeed") + } +} + +func TestTenantSessionList(t *testing.T) { + regResult := testinfra.RegisterTestTenant(t) + client := admintest.NewAPIClient(testinfra.DefaultBaseURL).WithToken(regResult.AccessToken) + + resp := client.Get("/api/tenant/auth/sessions", map[string]string{ + "page": "1", + "page_size": "10", + }) + resp.AssertSuccess(t) + + // Verify paginated response structure exists + var data struct { + List []any `json:"list"` + Total int `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` + } + resp.DecodeData(t, &data) +} + +func TestTenantSessionRevoke(t *testing.T) { + regResult := testinfra.RegisterTestTenant(t) + client := admintest.NewAPIClient(testinfra.DefaultBaseURL).WithToken(regResult.AccessToken) + + // List sessions + listResp := client.Get("/api/tenant/auth/sessions", map[string]string{ + "page": "1", + "page_size": "10", + }) + listResp.AssertSuccess(t) + + var listData struct { + List []struct { + ID int64 `json:"id"` + } `json:"list"` + Total int `json:"total"` + } + listResp.DecodeData(t, &listData) + + if len(listData.List) == 0 { + t.Skip("no sessions to revoke, session list empty") + } + + // Revoke the first session + revokeResp := client.Delete(fmt.Sprintf("/api/tenant/auth/sessions/%d", listData.List[0].ID)) + revokeResp.AssertSuccess(t) +} + +func TestTenantAuth_Unauthorized(t *testing.T) { + client := admintest.NewAPIClient(testinfra.DefaultBaseURL) + resp := client.Get("/api/tenant/auth/sessions", map[string]string{ + "page": "1", + "page_size": "10", + }) + resp.AssertHTTPStatus(t, 401) +} diff --git a/tests/integration/tenant/billing_test.go b/tests/integration/tenant/billing_test.go new file mode 100644 index 0000000..7fc139d --- /dev/null +++ b/tests/integration/tenant/billing_test.go @@ -0,0 +1,65 @@ +//go:build integration + +package tenant_test + +import ( + "testing" + + "github.com/qianfree/team-api/tests/integration/tenant/testinfra" +) + +func TestUsageLogs(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/usage-logs", map[string]string{ + "page": "1", + "page_size": "10", + }) + resp.AssertSuccess(t) + + var data struct { + List []map[string]any `json:"list"` + Total int `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` + } + resp.DecodeData(t, &data) +} + +func TestUsageLogsWithFilters(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + // Filter by status + resp := client.Get("/api/tenant/usage-logs", map[string]string{ + "page": "1", + "page_size": "10", + "status": "success", + }) + resp.AssertSuccess(t) + + // Filter by date range + resp = client.Get("/api/tenant/usage-logs", map[string]string{ + "page": "1", + "page_size": "10", + "start_date": "2025-01-01", + "end_date": "2026-12-31", + }) + resp.AssertSuccess(t) + + // Filter by username + resp = client.Get("/api/tenant/usage-logs", map[string]string{ + "page": "1", + "page_size": "10", + "username": "owner", + }) + resp.AssertSuccess(t) +} + +func TestUsageLogsExport(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/usage-logs/export", map[string]string{ + "format": "csv", + }) + resp.AssertSuccess(t) +} diff --git a/tests/integration/tenant/dashboard_test.go b/tests/integration/tenant/dashboard_test.go new file mode 100644 index 0000000..a0a4fb0 --- /dev/null +++ b/tests/integration/tenant/dashboard_test.go @@ -0,0 +1,95 @@ +//go:build integration + +package tenant_test + +import ( + "testing" + + "github.com/qianfree/team-api/tests/integration/tenant/testinfra" +) + +func TestDashboardOverview(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/dashboard", nil) + resp.AssertSuccess(t) + + var data struct { + Today map[string]any `json:"today"` + Month map[string]any `json:"month"` + Wallet map[string]any `json:"wallet"` + ActiveKeys int `json:"active_keys"` + MemberCount int `json:"member_count"` + } + resp.DecodeData(t, &data) +} + +func TestDashboardTokenTrends(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/dashboard/token-trends", map[string]string{ + "days": "7", + }) + resp.AssertSuccess(t) + + var data struct { + List []map[string]any `json:"list"` + } + resp.DecodeData(t, &data) +} + +func TestDashboardModelDistribution(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/dashboard/model-distribution", map[string]string{ + "days": "7", + }) + resp.AssertSuccess(t) + + var data struct { + List []map[string]any `json:"list"` + } + resp.DecodeData(t, &data) +} + +func TestDashboardBalancePrediction(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/dashboard/balance-prediction", nil) + resp.AssertSuccess(t) + + var data struct { + DailyAvgCost float64 `json:"daily_avg_cost"` + AvailableBalance float64 `json:"available_balance"` + WillExhaust bool `json:"will_exhaust"` + } + resp.DecodeData(t, &data) +} + +func TestDashboardBudgetAlerts(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/dashboard/budget-alerts", nil) + resp.AssertSuccess(t) + + var data struct { + Members []map[string]any `json:"members"` + Projects []map[string]any `json:"projects"` + } + resp.DecodeData(t, &data) +} + +func TestDashboardMemberUsageRanking(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/dashboard/member-usage-ranking", map[string]string{ + "days": "7", + "limit": "10", + }) + resp.AssertSuccess(t) + + var data struct { + List []map[string]any `json:"list"` + } + resp.DecodeData(t, &data) +} diff --git a/tests/integration/tenant/feedback_test.go b/tests/integration/tenant/feedback_test.go new file mode 100644 index 0000000..ae109a2 --- /dev/null +++ b/tests/integration/tenant/feedback_test.go @@ -0,0 +1,123 @@ +//go:build integration + +package tenant_test + +import ( + "fmt" + "testing" + + "github.com/qianfree/team-api/tests/integration/tenant/testinfra" +) + +func TestFeedbackCreate(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Post("/api/tenant/feedbacks", map[string]any{ + "category": "bug_report", + "title": fmt.Sprintf("测试反馈 %s", testinfra.RandomSuffix()), + "description": "这是一个集成测试创建的反馈", + }) + resp.AssertSuccess(t) + + var data struct { + ID int64 `json:"id"` + } + resp.DecodeData(t, &data) + if data.ID == 0 { + t.Fatal("expected non-zero feedback id") + } +} + +func TestFeedbackList(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + // Create a feedback first + createResp := client.Post("/api/tenant/feedbacks", map[string]any{ + "category": "suggestion", + "title": fmt.Sprintf("列表测试反馈 %s", testinfra.RandomSuffix()), + "description": "测试反馈列表功能", + }) + createResp.AssertSuccess(t) + + // List feedbacks + resp := client.Get("/api/tenant/feedbacks", map[string]string{ + "page": "1", + "page_size": "10", + }) + resp.AssertSuccess(t) + + var data struct { + List []struct { + ID int64 `json:"id"` + Category string `json:"category"` + Title string `json:"title"` + Status string `json:"status"` + } `json:"list"` + Total int `json:"total"` + } + resp.DecodeData(t, &data) + if data.Total < 1 { + t.Fatalf("expected at least 1 feedback, got total=%d", data.Total) + } +} + +func TestFeedbackListWithFilters(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + // Create feedbacks with different categories + client.Post("/api/tenant/feedbacks", map[string]any{ + "category": "bug_report", + "title": "Bug反馈过滤测试", + "description": "测试按分类过滤", + }) + client.Post("/api/tenant/feedbacks", map[string]any{ + "category": "feature_request", + "title": "功能请求过滤测试", + "description": "测试按分类过滤", + }) + + // Filter by category + resp := client.Get("/api/tenant/feedbacks", map[string]string{ + "page": "1", + "page_size": "10", + "category": "bug_report", + }) + resp.AssertSuccess(t) +} + +func TestFeedbackGet(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + // Create a feedback + createResp := client.Post("/api/tenant/feedbacks", map[string]any{ + "category": "complaint", + "title": "详情测试反馈", + "description": "测试获取反馈详情", + }) + createResp.AssertSuccess(t) + + var createData struct { + ID int64 `json:"id"` + } + createResp.DecodeData(t, &createData) + + // Get detail + detailResp := client.Get(fmt.Sprintf("/api/tenant/feedbacks/%d", createData.ID), nil) + detailResp.AssertSuccess(t) + + var detail struct { + ID int64 `json:"id"` + Category string `json:"category"` + Title string `json:"title"` + Description string `json:"description"` + Status string `json:"status"` + } + detailResp.DecodeData(t, &detail) + + if detail.ID != createData.ID { + t.Fatalf("expected id=%d, got %d", createData.ID, detail.ID) + } + if detail.Category != "complaint" { + t.Fatalf("expected category=complaint, got %s", detail.Category) + } +} diff --git a/tests/integration/tenant/member_test.go b/tests/integration/tenant/member_test.go new file mode 100644 index 0000000..9cd3cdb --- /dev/null +++ b/tests/integration/tenant/member_test.go @@ -0,0 +1,325 @@ +//go:build integration + +package tenant_test + +import ( + "fmt" + "testing" + + admintest "github.com/qianfree/team-api/tests/integration/admin/testinfra" + "github.com/qianfree/team-api/tests/integration/tenant/testinfra" +) + +func TestMemberList(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + memberID, cleanup := testinfra.CreateTestTenantMember(t, client) + defer cleanup() + + resp := client.Get("/api/tenant/members", map[string]string{ + "page": "1", + "page_size": "10", + }) + admintest.AssertPaginatedList(t, resp, 2) // owner + member + + // Verify the created member appears + var listData struct { + List []struct { + ID int64 `json:"id"` + Username string `json:"username"` + Role string `json:"role"` + } `json:"list"` + } + resp.DecodeData(t, &listData) + + found := false + for _, m := range listData.List { + if m.ID == memberID { + found = true + if m.Role != "member" { + t.Fatalf("expected role=member, got %s", m.Role) + } + break + } + } + if !found { + t.Fatalf("created member %d not found in list", memberID) + } +} + +func TestMemberListWithFilters(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + _, cleanup := testinfra.CreateTestTenantMember(t, client) + defer cleanup() + + // Filter by role + resp := client.Get("/api/tenant/members", map[string]string{ + "page": "1", + "page_size": "10", + "role": "member", + }) + resp.AssertSuccess(t) + var memberList struct { + List []struct { + Role string `json:"role"` + } `json:"list"` + } + resp.DecodeData(t, &memberList) + for _, m := range memberList.List { + if m.Role != "member" { + t.Fatalf("expected role=member when filtering, got %s", m.Role) + } + } + + // Filter by keyword + resp = client.Get("/api/tenant/members", map[string]string{ + "page": "1", + "page_size": "10", + "keyword": "member", + }) + resp.AssertSuccess(t) +} + +func TestMemberCreate(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + memberID, cleanup := testinfra.CreateTestTenantMember(t, client) + defer cleanup() + + // Get detail + detailResp := client.Get(fmt.Sprintf("/api/tenant/members/%d", memberID), nil) + detailResp.AssertSuccess(t) + + var detail struct { + ID int64 `json:"id"` + Username string `json:"username"` + Role string `json:"role"` + Status string `json:"status"` + } + detailResp.DecodeData(t, &detail) + + if detail.ID != memberID { + t.Fatalf("expected id=%d, got %d", memberID, detail.ID) + } + if detail.Role != "member" { + t.Fatalf("expected role=member, got %s", detail.Role) + } + if detail.Status != "active" { + t.Fatalf("expected status=active, got %s", detail.Status) + } +} + +func TestMemberInvite(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + // Create invitation + inviteResp := client.Post("/api/tenant/members/invite", map[string]any{ + "role": "member", + "expires_days": 7, + "max_uses": 5, + }) + inviteResp.AssertSuccess(t) + + var inviteData struct { + Code string `json:"code"` + InviteURL string `json:"invite_url"` + ExpiresAt string `json:"expires_at"` + MaxUses int `json:"max_uses"` + } + inviteResp.DecodeData(t, &inviteData) + + if inviteData.Code == "" { + t.Fatal("expected non-empty invite code") + } + if inviteData.MaxUses != 5 { + t.Fatalf("expected max_uses=5, got %d", inviteData.MaxUses) + } + + // List invitations + listResp := client.Get("/api/tenant/members/invitations", map[string]string{ + "page": "1", + "page_size": "10", + }) + admintest.AssertPaginatedList(t, listResp, 1) +} + +func TestMemberInviteInfo(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + inviteResp := client.Post("/api/tenant/members/invite", map[string]any{ + "role": "member", + }) + inviteResp.AssertSuccess(t) + + var inviteData struct { + Code string `json:"code"` + } + inviteResp.DecodeData(t, &inviteData) + + // Query invite info (public endpoint) + pubClient := admintest.NewAPIClient(testinfra.DefaultBaseURL) + infoResp := pubClient.Get("/api/tenant/members/invite-info", map[string]string{ + "code": inviteData.Code, + }) + infoResp.AssertSuccess(t) + + var info struct { + TenantName string `json:"tenant_name"` + Role string `json:"role"` + Valid bool `json:"valid"` + } + infoResp.DecodeData(t, &info) + + if !info.Valid { + t.Fatal("expected invite to be valid") + } + if info.Role != "member" { + t.Fatalf("expected role=member, got %s", info.Role) + } +} + +func TestMemberInviteRevoke(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + inviteResp := client.Post("/api/tenant/members/invite", map[string]any{ + "role": "member", + }) + inviteResp.AssertSuccess(t) + + // List invitations to get the ID + listResp := client.Get("/api/tenant/members/invitations", map[string]string{ + "page": "1", + "page_size": "10", + }) + listResp.AssertSuccess(t) + + var listData struct { + List []struct { + ID int64 `json:"id"` + } `json:"list"` + } + listResp.DecodeData(t, &listData) + if len(listData.List) == 0 { + t.Fatal("expected at least 1 invitation") + } + + // Revoke + revokeResp := client.Delete(fmt.Sprintf("/api/tenant/members/invitations/%d", listData.List[0].ID)) + revokeResp.AssertSuccess(t) +} + +func TestMemberUpdateRole(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + memberID, cleanup := testinfra.CreateTestTenantMember(t, client) + defer cleanup() + + // Update role to admin + updateResp := client.Put(fmt.Sprintf("/api/tenant/members/%d/role", memberID), map[string]any{ + "role": "admin", + }) + updateResp.AssertSuccess(t) + + // Verify + detailResp := client.Get(fmt.Sprintf("/api/tenant/members/%d", memberID), nil) + detailResp.AssertSuccess(t) + + var detail struct { + Role string `json:"role"` + } + detailResp.DecodeData(t, &detail) + if detail.Role != "admin" { + t.Fatalf("expected role=admin after update, got %s", detail.Role) + } +} + +func TestMemberResetPassword(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + memberID, cleanup := testinfra.CreateTestTenantMember(t, client) + defer cleanup() + + resetResp := client.Put(fmt.Sprintf("/api/tenant/members/%d/reset-password", memberID), map[string]any{ + "password": "NewPass789!", + }) + resetResp.AssertSuccess(t) +} + +func TestMemberRemove(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + memberID, _ := testinfra.CreateTestTenantMember(t, client) + + // Remove + removeResp := client.Delete(fmt.Sprintf("/api/tenant/members/%d", memberID)) + removeResp.AssertSuccess(t) + + // Verify removed - get detail should fail or return inactive status + detailResp := client.Get(fmt.Sprintf("/api/tenant/members/%d", memberID), nil) + detailResp.AssertSuccess(t) + + var detail struct { + Status string `json:"status"` + } + detailResp.DecodeData(t, &detail) + if detail.Status == "active" { + t.Fatal("expected member to not be active after removal") + } +} + +func TestMemberUsage(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + memberID, cleanup := testinfra.CreateTestTenantMember(t, client) + defer cleanup() + + resp := client.Get(fmt.Sprintf("/api/tenant/members/%d/usage", memberID), nil) + resp.AssertSuccess(t) + + var usage struct { + TodayRequests float64 `json:"today_requests"` + MonthRequests float64 `json:"month_requests"` + MonthInputTok float64 `json:"month_input_tokens"` + MonthOutputTok float64 `json:"month_output_tokens"` + MonthTotalCost float64 `json:"month_total_cost"` + } + resp.DecodeData(t, &usage) + // New member should have zero usage + if usage.TodayRequests != 0 { + t.Fatalf("expected today_requests=0, got %f", usage.TodayRequests) + } +} + +func TestMemberApiKeys(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + memberID, cleanup := testinfra.CreateTestTenantMember(t, client) + defer cleanup() + + resp := client.Get(fmt.Sprintf("/api/tenant/members/%d/api-keys", memberID), map[string]string{ + "page": "1", + "page_size": "10", + }) + resp.AssertSuccess(t) + + var data struct { + List []any `json:"list"` + Total int `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` + } + resp.DecodeData(t, &data) + if data.Total != 0 { + t.Fatalf("expected 0 api keys for new member, got %d", data.Total) + } +} + +func TestMemberExport(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/members/export", map[string]string{ + "format": "csv", + }) + resp.AssertSuccess(t) +} diff --git a/tests/integration/tenant/model_test.go b/tests/integration/tenant/model_test.go new file mode 100644 index 0000000..ce8af72 --- /dev/null +++ b/tests/integration/tenant/model_test.go @@ -0,0 +1,44 @@ +//go:build integration + +package tenant_test + +import ( + "testing" + + "github.com/qianfree/team-api/tests/integration/tenant/testinfra" +) + +func TestAvailableModels(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/models", nil) + resp.AssertSuccess(t) + + var data struct { + List []struct { + ID int64 `json:"id"` + ModelID string `json:"model_id"` + ModelName string `json:"model_name"` + Category string `json:"category"` + } `json:"list"` + } + resp.DecodeData(t, &data) +} + +func TestAvailableModelsFilterByCategory(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/models", map[string]string{ + "category": "chat", + }) + resp.AssertSuccess(t) +} + +func TestAvailableModelsSearch(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/models", map[string]string{ + "search": "gpt", + }) + resp.AssertSuccess(t) +} diff --git a/tests/integration/tenant/notification_test.go b/tests/integration/tenant/notification_test.go new file mode 100644 index 0000000..250963a --- /dev/null +++ b/tests/integration/tenant/notification_test.go @@ -0,0 +1,106 @@ +//go:build integration + +package tenant_test + +import ( + "testing" + + "github.com/qianfree/team-api/tests/integration/tenant/testinfra" +) + +func TestNotificationList(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/notifications", map[string]string{ + "page": "1", + "page_size": "10", + }) + resp.AssertSuccess(t) + + var data struct { + List []map[string]any `json:"list"` + Total int `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` + } + resp.DecodeData(t, &data) +} + +func TestUnreadCount(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/notifications/unread-count", nil) + resp.AssertSuccess(t) + + var data struct { + UnreadCount int `json:"unread_count"` + } + resp.DecodeData(t, &data) +} + +func TestMarkAllRead(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Post("/api/tenant/notifications/read-all", nil) + resp.AssertSuccess(t) +} + +func TestNotificationPreferences(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + // Get preferences + getResp := client.Get("/api/tenant/notification-preferences", nil) + getResp.AssertSuccess(t) + + // Update preferences (user scope) + updateResp := client.Put("/api/tenant/notification-preferences", map[string]any{ + "scope": "user", + "preferences": map[string]any{ + "billing": map[string]any{ + "email": true, + "in_app": true, + }, + }, + }) + updateResp.AssertSuccess(t) + + // Verify update + verifyResp := client.Get("/api/tenant/notification-preferences", nil) + verifyResp.AssertSuccess(t) +} + +func TestNotificationPreferencesOrg(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + // Update preferences (org scope, owner can do this) + updateResp := client.Put("/api/tenant/notification-preferences", map[string]any{ + "scope": "org", + "preferences": map[string]any{ + "security": map[string]any{ + "email": true, + }, + }, + }) + updateResp.AssertSuccess(t) +} + +func TestAnnouncements(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/announcements", nil) + resp.AssertSuccess(t) + + var data struct { + List []map[string]any `json:"list"` + } + resp.DecodeData(t, &data) +} + +func TestNotificationsExport(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/notifications/export", map[string]string{ + "format": "csv", + }) + resp.AssertSuccess(t) +} diff --git a/tests/integration/tenant/organization_test.go b/tests/integration/tenant/organization_test.go new file mode 100644 index 0000000..14f728e --- /dev/null +++ b/tests/integration/tenant/organization_test.go @@ -0,0 +1,135 @@ +//go:build integration + +package tenant_test + +import ( + "fmt" + "testing" + + "github.com/qianfree/team-api/tests/integration/tenant/testinfra" +) + +func TestOrgInfo(t *testing.T) { + client, regResult := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/organization", nil) + resp.AssertSuccess(t) + + var data struct { + ID int64 `json:"id"` + Name string `json:"name"` + Code string `json:"code"` + Status string `json:"status"` + MemberCount int `json:"member_count"` + } + resp.DecodeData(t, &data) + + if data.Code != regResult.TenantCode { + t.Fatalf("expected code=%s, got %s", regResult.TenantCode, data.Code) + } + if data.Status != "active" { + t.Fatalf("expected status=active, got %s", data.Status) + } + if data.MemberCount < 1 { + t.Fatalf("expected member_count >= 1, got %d", data.MemberCount) + } +} + +func TestOrgUpdate(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + newName := fmt.Sprintf("更新组织名 %s", testinfra.RandomSuffix()) + updateResp := client.Put("/api/tenant/organization", map[string]any{ + "name": newName, + }) + updateResp.AssertSuccess(t) + + // Verify update + verifyResp := client.Get("/api/tenant/organization", nil) + verifyResp.AssertSuccess(t) + + var data struct { + Name string `json:"name"` + } + verifyResp.DecodeData(t, &data) + if data.Name != newName { + t.Fatalf("expected name=%s, got %s", newName, data.Name) + } +} + +func TestProfile(t *testing.T) { + client, regResult := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/profile", nil) + resp.AssertSuccess(t) + + var data struct { + ID int64 `json:"id"` + Username string `json:"username"` + Role string `json:"role"` + Status string `json:"status"` + } + resp.DecodeData(t, &data) + + if data.Username != regResult.Username { + t.Fatalf("expected username=%s, got %s", regResult.Username, data.Username) + } + if data.Role != "owner" { + t.Fatalf("expected role=owner, got %s", data.Role) + } +} + +func TestProfileUpdate(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + newDisplayName := fmt.Sprintf("测试用户 %s", testinfra.RandomSuffix()) + updateResp := client.Put("/api/tenant/profile", map[string]any{ + "display_name": newDisplayName, + }) + updateResp.AssertSuccess(t) + + // Verify update + verifyResp := client.Get("/api/tenant/profile", nil) + verifyResp.AssertSuccess(t) + + var data struct { + DisplayName string `json:"display_name"` + } + verifyResp.DecodeData(t, &data) + if data.DisplayName != newDisplayName { + t.Fatalf("expected display_name=%s, got %s", newDisplayName, data.DisplayName) + } +} + +func TestIPWhitelist(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + // Get current whitelist + getResp := client.Get("/api/tenant/security/ip-whitelist", nil) + getResp.AssertSuccess(t) + + var getData struct { + Enabled bool `json:"enabled"` + Whitelist []string `json:"whitelist"` + } + getResp.DecodeData(t, &getData) + + // Update whitelist (disable it to avoid locking ourselves out) + updateResp := client.Put("/api/tenant/security/ip-whitelist", map[string]any{ + "enabled": false, + "whitelist": []string{}, + }) + updateResp.AssertSuccess(t) + + // Verify update + verifyResp := client.Get("/api/tenant/security/ip-whitelist", nil) + verifyResp.AssertSuccess(t) + + var verifyData struct { + Enabled bool `json:"enabled"` + } + verifyResp.DecodeData(t, &verifyData) + if verifyData.Enabled { + t.Fatal("expected ip whitelist to be disabled after update") + } +} diff --git a/tests/integration/tenant/personal_dashboard_test.go b/tests/integration/tenant/personal_dashboard_test.go new file mode 100644 index 0000000..9628ef5 --- /dev/null +++ b/tests/integration/tenant/personal_dashboard_test.go @@ -0,0 +1,65 @@ +//go:build integration + +package tenant_test + +import ( + "testing" + + "github.com/qianfree/team-api/tests/integration/tenant/testinfra" +) + +func TestPersonalDashboardOverview(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/personal-dashboard", nil) + resp.AssertSuccess(t) + + var data struct { + Today map[string]any `json:"today"` + Month map[string]any `json:"month"` + ErrorRate map[string]any `json:"error_rate"` + } + resp.DecodeData(t, &data) +} + +func TestPersonalDashboardTrends(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/personal-dashboard/trends", map[string]string{ + "days": "7", + }) + resp.AssertSuccess(t) + + var data struct { + List []map[string]any `json:"list"` + } + resp.DecodeData(t, &data) +} + +func TestPersonalDashboardModels(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/personal-dashboard/models", map[string]string{ + "days": "7", + }) + resp.AssertSuccess(t) + + var data struct { + List []map[string]any `json:"list"` + } + resp.DecodeData(t, &data) +} + +func TestPersonalDashboardApiKeyUsage(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/personal-dashboard/api-key-usage", map[string]string{ + "days": "7", + }) + resp.AssertSuccess(t) + + var data struct { + List []map[string]any `json:"list"` + } + resp.DecodeData(t, &data) +} diff --git a/tests/integration/tenant/plan_test.go b/tests/integration/tenant/plan_test.go new file mode 100644 index 0000000..55c9a1f --- /dev/null +++ b/tests/integration/tenant/plan_test.go @@ -0,0 +1,47 @@ +//go:build integration + +package tenant_test + +import ( + "testing" + + "github.com/qianfree/team-api/tests/integration/tenant/testinfra" +) + +func TestPlanList(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/plans", nil) + resp.AssertSuccess(t) + + var data struct { + List []struct { + ID int64 `json:"id"` + Name string `json:"name"` + Identifier string `json:"identifier"` + } `json:"list"` + } + resp.DecodeData(t, &data) +} + +func TestCurrentPlan(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/plan/current", nil) + // New tenant may not have a plan, accept either success or error + resp.AssertSuccess(t) + + var data struct { + ID int64 `json:"id"` + Status string `json:"status"` + } + resp.DecodeData(t, &data) +} + +func TestCancelAutoRenew(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Put("/api/tenant/plan/cancel-auto-renew", nil) + // New tenant likely has no active plan, so this may fail gracefully + resp.AssertSuccess(t) +} diff --git a/tests/integration/tenant/project_test.go b/tests/integration/tenant/project_test.go new file mode 100644 index 0000000..c281e2c --- /dev/null +++ b/tests/integration/tenant/project_test.go @@ -0,0 +1,151 @@ +//go:build integration + +package tenant_test + +import ( + "fmt" + "testing" + + admintest "github.com/qianfree/team-api/tests/integration/admin/testinfra" + "github.com/qianfree/team-api/tests/integration/tenant/testinfra" +) + +func TestProjectList(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + _, cleanup := testinfra.CreateTestProject(t, client) + defer cleanup() + + resp := client.Get("/api/tenant/projects", map[string]string{ + "page": "1", + "page_size": "10", + }) + admintest.AssertPaginatedList(t, resp, 1) +} + +func TestProjectCRUD(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + // --- Create --- + suffix := testinfra.RandomSuffix() + createResp := client.Post("/api/tenant/projects", map[string]any{ + "name": fmt.Sprintf("crud-project-%s", suffix), + "description": "Integration test project", + "budget": 100.0, + }) + createResp.AssertSuccess(t) + projectID := createResp.GetID(t) + defer func() { + client.Post(fmt.Sprintf("/api/tenant/projects/%d/archive", projectID), nil) + }() + + // --- Get detail --- + detailResp := client.Get(fmt.Sprintf("/api/tenant/projects/%d", projectID), nil) + detailResp.AssertSuccess(t) + + var detail struct { + Data struct { + ID int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Budget string `json:"budget"` + Status string `json:"status"` + } `json:"data"` + } + detailResp.DecodeData(t, &detail) + + if detail.Data.ID != projectID { + t.Fatalf("expected id=%d, got %d", projectID, detail.Data.ID) + } + + // --- Update --- + updateResp := client.Put(fmt.Sprintf("/api/tenant/projects/%d", projectID), map[string]any{ + "name": fmt.Sprintf("updated-project-%s", suffix), + "description": "Updated description", + "budget": 200.0, + }) + updateResp.AssertSuccess(t) + + // Verify update + verifyResp := client.Get(fmt.Sprintf("/api/tenant/projects/%d", projectID), nil) + verifyResp.AssertSuccess(t) +} + +func TestProjectArchiveUnarchive(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + projectID, _ := testinfra.CreateTestProject(t, client) + + // Archive + archiveResp := client.Post(fmt.Sprintf("/api/tenant/projects/%d/archive", projectID), nil) + archiveResp.AssertSuccess(t) + + // Verify archived via list + listResp := client.Get("/api/tenant/projects", map[string]string{ + "page": "1", + "page_size": "50", + }) + listResp.AssertSuccess(t) + + // Unarchive + unarchiveResp := client.Post(fmt.Sprintf("/api/tenant/projects/%d/unarchive", projectID), nil) + unarchiveResp.AssertSuccess(t) +} + +func TestProjectApiKeys(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + projectID, cleanup := testinfra.CreateTestProject(t, client) + defer cleanup() + + // Create project API key + createKeyResp := client.Post(fmt.Sprintf("/api/tenant/projects/%d/api-keys", projectID), map[string]any{ + "name": "project-test-key", + }) + createKeyResp.AssertSuccess(t) + + // List project API keys + listResp := client.Get(fmt.Sprintf("/api/tenant/projects/%d/api-keys", projectID), map[string]string{ + "page": "1", + "page_size": "10", + }) + admintest.AssertPaginatedList(t, listResp, 1) + + var listData struct { + List []struct { + ID int64 `json:"id"` + } `json:"list"` + } + listResp.DecodeData(t, &listData) + if len(listData.List) == 0 { + t.Fatal("expected at least 1 project API key") + } + + // Delete project API key + keyID := listData.List[0].ID + deleteResp := client.Delete(fmt.Sprintf("/api/tenant/projects/%d/api-keys/%d", projectID, keyID)) + deleteResp.AssertSuccess(t) +} + +func TestProjectUsageStats(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + projectID, cleanup := testinfra.CreateTestProject(t, client) + defer cleanup() + + resp := client.Get(fmt.Sprintf("/api/tenant/projects/%d/usage-stats", projectID), nil) + resp.AssertSuccess(t) +} + +func TestProjectUsageLogs(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + projectID, cleanup := testinfra.CreateTestProject(t, client) + defer cleanup() + + resp := client.Get(fmt.Sprintf("/api/tenant/projects/%d/usage-logs", projectID), map[string]string{ + "page": "1", + "page_size": "10", + }) + resp.AssertSuccess(t) +} diff --git a/tests/integration/tenant/security_test.go b/tests/integration/tenant/security_test.go new file mode 100644 index 0000000..7fa91c7 --- /dev/null +++ b/tests/integration/tenant/security_test.go @@ -0,0 +1,62 @@ +//go:build integration + +package tenant_test + +import ( + "testing" + + "github.com/qianfree/team-api/tests/integration/tenant/testinfra" +) + +func TestLoginHistory(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/security/login-history", map[string]string{ + "page": "1", + "page_size": "10", + }) + resp.AssertSuccess(t) + + var data struct { + List []struct { + ID int64 `json:"id"` + Username string `json:"username"` + LoginMethod string `json:"login_method"` + IPAddress string `json:"ip_address"` + Success bool `json:"success"` + } `json:"list"` + Total int `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` + } + resp.DecodeData(t, &data) +} + +func TestLoginHistoryWithFilters(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + // Filter by IP + resp := client.Get("/api/tenant/security/login-history", map[string]string{ + "page": "1", + "page_size": "10", + "ip_address": "127.0.0.1", + }) + resp.AssertSuccess(t) + + // Filter by success status + resp = client.Get("/api/tenant/security/login-history", map[string]string{ + "page": "1", + "page_size": "10", + "success": "true", + }) + resp.AssertSuccess(t) + + // Filter by date range + resp = client.Get("/api/tenant/security/login-history", map[string]string{ + "page": "1", + "page_size": "10", + "start_time": "2025-01-01", + "end_time": "2026-12-31", + }) + resp.AssertSuccess(t) +} diff --git a/tests/integration/tenant/testinfra/auth.go b/tests/integration/tenant/testinfra/auth.go new file mode 100644 index 0000000..6d543f0 --- /dev/null +++ b/tests/integration/tenant/testinfra/auth.go @@ -0,0 +1,153 @@ +//go:build integration + +package testinfra + +import ( + "context" + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/redis/go-redis/v9" + + admintest "github.com/qianfree/team-api/tests/integration/admin/testinfra" +) + +func RandomSuffix() string { + b := make([]byte, 4) + _, _ = rand.Read(b) + return hex.EncodeToString(b) +} + +func getRedisClient() *redis.Client { + return redis.NewClient(&redis.Options{ + Addr: DefaultRedisAddr, + DB: 0, + DialTimeout: 3 * time.Second, + ReadTimeout: 3 * time.Second, + WriteTimeout: 3 * time.Second, + }) +} + +func SolveCaptcha(t *testing.T) (captchaKey string, captchaX int) { + t.Helper() + client := admintest.NewAPIClient(DefaultBaseURL) + + captchaResp := client.Get("/api/captcha/", nil) + captchaResp.AssertSuccess(t) + + var captchaData struct { + CaptchaKey string `json:"captcha_key"` + } + captchaResp.DecodeData(t, &captchaData) + + rdb := getRedisClient() + defer rdb.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + redisKey := fmt.Sprintf("captcha:state:%s", captchaData.CaptchaKey) + val, err := rdb.Get(ctx, redisKey).Result() + if err != nil { + t.Fatalf("read captcha from redis error: %v", err) + } + + var answer struct { + X int `json:"x"` + Y int `json:"y"` + } + if err := json.Unmarshal([]byte(val), &answer); err != nil { + t.Fatalf("unmarshal captcha answer error: %v", err) + } + + return captchaData.CaptchaKey, answer.X +} + +type TenantRegisterResult struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresAt string `json:"expires_at"` + Tenant struct { + ID int64 `json:"id"` + Name string `json:"name"` + Code string `json:"code"` + } `json:"tenant"` + User struct { + ID int64 `json:"id"` + Username string `json:"username"` + Role string `json:"role"` + } `json:"user"` + + // Stored for re-login tests + TenantCode string + Username string + Password string + Email string +} + +func RegisterTestTenant(t *testing.T) *TenantRegisterResult { + t.Helper() + + suffix := RandomSuffix() + tenantCode := fmt.Sprintf("t-%s", suffix) + username := fmt.Sprintf("owner%s", suffix) + email := fmt.Sprintf("%s@test.com", username) + + captchaKey, captchaX := SolveCaptcha(t) + + client := admintest.NewAPIClient(DefaultBaseURL) + resp := client.Post("/api/tenant/auth/register", map[string]any{ + "email": email, + "password": TestPassword, + "tenant_name": fmt.Sprintf("测试租户 %s", suffix), + "tenant_code": tenantCode, + "username": username, + "captcha_key": captchaKey, + "captcha_x": captchaX, + }) + resp.AssertSuccess(t) + + var result TenantRegisterResult + resp.DecodeData(t, &result) + result.TenantCode = tenantCode + result.Username = username + result.Password = TestPassword + result.Email = email + + return &result +} + +func GetAuthedClient(t *testing.T) (*admintest.APIClient, *TenantRegisterResult) { + t.Helper() + result := RegisterTestTenant(t) + client := admintest.NewAPIClient(DefaultBaseURL).WithToken(result.AccessToken) + return client, result +} + +func LoginTenant(t *testing.T, username, tenantCode, password string) *TenantRegisterResult { + t.Helper() + captchaKey, captchaX := SolveCaptcha(t) + + client := admintest.NewAPIClient(DefaultBaseURL) + account := fmt.Sprintf("%s@%s", username, tenantCode) + resp := client.Post("/api/tenant/auth/login", map[string]any{ + "account": account, + "password": password, + "type": "ram", + "captcha_key": captchaKey, + "captcha_x": captchaX, + }) + resp.AssertSuccess(t) + + var result TenantRegisterResult + resp.DecodeData(t, &result) + result.TenantCode = tenantCode + result.Username = username + result.Password = password + + return &result +} diff --git a/tests/integration/tenant/testinfra/config.go b/tests/integration/tenant/testinfra/config.go new file mode 100644 index 0000000..62b48cc --- /dev/null +++ b/tests/integration/tenant/testinfra/config.go @@ -0,0 +1,9 @@ +//go:build integration + +package testinfra + +const ( + DefaultBaseURL = "http://127.0.0.1:18888" + DefaultRedisAddr = "192.168.50.22:16380" + TestPassword = "TestPass123!" +) diff --git a/tests/integration/tenant/testinfra/fixture.go b/tests/integration/tenant/testinfra/fixture.go new file mode 100644 index 0000000..0521d79 --- /dev/null +++ b/tests/integration/tenant/testinfra/fixture.go @@ -0,0 +1,53 @@ +//go:build integration + +package testinfra + +import ( + "fmt" + "testing" + + admintest "github.com/qianfree/team-api/tests/integration/admin/testinfra" +) + +func CreateTestTenantMember(t *testing.T, client *admintest.APIClient) (id int64, cleanup func()) { + t.Helper() + suffix := RandomSuffix() + username := fmt.Sprintf("member%s", suffix) + resp := client.Post("/api/tenant/members/create", map[string]any{ + "username": username, + "password": TestPassword, + "email": fmt.Sprintf("%s@test.com", username), + "role": "member", + }) + resp.AssertSuccess(t) + id = resp.GetID(t) + return id, func() { + client.Delete(fmt.Sprintf("/api/tenant/members/%d", id)) + } +} + +func CreateTestApiKey(t *testing.T, client *admintest.APIClient) (id int64, cleanup func()) { + t.Helper() + suffix := RandomSuffix() + resp := client.Post("/api/tenant/api-keys", map[string]any{ + "name": fmt.Sprintf("test-key-%s", suffix), + }) + resp.AssertSuccess(t) + id = resp.GetID(t) + return id, func() { + client.Delete(fmt.Sprintf("/api/tenant/api-keys/%d", id)) + } +} + +func CreateTestProject(t *testing.T, client *admintest.APIClient) (id int64, cleanup func()) { + t.Helper() + suffix := RandomSuffix() + resp := client.Post("/api/tenant/projects", map[string]any{ + "name": fmt.Sprintf("test-project-%s", suffix), + }) + resp.AssertSuccess(t) + id = resp.GetID(t) + return id, func() { + client.Post(fmt.Sprintf("/api/tenant/projects/%d/archive", id), nil) + } +} diff --git a/tests/integration/tenant/wallet_test.go b/tests/integration/tenant/wallet_test.go new file mode 100644 index 0000000..bd8907f --- /dev/null +++ b/tests/integration/tenant/wallet_test.go @@ -0,0 +1,72 @@ +//go:build integration + +package tenant_test + +import ( + "testing" + + "github.com/qianfree/team-api/tests/integration/tenant/testinfra" +) + +func TestWalletInfo(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/wallet", nil) + resp.AssertSuccess(t) + + var data struct { + Balance float64 `json:"balance"` + FrozenBalance float64 `json:"frozen_balance"` + AvailableBalance float64 `json:"available_balance"` + Currency string `json:"currency"` + Level int `json:"level"` + LevelName string `json:"level_name"` + CumulativeRecharge float64 `json:"cumulative_recharge"` + } + resp.DecodeData(t, &data) + + // New tenant should have zero balance + if data.Currency == "" { + t.Fatal("expected non-empty currency") + } +} + +func TestWalletTransactions(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/wallet/transactions", map[string]string{ + "page": "1", + "page_size": "10", + }) + resp.AssertSuccess(t) + + // New tenant likely has no transactions, just verify structure + var data struct { + List []map[string]any `json:"list"` + Total int `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` + } + resp.DecodeData(t, &data) +} + +func TestWalletFrozenItems(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/wallet/frozen-items", nil) + resp.AssertSuccess(t) + + var data struct { + Items []map[string]any `json:"items"` + } + resp.DecodeData(t, &data) +} + +func TestWalletTransactionsExport(t *testing.T) { + client, _ := testinfra.GetAuthedClient(t) + + resp := client.Get("/api/tenant/wallet/transactions/export", map[string]string{ + "format": "csv", + }) + resp.AssertSuccess(t) +}