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:
333
tests/server/onAuthenticate.ts
Normal file
333
tests/server/onAuthenticate.ts
Normal file
@@ -0,0 +1,333 @@
|
||||
import { WebSocketStatus } from "@hocuspocus/provider";
|
||||
import type {
|
||||
onAuthenticatePayload,
|
||||
onLoadDocumentPayload,
|
||||
} from "@hocuspocus/server";
|
||||
import test from "ava";
|
||||
import {
|
||||
newHocuspocus,
|
||||
newHocuspocusProvider,
|
||||
newHocuspocusProviderWebsocket,
|
||||
sleep,
|
||||
} from "../utils/index.ts";
|
||||
import { retryableAssertion } from "../utils/retryableAssertion.ts";
|
||||
|
||||
test("executes the onAuthenticate callback", async (t) => {
|
||||
await new Promise(async (resolve) => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate() {
|
||||
t.pass();
|
||||
resolve("done");
|
||||
},
|
||||
});
|
||||
|
||||
newHocuspocusProvider(t, server, {
|
||||
token: "SUPER-SECRET-TOKEN",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("executes the onAuthenticate callback from a custom extension", async (t) => {
|
||||
await new Promise(async (resolve) => {
|
||||
class CustomExtension {
|
||||
async onAuthenticate() {
|
||||
t.pass();
|
||||
resolve("done");
|
||||
}
|
||||
}
|
||||
|
||||
const server = await newHocuspocus(t, {
|
||||
extensions: [new CustomExtension()],
|
||||
});
|
||||
|
||||
newHocuspocusProvider(t, server, {
|
||||
token: "SUPER-SECRET-TOKEN",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("confirms the `Token` message with an `Authenticated` message", async (t) => {
|
||||
await new Promise(async (resolve) => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate() {
|
||||
// success
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
newHocuspocusProvider(t, server, {
|
||||
token: "SUPER-SECRET-TOKEN",
|
||||
onAuthenticated() {
|
||||
t.pass();
|
||||
resolve("done");
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("replies with a `PermissionDenied` message when authentication fails", async (t) => {
|
||||
await new Promise(async (resolve) => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate() {
|
||||
// fail
|
||||
throw Error();
|
||||
},
|
||||
});
|
||||
|
||||
newHocuspocusProvider(t, server, {
|
||||
token: "SUPER-SECRET-TOKEN",
|
||||
onAuthenticationFailed() {
|
||||
t.pass();
|
||||
resolve("done");
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("passes context from onAuthenticate to onLoadDocument", async (t) => {
|
||||
await new Promise(async (resolve) => {
|
||||
const mockContext = {
|
||||
user: 123,
|
||||
};
|
||||
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate() {
|
||||
return mockContext;
|
||||
},
|
||||
async onLoadDocument({ context }: onLoadDocumentPayload) {
|
||||
t.deepEqual(context, mockContext);
|
||||
|
||||
resolve("done");
|
||||
},
|
||||
});
|
||||
|
||||
newHocuspocusProvider(t, server, {
|
||||
token: "SUPER-SECRET-TOKEN",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("ignores the authentication token when having no onAuthenticate hook", async (t) => {
|
||||
await new Promise(async (resolve) => {
|
||||
const server = await newHocuspocus(t);
|
||||
|
||||
newHocuspocusProvider(t, server, {
|
||||
token: "SUPER-SECRET-TOKEN",
|
||||
onOpen() {
|
||||
t.pass();
|
||||
resolve("done");
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("has the authentication token", async (t) => {
|
||||
await new Promise(async (resolve) => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate({ token }: onAuthenticatePayload) {
|
||||
t.is(token, "SUPER-SECRET-TOKEN");
|
||||
|
||||
resolve("done");
|
||||
},
|
||||
});
|
||||
|
||||
newHocuspocusProvider(t, server, {
|
||||
token: "SUPER-SECRET-TOKEN",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("does not disconnect provider when the onAuthenticate hook throws an Error", async (t) => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate() {
|
||||
throw new Error();
|
||||
},
|
||||
// MUST NOT BE CALLED
|
||||
async onLoadDocument() {
|
||||
t.fail(
|
||||
"WARNING: When onAuthenticate fails onLoadDocument must not be called.",
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
onClose() {
|
||||
t.fail();
|
||||
},
|
||||
token: "SUPER-SECRET-TOKEN",
|
||||
});
|
||||
|
||||
await retryableAssertion(t, (tt) => {
|
||||
tt.is(
|
||||
provider.configuration.websocketProvider.status,
|
||||
WebSocketStatus.Connected,
|
||||
);
|
||||
tt.is(server.getDocumentsCount(), 0);
|
||||
tt.is(server.getConnectionsCount(), 0);
|
||||
});
|
||||
});
|
||||
|
||||
test("connects with the correct token", async (t) => {
|
||||
await new Promise(async (resolve) => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate({ token }: onAuthenticatePayload) {
|
||||
if (token !== "SUPER-SECRET-TOKEN") {
|
||||
throw new Error();
|
||||
}
|
||||
},
|
||||
async onLoadDocument() {
|
||||
t.pass();
|
||||
resolve("done");
|
||||
},
|
||||
});
|
||||
|
||||
newHocuspocusProvider(t, server, {
|
||||
token: "SUPER-SECRET-TOKEN",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("onAuthenticate has access to document name", async (t) => {
|
||||
const docName = "superSecretDoc";
|
||||
const requiredToken = "SUPER-SECRET-TOKEN";
|
||||
|
||||
await new Promise(async (resolve) => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate({ token, documentName }: onAuthenticatePayload) {
|
||||
if (documentName !== docName) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
if (token !== requiredToken) {
|
||||
throw new Error();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
newHocuspocusProvider(t, server, {
|
||||
token: requiredToken,
|
||||
name: docName,
|
||||
onAuthenticated() {
|
||||
t.pass();
|
||||
resolve("done");
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("onAuthenticate wrong auth only disconnects affected doc (when multiplexing)", async (t) => {
|
||||
const docName = "superSecretDoc";
|
||||
const requiredToken = "SUPER-SECRET-TOKEN";
|
||||
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate({ token, documentName }: onAuthenticatePayload) {
|
||||
if (documentName !== docName) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
if (token !== requiredToken) {
|
||||
throw new Error();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const socket = newHocuspocusProviderWebsocket(t, server);
|
||||
|
||||
const providerFail = newHocuspocusProvider(t, server, {
|
||||
websocketProvider: socket,
|
||||
token: "wrongToken",
|
||||
name: "otherDocu",
|
||||
onAuthenticated() {
|
||||
t.fail();
|
||||
},
|
||||
});
|
||||
|
||||
await sleep(100);
|
||||
|
||||
const providerOK = newHocuspocusProvider(t, server, {
|
||||
websocketProvider: socket,
|
||||
token: requiredToken,
|
||||
name: docName,
|
||||
onAuthenticationFailed() {
|
||||
t.fail();
|
||||
},
|
||||
});
|
||||
|
||||
await retryableAssertion(t, (tt) => {
|
||||
tt.is(socket.status, WebSocketStatus.Connected);
|
||||
tt.is(server.getDocumentsCount(), 1);
|
||||
tt.is(server.getConnectionsCount(), 1);
|
||||
});
|
||||
});
|
||||
|
||||
test("onAuthenticate readonly auth only affects 1 doc (when multiplexing)", async (t) => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate({
|
||||
token,
|
||||
documentName,
|
||||
connectionConfig,
|
||||
}: onAuthenticatePayload) {
|
||||
if (token === "readonly") {
|
||||
connectionConfig.readOnly = true;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const socket = newHocuspocusProviderWebsocket(t, server);
|
||||
|
||||
const providerReadOnly = newHocuspocusProvider(t, server, {
|
||||
websocketProvider: socket,
|
||||
token: "readonly",
|
||||
name: "doc1",
|
||||
onAuthenticationFailed() {
|
||||
t.fail();
|
||||
},
|
||||
});
|
||||
|
||||
const providerOK = newHocuspocusProvider(t, server, {
|
||||
websocketProvider: socket,
|
||||
token: "read+write",
|
||||
name: "doc2",
|
||||
onAuthenticationFailed() {
|
||||
t.fail();
|
||||
},
|
||||
});
|
||||
|
||||
await retryableAssertion(t, (tt) => {
|
||||
tt.is(socket.status, WebSocketStatus.Connected);
|
||||
tt.is(socket.status, WebSocketStatus.Connected);
|
||||
tt.is(server.getDocumentsCount(), 2);
|
||||
tt.is(server.getConnectionsCount(), 1);
|
||||
tt.is(socket.status, WebSocketStatus.Connected);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
t.is(
|
||||
server.documents.get("doc1")!.connections.keys().next().value!
|
||||
.readOnly,
|
||||
true,
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
t.is(
|
||||
server.documents.get("doc2")!.connections.keys().next().value!
|
||||
.readOnly,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("onAuthenticate is called even if no token is provided", async (t) => {
|
||||
const docName = "superSecretDoc";
|
||||
|
||||
await new Promise(async (resolve) => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate({ documentName }: onAuthenticatePayload) {
|
||||
t.pass();
|
||||
resolve("done");
|
||||
},
|
||||
});
|
||||
|
||||
newHocuspocusProvider(t, server, {
|
||||
name: docName,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user