fix profile activity and migration remediation
This commit is contained in:
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user