Skip to content

Commit ededb0b

Browse files
committed
feat: cookie auth test
1 parent 13de791 commit ededb0b

4 files changed

Lines changed: 280 additions & 1 deletion

File tree

src/controllers/sessions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const listSessions = async (req: Request, res: Response) => {
3838
current: session.id === currentSessionId,
3939
}));
4040

41-
return res.json({ sessions: response });
41+
return res.json({ sessions: response, total: response.length });
4242
};
4343

4444
export const revokeSession = async (req: Request, res: Response) => {
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { describe, it, expect, beforeEach, vi } from 'vitest';
2+
import { verifyCookieAuth } from '../../../src/middleware/verifyCookieAuth.js';
3+
4+
import {
5+
validateAccessToken,
6+
validateSessionRecord,
7+
getUserFromSession,
8+
verifyJwtWithKid,
9+
} from '../../../src/services/sessionService.js';
10+
11+
vi.mock('../../../src/models/authEvents.js', () => ({
12+
AuthEvent: {
13+
create: vi.fn(),
14+
},
15+
}));
16+
17+
vi.mock('bcrypt-ts', () => ({
18+
compareSync: vi.fn(),
19+
}));
20+
21+
import { User } from '../../../src/models/users.js';
22+
import { Session } from '../../../src/models/sessions.js';
23+
import { generateRefreshToken, hashRefreshToken, signAccessToken } from '../../../src/lib/token.js';
24+
import { compareSync } from 'bcrypt-ts';
25+
26+
function mockReqRes(cookies: any = {}) {
27+
const req: any = {
28+
cookies,
29+
ip: '127.0.0.1',
30+
headers: {},
31+
};
32+
33+
const res: any = {
34+
status: vi.fn().mockReturnThis(),
35+
json: vi.fn(),
36+
};
37+
38+
const next = vi.fn();
39+
40+
return { req, res, next };
41+
}
42+
43+
beforeEach(() => {
44+
vi.clearAllMocks();
45+
});
46+
47+
describe('verifyCookieAuth - ephemeral', () => {
48+
it('rejects missing cookie', async () => {
49+
const middleware = verifyCookieAuth('ephemeral');
50+
const { req, res, next } = mockReqRes();
51+
52+
await middleware(req, res, next);
53+
54+
expect(res.status).toHaveBeenCalledWith(401);
55+
});
56+
57+
it('accepts valid ephemeral token', async () => {
58+
(verifyJwtWithKid as any).mockResolvedValue({ sub: 'user-1' });
59+
60+
(User.findOne as any).mockResolvedValue({
61+
id: 'user-1',
62+
revoked: false,
63+
});
64+
65+
const middleware = verifyCookieAuth('ephemeral');
66+
67+
const { req, res, next } = mockReqRes({
68+
seamless_ephemeral: 'token',
69+
});
70+
71+
await middleware(req, res, next);
72+
73+
expect(req.user).toBeDefined();
74+
expect(next).toHaveBeenCalled();
75+
});
76+
});
77+
78+
describe('verifyCookieAuth - access token', () => {
79+
it('uses valid access token', async () => {
80+
(validateAccessToken as any).mockResolvedValue({
81+
sessionId: 'session-1',
82+
});
83+
84+
(validateSessionRecord as any).mockResolvedValue({
85+
id: 'session-1',
86+
});
87+
88+
(getUserFromSession as any).mockResolvedValue({
89+
id: 'user-1',
90+
});
91+
92+
const middleware = verifyCookieAuth('access');
93+
94+
const { req, res, next } = mockReqRes({
95+
seamless_access: 'access-token',
96+
});
97+
98+
await middleware(req, res, next);
99+
100+
expect(req.user).toBeDefined();
101+
expect(next).toHaveBeenCalled();
102+
});
103+
104+
it('returns 401 when no cookies', async () => {
105+
const middleware = verifyCookieAuth('access');
106+
107+
const { req, res, next } = mockReqRes();
108+
109+
await middleware(req, res, next);
110+
111+
expect(res.status).toHaveBeenCalledWith(401);
112+
});
113+
});
114+
115+
describe('verifyCookieAuth - silent refresh', () => {
116+
it('refreshes session when access token invalid', async () => {
117+
(validateAccessToken as any).mockResolvedValue(null);
118+
119+
(compareSync as any).mockReturnValue(true);
120+
121+
(Session.findAll as any).mockResolvedValue([
122+
{
123+
id: 'session-1',
124+
refreshTokenHash: 'hash',
125+
userId: 'user-1',
126+
infraId: 'app',
127+
mode: 'web',
128+
userAgent: 'agent',
129+
replacedBySessionId: null,
130+
revokedAt: null,
131+
save: vi.fn(),
132+
},
133+
]);
134+
135+
(User.findByPk as any).mockResolvedValue({
136+
id: 'user-1',
137+
});
138+
139+
(generateRefreshToken as any).mockReturnValue('refresh-token');
140+
(hashRefreshToken as any).mockResolvedValue('hashed-refresh');
141+
(signAccessToken as any).mockResolvedValue('access-token');
142+
143+
(Session.create as any).mockResolvedValue({
144+
id: 'new-session',
145+
});
146+
147+
const middleware = verifyCookieAuth('access');
148+
149+
const { req, res, next } = mockReqRes({
150+
seamless_refresh: 'refresh-token',
151+
});
152+
153+
await middleware(req, res, next);
154+
155+
expect(next).toHaveBeenCalled();
156+
});
157+
});
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import request from 'supertest';
2+
import { describe, it, expect, beforeAll, beforeEach, vi } from 'vitest';
3+
import { createApp } from '../../../src/app';
4+
import { Application } from 'express';
5+
6+
import { Session } from '../../../src/models/sessions.js';
7+
import { hardRevokeSession } from '../../../src/services/sessionService.js';
8+
9+
let app: Application;
10+
11+
beforeAll(async () => {
12+
app = await createApp();
13+
});
14+
15+
beforeEach(() => {
16+
vi.clearAllMocks();
17+
});
18+
19+
function buildSession(overrides: any = {}) {
20+
return {
21+
id: 'session-1',
22+
deviceName: 'MacBook',
23+
ipAddress: '127.0.0.1',
24+
userAgent: 'test-agent',
25+
lastUsedAt: new Date(),
26+
expiresAt: new Date(Date.now() + 100000),
27+
revokedAt: null,
28+
...overrides,
29+
};
30+
}
31+
32+
describe('GET /sessions', () => {
33+
it('returns active sessions', async () => {
34+
(Session.findAll as any).mockResolvedValue([
35+
buildSession({ id: 'session-1' }),
36+
buildSession({ id: 'session-2' }),
37+
]);
38+
39+
const res = await request(app).get('/sessions');
40+
41+
expect(res.status).toBe(200);
42+
expect(res.body.sessions).toHaveLength(2);
43+
44+
const current = res.body.sessions.find((s: any) => s.id === 'session-1');
45+
expect(current.current).toBe(true);
46+
});
47+
48+
it('returns empty list', async () => {
49+
(Session.findAll as any).mockResolvedValue([]);
50+
51+
const res = await request(app).get('/sessions');
52+
53+
expect(res.status).toBe(200);
54+
expect(res.body.sessions).toEqual([]);
55+
});
56+
});
57+
58+
describe('DELETE /sessions/:id', () => {
59+
it('revokes a session', async () => {
60+
const session = buildSession();
61+
62+
(Session.findOne as any).mockResolvedValue(session);
63+
64+
const res = await request(app).delete('/sessions/session-1');
65+
66+
expect(res.status).toBe(200);
67+
expect(hardRevokeSession).toHaveBeenCalledWith(session, 'user_revoked');
68+
});
69+
70+
it('returns 404 if session not found', async () => {
71+
(Session.findOne as any).mockResolvedValue(null);
72+
73+
const res = await request(app).delete('/sessions/bad-id');
74+
75+
expect(res.status).toBe(404);
76+
});
77+
});
78+
79+
describe('DELETE /sessions', () => {
80+
it('revokes all sessions', async () => {
81+
const sessions = [buildSession({ id: '1' }), buildSession({ id: '2' })];
82+
83+
(Session.findAll as any).mockResolvedValue(sessions);
84+
85+
const res = await request(app).delete('/sessions');
86+
87+
expect(res.status).toBe(200);
88+
expect(hardRevokeSession).toHaveBeenCalledTimes(2);
89+
});
90+
91+
it('handles no sessions gracefully', async () => {
92+
(Session.findAll as any).mockResolvedValue([]);
93+
94+
const res = await request(app).delete('/sessions');
95+
96+
expect(res.status).toBe(200);
97+
expect(hardRevokeSession).not.toHaveBeenCalled();
98+
});
99+
});

tests/setup/mocks.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,22 @@ vi.mock('../../src/models/sessions.js', () => ({
1414
},
1515
}));
1616

