# Testing applications and installable apps

Cicadel uses Go's testing framework. There is no second test language or hidden
registry: tests are ordinary `*_test.go` functions, dependencies are explicit,
and the same tests work with `go test`, editors, race detection, coverage, fuzzing,
and AI coding agents. `apptest` removes setup repetition without bypassing the
application's security or lifecycle. The PostgreSQL fixture lives in
`github.com/fauzaanu/cicadel/apptest/postgres`.

## Ask your agent

```text
Read https://cicadel.fauzaanu.com/en/dev/topics/testing/index.md.
Add application tests for [feature] using the matching Cicadel apptest API.
Exercise real routes, validation, permissions and CSRF. Use an isolated
PostgreSQL fixture for database behavior. Run the tests and report any skips.
```

## Run, discover, and diagnose

From a project directory with the matching `cicadel` CLI installed:

```sh
go test -list . ./...
cicadel test --report test-results.json -- -race -coverprofile=coverage.out ./...
cicadel test --strict --require-integration --report test-results.json -- -race ./...
cicadel test -- -json ./... > test-events.jsonl
go test -count=1 -run '^TestArticle$/^other_author$' ./apps/articles
go tool cover -func=coverage.out
```

Inside the framework repository, use `go run ./cmd/cicadel test ...`. Install a
project's matching CLI version with `go install github.com/fauzaanu/cicadel/cmd/cicadel@VERSION`.
The wrapper defaults to `-count=1 -timeout=2m` and `./...`; explicit Go flags can
override count/timeout. Put framework flags before `--` and Go flags after it.
The wrapper streams output, returns a failing exit status on build/test failures,
and atomically replaces the requested JSON report, including on failures.
The report directory must already exist. `version: 1` identifies the report
schema; `tests` contains named test/subtest outcomes, `packages` contains package
outcomes, and `skipped` counts named skips. Parent tests and subtests each have
records; their count is not a count of independent scenarios. Preserve JSON events
as well when detailed failure logs are needed. Test output can contain supplied
payloads, so use synthetic data and treat artifacts accordingly.

`--strict` additionally fails if any named test skips or no named tests execute.
It is intended for verification, not `-list` or benchmark-only discovery.
`--require-integration` sets `CICADEL_TEST_REQUIRE_INTEGRATION=1`; helpers using
`apptest.RequireEnv` fail instead of skipping missing prerequisites. Strict mode
also catches ordinary `t.Skip` in application tests. An absent database must never
be reported as verified integration coverage.

Like `go test`, `./...` covers the current module, not nested modules. Run the
command separately in each module (including `contrib/migrations`). Go fuzzing
usually selects one package and one fuzz target; use standard Go flags for that.

## Application and HTTP tests

```go
func TestIndex(t *testing.T) {
    t.Parallel()
    app := apptest.New(t, apptest.Settings(), []cicadel.App{notes.New()})
    client := apptest.NewClient(app)
    client.Get(t, "/notes/").Status(t, 200).JSON(t, `{"app":"notes"}`)
}
```

`apptest.Settings()` returns fresh, environment-independent settings with a
`testserver` allowed host and secure cookies enabled. Mutate the returned value
before construction for configuration-specific tests. `apptest.New` registers
cleanup before startup, runs the real application startup/checks, uses the test's
context for its lifetime, and closes with an independent bounded context.
Construct fresh app factories and dependencies per test. `apptest.Check` remains
the two-instance lifecycle conformance smoke test for installable app authors.

Each `NewClient` has an independent cookie jar. Requests run in process through
the supplied handler, using `https://testserver`, without a listener or network
access. Redirects remain responses so tests can assert status and location.
Cookies respect path, domain, expiry, and secure flags. Authentication, CSRF,
request limits, host checks, and middleware are not disabled. Explicit request
hosts can be changed to test host validation; other URL origins are rejected.

```go
alice, bob := apptest.NewClient(app), apptest.NewClient(app)
alice.PostForm(t, "/login", url.Values{
    "username": {"alice"}, "password": {"test password"},
    "csrf_token": {tokenObtainedFromLoginPage},
}).Redirect(t, http.StatusSeeOther, "/")
bob.Get(t, "/private/").Status(t, http.StatusUnauthorized)
alice.JSON(t, "POST", "/api/notes", map[string]string{"title":"hello"}).Status(t, 201)
```

Use `Request(t, method, path, body, headers)` for arbitrary methods, headers, or
multipart forms. It buffers at most 4 MiB and closes the body. `Response.HTTP`
exposes headers/status; `Response.Body` holds reusable bytes. `Status`, `Header`,
`Contains`, `Redirect`, and `JSON` report failures at the caller. `DecodeJSON`
decodes into a typed destination and rejects trailing values. JSON assertions
ignore whitespace and object-key ordering; array order and number spelling stay
significant (`1` differs from `1.0`), with no float64 precision loss.

`Client.Do` accepts a standard `*http.Request` for cancellation, explicit host,
and large-body tests; callers must close the response body. It does not emulate
a browser, TLS negotiation, streaming, network timeouts, or wire-level behavior
such as HEAD body suppression. Use Go's `httptest.NewServer` / `NewTLSServer`
for those transport tests. Browser cookie policies such as SameSite are outside
this in-process client's scope.

## PostgreSQL isolation and query budgets

Set `CICADEL_TEST_DATABASE_URL` to a **dedicated test PostgreSQL server** whose role
has `CREATEDB`. This is an administrator connection, not an application database
that the fixture truncates. The fixture creates a random database from `template0`
and drops only that database at cleanup. PostgreSQL 13+ is required for forced
cleanup. It never falls back to `DATABASE_URL`, synchronizes a schema, or invokes
migrations implicitly.

