Files
Sankofa/docs/guides/TEST_EXAMPLES.md
defiQUG fe0365757a Update documentation structure and enhance .gitignore
- 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.
2025-12-12 21:18:55 -08:00

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

  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

# 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