Ctrl K
ComunidadGuías…

server/models/Collection.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { randomUUID } from "node:crypto";
import { EmptyResultError } from "sequelize";
import { randomString } from "@shared/random";
import slugify from "@server/utils/slugify";
import { createContext } from "@server/context";
import {
  buildUser,
  buildGroup,
  buildCollection,
  buildTeam,
  buildDocument,
} from "@server/test/factories";
import { withAPIContext } from "@server/test/support";
import Collection from "./Collection";
import Document from "./Document";

beforeEach(() => {
  vi.resetAllMocks();
});

describe("#updateDeprecatedReason", () => {
  it("should reject a reason when a stale collection has already been restored", async () => {
    const user = await buildUser();
    const collection = await buildCollection({
      userId: user.id,
      teamId: user.teamId,
      archivedAt: new Date(),
    });
    await Collection.update(
      { archivedAt: null },
      { where: { id: collection.id } }
    );

    await expect(
      withAPIContext(user, (ctx) =>
        collection.updateDeprecatedReason(ctx, "Outdated")
      )
    ).rejects.toThrow("The collection must be archived");
    await collection.reload();
    expect(collection.deprecatedReason).toBeNull();
  });
});

describe("#url", () => {
  it("should return correct url for the collection", () => {
    const collection = new Collection({
      id: "1234",
    });
    expect(collection.path).toBe(`/collection/untitled-${collection.urlId}`);
  });

  it("should return correct url with slugified collection name", () => {
    const path = Collection.getPath({
      name: "Test Collection",
      urlId: "abcdefghij",
    });
    expect(path).toBe("/collection/test-collection-abcdefghij");
  });

  it("should return untitled when collection name is empty", () => {
    const path = Collection.getPath({
      name: "",
      urlId: "abcdefghij",
    });
    expect(path).toBe("/collection/untitled-abcdefghij");
  });
});

describe("getDocumentParents", () => {
  it("should return array of parent document ids", async () => {
    const parent = await buildDocument();
    const document = await buildDocument();
    const collection = await buildCollection({
      documentStructure: [
        {
          ...(await parent.toNavigationNode()),
          children: [await document.toNavigationNode()],
        },
      ],
    });
    const result = collection.getDocumentParents(document.id);
    expect(result?.length).toBe(1);
    expect(result ? result[0] : undefined).toBe(parent.id);
  });

  it("should return empty array for a root document", async () => {
    const parent = await buildDocument();
    const document = await buildDocument();
    const collection = await buildCollection({
      documentStructure: [
        {
          ...(await parent.toNavigationNode()),
          children: [await document.toNavigationNode()],
        },
      ],
    });
    const result = collection.getDocumentParents(parent.id);
    expect(result?.length).toBe(0);
  });

  it("should not error if documentStructure is empty", async () => {
    const parent = await buildDocument();
    await buildDocument();
    const collection = await buildCollection();
    const result = collection.getDocumentParents(parent.id);
    expect(result).toBe(undefined);
  });
});

describe("getDocumentTree", () => {
  it("should return document tree", async () => {
    const document = await buildDocument();
    const collection = await buildCollection({
      documentStructure: [await document.toNavigationNode()],
    });
    expect(collection.getDocumentTree(document.id)).toEqual(
      await document.toNavigationNode()
    );
  });

  it("should return nested documents in tree", async () => {
    const parent = await buildDocument();
    const document = await buildDocument();
    const collection = await buildCollection({
      documentStructure: [
        {
          ...(await parent.toNavigationNode()),
          children: [await document.toNavigationNode()],
        },
      ],
    });

    expect(collection.getDocumentTree(parent.id)).toEqual({
      ...(await parent.toNavigationNode()),
      children: [await document.toNavigationNode()],
    });
    expect(collection.getDocumentTree(document.id)).toEqual(
      await document.toNavigationNode()
    );
  });
});

