@massalabs/gossip-sdk 0.0.2-dev.20260128094509
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,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GossipSdk lifecycle and event wiring tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { GossipDatabase } from '../../src/db';
|
|
7
|
+
import type { EncryptionKey } from '../../src/wasm/encryption';
|
|
8
|
+
|
|
9
|
+
const protocolMock = vi.hoisted(() => ({
|
|
10
|
+
createMessageProtocolMock: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const eventState = vi.hoisted(() => ({
|
|
14
|
+
lastEvents: null as {
|
|
15
|
+
onMessageReceived?: (message: unknown) => void;
|
|
16
|
+
onDiscussionRequest?: (...args: unknown[]) => void;
|
|
17
|
+
onError?: (...args: unknown[]) => void;
|
|
18
|
+
} | null,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
const sessionMock = vi.hoisted(() => {
|
|
22
|
+
const userIdBytes = new Uint8Array(32).fill(9);
|
|
23
|
+
const userIdEncoded = 'gossip1testsessionuserid';
|
|
24
|
+
const state = { lastSessionInstance: null as MockSession | null };
|
|
25
|
+
|
|
26
|
+
class MockSession {
|
|
27
|
+
userId = userIdBytes;
|
|
28
|
+
userIdEncoded = userIdEncoded;
|
|
29
|
+
ourPk = { key: 'pk' };
|
|
30
|
+
load = vi.fn();
|
|
31
|
+
cleanup = vi.fn();
|
|
32
|
+
toEncryptedBlob = vi.fn().mockReturnValue(new Uint8Array([9]));
|
|
33
|
+
private onPersist?: () => void;
|
|
34
|
+
|
|
35
|
+
constructor(_keys: unknown, onPersist?: () => void) {
|
|
36
|
+
this.onPersist = onPersist;
|
|
37
|
+
state.lastSessionInstance = this;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
emitPersist() {
|
|
41
|
+
this.onPersist?.();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { MockSession, state, userIdBytes, userIdEncoded };
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
vi.mock('../../src/api/messageProtocol', () => ({
|
|
49
|
+
createMessageProtocol: () => protocolMock.createMessageProtocolMock(),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
vi.mock('../../src/wasm/loader', () => ({
|
|
53
|
+
startWasmInitialization: vi.fn(),
|
|
54
|
+
ensureWasmInitialized: vi.fn().mockResolvedValue(undefined),
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
vi.mock('../../src/wasm/userKeys', () => ({
|
|
58
|
+
generateUserKeys: vi.fn().mockResolvedValue({}),
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
vi.mock('../../src/wasm/session', async () => {
|
|
62
|
+
return {
|
|
63
|
+
SessionModule: sessionMock.MockSession,
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
vi.mock('../../src/services/auth', () => ({
|
|
68
|
+
AuthService: class {
|
|
69
|
+
constructor() {}
|
|
70
|
+
},
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
vi.mock('../../src/services/announcement', () => ({
|
|
74
|
+
AnnouncementService: class {
|
|
75
|
+
constructor(
|
|
76
|
+
_db: unknown,
|
|
77
|
+
_protocol: unknown,
|
|
78
|
+
_session: unknown,
|
|
79
|
+
events: typeof eventState.lastEvents
|
|
80
|
+
) {
|
|
81
|
+
eventState.lastEvents = events ?? null;
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
vi.mock('../../src/services/message', () => ({
|
|
87
|
+
MessageService: class {
|
|
88
|
+
sendMessage = vi.fn();
|
|
89
|
+
fetchMessages = vi.fn();
|
|
90
|
+
resendMessages = vi.fn();
|
|
91
|
+
findMessageBySeeker = vi.fn();
|
|
92
|
+
|
|
93
|
+
constructor(
|
|
94
|
+
_db: unknown,
|
|
95
|
+
_protocol: unknown,
|
|
96
|
+
_session: unknown,
|
|
97
|
+
events: typeof eventState.lastEvents
|
|
98
|
+
) {
|
|
99
|
+
eventState.lastEvents = events ?? null;
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
vi.mock('../../src/services/discussion', () => ({
|
|
105
|
+
DiscussionService: class {
|
|
106
|
+
initialize = vi.fn();
|
|
107
|
+
accept = vi.fn();
|
|
108
|
+
renew = vi.fn();
|
|
109
|
+
isStableState = vi.fn();
|
|
110
|
+
constructor(
|
|
111
|
+
_db: unknown,
|
|
112
|
+
_announcement: unknown,
|
|
113
|
+
_session: unknown,
|
|
114
|
+
events: typeof eventState.lastEvents
|
|
115
|
+
) {
|
|
116
|
+
eventState.lastEvents = events ?? null;
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
}));
|
|
120
|
+
|
|
121
|
+
vi.mock('../../src/services/refresh', () => ({
|
|
122
|
+
RefreshService: class {
|
|
123
|
+
handleSessionRefresh = vi.fn();
|
|
124
|
+
constructor(
|
|
125
|
+
_db: unknown,
|
|
126
|
+
_message: unknown,
|
|
127
|
+
_session: unknown,
|
|
128
|
+
events: typeof eventState.lastEvents
|
|
129
|
+
) {
|
|
130
|
+
eventState.lastEvents = events ?? null;
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
}));
|
|
134
|
+
|
|
135
|
+
describe('GossipSdkImpl lifecycle', () => {
|
|
136
|
+
beforeEach(() => {
|
|
137
|
+
vi.clearAllMocks();
|
|
138
|
+
protocolMock.createMessageProtocolMock.mockReturnValue({
|
|
139
|
+
fetchMessages: vi.fn(),
|
|
140
|
+
sendMessage: vi.fn(),
|
|
141
|
+
sendAnnouncement: vi.fn(),
|
|
142
|
+
fetchAnnouncements: vi.fn(),
|
|
143
|
+
fetchPublicKeyByUserId: vi.fn(),
|
|
144
|
+
postPublicKey: vi.fn(),
|
|
145
|
+
changeNode: vi.fn(),
|
|
146
|
+
});
|
|
147
|
+
sessionMock.state.lastSessionInstance = null;
|
|
148
|
+
eventState.lastEvents = null;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('initializes once and exposes auth service', async () => {
|
|
152
|
+
const { GossipSdkImpl } = await import('../../src/gossipSdk');
|
|
153
|
+
const sdk = new GossipSdkImpl();
|
|
154
|
+
|
|
155
|
+
await sdk.init({ db: new GossipDatabase() });
|
|
156
|
+
expect(sdk.isInitialized).toBe(true);
|
|
157
|
+
expect(() => sdk.auth).not.toThrow();
|
|
158
|
+
|
|
159
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
160
|
+
await sdk.init({ db: new GossipDatabase() });
|
|
161
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('throws on openSession before init', async () => {
|
|
165
|
+
const { GossipSdkImpl } = await import('../../src/gossipSdk');
|
|
166
|
+
const sdk = new GossipSdkImpl();
|
|
167
|
+
|
|
168
|
+
await expect(sdk.openSession({ mnemonic: 'test words' })).rejects.toThrow(
|
|
169
|
+
'SDK not initialized'
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('opens and closes session with getters wired', async () => {
|
|
174
|
+
const { GossipSdkImpl } = await import('../../src/gossipSdk');
|
|
175
|
+
const sdk = new GossipSdkImpl();
|
|
176
|
+
|
|
177
|
+
await sdk.init({ db: new GossipDatabase() });
|
|
178
|
+
await sdk.openSession({ mnemonic: 'test words' });
|
|
179
|
+
|
|
180
|
+
expect(sdk.isSessionOpen).toBe(true);
|
|
181
|
+
expect(sdk.userIdBytes).toBeInstanceOf(Uint8Array);
|
|
182
|
+
expect(sdk.userIdBytes.length).toBe(32);
|
|
183
|
+
expect(sdk.publicKeys).toBeDefined();
|
|
184
|
+
|
|
185
|
+
await sdk.closeSession();
|
|
186
|
+
expect(sdk.isSessionOpen).toBe(false);
|
|
187
|
+
expect(sessionMock.state.lastSessionInstance?.cleanup).toHaveBeenCalled();
|
|
188
|
+
expect(() => sdk.messages).toThrow('No session open');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('restores encrypted session when provided', async () => {
|
|
192
|
+
const { GossipSdkImpl } = await import('../../src/gossipSdk');
|
|
193
|
+
const sdk = new GossipSdkImpl();
|
|
194
|
+
const encryptedSession = new Uint8Array([1, 2, 3]);
|
|
195
|
+
const encryptionKey = {} as EncryptionKey;
|
|
196
|
+
|
|
197
|
+
await sdk.init({ db: new GossipDatabase() });
|
|
198
|
+
await sdk.openSession({
|
|
199
|
+
mnemonic: 'test words',
|
|
200
|
+
encryptedSession,
|
|
201
|
+
encryptionKey,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(sessionMock.state.lastSessionInstance?.load).toHaveBeenCalled();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('persists session via onPersist callback', async () => {
|
|
208
|
+
const { GossipSdkImpl } = await import('../../src/gossipSdk');
|
|
209
|
+
const sdk = new GossipSdkImpl();
|
|
210
|
+
const onPersist = vi.fn().mockResolvedValue(undefined);
|
|
211
|
+
const persistEncryptionKey = {} as EncryptionKey;
|
|
212
|
+
|
|
213
|
+
await sdk.init({ db: new GossipDatabase() });
|
|
214
|
+
await sdk.openSession({
|
|
215
|
+
mnemonic: 'test words',
|
|
216
|
+
onPersist,
|
|
217
|
+
persistEncryptionKey,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
sessionMock.state.lastSessionInstance?.emitPersist();
|
|
221
|
+
expect(onPersist).toHaveBeenCalledWith(
|
|
222
|
+
new Uint8Array([9]),
|
|
223
|
+
persistEncryptionKey
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('bridges message events to sdk.on handlers', async () => {
|
|
228
|
+
const { GossipSdkImpl } = await import('../../src/gossipSdk');
|
|
229
|
+
const sdk = new GossipSdkImpl();
|
|
230
|
+
const handler = vi.fn();
|
|
231
|
+
|
|
232
|
+
await sdk.init({ db: new GossipDatabase() });
|
|
233
|
+
sdk.on('message', handler);
|
|
234
|
+
await sdk.openSession({ mnemonic: 'test words' });
|
|
235
|
+
|
|
236
|
+
eventState.lastEvents?.onMessageReceived?.({ id: 1 });
|
|
237
|
+
expect(handler).toHaveBeenCalledWith({ id: 1 });
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('GossipSdkImpl.configurePersistence', () => {
|
|
242
|
+
beforeEach(() => {
|
|
243
|
+
vi.clearAllMocks();
|
|
244
|
+
protocolMock.createMessageProtocolMock.mockReturnValue({
|
|
245
|
+
fetchMessages: vi.fn(),
|
|
246
|
+
sendMessage: vi.fn(),
|
|
247
|
+
sendAnnouncement: vi.fn(),
|
|
248
|
+
fetchAnnouncements: vi.fn(),
|
|
249
|
+
fetchPublicKeyByUserId: vi.fn(),
|
|
250
|
+
postPublicKey: vi.fn(),
|
|
251
|
+
changeNode: vi.fn(),
|
|
252
|
+
});
|
|
253
|
+
sessionMock.state.lastSessionInstance = null;
|
|
254
|
+
eventState.lastEvents = null;
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('throws if called before session is opened', async () => {
|
|
258
|
+
const { GossipSdkImpl } = await import('../../src/gossipSdk');
|
|
259
|
+
const sdk = new GossipSdkImpl();
|
|
260
|
+
const onPersist = vi.fn();
|
|
261
|
+
const encryptionKey = {} as EncryptionKey;
|
|
262
|
+
|
|
263
|
+
await sdk.init({ db: new GossipDatabase() });
|
|
264
|
+
|
|
265
|
+
expect(() => sdk.configurePersistence(encryptionKey, onPersist)).toThrow(
|
|
266
|
+
'No session open'
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('configures persistence after session is opened without initial onPersist', async () => {
|
|
271
|
+
const { GossipSdkImpl } = await import('../../src/gossipSdk');
|
|
272
|
+
const sdk = new GossipSdkImpl();
|
|
273
|
+
const onPersist = vi.fn().mockResolvedValue(undefined);
|
|
274
|
+
const encryptionKey = {} as EncryptionKey;
|
|
275
|
+
|
|
276
|
+
await sdk.init({ db: new GossipDatabase() });
|
|
277
|
+
|
|
278
|
+
await sdk.openSession({ mnemonic: 'test words' });
|
|
279
|
+
|
|
280
|
+
sdk.configurePersistence(encryptionKey, onPersist);
|
|
281
|
+
|
|
282
|
+
sessionMock.state.lastSessionInstance?.emitPersist();
|
|
283
|
+
|
|
284
|
+
expect(onPersist).toHaveBeenCalledWith(new Uint8Array([9]), encryptionKey);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('replaces existing onPersist callback when reconfigured', async () => {
|
|
288
|
+
const { GossipSdkImpl } = await import('../../src/gossipSdk');
|
|
289
|
+
const sdk = new GossipSdkImpl();
|
|
290
|
+
const originalOnPersist = vi.fn().mockResolvedValue(undefined);
|
|
291
|
+
const newOnPersist = vi.fn().mockResolvedValue(undefined);
|
|
292
|
+
const originalKey = { original: true } as unknown as EncryptionKey;
|
|
293
|
+
const newKey = { new: true } as unknown as EncryptionKey;
|
|
294
|
+
|
|
295
|
+
await sdk.init({ db: new GossipDatabase() });
|
|
296
|
+
|
|
297
|
+
await sdk.openSession({
|
|
298
|
+
mnemonic: 'test words',
|
|
299
|
+
onPersist: originalOnPersist,
|
|
300
|
+
persistEncryptionKey: originalKey,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
sdk.configurePersistence(newKey, newOnPersist);
|
|
304
|
+
|
|
305
|
+
sessionMock.state.lastSessionInstance?.emitPersist();
|
|
306
|
+
|
|
307
|
+
expect(originalOnPersist).not.toHaveBeenCalled();
|
|
308
|
+
expect(newOnPersist).toHaveBeenCalledWith(new Uint8Array([9]), newKey);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('ensures persistence is called with correct encryption key', async () => {
|
|
312
|
+
const { GossipSdkImpl } = await import('../../src/gossipSdk');
|
|
313
|
+
const sdk = new GossipSdkImpl();
|
|
314
|
+
const onPersist = vi.fn().mockResolvedValue(undefined);
|
|
315
|
+
const specificKey = { keyId: 'test-key-123' } as unknown as EncryptionKey;
|
|
316
|
+
|
|
317
|
+
await sdk.init({ db: new GossipDatabase() });
|
|
318
|
+
await sdk.openSession({ mnemonic: 'test words' });
|
|
319
|
+
|
|
320
|
+
sdk.configurePersistence(specificKey, onPersist);
|
|
321
|
+
sessionMock.state.lastSessionInstance?.emitPersist();
|
|
322
|
+
|
|
323
|
+
expect(onPersist).toHaveBeenCalledWith(expect.any(Uint8Array), specificKey);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Mocks
|
|
3
|
+
*
|
|
4
|
+
* Only MockMessageProtocol is needed - it provides an in-memory
|
|
5
|
+
* implementation to avoid network calls during tests.
|
|
6
|
+
*
|
|
7
|
+
* SessionModule uses real WASM - no mock needed since WASM works in Node.
|
|
8
|
+
*/
|
|
9
|
+
export { MockMessageProtocol } from './mockMessageProtocol';
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-Memory Mock Message Protocol for Testing
|
|
3
|
+
*
|
|
4
|
+
* This mock stores messages and announcements in memory,
|
|
5
|
+
* allowing tests to verify messaging flows without network calls.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { IMessageProtocol } from '../../src/api/messageProtocol/types';
|
|
9
|
+
import type {
|
|
10
|
+
EncryptedMessage,
|
|
11
|
+
BulletinItem,
|
|
12
|
+
} from '../../src/api/messageProtocol/types';
|
|
13
|
+
|
|
14
|
+
export class MockMessageProtocol implements IMessageProtocol {
|
|
15
|
+
private messages: Map<string, Uint8Array> = new Map();
|
|
16
|
+
private announcements: BulletinItem[] = [];
|
|
17
|
+
private announcementCounter = 0;
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
public baseUrl: string = 'mock://test',
|
|
21
|
+
public timeout: number = 10000,
|
|
22
|
+
public retryAttempts: number = 3
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
async fetchMessages(seekers: Uint8Array[]): Promise<EncryptedMessage[]> {
|
|
26
|
+
const results: EncryptedMessage[] = [];
|
|
27
|
+
for (const seeker of seekers) {
|
|
28
|
+
const key = this.uint8ArrayToKey(seeker);
|
|
29
|
+
const ciphertext = this.messages.get(key);
|
|
30
|
+
if (ciphertext) {
|
|
31
|
+
results.push({ seeker, ciphertext });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return results;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async sendMessage(message: EncryptedMessage): Promise<void> {
|
|
38
|
+
const key = this.uint8ArrayToKey(message.seeker);
|
|
39
|
+
this.messages.set(key, message.ciphertext);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async sendAnnouncement(announcement: Uint8Array): Promise<string> {
|
|
43
|
+
const counter = String(++this.announcementCounter);
|
|
44
|
+
this.announcements.push({ counter, data: announcement });
|
|
45
|
+
return counter;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async fetchAnnouncements(
|
|
49
|
+
limit?: number,
|
|
50
|
+
cursor?: string
|
|
51
|
+
): Promise<BulletinItem[]> {
|
|
52
|
+
let results = [...this.announcements];
|
|
53
|
+
|
|
54
|
+
// Filter by cursor (return only announcements after this counter)
|
|
55
|
+
if (cursor) {
|
|
56
|
+
const cursorNum = parseInt(cursor, 10);
|
|
57
|
+
results = results.filter(a => parseInt(a.counter, 10) > cursorNum);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Apply limit
|
|
61
|
+
if (limit && limit > 0) {
|
|
62
|
+
results = results.slice(0, limit);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return results;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async fetchPublicKeyByUserId(_userId: Uint8Array): Promise<string> {
|
|
69
|
+
// Return empty string - tests should handle this
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async postPublicKey(_publicKey: string): Promise<string> {
|
|
74
|
+
return 'mock-counter';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async changeNode(newBaseUrl: string): Promise<{ success: boolean }> {
|
|
78
|
+
this.baseUrl = newBaseUrl;
|
|
79
|
+
return { success: true };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Test helper methods
|
|
83
|
+
clearMockData(): void {
|
|
84
|
+
this.messages.clear();
|
|
85
|
+
this.announcements = [];
|
|
86
|
+
this.announcementCounter = 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getStoredMessages(): Map<string, Uint8Array> {
|
|
90
|
+
return new Map(this.messages);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
getStoredAnnouncements(): BulletinItem[] {
|
|
94
|
+
return [...this.announcements];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private uint8ArrayToKey(arr: Uint8Array): string {
|
|
98
|
+
return Array.from(arr).join(',');
|
|
99
|
+
}
|
|
100
|
+
}
|