@auxiora/channels 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +191 -0
- package/dist/adapters/bluebubbles.d.ts +63 -0
- package/dist/adapters/bluebubbles.d.ts.map +1 -0
- package/dist/adapters/bluebubbles.js +197 -0
- package/dist/adapters/bluebubbles.js.map +1 -0
- package/dist/adapters/discord.d.ts +27 -0
- package/dist/adapters/discord.d.ts.map +1 -0
- package/dist/adapters/discord.js +202 -0
- package/dist/adapters/discord.js.map +1 -0
- package/dist/adapters/email.d.ts +39 -0
- package/dist/adapters/email.d.ts.map +1 -0
- package/dist/adapters/email.js +359 -0
- package/dist/adapters/email.js.map +1 -0
- package/dist/adapters/googlechat.d.ts +77 -0
- package/dist/adapters/googlechat.d.ts.map +1 -0
- package/dist/adapters/googlechat.js +232 -0
- package/dist/adapters/googlechat.js.map +1 -0
- package/dist/adapters/matrix.d.ts +37 -0
- package/dist/adapters/matrix.d.ts.map +1 -0
- package/dist/adapters/matrix.js +262 -0
- package/dist/adapters/matrix.js.map +1 -0
- package/dist/adapters/signal.d.ts +32 -0
- package/dist/adapters/signal.d.ts.map +1 -0
- package/dist/adapters/signal.js +216 -0
- package/dist/adapters/signal.js.map +1 -0
- package/dist/adapters/slack.d.ts +29 -0
- package/dist/adapters/slack.d.ts.map +1 -0
- package/dist/adapters/slack.js +202 -0
- package/dist/adapters/slack.js.map +1 -0
- package/dist/adapters/teams.d.ts +66 -0
- package/dist/adapters/teams.d.ts.map +1 -0
- package/dist/adapters/teams.js +227 -0
- package/dist/adapters/teams.js.map +1 -0
- package/dist/adapters/telegram.d.ts +28 -0
- package/dist/adapters/telegram.d.ts.map +1 -0
- package/dist/adapters/telegram.js +170 -0
- package/dist/adapters/telegram.js.map +1 -0
- package/dist/adapters/twilio.d.ts +63 -0
- package/dist/adapters/twilio.d.ts.map +1 -0
- package/dist/adapters/twilio.js +193 -0
- package/dist/adapters/twilio.js.map +1 -0
- package/dist/adapters/whatsapp.d.ts +99 -0
- package/dist/adapters/whatsapp.d.ts.map +1 -0
- package/dist/adapters/whatsapp.js +218 -0
- package/dist/adapters/whatsapp.js.map +1 -0
- package/dist/adapters/zalo.d.ts +64 -0
- package/dist/adapters/zalo.d.ts.map +1 -0
- package/dist/adapters/zalo.js +216 -0
- package/dist/adapters/zalo.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/manager.d.ts +35 -0
- package/dist/manager.d.ts.map +1 -0
- package/dist/manager.js +127 -0
- package/dist/manager.js.map +1 -0
- package/dist/types.d.ts +71 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +32 -0
- package/src/adapters/bluebubbles.ts +294 -0
- package/src/adapters/discord.ts +253 -0
- package/src/adapters/email.ts +457 -0
- package/src/adapters/googlechat.ts +364 -0
- package/src/adapters/matrix.ts +376 -0
- package/src/adapters/signal.ts +313 -0
- package/src/adapters/slack.ts +252 -0
- package/src/adapters/teams.ts +320 -0
- package/src/adapters/telegram.ts +208 -0
- package/src/adapters/twilio.ts +256 -0
- package/src/adapters/whatsapp.ts +342 -0
- package/src/adapters/zalo.ts +319 -0
- package/src/index.ts +78 -0
- package/src/manager.ts +180 -0
- package/src/types.ts +84 -0
- package/tests/bluebubbles.test.ts +438 -0
- package/tests/email.test.ts +136 -0
- package/tests/googlechat.test.ts +439 -0
- package/tests/matrix.test.ts +564 -0
- package/tests/signal.test.ts +404 -0
- package/tests/slack.test.ts +343 -0
- package/tests/teams.test.ts +429 -0
- package/tests/twilio.test.ts +269 -0
- package/tests/whatsapp.test.ts +530 -0
- package/tests/zalo.test.ts +499 -0
- package/tsconfig.json +8 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { TeamsAdapter } from '../src/adapters/teams.js';
|
|
3
|
+
|
|
4
|
+
// Mock audit
|
|
5
|
+
vi.mock('@auxiora/audit', () => ({
|
|
6
|
+
audit: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
describe('TeamsAdapter', () => {
|
|
10
|
+
let adapter: TeamsAdapter;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
adapter = new TeamsAdapter({
|
|
14
|
+
microsoftAppId: 'test-app-id',
|
|
15
|
+
microsoftAppPassword: 'test-app-password',
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
vi.restoreAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should have correct metadata', () => {
|
|
24
|
+
expect(adapter.type).toBe('teams');
|
|
25
|
+
expect(adapter.name).toBe('Microsoft Teams');
|
|
26
|
+
expect(adapter.isConnected()).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should connect successfully', async () => {
|
|
30
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
31
|
+
ok: true,
|
|
32
|
+
json: async () => ({
|
|
33
|
+
access_token: 'test-token',
|
|
34
|
+
expires_in: 3600,
|
|
35
|
+
token_type: 'Bearer',
|
|
36
|
+
}),
|
|
37
|
+
} as Response);
|
|
38
|
+
|
|
39
|
+
await adapter.connect();
|
|
40
|
+
expect(adapter.isConnected()).toBe(true);
|
|
41
|
+
|
|
42
|
+
await adapter.disconnect();
|
|
43
|
+
expect(adapter.isConnected()).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should fail to connect with invalid credentials', async () => {
|
|
47
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
48
|
+
ok: false,
|
|
49
|
+
status: 401,
|
|
50
|
+
statusText: 'Unauthorized',
|
|
51
|
+
} as Response);
|
|
52
|
+
|
|
53
|
+
await expect(adapter.connect()).rejects.toThrow('Failed to obtain Teams token');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should handle incoming webhook activity', async () => {
|
|
57
|
+
// Connect first
|
|
58
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
59
|
+
ok: true,
|
|
60
|
+
json: async () => ({
|
|
61
|
+
access_token: 'test-token',
|
|
62
|
+
expires_in: 3600,
|
|
63
|
+
token_type: 'Bearer',
|
|
64
|
+
}),
|
|
65
|
+
} as Response);
|
|
66
|
+
await adapter.connect();
|
|
67
|
+
|
|
68
|
+
const receivedMessages: unknown[] = [];
|
|
69
|
+
adapter.onMessage(async (msg) => {
|
|
70
|
+
receivedMessages.push(msg);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const activity = {
|
|
74
|
+
type: 'message',
|
|
75
|
+
id: 'activity-123',
|
|
76
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
77
|
+
channelId: 'msteams',
|
|
78
|
+
from: {
|
|
79
|
+
id: 'user-1',
|
|
80
|
+
name: 'Alice',
|
|
81
|
+
},
|
|
82
|
+
conversation: {
|
|
83
|
+
id: 'conv-1',
|
|
84
|
+
isGroup: false,
|
|
85
|
+
},
|
|
86
|
+
text: 'Hello Teams!',
|
|
87
|
+
serviceUrl: 'https://smba.trafficmanager.net/teams/',
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
await adapter.handleWebhook(activity);
|
|
91
|
+
|
|
92
|
+
expect(receivedMessages).toHaveLength(1);
|
|
93
|
+
const msg = receivedMessages[0] as { content: string; senderId: string; senderName: string };
|
|
94
|
+
expect(msg.content).toBe('Hello Teams!');
|
|
95
|
+
expect(msg.senderId).toBe('user-1');
|
|
96
|
+
expect(msg.senderName).toBe('Alice');
|
|
97
|
+
|
|
98
|
+
await adapter.disconnect();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should ignore non-message activities', async () => {
|
|
102
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
103
|
+
ok: true,
|
|
104
|
+
json: async () => ({
|
|
105
|
+
access_token: 'test-token',
|
|
106
|
+
expires_in: 3600,
|
|
107
|
+
token_type: 'Bearer',
|
|
108
|
+
}),
|
|
109
|
+
} as Response);
|
|
110
|
+
await adapter.connect();
|
|
111
|
+
|
|
112
|
+
const receivedMessages: unknown[] = [];
|
|
113
|
+
adapter.onMessage(async (msg) => {
|
|
114
|
+
receivedMessages.push(msg);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await adapter.handleWebhook({
|
|
118
|
+
type: 'conversationUpdate',
|
|
119
|
+
id: 'activity-456',
|
|
120
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
121
|
+
channelId: 'msteams',
|
|
122
|
+
from: { id: 'user-1' },
|
|
123
|
+
conversation: { id: 'conv-1' },
|
|
124
|
+
serviceUrl: 'https://smba.trafficmanager.net/teams/',
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(receivedMessages).toHaveLength(0);
|
|
128
|
+
|
|
129
|
+
await adapter.disconnect();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should send a message successfully', async () => {
|
|
133
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
134
|
+
ok: true,
|
|
135
|
+
json: async () => ({
|
|
136
|
+
access_token: 'test-token',
|
|
137
|
+
expires_in: 3600,
|
|
138
|
+
token_type: 'Bearer',
|
|
139
|
+
}),
|
|
140
|
+
} as Response);
|
|
141
|
+
await adapter.connect();
|
|
142
|
+
|
|
143
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
144
|
+
ok: true,
|
|
145
|
+
json: async () => ({ id: 'msg-1' }),
|
|
146
|
+
} as Response);
|
|
147
|
+
|
|
148
|
+
const result = await adapter.send('conv-1', {
|
|
149
|
+
content: 'Hello from the bot!',
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(result.success).toBe(true);
|
|
153
|
+
expect(result.messageId).toBe('msg-1');
|
|
154
|
+
|
|
155
|
+
await adapter.disconnect();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should handle send errors', async () => {
|
|
159
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
160
|
+
ok: true,
|
|
161
|
+
json: async () => ({
|
|
162
|
+
access_token: 'test-token',
|
|
163
|
+
expires_in: 3600,
|
|
164
|
+
token_type: 'Bearer',
|
|
165
|
+
}),
|
|
166
|
+
} as Response);
|
|
167
|
+
await adapter.connect();
|
|
168
|
+
|
|
169
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
170
|
+
ok: false,
|
|
171
|
+
status: 403,
|
|
172
|
+
statusText: 'Forbidden',
|
|
173
|
+
text: async () => 'Bot not authorized',
|
|
174
|
+
} as unknown as Response);
|
|
175
|
+
|
|
176
|
+
const result = await adapter.send('conv-1', {
|
|
177
|
+
content: 'Should fail',
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(result.success).toBe(false);
|
|
181
|
+
expect(result.error).toContain('403');
|
|
182
|
+
|
|
183
|
+
await adapter.disconnect();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should strip bot mention from message text', async () => {
|
|
187
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
188
|
+
ok: true,
|
|
189
|
+
json: async () => ({
|
|
190
|
+
access_token: 'test-token',
|
|
191
|
+
expires_in: 3600,
|
|
192
|
+
token_type: 'Bearer',
|
|
193
|
+
}),
|
|
194
|
+
} as Response);
|
|
195
|
+
await adapter.connect();
|
|
196
|
+
|
|
197
|
+
const receivedMessages: unknown[] = [];
|
|
198
|
+
adapter.onMessage(async (msg) => {
|
|
199
|
+
receivedMessages.push(msg);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await adapter.handleWebhook({
|
|
203
|
+
type: 'message',
|
|
204
|
+
id: 'activity-789',
|
|
205
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
206
|
+
channelId: 'msteams',
|
|
207
|
+
from: { id: 'user-1', name: 'Alice' },
|
|
208
|
+
conversation: { id: 'conv-1' },
|
|
209
|
+
recipient: { id: 'bot-1', name: 'Auxiora' },
|
|
210
|
+
text: '<at>Auxiora</at> what is the weather?',
|
|
211
|
+
serviceUrl: 'https://smba.trafficmanager.net/teams/',
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
expect(receivedMessages).toHaveLength(1);
|
|
215
|
+
const msg = receivedMessages[0] as { content: string };
|
|
216
|
+
expect(msg.content).toBe('what is the weather?');
|
|
217
|
+
|
|
218
|
+
await adapter.disconnect();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should cache access token', async () => {
|
|
222
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
223
|
+
ok: true,
|
|
224
|
+
json: async () => ({
|
|
225
|
+
access_token: 'test-token',
|
|
226
|
+
expires_in: 3600,
|
|
227
|
+
token_type: 'Bearer',
|
|
228
|
+
}),
|
|
229
|
+
} as Response);
|
|
230
|
+
await adapter.connect();
|
|
231
|
+
|
|
232
|
+
// Send two messages - should only call token endpoint once
|
|
233
|
+
fetchSpy.mockResolvedValueOnce({
|
|
234
|
+
ok: true,
|
|
235
|
+
json: async () => ({ id: 'msg-1' }),
|
|
236
|
+
} as Response);
|
|
237
|
+
await adapter.send('conv-1', { content: 'Message 1' });
|
|
238
|
+
|
|
239
|
+
fetchSpy.mockResolvedValueOnce({
|
|
240
|
+
ok: true,
|
|
241
|
+
json: async () => ({ id: 'msg-2' }),
|
|
242
|
+
} as Response);
|
|
243
|
+
await adapter.send('conv-1', { content: 'Message 2' });
|
|
244
|
+
|
|
245
|
+
// Only 1 token call (during connect) + 2 send calls = 3 total
|
|
246
|
+
expect(fetchSpy).toHaveBeenCalledTimes(3);
|
|
247
|
+
|
|
248
|
+
await adapter.disconnect();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should register error handler', () => {
|
|
252
|
+
const handler = vi.fn();
|
|
253
|
+
adapter.onError(handler);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('sender filtering', () => {
|
|
257
|
+
it('should allow all messages when allowlists are not set', async () => {
|
|
258
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
259
|
+
ok: true,
|
|
260
|
+
json: async () => ({ access_token: 'test-token', expires_in: 3600, token_type: 'Bearer' }),
|
|
261
|
+
} as Response);
|
|
262
|
+
await adapter.connect();
|
|
263
|
+
|
|
264
|
+
const receivedMessages: unknown[] = [];
|
|
265
|
+
adapter.onMessage(async (msg) => {
|
|
266
|
+
receivedMessages.push(msg);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
await adapter.handleWebhook({
|
|
270
|
+
type: 'message',
|
|
271
|
+
id: 'activity-filter-1',
|
|
272
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
273
|
+
channelId: 'msteams',
|
|
274
|
+
from: { id: 'any-user', name: 'Anyone' },
|
|
275
|
+
conversation: { id: 'conv-1', tenantId: 'any-tenant' },
|
|
276
|
+
text: 'Hello!',
|
|
277
|
+
serviceUrl: 'https://smba.trafficmanager.net/teams/',
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
expect(receivedMessages).toHaveLength(1);
|
|
281
|
+
await adapter.disconnect();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should allow messages from allowed tenants and users', async () => {
|
|
285
|
+
const filteredAdapter = new TeamsAdapter({
|
|
286
|
+
microsoftAppId: 'test-app-id',
|
|
287
|
+
microsoftAppPassword: 'test-app-password',
|
|
288
|
+
allowedTenants: ['tenant-1'],
|
|
289
|
+
allowedUsers: ['user-1'],
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
293
|
+
ok: true,
|
|
294
|
+
json: async () => ({ access_token: 'test-token', expires_in: 3600, token_type: 'Bearer' }),
|
|
295
|
+
} as Response);
|
|
296
|
+
await filteredAdapter.connect();
|
|
297
|
+
|
|
298
|
+
const receivedMessages: unknown[] = [];
|
|
299
|
+
filteredAdapter.onMessage(async (msg) => {
|
|
300
|
+
receivedMessages.push(msg);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
await filteredAdapter.handleWebhook({
|
|
304
|
+
type: 'message',
|
|
305
|
+
id: 'activity-filter-2',
|
|
306
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
307
|
+
channelId: 'msteams',
|
|
308
|
+
from: { id: 'user-1', name: 'Alice' },
|
|
309
|
+
conversation: { id: 'conv-1', tenantId: 'tenant-1' },
|
|
310
|
+
text: 'Allowed!',
|
|
311
|
+
serviceUrl: 'https://smba.trafficmanager.net/teams/',
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
expect(receivedMessages).toHaveLength(1);
|
|
315
|
+
await filteredAdapter.disconnect();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should block messages from non-allowed tenants', async () => {
|
|
319
|
+
const { audit } = await import('@auxiora/audit');
|
|
320
|
+
const filteredAdapter = new TeamsAdapter({
|
|
321
|
+
microsoftAppId: 'test-app-id',
|
|
322
|
+
microsoftAppPassword: 'test-app-password',
|
|
323
|
+
allowedTenants: ['tenant-1'],
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
327
|
+
ok: true,
|
|
328
|
+
json: async () => ({ access_token: 'test-token', expires_in: 3600, token_type: 'Bearer' }),
|
|
329
|
+
} as Response);
|
|
330
|
+
await filteredAdapter.connect();
|
|
331
|
+
|
|
332
|
+
const receivedMessages: unknown[] = [];
|
|
333
|
+
filteredAdapter.onMessage(async (msg) => {
|
|
334
|
+
receivedMessages.push(msg);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
await filteredAdapter.handleWebhook({
|
|
338
|
+
type: 'message',
|
|
339
|
+
id: 'activity-filter-3',
|
|
340
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
341
|
+
channelId: 'msteams',
|
|
342
|
+
from: { id: 'user-1', name: 'Alice' },
|
|
343
|
+
conversation: { id: 'conv-1', tenantId: 'wrong-tenant' },
|
|
344
|
+
text: 'Blocked!',
|
|
345
|
+
serviceUrl: 'https://smba.trafficmanager.net/teams/',
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
expect(receivedMessages).toHaveLength(0);
|
|
349
|
+
expect(audit).toHaveBeenCalledWith('message.filtered', expect.objectContaining({
|
|
350
|
+
channelType: 'teams',
|
|
351
|
+
tenantId: 'wrong-tenant',
|
|
352
|
+
reason: 'tenant_not_allowed',
|
|
353
|
+
}));
|
|
354
|
+
await filteredAdapter.disconnect();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('should block messages from non-allowed users', async () => {
|
|
358
|
+
const { audit } = await import('@auxiora/audit');
|
|
359
|
+
const filteredAdapter = new TeamsAdapter({
|
|
360
|
+
microsoftAppId: 'test-app-id',
|
|
361
|
+
microsoftAppPassword: 'test-app-password',
|
|
362
|
+
allowedUsers: ['user-1'],
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
366
|
+
ok: true,
|
|
367
|
+
json: async () => ({ access_token: 'test-token', expires_in: 3600, token_type: 'Bearer' }),
|
|
368
|
+
} as Response);
|
|
369
|
+
await filteredAdapter.connect();
|
|
370
|
+
|
|
371
|
+
const receivedMessages: unknown[] = [];
|
|
372
|
+
filteredAdapter.onMessage(async (msg) => {
|
|
373
|
+
receivedMessages.push(msg);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
await filteredAdapter.handleWebhook({
|
|
377
|
+
type: 'message',
|
|
378
|
+
id: 'activity-filter-4',
|
|
379
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
380
|
+
channelId: 'msteams',
|
|
381
|
+
from: { id: 'user-2', name: 'Eve' },
|
|
382
|
+
conversation: { id: 'conv-1' },
|
|
383
|
+
text: 'Blocked!',
|
|
384
|
+
serviceUrl: 'https://smba.trafficmanager.net/teams/',
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
expect(receivedMessages).toHaveLength(0);
|
|
388
|
+
expect(audit).toHaveBeenCalledWith('message.filtered', expect.objectContaining({
|
|
389
|
+
channelType: 'teams',
|
|
390
|
+
senderId: 'user-2',
|
|
391
|
+
reason: 'user_not_allowed',
|
|
392
|
+
}));
|
|
393
|
+
await filteredAdapter.disconnect();
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('should allow messages when tenantId is undefined even with allowedTenants set', async () => {
|
|
397
|
+
const filteredAdapter = new TeamsAdapter({
|
|
398
|
+
microsoftAppId: 'test-app-id',
|
|
399
|
+
microsoftAppPassword: 'test-app-password',
|
|
400
|
+
allowedTenants: ['tenant-1'],
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
404
|
+
ok: true,
|
|
405
|
+
json: async () => ({ access_token: 'test-token', expires_in: 3600, token_type: 'Bearer' }),
|
|
406
|
+
} as Response);
|
|
407
|
+
await filteredAdapter.connect();
|
|
408
|
+
|
|
409
|
+
const receivedMessages: unknown[] = [];
|
|
410
|
+
filteredAdapter.onMessage(async (msg) => {
|
|
411
|
+
receivedMessages.push(msg);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
await filteredAdapter.handleWebhook({
|
|
415
|
+
type: 'message',
|
|
416
|
+
id: 'activity-filter-5',
|
|
417
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
418
|
+
channelId: 'msteams',
|
|
419
|
+
from: { id: 'user-1', name: 'Alice' },
|
|
420
|
+
conversation: { id: 'conv-1' }, // no tenantId
|
|
421
|
+
text: 'No tenant!',
|
|
422
|
+
serviceUrl: 'https://smba.trafficmanager.net/teams/',
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
expect(receivedMessages).toHaveLength(1);
|
|
426
|
+
await filteredAdapter.disconnect();
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
});
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { TwilioAdapter } from '../src/adapters/twilio.js';
|
|
3
|
+
import type { TwilioWebhookBody } from '../src/adapters/twilio.js';
|
|
4
|
+
|
|
5
|
+
// Mock audit
|
|
6
|
+
vi.mock('@auxiora/audit', () => ({
|
|
7
|
+
audit: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
// Mock twilio
|
|
11
|
+
vi.mock('twilio', () => {
|
|
12
|
+
const mockCreate = vi.fn().mockResolvedValue({ sid: 'SM123' });
|
|
13
|
+
const mockFetch = vi.fn().mockResolvedValue({ sid: 'AC123' });
|
|
14
|
+
const mockTwilio = vi.fn(() => ({
|
|
15
|
+
api: { accounts: vi.fn(() => ({ fetch: mockFetch })) },
|
|
16
|
+
messages: { create: mockCreate },
|
|
17
|
+
}));
|
|
18
|
+
// Add validateRequest as a static method
|
|
19
|
+
mockTwilio.validateRequest = vi.fn(() => true);
|
|
20
|
+
return { default: mockTwilio };
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('TwilioAdapter', () => {
|
|
24
|
+
let adapter: TwilioAdapter;
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
adapter = new TwilioAdapter({
|
|
28
|
+
accountSid: 'AC123',
|
|
29
|
+
authToken: 'test-auth-token',
|
|
30
|
+
phoneNumber: '+1234567890',
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
vi.restoreAllMocks();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should have correct metadata', () => {
|
|
39
|
+
expect(adapter.type).toBe('twilio');
|
|
40
|
+
expect(adapter.name).toBe('Twilio');
|
|
41
|
+
expect(adapter.isConnected()).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should connect successfully', async () => {
|
|
45
|
+
await adapter.connect();
|
|
46
|
+
expect(adapter.isConnected()).toBe(true);
|
|
47
|
+
|
|
48
|
+
await adapter.disconnect();
|
|
49
|
+
expect(adapter.isConnected()).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should handle incoming SMS webhook', async () => {
|
|
53
|
+
await adapter.connect();
|
|
54
|
+
|
|
55
|
+
const receivedMessages: unknown[] = [];
|
|
56
|
+
adapter.onMessage(async (msg) => {
|
|
57
|
+
receivedMessages.push(msg);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const body: TwilioWebhookBody = {
|
|
61
|
+
MessageSid: 'SM456',
|
|
62
|
+
AccountSid: 'AC123',
|
|
63
|
+
From: '+0987654321',
|
|
64
|
+
To: '+1234567890',
|
|
65
|
+
Body: 'Hello via SMS!',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const result = await adapter.handleWebhook(body);
|
|
69
|
+
|
|
70
|
+
expect(result).toBeNull();
|
|
71
|
+
expect(receivedMessages).toHaveLength(1);
|
|
72
|
+
const msg = receivedMessages[0] as { content: string; senderId: string; channelId: string };
|
|
73
|
+
expect(msg.content).toBe('Hello via SMS!');
|
|
74
|
+
expect(msg.senderId).toBe('+0987654321');
|
|
75
|
+
expect(msg.channelId).toBe('sms');
|
|
76
|
+
|
|
77
|
+
await adapter.disconnect();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should handle incoming WhatsApp webhook', async () => {
|
|
81
|
+
await adapter.connect();
|
|
82
|
+
|
|
83
|
+
const receivedMessages: unknown[] = [];
|
|
84
|
+
adapter.onMessage(async (msg) => {
|
|
85
|
+
receivedMessages.push(msg);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const body: TwilioWebhookBody = {
|
|
89
|
+
MessageSid: 'SM789',
|
|
90
|
+
AccountSid: 'AC123',
|
|
91
|
+
From: 'whatsapp:+0987654321',
|
|
92
|
+
To: 'whatsapp:+1234567890',
|
|
93
|
+
Body: 'Hello via WhatsApp!',
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
await adapter.handleWebhook(body);
|
|
97
|
+
|
|
98
|
+
expect(receivedMessages).toHaveLength(1);
|
|
99
|
+
const msg = receivedMessages[0] as { channelId: string };
|
|
100
|
+
expect(msg.channelId).toBe('whatsapp');
|
|
101
|
+
|
|
102
|
+
await adapter.disconnect();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('sender filtering', () => {
|
|
106
|
+
it('should allow all messages when allowedNumbers is not set', async () => {
|
|
107
|
+
await adapter.connect();
|
|
108
|
+
|
|
109
|
+
const receivedMessages: unknown[] = [];
|
|
110
|
+
adapter.onMessage(async (msg) => {
|
|
111
|
+
receivedMessages.push(msg);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await adapter.handleWebhook({
|
|
115
|
+
MessageSid: 'SM001',
|
|
116
|
+
AccountSid: 'AC123',
|
|
117
|
+
From: '+5555555555',
|
|
118
|
+
To: '+1234567890',
|
|
119
|
+
Body: 'Anyone!',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(receivedMessages).toHaveLength(1);
|
|
123
|
+
await adapter.disconnect();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should allow messages from allowed numbers', async () => {
|
|
127
|
+
const filteredAdapter = new TwilioAdapter({
|
|
128
|
+
accountSid: 'AC123',
|
|
129
|
+
authToken: 'test-auth-token',
|
|
130
|
+
phoneNumber: '+1234567890',
|
|
131
|
+
allowedNumbers: ['+0987654321'],
|
|
132
|
+
});
|
|
133
|
+
await filteredAdapter.connect();
|
|
134
|
+
|
|
135
|
+
const receivedMessages: unknown[] = [];
|
|
136
|
+
filteredAdapter.onMessage(async (msg) => {
|
|
137
|
+
receivedMessages.push(msg);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await filteredAdapter.handleWebhook({
|
|
141
|
+
MessageSid: 'SM002',
|
|
142
|
+
AccountSid: 'AC123',
|
|
143
|
+
From: '+0987654321',
|
|
144
|
+
To: '+1234567890',
|
|
145
|
+
Body: 'Allowed!',
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(receivedMessages).toHaveLength(1);
|
|
149
|
+
await filteredAdapter.disconnect();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should block messages from non-allowed numbers', async () => {
|
|
153
|
+
const { audit } = await import('@auxiora/audit');
|
|
154
|
+
const filteredAdapter = new TwilioAdapter({
|
|
155
|
+
accountSid: 'AC123',
|
|
156
|
+
authToken: 'test-auth-token',
|
|
157
|
+
phoneNumber: '+1234567890',
|
|
158
|
+
allowedNumbers: ['+0987654321'],
|
|
159
|
+
});
|
|
160
|
+
await filteredAdapter.connect();
|
|
161
|
+
|
|
162
|
+
const receivedMessages: unknown[] = [];
|
|
163
|
+
filteredAdapter.onMessage(async (msg) => {
|
|
164
|
+
receivedMessages.push(msg);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const result = await filteredAdapter.handleWebhook({
|
|
168
|
+
MessageSid: 'SM003',
|
|
169
|
+
AccountSid: 'AC123',
|
|
170
|
+
From: '+5555555555',
|
|
171
|
+
To: '+1234567890',
|
|
172
|
+
Body: 'Blocked!',
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(result).toBeNull();
|
|
176
|
+
expect(receivedMessages).toHaveLength(0);
|
|
177
|
+
expect(audit).toHaveBeenCalledWith('message.filtered', expect.objectContaining({
|
|
178
|
+
channelType: 'twilio',
|
|
179
|
+
senderId: '+5555555555',
|
|
180
|
+
reason: 'number_not_allowed',
|
|
181
|
+
}));
|
|
182
|
+
await filteredAdapter.disconnect();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should strip whatsapp: prefix before matching allowedNumbers', async () => {
|
|
186
|
+
const filteredAdapter = new TwilioAdapter({
|
|
187
|
+
accountSid: 'AC123',
|
|
188
|
+
authToken: 'test-auth-token',
|
|
189
|
+
phoneNumber: '+1234567890',
|
|
190
|
+
allowedNumbers: ['+0987654321'],
|
|
191
|
+
});
|
|
192
|
+
await filteredAdapter.connect();
|
|
193
|
+
|
|
194
|
+
const receivedMessages: unknown[] = [];
|
|
195
|
+
filteredAdapter.onMessage(async (msg) => {
|
|
196
|
+
receivedMessages.push(msg);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// WhatsApp message with whatsapp: prefix, but number is in allowedNumbers
|
|
200
|
+
await filteredAdapter.handleWebhook({
|
|
201
|
+
MessageSid: 'SM004',
|
|
202
|
+
AccountSid: 'AC123',
|
|
203
|
+
From: 'whatsapp:+0987654321',
|
|
204
|
+
To: 'whatsapp:+1234567890',
|
|
205
|
+
Body: 'WhatsApp allowed!',
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
expect(receivedMessages).toHaveLength(1);
|
|
209
|
+
await filteredAdapter.disconnect();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should block WhatsApp messages from non-allowed numbers', async () => {
|
|
213
|
+
const { audit } = await import('@auxiora/audit');
|
|
214
|
+
const filteredAdapter = new TwilioAdapter({
|
|
215
|
+
accountSid: 'AC123',
|
|
216
|
+
authToken: 'test-auth-token',
|
|
217
|
+
phoneNumber: '+1234567890',
|
|
218
|
+
allowedNumbers: ['+0987654321'],
|
|
219
|
+
});
|
|
220
|
+
await filteredAdapter.connect();
|
|
221
|
+
|
|
222
|
+
const receivedMessages: unknown[] = [];
|
|
223
|
+
filteredAdapter.onMessage(async (msg) => {
|
|
224
|
+
receivedMessages.push(msg);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await filteredAdapter.handleWebhook({
|
|
228
|
+
MessageSid: 'SM005',
|
|
229
|
+
AccountSid: 'AC123',
|
|
230
|
+
From: 'whatsapp:+5555555555',
|
|
231
|
+
To: 'whatsapp:+1234567890',
|
|
232
|
+
Body: 'Blocked WhatsApp!',
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
expect(receivedMessages).toHaveLength(0);
|
|
236
|
+
expect(audit).toHaveBeenCalledWith('message.filtered', expect.objectContaining({
|
|
237
|
+
channelType: 'twilio',
|
|
238
|
+
reason: 'number_not_allowed',
|
|
239
|
+
}));
|
|
240
|
+
await filteredAdapter.disconnect();
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should handle message handler errors gracefully', async () => {
|
|
245
|
+
await adapter.connect();
|
|
246
|
+
|
|
247
|
+
const errorHandler = vi.fn();
|
|
248
|
+
adapter.onError(errorHandler);
|
|
249
|
+
adapter.onMessage(async () => {
|
|
250
|
+
throw new Error('Handler error');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
await adapter.handleWebhook({
|
|
254
|
+
MessageSid: 'SM999',
|
|
255
|
+
AccountSid: 'AC123',
|
|
256
|
+
From: '+0987654321',
|
|
257
|
+
To: '+1234567890',
|
|
258
|
+
Body: 'Trigger error',
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
expect(errorHandler).toHaveBeenCalledWith(expect.any(Error));
|
|
262
|
+
await adapter.disconnect();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should register error handler', () => {
|
|
266
|
+
const handler = vi.fn();
|
|
267
|
+
adapter.onError(handler);
|
|
268
|
+
});
|
|
269
|
+
});
|