# API Contract Tests - Implementation Guide

## Overview

This directory contains comprehensive API contract tests for all 310+ endpoints in Mawidi. Tests validate request/response schemas, status codes, error handling, and security features.

## Test Structure

```
__tests__/api/
├── contract-tests-README.md    # This file
├── auth/
│   ├── register.spec.ts        # Registration flow
│   ├── login.spec.ts           # Login (password & OTP)
│   ├── otp.spec.ts             # OTP generation/verification
│   └── password-reset.spec.ts  # Forgot/reset password
├── bookings/
│   ├── demo-booking.spec.ts    # Demo booking flow
│   ├── booking-crud.spec.ts    # Booking operations
│   └── payment-links.spec.ts   # Payment link generation
├── staff/
│   ├── auth.spec.ts            # Staff authentication
│   ├── users.spec.ts           # User management
│   └── analytics.spec.ts       # Analytics endpoints
├── webhooks/
│   ├── stripe.spec.ts          # Stripe webhook handling
│   ├── n8n.spec.ts             # n8n webhook endpoints
│   └── whatsapp.spec.ts        # WhatsApp webhook
├── health/
│   └── health-checks.spec.ts   # Health/status endpoints
├── common/
│   ├── error-format.spec.ts    # Error response consistency
│   ├── rate-limiting.spec.ts   # Rate limit enforcement
│   ├── cors.spec.ts            # CORS configuration
│   └── headers.spec.ts         # Security headers
└── helpers/
    ├── test-data.ts            # Test data generators
    ├── api-helpers.ts          # API call helpers
    └── assertions.ts           # Custom assertions
```

## Quick Start

### 1. Install Dependencies

```bash
npm install --save-dev @playwright/test zod
```

### 2. Start Test Environment

```bash
# Start infrastructure (PostgreSQL, Redis, MailHog)
npm run docker:infra:start

# Start app in test mode
NODE_ENV=test npm run dev
```

### 3. Run Tests

```bash
# All API tests
npm run test:e2e -- __tests__/api/

# Specific category
npm run test:e2e -- __tests__/api/auth/
npm run test:e2e -- __tests__/api/bookings/

# Single file
npm run test:e2e -- __tests__/api/auth/register.spec.ts

# With UI
npm run test:e2e -- __tests__/api/ --ui

# Generate report
npm run test:e2e -- __tests__/api/ --reporter=html
```

## Writing Contract Tests

### Test Template

```typescript
import { test, expect } from "@playwright/test";
import { z } from "zod";

// Define expected response schema
const registerResponseSchema = z.object({
  success: z.literal(true),
  message: z.string(),
  userId: z.string(),
});

test.describe("POST /api/register", () => {
  test.beforeEach(async () => {
    // Setup: Clear test data, reset rate limits, etc.
  });

  test("successful registration", async ({ request }) => {
    const response = await request.post("http://localhost:9000/api/register", {
      data: {
        email: `test-${Date.now()}@example.com`,
        password: "SecurePass123!",
        fullName: "Test User",
      },
    });

    // Validate status code
    expect(response.status()).toBe(201);

    // Validate response schema
    const body = await response.json();
    const parsed = registerResponseSchema.safeParse(body);
    expect(parsed.success).toBe(true);

    // Validate specific fields
    expect(body.userId).toMatch(/^[a-z0-9]{24,}$/);
  });

  test("validation errors", async ({ request }) => {
    const response = await request.post("http://localhost:9000/api/register", {
      data: {
        email: "invalid-email",
        password: "weak",
        fullName: "T",
      },
    });

    expect(response.status()).toBe(400);
    const body = await response.json();
    expect(body.error).toBeTruthy();
  });

  test("rate limiting", async ({ request }) => {
    for (let i = 0; i < 6; i++) {
      const response = await request.post(
        "http://localhost:9000/api/register",
        {
          data: {
            email: `rate-${i}@example.com`,
            password: "SecurePass123!",
            fullName: "Rate Test",
          },
        },
      );

      if (i < 5) {
        expect([201, 400, 409]).toContain(response.status());
      } else {
        expect(response.status()).toBe(429);
      }
    }
  });
});
```

### Response Schema Validation

