mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-03 10:37:48 +09:00
238 lines
6.5 KiB
TypeScript
238 lines
6.5 KiB
TypeScript
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
|
|
const patchBuild = vi.hoisted(() => vi.fn());
|
|
const authorizePatchEntries = vi.hoisted(() => vi.fn());
|
|
|
|
vi.mock("@webstudio-is/project/index.server", () => ({
|
|
patchBuild,
|
|
}));
|
|
|
|
vi.mock("./patch-auth.server", () => ({
|
|
assertProjectPermit: vi.fn(),
|
|
authorizePatchEntries,
|
|
createWriterContext: vi.fn(),
|
|
}));
|
|
|
|
import { applyPatchRequest } from "./patch-service.server";
|
|
import type { NormalizedPatchRequest } from "./patch-normalize.server";
|
|
|
|
const createContext = () =>
|
|
({
|
|
postgrest: {
|
|
client: {
|
|
from: () => ({
|
|
select: () => ({
|
|
eq: () => ({
|
|
single: async () => ({
|
|
data: { projectId: "project-1", version: 3 },
|
|
error: undefined,
|
|
}),
|
|
}),
|
|
}),
|
|
}),
|
|
},
|
|
},
|
|
}) as never;
|
|
|
|
const transaction = (id: string) => ({
|
|
id,
|
|
payload: [{ namespace: "props", patches: [] }],
|
|
});
|
|
|
|
const patch: NormalizedPatchRequest = {
|
|
buildId: "build-1",
|
|
projectId: "project-1",
|
|
clientVersion: 3,
|
|
entries: [
|
|
{
|
|
seq: 1,
|
|
transaction: transaction("tx-1") as never,
|
|
writer: { type: "token", authToken: "token-1" },
|
|
},
|
|
{
|
|
seq: 2,
|
|
transaction: transaction("tx-2") as never,
|
|
writer: { type: "token", authToken: "token-2" },
|
|
},
|
|
],
|
|
};
|
|
|
|
describe("applyPatchRequest", () => {
|
|
beforeEach(() => {
|
|
patchBuild.mockReset();
|
|
authorizePatchEntries.mockReset();
|
|
});
|
|
|
|
test("returns per-entry partial results while applying authorized entries", async () => {
|
|
authorizePatchEntries.mockResolvedValue({
|
|
authorized: [{ entry: patch.entries[1], context: { writer: 2 } }],
|
|
rejected: [
|
|
{
|
|
entry: patch.entries[0],
|
|
errors: "You don't have permission to edit this project.",
|
|
},
|
|
],
|
|
});
|
|
patchBuild.mockResolvedValue({ status: "ok", version: 4 });
|
|
|
|
const result = await applyPatchRequest(createContext(), patch);
|
|
|
|
expect(patchBuild).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
transactions: [patch.entries[1].transaction],
|
|
}),
|
|
{ writer: 2 }
|
|
);
|
|
expect(result).toEqual({
|
|
status: "partial",
|
|
version: 4,
|
|
entries: [
|
|
{
|
|
seq: 1,
|
|
transactionId: "tx-1",
|
|
status: "rejected",
|
|
errors: "You don't have permission to edit this project.",
|
|
},
|
|
{ seq: 2, transactionId: "tx-2", status: "accepted" },
|
|
],
|
|
});
|
|
});
|
|
|
|
test("falls back to per-entry apply when a mixed authorized batch fails", async () => {
|
|
const context = { writer: 1 };
|
|
authorizePatchEntries.mockResolvedValue({
|
|
authorized: [
|
|
{ entry: patch.entries[0], context },
|
|
{ entry: patch.entries[1], context },
|
|
],
|
|
rejected: [],
|
|
});
|
|
patchBuild
|
|
.mockResolvedValueOnce({ status: "error", errors: "batch failed" })
|
|
.mockResolvedValueOnce({ status: "ok", version: 4 })
|
|
.mockResolvedValueOnce({ status: "error", errors: "entry failed" });
|
|
|
|
const result = await applyPatchRequest(createContext(), patch);
|
|
|
|
expect(result).toEqual({
|
|
status: "partial",
|
|
version: 4,
|
|
entries: [
|
|
{ seq: 1, transactionId: "tx-1", status: "accepted" },
|
|
{
|
|
seq: 2,
|
|
transactionId: "tx-2",
|
|
status: "failed",
|
|
errors: "entry failed",
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test("applies mixed writer entries with their own context without batching", async () => {
|
|
authorizePatchEntries.mockResolvedValue({
|
|
authorized: [
|
|
{ entry: patch.entries[0], context: { writer: 1 } },
|
|
{ entry: patch.entries[1], context: { writer: 2 } },
|
|
],
|
|
rejected: [],
|
|
});
|
|
patchBuild
|
|
.mockResolvedValueOnce({ status: "ok", version: 4 })
|
|
.mockResolvedValueOnce({ status: "ok", version: 5 });
|
|
|
|
const result = await applyPatchRequest(createContext(), patch);
|
|
|
|
expect(patchBuild).toHaveBeenCalledTimes(2);
|
|
expect(patchBuild).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.objectContaining({
|
|
clientVersion: 3,
|
|
transactions: [patch.entries[0].transaction],
|
|
}),
|
|
{ writer: 1 }
|
|
);
|
|
expect(patchBuild).toHaveBeenNthCalledWith(
|
|
2,
|
|
expect.objectContaining({
|
|
clientVersion: 4,
|
|
transactions: [patch.entries[1].transaction],
|
|
}),
|
|
{ writer: 2 }
|
|
);
|
|
expect(result).toEqual({
|
|
status: "ok",
|
|
version: 5,
|
|
entries: [
|
|
{ seq: 1, transactionId: "tx-1", status: "accepted" },
|
|
{ seq: 2, transactionId: "tx-2", status: "accepted" },
|
|
],
|
|
});
|
|
});
|
|
|
|
test("preserves entry order when transaction ids are duplicated", async () => {
|
|
const duplicatePatch: NormalizedPatchRequest = {
|
|
...patch,
|
|
entries: [
|
|
{
|
|
seq: 1,
|
|
transaction: transaction("tx-duplicate") as never,
|
|
writer: { type: "token", authToken: "token-1" },
|
|
},
|
|
{
|
|
seq: 2,
|
|
transaction: transaction("tx-duplicate") as never,
|
|
writer: { type: "token", authToken: "token-2" },
|
|
},
|
|
],
|
|
};
|
|
authorizePatchEntries.mockResolvedValue({
|
|
authorized: [
|
|
{ entry: duplicatePatch.entries[1], context: { writer: 2 } },
|
|
],
|
|
rejected: [
|
|
{
|
|
entry: duplicatePatch.entries[0],
|
|
errors: "You don't have permission to edit this project.",
|
|
},
|
|
],
|
|
});
|
|
patchBuild.mockResolvedValue({ status: "ok", version: 4 });
|
|
|
|
const result = await applyPatchRequest(createContext(), duplicatePatch);
|
|
|
|
expect(result).toEqual({
|
|
status: "partial",
|
|
version: 4,
|
|
entries: [
|
|
{
|
|
seq: 1,
|
|
transactionId: "tx-duplicate",
|
|
status: "rejected",
|
|
errors: "You don't have permission to edit this project.",
|
|
},
|
|
{ seq: 2, transactionId: "tx-duplicate", status: "accepted" },
|
|
],
|
|
});
|
|
});
|
|
|
|
test("does not convert thrown per-entry persistence errors into terminal entry failures", async () => {
|
|
const context = { writer: 1 };
|
|
authorizePatchEntries.mockResolvedValue({
|
|
authorized: [
|
|
{ entry: patch.entries[0], context },
|
|
{ entry: patch.entries[1], context },
|
|
],
|
|
rejected: [],
|
|
});
|
|
patchBuild
|
|
.mockResolvedValueOnce({ status: "error", errors: "batch failed" })
|
|
.mockResolvedValueOnce({ status: "ok", version: 4 })
|
|
.mockRejectedValueOnce(new Error("PostgREST unavailable"));
|
|
|
|
await expect(applyPatchRequest(createContext(), patch)).rejects.toThrow(
|
|
"PostgREST unavailable"
|
|
);
|
|
});
|
|
});
|