Files
gridpilot.gg/core/shared/application/UseCase.test.ts
Marc Mintel 093eece3d7
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 5m47s
Contract Testing / contract-snapshot (pull_request) Has been skipped
core tests
2026-01-22 18:20:33 +01:00

325 lines
11 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { UseCase } from './UseCase';
import { Result } from '../domain/Result';
import { ApplicationErrorCode } from '../errors/ApplicationErrorCode';
describe('UseCase', () => {
describe('UseCase interface', () => {
it('should have execute method returning Promise<Result>', async () => {
// Concrete implementation for testing
class TestUseCase implements UseCase<{ value: number }, string, 'INVALID_VALUE'> {
async execute(input: { value: number }): Promise<Result<string, ApplicationErrorCode<'INVALID_VALUE'>>> {
if (input.value < 0) {
return Result.err({ code: 'INVALID_VALUE' });
}
return Result.ok(`Value: ${input.value}`);
}
}
const useCase = new TestUseCase();
const successResult = await useCase.execute({ value: 42 });
expect(successResult.isOk()).toBe(true);
expect(successResult.unwrap()).toBe('Value: 42');
const errorResult = await useCase.execute({ value: -1 });
expect(errorResult.isErr()).toBe(true);
expect(errorResult.unwrapErr()).toEqual({ code: 'INVALID_VALUE' });
});
it('should support different input types', async () => {
interface CreateUserInput {
name: string;
email: string;
}
interface UserDTO {
id: string;
name: string;
email: string;
}
type CreateUserErrorCode = 'INVALID_EMAIL' | 'USER_ALREADY_EXISTS';
class CreateUserUseCase implements UseCase<CreateUserInput, UserDTO, CreateUserErrorCode> {
async execute(input: CreateUserInput): Promise<Result<UserDTO, ApplicationErrorCode<CreateUserErrorCode>>> {
if (!input.email.includes('@')) {
return Result.err({ code: 'INVALID_EMAIL' });
}
// Simulate user creation
const user: UserDTO = {
id: `user-${Date.now()}`,
name: input.name,
email: input.email
};
return Result.ok(user);
}
}
const useCase = new CreateUserUseCase();
const successResult = await useCase.execute({
name: 'John Doe',
email: 'john@example.com'
});
expect(successResult.isOk()).toBe(true);
const user = successResult.unwrap();
expect(user.name).toBe('John Doe');
expect(user.email).toBe('john@example.com');
expect(user.id).toMatch(/^user-\d+$/);
const errorResult = await useCase.execute({
name: 'John Doe',
email: 'invalid-email'
});
expect(errorResult.isErr()).toBe(true);
expect(errorResult.unwrapErr()).toEqual({ code: 'INVALID_EMAIL' });
});
it('should support complex error codes', async () => {
interface ProcessPaymentInput {
amount: number;
currency: string;
paymentMethod: 'credit_card' | 'paypal' | 'bank_transfer';
}
interface PaymentReceipt {
receiptId: string;
amount: number;
currency: string;
status: 'completed' | 'pending' | 'failed';
}
type ProcessPaymentErrorCode =
| 'INSUFFICIENT_FUNDS'
| 'INVALID_CURRENCY'
| 'PAYMENT_METHOD_NOT_SUPPORTED'
| 'NETWORK_ERROR';
class ProcessPaymentUseCase implements UseCase<ProcessPaymentInput, PaymentReceipt, ProcessPaymentErrorCode> {
async execute(input: ProcessPaymentInput): Promise<Result<PaymentReceipt, ApplicationErrorCode<ProcessPaymentErrorCode>>> {
// Validate currency
const supportedCurrencies = ['USD', 'EUR', 'GBP'];
if (!supportedCurrencies.includes(input.currency)) {
return Result.err({ code: 'INVALID_CURRENCY' });
}
// Validate payment method
const supportedMethods = ['credit_card', 'paypal'];
if (!supportedMethods.includes(input.paymentMethod)) {
return Result.err({ code: 'PAYMENT_METHOD_NOT_SUPPORTED' });
}
// Simulate payment processing
if (input.amount > 10000) {
return Result.err({ code: 'INSUFFICIENT_FUNDS' });
}
const receipt: PaymentReceipt = {
receiptId: `receipt-${Date.now()}`,
amount: input.amount,
currency: input.currency,
status: 'completed'
};
return Result.ok(receipt);
}
}
const useCase = new ProcessPaymentUseCase();
// Success case
const successResult = await useCase.execute({
amount: 100,
currency: 'USD',
paymentMethod: 'credit_card'
});
expect(successResult.isOk()).toBe(true);
const receipt = successResult.unwrap();
expect(receipt.amount).toBe(100);
expect(receipt.currency).toBe('USD');
expect(receipt.status).toBe('completed');
// Error case - invalid currency
const currencyError = await useCase.execute({
amount: 100,
currency: 'JPY',
paymentMethod: 'credit_card'
});
expect(currencyError.isErr()).toBe(true);
expect(currencyError.unwrapErr()).toEqual({ code: 'INVALID_CURRENCY' });
// Error case - unsupported payment method
const methodError = await useCase.execute({
amount: 100,
currency: 'USD',
paymentMethod: 'bank_transfer'
});
expect(methodError.isErr()).toBe(true);
expect(methodError.unwrapErr()).toEqual({ code: 'PAYMENT_METHOD_NOT_SUPPORTED' });
// Error case - insufficient funds
const fundsError = await useCase.execute({
amount: 15000,
currency: 'USD',
paymentMethod: 'credit_card'
});
expect(fundsError.isErr()).toBe(true);
expect(fundsError.unwrapErr()).toEqual({ code: 'INSUFFICIENT_FUNDS' });
});
it('should support void success type', async () => {
interface DeleteUserInput {
userId: string;
}
type DeleteUserErrorCode = 'USER_NOT_FOUND' | 'INSUFFICIENT_PERMISSIONS';
class DeleteUserUseCase implements UseCase<DeleteUserInput, void, DeleteUserErrorCode> {
async execute(input: DeleteUserInput): Promise<Result<void, ApplicationErrorCode<DeleteUserErrorCode>>> {
if (input.userId === 'not-found') {
return Result.err({ code: 'USER_NOT_FOUND' });
}
if (input.userId === 'no-permission') {
return Result.err({ code: 'INSUFFICIENT_PERMISSIONS' });
}
// Simulate deletion
return Result.ok(undefined);
}
}
const useCase = new DeleteUserUseCase();
const successResult = await useCase.execute({ userId: 'user-123' });
expect(successResult.isOk()).toBe(true);
expect(successResult.unwrap()).toBeUndefined();
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: 'INSUFFICIENT_PERMISSIONS' });
});
it('should support complex success types', async () => {
interface SearchInput {
query: string;
filters?: {
category?: string;
priceRange?: { min: number; max: number };
inStock?: boolean;
};
page?: number;
limit?: number;
}
interface SearchResult {
items: Array<{
id: string;
name: string;
price: number;
category: string;
inStock: boolean;
}>;
total: number;
page: number;
totalPages: number;
}
type SearchErrorCode = 'INVALID_QUERY' | 'NO_RESULTS';
class SearchUseCase implements UseCase<SearchInput, SearchResult, SearchErrorCode> {
async execute(input: SearchInput): Promise<Result<SearchResult, ApplicationErrorCode<SearchErrorCode>>> {
if (!input.query || input.query.length < 2) {
return Result.err({ code: 'INVALID_QUERY' });
}
// Simulate search results
const items = [
{ id: '1', name: 'Product A', price: 100, category: 'electronics', inStock: true },
{ id: '2', name: 'Product B', price: 200, category: 'electronics', inStock: false },
{ id: '3', name: 'Product C', price: 150, category: 'clothing', inStock: true }
];
const filteredItems = items.filter(item => {
if (input.filters?.category && item.category !== input.filters.category) {
return false;
}
if (input.filters?.priceRange) {
if (item.price < input.filters.priceRange.min || item.price > input.filters.priceRange.max) {
return false;
}
}
if (input.filters?.inStock !== undefined && item.inStock !== input.filters.inStock) {
return false;
}
return true;
});
if (filteredItems.length === 0) {
return Result.err({ code: 'NO_RESULTS' });
}
const page = input.page || 1;
const limit = input.limit || 10;
const start = (page - 1) * limit;
const end = start + limit;
const paginatedItems = filteredItems.slice(start, end);
const result: SearchResult = {
items: paginatedItems,
total: filteredItems.length,
page,
totalPages: Math.ceil(filteredItems.length / limit)
};
return Result.ok(result);
}
}
const useCase = new SearchUseCase();
// Success case
const successResult = await useCase.execute({
query: 'product',
filters: { category: 'electronics' },
page: 1,
limit: 10
});
expect(successResult.isOk()).toBe(true);
const searchResult = successResult.unwrap();
expect(searchResult.items).toHaveLength(2);
expect(searchResult.total).toBe(2);
expect(searchResult.page).toBe(1);
expect(searchResult.totalPages).toBe(1);
// Error case - invalid query
const invalidQueryResult = await useCase.execute({ query: 'a' });
expect(invalidQueryResult.isErr()).toBe(true);
expect(invalidQueryResult.unwrapErr()).toEqual({ code: 'INVALID_QUERY' });
// Error case - no results
const noResultsResult = await useCase.execute({
query: 'product',
filters: { category: 'nonexistent' }
});
expect(noResultsResult.isErr()).toBe(true);
expect(noResultsResult.unwrapErr()).toEqual({ code: 'NO_RESULTS' });
});
});
});