1
mirror of https://github.com/jakejarvis/hoot.git synced 2025-10-18 20:14:25 -04:00
Files
hoot/server/services/seo.test.ts

158 lines
4.6 KiB
TypeScript

/* @vitest-environment node */
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// getSeo is imported dynamically after mocks are applied
let getSeo: typeof import("./seo").getSeo;
const utMock = vi.hoisted(() => ({
uploadFiles: vi.fn(async () => ({
data: { ufsUrl: "https://app.ufs.sh/f/mock-key", key: "mock-key" },
error: null,
})),
}));
vi.mock("uploadthing/server", async () => {
const actual =
await vi.importActual<typeof import("uploadthing/server")>(
"uploadthing/server",
);
return {
...actual,
UTApi: vi.fn().mockImplementation(() => utMock),
};
});
beforeEach(async () => {
vi.resetModules();
const { makePGliteDb } = await import("@/server/db/pglite");
const { db } = await makePGliteDb();
vi.doMock("@/server/db/client", () => ({ db }));
globalThis.__redisTestHelper.reset();
});
afterEach(() => {
vi.restoreAllMocks();
globalThis.__redisTestHelper.reset();
});
// Ensure module under test is loaded after mocks
beforeEach(async () => {
({ getSeo } = await import("./seo"));
});
function htmlResponse(html: string, url: string) {
return {
ok: true,
status: 200,
headers: new Headers({ "content-type": "text/html; charset=utf-8" }),
text: async () => html,
url,
} as unknown as Response;
}
function textResponse(text: string, contentType = "text/plain") {
return {
ok: true,
status: 200,
headers: new Headers({ "content-type": contentType }),
text: async () => text,
url: "",
} as unknown as Response;
}
// imageResponse helper removed along with flaky test
describe("getSeo", () => {
it("uses cached response when meta exists in cache", async () => {
const { upsertDomain } = await import("@/server/repos/domains");
const { upsertSeo } = await import("@/server/repos/seo");
const { ttlForSeo } = await import("@/server/db/ttl");
const now = new Date();
const d = await upsertDomain({
name: "example.com",
tld: "com",
unicodeName: "example.com",
});
await upsertSeo({
domainId: d.id,
sourceFinalUrl: "https://example.com/",
sourceStatus: 200,
metaOpenGraph: {},
metaTwitter: {},
metaGeneral: {},
previewTitle: null,
previewDescription: null,
previewImageUrl: null,
previewImageUploadedUrl: null,
canonicalUrl: null,
robots: { fetched: true, groups: [], sitemaps: [] },
robotsSitemaps: [],
errors: {},
fetchedAt: now,
expiresAt: ttlForSeo(now),
});
const out = await getSeo("example.com");
expect(out).toBeTruthy();
});
it("sets html error when non-HTML content-type returned", async () => {
const fetchMock = vi
.spyOn(global, "fetch")
.mockResolvedValueOnce({
ok: true,
status: 200,
headers: new Headers({ "content-type": "application/json" }),
text: async () => "{}",
url: "https://example.com/",
} as unknown as Response)
.mockResolvedValueOnce(textResponse("", "text/plain"));
const out = await getSeo("nonhtml.invalid");
expect(out.errors?.html).toMatch(/Non-HTML content-type/i);
fetchMock.mockRestore();
});
it("sets robots error when robots.txt non-text content-type", async () => {
const fetchMock = vi
.spyOn(global, "fetch")
.mockResolvedValueOnce(htmlResponse("<html></html>", "https://x/"))
.mockResolvedValueOnce(textResponse("{}", "application/json"));
const out = await getSeo("robots-content.invalid");
expect(out.errors?.robots ?? "").toMatch(/Unexpected robots content-type/i);
fetchMock.mockRestore();
});
it("sets preview.imageUploaded to null when image fetch fails and preserves original", async () => {
const fetchMock = vi
.spyOn(global, "fetch")
.mockResolvedValueOnce(
htmlResponse(
`<!doctype html><html><head>
<title>Site</title>
<meta property="og:image" content="/og.png" />
</head></html>`,
"https://example.com/",
),
)
.mockResolvedValueOnce(
textResponse("User-agent: *\nAllow: /", "text/plain"),
)
.mockResolvedValueOnce({
ok: false,
status: 404,
headers: new Headers({ "content-type": "text/plain" }),
arrayBuffer: async () => new ArrayBuffer(0),
url: "",
} as unknown as Response);
const out = await getSeo("img-fail.invalid");
// original image remains for Meta Tags display
expect(out.preview?.image ?? "").toContain("/og.png");
// uploaded url is null on failure for privacy-safe rendering
expect(out.preview?.imageUploaded ?? null).toBeNull();
fetchMock.mockRestore();
});
});