3 new modules: - src/main/domain/pkce.ts: PKCE pair (S256) + state (RFC 7636) - src/main/infra/loopback-server.ts: ephemeral 127.0.0.1:PORT redirect capture - src/main/domain/twitch-oauth.ts: startLoginFlow / awaitAuthorizationCode / exchangeCodeForToken / fetchTwitchUserInfo Flow runs entirely scaffold-ready — IPC wiring + Client ID config and shell.openExternal(authUrl) trigger come in a follow-on plan once the user registers a Twitch dev app. 164 unit tests gruen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
59 lines
2.4 KiB
TypeScript
59 lines
2.4 KiB
TypeScript
import { test, expect, describe } from 'vitest';
|
|
import * as http from 'http';
|
|
import { startLoopbackServer } from './loopback-server';
|
|
|
|
function httpGet(url: string): Promise<{ status: number; body: string }> {
|
|
return new Promise((resolve, reject) => {
|
|
const req = http.get(url, res => {
|
|
let body = '';
|
|
res.on('data', chunk => { body += chunk.toString(); });
|
|
res.on('end', () => resolve({ status: res.statusCode ?? 0, body }));
|
|
});
|
|
req.on('error', reject);
|
|
});
|
|
}
|
|
|
|
describe('startLoopbackServer', () => {
|
|
test('binds to 127.0.0.1 and returns url with pathPrefix', async () => {
|
|
const server = await startLoopbackServer({ pathPrefix: '/cb' });
|
|
expect(server.url).toMatch(/^http:\/\/127\.0\.0\.1:\d+\/cb$/);
|
|
server.close();
|
|
});
|
|
|
|
test('captures redirect params (code + state)', async () => {
|
|
const server = await startLoopbackServer({ pathPrefix: '/cb' });
|
|
const captureP = server.awaitParams({ timeoutMs: 3000 });
|
|
const response = await httpGet(`${server.url}?code=abc123&state=xyz`);
|
|
expect(response.status).toBe(200);
|
|
const params = await captureP;
|
|
expect(params.get('code')).toBe('abc123');
|
|
expect(params.get('state')).toBe('xyz');
|
|
server.close();
|
|
});
|
|
|
|
test('non-matching path returns 404, capture not triggered', async () => {
|
|
const server = await startLoopbackServer({ pathPrefix: '/cb' });
|
|
const captureP = server.awaitParams({ timeoutMs: 500 });
|
|
const response = await httpGet(`${server.url.replace('/cb', '/other')}`);
|
|
expect(response.status).toBe(404);
|
|
await expect(captureP).rejects.toThrow(/timeout/);
|
|
server.close();
|
|
});
|
|
|
|
test('error param renders errorHtml', async () => {
|
|
const server = await startLoopbackServer({ pathPrefix: '/cb' });
|
|
const captureP = server.awaitParams({ timeoutMs: 3000 });
|
|
const response = await httpGet(`${server.url}?error=access_denied`);
|
|
expect(response.body).toContain('Fehler');
|
|
const params = await captureP;
|
|
expect(params.get('error')).toBe('access_denied');
|
|
server.close();
|
|
});
|
|
|
|
test('timeout rejects', async () => {
|
|
const server = await startLoopbackServer({ pathPrefix: '/cb' });
|
|
await expect(server.awaitParams({ timeoutMs: 200 })).rejects.toThrow(/timeout/);
|
|
server.close();
|
|
});
|
|
});
|