Files

356 lines
11 KiB
Go

//go:build integration
package tenant_test
import (
"fmt"
"testing"
"github.com/qianfree/team-api/internal/consts"
admintest "github.com/qianfree/team-api/tests/integration/admin/testinfra"
"github.com/qianfree/team-api/tests/integration/tenant/testinfra"
)
func TestTenantRegister_Success(t *testing.T) {
result := testinfra.RegisterTestTenant(t)
if result.AccessToken == "" {
t.Fatal("expected non-empty access_token")
}
if result.RefreshToken == "" {
t.Fatal("expected non-empty refresh_token")
}
if result.ExpiresAt == "" {
t.Fatal("expected non-empty expires_at")
}
if result.Tenant.ID == 0 {
t.Fatal("expected tenant.id > 0")
}
if result.Tenant.Code == "" {
t.Fatal("expected tenant.code")
}
if result.User.Role != "owner" {
t.Fatalf("expected role=owner, got %s", result.User.Role)
}
}
func TestTenantRegister_DuplicateTenantCode(t *testing.T) {
result := testinfra.RegisterTestTenant(t)
// Try registering again with the same tenant_code
captchaKey, captchaX := testinfra.SolveCaptcha(t)
client := admintest.NewAPIClient(testinfra.DefaultBaseURL)
resp := client.Post("/api/tenant/auth/register", map[string]any{
"email": fmt.Sprintf("dup-%s@test.com", result.TenantCode),
"password": testinfra.TestPassword,
"tenant_name": "重复租户",
"tenant_code": result.TenantCode,
"username": "anotheruser",
"captcha_key": captchaKey,
"captcha_x": captchaX,
})
if resp.Code == 0 {
t.Fatal("expected error for duplicate tenant_code, but got success")
}
}
func TestTenantLogin_RAM_Success(t *testing.T) {
regResult := testinfra.RegisterTestTenant(t)
loginResult := testinfra.LoginTenant(t, regResult.Username, regResult.TenantCode, regResult.Password)
if loginResult.AccessToken == "" {
t.Fatal("expected non-empty access_token after RAM login")
}
if loginResult.RefreshToken == "" {
t.Fatal("expected non-empty refresh_token after RAM login")
}
}
func TestTenantLogin_InvalidPassword(t *testing.T) {
regResult := testinfra.RegisterTestTenant(t)
captchaKey, captchaX := testinfra.SolveCaptcha(t)
client := admintest.NewAPIClient(testinfra.DefaultBaseURL)
account := fmt.Sprintf("%s@%s", regResult.Username, regResult.TenantCode)
resp := client.Post("/api/tenant/auth/login", map[string]any{
"account": account,
"password": "WrongPassword123!",
"type": "ram",
"captcha_key": captchaKey,
"captcha_x": captchaX,
})
if resp.Code == 0 {
t.Fatal("expected login to fail with wrong password, but got success")
}
}
func TestTenantLogout_Success(t *testing.T) {
client, _ := testinfra.GetAuthedClient(t)
logoutResp := client.Post("/api/tenant/auth/logout", nil)
logoutResp.AssertSuccess(t)
// Verify token is invalidated
sessionResp := client.Get("/api/tenant/auth/sessions", map[string]string{
"page": "1",
"page_size": "10",
})
sessionResp.AssertHTTPStatus(t, 401)
}
func TestTenantTokenRefresh(t *testing.T) {
regResult := testinfra.RegisterTestTenant(t)
client := admintest.NewAPIClient(testinfra.DefaultBaseURL)
refreshResp := client.Post("/api/tenant/auth/refresh", map[string]any{
"refresh_token": regResult.RefreshToken,
})
refreshResp.AssertSuccess(t)
var refreshData struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresAt string `json:"expires_at"`
}
refreshResp.DecodeData(t, &refreshData)
if refreshData.AccessToken == "" {
t.Fatal("expected non-empty access_token after refresh")
}
// Verify new token works
authedClient := admintest.NewAPIClient(testinfra.DefaultBaseURL).WithToken(refreshData.AccessToken)
sessionsResp := authedClient.Get("/api/tenant/auth/sessions", map[string]string{
"page": "1",
"page_size": "10",
})
sessionsResp.AssertSuccess(t)
}
func TestSuspendedTenantExistingTokensAreRejected(t *testing.T) {
regResult := testinfra.RegisterTestTenant(t)
adminClient := admintest.GetAuthedClient(t)
setStatus := func(status string) {
t.Helper()
resp := adminClient.Put(fmt.Sprintf("/api/admin/tenants/%d/status", regResult.Tenant.ID), map[string]any{
"status": status,
})
resp.AssertSuccess(t)
}
setStatus("suspended")
defer setStatus("active")
tenantClient := admintest.NewAPIClient(testinfra.DefaultBaseURL).WithToken(regResult.AccessToken)
accessResp := tenantClient.Get("/api/tenant/auth/sessions", map[string]string{
"page": "1",
"page_size": "10",
})
accessResp.AssertHTTPStatus(t, 403)
accessResp.AssertError(t, consts.CodeTenantSuspended)
publicClient := admintest.NewAPIClient(testinfra.DefaultBaseURL)
refreshResp := publicClient.Post("/api/tenant/auth/refresh", map[string]any{
"refresh_token": regResult.RefreshToken,
})
refreshResp.AssertError(t, consts.CodeTenantSuspended)
setStatus("active")
refreshAfterActivation := publicClient.Post("/api/tenant/auth/refresh", map[string]any{
"refresh_token": regResult.RefreshToken,
})
refreshAfterActivation.AssertSuccess(t)
}
func TestTenantChangePassword(t *testing.T) {
regResult := testinfra.RegisterTestTenant(t)
client := admintest.NewAPIClient(testinfra.DefaultBaseURL).WithToken(regResult.AccessToken)
newPassword := "NewPass456!"
changeResp := client.Put("/api/tenant/auth/change-password", map[string]any{
"old_password": regResult.Password,
"new_password": newPassword,
})
changeResp.AssertSuccess(t)
// Verify can login with new password
loginResult := testinfra.LoginTenant(t, regResult.Username, regResult.TenantCode, newPassword)
if loginResult.AccessToken == "" {
t.Fatal("expected login with new password to succeed")
}
}
func TestTenantSessionList(t *testing.T) {
regResult := testinfra.RegisterTestTenant(t)
client := admintest.NewAPIClient(testinfra.DefaultBaseURL).WithToken(regResult.AccessToken)
resp := client.Get("/api/tenant/auth/sessions", map[string]string{
"page": "1",
"page_size": "10",
})
resp.AssertSuccess(t)
// Verify paginated response structure exists
var data struct {
List []any `json:"list"`
Total int `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
resp.DecodeData(t, &data)
}
func TestTenantSessionRevoke(t *testing.T) {
regResult := testinfra.RegisterTestTenant(t)
client := admintest.NewAPIClient(testinfra.DefaultBaseURL).WithToken(regResult.AccessToken)
// List sessions
listResp := client.Get("/api/tenant/auth/sessions", map[string]string{
"page": "1",
"page_size": "10",
})
listResp.AssertSuccess(t)
var listData struct {
List []struct {
ID int64 `json:"id"`
} `json:"list"`
Total int `json:"total"`
}
listResp.DecodeData(t, &listData)
if len(listData.List) == 0 {
t.Skip("no sessions to revoke, session list empty")
}
// Revoke the first session
revokeResp := client.Delete(fmt.Sprintf("/api/tenant/auth/sessions/%d", listData.List[0].ID))
revokeResp.AssertSuccess(t)
}
func TestTenantAuth_Unauthorized(t *testing.T) {
client := admintest.NewAPIClient(testinfra.DefaultBaseURL)
resp := client.Get("/api/tenant/auth/sessions", map[string]string{
"page": "1",
"page_size": "10",
})
resp.AssertHTTPStatus(t, 401)
}
// ─── 边界值测试 ────────────────────────────────────────────────────
// TestTenantRegister_EmptyRequiredFields 验证缺失必填字段时注册被拒绝
// Business rule: 注册时 username/password/tenant_name 不能为空
func TestTenantRegister_EmptyRequiredFields(t *testing.T) {
captchaKey, captchaX := testinfra.SolveCaptcha(t)
client := admintest.NewAPIClient(testinfra.DefaultBaseURL)
// 空用户名
resp := client.Post("/api/tenant/auth/register", map[string]any{
"email": "empty-user@test.com",
"password": testinfra.TestPassword,
"tenant_name": "空用户名租户",
"tenant_code": "t" + testinfra.RandomSuffix(),
"username": "",
"captcha_key": captchaKey,
"captcha_x": captchaX,
})
if resp.Code == 0 {
t.Fatal("empty username should be rejected")
}
t.Logf("empty username: code=%d msg=%q", resp.Code, resp.Message)
// 空密码
captchaKey, captchaX = testinfra.SolveCaptcha(t)
resp = client.Post("/api/tenant/auth/register", map[string]any{
"email": "empty-pwd@test.com",
"password": "",
"tenant_name": "空密码租户",
"tenant_code": "t" + testinfra.RandomSuffix(),
"username": "emptypwduser",
"captcha_key": captchaKey,
"captcha_x": captchaX,
})
if resp.Code == 0 {
t.Fatal("empty password should be rejected")
}
t.Logf("empty password: code=%d msg=%q", resp.Code, resp.Message)
// 空租户名称
captchaKey, captchaX = testinfra.SolveCaptcha(t)
resp = client.Post("/api/tenant/auth/register", map[string]any{
"email": "empty-name@test.com",
"password": testinfra.TestPassword,
"tenant_name": "",
"tenant_code": "t" + testinfra.RandomSuffix(),
"username": "emptynameuser",
"captcha_key": captchaKey,
"captcha_x": captchaX,
})
if resp.Code == 0 {
t.Fatal("empty tenant_name should be rejected")
}
t.Logf("empty tenant_name: code=%d msg=%q", resp.Code, resp.Message)
}
// TestTenantRegister_ShortPassword 验证短密码被拒绝
// Business rule: 密码长度最少 8 位
func TestTenantRegister_ShortPassword(t *testing.T) {
captchaKey, captchaX := testinfra.SolveCaptcha(t)
client := admintest.NewAPIClient(testinfra.DefaultBaseURL)
resp := client.Post("/api/tenant/auth/register", map[string]any{
"email": "short-pwd@test.com",
"password": "abc123",
"tenant_name": "短密码租户",
"tenant_code": "t" + testinfra.RandomSuffix(),
"username": "shortpwduser",
"captcha_key": captchaKey,
"captcha_x": captchaX,
})
if resp.Code == 0 {
t.Fatal("short password should be rejected (min 8 chars)")
}
t.Logf("short password: code=%d msg=%q", resp.Code, resp.Message)
}
// TestTenantRegister_InvalidEmail 验证无效邮箱格式被拒绝
// Business rule: email 必须是合法邮箱格式
func TestTenantRegister_InvalidEmail(t *testing.T) {
captchaKey, captchaX := testinfra.SolveCaptcha(t)
client := admintest.NewAPIClient(testinfra.DefaultBaseURL)
resp := client.Post("/api/tenant/auth/register", map[string]any{
"email": "not-an-email",
"password": testinfra.TestPassword,
"tenant_name": "无效邮箱租户",
"tenant_code": "t" + testinfra.RandomSuffix(),
"username": "bademailuser",
"captcha_key": captchaKey,
"captcha_x": captchaX,
})
if resp.Code == 0 {
t.Fatal("invalid email should be rejected")
}
t.Logf("invalid email: code=%d msg=%q", resp.Code, resp.Message)
}
// TestTenantLogin_EmptyFields 验证空字段登录被拒绝
func TestTenantLogin_EmptyFields(t *testing.T) {
captchaKey, captchaX := testinfra.SolveCaptcha(t)
client := admintest.NewAPIClient(testinfra.DefaultBaseURL)
resp := client.Post("/api/tenant/auth/login", map[string]any{
"account": "",
"password": "",
"type": "ram",
"captcha_key": captchaKey,
"captcha_x": captchaX,
})
if resp.Code == 0 {
t.Fatal("empty login fields should be rejected")
}
t.Logf("empty login fields: code=%d msg=%q", resp.Code, resp.Message)
}