test.deploy/core/steps/probe.step.test.ts

test.deploy/core/steps/probe.step.test.ts is a file in Codebase Testing. 153 lines of code and 0 definitions.

import {
    PROBE_FAILED,
    PROBE_LOGGED,
    PROBE_NO_RECORD,
    PROBE_PASSED,
    PROBE_STALE,
} from "@banes-lab/deploy/configuration/strings/deployment.strings.ts";
import type { ProbeFetcher, ProbeResponse } from "@banes-lab/deploy/types/deployment.types.ts";
import { describe, expect, it } from "vitest";
import { fakeJournal, fakeShell, ok } from "./shell.fixture.ts";
import { Buffer } from "node:buffer";
import { LOG_SIZE_COMMAND } from "@banes-lab/deploy/configuration/constants/nginx.constants.ts";
import { digestOf } from "@govlab/content-fingerprint";
import { probeSite } from "@banes-lab/deploy/core/steps/probe.step.ts";

const SITE = "https://x.test";
const JSON_TYPE = "application/json";
const MARKDOWN_TYPE = "text/markdown; charset=utf-8";
const LEAF_BODY = '{"title":"Leaf"}';
const COLUMNS = ["ref", "kind", "title", "summary", "json", "markdown", "bytes", "fingerprint"];
const RECORD_REF = "architecture:big-ball";
const RECORD_TITLE = "Big Ball";

const json = function json(value: unknown): ProbeResponse {
    return { body: JSON.stringify(value), status: 200, type: JSON_TYPE };
};

const markdown: ProbeResponse = { body: "", status: 200, type: MARKDOWN_TYPE };

const leafEntry = {
    bytes: Buffer.byteLength(LEAF_BODY),
    fingerprint: digestOf(LEAF_BODY),
    json: `${SITE}/json/leaf`,
    markdown: `${SITE}/leaf.md`,
};

const queryAddress = function queryAddress(query: Record<string, string>): string {
    return `GET ${SITE}/q?${new URLSearchParams(query).toString()}`;
};

const QUERY_ANSWERS: readonly string[] = [
    queryAddress({ ref: RECORD_REF }),
    queryAddress({ name: RECORD_TITLE }),
    queryAddress({ q: "big" }),
    queryAddress({ id: "architecture:*" }),
    queryAddress({ walk: RECORD_REF }),
    queryAddress({ collection: "architecture" }),
];

const rowOf = function rowOf(ref: string, title: string): readonly unknown[] {
    return [ref, "index", title, null, leafEntry.json, leafEntry.markdown, leafEntry.bytes, leafEntry.fingerprint];
};

const healthy = function healthy(): Map<string, ProbeResponse> {
    return new Map([
        [`GET ${SITE}/json/api`, json({ pages: [leafEntry] })],
        [
            `GET ${SITE}/json/api/ids`,
            json({ shards: [{ json: `${SITE}/json/api/ids/api` }, { json: `${SITE}/json/api/ids/architecture` }] }),
        ],
        [`GET ${SITE}/json/api/ids/api`, json({ columns: COLUMNS, rows: [rowOf("api:leaf", "Leaf")] })],
        [`GET ${SITE}/json/api/ids/architecture`, json({ columns: COLUMNS, rows: [rowOf(RECORD_REF, RECORD_TITLE)] })],
        [`GET ${leafEntry.json}`, { body: LEAF_BODY, status: 200, type: JSON_TYPE }],
        [`HEAD ${leafEntry.markdown}`, markdown],
        [`GET ${SITE}/api`, markdown],
        [`GET ${SITE}/json/api/`, json({})],
        [`OPTIONS ${SITE}/json/api`, { body: "", status: 204, type: "" }],
        [`GET ${SITE}/json/api/no-such-address`, { body: "{}", status: 404, type: JSON_TYPE }],
        [`GET ${SITE}/no-such-address.md`, { body: "", status: 404, type: MARKDOWN_TYPE }],
        [`GET ${SITE}/no-such-address.txt`, { body: "", status: 404, type: "text/plain; charset=utf-8" }],
        [`GET ${SITE}/no-such-address.json`, { body: "{}", status: 404, type: JSON_TYPE }],
        [`GET ${SITE}/json/records`, json({})],
        [`GET ${SITE}/json/source`, json({})],
        [`GET ${SITE}/q?ref=api:route`, json({})],
        [`GET ${SITE}/q`, { body: "{}", status: 400, type: JSON_TYPE }],
        [`GET ${SITE}/json/api?probe=1`, { body: "{}", status: 400, type: JSON_TYPE }],
        ...QUERY_ANSWERS.map((address): [string, ProbeResponse] => [address, json({})]),
    ]);
};

