Skip to content

AI Prompt: Forma3D.Connect — Phase 5h: Controller Test Coverage

Purpose: This prompt instructs an AI to add comprehensive unit tests for all backend controllers
Estimated Effort: 3-4 days (~20-28 hours)
Prerequisites: Phase 5g completed (Structured Logging)
Output: Controller tests for all 10 controllers, request validation tested, guards verified
Status: 🟡 PENDING


🎯 Mission

You are continuing development of Forma3D.Connect, building on the Phase 5g foundation. Your task is to implement Phase 5h: Controller Test Coverage — specifically addressing TD-006 (Missing Controller Tests) from the technical debt register.

Why This Matters:

The controller layer has minimal test coverage, with only fulfillment.controller.spec.ts existing. This causes:

  1. Request Validation Gaps: DTO validation not tested
  2. Guard Coverage Missing: API key guards not verified
  3. Error Response Untested: HTTP error formatting not validated
  4. Route Mapping Risk: Incorrect paths not caught at test time

Phase 5h delivers:

  • Controller tests for all 10 controllers
  • Request/response DTO validation coverage
  • API key guard verification
  • Error handling tests
  • Route mapping verification

📋 Context: Technical Debt Item

TD-006: Missing Controller Tests

Attribute Value
Type Test Debt
Priority High
Location apps/api/src/**/**.controller.ts
Interest Rate Medium-High
Principal (Effort) 3-4 days

Current State

Controller Has Tests Priority
fulfillment.controller.ts -
orders.controller.ts P1
print-jobs.controller.ts P1
product-mappings.controller.ts P2
shopify.controller.ts P1
simplyprint-webhook.controller.ts P1
shipments.controller.ts P2
sendcloud.controller.ts P2
health.controller.ts P3
event-log.controller.ts P3
cancellation.controller.ts P2

🛠️ Implementation Phases

Phase 1: Test Infrastructure Setup (2 hours)

Priority: Critical | Impact: High | Dependencies: None

1. Create Controller Test Utilities

Create apps/api/src/test/controller-test-utils.ts:

import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import { createMock } from '@golevelup/ts-jest';
import * as request from 'supertest';

/**
 * Create a testing module with mocked dependencies.
 */
export async function createTestingModule(
  controllers: any[],
  providers: { provide: any; useValue: any }[],
): Promise<TestingModule> {
  return Test.createTestingModule({
    controllers,
    providers,
  }).compile();
}

/**
 * Create a NestJS application for E2E-style controller tests.
 */
export async function createTestApp(module: TestingModule): Promise<INestApplication> {
  const app = module.createNestApplication();
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );
  await app.init();
  return app;
}

/**
 * Mock factory for service classes.
 */
export function createMockService<T>(type: new (...args: any[]) => T): jest.Mocked<T> {
  return createMock<T>();
}

/**
 * Common test headers.
 */
export const testHeaders = {
  withApiKey: (apiKey: string) => ({
    'X-API-Key': apiKey,
    'Content-Type': 'application/json',
  }),
  json: {
    'Content-Type': 'application/json',
  },
};

/**
 * Supertest request helper.
 */
export function testRequest(app: INestApplication) {
  return request(app.getHttpServer());
}

Phase 2: Orders Controller Tests (3 hours)

Priority: P1 | Impact: High | Dependencies: Phase 1

Create apps/api/src/orders/orders.controller.spec.ts:

import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { OrdersController } from './orders.controller';
import { OrdersService } from './orders.service';
import { createMock } from '@golevelup/ts-jest';
import { OrderStatus } from '@prisma/client';