```typescript
import { z } from "zod";

// Success response
const successSchema = z.object({
  success: z.literal(true),
  message: z.string().optional(),
  data: z.unknown().optional(),
});

// Error response
const errorSchema = z.object({
  error: z.string(),
  details: z.array(z.string()).optional(),
  code: z.string().optional(),
});

// Usage
test("validates success response schema", async ({ request }) => {
  const response = await request.get("/api/health");
  const body = await response.json();

  const result = successSchema.safeParse(body);
  expect(result.success).toBe(true);
});
```

### Custom Assertions

```typescript
// helpers/assertions.ts
export function expectValidEmail(email: string) {
  expect(email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
}

export function expectValidUUID(id: string) {
  expect(id).toMatch(
    /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
  );
}

export function expectValidISO8601(date: string) {
  expect(new Date(date).toISOString()).toBe(date);
}

export async function expectErrorResponse(
  response: Response,
  expectedStatus: number,
) {
  expect(response.status()).toBe(expectedStatus);
  const body = await response.json();
  expect(body.error).toBeTruthy();
  expect(typeof body.error).toBe("string");
}
```

### Test Data Generators

```typescript
// helpers/test-data.ts
export function generateUniqueEmail(): string {
  return `test-${Date.now()}-${Math.random().toString(36).slice(2)}@example.com`;
}

export function generateValidPassword(): string {
  return "SecurePass123!";
}

export function generateBookingData(overrides = {}) {
  return {
    fullName: "Test Client",
    email: generateUniqueEmail(),
    telephone: "+971501234567",
    appointmentDate: "2025-12-20",
    appointmentTime: "10:00",
    duration: "30min",
    language: "en",
    timeOnPageMs: 5000,
    ...overrides,
  };
}
```

## Common Test Patterns

### 1. Testing Authentication

```typescript
test.describe("Authentication", () => {
  let authToken: string;

  test.beforeAll(async ({ request }) => {
    // Register and login to get auth token
    const registerRes = await request.post("/api/register", {
      data: {
        email: "auth-test@example.com",
        password: "SecurePass123!",
        fullName: "Auth Test",
      },
    });

    const loginRes = await request.post("/api/auth/login", {
      data: {
        identifier: "auth-test@example.com",
        password: "SecurePass123!",
        language: "en",
      },
    });

    const body = await loginRes.json();
    authToken = body.token;
  });

  test("protected endpoint requires auth", async ({ request }) => {
    const response = await request.get("/api/user/profile");
    expect(response.status()).toBe(401);
  });

  test("protected endpoint works with token", async ({ request }) => {
    const response = await request.get("/api/user/profile", {
      headers: { Authorization: `Bearer ${authToken}` },
    });
    expect(response.status()).toBe(200);
  });
});
```

### 2. Testing CSRF Protection

```typescript
test.describe("CSRF Protection", () => {
  test("rejects requests without CSRF token", async ({ request }) => {
    const response = await request.post("/api/register", {
      data: {
        email: "test@example.com",
        password: "Pass123!",
        fullName: "Test",
      },
      // No X-CSRF-Token header
    });

    expect(response.status()).toBe(403);
  });

  test("accepts requests with valid CSRF token", async ({ request }) => {
    // Get CSRF token
    const csrfRes = await request.get("/api/auth/csrf");
    const { token } = await csrfRes.json();

    const response = await request.post("/api/register", {
      headers: { "X-CSRF-Token": token },
      data: {
        email: generateUniqueEmail(),
        password: "Pass123!",
        fullName: "Test",
      },
    });

    expect(response.status()).toBe(201);
  });
});
```

### 3. Testing Pagination

```typescript
test.describe("Pagination", () => {
  test("returns paginated results", async ({ request }) => {
    const response = await request.get("/api/bookings?page=1&limit=10");

    expect(response.status()).toBe(200);
    const body = await response.json();

    expect(body).toHaveProperty("data");
    expect(body).toHaveProperty("total");
    expect(body).toHaveProperty("page");
    expect(body).toHaveProperty("limit");
    expect(Array.isArray(body.data)).toBe(true);
    expect(body.data.length).toBeLessThanOrEqual(10);
  });

  test("validates page parameter", async ({ request }) => {
    const response = await request.get("/api/bookings?page=-1");
    expect(response.status()).toBe(400);
  });
});
```

### 4. Testing Idempotency

