@massalabs/gossip-sdk 0.0.1
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/README.md +484 -0
- package/package.json +41 -0
- package/src/api/messageProtocol/index.ts +53 -0
- package/src/api/messageProtocol/mock.ts +13 -0
- package/src/api/messageProtocol/rest.ts +209 -0
- package/src/api/messageProtocol/types.ts +70 -0
- package/src/config/protocol.ts +97 -0
- package/src/config/sdk.ts +131 -0
- package/src/contacts.ts +210 -0
- package/src/core/SdkEventEmitter.ts +91 -0
- package/src/core/SdkPolling.ts +134 -0
- package/src/core/index.ts +9 -0
- package/src/crypto/bip39.ts +84 -0
- package/src/crypto/encryption.ts +77 -0
- package/src/db.ts +465 -0
- package/src/gossipSdk.ts +994 -0
- package/src/index.ts +211 -0
- package/src/services/announcement.ts +653 -0
- package/src/services/auth.ts +95 -0
- package/src/services/discussion.ts +380 -0
- package/src/services/message.ts +1055 -0
- package/src/services/refresh.ts +234 -0
- package/src/sw.ts +17 -0
- package/src/types/events.ts +108 -0
- package/src/types.ts +70 -0
- package/src/utils/base64.ts +39 -0
- package/src/utils/contacts.ts +161 -0
- package/src/utils/discussions.ts +55 -0
- package/src/utils/logs.ts +86 -0
- package/src/utils/messageSerialization.ts +257 -0
- package/src/utils/queue.ts +106 -0
- package/src/utils/type.ts +7 -0
- package/src/utils/userId.ts +114 -0
- package/src/utils/validation.ts +144 -0
- package/src/utils.ts +47 -0
- package/src/wasm/encryption.ts +108 -0
- package/src/wasm/index.ts +20 -0
- package/src/wasm/loader.ts +123 -0
- package/src/wasm/session.ts +276 -0
- package/src/wasm/userKeys.ts +31 -0
- package/test/config/protocol.spec.ts +31 -0
- package/test/config/sdk.spec.ts +163 -0
- package/test/db/helpers.spec.ts +142 -0
- package/test/db/operations.spec.ts +128 -0
- package/test/db/states.spec.ts +535 -0
- package/test/integration/discussion-flow.spec.ts +422 -0
- package/test/integration/messaging-flow.spec.ts +708 -0
- package/test/integration/sdk-lifecycle.spec.ts +325 -0
- package/test/mocks/index.ts +9 -0
- package/test/mocks/mockMessageProtocol.ts +100 -0
- package/test/services/auth.spec.ts +311 -0
- package/test/services/discussion.spec.ts +279 -0
- package/test/services/message-deduplication.spec.ts +299 -0
- package/test/services/message-startup.spec.ts +331 -0
- package/test/services/message.spec.ts +817 -0
- package/test/services/refresh.spec.ts +199 -0
- package/test/services/session-status.spec.ts +349 -0
- package/test/session/wasm.spec.ts +227 -0
- package/test/setup.ts +52 -0
- package/test/utils/contacts.spec.ts +156 -0
- package/test/utils/discussions.spec.ts +66 -0
- package/test/utils/queue.spec.ts +52 -0
- package/test/utils/serialization.spec.ts +120 -0
- package/test/utils/userId.spec.ts +120 -0
- package/test/utils/validation.spec.ts +223 -0
- package/test/utils.ts +212 -0
- package/tsconfig.json +26 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +28 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Deduplication tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
GossipDatabase,
|
|
8
|
+
MessageStatus,
|
|
9
|
+
MessageDirection,
|
|
10
|
+
MessageType,
|
|
11
|
+
DiscussionStatus,
|
|
12
|
+
DiscussionDirection,
|
|
13
|
+
} from '../../src/db';
|
|
14
|
+
import { encodeUserId } from '../../src/utils/userId';
|
|
15
|
+
import { defaultSdkConfig, type SdkConfig } from '../../src/config/sdk';
|
|
16
|
+
|
|
17
|
+
const DEDUP_OWNER_USER_ID = encodeUserId(new Uint8Array(32).fill(1));
|
|
18
|
+
const DEDUP_CONTACT_USER_ID = encodeUserId(new Uint8Array(32).fill(2));
|
|
19
|
+
|
|
20
|
+
describe('Message Deduplication', () => {
|
|
21
|
+
let testDb: GossipDatabase;
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
testDb = new GossipDatabase();
|
|
25
|
+
if (!testDb.isOpen()) {
|
|
26
|
+
await testDb.open();
|
|
27
|
+
}
|
|
28
|
+
await Promise.all(testDb.tables.map(table => table.clear()));
|
|
29
|
+
|
|
30
|
+
await testDb.discussions.add({
|
|
31
|
+
ownerUserId: DEDUP_OWNER_USER_ID,
|
|
32
|
+
contactUserId: DEDUP_CONTACT_USER_ID,
|
|
33
|
+
direction: DiscussionDirection.RECEIVED,
|
|
34
|
+
status: DiscussionStatus.ACTIVE,
|
|
35
|
+
unreadCount: 0,
|
|
36
|
+
createdAt: new Date(),
|
|
37
|
+
updatedAt: new Date(),
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('isDuplicateMessage (via storeDecryptedMessages)', () => {
|
|
42
|
+
async function storeIncomingMessage(
|
|
43
|
+
content: string,
|
|
44
|
+
timestamp: Date,
|
|
45
|
+
seeker: Uint8Array
|
|
46
|
+
): Promise<number | null> {
|
|
47
|
+
const existing = await testDb.messages
|
|
48
|
+
.where('[ownerUserId+contactUserId]')
|
|
49
|
+
.equals([DEDUP_OWNER_USER_ID, DEDUP_CONTACT_USER_ID])
|
|
50
|
+
.and(
|
|
51
|
+
msg =>
|
|
52
|
+
msg.direction === MessageDirection.INCOMING &&
|
|
53
|
+
msg.content === content &&
|
|
54
|
+
msg.timestamp >= new Date(timestamp.getTime() - 30000) &&
|
|
55
|
+
msg.timestamp <= new Date(timestamp.getTime() + 30000)
|
|
56
|
+
)
|
|
57
|
+
.first();
|
|
58
|
+
|
|
59
|
+
if (existing) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return await testDb.messages.add({
|
|
64
|
+
ownerUserId: DEDUP_OWNER_USER_ID,
|
|
65
|
+
contactUserId: DEDUP_CONTACT_USER_ID,
|
|
66
|
+
content,
|
|
67
|
+
type: MessageType.TEXT,
|
|
68
|
+
direction: MessageDirection.INCOMING,
|
|
69
|
+
status: MessageStatus.DELIVERED,
|
|
70
|
+
timestamp,
|
|
71
|
+
seeker,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
it('should store first message normally', async () => {
|
|
76
|
+
const timestamp = new Date();
|
|
77
|
+
const id = await storeIncomingMessage(
|
|
78
|
+
'Hello world',
|
|
79
|
+
timestamp,
|
|
80
|
+
new Uint8Array([1, 2, 3])
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(id).not.toBeNull();
|
|
84
|
+
|
|
85
|
+
const message = await testDb.messages.get(id!);
|
|
86
|
+
expect(message?.content).toBe('Hello world');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should detect duplicate with same content and similar timestamp', async () => {
|
|
90
|
+
const timestamp = new Date();
|
|
91
|
+
|
|
92
|
+
const id1 = await storeIncomingMessage(
|
|
93
|
+
'Hello world',
|
|
94
|
+
timestamp,
|
|
95
|
+
new Uint8Array([1, 2, 3])
|
|
96
|
+
);
|
|
97
|
+
expect(id1).not.toBeNull();
|
|
98
|
+
|
|
99
|
+
const timestamp2 = new Date(timestamp.getTime() + 5000);
|
|
100
|
+
const id2 = await storeIncomingMessage(
|
|
101
|
+
'Hello world',
|
|
102
|
+
timestamp2,
|
|
103
|
+
new Uint8Array([4, 5, 6])
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
expect(id2).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should NOT detect duplicate if content differs', async () => {
|
|
110
|
+
const timestamp = new Date();
|
|
111
|
+
|
|
112
|
+
const id1 = await storeIncomingMessage(
|
|
113
|
+
'Hello world',
|
|
114
|
+
timestamp,
|
|
115
|
+
new Uint8Array([1, 2, 3])
|
|
116
|
+
);
|
|
117
|
+
expect(id1).not.toBeNull();
|
|
118
|
+
|
|
119
|
+
const id2 = await storeIncomingMessage(
|
|
120
|
+
'Goodbye world',
|
|
121
|
+
timestamp,
|
|
122
|
+
new Uint8Array([4, 5, 6])
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
expect(id2).not.toBeNull();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should NOT detect duplicate if timestamp outside window', async () => {
|
|
129
|
+
const timestamp = new Date();
|
|
130
|
+
|
|
131
|
+
const id1 = await storeIncomingMessage(
|
|
132
|
+
'Hello world',
|
|
133
|
+
timestamp,
|
|
134
|
+
new Uint8Array([1, 2, 3])
|
|
135
|
+
);
|
|
136
|
+
expect(id1).not.toBeNull();
|
|
137
|
+
|
|
138
|
+
const timestamp2 = new Date(timestamp.getTime() + 60000);
|
|
139
|
+
const id2 = await storeIncomingMessage(
|
|
140
|
+
'Hello world',
|
|
141
|
+
timestamp2,
|
|
142
|
+
new Uint8Array([4, 5, 6])
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
expect(id2).not.toBeNull();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should NOT flag outgoing messages as duplicates of incoming', async () => {
|
|
149
|
+
const timestamp = new Date();
|
|
150
|
+
|
|
151
|
+
await testDb.messages.add({
|
|
152
|
+
ownerUserId: DEDUP_OWNER_USER_ID,
|
|
153
|
+
contactUserId: DEDUP_CONTACT_USER_ID,
|
|
154
|
+
content: 'Hello world',
|
|
155
|
+
type: MessageType.TEXT,
|
|
156
|
+
direction: MessageDirection.INCOMING,
|
|
157
|
+
status: MessageStatus.DELIVERED,
|
|
158
|
+
timestamp,
|
|
159
|
+
seeker: new Uint8Array([1, 2, 3]),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const outgoingId = await testDb.messages.add({
|
|
163
|
+
ownerUserId: DEDUP_OWNER_USER_ID,
|
|
164
|
+
contactUserId: DEDUP_CONTACT_USER_ID,
|
|
165
|
+
content: 'Hello world',
|
|
166
|
+
type: MessageType.TEXT,
|
|
167
|
+
direction: MessageDirection.OUTGOING,
|
|
168
|
+
status: MessageStatus.SENT,
|
|
169
|
+
timestamp,
|
|
170
|
+
seeker: new Uint8Array([4, 5, 6]),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(outgoingId).toBeDefined();
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('deduplication window configuration', () => {
|
|
178
|
+
it('should respect custom deduplication window', async () => {
|
|
179
|
+
const customConfig: SdkConfig = {
|
|
180
|
+
...defaultSdkConfig,
|
|
181
|
+
messages: {
|
|
182
|
+
...defaultSdkConfig.messages,
|
|
183
|
+
deduplicationWindowMs: 5000,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const timestamp = new Date();
|
|
188
|
+
|
|
189
|
+
await testDb.messages.add({
|
|
190
|
+
ownerUserId: DEDUP_OWNER_USER_ID,
|
|
191
|
+
contactUserId: DEDUP_CONTACT_USER_ID,
|
|
192
|
+
content: 'Test message',
|
|
193
|
+
type: MessageType.TEXT,
|
|
194
|
+
direction: MessageDirection.INCOMING,
|
|
195
|
+
status: MessageStatus.DELIVERED,
|
|
196
|
+
timestamp,
|
|
197
|
+
seeker: new Uint8Array([1, 2, 3]),
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const timestamp2 = new Date(timestamp.getTime() + 10000);
|
|
201
|
+
const windowMs = customConfig.messages.deduplicationWindowMs;
|
|
202
|
+
const windowStart = new Date(timestamp2.getTime() - windowMs);
|
|
203
|
+
const windowEnd = new Date(timestamp2.getTime() + windowMs);
|
|
204
|
+
|
|
205
|
+
const duplicate = await testDb.messages
|
|
206
|
+
.where('[ownerUserId+contactUserId]')
|
|
207
|
+
.equals([DEDUP_OWNER_USER_ID, DEDUP_CONTACT_USER_ID])
|
|
208
|
+
.and(
|
|
209
|
+
msg =>
|
|
210
|
+
msg.direction === MessageDirection.INCOMING &&
|
|
211
|
+
msg.content === 'Test message' &&
|
|
212
|
+
msg.timestamp >= windowStart &&
|
|
213
|
+
msg.timestamp <= windowEnd
|
|
214
|
+
)
|
|
215
|
+
.first();
|
|
216
|
+
|
|
217
|
+
expect(duplicate).toBeUndefined();
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('edge cases', () => {
|
|
222
|
+
it('should handle empty content messages', async () => {
|
|
223
|
+
const timestamp = new Date();
|
|
224
|
+
|
|
225
|
+
const id1 = await testDb.messages.add({
|
|
226
|
+
ownerUserId: DEDUP_OWNER_USER_ID,
|
|
227
|
+
contactUserId: DEDUP_CONTACT_USER_ID,
|
|
228
|
+
content: '',
|
|
229
|
+
type: MessageType.KEEP_ALIVE,
|
|
230
|
+
direction: MessageDirection.INCOMING,
|
|
231
|
+
status: MessageStatus.DELIVERED,
|
|
232
|
+
timestamp,
|
|
233
|
+
seeker: new Uint8Array([1, 2, 3]),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const windowMs = 30000;
|
|
237
|
+
const windowStart = new Date(timestamp.getTime() - windowMs);
|
|
238
|
+
const windowEnd = new Date(timestamp.getTime() + windowMs);
|
|
239
|
+
|
|
240
|
+
const duplicate = await testDb.messages
|
|
241
|
+
.where('[ownerUserId+contactUserId]')
|
|
242
|
+
.equals([DEDUP_OWNER_USER_ID, DEDUP_CONTACT_USER_ID])
|
|
243
|
+
.and(
|
|
244
|
+
msg =>
|
|
245
|
+
msg.direction === MessageDirection.INCOMING &&
|
|
246
|
+
msg.content === '' &&
|
|
247
|
+
msg.timestamp >= windowStart &&
|
|
248
|
+
msg.timestamp <= windowEnd
|
|
249
|
+
)
|
|
250
|
+
.first();
|
|
251
|
+
|
|
252
|
+
expect(duplicate?.id).toBe(id1);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should handle messages from different contacts separately', async () => {
|
|
256
|
+
const timestamp = new Date();
|
|
257
|
+
const CONTACT_2_USER_ID = encodeUserId(new Uint8Array(32).fill(3));
|
|
258
|
+
|
|
259
|
+
await testDb.discussions.add({
|
|
260
|
+
ownerUserId: DEDUP_OWNER_USER_ID,
|
|
261
|
+
contactUserId: CONTACT_2_USER_ID,
|
|
262
|
+
direction: DiscussionDirection.RECEIVED,
|
|
263
|
+
status: DiscussionStatus.ACTIVE,
|
|
264
|
+
unreadCount: 0,
|
|
265
|
+
createdAt: new Date(),
|
|
266
|
+
updatedAt: new Date(),
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
await testDb.messages.add({
|
|
270
|
+
ownerUserId: DEDUP_OWNER_USER_ID,
|
|
271
|
+
contactUserId: DEDUP_CONTACT_USER_ID,
|
|
272
|
+
content: 'Hello',
|
|
273
|
+
type: MessageType.TEXT,
|
|
274
|
+
direction: MessageDirection.INCOMING,
|
|
275
|
+
status: MessageStatus.DELIVERED,
|
|
276
|
+
timestamp,
|
|
277
|
+
seeker: new Uint8Array([1, 2, 3]),
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const windowMs = 30000;
|
|
281
|
+
const windowStart = new Date(timestamp.getTime() - windowMs);
|
|
282
|
+
const windowEnd = new Date(timestamp.getTime() + windowMs);
|
|
283
|
+
|
|
284
|
+
const duplicateFromContact2 = await testDb.messages
|
|
285
|
+
.where('[ownerUserId+contactUserId]')
|
|
286
|
+
.equals([DEDUP_OWNER_USER_ID, CONTACT_2_USER_ID])
|
|
287
|
+
.and(
|
|
288
|
+
msg =>
|
|
289
|
+
msg.direction === MessageDirection.INCOMING &&
|
|
290
|
+
msg.content === 'Hello' &&
|
|
291
|
+
msg.timestamp >= windowStart &&
|
|
292
|
+
msg.timestamp <= windowEnd
|
|
293
|
+
)
|
|
294
|
+
.first();
|
|
295
|
+
|
|
296
|
+
expect(duplicateFromContact2).toBeUndefined();
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
});
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message startup behavior tests
|
|
3
|
+
*
|
|
4
|
+
* SENDING reset on startup, messages from unknown peers, seeker stabilization.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
8
|
+
import {
|
|
9
|
+
GossipDatabase,
|
|
10
|
+
MessageStatus,
|
|
11
|
+
MessageDirection,
|
|
12
|
+
MessageType,
|
|
13
|
+
DiscussionStatus,
|
|
14
|
+
DiscussionDirection,
|
|
15
|
+
} from '../../src/db';
|
|
16
|
+
import { encodeUserId } from '../../src/utils/userId';
|
|
17
|
+
import { defaultSdkConfig } from '../../src/config/sdk';
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// SENDING reset on startup
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
const RESET_OWNER_USER_ID = encodeUserId(new Uint8Array(32).fill(1));
|
|
24
|
+
const RESET_CONTACT_USER_ID = encodeUserId(new Uint8Array(32).fill(2));
|
|
25
|
+
|
|
26
|
+
describe('SENDING Reset on Startup', () => {
|
|
27
|
+
let testDb: GossipDatabase;
|
|
28
|
+
|
|
29
|
+
beforeEach(async () => {
|
|
30
|
+
testDb = new GossipDatabase();
|
|
31
|
+
if (!testDb.isOpen()) {
|
|
32
|
+
await testDb.open();
|
|
33
|
+
}
|
|
34
|
+
await Promise.all(testDb.tables.map(table => table.clear()));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('resetStuckSendingMessages behavior', () => {
|
|
38
|
+
async function resetStuckSendingMessages(): Promise<number> {
|
|
39
|
+
return await testDb.messages
|
|
40
|
+
.where('status')
|
|
41
|
+
.equals(MessageStatus.SENDING)
|
|
42
|
+
.modify({
|
|
43
|
+
status: MessageStatus.WAITING_SESSION,
|
|
44
|
+
encryptedMessage: undefined,
|
|
45
|
+
seeker: undefined,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
it('should reset SENDING messages to WAITING_SESSION', async () => {
|
|
50
|
+
const messageId = await testDb.messages.add({
|
|
51
|
+
ownerUserId: RESET_OWNER_USER_ID,
|
|
52
|
+
contactUserId: RESET_CONTACT_USER_ID,
|
|
53
|
+
content: 'Test message',
|
|
54
|
+
type: MessageType.TEXT,
|
|
55
|
+
direction: MessageDirection.OUTGOING,
|
|
56
|
+
status: MessageStatus.SENDING,
|
|
57
|
+
timestamp: new Date(),
|
|
58
|
+
encryptedMessage: new Uint8Array([1, 2, 3]),
|
|
59
|
+
seeker: new Uint8Array([4, 5, 6]),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const count = await resetStuckSendingMessages();
|
|
63
|
+
|
|
64
|
+
expect(count).toBe(1);
|
|
65
|
+
|
|
66
|
+
const message = await testDb.messages.get(messageId);
|
|
67
|
+
expect(message?.status).toBe(MessageStatus.WAITING_SESSION);
|
|
68
|
+
expect(message?.encryptedMessage).toBeUndefined();
|
|
69
|
+
expect(message?.seeker).toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should clear encryptedMessage and seeker for re-encryption', async () => {
|
|
73
|
+
const originalEncrypted = new Uint8Array([10, 20, 30, 40]);
|
|
74
|
+
const originalSeeker = new Uint8Array([50, 60, 70, 80]);
|
|
75
|
+
|
|
76
|
+
const messageId = await testDb.messages.add({
|
|
77
|
+
ownerUserId: RESET_OWNER_USER_ID,
|
|
78
|
+
contactUserId: RESET_CONTACT_USER_ID,
|
|
79
|
+
content: 'Message with encryption data',
|
|
80
|
+
type: MessageType.TEXT,
|
|
81
|
+
direction: MessageDirection.OUTGOING,
|
|
82
|
+
status: MessageStatus.SENDING,
|
|
83
|
+
timestamp: new Date(),
|
|
84
|
+
encryptedMessage: originalEncrypted,
|
|
85
|
+
seeker: originalSeeker,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await resetStuckSendingMessages();
|
|
89
|
+
|
|
90
|
+
const message = await testDb.messages.get(messageId);
|
|
91
|
+
|
|
92
|
+
expect(message?.encryptedMessage).toBeUndefined();
|
|
93
|
+
expect(message?.seeker).toBeUndefined();
|
|
94
|
+
expect(message?.content).toBe('Message with encryption data');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should NOT affect messages in other statuses', async () => {
|
|
98
|
+
const waitingId = await testDb.messages.add({
|
|
99
|
+
ownerUserId: RESET_OWNER_USER_ID,
|
|
100
|
+
contactUserId: RESET_CONTACT_USER_ID,
|
|
101
|
+
content: 'Waiting',
|
|
102
|
+
type: MessageType.TEXT,
|
|
103
|
+
direction: MessageDirection.OUTGOING,
|
|
104
|
+
status: MessageStatus.WAITING_SESSION,
|
|
105
|
+
timestamp: new Date(),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const sentId = await testDb.messages.add({
|
|
109
|
+
ownerUserId: RESET_OWNER_USER_ID,
|
|
110
|
+
contactUserId: RESET_CONTACT_USER_ID,
|
|
111
|
+
content: 'Sent',
|
|
112
|
+
type: MessageType.TEXT,
|
|
113
|
+
direction: MessageDirection.OUTGOING,
|
|
114
|
+
status: MessageStatus.SENT,
|
|
115
|
+
timestamp: new Date(),
|
|
116
|
+
seeker: new Uint8Array([1, 2, 3]),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const deliveredId = await testDb.messages.add({
|
|
120
|
+
ownerUserId: RESET_OWNER_USER_ID,
|
|
121
|
+
contactUserId: RESET_CONTACT_USER_ID,
|
|
122
|
+
content: 'Delivered',
|
|
123
|
+
type: MessageType.TEXT,
|
|
124
|
+
direction: MessageDirection.OUTGOING,
|
|
125
|
+
status: MessageStatus.DELIVERED,
|
|
126
|
+
timestamp: new Date(),
|
|
127
|
+
seeker: new Uint8Array([4, 5, 6]),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const failedId = await testDb.messages.add({
|
|
131
|
+
ownerUserId: RESET_OWNER_USER_ID,
|
|
132
|
+
contactUserId: RESET_CONTACT_USER_ID,
|
|
133
|
+
content: 'Failed',
|
|
134
|
+
type: MessageType.TEXT,
|
|
135
|
+
direction: MessageDirection.OUTGOING,
|
|
136
|
+
status: MessageStatus.FAILED,
|
|
137
|
+
timestamp: new Date(),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const count = await resetStuckSendingMessages();
|
|
141
|
+
|
|
142
|
+
expect(count).toBe(0);
|
|
143
|
+
expect((await testDb.messages.get(waitingId))?.status).toBe(
|
|
144
|
+
MessageStatus.WAITING_SESSION
|
|
145
|
+
);
|
|
146
|
+
expect((await testDb.messages.get(sentId))?.status).toBe(
|
|
147
|
+
MessageStatus.SENT
|
|
148
|
+
);
|
|
149
|
+
expect((await testDb.messages.get(deliveredId))?.status).toBe(
|
|
150
|
+
MessageStatus.DELIVERED
|
|
151
|
+
);
|
|
152
|
+
expect((await testDb.messages.get(failedId))?.status).toBe(
|
|
153
|
+
MessageStatus.FAILED
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should reset multiple SENDING messages', async () => {
|
|
158
|
+
await testDb.messages.bulkAdd([
|
|
159
|
+
{
|
|
160
|
+
ownerUserId: RESET_OWNER_USER_ID,
|
|
161
|
+
contactUserId: RESET_CONTACT_USER_ID,
|
|
162
|
+
content: 'Message 1',
|
|
163
|
+
type: MessageType.TEXT,
|
|
164
|
+
direction: MessageDirection.OUTGOING,
|
|
165
|
+
status: MessageStatus.SENDING,
|
|
166
|
+
timestamp: new Date(),
|
|
167
|
+
encryptedMessage: new Uint8Array([1]),
|
|
168
|
+
seeker: new Uint8Array([1]),
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
ownerUserId: RESET_OWNER_USER_ID,
|
|
172
|
+
contactUserId: RESET_CONTACT_USER_ID,
|
|
173
|
+
content: 'Message 2',
|
|
174
|
+
type: MessageType.TEXT,
|
|
175
|
+
direction: MessageDirection.OUTGOING,
|
|
176
|
+
status: MessageStatus.SENDING,
|
|
177
|
+
timestamp: new Date(),
|
|
178
|
+
encryptedMessage: new Uint8Array([2]),
|
|
179
|
+
seeker: new Uint8Array([2]),
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
ownerUserId: RESET_OWNER_USER_ID,
|
|
183
|
+
contactUserId: RESET_CONTACT_USER_ID,
|
|
184
|
+
content: 'Message 3',
|
|
185
|
+
type: MessageType.TEXT,
|
|
186
|
+
direction: MessageDirection.OUTGOING,
|
|
187
|
+
status: MessageStatus.SENDING,
|
|
188
|
+
timestamp: new Date(),
|
|
189
|
+
encryptedMessage: new Uint8Array([3]),
|
|
190
|
+
seeker: new Uint8Array([3]),
|
|
191
|
+
},
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
const count = await resetStuckSendingMessages();
|
|
195
|
+
|
|
196
|
+
expect(count).toBe(3);
|
|
197
|
+
|
|
198
|
+
const messages = await testDb.messages.toArray();
|
|
199
|
+
expect(
|
|
200
|
+
messages.every(m => m.status === MessageStatus.WAITING_SESSION)
|
|
201
|
+
).toBe(true);
|
|
202
|
+
expect(messages.every(m => m.encryptedMessage === undefined)).toBe(true);
|
|
203
|
+
expect(messages.every(m => m.seeker === undefined)).toBe(true);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should handle empty database gracefully', async () => {
|
|
207
|
+
const count = await resetStuckSendingMessages();
|
|
208
|
+
expect(count).toBe(0);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ============================================================================
|
|
214
|
+
// Messages from Unknown Peer
|
|
215
|
+
// ============================================================================
|
|
216
|
+
|
|
217
|
+
const EDGE_OWNER_USER_ID = encodeUserId(new Uint8Array(32).fill(1));
|
|
218
|
+
const EDGE_CONTACT_USER_ID = encodeUserId(new Uint8Array(32).fill(2));
|
|
219
|
+
const EDGE_UNKNOWN_USER_ID = encodeUserId(new Uint8Array(32).fill(99));
|
|
220
|
+
|
|
221
|
+
describe('Messages from Unknown Peer', () => {
|
|
222
|
+
let testDb: GossipDatabase;
|
|
223
|
+
|
|
224
|
+
beforeEach(async () => {
|
|
225
|
+
testDb = new GossipDatabase();
|
|
226
|
+
await testDb.open();
|
|
227
|
+
await Promise.all(testDb.tables.map(table => table.clear()));
|
|
228
|
+
|
|
229
|
+
await testDb.discussions.add({
|
|
230
|
+
ownerUserId: EDGE_OWNER_USER_ID,
|
|
231
|
+
contactUserId: EDGE_CONTACT_USER_ID,
|
|
232
|
+
direction: DiscussionDirection.RECEIVED,
|
|
233
|
+
status: DiscussionStatus.ACTIVE,
|
|
234
|
+
unreadCount: 0,
|
|
235
|
+
createdAt: new Date(),
|
|
236
|
+
updatedAt: new Date(),
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should not have discussion for unknown peer', async () => {
|
|
241
|
+
const discussion = await testDb.getDiscussionByOwnerAndContact(
|
|
242
|
+
EDGE_OWNER_USER_ID,
|
|
243
|
+
EDGE_UNKNOWN_USER_ID
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
expect(discussion).toBeUndefined();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should have discussion for known peer', async () => {
|
|
250
|
+
const discussion = await testDb.getDiscussionByOwnerAndContact(
|
|
251
|
+
EDGE_OWNER_USER_ID,
|
|
252
|
+
EDGE_CONTACT_USER_ID
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
expect(discussion).toBeDefined();
|
|
256
|
+
expect(discussion?.contactUserId).toBe(EDGE_CONTACT_USER_ID);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// ============================================================================
|
|
261
|
+
// Seeker Stabilization Logic
|
|
262
|
+
// ============================================================================
|
|
263
|
+
|
|
264
|
+
describe('Seeker Stabilization Logic', () => {
|
|
265
|
+
it('should detect when seekers are the same (stabilized)', () => {
|
|
266
|
+
const seekers1 = new Set(['seeker1', 'seeker2', 'seeker3']);
|
|
267
|
+
const seekers2 = new Set(['seeker1', 'seeker2', 'seeker3']);
|
|
268
|
+
|
|
269
|
+
const areSame =
|
|
270
|
+
seekers1.size === seekers2.size &&
|
|
271
|
+
[...seekers1].every(s => seekers2.has(s));
|
|
272
|
+
|
|
273
|
+
expect(areSame).toBe(true);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should detect when seekers changed (not stabilized)', () => {
|
|
277
|
+
const seekers1 = new Set(['seeker1', 'seeker2']);
|
|
278
|
+
const seekers2 = new Set(['seeker1', 'seeker2', 'seeker3']);
|
|
279
|
+
|
|
280
|
+
const areSame =
|
|
281
|
+
seekers1.size === seekers2.size &&
|
|
282
|
+
[...seekers1].every(s => seekers2.has(s));
|
|
283
|
+
|
|
284
|
+
expect(areSame).toBe(false);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should detect when seekers reduced', () => {
|
|
288
|
+
const seekers1 = new Set(['seeker1', 'seeker2', 'seeker3']);
|
|
289
|
+
const seekers2 = new Set(['seeker1', 'seeker2']);
|
|
290
|
+
|
|
291
|
+
const areSame =
|
|
292
|
+
seekers1.size === seekers2.size &&
|
|
293
|
+
[...seekers1].every(s => seekers2.has(s));
|
|
294
|
+
|
|
295
|
+
expect(areSame).toBe(false);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should handle empty seeker sets', () => {
|
|
299
|
+
const seekers1 = new Set<string>();
|
|
300
|
+
const seekers2 = new Set<string>();
|
|
301
|
+
|
|
302
|
+
const areSame =
|
|
303
|
+
seekers1.size === seekers2.size &&
|
|
304
|
+
[...seekers1].every(s => seekers2.has(s));
|
|
305
|
+
|
|
306
|
+
expect(areSame).toBe(true);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should respect maxFetchIterations limit', () => {
|
|
310
|
+
const maxIterations = defaultSdkConfig.messages.maxFetchIterations;
|
|
311
|
+
let iterations = 0;
|
|
312
|
+
const seekersNeverStabilize = () => new Set([`seeker${iterations++}`]);
|
|
313
|
+
|
|
314
|
+
let previousSeekers = new Set<string>();
|
|
315
|
+
let loopCount = 0;
|
|
316
|
+
|
|
317
|
+
while (loopCount < maxIterations) {
|
|
318
|
+
const currentSeekers = seekersNeverStabilize();
|
|
319
|
+
const stabilized =
|
|
320
|
+
previousSeekers.size === currentSeekers.size &&
|
|
321
|
+
[...previousSeekers].every(s => currentSeekers.has(s));
|
|
322
|
+
|
|
323
|
+
if (stabilized) break;
|
|
324
|
+
|
|
325
|
+
previousSeekers = currentSeekers;
|
|
326
|
+
loopCount++;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
expect(loopCount).toBe(maxIterations);
|
|
330
|
+
});
|
|
331
|
+
});
|