diff --git a/web/src/lib/services/user-admin.service.spec.ts b/web/src/lib/services/user-admin.service.spec.ts new file mode 100644 index 000000000..d7e284928 --- /dev/null +++ b/web/src/lib/services/user-admin.service.spec.ts @@ -0,0 +1,53 @@ +import { UserStatus, type UserAdminResponseDto } from '@immich/sdk'; +import type { MessageFormatter } from 'svelte-i18n'; +import { getUserAdminActions } from '$lib/services/user-admin.service'; + +vi.mock('$app/navigation', () => ({ goto: vi.fn() })); +vi.mock('@immich/ui', () => ({ modalManager: { show: vi.fn(), showDialog: vi.fn() } })); +vi.mock('$lib/managers/auth-manager.svelte', () => ({ authManager: { user: { id: 'self' } } })); +vi.mock('$lib/managers/server-config-manager.svelte', () => ({ + serverConfigManager: { value: { userDeleteDelay: 7 } }, +})); +vi.mock('$lib/modals/PasswordResetSuccessModal.svelte', () => ({ default: {} })); +vi.mock('$lib/modals/UserDeleteConfirmModal.svelte', () => ({ default: {} })); +vi.mock('$lib/modals/UserRestoreConfirmModal.svelte', () => ({ default: {} })); +vi.mock('$lib/queries/client', () => ({ executeMutation: vi.fn() })); +vi.mock('$lib/queries/user-admin.query', () => ({ updateUserAdminMutation: vi.fn() })); +vi.mock('$lib/route', () => ({ Route: { newUser: vi.fn(), viewUser: vi.fn(), editUser: vi.fn() } })); +vi.mock('$lib/utils/i18n', () => ({ getFormatter: vi.fn() })); + +describe('admin actions for active and deleted accounts', () => { + it('does not format a deletion date for an active account', () => { + const format = vi.fn((key: string) => key); + const user = { id: 'other', deletedAt: null } as UserAdminResponseDto; + const actions = getUserAdminActions(format as MessageFormatter, user); + expect(actions.Restore.data).toBeUndefined(); + expect((actions.Restore.$if as () => boolean)()).toBe(false); + expect(format).not.toHaveBeenCalledWith('admin.user_restore_scheduled_removal', expect.anything()); + expect((actions.Update.$if as () => boolean)()).toBe(true); + expect((actions.ResetPassword.$if as () => boolean)()).toBe(true); + }); + + it('offers restoration with a valid date and hides edits for a deleted account', () => { + const format = vi.fn((key: string, _options?: unknown) => key); + const user = { id: 'other', deletedAt: '2026-10-17T12:00:00Z', status: UserStatus.Deleted } as UserAdminResponseDto; + const actions = getUserAdminActions(format as MessageFormatter, user); + expect((actions.Restore.$if as () => boolean)()).toBe(true); + expect(format).toHaveBeenCalledWith('admin.user_restore_scheduled_removal', { + values: { date: new Date('2026-10-24T12:00:00Z') }, + }); + for (const action of [actions.Update, actions.Delete, actions.ResetPassword, actions.ResetPinCode]) { + expect((action.$if as () => boolean)()).toBe(false); + } + }); + + it('keeps destructive account actions hidden for the current administrator', () => { + const format = vi.fn((key: string) => key); + const actions = getUserAdminActions( + format as MessageFormatter, + { id: 'self', deletedAt: null } as UserAdminResponseDto, + ); + expect((actions.Delete.$if as () => boolean)()).toBe(false); + expect((actions.ResetPassword.$if as () => boolean)()).toBe(false); + }); +}); diff --git a/web/src/lib/services/user-admin.service.ts b/web/src/lib/services/user-admin.service.ts index 70c471aaa..6883eb7b8 100644 --- a/web/src/lib/services/user-admin.service.ts +++ b/web/src/lib/services/user-admin.service.ts @@ -65,9 +65,9 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons icon: mdiDeleteRestore, title: $t('restore'), color: 'primary', - data: { - title: $t('admin.user_restore_scheduled_removal', { values: { date: getDeleteDate(user.deletedAt!) } }), - }, + data: user.deletedAt + ? { title: $t('admin.user_restore_scheduled_removal', { values: { date: getDeleteDate(user.deletedAt) } }) } + : undefined, $if: () => !!user.deletedAt && user.status === UserStatus.Deleted, onAction: () => modalManager.show(UserRestoreConfirmModal, { user }), };