package pub import ( "context" "database/sql" "errors" "testing" "time" authHelper "perms-system-server/internal/logic/auth" permModel "perms-system-server/internal/model/perm" productmemberModel "perms-system-server/internal/model/productmember" userModel "perms-system-server/internal/model/user" "perms-system-server/internal/response" "perms-system-server/internal/testutil" "perms-system-server/internal/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func insertRefreshTestUser(t *testing.T, ctx context.Context, username, password string, status, isSuperAdmin int64) (int64, func()) { t.Helper() svcCtx := newTestSvcCtx() conn := testutil.GetTestSqlConn() now := time.Now().Unix() hashed := testutil.HashPassword(password) res, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{ Username: username, Password: hashed, Nickname: username, Avatar: sql.NullString{}, Email: username + "@test.com", Phone: "13800000000", Remark: "", DeptId: 0, IsSuperAdmin: isSuperAdmin, MustChangePassword: 2, Status: status, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) id, _ := res.LastInsertId() cleanup := func() { testutil.CleanTable(ctx, conn, "`sys_user`", id) } return id, cleanup } // TC-0026: 正常刷新(refreshToken从header获取,原样返回不重新生成) func TestRefreshToken_Normal(t *testing.T) { ctx := context.Background() svcCtx := newTestSvcCtx() username := testutil.UniqueId() password := "TestPass123" userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 1, 2) t.Cleanup(cleanUser) refreshToken, err := authHelper.GenerateRefreshToken( svcCtx.Config.Auth.RefreshSecret, svcCtx.Config.Auth.RefreshExpire, userId, "", 0, ) require.NoError(t, err) logic := NewRefreshTokenLogic(ctx, svcCtx) resp, err := logic.RefreshToken(&types.RefreshTokenReq{ Authorization: "Bearer " + refreshToken, }) require.NoError(t, err) require.NotNil(t, resp) assert.NotEmpty(t, resp.AccessToken) assert.NotEmpty(t, resp.RefreshToken, "应返回新的refreshToken") assert.NotEqual(t, resp.AccessToken, resp.RefreshToken, "accessToken和refreshToken应不同") assert.True(t, resp.Expires > time.Now().Unix(), "expires应为未来的unix时间戳") assert.Equal(t, userId, resp.UserInfo.UserId) assert.Equal(t, username, resp.UserInfo.Username) } // TC-0027: 不带productCode(回退) func TestRefreshToken_FallbackToClaimsProductCode(t *testing.T) { ctx := context.Background() svcCtx := newTestSvcCtx() conn := testutil.GetTestSqlConn() username := testutil.UniqueId() password := "TestPass123" pc := testutil.UniqueId() now := time.Now().Unix() userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 1, 2) t.Cleanup(cleanUser) _, cleanProduct := insertTestProduct(t, ctx, svcCtx, pc, testutil.UniqueId(), "secret") t.Cleanup(cleanProduct) pmRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &productmemberModel.SysProductMember{ ProductCode: pc, UserId: userId, MemberType: "ADMIN", Status: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) pmId, _ := pmRes.LastInsertId() t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product_member`", pmId) }) permCode := testutil.UniqueId() permRes, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{ ProductCode: pc, Name: "refresh_perm", Code: permCode, Status: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) permId, _ := permRes.LastInsertId() t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_perm`", permId) }) refreshToken, err := authHelper.GenerateRefreshToken( svcCtx.Config.Auth.RefreshSecret, svcCtx.Config.Auth.RefreshExpire, userId, pc, 0, ) require.NoError(t, err) logic := NewRefreshTokenLogic(ctx, svcCtx) resp, err := logic.RefreshToken(&types.RefreshTokenReq{ Authorization: "Bearer " + refreshToken, }) require.NoError(t, err) require.NotNil(t, resp) assert.Equal(t, "ADMIN", resp.UserInfo.MemberType) assert.Contains(t, resp.UserInfo.Perms, permCode) } // TC-0028: token无效 func TestRefreshToken_InvalidToken(t *testing.T) { ctx := context.Background() svcCtx := newTestSvcCtx() logic := NewRefreshTokenLogic(ctx, svcCtx) resp, err := logic.RefreshToken(&types.RefreshTokenReq{ Authorization: "Bearer invalid.token.string", }) require.Nil(t, resp) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 401, codeErr.Code()) assert.Equal(t, "refreshToken无效或已过期", codeErr.Error()) } // TC-0029: 用户已被删除 —— M-1 修复后必须区分"不存在"(401) 与"冻结"(403)。 // // 修复前:Loader 对不存在用户返回空壳 UserDetails(Status=0),RefreshToken 走到"账号已被冻结"分支 (403), // // 将"用户不存在"与"账号冻结"两个语义混淆,监控告警与运维处置策略无法区分。 // // 修复后:Loader 返回 (ud, nil) 且 ud.Username == "",RefreshToken 显式回 401 "用户不存在或已被删除"。 // // 这样客户端/前端才能走"注销本地会话 + 返回登录页"的终态流程,而不是提示"账号已冻结请联系管理员"。 func TestRefreshToken_UserDeleted(t *testing.T) { ctx := context.Background() svcCtx := newTestSvcCtx() nonExistentUserId := int64(999999999) refreshToken, err := authHelper.GenerateRefreshToken( svcCtx.Config.Auth.RefreshSecret, svcCtx.Config.Auth.RefreshExpire, nonExistentUserId, "", 0, ) require.NoError(t, err) logic := NewRefreshTokenLogic(ctx, svcCtx) resp, err := logic.RefreshToken(&types.RefreshTokenReq{ Authorization: "Bearer " + refreshToken, }) require.Nil(t, resp) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 401, codeErr.Code(), "M-1:用户不存在必须走 401,不得与冻结态 (403) 混淆") assert.Equal(t, "用户不存在或已被删除", codeErr.Error()) } // TC-0030: 账号冻结 func TestRefreshToken_AccountFrozen(t *testing.T) { ctx := context.Background() svcCtx := newTestSvcCtx() username := testutil.UniqueId() password := "TestPass123" userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 2, 2) t.Cleanup(cleanUser) refreshToken, err := authHelper.GenerateRefreshToken( svcCtx.Config.Auth.RefreshSecret, svcCtx.Config.Auth.RefreshExpire, userId, "", 0, ) require.NoError(t, err) logic := NewRefreshTokenLogic(ctx, svcCtx) resp, err := logic.RefreshToken(&types.RefreshTokenReq{ Authorization: "Bearer " + refreshToken, }) require.Nil(t, resp) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 403, codeErr.Code()) assert.Equal(t, "账号已被冻结", codeErr.Error()) } // TC-0032: 尝试切换产品被拒绝 func TestRefreshToken_ProductCodeSwitchRejected(t *testing.T) { ctx := context.Background() svcCtx := newTestSvcCtx() username := testutil.UniqueId() password := "TestPass123" userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 1, 2) t.Cleanup(cleanUser) refreshToken, err := authHelper.GenerateRefreshToken( svcCtx.Config.Auth.RefreshSecret, svcCtx.Config.Auth.RefreshExpire, userId, "product_a", 0, ) require.NoError(t, err) logic := NewRefreshTokenLogic(ctx, svcCtx) resp, err := logic.RefreshToken(&types.RefreshTokenReq{ Authorization: "Bearer " + refreshToken, ProductCode: "product_b", }) require.Nil(t, resp) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 400, codeErr.Code()) assert.Equal(t, "刷新令牌不允许切换产品", codeErr.Error()) } // TC-0033: TokenVersion不匹配时拒绝刷新 func TestRefreshToken_TokenVersionMismatch(t *testing.T) { ctx := context.Background() svcCtx := newTestSvcCtx() username := testutil.UniqueId() password := "TestPass123" userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 1, 2) t.Cleanup(cleanUser) refreshToken, err := authHelper.GenerateRefreshToken( svcCtx.Config.Auth.RefreshSecret, svcCtx.Config.Auth.RefreshExpire, userId, "", 999, ) require.NoError(t, err) logic := NewRefreshTokenLogic(ctx, svcCtx) resp, err := logic.RefreshToken(&types.RefreshTokenReq{ Authorization: "Bearer " + refreshToken, }) require.Nil(t, resp) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 401, codeErr.Code()) assert.Equal(t, "登录状态已失效,请重新登录", codeErr.Error()) } // TC-0034: 使用accessToken作为refreshToken被拒绝 func TestRefreshToken_AccessTokenRejected(t *testing.T) { ctx := context.Background() svcCtx := newTestSvcCtx() username := testutil.UniqueId() password := "TestPass123" userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 1, 2) t.Cleanup(cleanUser) accessToken, err := authHelper.GenerateAccessToken( svcCtx.Config.Auth.RefreshSecret, svcCtx.Config.Auth.AccessExpire, userId, username, "", "", 0, ) require.NoError(t, err) logic := NewRefreshTokenLogic(ctx, svcCtx) resp, err := logic.RefreshToken(&types.RefreshTokenReq{ Authorization: "Bearer " + accessToken, }) require.Nil(t, resp) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 401, codeErr.Code()) assert.Equal(t, "refreshToken无效或已过期", codeErr.Error()) } // TC-0035: 产品成员已移除时拒绝刷新 func TestRefreshToken_MemberRemovedRejected(t *testing.T) { ctx := context.Background() svcCtx := newTestSvcCtx() conn := testutil.GetTestSqlConn() username := testutil.UniqueId() password := "TestPass123" pc := testutil.UniqueId() now := time.Now().Unix() userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 1, 2) t.Cleanup(cleanUser) _, cleanProduct := insertTestProduct(t, ctx, svcCtx, pc, testutil.UniqueId(), "secret") t.Cleanup(cleanProduct) pmRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &productmemberModel.SysProductMember{ ProductCode: pc, UserId: userId, MemberType: "MEMBER", Status: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) pmId, _ := pmRes.LastInsertId() refreshToken, err := authHelper.GenerateRefreshToken( svcCtx.Config.Auth.RefreshSecret, svcCtx.Config.Auth.RefreshExpire, userId, pc, 0, ) require.NoError(t, err) testutil.CleanTable(ctx, conn, "`sys_product_member`", pmId) logic := NewRefreshTokenLogic(ctx, svcCtx) resp, err := logic.RefreshToken(&types.RefreshTokenReq{ Authorization: "Bearer " + refreshToken, }) require.Nil(t, resp) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 403, codeErr.Code()) assert.Equal(t, "您已不是该产品的成员", codeErr.Error()) } // TC-0031: 超管+productCode(refreshToken原样返回) func TestRefreshToken_SuperAdminWithProductCode(t *testing.T) { ctx := context.Background() svcCtx := newTestSvcCtx() conn := testutil.GetTestSqlConn() username := testutil.UniqueId() password := "TestPass123" pc := testutil.UniqueId() now := time.Now().Unix() userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 1, 1) t.Cleanup(cleanUser) _, cleanProduct := insertTestProduct(t, ctx, svcCtx, pc, testutil.UniqueId(), "secret") t.Cleanup(cleanProduct) permCode := testutil.UniqueId() permRes, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{ ProductCode: pc, Name: "sa_refresh_perm", Code: permCode, Status: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) permId, _ := permRes.LastInsertId() t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_perm`", permId) }) refreshToken, err := authHelper.GenerateRefreshToken( svcCtx.Config.Auth.RefreshSecret, svcCtx.Config.Auth.RefreshExpire, userId, pc, 0, ) require.NoError(t, err) logic := NewRefreshTokenLogic(ctx, svcCtx) resp, err := logic.RefreshToken(&types.RefreshTokenReq{ Authorization: "Bearer " + refreshToken, ProductCode: pc, }) require.NoError(t, err) require.NotNil(t, resp) assert.NotEmpty(t, resp.RefreshToken, "应返回新的refreshToken") assert.Equal(t, "SUPER_ADMIN", resp.UserInfo.MemberType) assert.Contains(t, resp.UserInfo.Perms, permCode) assert.Equal(t, int64(1), resp.UserInfo.IsSuperAdmin) }