Skip to content

Commit 1acdd04

Browse files
author
Marvin Zhang
committed
Refactor test suite: Remove unit tests, add integration tests, and implement project API client tests
- Removed unit tests from `api.test.ts` to streamline the testing approach. - Introduced a new integration test suite in `api-integration.test.ts` for end-to-end testing of the Devlog Web API. - Added comprehensive tests for project operations, devlog operations, and error handling in the integration test suite. - Created a new test file `project-api-client.test.ts` to test the `ProjectApiClient` with mocked API responses. - Updated README documentation to reflect changes in the testing architecture and removed outdated mock configurations.
1 parent d92bd0d commit 1acdd04

File tree

6 files changed

+614
-670
lines changed

6 files changed

+614
-670
lines changed

packages/web/app/lib/api/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55

66
export * from './api-client';
77
export * from './devlog-api-client';
8+
export * from './project-api-client';
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
/**
2+
* Project API client for handling project-related HTTP requests
3+
*
4+
* This client provides a higher-level interface for project operations,
5+
* building on top of the base ApiClient for standardized error handling
6+
* and response format processing.
7+
*/
8+
9+
import { ApiClient, ApiError } from './api-client.js';
10+
import type { Project } from '@codervisor/devlog-core';
11+
12+
/**
13+
* Project creation request data
14+
*/
15+
export interface CreateProjectRequest {
16+
name: string;
17+
description?: string;
18+
}
19+
20+
/**
21+
* Project update request data
22+
*/
23+
export interface UpdateProjectRequest {
24+
name?: string;
25+
description?: string;
26+
}
27+
28+
/**
29+
* Project deletion response
30+
*/
31+
export interface DeleteProjectResponse {
32+
deleted: boolean;
33+
projectId: number;
34+
}
35+
36+
/**
37+
* Client for project-related API operations
38+
*
39+
* Provides typed methods for all project CRUD operations while leveraging
40+
* the base ApiClient for consistent error handling and response processing.
41+
*
42+
* @example
43+
* ```typescript
44+
* const projectClient = new ProjectApiClient();
45+
*
46+
* // List all projects
47+
* const projects = await projectClient.list();
48+
*
49+
* // Get a specific project
50+
* const project = await projectClient.get('my-project');
51+
*
52+
* // Create a new project
53+
* const newProject = await projectClient.create({
54+
* name: 'New Project',
55+
* description: 'A new project for testing'
56+
* });
57+
* ```
58+
*/
59+
export class ProjectApiClient {
60+
private apiClient: ApiClient;
61+
62+
/**
63+
* Create a new ProjectApiClient instance
64+
*
65+
* @param baseUrl - Optional base URL for API requests (defaults to current origin)
66+
*/
67+
constructor(baseUrl?: string) {
68+
this.apiClient = new ApiClient({ baseUrl: baseUrl || '' });
69+
}
70+
71+
/**
72+
* List all projects
73+
*
74+
* @returns Promise resolving to array of projects
75+
* @throws {ApiError} When the request fails or server returns an error
76+
*/
77+
async list(): Promise<Project[]> {
78+
try {
79+
return await this.apiClient.get<Project[]>('/api/projects');
80+
} catch (error) {
81+
if (error instanceof ApiError) {
82+
throw error;
83+
}
84+
throw new ApiError(
85+
'PROJECT_LIST_FAILED',
86+
'Failed to fetch projects list',
87+
500,
88+
{ originalError: error }
89+
);
90+
}
91+
}
92+
93+
/**
94+
* Get a specific project by name
95+
*
96+
* @param projectName - The name of the project to retrieve
97+
* @returns Promise resolving to the project data
98+
* @throws {ApiError} When the project is not found or request fails
99+
*/
100+
async get(projectName: string): Promise<Project> {
101+
if (!projectName || typeof projectName !== 'string') {
102+
throw new ApiError(
103+
'INVALID_PROJECT_NAME',
104+
'Project name must be a non-empty string',
105+
400
106+
);
107+
}
108+
109+
try {
110+
return await this.apiClient.get<Project>(`/api/projects/${encodeURIComponent(projectName)}`);
111+
} catch (error) {
112+
if (error instanceof ApiError) {
113+
// Re-throw API errors with additional context
114+
if (error.isNotFound()) {
115+
throw new ApiError(
116+
'PROJECT_NOT_FOUND',
117+
`Project '${projectName}' not found`,
118+
404,
119+
{ projectName }
120+
);
121+
}
122+
throw error;
123+
}
124+
throw new ApiError(
125+
'PROJECT_GET_FAILED',
126+
`Failed to fetch project '${projectName}'`,
127+
500,
128+
{ projectName, originalError: error }
129+
);
130+
}
131+
}
132+
133+
/**
134+
* Create a new project
135+
*
136+
* @param projectData - The project data for creation
137+
* @returns Promise resolving to the created project
138+
* @throws {ApiError} When validation fails or creation fails
139+
*/
140+
async create(projectData: CreateProjectRequest): Promise<Project> {
141+
if (!projectData || !projectData.name) {
142+
throw new ApiError(
143+
'INVALID_PROJECT_DATA',
144+
'Project name is required',
145+
400,
146+
{ providedData: projectData }
147+
);
148+
}
149+
150+
try {
151+
return await this.apiClient.post<Project>('/api/projects', projectData);
152+
} catch (error) {
153+
if (error instanceof ApiError) {
154+
// Enhance validation errors with more context
155+
if (error.isValidation()) {
156+
throw new ApiError(
157+
'PROJECT_VALIDATION_FAILED',
158+
error.message,
159+
422,
160+
{ ...error.details, projectData }
161+
);
162+
}
163+
throw error;
164+
}
165+
throw new ApiError(
166+
'PROJECT_CREATE_FAILED',
167+
'Failed to create project',
168+
500,
169+
{ projectData, originalError: error }
170+
);
171+
}
172+
}
173+
174+
/**
175+
* Update an existing project
176+
*
177+
* @param projectName - The name of the project to update
178+
* @param updates - The updates to apply to the project
179+
* @returns Promise resolving to the updated project
180+
* @throws {ApiError} When the project is not found or update fails
181+
*/
182+
async update(projectName: string, updates: UpdateProjectRequest): Promise<Project> {
183+
if (!projectName || typeof projectName !== 'string') {
184+
throw new ApiError(
185+
'INVALID_PROJECT_NAME',
186+
'Project name must be a non-empty string',
187+
400
188+
);
189+
}
190+
191+
if (!updates || Object.keys(updates).length === 0) {
192+
throw new ApiError(
193+
'INVALID_UPDATE_DATA',
194+
'At least one field must be provided for update',
195+
400,
196+
{ providedData: updates }
197+
);
198+
}
199+
200+
try {
201+
return await this.apiClient.put<Project>(
202+
`/api/projects/${encodeURIComponent(projectName)}`,
203+
updates
204+
);
205+
} catch (error) {
206+
if (error instanceof ApiError) {
207+
if (error.isNotFound()) {
208+
throw new ApiError(
209+
'PROJECT_NOT_FOUND',
210+
`Project '${projectName}' not found`,
211+
404,
212+
{ projectName }
213+
);
214+
}
215+
if (error.isValidation()) {
216+
throw new ApiError(
217+
'PROJECT_VALIDATION_FAILED',
218+
error.message,
219+
422,
220+
{ ...error.details, projectName, updates }
221+
);
222+
}
223+
throw error;
224+
}
225+
throw new ApiError(
226+
'PROJECT_UPDATE_FAILED',
227+
`Failed to update project '${projectName}'`,
228+
500,
229+
{ projectName, updates, originalError: error }
230+
);
231+
}
232+
}
233+
234+
/**
235+
* Delete a project
236+
*
237+
* @param projectName - The name of the project to delete
238+
* @returns Promise resolving to deletion confirmation
239+
* @throws {ApiError} When the project is not found or deletion fails
240+
*/
241+
async delete(projectName: string): Promise<DeleteProjectResponse> {
242+
if (!projectName || typeof projectName !== 'string') {
243+
throw new ApiError(
244+
'INVALID_PROJECT_NAME',
245+
'Project name must be a non-empty string',
246+
400
247+
);
248+
}
249+
250+
try {
251+
return await this.apiClient.delete<DeleteProjectResponse>(
252+
`/api/projects/${encodeURIComponent(projectName)}`
253+
);
254+
} catch (error) {
255+
if (error instanceof ApiError) {
256+
if (error.isNotFound()) {
257+
throw new ApiError(
258+
'PROJECT_NOT_FOUND',
259+
`Project '${projectName}' not found`,
260+
404,
261+
{ projectName }
262+
);
263+
}
264+
throw error;
265+
}
266+
throw new ApiError(
267+
'PROJECT_DELETE_FAILED',
268+
`Failed to delete project '${projectName}'`,
269+
500,
270+
{ projectName, originalError: error }
271+
);
272+
}
273+
}
274+
275+
/**
276+
* Check if a project exists
277+
*
278+
* @param projectName - The name of the project to check
279+
* @returns Promise resolving to true if project exists, false otherwise
280+
*/
281+
async exists(projectName: string): Promise<boolean> {
282+
try {
283+
await this.get(projectName);
284+
return true;
285+
} catch (error) {
286+
if (error instanceof ApiError && error.isNotFound()) {
287+
return false;
288+
}
289+
// Re-throw non-404 errors
290+
throw error;
291+
}
292+
}
293+
}
294+
295+
/**
296+
* Default project API client instance
297+
*
298+
* Pre-configured client ready to use throughout the application
299+
*/
300+
export const projectApiClient = new ProjectApiClient();
301+
302+
/**
303+
* Type guard to check if an error is related to project operations
304+
*/
305+
export function isProjectApiError(error: unknown): error is ApiError {
306+
return error instanceof ApiError && (
307+
error.code.startsWith('PROJECT_') ||
308+
error.code.includes('PROJECT')
309+
);
310+
}

