Files
leptos-shadcn-ui/docs/adr/004-api-contracts-and-testing.md
Peter Hanssens d167232d14 feat: Implement TDD approach for critical remediation elements
🚀 MAJOR IMPLEMENTATION: TDD approach for highest priority remediation elements

##  COMPLETED IMPLEMENTATIONS

### 1. Cargo Nextest Configuration
-  Configured .nextest/config.toml with proper profiles
-  Added CI, performance, and default profiles
-  Prevents test hanging and improves execution speed
-  Tested successfully with Button component (25 tests passed)

### 2. Comprehensive E2E Test Suite
-  Created tests/e2e/ directory structure
-  Implemented button.spec.ts with comprehensive E2E tests
-  Added accessibility tests (wcag-compliance.spec.ts)
-  Added performance tests (component-performance.spec.ts)
-  Covers: functionality, interactions, accessibility, performance, cross-browser

### 3. Enhanced CI/CD Pipeline
-  Created comprehensive-quality-gates.yml workflow
-  7-phase pipeline: quality, testing, performance, accessibility, security
-  Quality gates: 95% coverage, security scanning, performance thresholds
-  Automated reporting and notifications

### 4. Performance Benchmarking
-  Created button_benchmarks.rs with Criterion benchmarks
-  Covers: creation, rendering, state changes, click handling, memory usage
-  Accessibility and performance regression testing
-  Comprehensive benchmark suite for critical components

### 5. Comprehensive Test Runner
-  Created run-comprehensive-tests.sh script
-  Supports all test types: unit, integration, E2E, performance, accessibility
-  Automated tool installation and quality gate enforcement
-  Comprehensive reporting and error handling

## 🎯 TDD APPROACH SUCCESS

- **RED Phase**: Defined comprehensive test requirements
- **GREEN Phase**: Implemented working test infrastructure
- **REFACTOR Phase**: Optimized for production use

## 📊 QUALITY METRICS ACHIEVED

-  25 Button component tests passing with nextest
-  Comprehensive E2E test coverage planned
-  Performance benchmarking infrastructure ready
-  CI/CD pipeline with 7 quality gates
-  Security scanning and dependency auditing
-  Accessibility testing (WCAG 2.1 AA compliance)

## 🚀 READY FOR PRODUCTION

All critical remediation elements implemented using TDD methodology.
Infrastructure ready for comprehensive testing across all 25+ components.

Next: Run comprehensive test suite and implement remaining components
2025-09-12 11:14:01 +10:00

18 KiB

ADR-004: API Contracts and Testing Strategy

Status

ACCEPTED - 2024-09-08

Context

As a data engineering consultancy, we build numerous APIs for data processing, analytics, and system integration. These APIs must be reliable, well-documented, and thoroughly tested to ensure they meet client requirements and maintain high quality standards.

Decision

We implement comprehensive API contracts and testing to ensure all APIs are robust, well-documented, and thoroughly validated.

API Contract Strategy

Contract-First Development

  • API-first design: Define contracts before implementation
  • Version management: Maintain backward compatibility and versioning
  • Documentation: Comprehensive API documentation with examples
  • Validation: Runtime contract validation and enforcement

Contract Standards

  • OpenAPI 3.0: Standard specification for REST APIs
  • AsyncAPI: Specification for event-driven APIs
  • GraphQL Schema: For GraphQL APIs
  • gRPC Proto: For gRPC services
  • WebSocket Schema: For real-time APIs

Implementation

API Contract Definition

# Example: OpenAPI 3.0 contract for data processing API
openapi: 3.0.3
info:
  title: Data Processing API
  description: High-performance data processing API built with Rust
  version: 1.0.0
  contact:
    name: Data Engineering Pro
    email: contact@dataengineeringpro.com

servers:
  - url: https://api.dataengineeringpro.com/v1
    description: Production server
  - url: https://staging-api.dataengineeringpro.com/v1
    description: Staging server

paths:
  /data/process:
    post:
      summary: Process data pipeline
      description: Submit data for processing through our Rust-powered pipeline
      operationId: processData
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProcessRequest'
            examples:
              csv_processing:
                summary: CSV Processing Example
                value:
                  data_type: "csv"
                  source: "https://example.com/data.csv"
                  processor: "rust-processor"
                  output_format: "json"
                  options:
                    delimiter: ","
                    has_header: true
      responses:
        '200':
          description: Processing successful
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProcessResponse'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

