Files
support_backend/tests/unit/identity/login-failure-parity.test.ts
T
saqib mirandClaude Sonnet 5 40687f68fa feat(010-identity-auth): real staff login, session verification, and role gating
Replaces the no-op fastify.authenticate stub and the never-implemented
identity/auth login with real bcrypt password verification, JWT session
issuance/verification (reusing the existing JWT_SECRET), and a Redis-backed
revocation denylist for logout. Adds requireRole('ADMIN') to admin-only
configuration writes across 002-009 that previously relied on a decorator
that never actually checked anything. Adds self-identity (GET /auth/me,
re-validated against live account state) and admin-provisioned accounts
(POST /admin/users).

Making the auth check genuinely reject invalid/missing tokens exposed that
~18 pre-existing integration test files called already-gated routes with no
Authorization header (safe against the old no-op stub, broken against a real
one) — fixed via a shared tests/helpers/auth.ts (loginAs/authHeader) and a
file-by-file pass, plus two related SLA-run cleanup races exposed once admin
setup calls in those files' own beforeAll blocks started actually succeeding.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 12:45:37 +05:30

63 lines
2.1 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest';
import bcrypt from 'bcryptjs';
import { AuthService } from '@/modules/identity/auth/service/auth.service';
const REAL_PASSWORD_HASH = bcrypt.hashSync('the-real-password', 10);
function fakeUser(overrides: Partial<Record<string, unknown>> = {}) {
return {
id: 'u1',
email: 'agent@example.com',
name: 'Agent',
role: 'AGENT',
passwordHash: REAL_PASSWORD_HASH,
active: true,
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
};
}
describe('AuthService.login failure parity', () => {
it('throws the identical error for a nonexistent email and a wrong password', async () => {
const repoFoundUser = { findByEmail: vi.fn().mockResolvedValue(fakeUser()) } as never;
const repoNoUser = { findByEmail: vi.fn().mockResolvedValue(null) } as never;
const serviceWithUser = new AuthService(repoFoundUser);
const serviceWithoutUser = new AuthService(repoNoUser);
let errorWithUser: Error | undefined;
let errorWithoutUser: Error | undefined;
try {
await serviceWithUser.login({ email: 'agent@example.com', password: 'definitely-wrong' });
} catch (e) {
errorWithUser = e as Error;
}
try {
await serviceWithoutUser.login({ email: 'nobody@example.com', password: 'anything' });
} catch (e) {
errorWithoutUser = e as Error;
}
expect(errorWithUser).toBeDefined();
expect(errorWithoutUser).toBeDefined();
expect(errorWithUser?.message).toBe(errorWithoutUser?.message);
expect((errorWithUser as { statusCode?: number })?.statusCode).toBe(
(errorWithoutUser as { statusCode?: number })?.statusCode,
);
});
it('rejects a deactivated account with the same error, never a distinguishable one', async () => {
const repo = {
findByEmail: vi.fn().mockResolvedValue(fakeUser({ active: false })),
} as never;
const service = new AuthService(repo);
await expect(
service.login({ email: 'agent@example.com', password: 'anything' }),
).rejects.toMatchObject({ statusCode: 401 });
});
});