describe('OrdersController', () => {
  let app: INestApplication;
  let service: jest.Mocked<OrdersService>;

  const mockOrder = {
    id: 'order-1',
    shopifyOrderId: '123456789',
    shopifyOrderNumber: '#1001',
    status: OrderStatus.PENDING,
    customerName: 'John Doe',
    customerEmail: 'john@example.com',
    shippingAddress: { address1: '123 Main St', city: 'Amsterdam', country: 'NL', zip: '1012' },
    totalPrice: '99.99',
    currency: 'EUR',
    totalParts: 3,
    completedParts: 0,
    notes: null,
    createdAt: new Date(),
    updatedAt: new Date(),
    completedAt: null,
    lineItems: [],
  };

  beforeAll(async () => {
    service = createMock<OrdersService>();

    const module: TestingModule = await Test.createTestingModule({
      controllers: [OrdersController],
      providers: [
        { provide: OrdersService, useValue: service },
      ],
    }).compile();

    app = module.createNestApplication();
    app.useGlobalPipes(
      new ValidationPipe({
        whitelist: true,
        forbidNonWhitelisted: true,
        transform: true,
      }),
    );
    await app.init();
  });

  afterAll(async () => {
    await app.close();
  });

  beforeEach(() => {
    jest.clearAllMocks();
  });

  describe('GET /api/v1/orders', () => {
    it('should return paginated orders', async () => {
      service.findAll.mockResolvedValue({
        orders: [mockOrder],
        total: 1,
        page: 1,
        pageSize: 10,
      });

      const response = await request(app.getHttpServer())
        .get('/api/v1/orders')
        .expect(200);

      expect(response.body).toEqual({
        orders: expect.arrayContaining([
          expect.objectContaining({ id: 'order-1' }),
        ]),
        total: 1,
        page: 1,
        pageSize: 10,
      });
      expect(service.findAll).toHaveBeenCalledWith({
        page: 1,
        pageSize: 10,
      });
    });

    it('should pass query parameters to service', async () => {
      service.findAll.mockResolvedValue({
        orders: [],
        total: 0,
        page: 2,
        pageSize: 20,
      });

      await request(app.getHttpServer())
        .get('/api/v1/orders')
        .query({ page: 2, pageSize: 20, status: 'PENDING' })
        .expect(200);

      expect(service.findAll).toHaveBeenCalledWith({
        page: 2,
        pageSize: 20,
        status: 'PENDING',
      });
    });

    it('should validate pageSize is positive', async () => {
      const response = await request(app.getHttpServer())
        .get('/api/v1/orders')
        .query({ pageSize: -1 })
        .expect(400);

      expect(response.body.message).toContain('pageSize');
    });
  });

  describe('GET /api/v1/orders/:id', () => {
    it('should return single order', async () => {
      service.findOne.mockResolvedValue(mockOrder);

      const response = await request(app.getHttpServer())
        .get('/api/v1/orders/order-1')
        .expect(200);

      expect(response.body).toEqual(
        expect.objectContaining({ id: 'order-1' }),
      );
    });

    it('should return 404 for non-existent order', async () => {
      service.findOne.mockResolvedValue(null);

      await request(app.getHttpServer())
        .get('/api/v1/orders/non-existent')
        .expect(404);
    });
  });

  describe('PUT /api/v1/orders/:id/status', () => {
    it('should require API key', async () => {
      await request(app.getHttpServer())
        .put('/api/v1/orders/order-1/status')
        .send({ status: 'PROCESSING' })
        .expect(401);
    });

    it('should update order status with valid API key', async () => {
      service.updateStatus.mockResolvedValue({
        ...mockOrder,
        status: OrderStatus.PROCESSING,
      });

      const response = await request(app.getHttpServer())
        .put('/api/v1/orders/order-1/status')
        .set('X-API-Key', 'valid-api-key')
        .send({ status: 'PROCESSING' })
        .expect(200);

      expect(response.body.status).toBe('PROCESSING');
    });

    it('should validate status enum', async () => {
      await request(app.getHttpServer())
        .put('/api/v1/orders/order-1/status')
        .set('X-API-Key', 'valid-api-key')
        .send({ status: 'INVALID_STATUS' })
        .expect(400);
    });
  });

  describe('PUT /api/v1/orders/:id/cancel', () => {
    it('should require API key', async () => {
      await request(app.getHttpServer())
        .put('/api/v1/orders/order-1/cancel')
        .expect(401);
    });

    it('should cancel order with valid API key', async () => {
      service.cancel.mockResolvedValue({
        ...mockOrder,
        status: OrderStatus.CANCELLED,
      });

      const response = await request(app.getHttpServer())
        .put('/api/v1/orders/order-1/cancel')
        .set('X-API-Key', 'valid-api-key')
        .expect(200);

      expect(response.body.status).toBe('CANCELLED');
    });
  });
});

