> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pylonsync.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing

> Test Pylon logic, React components, and functions over HTTP with `pylon test`.

Pylon apps test with Bun's test runner (`import { test, expect } from "bun:test"`). The `pylon test` command discovers your test files and runs them. New projects scaffold with a `test` script, a starter test, and React component testing already set up.

```bash theme={null}
pylon test            # run every *.test.ts / *.test.tsx under tests/ (or functions/)
pylon test credits    # only files whose path contains "credits"
npm test              # the scaffolded script — same as `pylon test`
```

`pylon test` discovers every `*.test.ts` / `*.test.tsx` (and `.js`/`.jsx`) file under `tests/` (or `functions/`) and runs each with Bun against an in-memory Pylon instance.

<Note>
  New apps created with `npm create @pylonsync/pylon@latest` already include `bunfig.toml` and `tests/setup.ts` (which registers [happy-dom](https://github.com/capricorn86/happy-dom) so component tests render), the `@testing-library/react` package as a dev dependency, and a starter test under `tests/`.
</Note>

## Three tiers

Reach for the cheapest tier that proves what you need. Most of your coverage should be Tier 1.

### Tier 1: pure logic

Keep the decisions that matter in pure functions under `lib/`: access and plan gating, pricing, credit math, validation, and formatting. Test them exhaustively. These tests need no server, run instantly, and cover the code where bugs matter most. Keep your `query`, `mutation`, and `action` handlers as thin wrappers around these functions, so you can test the logic without a running app.

```ts tests/credits.test.ts theme={null}
import { expect, test } from "bun:test";
import { creditBalance, CREDIT_COST } from "../lib/credits";

test("creditBalance sums grants − spends + refunds", () => {
  expect(creditBalance([])).toBe(0);
  expect(creditBalance([{ delta: 8000 }, { delta: -160 }])).toBe(7840);
});

test("CREDIT_COST matches the spec", () => {
  expect(CREDIT_COST.ugc).toBe(160);
});
```

### Tier 2: React components

`@testing-library/react` and happy-dom are set up in `tests/setup.ts`. Render a component. Assert on the DOM. The templates use the classic JSX transform, so add `import React from "react"` in `.tsx` tests.

```tsx tests/button.test.tsx theme={null}
import { afterEach, expect, test } from "bun:test";
import React from "react";
import { render, screen, cleanup } from "@testing-library/react";
import { Button } from "../components/ui/button";

afterEach(cleanup);

test("Button renders its label", () => {
  render(<Button>Click me</Button>);
  expect(screen.getByRole("button", { name: "Click me" })).toBeDefined();
});
```

For a component that reads Pylon data hooks (`db.useQuery`, `callFn`), mock the boundary with `mock.module`. Then dynamic-`import` the component, so the mock is in place first:

```tsx theme={null}
import { test, expect, mock } from "bun:test";
import React from "react";
import { render, screen } from "@testing-library/react";

mock.module("@pylonsync/react", () => ({
  db: { useQuery: () => ({ data: [{ id: "1", name: "Acme" }], loading: false }) },
}));
const { OrgList } = await import("../app/orgs/org-list"); // your component

test("renders orgs from the query", () => {
  render(<OrgList />);
  expect(screen.getByText("Acme")).toBeDefined();
});
```

### Tier 3: functions over HTTP

A handler's full behavior (policies, `ctx.db`, auth) only exists in the running app. When Tier 1 cannot cover a case, run `pylon dev` in another terminal. Call the API the way a client would. `resetDb()` from `@pylonsync/functions` clears the in-memory database between test cases. It does nothing if the server is not running, and it refuses to run against production.

```ts tests/things.test.ts theme={null}
import { afterEach, expect, test } from "bun:test";
import { resetDb } from "@pylonsync/functions";

const BASE = "http://localhost:4321";
afterEach(() => resetDb(BASE)); // or installTestIsolation(BASE) once at top-of-file

test("createThing then read it back", async () => {
  const t = await fetch(`${BASE}/api/fn/createThing`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ name: "hello" }),
  }).then((r) => r.json());

  const rows = await fetch(`${BASE}/api/entities/Thing`).then((r) => r.json());
  expect(rows.some((r: { id: string }) => r.id === t.id)).toBe(true);
});
```

<Note>
  These calls are unauthenticated. They pass only if the surface is reachable by an anonymous client. Server functions default to `auth: "user"`, so a call with no session gets a `401`. An entity with no read policy is default-denied (`403`). To test this way, mark the function `auth: "guest"` and give the entity a read policy. Otherwise, authenticate the request first: call `POST /api/auth/guest` for a guest session, then send the token as `Authorization: Bearer <token>`.
</Note>

## Adding tests to an existing project

If your project predates the test scaffolding, add the setup by hand:

<CodeGroup>
  ```json package.json theme={null}
  {
    "scripts": {
      "test": "pylon test"
    },
    "devDependencies": {
      "@happy-dom/global-registrator": "^20.10.0",
      "@testing-library/dom": "^10.4.0",
      "@testing-library/react": "^16.3.0"
    }
  }
  ```

  ```toml bunfig.toml theme={null}
  [test]
  preload = ["./tests/setup.ts"]
  ```

  ```ts tests/setup.ts theme={null}
  import { GlobalRegistrator } from "@happy-dom/global-registrator";
  GlobalRegistrator.register();
  ```
</CodeGroup>

(`bunfig.toml` and `tests/setup.ts` are only needed for Tier 2 component tests. Pure-logic tests run without them.)

## Security probe

`pylon test:security` is a separate adversarial probe. It hits a running app and reports auth and policy holes (unguarded functions, policies that allow what they should not). Start the app, then run it:

```bash theme={null}
pylon dev &
pylon test:security                 # probes http://localhost:4321
pylon test:security --target https://api.example.com --json
```

## CI

`pylon test` exits with a non-zero code on failure, so it runs directly in CI:

```yaml theme={null}
- run: pylon test
```

For end-to-end coverage that includes Tier 3 (functions over HTTP), start `pylon dev` in the background first. Then run `pylon test`.
