Twitch-VOD-Manager/src/main/infra/loopback-server.test.ts
xRangerDE 5a5d6f9c47 feat(auth): Twitch OAuth 2.1 Authorization Code Flow + PKCE + Loopback (21 tests)
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>
2026-05-11 23:46:22 +02:00

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();
});
});