mirror of
https://github.com/sinanyuntu/trade-message-center.git
synced 2026-09-17 13:22:11 +08:00
764 lines
25 KiB
TypeScript
764 lines
25 KiB
TypeScript
// 验证 OneTalk WebSocket 握手和授权边界
|
|
|
|
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
ONETALK_ERROR_CODES,
|
|
ONETALK_PROTOCOL_VERSION,
|
|
createMockAuthorizationReader,
|
|
type MockAuthorizationRecord,
|
|
type OneTalkAuthorizationRequest,
|
|
type OneTalkMindScope,
|
|
type OneTalkPluginScope,
|
|
type OneTalkScope,
|
|
} from "@trade-message-center/onetalk-contract";
|
|
import Fastify from "fastify";
|
|
|
|
import { createApp } from "../src/app.ts";
|
|
import type { DatabaseConnection } from "../src/database/index.ts";
|
|
import type { OneTalkProfileService, OneTalkService } from "../src/onetalk/index.ts";
|
|
import { createMindAuthorizationReader } from "../src/mind-authorization.ts";
|
|
import { installWebsocket } from "../src/websocket/index.ts";
|
|
import { createOneTalkCutoverPolicy } from "../src/cutover-policy.ts";
|
|
|
|
const testConfig = {
|
|
host: "127.0.0.1",
|
|
port: 3000,
|
|
databaseUrl: "postgres://test:test@localhost:5432/test",
|
|
environment: "non_development" as const,
|
|
mindAuthorization: {
|
|
baseUrl: "https://mind.example.com",
|
|
mindPageOrigin: "http://mind.localhost",
|
|
pluginOrigins: ["http://plugin.localhost"],
|
|
cutoverPolicy: createOneTalkCutoverPolicy(),
|
|
timeoutMs: 100,
|
|
},
|
|
};
|
|
|
|
const bindingScope: OneTalkPluginScope = {
|
|
channelAccountId: "onetalk-account-1",
|
|
deviceId: "device-1",
|
|
};
|
|
|
|
const developmentConfig = {
|
|
...testConfig,
|
|
environment: "development" as const,
|
|
};
|
|
|
|
const mindScope: OneTalkMindScope = {
|
|
mindUserId: "mind-user-1",
|
|
workspaceId: "workspace-1",
|
|
channelAccountId: bindingScope.channelAccountId,
|
|
};
|
|
|
|
const authorizationRecord: MockAuthorizationRecord = {
|
|
scope: bindingScope,
|
|
mindScope,
|
|
binding: "binding-1",
|
|
permissions: ["read", "send"],
|
|
authorizationVersion: "version-1",
|
|
active: true,
|
|
};
|
|
|
|
const createDatabaseStub = (): DatabaseConnection => {
|
|
return {
|
|
db: {} as DatabaseConnection["db"],
|
|
close: async () => {},
|
|
};
|
|
};
|
|
|
|
const createServiceStub = (): OneTalkService => {
|
|
return {
|
|
discoverConversation: async (_context, conversationId, _commitGuard, conversationKind) => ({
|
|
channelAccountId: bindingScope.channelAccountId,
|
|
conversationId,
|
|
conversationKind,
|
|
syncPhase: "initial",
|
|
syncResult: "incomplete",
|
|
latestMessageId: null,
|
|
historyComplete: false,
|
|
messageCount: 0,
|
|
}),
|
|
listAnchors: async () => [],
|
|
listConversations: async () => [],
|
|
readConversation: async () => null,
|
|
readHistory: async () => null,
|
|
observeMessage: async () => ({
|
|
status: "anomaly" as const,
|
|
anomalyCode: "test_only",
|
|
}),
|
|
completeSync: async () => ({
|
|
status: "rejected" as const,
|
|
reason: "conversation_not_discovered" as const,
|
|
}),
|
|
};
|
|
};
|
|
|
|
const createProfileServiceStub = (): OneTalkProfileService => ({
|
|
ingestProfiles: async (input) => ({
|
|
status: "accepted",
|
|
inputProfileCount: input.profiles.length,
|
|
profileCount: input.profiles.length,
|
|
writtenProfileCount: input.profiles.length,
|
|
staleProfileCount: 0,
|
|
}),
|
|
});
|
|
|
|
const helloFrame = (
|
|
scope: OneTalkScope = bindingScope,
|
|
binding = "binding-1",
|
|
): Record<string, unknown> => {
|
|
return {
|
|
protocolVersion: ONETALK_PROTOCOL_VERSION,
|
|
connectionType: "plugin",
|
|
type: "ws.hello",
|
|
requestId: "hello-1",
|
|
scope,
|
|
payload: {
|
|
binding,
|
|
requestedPermissions: ["read", "send"],
|
|
},
|
|
};
|
|
};
|
|
|
|
const nextMessages = async (
|
|
socket: {
|
|
once: (event: string, listener: (data: Buffer) => void) => void;
|
|
},
|
|
count: number,
|
|
): Promise<Record<string, unknown>[]> => {
|
|
return new Promise((resolve, reject) => {
|
|
const messages: Record<string, unknown>[] = [];
|
|
const listen = (): void => {
|
|
socket.once("message", (data) => {
|
|
try {
|
|
messages.push(JSON.parse(data.toString()) as Record<string, unknown>);
|
|
if (messages.length === count) {
|
|
resolve(messages);
|
|
return;
|
|
}
|
|
listen();
|
|
} catch (error: unknown) {
|
|
reject(error);
|
|
}
|
|
});
|
|
};
|
|
listen();
|
|
socket.once("error", reject);
|
|
});
|
|
};
|
|
|
|
const nextMessage = async (socket: {
|
|
once: (event: string, listener: (data: Buffer) => void) => void;
|
|
}): Promise<Record<string, unknown>> => {
|
|
const [message] = await nextMessages(socket, 1);
|
|
return message;
|
|
};
|
|
|
|
const nextCloseCode = async (socket: {
|
|
once: (event: string, listener: (code: number) => void) => void;
|
|
}): Promise<number> => {
|
|
return new Promise((resolve, reject) => {
|
|
socket.once("close", resolve);
|
|
socket.once("error", reject);
|
|
});
|
|
};
|
|
|
|
const openSocket = async (app: ReturnType<typeof createApp>, path = "/ws/plugin") => {
|
|
await app.ready();
|
|
const origin = path === "/ws/mind" ? "http://mind.localhost" : "http://plugin.localhost";
|
|
return app.injectWS(path, { headers: { origin } });
|
|
};
|
|
|
|
const assertAnchorSnapshot = (snapshot: Record<string, unknown>): void => {
|
|
assert.equal(snapshot.type, "anchor.snapshot");
|
|
assert.deepEqual(snapshot.payload, { anchors: [] });
|
|
};
|
|
|
|
const closeApp = async (
|
|
app: ReturnType<typeof createApp>,
|
|
socket: { terminate: () => void },
|
|
): Promise<void> => {
|
|
socket.terminate();
|
|
for (const client of app.websocketServer.clients) client.terminate();
|
|
await app.close();
|
|
};
|
|
|
|
test("accepts a mock-authorized plugin handshake and heartbeat", async () => {
|
|
const authorization = createMockAuthorizationReader([authorizationRecord]);
|
|
const app = createApp(testConfig, {
|
|
database: createDatabaseStub(),
|
|
authorization,
|
|
oneTalkService: createServiceStub(),
|
|
});
|
|
const socket = await openSocket(app);
|
|
|
|
try {
|
|
const handshake = nextMessages(socket, 2);
|
|
socket.send(JSON.stringify(helloFrame()));
|
|
const [accepted, snapshot] = await handshake;
|
|
assert.deepEqual(accepted, {
|
|
protocolVersion: ONETALK_PROTOCOL_VERSION,
|
|
connectionType: "plugin",
|
|
type: "ws.accepted",
|
|
requestId: "hello-1",
|
|
scope: bindingScope,
|
|
payload: {
|
|
authorizationVersion: "version-1",
|
|
permissions: ["read", "send"],
|
|
},
|
|
});
|
|
assertAnchorSnapshot(snapshot);
|
|
|
|
const heartbeat = nextMessage(socket);
|
|
socket.send(
|
|
JSON.stringify({
|
|
protocolVersion: ONETALK_PROTOCOL_VERSION,
|
|
connectionType: "plugin",
|
|
type: "heartbeat",
|
|
requestId: "heartbeat-1",
|
|
scope: bindingScope,
|
|
payload: { sentAtMs: 1_700_000_000_000 },
|
|
}),
|
|
);
|
|
assert.deepEqual(await heartbeat, {
|
|
protocolVersion: ONETALK_PROTOCOL_VERSION,
|
|
connectionType: "plugin",
|
|
type: "heartbeat.ack",
|
|
requestId: "heartbeat-1",
|
|
scope: bindingScope,
|
|
payload: { sentAtMs: 1_700_000_000_000 },
|
|
});
|
|
} finally {
|
|
await closeApp(app, socket);
|
|
}
|
|
});
|
|
|
|
test("accepts a development handshake using an injected authorization reader", async () => {
|
|
const app = createApp(developmentConfig, {
|
|
database: createDatabaseStub(),
|
|
authorization: createMockAuthorizationReader([authorizationRecord]),
|
|
oneTalkService: createServiceStub(),
|
|
});
|
|
const socket = await openSocket(app);
|
|
|
|
try {
|
|
const handshake = nextMessages(socket, 2);
|
|
socket.send(JSON.stringify(helloFrame(bindingScope, "binding-1")));
|
|
const [accepted, snapshot] = await handshake;
|
|
|
|
assert.deepEqual(accepted.payload, {
|
|
authorizationVersion: "version-1",
|
|
permissions: ["read", "send"],
|
|
});
|
|
assert.equal(JSON.stringify(accepted).includes("mock-session"), false);
|
|
assertAnchorSnapshot(snapshot);
|
|
|
|
const heartbeat = nextMessage(socket);
|
|
socket.send(
|
|
JSON.stringify({
|
|
protocolVersion: ONETALK_PROTOCOL_VERSION,
|
|
connectionType: "plugin",
|
|
type: "heartbeat",
|
|
requestId: "development-heartbeat",
|
|
scope: bindingScope,
|
|
payload: { sentAtMs: 1_700_000_000_000 },
|
|
}),
|
|
);
|
|
assert.deepEqual(await heartbeat, {
|
|
protocolVersion: ONETALK_PROTOCOL_VERSION,
|
|
connectionType: "plugin",
|
|
type: "heartbeat.ack",
|
|
requestId: "development-heartbeat",
|
|
scope: bindingScope,
|
|
payload: { sentAtMs: 1_700_000_000_000 },
|
|
});
|
|
} finally {
|
|
await closeApp(app, socket);
|
|
}
|
|
});
|
|
|
|
test("accepts a development Mind page handshake using an injected authorization reader", async () => {
|
|
const app = createApp(developmentConfig, {
|
|
database: createDatabaseStub(),
|
|
authorization: createMockAuthorizationReader([authorizationRecord]),
|
|
oneTalkService: createServiceStub(),
|
|
});
|
|
const socket = await openSocket(app, "/ws/mind");
|
|
|
|
try {
|
|
const accepted = nextMessage(socket);
|
|
socket.send(
|
|
JSON.stringify({
|
|
...helloFrame(mindScope),
|
|
connectionType: "mind_page",
|
|
payload: { requestedPermissions: ["read"] },
|
|
}),
|
|
);
|
|
|
|
const response = await accepted;
|
|
assert.deepEqual(response, {
|
|
protocolVersion: ONETALK_PROTOCOL_VERSION,
|
|
connectionType: "mind_page",
|
|
type: "ws.accepted",
|
|
requestId: "hello-1",
|
|
scope: mindScope,
|
|
payload: {
|
|
authorizationVersion: "version-1",
|
|
permissions: ["read"],
|
|
},
|
|
});
|
|
assert.equal(JSON.stringify(response).includes("binding-1"), false);
|
|
assert.equal(JSON.stringify(response).includes("mock-session"), false);
|
|
} finally {
|
|
await closeApp(app, socket);
|
|
}
|
|
});
|
|
|
|
test("keeps an explicitly injected authorization reader ahead of the development default", async () => {
|
|
const authorization = createMockAuthorizationReader([]);
|
|
const app = createApp(developmentConfig, {
|
|
database: createDatabaseStub(),
|
|
authorization,
|
|
oneTalkService: createServiceStub(),
|
|
});
|
|
const socket = await openSocket(app);
|
|
|
|
try {
|
|
const errorMessage = nextMessage(socket);
|
|
socket.send(JSON.stringify(helloFrame()));
|
|
assert.deepEqual(await errorMessage, {
|
|
protocolVersion: ONETALK_PROTOCOL_VERSION,
|
|
connectionType: "plugin",
|
|
type: "ws.error",
|
|
requestId: "hello-1",
|
|
scope: bindingScope,
|
|
payload: { code: ONETALK_ERROR_CODES.scopeMismatch },
|
|
});
|
|
} finally {
|
|
await closeApp(app, socket);
|
|
}
|
|
});
|
|
|
|
test("accepts a Mind page handshake without a plugin device identifier", async () => {
|
|
const authorization = createMockAuthorizationReader([authorizationRecord]);
|
|
const app = createApp(testConfig, {
|
|
database: createDatabaseStub(),
|
|
authorization,
|
|
oneTalkService: createServiceStub(),
|
|
});
|
|
const socket = await openSocket(app, "/ws/mind");
|
|
|
|
try {
|
|
const accepted = nextMessage(socket);
|
|
socket.send(
|
|
JSON.stringify({
|
|
...helloFrame(mindScope),
|
|
connectionType: "mind_page",
|
|
payload: { requestedPermissions: ["read"] },
|
|
}),
|
|
);
|
|
assert.deepEqual(await accepted, {
|
|
protocolVersion: ONETALK_PROTOCOL_VERSION,
|
|
connectionType: "mind_page",
|
|
type: "ws.accepted",
|
|
requestId: "hello-1",
|
|
scope: mindScope,
|
|
payload: {
|
|
authorizationVersion: "version-1",
|
|
permissions: ["read"],
|
|
},
|
|
});
|
|
} finally {
|
|
await closeApp(app, socket);
|
|
}
|
|
});
|
|
|
|
test("keeps Mind page authorization input separate from plugin binding", async () => {
|
|
const credentials: Array<string | undefined> = [];
|
|
const baseAuthorization = createMockAuthorizationReader([authorizationRecord]);
|
|
const authorization = {
|
|
authorize: async (request: OneTalkAuthorizationRequest) => {
|
|
if (request.connectionType === "mind_page") credentials.push(request.cookie);
|
|
return baseAuthorization.authorize(request);
|
|
},
|
|
readAuthorizationVersion: baseAuthorization.readAuthorizationVersion,
|
|
};
|
|
const app = createApp(testConfig, {
|
|
database: createDatabaseStub(),
|
|
authorization,
|
|
oneTalkService: createServiceStub(),
|
|
});
|
|
const socket = await openSocket(app, "/ws/mind");
|
|
|
|
try {
|
|
const accepted = nextMessage(socket);
|
|
socket.send(
|
|
JSON.stringify({
|
|
...helloFrame(mindScope),
|
|
connectionType: "mind_page",
|
|
payload: { requestedPermissions: ["read"] },
|
|
}),
|
|
);
|
|
assert.equal((await accepted).type, "ws.accepted");
|
|
|
|
const heartbeat = nextMessage(socket);
|
|
socket.send(
|
|
JSON.stringify({
|
|
protocolVersion: ONETALK_PROTOCOL_VERSION,
|
|
connectionType: "mind_page",
|
|
type: "heartbeat",
|
|
requestId: "mind-heartbeat",
|
|
scope: mindScope,
|
|
payload: { sentAtMs: 1_700_000_000_000 },
|
|
}),
|
|
);
|
|
assert.equal((await heartbeat).type, "heartbeat.ack");
|
|
assert.deepEqual(credentials, [undefined, undefined]);
|
|
} finally {
|
|
await closeApp(app, socket);
|
|
}
|
|
});
|
|
|
|
test("keeps direct WebSocket installation fail-closed by default", async () => {
|
|
const app = Fastify({ logger: false });
|
|
installWebsocket(app, {
|
|
service: createServiceStub(),
|
|
profileService: createProfileServiceStub(),
|
|
mindPageOrigin: "http://mind.localhost",
|
|
pluginOrigins: ["http://plugin.localhost"],
|
|
});
|
|
const socket = await openSocket(app);
|
|
|
|
try {
|
|
const errorMessage = nextMessage(socket);
|
|
socket.send(JSON.stringify(helloFrame()));
|
|
|
|
assert.deepEqual(await errorMessage, {
|
|
protocolVersion: ONETALK_PROTOCOL_VERSION,
|
|
connectionType: "plugin",
|
|
type: "ws.error",
|
|
requestId: "hello-1",
|
|
scope: bindingScope,
|
|
payload: { code: ONETALK_ERROR_CODES.authorizationUnavailable },
|
|
});
|
|
} finally {
|
|
await closeApp(app, socket);
|
|
}
|
|
});
|
|
|
|
test("does not treat an arbitrary Bearer header as a Mind WebSocket identity", async () => {
|
|
const bearerToken = "retired-internal-read-token";
|
|
const authorization = createMindAuthorizationReader({
|
|
baseUrl: "https://mind.example.com",
|
|
timeoutMs: 100,
|
|
fetch: async () => {
|
|
throw new Error("a missing Cookie must reject before upstream access");
|
|
},
|
|
});
|
|
const app = createApp(testConfig, {
|
|
database: createDatabaseStub(),
|
|
authorization,
|
|
oneTalkService: createServiceStub(),
|
|
});
|
|
await app.ready();
|
|
const socket = await app.injectWS("/ws/mind", {
|
|
headers: {
|
|
origin: "http://mind.localhost",
|
|
authorization: `Bearer ${bearerToken}`,
|
|
},
|
|
});
|
|
|
|
try {
|
|
const errorMessage = nextMessage(socket);
|
|
socket.send(
|
|
JSON.stringify({
|
|
protocolVersion: ONETALK_PROTOCOL_VERSION,
|
|
connectionType: "mind_page",
|
|
type: "ws.hello",
|
|
requestId: "bearer-token-hello",
|
|
scope: mindScope,
|
|
payload: { requestedPermissions: ["read", "send"] },
|
|
}),
|
|
);
|
|
assert.deepEqual(await errorMessage, {
|
|
protocolVersion: ONETALK_PROTOCOL_VERSION,
|
|
connectionType: "mind_page",
|
|
type: "ws.error",
|
|
requestId: "bearer-token-hello",
|
|
scope: mindScope,
|
|
payload: { code: ONETALK_ERROR_CODES.authRequired },
|
|
});
|
|
} finally {
|
|
await closeApp(app, socket);
|
|
}
|
|
});
|
|
|
|
test("fails closed when the authorization dependency is unavailable", async () => {
|
|
const app = createApp(testConfig, {
|
|
database: createDatabaseStub(),
|
|
oneTalkService: createServiceStub(),
|
|
});
|
|
const socket = await openSocket(app);
|
|
|
|
try {
|
|
const errorMessage = nextMessage(socket);
|
|
socket.send(
|
|
JSON.stringify({
|
|
...helloFrame(),
|
|
payload: { binding: "secret-binding", requestedPermissions: ["read", "send"] },
|
|
}),
|
|
);
|
|
const error = await errorMessage;
|
|
assert.deepEqual(error, {
|
|
protocolVersion: ONETALK_PROTOCOL_VERSION,
|
|
connectionType: "plugin",
|
|
type: "ws.error",
|
|
requestId: "hello-1",
|
|
scope: bindingScope,
|
|
payload: { code: ONETALK_ERROR_CODES.authorizationUnavailable },
|
|
});
|
|
assert.equal(JSON.stringify(error).includes("secret-binding"), false);
|
|
} finally {
|
|
await closeApp(app, socket);
|
|
}
|
|
});
|
|
|
|
test("rejects the legacy plugin credential field and mismatched binding", async () => {
|
|
const authorization = createMockAuthorizationReader([authorizationRecord]);
|
|
const app = createApp(testConfig, {
|
|
database: createDatabaseStub(),
|
|
authorization,
|
|
oneTalkService: createServiceStub(),
|
|
});
|
|
const socket = await openSocket(app);
|
|
|
|
try {
|
|
const legacyError = nextMessage(socket);
|
|
socket.send(
|
|
JSON.stringify({
|
|
...helloFrame(),
|
|
payload: { credential: "legacy-plugin-secret", requestedPermissions: ["read"] },
|
|
}),
|
|
);
|
|
const error = await legacyError;
|
|
assert.deepEqual(error.payload, { code: ONETALK_ERROR_CODES.invalidMessage });
|
|
assert.equal(JSON.stringify(error).includes("legacy-plugin-secret"), false);
|
|
} finally {
|
|
await closeApp(app, socket);
|
|
}
|
|
|
|
const mismatchApp = createApp(testConfig, {
|
|
database: createDatabaseStub(),
|
|
authorization: createMockAuthorizationReader([authorizationRecord]),
|
|
oneTalkService: createServiceStub(),
|
|
});
|
|
const mismatchSocket = await openSocket(mismatchApp);
|
|
try {
|
|
const errorMessage = nextMessage(mismatchSocket);
|
|
mismatchSocket.send(
|
|
JSON.stringify({
|
|
...helloFrame(),
|
|
payload: { binding: "wrong-binding", requestedPermissions: ["read"] },
|
|
}),
|
|
);
|
|
const error = await errorMessage;
|
|
assert.deepEqual(error.payload, { code: ONETALK_ERROR_CODES.bindingRevoked });
|
|
assert.equal(JSON.stringify(error).includes("wrong-binding"), false);
|
|
} finally {
|
|
await closeApp(mismatchApp, mismatchSocket);
|
|
}
|
|
});
|
|
|
|
test("rejects a send operation when the authorized binding lacks send permission", async () => {
|
|
const authorization = createMockAuthorizationReader([
|
|
{ ...authorizationRecord, permissions: ["read"] },
|
|
]);
|
|
const app = createApp(testConfig, {
|
|
database: createDatabaseStub(),
|
|
authorization,
|
|
oneTalkService: createServiceStub(),
|
|
});
|
|
const socket = await openSocket(app, "/ws/mind");
|
|
|
|
try {
|
|
const handshake = nextMessage(socket);
|
|
socket.send(
|
|
JSON.stringify({
|
|
...helloFrame(mindScope),
|
|
connectionType: "mind_page",
|
|
payload: { requestedPermissions: ["read"] },
|
|
}),
|
|
);
|
|
assert.equal((await handshake).type, "ws.accepted");
|
|
|
|
const errorMessage = nextMessage(socket);
|
|
socket.send(
|
|
JSON.stringify({
|
|
protocolVersion: ONETALK_PROTOCOL_VERSION,
|
|
connectionType: "mind_page",
|
|
type: "send.request",
|
|
requestId: "send-1",
|
|
sendRequestId: "send-request-1",
|
|
scope: mindScope,
|
|
payload: { conversationId: "conversation-1", content: "hello" },
|
|
}),
|
|
);
|
|
assert.deepEqual(await errorMessage, {
|
|
protocolVersion: ONETALK_PROTOCOL_VERSION,
|
|
connectionType: "mind_page",
|
|
type: "send.result",
|
|
requestId: "send-1",
|
|
sendRequestId: "send-request-1",
|
|
scope: mindScope,
|
|
payload: { status: "rejected_before_send", reason: "waiting_for_page" },
|
|
});
|
|
} finally {
|
|
await closeApp(app, socket);
|
|
}
|
|
});
|
|
|
|
test("allows heartbeat for a send-only connection", async () => {
|
|
const authorization = createMockAuthorizationReader([
|
|
{ ...authorizationRecord, permissions: ["send"] },
|
|
]);
|
|
const app = createApp(testConfig, {
|
|
database: createDatabaseStub(),
|
|
authorization,
|
|
oneTalkService: createServiceStub(),
|
|
});
|
|
const socket = await openSocket(app);
|
|
|
|
try {
|
|
const accepted = nextMessage(socket);
|
|
socket.send(JSON.stringify(helloFrame()));
|
|
assert.deepEqual((await accepted).payload, {
|
|
authorizationVersion: "version-1",
|
|
permissions: ["send"],
|
|
});
|
|
|
|
const heartbeat = nextMessage(socket);
|
|
socket.send(
|
|
JSON.stringify({
|
|
protocolVersion: ONETALK_PROTOCOL_VERSION,
|
|
connectionType: "plugin",
|
|
type: "heartbeat",
|
|
requestId: "heartbeat-send-only",
|
|
scope: bindingScope,
|
|
payload: { sentAtMs: 1_700_000_000_000 },
|
|
}),
|
|
);
|
|
assert.equal((await heartbeat).type, "heartbeat.ack");
|
|
} finally {
|
|
await closeApp(app, socket);
|
|
}
|
|
});
|
|
|
|
test("returns explicit protocol-upgrade rejection for an old frame", async () => {
|
|
const authorization = createMockAuthorizationReader([authorizationRecord]);
|
|
const app = createApp(testConfig, {
|
|
database: createDatabaseStub(),
|
|
authorization,
|
|
oneTalkService: createServiceStub(),
|
|
});
|
|
const socket = await openSocket(app);
|
|
|
|
try {
|
|
const errorMessage = nextMessage(socket);
|
|
const closeCode = nextCloseCode(socket);
|
|
socket.send(JSON.stringify({ ...helloFrame(), protocolVersion: 0 }));
|
|
assert.deepEqual(await errorMessage, {
|
|
protocolVersion: ONETALK_PROTOCOL_VERSION,
|
|
connectionType: "plugin",
|
|
type: "ws.error",
|
|
requestId: "hello-1",
|
|
scope: bindingScope,
|
|
payload: { code: ONETALK_ERROR_CODES.protocolUpgradeRequired },
|
|
});
|
|
assert.equal(await closeCode, 1003);
|
|
} finally {
|
|
await closeApp(app, socket);
|
|
}
|
|
});
|
|
|
|
test("rejects a scope change after authentication", async () => {
|
|
const authorization = createMockAuthorizationReader([authorizationRecord]);
|
|
const app = createApp(testConfig, {
|
|
database: createDatabaseStub(),
|
|
authorization,
|
|
oneTalkService: createServiceStub(),
|
|
});
|
|
const socket = await openSocket(app);
|
|
|
|
try {
|
|
const handshake = nextMessages(socket, 2);
|
|
socket.send(JSON.stringify(helloFrame()));
|
|
const [, snapshot] = await handshake;
|
|
assertAnchorSnapshot(snapshot);
|
|
|
|
const errorMessage = nextMessage(socket);
|
|
const changedScope = { ...bindingScope, deviceId: "other-device" };
|
|
socket.send(
|
|
JSON.stringify({
|
|
protocolVersion: ONETALK_PROTOCOL_VERSION,
|
|
connectionType: "plugin",
|
|
type: "heartbeat",
|
|
requestId: "heartbeat-2",
|
|
scope: changedScope,
|
|
payload: { sentAtMs: 1_700_000_000_000 },
|
|
}),
|
|
);
|
|
assert.deepEqual(await errorMessage, {
|
|
protocolVersion: ONETALK_PROTOCOL_VERSION,
|
|
connectionType: "plugin",
|
|
type: "ws.error",
|
|
requestId: "heartbeat-2",
|
|
scope: changedScope,
|
|
payload: { code: ONETALK_ERROR_CODES.scopeMismatch },
|
|
});
|
|
} finally {
|
|
await closeApp(app, socket);
|
|
}
|
|
});
|
|
|
|
test("closes a connection when the authorization version changes", async () => {
|
|
const authorization = createMockAuthorizationReader([authorizationRecord]);
|
|
const app = createApp(testConfig, {
|
|
database: createDatabaseStub(),
|
|
authorization,
|
|
oneTalkService: createServiceStub(),
|
|
});
|
|
const socket = await openSocket(app);
|
|
|
|
try {
|
|
const handshake = nextMessages(socket, 2);
|
|
socket.send(JSON.stringify(helloFrame()));
|
|
const [, snapshot] = await handshake;
|
|
assertAnchorSnapshot(snapshot);
|
|
authorization.upsert({ ...authorizationRecord, authorizationVersion: "version-2" });
|
|
|
|
const errorMessage = nextMessage(socket);
|
|
socket.send(
|
|
JSON.stringify({
|
|
protocolVersion: ONETALK_PROTOCOL_VERSION,
|
|
connectionType: "plugin",
|
|
type: "heartbeat",
|
|
requestId: "heartbeat-3",
|
|
scope: bindingScope,
|
|
payload: { sentAtMs: 1_700_000_000_000 },
|
|
}),
|
|
);
|
|
assert.deepEqual(await errorMessage, {
|
|
protocolVersion: ONETALK_PROTOCOL_VERSION,
|
|
connectionType: "plugin",
|
|
type: "ws.error",
|
|
requestId: "heartbeat-3",
|
|
scope: bindingScope,
|
|
payload: { code: ONETALK_ERROR_CODES.authorizationVersionChanged },
|
|
});
|
|
} finally {
|
|
await closeApp(app, socket);
|
|
}
|
|
});
|