diff --git a/bounty-notes/issue-42-webhooks-test.md b/bounty-notes/issue-42-webhooks-test.md new file mode 100644 index 0000000..ff7eca6 --- /dev/null +++ b/bounty-notes/issue-42-webhooks-test.md @@ -0,0 +1,34 @@ +# CodeBounty webhook verification for issue #42 + +Fixes #42. + +This adds a deterministic webhook payload validation fixture for the CodeBounty webhook test issue. The fixture and validator let maintainers confirm that a bounty webhook event carries the fields required to route a bounty notification back to the correct GitHub issue and visible reward. + +## Added artifacts + +- `test-fixtures/codebounty-webhook-issue-42.json` — sample `bounty.available` payload for `CodeBountyOrg/BountyTestRepository#42` with the $150 visible bounty amount. +- `scripts/validate-codebounty-webhook.mjs` — dependency-free Node.js validator that parses a webhook JSON file/stdin and checks: + - event name + - repository full name + - issue number + - positive bounty amount + - bounty status + +## Validation commands + +```bash +node --check scripts/validate-codebounty-webhook.mjs +node scripts/validate-codebounty-webhook.mjs test-fixtures/codebounty-webhook-issue-42.json +``` + +Expected normalized output: + +```json +{ + "event": "bounty.available", + "repository": "CodeBountyOrg/BountyTestRepository", + "issue": "#42", + "bounty": "$150", + "status": "available" +} +``` diff --git a/scripts/validate-codebounty-webhook.mjs b/scripts/validate-codebounty-webhook.mjs new file mode 100644 index 0000000..a6a2152 --- /dev/null +++ b/scripts/validate-codebounty-webhook.mjs @@ -0,0 +1,59 @@ +#!/usr/bin/env node +import { readFileSync } from 'node:fs'; + +const requiredString = (payload, path) => { + const value = path.split('.').reduce((current, key) => current?.[key], payload); + if (typeof value !== 'string' || value.trim() === '') { + throw new Error(`Missing required string: ${path}`); + } + return value.trim(); +}; + +const requiredNumber = (payload, path) => { + const value = path.split('.').reduce((current, key) => current?.[key], payload); + if (typeof value !== 'number' || Number.isNaN(value)) { + throw new Error(`Missing required number: ${path}`); + } + return value; +}; + +const readPayload = () => { + const inputPath = process.argv[2]; + const raw = inputPath ? readFileSync(inputPath, 'utf8') : readFileSync(0, 'utf8'); + try { + return JSON.parse(raw); + } catch (error) { + throw new Error(`Webhook payload is not valid JSON: ${error.message}`); + } +}; + +const normalizeWebhookPayload = (payload) => { + const event = requiredString(payload, 'event'); + const repository = requiredString(payload, 'repository.full_name'); + const issueNumber = requiredNumber(payload, 'issue.number'); + const bountyAmount = requiredNumber(payload, 'bounty.amount_usd'); + const bountyStatus = requiredString(payload, 'bounty.status'); + + if (issueNumber <= 0) { + throw new Error('issue.number must be positive'); + } + + if (bountyAmount <= 0) { + throw new Error('bounty.amount_usd must be positive'); + } + + return { + event, + repository, + issue: `#${issueNumber}`, + bounty: `$${bountyAmount}`, + status: bountyStatus, + }; +}; + +const main = () => { + const normalized = normalizeWebhookPayload(readPayload()); + console.log(JSON.stringify(normalized, null, 2)); +}; + +main(); diff --git a/test-fixtures/codebounty-webhook-issue-42.json b/test-fixtures/codebounty-webhook-issue-42.json new file mode 100644 index 0000000..478507d --- /dev/null +++ b/test-fixtures/codebounty-webhook-issue-42.json @@ -0,0 +1,15 @@ +{ + "event": "bounty.available", + "repository": { + "full_name": "CodeBountyOrg/BountyTestRepository" + }, + "issue": { + "number": 42, + "title": "webhooks test" + }, + "bounty": { + "amount_usd": 150, + "status": "available", + "provider": "CodeBounty" + } +}