const fetcherOf = function fetcherOf(site: Map<string, ProbeResponse>): ProbeFetcher {
    return async (url, method) => {
        await Promise.resolve();
        return site.get(`${method} ${url}`) ?? { body: "", status: 404, type: "text/html" };
    };
};

const quietServer = function quietServer(): ReturnType<typeof fakeShell> {
    return fakeShell((command) => ok(command.startsWith(LOG_SIZE_COMMAND) ? "120" : ""));
};

describe("probeSite", () => {
    it("passes a site that answers every checked address as the catalog records it", async () => {
        const journal = fakeJournal();
        await probeSite(journal, SITE, quietServer(), fetcherOf(healthy()));
        expect(journal.lines).toContain(PROBE_PASSED);
    });

    it("runs every query operation against a record the catalog lists", async () => {
        const site = healthy();
        site.delete(queryAddress({ q: "big" }));
        await expect(probeSite(fakeJournal(), SITE, quietServer(), fetcherOf(site))).rejects.toThrow(
            `${SITE}/q?q=big answered 404`,
        );
    });

    it("fails when the catalog lists no record to query", async () => {
        const site = healthy();
        site.set(`GET ${SITE}/json/api/ids/architecture`, json({ columns: COLUMNS, rows: [] }));
        await expect(probeSite(fakeJournal(), SITE, quietServer(), fetcherOf(site))).rejects.toThrow(PROBE_NO_RECORD);
    });

    it("fails with the lines the server wrote to its error log during the check", async () => {
        const sizes = ["120", "180"];
        const server = fakeShell((command) => {
            if (command.startsWith(LOG_SIZE_COMMAND)) {
                return ok(sizes.shift() ?? "180");
            }
            return ok("2026/10/02 18:14:27 [error] js call exception: TypeError: not a function\n");
        });
        const fetcher = fetcherOf(healthy());
        await expect(probeSite(fakeJournal(), SITE, server, fetcher)).rejects.toThrow(
            `${PROBE_LOGGED}2026/10/02 18:14:27 [error] js call exception`,
        );
        expect(server.commands.at(-1)).toContain("tail -c +121 ");
    });

    it("names an address that answers the wrong status or type", async () => {
        const site = healthy();
        site.set(`GET ${SITE}/api`, { body: "", status: 301, type: "text/html" });
        await expect(probeSite(fakeJournal(), SITE, quietServer(), fetcherOf(site))).rejects.toThrow(PROBE_FAILED);
        await expect(probeSite(fakeJournal(), SITE, quietServer(), fetcherOf(site))).rejects.toThrow(
            `${SITE}/api answered 301 text/html instead of 200 text/markdown.`,
        );
    });

    it("names a missing text or JSON address that answers with a page instead of its own type", async () => {
        const site = healthy();
        site.set(`GET ${SITE}/no-such-address.txt`, { body: "", status: 404, type: "text/html" });
        await expect(probeSite(fakeJournal(), SITE, quietServer(), fetcherOf(site))).rejects.toThrow(
            `${SITE}/no-such-address.txt answered 404 text/html instead of 404 text/plain.`,
        );
        site.delete(`GET ${SITE}/no-such-address.json`);
        await expect(probeSite(fakeJournal(), SITE, quietServer(), fetcherOf(site))).rejects.toThrow(
            `${SITE}/no-such-address.json answered 404 text/html instead of 404 application/json.`,
        );
    });

    it("names a leaf whose served bytes differ from its catalog record", async () => {
        const site = healthy();
        site.set(`GET ${leafEntry.json}`, { body: '{"title":"Changed"}', status: 200, type: JSON_TYPE });
        await expect(probeSite(fakeJournal(), SITE, quietServer(), fetcherOf(site))).rejects.toThrow(
            leafEntry.json + PROBE_STALE,
        );
    });

    it("names a catalog folder that does not answer with its index", async () => {
        const site = healthy();
        site.delete(`GET ${SITE}/json/records`);
        await expect(probeSite(fakeJournal(), SITE, quietServer(), fetcherOf(site))).rejects.toThrow(
            `${SITE}/json/records answered 404 text/html instead of 200 application/json.`,
        );
    });

    it("names a Markdown leaf the server does not answer", async () => {
        const site = healthy();
        site.delete(`HEAD ${leafEntry.markdown}`);
        await expect(probeSite(fakeJournal(), SITE, quietServer(), fetcherOf(site))).rejects.toThrow(
            leafEntry.markdown,
        );
    });
});