@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,438 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { BlueBubblesAdapter } from '../src/adapters/bluebubbles.js';
|
|
3
|
+
|
|
4
|
+
// Mock audit
|
|
5
|
+
vi.mock('@auxiora/audit', () => ({
|
|
6
|
+
audit: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
describe('BlueBubblesAdapter', () => {
|
|
10
|
+
let adapter: BlueBubblesAdapter;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
adapter = new BlueBubblesAdapter({
|
|
14
|
+
serverUrl: 'http://localhost:1234',
|
|
15
|
+
password: 'test-password',
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
vi.restoreAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should have correct metadata', () => {
|
|
24
|
+
expect(adapter.type).toBe('bluebubbles');
|
|
25
|
+
expect(adapter.name).toBe('BlueBubbles (iMessage)');
|
|
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 () => ({ status: 200, message: 'OK', data: {} }),
|
|
33
|
+
} as Response);
|
|
34
|
+
|
|
35
|
+
await adapter.connect();
|
|
36
|
+
expect(adapter.isConnected()).toBe(true);
|
|
37
|
+
|
|
38
|
+
await adapter.disconnect();
|
|
39
|
+
expect(adapter.isConnected()).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should fail to connect on API error', async () => {
|
|
43
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
44
|
+
ok: false,
|
|
45
|
+
status: 401,
|
|
46
|
+
statusText: 'Unauthorized',
|
|
47
|
+
} as Response);
|
|
48
|
+
|
|
49
|
+
await expect(adapter.connect()).rejects.toThrow('BlueBubbles API error 401');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should handle incoming new-message webhook', async () => {
|
|
53
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
54
|
+
ok: true,
|
|
55
|
+
json: async () => ({ status: 200, message: 'OK', data: {} }),
|
|
56
|
+
} as Response);
|
|
57
|
+
await adapter.connect();
|
|
58
|
+
|
|
59
|
+
const receivedMessages: unknown[] = [];
|
|
60
|
+
adapter.onMessage(async (msg) => {
|
|
61
|
+
receivedMessages.push(msg);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await adapter.handleWebhook({
|
|
65
|
+
type: 'new-message',
|
|
66
|
+
data: {
|
|
67
|
+
guid: 'msg-guid-123',
|
|
68
|
+
text: 'Hello iMessage!',
|
|
69
|
+
handle: {
|
|
70
|
+
address: '+1234567890',
|
|
71
|
+
service: 'iMessage',
|
|
72
|
+
uncanonicalizedId: 'alice@icloud.com',
|
|
73
|
+
},
|
|
74
|
+
chats: [
|
|
75
|
+
{
|
|
76
|
+
guid: 'iMessage;-;+1234567890',
|
|
77
|
+
chatIdentifier: '+1234567890',
|
|
78
|
+
displayName: 'Alice',
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
dateCreated: 1700000000000,
|
|
82
|
+
isFromMe: false,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(receivedMessages).toHaveLength(1);
|
|
87
|
+
const msg = receivedMessages[0] as {
|
|
88
|
+
content: string;
|
|
89
|
+
senderId: string;
|
|
90
|
+
senderName: string;
|
|
91
|
+
channelId: string;
|
|
92
|
+
};
|
|
93
|
+
expect(msg.content).toBe('Hello iMessage!');
|
|
94
|
+
expect(msg.senderId).toBe('+1234567890');
|
|
95
|
+
expect(msg.senderName).toBe('alice@icloud.com');
|
|
96
|
+
expect(msg.channelId).toBe('iMessage;-;+1234567890');
|
|
97
|
+
|
|
98
|
+
await adapter.disconnect();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should ignore own messages', async () => {
|
|
102
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
103
|
+
ok: true,
|
|
104
|
+
json: async () => ({ status: 200, message: 'OK', data: {} }),
|
|
105
|
+
} as Response);
|
|
106
|
+
await adapter.connect();
|
|
107
|
+
|
|
108
|
+
const receivedMessages: unknown[] = [];
|
|
109
|
+
adapter.onMessage(async (msg) => {
|
|
110
|
+
receivedMessages.push(msg);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await adapter.handleWebhook({
|
|
114
|
+
type: 'new-message',
|
|
115
|
+
data: {
|
|
116
|
+
guid: 'msg-guid-own',
|
|
117
|
+
text: 'My own message',
|
|
118
|
+
handle: {
|
|
119
|
+
address: '+1234567890',
|
|
120
|
+
service: 'iMessage',
|
|
121
|
+
},
|
|
122
|
+
chats: [{ guid: 'iMessage;-;+1234567890', chatIdentifier: '+1234567890' }],
|
|
123
|
+
dateCreated: 1700000000000,
|
|
124
|
+
isFromMe: true,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(receivedMessages).toHaveLength(0);
|
|
129
|
+
|
|
130
|
+
await adapter.disconnect();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should ignore non-new-message events', async () => {
|
|
134
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
135
|
+
ok: true,
|
|
136
|
+
json: async () => ({ status: 200, message: 'OK', data: {} }),
|
|
137
|
+
} as Response);
|
|
138
|
+
await adapter.connect();
|
|
139
|
+
|
|
140
|
+
const receivedMessages: unknown[] = [];
|
|
141
|
+
adapter.onMessage(async (msg) => {
|
|
142
|
+
receivedMessages.push(msg);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await adapter.handleWebhook({
|
|
146
|
+
type: 'updated-message',
|
|
147
|
+
data: {
|
|
148
|
+
guid: 'msg-guid-update',
|
|
149
|
+
text: 'Updated',
|
|
150
|
+
dateCreated: 1700000000000,
|
|
151
|
+
isFromMe: false,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(receivedMessages).toHaveLength(0);
|
|
156
|
+
|
|
157
|
+
await adapter.disconnect();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should handle messages with attachments', async () => {
|
|
161
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
162
|
+
ok: true,
|
|
163
|
+
json: async () => ({ status: 200, message: 'OK', data: {} }),
|
|
164
|
+
} as Response);
|
|
165
|
+
await adapter.connect();
|
|
166
|
+
|
|
167
|
+
const receivedMessages: unknown[] = [];
|
|
168
|
+
adapter.onMessage(async (msg) => {
|
|
169
|
+
receivedMessages.push(msg);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await adapter.handleWebhook({
|
|
173
|
+
type: 'new-message',
|
|
174
|
+
data: {
|
|
175
|
+
guid: 'msg-guid-attach',
|
|
176
|
+
text: 'Check this photo',
|
|
177
|
+
handle: {
|
|
178
|
+
address: '+1234567890',
|
|
179
|
+
service: 'iMessage',
|
|
180
|
+
},
|
|
181
|
+
chats: [{ guid: 'iMessage;-;+1234567890', chatIdentifier: '+1234567890' }],
|
|
182
|
+
dateCreated: 1700000000000,
|
|
183
|
+
isFromMe: false,
|
|
184
|
+
attachments: [
|
|
185
|
+
{
|
|
186
|
+
guid: 'att-1',
|
|
187
|
+
mimeType: 'image/jpeg',
|
|
188
|
+
transferName: 'photo.jpg',
|
|
189
|
+
totalBytes: 1024000,
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(receivedMessages).toHaveLength(1);
|
|
196
|
+
const msg = receivedMessages[0] as {
|
|
197
|
+
attachments: Array<{ type: string; mimeType: string; filename: string }>;
|
|
198
|
+
};
|
|
199
|
+
expect(msg.attachments).toHaveLength(1);
|
|
200
|
+
expect(msg.attachments[0].type).toBe('image');
|
|
201
|
+
expect(msg.attachments[0].mimeType).toBe('image/jpeg');
|
|
202
|
+
expect(msg.attachments[0].filename).toBe('photo.jpg');
|
|
203
|
+
|
|
204
|
+
await adapter.disconnect();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should handle reply threads', async () => {
|
|
208
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
209
|
+
ok: true,
|
|
210
|
+
json: async () => ({ status: 200, message: 'OK', data: {} }),
|
|
211
|
+
} as Response);
|
|
212
|
+
await adapter.connect();
|
|
213
|
+
|
|
214
|
+
const receivedMessages: unknown[] = [];
|
|
215
|
+
adapter.onMessage(async (msg) => {
|
|
216
|
+
receivedMessages.push(msg);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
await adapter.handleWebhook({
|
|
220
|
+
type: 'new-message',
|
|
221
|
+
data: {
|
|
222
|
+
guid: 'msg-guid-reply',
|
|
223
|
+
text: 'This is a reply',
|
|
224
|
+
handle: { address: '+1234567890', service: 'iMessage' },
|
|
225
|
+
chats: [{ guid: 'iMessage;-;+1234567890', chatIdentifier: '+1234567890' }],
|
|
226
|
+
dateCreated: 1700000000000,
|
|
227
|
+
isFromMe: false,
|
|
228
|
+
threadOriginatorGuid: 'msg-guid-original',
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
expect(receivedMessages).toHaveLength(1);
|
|
233
|
+
const msg = receivedMessages[0] as { replyToId: string };
|
|
234
|
+
expect(msg.replyToId).toBe('msg-guid-original');
|
|
235
|
+
|
|
236
|
+
await adapter.disconnect();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should send a message successfully', async () => {
|
|
240
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
241
|
+
ok: true,
|
|
242
|
+
json: async () => ({ status: 200, message: 'OK', data: {} }),
|
|
243
|
+
} as Response);
|
|
244
|
+
await adapter.connect();
|
|
245
|
+
|
|
246
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
247
|
+
ok: true,
|
|
248
|
+
json: async () => ({
|
|
249
|
+
status: 200,
|
|
250
|
+
message: 'Message sent!',
|
|
251
|
+
data: {
|
|
252
|
+
guid: 'msg-sent-guid',
|
|
253
|
+
text: 'Hello from bot!',
|
|
254
|
+
dateCreated: 1700000001000,
|
|
255
|
+
isFromMe: true,
|
|
256
|
+
},
|
|
257
|
+
}),
|
|
258
|
+
} as Response);
|
|
259
|
+
|
|
260
|
+
const result = await adapter.send('iMessage;-;+1234567890', {
|
|
261
|
+
content: 'Hello from bot!',
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
expect(result.success).toBe(true);
|
|
265
|
+
expect(result.messageId).toBe('msg-sent-guid');
|
|
266
|
+
|
|
267
|
+
await adapter.disconnect();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should handle send errors', async () => {
|
|
271
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
272
|
+
ok: true,
|
|
273
|
+
json: async () => ({ status: 200, message: 'OK', data: {} }),
|
|
274
|
+
} as Response);
|
|
275
|
+
await adapter.connect();
|
|
276
|
+
|
|
277
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
278
|
+
ok: false,
|
|
279
|
+
status: 500,
|
|
280
|
+
statusText: 'Internal Server Error',
|
|
281
|
+
} as Response);
|
|
282
|
+
|
|
283
|
+
const result = await adapter.send('iMessage;-;+1234567890', {
|
|
284
|
+
content: 'Should fail',
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
expect(result.success).toBe(false);
|
|
288
|
+
expect(result.error).toContain('500');
|
|
289
|
+
|
|
290
|
+
await adapter.disconnect();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should send reply with selectedMessageGuid', async () => {
|
|
294
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
295
|
+
ok: true,
|
|
296
|
+
json: async () => ({ status: 200, message: 'OK', data: {} }),
|
|
297
|
+
} as Response);
|
|
298
|
+
await adapter.connect();
|
|
299
|
+
|
|
300
|
+
fetchSpy.mockResolvedValueOnce({
|
|
301
|
+
ok: true,
|
|
302
|
+
json: async () => ({
|
|
303
|
+
status: 200,
|
|
304
|
+
message: 'Message sent!',
|
|
305
|
+
data: {
|
|
306
|
+
guid: 'msg-reply-sent',
|
|
307
|
+
text: 'Reply',
|
|
308
|
+
dateCreated: 1700000001000,
|
|
309
|
+
isFromMe: true,
|
|
310
|
+
},
|
|
311
|
+
}),
|
|
312
|
+
} as Response);
|
|
313
|
+
|
|
314
|
+
await adapter.send('iMessage;-;+1234567890', {
|
|
315
|
+
content: 'Reply',
|
|
316
|
+
replyToId: 'msg-guid-original',
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const sendCall = fetchSpy.mock.calls.find(
|
|
320
|
+
(call) =>
|
|
321
|
+
typeof call[0] === 'string' &&
|
|
322
|
+
call[0].includes('/api/v1/message/text'),
|
|
323
|
+
);
|
|
324
|
+
expect(sendCall).toBeDefined();
|
|
325
|
+
const body = JSON.parse(sendCall![1]?.body as string);
|
|
326
|
+
expect(body.selectedMessageGuid).toBe('msg-guid-original');
|
|
327
|
+
|
|
328
|
+
await adapter.disconnect();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should register error handler', () => {
|
|
332
|
+
const handler = vi.fn();
|
|
333
|
+
adapter.onError(handler);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe('sender filtering', () => {
|
|
337
|
+
it('should allow all messages when allowedAddresses is not set', async () => {
|
|
338
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
339
|
+
ok: true,
|
|
340
|
+
json: async () => ({ status: 200, message: 'OK', data: {} }),
|
|
341
|
+
} as Response);
|
|
342
|
+
await adapter.connect();
|
|
343
|
+
|
|
344
|
+
const receivedMessages: unknown[] = [];
|
|
345
|
+
adapter.onMessage(async (msg) => {
|
|
346
|
+
receivedMessages.push(msg);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
await adapter.handleWebhook({
|
|
350
|
+
type: 'new-message',
|
|
351
|
+
data: {
|
|
352
|
+
guid: 'msg-filter-1',
|
|
353
|
+
text: 'Hello!',
|
|
354
|
+
handle: { address: '+anyone', service: 'iMessage' },
|
|
355
|
+
chats: [{ guid: 'iMessage;-;+anyone', chatIdentifier: '+anyone' }],
|
|
356
|
+
dateCreated: 1700000000000,
|
|
357
|
+
isFromMe: false,
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
expect(receivedMessages).toHaveLength(1);
|
|
362
|
+
await adapter.disconnect();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('should block messages from non-allowed addresses', async () => {
|
|
366
|
+
const { audit } = await import('@auxiora/audit');
|
|
367
|
+
const filteredAdapter = new BlueBubblesAdapter({
|
|
368
|
+
serverUrl: 'http://localhost:1234',
|
|
369
|
+
password: 'test-password',
|
|
370
|
+
allowedAddresses: ['+1234567890'],
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
374
|
+
ok: true,
|
|
375
|
+
json: async () => ({ status: 200, message: 'OK', data: {} }),
|
|
376
|
+
} as Response);
|
|
377
|
+
await filteredAdapter.connect();
|
|
378
|
+
|
|
379
|
+
const receivedMessages: unknown[] = [];
|
|
380
|
+
filteredAdapter.onMessage(async (msg) => {
|
|
381
|
+
receivedMessages.push(msg);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
await filteredAdapter.handleWebhook({
|
|
385
|
+
type: 'new-message',
|
|
386
|
+
data: {
|
|
387
|
+
guid: 'msg-filter-blocked',
|
|
388
|
+
text: 'Blocked!',
|
|
389
|
+
handle: { address: '+5555555555', service: 'iMessage' },
|
|
390
|
+
chats: [{ guid: 'iMessage;-;+5555555555', chatIdentifier: '+5555555555' }],
|
|
391
|
+
dateCreated: 1700000000000,
|
|
392
|
+
isFromMe: false,
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
expect(receivedMessages).toHaveLength(0);
|
|
397
|
+
expect(audit).toHaveBeenCalledWith(
|
|
398
|
+
'message.filtered',
|
|
399
|
+
expect.objectContaining({
|
|
400
|
+
channelType: 'bluebubbles',
|
|
401
|
+
senderId: '+5555555555',
|
|
402
|
+
reason: 'address_not_allowed',
|
|
403
|
+
}),
|
|
404
|
+
);
|
|
405
|
+
await filteredAdapter.disconnect();
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('should handle message handler errors gracefully', async () => {
|
|
410
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
411
|
+
ok: true,
|
|
412
|
+
json: async () => ({ status: 200, message: 'OK', data: {} }),
|
|
413
|
+
} as Response);
|
|
414
|
+
await adapter.connect();
|
|
415
|
+
|
|
416
|
+
const errorHandler = vi.fn();
|
|
417
|
+
adapter.onError(errorHandler);
|
|
418
|
+
adapter.onMessage(async () => {
|
|
419
|
+
throw new Error('Handler error');
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
await adapter.handleWebhook({
|
|
423
|
+
type: 'new-message',
|
|
424
|
+
data: {
|
|
425
|
+
guid: 'msg-err',
|
|
426
|
+
text: 'Trigger error',
|
|
427
|
+
handle: { address: '+1234567890', service: 'iMessage' },
|
|
428
|
+
chats: [{ guid: 'iMessage;-;+1234567890', chatIdentifier: '+1234567890' }],
|
|
429
|
+
dateCreated: 1700000000000,
|
|
430
|
+
isFromMe: false,
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
expect(errorHandler).toHaveBeenCalledWith(expect.any(Error));
|
|
435
|
+
|
|
436
|
+
await adapter.disconnect();
|
|
437
|
+
});
|
|
438
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { EmailAdapter } from '../src/adapters/email.js';
|
|
3
|
+
|
|
4
|
+
// Mock audit
|
|
5
|
+
vi.mock('@auxiora/audit', () => ({
|
|
6
|
+
audit: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
const createMockSocket = () => {
|
|
10
|
+
const handlers: Record<string, Array<(...args: unknown[]) => void>> = {};
|
|
11
|
+
const socket = {
|
|
12
|
+
on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
|
|
13
|
+
if (!handlers[event]) handlers[event] = [];
|
|
14
|
+
handlers[event].push(handler);
|
|
15
|
+
return socket;
|
|
16
|
+
}),
|
|
17
|
+
once: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
|
|
18
|
+
if (!handlers[event]) handlers[event] = [];
|
|
19
|
+
handlers[event].push(handler);
|
|
20
|
+
// Auto-fire connect/secureConnect immediately
|
|
21
|
+
if (event === 'connect' || event === 'secureConnect') {
|
|
22
|
+
queueMicrotask(() => handler());
|
|
23
|
+
}
|
|
24
|
+
return socket;
|
|
25
|
+
}),
|
|
26
|
+
removeListener: vi.fn(),
|
|
27
|
+
write: vi.fn((_data: string, cb?: (err?: Error | null) => void) => {
|
|
28
|
+
cb?.();
|
|
29
|
+
return true;
|
|
30
|
+
}),
|
|
31
|
+
destroy: vi.fn(),
|
|
32
|
+
emit: (event: string, ...args: unknown[]) => {
|
|
33
|
+
handlers[event]?.forEach((h) => h(...args));
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
return socket;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Mock node:net and node:tls
|
|
40
|
+
vi.mock('node:net', () => ({
|
|
41
|
+
default: { connect: vi.fn(() => createMockSocket()) },
|
|
42
|
+
connect: vi.fn(() => createMockSocket()),
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
vi.mock('node:tls', () => ({
|
|
46
|
+
default: { connect: vi.fn(() => createMockSocket()) },
|
|
47
|
+
connect: vi.fn(() => createMockSocket()),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
describe('EmailAdapter', () => {
|
|
51
|
+
let adapter: EmailAdapter;
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
adapter = new EmailAdapter({
|
|
55
|
+
imapHost: 'imap.example.com',
|
|
56
|
+
imapPort: 993,
|
|
57
|
+
smtpHost: 'smtp.example.com',
|
|
58
|
+
smtpPort: 465,
|
|
59
|
+
email: 'bot@example.com',
|
|
60
|
+
password: 'secret-password',
|
|
61
|
+
pollInterval: 60000,
|
|
62
|
+
allowedSenders: ['alice@example.com', 'bob@example.com'],
|
|
63
|
+
tls: false,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterEach(async () => {
|
|
68
|
+
await adapter.disconnect();
|
|
69
|
+
vi.restoreAllMocks();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should have correct metadata', () => {
|
|
73
|
+
expect(adapter.type).toBe('email');
|
|
74
|
+
expect(adapter.name).toBe('Email');
|
|
75
|
+
expect(adapter.isConnected()).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should connect and disconnect', async () => {
|
|
79
|
+
await adapter.connect();
|
|
80
|
+
expect(adapter.isConnected()).toBe(true);
|
|
81
|
+
|
|
82
|
+
await adapter.disconnect();
|
|
83
|
+
expect(adapter.isConnected()).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should register message handler', () => {
|
|
87
|
+
const handler = vi.fn();
|
|
88
|
+
adapter.onMessage(handler);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should register error handler', () => {
|
|
92
|
+
const handler = vi.fn();
|
|
93
|
+
adapter.onError(handler);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should handle send when SMTP not connected', async () => {
|
|
97
|
+
const result = await adapter.send('user@example.com', {
|
|
98
|
+
content: 'Hello!',
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(result.success).toBe(false);
|
|
102
|
+
expect(result.error).toBe('SMTP not connected');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should handle disconnect when not connected', async () => {
|
|
106
|
+
await adapter.disconnect();
|
|
107
|
+
expect(adapter.isConnected()).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should accept allowed senders config', () => {
|
|
111
|
+
const adapterWithAllowed = new EmailAdapter({
|
|
112
|
+
imapHost: 'imap.example.com',
|
|
113
|
+
imapPort: 993,
|
|
114
|
+
smtpHost: 'smtp.example.com',
|
|
115
|
+
smtpPort: 465,
|
|
116
|
+
email: 'bot@example.com',
|
|
117
|
+
password: 'pass',
|
|
118
|
+
allowedSenders: ['trusted@example.com'],
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(adapterWithAllowed.type).toBe('email');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should set default poll interval', () => {
|
|
125
|
+
const adapterNoPoll = new EmailAdapter({
|
|
126
|
+
imapHost: 'imap.example.com',
|
|
127
|
+
imapPort: 993,
|
|
128
|
+
smtpHost: 'smtp.example.com',
|
|
129
|
+
smtpPort: 465,
|
|
130
|
+
email: 'bot@example.com',
|
|
131
|
+
password: 'pass',
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(adapterNoPoll.type).toBe('email');
|
|
135
|
+
});
|
|
136
|
+
});
|