import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
import { fetchTool } from "../fetch";
// Mock fetch globally
const originalFetch = globalThis.fetch;
let mockFetch: any;
beforeEach(() => {
mockFetch = mock(() => {});
globalThis.fetch = mockFetch;
});
afterEach(() => {
globalThis.fetch = originalFetch;
mockFetch.mockClear();
});
describe("fetch tool", () => {
test("should fetch simple text content", async () => {
const mockContent = "This is plain text content";
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: "OK",
url: "https://example.com/test.txt",
headers: new Headers({ "content-type": "text/plain" }),
text: () => Promise.resolve(mockContent),
});
const result = await fetchTool.execute({
url: "https://example.com/test.txt",
});
expect(result).toContain("Contents of https://example.com/test.txt:");
expect(result).toContain(mockContent);
expect(result).toContain("Content type: text/plain");
});
test("should convert HTML to markdown", async () => {
const mockHtml = `
Test Page
Main Title
This is a paragraph with emphasis.
Link text
`;
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: "OK",
url: "https://example.com/test.html",
headers: new Headers({ "content-type": "text/html" }),
text: () => Promise.resolve(mockHtml),
});
const result = await fetchTool.execute({
url: "https://example.com/test.html",
});
expect(result).toContain("# Main Title");
expect(result).toContain("**paragraph**");
expect(result).toContain("_emphasis_");
expect(result).toContain("* First item");
expect(result).toContain("* Second item");
expect(result).toContain("[Link text](https://example.com)");
});
test("should return raw HTML when raw=true", async () => {
const mockHtml = "Raw HTML
";
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: "OK",
url: "https://example.com/raw.html",
headers: new Headers({ "content-type": "text/html" }),
text: () => Promise.resolve(mockHtml),
});
const result = await fetchTool.execute({
url: "https://example.com/raw.html",
raw: true,
});
expect(result).toContain(mockHtml);
expect(result).not.toContain("# Raw HTML");
});
test("should handle content truncation", async () => {
const longContent = "x".repeat(10000);
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: "OK",
url: "https://example.com/long.txt",
headers: new Headers({ "content-type": "text/plain" }),
text: () => Promise.resolve(longContent),
});
const result = await fetchTool.execute({
url: "https://example.com/long.txt",
maxLength: 1000,
});
expect(result).toContain("");
expect(result).toContain("Showing characters 0-1000 of 10000");
expect(result).toContain("Use startIndex=1000 to continue");
});
test("should handle pagination with startIndex", async () => {
const content = "0123456789".repeat(100); // 1000 chars
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: "OK",
url: "https://example.com/paginate.txt",
headers: new Headers({ "content-type": "text/plain" }),
text: () => Promise.resolve(content),
});
const result = await fetchTool.execute({
url: "https://example.com/paginate.txt",
startIndex: 500,
maxLength: 300,
});
expect(result).toContain("Showing characters 500-800 of 1000");
expect(result).toContain("200 characters remaining");
});
test("should handle startIndex beyond content length", async () => {
const content = "Short content";
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: "OK",
url: "https://example.com/short.txt",
headers: new Headers({ "content-type": "text/plain" }),
text: () => Promise.resolve(content),
});
const result = await fetchTool.execute({
url: "https://example.com/short.txt",
startIndex: 100,
});
expect(result).toContain("No more content available");
expect(result).toContain("Start index 100 exceeds content length 13");
});
test("should return error message for HTTP errors", async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
statusText: "Not Found",
});
const result = await fetchTool.execute({
url: "https://example.com/notfound.txt",
});
expect(result).toContain(
"Failed to fetch https://example.com/notfound.txt - HTTP 404: Not Found"
);
});
test("should return error message for network timeouts", async () => {
mockFetch.mockRejectedValueOnce(new Error("AbortError"));
const result = await fetchTool.execute({
url: "https://example.com/timeout.txt",
timeout: 1000,
});
expect(result).toContain(
"Request timeout: Failed to fetch https://example.com/timeout.txt within 1000ms"
);
});
test("should return error message for network errors", async () => {
mockFetch.mockRejectedValueOnce(new Error("Network connection failed"));
const result = await fetchTool.execute({
url: "https://example.com/network-error.txt",
});
expect(result).toContain(
"Network error: Network connection failed"
);
});
test("should return error message for invalid URLs", async () => {
const result = await fetchTool.execute({
url: "not-a-valid-url",
});
expect(result).toContain("Invalid URL: not-a-valid-url");
});
test("should handle redirects", async () => {
const content = "Redirected content";
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: "OK",
url: "https://example.com/redirected.txt", // Final URL after redirect
headers: new Headers({ "content-type": "text/plain" }),
text: () => Promise.resolve(content),
});
const result = await fetchTool.execute({
url: "https://example.com/redirect-me.txt",
});
expect(result).toContain("Contents of https://example.com/redirected.txt:");
expect(result).toContain(content);
});
test("should clean up HTML content properly", async () => {
const htmlWithScripts = `
Clean Title
Clean content & entities <test>
`;
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: "OK",
url: "https://example.com/clean.html",
headers: new Headers({ "content-type": "text/html" }),
text: () => Promise.resolve(htmlWithScripts),
});
const result = await fetchTool.execute({
url: "https://example.com/clean.html",
});
expect(result).toContain("# Clean Title");
expect(result).toContain("Clean content & entities ");
expect(result).not.toContain("alert");
expect(result).not.toContain("evil");
expect(result).not.toContain("