| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287 |
- 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: "[email protected]",
- 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 := "[email protected]"
- 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", "[email protected]", "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", "", "[email protected]", "13800000000", snapshotUpdateTime)
- require.NoError(t, err)
- // session B 用相同旧 updateTime 提交(冲突,因为 session A 已将 updateTime 推进到 now)
- err = svcCtx.SysUserModel.UpdateSelfInfo(ctx, userId, username, "sessionB", "", "[email protected]", "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)
- }
|