fix profile activity and migration remediation

This commit is contained in:
IanShaw027
2026-04-21 02:08:56 +08:00
parent a27a7add3d
commit ebe7524415
12 changed files with 703 additions and 28 deletions

View File

@@ -31,13 +31,26 @@ type mockUserRepo struct {
deleteAvatarFn func(ctx context.Context, userID int64) error
deleteAvatarIDs []int64
getAvatarFn func(ctx context.Context, userID int64) (*UserAvatar, error)
txCalls int
}
type mockUserRepoTxKey struct{}
type mockUserRepoTxState struct {
getByIDUser *User
upsertAvatarArgs []UpsertUserAvatarInput
deleteAvatarIDs []int64
}
func (m *mockUserRepo) Create(context.Context, *User) error { return nil }
func (m *mockUserRepo) GetByID(context.Context, int64) (*User, error) {
func (m *mockUserRepo) GetByID(ctx context.Context, _ int64) (*User, error) {
if m.getByIDErr != nil {
return nil, m.getByIDErr
}
if txState, _ := ctx.Value(mockUserRepoTxKey{}).(*mockUserRepoTxState); txState != nil && txState.getByIDUser != nil {
cloned := *txState.getByIDUser
return &cloned, nil
}
if m.getByIDUser != nil {
cloned := *m.getByIDUser
return &cloned, nil
@@ -61,6 +74,27 @@ func (m *mockUserRepo) GetUserAvatar(ctx context.Context, userID int64) (*UserAv
return nil, nil
}
func (m *mockUserRepo) UpsertUserAvatar(ctx context.Context, userID int64, input UpsertUserAvatarInput) (*UserAvatar, error) {
if txState, _ := ctx.Value(mockUserRepoTxKey{}).(*mockUserRepoTxState); txState != nil {
txState.upsertAvatarArgs = append(txState.upsertAvatarArgs, input)
if txState.getByIDUser != nil {
txState.getByIDUser.AvatarURL = input.URL
txState.getByIDUser.AvatarSource = input.StorageProvider
txState.getByIDUser.AvatarMIME = input.ContentType
txState.getByIDUser.AvatarByteSize = input.ByteSize
txState.getByIDUser.AvatarSHA256 = input.SHA256
}
if m.upsertAvatarFn != nil {
return m.upsertAvatarFn(ctx, userID, input)
}
return &UserAvatar{
StorageProvider: input.StorageProvider,
StorageKey: input.StorageKey,
URL: input.URL,
ContentType: input.ContentType,
ByteSize: input.ByteSize,
SHA256: input.SHA256,
}, nil
}
m.upsertAvatarArgs = append(m.upsertAvatarArgs, input)
if m.upsertAvatarFn != nil {
return m.upsertAvatarFn(ctx, userID, input)
@@ -75,6 +109,20 @@ func (m *mockUserRepo) UpsertUserAvatar(ctx context.Context, userID int64, input
}, nil
}
func (m *mockUserRepo) DeleteUserAvatar(ctx context.Context, userID int64) error {
if txState, _ := ctx.Value(mockUserRepoTxKey{}).(*mockUserRepoTxState); txState != nil {
txState.deleteAvatarIDs = append(txState.deleteAvatarIDs, userID)
if txState.getByIDUser != nil {
txState.getByIDUser.AvatarURL = ""
txState.getByIDUser.AvatarSource = ""
txState.getByIDUser.AvatarMIME = ""
txState.getByIDUser.AvatarByteSize = 0
txState.getByIDUser.AvatarSHA256 = ""
}
if m.deleteAvatarFn != nil {
return m.deleteAvatarFn(ctx, userID)
}
return nil
}
m.deleteAvatarIDs = append(m.deleteAvatarIDs, userID)
if m.deleteAvatarFn != nil {
return m.deleteAvatarFn(ctx, userID)
@@ -116,6 +164,26 @@ func (m *mockUserRepo) RemoveGroupFromUserAllowedGroups(context.Context, int64,
return nil
}
func (m *mockUserRepo) WithUserProfileIdentityTx(ctx context.Context, fn func(txCtx context.Context) error) error {
m.txCalls++
txState := &mockUserRepoTxState{
upsertAvatarArgs: append([]UpsertUserAvatarInput(nil), m.upsertAvatarArgs...),
deleteAvatarIDs: append([]int64(nil), m.deleteAvatarIDs...),
}
if m.getByIDUser != nil {
userCopy := *m.getByIDUser
txState.getByIDUser = &userCopy
}
err := fn(context.WithValue(ctx, mockUserRepoTxKey{}, txState))
if err != nil {
return err
}
m.getByIDUser = txState.getByIDUser
m.upsertAvatarArgs = txState.upsertAvatarArgs
m.deleteAvatarIDs = txState.deleteAvatarIDs
return nil
}
// --- mock: APIKeyAuthCacheInvalidator ---
type mockAuthCacheInvalidator struct {
@@ -360,6 +428,33 @@ func TestUpdateProfile_DeletesAvatarOnEmptyString(t *testing.T) {
require.Empty(t, updated.AvatarSource)
}
func TestUpdateProfile_RollsBackAvatarMutationWhenUserUpdateFails(t *testing.T) {
repo := &mockUserRepo{
getByIDUser: &User{
ID: 11,
Email: "rollback@example.com",
AvatarURL: "https://cdn.example.com/original.png",
AvatarSource: "remote_url",
},
updateFn: func(context.Context, *User) error {
return errors.New("write user failed")
},
}
svc := NewUserService(repo, nil, nil, nil)
remoteURL := "https://cdn.example.com/new.png"
_, err := svc.UpdateProfile(context.Background(), 11, UpdateProfileRequest{
AvatarURL: &remoteURL,
})
require.EqualError(t, err, "update user: write user failed")
require.Equal(t, 1, repo.txCalls)
require.Empty(t, repo.upsertAvatarArgs)
require.Empty(t, repo.deleteAvatarIDs)
require.Equal(t, "https://cdn.example.com/original.png", repo.getByIDUser.AvatarURL)
require.Equal(t, "remote_url", repo.getByIDUser.AvatarSource)
}
func TestGetProfile_HydratesAvatarFromRepository(t *testing.T) {
repo := &mockUserRepo{
getByIDUser: &User{