This commit is contained in:
2026-09-28 16:15:50 +02:00
parent a0ae396b33
commit 34b7dc977a
25 changed files with 1595 additions and 8 deletions
+121
View File
@@ -0,0 +1,121 @@
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);
});
});