---
title: Test the Orders API
description: Verify pricing, validation, persistence, authentication, and order ownership with executable tests.
sidebar:
  icon: flask-conical
---

This chapter tests the `relkit-orders` app from [Protect your orders](/docs/learn/protect-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. ## 1. Test pricing without HTTP [#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:

    ```ts title="tests/unit/orders-pricing.test.ts"
    /** 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.

    ```ts title="tests/unit/orders.function.test.ts"
    /** 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`.

    ```ts title="tests/integration/hello.route.test.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. ## 2. Test the HTTP and database boundary [#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`.

    ```ts title="tests/integration/orders.route.test.ts"
    /** 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. ## 3. Run the checks [#run-the-checks]

    From the application root, with the migration files still present:

    ```sh title="Terminal — relkit-orders"
    bun run check
    bun run typecheck
    bun run test
    ```

    Expect 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.

{/* Generated by apps/docs/scripts/generate.ts. Do not edit. */}

**Reference and source**

- [`@relkit/testing` API](/docs/api/testing)
- [`@relkit/routes` API](/docs/api/routes)

- [`templates/default/v1/api/tests/integration/orders.route.test.ts`](https://github.com/rel-kit/relkit/blob/main/templates/default/v1/api/tests/integration/orders.route.test.ts)
