# ADR-045: Test Data Factories and Fixtures

**Date**: 2026-01-09

**Status**: Accepted

**Implemented**: 2026-01-09

## Context

The application has a complex domain model with many entity types:

- Users, Profiles, Addresses
- Flyers, FlyerItems, Stores
- ShoppingLists, ShoppingListItems
- Recipes, RecipeIngredients
- Gamification (points, badges, leaderboards)
- And more...

Testing requires realistic mock data that:

1. Satisfies TypeScript types.
2. Has valid relationships between entities.
3. Is customizable for specific test scenarios.
4. Is consistent across test suites.
5. Avoids boilerplate in test files.

## Decision

We will implement a **factory function pattern** for test data generation:

1. **Centralized Mock Factories**: All factories in a single, organized file.
2. **Sensible Defaults**: Each factory produces valid data with minimal input.
3. **Override Support**: Factories accept partial overrides for customization.
4. **Relationship Helpers**: Factories can generate related entities.
5. **Type Safety**: Factories return properly typed objects.

### Design Principles

- **Convention over Configuration**: Factories work with zero arguments.
- **Composability**: Factories can call other factories.
- **Immutability**: Each call returns a new object (no shared references).
- **Predictability**: Deterministic output when seeded.

## Implementation Details

### Factory File Structure

Located in `src/test/mockFactories.ts`:

```typescript
import { v4 as uuidv4 } from 'uuid';
import type {
  User,
  UserProfile,
  Flyer,
  FlyerItem,
  ShoppingList,
  // ... other types
} from '../types';

// ============================================
// PRIMITIVE HELPERS
// ============================================
let idCounter = 1;
export const nextId = () => idCounter++;
export const resetIdCounter = () => {
  idCounter = 1;
};

export const randomEmail = () => `user-${uuidv4().slice(0, 8)}@test.com`;
export const randomDate = (daysAgo = 0) => {
  const date = new Date();
  date.setDate(date.getDate() - daysAgo);
  return date.toISOString();
};

// ============================================
// USER FACTORIES
// ============================================
export const createMockUser = (overrides: Partial<User> = {}): User => ({
  user_id: nextId(),
  email: randomEmail(),
  name: 'Test User',
  role: 'user',
  created_at: randomDate(30),
  updated_at: randomDate(),
  ...overrides,
});

export const createMockUserProfile = (overrides: Partial<UserProfile> = {}): UserProfile => {
  const user = createMockUser(overrides.user);
  return {
    user,
    profile: createMockProfile({ user_id: user.user_id, ...overrides.profile }),
    address: overrides.address ?? null,
    preferences: overrides.preferences ?? null,
  };
};

// ============================================
// FLYER FACTORIES
// ============================================
export const createMockFlyer = (overrides: Partial<Flyer> = {}): Flyer => ({
  flyer_id: nextId(),
  file_name: 'test-flyer.jpg',
  image_url: 'https://example.com/flyer.jpg',
  icon_url: 'https://example.com/flyer-icon.jpg',
  checksum: uuidv4(),
  store_name: 'Test Store',
  store_address: '123 Test St',
  valid_from: randomDate(7),
  valid_to: randomDate(-7), // 7 days in future
  item_count: 10,
  status: 'approved',
  uploaded_by: null,
  created_at: randomDate(7),
  updated_at: randomDate(),
  ...overrides,
});

export const createMockFlyerItem = (overrides: Partial<FlyerItem> = {}): FlyerItem => ({
  flyer_item_id: nextId(),
  flyer_id: overrides.flyer_id ?? nextId(),
  item: 'Test Product',
  price_display: '$2.99',
  price_in_cents: 299,
  quantity: 'each',
  category_name: 'Groceries',
  master_item_id: null,
  view_count: 0,
  click_count: 0,
  created_at: randomDate(7),
  updated_at: randomDate(),
  ...overrides,
});

// ============================================
// FLYER WITH ITEMS (COMPOSITE)
// ============================================
export const createMockFlyerWithItems = (
  flyerOverrides: Partial<Flyer> = {},
  itemCount = 5,
): { flyer: Flyer; items: FlyerItem[] } => {
  const flyer = createMockFlyer(flyerOverrides);
  const items = Array.from({ length: itemCount }, (_, i) =>
    createMockFlyerItem({
      flyer_id: flyer.flyer_id,
      item: `Product ${i + 1}`,
      price_in_cents: 100 + i * 50,
    }),
  );
  flyer.item_count = items.length;
  return { flyer, items };
};

// ============================================
// SHOPPING LIST FACTORIES
// ============================================
export const createMockShoppingList = (overrides: Partial<ShoppingList> = {}): ShoppingList => ({
  shopping_list_id: nextId(),
  user_id: overrides.user_id ?? nextId(),
  name: 'Weekly Groceries',
  is_active: true,
  created_at: randomDate(14),
  updated_at: randomDate(),
  ...overrides,
});

export const createMockShoppingListItem = (
  overrides: Partial<ShoppingListItem> = {},
): ShoppingListItem => ({
  shopping_list_item_id: nextId(),
  shopping_list_id: overrides.shopping_list_id ?? nextId(),
  item_name: 'Milk',
  quantity: 1,
  is_purchased: false,
  created_at: randomDate(7),
  updated_at: randomDate(),
  ...overrides,
});
```

