test(search): cover cursor, snippet, and searchNotes query body
Some checks failed
CI / check (push) Failing after 10s
Some checks failed
CI / check (push) Failing after 10s
This commit is contained in:
204
tests/opensearch_test.ts
Normal file
204
tests/opensearch_test.ts
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import { assertEquals, assertRejects } from "@std/assert";
|
||||||
|
import {
|
||||||
|
decodeCursor,
|
||||||
|
encodeCursor,
|
||||||
|
firstSnippet,
|
||||||
|
searchNotes,
|
||||||
|
} from "../src/search/opensearch.ts";
|
||||||
|
import { installFetchStub, jsonResponse } from "./_helpers/fetch_stub.ts";
|
||||||
|
|
||||||
|
const withOpenSearchEnv = (fn: () => Promise<void> | void) => {
|
||||||
|
return async () => {
|
||||||
|
const previousUrl = Deno.env.get("OPENSEARCH_URL");
|
||||||
|
const previousIndex = Deno.env.get("OPENSEARCH_INDEX");
|
||||||
|
Deno.env.set("OPENSEARCH_URL", "http://opensearch.test");
|
||||||
|
Deno.env.set("OPENSEARCH_INDEX", "notes");
|
||||||
|
try {
|
||||||
|
await fn();
|
||||||
|
} finally {
|
||||||
|
if (previousUrl === undefined) Deno.env.delete("OPENSEARCH_URL");
|
||||||
|
else Deno.env.set("OPENSEARCH_URL", previousUrl);
|
||||||
|
if (previousIndex === undefined) Deno.env.delete("OPENSEARCH_INDEX");
|
||||||
|
else Deno.env.set("OPENSEARCH_INDEX", previousIndex);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
Deno.test("encodeCursor and decodeCursor round-trip", () => {
|
||||||
|
const cursor = encodeCursor(3.14, "abc123");
|
||||||
|
assertEquals(decodeCursor(cursor), [3.14, "abc123"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("decodeCursor returns undefined on garbage input", () => {
|
||||||
|
assertEquals(decodeCursor("not-base64-at-all!!!"), undefined);
|
||||||
|
assertEquals(decodeCursor(btoa("not-json")), undefined);
|
||||||
|
assertEquals(decodeCursor(btoa(JSON.stringify([1]))), undefined);
|
||||||
|
assertEquals(
|
||||||
|
decodeCursor(btoa(JSON.stringify(["wrong", "type"]))),
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("firstSnippet prefers highlight.content > title > source content > title fallback", () => {
|
||||||
|
assertEquals(
|
||||||
|
firstSnippet(
|
||||||
|
{ content: ["hl-content"], title: ["hl-title"] },
|
||||||
|
{ title: "src-title", content: "src-content" },
|
||||||
|
),
|
||||||
|
"hl-content",
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
firstSnippet({ title: ["hl-title"] }, {
|
||||||
|
title: "src-title",
|
||||||
|
content: "src-content",
|
||||||
|
}),
|
||||||
|
"hl-title",
|
||||||
|
);
|
||||||
|
const longContent = "a".repeat(500);
|
||||||
|
assertEquals(
|
||||||
|
firstSnippet(undefined, { content: longContent, title: "ignored" }).length,
|
||||||
|
200,
|
||||||
|
);
|
||||||
|
assertEquals(firstSnippet(undefined, { title: "only-title" }), "only-title");
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
"searchNotes returns empty when OPENSEARCH_URL is unset",
|
||||||
|
async () => {
|
||||||
|
const previousUrl = Deno.env.get("OPENSEARCH_URL");
|
||||||
|
Deno.env.delete("OPENSEARCH_URL");
|
||||||
|
try {
|
||||||
|
const stub = installFetchStub(() =>
|
||||||
|
jsonResponse({ should: "not be called" })
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const result = await searchNotes({
|
||||||
|
q: "hello",
|
||||||
|
discoverableOnly: true,
|
||||||
|
limit: 10,
|
||||||
|
});
|
||||||
|
assertEquals(result, { results: [] });
|
||||||
|
assertEquals(stub.calls.length, 0);
|
||||||
|
} finally {
|
||||||
|
stub.restore();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (previousUrl !== undefined) {
|
||||||
|
Deno.env.set("OPENSEARCH_URL", previousUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
"searchNotes builds multi_match body with filters and cursor",
|
||||||
|
withOpenSearchEnv(async () => {
|
||||||
|
const stub = installFetchStub(() =>
|
||||||
|
jsonResponse({
|
||||||
|
hits: {
|
||||||
|
hits: [
|
||||||
|
{
|
||||||
|
_source: {
|
||||||
|
did: "did:plc:a",
|
||||||
|
rkey: "rk-a",
|
||||||
|
title: "T",
|
||||||
|
content: "c",
|
||||||
|
},
|
||||||
|
_score: 1.5,
|
||||||
|
highlight: { content: ["snippet"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_source: {
|
||||||
|
did: "did:plc:b",
|
||||||
|
rkey: "rk-b",
|
||||||
|
title: "U",
|
||||||
|
content: "d",
|
||||||
|
},
|
||||||
|
_score: 1.0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const result = await searchNotes({
|
||||||
|
q: "hello world",
|
||||||
|
discoverableOnly: true,
|
||||||
|
did: "did:plc:a",
|
||||||
|
limit: 2,
|
||||||
|
cursor: encodeCursor(2.0, "rk-prev"),
|
||||||
|
});
|
||||||
|
assertEquals(result.results.length, 2);
|
||||||
|
assertEquals(result.results[0].snippet, "snippet");
|
||||||
|
assertEquals(result.results[1].snippet, "d");
|
||||||
|
assertEquals(result.cursor, encodeCursor(1.0, "rk-b"));
|
||||||
|
|
||||||
|
assertEquals(stub.calls.length, 1);
|
||||||
|
const sent = JSON.parse(stub.calls[0].body) as Record<string, unknown>;
|
||||||
|
assertEquals(sent.size, 2);
|
||||||
|
assertEquals(sent.search_after, [2.0, "rk-prev"]);
|
||||||
|
const query = sent.query as {
|
||||||
|
bool: { must: unknown[]; filter: unknown[] };
|
||||||
|
};
|
||||||
|
const multi =
|
||||||
|
(query.bool.must[0] as { multi_match: Record<string, unknown> })
|
||||||
|
.multi_match;
|
||||||
|
assertEquals(multi.query, "hello world");
|
||||||
|
assertEquals(multi.fields, ["title^2", "content"]);
|
||||||
|
assertEquals(multi.fuzziness, "AUTO");
|
||||||
|
assertEquals(query.bool.filter, [
|
||||||
|
{ term: { discoverable: true } },
|
||||||
|
{ term: { did: "did:plc:a" } },
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
stub.restore();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
"searchNotes returns cursor only when the page is full",
|
||||||
|
withOpenSearchEnv(async () => {
|
||||||
|
const stub = installFetchStub(() =>
|
||||||
|
jsonResponse({
|
||||||
|
hits: {
|
||||||
|
hits: [
|
||||||
|
{
|
||||||
|
_source: { did: "did:plc:a", rkey: "rk", title: "T" },
|
||||||
|
_score: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const result = await searchNotes({
|
||||||
|
q: "hi",
|
||||||
|
discoverableOnly: false,
|
||||||
|
limit: 5,
|
||||||
|
});
|
||||||
|
assertEquals(result.results.length, 1);
|
||||||
|
assertEquals(result.cursor, undefined);
|
||||||
|
} finally {
|
||||||
|
stub.restore();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
"searchNotes throws on non-ok response",
|
||||||
|
withOpenSearchEnv(async () => {
|
||||||
|
const stub = installFetchStub(() => new Response("boom", { status: 500 }));
|
||||||
|
try {
|
||||||
|
await assertRejects(() =>
|
||||||
|
searchNotes({
|
||||||
|
q: "hi",
|
||||||
|
discoverableOnly: false,
|
||||||
|
limit: 5,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
stub.restore();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user