Skip to content

Commit f4625b0

Browse files
alexandre-mrtclaude
andcommitted
feat: add /api/stats endpoint for drop statistics (110 tests)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 87fcd36 commit f4625b0

File tree

3 files changed

+56
-0
lines changed

3 files changed

+56
-0
lines changed

src/routes/drop.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,13 @@ dropRouter.get("/:id/download", async (c) => {
164164
return c.json({ error: message }, 500);
165165
}
166166
});
167+
168+
// Public stats
169+
dropRouter.get("/stats", (c) => {
170+
const stats = queries.stats.get();
171+
return c.json({
172+
totalDrops: stats?.total_drops ?? 0,
173+
totalSize: stats?.total_size ?? 0,
174+
totalDownloads: stats?.total_downloads ?? 0,
175+
});
176+
});

src/services/db.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,8 @@ export const queries = {
7272
recentDrops: db.prepare<DropRow, [string]>(
7373
"SELECT * FROM drops WHERE ip_address = ? ORDER BY created_at DESC LIMIT 10",
7474
),
75+
76+
stats: db.prepare<{ total_drops: number; total_size: number; total_downloads: number }, []>(
77+
"SELECT COUNT(*) as total_drops, COALESCE(SUM(file_size), 0) as total_size, COALESCE(SUM(downloads), 0) as total_downloads FROM drops",
78+
),
7579
};

tests/stats.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { Hono } from "hono";
3+
4+
process.env.DATABASE_URL = ":memory:";
5+
const { dropRouter } = await import("../src/routes/drop");
6+
const { queries } = await import("../src/services/db");
7+
8+
const app = new Hono();
9+
app.route("/api", dropRouter);
10+
11+
// Seed data
12+
queries.insertDrop.run({
13+
$id: "stats-1", $rootHash: "0xs1", $fileName: "a.txt",
14+
$fileSize: 1000, $mimeType: "text/plain", $passwordHash: null,
15+
$maxDownloads: null, $expiresAt: null, $ipAddress: null,
16+
});
17+
queries.insertDrop.run({
18+
$id: "stats-2", $rootHash: "0xs2", $fileName: "b.txt",
19+
$fileSize: 2000, $mimeType: "text/plain", $passwordHash: null,
20+
$maxDownloads: null, $expiresAt: null, $ipAddress: null,
21+
});
22+
queries.incrementDownloads.run("stats-1");
23+
queries.incrementDownloads.run("stats-1");
24+
queries.incrementDownloads.run("stats-2");
25+
26+
describe("GET /api/stats", () => {
27+
test("returns aggregate statistics", async () => {
28+
const res = await app.request("/api/stats");
29+
expect(res.status).toBe(200);
30+
const json = await res.json();
31+
expect(json.totalDrops).toBeGreaterThanOrEqual(2);
32+
expect(json.totalSize).toBeGreaterThanOrEqual(3000);
33+
expect(json.totalDownloads).toBeGreaterThanOrEqual(3);
34+
});
35+
36+
test("returns numeric values", async () => {
37+
const json = await (await app.request("/api/stats")).json();
38+
expect(typeof json.totalDrops).toBe("number");
39+
expect(typeof json.totalSize).toBe("number");
40+
expect(typeof json.totalDownloads).toBe("number");
41+
});
42+
});

0 commit comments

Comments
 (0)