Skip to content

Commit 75b7a60

Browse files
committed
Test define/get
1 parent 261adcb commit 75b7a60

1 file changed

Lines changed: 262 additions & 2 deletions

File tree

src/room/participant/LocalParticipant.test.ts

Lines changed: 262 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
1-
import { PacketTrailerFeature } from '@livekit/protocol';
2-
import { describe, expect, it, vi } from 'vitest';
1+
import {
2+
DataBlob,
3+
type DataBlobKey,
4+
GetDataBlobResponse,
5+
PacketTrailerFeature,
6+
RequestResponse,
7+
RequestResponse_Reason,
8+
StoreDataBlobResponse,
9+
} from '@livekit/protocol';
10+
import { EventEmitter } from 'events';
11+
import { afterEach, describe, expect, it, vi } from 'vitest';
12+
import type { InternalRoomOptions } from '../../options';
13+
import type RTCEngine from '../RTCEngine';
14+
import {
15+
DataTrackSchemaStorageError,
16+
DataTrackSchemaStorageErrorReason,
17+
} from '../data-track/schema-storage';
18+
import { DataTrackSchemaId } from '../data-track/types';
19+
import { EngineEvent } from '../events';
320
import type LocalTrack from '../track/LocalTrack';
421
import { Track } from '../track/Track';
522
import type { TrackPublishOptions } from '../track/options';
@@ -79,3 +96,246 @@ describe('LocalParticipant frame metadata publish options', () => {
7996
expect(participant.log.warn).toHaveBeenCalledOnce();
8097
});
8198
});
99+
100+
describe('LocalParticipant schema storage', () => {
101+
const schemaId: DataTrackSchemaId = { name: 'rgb', encoding: 'jsonSchema' };
102+
103+
type MockEngine = RTCEngine & {
104+
client: {
105+
sendStoreDataBlobRequest: ReturnType<typeof vi.fn>;
106+
sendGetDataBlobRequest: ReturnType<typeof vi.fn>;
107+
};
108+
};
109+
110+
function makeEngine(requestId: number): MockEngine {
111+
const engine = new EventEmitter() as unknown as MockEngine;
112+
engine.client = {
113+
sendStoreDataBlobRequest: vi.fn(async () => requestId),
114+
sendGetDataBlobRequest: vi.fn(async () => requestId),
115+
};
116+
return engine;
117+
}
118+
119+
function createParticipant(engine: MockEngine) {
120+
return new LocalParticipant(
121+
'participant-sid',
122+
'test-identity',
123+
engine,
124+
{} as InternalRoomOptions,
125+
undefined as any,
126+
undefined as any,
127+
undefined as any,
128+
undefined as any,
129+
);
130+
}
131+
132+
/** Waits for a request to be sent and its pending future to be registered. */
133+
function flush() {
134+
return new Promise<void>((resolve) => setTimeout(resolve, 0));
135+
}
136+
137+
function pendingRequests(participant: LocalParticipant) {
138+
return participant as unknown as {
139+
pendingStoreDataBlobRequests: Map<number, unknown>;
140+
pendingGetDataBlobRequests: Map<number, unknown>;
141+
};
142+
}
143+
144+
afterEach(() => {
145+
vi.useRealTimers();
146+
});
147+
148+
it('defines a schema by storing its definition as a data blob', async () => {
149+
const engine = makeEngine(7);
150+
const participant = createParticipant(engine);
151+
152+
const promise = participant.defineSchema(schemaId, '{"type":"object"}');
153+
await flush();
154+
155+
expect(engine.client.sendStoreDataBlobRequest).toHaveBeenCalledOnce();
156+
const blob = engine.client.sendStoreDataBlobRequest.mock.calls[0][0] as DataBlob;
157+
expect(blob.key?.key.case).toStrictEqual('schemaId');
158+
expect(DataTrackSchemaId.from(blob.key!.key.value! as never)).toStrictEqual(schemaId);
159+
expect(new TextDecoder().decode(blob.contents)).toStrictEqual('{"type":"object"}');
160+
161+
engine.emit(EngineEvent.StoreDataBlobResponse, new StoreDataBlobResponse({ requestId: 7 }));
162+
await expect(promise).resolves.toBeUndefined();
163+
expect(pendingRequests(participant).pendingStoreDataBlobRequests.size).toBe(0);
164+
});
165+
166+
it('ignores a store response with a mismatched request id', async () => {
167+
const engine = makeEngine(7);
168+
const participant = createParticipant(engine);
169+
170+
const promise = participant.defineSchema(schemaId, 'definition');
171+
await flush();
172+
173+
engine.emit(EngineEvent.StoreDataBlobResponse, new StoreDataBlobResponse({ requestId: 8 }));
174+
await flush();
175+
expect(pendingRequests(participant).pendingStoreDataBlobRequests.size).toBe(1);
176+
177+
engine.emit(EngineEvent.StoreDataBlobResponse, new StoreDataBlobResponse({ requestId: 7 }));
178+
await expect(promise).resolves.toBeUndefined();
179+
});
180+
181+
it('rejects defining a schema when the server reports an error', async () => {
182+
const engine = makeEngine(7);
183+
const participant = createParticipant(engine);
184+
185+
const promise = participant.defineSchema(schemaId, 'definition');
186+
await flush();
187+
188+
engine.emit(
189+
EngineEvent.SignalRequestResponse,
190+
new RequestResponse({
191+
requestId: 7,
192+
reason: RequestResponse_Reason.INVALID_REQUEST,
193+
message: 'schema already defined',
194+
}),
195+
);
196+
await expect(promise).rejects.toStrictEqual(
197+
DataTrackSchemaStorageError.requestFailed(
198+
RequestResponse_Reason.INVALID_REQUEST,
199+
'schema already defined',
200+
),
201+
);
202+
expect(pendingRequests(participant).pendingStoreDataBlobRequests.size).toBe(0);
203+
});
204+
205+
it('ignores an OK request response for a pending blob request', async () => {
206+
const engine = makeEngine(7);
207+
const participant = createParticipant(engine);
208+
209+
const promise = participant.defineSchema(schemaId, 'definition');
210+
await flush();
211+
212+
engine.emit(
213+
EngineEvent.SignalRequestResponse,
214+
new RequestResponse({ requestId: 7, reason: RequestResponse_Reason.OK }),
215+
);
216+
await flush();
217+
expect(pendingRequests(participant).pendingStoreDataBlobRequests.size).toBe(1);
218+
219+
engine.emit(EngineEvent.StoreDataBlobResponse, new StoreDataBlobResponse({ requestId: 7 }));
220+
await expect(promise).resolves.toBeUndefined();
221+
});
222+
223+
it('retrieves a schema definition', async () => {
224+
const engine = makeEngine(3);
225+
const participant = createParticipant(engine);
226+
227+
const promise = participant.getSchema(schemaId, 'publisher-identity');
228+
await flush();
229+
230+
expect(engine.client.sendGetDataBlobRequest).toHaveBeenCalledOnce();
231+
const [key, identity] = engine.client.sendGetDataBlobRequest.mock.calls[0] as [
232+
DataBlobKey,
233+
string,
234+
];
235+
expect(key.key.case).toStrictEqual('schemaId');
236+
expect(identity).toStrictEqual('publisher-identity');
237+
238+
engine.emit(
239+
EngineEvent.GetDataBlobResponse,
240+
new GetDataBlobResponse({
241+
requestId: 3,
242+
blob: new DataBlob({ contents: new TextEncoder().encode('{"type":"object"}') }),
243+
}),
244+
);
245+
await expect(promise).resolves.toStrictEqual('{"type":"object"}');
246+
expect(pendingRequests(participant).pendingGetDataBlobRequests.size).toBe(0);
247+
});
248+
249+
it('rejects retrieving an undefined schema', async () => {
250+
const engine = makeEngine(3);
251+
const participant = createParticipant(engine);
252+
253+
const promise = participant.getSchema(schemaId, 'publisher-identity');
254+
await flush();
255+
256+
engine.emit(
257+
EngineEvent.SignalRequestResponse,
258+
new RequestResponse({
259+
requestId: 3,
260+
reason: RequestResponse_Reason.NOT_FOUND,
261+
message: 'blob not found',
262+
}),
263+
);
264+
await expect(promise).rejects.toStrictEqual(
265+
DataTrackSchemaStorageError.notFound('blob not found'),
266+
);
267+
expect(pendingRequests(participant).pendingGetDataBlobRequests.size).toBe(0);
268+
});
269+
270+
it('rejects a malformed get response missing the blob', async () => {
271+
const engine = makeEngine(3);
272+
const participant = createParticipant(engine);
273+
274+
const promise = participant.getSchema(schemaId, 'publisher-identity');
275+
await flush();
276+
277+
engine.emit(EngineEvent.GetDataBlobResponse, new GetDataBlobResponse({ requestId: 3 }));
278+
await expect(promise).rejects.toStrictEqual(DataTrackSchemaStorageError.malformedResponse());
279+
});
280+
281+
it('rejects a schema definition that is not valid UTF-8', async () => {
282+
const engine = makeEngine(3);
283+
const participant = createParticipant(engine);
284+
285+
const promise = participant.getSchema(schemaId, 'publisher-identity');
286+
await flush();
287+
288+
engine.emit(
289+
EngineEvent.GetDataBlobResponse,
290+
new GetDataBlobResponse({
291+
requestId: 3,
292+
blob: new DataBlob({ contents: new Uint8Array([0xff, 0xfe, 0xfd]) }),
293+
}),
294+
);
295+
await expect(promise).rejects.toMatchObject({
296+
reason: DataTrackSchemaStorageErrorReason.InvalidDefinition,
297+
});
298+
});
299+
300+
it('rejects when the request times out', async () => {
301+
vi.useFakeTimers();
302+
const engine = makeEngine(7);
303+
const participant = createParticipant(engine);
304+
305+
const promise = participant.defineSchema(schemaId, 'definition');
306+
const expectation = expect(promise).rejects.toStrictEqual(
307+
DataTrackSchemaStorageError.timeout(),
308+
);
309+
await vi.advanceTimersByTimeAsync(0);
310+
expect(engine.client.sendStoreDataBlobRequest).toHaveBeenCalledOnce();
311+
312+
await vi.advanceTimersByTimeAsync(5_000);
313+
await expectation;
314+
expect(pendingRequests(participant).pendingStoreDataBlobRequests.size).toBe(0);
315+
});
316+
317+
it('rejects when the caller aborts the request', async () => {
318+
const engine = makeEngine(3);
319+
const participant = createParticipant(engine);
320+
const controller = new AbortController();
321+
322+
const promise = participant.getSchema(schemaId, 'publisher-identity', controller.signal);
323+
await flush();
324+
325+
controller.abort();
326+
await expect(promise).rejects.toStrictEqual(DataTrackSchemaStorageError.cancelled());
327+
expect(pendingRequests(participant).pendingGetDataBlobRequests.size).toBe(0);
328+
});
329+
330+
it('rejects pending requests when the engine closes', async () => {
331+
const engine = makeEngine(7);
332+
const participant = createParticipant(engine);
333+
334+
const storePromise = participant.defineSchema(schemaId, 'definition');
335+
await flush();
336+
337+
engine.emit(EngineEvent.Closing);
338+
await expect(storePromise).rejects.toStrictEqual(DataTrackSchemaStorageError.disconnected());
339+
expect(pendingRequests(participant).pendingStoreDataBlobRequests.size).toBe(0);
340+
});
341+
});

0 commit comments

Comments
 (0)