Test Coverage Gap Report
Date: 2026-03-21
Context: A production bug in the /api/share route slipped through with zero test coverage (issue #151). This report audits the full project for similar gaps.
Summary
| Category | Total | Covered | Gap |
|---|---|---|---|
| API route files | 26 | 8 | 18 missing |
| Service files | ~50 | ~45 | 5 partial |
| Frontend components | ~40 | ~3 | Low priority |
Overall route test coverage: 31% (8 of 26 route files have tests).
1. Route Coverage Audit
Routes With Test Files ✅
| Route File | Test File | Tests |
|---|---|---|
auth.ts | auth.test.ts | 2 |
files.ts | files.test.ts | 16 |
convert.ts | convert.test.ts | ~20 (mocked services) |
export.ts | export.test.ts | 8 |
benchmark.ts | benchmark.test.ts | 7 |
share.ts | share.test.ts | 22 — added 2026-03-21 after prod bug |
spend-controls.ts | spend-controls.test.ts | 7 |
webhooks.ts | webhooks.test.ts | 22 |
Routes Missing Test Files ❌
Grouped by priority:
High Priority (financial / security / data access)
| Route | Endpoints | Risk |
|---|---|---|
stripe.ts | POST /webhook, POST /checkout, GET /session/:id | Critical — payment processing, Stripe event handling |
credits.ts | GET /, GET /packages, POST /checkout | Critical — credit balance, purchase flow |
download.ts | GET /:fileId/zip?token= | High — presigned URL token verification, file access control |
admin.ts | GET /users, GET /users/:id, PATCH /users/:id, POST /credits/grant, POST /credits/refund, POST /credits/adjust, POST /credits/expire, PUT /users/:id/pricing, DELETE /users/:id/pricing, GET /stats, GET /packages, POST /packages, PATCH /packages/:id, GET /users/:id/spend-limits, PUT /users/:id/spend-limits, GET /costs, GET /costs/recent, GET /predictions/accuracy, GET /predictions | High — admin-only operations, data integrity |
Medium Priority
| Route | Endpoints | Risk |
|---|---|---|
remediate.ts | POST /:fileId | Medium — accessibility remediation processing |
reports.ts | GET /:fileId, GET /:fileId/download | Medium — report generation/download |
tenant.ts | GET /, POST /verify-domain | Medium — multi-tenancy |
tenant-admin.ts | GET /users, GET /settings, PUT /settings | Medium — tenant admin |
budget-alerts.ts | GET /, POST /, PUT /:id, DELETE /:id | Medium — budget enforcement |
gateway.ts | GET /:url | Medium — proxy, security validation |
lti.ts | GET /login, GET /callback, POST /lineitems, GET /lineitems/:id | Medium — LTI integration |
Low Priority
| Route | Endpoints | Risk |
|---|---|---|
contact.ts | POST / | Low — contact form |
notifications.ts | GET /, PATCH /:id, DELETE /:id | Low |
preferences.ts | GET /, PATCH / | Low |
push.ts | POST /subscribe, POST /unsubscribe, POST /send-test | Low |
alerts.ts | POST /supabase-down, POST /file-too-large | Low |
tags.ts | POST /, GET /, PATCH /:id, DELETE /:id, PATCH /files/:fileId/tags | Low |
2. Frontend Integration Audit
Multi-Path Endpoints (canonical share bug pattern)
The original bug was: POST /api/share had two frontend call paths with different payloads, only one was exercised by tests.
Similar patterns found:
File Upload (3-step flow)
Step 1: POST /api/files/upload { fileName, fileType, fileSize }Step 2: PUT [presigned-R2-url] [binary]Step 3: PATCH /api/files/:fileId { status: 'uploaded' }Existing tests cover step 1 and step 3, but step 2 is direct-to-R2 (no API test needed). ✅
Conversion (2 paths to same endpoint)
Path 1: POST /api/convert { parser, includeMathml, pageRanges, ... } — full conversionPath 2: POST /api/convert/estimate { same } — cost estimate (different endpoint)The convert.test.ts covers the main flow but POST /api/convert/estimate is not tested.
Share (fixed but documented)
Path 1: POST /api/share { fileIds, emails: [...] } — ShareDialog (always has emails)Path 2: POST /api/share { fileIds, emails: [] } — copy-link button (empty emails)Both paths now covered in share.test.ts (regression test added). ✅
Optional/Empty Field Coverage Gaps
These combinations exist in the frontend but are not exercised by current tests:
| Endpoint | Optional Field | Frontend Path |
|---|---|---|
POST /api/convert | pageRanges omitted | Most uploads (no range selected) |
POST /api/convert | enhanceImages: false | Default conversion path |
PUT /api/files/:id/settings | All fields optional | Partial settings save |
POST /api/share | expiresIn omitted | Default expiry share |
POST /api/webhooks | events: [] | Empty events webhook |
3. E2E Test Assessment
Current state: No Playwright/E2E tests exist.
Recommendation: A small set of critical-path smoke tests would be high value:
- Upload → Convert → Download — the core user journey
- Share → Access via token — the exact flow that had the production bug
- Purchase credits → Credit balance updated — payment critical path
Full E2E is expensive to maintain. Recommend 3-5 smoke tests covering critical paths rather than comprehensive coverage. Medium-term priority — the unit/integration gap is more urgent.
4. Remediation Plan
Immediate (this sprint)
-
share.test.ts— added (triggered by prod bug) -
stripe.test.ts— payment webhook processing -
credits.test.ts— credit balance + purchase flow -
download.test.ts— presigned token verification -
admin.test.ts— admin endpoints + role enforcement
Short Term
-
remediate.test.ts -
reports.test.ts -
tenant.test.ts -
tenant-admin.test.ts -
budget-alerts.test.ts -
gateway.test.ts -
lti.test.ts
Low Priority
-
contact.test.ts -
notifications.test.ts -
preferences.test.ts -
push.test.ts -
alerts.test.ts -
tags.test.ts
Structural: Prevent Regressions
Add a CI check (scripts/check-route-test-coverage.mjs) that:
- Lists all
.tsfiles inworkers/api/src/routes/(excluding*-utils.tsandconvert-stream.ts) - For each, asserts a corresponding
.test.tsexists inworkers/api/src/__tests__/routes/ - Fails the CI build if any route is missing a test file
Run this as part of npm run test:ci.
5. Related GitHub Issues
- #151 — This report (parent issue)
- Issues created per gap: see linked issues in #151 comments
Appendix: How Tests Are Structured
Route tests follow this pattern (see share.test.ts as the canonical example):
import { describe, it, expect, vi, beforeEach } from 'vitest';import { Hono } from 'hono';import { myRoutes } from '../../routes/my-route';import { createMockEnv, testStorageMiddleware } from '../__mocks__/env';
describe('my-route routes', () => { let env: ReturnType<typeof createMockEnv>; let app: Hono;
beforeEach(() => { env = createMockEnv(); app = new Hono(); app.use('*', testStorageMiddleware); app.route('/api/my-route', myRoutes); });
it('returns 401 without auth', async () => { const res = await app.request('/api/my-route', {}, env); expect(res.status).toBe(401); }); // ...});Key helpers in __mocks__/env.ts:
createMockEnv()— mock Cloudflare bindings (KV, R2, Supabase)testStorageMiddleware— injects mock storage contextcreateMockKV(),createMockR2()— isolated in-memory stores