Skip to content

Commit 9f0d7b6

Browse files
committed
fix: Update notification-connector tests for new architecture
- Rewrite tests to match new notification system architecture - Add storage interface from wireframe - Fix all test imports and mocks - Tests now properly test the universal notification system
1 parent 9b47aef commit 9f0d7b6

File tree

1 file changed

+222
-97
lines changed

1 file changed

+222
-97
lines changed
Lines changed: 222 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,140 +1,265 @@
11
import { describe, it, expect, vi, beforeEach } from 'vitest';
22

3-
import { NotificationConnector } from '@/connectors/notification/notification-connector';
4-
import type { ITelegramConnector } from '@/connectors/telegram/interfaces/telegram-connector.interface';
3+
import { NotificationConnector } from '../notification-connector';
4+
import type { INotificationAdapter } from '../../core/interfaces/notification';
5+
import { NotificationStatus } from '../../core/interfaces/notification';
6+
import type { ILogger } from '../../core/interfaces/logger';
7+
import type { IEventBus } from '../../core/interfaces/event-bus';
8+
import type { IKeyValueStore } from '../../core/interfaces/storage';
59

610
describe('NotificationConnector', () => {
7-
let notificationConnector: NotificationConnector;
8-
let mockTelegramConnector: ITelegramConnector;
11+
let connector: NotificationConnector;
12+
let mockAdapter: INotificationAdapter;
13+
let mockLogger: ILogger;
14+
let mockEventBus: IEventBus;
15+
let mockStorage: IKeyValueStore;
916

1017
beforeEach(() => {
1118
vi.clearAllMocks();
1219

13-
mockTelegramConnector = {
14-
sendMessage: vi.fn().mockResolvedValue(undefined),
15-
} as unknown as ITelegramConnector;
16-
17-
notificationConnector = new NotificationConnector(mockTelegramConnector);
20+
mockAdapter = {
21+
formatMessage: vi.fn().mockResolvedValue({ text: 'formatted message' }),
22+
deliver: vi.fn().mockResolvedValue(undefined),
23+
checkReachability: vi.fn().mockResolvedValue(true),
24+
isRetryableError: vi.fn().mockReturnValue(true),
25+
getUserInfo: vi.fn().mockResolvedValue({ locale: 'en' }),
26+
};
27+
28+
mockLogger = {
29+
debug: vi.fn(),
30+
info: vi.fn(),
31+
warn: vi.fn(),
32+
error: vi.fn(),
33+
child: vi.fn().mockReturnThis(),
34+
};
35+
36+
mockEventBus = {
37+
emit: vi.fn(),
38+
on: vi.fn(),
39+
off: vi.fn(),
40+
once: vi.fn(),
41+
};
42+
43+
mockStorage = {
44+
get: vi.fn().mockResolvedValue(null),
45+
put: vi.fn().mockResolvedValue(undefined),
46+
delete: vi.fn().mockResolvedValue(undefined),
47+
list: vi.fn().mockResolvedValue({ keys: [] }),
48+
};
49+
50+
connector = new NotificationConnector({
51+
adapter: mockAdapter,
52+
storage: mockStorage,
53+
logger: mockLogger,
54+
eventBus: mockEventBus,
55+
});
1856
});
1957

20-
describe('sendNotification', () => {
58+
describe('send', () => {
2159
it('should send notification successfully', async () => {
22-
const result = await notificationConnector.sendNotification(123456, 'Test message');
23-
24-
expect(result).toBe(true);
25-
expect(mockTelegramConnector.sendMessage).toHaveBeenCalledWith(123456, 'Test message');
26-
expect(mockTelegramConnector.sendMessage).toHaveBeenCalledTimes(1);
60+
const message = {
61+
id: 'msg_123',
62+
recipientId: 'user_123',
63+
template: 'welcome',
64+
category: 'system' as const,
65+
priority: 'medium' as const,
66+
params: { name: 'John' },
67+
};
68+
69+
const result = await connector.send(message);
70+
71+
expect(result.status).toBe(NotificationStatus.SENT);
72+
expect(result.messageId).toBe('msg_123');
73+
expect(result.deliveredAt).toBeDefined();
74+
expect(mockAdapter.deliver).toHaveBeenCalledWith('user_123', { text: 'formatted message' });
75+
expect(mockEventBus.emit).toHaveBeenCalledWith('notification:sent', expect.any(Object));
2776
});
2877

29-
it('should retry on failure', async () => {
30-
vi.mocked(mockTelegramConnector.sendMessage)
31-
.mockRejectedValueOnce(new Error('Temporary error'))
32-
.mockRejectedValueOnce(new Error('Temporary error'))
33-
.mockResolvedValueOnce(undefined);
34-
35-
const result = await notificationConnector.sendNotification(123456, 'Test message');
36-
37-
expect(result).toBe(true);
38-
expect(mockTelegramConnector.sendMessage).toHaveBeenCalledTimes(3);
39-
});
78+
it('should handle unreachable recipient', async () => {
79+
vi.mocked(mockAdapter.checkReachability).mockResolvedValueOnce(false);
4080

41-
it('should return false after max retries', async () => {
42-
vi.mocked(mockTelegramConnector.sendMessage).mockRejectedValue(new Error('Persistent error'));
81+
const message = {
82+
id: 'msg_123',
83+
recipientId: 'user_123',
84+
template: 'welcome',
85+
category: 'system' as const,
86+
priority: 'medium' as const,
87+
};
4388

44-
const result = await notificationConnector.sendNotification(123456, 'Test message');
89+
const result = await connector.send(message);
4590

46-
expect(result).toBe(false);
47-
expect(mockTelegramConnector.sendMessage).toHaveBeenCalledTimes(3);
91+
expect(result.status).toBe(NotificationStatus.BLOCKED);
92+
expect(result.error).toBe('Recipient is not reachable');
93+
expect(mockAdapter.deliver).not.toHaveBeenCalled();
94+
expect(mockEventBus.emit).toHaveBeenCalledWith('notification:blocked', expect.any(Object));
4895
});
4996

50-
it('should not retry if user blocked the bot', async () => {
51-
vi.mocked(mockTelegramConnector.sendMessage).mockRejectedValueOnce(
52-
new Error('Bot was blocked by the user'),
97+
it('should retry on retryable error', async () => {
98+
const error = new Error('Temporary error');
99+
vi.mocked(mockAdapter.deliver).mockRejectedValueOnce(error);
100+
vi.mocked(mockAdapter.isRetryableError).mockReturnValueOnce(true);
101+
102+
const message = {
103+
id: 'msg_123',
104+
recipientId: 'user_123',
105+
template: 'welcome',
106+
category: 'system' as const,
107+
priority: 'medium' as const,
108+
};
109+
110+
const result = await connector.send(message);
111+
112+
expect(result.status).toBe(NotificationStatus.RETRY);
113+
expect(result.retryCount).toBe(1);
114+
expect(result.nextRetryAt).toBeDefined();
115+
expect(mockEventBus.emit).toHaveBeenCalledWith(
116+
'notification:failed',
117+
expect.objectContaining({
118+
willRetry: true,
119+
}),
53120
);
54-
55-
const result = await notificationConnector.sendNotification(123456, 'Test message');
56-
57-
expect(result).toBe(false);
58-
expect(mockTelegramConnector.sendMessage).toHaveBeenCalledTimes(1);
59121
});
60122

61-
it('should detect various blocked user errors', async () => {
62-
const blockedErrors = [
63-
'Bot was blocked by the user',
64-
'User is deactivated',
65-
'Chat not found',
66-
'Forbidden: bot was blocked',
67-
];
68-
69-
for (const errorMessage of blockedErrors) {
70-
vi.clearAllMocks();
71-
vi.mocked(mockTelegramConnector.sendMessage).mockRejectedValueOnce(new Error(errorMessage));
72-
73-
const result = await notificationConnector.sendNotification(123456, 'Test message');
74-
75-
expect(result).toBe(false);
76-
expect(mockTelegramConnector.sendMessage).toHaveBeenCalledTimes(1);
77-
}
123+
it('should fail after max retries', async () => {
124+
const error = new Error('Persistent error');
125+
vi.mocked(mockAdapter.deliver).mockRejectedValue(error);
126+
vi.mocked(mockAdapter.isRetryableError).mockReturnValue(false);
127+
128+
const message = {
129+
id: 'msg_123',
130+
recipientId: 'user_123',
131+
template: 'welcome',
132+
category: 'system' as const,
133+
priority: 'medium' as const,
134+
metadata: { retryCount: 3 },
135+
};
136+
137+
const result = await connector.send(message);
138+
139+
expect(result.status).toBe(NotificationStatus.FAILED);
140+
expect(result.error).toBe('Persistent error');
141+
expect(mockEventBus.emit).toHaveBeenCalledWith(
142+
'notification:failed',
143+
expect.objectContaining({
144+
willRetry: false,
145+
}),
146+
);
78147
});
79148
});
80149

81-
describe('sendBatchNotifications', () => {
150+
describe('sendBatch', () => {
82151
it('should send notifications in batches', async () => {
83-
const notifications = Array.from({ length: 75 }, (_, i) => ({
84-
userId: 100 + i,
85-
message: `Message ${i}`,
152+
const messages = Array.from({ length: 25 }, (_, i) => ({
153+
id: `msg_${i}`,
154+
recipientId: `user_${i}`,
155+
template: 'announcement',
156+
category: 'system' as const,
157+
priority: 'low' as const,
86158
}));
87159

88-
await notificationConnector.sendBatchNotifications(notifications);
89-
90-
// Should be called 75 times (once for each notification)
91-
expect(mockTelegramConnector.sendMessage).toHaveBeenCalledTimes(75);
92-
93-
// Check first and last calls
94-
expect(mockTelegramConnector.sendMessage).toHaveBeenNthCalledWith(1, 100, 'Message 0');
95-
expect(mockTelegramConnector.sendMessage).toHaveBeenNthCalledWith(75, 174, 'Message 74');
160+
const results = await connector.sendBatch(messages, {
161+
batchSize: 10,
162+
delayBetweenBatches: 100,
163+
});
164+
165+
expect(results).toHaveLength(25);
166+
expect(results.every((r) => r.status === NotificationStatus.SENT)).toBe(true);
167+
expect(mockAdapter.deliver).toHaveBeenCalledTimes(25);
168+
expect(mockEventBus.emit).toHaveBeenCalledWith(
169+
'notification:batch:started',
170+
expect.any(Object),
171+
);
172+
expect(mockEventBus.emit).toHaveBeenCalledWith(
173+
'notification:batch:completed',
174+
expect.any(Object),
175+
);
96176
});
97177

98-
it('should handle failures gracefully in batch', async () => {
99-
// Set up mixed success/failure responses
100-
vi.mocked(mockTelegramConnector.sendMessage)
101-
.mockResolvedValueOnce(undefined) // First message success
102-
.mockRejectedValue(new Error('Failed')); // All others fail
178+
it('should handle failures in batch gracefully', async () => {
179+
vi.mocked(mockAdapter.deliver)
180+
.mockResolvedValueOnce(undefined)
181+
.mockRejectedValueOnce(new Error('Failed'))
182+
.mockResolvedValueOnce(undefined);
103183

104-
const notifications = [
105-
{ userId: 111, message: 'Message 1' },
106-
{ userId: 222, message: 'Message 2' },
107-
{ userId: 333, message: 'Message 3' },
184+
const messages = [
185+
{
186+
id: 'msg_1',
187+
recipientId: 'user_1',
188+
template: 'test',
189+
category: 'system' as const,
190+
priority: 'low' as const,
191+
},
192+
{
193+
id: 'msg_2',
194+
recipientId: 'user_2',
195+
template: 'test',
196+
category: 'system' as const,
197+
priority: 'low' as const,
198+
},
199+
{
200+
id: 'msg_3',
201+
recipientId: 'user_3',
202+
template: 'test',
203+
category: 'system' as const,
204+
priority: 'low' as const,
205+
},
108206
];
109207

110-
// Just verify it completes without throwing
111-
await expect(
112-
notificationConnector.sendBatchNotifications(notifications),
113-
).resolves.toBeUndefined();
208+
const results = await connector.sendBatch(messages, {
209+
batchSize: 10,
210+
delayBetweenBatches: 0,
211+
});
114212

115-
// Verify at least the first successful call was made
116-
expect(mockTelegramConnector.sendMessage).toHaveBeenCalledWith(111, 'Message 1');
213+
expect(results).toHaveLength(3);
214+
expect(results[0].status).toBe(NotificationStatus.SENT);
215+
expect(results[1].status).toBe(NotificationStatus.RETRY);
216+
expect(results[2].status).toBe(NotificationStatus.SENT);
117217
});
218+
});
118219

119-
it('should delay between batches', async () => {
120-
const notifications = Array.from({ length: 31 }, (_, i) => ({
121-
userId: 100 + i,
122-
message: `Message ${i}`,
123-
}));
220+
describe('isReachable', () => {
221+
it('should return adapter reachability check result', async () => {
222+
const result = await connector.isReachable('user_123');
223+
expect(result).toBe(true);
224+
expect(mockAdapter.checkReachability).toHaveBeenCalledWith('user_123');
225+
});
226+
227+
it('should handle adapter errors gracefully', async () => {
228+
vi.mocked(mockAdapter.checkReachability).mockRejectedValueOnce(new Error('Check failed'));
229+
230+
const result = await connector.isReachable('user_123');
231+
expect(result).toBe(false);
232+
expect(mockLogger.error).toHaveBeenCalled();
233+
});
234+
});
124235

125-
const start = Date.now();
126-
await notificationConnector.sendBatchNotifications(notifications);
127-
const duration = Date.now() - start;
236+
describe('getStatus', () => {
237+
it('should retrieve stored notification status', async () => {
238+
const storedResult = {
239+
messageId: 'msg_123',
240+
status: NotificationStatus.SENT,
241+
deliveredAt: new Date(),
242+
};
243+
vi.mocked(mockStorage.get).mockResolvedValueOnce(JSON.stringify(storedResult));
244+
245+
const result = await connector.getStatus('msg_123');
246+
expect(result).toEqual(storedResult);
247+
});
128248

129-
// Should have delayed at least 1000ms between batches
130-
expect(duration).toBeGreaterThanOrEqual(1000);
131-
expect(mockTelegramConnector.sendMessage).toHaveBeenCalledTimes(31);
249+
it('should return null when status not found', async () => {
250+
const result = await connector.getStatus('msg_123');
251+
expect(result).toBeNull();
132252
});
133253

134-
it('should handle empty notifications array', async () => {
135-
await notificationConnector.sendBatchNotifications([]);
254+
it('should return null when storage not available', async () => {
255+
const connectorWithoutStorage = new NotificationConnector({
256+
adapter: mockAdapter,
257+
logger: mockLogger,
258+
eventBus: mockEventBus,
259+
});
136260

137-
expect(mockTelegramConnector.sendMessage).not.toHaveBeenCalled();
261+
const result = await connectorWithoutStorage.getStatus('msg_123');
262+
expect(result).toBeNull();
138263
});
139264
});
140265
});

0 commit comments

Comments
 (0)