### Usage in Tests

```typescript
import {
  createMockUser,
  createMockFlyer,
  createMockFlyerWithItems,
  resetIdCounter,
} from '../test/mockFactories';

describe('FlyerService', () => {
  beforeEach(() => {
    resetIdCounter(); // Consistent IDs across tests
  });

  it('should get flyer by ID', async () => {
    const mockFlyer = createMockFlyer({ store_name: 'Walmart' });

    mockDb.query.mockResolvedValue({ rows: [mockFlyer] });

    const result = await flyerService.getFlyerById(mockFlyer.flyer_id);

    expect(result.store_name).toBe('Walmart');
  });

  it('should return flyer with items', async () => {
    const { flyer, items } = createMockFlyerWithItems(
      { store_name: 'Costco' },
      10, // 10 items
    );

    mockDb.query.mockResolvedValueOnce({ rows: [flyer] }).mockResolvedValueOnce({ rows: items });

    const result = await flyerService.getFlyerWithItems(flyer.flyer_id);

    expect(result.flyer.store_name).toBe('Costco');
    expect(result.items).toHaveLength(10);
  });
});
```

### Bulk Data Generation

For integration tests or seeding:

```typescript
export const createMockDataset = () => {
  const users = Array.from({ length: 10 }, () => createMockUser());
  const flyers = Array.from({ length: 5 }, () => createMockFlyer());
  const flyersWithItems = flyers.map((flyer) => ({
    flyer,
    items: Array.from({ length: Math.floor(Math.random() * 20) + 5 }, () =>
      createMockFlyerItem({ flyer_id: flyer.flyer_id }),
    ),
  }));

  return { users, flyers, flyersWithItems };
};
```

### API Response Factories

For testing API handlers:

```typescript
export const createMockApiResponse = <T>(
  data: T,
  overrides: Partial<ApiResponse<T>> = {},
): ApiResponse<T> => ({
  success: true,
  data,
  meta: {
    timestamp: new Date().toISOString(),
    requestId: uuidv4(),
    ...overrides.meta,
  },
  ...overrides,
});

export const createMockPaginatedResponse = <T>(
  items: T[],
  page = 1,
  pageSize = 20,
): PaginatedApiResponse<T> => ({
  success: true,
  data: items,
  meta: {
    timestamp: new Date().toISOString(),
    requestId: uuidv4(),
  },
  pagination: {
    page,
    pageSize,
    totalItems: items.length,
    totalPages: Math.ceil(items.length / pageSize),
    hasMore: false,
  },
});
```

### Database Query Mock Helpers

```typescript
export const mockQueryResult = <T>(rows: T[]) => ({
  rows,
  rowCount: rows.length,
});

export const mockEmptyResult = () => ({
  rows: [],
  rowCount: 0,
});

export const mockInsertResult = <T>(inserted: T) => ({
  rows: [inserted],
  rowCount: 1,
});
```

## Test Cleanup Utilities

```typescript
// For integration tests with real database
export const cleanupTestData = async (pool: Pool) => {
  await pool.query('DELETE FROM flyer_items WHERE flyer_id > 1000000');
  await pool.query('DELETE FROM flyers WHERE flyer_id > 1000000');
  await pool.query('DELETE FROM users WHERE user_id > 1000000');
};

// Mark test data with high IDs
export const createTestFlyer = (overrides: Partial<Flyer> = {}) =>
  createMockFlyer({ flyer_id: 1000000 + nextId(), ...overrides });
```

## Consequences

### Positive

- **Consistency**: All tests use the same factory patterns.
- **Type Safety**: Factories return correctly typed objects.
- **Reduced Boilerplate**: Tests focus on behavior, not data setup.
- **Maintainability**: Update factory once, all tests benefit.
- **Flexibility**: Easy to create edge case data.

### Negative

- **Single Large File**: Factory file can become large.
- **Learning Curve**: New developers must learn factory patterns.
- **Maintenance**: Factories must be updated when types change.

### Mitigation

- Split factories into multiple files if needed (by domain).
- Add JSDoc comments explaining each factory.
- Use TypeScript to catch type mismatches automatically.

## Key Files

- `src/test/mockFactories.ts` - All mock factory functions
- `src/test/testUtils.ts` - Test helper utilities
- `src/test/setup.ts` - Global test setup with factory reset

## Related ADRs

- [ADR-010](./0010-testing-strategy-and-standards.md) - Testing Strategy
- [ADR-040](./0040-testing-economics-and-priorities.md) - Testing Economics
- [ADR-027](./0027-standardized-naming-convention-for-ai-and-database-types.md) - Type Naming
