@dbs-portal/tool-mock
Version:
API mocking toolkit using MSW for DBS Portal development workflows
665 lines (577 loc) • 16.8 kB
Markdown
# Usage Examples - @dbs-portal/tool-mock
Real-world examples demonstrating common usage patterns and integration scenarios.
## Basic Setup Examples
### Quick Start - Auto Setup
```typescript
// main.tsx
import { autoSetupMocks } from '@dbs-portal/tool-mock'
// Automatically setup MSW based on environment
await autoSetupMocks()
// Start your React app
ReactDOM.createRoot(document.getElementById('root')!).render(<App />)
```
### Manual Setup with Custom Configuration
```typescript
// main.tsx
import { setupMocks, createCrudHandlers } from '@dbs-portal/tool-mock'
await setupMocks({
config: {
enabled: process.env.NODE_ENV === 'development',
mode: 'development',
logging: true,
delay: [100, 300],
errorSimulation: {
networkErrorRate: 0.01, // 1% network errors
serverErrorRate: 0.005, // 0.5% server errors
},
},
handlers: [
...createCrudHandlers({
basePath: '/api/users',
dataFactory: userFactory,
}),
],
})
```
## CRUD Operations
### Complete User Management Example
```typescript
// types/user.ts
export interface User {
id: string
email: string
firstName: string
lastName: string
department: string
role: string
isActive: boolean
createdAt: string
updatedAt: string
}
export interface CreateUserDto {
email: string
firstName: string
lastName: string
department: string
role: string
}
export interface UpdateUserDto extends Partial<CreateUserDto> {}
```
```typescript
// mocks/user-factory.ts
import { createDataFactory, generateId } from '@dbs-portal/tool-mock'
import type { User } from '../types/user'
export const userFactory = createDataFactory<User>((overrides = {}) => ({
id: generateId(),
email: `user${Math.floor(Math.random() * 1000)}@example.com`,
firstName: ['John', 'Jane', 'Mike', 'Sarah', 'David'][Math.floor(Math.random() * 5)],
lastName: ['Doe', 'Smith', 'Johnson', 'Brown', 'Davis'][Math.floor(Math.random() * 5)],
department: ['Engineering', 'Marketing', 'Sales', 'HR', 'Finance'][Math.floor(Math.random() * 5)],
role: ['Developer', 'Manager', 'Analyst', 'Coordinator'][Math.floor(Math.random() * 4)],
isActive: Math.random() > 0.2, // 80% active
createdAt: new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000).toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
}))
```
```typescript
// mocks/user-handlers.ts
import { createCrudHandlers } from '@dbs-portal/tool-mock'
import { userFactory } from './user-factory'
export const userHandlers = createCrudHandlers({
basePath: '/api/users',
dataFactory: userFactory,
initialData: [
userFactory({
id: '1',
email: 'admin@example.com',
firstName: 'Admin',
lastName: 'User',
role: 'Admin',
department: 'IT',
}),
userFactory({
id: '2',
email: 'manager@example.com',
firstName: 'Manager',
lastName: 'User',
role: 'Manager',
department: 'Engineering',
}),
...Array.from({ length: 48 }, () => userFactory()), // 50 total users
],
pagination: {
defaultPageSize: 10,
maxPageSize: 100,
},
validate: (data) => {
const errors: string[] = []
if (!data.email) {
errors.push('Email is required')
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
errors.push('Email format is invalid')
}
if (!data.firstName) errors.push('First name is required')
if (!data.lastName) errors.push('Last name is required')
if (!data.department) errors.push('Department is required')
if (!data.role) errors.push('Role is required')
return errors.length > 0 ? errors : null
},
})
```
### Custom Endpoints
```typescript
// mocks/custom-handlers.ts
import { http, HttpResponse } from 'msw'
import { mockResponseBuilder } from '@dbs-portal/tool-mock'
import { userFactory } from './user-factory'
export const customUserHandlers = [
// Bulk operations
http.post('/api/users/bulk', async ({ request }) => {
const { userIds, action } = await request.json()
return mockResponseBuilder()
.data({
processedCount: userIds.length,
action,
timestamp: new Date().toISOString(),
})
.status(200)
.delay([200, 500])
.build()
}),
// User statistics
http.get('/api/users/stats', () => {
return HttpResponse.json({
success: true,
data: {
total: 50,
active: 40,
inactive: 10,
byDepartment: {
Engineering: 15,
Marketing: 10,
Sales: 12,
HR: 8,
Finance: 5,
},
byRole: {
Developer: 20,
Manager: 10,
Analyst: 12,
Coordinator: 8,
},
},
})
}),
// User search
http.get('/api/users/search', ({ request }) => {
const url = new URL(request.url)
const query = url.searchParams.get('q') || ''
const department = url.searchParams.get('department')
// Simulate search results
const results = Array.from({ length: Math.floor(Math.random() * 10) + 1 }, () =>
userFactory({
firstName: query ? `${query}Match` : undefined,
department: department || undefined,
})
)
return HttpResponse.json({
success: true,
data: results,
meta: {
query,
total: results.length,
},
})
}),
// User avatar upload
http.post('/api/users/:id/avatar', ({ params }) => {
return mockResponseBuilder()
.data({
userId: params.id,
avatarUrl: `https://api.dicebear.com/7.x/avataaars/svg?seed=${params.id}`,
uploadedAt: new Date().toISOString(),
})
.status(201)
.delay([1000, 2000]) // Simulate file upload time
.build()
}),
]
```
## Authentication Examples
### Complete Auth Flow
```typescript
// mocks/auth-handlers.ts
import { createAuthHandlers } from '@dbs-portal/tool-mock'
export const authHandlers = createAuthHandlers('/api/auth', {
users: [
{
id: '1',
email: 'admin@example.com',
password: 'admin123',
firstName: 'Admin',
lastName: 'User',
roles: ['admin'],
permissions: [
'users:read',
'users:write',
'users:delete',
'roles:read',
'roles:write',
'settings:read',
'settings:write',
],
},
{
id: '2',
email: 'manager@example.com',
password: 'manager123',
firstName: 'Manager',
lastName: 'User',
roles: ['manager'],
permissions: [
'users:read',
'users:write',
'roles:read',
],
},
{
id: '3',
email: 'user@example.com',
password: 'user123',
firstName: 'Regular',
lastName: 'User',
roles: ['user'],
permissions: [
'users:read',
],
},
],
tokenExpiry: '1h',
refreshTokenExpiry: '7d',
enableRegistration: true,
enablePasswordReset: true,
})
```
### Protected Routes Example
```typescript
// mocks/protected-handlers.ts
import { withMockAuth, createCrudHandlers } from '@dbs-portal/tool-mock'
import { userFactory } from './user-factory'
// Admin-only user management
const adminUserHandlers = withMockAuth(
createCrudHandlers({
basePath: '/api/admin/users',
dataFactory: userFactory,
}),
{
requireAuth: true,
requiredRoles: ['admin'],
requiredPermissions: ['users:write'],
}
)
// Manager-level user viewing
const managerUserHandlers = withMockAuth(
[
http.get('/api/manager/users', () => {
return HttpResponse.json({
success: true,
data: Array.from({ length: 10 }, () => userFactory()),
})
}),
],
{
requireAuth: true,
requiredRoles: ['manager', 'admin'],
requiredPermissions: ['users:read'],
}
)
// User profile (own data only)
const profileHandlers = withMockAuth(
[
http.get('/api/profile', ({ request }) => {
// Extract user from auth token (mocked)
const authHeader = request.headers.get('Authorization')
const userId = extractUserIdFromToken(authHeader) // Mock implementation
return HttpResponse.json({
success: true,
data: userFactory({ id: userId }),
})
}),
],
{
requireAuth: true,
// No role/permission requirements - any authenticated user
}
)
```
## Error Simulation Examples
### Network Conditions
```typescript
// mocks/error-simulation.ts
import { setupMocks, createCrudHandlers } from '@dbs-portal/tool-mock'
await setupMocks({
config: {
errorSimulation: {
networkErrorRate: 0.05, // 5% network errors
serverErrorRate: 0.02, // 2% server errors
timeoutErrorRate: 0.01, // 1% timeout errors
customErrors: [
{
pattern: '/api/users',
method: 'POST',
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'Too many requests',
status: 429,
},
probability: 0.1, // 10% chance for user creation
},
{
pattern: /\/api\/users\/\d+/,
method: 'DELETE',
error: () => ({
code: 'CANNOT_DELETE_ADMIN',
message: 'Cannot delete admin user',
status: 403,
}),
probability: 0.3, // 30% chance for user deletion
},
],
},
},
handlers: [
...createCrudHandlers({
basePath: '/api/users',
dataFactory: userFactory,
}),
],
})
```
### Specific Error Scenarios
```typescript
// mocks/error-handlers.ts
import { http, HttpResponse } from 'msw'
import { createErrorResponse } from '@dbs-portal/tool-mock'
export const errorHandlers = [
// Validation errors
http.post('/api/users', async ({ request }) => {
const data = await request.json()
// Simulate validation errors
if (!data.email || !data.firstName) {
return createErrorResponse({
code: 'VALIDATION_ERROR',
message: 'Validation failed',
details: {
email: !data.email ? ['Email is required'] : [],
firstName: !data.firstName ? ['First name is required'] : [],
},
status: 400,
})
}
// Simulate duplicate email
if (data.email === 'duplicate@example.com') {
return createErrorResponse({
code: 'DUPLICATE_EMAIL',
message: 'Email already exists',
status: 409,
})
}
// Success case
return HttpResponse.json({
success: true,
data: userFactory(data),
})
}),
// Server errors
http.get('/api/users/problematic-endpoint', () => {
return createErrorResponse({
code: 'INTERNAL_SERVER_ERROR',
message: 'Something went wrong on our end',
status: 500,
})
}),
// Timeout simulation
http.get('/api/users/slow-endpoint', () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(HttpResponse.json({
success: true,
data: [],
}))
}, 10000) // 10 second delay
})
}),
]
```
## File Upload Examples
### File Upload Mocking
```typescript
// mocks/file-handlers.ts
import { http, HttpResponse } from 'msw'
import { mockResponseBuilder, generateId } from '@dbs-portal/tool-mock'
export const fileHandlers = [
// Single file upload
http.post('/api/files/upload', async ({ request }) => {
const formData = await request.formData()
const file = formData.get('file') as File
if (!file) {
return mockResponseBuilder()
.error({
code: 'NO_FILE_PROVIDED',
message: 'No file provided',
})
.status(400)
.build()
}
// Simulate file validation
const maxSize = 10 * 1024 * 1024 // 10MB
if (file.size > maxSize) {
return mockResponseBuilder()
.error({
code: 'FILE_TOO_LARGE',
message: 'File size exceeds 10MB limit',
})
.status(413)
.build()
}
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf']
if (!allowedTypes.includes(file.type)) {
return mockResponseBuilder()
.error({
code: 'INVALID_FILE_TYPE',
message: 'File type not allowed',
})
.status(415)
.build()
}
// Simulate upload progress (in real app, this would be handled differently)
return mockResponseBuilder()
.data({
id: generateId(),
filename: file.name,
originalName: file.name,
mimeType: file.type,
size: file.size,
url: `https://example.com/files/${generateId()}`,
uploadedAt: new Date().toISOString(),
})
.status(201)
.delay([1000, 3000]) // Simulate upload time
.build()
}),
// Multiple file upload
http.post('/api/files/upload/multiple', async ({ request }) => {
const formData = await request.formData()
const files = formData.getAll('files') as File[]
const uploadedFiles = files.map((file) => ({
id: generateId(),
filename: file.name,
originalName: file.name,
mimeType: file.type,
size: file.size,
url: `https://example.com/files/${generateId()}`,
uploadedAt: new Date().toISOString(),
}))
return mockResponseBuilder()
.data({
files: uploadedFiles,
totalCount: uploadedFiles.length,
totalSize: uploadedFiles.reduce((sum, file) => sum + file.size, 0),
})
.status(201)
.delay([2000, 5000]) // Longer delay for multiple files
.build()
}),
// File download
http.get('/api/files/:id/download', ({ params }) => {
// Simulate file content
const content = new Blob(['Mock file content'], { type: 'text/plain' })
return new HttpResponse(content, {
status: 200,
headers: {
'Content-Type': 'application/octet-stream',
'Content-Disposition': `attachment; filename="file-${params.id}.txt"`,
},
})
}),
]
```
## Testing Examples
### Component Testing with MSW
```typescript
// __tests__/UserList.test.tsx
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { mockApiCall, userFactory } from '@dbs-portal/tool-mock'
import { UserList } from '../UserList'
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
})
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}
describe('UserList', () => {
test('should display users', async () => {
const mockUsers = [
userFactory({ id: '1', firstName: 'John', lastName: 'Doe' }),
userFactory({ id: '2', firstName: 'Jane', lastName: 'Smith' }),
]
mockApiCall('GET', '/api/users', {
success: true,
data: mockUsers,
meta: { total: 2, page: 1, pageSize: 10 },
})
render(<UserList />, { wrapper: createWrapper() })
expect(await screen.findByText('John Doe')).toBeInTheDocument()
expect(await screen.findByText('Jane Smith')).toBeInTheDocument()
})
test('should handle loading state', async () => {
mockApiCall('GET', '/api/users', {
delay: 1000,
success: true,
data: [],
})
render(<UserList />, { wrapper: createWrapper() })
expect(screen.getByText('Loading...')).toBeInTheDocument()
})
test('should handle error state', async () => {
mockApiCall('GET', '/api/users', {
status: 500,
success: false,
error: { message: 'Internal server error' },
})
render(<UserList />, { wrapper: createWrapper() })
expect(await screen.findByText(/error/i)).toBeInTheDocument()
})
test('should handle user creation', async () => {
const user = userEvent.setup()
const newUser = userFactory({ firstName: 'New', lastName: 'User' })
// Mock the create endpoint
mockApiCall('POST', '/api/users', {
success: true,
data: newUser,
})
// Mock the updated list
mockApiCall('GET', '/api/users', {
success: true,
data: [newUser],
})
render(<UserList />, { wrapper: createWrapper() })
const createButton = screen.getByRole('button', { name: /create user/i })
await user.click(createButton)
// Fill form and submit
await user.type(screen.getByLabelText(/first name/i), 'New')
await user.type(screen.getByLabelText(/last name/i), 'User')
await user.click(screen.getByRole('button', { name: /save/i }))
expect(await screen.findByText('New User')).toBeInTheDocument()
})
})