mirror of
https://github.com/tale/headplane.git
synced 2026-10-26 22:47:55 +00:00
104 lines
4.2 KiB
Markdown
104 lines
4.2 KiB
Markdown
# Testing
|
|
|
|
Headplane has two test suites. Both run with [Vitest](https://vitest.dev).
|
|
|
|
| Command | What it runs | Needs |
|
|
| ----------------------- | ----------------------------------------------------- | ------ |
|
|
| `pnpm test:unit` | `tests/unit`, in a couple of seconds | Node |
|
|
| `pnpm test:integration` | `tests/integration`, against real Headscale and nodes | Docker |
|
|
| `pnpm harness up` | A seeded tailnet that keeps running until you stop it | Docker |
|
|
|
|
Every test should be able to fail because of a regression you can name. Tests
|
|
that only restate the code, or check that something exists, get deleted. See
|
|
`AGENTS.md` for the full rules.
|
|
|
|
## Unit tests
|
|
|
|
Unit tests use Headplane's real code wherever it's cheap to: the real auth
|
|
service on an in-memory SQLite database, the real Headscale API client, and real
|
|
route loaders and actions. Two helpers in `tests/support` make that possible.
|
|
|
|
`createTestApp()` builds the same app context production does, around a fake
|
|
Headscale. Sign in as any role and call a route the way React Router would:
|
|
|
|
```ts
|
|
import { action } from "~/routes/settings/auth-keys/overview";
|
|
|
|
import { createTestApp, statusOf } from "../../support/app";
|
|
|
|
test("a viewer can't create a tag-only key", async () => {
|
|
await using app = await createTestApp();
|
|
const { cookie } = await app.signIn("viewer");
|
|
|
|
const result = await app.call(action, "/settings/auth-keys", {
|
|
as: cookie,
|
|
form: { intent: "create", tags: "tag:prod", expiry: "90" },
|
|
});
|
|
|
|
expect(statusOf(result)).toBe(403);
|
|
});
|
|
```
|
|
|
|
`createFakeHeadscale(version)` is an in-memory Headscale that answers at the
|
|
HTTP level, so Headplane's own API client runs against it unchanged. It only
|
|
models the parts of Headscale the unit tests use: users, pre-auth keys, API
|
|
keys, and nodes joined with a key. Anything else throws, so a test that needs
|
|
more fails instead of passing on made-up data.
|
|
|
|
### The Headscale contract
|
|
|
|
`tests/contract/headscale.ts` describes what Headplane relies on from
|
|
Headscale's API. All of it runs against real Headscale in the integration
|
|
tests. The part the fake models also runs against the fake in the unit tests,
|
|
so the two can't drift apart. When a unit test needs the fake to do more, move
|
|
the contract test for that behavior into the shared part, check it against real
|
|
Headscale, then make the fake pass it.
|
|
|
|
## Integration tests
|
|
|
|
Integration tests run against real containers through the harness in
|
|
`tests/harness`. It starts disposable tailnets built on
|
|
[Testcontainers](https://testcontainers.com), and works with Docker, Podman,
|
|
Colima, and OrbStack.
|
|
|
|
```ts
|
|
import { createTailnet } from "../../harness/tailnet";
|
|
|
|
test("peers see a renamed machine", async () => {
|
|
await using tailnet = await createTailnet({ policy: allowAll });
|
|
const web = await tailnet.node({ name: "web", user: "alice@", ssh: true });
|
|
const db = await tailnet.node({ name: "db", user: "alice@" });
|
|
|
|
const app = await tailnet.headplane();
|
|
// ...rename web through Headplane, then read db's `tailscale status`
|
|
});
|
|
```
|
|
|
|
Each tailnet has its own Docker network, a Headscale served over HTTPS with a
|
|
generated certificate, and Tailscale nodes that join with pre-auth keys. A node
|
|
can advertise routes, an exit node, tags, or SSH. `node.down()` and `node.up()`
|
|
switch it off and on. `tailnet.pendingNode()` starts one that waits for
|
|
interactive registration, and `tailnet.headplane()` runs Headplane in-process
|
|
against the tailnet.
|
|
|
|
The supported Headscale versions live in `tests/harness/versions.ts`. Locally,
|
|
integration tests run against the latest one. Set
|
|
`HP_HEADSCALE_VERSIONS=all` (as CI does) or a comma-separated list to run
|
|
others.
|
|
|
|
## Scenarios
|
|
|
|
A scenario is a seeded tailnet described as data: users, machines, routes, a
|
|
policy, and DNS settings. They live in `tests/harness/scenarios`.
|
|
|
|
```sh
|
|
pnpm harness list
|
|
pnpm harness up homelab --headplane
|
|
pnpm harness up fleet --version 0.28.0 # HP_FLEET_SIZE=200 for more machines
|
|
```
|
|
|
|
`up` prints the Headscale URL and API key and keeps everything running until
|
|
you press Ctrl-C. With `--headplane` it also starts the dev server against it on
|
|
port 3001. Use scenarios to try a change by hand, to reproduce a setup from a
|
|
bug report, and to take the screenshots in these docs.
|