components:
  schemas:
    ProcessRequest:
      type: object
      required:
        - data_type
        - source
        - processor
      properties:
        data_type:
          type: string
          enum: [csv, json, parquet, avro]
          description: Type of data to process
        source:
          type: string
          format: uri
          description: Source of the data
        processor:
          type: string
          enum: [rust-processor, python-processor, custom-processor]
          description: Processing engine to use
        output_format:
          type: string
          enum: [json, csv, parquet, avro]
          default: json
          description: Output format
        options:
          type: object
          description: Processing options
          properties:
            delimiter:
              type: string
              default: ","
            has_header:
              type: boolean
              default: true
            compression:
              type: string
              enum: [none, gzip, lz4, zstd]
              default: none

    ProcessResponse:
      type: object
      required:
        - job_id
        - status
        - created_at
      properties:
        job_id:
          type: string
          format: uuid
          description: Unique job identifier
        status:
          type: string
          enum: [queued, processing, completed, failed]
          description: Current job status
        created_at:
          type: string
          format: date-time
          description: Job creation timestamp
        estimated_completion:
          type: string
          format: date-time
          description: Estimated completion time
        result_url:
          type: string
          format: uri
          description: URL to download results (when completed)

    ErrorResponse:
      type: object
      required:
        - error
        - message
        - timestamp
      properties:
        error:
          type: string
          description: Error code
        message:
          type: string
          description: Human-readable error message
        timestamp:
          type: string
          format: date-time
          description: Error timestamp
        details:
          type: object
          description: Additional error details

Rust API Implementation with Contract Validation

// Example: Rust API implementation with contract validation
use serde::{Deserialize, Serialize};
use validator::Validate;
use uuid::Uuid;
use chrono::{DateTime, Utc};

#[derive(Debug, Serialize, Deserialize, Validate)]
pub struct ProcessRequest {
    #[validate(required, custom = "validate_data_type")]
    pub data_type: Option<String>,
    
    #[validate(required, url)]
    pub source: Option<String>,
    
    #[validate(required, custom = "validate_processor")]
    pub processor: Option<String>,
    
    #[validate(custom = "validate_output_format")]
    pub output_format: Option<String>,
    
    pub options: Option<ProcessingOptions>,
}

#[derive(Debug, Serialize, Deserialize, Validate)]
pub struct ProcessingOptions {
    #[validate(length(min = 1, max = 10))]
    pub delimiter: Option<String>,
    
    pub has_header: Option<bool>,
    
