325 lines
11 KiB
TypeScript
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' });
|
|
});
|
|
});
|
|
});
|