mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-03 18:48:06 +09:00
190 lines
5.4 KiB
TypeScript
190 lines
5.4 KiB
TypeScript
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
|
|
const hasProjectPermit = vi.hoisted(() => vi.fn());
|
|
const readAccessToken = vi.hoisted(() => vi.fn());
|
|
|
|
vi.mock("@webstudio-is/trpc-interface/index.server", () => {
|
|
class AuthorizationError extends Error {}
|
|
return {
|
|
AuthorizationError,
|
|
authorizeProject: { hasProjectPermit },
|
|
};
|
|
});
|
|
|
|
vi.mock("@webstudio-is/project/index.server", () => ({
|
|
getRequiredPermitForBuildPatchTransaction: (transaction: {
|
|
payload: Array<{ namespace: string }>;
|
|
}) =>
|
|
transaction.payload.some((change) => change.namespace === "styles")
|
|
? "build"
|
|
: "edit",
|
|
}));
|
|
|
|
vi.mock("~/env/env.server", () => ({
|
|
default: {
|
|
AUTH_WS_CLIENT_SECRET: "secret",
|
|
},
|
|
}));
|
|
|
|
vi.mock("~/services/token.server", () => ({
|
|
readAccessToken,
|
|
}));
|
|
|
|
import {
|
|
authorizePatchEntries,
|
|
createWriterContext,
|
|
} from "./patch-auth.server";
|
|
import type { NormalizedPatchRequest } from "./patch-normalize.server";
|
|
|
|
const createContext = () => {
|
|
const context = {
|
|
authorization: { type: "anonymous" },
|
|
createTokenContext: vi.fn(async (authToken: string) => ({
|
|
authorization: { type: "token", authToken },
|
|
})),
|
|
};
|
|
return context;
|
|
};
|
|
|
|
const transaction = (namespace: string, id = `tx-${namespace}`) => ({
|
|
id,
|
|
payload: [{ namespace, patches: [{ op: "replace", path: ["x"], value: 1 }] }],
|
|
});
|
|
|
|
describe("createWriterContext", () => {
|
|
beforeEach(() => {
|
|
readAccessToken.mockReset();
|
|
});
|
|
|
|
test("rejects empty and legacy session tokens", async () => {
|
|
const context = createContext();
|
|
|
|
await expect(createWriterContext(context as never, "")).rejects.toThrow(
|
|
"Collab writer token is not authorized"
|
|
);
|
|
await expect(
|
|
createWriterContext(context as never, "session")
|
|
).rejects.toThrow("Collab writer token is not authorized");
|
|
});
|
|
|
|
test("creates a user context from a signed collab token", async () => {
|
|
readAccessToken.mockResolvedValue({
|
|
userId: "user-1",
|
|
projectId: "project-1",
|
|
});
|
|
|
|
const writerContext = await createWriterContext(
|
|
createContext() as never,
|
|
"signed-token"
|
|
);
|
|
|
|
expect(writerContext.authorization).toMatchObject({
|
|
type: "user",
|
|
userId: "user-1",
|
|
});
|
|
const authorization = writerContext.authorization as {
|
|
isLoggedInToBuilder: (projectId: string) => Promise<boolean>;
|
|
};
|
|
expect(await authorization.isLoggedInToBuilder("project-1")).toBe(true);
|
|
expect(await authorization.isLoggedInToBuilder("project-2")).toBe(false);
|
|
});
|
|
|
|
test("falls back to the shared-link token context", async () => {
|
|
readAccessToken.mockResolvedValue(undefined);
|
|
const context = createContext();
|
|
|
|
const writerContext = await createWriterContext(
|
|
context as never,
|
|
"shared-link-token"
|
|
);
|
|
|
|
expect(context.createTokenContext).toHaveBeenCalledWith(
|
|
"shared-link-token"
|
|
);
|
|
expect(writerContext.authorization).toEqual({
|
|
type: "token",
|
|
authToken: "shared-link-token",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("authorizePatchEntries", () => {
|
|
beforeEach(() => {
|
|
hasProjectPermit.mockReset();
|
|
readAccessToken.mockReset();
|
|
});
|
|
|
|
test("checks every transaction with its own writer and required permit", async () => {
|
|
readAccessToken
|
|
.mockResolvedValueOnce({ userId: "user-1", projectId: "project-1" })
|
|
.mockResolvedValueOnce({ userId: "user-2", projectId: "project-1" });
|
|
hasProjectPermit.mockResolvedValue(true);
|
|
const context = createContext();
|
|
const patch: NormalizedPatchRequest = {
|
|
buildId: "build-1",
|
|
projectId: "project-1",
|
|
clientVersion: 1,
|
|
entries: [
|
|
{
|
|
transaction: transaction("props") as never,
|
|
writer: { type: "token", authToken: "token-1" },
|
|
},
|
|
{
|
|
transaction: transaction("styles") as never,
|
|
writer: { type: "token", authToken: "token-2" },
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = await authorizePatchEntries(context as never, patch);
|
|
|
|
expect(result.rejected).toEqual([]);
|
|
expect(result.authorized[0].context.authorization).toMatchObject({
|
|
type: "user",
|
|
userId: "user-1",
|
|
});
|
|
expect(hasProjectPermit).toHaveBeenCalledTimes(2);
|
|
expect(hasProjectPermit).toHaveBeenNthCalledWith(
|
|
1,
|
|
{ projectId: "project-1", permit: "edit" },
|
|
expect.objectContaining({
|
|
authorization: expect.objectContaining({ userId: "user-1" }),
|
|
})
|
|
);
|
|
expect(hasProjectPermit).toHaveBeenNthCalledWith(
|
|
2,
|
|
{ projectId: "project-1", permit: "build" },
|
|
expect.objectContaining({
|
|
authorization: expect.objectContaining({ userId: "user-2" }),
|
|
})
|
|
);
|
|
});
|
|
|
|
test("returns rejected entries when a writer does not have the required permit", async () => {
|
|
readAccessToken.mockResolvedValue({
|
|
userId: "user-1",
|
|
projectId: "project-1",
|
|
});
|
|
hasProjectPermit.mockResolvedValue(false);
|
|
|
|
const result = await authorizePatchEntries(createContext() as never, {
|
|
buildId: "build-1",
|
|
projectId: "project-1",
|
|
clientVersion: 1,
|
|
entries: [
|
|
{
|
|
transaction: transaction("styles") as never,
|
|
writer: { type: "token", authToken: "token-1" },
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(result.authorized).toEqual([]);
|
|
expect(result.rejected).toEqual([
|
|
expect.objectContaining({
|
|
errors: "You don't have permission to build this project.",
|
|
}),
|
|
]);
|
|
});
|
|
});
|