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:
- Request Validation Gaps: DTO validation not tested
- Guard Coverage Missing: API key guards not verified
- Error Response Untested: HTTP error formatting not validated
- 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.