refactor(auth): auth, passkey, profile, token and admin services refuse with DomainError instead of returning error envelopes

This commit is contained in:
Maurice
2026-10-10 15:12:44 +02:00
parent 66042ae1c7
commit e81efcbc57
24 changed files with 460 additions and 516 deletions
@@ -1,3 +1,4 @@
import { DomainError } from '../../../src/nest/common/domain-error';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { HttpException, NotFoundException } from '@nestjs/common';
import type { Request } from 'express';
@@ -88,7 +89,7 @@ afterEach(() => { delete process.env.NODE_ENV; });
describe('AdminController users', () => {
it('lists, creates (201 + audit), maps an error', async () => {
expect(await adminCtl(svc({ listUsers: vi.fn().mockReturnValue([{ id: 1 }]) } as Partial<AdminService>)).listUsers()).toEqual({ users: [{ id: 1 }] });
expect(await thrown(() => adminCtl(svc({ createUser: vi.fn().mockReturnValue({ error: 'Email taken', status: 409 }) } as Partial<AdminService>)).createUser(user, {}, req))).toEqual({ status: 409, body: { error: 'Email taken' } });
expect(await thrown(() => adminCtl(svc({ createUser: vi.fn().mockRejectedValue(new DomainError(409, 'Email taken')) } as Partial<AdminService>)).createUser(user, {}, req))).toEqual({ status: 409, body: { error: 'Email taken' } });
const c = adminCtl(svc({ createUser: vi.fn().mockReturnValue({ user: { id: 2 }, insertedId: 2, auditDetails: {} }) } as Partial<AdminService>));
expect(await c.createUser(user, { email: 'a@b.c' }, req)).toEqual({ user: { id: 2 } });
expect(writeAudit).toHaveBeenCalledWith(expect.objectContaining({ action: 'admin.user_create' }));
@@ -96,7 +97,7 @@ describe('AdminController users', () => {
it('update + delete audit and map errors', async () => {
expect(await adminCtl(svc({ updateUser: vi.fn().mockReturnValue({ user: { id: 2 }, previousEmail: 'a@b.c', changed: ['role'] }) } as Partial<AdminService>)).updateUser(user, '2', {}, req)).toEqual({ user: { id: 2 } });
expect(await thrown(() => adminCtl(svc({ deleteUser: vi.fn().mockReturnValue({ error: 'Cannot delete self', status: 400 }) } as Partial<AdminService>)).deleteUser(user, '1', req))).toEqual({ status: 400, body: { error: 'Cannot delete self' } });
expect(await thrown(() => adminCtl(svc({ deleteUser: vi.fn().mockRejectedValue(new DomainError(400, 'Cannot delete self')) } as Partial<AdminService>)).deleteUser(user, '1', req))).toEqual({ status: 400, body: { error: 'Cannot delete self' } });
expect(await adminCtl(svc({ deleteUser: vi.fn().mockReturnValue({ email: 'a@b.c' }) } as Partial<AdminService>)).deleteUser(user, '2', req)).toEqual({ success: true });
});
});
@@ -114,7 +115,7 @@ describe('AdminController permissions + oidc + misc', () => {
it('save-demo-baseline maps error, else returns message', async () => {
expect(await thrown(() => adminCtl(svc({ saveDemoBaseline: vi.fn().mockReturnValue({ error: 'not demo', status: 400 }) } as Partial<AdminService>)).saveDemoBaseline(user, req))).toEqual({ status: 400, body: { error: 'not demo' } });
expect(await thrown(() => adminCtl(svc({ saveDemoBaseline: vi.fn().mockRejectedValue(new DomainError(400, 'not demo')) } as Partial<AdminService>)).saveDemoBaseline(user, req))).toEqual({ status: 400, body: { error: 'not demo' } });
expect(await adminCtl(svc({ saveDemoBaseline: vi.fn().mockReturnValue({ message: 'saved' }) } as Partial<AdminService>)).saveDemoBaseline(user, req)).toEqual({ success: true, message: 'saved' });
});
});
@@ -127,7 +128,7 @@ describe('AdminController invites', () => {
const c = adminCtl(svc(), undefined, undefined, {}, { createInvite: vi.fn().mockReturnValue({ invite: { id: 5 }, inviteId: 5, uses: 1, expiresInDays: 7 }) });
expect(await c.createInvite(user, {}, req)).toEqual({ invite: { id: 5 } });
expect(writeAudit).toHaveBeenCalledWith(expect.objectContaining({ action: 'admin.invite_create' }));
expect(await thrown(() => adminCtl(svc(), undefined, undefined, {}, { deleteInvite: vi.fn().mockReturnValue({ error: 'not found', status: 404 }) }).deleteInvite(user, '5', req))).toEqual({ status: 404, body: { error: 'not found' } });
expect(await thrown(() => adminCtl(svc(), undefined, undefined, {}, { deleteInvite: vi.fn().mockRejectedValue(new DomainError(404, 'not found')) }).deleteInvite(user, '5', req))).toEqual({ status: 404, body: { error: 'not found' } });
});
@@ -157,7 +158,7 @@ describe('AdminController addons + sessions + jwt + defaults', () => {
});
it('addon update maps the journey-dependency refusal to its status, before any audit', async () => {
const c = adminCtl(svc({ updateAddon: vi.fn().mockReturnValue({ error: 'Enable the Journey addon first', status: 409 }) } as Partial<AdminService>));
const c = adminCtl(svc({ updateAddon: vi.fn().mockRejectedValue(new DomainError(409, 'Enable the Journey addon first')) } as Partial<AdminService>));
const err = await c.updateAddon(user, 'immich', { enabled: true }, req).then(
() => null,
(e: unknown) => e as HttpException,
@@ -170,20 +171,12 @@ describe('AdminController addons + sessions + jwt + defaults', () => {
it('oauth-sessions revoke audits; rotate-jwt maps error', async () => {
expect(await adminCtl(svc(), undefined, undefined, {}, {}, { adminRevokeOAuthSession: vi.fn().mockReturnValue({}) }).revokeOAuthSession(user, '3', req)).toEqual({ success: true });
expect(await thrown(() => adminCtl(svc({ rotateJwtSecret: vi.fn().mockReturnValue({ error: 'locked', status: 409 }) } as Partial<AdminService>)).rotateJwtSecret(user, req))).toEqual({ status: 409, body: { error: 'locked' } });
expect(await thrown(() => adminCtl(svc({ rotateJwtSecret: vi.fn(() => { throw new DomainError(409, 'locked'); }) } as Partial<AdminService>)).rotateJwtSecret(user, req))).toEqual({ status: 409, body: { error: 'locked' } });
expect(await adminCtl(svc({ rotateJwtSecret: vi.fn().mockReturnValue({}) } as Partial<AdminService>)).rotateJwtSecret(user, req)).toEqual({ success: true });
});
});
describe('AdminController error envelope fallbacks', () => {
it('ok() defaults to 400 when the error envelope omits a status', async () => {
expect(await thrown(() => adminCtl(svc({ createUser: vi.fn().mockReturnValue({ error: 'boom' }) } as Partial<AdminService>)).createUser(user, {}, req))).toEqual({ status: 400, body: { error: 'boom' } });
});
});
describe('AdminController read-only getters', () => {
it('return service values verbatim', async () => {
expect(await adminCtl(svc({ resetUserPasskeys: vi.fn().mockReturnValue({ email: 'a@b.c', deleted: 2 }) } as Partial<AdminService>)).resetUserPasskeys(user, '4', req)).toEqual({ success: true, deleted: 2 });
@@ -213,8 +206,8 @@ describe('AdminController read-only getters', () => {
describe('AdminController tokens + sessions', () => {
it('mcp token + oauth session deletes return success and map errors', async () => {
expect(await adminCtl(svc(), undefined, undefined, { adminDeleteMcpToken: vi.fn().mockReturnValue({}) }).deleteMcpToken(user, '2', req)).toEqual({ success: true });
expect(await thrown(() => adminCtl(svc(), undefined, undefined, { adminDeleteMcpToken: vi.fn().mockReturnValue({ error: 'no token', status: 404 }) }).deleteMcpToken(user, '9', req))).toEqual({ status: 404, body: { error: 'no token' } });
expect(await thrown(() => adminCtl(svc(), undefined, undefined, {}, {}, { adminRevokeOAuthSession: vi.fn().mockReturnValue({ error: 'no session', status: 404 }) }).revokeOAuthSession(user, '9', req))).toEqual({ status: 404, body: { error: 'no session' } });
expect(await thrown(() => adminCtl(svc(), undefined, undefined, { adminDeleteMcpToken: vi.fn().mockRejectedValue(new DomainError(404, 'no token')) }).deleteMcpToken(user, '9', req))).toEqual({ status: 404, body: { error: 'no token' } });
expect(await thrown(() => adminCtl(svc(), undefined, undefined, {}, {}, { adminRevokeOAuthSession: vi.fn().mockRejectedValue(new DomainError(404, 'no session')) }).revokeOAuthSession(user, '9', req))).toEqual({ status: 404, body: { error: 'no session' } });
});
});