describe("#addDocumentToStructure", () => {
  it("should add as last element without index", async () => {
    const collection = await buildCollection();
    const id = randomUUID();
    const newDocument = await buildDocument({
      id,
      title: "New end node",
      parentDocumentId: null,
      teamId: collection.teamId,
    });
    await collection.addDocumentToStructure(newDocument);
    expect(collection.documentStructure!.length).toBe(1);
    expect(collection.documentStructure![0].id).toBe(id);

    // should not append multiple times
    await collection.addDocumentToStructure(newDocument);
    expect(collection.documentStructure!.length).toBe(1);
  });

  it("should add with an index", async () => {
    const collection = await buildCollection();
    const id = randomUUID();
    const newDocument = await buildDocument({
      id,
      title: "New end node",
      parentDocumentId: null,
      teamId: collection.teamId,
    });
    await collection.addDocumentToStructure(newDocument, 1);
    expect(collection.documentStructure!.length).toBe(1);
    expect(collection.documentStructure![0].id).toBe(id);
  });

  it("should add as a child if with parent", async () => {
    const collection = await buildCollection();
    const document = await buildDocument({ collectionId: collection.id });
    await collection.reload();

    const id = randomUUID();
    const newDocument = await buildDocument({
      id,
      title: "New end node",
      parentDocumentId: document.id,
      teamId: collection.teamId,
    });
    await collection.addDocumentToStructure(newDocument, 1);
    expect(collection.documentStructure!.length).toBe(1);
    expect(collection.documentStructure![0].id).toBe(document.id);
    expect(collection.documentStructure![0].children.length).toBe(1);
    expect(collection.documentStructure![0].children[0].id).toBe(id);
  });

  it("should add as a child if with parent with index", async () => {
    const collection = await buildCollection();
    const document = await buildDocument({ collectionId: collection.id });
    await collection.reload();

    const newDocument = await buildDocument({
      id: randomUUID(),
      title: "node",
      parentDocumentId: document.id,
      teamId: collection.teamId,
    });
    const id = randomUUID();
    const secondDocument = await buildDocument({
      id,
      title: "New start node",
      parentDocumentId: document.id,
      teamId: collection.teamId,
    });
    await collection.addDocumentToStructure(newDocument);
    await collection.addDocumentToStructure(secondDocument, 0);
    expect(collection.documentStructure!.length).toBe(1);
    expect(collection.documentStructure![0].id).toBe(document.id);
    expect(collection.documentStructure![0].children.length).toBe(2);
    expect(collection.documentStructure![0].children[0].id).toBe(id);
  });

  it("should add the document along with its nested document(s)", async () => {
    const collection = await buildCollection();

    const document = await buildDocument({
      title: "New doc",
      teamId: collection.teamId,
    });

    // create a nested doc within New doc
    const nestedDocument = await buildDocument({
      title: "Nested doc",
      parentDocumentId: document.id,
      teamId: collection.teamId,
    });

    expect(collection.documentStructure).toBeNull();

    await collection.addDocumentToStructure(document);

    expect(collection.documentStructure).not.toBeNull();
    expect(collection.documentStructure).toHaveLength(1);
    expect(collection.documentStructure![0].id).toBe(document.id);
    expect(collection.documentStructure![0].children).toHaveLength(1);
    expect(collection.documentStructure![0].children[0].id).toBe(
      nestedDocument.id
    );
  });

  it("should add the document along with its archived nested document(s)", async () => {
    const collection = await buildCollection();

    const document = await buildDocument({
      title: "New doc",
      teamId: collection.teamId,
    });

    // create a nested doc within New doc
    const nestedDocument = await buildDocument({
      title: "Nested doc",
      parentDocumentId: document.id,
      teamId: collection.teamId,
    });

    nestedDocument.archivedAt = new Date();
    await nestedDocument.save();

    expect(collection.documentStructure).toBeNull();

    await collection.addDocumentToStructure(document, undefined, {
      includeArchived: true,
    });

    expect(collection.documentStructure).not.toBeNull();
    expect(collection.documentStructure).toHaveLength(1);
    expect(collection.documentStructure![0].id).toBe(document.id);
    expect(collection.documentStructure![0].children).toHaveLength(1);
    expect(collection.documentStructure![0].children[0].id).toBe(
      nestedDocument.id
    );
  });
  describe("options: documentJson", () => {
    it("should append supplied json over document's own", async () => {
      const collection = await buildCollection();
      const id = randomUUID();
      const newDocument = await buildDocument({
        id: randomUUID(),
        title: "New end node",
        parentDocumentId: null,
        teamId: collection.teamId,
      });
      await collection.addDocumentToStructure(newDocument, undefined, {
        documentJson: {
          id,
          title: "Parent",
          url: "parent",
          children: [
            {
              id,
              title: "Totally fake",
              children: [],
              url: "totally-fake",
            },
          ],
        },
      });
      expect(collection.documentStructure![0].children.length).toBe(1);
      expect(collection.documentStructure![0].children[0].id).toBe(id);
    });
  });
});