```typescript
test.describe("Idempotency", () => {
  test("webhook is idempotent", async ({ request }) => {
    const eventData = {
      id: "evt_unique_123",
      type: "payment_intent.succeeded",
      data: { object: { id: "pi_123" } },
    };

    const response1 = await request.post("/api/stripe/webhook", {
      headers: { "stripe-signature": generateTestSignature(eventData) },
      data: eventData,
    });

    const response2 = await request.post("/api/stripe/webhook", {
      headers: { "stripe-signature": generateTestSignature(eventData) },
      data: eventData,
    });

    expect(response1.status()).toBe(200);
    expect(response2.status()).toBe(200);
    // Verify in DB that only one record was created
  });
});
```

## Test Categories

### 1. Happy Path Tests

- Valid input → Expected output
- Status code 200/201
- Response schema matches

### 2. Validation Tests

- Invalid email format
- Missing required fields
- Out of range values
- Malformed data types

### 3. Security Tests

- CSRF protection
- Rate limiting
- Authentication
- Authorization
- SQL injection attempts
- XSS attempts

### 4. Edge Cases

- Empty strings
- Very long strings
- Special characters
- Unicode
- Null values
- Boundary values

### 5. Performance Tests

- Response time < target
- No N+1 queries
- Pagination works
- Large payload handling

### 6. Error Handling

- 400 Bad Request
- 401 Unauthorized
- 403 Forbidden
- 404 Not Found
- 409 Conflict
- 429 Too Many Requests
- 500 Internal Server Error

## Continuous Integration

### GitHub Actions Workflow

```yaml
name: API Contract Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: postgres
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

      redis:
        image: redis:7-alpine
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: "18"

      - name: Install dependencies
        run: npm ci

      - name: Run database migrations
        run: npx prisma migrate deploy

      - name: Start app
        run: npm run dev &
        env:
          NODE_ENV: test
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
          REDIS_URL: redis://localhost:6379

      - name: Wait for app to start
        run: npx wait-on http://localhost:9000/api/health

      - name: Run API contract tests
        run: npm run test:e2e -- __tests__/api/

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: playwright-report
          path: playwright-report/
```

## Best Practices

### 1. Use Unique Test Data

```typescript
// ✅ GOOD: Unique email per test
const email = generateUniqueEmail();

// ❌ BAD: Hardcoded email (tests interfere with each other)
const email = "test@example.com";
```

### 2. Clean Up After Tests

```typescript
test.afterEach(async () => {
  // Delete test data created in this test
  await prisma.users.deleteMany({
    where: { email: { contains: "test-" } },
  });
});
```

### 3. Test Independent Execution

```typescript
// Each test should work in isolation
test("test 1", () => {
  /* ... */
});
test("test 2", () => {
  /* ... */
}); // Should not depend on test 1
```

### 4. Use Descriptive Test Names

```typescript
// ✅ GOOD
test("returns 409 when registering with duplicate email", () => {});

// ❌ BAD
test("duplicate email", () => {});
```

### 5. Assert Status Code First

```typescript
// ✅ GOOD: Check status before parsing body
expect(response.status()).toBe(200);
const body = await response.json();

// ❌ BAD: Parse before checking status (may throw)
const body = await response.json();
expect(response.status()).toBe(200);
```

## Troubleshooting

### Issue: Rate limit tests failing

**Solution:** Reset Redis between test runs

```bash
docker exec -it mawidi-redis redis-cli FLUSHDB
```

### Issue: Database conflicts

**Solution:** Use transactions or unique test data

```typescript
const email = `test-${Date.now()}@example.com`;
```

### Issue: CSRF token expired

**Solution:** Get fresh token for each test

```typescript
test.beforeEach(async ({ request }) => {
  const res = await request.get("/api/auth/csrf");
  csrfToken = (await res.json()).token;
});
```

### Issue: Tests timing out

**Solution:** Increase timeout for slow operations

```typescript
test("slow operation", async ({ request }) => {
  test.setTimeout(10000); // 10 seconds

  const response = await request.post("/api/slow-endpoint", {
    timeout: 10000,
  });
});
```

## Coverage Goals

- **Unit tests**: 80%+ code coverage
- **Integration tests**: 100% of API endpoints
- **Contract tests**: 100% of public APIs
- **E2E tests**: Critical user flows

## Resources

- [Playwright Documentation](https://playwright.dev/docs/intro)
- [Zod Documentation](https://zod.dev)
- [API Testing Best Practices](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library)
- [REST API Testing Guide](https://www.guru99.com/api-testing.html)

## Next Steps

1. Run existing contract tests
2. Identify gaps in coverage
3. Write missing tests
4. Set up CI/CD integration
5. Monitor test results
6. Refactor based on findings
