test(auth): add SmartAuth integration tests + fix auth.routes.test mocks
- Add 5 new tests: MFA challenge integration, risk scorer edge cases, login events wiring, device trust pure function - Fix auth.routes.test.ts: add vi.mock stubs for login-events, risk-scorer, mfa, devices, config, event-bus - Change afterEach from restoreAllMocks to clearAllMocks (preserves mock implementations between tests) - Total: 42 smartauth tests, 951 platform-service tests all passing
This commit is contained in:
parent
0f4be0c325
commit
a613cf1bf9
@ -37,6 +37,43 @@ vi.mock('./jwt.js', () => jwtMock);
|
||||
vi.mock('../products/cache.js', () => productCacheMock);
|
||||
vi.mock('../subscriptions/repository.js', () => subscriptionRepoMock);
|
||||
vi.mock('../licenses/repository.js', () => licenseRepoMock);
|
||||
vi.mock('./login-events/repository.js', () => ({
|
||||
record: vi.fn().mockResolvedValue(undefined),
|
||||
countRecentFailures: vi.fn().mockResolvedValue(0),
|
||||
}));
|
||||
vi.mock('./login-events/risk-scorer.js', () => ({
|
||||
scoreLoginRisk: vi.fn().mockReturnValue({ score: 0, level: 'low', flags: [] }),
|
||||
}));
|
||||
vi.mock('./mfa/repository.js', () => ({
|
||||
getByUserId: vi.fn().mockResolvedValue(null),
|
||||
}));
|
||||
vi.mock('./mfa/challenge-store.js', () => ({
|
||||
createChallenge: vi.fn().mockReturnValue('mfa_test'),
|
||||
}));
|
||||
vi.mock('./devices/repository.js', () => ({
|
||||
getByFingerprint: vi.fn().mockResolvedValue(null),
|
||||
isDeviceTrusted: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
vi.mock('../../lib/config.js', () => ({
|
||||
config: {
|
||||
PORT: 4003,
|
||||
HOST: '0.0.0.0',
|
||||
NODE_ENV: 'test',
|
||||
SERVICE_NAME: 'platform-service',
|
||||
COSMOS_ENDPOINT: 'https://fake.documents.azure.com:443/',
|
||||
COSMOS_KEY: 'dGVzdA==',
|
||||
COSMOS_DATABASE: 'test',
|
||||
JWT_SECRET: 'test-secret-at-least-32-chars-long!!',
|
||||
MICROSOFT_TENANT_ID: 'common',
|
||||
WEBAUTHN_RP_ID: 'localhost',
|
||||
WEBAUTHN_RP_NAME: 'Test',
|
||||
USAGE_WARN_THRESHOLD: 0.8,
|
||||
BACKEND_URL: 'http://localhost:8000',
|
||||
},
|
||||
}));
|
||||
vi.mock('../../lib/event-bus.js', () => ({
|
||||
bus: { emit: vi.fn().mockResolvedValue(undefined) },
|
||||
}));
|
||||
|
||||
describe('authRoutes register provisioning', () => {
|
||||
beforeEach(() => {
|
||||
@ -70,7 +107,7 @@ describe('authRoutes register provisioning', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('provisions subscription/license from product defaults during register', async () => {
|
||||
|
||||
@ -463,6 +463,109 @@ describe('Login event types', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Login flow integration tests ─────────────────────────────
|
||||
|
||||
describe('Login flow — MFA challenge integration', () => {
|
||||
it('createChallenge from mfa/challenge-store returns mfa_ prefixed token', async () => {
|
||||
const { createChallenge, consumeChallenge, _clearChallenges } =
|
||||
await import('./mfa/challenge-store.js');
|
||||
_clearChallenges();
|
||||
const token = createChallenge({
|
||||
userId: 'usr_mfa',
|
||||
productId: 'lysnrai',
|
||||
email: 'mfa@test.com',
|
||||
role: 'user',
|
||||
plan: 'pro',
|
||||
});
|
||||
expect(token).toMatch(/^mfa_/);
|
||||
const data = consumeChallenge(token);
|
||||
expect(data?.plan).toBe('pro');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Risk scorer — edge cases', () => {
|
||||
it('returns low for OAuth on known device', async () => {
|
||||
const { scoreLoginRisk } = await import('./login-events/risk-scorer.js');
|
||||
const result = scoreLoginRisk({
|
||||
ip: '10.0.0.1',
|
||||
isNewIp: false,
|
||||
isNewDevice: false,
|
||||
isDeviceTrusted: true,
|
||||
recentFailures: 0,
|
||||
method: 'oauth_google',
|
||||
hourOfDay: 14,
|
||||
});
|
||||
expect(result.score).toBe(0);
|
||||
expect(result.level).toBe('low');
|
||||
});
|
||||
|
||||
it('password on new device at 3am scores higher than OAuth same conditions', async () => {
|
||||
const { scoreLoginRisk } = await import('./login-events/risk-scorer.js');
|
||||
const passwordResult = scoreLoginRisk({
|
||||
ip: '5.5.5.5',
|
||||
isNewIp: true,
|
||||
isNewDevice: true,
|
||||
isDeviceTrusted: false,
|
||||
recentFailures: 0,
|
||||
method: 'password',
|
||||
hourOfDay: 3,
|
||||
});
|
||||
const oauthResult = scoreLoginRisk({
|
||||
ip: '5.5.5.5',
|
||||
isNewIp: true,
|
||||
isNewDevice: true,
|
||||
isDeviceTrusted: false,
|
||||
recentFailures: 0,
|
||||
method: 'oauth_google',
|
||||
hourOfDay: 3,
|
||||
});
|
||||
expect(passwordResult.score).toBeGreaterThan(oauthResult.score);
|
||||
expect(passwordResult.flags).toContain('password_new_device');
|
||||
expect(oauthResult.flags).not.toContain('password_new_device');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Login events wiring exports', () => {
|
||||
it('login-events/repository exports record + listByUser + countRecentFailures', async () => {
|
||||
const repo = await import('./login-events/repository.js');
|
||||
expect(typeof repo.record).toBe('function');
|
||||
expect(typeof repo.listByUser).toBe('function');
|
||||
expect(typeof repo.countRecentFailures).toBe('function');
|
||||
});
|
||||
|
||||
it('devices/repository exports isDeviceTrusted as pure function', async () => {
|
||||
const repo = await import('./devices/repository.js');
|
||||
// isDeviceTrusted is a sync function that works without DB
|
||||
expect(repo.isDeviceTrusted(null)).toBe(false);
|
||||
expect(
|
||||
repo.isDeviceTrusted({
|
||||
id: 'dev_x',
|
||||
userId: 'u',
|
||||
productId: 'smartauth',
|
||||
fingerprint: 'x',
|
||||
trustLevel: 'trusted',
|
||||
deviceInfo: {},
|
||||
trustExpiresAt: new Date(Date.now() + 86400000).toISOString(),
|
||||
createdAt: '2026-01-01T00:00:00Z',
|
||||
lastSeenAt: '2026-01-01T00:00:00Z',
|
||||
})
|
||||
).toBe(true);
|
||||
expect(
|
||||
repo.isDeviceTrusted({
|
||||
id: 'dev_x',
|
||||
userId: 'u',
|
||||
productId: 'smartauth',
|
||||
fingerprint: 'x',
|
||||
trustLevel: 'trusted',
|
||||
deviceInfo: {},
|
||||
trustExpiresAt: new Date(Date.now() - 86400000).toISOString(),
|
||||
createdAt: '2026-01-01T00:00:00Z',
|
||||
lastSeenAt: '2026-01-01T00:00:00Z',
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── SmartAuth event types in @bytelyst/events ───────────────
|
||||
|
||||
describe('SmartAuth event schemas', () => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user