Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions lib/src/remote/test-rendezvous.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, expect, it } from 'vitest';
import {
MAX_ONE_TIME_FORWARDED,
ONE_TIME_WS_ROUTES,
WS_CLOSE_ONE_TIME_EXPIRED,
WS_CLOSE_ONE_TIME_TAKEN,
WS_CLOSE_ONE_TIME_UNAVAILABLE,
WS_CLOSE_ONE_TIME_VIOLATION,
} from 'remote-lib-common';
import { createTestRendezvous } from './test-rendezvous';
import { createTestClock } from './test-timers';

// The fake must close as Hosted's `OneTimeRoom` does (`hosted/server/one-time-room.ts`;
// `docs/specs/one-time.md` -> "Hosted rendezvous"): the phone reads its copy off the code.
describe('test rendezvous parity with the one-time room', () => {
const START = 1_700_000_000_000;
async function mint() {
const clock = createTestClock(START);
const rendezvous = createTestRendezvous({ now: clock.now, setTimer: clock.setTimer });
rendezvous.createBurrowSocket(`wss://hosted.invalid${ONE_TIME_WS_ROUTES.burrow}`);
await Promise.resolve();
const room = rendezvous.room();
const join = async () => {
const socket = rendezvous.createClientSocket(rendezvous.clientUrl(room.roomId));
await Promise.resolve();
return socket;
};
return { clock, rendezvous, room, join };
}

it('refuses an unknown room 4012, a joined one 4011, and a join past expiresAt 4010', async () => {
const { clock, rendezvous, room, join } = await mint();
const unknown = rendezvous.createClientSocket(rendezvous.clientUrl('nope'));
await Promise.resolve();
expect(unknown.closeCode).toBe(WS_CLOSE_ONE_TIME_UNAVAILABLE);

clock.jump(room.expiresAt + 1 - clock.now());
expect((await join()).closeCode).toBe(WS_CLOSE_ONE_TIME_EXPIRED);

const late = await mint();
expect((await late.join()).closeCode).toBeNull();
late.clock.jump(late.room.expiresAt + 1 - late.clock.now());
expect((await late.join()).closeCode).toBe(WS_CLOSE_ONE_TIME_TAKEN);
});

it('counts a frame sent before any phone joins toward the cap', async () => {
const { room } = await mint();
for (let i = 0; i < MAX_ONE_TIME_FORWARDED; i += 1) room.burrow.send(`frame ${i}`);
expect(room.forwarded).toBe(MAX_ONE_TIME_FORWARDED);
expect(room.burrow.closeCode).toBeNull();
room.burrow.send('one past the cap');
expect(room.burrow.closeCode).toBe(WS_CLOSE_ONE_TIME_VIOLATION);
});
});
14 changes: 7 additions & 7 deletions lib/src/remote/test-rendezvous.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@
*
* - **A room per Burrow socket**, announced by exactly one `one-time-room` frame
* the moment the socket opens.
* - **One join, ever.** A second join is accepted and then closed `4011`; an
* unknown or deleted room, or one past its `expiresAt`, `4012`.
* - **One join, ever.** A refused join is accepted and then closed: an
* unknown or deleted room `4012`, then a joined one `4011`, then one past its
* `expiresAt` `4010`.
* - **Strings forwarded verbatim, never parsed**, each counted toward
* `MAX_ONE_TIME_FORWARDED` across both directions; a message with nobody on
* the other end yet is dropped uncounted.
* the other end yet is counted, then dropped.
* - **`RELAY_PING` answered with `RELAY_PONG`**, never forwarded or
* counted.
* - **A non-string, an oversize string, or one past the cap closes both
Expand Down Expand Up @@ -254,14 +255,12 @@ export function createTestRendezvous(options: TestRendezvousOptions = {}): TestR
violate(room);
return;
}
const to = from === room.burrow ? room.client : room.burrow;
if (!to) return;
room.forwarded += 1;
if (room.forwarded > MAX_ONE_TIME_FORWARDED) {
violate(room);
return;
}
to.deliver(data);
(from === room.burrow ? room.client : room.burrow)?.deliver(data);
};

const closed = (room: Room, from: RendezvousSocket): void => {
Expand Down Expand Up @@ -337,8 +336,9 @@ export function createTestRendezvous(options: TestRendezvousOptions = {}): TestR
// Decided now, so a second join made in the same turn is refused; told
// after the open, as accept-then-close does.
let refusal: number | null = null;
if (!room || room.deleted || now() > room.expiresAt) refusal = WS_CLOSE_ONE_TIME_UNAVAILABLE;
if (!room || room.deleted) refusal = WS_CLOSE_ONE_TIME_UNAVAILABLE;
else if (room.client) refusal = WS_CLOSE_ONE_TIME_TAKEN;
else if (now() > room.expiresAt) refusal = WS_CLOSE_ONE_TIME_EXPIRED;
else {
room.client = socket;
socket.onSend = (data) => forward(room, socket, data);
Expand Down
Loading