Skip to content

Commit 5abea44

Browse files
committed
Add Hocuspocus provider/server wire-protocol integration test
The existing extension specs exercise the server hooks with a mocked API but never a real client, so nothing proves the @hocuspocus/provider actually speaks the wire protocol to the server. This boots the server in-process and connects a real provider, asserting the connect/authenticate/load/sync handshake for both the current and previous provider majors — making the one-major skew the version-skew guard tolerates a standing assertion.
1 parent a2d05eb commit 5abea44

3 files changed

Lines changed: 145 additions & 2 deletions

File tree

extensions/op-blocknote-hocuspocus/package-lock.json

Lines changed: 66 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extensions/op-blocknote-hocuspocus/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,17 @@
3333
"@blocknote/core": "^0.51.3",
3434
"@eslint/js": "^9.35.0",
3535
"@eslint/json": "^1.2.0",
36+
"@hocuspocus/provider": "^4.0.0",
37+
"@hocuspocus/provider-prev": "npm:@hocuspocus/provider@^3",
3638
"@stylistic/eslint-plugin": "^5.3.1",
3739
"@types/node": "^25.0.2",
40+
"@types/ws": "^8.18.1",
3841
"eslint": "^9.35.0",
3942
"globals": "^17.3.0",
4043
"msw": "^2.12.7",
4144
"typescript": "^6.0.3",
4245
"typescript-eslint": "^8.48.1",
43-
"vitest": "^4.1.0"
46+
"vitest": "^4.1.0",
47+
"ws": "^8.18.3"
4448
}
4549
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
2+
import { Server } from "@hocuspocus/server";
3+
import { HocuspocusProvider } from "@hocuspocus/provider";
4+
import { HocuspocusProvider as HocuspocusProviderPrev } from "@hocuspocus/provider-prev";
5+
import * as Y from "yjs";
6+
import { http, passthrough } from "msw";
7+
import { WebSocket } from "ws";
8+
import { OpenProjectApi } from "../../src/extensions/openProjectApi";
9+
import { createTestToken } from "../helpers/tokenHelper";
10+
import { server as apiMock } from "../mocks/node";
11+
12+
// Proves a real @hocuspocus/provider completes the connect -> authenticate -> load ->
13+
// sync handshake against our server, exercising the actual wire protocol. The previous
14+
// provider major runs too, so the one-major skew the version-skew guard tolerates is a
15+
// standing assertion rather than a claim. The server boots in-process so the existing
16+
// msw mocks intercept its outbound Rails calls.
17+
const PORT = 9678;
18+
// Must equal the token's resource_url: onAuthenticate sets resourceUrl = documentName
19+
// and validates they match. createTestToken() defaults to this URL.
20+
const DOC_NAME = "https://test.api/api/v3/documents/1";
21+
22+
let hocuspocus: Server;
23+
24+
beforeAll(async () => {
25+
hocuspocus = new Server({ port: PORT, quiet: true, extensions: [new OpenProjectApi()] });
26+
await hocuspocus.listen();
27+
});
28+
29+
afterAll(async () => {
30+
await hocuspocus?.destroy();
31+
});
32+
33+
beforeEach(() => {
34+
// setup.ts runs msw with onUnhandledRequest:'error', which also intercepts the ws
35+
// client's HTTP upgrade. Let the upgrade reach the in-process server; Rails calls to
36+
// test.api stay mocked by the default handlers.
37+
apiMock.use(http.all(/127\.0\.0\.1:9678/, () => passthrough()));
38+
});
39+
40+
interface ProviderConfig {
41+
url: string;
42+
name: string;
43+
token: string;
44+
document: Y.Doc;
45+
// The `ws` client, not the global WebSocket: msw patches the global and would drop
46+
// the connection to our in-process server.
47+
WebSocketPolyfill: typeof WebSocket;
48+
}
49+
interface ProviderInstance {
50+
synced: boolean;
51+
destroy(): void;
52+
}
53+
type ProviderConstructor = new (config: ProviderConfig) => ProviderInstance;
54+
55+
const providerMajors: ReadonlyArray<readonly [string, ProviderConstructor]> = [
56+
["current", HocuspocusProvider as unknown as ProviderConstructor],
57+
["previous major", HocuspocusProviderPrev as unknown as ProviderConstructor],
58+
];
59+
60+
describe.each(providerMajors)("@hocuspocus/provider (%s) <-> server", (_label, Provider) => {
61+
it("connects, authenticates, and syncs", async () => {
62+
const provider = new Provider({
63+
url: `ws://127.0.0.1:${PORT}`,
64+
name: DOC_NAME,
65+
token: createTestToken(),
66+
document: new Y.Doc(),
67+
WebSocketPolyfill: WebSocket,
68+
});
69+
70+
await expect.poll(() => provider.synced, { timeout: 10000 }).toBe(true);
71+
72+
provider.destroy();
73+
});
74+
});

0 commit comments

Comments
 (0)