describe("#updateDocument", () => {
  it("should update root document's data", async () => {
    const collection = await buildCollection();
    const document = await buildDocument({ collectionId: collection.id });
    await collection.reload();

    document.title = "Updated title";
    await document.save();
    await collection.updateDocument(document);
    expect(collection.documentStructure![0].title).toBe("Updated title");
  });

  it("should update child document's data", async () => {
    const collection = await buildCollection();
    const document = await buildDocument({ collectionId: collection.id });
    await collection.reload();

    const newDocument = await buildDocument({
      parentDocumentId: document.id,
      collectionId: collection.id,
      teamId: collection.teamId,
      lastModifiedById: collection.createdById,
      createdById: collection.createdById,
      title: "Child document",
      text: "content",
    });
    await collection.addDocumentToStructure(newDocument);
    newDocument.title = "Updated title";
    await newDocument.save();
    await collection.updateDocument(newDocument);
    const reloaded = await collection.reload();
    expect(reloaded!.documentStructure![0].children[0].title).toBe(
      "Updated title"
    );
  });
});

describe("#removeDocument", () => {
  it("should save if removing", async () => {
    const collection = await buildCollection();
    const user = await buildUser({ teamId: collection.teamId });
    const document = await buildDocument({ collectionId: collection.id });
    await collection.reload();

    const saveSpy = vi.spyOn(collection, "save");
    await collection.deleteDocument(createContext({ user }), document);
    expect(saveSpy).toHaveBeenCalled();
  });

  it("should remove documents from root", async () => {
    const collection = await buildCollection();
    const user = await buildUser({ teamId: collection.teamId });
    const document = await buildDocument({ collectionId: collection.id });
    await collection.reload();

    await collection.deleteDocument(createContext({ user }), document);
    expect(collection.documentStructure!.length).toBe(0);
    // Verify that the document was removed
    const collectionDocuments = await Document.findAndCountAll({
      where: {
        collectionId: collection.id,
      },
    });
    expect(collectionDocuments.count).toBe(0);
  });

  it("should remove a document with child documents", async () => {
    const collection = await buildCollection();
    const user = await buildUser({ teamId: collection.teamId });
    const document = await buildDocument({ collectionId: collection.id });
    await collection.reload();

    // Add a child for testing
    const newDocument = await buildDocument({
      parentDocumentId: document.id,
      collectionId: collection.id,
      teamId: collection.teamId,
      lastModifiedById: collection.createdById,
      createdById: collection.createdById,
      title: "Child document",
      text: "content",
    });
    await collection.addDocumentToStructure(newDocument);
    expect(collection.documentStructure![0].children.length).toBe(1);
    // Remove the document
    await collection.deleteDocument(createContext({ user }), document);
    expect(collection.documentStructure!.length).toBe(0);
    const collectionDocuments = await Document.findAndCountAll({
      where: {
        collectionId: collection.id,
      },
    });
    expect(collectionDocuments.count).toBe(0);
  });

  it("should remove a document with deeply nested child documents", async () => {
    const collection = await buildCollection();
    const user = await buildUser({ teamId: collection.teamId });
    const document = await buildDocument({ collectionId: collection.id });
    await collection.reload();

    let parentDocumentId = document.id;
    for (let depth = 0; depth < 5; depth++) {
      const child = await buildDocument({
        parentDocumentId,
        collectionId: collection.id,
        teamId: collection.teamId,
        lastModifiedById: collection.createdById,
        createdById: collection.createdById,
        title: `Child document ${depth}`,
        text: "content",
      });
      await collection.addDocumentToStructure(child);
      parentDocumentId = child.id;
    }

    await collection.deleteDocument(createContext({ user }), document);
    expect(collection.documentStructure!.length).toBe(0);
    const collectionDocuments = await Document.findAndCountAll({
      where: {
        collectionId: collection.id,
      },
    });
    expect(collectionDocuments.count).toBe(0);
  });

  it("should remove a child document", async () => {
    const collection = await buildCollection();
    const user = await buildUser({ teamId: collection.teamId });
    const document = await buildDocument({ collectionId: collection.id });
    await collection.reload();

    // Add a child for testing
    const newDocument = await buildDocument({
      parentDocumentId: document.id,
      collectionId: collection.id,
      teamId: collection.teamId,
      lastModifiedById: collection.createdById,
      createdById: collection.createdById,
      publishedAt: new Date(),
      title: "Child document",
      text: "content",
    });
    await collection.addDocumentToStructure(newDocument);
    expect(collection.documentStructure!.length).toBe(1);
    expect(collection.documentStructure![0].children.length).toBe(1);
    // Remove the document
    await collection.deleteDocument(createContext({ user }), newDocument);
    const reloaded = await collection.reload();
    expect(reloaded!.documentStructure!.length).toBe(1);
    expect(reloaded!.documentStructure![0].children.length).toBe(0);
    const collectionDocuments = await Document.findAndCountAll({
      where: {
        collectionId: collection.id,
      },
    });
    expect(collectionDocuments.count).toBe(1);
  });
});

