package auth import ( "context" "database/sql" "errors" "strings" "testing" "time" "perms-system-server/internal/loaders" "perms-system-server/internal/middleware" userModel "perms-system-server/internal/model/user" "perms-system-server/internal/response" "perms-system-server/internal/svc" "perms-system-server/internal/testutil" "perms-system-server/internal/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func ptrStr(s string) *string { return &s } func insertTestUserForUpdate(t *testing.T, ctx context.Context, svcCtx *svc.ServiceContext, username, password string) (int64, func()) { t.Helper() conn := testutil.GetTestSqlConn() now := time.Now().Unix() hashed := testutil.HashPassword(password) res, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{ Username: username, Password: hashed, Nickname: "old_nick", Avatar: sql.NullString{String: "old_avatar.png", Valid: true}, Email: "old@test.com", Phone: "13800000000", Remark: "", DeptId: 0, IsSuperAdmin: 2, MustChangePassword: 2, Status: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) id, _ := res.LastInsertId() cleanup := func() { testutil.CleanTable(ctx, conn, "`sys_user`", id) } return id, cleanup } // TC-1230: 未登录 func TestUpdateSelfInfo_NotLoggedIn(t *testing.T) { svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) logic := NewUpdateSelfInfoLogic(context.Background(), svcCtx) err := logic.UpdateSelfInfo(&types.UpdateSelfInfoReq{Nickname: ptrStr("new")}) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 401, codeErr.Code()) assert.Contains(t, codeErr.Error(), "未登录") } // TC-1231: 所有字段为 nil func TestUpdateSelfInfo_AllFieldsNil(t *testing.T) { ctx := context.Background() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) username := testutil.UniqueId() userId, cleanUser := insertTestUserForUpdate(t, ctx, svcCtx, username, "Pass123456") t.Cleanup(cleanUser) logicCtx := middleware.WithUserDetails(ctx, &loaders.UserDetails{UserId: userId}) logic := NewUpdateSelfInfoLogic(logicCtx, svcCtx) err := logic.UpdateSelfInfo(&types.UpdateSelfInfoReq{}) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 400, codeErr.Code()) assert.Contains(t, codeErr.Error(), "至少需要修改一个字段") } // TC-1232: 正常更新 nickname func TestUpdateSelfInfo_UpdateNickname(t *testing.T) { ctx := context.Background() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) username := testutil.UniqueId() userId, cleanUser := insertTestUserForUpdate(t, ctx, svcCtx, username, "Pass123456") t.Cleanup(cleanUser) logicCtx := middleware.WithUserDetails(ctx, &loaders.UserDetails{UserId: userId}) logic := NewUpdateSelfInfoLogic(logicCtx, svcCtx) newNick := "new_nickname" err := logic.UpdateSelfInfo(&types.UpdateSelfInfoReq{Nickname: &newNick}) require.NoError(t, err) user, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, newNick, user.Nickname) } // TC-1233: 正常更新 avatar func TestUpdateSelfInfo_UpdateAvatar(t *testing.T) { ctx := context.Background() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) username := testutil.UniqueId() userId, cleanUser := insertTestUserForUpdate(t, ctx, svcCtx, username, "Pass123456") t.Cleanup(cleanUser) logicCtx := middleware.WithUserDetails(ctx, &loaders.UserDetails{UserId: userId}) logic := NewUpdateSelfInfoLogic(logicCtx, svcCtx) newAvatar := "https://example.com/new_avatar.png" err := logic.UpdateSelfInfo(&types.UpdateSelfInfoReq{Avatar: &newAvatar}) require.NoError(t, err) user, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, newAvatar, user.Avatar.String) } // TC-1234: 正常更新 email + phone func TestUpdateSelfInfo_UpdateEmailAndPhone(t *testing.T) { ctx := context.Background() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) username := testutil.UniqueId() userId, cleanUser := insertTestUserForUpdate(t, ctx, svcCtx, username, "Pass123456") t.Cleanup(cleanUser) logicCtx := middleware.WithUserDetails(ctx, &loaders.UserDetails{UserId: userId}) logic := NewUpdateSelfInfoLogic(logicCtx, svcCtx) newEmail := "new@example.com" newPhone := "13900000000" err := logic.UpdateSelfInfo(&types.UpdateSelfInfoReq{Email: &newEmail, Phone: &newPhone}) require.NoError(t, err) user, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, newEmail, user.Email) assert.Equal(t, newPhone, user.Phone) } // TC-1235: nickname 超过 64 字符 func TestUpdateSelfInfo_NicknameTooLong(t *testing.T) { ctx := context.Background() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) username := testutil.UniqueId() userId, cleanUser := insertTestUserForUpdate(t, ctx, svcCtx, username, "Pass123456") t.Cleanup(cleanUser) logicCtx := middleware.WithUserDetails(ctx, &loaders.UserDetails{UserId: userId}) logic := NewUpdateSelfInfoLogic(logicCtx, svcCtx) longNick := strings.Repeat("x", 65) err := logic.UpdateSelfInfo(&types.UpdateSelfInfoReq{Nickname: &longNick}) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 400, codeErr.Code()) assert.Contains(t, codeErr.Error(), "昵称长度不能超过64个字符") } // TC-1236: avatar 超过 255 字符 func TestUpdateSelfInfo_AvatarTooLong(t *testing.T) { ctx := context.Background() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) username := testutil.UniqueId() userId, cleanUser := insertTestUserForUpdate(t, ctx, svcCtx, username, "Pass123456") t.Cleanup(cleanUser) logicCtx := middleware.WithUserDetails(ctx, &loaders.UserDetails{UserId: userId}) logic := NewUpdateSelfInfoLogic(logicCtx, svcCtx) longAvatar := strings.Repeat("a", 256) err := logic.UpdateSelfInfo(&types.UpdateSelfInfoReq{Avatar: &longAvatar}) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 400, codeErr.Code()) assert.Contains(t, codeErr.Error(), "头像地址长度不能超过255个字符") } // TC-1237: email 超过 64 字符 func TestUpdateSelfInfo_EmailTooLong(t *testing.T) { ctx := context.Background() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) username := testutil.UniqueId() userId, cleanUser := insertTestUserForUpdate(t, ctx, svcCtx, username, "Pass123456") t.Cleanup(cleanUser) logicCtx := middleware.WithUserDetails(ctx, &loaders.UserDetails{UserId: userId}) logic := NewUpdateSelfInfoLogic(logicCtx, svcCtx) longEmail := strings.Repeat("e", 65) err := logic.UpdateSelfInfo(&types.UpdateSelfInfoReq{Email: &longEmail}) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 400, codeErr.Code()) assert.Contains(t, codeErr.Error(), "邮箱长度不能超过64个字符") } // TC-1238: phone 超过 32 字符 func TestUpdateSelfInfo_PhoneTooLong(t *testing.T) { ctx := context.Background() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) username := testutil.UniqueId() userId, cleanUser := insertTestUserForUpdate(t, ctx, svcCtx, username, "Pass123456") t.Cleanup(cleanUser) logicCtx := middleware.WithUserDetails(ctx, &loaders.UserDetails{UserId: userId}) logic := NewUpdateSelfInfoLogic(logicCtx, svcCtx) longPhone := strings.Repeat("1", 33) err := logic.UpdateSelfInfo(&types.UpdateSelfInfoReq{Phone: &longPhone}) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 400, codeErr.Code()) assert.Contains(t, codeErr.Error(), "手机号长度不能超过32个字符") } // TC-1239: 并发更新冲突 func TestUpdateSelfInfo_ConcurrentConflict(t *testing.T) { ctx := context.Background() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() // 使用过去的 updateTime(1 秒前),确保 UpdateSelfInfo 写入的 now > expectedUpdateTime pastTime := time.Now().Unix() - 1 hashed := testutil.HashPassword("Pass123456") res, err := conn.ExecCtx(ctx, "INSERT INTO `sys_user` (`username`,`password`,`nickname`,`avatar`,`email`,`phone`,`remark`,`deptId`,`isSuperAdmin`,`mustChangePassword`,`status`,`tokenVersion`,`createTime`,`updateTime`) VALUES (?,?,?,NULL,?,?,?,?,?,?,?,?,?,?)", username, hashed, "old_nick", "old@test.com", "13800000000", "", 0, 2, 2, 1, 0, pastTime, pastTime) require.NoError(t, err) userId, _ := res.LastInsertId() t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) // session A 读取快照(updateTime=pastTime) snapshotUpdateTime := pastTime // session A 先提交(成功,因为 WHERE updateTime=pastTime 匹配) err = svcCtx.SysUserModel.UpdateSelfInfo(ctx, userId, username, "sessionA", "", "old@test.com", "13800000000", snapshotUpdateTime) require.NoError(t, err) // session B 用相同旧 updateTime 提交(冲突,因为 session A 已将 updateTime 推进到 now) err = svcCtx.SysUserModel.UpdateSelfInfo(ctx, userId, username, "sessionB", "", "old@test.com", "13800000000", snapshotUpdateTime) require.Error(t, err) assert.True(t, errors.Is(err, userModel.ErrUpdateConflict)) } // TC-1240: 更新后 UserDetails 缓存失效 func TestUpdateSelfInfo_CacheInvalidation(t *testing.T) { ctx := context.Background() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) username := testutil.UniqueId() userId, cleanUser := insertTestUserForUpdate(t, ctx, svcCtx, username, "Pass123456") t.Cleanup(cleanUser) // 预热缓存 ud, err := svcCtx.UserDetailsLoader.Load(ctx, userId, "") require.NoError(t, err) assert.Equal(t, "old_nick", ud.Nickname) // 执行更新 logicCtx := middleware.WithUserDetails(ctx, &loaders.UserDetails{UserId: userId}) logic := NewUpdateSelfInfoLogic(logicCtx, svcCtx) newNick := "cache_test_nick" err = logic.UpdateSelfInfo(&types.UpdateSelfInfoReq{Nickname: &newNick}) require.NoError(t, err) // 再次 Load 应读到新值 ud2, err := svcCtx.UserDetailsLoader.Load(ctx, userId, "") require.NoError(t, err) assert.Equal(t, newNick, ud2.Nickname) }