Spaces:
Running
Running
File size: 4,839 Bytes
10d1fd4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | import {
afterEach,
beforeEach,
describe,
expect,
it,
type MockedFunction,
vi,
} from "vitest";
import { fetchSearXNG, getWebSearchStatus } from "./webSearchService";
function createMockResponse(
text: string,
ok = true,
status?: number,
): Response {
const resolvedStatus = status ?? (ok ? 200 : 503);
return {
ok,
status: resolvedStatus,
statusText: ok ? "OK" : "Error",
headers: new Headers(),
redirected: false,
type: "basic" as ResponseType,
url: "http://test.com",
clone: function () {
return this;
},
body: null,
bodyUsed: false,
arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
blob: () => Promise.resolve(new Blob()),
formData: () => Promise.resolve(new FormData()),
json: () => Promise.resolve(JSON.parse(text)),
text: () => Promise.resolve(text),
} as unknown as Response;
}
const successResponse = () =>
createMockResponse(
JSON.stringify({
results: [
{
title: "example",
url: "https://example.com",
content: "example content",
category: "general",
},
],
}),
);
let originalFetch: typeof fetch;
let fetchMock: MockedFunction<typeof fetch>;
beforeEach(() => {
originalFetch = global.fetch;
fetchMock = vi.fn() as unknown as MockedFunction<typeof fetch>;
global.fetch = fetchMock;
});
afterEach(() => {
global.fetch = originalFetch;
});
describe("WebSearchService", () => {
it("should report service not available when fetch throws", async () => {
(global.fetch as MockedFunction<typeof fetch>).mockImplementation(() => {
throw new Error("Network error");
});
const status = await getWebSearchStatus();
expect(status).toBe(false);
});
it("should return false when health endpoint does not return OK", async () => {
(global.fetch as MockedFunction<typeof fetch>).mockResolvedValue(
createMockResponse("NOT_OK", false),
);
const status = await getWebSearchStatus();
expect(status).toBe(false);
});
it("should return true when health endpoint returns OK", async () => {
(global.fetch as MockedFunction<typeof fetch>)
.mockResolvedValueOnce(createMockResponse("OK"))
.mockResolvedValueOnce(successResponse());
const status = await getWebSearchStatus();
expect(status).toBe(true);
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(fetchMock.mock.calls[1]?.[0]).toContain("/search?");
});
it("should return empty array on fetchSearXNG error", async () => {
(global.fetch as MockedFunction<typeof fetch>).mockRejectedValue(
new Error("Network failure"),
);
const results = await fetchSearXNG("test query", "text");
expect(Array.isArray(results)).toBe(true);
expect(results).toHaveLength(0);
});
});
describe("retry logic", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("retries on 500 and returns results on eventual success", async () => {
fetchMock
.mockResolvedValueOnce(createMockResponse("", false, 500))
.mockResolvedValueOnce(createMockResponse("", false, 500))
.mockResolvedValueOnce(successResponse());
const promise = fetchSearXNG("test", "text");
await vi.runAllTimersAsync();
const results = await promise;
expect(fetchMock).toHaveBeenCalledTimes(3);
expect(results).toHaveLength(1);
});
it("returns empty array when all retries return 500", async () => {
fetchMock.mockResolvedValue(createMockResponse("", false, 500));
const promise = fetchSearXNG("test", "text");
await vi.runAllTimersAsync();
const results = await promise;
expect(fetchMock).toHaveBeenCalledTimes(4);
expect(results).toHaveLength(0);
});
});
describe("circuit breaker", () => {
let isolatedFetchSearXNG: typeof fetchSearXNG;
beforeEach(async () => {
vi.resetModules();
const module = await import("./webSearchService");
isolatedFetchSearXNG = module.fetchSearXNG;
});
it("opens after exactly 5 non-retriable failures", async () => {
fetchMock.mockResolvedValue(createMockResponse("", false, 503));
for (let i = 0; i < 5; i++) {
await isolatedFetchSearXNG("test", "text");
}
const callsBeforeBreak = fetchMock.mock.calls.length;
await isolatedFetchSearXNG("test", "text");
expect(fetchMock.mock.calls.length).toBe(callsBeforeBreak);
});
it("does not open before 5 failures", async () => {
fetchMock.mockResolvedValue(createMockResponse("", false, 503));
for (let i = 0; i < 4; i++) {
await isolatedFetchSearXNG("test", "text");
}
const callsBefore = fetchMock.mock.calls.length;
await isolatedFetchSearXNG("test", "text");
expect(fetchMock.mock.calls.length).toBe(callsBefore + 1);
});
});
|