Test the Orders API
Verify pricing, validation, persistence, authentication, and order ownership with executable tests.
This chapter tests the relkit-orders app from Protect your orders.
It needs the auth migration generated in that chapter. Tests create an isolated SQLite file and
two accounts, so they do not alter the database used by bun run dev.
The minimal starter already installs @relkit/testing as development tooling. Its generic harness
does not provide database/auth SDKs: keep the selected installations and repairs from the previous
chapters, and use the @relkit/testing/better-auth leaf shown in the integration tests.
1. Test pricing without HTTP
The price function is reusable application behavior. Add tests/unit/orders-pricing.test.ts
to check the regular and bulk prices directly, without starting a server:
/** Verifies regular and ten-item bulk totals through the tutorial's original checked pricing function. */
import { expect, test } from "bun:test";
import { invokeFunction } from "@relkit/testing";
import priceOrder from "../../src/services/orders/functions/price-order.function.js";
test("bulk pricing preserves the regular price below ten", async () => {
expect(await invokeFunction(priceOrder, { quantity: 3 })).toEqual({ totalCents: 300 });
expect(await invokeFunction(priceOrder, { quantity: 10 })).toEqual({ totalCents: 900 });
});Add tests/unit/orders.function.test.ts with the version below. It tests the public
pricing operation without acquiring a database.
/** Invokes the tutorial pricing member directly to verify its ten-item discount independently of HTTP. */
import { expect, test } from "bun:test";
import { invokeFunction } from "@relkit/testing";
import orders from "@app/services/orders/service.js";
test("priceOrder is available without the HTTP route", async () => {
expect(await invokeFunction(orders.priceOrder, { quantity: 10 })).toEqual({ totalCents: 900 });
});Replace the minimal starter’s Hello integration test with the version below. The endpoint
is unchanged, but every test application now needs the auth secret declared in src/platform/env.ts.
/** Exercises the complete auth tutorial application through its selected native test owner. */
import { afterAll, expect, test } from "bun:test";
import { createTestApplication } from "@relkit/testing/better-auth";
import config from "../../relkit.config.js";
const testApp = await createTestApplication(config, {
env: { BETTER_AUTH_SECRET: "isolated-test-secret-never-use-in-production-12345" },
});
test("GET /hello", async () => {
const response = await testApp.http.get("/hello?name=Mustafa");
expect(response.status).toBe(200);
expect(await response.json()).toEqual({ message: "Hello, Mustafa!" });
});
afterAll(() => testApp.close());2. Test the HTTP and database boundary
Add tests/integration/orders.route.test.ts with this test. It migrates a
temporary SQLite database, signs up Alice and Bob, creates an order for Alice, checks Bob cannot
read it, and rejects invalid input. It also checks that a signed-out request receives 401.
/** Verifies migrated order persistence and authentication through the selected native test owner. */
import { expect, test } from "bun:test";
import { Database } from "bun:sqlite";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { createTestApplication, type TestApplication } from "@relkit/testing/better-auth";
import { drizzle } from "drizzle-orm/bun-sqlite";
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
import config from "../../relkit.config.js";
const headers = { "content-type": "application/json", origin: "http://127.0.0.1:3000" };
test("orders persist and remain private to their owner", async () => {
const root = await mkdtemp(join(tmpdir(), "relkit-orders-guide-"));
const path = join(root, "orders.sqlite");
const previous = process.env.BETTER_AUTH_SECRET;
process.env.BETTER_AUTH_SECRET = "isolated-test-secret-never-use-in-production-12345";
const sqlite = new Database(path);
try {
await migrate(drizzle({ client: sqlite }), {
migrationsFolder: resolve(import.meta.dir, "../../src/services/database/migrations"),
});
} finally {
sqlite.close();
}
const app = await createTestApplication(config, {
env: { DATABASE_PATH: path, BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET },
});
try {
expect((await app.http.get("/orders/order-1")).status).toBe(401);
const alice = await signUp(app, "Alice", "alice@example.com");
const bob = await signUp(app, "Bob", "bob@example.com");
const created = await app.http.post("/orders", {
headers: { ...headers, ...alice },
body: JSON.stringify({ orderId: "order-1", sku: "book", quantity: 10 }),
});
expect(created.status).toBe(201);
expect(await created.json()).toMatchObject({ totalCents: 900 });
expect(await (await app.http.get("/orders/order-1", { headers: alice })).json()).toMatchObject({
found: true,
});
expect(await (await app.http.get("/orders/order-1", { headers: bob })).json()).toEqual({
found: false,
});
const invalid = await app.http.post("/orders", {
headers: { ...headers, ...alice },
body: JSON.stringify({ orderId: "bad", sku: "book", quantity: 0 }),
});
expect(invalid.status, await invalid.text()).toBe(422);
} finally {
await app.close();
if (previous === undefined) delete process.env.BETTER_AUTH_SECRET;
else process.env.BETTER_AUTH_SECRET = previous;
await rm(root, { recursive: true, force: true });
}
});
/** Acquires the same native session cookie for each distinct order owner. */
async function signUp(app: TestApplication, name: string, email: string) {
const response = await app.http.post("/api/auth/sign-up/email", {
headers,
body: JSON.stringify({ name, email, password: "local-test-password-123" }),
});
expect(response.status).toBe(200);
return { cookie: response.headers.get("set-cookie")?.split(";", 1)[0] ?? "" };
}The test uses createTestApplication for the in-process HTTP boundary. It covers these paths in
the test harness; it does not prove every production server behavior. Keep the manual requests
from the previous chapters for the running backend.
3. Run the checks
From the application root, with the migration files still present:
bun run check
bun run typecheck
bun run testExpect all commands to exit 0. If the integration test cannot find drizzle/, finish the
two migrations from the preceding chapters. If another test still asserts the starter’s old
response shape, update that assertion to include quantity and stored behavior.