17+
vi.mock('../../src/models/users.js', () => ({
18+
User: {
19+
findOne: vi.fn(),
20+
findByPk: vi.fn(),
21+
},
22+
}));
23+
24+
vi.mock('../../src/services/sessionService.js', () => ({
25+
validateAccessToken: vi.fn(),
26+
validateSessionRecord: vi.fn(),
27+
getUserFromSession: vi.fn(),
28+
verifyJwtWithKid: vi.fn(),
29+
revokeSessionChain: vi.fn(),
30+
hardRevokeSession: vi.fn(),
31+
}));
32+
1733
vi.mock('../../src/middleware/attachAuthMiddleware.js', () => ({
1834
attachAuthMiddleware: () => (req: any, _res: any, next: any) => {
1935
// inject fake authenticated user
@@ -36,6 +52,8 @@ vi.mock('../../src/middleware/attachAuthMiddleware.js', () => ({
3652

3753
update: vi.fn(),
3854
};
55+
56+
req.sessionId = 'session-1';
3957
next();
4058
},
4159
}));
@@ -53,3 +71,8 @@ vi.mock('../../src/lib/token.js', () => ({
5371
generateRefreshToken: vi.fn(),
5472
hashRefreshToken: vi.fn(),
5573
}));
74+
75+
vi.mock('../../src/lib/cookie.js', () => ({
76+
setAuthCookies: vi.fn(),
77+
clearAuthCookies: vi.fn(),
78+
}));

0 commit comments

Comments
 (0)