describe("#membershipUserIds", () => {
  it("should return collection and group memberships", async () => {
    const team = await buildTeam();
    const teamId = team.id;
    // Make 6 users
    const users = await Promise.all(
      Array(6)
        .fill(undefined)
        .map(() =>
          buildUser({
            teamId,
          })
        )
    );
    const collection = await buildCollection({
      userId: users[0].id,
      permission: null,
      teamId,
    });
    const group1 = await buildGroup({
      teamId,
    });
    const group2 = await buildGroup({
      teamId,
    });
    const createdById = users[0].id;
    await group1.$add("user", users[0], {
      through: {
        createdById,
      },
    });
    await group1.$add("user", users[1], {
      through: {
        createdById,
      },
    });
    await group2.$add("user", users[2], {
      through: {
        createdById,
      },
    });
    await group2.$add("user", users[3], {
      through: {
        createdById,
      },
    });
    await collection.$add("user", users[4], {
      through: {
        createdById,
      },
    });
    await collection.$add("user", users[5], {
      through: {
        createdById,
      },
    });
    await collection.$add("group", group1, {
      through: {
        createdById,
      },
    });
    await collection.$add("group", group2, {
      through: {
        createdById,
      },
    });
    const membershipUserIds = await Collection.membershipUserIds(collection.id);
    expect(membershipUserIds.length).toBe(6);
  });
});

describe("#findByPk", () => {
  it("should return collection with collection Id", async () => {
    const collection = await buildCollection();
    const response = await Collection.findByPk(collection.id);
    expect(response!.id).toBe(collection.id);
  });

  it("should not allow a passed where to override the id", async () => {
    const collection = await buildCollection();
    const other = await buildCollection();

    const response = await Collection.findByPk(collection.id, {
      where: { id: other.id },
    });
    expect(response!.id).toBe(collection.id);

    const byUrlId = await Collection.findByPk(collection.urlId, {
      where: { urlId: other.urlId },
    });
    expect(byUrlId!.id).toBe(collection.id);
  });

  it("should throw the passed error when rejectOnEmpty is an error", async () => {
    const error = new Error("does not exist");
    await expect(
      Collection.findByPk("0e8280ea-7b4c-40e5-98ba-ec8a2f00f5e8", {
        rejectOnEmpty: error,
      })
    ).rejects.toThrow(error);
  });

  it("should throw when rejectOnEmpty is set and the id is malformed", async () => {
    await expect(
      Collection.findByPk("not a valid id", { rejectOnEmpty: true })
    ).rejects.toThrow(EmptyResultError);

    const error = new Error("does not exist");
    await expect(
      Collection.findByPk("not a valid id", { rejectOnEmpty: error })
    ).rejects.toThrow(error);
  });

  it("should throw when rejectOnEmpty is set and the id is not a string", async () => {
    await expect(
      Collection.findByPk(123, { rejectOnEmpty: true })
    ).rejects.toThrow(EmptyResultError);
  });

  it("should not return documentStructure by default", async () => {
    const collection = await buildCollection();
    const response = await Collection.findByPk(collection.id);
    expect(() => response!.documentStructure).toThrow();
  });

  it("should return collection when urlId is present", async () => {
    const collection = await buildCollection();
    const id = `${slugify(collection.name)}-${collection.urlId}`;
    const response = await Collection.findByPk(id);
    expect(response!.id).toBe(collection.id);
  });

  it("should return collection when urlId is present, but missing slug", async () => {
    const collection = await buildCollection();
    const id = collection.urlId;
    const response = await Collection.findByPk(id);
    expect(response!.id).toBe(collection.id);
  });

  it("should return null when incorrect uuid type", async () => {
    const collection = await buildCollection();
    const response = await Collection.findByPk(collection.id + "-incorrect");
    expect(response).toBe(null);
  });

  it("should return null when incorrect urlId length", async () => {
    const collection = await buildCollection();
    const id = `${slugify(collection.name)}-${collection.urlId}incorrect`;
    const response = await Collection.findByPk(id);
    expect(response).toBe(null);
  });

  it("should return null when no collection is found with uuid", async () => {
    const response = await Collection.findByPk(
      "a9e71a81-7342-4ea3-9889-9b9cc8f667da"
    );
    expect(response).toBe(null);
  });

  it("should return null when no collection is found with urlId", async () => {
    const id = `${slugify("test collection")}-${randomString(15)}`;
    const response = await Collection.findByPk(id);
    expect(response).toBe(null);
  });
});

