/** * Unit tests for searchRows — covers the SQL fallback contract: tenant * + entity scoping, value-text predicate, limit, and total accounting. */ import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("~/utils/db.server", () => ({ db: { row: { findMany: vi.fn(), }, }, })); import { db } from "~/utils/db.server"; import { searchRows } from "./rowSearch.server"; const dbMock = db as unknown as { row: { findMany: ReturnType; }; }; const originalMeili = process.env.MEILI_URL; const originalBackend = process.env.SEARCH_BACKEND; afterAll(() => { if (originalMeili === undefined) delete process.env.MEILI_URL; else process.env.MEILI_URL = originalMeili; if (originalBackend === undefined) delete process.env.SEARCH_BACKEND; else process.env.SEARCH_BACKEND = originalBackend; }); describe("searchRows (sql fallback)", () => { beforeEach(() => { delete process.env.MEILI_URL; delete process.env.SEARCH_BACKEND; dbMock.row.findMany.mockReset(); }); it("scopes by tenantId + entityId and maps results", async () => { dbMock.row.findMany.mockResolvedValueOnce([ { id: "row-1", folio: 12, entity: { name: "deal" } }, { id: "row-2", folio: 7, entity: { name: "deal" } }, ]); const result = await searchRows({ tenantId: "tenant-1", entityId: "entity-1", query: "acme" }); expect(dbMock.row.findMany).toHaveBeenCalledTimes(1); const where = dbMock.row.findMany.mock.calls[0][0].where as Record; expect(where.tenantId).toBe("tenant-1"); expect(where.entityId).toBe("entity-1"); expect(where.deletedAt).toBeNull(); expect(where.values).toBeDefined(); expect(result.fellBack).toBe(true); expect(result.hits).toEqual([ { id: "row-1", folio: 12, entityName: "deal" }, { id: "row-2", folio: 7, entityName: "deal" }, ]); expect(result.total).toBe(2); }); it("omits value-text predicate when the query is empty", async () => { dbMock.row.findMany.mockResolvedValueOnce([]); await searchRows({ tenantId: "tenant-1", entityId: "entity-1", query: "" }); const where = dbMock.row.findMany.mock.calls[0][0].where as Record; expect(where.values).toBeUndefined(); }); it("honours the limit option", async () => { dbMock.row.findMany.mockResolvedValueOnce([]); await searchRows({ tenantId: "tenant-1", entityId: "entity-1", query: "x", limit: 25 }); expect(dbMock.row.findMany.mock.calls[0][0].take).toBe(25); }); it("defaults folio to 0 when missing on the row", async () => { dbMock.row.findMany.mockResolvedValueOnce([{ id: "row-x", folio: null, entity: null }]); const result = await searchRows({ tenantId: "tenant-1", entityId: "entity-1", query: "x" }); expect(result.hits[0]).toEqual({ id: "row-x", folio: 0, entityName: "" }); }); });