Skip to content

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

CategoryTotalCoveredGap
API route files26818 missing
Service files~50~455 partial
Frontend components~40~3Low priority

Overall route test coverage: 31% (8 of 26 route files have tests).


1. Route Coverage Audit

Routes With Test Files ✅

Route FileTest FileTests
auth.tsauth.test.ts2
files.tsfiles.test.ts16
convert.tsconvert.test.ts~20 (mocked services)
export.tsexport.test.ts8
benchmark.tsbenchmark.test.ts7
share.tsshare.test.ts22 — added 2026-03-21 after prod bug
spend-controls.tsspend-controls.test.ts7
webhooks.tswebhooks.test.ts22

Routes Missing Test Files ❌

Grouped by priority:

High Priority (financial / security / data access)

RouteEndpointsRisk
stripe.tsPOST /webhook, POST /checkout, GET /session/:idCritical — payment processing, Stripe event handling
credits.tsGET /, GET /packages, POST /checkoutCritical — credit balance, purchase flow
download.tsGET /:fileId/zip?token=High — presigned URL token verification, file access control
admin.tsGET /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 /predictionsHigh — admin-only operations, data integrity

Medium Priority

RouteEndpointsRisk
remediate.tsPOST /:fileIdMedium — accessibility remediation processing
reports.tsGET /:fileId, GET /:fileId/downloadMedium — report generation/download
tenant.tsGET /, POST /verify-domainMedium — multi-tenancy
tenant-admin.tsGET /users, GET /settings, PUT /settingsMedium — tenant admin
budget-alerts.tsGET /, POST /, PUT /:id, DELETE /:idMedium — budget enforcement
gateway.tsGET /:urlMedium — proxy, security validation
lti.tsGET /login, GET /callback, POST /lineitems, GET /lineitems/:idMedium — LTI integration

Low Priority

RouteEndpointsRisk
contact.tsPOST /Low — contact form
notifications.tsGET /, PATCH /:id, DELETE /:idLow
preferences.tsGET /, PATCH /Low
push.tsPOST /subscribe, POST /unsubscribe, POST /send-testLow
alerts.tsPOST /supabase-down, POST /file-too-largeLow
tags.tsPOST /, GET /, PATCH /:id, DELETE /:id, PATCH /files/:fileId/tagsLow

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 conversion
Path 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:

EndpointOptional FieldFrontend Path
POST /api/convertpageRanges omittedMost uploads (no range selected)
POST /api/convertenhanceImages: falseDefault conversion path
PUT /api/files/:id/settingsAll fields optionalPartial settings save
POST /api/shareexpiresIn omittedDefault expiry share
POST /api/webhooksevents: []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:

  1. Upload → Convert → Download — the core user journey
  2. Share → Access via token — the exact flow that had the production bug
  3. 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:

  1. Lists all .ts files in workers/api/src/routes/ (excluding *-utils.ts and convert-stream.ts)
  2. For each, asserts a corresponding .test.ts exists in workers/api/src/__tests__/routes/
  3. Fails the CI build if any route is missing a test file

Run this as part of npm run test:ci.


  • #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 context
  • createMockKV(), createMockR2() — isolated in-memory stores