Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { afterAll, beforeAll, describe, expect, it } from "vitest";

import { bootTestPayload } from "./bootTestPayload";
import type { TestPayload } from "./bootTestPayload";

// Postgres only: the other adapters do not isolate a transaction, so the spec would assert nothing.
const POSTGRES = process.env.DB_ADAPTER === "postgres";

let ctx: TestPayload;

describe.skipIf(!POSTGRES)("a translation that fails inside the caller's transaction", () => {
beforeAll(async () => {
ctx = await bootTestPayload({
autoTranslate: { targets: ["de", "fr"] },
failFor: ["de"],
});
});
afterAll(async () => {
await ctx?.cleanup();
});

it("does not fail the save that triggered it", async () => {
const created = await ctx.payload.create({
collection: "docs" as "pages",
locale: "en",
data: { title: "Survives", _status: "published" } as never,
});

const read = await ctx.payload.findByID({
collection: "docs" as "pages",
id: created.id,
locale: "en",
});
expect((read as { title?: string }).title).toBe("Survives");
});

it("still translates the locales that did not fail", async () => {
const created = await ctx.payload.create({
collection: "docs" as "pages",
locale: "en",
data: { title: "Partial", _status: "published" } as never,
});

const fr = await ctx.payload.findByID({
collection: "docs" as "pages",
id: created.id,
locale: "fr",
});
const de = await ctx.payload.findByID({
collection: "docs" as "pages",
id: created.id,
locale: "de",
});
expect((fr as { title?: string }).title).toBe("fr:Partial");
expect((de as { title?: string }).title).not.toBe("de:Partial");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { afterAll, beforeAll, describe, expect, it } from "vitest";

import { bootTestPayload } from "./bootTestPayload";
import type { TestPayload } from "./bootTestPayload";

// Postgres only. An explicit begin/rollback does not roll the document back on SQLite — the row
// survives — so the same spec would assert nothing there. MongoDB needs a replica set for
// transactions at all.
const POSTGRES = process.env.DB_ADAPTER === "postgres";

let ctx: TestPayload;

const beginTransaction = async (): Promise<string | number> => {
const transactionID = await ctx.payload.db.beginTransaction?.({});
expect(transactionID, "the adapter opened no transaction").toBeTruthy();
return transactionID as string | number;
};

const provenanceFor = async (documentId: string) => {
const { docs } = await ctx.payload.find({
collection: "translator-provenance" as "pages",
pagination: false,
where: { documentId: { equals: documentId } } as never,
});
return docs;
};

describe.skipIf(!POSTGRES)("a translation is atomic with the save that triggered it", () => {
beforeAll(async () => {
ctx = await bootTestPayload({ autoTranslate: { targets: ["de"] } });
});
afterAll(async () => {
await ctx?.cleanup();
});

it("leaves no translation and no receipt behind when the save rolls back", async () => {
const transactionID = await beginTransaction();
const created = await ctx.payload.create({
collection: "docs" as "pages",
locale: "en",
data: { title: "Rolled back", _status: "published" } as never,
req: { transactionID } as never,
});
const id = String(created.id);

await ctx.payload.db.rollbackTransaction?.(transactionID);

const { docs } = await ctx.payload.find({
collection: "docs" as "pages",
pagination: false,
where: { id: { equals: id } } as never,
});
expect(docs, "the rolled-back document survived").toHaveLength(0);
expect(await provenanceFor(id), "a receipt outlived the write it certifies").toHaveLength(0);
});

it("commits the translation and the receipt when the save commits", async () => {
const created = await ctx.payload.create({
collection: "docs" as "pages",
locale: "en",
data: { title: "Committed", _status: "published" } as never,
});
const id = String(created.id);

const de = await ctx.payload.findByID({ collection: "docs" as "pages", id, locale: "de" });
expect((de as { title?: string }).title).toBe("de:Committed");
expect(await provenanceFor(id)).toHaveLength(1);
});

it("translates every document of a bulk publish, not just the first", async () => {
const ids = [];
for (const title of ["Bulk one", "Bulk two"]) {
const doc = await ctx.payload.create({
collection: "docs" as "pages",
locale: "en",
data: { title, ref: "bulk-batch" } as never,
});
ids.push(String(doc.id));
}

await ctx.payload.update({
collection: "docs" as "pages",
locale: "en",
where: { ref: { equals: "bulk-batch" } } as never,
// The content must change too: a publish that edits nothing is skipped by the drift-gate.
data: { _status: "published", title: "Bulk edited" } as never,
});

for (const [i, id] of ids.entries()) {
const de = await ctx.payload.findByID({ collection: "docs" as "pages", id, locale: "de" });
expect((de as { title?: string }).title, `document ${i + 1} of the batch`).toBe(
"de:Bulk edited"
);
}
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import type { CollectionConfig } from "payload";

import { bootTestPayload } from "./bootTestPayload";
import type { TestPayload } from "./bootTestPayload";
import { buildTestCollections } from "./testCollections";

// Postgres: the adapter that isolates a transaction.
const POSTGRES = process.env.DB_ADAPTER === "postgres";

let ctx: TestPayload;

// The target-locale write fails Payload's own validation, so the error is raised by a Payload
// *operation* rather than by the provider — the case that reaches `killTransaction`.
const rejectTranslated = (collections: CollectionConfig[]): CollectionConfig[] =>
collections.map((c) => {
if (c.slug !== "docs") return c;
return {
...c,
fields: c.fields.map((f) =>
"name" in f && f.name === "title"
? {
...f,
validate: (value: unknown) =>
typeof value === "string" && value.startsWith("de:")
? "the de locale rejects this value"
: true,
}
: f
),
} as CollectionConfig;
});

describe.skipIf(!POSTGRES)("a target write rejected by validation", () => {
beforeAll(async () => {
ctx = await bootTestPayload({
collections: rejectTranslated(buildTestCollections()),
autoTranslate: { targets: ["de"] },
});
});
afterAll(async () => {
await ctx?.cleanup();
});

it("fails the save visibly instead of discarding it in silence", async () => {
await expect(
ctx.payload.create({
collection: "docs" as "pages",
locale: "en",
data: { title: "Editor wrote this", _status: "published" } as never,
})
).rejects.toThrow();

const { docs } = await ctx.payload.find({
collection: "docs" as "pages",
locale: "en",
pagination: false,
where: { title: { equals: "Editor wrote this" } } as never,
});
expect(docs, "the rejected save left a row behind").toHaveLength(0);
});
});
Loading
Loading