describe("#setIndex", () => {
  it("should set index before creating a collection", async () => {
    const collection = await buildCollection();
    expect(collection.index).not.toBeNull();
  });

  it("should resolve index collision when creating a collection", async () => {
    const collection = await buildCollection();
    const anotherCollection = await buildCollection({
      teamId: collection.teamId,
      index: collection.index,
    });
    expect(anotherCollection.index).not.toBeNull();
    expect(anotherCollection.index).not.toEqual(collection.index);
  });

  it("should ensure only transaction is used for finding the first collection for team", async () => {
    const collection = await buildCollection();
    const [anotherCollection] = await Collection.findOrCreate({
      where: {
        name: "Another collection",
        teamId: collection.teamId,
      },
      defaults: {
        createdById: collection.createdById,
        index: collection.index,
      },
    });
    expect(anotherCollection.index).not.toBeNull();
    expect(anotherCollection.index).not.toEqual(collection.index);
  });
});

describe("#archiveWithCtx", () => {
  it("should archive the collection and its non-archived documents", async () => {
    const team = await buildTeam();
    const user = await buildUser({ teamId: team.id });
    const collection = await buildCollection({ teamId: team.id });
    const document = await buildDocument({
      collectionId: collection.id,
      teamId: team.id,
      publishedAt: new Date(),
    });
    const alreadyArchived = await buildDocument({
      collectionId: collection.id,
      teamId: team.id,
      publishedAt: new Date(),
      archivedAt: new Date("2024-01-01"),
    });

    await withAPIContext(user, (ctx) => collection.archiveWithCtx(ctx));

    await Promise.all([
      collection.reload(),
      document.reload(),
      alreadyArchived.reload(),
    ]);

    expect(collection.archivedAt).not.toBeNull();
    expect(collection.archivedById).toBe(user.id);
    expect(document.archivedAt).not.toBeNull();
    expect(document.archivedAt?.getTime()).toBe(
      collection.archivedAt!.getTime()
    );
    expect(document.lastModifiedById).toBe(user.id);
    // Previously-archived documents keep their original archivedAt timestamp.
    expect(alreadyArchived.archivedAt?.getTime()).toBe(
      new Date("2024-01-01").getTime()
    );
  });

  it("should leave documents in other collections untouched", async () => {
    const team = await buildTeam();
    const user = await buildUser({ teamId: team.id });
    const collection = await buildCollection({ teamId: team.id });
    const otherCollection = await buildCollection({ teamId: team.id });
    const otherDocument = await buildDocument({
      collectionId: otherCollection.id,
      teamId: team.id,
      publishedAt: new Date(),
    });

    await withAPIContext(user, (ctx) => collection.archiveWithCtx(ctx));

    await otherDocument.reload();
    expect(otherDocument.archivedAt).toBeNull();
  });
});