server/models/FileOperation.test.ts
Rama main · réplica de lectura del repositorio original.
import { randomUUID } from "node:crypto";
import path from "node:path";
import { EmptyResultError } from "sequelize";
import {
FileOperationFormat,
FileOperationState,
FileOperationType,
} from "@shared/types";
import { createContext } from "@server/context";
import {
buildCollection,
buildFileOperation,
buildTeam,
buildUser,
} from "@server/test/factories";
import { ValidateKey } from "@server/validation";
import FileOperation from "./FileOperation";
describe("FileOperation", () => {
describe("createExport", () => {
it("should create a team export with the requested options", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const fileOperation = await FileOperation.createExport(
createContext({ user }),
{ team, includeAttachments: false, includePrivate: false }
);
expect(fileOperation.type).toBe(FileOperationType.Export);
expect(fileOperation.state).toBe(FileOperationState.Creating);
expect(fileOperation.format).toBe(FileOperationFormat.MarkdownZip);
expect(fileOperation.key).toContain(`uploads/${team.id}/`);
expect(fileOperation.options).toEqual({
includeAttachments: false,
includePrivate: false,
});
expect(fileOperation.user.id).toBe(user.id);
});
it("should set the collection association for a collection export", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const collection = await buildCollection({ teamId: team.id });
const fileOperation = await FileOperation.createExport(
createContext({ user }),
{ team, collection }
);
expect(fileOperation.collectionId).toBe(collection.id);
expect(fileOperation.collection?.id).toBe(collection.id);
expect(fileOperation.options).toEqual({
includeAttachments: true,
includePrivate: true,
});
});
});
describe("getExportKey", () => {
it("should write to the uploads bucket", () => {
const teamId = randomUUID();
const key = FileOperation.getExportKey({
name: "My Collection",
teamId,
format: FileOperationFormat.MarkdownZip,
});
expect(key).toEqual(
expect.stringMatching(
new RegExp(
`^uploads/${teamId}/[^/]+/My Collection-export\\.markdown\\.zip$`
)
)
);
expect(ValidateKey.isValid(key)).toBe(true);
});
it("should truncate a long name", () => {
const key = FileOperation.getExportKey({
name: "a".repeat(255),
teamId: randomUUID(),
format: FileOperationFormat.MarkdownZip,
});
expect(key.length).toBeLessThanOrEqual(255);
expect(key.endsWith(`/${"a".repeat(100)}-export.markdown.zip`)).toBe(
true
);
});
it("should not allow the name to traverse to another bucket", () => {
const key = FileOperation.getExportKey({
name: `../../../public/${randomUUID()}/${randomUUID()}/x`,
teamId: randomUUID(),
format: FileOperationFormat.MarkdownZip,
});
expect(key.split("/")).toHaveLength(4);
// The key must still point inside the uploads bucket once normalized.
expect(path.posix.normalize(key).startsWith("uploads/")).toBe(true);
});
});
describe("findByPk", () => {
it("should not allow a passed where to override the id", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const fileOperation = await buildFileOperation({
teamId: team.id,
userId: user.id,
});
const other = await buildFileOperation({
teamId: team.id,
userId: user.id,
});
const found = await FileOperation.findByPk(fileOperation.id, {
where: { id: other.id },
});
expect(found?.id).toEqual(fileOperation.id);
});
it("should throw the passed error when rejectOnEmpty is an error", async () => {
const error = new Error("does not exist");
await expect(
FileOperation.findByPk("3a1b2c3d-0000-4000-8000-000000000000", {
rejectOnEmpty: error,
})
).rejects.toThrow(error);
});
it("should throw when rejectOnEmpty is set and the id is not a string", async () => {
await expect(
FileOperation.findByPk(123, { rejectOnEmpty: true })
).rejects.toThrow(EmptyResultError);
});
});
});