feat(test):新增管理端API自动化测试

This commit is contained in:
qian committed 2026-05-27 22:44:11 +08:00
1 parent 706b2da5b4
commit 104f1de0b3
36 files changed
+4195

No files matched your search

+204
View File
@@ -0,0 +1,204 @@
//go:build integration
package admin_test
import (
"fmt"
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestAdminUserList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/users", map[string]string{
"page": "1",
"page_size": "10",
})
testinfra.AssertPaginatedList(t, resp, 1)
}
func TestAdminUserCRUD(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// Create
userID, cleanup := testinfra.CreateTestAdminUser(t, client)
defer cleanup()
t.Logf("created admin user id=%d", userID)
// Read — find the created user via list with keyword filter
listResp := client.Get("/api/admin/users", map[string]string{
"page": "1",
"page_size": "10",
})
listResp.AssertSuccess(t)
var listData struct {
List []struct {
ID int64 `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
DisplayName string `json:"display_name"`
Role string `json:"role"`
Status string `json:"status"`
} `json:"list"`
Total int `json:"total"`
}
listResp.DecodeData(t, &listData)
found := false
for _, u := range listData.List {
if u.ID == userID {
found = true
if u.Role != "admin" {
t.Fatalf("expected role=admin, got %s", u.Role)
}
break
}
}
if !found {
t.Fatalf("created user id=%d not found in list", userID)
}
// Update
updateResp := client.Put(fmt.Sprintf("/api/admin/users/%d", userID), map[string]any{
"display_name": "Updated Name",
"email": fmt.Sprintf("updated-%d@test.com", userID),
"role": "admin",
})
updateResp.AssertSuccess(t)
// Verify update
listResp2 := client.Get("/api/admin/users", map[string]string{
"page": "1",
"page_size": "100",
})
listResp2.AssertSuccess(t)
listResp2.DecodeData(t, &listData)
for _, u := range listData.List {
if u.ID == userID {
if u.DisplayName != "Updated Name" {
t.Fatalf("expected display_name=Updated Name, got %s", u.DisplayName)
}
break
}
}
// Delete
deleteResp := client.Delete(fmt.Sprintf("/api/admin/users/%d", userID))
deleteResp.AssertSuccess(t)
// Verify deletion — cleanup is already called but should be a no-op now
// Prevent double-delete in cleanup
cleanup = func() {}
}
func TestAdminUserStatus(t *testing.T) {
client := testinfra.GetAuthedClient(t)
userID, cleanup := testinfra.CreateTestAdminUser(t, client)
defer cleanup()
// Disable user
disableResp := client.Put(fmt.Sprintf("/api/admin/users/%d/status", userID), map[string]any{
"status": "disabled",
})
disableResp.AssertSuccess(t)
// Verify disabled status via list
listResp := client.Get("/api/admin/users", map[string]string{
"page": "1",
"page_size": "100",
})
listResp.AssertSuccess(t)
var listData struct {
List []struct {
ID int64 `json:"id"`
Status string `json:"status"`
} `json:"list"`
}
listResp.DecodeData(t, &listData)
for _, u := range listData.List {
if u.ID == userID {
if u.Status != "disabled" {
t.Fatalf("expected disabled status, got %s", u.Status)
}
break
}
}
// Re-enable user
enableResp := client.Put(fmt.Sprintf("/api/admin/users/%d/status", userID), map[string]any{
"status": "active",
})
enableResp.AssertSuccess(t)
// Verify re-enabled status
listResp2 := client.Get("/api/admin/users", map[string]string{
"page": "1",
"page_size": "100",
})
listResp2.AssertSuccess(t)
listResp2.DecodeData(t, &listData)
for _, u := range listData.List {
if u.ID == userID {
if u.Status != "active" {
t.Fatalf("expected active status, got %s", u.Status)
}
break
}
}
}
func TestAdminUserResetPassword(t *testing.T) {
client := testinfra.GetAuthedClient(t)
userID, cleanup := testinfra.CreateTestAdminUser(t, client)
defer cleanup()
// Reset password
newPassword := "NewPassword456!"
resetResp := client.Put(fmt.Sprintf("/api/admin/users/%d/reset-password", userID), map[string]any{
"new_password": newPassword,
})
resetResp.AssertSuccess(t)
// Verify the new password works by logging in as that user
// Note: We need the username to login. Retrieve it from the list.
listResp := client.Get("/api/admin/users", map[string]string{
"page": "1",
"page_size": "100",
})
listResp.AssertSuccess(t)
var listData struct {
List []struct {
ID int64 `json:"id"`
Username string `json:"username"`
} `json:"list"`
}
listResp.DecodeData(t, &listData)
var username string
for _, u := range listData.List {
if u.ID == userID {
username = u.Username
break
}
}
if username == "" {
t.Fatalf("could not find username for user id=%d", userID)
}
// Authenticate with the new password
loginResult := testinfra.AuthenticateWithCreds(t, username, newPassword)
if loginResult.AccessToken == "" {
t.Fatal("expected login with new password to succeed")
}
}
+114
View File
@@ -0,0 +1,114 @@
//go:build integration
package admin_test
import (
"fmt"
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestAlertRuleList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/alert/rules", map[string]string{
"page": "1",
"page_size": "10",
})
testinfra.AssertPaginatedList(t, resp, 0)
}
func TestAlertOptions(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/alert/options", nil)
resp.AssertSuccess(t)
var data map[string]any
resp.DecodeData(t, &data)
t.Logf("Alert options returned %d keys", len(data))
}
func TestAlertRuleCRUD(t *testing.T) {
client := testinfra.GetAuthedClient(t)
suffix := randomSuffix()
// --- Create ---
createResp := client.Post("/api/admin/alert/rules", map[string]any{
"name": fmt.Sprintf("测试告警规则 %s", suffix),
"metric_type": "error_rate",
"condition": "gt",
"threshold": 0.5,
"duration_seconds": 300,
"notification_methods": []string{"in_app"},
"notify_user_ids": []int64{1},
"level": "warning",
"cooldown_seconds": 600,
})
createResp.AssertSuccess(t)
ruleID := createResp.GetID(t)
defer func() {
client.Delete(fmt.Sprintf("/api/admin/alert/rules/%d", ruleID))
}()
t.Logf("Created alert rule: id=%d", ruleID)
// --- List should contain the rule ---
listResp := client.Get("/api/admin/alert/rules", map[string]string{
"page": "1",
"page_size": "50",
})
testinfra.AssertPaginatedList(t, listResp, 1)
// --- Update ---
updateResp := client.Put(fmt.Sprintf("/api/admin/alert/rules/%d", ruleID), map[string]any{
"name": fmt.Sprintf("更新告警规则 %s", suffix),
"metric_type": "error_rate",
"condition": "gt",
"threshold": 0.8,
"duration_seconds": 600,
"notification_methods": []string{"in_app"},
"notify_user_ids": []int64{1},
"level": "critical",
"cooldown_seconds": 900,
})
updateResp.AssertSuccess(t)
t.Logf("Updated alert rule %d", ruleID)
// --- Toggle (disable) ---
toggleResp := client.Put(fmt.Sprintf("/api/admin/alert/rules/%d/toggle", ruleID), nil)
toggleResp.AssertSuccess(t)
t.Logf("Toggled alert rule %d", ruleID)
// --- Test rule ---
testResp := client.Post(fmt.Sprintf("/api/admin/alert/rules/%d/test", ruleID), nil)
testResp.AssertSuccess(t)
var testResult struct {
Message string `json:"message"`
}
testResp.DecodeData(t, &testResult)
t.Logf("Test alert rule %d: message=%s", ruleID, testResult.Message)
// --- Delete ---
deleteResp := client.Delete(fmt.Sprintf("/api/admin/alert/rules/%d", ruleID))
deleteResp.AssertSuccess(t)
t.Logf("Deleted alert rule %d", ruleID)
}
func TestAlertEventList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/alert/events", map[string]string{
"page": "1",
"page_size": "10",
})
testinfra.AssertPaginatedList(t, resp, 0)
}
+185
View File
@@ -0,0 +1,185 @@
//go:build integration
package admin_test
import (
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestAuditConfigGet(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/audit/config", nil)
resp.AssertSuccess(t)
var data struct {
AuditLevel string `json:"audit_level"`
}
resp.DecodeData(t, &data)
validLevels := map[string]bool{
"full": true, "full_text": true, "masked": true,
"question_only": true, "none": true,
}
if !validLevels[data.AuditLevel] {
t.Fatalf("unexpected audit_level %q", data.AuditLevel)
}
t.Logf("Audit config: level=%s", data.AuditLevel)
}
func TestAuditConfigUpdate(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// First get current config
getResp := client.Get("/api/admin/audit/config", nil)
getResp.AssertSuccess(t)
var current struct {
AuditLevel string `json:"audit_level"`
}
getResp.DecodeData(t, &current)
// Update with same value (non-destructive)
updateResp := client.Put("/api/admin/audit/config", map[string]any{
"audit_level": current.AuditLevel,
})
updateResp.AssertSuccess(t)
// Verify it stayed the same
verifyResp := client.Get("/api/admin/audit/config", nil)
verifyResp.AssertSuccess(t)
var after struct {
AuditLevel string `json:"audit_level"`
}
verifyResp.DecodeData(t, &after)
if after.AuditLevel != current.AuditLevel {
t.Fatalf("audit_level changed: %s -> %s", current.AuditLevel, after.AuditLevel)
}
t.Logf("Audit config update verified: level=%s", after.AuditLevel)
}
func TestOperationLogList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/audit/operation-logs", map[string]string{
"page": "1",
"page_size": "10",
})
testinfra.AssertPaginatedList(t, resp, 0)
}
func TestOperationLogListWithFilters(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// Filter by user_type
resp := client.Get("/api/admin/audit/operation-logs", map[string]string{
"page": "1",
"page_size": "10",
"user_type": "admin",
})
resp.AssertSuccess(t)
// Filter by action
resp = client.Get("/api/admin/audit/operation-logs", map[string]string{
"page": "1",
"page_size": "10",
"action": "create",
})
resp.AssertSuccess(t)
t.Logf("Operation log filters applied successfully")
}
func TestSensitiveLogList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/audit/sensitive-logs", map[string]string{
"page": "1",
"page_size": "10",
})
testinfra.AssertPaginatedList(t, resp, 0)
}
func TestSensitiveLogListWithFilters(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// Filter by resource_type
resp := client.Get("/api/admin/audit/sensitive-logs", map[string]string{
"page": "1",
"page_size": "10",
"resource_type": "api_key",
})
resp.AssertSuccess(t)
t.Logf("Sensitive log filters applied successfully")
}
func TestRequestAuditLogList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/audit/request-logs", map[string]string{
"page": "1",
"page_size": "10",
})
testinfra.AssertPaginatedList(t, resp, 0)
}
func TestRequestAuditLogListWithFilters(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// Filter by method
resp := client.Get("/api/admin/audit/request-logs", map[string]string{
"page": "1",
"page_size": "10",
"method": "GET",
})
resp.AssertSuccess(t)
// Filter by status_code
resp = client.Get("/api/admin/audit/request-logs", map[string]string{
"page": "1",
"page_size": "10",
"status_code": "200",
})
resp.AssertSuccess(t)
t.Logf("Request audit log filters applied successfully")
}
func TestContentFilterLogList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/audit/content-filter-logs", map[string]string{
"page": "1",
"page_size": "10",
})
testinfra.AssertPaginatedList(t, resp, 0)
}
func TestContentFilterLogListWithFilters(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// Filter by blocked
resp := client.Get("/api/admin/audit/content-filter-logs", map[string]string{
"page": "1",
"page_size": "10",
"blocked": "true",
})
resp.AssertSuccess(t)
// Filter by mode
resp = client.Get("/api/admin/audit/content-filter-logs", map[string]string{
"page": "1",
"page_size": "10",
"mode": "input",
})
resp.AssertSuccess(t)
t.Logf("Content filter log filters applied successfully")
}
+151
View File
@@ -0,0 +1,151 @@
//go:build integration
package admin_test
import (
"context"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/redis/go-redis/v9"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestLogin_Success(t *testing.T) {
result := testinfra.Authenticate(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")
}
}
func TestLogin_InvalidPassword(t *testing.T) {
client := testinfra.NewAPIClient(testinfra.DefaultBaseURL)
// Step 1: Get captcha
captchaResp := client.Get("/api/captcha/", nil)
captchaResp.AssertSuccess(t)
var captchaData struct {
CaptchaKey string `json:"captcha_key"`
}
captchaResp.DecodeData(t, &captchaData)
// Step 2: Read captcha answer from Redis
rdb := redis.NewClient(&redis.Options{
Addr: testinfra.DefaultRedisAddr,
DialTimeout: 3 * time.Second,
ReadTimeout: 3 * time.Second,
WriteTimeout: 3 * time.Second,
})
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)
}
// Step 3: Login with wrong password
loginResp := client.Post("/api/admin/auth/login", map[string]any{
"username": testinfra.DefaultUsername,
"password": "WrongPassword123!",
"captcha_key": captchaData.CaptchaKey,
"captcha_x": answer.X,
})
if loginResp.Code == 0 {
t.Fatal("expected login to fail with wrong password, but got success")
}
}
func TestLogout_Success(t *testing.T) {
// Login
result := testinfra.Authenticate(t)
client := testinfra.NewAPIClient(testinfra.DefaultBaseURL).WithToken(result.AccessToken)
// Logout
logoutResp := client.Post("/api/admin/auth/logout", nil)
logoutResp.AssertSuccess(t)
// Verify token is invalidated by calling an auth-required endpoint
sessionResp := client.Get("/api/admin/auth/sessions", map[string]string{
"page": "1",
"page_size": "10",
})
sessionResp.AssertHTTPStatus(t, 401)
}
func TestTokenRefresh(t *testing.T) {
// Login to get initial tokens
result := testinfra.Authenticate(t)
// Use refresh_token to get new tokens
client := testinfra.NewAPIClient(testinfra.DefaultBaseURL)
refreshResp := client.Post("/api/admin/auth/refresh", map[string]any{
"refresh_token": result.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")
}
if refreshData.RefreshToken == "" {
t.Fatal("expected non-empty refresh_token after refresh")
}
// Verify the new access_token works on an auth-required endpoint
authedClient := testinfra.NewAPIClient(testinfra.DefaultBaseURL).WithToken(refreshData.AccessToken)
sessionsResp := authedClient.Get("/api/admin/auth/sessions", map[string]string{
"page": "1",
"page_size": "10",
})
sessionsResp.AssertSuccess(t)
}
func TestSessionList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/auth/sessions", map[string]string{
"page": "1",
"page_size": "10",
})
testinfra.AssertPaginatedList(t, resp, 1)
}
func TestChangePassword_Unauthorized(t *testing.T) {
// Verify endpoint requires auth (no token sent)
client := testinfra.NewAPIClient(testinfra.DefaultBaseURL)
resp := client.Put("/api/admin/auth/change-password", map[string]any{
"old_password": "anything",
"new_password": "anything",
})
resp.AssertHTTPStatus(t, 401)
}
@@ -0,0 +1,30 @@
//go:build integration
package admin_test
import (
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestBillingRecordList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/billing-records", map[string]string{
"page": "1",
"page_size": "10",
})
testinfra.AssertPaginatedList(t, resp, 0)
t.Logf("Billing record list: total=%d", resp.GetTotal(t))
// List with tenant_id filter (use 0 to get all, verify no crash)
respFiltered := client.Get("/api/admin/billing-records", map[string]string{
"page": "1",
"page_size": "5",
})
testinfra.AssertPaginatedList(t, respFiltered, 0)
t.Logf("Billing record list with filters returned successfully")
}
+70
View File
@@ -0,0 +1,70 @@
//go:build integration
package admin_test
import (
"fmt"
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestChangelogCRUD(t *testing.T) {
client := testinfra.GetAuthedClient(t)
suffix := randomSuffix()
// --- Create ---
createResp := client.Post("/api/admin/changelogs", map[string]any{
"version": fmt.Sprintf("0.0.%s", suffix),
"title": fmt.Sprintf("测试更新日志 %s", suffix),
"content": "## 新功能\n\n- 集成测试创建的更新日志\n\n## 修复\n\n- 无",
"type": "feature",
})
createResp.AssertSuccess(t)
changelogID := createResp.GetID(t)
defer func() {
client.Delete(fmt.Sprintf("/api/admin/changelogs/%d", changelogID))
}()
t.Logf("Created changelog: id=%d", changelogID)
// --- List should contain the changelog ---
listResp := client.Get("/api/admin/changelogs", map[string]string{
"page": "1",
"page_size": "50",
})
testinfra.AssertPaginatedList(t, listResp, 1)
// --- List with type filter ---
listByType := client.Get("/api/admin/changelogs", map[string]string{
"page": "1",
"page_size": "10",
"type": "feature",
})
listByType.AssertSuccess(t)
// --- Update ---
updateResp := client.Put(fmt.Sprintf("/api/admin/changelogs/%d", changelogID), map[string]any{
"version": fmt.Sprintf("0.0.%s", suffix),
"title": fmt.Sprintf("更新日志-已编辑 %s", suffix),
"content": "## 更新内容\n\n- 编辑后的内容",
"type": "improvement",
"status": "draft",
})
updateResp.AssertSuccess(t)
t.Logf("Updated changelog %d", changelogID)
// --- Publish ---
publishResp := client.Post(fmt.Sprintf("/api/admin/changelogs/%d/publish", changelogID), nil)
publishResp.AssertSuccess(t)
t.Logf("Published changelog %d", changelogID)
// --- Delete ---
deleteResp := client.Delete(fmt.Sprintf("/api/admin/changelogs/%d", changelogID))
deleteResp.AssertSuccess(t)
t.Logf("Deleted changelog %d", changelogID)
}
+215
View File
@@ -0,0 +1,215 @@
//go:build integration
package admin_test
import (
"fmt"
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestChannelList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
_, cleanup := testinfra.CreateTestChannel(t, client)
defer cleanup()
resp := client.Get("/api/admin/channels", map[string]string{
"page": "1",
"page_size": "10",
})
testinfra.AssertPaginatedList(t, resp, 1)
}
func TestChannelListWithFilters(t *testing.T) {
client := testinfra.GetAuthedClient(t)
_, cleanup := testinfra.CreateTestChannel(t, client)
defer cleanup()
resp := client.Get("/api/admin/channels", map[string]string{
"page": "1",
"page_size": "10",
"type": "1",
})
resp.AssertSuccess(t)
resp = client.Get("/api/admin/channels", map[string]string{
"page": "1",
"page_size": "10",
"status": "active",
})
resp.AssertSuccess(t)
}
func TestChannelCRUD(t *testing.T) {
client := testinfra.GetAuthedClient(t)
suffix := randomSuffix()
createResp := client.Post("/api/admin/channels", map[string]any{
"name": fmt.Sprintf("CRUD测试渠道 %s", suffix),
"type": 1,
"api_key": "sk-test-key-" + suffix,
"base_url": "https://api.openai.com",
"priority": 10,
"weight": 5,
})
createResp.AssertSuccess(t)
channelID := createResp.GetID(t)
defer client.Delete(fmt.Sprintf("/api/admin/channels/%d", channelID))
// Get detail
detailResp := client.Get(fmt.Sprintf("/api/admin/channels/%d", channelID), nil)
detailResp.AssertSuccess(t)
var detail struct {
ID int64 `json:"id"`
Priority int `json:"priority"`
Weight int `json:"weight"`
}
detailResp.DecodeData(t, &detail)
if detail.Priority != 10 {
t.Fatalf("expected priority=10, got %d", detail.Priority)
}
// Update
updateResp := client.Put(fmt.Sprintf("/api/admin/channels/%d", channelID), map[string]any{
"name": fmt.Sprintf("更新渠道名 %s", suffix),
"priority": 20,
"weight": 10,
"status": "active",
})
updateResp.AssertSuccess(t)
// Verify update
verifyResp := client.Get(fmt.Sprintf("/api/admin/channels/%d", channelID), nil)
verifyResp.AssertSuccess(t)
var updated struct {
Priority int `json:"priority"`
Weight int `json:"weight"`
}
verifyResp.DecodeData(t, &updated)
if updated.Priority != 20 {
t.Fatalf("expected priority=20, got %d", updated.Priority)
}
// Delete
deleteResp := client.Delete(fmt.Sprintf("/api/admin/channels/%d", channelID))
deleteResp.AssertSuccess(t)
}
func TestChannelKeys(t *testing.T) {
client := testinfra.GetAuthedClient(t)
channelID, channelCleanup := testinfra.CreateTestChannel(t, client)
defer channelCleanup()
// Channel created with one key — list and verify
listKeysResp := client.Get(fmt.Sprintf("/api/admin/channels/%d/keys", channelID), nil)
listKeysResp.AssertSuccess(t)
var keysResult struct {
List []struct {
ID int64 `json:"id"`
Name string `json:"name"`
ApiKey string `json:"api_key"`
Status string `json:"status"`
} `json:"list"`
}
listKeysResp.DecodeData(t, &keysResult)
if len(keysResult.List) < 1 {
t.Fatalf("expected at least 1 key, got %d", len(keysResult.List))
}
// Delete the existing key
keyID := keysResult.List[0].ID
deleteKeyResp := client.Delete(fmt.Sprintf("/api/admin/channels/%d/keys/%d", channelID, keyID))
deleteKeyResp.AssertSuccess(t)
}
func TestChannelAbilities(t *testing.T) {
client := testinfra.GetAuthedClient(t)
channelID, channelCleanup := testinfra.CreateTestChannel(t, client)
defer channelCleanup()
modelID, modelCleanup := testinfra.CreateTestModel(t, client)
defer modelCleanup()
// Get the model_id string for the created model
listResp := client.Get("/api/admin/models", map[string]string{
"page": "1",
"page_size": "50",
})
listResp.AssertSuccess(t)
var listResult struct {
List []struct {
ID int64 `json:"id"`
ModelID string `json:"model_id"`
} `json:"list"`
}
listResp.DecodeData(t, &listResult)
var modelIDStr string
for _, m := range listResult.List {
if m.ID == modelID {
modelIDStr = m.ModelID
break
}
}
if modelIDStr == "" {
t.Fatalf("could not find model_id string for id=%d", modelID)
}
// Set abilities — tolerate server errors (may need upstream model config)
putResp := client.Put(fmt.Sprintf("/api/admin/channels/%d/abilities", channelID), map[string]any{
"abilities": []map[string]any{
{
"model_name": modelIDStr,
"upstream_model": modelIDStr,
"enabled": true,
},
},
})
if putResp.Code != 0 {
t.Logf("abilities put returned code=%d msg=%s (may be expected)", putResp.Code, putResp.Message)
return
}
// Get abilities
getResp := client.Get(fmt.Sprintf("/api/admin/channels/%d/abilities", channelID), nil)
getResp.AssertSuccess(t)
var abilitiesResult struct {
List []struct {
ModelName string `json:"model_name"`
Enabled bool `json:"enabled"`
} `json:"list"`
}
getResp.DecodeData(t, &abilitiesResult)
if len(abilitiesResult.List) < 1 {
t.Fatalf("expected at least 1 ability, got %d", len(abilitiesResult.List))
}
}
func TestChannelClone(t *testing.T) {
client := testinfra.GetAuthedClient(t)
channelID, channelCleanup := testinfra.CreateTestChannel(t, client)
defer channelCleanup()
suffix := randomSuffix()
cloneResp := client.Post(fmt.Sprintf("/api/admin/channels/%d/clone", channelID), map[string]any{
"name": fmt.Sprintf("克隆渠道 %s", suffix),
"api_key": "sk-cloned-key-" + suffix,
})
cloneResp.AssertSuccess(t)
clonedID := cloneResp.GetID(t)
defer client.Delete(fmt.Sprintf("/api/admin/channels/%d", clonedID))
// Verify cloned channel exists
detailResp := client.Get(fmt.Sprintf("/api/admin/channels/%d", clonedID), nil)
detailResp.AssertSuccess(t)
}
+173
View File
@@ -0,0 +1,173 @@
//go:build integration
package admin_test
import (
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestDashboard(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/dashboard", nil)
resp.AssertSuccess(t)
var data struct {
Tenants int `json:"tenants"`
Members int `json:"members"`
ActiveChannels int `json:"active_channels"`
Today struct {
Requests int `json:"requests"`
ActiveTenants int `json:"active_tenants"`
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
TotalCost float64 `json:"total_cost"`
SuccessRate float64 `json:"success_rate"`
} `json:"today"`
Yesterday struct {
Requests int `json:"requests"`
ActiveTenants int `json:"active_tenants"`
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
TotalCost float64 `json:"total_cost"`
SuccessRate float64 `json:"success_rate"`
} `json:"yesterday"`
Month struct {
Requests int `json:"requests"`
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
TotalCost float64 `json:"total_cost"`
Revenue float64 `json:"revenue"`
} `json:"month"`
}
resp.DecodeData(t, &data)
if data.Tenants < 0 {
t.Fatalf("expected tenants >= 0, got %d", data.Tenants)
}
if data.Members < 0 {
t.Fatalf("expected members >= 0, got %d", data.Members)
}
if data.ActiveChannels < 0 {
t.Fatalf("expected active_channels >= 0, got %d", data.ActiveChannels)
}
if data.Today.SuccessRate < 0 || data.Today.SuccessRate > 100 {
t.Fatalf("expected today success_rate between 0 and 100, got %f", data.Today.SuccessRate)
}
if data.Month.Revenue < 0 {
t.Fatalf("expected month revenue >= 0, got %f", data.Month.Revenue)
}
t.Logf("Dashboard: tenants=%d, members=%d, active_channels=%d",
data.Tenants, data.Members, data.ActiveChannels)
}
func TestDashboardTrends(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/dashboard/trends", map[string]string{
"days": "7",
})
resp.AssertSuccess(t)
var data struct {
List []map[string]any `json:"list"`
}
resp.DecodeData(t, &data)
t.Logf("Trends returned %d entries for 7 days", len(data.List))
}
func TestDashboardTopTenants(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/dashboard/top-tenants", map[string]string{
"days": "30",
})
resp.AssertSuccess(t)
var data struct {
List []map[string]any `json:"list"`
}
resp.DecodeData(t, &data)
t.Logf("Top tenants returned %d entries", len(data.List))
}
func TestDashboardModelDistribution(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/dashboard/model-distribution", map[string]string{
"days": "30",
})
resp.AssertSuccess(t)
var data struct {
List []map[string]any `json:"list"`
}
resp.DecodeData(t, &data)
t.Logf("Model distribution returned %d entries", len(data.List))
}
func TestDashboardChannelHealth(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/dashboard/channel-health", nil)
resp.AssertSuccess(t)
var data struct {
List []struct {
ChannelId int64 `json:"channel_id"`
ChannelName string `json:"channel_name"`
Status string `json:"status"`
HealthScore float64 `json:"health_score"`
SuccessRate float64 `json:"success_rate"`
LatencyMs int `json:"latency_ms"`
} `json:"list"`
}
resp.DecodeData(t, &data)
for _, item := range data.List {
if item.HealthScore < 0 || item.HealthScore > 100 {
t.Errorf("channel %s: health_score %f out of range [0,100]",
item.ChannelName, item.HealthScore)
}
if item.LatencyMs < 0 {
t.Errorf("channel %s: latency_ms %d should be >= 0",
item.ChannelName, item.LatencyMs)
}
}
t.Logf("Channel health returned %d channels", len(data.List))
}
func TestDashboardRecentAlerts(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/dashboard/recent-alerts", nil)
resp.AssertSuccess(t)
var data struct {
List []struct {
Id int64 `json:"id"`
RuleName string `json:"rule_name"`
Level string `json:"level"`
Status string `json:"status"`
TriggerMessage string `json:"trigger_message"`
CreatedAt string `json:"created_at"`
} `json:"list"`
}
resp.DecodeData(t, &data)
for _, alert := range data.List {
validLevels := map[string]bool{"info": true, "warning": true, "critical": true}
if !validLevels[alert.Level] && alert.Level != "" {
t.Errorf("alert %d: unexpected level %q", alert.Id, alert.Level)
}
}
t.Logf("Recent alerts returned %d entries", len(data.List))
}
+69
View File
@@ -0,0 +1,69 @@
//go:build integration
package admin_test
import (
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestErrorLogList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/error-logs", map[string]string{
"page": "1",
"page_size": "10",
})
testinfra.AssertPaginatedList(t, resp, 0)
}
func TestErrorLogListWithFilters(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// Filter by source
resp := client.Get("/api/admin/error-logs", map[string]string{
"page": "1",
"page_size": "10",
"source": "relay",
})
resp.AssertSuccess(t)
// Filter by resolved status
resp = client.Get("/api/admin/error-logs", map[string]string{
"page": "1",
"page_size": "10",
"resolved": "false",
})
resp.AssertSuccess(t)
// Filter by error_code
resp = client.Get("/api/admin/error-logs", map[string]string{
"page": "1",
"page_size": "10",
"error_code": "500",
})
resp.AssertSuccess(t)
// Filter by keyword
resp = client.Get("/api/admin/error-logs", map[string]string{
"page": "1",
"page_size": "10",
"keyword": "timeout",
})
resp.AssertSuccess(t)
t.Logf("Error log filters applied successfully")
}
func TestErrorLogStats(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/error-logs/stats", nil)
resp.AssertSuccess(t)
var data map[string]any
resp.DecodeData(t, &data)
t.Logf("Error log stats returned %d keys", len(data))
}
+80
View File
@@ -0,0 +1,80 @@
//go:build integration
package admin_test
import (
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestFeedbackList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/feedbacks", map[string]string{
"page": "1",
"page_size": "10",
})
testinfra.AssertPaginatedList(t, resp, 0)
}
func TestFeedbackListWithFilters(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// Filter by status
resp := client.Get("/api/admin/feedbacks", map[string]string{
"page": "1",
"page_size": "10",
"status": "pending",
})
resp.AssertSuccess(t)
// Filter by category
resp = client.Get("/api/admin/feedbacks", map[string]string{
"page": "1",
"page_size": "10",
"category": "bug",
})
resp.AssertSuccess(t)
// Filter by priority
resp = client.Get("/api/admin/feedbacks", map[string]string{
"page": "1",
"page_size": "10",
"priority": "high",
})
resp.AssertSuccess(t)
t.Logf("Feedback list filters applied successfully")
}
func TestFeedbackStats(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/feedbacks/stats", nil)
resp.AssertSuccess(t)
var data struct {
Total int `json:"total"`
Pending int `json:"pending"`
Acknowledged int `json:"acknowledged"`
InProgress int `json:"in_progress"`
Resolved int `json:"resolved"`
Closed int `json:"closed"`
ByCategory map[string]int `json:"by_category"`
RecentTrend []map[string]any `json:"recent_trend"`
}
resp.DecodeData(t, &data)
if data.Total < 0 {
t.Fatalf("expected total >= 0, got %d", data.Total)
}
sum := data.Pending + data.Acknowledged + data.InProgress + data.Resolved + data.Closed
if sum > data.Total {
t.Fatalf("status sum (%d) exceeds total (%d)", sum, data.Total)
}
t.Logf("Feedback stats: total=%d, pending=%d, acknowledged=%d, in_progress=%d, resolved=%d, closed=%d",
data.Total, data.Pending, data.Acknowledged, data.InProgress, data.Resolved, data.Closed)
}
+125
View File
@@ -0,0 +1,125 @@
//go:build integration
package admin_test
import (
"fmt"
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestHelpCategoryCRUD(t *testing.T) {
client := testinfra.GetAuthedClient(t)
suffix := randomSuffix()
// --- Create ---
createResp := client.Post("/api/admin/help-categories", map[string]any{
"name": fmt.Sprintf("测试帮助分类 %s", suffix),
"slug": fmt.Sprintf("test-category-%s", suffix),
"description": "集成测试帮助分类",
"sort_order": 100,
"is_visible": true,
})
createResp.AssertSuccess(t)
categoryID := createResp.GetID(t)
defer func() {
client.Delete(fmt.Sprintf("/api/admin/help-categories/%d", categoryID))
}()
t.Logf("Created help category: id=%d", categoryID)
// --- List should contain the category ---
listResp := client.Get("/api/admin/help-categories", map[string]string{
"page": "1",
"page_size": "50",
})
testinfra.AssertPaginatedList(t, listResp, 1)
// --- Update ---
updateResp := client.Put(fmt.Sprintf("/api/admin/help-categories/%d", categoryID), map[string]any{
"name": fmt.Sprintf("更新帮助分类 %s", suffix),
"slug": fmt.Sprintf("updated-category-%s", suffix),
"description": "更新后的帮助分类描述",
"sort_order": 200,
"is_visible": true,
})
updateResp.AssertSuccess(t)
t.Logf("Updated help category %d", categoryID)
// --- Delete ---
deleteResp := client.Delete(fmt.Sprintf("/api/admin/help-categories/%d", categoryID))
deleteResp.AssertSuccess(t)
t.Logf("Deleted help category %d", categoryID)
}
func TestHelpArticleCRUD(t *testing.T) {
client := testinfra.GetAuthedClient(t)
suffix := randomSuffix()
// Create a category first
catResp := client.Post("/api/admin/help-categories", map[string]any{
"name": fmt.Sprintf("文章分类 %s", suffix),
"slug": fmt.Sprintf("article-cat-%s", suffix),
"description": "用于文章测试的分类",
"sort_order": 50,
"is_visible": true,
})
catResp.AssertSuccess(t)
categoryID := catResp.GetID(t)
defer func() {
client.Delete(fmt.Sprintf("/api/admin/help-categories/%d", categoryID))
}()
// --- Create article ---
createResp := client.Post("/api/admin/help-articles", map[string]any{
"category_id": categoryID,
"title": fmt.Sprintf("测试帮助文章 %s", suffix),
"slug": fmt.Sprintf("test-article-%s", suffix),
"content": "这是集成测试帮助文章的正文内容",
"summary": "测试文章摘要",
"status": "draft",
"sort_order": 10,
"keywords": []string{"测试", "帮助"},
})
createResp.AssertSuccess(t)
articleID := createResp.GetID(t)
defer func() {
client.Delete(fmt.Sprintf("/api/admin/help-articles/%d", articleID))
}()
t.Logf("Created help article: id=%d, category_id=%d", articleID, categoryID)
// --- List should contain the article ---
listResp := client.Get("/api/admin/help-articles", map[string]string{
"category_id": fmt.Sprintf("%d", categoryID),
"page": "1",
"page_size": "50",
})
testinfra.AssertPaginatedList(t, listResp, 1)
// --- Update ---
updateResp := client.Put(fmt.Sprintf("/api/admin/help-articles/%d", articleID), map[string]any{
"category_id": categoryID,
"title": fmt.Sprintf("更新帮助文章 %s", suffix),
"slug": fmt.Sprintf("updated-article-%s", suffix),
"content": "更新后的文章正文",
"summary": "更新后的摘要",
"status": "published",
"sort_order": 20,
"keywords": []string{"更新", "测试"},
})
updateResp.AssertSuccess(t)
t.Logf("Updated help article %d", articleID)
// --- Delete ---
deleteResp := client.Delete(fmt.Sprintf("/api/admin/help-articles/%d", articleID))
deleteResp.AssertSuccess(t)
t.Logf("Deleted help article %d", articleID)
}
+222
View File
@@ -0,0 +1,222 @@
//go:build integration
package admin_test
import (
"fmt"
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestModelGroupCRUD(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// --- Create ---
suffix := randomSuffix()
createResp := client.Post("/api/admin/model-groups", map[string]any{
"name": fmt.Sprintf("CRUD测试分组 %s", suffix),
"code": fmt.Sprintf("crud-group-%s", suffix),
"description": "Integration test model group",
})
createResp.AssertSuccess(t)
groupID := createResp.GetID(t)
defer func() {
client.Delete(fmt.Sprintf("/api/admin/model-groups/%d", groupID))
}()
// --- List ---
listResp := client.Get("/api/admin/model-groups", map[string]string{
"page": "1",
"page_size": "10",
})
testinfra.AssertPaginatedList(t, listResp, 1)
var listResult struct {
List []struct {
ID int64 `json:"id"`
Name string `json:"name"`
Code string `json:"code"`
} `json:"list"`
}
listResp.DecodeData(t, &listResult)
found := false
for _, g := range listResult.List {
if g.ID == groupID {
found = true
break
}
}
if !found {
t.Fatalf("created group id=%d not found in list", groupID)
}
// --- List with filters ---
filterResp := client.Get("/api/admin/model-groups", map[string]string{
"page": "1",
"page_size": "10",
"status": "active",
})
filterResp.AssertSuccess(t)
searchResp := client.Get("/api/admin/model-groups", map[string]string{
"page": "1",
"page_size": "10",
"search": fmt.Sprintf("crud-group-%s", suffix),
})
searchResp.AssertSuccess(t)
// --- Update ---
updateResp := client.Put(fmt.Sprintf("/api/admin/model-groups/%d", groupID), map[string]any{
"name": fmt.Sprintf("更新分组名 %s", suffix),
"description": "Updated integration test model group",
"status": "active",
})
updateResp.AssertSuccess(t)
// --- Delete ---
deleteResp := client.Delete(fmt.Sprintf("/api/admin/model-groups/%d", groupID))
deleteResp.AssertSuccess(t)
}
func TestModelGroupModels(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// Create prerequisites: a group and two models
groupID, groupCleanup := testinfra.CreateTestModelGroup(t, client)
defer groupCleanup()
model1ID, model1Cleanup := testinfra.CreateTestModel(t, client)
defer model1Cleanup()
model2ID, model2Cleanup := testinfra.CreateTestModel(t, client)
defer model2Cleanup()
// --- Assign models to group ---
assignResp := client.Put(fmt.Sprintf("/api/admin/model-groups/%d/models", groupID), map[string]any{
"model_ids": []int64{model1ID, model2ID},
})
assignResp.AssertSuccess(t)
// --- Get models in group ---
getModelsResp := client.Get(fmt.Sprintf("/api/admin/model-groups/%d/models", groupID), nil)
getModelsResp.AssertSuccess(t)
var modelsResult struct {
List []struct {
ModelID string `json:"model_id"`
ModelName string `json:"model_name"`
Category string `json:"category"`
Status string `json:"status"`
} `json:"list"`
}
getModelsResp.DecodeData(t, &modelsResult)
if len(modelsResult.List) < 2 {
t.Fatalf("expected at least 2 models in group, got %d", len(modelsResult.List))
}
// --- Replace models (only model1) ---
replaceResp := client.Put(fmt.Sprintf("/api/admin/model-groups/%d/models", groupID), map[string]any{
"model_ids": []int64{model1ID},
})
replaceResp.AssertSuccess(t)
// Verify only model1 remains
verifyResp := client.Get(fmt.Sprintf("/api/admin/model-groups/%d/models", groupID), nil)
verifyResp.AssertSuccess(t)
var verifyResult struct {
List []struct {
ModelID string `json:"model_id"`
} `json:"list"`
}
verifyResp.DecodeData(t, &verifyResult)
if len(verifyResult.List) != 1 {
t.Fatalf("expected 1 model after replacement, got %d", len(verifyResult.List))
}
}
func TestModelGroupOptions(t *testing.T) {
client := testinfra.GetAuthedClient(t)
_, cleanup := testinfra.CreateTestModelGroup(t, client)
defer cleanup()
resp := client.Get("/api/admin/model-groups/options", nil)
resp.AssertSuccess(t)
var result struct {
List []struct {
ID int64 `json:"id"`
Name string `json:"name"`
Code string `json:"code"`
ModelCount int `json:"model_count"`
} `json:"list"`
}
resp.DecodeData(t, &result)
if len(result.List) < 1 {
t.Fatal("expected at least 1 model group option")
}
}
func TestTenantGroupAssignment(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// Create prerequisites
tenantID, tenantCleanup := testinfra.CreateTestTenant(t, client)
defer tenantCleanup()
group1ID, group1Cleanup := testinfra.CreateTestModelGroup(t, client)
defer group1Cleanup()
group2ID, group2Cleanup := testinfra.CreateTestModelGroup(t, client)
defer group2Cleanup()
// --- Assign groups to tenant ---
assignResp := client.Put(fmt.Sprintf("/api/admin/tenants/%d/groups", tenantID), map[string]any{
"group_ids": []int64{group1ID, group2ID},
})
assignResp.AssertSuccess(t)
// --- Get tenant groups ---
getGroupsResp := client.Get(fmt.Sprintf("/api/admin/tenants/%d/groups", tenantID), nil)
getGroupsResp.AssertSuccess(t)
var groupsResult struct {
List []struct {
GroupID int64 `json:"group_id"`
Name string `json:"name"`
Code string `json:"code"`
Status string `json:"status"`
ModelCount int `json:"model_count"`
} `json:"list"`
}
getGroupsResp.DecodeData(t, &groupsResult)
if len(groupsResult.List) != 2 {
t.Fatalf("expected 2 groups assigned to tenant, got %d", len(groupsResult.List))
}
// --- Replace with single group ---
replaceResp := client.Put(fmt.Sprintf("/api/admin/tenants/%d/groups", tenantID), map[string]any{
"group_ids": []int64{group1ID},
})
replaceResp.AssertSuccess(t)
// Verify only one group remains
verifyResp := client.Get(fmt.Sprintf("/api/admin/tenants/%d/groups", tenantID), nil)
verifyResp.AssertSuccess(t)
var verifyResult struct {
List []struct {
GroupID int64 `json:"group_id"`
} `json:"list"`
}
verifyResp.DecodeData(t, &verifyResult)
if len(verifyResult.List) != 1 {
t.Fatalf("expected 1 group after replacement, got %d", len(verifyResult.List))
}
if verifyResult.List[0].GroupID != group1ID {
t.Fatalf("expected group_id=%d, got %d", group1ID, verifyResult.List[0].GroupID)
}
}
+257
View File
@@ -0,0 +1,257 @@
//go:build integration
package admin_test
import (
"fmt"
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestModelList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// Create a model to ensure at least one exists
_, cleanup := testinfra.CreateTestModel(t, client)
defer cleanup()
resp := client.Get("/api/admin/models", map[string]string{
"page": "1",
"page_size": "10",
})
testinfra.AssertPaginatedList(t, resp, 1)
}
func TestModelListWithFilters(t *testing.T) {
client := testinfra.GetAuthedClient(t)
_, cleanup := testinfra.CreateTestModel(t, client)
defer cleanup()
// Filter by category
resp := client.Get("/api/admin/models", map[string]string{
"page": "1",
"page_size": "10",
"category": "chat",
})
resp.AssertSuccess(t)
// Filter by status
resp = client.Get("/api/admin/models", map[string]string{
"page": "1",
"page_size": "10",
"status": "active",
})
resp.AssertSuccess(t)
// Search by keyword
resp = client.Get("/api/admin/models", map[string]string{
"page": "1",
"page_size": "10",
"search": "test-model",
})
resp.AssertSuccess(t)
}
func TestModelCRUD(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// --- Create ---
suffix := randomSuffix()
modelID := fmt.Sprintf("crud-model-%s", suffix)
createResp := client.Post("/api/admin/models", map[string]any{
"model_id": modelID,
"model_name": fmt.Sprintf("CRUD测试模型 %s", suffix),
"category": "chat",
"max_context_tokens": 128000,
"max_output_tokens": 4096,
"capabilities": map[string]any{"vision": true, "streaming": true},
"description": "Integration test model",
"tags": []string{"test", "chat"},
})
createResp.AssertSuccess(t)
id := createResp.GetID(t)
defer func() {
client.Delete(fmt.Sprintf("/api/admin/models/%d", id))
}()
// --- Get detail (using model list to find the created model) ---
listResp := client.Get("/api/admin/models", map[string]string{
"page": "1",
"page_size": "50",
"search": modelID,
})
listResp.AssertSuccess(t)
var listResult struct {
List []struct {
ID int64 `json:"id"`
ModelID string `json:"model_id"`
ModelName string `json:"model_name"`
Category string `json:"category"`
Status string `json:"status"`
MaxContextTokens int `json:"max_context_tokens"`
MaxOutputTokens int `json:"max_output_tokens"`
} `json:"list"`
Total int `json:"total"`
}
listResp.DecodeData(t, &listResult)
found := false
for _, m := range listResult.List {
if m.ModelID == modelID {
found = true
if m.MaxContextTokens != 128000 {
t.Fatalf("expected max_context_tokens=128000, got %d", m.MaxContextTokens)
}
if m.Category != "chat" {
t.Fatalf("expected category=chat, got %s", m.Category)
}
break
}
}
if !found {
t.Fatalf("created model %s not found in list", modelID)
}
// --- Update ---
updateResp := client.Put(fmt.Sprintf("/api/admin/models/%d", id), map[string]any{
"model_name": fmt.Sprintf("更新模型名 %s", suffix),
"category": "chat",
"status": "active",
"max_output_tokens": 8192,
"description": "Updated integration test model",
})
updateResp.AssertSuccess(t)
// Verify update
verifyResp := client.Get("/api/admin/models", map[string]string{
"page": "1",
"page_size": "50",
"search": modelID,
})
verifyResp.AssertSuccess(t)
var verifyResult struct {
List []struct {
ModelID string `json:"model_id"`
MaxOutputTokens int `json:"max_output_tokens"`
} `json:"list"`
}
verifyResp.DecodeData(t, &verifyResult)
for _, m := range verifyResult.List {
if m.ModelID == modelID {
if m.MaxOutputTokens != 8192 {
t.Fatalf("expected max_output_tokens=8192 after update, got %d", m.MaxOutputTokens)
}
break
}
}
// --- Delete ---
deleteResp := client.Delete(fmt.Sprintf("/api/admin/models/%d", id))
deleteResp.AssertSuccess(t)
}
func TestModelPricing(t *testing.T) {
client := testinfra.GetAuthedClient(t)
modelID, cleanup := testinfra.CreateTestModel(t, client)
defer cleanup()
// Get model_id string from the created model
listResp := client.Get("/api/admin/models", map[string]string{
"page": "1",
"page_size": "50",
})
listResp.AssertSuccess(t)
var listResult struct {
List []struct {
ID int64 `json:"id"`
ModelID string `json:"model_id"`
} `json:"list"`
}
listResp.DecodeData(t, &listResult)
var modelIDStr string
for _, m := range listResult.List {
if m.ID == modelID {
modelIDStr = m.ModelID
break
}
}
if modelIDStr == "" {
t.Fatalf("could not find model_id string for id=%d", modelID)
}
// --- Get pricing (should be empty or default) ---
getPricingResp := client.Get(fmt.Sprintf("/api/admin/models/%s/pricing", modelIDStr), nil)
getPricingResp.AssertSuccess(t)
// --- Update pricing ---
putPricingResp := client.Put(fmt.Sprintf("/api/admin/models/%s/pricing", modelIDStr), map[string]any{
"items": []map[string]any{
{
"billing_mode": "token",
"min_tokens": 0,
"max_tokens": 128000,
"input_price": 0.0015,
"output_price": 0.006,
},
{
"billing_mode": "token",
"min_tokens": 128001,
"max_tokens": 200000,
"input_price": 0.002,
"output_price": 0.008,
},
},
})
putPricingResp.AssertSuccess(t)
// --- Verify pricing was saved ---
verifyPricingResp := client.Get(fmt.Sprintf("/api/admin/models/%s/pricing", modelIDStr), nil)
verifyPricingResp.AssertSuccess(t)
var pricingResult struct {
List []struct {
BillingMode string `json:"billing_mode"`
MinTokens int `json:"min_tokens"`
MaxTokens int `json:"max_tokens"`
InputPrice float64 `json:"input_price"`
OutputPrice float64 `json:"output_price"`
} `json:"list"`
}
verifyPricingResp.DecodeData(t, &pricingResult)
if len(pricingResult.List) != 2 {
t.Fatalf("expected 2 pricing tiers, got %d", len(pricingResult.List))
}
}
func TestModelOptions(t *testing.T) {
client := testinfra.GetAuthedClient(t)
_, cleanup := testinfra.CreateTestModel(t, client)
defer cleanup()
resp := client.Get("/api/admin/models/options", map[string]string{
"status": "active",
"category": "chat",
})
resp.AssertSuccess(t)
var result 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, &result)
if len(result.List) < 1 {
t.Fatal("expected at least 1 model option")
}
}
+95
View File
@@ -0,0 +1,95 @@
//go:build integration
package admin_test
import (
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestMonitorDashboard(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/monitor/dashboard", map[string]string{
"minutes": "30",
})
resp.AssertSuccess(t)
var data map[string]any
resp.DecodeData(t, &data)
t.Logf("Monitor dashboard returned %d keys", len(data))
}
func TestMonitorTraffic(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/monitor/traffic", map[string]string{
"minutes": "60",
})
resp.AssertSuccess(t)
t.Logf("Monitor traffic response received")
}
func TestMonitorLatency(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/monitor/latency", map[string]string{
"minutes": "30",
})
resp.AssertSuccess(t)
var data map[string]any
resp.DecodeData(t, &data)
t.Logf("Monitor latency returned %d keys", len(data))
}
func TestMonitorSystem(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/monitor/system", map[string]string{
"minutes": "30",
})
resp.AssertSuccess(t)
var data map[string]any
resp.DecodeData(t, &data)
t.Logf("Monitor system returned %d keys", len(data))
}
func TestMonitorDBPool(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/monitor/db-pool", nil)
resp.AssertSuccess(t)
var data map[string]any
resp.DecodeData(t, &data)
t.Logf("DB pool monitor returned %d keys", len(data))
}
func TestMonitorRedisPool(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/monitor/redis-pool", nil)
resp.AssertSuccess(t)
var data map[string]any
resp.DecodeData(t, &data)
t.Logf("Redis pool monitor returned %d keys", len(data))
}
func TestMonitorRealtime(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/monitor/realtime", nil)
resp.AssertSuccess(t)
t.Logf("Realtime monitor response received")
}
@@ -0,0 +1,166 @@
//go:build integration
package admin_test
import (
"fmt"
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestTemplateList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/notification/templates", map[string]string{
"page": "1",
"page_size": "10",
})
testinfra.AssertPaginatedList(t, resp, 0)
}
func TestTemplateGetAndUpdate(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// First list to find a template code
listResp := client.Get("/api/admin/notification/templates", map[string]string{
"page": "1",
"page_size": "50",
})
listResp.AssertSuccess(t)
var listData struct {
List []struct {
Code string `json:"code"`
} `json:"list"`
Total int `json:"total"`
}
listResp.DecodeData(t, &listData)
if len(listData.List) == 0 {
t.Skip("No notification templates found, skipping get/update test")
}
templateCode := listData.List[0].Code
// --- Get template detail ---
getResp := client.Get(fmt.Sprintf("/api/admin/notification/templates/%s", templateCode), nil)
getResp.AssertSuccess(t)
var template struct {
Code string `json:"code"`
Subject string `json:"subject"`
BodyTemplate string `json:"body_template"`
Channel string `json:"channel"`
}
getResp.DecodeData(t, &template)
t.Logf("Got template: code=%s, channel=%s", template.Code, template.Channel)
// --- Update with same values (non-destructive) ---
updateResp := client.Put(fmt.Sprintf("/api/admin/notification/templates/%s", templateCode), map[string]any{
"subject": template.Subject,
"body_template": template.BodyTemplate,
"channel": template.Channel,
})
updateResp.AssertSuccess(t)
t.Logf("Template %s updated successfully", templateCode)
}
func TestMessageList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/notification/messages", map[string]string{
"page": "1",
"page_size": "10",
})
testinfra.AssertPaginatedList(t, resp, 0)
}
func TestMessageSend(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// Create a tenant to send message to
tenantID, cleanup := testinfra.CreateTestTenant(t, client)
defer cleanup()
resp := client.Post("/api/admin/notification/messages/send", map[string]any{
"tenant_id": tenantID,
"title": fmt.Sprintf("测试通知消息 %s", randomSuffix()),
"content": "这是一条集成测试通知消息",
"channel": "system",
})
resp.AssertSuccess(t)
t.Logf("Message sent to tenant %d", tenantID)
}
func TestMessageBroadcast(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Post("/api/admin/notification/messages/broadcast", map[string]any{
"title": fmt.Sprintf("测试广播消息 %s", randomSuffix()),
"content": "这是一条集成测试广播消息",
"target_roles": []string{"owner", "admin"},
})
resp.AssertSuccess(t)
t.Logf("Broadcast message sent successfully")
}
func TestAnnouncementCRUD(t *testing.T) {
client := testinfra.GetAuthedClient(t)
suffix := randomSuffix()
// --- Create ---
createResp := client.Post("/api/admin/announcements", map[string]any{
"title": fmt.Sprintf("测试公告 %s", suffix),
"type": "system",
"content": "这是一条集成测试公告内容",
"status": "draft",
"is_pinned": false,
"display_position": "dashboard",
})
createResp.AssertSuccess(t)
announcementID := createResp.GetID(t)
defer func() {
client.Delete(fmt.Sprintf("/api/admin/announcements/%d", announcementID))
}()
t.Logf("Created announcement: id=%d", announcementID)
// --- List should contain the announcement ---
listResp := client.Get("/api/admin/announcements", map[string]string{
"page": "1",
"page_size": "50",
"status": "draft",
})
testinfra.AssertPaginatedList(t, listResp, 1)
// --- Update ---
updateResp := client.Put(fmt.Sprintf("/api/admin/announcements/%d", announcementID), map[string]any{
"title": fmt.Sprintf("更新公告 %s", suffix),
"type": "system",
"content": "更新后的公告内容",
"status": "draft",
"is_pinned": true,
"display_position": "dashboard",
})
updateResp.AssertSuccess(t)
t.Logf("Updated announcement %d", announcementID)
// --- Publish ---
publishResp := client.Put(fmt.Sprintf("/api/admin/announcements/%d/publish", announcementID), nil)
publishResp.AssertSuccess(t)
t.Logf("Published announcement %d", announcementID)
// --- Archive ---
archiveResp := client.Put(fmt.Sprintf("/api/admin/announcements/%d/archive", announcementID), nil)
archiveResp.AssertSuccess(t)
t.Logf("Archived announcement %d", announcementID)
}
+33
View File
@@ -0,0 +1,33 @@
//go:build integration
package admin_test
import (
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestOrderList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// Basic list with default pagination
resp := client.Get("/api/admin/orders", map[string]string{
"page": "1",
"page_size": "20",
})
testinfra.AssertPaginatedList(t, resp, 0)
total := resp.GetTotal(t)
t.Logf("Order list: total=%d", total)
// Filter by status
respByStatus := client.Get("/api/admin/orders", map[string]string{
"page": "1",
"page_size": "10",
"status": "paid",
})
testinfra.AssertPaginatedList(t, respByStatus, 0)
t.Logf("Order list filtered by status=paid: total=%d", respByStatus.GetTotal(t))
}
+120
View File
@@ -0,0 +1,120 @@
//go:build integration
package admin_test
import (
"fmt"
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestPaymentChannelList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/payment-channels", nil)
resp.AssertSuccess(t)
var data struct {
List []map[string]any `json:"list"`
}
resp.DecodeData(t, &data)
t.Logf("Payment channels returned %d entries", len(data.List))
}
func TestPaymentSettingsGet(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/payment-settings", nil)
resp.AssertSuccess(t)
var data struct {
AmountOptions []int `json:"amount_options"`
AmountDiscount map[int]float64 `json:"amount_discount"`
MinTopUp float64 `json:"min_topup"`
Currency string `json:"currency"`
CallbackBaseURL string `json:"callback_base_url"`
}
resp.DecodeData(t, &data)
t.Logf("Payment settings: currency=%s, min_topup=%f, %d amount_options",
data.Currency, data.MinTopUp, len(data.AmountOptions))
}
func TestPaymentSettingsUpdate(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// First get current settings
getResp := client.Get("/api/admin/payment-settings", nil)
getResp.AssertSuccess(t)
var current struct {
AmountOptions []int `json:"amount_options"`
AmountDiscount map[int]float64 `json:"amount_discount"`
MinTopUp float64 `json:"min_topup"`
Currency string `json:"currency"`
CallbackBaseURL string `json:"callback_base_url"`
}
getResp.DecodeData(t, &current)
// Update with same values (safe, non-destructive)
updateResp := client.Put("/api/admin/payment-settings", map[string]any{
"amount_options": current.AmountOptions,
"amount_discount": current.AmountDiscount,
"min_topup": current.MinTopUp,
"currency": current.Currency,
"callback_base_url": current.CallbackBaseURL,
})
updateResp.AssertSuccess(t)
t.Logf("Payment settings update succeeded")
}
func TestMemberCRUD(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// Create a tenant first to host the member
tenantID, tenantCleanup := testinfra.CreateTestTenant(t, client)
defer tenantCleanup()
// Create member
suffix := randomSuffix()
createResp := client.Post("/api/admin/members", map[string]any{
"tenant_id": tenantID,
"username": fmt.Sprintf("mem%s", suffix),
"email": fmt.Sprintf("mem%s@test.com", suffix),
"password": "MemberPass123!",
"display_name": "Test Member",
"role": "member",
})
createResp.AssertSuccess(t)
memberID := createResp.GetID(t)
t.Logf("Created member: id=%d, tenant_id=%d", memberID, tenantID)
// Disable member
disableResp := client.Put(fmt.Sprintf("/api/admin/members/%d/disable", memberID), nil)
disableResp.AssertSuccess(t)
t.Logf("Member %d disabled", memberID)
// Enable member
enableResp := client.Put(fmt.Sprintf("/api/admin/members/%d/enable", memberID), nil)
enableResp.AssertSuccess(t)
t.Logf("Member %d enabled", memberID)
// Reset password
resetResp := client.Put(fmt.Sprintf("/api/admin/members/%d/reset-password", memberID), nil)
resetResp.AssertSuccess(t)
var resetData struct {
NewPassword string `json:"new_password"`
}
resetResp.DecodeData(t, &resetData)
if resetData.NewPassword == "" {
t.Fatal("expected new_password to be non-empty after reset")
}
t.Logf("Member %d password reset successfully", memberID)
}
+129
View File
@@ -0,0 +1,129 @@
//go:build integration
package admin_test
import (
"fmt"
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestAllPermissions(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/permissions", nil)
resp.AssertSuccess(t)
var data struct {
Groups []struct {
Name string `json:"name"`
Label string `json:"label"`
Permissions []any `json:"permissions"`
} `json:"groups"`
}
resp.DecodeData(t, &data)
if len(data.Groups) == 0 {
t.Fatal("expected at least one permission group")
}
t.Logf("found %d permission groups", len(data.Groups))
}
func TestPermissionList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
userID, cleanup := testinfra.CreateTestAdminUser(t, client)
defer cleanup()
resp := client.Get(fmt.Sprintf("/api/admin/users/%d/permissions", userID), nil)
resp.AssertSuccess(t)
var data struct {
Permissions []string `json:"permissions"`
DataScopes []struct {
ID int64 `json:"id"`
ScopeType string `json:"scope_type"`
ScopeValue string `json:"scope_value"`
} `json:"data_scopes"`
}
resp.DecodeData(t, &data)
// A newly created user may have no permissions; just verify the structure
t.Logf("user %d has %d permissions and %d data scopes", userID, len(data.Permissions), len(data.DataScopes))
}
func TestPermissionUpdate(t *testing.T) {
client := testinfra.GetAuthedClient(t)
userID, cleanup := testinfra.CreateTestAdminUser(t, client)
defer cleanup()
// Update permissions
perms := []string{"tenant:view", "tenant:create", "model:view"}
updateResp := client.Put(fmt.Sprintf("/api/admin/users/%d/permissions", userID), map[string]any{
"permissions": perms,
})
updateResp.AssertSuccess(t)
// Get permissions and verify
getResp := client.Get(fmt.Sprintf("/api/admin/users/%d/permissions", userID), nil)
getResp.AssertSuccess(t)
var data struct {
Permissions []string `json:"permissions"`
}
getResp.DecodeData(t, &data)
if len(data.Permissions) != len(perms) {
t.Fatalf("expected %d permissions, got %d", len(perms), len(data.Permissions))
}
permMap := make(map[string]bool)
for _, p := range data.Permissions {
permMap[p] = true
}
for _, expected := range perms {
if !permMap[expected] {
t.Fatalf("expected permission %q not found", expected)
}
}
}
func TestDataScopeUpdate(t *testing.T) {
client := testinfra.GetAuthedClient(t)
userID, cleanup := testinfra.CreateTestAdminUser(t, client)
defer cleanup()
// Update data scopes
dataScopes := []map[string]any{
{"scope_type": "all", "scope_value": ""},
}
updateResp := client.Put(fmt.Sprintf("/api/admin/users/%d/data-scopes", userID), map[string]any{
"data_scopes": dataScopes,
})
updateResp.AssertSuccess(t)
// Get permissions and verify data scopes
getResp := client.Get(fmt.Sprintf("/api/admin/users/%d/permissions", userID), nil)
getResp.AssertSuccess(t)
var data struct {
DataScopes []struct {
ID int64 `json:"id"`
ScopeType string `json:"scope_type"`
ScopeValue string `json:"scope_value"`
} `json:"data_scopes"`
}
getResp.DecodeData(t, &data)
if len(data.DataScopes) != 1 {
t.Fatalf("expected 1 data scope, got %d", len(data.DataScopes))
}
if data.DataScopes[0].ScopeType != "all" {
t.Fatalf("expected scope_type=all, got %s", data.DataScopes[0].ScopeType)
}
}
+121
View File
@@ -0,0 +1,121 @@
//go:build integration
package admin_test
import (
"fmt"
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestPlanList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/plans", map[string]string{
"page": "1",
"page_size": "20",
})
testinfra.AssertPaginatedList(t, resp, 0)
t.Logf("Plan list: total=%d", resp.GetTotal(t))
// Filter by active status
respActive := client.Get("/api/admin/plans", map[string]string{
"page": "1",
"page_size": "20",
"status": "active",
})
testinfra.AssertPaginatedList(t, respActive, 0)
t.Logf("Active plan list: total=%d", respActive.GetTotal(t))
}
func TestPlanCRUD(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// Create
planID, cleanup := testinfra.CreateTestPlan(t, client)
defer cleanup()
t.Logf("Created plan: id=%d", planID)
// Read
detailResp := client.Get(fmt.Sprintf("/api/admin/plans/%d", planID), nil)
detailResp.AssertSuccess(t)
var detail struct {
Data struct {
Id int64 `json:"id"`
Name string `json:"name"`
Identifier string `json:"identifier"`
Status string `json:"status"`
} `json:"data"`
}
detailResp.DecodeData(t, &detail)
if detail.Data.Id != planID {
t.Fatalf("expected plan id=%d, got %d", planID, detail.Data.Id)
}
t.Logf("Plan detail: name=%s, identifier=%s, status=%s",
detail.Data.Name, detail.Data.Identifier, detail.Data.Status)
// Update
updateResp := client.Put(fmt.Sprintf("/api/admin/plans/%d", planID), map[string]any{
"update": map[string]any{
"description": "updated by integration test",
},
})
updateResp.AssertSuccess(t)
t.Logf("Plan updated successfully")
// Verify update
verifyResp := client.Get(fmt.Sprintf("/api/admin/plans/%d", planID), nil)
verifyResp.AssertSuccess(t)
var verifyData struct {
Data struct {
Description string `json:"description"`
} `json:"data"`
}
verifyResp.DecodeData(t, &verifyData)
if verifyData.Data.Description != "updated by integration test" {
t.Fatalf("expected description 'updated by integration test', got %q",
verifyData.Data.Description)
}
t.Logf("Plan update verified")
}
func TestPlanToggleRecommend(t *testing.T) {
client := testinfra.GetAuthedClient(t)
planID, cleanup := testinfra.CreateTestPlan(t, client)
defer cleanup()
// Toggle recommend on
resp := client.Put(fmt.Sprintf("/api/admin/plans/%d/toggle-recommend", planID), nil)
resp.AssertSuccess(t)
// Verify toggle
detailResp := client.Get(fmt.Sprintf("/api/admin/plans/%d", planID), nil)
detailResp.AssertSuccess(t)
var detail struct {
Data struct {
IsRecommended bool `json:"is_recommended"`
} `json:"data"`
}
detailResp.DecodeData(t, &detail)
t.Logf("Plan %d is_recommended=%v after first toggle", planID, detail.Data.IsRecommended)
// Toggle again to revert
resp2 := client.Put(fmt.Sprintf("/api/admin/plans/%d/toggle-recommend", planID), nil)
resp2.AssertSuccess(t)
t.Logf("Plan toggle-recommend completed successfully")
}
+51
View File
@@ -0,0 +1,51 @@
//go:build integration
package admin_test
import (
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestPluginList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/plugins", nil)
resp.AssertSuccess(t)
var data struct {
List []struct {
Name string `json:"name"`
Label string `json:"label"`
Description string `json:"description"`
Version string `json:"version"`
Category string `json:"category"`
Author string `json:"author"`
Status string `json:"status"`
Installed bool `json:"installed"`
Config any `json:"config"`
} `json:"list"`
}
resp.DecodeData(t, &data)
t.Logf("Plugin list returned %d entries", len(data.List))
}
func TestPluginListWithFilters(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// Filter by category
resp := client.Get("/api/admin/plugins", map[string]string{
"category": "payment",
})
resp.AssertSuccess(t)
// Filter by status
resp = client.Get("/api/admin/plugins", map[string]string{
"status": "installed",
})
resp.AssertSuccess(t)
t.Logf("Plugin list filters applied successfully")
}
+19
View File
@@ -0,0 +1,19 @@
//go:build integration
package admin_test
import (
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestPromoCodeList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/promo-codes", map[string]string{
"page": "1",
"page_size": "10",
})
testinfra.AssertPaginatedList(t, resp, 0)
}
@@ -0,0 +1,41 @@
//go:build integration
package admin_test
import (
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestRedemptionList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/redemptions", map[string]string{
"page": "1",
"page_size": "10",
})
testinfra.AssertPaginatedList(t, resp, 0)
}
func TestRedemptionListWithStatusFilter(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// Filter by status
resp := client.Get("/api/admin/redemptions", map[string]string{
"page": "1",
"page_size": "10",
"status": "active",
})
resp.AssertSuccess(t)
// Filter by unused status
resp = client.Get("/api/admin/redemptions", map[string]string{
"page": "1",
"page_size": "10",
"status": "used",
})
resp.AssertSuccess(t)
t.Logf("Redemption list filters applied successfully")
}
+87
View File
@@ -0,0 +1,87 @@
//go:build integration
package admin_test
import (
"fmt"
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestLoginHistory(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/security/login-history", map[string]string{
"page": "1",
"page_size": "10",
})
testinfra.AssertPaginatedList(t, resp, 0)
}
func TestLoginHistoryWithFilters(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// Filter by success status
resp := client.Get("/api/admin/security/login-history", map[string]string{
"page": "1",
"page_size": "10",
"success": "true",
})
resp.AssertSuccess(t)
// Filter by login method
resp = client.Get("/api/admin/security/login-history", map[string]string{
"page": "1",
"page_size": "10",
"login_method": "password",
})
resp.AssertSuccess(t)
// Filter by username
resp = client.Get("/api/admin/security/login-history", map[string]string{
"page": "1",
"page_size": "10",
"username": "admin",
})
resp.AssertSuccess(t)
t.Logf("Login history filters applied successfully")
}
func TestTenantLoginHistory(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// Create a tenant to filter by
tenantID, cleanup := testinfra.CreateTestTenant(t, client)
defer cleanup()
resp := client.Get("/api/admin/security/tenant-login-history", map[string]string{
"page": "1",
"page_size": "10",
"tenant_id": fmt.Sprintf("%d", tenantID),
})
testinfra.AssertPaginatedList(t, resp, 0)
}
func TestTenantLoginHistoryWithFilters(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// Filter by IP address
resp := client.Get("/api/admin/security/tenant-login-history", map[string]string{
"page": "1",
"page_size": "10",
"ip_address": "127.0.0.1",
})
resp.AssertSuccess(t)
// Filter by success
resp = client.Get("/api/admin/security/tenant-login-history", map[string]string{
"page": "1",
"page_size": "10",
"success": "false",
})
resp.AssertSuccess(t)
t.Logf("Tenant login history filters applied successfully")
}
+133
View File
@@ -0,0 +1,133 @@
//go:build integration
package admin_test
import (
"fmt"
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestSettingsCategories(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/settings/categories", nil)
resp.AssertSuccess(t)
var data struct {
List []struct {
Key string `json:"key"`
Label string `json:"label"`
Icon string `json:"icon"`
Order int `json:"order"`
} `json:"list"`
}
resp.DecodeData(t, &data)
if len(data.List) == 0 {
t.Fatal("expected at least one settings category")
}
for _, cat := range data.List {
if cat.Key == "" {
t.Fatal("settings category key should not be empty")
}
if cat.Label == "" {
t.Fatalf("category %s has empty label", cat.Key)
}
}
t.Logf("Settings categories returned %d entries", len(data.List))
}
func TestSettingsGet(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// First get categories to find a valid one
catResp := client.Get("/api/admin/settings/categories", nil)
catResp.AssertSuccess(t)
var catData struct {
List []struct {
Key string `json:"key"`
} `json:"list"`
}
catResp.DecodeData(t, &catData)
if len(catData.List) == 0 {
t.Skip("No settings categories found, skipping get test")
}
// Get settings for the first category
category := catData.List[0].Key
resp := client.Get(fmt.Sprintf("/api/admin/settings/%s", category), nil)
resp.AssertSuccess(t)
var data struct {
List []struct {
Key string `json:"key"`
Value any `json:"value"`
Type string `json:"type"`
Label string `json:"label"`
Description string `json:"description"`
Sensitive bool `json:"sensitive"`
Validation string `json:"validation"`
Default any `json:"default"`
} `json:"list"`
}
resp.DecodeData(t, &data)
t.Logf("Settings for category %q returned %d entries", category, len(data.List))
}
func TestSettingsUpdate(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// First get categories to find a valid one
catResp := client.Get("/api/admin/settings/categories", nil)
catResp.AssertSuccess(t)
var catData struct {
List []struct {
Key string `json:"key"`
} `json:"list"`
}
catResp.DecodeData(t, &catData)
if len(catData.List) == 0 {
t.Skip("No settings categories found, skipping update test")
}
category := catData.List[0].Key
// Get current settings
getResp := client.Get(fmt.Sprintf("/api/admin/settings/%s", category), nil)
getResp.AssertSuccess(t)
var getData struct {
List []struct {
Key string `json:"key"`
Value any `json:"value"`
} `json:"list"`
}
getResp.DecodeData(t, &getData)
if len(getData.List) == 0 {
t.Skipf("No settings found in category %q, skipping update test", category)
}
// Build settings map from current values (non-destructive)
settings := make(map[string]any)
for _, item := range getData.List {
settings[item.Key] = item.Value
}
// Update with same values
updateResp := client.Put(fmt.Sprintf("/api/admin/settings/%s", category), map[string]any{
"settings": settings,
})
updateResp.AssertSuccess(t)
t.Logf("Settings update for category %q succeeded (%d keys)", category, len(settings))
}
+49
View File
@@ -0,0 +1,49 @@
//go:build integration
package admin_test
import (
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestTaskList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/tasks", map[string]string{
"page": "1",
"page_size": "10",
})
testinfra.AssertPaginatedList(t, resp, 0)
}
func TestTaskListWithFilters(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// Filter by status
resp := client.Get("/api/admin/tasks", map[string]string{
"page": "1",
"page_size": "10",
"status": "completed",
})
resp.AssertSuccess(t)
// Filter by platform
resp = client.Get("/api/admin/tasks", map[string]string{
"page": "1",
"page_size": "10",
"platform": "suno",
})
resp.AssertSuccess(t)
// Filter by public_task_id
resp = client.Get("/api/admin/tasks", map[string]string{
"page": "1",
"page_size": "10",
"public_task_id": "nonexistent",
})
resp.AssertSuccess(t)
t.Logf("Task list filters applied successfully")
}
@@ -0,0 +1,143 @@
//go:build integration
package admin_test
import (
"fmt"
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestTenantLevelList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// Create a level to ensure at least one exists
_, cleanup := testinfra.CreateTestTenantLevel(t, client)
defer cleanup()
resp := client.Get("/api/admin/tenant-level-configs", nil)
resp.AssertSuccess(t)
var result struct {
List []struct {
ID int64 `json:"id"`
Level int `json:"level"`
Name string `json:"name"`
CumulativeRechargeThreshold float64 `json:"cumulative_recharge_threshold"`
MaxMembers int `json:"max_members"`
MaxConcurrency int `json:"max_concurrency"`
PriceMultiplier float64 `json:"price_multiplier"`
SortOrder int `json:"sort_order"`
} `json:"list"`
}
resp.DecodeData(t, &result)
if len(result.List) < 1 {
t.Fatalf("expected at least 1 tenant level config, got %d", len(result.List))
}
}
func TestTenantLevelCRUD(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// --- Create ---
suffix := randomSuffix()
createResp := client.Post("/api/admin/tenant-level-configs", map[string]any{
"level": 80 + int(suffix[0]%10),
"name": fmt.Sprintf("CRUD测试等级 %s", suffix),
"cumulative_recharge_threshold": 200.0,
"max_members": 100,
"max_concurrency": 20,
"price_multiplier": 0.85,
"sort_order": 10,
})
createResp.AssertSuccess(t)
levelID := createResp.GetID(t)
// --- List should contain the created level ---
listResp := client.Get("/api/admin/tenant-level-configs", nil)
listResp.AssertSuccess(t)
var listResult struct {
List []struct {
ID int64 `json:"id"`
Name string `json:"name"`
MaxMembers int `json:"max_members"`
MaxConcurrency int `json:"max_concurrency"`
PriceMultiplier float64 `json:"price_multiplier"`
} `json:"list"`
}
listResp.DecodeData(t, &listResult)
found := false
for _, item := range listResult.List {
if item.ID == levelID {
found = true
if item.MaxMembers != 100 {
t.Fatalf("expected max_members=100, got %d", item.MaxMembers)
}
if item.MaxConcurrency != 20 {
t.Fatalf("expected max_concurrency=20, got %d", item.MaxConcurrency)
}
break
}
}
if !found {
t.Fatalf("created level id=%d not found in list", levelID)
}
// --- Update ---
updateResp := client.Put(fmt.Sprintf("/api/admin/tenant-level-configs/%d", levelID), map[string]any{
"name": fmt.Sprintf("更新等级名 %s", suffix),
"max_members": 200,
"max_concurrency": 50,
"price_multiplier": 0.80,
})
updateResp.AssertSuccess(t)
// Verify update via list
verifyResp := client.Get("/api/admin/tenant-level-configs", nil)
verifyResp.AssertSuccess(t)
var verifyResult struct {
List []struct {
ID int64 `json:"id"`
MaxMembers int `json:"max_members"`
MaxConcurrency int `json:"max_concurrency"`
PriceMultiplier float64 `json:"price_multiplier"`
} `json:"list"`
}
verifyResp.DecodeData(t, &verifyResult)
for _, item := range verifyResult.List {
if item.ID == levelID {
if item.MaxMembers != 200 {
t.Fatalf("expected max_members=200 after update, got %d", item.MaxMembers)
}
if item.MaxConcurrency != 50 {
t.Fatalf("expected max_concurrency=50 after update, got %d", item.MaxConcurrency)
}
break
}
}
// --- Delete ---
deleteResp := client.Delete(fmt.Sprintf("/api/admin/tenant-level-configs/%d", levelID))
deleteResp.AssertSuccess(t)
// Verify deletion
afterDeleteResp := client.Get("/api/admin/tenant-level-configs", nil)
afterDeleteResp.AssertSuccess(t)
var afterDeleteResult struct {
List []struct {
ID int64 `json:"id"`
} `json:"list"`
}
afterDeleteResp.DecodeData(t, &afterDeleteResult)
for _, item := range afterDeleteResult.List {
if item.ID == levelID {
t.Fatalf("level id=%d should have been deleted", levelID)
}
}
}
@@ -0,0 +1,216 @@
//go:build integration
package admin_test
import (
"fmt"
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestTenantModelAssign(t *testing.T) {
client := testinfra.GetAuthedClient(t)
tenantID, tenantCleanup := testinfra.CreateTestTenant(t, client)
defer tenantCleanup()
model1ID, model1Cleanup := testinfra.CreateTestModel(t, client)
defer model1Cleanup()
model2ID, model2Cleanup := testinfra.CreateTestModel(t, client)
defer model2Cleanup()
// --- Assign models to tenant ---
assignResp := client.Post(fmt.Sprintf("/api/admin/tenants/%d/models", tenantID), map[string]any{
"assignments": []map[string]any{
{
"model_id": model1ID,
"enabled": true,
"billing_mode": "token",
},
{
"model_id": model2ID,
"enabled": true,
"billing_mode": "token",
},
},
})
assignResp.AssertSuccess(t)
var assignResult struct {
Assigned int `json:"assigned"`
}
assignResp.DecodeData(t, &assignResult)
if assignResult.Assigned != 2 {
t.Fatalf("expected assigned=2, got %d", assignResult.Assigned)
}
// --- Verify via list ---
listResp := client.Get(fmt.Sprintf("/api/admin/tenants/%d/models", tenantID), nil)
listResp.AssertSuccess(t)
var listResult struct {
List []struct {
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
ModelID int64 `json:"model_id"`
ModelCode string `json:"model_code"`
ModelName string `json:"model_name"`
Category string `json:"category"`
Enabled bool `json:"enabled"`
} `json:"list"`
}
listResp.DecodeData(t, &listResult)
if len(listResult.List) < 2 {
t.Fatalf("expected at least 2 tenant models, got %d", len(listResult.List))
}
}
func TestTenantModelList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
tenantID, tenantCleanup := testinfra.CreateTestTenant(t, client)
defer tenantCleanup()
modelID, modelCleanup := testinfra.CreateTestModel(t, client)
defer modelCleanup()
// Assign a model first
assignResp := client.Post(fmt.Sprintf("/api/admin/tenants/%d/models", tenantID), map[string]any{
"assignments": []map[string]any{
{
"model_id": modelID,
"enabled": true,
"billing_mode": "token",
},
},
})
assignResp.AssertSuccess(t)
// List tenant models
listResp := client.Get(fmt.Sprintf("/api/admin/tenants/%d/models", tenantID), nil)
listResp.AssertSuccess(t)
var result struct {
List []struct {
ModelID int64 `json:"model_id"`
ModelName string `json:"model_name"`
Category string `json:"category"`
Enabled bool `json:"enabled"`
} `json:"list"`
}
listResp.DecodeData(t, &result)
found := false
for _, m := range result.List {
if m.ModelID == modelID {
found = true
if !m.Enabled {
t.Fatal("expected model to be enabled")
}
break
}
}
if !found {
t.Fatalf("assigned model id=%d not found in tenant model list", modelID)
}
}
func TestTenantModelUpdate(t *testing.T) {
client := testinfra.GetAuthedClient(t)
tenantID, tenantCleanup := testinfra.CreateTestTenant(t, client)
defer tenantCleanup()
modelID, modelCleanup := testinfra.CreateTestModel(t, client)
defer modelCleanup()
// Assign model
assignResp := client.Post(fmt.Sprintf("/api/admin/tenants/%d/models", tenantID), map[string]any{
"assignments": []map[string]any{
{
"model_id": modelID,
"enabled": true,
"billing_mode": "token",
},
},
})
assignResp.AssertSuccess(t)
// --- Update tenant model ---
updateResp := client.Put(fmt.Sprintf("/api/admin/tenants/%d/models/%d", tenantID, modelID), map[string]any{
"enabled": false,
"billing_mode": "per_request",
})
updateResp.AssertSuccess(t)
// Verify update
listResp := client.Get(fmt.Sprintf("/api/admin/tenants/%d/models", tenantID), nil)
listResp.AssertSuccess(t)
var result struct {
List []struct {
ModelID int64 `json:"model_id"`
Enabled bool `json:"enabled"`
BillingMode string `json:"billing_mode"`
} `json:"list"`
}
listResp.DecodeData(t, &result)
for _, m := range result.List {
if m.ModelID == modelID {
if m.Enabled {
t.Fatal("expected model to be disabled after update")
}
if m.BillingMode != "per_request" {
t.Fatalf("expected billing_mode=per_request, got %s", m.BillingMode)
}
return
}
}
t.Fatalf("model id=%d not found after update", modelID)
}
func TestTenantModelDelete(t *testing.T) {
client := testinfra.GetAuthedClient(t)
tenantID, tenantCleanup := testinfra.CreateTestTenant(t, client)
defer tenantCleanup()
modelID, modelCleanup := testinfra.CreateTestModel(t, client)
defer modelCleanup()
// Assign model
assignResp := client.Post(fmt.Sprintf("/api/admin/tenants/%d/models", tenantID), map[string]any{
"assignments": []map[string]any{
{
"model_id": modelID,
"enabled": true,
"billing_mode": "token",
},
},
})
assignResp.AssertSuccess(t)
// --- Delete tenant model ---
deleteResp := client.Delete(fmt.Sprintf("/api/admin/tenants/%d/models/%d", tenantID, modelID))
deleteResp.AssertSuccess(t)
// Verify deletion
listResp := client.Get(fmt.Sprintf("/api/admin/tenants/%d/models", tenantID), nil)
listResp.AssertSuccess(t)
var result struct {
List []struct {
ModelID int64 `json:"model_id"`
} `json:"list"`
}
listResp.DecodeData(t, &result)
for _, m := range result.List {
if m.ModelID == modelID {
t.Fatalf("model id=%d should have been deleted from tenant", modelID)
}
}
}
+238
View File
@@ -0,0 +1,238 @@
//go:build integration
package admin_test
import (
"crypto/rand"
"encoding/hex"
"fmt"
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func randomSuffix() string {
b := make([]byte, 4)
_, _ = rand.Read(b)
return hex.EncodeToString(b)
}
func TestTenantList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// Create a tenant to ensure at least one exists
_, cleanup := testinfra.CreateTestTenant(t, client)
defer cleanup()
resp := client.Get("/api/admin/tenants", map[string]string{
"page": "1",
"page_size": "10",
})
testinfra.AssertPaginatedList(t, resp, 1)
}
func TestTenantListWithFilters(t *testing.T) {
client := testinfra.GetAuthedClient(t)
_, cleanup := testinfra.CreateTestTenant(t, client)
defer cleanup()
// Filter by status
resp := client.Get("/api/admin/tenants", map[string]string{
"page": "1",
"page_size": "10",
"status": "active",
})
resp.AssertSuccess(t)
// Filter by keyword
resp = client.Get("/api/admin/tenants", map[string]string{
"page": "1",
"page_size": "10",
"keyword": "test-tenant",
})
resp.AssertSuccess(t)
}
func TestTenantCRUD(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// --- Create ---
suffix := randomSuffix()
createResp := client.Post("/api/admin/tenants", map[string]any{
"tenant_name": fmt.Sprintf("CRUD测试租户 %s", suffix),
"tenant_code": fmt.Sprintf("crud-tenant-%s", suffix),
"username": fmt.Sprintf("crudowner%s", suffix),
"email": fmt.Sprintf("crudowner-%s@test.com", suffix),
"password": "OwnerPass123!",
"max_members": 20,
"max_concurrency": 5,
})
createResp.AssertSuccess(t)
tenantID := createResp.GetID(t)
defer func() {
client.Put(fmt.Sprintf("/api/admin/tenants/%d/status", tenantID), map[string]any{
"status": "closed",
})
}()
// --- Get detail ---
detailResp := client.Get(fmt.Sprintf("/api/admin/tenants/%d", tenantID), nil)
detailResp.AssertSuccess(t)
var detail struct {
ID int64 `json:"id"`
TenantName string `json:"tenant_name"`
TenantCode string `json:"tenant_code"`
MaxMembers int `json:"max_members"`
MaxConcurrency int `json:"max_concurrency"`
}
detailResp.DecodeData(t, &detail)
if detail.ID != tenantID {
t.Fatalf("expected tenant id %d, got %d", tenantID, detail.ID)
}
if detail.MaxMembers != 20 {
t.Fatalf("expected max_members=20, got %d", detail.MaxMembers)
}
// --- Update ---
updateResp := client.Put(fmt.Sprintf("/api/admin/tenants/%d", tenantID), map[string]any{
"name": fmt.Sprintf("更新租户名 %s", suffix),
"max_members": 50,
"max_concurrency": 10,
})
updateResp.AssertSuccess(t)
// Verify update
verifyResp := client.Get(fmt.Sprintf("/api/admin/tenants/%d", tenantID), nil)
verifyResp.AssertSuccess(t)
var updated struct {
MaxMembers int `json:"max_members"`
MaxConcurrency int `json:"max_concurrency"`
}
verifyResp.DecodeData(t, &updated)
if updated.MaxMembers != 50 {
t.Fatalf("expected max_members=50 after update, got %d", updated.MaxMembers)
}
// --- List should contain the created tenant ---
listResp := client.Get("/api/admin/tenants", map[string]string{
"page": "1",
"page_size": "50",
})
testinfra.AssertPaginatedList(t, listResp, 1)
total := listResp.GetTotal(t)
if total < 1 {
t.Fatalf("expected at least 1 tenant in list, got total=%d", total)
}
}
func TestTenantStatusUpdate(t *testing.T) {
client := testinfra.GetAuthedClient(t)
tenantID, cleanup := testinfra.CreateTestTenant(t, client)
defer cleanup()
// Suspend the tenant
suspendResp := client.Put(fmt.Sprintf("/api/admin/tenants/%d/status", tenantID), map[string]any{
"status": "suspended",
})
suspendResp.AssertSuccess(t)
// Verify status
detailResp := client.Get(fmt.Sprintf("/api/admin/tenants/%d", tenantID), nil)
detailResp.AssertSuccess(t)
var detail struct {
Status string `json:"status"`
}
detailResp.DecodeData(t, &detail)
if detail.Status != "suspended" {
t.Fatalf("expected status=suspended, got %s", detail.Status)
}
// Reactivate
activateResp := client.Put(fmt.Sprintf("/api/admin/tenants/%d/status", tenantID), map[string]any{
"status": "active",
})
activateResp.AssertSuccess(t)
// Verify reactivation
detailResp2 := client.Get(fmt.Sprintf("/api/admin/tenants/%d", tenantID), nil)
detailResp2.AssertSuccess(t)
var detail2 struct {
Status string `json:"status"`
}
detailResp2.DecodeData(t, &detail2)
if detail2.Status != "active" {
t.Fatalf("expected status=active after reactivation, got %s", detail2.Status)
}
}
func TestTenantChannelScope(t *testing.T) {
client := testinfra.GetAuthedClient(t)
tenantID, cleanup := testinfra.CreateTestTenant(t, client)
defer cleanup()
// Update channel scope
scopeResp := client.Put(fmt.Sprintf("/api/admin/tenants/%d/channel-scope", tenantID), map[string]any{
"default_channel_scope": "all",
})
scopeResp.AssertSuccess(t)
}
func TestTenantSelect(t *testing.T) {
client := testinfra.GetAuthedClient(t)
_, cleanup := testinfra.CreateTestTenant(t, client)
defer cleanup()
resp := client.Get("/api/admin/tenants/select", map[string]string{
"page": "1",
"page_size": "10",
})
resp.AssertSuccess(t)
var result struct {
List []struct {
ID int64 `json:"id"`
Name string `json:"name"`
Code string `json:"code"`
} `json:"list"`
Total int `json:"total"`
}
resp.DecodeData(t, &result)
if result.Total < 1 {
t.Fatalf("expected at least 1 tenant in select, got total=%d", result.Total)
}
// Filter by keyword
resp2 := client.Get("/api/admin/tenants/select", map[string]string{
"keyword": "test-tenant",
"page": "1",
"page_size": "10",
})
resp2.AssertSuccess(t)
}
func TestMemberList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
_, cleanup := testinfra.CreateTestTenant(t, client)
defer cleanup()
resp := client.Get("/api/admin/members", map[string]string{
"page": "1",
"page_size": "10",
})
resp.AssertSuccess(t)
// List with filters
resp = client.Get("/api/admin/members", map[string]string{
"page": "1",
"page_size": "10",
"status": "active",
})
resp.AssertSuccess(t)
}
@@ -0,0 +1,45 @@
//go:build integration
package testinfra
import (
"encoding/json"
"testing"
)
func AssertPaginatedList(t *testing.T, resp *APIResponse, minCount int) {
t.Helper()
resp.AssertSuccess(t)
var result struct {
List json.RawMessage `json:"list"`
Total int `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
resp.DecodeData(t, &result)
if result.Total < minCount {
t.Fatalf("expected total >= %d, got %d", minCount, result.Total)
}
if result.Page < 1 {
t.Fatalf("expected page >= 1, got %d", result.Page)
}
if result.PageSize < 1 {
t.Fatalf("expected page_size >= 1, got %d", result.PageSize)
}
}
func AssertNonEmptyList(t *testing.T, resp *APIResponse) {
t.Helper()
resp.AssertSuccess(t)
var result struct {
List json.RawMessage `json:"list"`
}
resp.DecodeData(t, &result)
if string(result.List) == "null" || string(result.List) == "[]" {
t.Fatal("expected non-empty list")
}
}
+94
View File
@@ -0,0 +1,94 @@
//go:build integration
package testinfra
import (
"context"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/redis/go-redis/v9"
)
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,
})
}
type LoginResult struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresAt string `json:"expires_at"`
TotpRequired bool `json:"totp_required"`
}
func Authenticate(t *testing.T) *LoginResult {
t.Helper()
return AuthenticateWithCreds(t, DefaultUsername, DefaultPassword)
}
func AuthenticateWithCreds(t *testing.T, username, password string) *LoginResult {
t.Helper()
client := NewAPIClient(DefaultBaseURL)
// Step 1: Get captcha
captchaResp := client.Get("/api/captcha/", nil)
captchaResp.AssertSuccess(t)
var captchaData struct {
CaptchaKey string `json:"captcha_key"`
}
captchaResp.DecodeData(t, &captchaData)
// Step 2: Read answer from Redis
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)
}
// Step 3: Login with the answer
loginResp := client.Post("/api/admin/auth/login", map[string]any{
"username": username,
"password": password,
"captcha_key": captchaData.CaptchaKey,
"captcha_x": answer.X,
})
loginResp.AssertSuccess(t)
var loginResult LoginResult
loginResp.DecodeData(t, &loginResult)
if loginResult.TotpRequired {
t.Fatal("admin user has 2FA enabled, cannot auto-login for tests")
}
return &loginResult
}
func GetAuthedClient(t *testing.T) *APIClient {
t.Helper()
result := Authenticate(t)
return NewAPIClient(DefaultBaseURL).WithToken(result.AccessToken)
}
+158
View File
@@ -0,0 +1,158 @@
//go:build integration
package testinfra
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"testing"
"time"
)
type APIResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data json.RawMessage `json:"data"`
RequestID string `json:"request_id"`
HTTPStatus int
}
type APIClient struct {
BaseURL string
HTTPClient *http.Client
Token string
}
func NewAPIClient(baseURL string) *APIClient {
return &APIClient{
BaseURL: baseURL,
HTTPClient: &http.Client{Timeout: 10 * time.Second},
}
}
func (c *APIClient) WithToken(token string) *APIClient {
clone := *c
clone.Token = token
return &clone
}
func (c *APIClient) Get(path string, params map[string]string) *APIResponse {
return c.doRequest("GET", path, params, nil)
}
func (c *APIClient) Post(path string, body any) *APIResponse {
return c.doRequest("POST", path, nil, body)
}
func (c *APIClient) Put(path string, body any) *APIResponse {
return c.doRequest("PUT", path, nil, body)
}
func (c *APIClient) Delete(path string) *APIResponse {
return c.doRequest("DELETE", path, nil, nil)
}
func (c *APIClient) doRequest(method, path string, params map[string]string, body any) *APIResponse {
var bodyReader io.Reader
if body != nil {
b, err := json.Marshal(body)
if err != nil {
return &APIResponse{HTTPStatus: 0, Message: fmt.Sprintf("marshal error: %v", err)}
}
bodyReader = bytes.NewReader(b)
}
fullURL := c.BaseURL + path
if len(params) > 0 {
q := url.Values{}
for k, v := range params {
q.Set(k, v)
}
if strings.Contains(fullURL, "?") {
fullURL += "&" + q.Encode()
} else {
fullURL += "?" + q.Encode()
}
}
req, err := http.NewRequest(method, fullURL, bodyReader)
if err != nil {
return &APIResponse{HTTPStatus: 0, Message: fmt.Sprintf("create request error: %v", err)}
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
if c.Token != "" {
req.Header.Set("Authorization", "Bearer "+c.Token)
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return &APIResponse{HTTPStatus: 0, Message: fmt.Sprintf("request error: %v", err)}
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return &APIResponse{HTTPStatus: resp.StatusCode, Message: fmt.Sprintf("read body error: %v", err)}
}
apiResp := &APIResponse{HTTPStatus: resp.StatusCode}
_ = json.Unmarshal(data, apiResp)
return apiResp
}
func (r *APIResponse) AssertSuccess(t *testing.T) {
t.Helper()
if r.HTTPStatus == 0 {
t.Fatalf("request failed: %s", r.Message)
}
if r.Code != 0 {
t.Fatalf("expected code=0, got code=%d message=%q request_id=%s", r.Code, r.Message, r.RequestID)
}
}
func (r *APIResponse) AssertError(t *testing.T, expectedCode int) {
t.Helper()
if r.Code != expectedCode {
t.Fatalf("expected code=%d, got code=%d message=%q", expectedCode, r.Code, r.Message)
}
}
func (r *APIResponse) AssertHTTPStatus(t *testing.T, expected int) {
t.Helper()
if r.HTTPStatus != expected {
t.Fatalf("expected HTTP %d, got %d", expected, r.HTTPStatus)
}
}
func (r *APIResponse) DecodeData(t *testing.T, target any) {
t.Helper()
if err := json.Unmarshal(r.Data, target); err != nil {
t.Fatalf("decode data error: %v, raw: %s", err, string(r.Data))
}
}
func (r *APIResponse) GetID(t *testing.T) int64 {
t.Helper()
var result struct {
ID int64 `json:"id"`
}
r.DecodeData(t, &result)
return result.ID
}
func (r *APIResponse) GetTotal(t *testing.T) int {
t.Helper()
var result struct {
Total int `json:"total"`
}
r.DecodeData(t, &result)
return result.Total
}
@@ -0,0 +1,10 @@
//go:build integration
package testinfra
const (
DefaultBaseURL = "http://127.0.0.1:18888"
DefaultUsername = "admin"
DefaultPassword = "Admin123"
DefaultRedisAddr = "192.168.50.22:16380"
)
@@ -0,0 +1,127 @@
//go:build integration
package testinfra
import (
"crypto/rand"
"encoding/hex"
"fmt"
"testing"
)
func randomSuffix() string {
b := make([]byte, 4)
_, _ = rand.Read(b)
return hex.EncodeToString(b)
}
func CreateTestAdminUser(t *testing.T, client *APIClient) (id int64, cleanup func()) {
t.Helper()
username := fmt.Sprintf("testuser%s", randomSuffix())
resp := client.Post("/api/admin/users", map[string]any{
"username": username,
"password": "TestPass123!",
"email": fmt.Sprintf("%s@test.com", username),
"role": "admin",
})
resp.AssertSuccess(t)
id = resp.GetID(t)
return id, func() {
client.Delete(fmt.Sprintf("/api/admin/users/%d", id))
}
}
func CreateTestTenant(t *testing.T, client *APIClient) (id int64, cleanup func()) {
t.Helper()
code := fmt.Sprintf("test-tenant-%s", randomSuffix())
resp := client.Post("/api/admin/tenants", map[string]any{
"tenant_name": fmt.Sprintf("测试租户 %s", code),
"tenant_code": code,
"username": fmt.Sprintf("owner%s", randomSuffix()),
"email": fmt.Sprintf("owner-%s@test.com", code),
"password": "OwnerPass123!",
})
resp.AssertSuccess(t)
id = resp.GetID(t)
return id, func() {
client.Put(fmt.Sprintf("/api/admin/tenants/%d/status", id), map[string]any{
"status": "closed",
})
}
}
func CreateTestModel(t *testing.T, client *APIClient) (id int64, cleanup func()) {
t.Helper()
modelID := fmt.Sprintf("test-model-%s", randomSuffix())
resp := client.Post("/api/admin/models", map[string]any{
"model_id": modelID,
"model_name": fmt.Sprintf("Test Model %s", modelID),
"category": "chat",
})
resp.AssertSuccess(t)
id = resp.GetID(t)
return id, func() {
client.Delete(fmt.Sprintf("/api/admin/models/%d", id))
}
}
func CreateTestChannel(t *testing.T, client *APIClient) (id int64, cleanup func()) {
t.Helper()
name := fmt.Sprintf("Test Channel %s", randomSuffix())
resp := client.Post("/api/admin/channels", map[string]any{
"name": name,
"type": 1,
"api_key": "sk-test-key-" + randomSuffix(),
})
resp.AssertSuccess(t)
id = resp.GetID(t)
return id, func() {
client.Delete(fmt.Sprintf("/api/admin/channels/%d", id))
}
}
func CreateTestPlan(t *testing.T, client *APIClient) (id int64, cleanup func()) {
t.Helper()
ident := fmt.Sprintf("test-plan-%s", randomSuffix())
resp := client.Post("/api/admin/plans", map[string]any{
"name": fmt.Sprintf("Test Plan %s", ident),
"identifier": ident,
})
resp.AssertSuccess(t)
id = resp.GetID(t)
return id, func() {
client.Delete(fmt.Sprintf("/api/admin/plans/%d", id))
}
}
func CreateTestModelGroup(t *testing.T, client *APIClient) (id int64, cleanup func()) {
t.Helper()
code := fmt.Sprintf("test-group-%s", randomSuffix())
resp := client.Post("/api/admin/model-groups", map[string]any{
"name": fmt.Sprintf("Test Group %s", code),
"code": code,
})
resp.AssertSuccess(t)
id = resp.GetID(t)
return id, func() {
client.Delete(fmt.Sprintf("/api/admin/model-groups/%d", id))
}
}
func CreateTestTenantLevel(t *testing.T, client *APIClient) (id int64, cleanup func()) {
t.Helper()
suffix := randomSuffix()
resp := client.Post("/api/admin/tenant-level-configs", map[string]any{
"level": 90 + int(suffix[0]%10),
"name": fmt.Sprintf("测试等级 %s", suffix),
"cumulative_recharge_threshold": 100.0,
"max_members": 50,
"max_concurrency": 10,
"price_multiplier": 0.9,
})
resp.AssertSuccess(t)
id = resp.GetID(t)
return id, func() {
client.Delete(fmt.Sprintf("/api/admin/tenant-level-configs/%d", id))
}
}
+53
View File
@@ -0,0 +1,53 @@
//go:build integration
package admin_test
import (
"fmt"
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestTicketList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/tickets", map[string]string{
"page": "1",
"page_size": "10",
})
testinfra.AssertPaginatedList(t, resp, 0)
}
func TestTicketListWithFilters(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// Filter by status
resp := client.Get("/api/admin/tickets", map[string]string{
"page": "1",
"page_size": "10",
"status": "open",
})
resp.AssertSuccess(t)
// Filter by category
resp = client.Get("/api/admin/tickets", map[string]string{
"page": "1",
"page_size": "10",
"category": "billing",
})
resp.AssertSuccess(t)
// Create tenant and filter by tenant_id
tenantID, cleanup := testinfra.CreateTestTenant(t, client)
defer cleanup()
resp = client.Get("/api/admin/tickets", map[string]string{
"page": "1",
"page_size": "10",
"tenant_id": fmt.Sprintf("%d", tenantID),
})
resp.AssertSuccess(t)
t.Logf("Ticket list filters applied successfully")
}
+41
View File
@@ -0,0 +1,41 @@
//go:build integration
package admin_test
import (
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestUsageLogList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
// Basic list with default pagination
resp := client.Get("/api/admin/usage-logs", map[string]string{
"page": "1",
"page_size": "10",
})
testinfra.AssertPaginatedList(t, resp, 0)
t.Logf("Usage log list: total=%d", resp.GetTotal(t))
// List with date filters
respFiltered := client.Get("/api/admin/usage-logs", map[string]string{
"page": "1",
"page_size": "5",
"start_date": "2025-01-01",
"end_date": "2030-12-31",
})
testinfra.AssertPaginatedList(t, respFiltered, 0)
// List with status filter
respByStatus := client.Get("/api/admin/usage-logs", map[string]string{
"page": "1",
"page_size": "5",
"status": "success",
})
testinfra.AssertPaginatedList(t, respByStatus, 0)
t.Logf("Usage log list with filters returned successfully")
}
+131
View File
@@ -0,0 +1,131 @@
//go:build integration
package admin_test
import (
"fmt"
"testing"
"github.com/qianfree/team-api/tests/integration/admin/testinfra"
)
func TestWalletList(t *testing.T) {
client := testinfra.GetAuthedClient(t)
resp := client.Get("/api/admin/wallets", map[string]string{
"page": "1",
"page_size": "10",
})
testinfra.AssertPaginatedList(t, resp, 0)
t.Logf("Wallet list retrieved successfully")
}
func TestWalletInfo(t *testing.T) {
client := testinfra.GetAuthedClient(t)
tenantID, cleanup := testinfra.CreateTestTenant(t, client)
defer cleanup()
resp := client.Get(fmt.Sprintf("/api/admin/wallets/%d", tenantID), nil)
resp.AssertSuccess(t)
var data struct {
Balance float64 `json:"balance"`
FrozenBalance float64 `json:"frozen_balance"`
WarningThreshold *float64 `json:"warning_threshold"`
}
resp.DecodeData(t, &data)
if data.Balance < 0 {
t.Fatalf("expected balance >= 0, got %f", data.Balance)
}
if data.FrozenBalance < 0 {
t.Fatalf("expected frozen_balance >= 0, got %f", data.FrozenBalance)
}
t.Logf("Wallet info for tenant %d: balance=%f, frozen=%f",
tenantID, data.Balance, data.FrozenBalance)
}
func TestWalletAdjust(t *testing.T) {
client := testinfra.GetAuthedClient(t)
tenantID, cleanup := testinfra.CreateTestTenant(t, client)
defer cleanup()
// Add balance
resp := client.Post(fmt.Sprintf("/api/admin/wallets/%d/adjust", tenantID), map[string]any{
"amount": 10.5,
"description": "integration test adjustment",
})
resp.AssertSuccess(t)
// Verify balance increased
infoResp := client.Get(fmt.Sprintf("/api/admin/wallets/%d", tenantID), nil)
infoResp.AssertSuccess(t)
var data struct {
Balance float64 `json:"balance"`
}
infoResp.DecodeData(t, &data)
if data.Balance < 10.0 {
t.Fatalf("expected balance >= 10.0 after adjustment, got %f", data.Balance)
}
t.Logf("Wallet adjust succeeded, new balance: %f", data.Balance)
}
func TestWalletTransactions(t *testing.T) {
client := testinfra.GetAuthedClient(t)
tenantID, cleanup := testinfra.CreateTestTenant(t, client)
defer cleanup()
// First adjust wallet to create a transaction
adjustResp := client.Post(fmt.Sprintf("/api/admin/wallets/%d/adjust", tenantID), map[string]any{
"amount": 5.0,
"description": "test transaction for listing",
})
adjustResp.AssertSuccess(t)
// Then list transactions
resp := client.Get(fmt.Sprintf("/api/admin/wallets/%d/transactions", tenantID), map[string]string{
"page": "1",
"page_size": "20",
})
testinfra.AssertPaginatedList(t, resp, 1)
total := resp.GetTotal(t)
t.Logf("Wallet transactions for tenant %d: total=%d", tenantID, total)
}
func TestWalletWarningThreshold(t *testing.T) {
client := testinfra.GetAuthedClient(t)
tenantID, cleanup := testinfra.CreateTestTenant(t, client)
defer cleanup()
// Set warning threshold
threshold := 1.5
resp := client.Put(fmt.Sprintf("/api/admin/wallets/%d/warning-threshold", tenantID), map[string]any{
"threshold": threshold,
})
resp.AssertSuccess(t)
// Verify threshold was set
infoResp := client.Get(fmt.Sprintf("/api/admin/wallets/%d", tenantID), nil)
infoResp.AssertSuccess(t)
var data struct {
WarningThreshold *float64 `json:"warning_threshold"`
}
infoResp.DecodeData(t, &data)
if data.WarningThreshold == nil {
t.Fatal("expected warning_threshold to be set, got nil")
}
t.Logf("Warning threshold set to %f for tenant %d", *data.WarningThreshold, tenantID)
}