core tests
This commit is contained in:
324
core/shared/application/UseCase.test.ts
Normal file
324
core/shared/application/UseCase.test.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
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' });
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user