flake.nix fetches the release tarballs and carries their SRI hashes in a generated block. The cut tool builds the release locally with the toolchain gates.yml pins, in a normalized nine-variable environment, writes the hashes into flake.nix, and commits it with build.zig.zon as the single bump commit. The package job verifies the pins on the bump commit and the publish job verifies them again on the tag, before anything is uploaded. The tarballs are written by dist_stage (std.tar.Writer, flate gzip) instead of the runner's tar and gzip, and -ffile-prefix-map keeps checkout paths out of the C objects; two checkouts at different absolute paths produce byte-identical archives. nxdns version, /api/version and the admin footer report the version only: the bump commit cannot know its own sha.
130 lines
4.0 KiB
TypeScript
130 lines
4.0 KiB
TypeScript
/**
|
|
* The Clients pages under a real router: both of them read the same list, and
|
|
* the detail route is reached by deep link as often as by click, so the tests
|
|
* drive the router rather than the components.
|
|
*/
|
|
|
|
import { act, render, screen } from "@testing-library/react";
|
|
import { QueryClientProvider, type QueryClient } from "@tanstack/react-query";
|
|
import { RouterProvider, createMemoryHistory } from "@tanstack/react-router";
|
|
import { AuthProvider } from "@/auth/store";
|
|
import { createQueryClient } from "@/lib/queryClient";
|
|
import { queryKeys } from "@/lib/queries";
|
|
import { createAppRouter } from "@/routes";
|
|
|
|
export const GROUPS = {
|
|
groups: [
|
|
{ id: 1, name: "default", safe_search: false },
|
|
{ id: 2, name: "kids", safe_search: true },
|
|
],
|
|
};
|
|
|
|
export const CLIENTS = {
|
|
clients: [
|
|
{
|
|
id: 1,
|
|
ip: "192.168.1.10",
|
|
name: "laptop",
|
|
learned_name: "laptop-1.lan",
|
|
group_id: 1,
|
|
group: "default",
|
|
hand_edited: true,
|
|
first_seen: 1700000000,
|
|
last_seen: 1700003600,
|
|
},
|
|
{
|
|
id: 2,
|
|
ip: "192.168.1.11",
|
|
name: "",
|
|
learned_name: "kids-tablet.lan",
|
|
group_id: 2,
|
|
group: "kids",
|
|
hand_edited: false,
|
|
first_seen: 1700000000,
|
|
last_seen: 1700007200,
|
|
},
|
|
],
|
|
};
|
|
|
|
export const PREFIXES = {
|
|
client_prefixes: [{ id: 1, prefix: "192.168.1.0/24", group_id: 2, group: "kids", priority: 100 }],
|
|
};
|
|
|
|
const VERSION = { version: "0.0.0-test", zig_version: "0.16.0", uptime_seconds: 1 };
|
|
|
|
export const DATABASE = { authority: "database", path: null, reconciled_at: null, restart_pending: false };
|
|
export const MANAGED_FILE = {
|
|
authority: "managed_file",
|
|
path: "/etc/nxdns/config.zon",
|
|
reconciled_at: 1754899200,
|
|
restart_pending: false,
|
|
};
|
|
|
|
export const BASE: Record<string, unknown> = {
|
|
"GET /api/clients": CLIENTS,
|
|
"GET /api/client-prefixes": PREFIXES,
|
|
"GET /api/groups": GROUPS,
|
|
"GET /api/version": VERSION,
|
|
"GET /api/config/status": DATABASE,
|
|
};
|
|
|
|
/** A request that never settles, so its query stays pending for the whole test. */
|
|
export const NEVER = Symbol("never");
|
|
|
|
export function stubFetch(map: Record<string, unknown>): ReturnType<typeof vi.fn> {
|
|
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
const key = `${init?.method ?? "GET"} ${String(input)}`;
|
|
const payload = map[key];
|
|
if (payload === NEVER) return new Promise<Response>(() => {});
|
|
if (payload === undefined) {
|
|
return new Response(JSON.stringify({ error: `not stubbed: ${key}` }), { status: 404 });
|
|
}
|
|
return new Response(JSON.stringify(payload), {
|
|
status: 200,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
return fetchMock;
|
|
}
|
|
|
|
export function renderAt(path: string, map: Record<string, unknown>) {
|
|
const fetchMock = stubFetch(map);
|
|
const queryClient = createQueryClient();
|
|
const router = createAppRouter(createMemoryHistory({ initialEntries: [path] }), queryClient);
|
|
render(
|
|
<AuthProvider>
|
|
<QueryClientProvider client={queryClient}>
|
|
<RouterProvider router={router} />
|
|
</QueryClientProvider>
|
|
</AuthProvider>,
|
|
);
|
|
return { router, fetchMock, queryClient };
|
|
}
|
|
|
|
export async function renderClientsPage(map: Record<string, unknown> = BASE) {
|
|
const handles = renderAt("/clients", map);
|
|
await screen.findByRole("heading", { name: "Clients" });
|
|
return handles;
|
|
}
|
|
|
|
/**
|
|
* Turns configuration authority under a mounted page, the way the status
|
|
* query's own poll would: the stub reads the map on each call, so replacing the
|
|
* entry and refetching is the whole transition. `undefined` stands for a failed
|
|
* status — the stub answers 404 for a key it does not hold.
|
|
*/
|
|
export async function setConfigStatus(
|
|
map: Record<string, unknown>,
|
|
queryClient: QueryClient,
|
|
status: unknown,
|
|
): Promise<void> {
|
|
map["GET /api/config/status"] = status;
|
|
await act(async () => {
|
|
await queryClient.refetchQueries({ queryKey: queryKeys.configStatus });
|
|
// The cache lands before its observers are notified — react-query defers
|
|
// that notification — so the render pass needs one more turn inside act.
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
});
|
|
}
|