ORIGIN.md pinned the intended checkout to an absolute path under a personal home directory, docs/LOCAL-SETUP.md listed folders on one developer's machine, and two tests used what looks like a real tailnet address. Use a home-relative path, drop the machine-specific folder list, and use 100.100.100.100 in the tests; it is still inside 100.64.0.0/10, so both tests check the same thing as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
180 lines
6.3 KiB
TypeScript
180 lines
6.3 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
API_PROBE_TIMEOUT_MS,
|
|
apiBaseWarning,
|
|
defaultApiBase,
|
|
displayApiHost,
|
|
MAX_API_PROBE_RESPONSE_BYTES,
|
|
normalizeApiBase,
|
|
probeApiBase,
|
|
usesCustomApiBase,
|
|
} from "./endpoint.js";
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("normalizeApiBase", () => {
|
|
it("trims, adds https, and keeps only the origin", () => {
|
|
expect(normalizeApiBase(" app.example.com/rpc ")).toEqual({
|
|
ok: true,
|
|
url: "https://app.example.com",
|
|
});
|
|
expect(normalizeApiBase("https://rakazo.example.com:8443/api/")).toEqual({
|
|
ok: true,
|
|
url: "https://rakazo.example.com:8443",
|
|
});
|
|
expect(normalizeApiBase("http://192.168.1.20:3100/")).toEqual({
|
|
ok: true,
|
|
url: "http://192.168.1.20:3100",
|
|
});
|
|
expect(normalizeApiBase("http://app.example.com")).toEqual({
|
|
ok: false,
|
|
error: "Public servers need https://",
|
|
});
|
|
});
|
|
|
|
it("rejects empty, non-http, and malformed values", () => {
|
|
expect(normalizeApiBase("")).toMatchObject({ ok: false });
|
|
expect(normalizeApiBase(" ")).toMatchObject({ ok: false });
|
|
expect(normalizeApiBase("javascript:alert(1)")).toMatchObject({ ok: false });
|
|
expect(normalizeApiBase("ftp://files.example.com")).toMatchObject({ ok: false });
|
|
expect(normalizeApiBase("http://")).toMatchObject({ ok: false });
|
|
});
|
|
|
|
it("strips credentials from the stored origin", () => {
|
|
expect(normalizeApiBase("https://user:pass@app.example.com/rpc")).toEqual({
|
|
ok: true,
|
|
url: "https://app.example.com",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("display and warnings", () => {
|
|
it("falls back to loopback when the compile-time endpoint is invalid", async () => {
|
|
vi.stubEnv("EXPO_PUBLIC_API_URL", "ftp://files.example.com");
|
|
vi.resetModules();
|
|
const endpoint = await import("./endpoint.js");
|
|
|
|
expect(endpoint.defaultApiBase()).toBe("http://127.0.0.1:3100");
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
it("falls back to loopback when the compile-time endpoint is public HTTP", async () => {
|
|
vi.stubEnv("EXPO_PUBLIC_API_URL", "http://app.example.com");
|
|
vi.resetModules();
|
|
const endpoint = await import("./endpoint.js");
|
|
|
|
expect(endpoint.defaultApiBase()).toBe("http://127.0.0.1:3100");
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
it("shows host and non-default port", () => {
|
|
expect(displayApiHost("https://rakazo.example.com")).toBe("rakazo.example.com");
|
|
expect(displayApiHost("http://10.0.0.8:3100")).toBe("10.0.0.8:3100");
|
|
});
|
|
|
|
it("warns on public http but not LAN or loopback", () => {
|
|
expect(apiBaseWarning("https://app.example.com")).toBeNull();
|
|
expect(apiBaseWarning("http://127.0.0.1:3100")).toBeNull();
|
|
expect(apiBaseWarning("http://192.168.1.20:3100")).toBeNull();
|
|
expect(apiBaseWarning("http://100.64.0.1:3100")).toBeNull();
|
|
expect(apiBaseWarning("http://100.100.100.100:3100")).toBeNull();
|
|
expect(apiBaseWarning("http://100.127.255.255:3100")).toBeNull();
|
|
expect(apiBaseWarning("http://app.example.com")).toMatch(/https/i);
|
|
});
|
|
|
|
it("treats the compile-time default as not custom", () => {
|
|
expect(usesCustomApiBase(defaultApiBase())).toBe(false);
|
|
expect(usesCustomApiBase("https://rakazo.example.com")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("probeApiBase", () => {
|
|
it("accepts a Rakazo /rpc/health response", async () => {
|
|
const fetchImpl = vi.fn(
|
|
async () =>
|
|
new Response(JSON.stringify({ json: { ok: true, version: "0.1.0" } }), { status: 200 }),
|
|
) as unknown as typeof fetch;
|
|
await expect(probeApiBase("https://app.example.com", fetchImpl)).resolves.toEqual({
|
|
ok: true,
|
|
url: "https://app.example.com",
|
|
});
|
|
expect(fetchImpl).toHaveBeenCalledWith(
|
|
"https://app.example.com/rpc/health",
|
|
expect.objectContaining({ method: "POST" }),
|
|
);
|
|
});
|
|
|
|
it("rejects a host that is up but is not Rakazo", async () => {
|
|
const fetchImpl = vi.fn(
|
|
async () => new Response("ok", { status: 200 }),
|
|
) as unknown as typeof fetch;
|
|
await expect(probeApiBase("https://example.com", fetchImpl)).resolves.toMatchObject({
|
|
ok: false,
|
|
});
|
|
});
|
|
|
|
it("rejects an oversized health response", async () => {
|
|
const fetchImpl = vi.fn(
|
|
async () =>
|
|
new Response(JSON.stringify({ json: { ok: true } }), {
|
|
headers: { "content-length": String(MAX_API_PROBE_RESPONSE_BYTES + 1) },
|
|
}),
|
|
) as unknown as typeof fetch;
|
|
|
|
await expect(probeApiBase("https://app.example.com", fetchImpl)).resolves.toMatchObject({
|
|
ok: false,
|
|
error: "Could not reach that server",
|
|
});
|
|
});
|
|
|
|
it("returns a failure when an injected fetch ignores the probe signal", async () => {
|
|
vi.useFakeTimers();
|
|
const fetchImpl = vi.fn(
|
|
(_input: RequestInfo | URL, _init?: RequestInit) => new Promise<Response>(() => undefined),
|
|
);
|
|
|
|
const pending = probeApiBase("https://app.example.com", fetchImpl as typeof fetch);
|
|
await vi.advanceTimersByTimeAsync(API_PROBE_TIMEOUT_MS);
|
|
|
|
await expect(pending).resolves.toMatchObject({
|
|
ok: false,
|
|
error: "Could not reach that server",
|
|
});
|
|
expect(fetchImpl.mock.calls[0]?.[1]?.signal?.aborted).toBe(true);
|
|
});
|
|
|
|
it("returns a failure when a health response body never settles", async () => {
|
|
vi.useFakeTimers();
|
|
const cancel = vi.fn();
|
|
const fetchImpl = vi.fn(async () => new Response(new ReadableStream({ cancel })));
|
|
|
|
const pending = probeApiBase("https://app.example.com", fetchImpl as typeof fetch);
|
|
await vi.advanceTimersByTimeAsync(API_PROBE_TIMEOUT_MS);
|
|
|
|
await expect(pending).resolves.toMatchObject({
|
|
ok: false,
|
|
error: "Could not reach that server",
|
|
});
|
|
expect(cancel).toHaveBeenCalledOnce();
|
|
});
|
|
});
|
|
|
|
describe("mobile custom server UI", () => {
|
|
it("exposes a sign-in control that writes the stored origin", () => {
|
|
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
const signIn = readFileSync(path.join(dir, "../app/sign-in.tsx"), "utf8");
|
|
const api = readFileSync(path.join(dir, "api.ts"), "utf8");
|
|
expect(signIn).toContain("Use a custom server");
|
|
expect(signIn).toContain("saveApiBase");
|
|
expect(signIn).toContain("probeApiBase");
|
|
expect(api).toContain("currentApiBase()");
|
|
expect(api).not.toMatch(/export const API /);
|
|
});
|
|
});
|