```go
func TestCreateArticle(t *testing.T) {
    t.Parallel()
    fixture := postgres.New(t)
    // Apply the application's real migrations explicitly using fixture.SQL/URL.
    // Seed typed fixtures after migration, checking every returned error.
    db, queries := postgres.Capture(fixture.DB)
    s := apptest.Settings()
    s.DatabaseURL = fixture.URL
    app := apptest.New(t, s, []cicadel.App{articles.New(db)})
    client := apptest.NewClient(app)
    queries.Reset() // exclude startup/setup from the request budget
    client.Get(t, "/articles/").Status(t, 200)
    queries.AssertMax(t, 3)
}
```

Create the database first, then app/client/server resources. Go's cleanup runs in
reverse registration order, letting workers and application pools close before
the fixture drops the database. Give every additional connection (session stores,
workers, migrations) `fixture.URL`. Do not reuse database fixtures across parallel
tests. Process termination can prevent cleanup; disposable CI servers are still
appropriate. Cleanup failures fail the test.

`postgres.Transaction(t, db)` starts a transaction and rolls it back on cleanup.
Inject that transaction into every code path under test; do not commit it.
Connections opened elsewhere cannot see uncommitted fixtures. Use a fresh database
for tests involving commits, concurrent connections, constraints, or workers.

`postgres.Capture(db)` returns a private GORM session and a concurrency-safe query
recorder without changing the original session's logger. Inject the returned
session. `Snapshot`, `Reset`, `AssertCount`, and `AssertMax` support exact/budget
assertions. Failed statements count. SQL text and arguments are not retained.
Raw `database/sql` calls, other sessions, and transaction control statements are
outside this recorder; it is a GORM query budget, not a global SQL audit. Wait for
background work before assertions and resets.

## Fixtures and side effects

Keep domain fixture functions in `_test.go` with typed inputs, explicit defaults,
and returned objects. Use fresh dependencies per test and `t.Cleanup`; avoid global
settings, global registries, or environment mutation in parallel tests.

Existing framework components already support observable side effects:

- Inject `mail.Capture` as the mail transport and assert `Messages()` contents.
- Inject `storage.NewLocal(t.TempDir(), limit)` and register its `Close` with cleanup.
- Create a fresh `cache.NewMemory` per test; its contents are not shared.
- Give management commands a `bytes.Buffer` via `management.Manager.Output`,
  run `Manager.Run(t.Context(), args)`, and assert errors, output, and persisted state.
- Test River worker logic with explicit dependencies; use a fresh PostgreSQL
  fixture for transactional enqueue/rollback and actual worker integration.

There is no implicit privileged login or automatic CSRF bypass. Test unauthorized,
forbidden, invalid-input, and cross-user cases alongside successful workflows.
Verify actual persistence and side effects, not just HTTP status.

## AI authoring and verification contract

Generated projects, apps, and installable packages include executable starter tests;
projects/packages also include `TESTING.md`. An agent should:

1. Discover existing tests and public APIs; identify the behavior and failure mode.
2. Add a minimal regression, then run it against the broken behavior to prove it fails.
3. Implement the change and rerun the targeted test, followed by package tests.
4. Run the full suite with race detection, real integration prerequisites, and strict
   reporting. Inspect failed **and skipped** outcomes. Run each nested module separately.
5. Inspect coverage for missed behavior; add boundary, authorization, rollback, and
   failure-path cases. Coverage percentages alone do not establish correctness.
6. Report exact commands and limitations. Never weaken assertions, disable security,
   auto-accept snapshots, or substitute mocks for a claimed database integration check.

Use deterministic values and synchronization instead of sleeps. Fuzz parsers,
normalizers, routing, and validation using standard Go fuzz targets; preserve failing
inputs in Go's corpus. This system provides foundations for reliable agent-written
tests, not a guarantee that generated tests are complete or correct.

## Component verification

Install the pinned Tailwind CLI once with `go tool cicadel ui css install` when using
the default CSS workflow. Use `go tool cicadel ui build`, then `go tool cicadel ui check --json` in generated
projects. The source check is offline and compiles tests without executing them;
`manage.go check` separately validates the actual registered libraries and assets.
Use `apptest` HTTP assertions and ordinary Go tests for component behavior. Render
with `(ui.Renderer{}).Render(t.Context(), content)` for focused checks; no separate
component test language or snapshot acceptance tool is required.

See [the component authoring loop](https://cicadel.fauzaanu.com/en/dev/topics/component-ui/index.md#exact-authoring-and-verification-loop)
and [recorded verification](https://cicadel.fauzaanu.com/en/dev/sources/docs/component-verification.md). Run the independent
`examples/component-library` module separately. The generated-consumer test exercises
actual versioned module installation, explicit overrides and compiled operation
outside the checkout, without browser automation.

The framework's Tailwind/scaffold documentation tests require that installed tool;
run `go run ./cmd/cicadel ui css install` before the root suite. Tests never download
it implicitly. `TestComponentAuthoringGuide` extracts the guide's file-marked code
blocks into a fresh starter, generates markup/CSS, type-checks, and runs its HTTP,
escaping and contract tests under the race detector. `TestStarterTailwind` checks
that the shipped stylesheet is genuine up-to-date compiler output.

## Accounts, mail and logging

Run `contrib/accounts` separately: `ui build`, `ui check --json`, `go test -race ./...`, `sh verify-host.sh`, vet/build and vulnerability scanning. Set `CICADEL_TEST_DATABASE_URL` to the disposable PostgreSQL server. The root scaffold suite now tests `startproject` in a separate module; it requires the same integration prerequisites. SMTP tests use isolated loopback servers and test certificates, and logging alerts use an in-memory outbox. They do not deliver external email. See the workflow commands in `.github/workflows/accounts.yml`.
