- Added generated index files and report directories to .gitignore to prevent unnecessary tracking of transient files. - Updated README links to reflect new documentation paths for better navigation. - Improved documentation organization by ensuring all links point to the correct locations, enhancing user experience and accessibility.
315 lines
7.2 KiB
Markdown
315 lines
7.2 KiB
Markdown
# Test Examples and Patterns
|
|
|
|
This document provides examples and patterns for writing tests in the Sankofa Phoenix project.
|
|
|
|
## Unit Tests
|
|
|
|
### Testing Service Functions
|
|
|
|
```typescript
|
|
// api/src/services/auth.test.ts
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { login } from './auth'
|
|
import { getDb } from '../db'
|
|
import { AppErrors } from '../lib/errors'
|
|
|
|
// Mock dependencies
|
|
vi.mock('../db')
|
|
vi.mock('../lib/errors')
|
|
|
|
describe('auth service', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('should authenticate valid user', async () => {
|
|
const mockDb = {
|
|
query: vi.fn().mockResolvedValue({
|
|
rows: [{
|
|
id: '1',
|
|
email: 'user@example.com',
|
|
name: 'Test User',
|
|
password_hash: '$2a$10$hashed',
|
|
role: 'USER',
|
|
created_at: new Date(),
|
|
updated_at: new Date(),
|
|
}]
|
|
})
|
|
}
|
|
|
|
vi.mocked(getDb).mockReturnValue(mockDb as any)
|
|
// Mock bcrypt.compare to return true
|
|
vi.mock('bcryptjs', () => ({
|
|
compare: vi.fn().mockResolvedValue(true)
|
|
}))
|
|
|
|
const result = await login('user@example.com', 'password123')
|
|
|
|
expect(result).toHaveProperty('token')
|
|
expect(result.user.email).toBe('user@example.com')
|
|
})
|
|
|
|
it('should throw error for invalid credentials', async () => {
|
|
const mockDb = {
|
|
query: vi.fn().mockResolvedValue({
|
|
rows: []
|
|
})
|
|
}
|
|
|
|
vi.mocked(getDb).mockReturnValue(mockDb as any)
|
|
|
|
await expect(login('invalid@example.com', 'wrong')).rejects.toThrow()
|
|
})
|
|
})
|
|
```
|
|
|
|
### Testing GraphQL Resolvers
|
|
|
|
```typescript
|
|
// api/src/schema/resolvers.test.ts
|
|
import { describe, it, expect, vi } from 'vitest'
|
|
import { resolvers } from './resolvers'
|
|
import * as resourceService from '../services/resource'
|
|
|
|
vi.mock('../services/resource')
|
|
|
|
describe('GraphQL resolvers', () => {
|
|
it('should return resources', async () => {
|
|
const mockContext = {
|
|
user: { id: '1', email: 'test@example.com', role: 'USER' },
|
|
db: {} as any,
|
|
tenantContext: null
|
|
}
|
|
|
|
const mockResources = [
|
|
{ id: '1', name: 'Resource 1', type: 'VM', status: 'RUNNING' }
|
|
]
|
|
|
|
vi.mocked(resourceService.getResources).mockResolvedValue(mockResources as any)
|
|
|
|
const result = await resolvers.Query.resources({}, {}, mockContext)
|
|
|
|
expect(result).toEqual(mockResources)
|
|
expect(resourceService.getResources).toHaveBeenCalledWith(mockContext, undefined)
|
|
})
|
|
})
|
|
```
|
|
|
|
### Testing Adapters
|
|
|
|
```typescript
|
|
// api/src/adapters/proxmox/adapter.test.ts
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { ProxmoxAdapter } from './adapter'
|
|
|
|
// Mock fetch
|
|
global.fetch = vi.fn()
|
|
|
|
describe('ProxmoxAdapter', () => {
|
|
let adapter: ProxmoxAdapter
|
|
|
|
beforeEach(() => {
|
|
adapter = new ProxmoxAdapter({
|
|
apiUrl: 'https://proxmox.example.com:8006',
|
|
apiToken: 'test-token'
|
|
})
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('should discover resources', async () => {
|
|
vi.mocked(fetch)
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
data: [{ node: 'node1' }]
|
|
})
|
|
} as Response)
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
data: [
|
|
{ vmid: 100, name: 'vm-100', status: 'running' }
|
|
]
|
|
})
|
|
} as Response)
|
|
|
|
const resources = await adapter.discoverResources()
|
|
|
|
expect(resources).toHaveLength(1)
|
|
expect(resources[0].name).toBe('vm-100')
|
|
})
|
|
|
|
it('should handle API errors', async () => {
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 401,
|
|
statusText: 'Unauthorized',
|
|
text: async () => 'Authentication failed'
|
|
} as Response)
|
|
|
|
await expect(adapter.discoverResources()).rejects.toThrow()
|
|
})
|
|
})
|
|
```
|
|
|
|
## Integration Tests
|
|
|
|
### Testing Database Operations
|
|
|
|
```typescript
|
|
// api/src/services/resource.integration.test.ts
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
import { getDb } from '../db'
|
|
import { createResource, getResource } from './resource'
|
|
|
|
describe('resource service integration', () => {
|
|
let db: any
|
|
let context: any
|
|
|
|
beforeAll(async () => {
|
|
db = getDb()
|
|
context = {
|
|
user: { id: 'test-user', role: 'ADMIN' },
|
|
db,
|
|
tenantContext: null
|
|
}
|
|
})
|
|
|
|
afterAll(async () => {
|
|
// Cleanup test data
|
|
await db.query('DELETE FROM resources WHERE name LIKE $1', ['test-%'])
|
|
await db.end()
|
|
})
|
|
|
|
it('should create and retrieve resource', async () => {
|
|
const input = {
|
|
name: 'test-vm',
|
|
type: 'VM',
|
|
siteId: 'test-site'
|
|
}
|
|
|
|
const created = await createResource(context, input)
|
|
expect(created.name).toBe('test-vm')
|
|
|
|
const retrieved = await getResource(context, created.id)
|
|
expect(retrieved.id).toBe(created.id)
|
|
expect(retrieved.name).toBe('test-vm')
|
|
})
|
|
})
|
|
```
|
|
|
|
## E2E Tests
|
|
|
|
### Testing API Endpoints
|
|
|
|
```typescript
|
|
// e2e/api.test.ts
|
|
import { describe, it, expect, beforeAll } from 'vitest'
|
|
import { request } from './helpers'
|
|
|
|
describe('API E2E tests', () => {
|
|
let authToken: string
|
|
|
|
beforeAll(async () => {
|
|
// Login to get token
|
|
const response = await request('/graphql', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
query: `
|
|
mutation {
|
|
login(email: "test@example.com", password: "test123") {
|
|
token
|
|
}
|
|
}
|
|
`
|
|
})
|
|
})
|
|
|
|
const data = await response.json()
|
|
authToken = data.data.login.token
|
|
})
|
|
|
|
it('should get resources', async () => {
|
|
const response = await request('/graphql', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${authToken}`
|
|
},
|
|
body: JSON.stringify({
|
|
query: `
|
|
query {
|
|
resources {
|
|
id
|
|
name
|
|
type
|
|
}
|
|
}
|
|
`
|
|
})
|
|
})
|
|
|
|
const data = await response.json()
|
|
expect(data.data.resources).toBeInstanceOf(Array)
|
|
})
|
|
})
|
|
```
|
|
|
|
## React Component Tests
|
|
|
|
```typescript
|
|
// portal/src/components/Dashboard.test.tsx
|
|
import { describe, it, expect, vi } from 'vitest'
|
|
import { render, screen, waitFor } from '@testing-library/react'
|
|
import { Dashboard } from './Dashboard'
|
|
|
|
vi.mock('../lib/crossplane-client', () => ({
|
|
createCrossplaneClient: () => ({
|
|
getVMs: vi.fn().mockResolvedValue([
|
|
{ id: '1', name: 'vm-1', status: 'running' }
|
|
])
|
|
})
|
|
}))
|
|
|
|
describe('Dashboard', () => {
|
|
it('should render VM list', async () => {
|
|
render(<Dashboard />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('vm-1')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
1. **Use descriptive test names**: Describe what is being tested
|
|
2. **Arrange-Act-Assert pattern**: Structure tests clearly
|
|
3. **Mock external dependencies**: Don't rely on real external services
|
|
4. **Test error cases**: Verify error handling
|
|
5. **Clean up test data**: Remove data created during tests
|
|
6. **Use fixtures**: Create reusable test data
|
|
7. **Test edge cases**: Include boundary conditions
|
|
8. **Keep tests isolated**: Tests should not depend on each other
|
|
|
|
## Running Tests
|
|
|
|
```bash
|
|
# Run all tests
|
|
pnpm test
|
|
|
|
# Run tests in watch mode
|
|
pnpm test:watch
|
|
|
|
# Run tests with coverage
|
|
pnpm test:coverage
|
|
|
|
# Run specific test file
|
|
pnpm test path/to/test/file.test.ts
|
|
```
|
|
|
|
---
|
|
|
|
**Last Updated**: 2025-01-09
|
|
|