Athena, testing
Vitest + Playwright patterns used in Athena
Athena's test suite is Vitest (unit/integration) + Playwright (e2e), following the patterns described in Develop, Testing strategy. This page goes into the Athena-specific specifics.
What's tested
Vitest
services/kratos/*andservices/hydra/*, adapter functions, withfetchmocked.lib/*, utility functions (cookie parsing, error mapping, etc.).features/<x>/actions.ts, server actions, with services mocked.- A few representative
route.tshandlers, most are pass-through.
Playwright
- Login → land in dashboard.
- Create identity → see in list → delete.
- View OAuth2 client → rotate secret → confirm.
- A small smoke test that visits every page.
Mocking the Ory clients
Tests almost never hit a real Kratos or Hydra. Pattern:
import { vi } from "vitest";
import * as identitiesService from "@/services/kratos/identities";
vi.mock("@/services/kratos/identities", () => ({
listIdentities: vi.fn(),
createIdentity: vi.fn(),
patchIdentity: vi.fn(),
deleteIdentity: vi.fn(),
}));
test("delete identity calls service then re-fetches list", async () => {
vi.mocked(identitiesService.deleteIdentity).mockResolvedValue(undefined);
vi.mocked(identitiesService.listIdentities).mockResolvedValue([]);
await deleteIdentityAction("uuid");
expect(identitiesService.deleteIdentity).toHaveBeenCalledWith("ciam", "uuid");
});The service-layer abstraction makes this clean. Real Kratos is only touched in dev / staging.
Mocking the SDK
Similar pattern for @olympusoss/sdk:
vi.mock("@olympusoss/sdk", () => ({
getSetting: vi.fn().mockResolvedValue("some-value"),
setSetting: vi.fn(),
encrypt: (s: string) => `v2:test:${s}`,
decrypt: (s: string) => s.split(":").pop(),
}));For tests that exercise the encryption flow, we use a deterministic test key:
vi.mock("@olympusoss/sdk/crypto", () => ({
// ... but real-ish crypto for round-trip tests
}));Testing the middleware
Edge middleware is hard to test in isolation because it uses Web Crypto. Approach:
import { middleware } from "@/middleware";
test("rejects request without session cookie", async () => {
const request = new Request("http://test/api/identities", {
headers: { /* no cookie */ }
});
const response = await middleware(request);
expect(response.status).toBe(401);
});We construct Request objects manually rather than running the Next dev server.
Playwright setup
athena/playwright.config.ts:
export default defineConfig({
testDir: "./tests",
use: {
baseURL: "http://localhost:3001",
trace: "on-first-retry",
},
webServer: {
command: "bun run dev",
port: 3001,
reuseExistingServer: !process.env.CI,
},
});CI spins up the full dev stack (podman compose up -d in a pre-test step), then runs Playwright against it.
Snapshot tests
Vitest's toMatchSnapshot is used sparingly, mostly for the shape of audit events:
test("emits expected audit event", async () => {
const event = await handleLogin(...);
expect(event).toMatchSnapshot();
});Snapshots live in __snapshots__/. Update with bun run test -- -u when the shape intentionally changes.
Coverage
Run with coverage:
bun run test -- --coverageCoverage isn't enforced as a CI gate (per directive). Useful for spotting un-tested branches.
When tests are slow
The whole Athena Vitest suite runs in under 30s. If you find one test taking more than 5s, look at:
- Unmocked fetch, accidentally hitting real Kratos.
- Real database connection, should be mocked too.
- Unawaited promises causing test hangs.
When tests are flaky
Common causes:
- Time-based assertions without mock clocks. Use
vi.useFakeTimers(). - Tests sharing state. Each test should reset its mocks.
- Playwright tests racing async UI updates. Use
expect(locator).toBeVisible()(auto-retries) overwaitForwith hardcoded timeouts.