Skip to content

Commit ed71717

Browse files
committed
feat: adding product domain
1 parent 77a125a commit ed71717

File tree

11 files changed

+750
-9
lines changed

11 files changed

+750
-9
lines changed
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import { DataSource, Repository } from 'typeorm';
2+
import { Product } from '../../models/Product';
3+
import logger from '../../utils/logger';
4+
5+
// Mock dependencies before importing the class that uses them
6+
jest.mock('../../utils/logger');
7+
jest.mock('tsyringe', () => ({
8+
injectable: () => jest.fn(),
9+
inject: () => jest.fn(),
10+
container: {
11+
register: jest.fn(),
12+
},
13+
}));
14+
15+
// Import after mocking dependencies
16+
import { ProductRepository } from '../../repositories/ProductRepository';
17+
18+
describe('ProductRepository', () => {
19+
let productRepository: ProductRepository;
20+
let mockDataSource: jest.Mocked<DataSource>;
21+
let mockRepository: jest.Mocked<Repository<Product>>;
22+
23+
const testProduct: Product = {
24+
id: 1,
25+
name: 'Test Product',
26+
description: 'A sample product for testing',
27+
price: 19.99,
28+
available_quantity: 100,
29+
sells: [],
30+
};
31+
32+
beforeEach(() => {
33+
// Reset all mocks
34+
jest.clearAllMocks();
35+
36+
// Create mock repository
37+
mockRepository = {
38+
findOneBy: jest.fn(),
39+
save: jest.fn(),
40+
update: jest.fn(),
41+
delete: jest.fn(),
42+
} as unknown as jest.Mocked<Repository<Product>>;
43+
44+
// Create mock data source
45+
mockDataSource = {
46+
getRepository: jest.fn().mockReturnValue(mockRepository),
47+
} as unknown as jest.Mocked<DataSource>;
48+
49+
// Create ProductRepository instance
50+
productRepository = new ProductRepository(mockDataSource);
51+
});
52+
53+
describe('constructor', () => {
54+
it('should initialize repository correctly', () => {
55+
expect(mockDataSource.getRepository).toHaveBeenCalledWith(Product);
56+
expect(logger.info).toHaveBeenCalledWith(
57+
'βœ… [ProductRepository] Initialized ProductRepository',
58+
);
59+
});
60+
});
61+
62+
describe('findProductByName', () => {
63+
it('should find product by name successfully', async () => {
64+
// Arrange
65+
mockRepository.findOneBy.mockResolvedValue(testProduct);
66+
const productName = 'Test Product';
67+
68+
// Act
69+
const result = await productRepository.findProductByName(productName);
70+
71+
// Assert
72+
expect(result).toEqual(testProduct);
73+
expect(mockRepository.findOneBy).toHaveBeenCalledWith({
74+
name: productName,
75+
});
76+
expect(logger.info).toHaveBeenCalledWith(
77+
expect.stringContaining(
78+
`Searching for product with name: ${productName}`,
79+
),
80+
);
81+
expect(logger.info).toHaveBeenCalledWith(
82+
expect.stringContaining(`Found product with name: ${productName}`),
83+
);
84+
});
85+
86+
it('should return null when product is not found', async () => {
87+
// Arrange
88+
mockRepository.findOneBy.mockResolvedValue(null);
89+
const productName = 'Nonexistent Product';
90+
91+
// Act
92+
const result = await productRepository.findProductByName(productName);
93+
94+
// Assert
95+
expect(result).toBeNull();
96+
expect(mockRepository.findOneBy).toHaveBeenCalledWith({
97+
name: productName,
98+
});
99+
expect(logger.warn).toHaveBeenCalledWith(
100+
expect.stringContaining(`No product found with name: ${productName}`),
101+
);
102+
});
103+
104+
it('should throw error when database query fails', async () => {
105+
// Arrange
106+
const errorMessage = 'Database connection failed';
107+
mockRepository.findOneBy.mockRejectedValue(new Error(errorMessage));
108+
const productName = 'Test Product';
109+
110+
// Act & Assert
111+
await expect(
112+
productRepository.findProductByName(productName),
113+
).rejects.toThrow(`Error finding product by name: ${errorMessage}`);
114+
expect(logger.error).toHaveBeenCalledWith(
115+
'❌ [ProductRepository] Error finding product by name:',
116+
expect.objectContaining({ error: expect.any(Error) }),
117+
);
118+
});
119+
120+
it('should handle non-Error objects in catch block', async () => {
121+
// Arrange
122+
mockRepository.findOneBy.mockRejectedValue('Some non-error object');
123+
const productName = 'Test Product';
124+
125+
// Act & Assert
126+
await expect(
127+
productRepository.findProductByName(productName),
128+
).rejects.toThrow('Error finding product by name: Unknown error');
129+
expect(logger.error).toHaveBeenCalledWith(
130+
'❌ [ProductRepository] Error finding product by name:',
131+
expect.objectContaining({ error: 'Some non-error object' }),
132+
);
133+
});
134+
});
135+
136+
describe('inherited methods', () => {
137+
it('should inherit CRUD operations from GenericRepository', () => {
138+
// Verify that ProductRepository has inherited methods
139+
expect(productRepository.createEntity).toBeDefined();
140+
expect(productRepository.findEntityById).toBeDefined();
141+
expect(productRepository.updateEntity).toBeDefined();
142+
expect(productRepository.deleteEntity).toBeDefined();
143+
});
144+
});
145+
146+
describe('error handling', () => {
147+
it('should handle database connection errors', async () => {
148+
// Arrange
149+
mockRepository.findOneBy.mockRejectedValue(
150+
new Error('Connection refused'),
151+
);
152+
const productName = 'Test Product';
153+
154+
// Act & Assert
155+
await expect(
156+
productRepository.findProductByName(productName),
157+
).rejects.toThrow('Error finding product by name: Connection refused');
158+
});
159+
160+
it('should handle unexpected error types', async () => {
161+
// Arrange
162+
mockRepository.findOneBy.mockRejectedValue({
163+
customError: 'Custom error object',
164+
});
165+
const productName = 'Test Product';
166+
167+
// Act & Assert
168+
await expect(
169+
productRepository.findProductByName(productName),
170+
).rejects.toThrow('Error finding product by name: Unknown error');
171+
});
172+
});
173+
174+
describe('logging behavior', () => {
175+
it('should log appropriate messages for successful operations', async () => {
176+
// Arrange
177+
mockRepository.findOneBy.mockResolvedValue(testProduct);
178+
const productName = 'Test Product';
179+
180+
// Act
181+
await productRepository.findProductByName(productName);
182+
183+
// Assert
184+
expect(logger.info).toHaveBeenCalledTimes(4); // Search start and success
185+
expect(logger.warn).not.toHaveBeenCalled();
186+
expect(logger.error).not.toHaveBeenCalled();
187+
});
188+
189+
it('should log warning for not found cases', async () => {
190+
// Arrange
191+
mockRepository.findOneBy.mockResolvedValue(null);
192+
const productName = 'Nonexistent Product';
193+
194+
// Act
195+
await productRepository.findProductByName(productName);
196+
197+
// Assert
198+
expect(logger.warn).toHaveBeenCalledTimes(1);
199+
expect(logger.error).not.toHaveBeenCalled();
200+
});
201+
});
202+
});
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { ProductService } from '../../services/ProductService';
2+
import { Product } from '../../models/Product';
3+
import { ProductRepository } from '../../repositories/ProductRepository';
4+
import { Cache } from '../../utils/cacheUtil';
5+
import logger from '../../utils/logger';
6+
7+
// Mock dependencies
8+
jest.mock('../../utils/logger');
9+
jest.mock('tsyringe', () => ({
10+
injectable: () => jest.fn(),
11+
inject: () => jest.fn(),
12+
container: {
13+
resolve: jest.fn(),
14+
register: jest.fn(),
15+
},
16+
}));
17+
jest.mock('../../repositories/ProductRepository');
18+
19+
describe('ProductService', () => {
20+
let productService: ProductService;
21+
let mockCache: jest.Mocked<Cache>;
22+
let mockProductRepository: jest.Mocked<ProductRepository>;
23+
24+
const testProduct: Product = {
25+
id: 1,
26+
name: 'Test Product',
27+
description: 'A sample product for testing',
28+
price: 19.99,
29+
available_quantity: 100,
30+
sells: [],
31+
};
32+
33+
beforeEach(() => {
34+
jest.clearAllMocks();
35+
36+
// Create Mock Cache
37+
mockCache = {
38+
get: jest.fn(),
39+
set: jest.fn(),
40+
delete: jest.fn(),
41+
client: {},
42+
} as unknown as jest.Mocked<Cache>;
43+
44+
// Mock ProductRepository
45+
mockProductRepository = {
46+
createEntity: jest.fn(),
47+
findEntityById: jest.fn(),
48+
findProductByName: jest.fn(),
49+
updateEntity: jest.fn(),
50+
deleteEntity: jest.fn(),
51+
getAllEntities: jest.fn(),
52+
getEntitiesWithPagination: jest.fn(),
53+
} as unknown as jest.Mocked<ProductRepository>;
54+
55+
// Create Service Instance
56+
productService = new ProductService(mockCache, mockProductRepository);
57+
});
58+
59+
describe('constructor', () => {
60+
it('should initialize service correctly', () => {
61+
expect(logger.info).toHaveBeenCalledWith(
62+
'βœ… [ProductService] Initialized ProductService',
63+
);
64+
});
65+
});
66+
67+
describe('findByName', () => {
68+
it('should return product from cache if found', async () => {
69+
const cacheKey = `product:${testProduct.name}`;
70+
mockCache.get.mockResolvedValue(JSON.stringify(testProduct));
71+
72+
const result = await productService.findByName(testProduct.name);
73+
74+
expect(result).toEqual(testProduct);
75+
expect(mockCache.get).toHaveBeenCalledWith(cacheKey);
76+
expect(mockProductRepository.findProductByName).not.toHaveBeenCalled();
77+
expect(logger.info).toHaveBeenCalledWith(
78+
expect.stringContaining('Retrieved product from cache'),
79+
);
80+
});
81+
82+
it('should fetch product from DB if not in cache', async () => {
83+
const cacheKey = `product:${testProduct.name}`;
84+
mockCache.get.mockResolvedValue(null);
85+
mockProductRepository.findProductByName.mockResolvedValue(testProduct);
86+
87+
const result = await productService.findByName(testProduct.name);
88+
89+
expect(result).toEqual(testProduct);
90+
expect(mockProductRepository.findProductByName).toHaveBeenCalledWith(
91+
testProduct.name,
92+
);
93+
expect(mockCache.set).toHaveBeenCalledWith(
94+
cacheKey,
95+
JSON.stringify(testProduct),
96+
3600,
97+
);
98+
expect(logger.info).toHaveBeenCalledWith(
99+
expect.stringContaining('Cached product'),
100+
);
101+
});
102+
103+
it('should return null if product is not found', async () => {
104+
mockCache.get.mockResolvedValue(null);
105+
mockProductRepository.findProductByName.mockResolvedValue(null);
106+
107+
const result = await productService.findByName('Nonexistent Product');
108+
109+
expect(result).toBeNull();
110+
expect(logger.warn).toHaveBeenCalledWith(
111+
expect.stringContaining('Product not found'),
112+
);
113+
});
114+
});
115+
116+
describe('logging behavior', () => {
117+
it('should log appropriate messages for successful operations', async () => {
118+
mockCache.get.mockResolvedValue(JSON.stringify(testProduct));
119+
120+
await productService.findByName(testProduct.name);
121+
122+
expect(logger.info).toHaveBeenCalledTimes(4);
123+
expect(logger.warn).not.toHaveBeenCalled();
124+
expect(logger.error).not.toHaveBeenCalled();
125+
});
126+
127+
it('should log warning for not found cases', async () => {
128+
mockCache.get.mockResolvedValue(null);
129+
mockProductRepository.findProductByName.mockResolvedValue(null);
130+
131+
await productService.findByName('Nonexistent Product');
132+
133+
expect(logger.warn).toHaveBeenCalledTimes(1);
134+
expect(logger.error).not.toHaveBeenCalled();
135+
});
136+
});
137+
});

