import { test } from 'node:test'; import assert from 'node:assert/strict'; import crypto from 'node:crypto'; import { createServer } from '../src/server.js'; import { createLogger } from '../src/logger.js'; process.env.LOG_LEVEL = 'error'; const logger = createLogger({}); function baseConfig(overrides = {}) { return { ownerLogin: 'selimaj-dev', triggerPhrase: '@claude', webhookSecret: null, ...overrides }; } async function withServer(config, fn) { const triggers = []; const server = createServer({ config, logger, onTrigger: (t) => triggers.push(t) }); await new Promise((resolve) => server.listen(0, resolve)); const { port } = server.address(); try { await fn(`http://127.0.0.1:${port}`, triggers); } finally { await new Promise((resolve) => server.close(resolve)); } } function issuePayload() { return { action: 'created', comment: { user: { login: 'selimaj-dev' }, body: '@claude fix it' }, issue: { number: 1, title: 't', body: 'b' }, repository: { full_name: 'o/r', default_branch: 'main' }, }; } test('GET /healthz returns 200 without touching onTrigger', async () => { await withServer(baseConfig(), async (base, triggers) => { const res = await fetch(`${base}/healthz`); assert.equal(res.status, 200); assert.equal(triggers.length, 0); }); }); test('rejects non-POST, non-health requests', async () => { await withServer(baseConfig(), async (base) => { const res = await fetch(base, { method: 'GET' }); assert.equal(res.status, 405); }); }); test('accepts a matching issue_comment and calls onTrigger', async () => { await withServer(baseConfig(), async (base, triggers) => { const res = await fetch(base, { method: 'POST', headers: { 'X-Gitea-Event': 'issue_comment', 'Content-Type': 'application/json' }, body: JSON.stringify(issuePayload()), }); assert.equal(res.status, 202); assert.equal(triggers.length, 1); assert.equal(triggers[0].repo, 'o/r'); }); }); test('does not call onTrigger for an unrelated author', async () => { await withServer(baseConfig(), async (base, triggers) => { const payload = issuePayload(); payload.comment.user.login = 'someone-else'; await fetch(base, { method: 'POST', headers: { 'X-Gitea-Event': 'issue_comment' }, body: JSON.stringify(payload), }); assert.equal(triggers.length, 0); }); }); test('accepts requests with no event header (health probes) without error', async () => { await withServer(baseConfig(), async (base, triggers) => { const res = await fetch(base, { method: 'POST', body: '{}' }); assert.equal(res.status, 202); assert.equal(triggers.length, 0); }); }); test('rejects a request with a bad signature when a secret is configured', async () => { await withServer(baseConfig({ webhookSecret: 's3cret' }), async (base, triggers) => { const res = await fetch(base, { method: 'POST', headers: { 'X-Gitea-Event': 'issue_comment', 'X-Gitea-Signature': 'wrong' }, body: JSON.stringify(issuePayload()), }); assert.equal(res.status, 401); assert.equal(triggers.length, 0); }); }); test('accepts a request with a correct signature when a secret is configured', async () => { const secret = 's3cret'; await withServer(baseConfig({ webhookSecret: secret }), async (base, triggers) => { const body = JSON.stringify(issuePayload()); const sig = crypto.createHmac('sha256', secret).update(body).digest('hex'); const res = await fetch(base, { method: 'POST', headers: { 'X-Gitea-Event': 'issue_comment', 'X-Gitea-Signature': sig }, body, }); assert.equal(res.status, 202); assert.equal(triggers.length, 1); }); }); test('does not crash on invalid JSON body', async () => { await withServer(baseConfig(), async (base, triggers) => { const res = await fetch(base, { method: 'POST', headers: { 'X-Gitea-Event': 'issue_comment' }, body: 'not json', }); assert.equal(res.status, 202); // already acknowledged before parse failure assert.equal(triggers.length, 0); }); });