86 lines
2.9 KiB
TypeScript
86 lines
2.9 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { createHmac } from "node:crypto";
|
|
import { authorizeChannel, parseTopic, verifyTicket } from "./chat.js";
|
|
|
|
const workspaceId = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
|
|
const channelId = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb";
|
|
const userId = "cccccccc-cccc-4ccc-8ccc-cccccccccccc";
|
|
const secret = "a".repeat(40);
|
|
const claims = {
|
|
iss: "echodo-web",
|
|
aud: "echodo-chat-v1",
|
|
scope: "chat:subscribe",
|
|
sub: userId,
|
|
workspaceId,
|
|
exp: Math.floor(Date.now() / 1000) + 30,
|
|
};
|
|
function ticket(payload: object, key = secret) {
|
|
const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
return `${body}.${createHmac("sha256", key).update(body).digest("base64url")}`;
|
|
}
|
|
|
|
describe("chat ticket", () => {
|
|
it("rejects unsigned, altered, expired and wrong-scope tickets", () => {
|
|
expect(verifyTicket(ticket(claims), secret)).toEqual(claims);
|
|
expect(verifyTicket(ticket(claims).replace(/.$/, "x"), secret)).toBeNull();
|
|
expect(verifyTicket(ticket({ ...claims, exp: 1 }), secret)).toBeNull();
|
|
expect(
|
|
verifyTicket(ticket({ ...claims, scope: "yjs" }), secret),
|
|
).toBeNull();
|
|
expect(
|
|
verifyTicket(ticket({ ...claims, aud: "other" }), secret),
|
|
).toBeNull();
|
|
expect(verifyTicket(ticket(claims, "b".repeat(40)), secret)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("topic and authorization", () => {
|
|
it("accepts only exact workspace and channel UUID topics", () => {
|
|
expect(
|
|
parseTopic(`chat:v1:workspace:${workspaceId}:channel:${channelId}`),
|
|
).toEqual({ workspaceId, channelId });
|
|
expect(
|
|
parseTopic(`chat:v1:workspace:${workspaceId}:channel:${channelId}:evil`),
|
|
).toBeNull();
|
|
});
|
|
it("requires workspace membership, and channel membership for private/direct (no admin bypass)", async () => {
|
|
const lookup = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
kind: "public",
|
|
workspaceMember: true,
|
|
channelMember: false,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
kind: "private",
|
|
workspaceMember: true,
|
|
channelMember: false,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
kind: "direct",
|
|
workspaceMember: true,
|
|
channelMember: true,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
kind: "public",
|
|
workspaceMember: false,
|
|
channelMember: true,
|
|
})
|
|
.mockRejectedValueOnce(new Error("db down"));
|
|
expect(await authorizeChannel(lookup, workspaceId, channelId, userId)).toBe(
|
|
true,
|
|
);
|
|
expect(await authorizeChannel(lookup, workspaceId, channelId, userId)).toBe(
|
|
false,
|
|
);
|
|
expect(await authorizeChannel(lookup, workspaceId, channelId, userId)).toBe(
|
|
true,
|
|
);
|
|
expect(await authorizeChannel(lookup, workspaceId, channelId, userId)).toBe(
|
|
false,
|
|
);
|
|
expect(await authorizeChannel(lookup, workspaceId, channelId, userId)).toBe(
|
|
false,
|
|
);
|
|
});
|
|
});
|