- 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.
7.2 KiB
7.2 KiB
Test Examples and Patterns
This document provides examples and patterns for writing tests in the Sankofa Phoenix project.
Unit Tests
Testing Service Functions
// 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
// 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
// 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
// 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
// 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
// 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
- Use descriptive test names: Describe what is being tested
- Arrange-Act-Assert pattern: Structure tests clearly
- Mock external dependencies: Don't rely on real external services
- Test error cases: Verify error handling
- Clean up test data: Remove data created during tests
- Use fixtures: Create reusable test data
- Test edge cases: Include boundary conditions
- Keep tests isolated: Tests should not depend on each other
Running Tests
# 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