feat(test):新增管理端API自动化测试
This commit is contained in:
1 parent
706b2da5b4
commit
104f1de0b3
36 files changed
+4195
No files matched your search
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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, ¤t)
|
||||
|
||||
// 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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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, ¤t)
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user