Files
outline/plugins/search-postgres/server/PostgresSearchProvider.test.ts
T
Tom Moor 091346dfe8 chore: Migrate to vitest (#12272)
* 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>
2026-05-06 21:10:51 -04:00

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"`
);
});
});
});