Skip to content

Go Tests

Dejan Strbac edited this page Nov 12, 2026 · 1 revision

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.

Running the tests

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/providertest and tlsmanager/storage/storagetest, print [no test files].
  • make test runs go test -v ./.... It prints the name of every test and subtest it runs, thousands of lines in all. Each package still ends with its ok or FAIL line, and make exits non-zero when any package fails.
  • The test target takes no arguments. To pass a flag through it, set GOFLAGS, for example GOFLAGS=-short make test, or call go test yourself.
  • -short skips TestAnswerSavedWhenTheReplyCannotBeSent in plugins/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/base is 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=1 to 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 -race needs 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.

What the tests need

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.go and plugins/base/http_password_command_test.go) write small shell scripts into a temporary directory and run them. The scripts use standard commands such as head, tr, sleep and env. The temporary directory (TMPDIR) must allow running programs. On Windows these tests skip themselves.

  • bash. scripts/e2e_results_test.go runs the first part of scripts/e2e.sh, up to the build, with bash and with small stand-ins for go, npm, psql, curl and lsof, in a temporary directory. It checks that the record Playwright's --last-failed reads survives the script's cleanup, and that --last-failed without a record stops with a clear message. On Windows it skips itself.

  • The go command. Eight cmd/alps tests build ALPS with the go found on PATH into 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 leaves enabled out or sets it to false;
    • the startup warnings for unknown configuration keys and for WebAuthn origins. TestStartupExamplesDoNotWarnAboutDynamicOptionKeys starts ALPS with the checkout's config.example.toml and fails on any such warning;
    • that a node whose startup fails leaves the cluster before it exits.
  • Time zone data. A few plugins/caldav tests load Europe/Berlin. Without the system's tzdata they skip themselves with no tzdata:.

  • Frontend files. The root package embeds frontend/dist (//go:embed all:frontend/dist in server.go). That directory is checked in. While it is missing or empty, the root package does not compile, and neither do cmd/alps, the plugins and internal/e2estack, which import it directly or through a plugin. Some root-package tests also read frontend files:

    • static_test.go reads the embedded frontend/dist/index.html and the bundle it names, and checks the names of the files in frontend/public;
    • branding_test.go checks that the themes in frontend/src/store/themes.ts, and the logos in frontend/src/utils/branding.ts and frontend/public/assets/icons/sprite.svg, match the Go side, and that each of those logos has its icon files under frontend/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.

Running one package or one test

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' .
  • -run takes a regular expression over test names. Without ^ and $, -run TestHTTP_AuthVerdicts also runs TestHTTP_AuthVerdictsFromAProviderWithoutThem.
  • A slash selects subtests: TestSessionEndIsLogged/idle runs only the idle case of that test.
  • A pattern that matches nothing is not an error. The package prints ok … [no tests to run] and go test exits 0, so check the name when a run finishes suspiciously fast.
  • -v shows 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.

Coverage

make test-coverage runs two commands:

go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

The 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 packages

Each 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.

Where the tests live

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/multi and 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/providertest and tlsmanager/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.

Test servers and harnesses

Tests that need a server start their own:

  • The real ALPS server, over a maildir. newTestServer in plugins/base/http_test.go, and its variants (newTestServerWithSMTP, newTestServerWithOptions, newTestServerWrapping and others), build a real server with alps.New:

    • one account, ada@example.com, in a passwd file and a maildir under t.TempDir(), with INBOX, Trash and Archive and two messages;
    • a generated login key and the cache on;
    • served with net/http/httptest, behind a client with a cookie jar.

    Its do method sends requests the way the frontend does: JSON bodies (or form fields), and the page's own Origin on every request that is not a GET. plugins/gpg has a smaller harness of the same kind, signedIn in routes_test.go.

  • The real ALPS server, over IMAP. newIMAPTestServer and newIMAPTestServerWith in plugins/base/verdicts_test.go do the same with the IMAP provider, against an in-memory IMAP server that holds the given messages in INBOX. newIMAPTestServerWith also sets the capabilities the server lists, and can wrap each IMAP session to change how it answers. The provider/imap tests 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 -short skips TestAnswerSavedWhenTheReplyCannotBeSent. A test that needs the mail to arrive starts an SMTP server on a loopback port and passes its URL, as newTestServerWithSMTP takes it.

  • CalDAV and CardDAV. plugins/caldav and plugins/carddav each have newHarness, in their conflict_test.go. It runs the real server over a maildir, with the plugin pointed at an in-memory CalDAV or CardDAV server served by httptest. 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/managesieve client 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 cluster tests 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 mock provider and store

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.Get returns only an error. It never fills out unless the test adds a Run(...) that does. For a store that keeps what is written and gives it back, the tests use small in-memory stores of their own named rawStore, which keep each entry as JSON. The root package's is in auth_utils_test.go, and the base plugin's is in plugins/base/settings_test.go.
  • ParseMessageID is not a mocked call. It returns a providertest.MockMessageID holding the string, unless the test sets ParseMessageIDFunc.
  • Optional interfaces. MockProvider implements MailProvider and nothing more. To test code that uses an optional interface from provider/types.go, such as ChangeTracker, Idler or QuotaReader, embed providertest.MockProvider in a struct of your own and add the methods. trackingConnection in session_condstore_test.go and idlingConnection in session_push_test.go work this way.
  • The mocks are a package of their own. Their files are ordinary files, not _test.go files, so tests in any package can import them, and only tests do, so neither the mocks nor testify are built into the alps binary. MockOptions returns a MockProvider as a MailProvider, so a method added to MailProvider stops providertest, and every test package that imports it, from compiling until MockProvider has 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.

Two runs at once

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.

Clone this wiki locally