mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-21 22:32:22 +03:00
Boarded with #11756 (a duplicate fix for the same underlying issue #11650). Compared both implementations directly: this one is technically superior — guards the json_extract() call with json_valid(metadata) so malformed/legacy metadata returns no match instead of throwing a 500, and covers genericBackend.ts/obsidianBackend.ts in addition to sqliteBackend.ts. #11756 only touched SQLite and had no malformed-JSON guard. Closing #11756 with credit. Resynced onto the updated release tip: the test file's `await import("../../src/lib/localDb.ts")` broke after #12055 deleted the barrel earlier this session (your branch forked before that migration) — fixed to import updateSettings directly from @/lib/db/settings, matching the pattern already used by other integration tests. typecheck:core, check:dashboard-typecheck, check:file-size, check:changelog-integrity, check:complexity, check:cognitive-complexity, check:cycles, check-deps all green; 4/4 integration + 35/35 vitest pass after resync. Thanks for the thorough, well-tested fix.
594 lines
22 KiB
TypeScript
594 lines
22 KiB
TypeScript
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
|
|
import { GenericMemoryBackend, createGenericMemoryBackend } from "../genericBackend";
|
|
import type { Memory } from "../types";
|
|
import { MemoryType } from "../types";
|
|
|
|
// ────────────────────────────────────────────────────────────
|
|
// GenericMemoryBackend — unit tests
|
|
// ────────────────────────────────────────────────────────────
|
|
|
|
const BASE_URL = "http://memory.test:8080";
|
|
const BACKEND_ID = "test-backend";
|
|
const BACKEND_NAME = "Test Backend";
|
|
|
|
const SAMPLE_MEMORY: Memory = {
|
|
id: "mem-001",
|
|
apiKeyId: "key-1",
|
|
sessionId: "sess-1",
|
|
type: MemoryType.FACTUAL,
|
|
key: "my-key",
|
|
content: "Hello world",
|
|
metadata: { source: "test" },
|
|
embedding: null,
|
|
createdAt: new Date("2026-01-01T00:00:00.000Z"),
|
|
updatedAt: new Date("2026-01-01T00:00:00.000Z"),
|
|
lastAccessedAt: new Date("2026-01-01T00:00:00.000Z"),
|
|
expiresAt: null,
|
|
};
|
|
|
|
const SAMPLE_MEMORY_JSON = {
|
|
...SAMPLE_MEMORY,
|
|
createdAt: "2026-01-01T00:00:00.000Z",
|
|
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
lastAccessedAt: "2026-01-01T00:00:00.000Z",
|
|
};
|
|
|
|
function createBackend(configOverrides: Record<string, unknown> = {}) {
|
|
return createGenericMemoryBackend(BACKEND_ID, BACKEND_NAME, {
|
|
baseUrl: BASE_URL,
|
|
...configOverrides,
|
|
});
|
|
}
|
|
|
|
describe("GenericMemoryBackend", () => {
|
|
let backend: GenericMemoryBackend;
|
|
|
|
beforeEach(() => {
|
|
vi.resetAllMocks();
|
|
backend = createBackend();
|
|
});
|
|
|
|
// ─── Constructor ─────────────────────────────────────────
|
|
|
|
describe("constructor", () => {
|
|
test("sets id and displayName from constructor args", () => {
|
|
expect(backend.id).toBe(BACKEND_ID);
|
|
expect(backend.displayName).toBe(BACKEND_NAME);
|
|
});
|
|
|
|
test("accepts custom timeout", () => {
|
|
const b = createBackend({ timeout: 5000 });
|
|
expect(b).toBeInstanceOf(GenericMemoryBackend);
|
|
});
|
|
});
|
|
|
|
// ─── Health ──────────────────────────────────────────────
|
|
|
|
describe("health()", () => {
|
|
test("returns ok=true when backend responds 200", async () => {
|
|
const fetchMock = vi
|
|
.spyOn(globalThis, "fetch")
|
|
.mockResolvedValue(new Response(JSON.stringify({ status: "ok" }), { status: 200 }));
|
|
|
|
const result = await backend.health();
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.latencyMs).toBeGreaterThanOrEqual(0);
|
|
expect(result.error).toBeUndefined();
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
`${BASE_URL}/health`,
|
|
expect.objectContaining({ method: "GET" })
|
|
);
|
|
});
|
|
|
|
test("returns ok=false when backend responds 500", async () => {
|
|
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
new Response("Internal error", { status: 500 })
|
|
);
|
|
|
|
const result = await backend.health();
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.error).toContain("HTTP 500");
|
|
});
|
|
|
|
test("returns ok=false on network failure", async () => {
|
|
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("ECONNREFUSED"));
|
|
|
|
const result = await backend.health();
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.error).toContain("ECONNREFUSED");
|
|
});
|
|
|
|
test("reports latency in ms", async () => {
|
|
const start = Date.now();
|
|
vi.spyOn(globalThis, "fetch").mockImplementation(
|
|
() =>
|
|
new Promise((r) =>
|
|
setTimeout(() => r(new Response(JSON.stringify({ status: "ok" }), { status: 200 })), 10)
|
|
)
|
|
);
|
|
|
|
const result = await backend.health();
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.latencyMs).toBeGreaterThanOrEqual(5);
|
|
});
|
|
});
|
|
|
|
// ─── Initialize ──────────────────────────────────────────
|
|
|
|
describe("initialize()", () => {
|
|
test("calls health and throws on failure", async () => {
|
|
vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("fail", { status: 503 }));
|
|
|
|
await expect(backend.initialize()).rejects.toThrow("Cannot connect to Test Backend");
|
|
});
|
|
|
|
test("passes when health succeeds", async () => {
|
|
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
new Response(JSON.stringify({ status: "ok" }), { status: 200 })
|
|
);
|
|
|
|
await expect(backend.initialize()).resolves.toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ─── CRUD helpers ────────────────────────────────────────
|
|
|
|
/**
|
|
* Set up a mock that health-check endpoint returns 200 while
|
|
* other endpoints return a custom response. This avoids the
|
|
* initialize() health gate.
|
|
*/
|
|
function mockHealthOkThen(secondResponse: Response) {
|
|
let callCount = 0;
|
|
return vi.spyOn(globalThis, "fetch").mockImplementation(async (url: string) => {
|
|
callCount++;
|
|
if (callCount === 1 && url.toString().endsWith("/health")) {
|
|
return new Response(JSON.stringify({ status: "ok" }), { status: 200 });
|
|
}
|
|
return secondResponse;
|
|
});
|
|
}
|
|
|
|
// ─── Create ──────────────────────────────────────────────
|
|
|
|
describe("create()", () => {
|
|
test("POSTs to /memories with input body", async () => {
|
|
const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async (url: string) => {
|
|
if (url.toString().endsWith("/health")) {
|
|
return new Response(JSON.stringify({ status: "ok" }), { status: 200 });
|
|
}
|
|
return new Response(JSON.stringify(SAMPLE_MEMORY), { status: 200 });
|
|
});
|
|
|
|
const result = await backend.create({
|
|
apiKeyId: "key-1",
|
|
sessionId: "sess-1",
|
|
type: MemoryType.FACTUAL,
|
|
key: "my-key",
|
|
content: "Hello world",
|
|
metadata: {},
|
|
expiresAt: null,
|
|
});
|
|
|
|
expect(result).toEqual(SAMPLE_MEMORY_JSON);
|
|
});
|
|
|
|
test("uses custom create endpoint when configured", async () => {
|
|
const b = createBackend({ endpoints: { create: "/api/v1/mem" } });
|
|
const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async (url: string) => {
|
|
if (url.toString().endsWith("/health")) {
|
|
return new Response(JSON.stringify({ status: "ok" }), { status: 200 });
|
|
}
|
|
return new Response(JSON.stringify(SAMPLE_MEMORY), { status: 200 });
|
|
});
|
|
|
|
await b.create({
|
|
apiKeyId: "k1",
|
|
sessionId: "s1",
|
|
type: MemoryType.FACTUAL,
|
|
key: "k",
|
|
content: "c",
|
|
metadata: {},
|
|
expiresAt: null,
|
|
});
|
|
|
|
const createUrl = fetchMock.mock.calls.find(
|
|
([url]) => !url.toString().endsWith("/health")
|
|
)![0] as string;
|
|
expect(new URL(createUrl).pathname).toBe("/api/v1/mem");
|
|
});
|
|
});
|
|
|
|
// ─── Get ─────────────────────────────────────────────────
|
|
|
|
describe("get()", () => {
|
|
test("GETs /memories/{id} and returns memory", async () => {
|
|
const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async (url: string) => {
|
|
if (url.toString().endsWith("/health")) {
|
|
return new Response(JSON.stringify({ status: "ok" }), { status: 200 });
|
|
}
|
|
return new Response(JSON.stringify(SAMPLE_MEMORY), { status: 200 });
|
|
});
|
|
|
|
const result = await backend.get("mem-001");
|
|
|
|
expect(result).toEqual(SAMPLE_MEMORY_JSON);
|
|
const getUrl = fetchMock.mock.calls.find(
|
|
([url]) => !url.toString().endsWith("/health")
|
|
)![0] as string;
|
|
expect(new URL(getUrl).pathname).toBe("/memories/mem-001");
|
|
});
|
|
|
|
test("returns null on 404", async () => {
|
|
vi.spyOn(globalThis, "fetch").mockImplementation(async (url: string) => {
|
|
if (url.toString().endsWith("/health")) {
|
|
return new Response(JSON.stringify({ status: "ok" }), { status: 200 });
|
|
}
|
|
return new Response("Not found", { status: 404 });
|
|
});
|
|
|
|
const result = await backend.get("mem-999");
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
test("throws on non-404 errors", async () => {
|
|
vi.spyOn(globalThis, "fetch").mockImplementation(async (url: string) => {
|
|
if (url.toString().endsWith("/health")) {
|
|
return new Response(JSON.stringify({ status: "ok" }), { status: 200 });
|
|
}
|
|
return new Response("Server error", { status: 500 });
|
|
});
|
|
|
|
await expect(backend.get("mem-001")).rejects.toThrow("HTTP 500");
|
|
});
|
|
|
|
test("uses custom get endpoint with path params", async () => {
|
|
const b = createBackend({
|
|
endpoints: { get: "/records/{memoryId}" },
|
|
pathParams: { memoryId: "memoryId" },
|
|
});
|
|
const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async (url: string) => {
|
|
if (url.toString().endsWith("/health")) {
|
|
return new Response(JSON.stringify({ status: "ok" }), { status: 200 });
|
|
}
|
|
return new Response(JSON.stringify(SAMPLE_MEMORY), { status: 200 });
|
|
});
|
|
|
|
await b.get("mem-001");
|
|
|
|
const getUrl = fetchMock.mock.calls.find(
|
|
([url]) => !url.toString().endsWith("/health")
|
|
)![0] as string;
|
|
expect(new URL(getUrl).pathname).toBe("/records/mem-001");
|
|
});
|
|
});
|
|
|
|
// ─── Update ──────────────────────────────────────────────
|
|
|
|
describe("update()", () => {
|
|
test("PATCHes /memories/{id} with updates", async () => {
|
|
const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async (url: string) => {
|
|
if (url.toString().endsWith("/health")) {
|
|
return new Response(JSON.stringify({ status: "ok" }), { status: 200 });
|
|
}
|
|
return new Response(null, { status: 204 });
|
|
});
|
|
|
|
const result = await backend.update("mem-001", { content: "updated" });
|
|
|
|
expect(result).toBe(true);
|
|
const updateUrl = fetchMock.mock.calls.find(
|
|
([url]) => !url.toString().endsWith("/health")
|
|
)![0] as string;
|
|
expect(new URL(updateUrl).pathname).toBe("/memories/mem-001");
|
|
});
|
|
|
|
test("returns false on 404", async () => {
|
|
vi.spyOn(globalThis, "fetch").mockImplementation(async (url: string) => {
|
|
if (url.toString().endsWith("/health")) {
|
|
return new Response(JSON.stringify({ status: "ok" }), { status: 200 });
|
|
}
|
|
return new Response("Not found", { status: 404 });
|
|
});
|
|
|
|
const result = await backend.update("mem-999", { content: "x" });
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ─── Delete ──────────────────────────────────────────────
|
|
|
|
describe("delete()", () => {
|
|
test("DELETEs /memories/{id}", async () => {
|
|
const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async (url: string) => {
|
|
if (url.toString().endsWith("/health")) {
|
|
return new Response(JSON.stringify({ status: "ok" }), { status: 200 });
|
|
}
|
|
return new Response(null, { status: 204 });
|
|
});
|
|
|
|
const result = await backend.delete("mem-001");
|
|
|
|
expect(result).toBe(true);
|
|
const delUrl = fetchMock.mock.calls.find(
|
|
([url]) => !url.toString().endsWith("/health")
|
|
)![0] as string;
|
|
expect(new URL(delUrl).pathname).toBe("/memories/mem-001");
|
|
});
|
|
|
|
test("returns false on 404", async () => {
|
|
vi.spyOn(globalThis, "fetch").mockImplementation(async (url: string) => {
|
|
if (url.toString().endsWith("/health")) {
|
|
return new Response(JSON.stringify({ status: "ok" }), { status: 200 });
|
|
}
|
|
return new Response("Not found", { status: 404 });
|
|
});
|
|
|
|
const result = await backend.delete("mem-999");
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ─── List ────────────────────────────────────────────────
|
|
|
|
describe("list()", () => {
|
|
test("GETs /memories with query params", async () => {
|
|
const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async (url: string) => {
|
|
if (url.toString().endsWith("/health")) {
|
|
return new Response(JSON.stringify({ status: "ok" }), { status: 200 });
|
|
}
|
|
return new Response(
|
|
JSON.stringify({ data: [SAMPLE_MEMORY], total: 1, byType: { factual: 1 } }),
|
|
{ status: 200 }
|
|
);
|
|
});
|
|
|
|
const result = await backend.list({
|
|
apiKeyId: "key-1",
|
|
type: MemoryType.FACTUAL,
|
|
limit: 10,
|
|
offset: 0,
|
|
});
|
|
|
|
expect(result.data).toHaveLength(1);
|
|
expect(result.total).toBe(1);
|
|
const listUrl = fetchMock.mock.calls.find(
|
|
([url]) => !url.toString().endsWith("/health")
|
|
)![0] as string;
|
|
expect(listUrl).toContain("apiKeyId=key-1");
|
|
expect(listUrl).toContain("limit=10");
|
|
expect(listUrl).toContain("offset=0");
|
|
});
|
|
|
|
test("applies custom query param names", async () => {
|
|
const b = createBackend({
|
|
queryParams: { apiKeyId: "owner", category: "memoryCategory", limit: "count" },
|
|
});
|
|
const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async (url: string) => {
|
|
if (url.toString().endsWith("/health")) {
|
|
return new Response(JSON.stringify({ status: "ok" }), { status: 200 });
|
|
}
|
|
return new Response(JSON.stringify({ data: [], total: 0, byType: {} }), { status: 200 });
|
|
});
|
|
|
|
await b.list({ apiKeyId: "key-1", category: "codegraph", limit: 5 });
|
|
const listUrl = fetchMock.mock.calls.find(
|
|
([url]) => !url.toString().endsWith("/health")
|
|
)![0] as string;
|
|
|
|
expect(listUrl).toContain("owner=key-1");
|
|
expect(listUrl).toContain("memoryCategory=codegraph");
|
|
expect(listUrl).toContain("count=5");
|
|
expect(listUrl).not.toContain("apiKeyId=");
|
|
expect(listUrl).not.toContain("category=");
|
|
});
|
|
});
|
|
|
|
// ─── Search ──────────────────────────────────────────────
|
|
|
|
describe("search()", () => {
|
|
test("GETs /memories/search with query params", async () => {
|
|
const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async (url: string) => {
|
|
if (url.toString().endsWith("/health")) {
|
|
return new Response(JSON.stringify({ status: "ok" }), { status: 200 });
|
|
}
|
|
return new Response(JSON.stringify([SAMPLE_MEMORY]), { status: 200 });
|
|
});
|
|
|
|
const result = await backend.search({
|
|
query: "hello",
|
|
apiKeyId: "key-1",
|
|
strategy: "semantic",
|
|
limit: 5,
|
|
});
|
|
|
|
expect(result).toHaveLength(1);
|
|
const searchUrl = fetchMock.mock.calls.find(
|
|
([url]) => !url.toString().endsWith("/health")
|
|
)![0] as string;
|
|
expect(searchUrl).toContain("/memories/search");
|
|
expect(searchUrl).toContain("query=hello");
|
|
expect(searchUrl).toContain("strategy=semantic");
|
|
});
|
|
|
|
test("uses custom search endpoint", async () => {
|
|
const b = createBackend({ endpoints: { search: "/api/search" } });
|
|
const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async (url: string) => {
|
|
if (url.toString().endsWith("/health")) {
|
|
return new Response(JSON.stringify({ status: "ok" }), { status: 200 });
|
|
}
|
|
return new Response(JSON.stringify([]), { status: 200 });
|
|
});
|
|
|
|
await b.search({ query: "q", apiKeyId: "k" });
|
|
|
|
const searchUrl = fetchMock.mock.calls.find(
|
|
([url]) => !url.toString().endsWith("/health")
|
|
)![0] as string;
|
|
expect(searchUrl).toContain("/api/search");
|
|
});
|
|
|
|
test("serializes options as JSON query param", async () => {
|
|
const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async (url: string) => {
|
|
if (url.toString().endsWith("/health")) {
|
|
return new Response(JSON.stringify({ status: "ok" }), { status: 200 });
|
|
}
|
|
return new Response(JSON.stringify([]), { status: 200 });
|
|
});
|
|
|
|
await backend.search({
|
|
query: "hello",
|
|
apiKeyId: "key-1",
|
|
options: { filter: { lang: "en" } },
|
|
});
|
|
const searchUrl = fetchMock.mock.calls.find(
|
|
([url]) => !url.toString().endsWith("/health")
|
|
)![0] as string;
|
|
|
|
expect(searchUrl).toContain(encodeURIComponent(JSON.stringify({ filter: { lang: "en" } })));
|
|
});
|
|
});
|
|
|
|
// ─── Auth headers ────────────────────────────────────────
|
|
|
|
describe("authentication", () => {
|
|
test("sends Authorization header when apiKey is configured", async () => {
|
|
const b = createBackend({ apiKey: "secret-123" });
|
|
const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async (url: string) => {
|
|
if (url.toString().endsWith("/health")) {
|
|
return new Response(JSON.stringify({ status: "ok" }), { status: 200 });
|
|
}
|
|
return new Response(JSON.stringify({ status: "ok" }), { status: 200 });
|
|
});
|
|
|
|
await b.health();
|
|
|
|
const headers = fetchMock.mock.calls[0]![1] as RequestInit;
|
|
expect(headers.headers).toMatchObject({
|
|
Authorization: "Bearer secret-123",
|
|
});
|
|
});
|
|
|
|
test("sends custom headers when configured", async () => {
|
|
const b = createBackend({
|
|
headers: { "X-Api-Key": "abc", "Notion-Version": "2022-06-28" },
|
|
});
|
|
const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async (url: string) => {
|
|
if (url.toString().endsWith("/health")) {
|
|
return new Response(JSON.stringify({ status: "ok" }), { status: 200 });
|
|
}
|
|
return new Response(JSON.stringify({ status: "ok" }), { status: 200 });
|
|
});
|
|
|
|
await b.health();
|
|
|
|
const headers = fetchMock.mock.calls[0]![1] as RequestInit;
|
|
expect(headers.headers).toMatchObject({
|
|
"X-Api-Key": "abc",
|
|
"Notion-Version": "2022-06-28",
|
|
});
|
|
});
|
|
});
|
|
|
|
// ─── Factory ─────────────────────────────────────────────
|
|
|
|
describe("createGenericMemoryBackend factory", () => {
|
|
test("returns a GenericMemoryBackend instance", () => {
|
|
const b = createGenericMemoryBackend("fac", "Factory", { baseUrl: "http://x" });
|
|
expect(b).toBeInstanceOf(GenericMemoryBackend);
|
|
expect(b.id).toBe("fac");
|
|
});
|
|
});
|
|
|
|
// ─── SSRF guard ──────────────────────────────────────────
|
|
|
|
describe("SSRF prevention", () => {
|
|
test("blocks requests to loopback IPv4 (127.0.0.1)", async () => {
|
|
const b = createBackend({ baseUrl: "http://127.0.0.1:20128" });
|
|
const result = await b.health();
|
|
expect(result.ok).toBe(false);
|
|
expect(result.error).toContain("SSRF guard");
|
|
});
|
|
|
|
test("blocks requests to private IPv4 (10.x.x.x)", async () => {
|
|
const b = createBackend({ baseUrl: "http://10.0.0.5/api" });
|
|
const result = await b.health();
|
|
expect(result.ok).toBe(false);
|
|
expect(result.error).toContain("SSRF guard");
|
|
});
|
|
|
|
test("blocks requests to private IPv4 (192.168.x.x)", async () => {
|
|
const b = createBackend({ baseUrl: "http://192.168.1.100" });
|
|
const result = await b.health();
|
|
expect(result.ok).toBe(false);
|
|
expect(result.error).toContain("SSRF guard");
|
|
});
|
|
|
|
test("blocks requests to cloud metadata IP (169.254.169.254)", async () => {
|
|
const b = createBackend({ baseUrl: "http://169.254.169.254/latest/meta-data/" });
|
|
const result = await b.health();
|
|
expect(result.ok).toBe(false);
|
|
expect(result.error).toContain("SSRF guard");
|
|
});
|
|
|
|
test("blocks requests to loopback IPv6 (::1)", async () => {
|
|
const b = createBackend({ baseUrl: "http://[::1]:20128" });
|
|
const result = await b.health();
|
|
expect(result.ok).toBe(false);
|
|
expect(result.error).toContain("SSRF guard");
|
|
});
|
|
|
|
test("blocks non-http schemes (file://)", async () => {
|
|
const b = createBackend({ baseUrl: "file:///etc/passwd" });
|
|
const result = await b.health();
|
|
expect(result.ok).toBe(false);
|
|
expect(result.error).toContain("SSRF guard");
|
|
});
|
|
|
|
test("allows public IP addresses", async () => {
|
|
const b = createBackend({ baseUrl: "http://93.184.216.34:8080" });
|
|
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
new Response(JSON.stringify({ status: "ok" }), { status: 200 })
|
|
);
|
|
// Should pass SSRF guard and proceed to the actual fetch (which will
|
|
// hit the mock, not the real host)
|
|
await expect(b.health()).resolves.toHaveProperty("ok", true);
|
|
});
|
|
|
|
test("allows hostnames (passes structural check)", async () => {
|
|
const b = createBackend({ baseUrl: "https://api.example.com" });
|
|
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
new Response(JSON.stringify({ status: "ok" }), { status: 200 })
|
|
);
|
|
await expect(b.health()).resolves.toHaveProperty("ok", true);
|
|
});
|
|
|
|
test("SSRF guard fires during create() via request()", async () => {
|
|
const b = createBackend({ baseUrl: "http://127.0.0.1:20128" });
|
|
// Mock the fetch so health fails (SSRF guard) — but the CRUD method
|
|
// calls initialize() first, which calls health(), which should throw
|
|
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
new Response(JSON.stringify({ status: "ok" }), { status: 200 })
|
|
);
|
|
await expect(
|
|
b.create({
|
|
apiKeyId: "k1",
|
|
sessionId: "s1",
|
|
type: MemoryType.FACTUAL,
|
|
key: "k",
|
|
content: "c",
|
|
metadata: {},
|
|
expiresAt: null,
|
|
})
|
|
).rejects.toThrow("SSRF guard");
|
|
});
|
|
});
|
|
});
|