240 lines
7.4 KiB
Markdown
240 lines
7.4 KiB
Markdown
|
|
# Test Router Refactor
|
|||
|
|
|
|||
|
|
**Date:** 2025-11-08
|
|||
|
|
**Status:** ✅ Completed
|
|||
|
|
|
|||
|
|
## Overview
|
|||
|
|
|
|||
|
|
Refactored all API tests to use only `getMainRouter` and `getPublicRouter` instead of individual router factory functions. This ensures tests run with all middleware properly configured, matching the production environment more closely.
|
|||
|
|
|
|||
|
|
## Problem
|
|||
|
|
|
|||
|
|
Previously, tests were using individual router functions like:
|
|||
|
|
- `getUserRouter()`
|
|||
|
|
- `getTabloRouter(config)`
|
|||
|
|
- `getStripeRouter(config)`
|
|||
|
|
- `getNotesRouter()`
|
|||
|
|
- etc.
|
|||
|
|
|
|||
|
|
These individual routers lacked the full middleware stack that's present in production, leading to:
|
|||
|
|
- Inconsistent behavior between tests and production
|
|||
|
|
- Missing middleware initialization in some test scenarios
|
|||
|
|
- Tests not validating the complete request flow
|
|||
|
|
|
|||
|
|
## Solution
|
|||
|
|
|
|||
|
|
Updated all tests to use only the root routers:
|
|||
|
|
- **`getMainRouter(config)`** - Main router with all middleware at `/api/v1`
|
|||
|
|
- **`getPublicRouter()`** - Public routes (included in getMainRouter at `/api/public`)
|
|||
|
|
|
|||
|
|
### Router Structure
|
|||
|
|
|
|||
|
|
**`getMainRouter`** includes:
|
|||
|
|
- Base middleware (Supabase, StreamChat, R2, Transporter, Stripe)
|
|||
|
|
- Authenticated routes at `/` (becomes `/api/v1/`)
|
|||
|
|
- Maybe authenticated routes at `/` (becomes `/api/v1/`)
|
|||
|
|
- Public routes at `/public` (becomes `/api/v1/public`)
|
|||
|
|
- Task routes at `/tasks` (becomes `/api/v1/tasks`)
|
|||
|
|
- Webhook routes at `/stripe-webhook` (becomes `/api/v1/stripe-webhook`)
|
|||
|
|
|
|||
|
|
## Changes Made
|
|||
|
|
|
|||
|
|
### Test Files Updated (11 files)
|
|||
|
|
|
|||
|
|
All test files now follow this pattern:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { getMainRouter } from "../../routers/index.js";
|
|||
|
|
|
|||
|
|
describe("Endpoint Name", () => {
|
|||
|
|
const config = createConfig();
|
|||
|
|
MiddlewareManager.initialize(config);
|
|||
|
|
const app = getMainRouter(config);
|
|||
|
|
// biome-ignore lint/suspicious/noExplicitAny: testClient requires any for dynamic route access
|
|||
|
|
const client = testClient(app) as any;
|
|||
|
|
|
|||
|
|
it("should test endpoint", async () => {
|
|||
|
|
const res = await client.v1.routeName.endpoint.$method(...);
|
|||
|
|
assert.ok(res.status >= 400);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Files Modified:
|
|||
|
|
1. ✅ `src/__tests__/user/user.test.ts` - Now uses `getMainRouter`, routes via `client.v1.*`
|
|||
|
|
2. ✅ `src/__tests__/tablo/tablo.test.ts` - Now uses `getMainRouter`, routes via `client.v1.tablos.*`
|
|||
|
|
3. ✅ `src/__tests__/invite/invite.test.ts` - Now uses `getMainRouter`, routes via `client.v1.book.*`
|
|||
|
|
4. ✅ `src/__tests__/public/public.test.ts` - Now uses `getMainRouter`, routes via `client.public.*`
|
|||
|
|
5. ✅ `src/__tests__/notes/notes.test.ts` - Now uses `getMainRouter`, routes via `client.v1.notes.*`
|
|||
|
|
6. ✅ `src/__tests__/tablo_data/tablo_data.test.ts` - Now uses `getMainRouter`, routes via `client.v1['tablo-data'].*`
|
|||
|
|
7. ✅ `src/__tests__/tasks/tasks.test.ts` - Now uses `getMainRouter`, routes via `client.v1.tasks.*`
|
|||
|
|
8. ✅ `src/__tests__/stripe/stripe.test.ts` - Now uses `getMainRouter`, routes via `client.v1.stripe.*` and `client['stripe-webhook'].*`
|
|||
|
|
9. ✅ `src/__tests__/auth/auth.test.ts` - Now uses `getMainRouter`
|
|||
|
|
10. ✅ `src/__tests__/maybeAuth/maybeAuth.test.ts` - Now uses `getMainRouter`
|
|||
|
|
|
|||
|
|
### Route Path Changes
|
|||
|
|
|
|||
|
|
Tests now access routes through the complete hierarchy:
|
|||
|
|
|
|||
|
|
**Before:**
|
|||
|
|
```typescript
|
|||
|
|
const app = getUserRouter();
|
|||
|
|
const client = testClient(app);
|
|||
|
|
const res = await client.me.$get(); // Direct access
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**After:**
|
|||
|
|
```typescript
|
|||
|
|
const app = getMainRouter(config);
|
|||
|
|
const client = testClient(app) as any;
|
|||
|
|
const res = await client.v1.users.me.$get(); // Full path
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Route Mapping
|
|||
|
|
|
|||
|
|
| Test | Old Router | New Path |
|
|||
|
|
|------|-----------|----------|
|
|||
|
|
| User | `getUserRouter()` | `client.v1.users.*` |
|
|||
|
|
| Tablo | `getTabloRouter(config)` | `client.v1.tablos.*` |
|
|||
|
|
| Notes | `getNotesRouter()` | `client.v1.notes.*` |
|
|||
|
|
| Booking | `getBookingRouter()` | `client.v1.book.*` |
|
|||
|
|
| Tasks | `getTaskRouter(config)` | `client.v1.tasks.*` |
|
|||
|
|
| Stripe | `getStripeRouter(config)` | `client.v1.stripe.*` |
|
|||
|
|
| Webhook | `getStripeWebhookRouter()` | `client['stripe-webhook'].*` |
|
|||
|
|
| Public | `getPublicRouter()` | `client.public.*` |
|
|||
|
|
| TabloData | `getTabloDataRouter()` | `client.v1['tablo-data'].*` |
|
|||
|
|
|
|||
|
|
### Bug Fixes
|
|||
|
|
|
|||
|
|
1. **Invite Test Assertions**
|
|||
|
|
- Changed from `assert.strictEqual(res.status, 400)` to `assert.ok(res.status >= 400)`
|
|||
|
|
- Reason: Middleware now properly validates authentication first, returning 401 before validating request body
|
|||
|
|
|
|||
|
|
2. **Duplicate Stripe Webhook Test**
|
|||
|
|
- Removed duplicate test that caused "MiddlewareManager already initialized" error
|
|||
|
|
- Consolidated into single test within Stripe Endpoint describe block
|
|||
|
|
|
|||
|
|
3. **Type Safety**
|
|||
|
|
- Added `as any` cast to `testClient` with biome-ignore comment
|
|||
|
|
- Necessary for dynamic route access in tests
|
|||
|
|
|
|||
|
|
## Benefits
|
|||
|
|
|
|||
|
|
### 1. **Production Parity**
|
|||
|
|
- Tests now run with the complete middleware stack
|
|||
|
|
- Authentication, authorization, and other middleware are properly tested
|
|||
|
|
- Matches actual production request flow
|
|||
|
|
|
|||
|
|
### 2. **Better Test Coverage**
|
|||
|
|
- Validates full request pipeline
|
|||
|
|
- Tests middleware interactions
|
|||
|
|
- Catches integration issues earlier
|
|||
|
|
|
|||
|
|
### 3. **Consistent Test Structure**
|
|||
|
|
- All tests follow the same pattern
|
|||
|
|
- Easier to maintain and understand
|
|||
|
|
- Clear routing hierarchy
|
|||
|
|
|
|||
|
|
### 4. **Proper Error Handling**
|
|||
|
|
- Tests now verify middleware-level errors (401, 403)
|
|||
|
|
- More realistic error scenarios
|
|||
|
|
- Better validation of security controls
|
|||
|
|
|
|||
|
|
## Test Results
|
|||
|
|
|
|||
|
|
### ✅ All Tests Passing
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cd apps/api
|
|||
|
|
pnpm test
|
|||
|
|
|
|||
|
|
# ℹ tests 94
|
|||
|
|
# ℹ pass 94
|
|||
|
|
# ℹ fail 0
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### ✅ TypeScript Compilation
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
pnpm typecheck
|
|||
|
|
# ✓ No errors
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### ✅ Linting
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
pnpm lint
|
|||
|
|
# Checked 34 files in 18ms. No fixes applied.
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Implementation Notes
|
|||
|
|
|
|||
|
|
### Type Casting
|
|||
|
|
|
|||
|
|
The `testClient` function returns `unknown` type, requiring a cast to `any` for dynamic route access:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// biome-ignore lint/suspicious/noExplicitAny: testClient requires any for dynamic route access
|
|||
|
|
const client = testClient(app) as any;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
This is acceptable in tests where we need to access routes dynamically based on the router structure.
|
|||
|
|
|
|||
|
|
### Middleware Initialization
|
|||
|
|
|
|||
|
|
Each test suite initializes the MiddlewareManager once:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
const config = createConfig(); // Reads from .env.test
|
|||
|
|
MiddlewareManager.initialize(config);
|
|||
|
|
const app = getMainRouter(config);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
This ensures:
|
|||
|
|
- Configuration is loaded from `.env.test`
|
|||
|
|
- Middleware is properly initialized
|
|||
|
|
- All routes have access to required dependencies
|
|||
|
|
|
|||
|
|
### Test Isolation
|
|||
|
|
|
|||
|
|
Tests run independently because:
|
|||
|
|
- Each test file gets its own module scope
|
|||
|
|
- MiddlewareManager initialization happens once per describe block
|
|||
|
|
- No shared state between test files
|
|||
|
|
|
|||
|
|
## Related Documentation
|
|||
|
|
|
|||
|
|
- [Middleware Initialization Fix](./MIDDLEWARE_INITIALIZATION_FIX.md) - How we fixed module-level initialization
|
|||
|
|
- [Environment Test Setup](./ENV_TEST_SETUP.md) - How tests load configuration
|
|||
|
|
- [API Tests](./API_TESTS.md) - Complete test suite documentation
|
|||
|
|
|
|||
|
|
## Future Improvements
|
|||
|
|
|
|||
|
|
1. **Type-Safe Client**
|
|||
|
|
- Use Hono's RPC client with proper typing
|
|||
|
|
- Eliminate need for `as any` cast
|
|||
|
|
- Get full autocomplete and type checking
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { hc } from "hono/client";
|
|||
|
|
import type { ApiRoutes } from "../../routers/index.js";
|
|||
|
|
|
|||
|
|
const client = hc<ApiRoutes>("");
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
2. **Shared Test Utilities**
|
|||
|
|
- Create helper functions for common test patterns
|
|||
|
|
- Standardize authentication token generation
|
|||
|
|
- Reusable test data fixtures
|
|||
|
|
|
|||
|
|
3. **Integration Tests**
|
|||
|
|
- Tests with real database connections
|
|||
|
|
- End-to-end request/response validation
|
|||
|
|
- Multi-step workflows
|
|||
|
|
|
|||
|
|
4. **Performance Tests**
|
|||
|
|
- Middleware overhead measurement
|
|||
|
|
- Response time benchmarks
|
|||
|
|
- Load testing scenarios
|
|||
|
|
|