# Write an HTTP test

An HTTP test drives your running service with k6. Use it for int, e2e, and load files.

## Write the test

```ts title=src/checkout/__testkit__/health.int.testkit.ts
import { defineFile } from "@gfdelarue/testkit";
import { defineHttpSuite } from "@gfdelarue/testkit/k6";
import { check } from "k6";

export const testkit = defineFile({ engine: "k6" });

const suite = defineHttpSuite(({ rawReq }) => {
	const response = rawReq.get("/health");
	check(response, {
		"health returns 200": (r) => r.status === 200,
	});
});

export const options = suite.options;
export const setup = suite.setup;
export default suite.exec;
```

The last three lines are required. A k6 module exports `options`, `setup`, and a default function, and Testkit does not write them for you.

`rawReq` sends a request with no session. The path is relative to the service's base URL.

## Run it

```console $ npx testkit run int
[1/2] PASS api int src/account/__testkit__/profile.int.testkit.ts 0s
[2/2] PASS api int src/checkout/__testkit__/health.int.testkit.ts 0s

Summary
Result: PASSED
Passed: 2
Failed: 0
Skipped: 0
Not run: 0
Files: 2
Duration: 5s
Run ID: 1791890151637-1344059-9ba3838f
```

## Name the check that failed

A failed k6 `check` fails the file, but the run prints only the threshold.

```console $ npx testkit run int
[1/2] PASS api int src/account/__testkit__/profile.int.testkit.ts 0s
[2/2] FAIL api int src/checkout/__testkit__/health.int.testkit.ts 0s
    k6 thresholds failed: checks(rate==1.0)
```

Testkit's `expect.value` takes a value, a predicate, and a label. When the predicate returns false, the run prints the label.

```ts
import { defineHttpSuite, expect } from "@gfdelarue/testkit/k6";

const suite = defineHttpSuite(({ rawReq }) => {
	const response = rawReq.get("/health");
	expect.value(response.status, (status) => status === 200, "health returns 200");
});
```

```console $ npx testkit run int
[1/2] PASS api int src/account/__testkit__/profile.int.testkit.ts 0s
[2/2] FAIL api int src/checkout/__testkit__/health.int.testkit.ts 0s
    health returns 200
    k6 thresholds failed: checks(rate==1.0)
```

## Sign in as named actors

Put the sign-in code in a helper beside the tests. An auth adapter has two functions. `setup` signs every actor in once and returns their sessions. `headers` turns a session into request headers.

```ts title=src/account/__testkit__/helpers/session.ts
import type { AuthAdapter, AuthSessionBundle } from "@gfdelarue/testkit/k6";

export function sessionAuth(actorNames: string[]): AuthAdapter<AuthSessionBundle> {
	return {
		setup({ env, http }) {
			const actors = Object.fromEntries(
				actorNames.map((actorName, actorIndex) => {
					const login = http.post(`${env.BASE}/login`, JSON.stringify({ name: actorName }));
					const token = String(JSON.parse(String(login.body)).token);
					return [actorName, { actorIndex, actorName, session: { token } }];
				}),
			);
			return { actors, primaryActor: actorNames[0] };
		},
		headers(bundle, { actor }) {
			const session = bundle?.actors[actor ?? bundle.primaryActor ?? ""]?.session;
			return session ? { Authorization: `Bearer ${session.token}` } : {};
		},
	};
}
```

Pass the adapter to `defineHttpSuite`. `req` sends requests as the primary actor, and `req.as(name)` sends them as another one.

```ts title=src/account/__testkit__/profile.int.testkit.ts
import { defineFile } from "@gfdelarue/testkit";
import { defineHttpSuite } from "@gfdelarue/testkit/k6";
import { check } from "k6";

import { sessionAuth } from "./helpers/session.js";

export const testkit = defineFile({ engine: "k6" });

const suite = defineHttpSuite({ auth: sessionAuth(["reviewer", "outsider"]) }, ({ rawReq, req }) => {
	check(req.get("/me"), {
		"the reviewer reads their profile": (r) => r.status === 200,
	});
	check(req.as("outsider").get("/me"), {
		"an outsider is refused": (r) => r.status === 403,
	});
	check(rawReq.get("/me"), {
		"a request with no session is refused": (r) => r.status === 401,
	});
});

export const options = suite.options;
export const setup = suite.setup;
export default suite.exec;
```

Three checks cover the signed-in user, the wrong user, and no user. Most access bugs sit in the second one.

## Wait for work to finish

`waitFor` calls a function until a predicate accepts its result. It spends the file's time budget, so do not set a timeout of your own.

```ts
import { parse, waitFor } from "@gfdelarue/testkit/k6";

const response = waitFor(
	() => req.get("/api/v1/jobs/123"),
	(res) => parse.json(res).data?.status === "completed",
	{ description: "job 123 to complete" },
);
```

Set the budget for every file with [`execution.fileTimeoutSeconds`](https://turnstone.cloud/docs/configuration.md#execution), or for one run with `--file-timeout-seconds`.

## Use k6 directly

Native k6 imports work in every HTTP test.

```ts
import { check, group } from "k6";
import http from "k6/http";
import { Rate, Trend } from "k6/metrics";
```

To set k6 options such as `vus` and `iterations`, name an HTTP profile in [`profiles.http`](https://turnstone.cloud/docs/configuration.md#profileshttp) and pass `{ profile: "name" }` to `defineHttpSuite`. A `load` file uses the same suite shape as an `int` file.
