Repository navigation
Go Tests
This page covers ALPS's Go tests: how to run them with go test and make, what they need, where they live, the test servers and mocks they use, and how to run one package or one test. The other two tiers are on Frontend Unit Tests and Browser Tests, and Testing Overview shows how the three fit together. The reference is the Go tests section of docs/E2E.md.
From the repository root:
go test ./... # every package, one ok or FAIL line each
go test -short ./... # the same, without the one slow test
make test # go test -v ./...: every test by name-
go test ./...prints one line per package. The four packages with no tests of their own,internal/netguard,internal/serviceurl,provider/providertestandtlsmanager/storage/storagetest, print[no test files]. -
make testrunsgo test -v ./.... It prints the name of every test and subtest it runs, thousands of lines in all. Each package still ends with itsokorFAILline, and make exits non-zero when any package fails. - The
testtarget takes no arguments. To pass a flag through it, setGOFLAGS, for exampleGOFLAGS=-short make test, or callgo testyourself. -
-shortskipsTestAnswerSavedWhenTheReplyCannotBeSentinplugins/caldav, which waits out the SMTP submission retries. No other test checks-short. - At main d1cc02cc the whole suite took about 40 seconds on a current laptop.
plugins/baseis the longest package, at about 35 seconds. - A package whose code and test inputs have not changed since its last passing run prints
(cached)and does not run again. Add-count=1to run it anyway. -
go test -race ./...runs the same tests under Go's race detector. On macOS that needs nothing more. On Linux, FreeBSD, NetBSD and Windows, Go's-raceneeds cgo, so the machine needs a C compiler. Go has no race detector for OpenBSD.
The repository has no CI, so these tests run only when someone runs them. The README asks for the Go tests and the frontend unit tests before every change.
Only Go. The module needs Go 1.25.0 or later and names toolchain go1.26.5. With the default GOTOOLCHAIN=auto, an older go command (Go 1.21 or later) downloads go1.26.5 first, as it does for a build. The README's requirements have the details.
The tests need no Node.js, PostgreSQL, sora, Docker, mail server, network or configuration file. Every server a test talks to runs inside the test process, listening on 127.0.0.1 or on an in-memory connection, and the system picks every port, the cluster package's included.
Tests that would otherwise look names up in DNS answer the lookups themselves. The provider/multi tests, which route logins by domain and discover servers through SRV records, are given a resolver that refuses every query, and TestRoutingFixturesDoNotUseSystemDNS fails if the routing tests send a query to the system's resolver. The base plugin's BIMI tests replace the TXT lookup with a function of their own.
These things come from the machine or the checkout:
-
/bin/sh. The tests of the password plugin's command backend (plugins/password/command_test.goandplugins/base/http_password_command_test.go) write small shell scripts into a temporary directory and run them. The scripts use standard commands such ashead,tr,sleepandenv. The temporary directory (TMPDIR) must allow running programs. On Windows these tests skip themselves. -
bash.scripts/e2e_results_test.goruns the first part ofscripts/e2e.sh, up to the build, withbashand with small stand-ins forgo,npm,psql,curlandlsof, in a temporary directory. It checks that the record Playwright's--last-failedreads survives the script's cleanup, and that--last-failedwithout a record stops with a clear message. On Windows it skips itself. -
The
gocommand. Eightcmd/alpstests build ALPS with thegofound onPATHinto a temporary directory, start it with a configuration they write, and read what it logs. The ALPS they start listens on a port the system picks. On Windows these tests skip themselves. They check:- where log lines go, and what ALPS does on SIGHUP, also during startup;
- which plugins load when a
[plugin.<name>]section leavesenabledout or sets it tofalse; - the startup warnings for unknown configuration keys and for WebAuthn origins.
TestStartupExamplesDoNotWarnAboutDynamicOptionKeysstarts ALPS with the checkout'sconfig.example.tomland fails on any such warning; - that a node whose startup fails leaves the cluster before it exits.
-
Time zone data. A few
plugins/caldavtests loadEurope/Berlin. Without the system's tzdata they skip themselves withno tzdata:. -
Frontend files. The root package embeds
frontend/dist(//go:embed all:frontend/distinserver.go). That directory is checked in. While it is missing or empty, the root package does not compile, and neither docmd/alps, the plugins andinternal/e2estack, which import it directly or through a plugin. Some root-package tests also read frontend files:-
static_test.goreads the embeddedfrontend/dist/index.htmland the bundle it names, and checks the names of the files infrontend/public; -
branding_test.gochecks that the themes infrontend/src/store/themes.ts, and the logos infrontend/src/utils/branding.tsandfrontend/public/assets/icons/sprite.svg, match the Go side, and that each of those logos has its icon files underfrontend/public/assets/logos/.
A frontend change can therefore fail a Go test. The Go tests only read these files. They do not build or run the frontend, so they need no Node.js.
-
Name a package by its directory. The root package is .:
go test ./provider/maildir
go test .
go test -v -run '^TestHTTP_AuthVerdicts$' ./plugins/base
go test -v -run 'TestSessionEndIsLogged/idle' .-
-runtakes a regular expression over test names. Without^and$,-run TestHTTP_AuthVerdictsalso runsTestHTTP_AuthVerdictsFromAProviderWithoutThem. - A slash selects subtests:
TestSessionEndIsLogged/idleruns only theidlecase of that test. - A pattern that matches nothing is not an error. The package prints
ok … [no tests to run]andgo testexits 0, so check the name when a run finishes suspiciously fast. -
-vshows each test's name and what it logs.
WEBAUTHN_COORDINATION.md lists the tests for how ALPS stores security keys and recovery codes, and the commands that run them.
make test-coverage runs two commands:
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.outThe first writes coverage.out in the repository root, where .gitignore already lists it. The second writes the HTML report to a temporary file and opens it in a browser. When no browser can be opened, it prints HTML output written to <file> instead.
To keep the report as a file, or on a machine without a browser, run the steps yourself:
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
go tool cover -func=coverage.out | tail -1 # the total over all packagesEach package's percentage counts only the code that package's own tests run. The four packages without tests of their own show 0.0% in the coverage run, although other packages' tests run their code.
Each test file sits beside the code it tests, in the same package, so tests can use unexported names. The one exception is scripts/e2e_results_test.go, in package scripts_test, since scripts/ holds no Go code of its own. Every package has tests except four:
-
internal/netguard, the shared check that keeps outgoing connections to untrusted URLs on public addresses. It is tested through the packages that use it:plugins/base,provider/multiand the root package's CalDAV and CardDAV connections. -
internal/serviceurl, which reads the configured SMTP, CalDAV, CardDAV and ManageSieve server URLs. It is tested through the packages that use it, such as the root package (smtp_scheme_test.go) and the DAV plugins (server_url_test.go). -
provider/providertestandtlsmanager/storage/storagetest, which hold the shared test doubles (see The mock provider and store).
| Package | What its tests cover |
|---|---|
. (the root package, alps) |
Sessions and the session manager, sign-in, linked accounts, the CalDAV and CardDAV connections users set in Settings, middleware, rate limiting, the cache, the access log and syslog output, push, CONDSTORE, which plugins load, SMTP submission, uploads and the temporary-file sweep, startup and shutdown, and the embedded frontend and branding |
cmd/alps |
Reading the TOML configuration (config_*_test.go), the cluster bind address (cluster_bind_test.go), the TLS settings built from it (main_test.go), the Let's Encrypt HTTP listener and how the listeners shut down (acme_http_test.go), and the built binary's startup, logging and signals (see What the tests need) |
cluster |
Gossip membership, leader election, broadcasts, rejoining after the cluster splits, and the bind address check |
internal/davsave |
Saving a CalDAV or CardDAV object only while it is still the version that was read |
internal/imip, internal/itip
|
Sending calendar replies by email, and reading and answering invitations |
internal/debuglog |
Protocol traces written through [logging] at debug level, with credential headers left out of HTTP traces |
internal/e2estack |
The test stack the browser tests run against: its object store, outbox and DAV server |
plugins/base |
The HTTP API end to end: the real server, called over HTTP the way the frontend calls it |
plugins/caldav, plugins/carddav
|
Calendars, tasks, invitations and contacts, against an in-memory DAV server |
plugins/gpg |
The keypair routes: a stored keypair is kept until a save asks to replace it or the keypair is purged |
plugins/managesieve |
The ManageSieve client and the filter routes |
plugins/password |
Password change, including the command backend |
provider |
The provider registry, MIME part decoding, message references and the shared types |
provider/imap |
The IMAP provider, against in-memory and scripted IMAP servers |
provider/maildir |
The maildir provider, in temporary directories |
provider/multi |
Routing a login to its backend, and DNS autodiscovery |
scripts |
How scripts/e2e.sh keeps Playwright's record of the last run (see What the tests need) |
tlsmanager, tlsmanager/storage
|
The certificate warmer and its expiry reports, where staging certificates are kept, how long a TLS handshake may wait for a new certificate, the startup check of the S3 bucket, and certificate storage in S3: the local cache, the key prefix and the sync worker |
A plugin's Go tests go in plugins/<name>/*_test.go. Testing a Plugin in PLUGINS.md says where its frontend tests and browser specs go. See also Plugins.
Tests that need a server start their own:
-
The real ALPS server, over a maildir.
newTestServerinplugins/base/http_test.go, and its variants (newTestServerWithSMTP,newTestServerWithOptions,newTestServerWrappingand others), build a real server withalps.New:- one account,
ada@example.com, in a passwd file and a maildir undert.TempDir(), withINBOX,TrashandArchiveand two messages; - a generated login key and the cache on;
- served with
net/http/httptest, behind a client with a cookie jar.
Its
domethod sends requests the way the frontend does: JSON bodies (or form fields), and the page's ownOriginon every request that is not a GET.plugins/gpghas a smaller harness of the same kind,signedIninroutes_test.go. - one account,
-
The real ALPS server, over IMAP.
newIMAPTestServerandnewIMAPTestServerWithinplugins/base/verdicts_test.godo the same with the IMAP provider, against an in-memory IMAP server that holds the given messages inINBOX.newIMAPTestServerWithalso sets the capabilities the server lists, and can wrap each IMAP session to change how it answers. Theprovider/imaptests start IMAP servers of their own on loopback: the same in-memory server, or scripted servers that answer with fixed lines. -
SMTP. These harnesses point SMTP at
smtp://127.0.0.1:1, where nothing listens, so every connection is refused. ALPS makes three attempts at a submission whose connection fails, one and then two seconds apart, so a test that sends to that address takes about three seconds. That is why-shortskipsTestAnswerSavedWhenTheReplyCannotBeSent. A test that needs the mail to arrive starts an SMTP server on a loopback port and passes its URL, asnewTestServerWithSMTPtakes it. -
CalDAV and CardDAV.
plugins/caldavandplugins/carddaveach havenewHarness, in theirconflict_test.go. It runs the real server over a maildir, with the plugin pointed at an in-memory CalDAV or CardDAV server served byhttptest. The caldav harness fixes the plugin's clock to one moment, so tests about upcoming events do not depend on the day they run. -
ManageSieve. The
plugins/managesieveclient tests feed the client scripted server responses over an in-memory connection (net.Pipe). The route tests (routes_test.go) run the real ALPS server over a maildir, with the plugin pointed at a scripted ManageSieve server on a loopback port. -
Cluster. The
clustertests start real gossip members on 127.0.0.1. Each asks for port 0, so the system picks a free port, and the members find each other by the addresses they were given.
The package provider/providertest (mock_provider.go and mock_store.go) holds test doubles for code that talks to the mail store:
| Type | Stands in for |
|---|---|
providertest.MockProvider |
provider.MailProvider: one connection to the mail store |
providertest.MockStore |
provider.Store: the per-account store ALPS keeps settings in (see Storage in IMAP METADATA) |
providertest.MockOptions |
provider.Options: a configured provider. Its Type() is "mock", and its factory returns a new MockProvider, with no calls set up, for any login |
MockProvider and MockStore embed mock.Mock. A test declares each call it expects with On(method, args...) and its answer with Return(...). mock.Anything matches any argument, and a call the test did not declare panics. The root package's tests set up a connection whose store holds nothing like this:
p := &providertest.MockProvider{}
store := &providertest.MockStore{}
p.On("Close").Return(nil)
p.On("GetStore").Return(store, nil)
store.On("Get", mock.Anything, mock.Anything).Return(provider.ErrNoStoreEntry)-
MockStore.Getreturns only an error. It never fillsoutunless the test adds aRun(...)that does. For a store that keeps what is written and gives it back, the tests use small in-memory stores of their own namedrawStore, which keep each entry as JSON. The root package's is inauth_utils_test.go, and the base plugin's is inplugins/base/settings_test.go. -
ParseMessageIDis not a mocked call. It returns aprovidertest.MockMessageIDholding the string, unless the test setsParseMessageIDFunc. -
Optional interfaces.
MockProviderimplementsMailProviderand nothing more. To test code that uses an optional interface fromprovider/types.go, such asChangeTracker,IdlerorQuotaReader, embedprovidertest.MockProviderin a struct of your own and add the methods.trackingConnectioninsession_condstore_test.goandidlingConnectioninsession_push_test.gowork this way. -
The mocks are a package of their own. Their files are ordinary files, not
_test.gofiles, so tests in any package can import them, and only tests do, so neither the mocks nor testify are built into thealpsbinary.MockOptionsreturns aMockProvideras aMailProvider, so a method added toMailProviderstopsprovidertest, and every test package that imports it, from compiling untilMockProviderhas it too. -
The mock cannot be configured.
"mock"is not a registered provider type, so ALPS refuses a configuration file that sets[provider] type = "mock".
One more shared double is storagetest.MockS3Client, in tlsmanager/storage/storagetest/mock_s3.go. It keeps objects in a map, counts reads, and can be set to fail reads, writes or deletes.
Every Go test listens on ports the system picks, the cluster package's gossip members included, so two Go test runs can overlap on one machine, from one checkout or two, and a Go test run does not compete with a browser test run for ports. The built binaries and the stand-in tools the tests use go into temporary directories of their own. The browser run's frontend build is another matter: unless E2E_SKIP_BUILD is set, it rewrites frontend/dist in place, and the root package embeds that directory. In the same checkout, start the Go tests once that build has finished, or run them from another checkout. Running several browser runs side by side is covered on Browser Tests.
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