snapshot of https://github.com/ueberdosis/hocuspocus.git @ cf3cfa355b4d3bb61c6b6c80071f71783e32470e (2026-09-15)
Some checks failed
build / lint (22) (push) Failing after 17s
build / test (22) (push) Failing after 8s
build / lint (23) (push) Failing after 2s
build / build (22) (push) Has been skipped
build / build (23) (push) Has been skipped
build / test (23) (push) Failing after 4s
docsearch / docsearch (push) Failing after 1s
Some checks failed
build / lint (22) (push) Failing after 17s
build / test (22) (push) Failing after 8s
build / lint (23) (push) Failing after 2s
build / build (22) (push) Has been skipped
build / build (23) (push) Has been skipped
build / test (23) (push) Failing after 4s
docsearch / docsearch (push) Failing after 1s
This commit is contained in:
329
tests/server/sessionAwareness.ts
Normal file
329
tests/server/sessionAwareness.ts
Normal file
@@ -0,0 +1,329 @@
|
||||
import { parseRoutingKey } from "@hocuspocus/common";
|
||||
import type { onAuthenticatePayload, connectedPayload } from "@hocuspocus/server";
|
||||
import test from "ava";
|
||||
import { readVarString, createDecoder } from "lib0/decoding";
|
||||
import {
|
||||
newHocuspocus,
|
||||
newHocuspocusProvider,
|
||||
newHocuspocusProviderWebsocket,
|
||||
} from "../utils/index.ts";
|
||||
import { retryableAssertion } from "../utils/retryableAssertion.ts";
|
||||
|
||||
test("sessionAwareness: two providers with same doc name both connect successfully", async (t) => {
|
||||
await new Promise(async (resolve) => {
|
||||
let connectedCount = 0;
|
||||
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate() {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
const socket = newHocuspocusProviderWebsocket(t, server);
|
||||
|
||||
const provider1 = newHocuspocusProvider(
|
||||
t,
|
||||
server,
|
||||
{
|
||||
websocketProvider: socket,
|
||||
token: "token-1",
|
||||
name: "shared-doc",
|
||||
sessionAwareness: true,
|
||||
onAuthenticated() {
|
||||
connectedCount++;
|
||||
if (connectedCount === 2) {
|
||||
resolve("done");
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const provider2 = newHocuspocusProvider(
|
||||
t,
|
||||
server,
|
||||
{
|
||||
websocketProvider: socket,
|
||||
token: "token-2",
|
||||
name: "shared-doc",
|
||||
sessionAwareness: true,
|
||||
onAuthenticated() {
|
||||
connectedCount++;
|
||||
if (connectedCount === 2) {
|
||||
resolve("done");
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
t.truthy(provider1);
|
||||
t.truthy(provider2);
|
||||
});
|
||||
});
|
||||
|
||||
test("sessionAwareness: auth failure isolation - provider A fails, provider B succeeds", async (t) => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate({ token }: onAuthenticatePayload) {
|
||||
if (token === "bad-token") {
|
||||
throw new Error("unauthorized");
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
const socket = newHocuspocusProviderWebsocket(t, server);
|
||||
|
||||
const providerFail = newHocuspocusProvider(
|
||||
t,
|
||||
server,
|
||||
{
|
||||
websocketProvider: socket,
|
||||
token: "bad-token",
|
||||
name: "shared-doc",
|
||||
sessionAwareness: true,
|
||||
onAuthenticated() {
|
||||
t.fail("providerFail should not authenticate");
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const providerOK = newHocuspocusProvider(
|
||||
t,
|
||||
server,
|
||||
{
|
||||
websocketProvider: socket,
|
||||
token: "good-token",
|
||||
name: "shared-doc",
|
||||
sessionAwareness: true,
|
||||
onAuthenticationFailed() {
|
||||
t.fail("providerOK should not fail auth");
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await retryableAssertion(t, (tt) => {
|
||||
tt.is(providerFail.isAuthenticated, false);
|
||||
tt.is(providerOK.isAuthenticated, true);
|
||||
tt.is(server.getDocumentsCount(), 1);
|
||||
});
|
||||
});
|
||||
|
||||
test("sessionAwareness: false - two providers with same name on same socket throws when first is authenticated", async (t) => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate() {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
const socket = newHocuspocusProviderWebsocket(t, server);
|
||||
|
||||
const provider1 = newHocuspocusProvider(t, server, {
|
||||
websocketProvider: socket,
|
||||
token: "token",
|
||||
name: "same-doc",
|
||||
sessionAwareness: false,
|
||||
});
|
||||
|
||||
await retryableAssertion(t, (tt) => {
|
||||
tt.is(provider1.isAuthenticated, true);
|
||||
});
|
||||
|
||||
// Now that provider1 is authenticated, attaching a second with the same name should throw
|
||||
t.throws(() => {
|
||||
newHocuspocusProvider(t, server, {
|
||||
websocketProvider: socket,
|
||||
token: "token",
|
||||
name: "same-doc",
|
||||
sessionAwareness: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("sessionAwareness: connection has correct sessionId", async (t) => {
|
||||
await new Promise(async (resolve) => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate() {
|
||||
return true;
|
||||
},
|
||||
async connected({ connection }: connectedPayload) {
|
||||
t.truthy(connection.sessionId);
|
||||
t.is(typeof connection.sessionId, "string");
|
||||
t.truthy(connection.sessionId!.length > 0);
|
||||
resolve("done");
|
||||
},
|
||||
});
|
||||
|
||||
const socket = newHocuspocusProviderWebsocket(t, server);
|
||||
|
||||
newHocuspocusProvider(t, server, {
|
||||
websocketProvider: socket,
|
||||
token: "test-token",
|
||||
name: "session-doc",
|
||||
sessionAwareness: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("sessionAwareness: connection has correct providerVersion", async (t) => {
|
||||
await new Promise(async (resolve) => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate() {
|
||||
return true;
|
||||
},
|
||||
async connected({ connection }: connectedPayload) {
|
||||
t.is(typeof connection.providerVersion, "string");
|
||||
t.truthy(connection.providerVersion!.length > 0);
|
||||
resolve("done");
|
||||
},
|
||||
});
|
||||
|
||||
const socket = newHocuspocusProviderWebsocket(t, server);
|
||||
|
||||
newHocuspocusProvider(t, server, {
|
||||
websocketProvider: socket,
|
||||
token: "test-token",
|
||||
name: "session-doc",
|
||||
sessionAwareness: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("sessionAwareness: all outgoing messages from provider include sessionId in routing key", async (t) => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate() {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
const socket = newHocuspocusProviderWebsocket(t, server);
|
||||
|
||||
const sentMessages: Uint8Array[] = [];
|
||||
const originalSend = socket.webSocket!.send.bind(socket.webSocket!);
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
websocketProvider: socket,
|
||||
token: "test-token",
|
||||
name: "session-doc",
|
||||
sessionAwareness: true,
|
||||
});
|
||||
|
||||
await retryableAssertion(t, (tt) => {
|
||||
tt.is(provider.isSynced, true);
|
||||
});
|
||||
|
||||
// Monkey-patch send to capture outgoing messages
|
||||
socket.webSocket!.send = (data: any) => {
|
||||
if (data instanceof Uint8Array || data instanceof ArrayBuffer) {
|
||||
sentMessages.push(new Uint8Array(data));
|
||||
}
|
||||
originalSend(data);
|
||||
};
|
||||
|
||||
// Trigger a document update which causes the provider to send an UpdateMessage
|
||||
provider.document.getMap("test").set("key", "value");
|
||||
|
||||
await retryableAssertion(t, (tt) => {
|
||||
tt.true(sentMessages.length > 0, "should have captured outgoing messages");
|
||||
|
||||
for (const msg of sentMessages) {
|
||||
const decoder = createDecoder(msg);
|
||||
const routingKey = readVarString(decoder);
|
||||
const { documentName, sessionId } = parseRoutingKey(routingKey);
|
||||
tt.is(documentName, "session-doc", `message routing key should contain correct document name, got: ${routingKey}`);
|
||||
tt.truthy(sessionId, `every outgoing message should include a sessionId in the routing key, got: ${routingKey}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("sessionAwareness: SyncStep2 reply includes sessionId in routing key", async (t) => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate() {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
const socket = newHocuspocusProviderWebsocket(t, server);
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
websocketProvider: socket,
|
||||
token: "test-token",
|
||||
name: "sync-reply-doc",
|
||||
sessionAwareness: true,
|
||||
});
|
||||
|
||||
await retryableAssertion(t, (tt) => {
|
||||
tt.is(provider.isSynced, true);
|
||||
});
|
||||
|
||||
// Capture all outgoing messages
|
||||
const sentMessages: Uint8Array[] = [];
|
||||
const originalSend = socket.webSocket!.send.bind(socket.webSocket!);
|
||||
socket.webSocket!.send = (data: any) => {
|
||||
if (data instanceof Uint8Array || data instanceof ArrayBuffer) {
|
||||
sentMessages.push(new Uint8Array(data));
|
||||
}
|
||||
originalSend(data);
|
||||
};
|
||||
|
||||
// Force a sync which triggers SyncStep1 -> server replies with SyncStep1 -> provider replies with SyncStep2
|
||||
provider.forceSync();
|
||||
|
||||
await retryableAssertion(t, (tt) => {
|
||||
// We expect at least the SyncStep1 from forceSync plus the SyncStep2 reply
|
||||
tt.true(sentMessages.length >= 2, `should have at least 2 messages, got ${sentMessages.length}`);
|
||||
|
||||
for (const msg of sentMessages) {
|
||||
const decoder = createDecoder(msg);
|
||||
const routingKey = readVarString(decoder);
|
||||
const { sessionId } = parseRoutingKey(routingKey);
|
||||
tt.truthy(sessionId, `message should include sessionId, got routing key: ${routingKey}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("sessionAwareness: providers with different doc names still work without sessionAwareness", async (t) => {
|
||||
await new Promise(async (resolve) => {
|
||||
let connectedCount = 0;
|
||||
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate() {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
const socket = newHocuspocusProviderWebsocket(t, server);
|
||||
|
||||
newHocuspocusProvider(
|
||||
t,
|
||||
server,
|
||||
{
|
||||
websocketProvider: socket,
|
||||
token: "token-1",
|
||||
name: "doc-1",
|
||||
onAuthenticated() {
|
||||
connectedCount++;
|
||||
if (connectedCount === 2) {
|
||||
resolve("done");
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
newHocuspocusProvider(
|
||||
t,
|
||||
server,
|
||||
{
|
||||
websocketProvider: socket,
|
||||
token: "token-2",
|
||||
name: "doc-2",
|
||||
onAuthenticated() {
|
||||
connectedCount++;
|
||||
if (connectedCount === 2) {
|
||||
resolve("done");
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
t.pass();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user