Files
gridpilot.gg/core/shared/application/AsyncUseCase.test.ts
Marc Mintel 648dce2193
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 5m43s
Contract Testing / contract-snapshot (pull_request) Has been skipped
core tests
2026-01-22 18:52:22 +01:00

413 lines
14 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { AsyncUseCase } from './AsyncUseCase';
import { Result } from '../domain/Result';
import { ApplicationErrorCode } from '../errors/ApplicationErrorCode';
describe('AsyncUseCase', () => {
describe('AsyncUseCase interface', () => {
it('should have execute method returning Promise<Result>', async () => {
// Concrete implementation for testing
class TestAsyncUseCase implements AsyncUseCase<{ id: string }, { data: string }, 'NOT_FOUND'> {
async execute(input: { id: string }): Promise<Result<{ data: string }, ApplicationErrorCode<'NOT_FOUND'>>> {
if (input.id === 'not-found') {
return Result.err({ code: 'NOT_FOUND' });
}
return Result.ok({ data: `Data for ${input.id}` });
}
}
const useCase = new TestAsyncUseCase();
const successResult = await useCase.execute({ id: 'test-123' });
expect(successResult.isOk()).toBe(true);
expect(successResult.unwrap()).toEqual({ data: 'Data for test-123' });
const errorResult = await useCase.execute({ id: 'not-found' });
expect(errorResult.isErr()).toBe(true);
expect(errorResult.unwrapErr()).toEqual({ code: 'NOT_FOUND' });
});
it('should support different input types', async () => {
interface GetUserInput {
userId: string;
includeProfile?: boolean;
}
interface UserDTO {
id: string;
name: string;
email: string;
profile?: {
avatar: string;
bio: string;
};
}
type GetUserErrorCode = 'USER_NOT_FOUND' | 'PERMISSION_DENIED';
class GetUserUseCase implements AsyncUseCase<GetUserInput, UserDTO, GetUserErrorCode> {
async execute(input: GetUserInput): Promise<Result<UserDTO, ApplicationErrorCode<GetUserErrorCode>>> {
if (input.userId === 'not-found') {
return Result.err({ code: 'USER_NOT_FOUND' });
}
if (input.userId === 'no-permission') {
return Result.err({ code: 'PERMISSION_DENIED' });
}
const user: UserDTO = {
id: input.userId,
name: 'John Doe',
email: 'john@example.com'
};
if (input.includeProfile) {
user.profile = {
avatar: 'avatar.jpg',
bio: 'Software developer'
};
}
return Result.ok(user);
}
}
const useCase = new GetUserUseCase();
// Success case with profile
const successWithProfile = await useCase.execute({
userId: 'user-123',
includeProfile: true
});
expect(successWithProfile.isOk()).toBe(true);
const userWithProfile = successWithProfile.unwrap();
expect(userWithProfile.id).toBe('user-123');
expect(userWithProfile.profile).toBeDefined();
expect(userWithProfile.profile?.avatar).toBe('avatar.jpg');
// Success case without profile
const successWithoutProfile = await useCase.execute({
userId: 'user-456',
includeProfile: false
});
expect(successWithoutProfile.isOk()).toBe(true);
const userWithoutProfile = successWithoutProfile.unwrap();
expect(userWithoutProfile.id).toBe('user-456');
expect(userWithoutProfile.profile).toBeUndefined();
// Error cases
const notFoundResult = await useCase.execute({ userId: 'not-found' });
expect(notFoundResult.isErr()).toBe(true);
expect(notFoundResult.unwrapErr()).toEqual({ code: 'USER_NOT_FOUND' });
const permissionResult = await useCase.execute({ userId: 'no-permission' });
expect(permissionResult.isErr()).toBe(true);
expect(permissionResult.unwrapErr()).toEqual({ code: 'PERMISSION_DENIED' });
});
it('should support complex query patterns', async () => {
interface SearchOrdersInput {
customerId?: string;
status?: 'pending' | 'completed' | 'cancelled';
dateRange?: { start: Date; end: Date };
page?: number;
limit?: number;
}
interface OrderDTO {
id: string;
customerId: string;
status: string;
total: number;
items: Array<{ productId: string; quantity: number; price: number }>;
createdAt: Date;
}
interface OrdersResult {
orders: OrderDTO[];
total: number;
page: number;
totalPages: number;
filters: SearchOrdersInput;
}
type SearchOrdersErrorCode = 'INVALID_FILTERS' | 'NO_ORDERS_FOUND';
class SearchOrdersUseCase implements AsyncUseCase<SearchOrdersInput, OrdersResult, SearchOrdersErrorCode> {
async execute(input: SearchOrdersInput): Promise<Result<OrdersResult, ApplicationErrorCode<SearchOrdersErrorCode>>> {
// Validate at least one filter
if (!input.customerId && !input.status && !input.dateRange) {
return Result.err({ code: 'INVALID_FILTERS' });
}
// Simulate database query
const allOrders: OrderDTO[] = [
{
id: 'order-1',
customerId: 'cust-1',
status: 'completed',
total: 150,
items: [{ productId: 'prod-1', quantity: 2, price: 75 }],
createdAt: new Date('2024-01-01')
},
{
id: 'order-2',
customerId: 'cust-1',
status: 'pending',
total: 200,
items: [{ productId: 'prod-2', quantity: 1, price: 200 }],
createdAt: new Date('2024-01-02')
},
{
id: 'order-3',
customerId: 'cust-2',
status: 'completed',
total: 300,
items: [{ productId: 'prod-3', quantity: 3, price: 100 }],
createdAt: new Date('2024-01-03')
}
];
// Apply filters
let filteredOrders = allOrders;
if (input.customerId) {
filteredOrders = filteredOrders.filter(o => o.customerId === input.customerId);
}
if (input.status) {
filteredOrders = filteredOrders.filter(o => o.status === input.status);
}
if (input.dateRange) {
filteredOrders = filteredOrders.filter(o => {
const orderDate = o.createdAt.getTime();
return orderDate >= input.dateRange!.start.getTime() &&
orderDate <= input.dateRange!.end.getTime();
});
}
if (filteredOrders.length === 0) {
return Result.err({ code: 'NO_ORDERS_FOUND' });
}
// Apply pagination
const page = input.page || 1;
const limit = input.limit || 10;
const start = (page - 1) * limit;
const end = start + limit;
const paginatedOrders = filteredOrders.slice(start, end);
const result: OrdersResult = {
orders: paginatedOrders,
total: filteredOrders.length,
page,
totalPages: Math.ceil(filteredOrders.length / limit),
filters: input
};
return Result.ok(result);
}
}
const useCase = new SearchOrdersUseCase();
// Success case - filter by customer
const customerResult = await useCase.execute({ customerId: 'cust-1' });
expect(customerResult.isOk()).toBe(true);
const customerOrders = customerResult.unwrap();
expect(customerOrders.orders).toHaveLength(2);
expect(customerOrders.total).toBe(2);
// Success case - filter by status
const statusResult = await useCase.execute({ status: 'completed' });
expect(statusResult.isOk()).toBe(true);
const completedOrders = statusResult.unwrap();
expect(completedOrders.orders).toHaveLength(2);
expect(completedOrders.total).toBe(2);
// Success case - filter by date range
const dateResult = await useCase.execute({
dateRange: {
start: new Date('2024-01-01'),
end: new Date('2024-01-02')
}
});
expect(dateResult.isOk()).toBe(true);
const dateOrders = dateResult.unwrap();
expect(dateOrders.orders).toHaveLength(2);
expect(dateOrders.total).toBe(2);
// Error case - no filters
const noFiltersResult = await useCase.execute({});
expect(noFiltersResult.isErr()).toBe(true);
expect(noFiltersResult.unwrapErr()).toEqual({ code: 'INVALID_FILTERS' });
// Error case - no matching orders
const noOrdersResult = await useCase.execute({ customerId: 'nonexistent' });
expect(noOrdersResult.isErr()).toBe(true);
expect(noOrdersResult.unwrapErr()).toEqual({ code: 'NO_ORDERS_FOUND' });
});
it('should support async operations with delays', async () => {
interface ProcessBatchInput {
items: Array<{ id: string; data: string }>;
delayMs?: number;
}
interface ProcessBatchResult {
processed: number;
failed: number;
results: Array<{ id: string; status: 'success' | 'failed'; message?: string }>;
}
type ProcessBatchErrorCode = 'EMPTY_BATCH' | 'PROCESSING_ERROR';
class ProcessBatchUseCase implements AsyncUseCase<ProcessBatchInput, ProcessBatchResult, ProcessBatchErrorCode> {
async execute(input: ProcessBatchInput): Promise<Result<ProcessBatchResult, ApplicationErrorCode<ProcessBatchErrorCode>>> {
if (input.items.length === 0) {
return Result.err({ code: 'EMPTY_BATCH' });
}
const delay = input.delayMs || 10;
const results: Array<{ id: string; status: 'success' | 'failed'; message?: string }> = [];
let processed = 0;
let failed = 0;
for (const item of input.items) {
// Simulate async processing with delay
await new Promise(resolve => setTimeout(resolve, delay));
// Simulate some failures
if (item.id === 'fail-1' || item.id === 'fail-2') {
results.push({ id: item.id, status: 'failed', message: 'Processing failed' });
failed++;
} else {
results.push({ id: item.id, status: 'success' });
processed++;
}
}
return Result.ok({
processed,
failed,
results
});
}
}
const useCase = new ProcessBatchUseCase();
// Success case
const successResult = await useCase.execute({
items: [
{ id: 'item-1', data: 'data1' },
{ id: 'item-2', data: 'data2' },
{ id: 'item-3', data: 'data3' }
],
delayMs: 5
});
expect(successResult.isOk()).toBe(true);
const batchResult = successResult.unwrap();
expect(batchResult.processed).toBe(3);
expect(batchResult.failed).toBe(0);
expect(batchResult.results).toHaveLength(3);
// Mixed success/failure case
const mixedResult = await useCase.execute({
items: [
{ id: 'item-1', data: 'data1' },
{ id: 'fail-1', data: 'data2' },
{ id: 'item-3', data: 'data3' },
{ id: 'fail-2', data: 'data4' }
],
delayMs: 5
});
expect(mixedResult.isOk()).toBe(true);
const mixedBatchResult = mixedResult.unwrap();
expect(mixedBatchResult.processed).toBe(2);
expect(mixedBatchResult.failed).toBe(2);
expect(mixedBatchResult.results).toHaveLength(4);
// Error case - empty batch
const emptyResult = await useCase.execute({ items: [] });
expect(emptyResult.isErr()).toBe(true);
expect(emptyResult.unwrapErr()).toEqual({ code: 'EMPTY_BATCH' });
});
it('should support streaming-like operations', async () => {
interface StreamInput {
source: string;
chunkSize?: number;
}
interface StreamResult {
chunks: string[];
totalSize: number;
source: string;
}
type StreamErrorCode = 'SOURCE_NOT_FOUND' | 'STREAM_ERROR';
class StreamUseCase implements AsyncUseCase<StreamInput, StreamResult, StreamErrorCode> {
async execute(input: StreamInput): Promise<Result<StreamResult, ApplicationErrorCode<StreamErrorCode>>> {
if (input.source === 'not-found') {
return Result.err({ code: 'SOURCE_NOT_FOUND' });
}
if (input.source === 'error') {
return Result.err({ code: 'STREAM_ERROR' });
}
const chunkSize = input.chunkSize || 10;
const data = 'This is a test data stream that will be split into chunks';
const chunks: string[] = [];
for (let i = 0; i < data.length; i += chunkSize) {
// Simulate async chunk reading
await new Promise(resolve => setTimeout(resolve, 1));
chunks.push(data.slice(i, i + chunkSize));
}
return Result.ok({
chunks,
totalSize: data.length,
source: input.source
});
}
}
const useCase = new StreamUseCase();
// Success case with default chunk size
const defaultResult = await useCase.execute({ source: 'test-source' });
expect(defaultResult.isOk()).toBe(true);
const defaultStream = defaultResult.unwrap();
expect(defaultStream.chunks).toHaveLength(6);
expect(defaultStream.totalSize).toBe(57);
expect(defaultStream.source).toBe('test-source');
// Success case with custom chunk size
const customResult = await useCase.execute({ source: 'test-source', chunkSize: 15 });
expect(customResult.isOk()).toBe(true);
const customStream = customResult.unwrap();
expect(customStream.chunks).toHaveLength(4);
expect(customStream.totalSize).toBe(57);
// Error case - source not found
const notFoundResult = await useCase.execute({ source: 'not-found' });
expect(notFoundResult.isErr()).toBe(true);
expect(notFoundResult.unwrapErr()).toEqual({ code: 'SOURCE_NOT_FOUND' });
// Error case - stream error
const errorResult = await useCase.execute({ source: 'error' });
expect(errorResult.isErr()).toBe(true);
expect(errorResult.unwrapErr()).toEqual({ code: 'STREAM_ERROR' });
});
});
});