-
Notifications
You must be signed in to change notification settings - Fork 4
fix: Bootstrap w/ API for sending payment #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 9 commits
415a869
82ed15b
7e66ec3
15d1fe5
ebafa74
bc43957
7bf477a
7033d84
edf17a3
8465ce3
c59dbf9
c64f31d
9232add
0ed57a2
de98d70
726ebbe
b6cf67e
139368b
3bc0542
55c8a89
797f0ea
1b22413
91f2510
fbe9f3e
eba448e
d6036ad
0625c0d
1a45086
c35165a
c072b7d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,27 +1,28 @@ | ||
| { | ||
| "name": "debug-api", | ||
| "module": "index.ts", | ||
| "type": "module", | ||
| "devDependencies": { | ||
| "@biomejs/biome": "^1.8.3", | ||
| "@types/bun": "latest", | ||
| "@types/react": "18.3.4", | ||
| "next": "^14.2.5", | ||
| "redaxios": "^0.5.1" | ||
| }, | ||
| "peerDependencies": { | ||
| "typescript": "^5.0.0" | ||
| "name": "fake-algora", | ||
| "version": "0.1.0", | ||
| "description": "A fake/mock Algora API server for testing bounty payment flows", | ||
| "main": "dist/index.js", | ||
| "scripts": { | ||
| "dev": "ts-node-dev --respawn --transpile-only src/index.ts", | ||
| "build": "tsc", | ||
| "start": "node dist/index.js", | ||
| "test": "vitest run", | ||
| "test:watch": "vitest" | ||
| }, | ||
| "dependencies": { | ||
| "winterspec": "^0.0.86", | ||
| "zod": "^3.23.8", | ||
| "zustand": "^4.5.5", | ||
| "zustand-hoist": "^2.0.1" | ||
| "express": "^4.18.2", | ||
| "zod": "^3.22.4", | ||
| "uuid": "^9.0.0" | ||
| }, | ||
| "scripts": { | ||
| "start": "bun run dev", | ||
| "dev": "winterspec dev", | ||
| "build": "winterspec bundle -o dist/bundle.js", | ||
| "next:dev": "next dev" | ||
| "devDependencies": { | ||
| "@types/express": "^4.17.21", | ||
| "@types/node": "^20.11.5", | ||
| "@types/uuid": "^9.0.7", | ||
| "ts-node-dev": "^2.0.0", | ||
| "typescript": "^5.3.3", | ||
| "vitest": "^1.2.2", | ||
| "supertest": "^6.3.4", | ||
| "@types/supertest": "^6.0.2" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import express from "express"; | ||
| import bountiesRouter from "./routes/bounties"; | ||
| import paymentsRouter from "./routes/payments"; | ||
|
|
||
| const app = express(); | ||
|
|
||
| app.use(express.json()); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Health check | ||
| // --------------------------------------------------------------------------- | ||
| app.get("/health", (_req, res) => { | ||
| res.json({ status: "ok", service: "fake-algora" }); | ||
| }); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Routes | ||
| // --------------------------------------------------------------------------- | ||
| app.use("/bounties", bountiesRouter); | ||
| app.use("/payments", paymentsRouter); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // 404 fallthrough | ||
| // --------------------------------------------------------------------------- | ||
| app.use((_req, res) => { | ||
| res.status(404).json({ error: "Not found" }); | ||
| }); | ||
|
|
||
| export default app; |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,14 @@ | ||||||
| import app from "./app"; | ||||||
|
|
||||||
| const PORT = process.env.PORT ?? 3000; | ||||||
|
||||||
| const PORT = process.env.PORT ?? 3000; | |
| const PORT = Number(process.env.PORT) || 3000; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import { describe, it, expect, beforeEach } from "vitest"; | ||
| import request from "supertest"; | ||
| import app from "../app"; | ||
| import { store } from "../store"; | ||
|
|
||
| beforeEach(() => { | ||
| store.reset(); | ||
| }); | ||
|
|
||
| describe("GET /bounties", () => { | ||
| it("returns an empty list when no bounties exist", async () => { | ||
| const res = await request(app).get("/bounties"); | ||
| expect(res.status).toBe(200); | ||
| expect(res.body.bounties).toEqual([]); | ||
| }); | ||
|
|
||
| it("returns seeded bounties when store is populated", async () => { | ||
| store.createBounty({ | ||
| issue_number: 1, | ||
| repo: "tscircuit/fake-algora", | ||
| amount_usd: 10, | ||
| currency: "USD", | ||
| status: "open", | ||
| recipient_username: null, | ||
| }); | ||
|
||
| const res = await request(app).get("/bounties"); | ||
| expect(res.status).toBe(200); | ||
| expect(res.body.bounties).toHaveLength(1); | ||
| }); | ||
| }); | ||
|
|
||
| describe("POST /bounties", () => { | ||
| it("creates a bounty with valid body", async () => { | ||
| const res = await request(app).post("/bounties").send({ | ||
| issue_number: 5, | ||
| repo: "tscircuit/core", | ||
| amount_usd: 25, | ||
| }); | ||
| expect(res.status).toBe(201); | ||
| expect(res.body.bounty.amount_usd).toBe(25); | ||
| expect(res.body.bounty.status).toBe("open"); | ||
| }); | ||
|
|
||
| it("returns 400 for invalid body", async () => { | ||
| const res = await request(app).post("/bounties").send({}); | ||
| expect(res.status).toBe(400); | ||
| expect(res.body.error).toMatch(/invalid/i); | ||
| }); | ||
| }); | ||
|
|
||
| describe("GET /bounties/:id", () => { | ||
| it("returns 404 for unknown id", async () => { | ||
| const res = await request(app).get("/bounties/nonexistent"); | ||
| expect(res.status).toBe(404); | ||
| }); | ||
|
|
||
| it("returns the bounty for a known id", async () => { | ||
| const bounty = store.createBounty({ | ||
| issue_number: 10, | ||
| repo: "tscircuit/fake-algora", | ||
| amount_usd: 100, | ||
| currency: "USD", | ||
| status: "open", | ||
| recipient_username: null, | ||
| }); | ||
| const res = await request(app).get(`/bounties/${bounty.id}`); | ||
| expect(res.status).toBe(200); | ||
| expect(res.body.bounty.id).toBe(bounty.id); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,44 @@ | ||||||||||||
| import { Router } from "express"; | ||||||||||||
| import { z } from "zod"; | ||||||||||||
| import { store } from "../store"; | ||||||||||||
|
|
||||||||||||
| const router = Router(); | ||||||||||||
|
|
||||||||||||
| // GET /bounties — list all bounties | ||||||||||||
| router.get("/", (_req, res) => { | ||||||||||||
| res.json({ bounties: store.listBounties() }); | ||||||||||||
| }); | ||||||||||||
|
|
||||||||||||
| // GET /bounties/:id — get a single bounty | ||||||||||||
| router.get("/:id", (req, res) => { | ||||||||||||
| const bounty = store.getBounty(req.params.id); | ||||||||||||
| if (!bounty) { | ||||||||||||
| return res.status(404).json({ error: "Bounty not found" }); | ||||||||||||
| } | ||||||||||||
| res.json({ bounty }); | ||||||||||||
| }); | ||||||||||||
|
|
||||||||||||
| // POST /bounties — create a new bounty | ||||||||||||
| const CreateBountySchema = z.object({ | ||||||||||||
| issue_number: z.number().int().positive(), | ||||||||||||
| repo: z.string().min(1), | ||||||||||||
| amount_usd: z.number().positive(), | ||||||||||||
| currency: z.string().default("USD"), | ||||||||||||
| recipient_username: z.string().nullable().default(null), | ||||||||||||
| status: z | ||||||||||||
| .enum(["open", "in_progress", "completed", "payment_pending", "paid"]) | ||||||||||||
| .default("open"), | ||||||||||||
| }); | ||||||||||||
|
|
||||||||||||
| router.post("/", (req, res) => { | ||||||||||||
| const result = CreateBountySchema.safeParse(req.body); | ||||||||||||
| if (!result.success) { | ||||||||||||
| return res | ||||||||||||
| .status(400) | ||||||||||||
| .json({ error: "Invalid request body", details: result.error.format() }); | ||||||||||||
|
||||||||||||
| .json({ error: "Invalid request body", details: result.error.format() }); | |
| .json({ | |
| error: "Invalid request body", | |
| details: JSON.stringify(result.error.format()), | |
| }); |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,87 @@ | ||||||
| import { Router } from "express"; | ||||||
| import { z } from "zod"; | ||||||
| import { v4 as uuidv4 } from "uuid"; | ||||||
| import { store } from "../store"; | ||||||
|
|
||||||
| const router = Router(); | ||||||
|
|
||||||
| // --------------------------------------------------------------------------- | ||||||
| // Schema | ||||||
| // --------------------------------------------------------------------------- | ||||||
| const SendPaymentSchema = z.object({ | ||||||
| bounty_id: z.string().min(1, "bounty_id is required"), | ||||||
| recipient_username: z.string().min(1, "recipient_username is required"), | ||||||
| amount_usd: z.number().positive("amount_usd must be positive"), | ||||||
| currency: z.string().default("USD"), | ||||||
| }); | ||||||
|
|
||||||
| // --------------------------------------------------------------------------- | ||||||
| // GET /payments — list all payments | ||||||
| // --------------------------------------------------------------------------- | ||||||
| router.get("/", (_req, res) => { | ||||||
| res.json({ payments: store.listPayments() }); | ||||||
| }); | ||||||
|
|
||||||
| // --------------------------------------------------------------------------- | ||||||
| // GET /payments/:id — get a single payment | ||||||
| // --------------------------------------------------------------------------- | ||||||
| router.get("/:id", (req, res) => { | ||||||
| const payment = store.getPayment(req.params.id); | ||||||
| if (!payment) { | ||||||
| return res.status(404).json({ error: "Payment not found" }); | ||||||
| } | ||||||
| res.json({ payment }); | ||||||
| }); | ||||||
|
|
||||||
| // --------------------------------------------------------------------------- | ||||||
| // POST /payments/send — send a payment for a bounty | ||||||
| // --------------------------------------------------------------------------- | ||||||
| router.post("/send", (req, res) => { | ||||||
| const result = SendPaymentSchema.safeParse(req.body); | ||||||
| if (!result.success) { | ||||||
| return res | ||||||
| .status(400) | ||||||
| .json({ error: "Invalid request body", details: result.error.format() }); | ||||||
|
||||||
| .json({ error: "Invalid request body", details: result.error.format() }); | |
| .json({ error: "Invalid request body", details: result.error.message }); |
Outdated
Copilot
AI
Mar 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
amount_usd, currency, and recipient_username are accepted from the request body even though the bounty already has amount/currency (and possibly an assigned recipient). This can create inconsistent records (paying a bounty for the wrong amount/currency/recipient). Consider deriving these fields from the bounty (or at least validating they match) before creating the payment and marking the bounty as paid.
Outdated
Copilot
AI
Mar 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
POST /payments/send introduces new behavior (payment creation, double-payment guard, bounty status update) but there are no corresponding Vitest/Supertest tests. Add tests for success, invalid body (400), unknown bounty (404), and already-paid bounty (409) to prevent regressions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
testscript runsvitest, but the repo still contains existingtests/**/*.test.tsthat importbun:test(and other Bun-only tooling). With the current setup,vitest runwill try to execute those files and fail. Either migrate/replace the Bun tests, or configure Vitest's include/exclude patterns so it only runs the intended Vitest/Supertest tests.