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:
saravanakumardb1 2026-03-12 11:27:50 -07:00
parent 0f4be0c325
commit a613cf1bf9
2 changed files with 141 additions and 1 deletions

View File

@ -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 () => {

View File

@ -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', () => {