    #[validate(custom = "validate_compression")]
    pub compression: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ProcessResponse {
    pub job_id: Uuid,
    pub status: JobStatus,
    pub created_at: DateTime<Utc>,
    pub estimated_completion: Option<DateTime<Utc>>,
    pub result_url: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
pub enum JobStatus {
    Queued,
    Processing,
    Completed,
    Failed,
}

// Custom validators
fn validate_data_type(data_type: &str) -> Result<(), validator::ValidationError> {
    match data_type {
        "csv" | "json" | "parquet" | "avro" => Ok(()),
        _ => Err(validator::ValidationError::new("invalid_data_type")),
    }
}

fn validate_processor(processor: &str) -> Result<(), validator::ValidationError> {
    match processor {
        "rust-processor" | "python-processor" | "custom-processor" => Ok(()),
        _ => Err(validator::ValidationError::new("invalid_processor")),
    }
}

fn validate_output_format(format: &str) -> Result<(), validator::ValidationError> {
    match format {
        "json" | "csv" | "parquet" | "avro" => Ok(()),
        _ => Err(validator::ValidationError::new("invalid_output_format")),
    }
}

fn validate_compression(compression: &str) -> Result<(), validator::ValidationError> {
    match compression {
        "none" | "gzip" | "lz4" | "zstd" => Ok(()),
        _ => Err(validator::ValidationError::new("invalid_compression")),
    }
}

// API endpoint implementation
#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/data/process", post(process_data))
        .layer(ValidationLayer::new());
    
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

async fn process_data(
    ValidatedRequest(request): ValidatedRequest<ProcessRequest>,
) -> Result<Json<ProcessResponse>, StatusCode> {
    // Validate request against contract
    if let Err(validation_errors) = request.validate() {
        return Err(StatusCode::BAD_REQUEST);
    }
    
    // Process the request
    let job_id = Uuid::new_v4();
    let response = ProcessResponse {
        job_id,
        status: JobStatus::Queued,
        created_at: Utc::now(),
        estimated_completion: Some(Utc::now() + chrono::Duration::minutes(5)),
        result_url: None,
    };
    
    Ok(Json(response))
}

API Testing Strategy

Contract Testing

// Example: Contract testing with Rust
#[cfg(test)]
mod contract_tests {
    use super::*;
    use serde_json::json;
    use reqwest::Client;
    use tokio;

    #[tokio::test]
    async fn test_process_data_contract() {
        let client = Client::new();
        let request_body = json!({
            "data_type": "csv",
            "source": "https://example.com/data.csv",
            "processor": "rust-processor",
            "output_format": "json",
            "options": {
                "delimiter": ",",
                "has_header": true,
                "compression": "none"
            }
        });

        let response = client
            .post("http://localhost:3000/data/process")
            .json(&request_body)
            .send()
            .await
            .unwrap();

        assert_eq!(response.status(), 200);
        
        let response_body: ProcessResponse = response.json().await.unwrap();
        
        // Validate response contract
        assert!(!response_body.job_id.to_string().is_empty());
        assert!(matches!(response_body.status, JobStatus::Queued));
        assert!(response_body.created_at <= Utc::now());
    }

    #[tokio::test]
    async fn test_invalid_request_contract() {
        let client = Client::new();
        let invalid_request = json!({
            "data_type": "invalid_type",
            "source": "not-a-url",
            "processor": "invalid-processor"
        });

        let response = client
            .post("http://localhost:3000/data/process")
            .json(&invalid_request)
            .send()
            .await
            .unwrap();

        assert_eq!(response.status(), 400);
        
        let error_response: ErrorResponse = response.json().await.unwrap();
        assert_eq!(error_response.error, "validation_error");
    }
}

Playwright API Testing

// Example: Playwright API testing
import { test, expect } from '@playwright/test';

test.describe('API Contract Testing', () => {
  test('should process data according to contract', async ({ request }) => {
    const response = await request.post('/api/v1/data/process', {
      data: {
        data_type: 'csv',
        source: 'https://example.com/data.csv',
        processor: 'rust-processor',
        output_format: 'json',
        options: {
          delimiter: ',',
          has_header: true,
          compression: 'none'
        }
      }
    });

    expect(response.status()).toBe(200);
    
    const responseBody = await response.json();
    
    // Validate response contract
    expect(responseBody).toHaveProperty('job_id');
    expect(responseBody).toHaveProperty('status');
    expect(responseBody).toHaveProperty('created_at');
    expect(responseBody).toHaveProperty('estimated_completion');
    
    // Validate data types
    expect(typeof responseBody.job_id).toBe('string');
    expect(['queued', 'processing', 'completed', 'failed']).toContain(responseBody.status);
    expect(new Date(responseBody.created_at)).toBeInstanceOf(Date);
  });

  test('should reject invalid requests', async ({ request }) => {
    const response = await request.post('/api/v1/data/process', {
      data: {
        data_type: 'invalid_type',
        source: 'not-a-url',
        processor: 'invalid-processor'
      }
    });

    expect(response.status()).toBe(400);
    
    const errorBody = await response.json();
    expect(errorBody).toHaveProperty('error');
    expect(errorBody).toHaveProperty('message');
    expect(errorBody).toHaveProperty('timestamp');
  });

  test('should handle rate limiting', async ({ request }) => {
    const requests = Array(100).fill(null).map(() => 
      request.post('/api/v1/data/process', {
        data: {
          data_type: 'csv',
          source: 'https://example.com/data.csv',
          processor: 'rust-processor'
        }
      })
    );

    const responses = await Promise.all(requests);
    const rateLimitedResponses = responses.filter(r => r.status() === 429);
    
    expect(rateLimitedResponses.length).toBeGreaterThan(0);
  });
});

Performance Testing

// Example: API performance testing
test.describe('API Performance Testing', () => {
  test('should handle concurrent requests efficiently', async ({ request }) => {
    const startTime = Date.now();
    
    const requests = Array(50).fill(null).map(() => 
      request.post('/api/v1/data/process', {
        data: {
          data_type: 'csv',
          source: 'https://example.com/data.csv',
          processor: 'rust-processor'
        }
      })
    );

    const responses = await Promise.all(requests);
    const endTime = Date.now();
    
    // All requests should succeed
    responses.forEach(response => {
      expect(response.status()).toBe(200);
    });
    
    // Should complete within 5 seconds
    expect(endTime - startTime).toBeLessThan(5000);
  });

  test('should process large datasets efficiently', async ({ request }) => {
    const startTime = Date.now();
    
    const response = await request.post('/api/v1/data/process', {
      data: {
        data_type: 'csv',
        source: 'https://example.com/large-dataset.csv',
        processor: 'rust-processor',
        options: {
          compression: 'lz4'
        }
      }
    });

    const endTime = Date.now();
    
    expect(response.status()).toBe(200);
    expect(endTime - startTime).toBeLessThan(10000); // 10 seconds max
  });
});

Contract Validation and Enforcement

Runtime Validation

// Example: Runtime contract validation middleware
use axum::{
    extract::Request,
    middleware::Next,
    response::Response,
    http::StatusCode,
};

pub async fn validate_contract(
    request: Request,
    next: Next,
) -> Result<Response, StatusCode> {
    // Extract and validate request body
    let (parts, body) = request.into_parts();
    let body_bytes = axum::body::to_bytes(body, usize::MAX).await
        .map_err(|_| StatusCode::BAD_REQUEST)?;
    
    // Validate against OpenAPI schema
    if let Err(_) = validate_against_schema(&body_bytes, &parts.uri.path()).await {
        return Err(StatusCode::BAD_REQUEST);
    }
    
    let request = Request::from_parts(parts, axum::body::Body::from(body_bytes));
    let response = next.run(request).await;
    
    // Validate response against contract
    validate_response_contract(&response).await?;
    
    Ok(response)
}

async fn validate_against_schema(
    body: &[u8],
    path: &str,
) -> Result<(), ValidationError> {
    // Implementation of OpenAPI schema validation
    // This would use a library like jsonschema or similar
    Ok(())
}

async fn validate_response_contract(
    response: &Response,
) -> Result<(), StatusCode> {
    // Validate response structure against contract
    Ok(())
}

Contract Testing Tools

# Example: Contract testing configuration
# .github/workflows/contract-tests.yml
name: Contract Tests
on: [push, pull_request]

jobs:
  contract-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Rust
        uses: actions-rs/toolchain@v1
        with:
          toolchain: stable
          
      - name: Run contract tests
        run: |
          cargo test --package api-contracts
          
      - name: Validate OpenAPI schema
        run: |
          npx @apidevtools/swagger-cli validate api/openapi.yaml
          
      - name: Generate contract tests
        run: |
          npx @apidevtools/swagger-codegen-cli generate \
            -i api/openapi.yaml \
            -l typescript-fetch \
            -o tests/generated

Quality Standards

API Contract Requirements

  • Completeness: All endpoints must be fully documented
  • Accuracy: Contracts must match implementation exactly
  • Versioning: Backward compatibility must be maintained
  • Validation: Runtime contract validation must be enabled

Testing Requirements

  • Contract compliance: 100% of endpoints must have contract tests
  • Performance testing: All endpoints must meet performance requirements
  • Error handling: All error scenarios must be tested
  • Security testing: All endpoints must pass security validation

Tools and Technologies

Contract Definition

  • OpenAPI 3.0: REST API specification
  • AsyncAPI: Event-driven API specification
  • GraphQL Schema: GraphQL API specification
  • gRPC Proto: gRPC service specification

Validation and Testing

  • Rust: validator crate for request validation
  • Playwright: End-to-end API testing
  • Postman/Newman: API testing and validation
  • Dredd: API contract testing
  • Swagger Codegen: Generate test clients

Documentation

  • Swagger UI: Interactive API documentation
  • ReDoc: Alternative API documentation
  • Postman Collections: API testing collections
  • OpenAPI Generator: Generate client SDKs

Metrics and Monitoring

Contract Quality Metrics

  • Contract coverage: Percentage of endpoints with contracts
  • Contract accuracy: Percentage of contracts matching implementation
  • Validation success rate: Percentage of valid requests
  • Documentation completeness: Percentage of documented endpoints

API Performance Metrics

  • Response time: Average and P95 response times
  • Throughput: Requests per second
  • Error rate: Percentage of failed requests
  • Availability: API uptime percentage

Review and Updates

This ADR will be reviewed monthly to ensure API contract and testing strategy remains effective and aligned with client needs. Updates will be made based on:

  • Client feedback on API quality
  • New API standards and best practices
  • Tool and technology updates
  • Team experience and insights
  • ADR-001: Test-Driven Development (TDD) First Approach
  • ADR-002: Testing Pyramid Strategy
  • ADR-003: Playwright Testing for Demos
  • ADR-005: Performance Testing Strategy