mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
091346dfe8
* wip * Remove obsolete snapshots * simplify * chore(test): Convert mocks to TypeScript and tighten fetch mock types Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Remove unneccessary patches * Migrate to msw instead of custom fetch mock * Address PR review comments - Split chained vi.useFakeTimers().setSystemTime() into separate calls. - Switch test setup to dynamic imports so EventEmitter.defaultMaxListeners assignment runs before module init (static imports were hoisted above it). - Drop redundant NODE_ENV guard in monkeyPatchSequelizeErrorsForJest; its sole caller already gates on env.isTest. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1417 lines
42 KiB
TypeScript
1417 lines
42 KiB
TypeScript
import {
|
|
DirectionFilter,
|
|
DocumentPermission,
|
|
SortFilter,
|
|
StatusFilter,
|
|
} from "@shared/types";
|
|
import {
|
|
buildDocument,
|
|
buildDraftDocument,
|
|
buildCollection,
|
|
buildTeam,
|
|
buildUser,
|
|
buildShare,
|
|
buildGroup,
|
|
} from "@server/test/factories";
|
|
import UserMembership from "@server/models/UserMembership";
|
|
import GroupMembership from "@server/models/GroupMembership";
|
|
import SearchProviderManager from "@server/utils/SearchProviderManager";
|
|
import PostgresSearchProvider from "./PostgresSearchProvider";
|
|
|
|
const provider = SearchProviderManager.getProvider();
|
|
|
|
beforeEach(async () => {
|
|
vi.resetAllMocks();
|
|
await buildDocument();
|
|
});
|
|
|
|
describe("PostgresSearchProvider", () => {
|
|
describe("#searchForTeam", () => {
|
|
it("should return search results from public collections", async () => {
|
|
const team = await buildTeam();
|
|
const collection = await buildCollection({
|
|
teamId: team.id,
|
|
});
|
|
const document = await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test",
|
|
});
|
|
const { results } = await provider.searchForTeam(team, {
|
|
query: "test",
|
|
});
|
|
expect(results.length).toBe(1);
|
|
expect(results[0].document?.id).toBe(document.id);
|
|
});
|
|
|
|
it("should return search results from a collection without search term", async () => {
|
|
const team = await buildTeam();
|
|
const collection = await buildCollection({
|
|
teamId: team.id,
|
|
});
|
|
const documents = await Promise.all([
|
|
buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "document 1",
|
|
}),
|
|
buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "document 2",
|
|
}),
|
|
]);
|
|
const { results } = await provider.searchForTeam(team);
|
|
expect(results.length).toBe(2);
|
|
expect(results.map((r) => r.document.id).sort()).toEqual(
|
|
documents.map((doc) => doc.id).sort()
|
|
);
|
|
});
|
|
|
|
it("should not return results from private collections without providing collectionId", async () => {
|
|
const team = await buildTeam();
|
|
const collection = await buildCollection({
|
|
permission: null,
|
|
teamId: team.id,
|
|
});
|
|
await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test",
|
|
});
|
|
const { results } = await provider.searchForTeam(team, {
|
|
query: "test",
|
|
});
|
|
expect(results.length).toBe(0);
|
|
});
|
|
|
|
it("should return results from private collections when collectionId is provided", async () => {
|
|
const team = await buildTeam();
|
|
const collection = await buildCollection({
|
|
permission: null,
|
|
teamId: team.id,
|
|
});
|
|
await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test",
|
|
});
|
|
const { results } = await provider.searchForTeam(team, {
|
|
query: "test",
|
|
collectionId: collection.id,
|
|
});
|
|
expect(results.length).toBe(1);
|
|
});
|
|
|
|
it("should return results from document tree of shared document", async () => {
|
|
const team = await buildTeam();
|
|
const collection = await buildCollection({
|
|
permission: null,
|
|
teamId: team.id,
|
|
});
|
|
const document = await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test 1",
|
|
});
|
|
await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test 2",
|
|
});
|
|
|
|
const share = await buildShare({
|
|
documentId: document.id,
|
|
includeChildDocuments: true,
|
|
});
|
|
|
|
const { results } = await provider.searchForTeam(team, {
|
|
query: "test",
|
|
collectionId: collection.id,
|
|
share,
|
|
});
|
|
expect(results.length).toBe(1);
|
|
});
|
|
|
|
it("should handle no collections", async () => {
|
|
const team = await buildTeam();
|
|
const { results } = await provider.searchForTeam(team, {
|
|
query: "test",
|
|
});
|
|
expect(results.length).toBe(0);
|
|
});
|
|
|
|
it("should handle backslashes in search term", async () => {
|
|
const team = await buildTeam();
|
|
const collection = await buildCollection({
|
|
teamId: team.id,
|
|
});
|
|
const document = await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test with backslash \\",
|
|
});
|
|
const { results } = await provider.searchForTeam(team, {
|
|
query: "test with backslash \\",
|
|
});
|
|
expect(results.length).toBe(1);
|
|
expect(results[0].document?.id).toBe(document.id);
|
|
});
|
|
|
|
it("should return the total count of search results", async () => {
|
|
const team = await buildTeam();
|
|
const collection = await buildCollection({
|
|
teamId: team.id,
|
|
});
|
|
await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test number 1",
|
|
});
|
|
await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test number 2",
|
|
});
|
|
const { total } = await provider.searchForTeam(team, {
|
|
query: "test",
|
|
});
|
|
expect(total).toBe(2);
|
|
});
|
|
|
|
it("should return the document when searched with their previous titles", async () => {
|
|
const team = await buildTeam();
|
|
const collection = await buildCollection({
|
|
teamId: team.id,
|
|
});
|
|
const document = await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test number 1",
|
|
});
|
|
document.title = "change";
|
|
await document.save();
|
|
const { total } = await provider.searchForTeam(team, {
|
|
query: "test number",
|
|
});
|
|
expect(total).toBe(1);
|
|
});
|
|
|
|
it("should not return the document when searched with neither the titles nor the previous titles", async () => {
|
|
const team = await buildTeam();
|
|
const collection = await buildCollection({
|
|
teamId: team.id,
|
|
});
|
|
const document = await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test number 1",
|
|
});
|
|
document.title = "change";
|
|
await document.save();
|
|
const { total } = await provider.searchForTeam(team, {
|
|
query: "title doesn't exist",
|
|
});
|
|
expect(total).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("#searchForUser", () => {
|
|
it("should return search results from collections", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const collection = await buildCollection({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
});
|
|
const document = await buildDocument({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
deletedAt: new Date(),
|
|
title: "test",
|
|
});
|
|
const { results } = await provider.searchForUser(user, {
|
|
query: "test",
|
|
});
|
|
expect(results.length).toBe(1);
|
|
expect(results[0].ranking).toBeTruthy();
|
|
expect(results[0].document?.id).toBe(document.id);
|
|
});
|
|
|
|
it("should return search results for a user without search term", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const collection = await buildCollection({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
});
|
|
const documents = await Promise.all([
|
|
buildDocument({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
collectionId: collection.id,
|
|
title: "document 1",
|
|
}),
|
|
buildDocument({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
collectionId: collection.id,
|
|
title: "document 2",
|
|
}),
|
|
]);
|
|
const { results } = await provider.searchForUser(user);
|
|
expect(results.length).toBe(2);
|
|
expect(results.map((r) => r.document.id).sort()).toEqual(
|
|
documents.map((doc) => doc.id).sort()
|
|
);
|
|
});
|
|
|
|
it("should return search results from a collection without search term", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const collection = await buildCollection({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
});
|
|
const documents = await Promise.all([
|
|
buildDocument({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
collectionId: collection.id,
|
|
title: "document 1",
|
|
}),
|
|
buildDocument({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
collectionId: collection.id,
|
|
title: "document 2",
|
|
}),
|
|
]);
|
|
const { results } = await provider.searchForUser(user, {
|
|
collectionId: collection.id,
|
|
});
|
|
expect(results.length).toBe(2);
|
|
expect(results.map((r) => r.document.id).sort()).toEqual(
|
|
documents.map((doc) => doc.id).sort()
|
|
);
|
|
});
|
|
|
|
it("should not return documents from other collections when filtering by specific collection without search term", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const collection1 = await buildCollection({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
});
|
|
const collection2 = await buildCollection({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
});
|
|
const docsInCollection1 = await Promise.all([
|
|
buildDocument({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
collectionId: collection1.id,
|
|
title: "document 1 in collection 1",
|
|
}),
|
|
buildDocument({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
collectionId: collection1.id,
|
|
title: "document 2 in collection 1",
|
|
}),
|
|
]);
|
|
await Promise.all([
|
|
buildDocument({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
collectionId: collection2.id,
|
|
title: "document 1 in collection 2",
|
|
}),
|
|
buildDocument({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
collectionId: collection2.id,
|
|
title: "document 2 in collection 2",
|
|
}),
|
|
]);
|
|
const { results } = await provider.searchForUser(user, {
|
|
collectionId: collection1.id,
|
|
});
|
|
expect(results.length).toBe(2);
|
|
expect(results.map((r) => r.document.id).sort()).toEqual(
|
|
docsInCollection1.map((doc) => doc.id).sort()
|
|
);
|
|
});
|
|
|
|
it("should handle no collections", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const { results } = await provider.searchForUser(user, {
|
|
query: "test",
|
|
});
|
|
expect(results.length).toBe(0);
|
|
});
|
|
|
|
it("should search only drafts created by user", async () => {
|
|
const user = await buildUser();
|
|
await buildDraftDocument({
|
|
title: "test",
|
|
});
|
|
await buildDraftDocument({
|
|
teamId: user.teamId,
|
|
userId: user.id,
|
|
createdById: user.id,
|
|
title: "test",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
createdById: user.id,
|
|
title: "test",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
createdById: user.id,
|
|
title: "test",
|
|
archivedAt: new Date(),
|
|
});
|
|
const { results } = await provider.searchForUser(user, {
|
|
query: "test",
|
|
statusFilter: [StatusFilter.Draft],
|
|
});
|
|
expect(results.length).toBe(1);
|
|
});
|
|
|
|
it("should include drafts with no collection created by user", async () => {
|
|
const user = await buildUser();
|
|
const draft = await buildDraftDocument({
|
|
teamId: user.teamId,
|
|
userId: user.id,
|
|
createdById: user.id,
|
|
collectionId: null,
|
|
title: "test",
|
|
});
|
|
const { results } = await provider.searchForUser(user, {
|
|
query: "test",
|
|
statusFilter: [StatusFilter.Draft],
|
|
});
|
|
expect(results.length).toBe(1);
|
|
expect(results[0].document?.id).toBe(draft.id);
|
|
});
|
|
|
|
it("should not include drafts created by user in inaccessible collections", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const otherUser = await buildUser({ teamId: team.id });
|
|
const privateCollection = await buildCollection({
|
|
teamId: team.id,
|
|
userId: otherUser.id,
|
|
permission: null,
|
|
});
|
|
await buildDraftDocument({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
createdById: user.id,
|
|
collectionId: privateCollection.id,
|
|
title: "test",
|
|
});
|
|
const { results } = await provider.searchForUser(user, {
|
|
query: "test",
|
|
statusFilter: [StatusFilter.Draft],
|
|
});
|
|
expect(results.length).toBe(0);
|
|
});
|
|
|
|
it("should not include drafts with user read permission", async () => {
|
|
const user = await buildUser();
|
|
await buildDraftDocument({
|
|
title: "test",
|
|
});
|
|
const draft = await buildDraftDocument({
|
|
teamId: user.teamId,
|
|
userId: user.id,
|
|
createdById: user.id,
|
|
title: "test",
|
|
});
|
|
await UserMembership.create({
|
|
createdById: user.id,
|
|
documentId: draft.id,
|
|
userId: user.id,
|
|
permission: DocumentPermission.Read,
|
|
});
|
|
|
|
const { results } = await provider.searchForUser(user, {
|
|
query: "test",
|
|
statusFilter: [StatusFilter.Published, StatusFilter.Archived],
|
|
});
|
|
expect(results.length).toBe(0);
|
|
});
|
|
|
|
it("should search only published created by user", async () => {
|
|
const user = await buildUser();
|
|
await buildDocument({
|
|
title: "test",
|
|
});
|
|
await buildDraftDocument({
|
|
teamId: user.teamId,
|
|
userId: user.id,
|
|
createdById: user.id,
|
|
title: "test",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
createdById: user.id,
|
|
title: "test",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
createdById: user.id,
|
|
title: "test",
|
|
archivedAt: new Date(),
|
|
});
|
|
const { results } = await provider.searchForUser(user, {
|
|
query: "test",
|
|
statusFilter: [StatusFilter.Published],
|
|
});
|
|
expect(results.length).toBe(1);
|
|
});
|
|
|
|
it("should search only archived documents created by user", async () => {
|
|
const user = await buildUser();
|
|
await buildDocument({
|
|
title: "test",
|
|
});
|
|
await buildDraftDocument({
|
|
teamId: user.teamId,
|
|
userId: user.id,
|
|
createdById: user.id,
|
|
title: "test",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
createdById: user.id,
|
|
title: "test",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
createdById: user.id,
|
|
title: "test",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
createdById: user.id,
|
|
title: "test",
|
|
archivedAt: new Date(),
|
|
});
|
|
const { results } = await provider.searchForUser(user, {
|
|
query: "test",
|
|
statusFilter: [StatusFilter.Archived],
|
|
});
|
|
expect(results.length).toBe(1);
|
|
});
|
|
|
|
it("should return results from archived and published", async () => {
|
|
const user = await buildUser();
|
|
await buildDraftDocument({
|
|
teamId: user.teamId,
|
|
userId: user.id,
|
|
createdById: user.id,
|
|
title: "test",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
createdById: user.id,
|
|
title: "test",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
createdById: user.id,
|
|
title: "test",
|
|
archivedAt: new Date(),
|
|
});
|
|
const { results } = await provider.searchForUser(user, {
|
|
query: "test",
|
|
statusFilter: [StatusFilter.Archived, StatusFilter.Published],
|
|
});
|
|
expect(results.length).toBe(2);
|
|
});
|
|
|
|
it("should return results from drafts and published", async () => {
|
|
const user = await buildUser();
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
createdById: user.id,
|
|
title: "not draft",
|
|
});
|
|
await buildDraftDocument({
|
|
teamId: user.teamId,
|
|
userId: user.id,
|
|
createdById: user.id,
|
|
title: "draft",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
createdById: user.id,
|
|
title: "archived not draft",
|
|
archivedAt: new Date(),
|
|
});
|
|
const { results } = await provider.searchForUser(user, {
|
|
query: "draft",
|
|
statusFilter: [StatusFilter.Published, StatusFilter.Draft],
|
|
});
|
|
expect(results.length).toBe(2);
|
|
});
|
|
|
|
it("should include results from drafts and archived", async () => {
|
|
const user = await buildUser();
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
createdById: user.id,
|
|
title: "not draft",
|
|
});
|
|
await buildDraftDocument({
|
|
teamId: user.teamId,
|
|
userId: user.id,
|
|
createdById: user.id,
|
|
title: "draft",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
createdById: user.id,
|
|
title: "archived not draft",
|
|
archivedAt: new Date(),
|
|
});
|
|
const { results } = await provider.searchForUser(user, {
|
|
query: "draft",
|
|
statusFilter: [StatusFilter.Draft, StatusFilter.Archived],
|
|
});
|
|
expect(results.length).toBe(2);
|
|
});
|
|
|
|
it("should return the total count of search results", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const collection = await buildCollection({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test number 1",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test number 2",
|
|
});
|
|
const { total } = await provider.searchForUser(user, {
|
|
query: "test",
|
|
});
|
|
expect(total).toBe(2);
|
|
});
|
|
|
|
it("should return the document when searched with their previous titles", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const collection = await buildCollection({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
});
|
|
const document = await buildDocument({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
collectionId: collection.id,
|
|
title: "test number 1",
|
|
});
|
|
document.title = "change";
|
|
await document.save();
|
|
const { total } = await provider.searchForUser(user, {
|
|
query: "test number",
|
|
});
|
|
expect(total).toBe(1);
|
|
});
|
|
|
|
it("should not return the document when searched with neither the titles nor the previous titles", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const collection = await buildCollection({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
});
|
|
const document = await buildDocument({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
collectionId: collection.id,
|
|
title: "test number 1",
|
|
});
|
|
document.title = "change";
|
|
await document.save();
|
|
const { total } = await provider.searchForUser(user, {
|
|
query: "title doesn't exist",
|
|
});
|
|
expect(total).toBe(0);
|
|
});
|
|
|
|
it("should find exact phrases", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const collection = await buildCollection({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
});
|
|
const document = await buildDocument({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
collectionId: collection.id,
|
|
text: "test number 1",
|
|
});
|
|
document.title = "change";
|
|
await document.save();
|
|
const { total } = await provider.searchForUser(user, {
|
|
query: `"test number"`,
|
|
});
|
|
expect(total).toBe(1);
|
|
});
|
|
|
|
it("should correctly handle removal of trailing spaces", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const collection = await buildCollection({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
});
|
|
const document = await buildDocument({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
collectionId: collection.id,
|
|
text: "env: some env",
|
|
});
|
|
document.title = "change";
|
|
await document.save();
|
|
const { total } = await provider.searchForUser(user, {
|
|
query: "env: ",
|
|
});
|
|
expect(total).toBe(1);
|
|
});
|
|
|
|
it("should return search results from group memberships", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const otherUser = await buildUser({ teamId: team.id });
|
|
const collection = await buildCollection({
|
|
userId: otherUser.id,
|
|
teamId: team.id,
|
|
permission: null,
|
|
});
|
|
const document = await buildDocument({
|
|
userId: otherUser.id,
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "group test document",
|
|
});
|
|
|
|
await buildDocument({
|
|
userId: otherUser.id,
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "group test document 2",
|
|
});
|
|
|
|
const group = await buildGroup({
|
|
teamId: team.id,
|
|
});
|
|
await group.$add("user", user, {
|
|
through: {
|
|
createdById: otherUser.id,
|
|
},
|
|
});
|
|
|
|
await GroupMembership.create({
|
|
createdById: otherUser.id,
|
|
groupId: group.id,
|
|
documentId: document.id,
|
|
});
|
|
|
|
const { results } = await provider.searchForUser(user, {
|
|
query: "group test",
|
|
});
|
|
|
|
expect(results.length).toBe(1);
|
|
expect(results[0].ranking).toBeTruthy();
|
|
expect(results[0].document?.id).toBe(document.id);
|
|
});
|
|
});
|
|
|
|
describe("#searchTitlesForUser", () => {
|
|
it("should return search results from collections", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const collection = await buildCollection({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
});
|
|
const document = await buildDocument({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test",
|
|
});
|
|
const documents = await provider.searchTitlesForUser(user, {
|
|
query: "test",
|
|
});
|
|
expect(documents.length).toBe(1);
|
|
expect(documents[0]?.id).toBe(document.id);
|
|
});
|
|
|
|
it("should filter to specific collection", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const collection = await buildCollection({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
});
|
|
const collection1 = await buildCollection({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
});
|
|
const document = await buildDocument({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test",
|
|
});
|
|
await buildDraftDocument({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
title: "test",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
collectionId: collection1.id,
|
|
title: "test",
|
|
});
|
|
const documents = await provider.searchTitlesForUser(user, {
|
|
query: "test",
|
|
collectionId: collection.id,
|
|
});
|
|
expect(documents.length).toBe(1);
|
|
expect(documents[0]?.id).toBe(document.id);
|
|
});
|
|
|
|
it("should handle no collections", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const documents = await provider.searchTitlesForUser(user, {
|
|
query: "test",
|
|
});
|
|
expect(documents.length).toBe(0);
|
|
});
|
|
|
|
it("should search only drafts created by user", async () => {
|
|
const user = await buildUser();
|
|
await buildDraftDocument({
|
|
title: "test",
|
|
});
|
|
await buildDraftDocument({
|
|
teamId: user.teamId,
|
|
userId: user.id,
|
|
createdById: user.id,
|
|
title: "test",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
createdById: user.id,
|
|
title: "test",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
createdById: user.id,
|
|
title: "test",
|
|
archivedAt: new Date(),
|
|
});
|
|
const documents = await provider.searchTitlesForUser(user, {
|
|
query: "test",
|
|
statusFilter: [StatusFilter.Draft],
|
|
});
|
|
expect(documents.length).toBe(1);
|
|
});
|
|
|
|
it("should search only published created by user", async () => {
|
|
const user = await buildUser();
|
|
await buildDocument({
|
|
title: "test",
|
|
});
|
|
await buildDraftDocument({
|
|
teamId: user.teamId,
|
|
userId: user.id,
|
|
createdById: user.id,
|
|
title: "test",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
createdById: user.id,
|
|
title: "test",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
createdById: user.id,
|
|
title: "test",
|
|
archivedAt: new Date(),
|
|
});
|
|
const documents = await provider.searchTitlesForUser(user, {
|
|
query: "test",
|
|
statusFilter: [StatusFilter.Published],
|
|
});
|
|
expect(documents.length).toBe(1);
|
|
});
|
|
|
|
it("should search only archived documents created by user", async () => {
|
|
const user = await buildUser();
|
|
await buildDocument({
|
|
title: "test",
|
|
});
|
|
await buildDraftDocument({
|
|
teamId: user.teamId,
|
|
userId: user.id,
|
|
createdById: user.id,
|
|
title: "test",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
createdById: user.id,
|
|
title: "test",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
createdById: user.id,
|
|
title: "test",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
createdById: user.id,
|
|
title: "test",
|
|
archivedAt: new Date(),
|
|
});
|
|
const documents = await provider.searchTitlesForUser(user, {
|
|
query: "test",
|
|
statusFilter: [StatusFilter.Archived],
|
|
});
|
|
expect(documents.length).toBe(1);
|
|
});
|
|
|
|
it("should return results from archived and published", async () => {
|
|
const user = await buildUser();
|
|
await buildDraftDocument({
|
|
teamId: user.teamId,
|
|
userId: user.id,
|
|
createdById: user.id,
|
|
title: "test",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
createdById: user.id,
|
|
title: "test",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
createdById: user.id,
|
|
title: "test",
|
|
archivedAt: new Date(),
|
|
});
|
|
const documents = await provider.searchTitlesForUser(user, {
|
|
query: "test",
|
|
statusFilter: [StatusFilter.Archived, StatusFilter.Published],
|
|
});
|
|
expect(documents.length).toBe(2);
|
|
});
|
|
|
|
it("should return results from drafts and published", async () => {
|
|
const user = await buildUser();
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
createdById: user.id,
|
|
title: "not draft",
|
|
});
|
|
await buildDraftDocument({
|
|
teamId: user.teamId,
|
|
userId: user.id,
|
|
createdById: user.id,
|
|
title: "draft",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
createdById: user.id,
|
|
title: "archived not draft",
|
|
archivedAt: new Date(),
|
|
});
|
|
const documents = await provider.searchTitlesForUser(user, {
|
|
query: "draft",
|
|
statusFilter: [StatusFilter.Published, StatusFilter.Draft],
|
|
});
|
|
expect(documents.length).toBe(2);
|
|
});
|
|
|
|
it("should include results from drafts and archived", async () => {
|
|
const user = await buildUser();
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
createdById: user.id,
|
|
title: "not draft",
|
|
});
|
|
await buildDraftDocument({
|
|
teamId: user.teamId,
|
|
userId: user.id,
|
|
createdById: user.id,
|
|
title: "draft",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
createdById: user.id,
|
|
title: "archived not draft",
|
|
archivedAt: new Date(),
|
|
});
|
|
const documents = await provider.searchTitlesForUser(user, {
|
|
query: "draft",
|
|
statusFilter: [StatusFilter.Draft, StatusFilter.Archived],
|
|
});
|
|
expect(documents.length).toBe(2);
|
|
});
|
|
|
|
it("should return search results from group memberships", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const otherUser = await buildUser({ teamId: team.id });
|
|
const collection = await buildCollection({
|
|
userId: otherUser.id,
|
|
teamId: team.id,
|
|
permission: null,
|
|
});
|
|
const document = await buildDocument({
|
|
userId: otherUser.id,
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "group title test document",
|
|
});
|
|
|
|
await buildDocument({
|
|
userId: otherUser.id,
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "group title test document 2",
|
|
});
|
|
|
|
const group = await buildGroup({
|
|
teamId: team.id,
|
|
});
|
|
await group.$add("user", user, {
|
|
through: {
|
|
createdById: otherUser.id,
|
|
},
|
|
});
|
|
|
|
await GroupMembership.create({
|
|
createdById: otherUser.id,
|
|
groupId: group.id,
|
|
documentId: document.id,
|
|
});
|
|
|
|
const documents = await provider.searchTitlesForUser(user, {
|
|
query: "group title",
|
|
});
|
|
|
|
expect(documents.length).toBe(1);
|
|
expect(documents[0].id).toBe(document.id);
|
|
});
|
|
});
|
|
|
|
describe("#searchCollectionsForUser", () => {
|
|
it("should return search results from collections", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const collection1 = await buildCollection({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
name: "Test Collection",
|
|
});
|
|
await buildCollection({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
name: "Other Collection",
|
|
});
|
|
|
|
const results = await provider.searchCollectionsForUser(user, {
|
|
query: "test",
|
|
});
|
|
|
|
expect(results.length).toBe(1);
|
|
expect(results[0].id).toBe(collection1.id);
|
|
});
|
|
|
|
it("should return all collections when no query provided", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const collection1 = await buildCollection({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
name: "Alpha",
|
|
});
|
|
const collection2 = await buildCollection({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
name: "Beta",
|
|
});
|
|
|
|
const results = await provider.searchCollectionsForUser(user);
|
|
|
|
expect(results.length).toBe(2);
|
|
expect(results[0].id).toBe(collection1.id);
|
|
expect(results[1].id).toBe(collection2.id);
|
|
});
|
|
});
|
|
|
|
describe("sorting", () => {
|
|
it("should sort search results by title ascending", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const collection = await buildCollection({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
});
|
|
const doc1 = await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
userId: user.id,
|
|
title: "Zebra Document",
|
|
});
|
|
const doc2 = await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
userId: user.id,
|
|
title: "Alpha Document",
|
|
});
|
|
const doc3 = await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
userId: user.id,
|
|
title: "Beta Document",
|
|
});
|
|
|
|
const { results } = await provider.searchForUser(user, {
|
|
sort: SortFilter.Title,
|
|
direction: DirectionFilter.ASC,
|
|
});
|
|
|
|
expect(results.length).toBe(3);
|
|
expect(results[0].document.id).toBe(doc2.id);
|
|
expect(results[1].document.id).toBe(doc3.id);
|
|
expect(results[2].document.id).toBe(doc1.id);
|
|
});
|
|
|
|
it("should sort search results by title descending", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const collection = await buildCollection({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
});
|
|
const doc1 = await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
userId: user.id,
|
|
title: "Zebra Document",
|
|
});
|
|
const doc2 = await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
userId: user.id,
|
|
title: "Alpha Document",
|
|
});
|
|
const doc3 = await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
userId: user.id,
|
|
title: "Beta Document",
|
|
});
|
|
|
|
const { results } = await provider.searchForUser(user, {
|
|
sort: SortFilter.Title,
|
|
direction: DirectionFilter.DESC,
|
|
});
|
|
|
|
expect(results.length).toBe(3);
|
|
expect(results[0].document.id).toBe(doc1.id);
|
|
expect(results[1].document.id).toBe(doc3.id);
|
|
expect(results[2].document.id).toBe(doc2.id);
|
|
});
|
|
|
|
it("should sort search results by createdAt ascending", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const collection = await buildCollection({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
});
|
|
const doc1 = await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
userId: user.id,
|
|
title: "First Document",
|
|
createdAt: new Date("2023-01-01"),
|
|
updatedAt: new Date("2023-12-03"),
|
|
});
|
|
const doc2 = await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
userId: user.id,
|
|
title: "Second Document",
|
|
createdAt: new Date("2023-06-01"),
|
|
updatedAt: new Date("2023-12-02"),
|
|
});
|
|
const doc3 = await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
userId: user.id,
|
|
title: "Third Document",
|
|
createdAt: new Date("2023-12-01"),
|
|
updatedAt: new Date("2023-12-01"),
|
|
});
|
|
|
|
const { results } = await provider.searchForUser(user, {
|
|
sort: SortFilter.CreatedAt,
|
|
direction: DirectionFilter.ASC,
|
|
});
|
|
|
|
expect(results.length).toBe(3);
|
|
expect(results[0].document.id).toBe(doc1.id);
|
|
expect(results[1].document.id).toBe(doc2.id);
|
|
expect(results[2].document.id).toBe(doc3.id);
|
|
});
|
|
|
|
it("should sort search results by updatedAt descending by default", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const collection = await buildCollection({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
});
|
|
const doc1 = await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
userId: user.id,
|
|
title: "Document 1",
|
|
updatedAt: new Date("2023-01-01"),
|
|
});
|
|
const doc2 = await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
userId: user.id,
|
|
title: "Document 2",
|
|
updatedAt: new Date("2023-12-01"),
|
|
});
|
|
const doc3 = await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
userId: user.id,
|
|
title: "Document 3",
|
|
updatedAt: new Date("2023-06-01"),
|
|
});
|
|
|
|
const { results } = await provider.searchForUser(user);
|
|
|
|
expect(results.length).toBe(3);
|
|
expect(results[0].document.id).toBe(doc2.id);
|
|
expect(results[1].document.id).toBe(doc3.id);
|
|
expect(results[2].document.id).toBe(doc1.id);
|
|
});
|
|
|
|
it("should order results by search ranking when query is provided and no explicit sort", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const collection = await buildCollection({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
});
|
|
|
|
// Create a document with a less relevant title but more recent updatedAt
|
|
const recentDoc = await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
userId: user.id,
|
|
title: "unrelated recent",
|
|
text: "This document mentions search only once",
|
|
updatedAt: new Date("2025-12-01"),
|
|
});
|
|
|
|
// Create a document with a highly relevant title but older updatedAt
|
|
const relevantDoc = await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
userId: user.id,
|
|
title: "search search search",
|
|
text: "search search search search search",
|
|
updatedAt: new Date("2023-01-01"),
|
|
});
|
|
|
|
const { results } = await provider.searchForUser(user, {
|
|
query: "search",
|
|
});
|
|
|
|
expect(results.length).toBe(2);
|
|
// The more relevant document should come first, despite being older
|
|
expect(results[0].document.id).toBe(relevantDoc.id);
|
|
expect(results[1].document.id).toBe(recentDoc.id);
|
|
});
|
|
|
|
it("should use explicit sort over search ranking when sort is provided", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const collection = await buildCollection({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
});
|
|
|
|
await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
userId: user.id,
|
|
title: "search search search",
|
|
text: "search search search search search",
|
|
updatedAt: new Date("2023-01-01"),
|
|
});
|
|
|
|
const recentDoc = await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
userId: user.id,
|
|
title: "unrelated recent",
|
|
text: "This document mentions search only once",
|
|
updatedAt: new Date("2025-12-01"),
|
|
});
|
|
|
|
const { results } = await provider.searchForUser(user, {
|
|
query: "search",
|
|
sort: SortFilter.UpdatedAt,
|
|
direction: DirectionFilter.DESC,
|
|
});
|
|
|
|
expect(results.length).toBe(2);
|
|
// With explicit sort, updatedAt should take priority
|
|
expect(results[0].document.id).toBe(recentDoc.id);
|
|
});
|
|
});
|
|
|
|
describe("popularity boost", () => {
|
|
it("should not apply popularity boost when usePopularityBoost is false", async () => {
|
|
const team = await buildTeam();
|
|
const collection = await buildCollection({
|
|
teamId: team.id,
|
|
});
|
|
|
|
// Create a less relevant document with a high popularity score
|
|
await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "guide",
|
|
text: "This is a guide that mentions testing once",
|
|
popularityScore: 1000,
|
|
});
|
|
|
|
// Create a highly relevant document with no popularity
|
|
const relevantDoc = await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "testing testing testing",
|
|
text: "testing testing testing testing testing",
|
|
popularityScore: 0,
|
|
});
|
|
|
|
// Without popularity boost, pure relevance should win
|
|
const { results: withoutBoost } = await provider.searchForTeam(team, {
|
|
query: "testing",
|
|
usePopularityBoost: false,
|
|
});
|
|
|
|
expect(withoutBoost.length).toBe(2);
|
|
expect(withoutBoost[0].document.id).toBe(relevantDoc.id);
|
|
|
|
// With popularity boost, the popular document may rank higher
|
|
const { results: withBoost } = await provider.searchForTeam(team, {
|
|
query: "testing",
|
|
usePopularityBoost: true,
|
|
});
|
|
|
|
expect(withBoost.length).toBe(2);
|
|
// The popular document should have a higher ranking with boost
|
|
expect(withBoost[0].ranking).toBeGreaterThanOrEqual(
|
|
withoutBoost[0].ranking
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("webSearchQuery", () => {
|
|
it("should correctly sanitize query", () => {
|
|
expect(PostgresSearchProvider.webSearchQuery("one/two")).toBe(
|
|
"one/two:*"
|
|
);
|
|
expect(PostgresSearchProvider.webSearchQuery("one\\two")).toBe(
|
|
"one\\\\two:*"
|
|
);
|
|
expect(PostgresSearchProvider.webSearchQuery("test''")).toBe("test");
|
|
});
|
|
it("should wildcard unquoted queries", () => {
|
|
expect(PostgresSearchProvider.webSearchQuery("test")).toBe("test:*");
|
|
expect(PostgresSearchProvider.webSearchQuery("'")).toBe("");
|
|
expect(PostgresSearchProvider.webSearchQuery("'quoted'")).toBe(
|
|
`"quoted":*`
|
|
);
|
|
});
|
|
it("should wildcard multi-word queries", () => {
|
|
expect(PostgresSearchProvider.webSearchQuery("this is a test")).toBe(
|
|
"this&is&a&test:*"
|
|
);
|
|
});
|
|
it("should not wildcard quoted queries", () => {
|
|
expect(PostgresSearchProvider.webSearchQuery(`"this is a test"`)).toBe(
|
|
`"this<->is<->a<->test"`
|
|
);
|
|
});
|
|
});
|
|
});
|