packages/web/tests/README.md

Lines changed: 3 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,6 @@ This document describes the comprehensive test suite for the Devlog Web API, des
66

77
## Test Architecture
88

9-
### 🔬 **Unit Tests** (`tests/api.test.ts`)
10-
11-
- **Isolated testing** using mocks and no external dependencies
12-
- **Fast execution** - runs in milliseconds
13-
- **Safe for CI/CD** - no database or network dependencies
14-
- **Always run** during development and deployment
15-
169
### 🚀 **Integration Tests** (`tests/api-integration.test.ts`)
1710

1811
- **End-to-end testing** against actual API endpoints
@@ -153,26 +146,6 @@ DATABASE_URL=sqlite::memory: # In-memory database for unit tests
153146
NODE_ENV=test # Test environment marker
154147
```
155148

156-
### Mock Configuration
157-
158-
```typescript
159-
// Service mocks
160-
const mockProjectService = {
161-
getInstance: vi.fn(),
162-
get: vi.fn(),
163-
update: vi.fn(),
164-
delete: vi.fn(),
165-
};
166-
167-
const mockDevlogService = {
168-
getInstance: vi.fn(),
169-
get: vi.fn(),
170-
save: vi.fn(),
171-
delete: vi.fn(),
172-
// ... other methods
173-
};
174-
```
175-
176149
## Test Safety Features
177150

178151
### 🛡️ **Production Protection**
@@ -200,10 +173,9 @@ const mockDevlogService = {
200173

201174
### Adding New Tests
202175

203-
1. **Unit tests**: Add to `tests/api.test.ts` with proper mocking
204-
2. **Integration tests**: Add to `tests/api-integration.test.ts` with safety guards
205-
3. **New utilities**: Mock in test setup and add comprehensive unit tests
206-
4. **New endpoints**: Follow existing patterns for parameter validation
176+
1. **Integration tests**: Add to `tests/api-integration.test.ts` with safety guards
177+
2. **New utilities**: Mock in test setup and add comprehensive unit tests
178+
3. **New endpoints**: Follow existing patterns for parameter validation
207179

208180
### Test Data Management
209181

@@ -267,9 +239,3 @@ DEBUG=* pnpm --filter @codervisor/devlog-web test
267239
- ✅ All service integration patterns verified
268240
- ✅ Response format consistency validated
269241
- ✅ No production data dependencies
270-
271-
---
272-
273-
**Status**: ✅ **COMPLETE - Comprehensive test suite implemented with safety isolation**
274-
275-
The test suite provides robust coverage of the API route overhaul while maintaining complete isolation from production systems through extensive mocking and environment-specific configurations.

0 commit comments

Comments
 (0)