Skip to content

Commit 9058e80

Browse files
committed
test: add happy-path OAuth2 callback test with state seeding helper
- Export _seedOAuthState() from auth.js for test-seeding CSRF states - Add integration test verifying full OAuth callback flow: code exchange → user fetch → session store → JWT redirect - Verifies session stored server-side and JWT contains correct user info Addresses PR #73 review thread #18
1 parent 32a8084 commit 9058e80

2 files changed

Lines changed: 56 additions & 1 deletion

File tree

src/api/routes/auth.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,17 @@ export const sessionStore = new SessionStore();
6060
const oauthStates = new Map();
6161
const STATE_TTL_MS = 10 * 60 * 1000; // 10 minutes
6262

63+
/**
64+
* Seed an OAuth state for testing purposes.
65+
* Adds a state entry with the default TTL so integration tests can exercise
66+
* the callback endpoint without performing the redirect flow.
67+
*
68+
* @param {string} state - The state value to seed
69+
*/
70+
export function _seedOAuthState(state) {
71+
oauthStates.set(state, Date.now() + STATE_TTL_MS);
72+
}
73+
6374
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
6475

6576
/**

tests/api/routes/auth.test.js

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ vi.mock('../../../src/logger.js', () => ({
88
error: vi.fn(),
99
}));
1010

11-
import { sessionStore } from '../../../src/api/routes/auth.js';
11+
import { _seedOAuthState, sessionStore } from '../../../src/api/routes/auth.js';
1212
import { createApp } from '../../../src/api/server.js';
1313
import { guildCache } from '../../../src/api/utils/discordApi.js';
1414

@@ -84,6 +84,50 @@ describe('auth routes', () => {
8484
expect(res.status).toBe(403);
8585
expect(res.body.error).toContain('Invalid or expired OAuth state');
8686
});
87+
88+
it('should exchange code for token and redirect with JWT on success', async () => {
89+
vi.stubEnv('DISCORD_CLIENT_ID', 'client-id-123');
90+
vi.stubEnv('DISCORD_CLIENT_SECRET', 'client-secret');
91+
vi.stubEnv('DISCORD_REDIRECT_URI', 'http://localhost:3001/callback');
92+
vi.stubEnv('SESSION_SECRET', 'test-session-secret');
93+
vi.stubEnv('DASHBOARD_URL', 'http://localhost:3000');
94+
95+
// Seed a valid OAuth state
96+
const state = 'test-state-abc';
97+
_seedOAuthState(state);
98+
99+
// Mock token exchange and user info fetch
100+
vi.spyOn(globalThis, 'fetch')
101+
.mockResolvedValueOnce({
102+
ok: true,
103+
json: async () => ({ access_token: 'discord-access-token' }),
104+
})
105+
.mockResolvedValueOnce({
106+
ok: true,
107+
json: async () => ({
108+
id: '999',
109+
username: 'newuser',
110+
discriminator: '0001',
111+
avatar: 'avatar123',
112+
}),
113+
});
114+
115+
const res = await request(app).get(
116+
`/api/v1/auth/discord/callback?code=valid-code&state=${state}`,
117+
);
118+
119+
expect(res.status).toBe(302);
120+
expect(res.headers.location).toMatch(/^http:\/\/localhost:3000#token=.+/);
121+
122+
// Verify session was stored server-side
123+
expect(sessionStore.get('999')).toBe('discord-access-token');
124+
125+
// Verify the JWT in the redirect contains user info
126+
const token = res.headers.location.split('#token=')[1];
127+
const decoded = jwt.verify(token, 'test-session-secret', { algorithms: ['HS256'] });
128+
expect(decoded.userId).toBe('999');
129+
expect(decoded.username).toBe('newuser');
130+
});
87131
});
88132

89133
describe('GET /api/v1/auth/me', () => {

0 commit comments

Comments
 (0)