Repository navigation
Testing Overview
ALPS has three test tiers: Go tests for the backend, frontend unit tests that run in Node.js under jsdom, and browser tests that drive the built application in Chromium against a real mail stack. This page covers what each tier catches and how to run all three before you open a pull request. The repository has no CI, so these local runs are the only testing a change gets. The details are on Go Tests, Frontend Unit Tests, Browser Tests and in docs/E2E.md.
| Tier | Run with | Needs | Tests live in |
|---|---|---|---|
| Go tests |
go test ./... or make test
|
Go 1.25.0 or later, nothing else |
*_test.go beside the sources |
| Frontend unit tests |
cd frontend && npm test (a typecheck, then Vitest) |
Node.js, and npm install in frontend/
|
frontend/test/**/*.test.ts |
| Browser tests |
scripts/e2e.sh or make test-e2e
|
Go, Node.js, PostgreSQL, a sora checkout, Playwright's Chromium | frontend/e2e/*.spec.ts |
Use Node.js 20 (20.19 or later), 22 (22.12 or later), or 24 and later. On Node.js 23, npm warns during install that the locked Vitest does not support it (EBADENGINE). The README has the version details.
The Go tests cover the backend: HTTP handlers, configuration loading and startup (cmd/alps), the imap, maildir and multi providers, sessions, protocol handling, the cluster, the TLS manager, the Go code of all six plugins, the browser harness's test stack (internal/e2estack), and how scripts/e2e.sh keeps the record of the last browser run (scripts). Every package has tests except four. internal/netguard and internal/serviceurl are tested through the packages that use them, and provider/providertest and tlsmanager/storage/storagetest hold the shared test doubles.
They need no outside server and no setup. The servers a test talks to run inside the test process on loopback, on ports the system picks: in-memory IMAP servers, SMTP servers, HTTP test servers, and, in the cluster package, gossip members. The base plugin's tests run the real server over a Maildir in a temporary directory or over an in-memory IMAP server. Eight cmd/alps tests build the alps binary and run it, and the scripts test runs the first part of scripts/e2e.sh with stand-in tools. No Go test runs frontend code, but the root package embeds the checked-in frontend/dist, and static_test.go checks how the server serves it. See Go Tests.
npm test first typechecks the tests (npm run typecheck:test), then runs vitest run under jsdom. These tests check logic and render decisions: what a component shows for given data, what it does on an event, what a store keeps. jsdom does no layout or painting, and no ALPS backend runs behind the tests.
Only files under frontend/test/ that end in .test.ts run, and the tests for plugin frontends live there too. A *.test.ts file next to a source file never runs. See Frontend Unit Tests.
scripts/e2e.sh builds the frontend, ALPS, the test stack (internal/e2estack) and sora, and creates a throwaway PostgreSQL database for the run. It then starts three things in order:
- the test stack: an object store for sora's message bodies, the SMTP submission server ALPS sends through, a relay for sora's own outgoing mail, a CalDAV/CardDAV server and a control API for the tests;
- sora, with IMAP, ManageSieve and its admin API;
- ALPS, with every plugin enabled and a fixed test
login_key.
Last, it runs Playwright in Chromium against ALPS.
This tier finds what the other two cannot see:
- a component that reads a field the backend does not send;
- a request the browser refuses;
- a page that logs an error and renders nothing;
- a flow that breaks only when a real IMAP server, a real SMTP submission and a real browser are all involved.
A browser test fails on any console error or uncaught exception on its page, as well as on its own assertions. Chromium logs a console error for every response that is not 2xx, so an unexpected refusal from the server fails the test too. A test that provokes an error on purpose allows that one line with allowConsoleErrors.
Some things the browser tests leave out, or test only when you ask for them:
-
Restoring a session, linked accounts and account switching. The harness sets a fixed test
login_key, which the DAV connection tests need, so every sign-in also gets thealps_login_tokencookie. No spec tests restoring a session from it, linked accounts or account switching (see Sessions and Sign-in). The Go tests cover these features. -
Two ALPS nodes.
e2e/cluster-recovery.spec.tsruns only withE2E_CLUSTER=1, which starts a second ALPS clustered with the first. Without that variable, the spec skips itself. -
Message previews. The default run checks that ALPS shows no previews when the server offers none. The test that previews do appear runs only with
E2E_PREVIEW=1, which needs aSORA_DIRcheckout that supports FETCH PREVIEW. -
A DAV connection with no default server. The default run tests the CalDAV and CardDAV connections users set in Settings against the harness's default servers.
E2E_DAV_PERSONAL=1leaves those servers out, so thate2e/dav-connections.spec.tstests setting up a connection from nothing.
See Browser Tests.
Set up once:
cd frontend
npm install
npx playwright install chromium
cd ..Then, before each pull request, from the repository root:
go test ./...
(cd frontend && npm test)
scripts/e2e.sh
git status-
go test ./...needs nothing running.make testruns the same tests with-v.go test -short ./...skipsTestAnswerSavedWhenTheReplyCannotBeSent, which waits out the SMTP submission retries. Some caldav tests skip themselves when the system has no time zone data.make test-coverageruns the Go tests with a coverage profile, writescoverage.outand opens its HTML report. -
npm testinfrontend/typechecksfrontend/test, then runs the whole Vitest suite. A type error in a test stops it before any test runs. Arguments after--go to Vitest, sonpm test -- test/compose-store.test.tsruns one file.npm run test:shuffleruns the same typecheck, then the suite in a random order, which shows a test that depends on what an earlier one left behind. A passing run can still print jsdom'sNot implemented: navigation to another Document. That line is not a failure: the exit status and theTest FilesandTestssummary lines give the result. -
scripts/e2e.shneeds these:-
go,npm,psql,curlandlsofonPATH; - a PostgreSQL server where the
pg_trgmandbtree_ginextensions can be installed (sora asks for PostgreSQL 14 or later), reached as a user that can connect to thepostgresdatabase and create and drop databases (E2E_PG_HOST,E2E_PG_PORT,E2E_PG_USER,E2E_PG_PASSWORD); - a sora checkout at
../sora, or whereverSORA_DIRpoints; - Playwright's Chromium from the setup step, or an installed Chromium named with
PW_CHROMIUM_PATH.
By default ALPS listens on port 8900 and the other servers on the next eight ports. The script stops before it builds anything if one of those ports is already taken. To run one spec file, give its path with the
e2e/prefix, as inscripts/e2e.sh e2e/reading.spec.ts. Playwright reads a file argument as a regular expression, so a barereading.spec.tsalso runsthreading.spec.ts. Running the browser tests lists every variable, and When a run fails explains each error message. -
-
git status, because the browser run rebuilds the trackedfrontend/distin place withnpm run build, unlessE2E_SKIP_BUILDis set. When the committedfrontend/distwas built from the same sources, the rebuild leaves it unchanged. Iffrontend/distshows as modified, decide whether the change belongs in your pull request.
The README asks for the Go tests and the frontend unit tests before every change, and for the browser suite as well when a change touches UI flows. A change to the frontend or to a plugin's frontend should include the rebuilt frontend/dist. Run npm run build in frontend/ (tsc, then vite build) and commit the result.
The same Contributing section says where to send patches and report bugs. The README's AI Assistance Policy asks you to disclose, in the commit messages or the pull request description, any substantive use of generative AI that materially affected the change.
Some changes need an extra check:
| When you change | Also run |
|---|---|
A browser spec or a helper in frontend/e2e/, such as fixtures.ts
|
cd frontend && npm run typecheck:e2e, which typechecks frontend/e2e/ and playwright.config.ts. It cannot tell whether a locator still matches what the page renders; only running the suite shows that. |
| Frontend or plugin frontend sources |
cd frontend && npm run build. Its tsc step typechecks frontend/src/ and plugins/*/frontend/. |
| The code that stores and updates security keys and recovery codes | The checks in WEBAUTHN_COORDINATION.md: the Go tests named there, the E2E_CLUSTER=1 browser run, and python3 scripts/model-recovery-versions.py, which runs on Python 3.9 or later. |
npm test is the step that typechecks frontend/test. It runs npm run typecheck:test first, which checks the tests, and the sources they import, through tsconfig.test.json. A type error in a test, or a fake that no longer matches the interface it stands in for, fails npm test before any test runs. Neither npm run build nor npm run typecheck:e2e checks the tests, and npx vitest run on its own skips the typecheck.
The repository has no CI configuration: no .github workflows and no other pipeline file. No tests run automatically when you open a pull request. They run only when someone runs them locally, so a reviewer knows which tiers ran only if the pull request says so.
No single command runs all three tiers. make test runs only the Go tests, make test-e2e runs only the browser suite, and no Makefile target runs Vitest.
-
Go tests. Every Go test listens on ports the system assigns, the
clusterpackage's gossip members included, so two Go test runs can overlap on one machine, from one checkout or two. -
Browser tests. Two runs can go side by side when their
E2E_PORTvalues are at least 9 apart (12 apart when either usesE2E_CLUSTER=1) and they do not share anE2E_STATE,E2E_LOGSorE2E_RESULTS. The defaults of all three followE2E_PORT, so only values you set yourself can clash. They take turns at the build step, so a second run may printwaiting for another run's build. See Runs side by side. -
One of each. Because the Go tests listen only on ports the system assigns, a Go test run does not collide with a browser run on the default ports. In one checkout, keep
go test ./...from compiling while the browser run builds the frontend, because the Go tests compile infrontend/distand that build rewrites it. Start the Go tests after the browser run printsbuilding alps, sora and the test stack, or give the browser runE2E_SKIP_BUILD=1.
-
Go tests: one
okorFAILline per package. The four packages without tests of their own (internal/netguard,internal/serviceurl,provider/providertestandtlsmanager/storage/storagetest) report[no test files]. -
Vitest: the summary at the end gives the number of test files and tests that passed or failed. When the typecheck fails,
tscprints each error with its file and line, and Vitest does not start. -
Browser tests: Playwright's
listreporter prints each test as it runs.- If the run ends with
a server died during the run, every result after that point is void, and the run exits 1 whatever Playwright reported. Before you look at the failures, read the named server's log in the logs directory that the message prints. - A test that fails with
the page logged errorsmeans the application logged a console error or threw an uncaught exception. That is a finding, not noise: look at what was logged. - Traces and screenshots of failed tests are in
E2E_RESULTS, by defaulttest-results/in the logs directory (${TMPDIR:-/tmp}/alps-e2e-logs-<port>/test-results). When tests fail and every server is still running, the script ends by printing both directories, as==> server logs:and==> traces and screenshots:. Open a trace fromfrontend/withnpx playwright show-trace <path to trace.zip>. - To run only the tests that failed, run
scripts/e2e.sh --last-failedwith the sameE2E_PORT, and the sameE2E_LOGSorE2E_RESULTSif you set them. See Browser Tests.
- If the run ends with
These pages describe ALPS as it is on the main branch; the reference documents live in the repository's docs/ directory.
Architecture
- Architecture Overview
- Mail Server Requirements
- Sessions and Sign-in
- Storage in IMAP METADATA
- Mail Providers
- Plugins
- Features
- Security Features
- Clustering
Tests
Configuration
- Configuration Overview
- Server
- Providers and SMTP
- TLS
- Cluster
- Rate Limiting and WebAuthn
- Logging and Cache
- Plugins
- Branding
Common Setups