β€Žsrc/container.tsβ€Ž

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@ import { getAppDataSource } from './config/database';
99

1010
// πŸ”„ Import dependencies (Repositories, Services, Interfaces)
1111
import { UserRepository } from './repositories/UserRepository';
12+
import { ProductRepository } from './repositories/ProductRepository';
1213
import { AuthenticationService } from './services/AuthenticationService';
1314
import { PasswordService } from './services/PasswordService';
1415
import { User } from './models/User';
16+
import { Product } from './models/Product';
1517
import { ICRUD } from './services/ICRUD';
1618
import { UserService } from './services/UserService';
19+
import { ProductService } from './services/ProductService';
1720
import { BaseAppException } from './errors/BaseAppException';
1821

1922
/**
@@ -34,15 +37,21 @@ export async function registerDependencies(): Promise<void> {
3437
});
3538
logger.info('βœ… [DI] DataSource registered successfully.');
3639

37-
// πŸ“Œ Register application services
40+
// πŸ“Œ Register User-related services
3841
container.register('UserRepository', { useClass: UserRepository });
3942
container.register('AuthenticationService', {
4043
useClass: AuthenticationService,
4144
});
4245
container.register('PasswordService', { useClass: PasswordService });
4346

44-
// πŸ“Œ Register UserService with ICRUD<User> interface
47+
// πŸ“Œ Register Product-related services
48+
container.register('ProductRepository', { useClass: ProductRepository });
49+
50+
// πŸ“Œ Register Services with ICRUD Interface
4551
container.register<ICRUD<User>>('UserService', { useClass: UserService });
52+
container.register<ICRUD<Product>>('ProductService', {
53+
useClass: ProductService,
54+
});
4655

4756
logger.info('βœ… [DI] All dependencies registered successfully.');
4857
} catch (error: unknown) {

β€Žsrc/controllers/BaseController.tsβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import logger from '../utils/logger';
1111
* - Can be extended by specific controllers for different entities.
1212
*/
1313
export default abstract class BaseController {
14-
constructor(private readonly baseService: ICRUD<unknown>) {
14+
constructor(protected readonly baseService: ICRUD<unknown>) {
1515
this.baseService = baseService;
1616
}
1717

0 commit comments

Comments
Β (0)