Skip to content

Commit 22b5fd4

Browse files
committed
feat: full end to end test coverage for happy auth pattern
1 parent 62552c9 commit 22b5fd4

8 files changed

Lines changed: 178 additions & 38 deletions

File tree

src/controllers/authentication.ts

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -227,23 +227,12 @@ export const refreshSession = async (req: Request, res: Response) => {
227227
const authUser = authReq.user;
228228
logger.info(`Refreshing user token`);
229229

230-
let refreshToken;
230+
let refreshToken: string | null = null;
231231

232-
refreshToken = req.headers['authorization']?.toString().startsWith('Bearer ')
233-
? req.headers['authorization']!.slice('Bearer '.length)
234-
: null;
235-
236-
if (!refreshToken) {
237-
return res.status(401).json('Not allowed');
232+
if (req.headers.authorization?.startsWith('Bearer ')) {
233+
refreshToken = req.headers.authorization.slice('Bearer '.length);
238234
}
239235

240-
const serviceSecret = await getSecret('API_SERVICE_TOKEN');
241-
242-
const payload = jwt.verify(refreshToken, serviceSecret, {
243-
issuer: process.env.APP_ORIGIN,
244-
audience: process.env.ISSUER,
245-
}) as jwt.JwtPayload;
246-
247236
if (!refreshToken) {
248237
logger.error('Refresh token provided is not of expected type for auth server configurations');
249238
await AuthEventService.log({
@@ -252,10 +241,17 @@ export const refreshSession = async (req: Request, res: Response) => {
252241
req,
253242
metadata: { reason: 'Missing all required headers and tokens needed to perform a refresh' },
254243
});
255-
res.status(401).json({ error: 'Missing refresh token parameters' });
244+
res.status(401).json({ error: 'Not allowed' });
256245
return;
257246
}
258247

248+
const serviceSecret = await getSecret('API_SERVICE_TOKEN');
249+
250+
const payload = jwt.verify(refreshToken, serviceSecret, {
251+
issuer: process.env.APP_ORIGIN,
252+
audience: process.env.ISSUER,
253+
}) as jwt.JwtPayload;
254+
259255
const now = new Date();
260256

261257
// Find session that is not revoked, not replaced, and not expired

src/controllers/otp.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -241,11 +241,11 @@ export const verifyPhoneNumber = async (req: Request, res: Response) => {
241241

242242
if (token && refreshToken) {
243243
if (AUTH_MODE === 'web') {
244-
await setAuthCookies(res, { accessToken: token, refreshToken: refreshTokenHash });
244+
await setAuthCookies(res, { accessToken: token, refreshToken });
245245
return res.status(200).json({ message: 'Success' });
246246
}
247247

248-
return res.status(200).json({ message: 'Success', token, refreshTokenHash });
248+
return res.status(200).json({ message: 'Success', token, refreshToken });
249249
}
250250
res.json({ message: 'Success' });
251251
} else {
@@ -352,11 +352,11 @@ export const verifyEmail = async (req: Request, res: Response) => {
352352

353353
if (token && refreshToken) {
354354
if (AUTH_MODE === 'web') {
355-
await setAuthCookies(res, { accessToken: token, refreshToken: refreshTokenHash });
355+
await setAuthCookies(res, { accessToken: token, refreshToken });
356356
return res.status(200).json({ message: 'Success' });
357357
}
358358

359-
return res.status(200).json({ message: 'Success', token, refreshTokenHash });
359+
return res.status(200).json({ message: 'Success', token, refreshToken });
360360
}
361361
return res.json({ message: 'Success' });
362362
} else {
@@ -453,11 +453,11 @@ export const verifyLoginPhoneNumber = async (req: Request, res: Response) => {
453453
logger.warn(`An error occured saving user last login - ${error}`);
454454
}
455455
if (AUTH_MODE === 'web') {
456-
await setAuthCookies(res, { accessToken: token, refreshToken: refreshTokenHash });
456+
await setAuthCookies(res, { accessToken: token, refreshToken });
457457
return res.status(200).json({ message: 'Success' });
458458
}
459459

460-
return res.status(200).json({ message: 'Success', token, refreshTokenHash });
460+
return res.status(200).json({ message: 'Success', token, refreshToken });
461461
}
462462
return res.json({ message: 'Success' });
463463
} else {
@@ -575,11 +575,11 @@ export const verifyLoginEmail = async (req: Request, res: Response) => {
575575
logger.warn(`An error occured saving user last login - ${error}`);
576576
}
577577
if (AUTH_MODE === 'web') {
578-
await setAuthCookies(res, { accessToken: token, refreshToken: refreshTokenHash });
578+
await setAuthCookies(res, { accessToken: token, refreshToken });
579579
return res.status(200).json({ message: 'Success' });
580580
}
581581

582-
return res.status(200).json({ message: 'Success', token, refreshTokenHash });
582+
return res.status(200).json({ message: 'Success', token, refreshToken });
583583
}
584584
return res.json({ message: 'Success' });
585585
} else {

src/controllers/sessions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ export const listSessions = async (req: Request, res: Response) => {
3333
deviceName: session.deviceName,
3434
ipAddress: session.ipAddress,
3535
userAgent: session.userAgent,
36-
lastUsedAt: session.lastUsedAt,
37-
expiresAt: session.expiresAt,
36+
lastUsedAt: session.lastUsedAt.toISOString(),
37+
expiresAt: session.expiresAt.toISOString(),
3838
current: session.id === currentSessionId,
3939
}));
4040

src/controllers/webauthn.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ const verifyWebAuthn = async (req: Request, res: Response) => {
513513
clearAuthCookies(res);
514514

515515
if (AUTH_MODE === 'web') {
516-
await setAuthCookies(res, { accessToken: token, refreshToken: refreshToken });
516+
await setAuthCookies(res, { accessToken: token, refreshToken });
517517
res.status(200).json({ message: 'Success' });
518518
return;
519519
}

src/models/index.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,6 @@ export function getSequelize(): Sequelize {
4949
return sequelizeInstance;
5050
}
5151

52-
if (process.env.NODE_ENV === 'test' && testDbMode === 'mock') {
53-
logger.warn('TEST_DB=mock → Sequelize initialized but should not be used');
54-
55-
sequelizeInstance = new Sequelize('sqlite::memory:', {
56-
logging: false,
57-
});
58-
59-
return sequelizeInstance;
60-
}
61-
6252
const DATABASE_URL = buildDatabaseUrl();
6353

6454
logger.info('Using Postgres database');

src/services/messagingService.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export const sendOTPEmail = async (to: string, token: string) => {
1818

1919
export const sendOTPSMS = async (to: string, token: number) => {
2020
logger.debug(`Sending verification SMS: ${to} with ${token}`);
21-
2221
if (isDevelopment) {
2322
return;
2423
}

tests/e2e/auth.happy.spec.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import request from 'supertest';
2+
import { describe, it, expect, beforeAll, vi, afterAll } from 'vitest';
3+
4+
vi.unmock('../../src/models/authEvents.js');
5+
vi.unmock('../../src/models/sessions.js');
6+
vi.unmock('../../src/models/users.js');
7+
vi.unmock('../../src/models/systemConfig.js');
8+
vi.unmock('../../src/models/credentials.js');
9+
vi.unmock('../../src/models/magicLinks.js');
10+
vi.unmock('../../src/services/sessionService.js');
11+
vi.unmock('../../src/services/authEventService.js');
12+
vi.unmock('../../src/models');
13+
vi.unmock('../../src/services/messagingService.js');
14+
vi.unmock('../../src/lib/cookie.js');
15+
vi.unmock('../../src/lib/token.js');
16+
vi.unmock('../../src/middleware/attachAuthMiddleware.js');
17+
vi.unmock('../../src/middleware/verifyCookieAuth.js');
18+
19+
vi.unmock('../../src/config/getSystemConfig.js');
20+
vi.unmock('../../src/utils/utils.js');
21+
vi.unmock('../../src/utils/otp.js');
22+
vi.unmock('../../src/utils/token.js');
23+
vi.unmock('../../src/utils/cookie.js');
24+
vi.unmock('../../src/utils/secretStore.js');
25+
26+
vi.unmock('bcrypt-ts');
27+
28+
let app: any;
29+
30+
beforeAll(async () => {
31+
vi.stubEnv('NODE_ENV', 'test');
32+
vi.stubEnv('AUTH_MODE', 'web');
33+
34+
vi.stubEnv('DB_DIALECT', 'postgres');
35+
vi.stubEnv('DB_HOST', 'localhost');
36+
vi.stubEnv('DB_PORT', '5432');
37+
vi.stubEnv('DB_NAME', 'seamless_auth_test');
38+
vi.stubEnv('DB_USER', 'myuser');
39+
vi.stubEnv('DB_PASSWORD', 'mypassword');
40+
41+
vi.stubEnv('ISSUER', 'test-issuer');
42+
vi.stubEnv('APP_ID', 'test-app');
43+
vi.stubEnv('APP_ORIGIN', 'http://localhost');
44+
45+
vi.stubEnv('JWKS_ACTIVE_KIDe', 'dev-main');
46+
vi.stubEnv('API_SERVICE_TOKEN', 'service-token');
47+
48+
vi.stubEnv('DEFAULT_ROLES', 'user');
49+
vi.stubEnv('AVAILABLE_ROLES', 'user,admin');
50+
vi.stubEnv('ACCESS_TOKEN_TTL', '15m');
51+
vi.stubEnv('REFRESH_TOKEN_TTL', '1h');
52+
vi.stubEnv('RATE_LIMIT', '100');
53+
vi.stubEnv('DELAY_AFTER', '50');
54+
vi.stubEnv('RPID', 'localhost');
55+
vi.stubEnv('ORIGINS', 'http://localhost');
56+
vi.stubEnv('APP_NAME', 'TestApp');
57+
58+
const { initializeModels } = await import('../../src/models');
59+
const models = await initializeModels();
60+
61+
await models.sequelize.sync({ force: true });
62+
63+
const { bootstrapSystemConfig } = await import('../../src/config/bootstrapSystemConfig');
64+
await bootstrapSystemConfig();
65+
66+
const { createApp } = await import('../../src/app');
67+
app = await createApp();
68+
});
69+
70+
afterAll(() => {
71+
vi.unstubAllEnvs();
72+
});
73+
74+
it('full auth lifecycle works', async () => {
75+
const email = 'test@example.com';
76+
const phone = '+14155552671';
77+
78+
const registerRes = await request(app).post('/registration/register').send({ email, phone });
79+
80+
expect(registerRes.status).toBe(200);
81+
82+
const cookies = registerRes.headers['set-cookie'];
83+
expect(cookies).toBeDefined();
84+
85+
const otpRes = await request(app).get('/otp/generate-phone-otp').set('Cookie', cookies);
86+
87+
expect(otpRes.status).toBe(200);
88+
89+
const { User } = await import('../../src/models/users');
90+
91+
const user = await User.findOne({ where: { email } });
92+
93+
expect(user).toBeDefined();
94+
const otp = user?.phoneVerificationToken;
95+
96+
expect(otp).toBeDefined();
97+
98+
const verifyRes = await request(app)
99+
.post('/otp/verify-phone-otp')
100+
.set('Cookie', cookies)
101+
.send({ verificationToken: otp });
102+
103+
expect(verifyRes.status).toBe(200);
104+
105+
const emailOtpRes = await request(app).get('/otp/generate-email-otp').set('Cookie', cookies);
106+
107+
expect(emailOtpRes.status).toBe(200);
108+
109+
await user?.reload();
110+
const emailOtp = user?.emailVerificationToken;
111+
112+
expect(emailOtp).toBeDefined();
113+
114+
const emailVerifyRes = await request(app)
115+
.post('/otp/verify-email-otp')
116+
.set('Cookie', cookies)
117+
.send({ verificationToken: emailOtp });
118+
119+
expect(emailVerifyRes.status).toBe(200);
120+
121+
let authCookies = emailVerifyRes.headers['set-cookie'];
122+
expect(authCookies).toBeDefined();
123+
124+
const meRes = await request(app).get('/users/me').set('Cookie', authCookies);
125+
126+
const maybeNewCookies = meRes.headers['set-cookie'];
127+
if (maybeNewCookies) {
128+
authCookies = maybeNewCookies;
129+
}
130+
131+
expect(meRes.status).toBe(200);
132+
expect(Array.isArray(meRes.body.user)).toBeDefined();
133+
134+
const brokenCookies = (authCookies as unknown as string[]).filter(
135+
(c: string) => !c.includes('seamless_access'),
136+
);
137+
138+
expect(brokenCookies.some((c) => c.includes('seamless_refresh'))).toBe(true);
139+
140+
const refreshRes = await request(app).get('/users/me').set('Cookie', brokenCookies);
141+
142+
expect(refreshRes.status).toBe(200);
143+
144+
const refreshedCookies = refreshRes.headers['set-cookie'];
145+
expect(refreshedCookies).toBeDefined();
146+
147+
authCookies = refreshedCookies;
148+
149+
const logoutRes = await request(app).get('/logout').set('Cookie', authCookies);
150+
151+
expect(logoutRes.status).toBe(200);
152+
});

tests/unit/middleware/attachAuthMiddleware.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect, vi, beforeEach } from 'vitest';
1+
import { describe, it, expect, vi, beforeEach, afterAll } from 'vitest';
22

33
vi.unmock('../../../src/middleware/verifyBearerAuth');
44
vi.unmock('../../../src/middleware/verifyCookieAuth');
@@ -28,6 +28,9 @@ describe('attachAuthMiddleware', () => {
2828
delete process.env.AUTH_MODE;
2929
});
3030

31+
afterAll(() => {
32+
vi.unstubAllEnvs();
33+
});
3134
it('defaults to cookie auth', async () => {
3235
attachAuthMiddleware();
3336

0 commit comments

Comments
 (0)