Phase 3: Print Jobs Controller Tests (3 hours)

Priority: P1 | Impact: High | Dependencies: Phase 1

Create apps/api/src/print-jobs/print-jobs.controller.spec.ts:

import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { PrintJobsController } from './print-jobs.controller';
import { PrintJobsService } from './print-jobs.service';
import { createMock } from '@golevelup/ts-jest';
import { PrintJobStatus } from '@prisma/client';

describe('PrintJobsController', () => {
  let app: INestApplication;
  let service: jest.Mocked<PrintJobsService>;

  const mockPrintJob = {
    id: 'job-1',
    lineItemId: 'line-1',
    assemblyPartId: null,
    simplyPrintJobId: 'sp-123',
    status: PrintJobStatus.PRINTING,
    copyNumber: 1,
    printerId: 'printer-1',
    printerName: 'Prusa MK4',
    fileId: 'file-1',
    fileName: 'model.gcode',
    queuedAt: new Date(),
    startedAt: new Date(),
    completedAt: null,
    estimatedDuration: 3600,
    actualDuration: null,
    progress: 45,
    errorMessage: null,
    retryCount: 0,
    maxRetries: 3,
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  beforeAll(async () => {
    service = createMock<PrintJobsService>();

    const module: TestingModule = await Test.createTestingModule({
      controllers: [PrintJobsController],
      providers: [
        { provide: PrintJobsService, useValue: service },
      ],
    }).compile();

    app = module.createNestApplication();
    app.useGlobalPipes(
      new ValidationPipe({
        whitelist: true,
        forbidNonWhitelisted: true,
        transform: true,
      }),
    );
    await app.init();
  });

  afterAll(async () => {
    await app.close();
  });

  beforeEach(() => {
    jest.clearAllMocks();
  });

  describe('GET /api/v1/print-jobs', () => {
    it('should return paginated print jobs', async () => {
      service.findAll.mockResolvedValue({
        data: [mockPrintJob],
        total: 1,
        page: 1,
        pageSize: 10,
      });

      const response = await request(app.getHttpServer())
        .get('/api/v1/print-jobs')
        .expect(200);

      expect(response.body.data).toHaveLength(1);
      expect(response.body.total).toBe(1);
    });

    it('should filter by status', async () => {
      service.findAll.mockResolvedValue({
        data: [],
        total: 0,
        page: 1,
        pageSize: 10,
      });

      await request(app.getHttpServer())
        .get('/api/v1/print-jobs')
        .query({ status: 'PRINTING' })
        .expect(200);

      expect(service.findAll).toHaveBeenCalledWith(
        expect.objectContaining({ status: 'PRINTING' }),
      );
    });
  });

  describe('GET /api/v1/print-jobs/active', () => {
    it('should return active print jobs', async () => {
      service.findActive.mockResolvedValue([mockPrintJob]);

      const response = await request(app.getHttpServer())
        .get('/api/v1/print-jobs/active')
        .expect(200);

      expect(response.body).toHaveLength(1);
      expect(response.body[0].status).toBe('PRINTING');
    });
  });

  describe('GET /api/v1/print-jobs/order/:orderId', () => {
    it('should return print jobs for order', async () => {
      service.findByOrderId.mockResolvedValue([mockPrintJob]);

      const response = await request(app.getHttpServer())
        .get('/api/v1/print-jobs/order/order-1')
        .expect(200);

      expect(response.body).toHaveLength(1);
    });
  });

  describe('POST /api/v1/print-jobs/:id/retry', () => {
    it('should require API key', async () => {
      await request(app.getHttpServer())
        .post('/api/v1/print-jobs/job-1/retry')
        .expect(401);
    });

    it('should retry print job', async () => {
      service.retry.mockResolvedValue({
        ...mockPrintJob,
        status: PrintJobStatus.QUEUED,
        retryCount: 1,
      });

      const response = await request(app.getHttpServer())
        .post('/api/v1/print-jobs/job-1/retry')
        .set('X-API-Key', 'valid-api-key')
        .expect(200);

      expect(response.body.status).toBe('QUEUED');
      expect(response.body.retryCount).toBe(1);
    });
  });

  describe('POST /api/v1/print-jobs/:id/cancel', () => {
    it('should require API key', async () => {
      await request(app.getHttpServer())
        .post('/api/v1/print-jobs/job-1/cancel')
        .expect(401);
    });

    it('should cancel print job with reason', async () => {
      service.cancel.mockResolvedValue({
        ...mockPrintJob,
        status: PrintJobStatus.CANCELLED,
      });

      const response = await request(app.getHttpServer())
        .post('/api/v1/print-jobs/job-1/cancel')
        .set('X-API-Key', 'valid-api-key')
        .send({ reason: 'User requested cancellation' })
        .expect(200);

      expect(response.body.status).toBe('CANCELLED');
      expect(service.cancel).toHaveBeenCalledWith('job-1', 'User requested cancellation');
    });
  });
});

Phase 4: Webhook Controller Tests (3 hours)

Priority: P1 | Impact: High | Dependencies: Phase 1

1. Shopify Controller Tests

Create apps/api/src/shopify/shopify.controller.spec.ts:

import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { ShopifyController } from './shopify.controller';
import { ShopifyService } from './shopify.service';
import { createMock } from '@golevelup/ts-jest';

describe('ShopifyController', () => {
  let app: INestApplication;
  let service: jest.Mocked<ShopifyService>;

  beforeAll(async () => {
    service = createMock<ShopifyService>();

    const module: TestingModule = await Test.createTestingModule({
      controllers: [ShopifyController],
      providers: [
        { provide: ShopifyService, useValue: service },
      ],
    }).compile();

    app = module.createNestApplication();
    await app.init();
  });

  afterAll(async () => {
    await app.close();
  });

  describe('POST /webhooks/shopify/orders/create', () => {
    it('should process order created webhook', async () => {
      service.handleOrderCreated.mockResolvedValue(undefined);

      const payload = {
        id: 123456789,
        order_number: 1001,
        email: 'customer@example.com',
        line_items: [],
      };

      await request(app.getHttpServer())
        .post('/webhooks/shopify/orders/create')
        .set('X-Shopify-Hmac-SHA256', 'valid-hmac')
        .set('X-Shopify-Topic', 'orders/create')
        .set('X-Shopify-Webhook-Id', 'webhook-123')
        .send(payload)
        .expect(200);

      expect(service.handleOrderCreated).toHaveBeenCalled();
    });
  });

  describe('POST /webhooks/shopify/orders/cancelled', () => {
    it('should process order cancelled webhook', async () => {
      service.handleOrderCancelled.mockResolvedValue(undefined);

      const payload = {
        id: 123456789,
        order_number: 1001,
      };

      await request(app.getHttpServer())
        .post('/webhooks/shopify/orders/cancelled')
        .set('X-Shopify-Hmac-SHA256', 'valid-hmac')
        .set('X-Shopify-Topic', 'orders/cancelled')
        .set('X-Shopify-Webhook-Id', 'webhook-123')
        .send(payload)
        .expect(200);

      expect(service.handleOrderCancelled).toHaveBeenCalled();
    });
  });
});

2. SimplyPrint Webhook Controller Tests

Create apps/api/src/simplyprint/simplyprint-webhook.controller.spec.ts:

import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { SimplyPrintWebhookController } from './simplyprint-webhook.controller';
import { SimplyPrintWebhookService } from './simplyprint-webhook.service';
import { createMock } from '@golevelup/ts-jest';

describe('SimplyPrintWebhookController', () => {
  let app: INestApplication;
  let service: jest.Mocked<SimplyPrintWebhookService>;

  beforeAll(async () => {
    service = createMock<SimplyPrintWebhookService>();

    const module: TestingModule = await Test.createTestingModule({
      controllers: [SimplyPrintWebhookController],
      providers: [
        { provide: SimplyPrintWebhookService, useValue: service },
      ],
    }).compile();

    app = module.createNestApplication();
    await app.init();
  });

  afterAll(async () => {
    await app.close();
  });

  describe('POST /webhooks/simplyprint', () => {
    it('should process job started event', async () => {
      service.handleWebhook.mockResolvedValue(undefined);

      const payload = {
        event: 'job.started',
        job_id: 'sp-123',
        printer_id: 'printer-1',
      };

      await request(app.getHttpServer())
        .post('/webhooks/simplyprint')
        .send(payload)
        .expect(200);

      expect(service.handleWebhook).toHaveBeenCalledWith(
        expect.objectContaining({ event: 'job.started' }),
      );
    });

    it('should process job completed event', async () => {
      service.handleWebhook.mockResolvedValue(undefined);

      const payload = {
        event: 'job.completed',
        job_id: 'sp-123',
      };

      await request(app.getHttpServer())
        .post('/webhooks/simplyprint')
        .send(payload)
        .expect(200);
    });

    it('should process job failed event', async () => {
      service.handleWebhook.mockResolvedValue(undefined);

      const payload = {
        event: 'job.failed',
        job_id: 'sp-123',
        error: 'Filament runout',
      };

      await request(app.getHttpServer())
        .post('/webhooks/simplyprint')
        .send(payload)
        .expect(200);
    });
  });
});

Phase 5: Remaining Controller Tests (4 hours)

Priority: P2-P3 | Impact: Medium | Dependencies: Phase 1

1. Product Mappings Controller Tests

Create apps/api/src/product-mappings/product-mappings.controller.spec.ts:

import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { ProductMappingsController } from './product-mappings.controller';
import { ProductMappingsService } from './product-mappings.service';
import { createMock } from '@golevelup/ts-jest';

describe('ProductMappingsController', () => {
  let app: INestApplication;
  let service: jest.Mocked<ProductMappingsService>;

  const mockMapping = {
    id: 'mapping-1',
    shopifyProductId: 'sp-123',
    shopifyVariantId: null,
    sku: 'TEST-SKU',
    productName: 'Test Product',
    description: 'A test product',
    isAssembly: false,
    isActive: true,
    modelFileId: 'file-1',
    modelFileName: 'model.stl',
    defaultPrintProfile: null,
    createdAt: new Date(),
    updatedAt: new Date(),
    assemblyParts: [],
  };

  beforeAll(async () => {
    service = createMock<ProductMappingsService>();

    const module: TestingModule = await Test.createTestingModule({
      controllers: [ProductMappingsController],
      providers: [
        { provide: ProductMappingsService, useValue: service },
      ],
    }).compile();

    app = module.createNestApplication();
    app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
    await app.init();
  });

  afterAll(async () => {
    await app.close();
  });

  describe('GET /api/v1/product-mappings', () => {
    it('should return paginated mappings', async () => {
      service.findAll.mockResolvedValue({
        mappings: [mockMapping],
        total: 1,
        page: 1,
        pageSize: 10,
      });

      const response = await request(app.getHttpServer())
        .get('/api/v1/product-mappings')
        .expect(200);

      expect(response.body.mappings).toHaveLength(1);
    });
  });

  describe('POST /api/v1/product-mappings', () => {
    it('should require API key', async () => {
      await request(app.getHttpServer())
        .post('/api/v1/product-mappings')
        .send({ sku: 'TEST', productName: 'Test' })
        .expect(401);
    });

    it('should create mapping with valid data', async () => {
      service.create.mockResolvedValue(mockMapping);

      const response = await request(app.getHttpServer())
        .post('/api/v1/product-mappings')
        .set('X-API-Key', 'valid-api-key')
        .send({
          shopifyProductId: 'sp-123',
          sku: 'TEST-SKU',
          productName: 'Test Product',
        })
        .expect(201);

      expect(response.body.id).toBe('mapping-1');
    });

    it('should validate required fields', async () => {
      await request(app.getHttpServer())
        .post('/api/v1/product-mappings')
        .set('X-API-Key', 'valid-api-key')
        .send({})
        .expect(400);
    });
  });

  describe('DELETE /api/v1/product-mappings/:id', () => {
    it('should delete mapping', async () => {
      service.delete.mockResolvedValue(undefined);

      await request(app.getHttpServer())
        .delete('/api/v1/product-mappings/mapping-1')
        .set('X-API-Key', 'valid-api-key')
        .expect(204);
    });
  });
});

2. Health Controller Tests

Create apps/api/src/health/health.controller.spec.ts:

import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { HealthController } from './health.controller';
import { HealthService } from './health.service';
import { createMock } from '@golevelup/ts-jest';

describe('HealthController', () => {
  let app: INestApplication;
  let service: jest.Mocked<HealthService>;

  beforeAll(async () => {
    service = createMock<HealthService>();

    const module: TestingModule = await Test.createTestingModule({
      controllers: [HealthController],
      providers: [
        { provide: HealthService, useValue: service },
      ],
    }).compile();

    app = module.createNestApplication();
    await app.init();
  });

  afterAll(async () => {
    await app.close();
  });

  describe('GET /health', () => {
    it('should return healthy status', async () => {
      service.check.mockResolvedValue({
        status: 'ok',
        timestamp: new Date().toISOString(),
      });

      const response = await request(app.getHttpServer())
        .get('/health')
        .expect(200);

      expect(response.body.status).toBe('ok');
    });
  });

  describe('GET /health/live', () => {
    it('should return liveness probe', async () => {
      service.liveness.mockResolvedValue({ status: 'ok' });

      await request(app.getHttpServer())
        .get('/health/live')
        .expect(200);
    });
  });

  describe('GET /health/ready', () => {
    it('should return readiness with database status', async () => {
      service.readiness.mockResolvedValue({
        status: 'ok',
        database: 'connected',
      });

      const response = await request(app.getHttpServer())
        .get('/health/ready')
        .expect(200);

      expect(response.body.database).toBe('connected');
    });

    it('should return 503 when database disconnected', async () => {
      service.readiness.mockResolvedValue({
        status: 'unhealthy',
        database: 'disconnected',
      });

      await request(app.getHttpServer())
        .get('/health/ready')
        .expect(503);
    });
  });
});

3. Event Log Controller Tests

Create apps/api/src/event-log/event-log.controller.spec.ts:

import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { EventLogController } from './event-log.controller';
import { EventLogService } from './event-log.service';
import { createMock } from '@golevelup/ts-jest';

describe('EventLogController', () => {
  let app: INestApplication;
  let service: jest.Mocked<EventLogService>;

  const mockLog = {
    id: 'log-1',
    orderId: 'order-1',
    printJobId: null,
    eventType: 'order.created',
    severity: 'INFO',
    message: 'Order created',
    metadata: {},
    createdAt: new Date(),
  };

  beforeAll(async () => {
    service = createMock<EventLogService>();

    const module: TestingModule = await Test.createTestingModule({
      controllers: [EventLogController],
      providers: [
        { provide: EventLogService, useValue: service },
      ],
    }).compile();

    app = module.createNestApplication();
    app.useGlobalPipes(new ValidationPipe({ transform: true }));
    await app.init();
  });

  afterAll(async () => {
    await app.close();
  });

  describe('GET /api/v1/logs', () => {
    it('should return paginated logs', async () => {
      service.findAll.mockResolvedValue({
        logs: [mockLog],
        total: 1,
        page: 1,
        pageSize: 10,
      });

      const response = await request(app.getHttpServer())
        .get('/api/v1/logs')
        .expect(200);

      expect(response.body.logs).toHaveLength(1);
    });

    it('should filter by severity', async () => {
      service.findAll.mockResolvedValue({
        logs: [],
        total: 0,
        page: 1,
        pageSize: 10,
      });

      await request(app.getHttpServer())
        .get('/api/v1/logs')
        .query({ severity: 'ERROR' })
        .expect(200);

      expect(service.findAll).toHaveBeenCalledWith(
        expect.objectContaining({ severity: 'ERROR' }),
      );
    });

    it('should filter by orderId', async () => {
      await request(app.getHttpServer())
        .get('/api/v1/logs')
        .query({ orderId: 'order-1' })
        .expect(200);

      expect(service.findAll).toHaveBeenCalledWith(
        expect.objectContaining({ orderId: 'order-1' }),
      );
    });
  });
});

Phase 6: Documentation Updates (30 minutes)

Priority: Medium | Impact: Medium | Dependencies: Phase 5

1. Update Technical Debt Register

Update docs/04-development/techdebt/technical-debt-register.md:

### ~~TD-006: Missing Controller Tests~~ ✅ RESOLVED

**Type:** Test Debt  
**Status:****Resolved in Phase 5h**  
**Resolution Date:** 2026-XX-XX

#### Resolution

Added comprehensive controller tests for all 10 controllers:

| Controller | Test File | Tests |
|------------|-----------|-------|
| OrdersController | `orders.controller.spec.ts` | 12 |
| PrintJobsController | `print-jobs.controller.spec.ts` | 10 |
| ShopifyController | `shopify.controller.spec.ts` | 4 |
| SimplyPrintWebhookController | `simplyprint-webhook.controller.spec.ts` | 4 |
| ProductMappingsController | `product-mappings.controller.spec.ts` | 6 |
| HealthController | `health.controller.spec.ts` | 4 |
| EventLogController | `event-log.controller.spec.ts` | 4 |
| ShipmentsController | `shipments.controller.spec.ts` | 4 |
| SendcloudController | `sendcloud.controller.spec.ts` | 3 |
| CancellationController | `cancellation.controller.spec.ts` | 3 |

**Test Coverage:**
- Request/response DTO validation
- API key guard verification
- Error handling
- Route mapping

📁 Files to Create/Modify

New Files

apps/api/src/test/controller-test-utils.ts
apps/api/src/orders/orders.controller.spec.ts
apps/api/src/print-jobs/print-jobs.controller.spec.ts
apps/api/src/shopify/shopify.controller.spec.ts
apps/api/src/simplyprint/simplyprint-webhook.controller.spec.ts
apps/api/src/product-mappings/product-mappings.controller.spec.ts
apps/api/src/shipments/shipments.controller.spec.ts
apps/api/src/sendcloud/sendcloud.controller.spec.ts
apps/api/src/health/health.controller.spec.ts
apps/api/src/event-log/event-log.controller.spec.ts
apps/api/src/cancellation/cancellation.controller.spec.ts

Modified Files

docs/04-development/techdebt/technical-debt-register.md

✅ Validation Checklist

All Controllers Tested

  • OrdersController - GET list, GET single, PUT status, PUT cancel
  • PrintJobsController - GET list, GET active, GET by order, POST retry, POST cancel
  • ShopifyController - Webhook endpoints
  • SimplyPrintWebhookController - Webhook handling
  • ProductMappingsController - CRUD operations
  • ShipmentsController - GET, POST label
  • SendcloudController - Webhooks
  • HealthController - All probe endpoints
  • EventLogController - GET with filters
  • CancellationController - Cancel operations

Final Verification

# All tests pass
pnpm nx test api

# Coverage improved
pnpm nx test api --coverage

# Build passes
pnpm nx build api

END OF PROMPT


This prompt resolves TD-006 from the technical debt register by adding comprehensive controller tests for all backend controllers.