Files
outline/server/models/Collection.test.ts
Corey Alexander 2449434fef feat: Allow Document to be fetched without Slug (#3453)
* Allow Document to be fetched without Slug

Fixes #3423

This PR refactors the `Document.findByPk` method to not require the
`slug` portion of the urlID.

Before this function accepted two different 'formats' for the ID.

 - The `uuid` ID of the Document
 - The full `urlID` which looked something like
   `some-document-1234567890`

However the `some-document` slug portion of this identifier wasn't
actually used when looking for a document.

We now allow searching by JUST the postfix of the `urlID`, in the above
example that is `1234567890`.
We do this via a new Regex pattern to match on that just looks for the
right looking id alone, without the prefix.

This codepath looks the same as when we find it by the full `urlID`
besides the different regex that we match on.

The issue #3423 mentions that this should apply to all the API
endpoints. I believe that this `findByPk` method is all that should be
needed for that change. But if this is incorrect, OR you would like more
test coverage on the API endpoints as a more 'end to end test' please
let me know!

* Change original regex to make the slug optional

This has the, I believe to be good, side-effect of making the same logic
apply to `Collection` as well. Since `Collection` was always doing the
same stripping of the slug before the lookup I believe it should be just
as safe to do there.

We don't have to touch the code in Collections but we add a test of this
behavior there as well.

* No reason to rename this now that we aren't doing two matches
2022-04-26 20:49:37 -07:00

408 lines
13 KiB
TypeScript

import randomstring from "randomstring";
import { v4 as uuidv4 } from "uuid";
import {
buildUser,
buildGroup,
buildCollection,
buildTeam,
buildDocument,
} from "@server/test/factories";
import { flushdb, seed } from "@server/test/support";
import slugify from "@server/utils/slugify";
import Collection from "./Collection";
import Document from "./Document";
beforeEach(() => flushdb());
beforeEach(jest.resetAllMocks);
describe("#url", () => {
test("should return correct url for the collection", () => {
const collection = new Collection({
id: "1234",
});
expect(collection.url).toBe(`/collection/untitled-${collection.urlId}`);
});
});
describe("getDocumentParents", () => {
test("should return array of parent document ids", async () => {
const parent = await buildDocument();
const document = await buildDocument();
const collection = await buildCollection({
documentStructure: [
{ ...parent.toJSON(), children: [document.toJSON()] },
],
});
const result = collection.getDocumentParents(document.id);
expect(result?.length).toBe(1);
expect(result[0]).toBe(parent.id);
});
test("should return array of parent document ids", async () => {
const parent = await buildDocument();
const document = await buildDocument();
const collection = await buildCollection({
documentStructure: [
{ ...parent.toJSON(), children: [document.toJSON()] },
],
});
const result = collection.getDocumentParents(parent.id);
expect(result?.length).toBe(0);
});
test("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", () => {
test("should return document tree", async () => {
const document = await buildDocument();
const collection = await buildCollection({
documentStructure: [document.toJSON()],
});
expect(collection.getDocumentTree(document.id)).toEqual(document.toJSON());
});
test("should return nested documents in tree", async () => {
const parent = await buildDocument();
const document = await buildDocument();
const collection = await buildCollection({
documentStructure: [
{ ...parent.toJSON(), children: [document.toJSON()] },
],
});
expect(collection.getDocumentTree(parent.id)).toEqual({
...parent.toJSON(),
children: [document.toJSON()],
});
expect(collection.getDocumentTree(document.id)).toEqual(document.toJSON());
});
});
describe("#addDocumentToStructure", () => {
test("should add as last element without index", async () => {
const { collection } = await seed();
const id = uuidv4();
const newDocument = new Document({
id,
title: "New end node",
parentDocumentId: null,
});
await collection.addDocumentToStructure(newDocument);
expect(collection.documentStructure!.length).toBe(2);
expect(collection.documentStructure![1].id).toBe(id);
});
test("should add with an index", async () => {
const { collection } = await seed();
const id = uuidv4();
const newDocument = new Document({
id,
title: "New end node",
parentDocumentId: null,
});
await collection.addDocumentToStructure(newDocument, 1);
expect(collection.documentStructure!.length).toBe(2);
expect(collection.documentStructure![1].id).toBe(id);
});
test("should add as a child if with parent", async () => {
const { collection, document } = await seed();
const id = uuidv4();
const newDocument = new Document({
id,
title: "New end node",
parentDocumentId: document.id,
});
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);
});
test("should add as a child if with parent with index", async () => {
const { collection, document } = await seed();
const newDocument = new Document({
id: uuidv4(),
title: "node",
parentDocumentId: document.id,
});
const id = uuidv4();
const secondDocument = new Document({
id,
title: "New start node",
parentDocumentId: document.id,
});
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);
});
describe("options: documentJson", () => {
test("should append supplied json over document's own", async () => {
const { collection } = await seed();
const id = uuidv4();
const newDocument = new Document({
id: uuidv4(),
title: "New end node",
parentDocumentId: null,
});
await collection.addDocumentToStructure(newDocument, undefined, {
documentJson: {
id,
title: "Parent",
url: "parent",
children: [
{
id,
title: "Totally fake",
children: [],
url: "totally-fake",
},
],
},
});
expect(collection.documentStructure![1].children.length).toBe(1);
expect(collection.documentStructure![1].children[0].id).toBe(id);
});
});
});
describe("#updateDocument", () => {
test("should update root document's data", async () => {
const { collection, document } = await seed();
document.title = "Updated title";
await document.save();
await collection.updateDocument(document);
expect(collection.documentStructure![0].title).toBe("Updated title");
});
test("should update child document's data", async () => {
const { collection, document } = await seed();
const newDocument = await Document.create({
parentDocumentId: document.id,
collectionId: collection.id,
teamId: collection.teamId,
userId: collection.createdById,
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.findByPk(collection.id);
expect(reloaded!.documentStructure![0].children[0].title).toBe(
"Updated title"
);
});
});
describe("#removeDocument", () => {
test("should save if removing", async () => {
const { collection, document } = await seed();
jest.spyOn(collection, "save");
await collection.deleteDocument(document);
expect(collection.save).toBeCalled();
});
test("should remove documents from root", async () => {
const { collection, document } = await seed();
await collection.deleteDocument(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);
});
test("should remove a document with child documents", async () => {
const { collection, document } = await seed();
// Add a child for testing
const newDocument = await Document.create({
parentDocumentId: document.id,
collectionId: collection.id,
teamId: collection.teamId,
userId: collection.createdById,
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(document);
expect(collection.documentStructure!.length).toBe(0);
const collectionDocuments = await Document.findAndCountAll({
where: {
collectionId: collection.id,
},
});
expect(collectionDocuments.count).toBe(0);
});
test("should remove a child document", async () => {
const { collection, document } = await seed();
// Add a child for testing
const newDocument = await Document.create({
parentDocumentId: document.id,
collectionId: collection.id,
teamId: collection.teamId,
userId: collection.createdById,
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(newDocument);
const reloaded = await Collection.findByPk(collection.id);
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", () => {
test("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", () => {
test("should return collection with collection Id", async () => {
const collection = await buildCollection();
const response = await Collection.findByPk(collection.id);
expect(response!.id).toBe(collection.id);
});
test("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);
});
test("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);
});
test("should return undefined when incorrect uuid type", async () => {
const collection = await buildCollection();
const response = await Collection.findByPk(collection.id + "-incorrect");
expect(response).toBe(undefined);
});
test("should return undefined 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(undefined);
});
test("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);
});
test("should return null when no collection is found with urlId", async () => {
const id = `${slugify("test collection")}-${randomstring.generate(15)}`;
const response = await Collection.findByPk(id);
expect(response).toBe(null);
});
});