Skip to content

Testing and In Memory

maxlandon edited this page Jul 18, 2026 · 1 revision

Testing & In-Memory Use · [Dev]

The cores can run entirely without touching the host filesystem or network. That makes them easy to embed in tests, in a demo, or in a program that isn't ready to persist anything yet. The public API is identical either way — only the backends move to memory.


In-memory mode

WithInMemory() on either core redirects all filesystem interaction to an abstracted in-memory filesystem, and puts the server on an in-memory SQLite database:

teamserver, _ := server.New("myapp",
    server.WithInMemory(),
    server.WithDefaultPort(31340),
)
teamclient := teamserver.Self(client.WithInMemory())

Implications:

  • Log files, config files and the SQLite DB live in memory — nothing is written to disk.
  • WithInMemory() also sets noLogs, so file logging is off; the console logger still works.
  • Non-SQLite databases cannot run in memory, so combining one with WithInMemory() errors.
  • Everything else — users, certificates, TLS, the whole API — behaves exactly as on disk.

The in-memory filesystem is reachable via Server.Filesystem() / Client.Filesystem() if a test needs to assert on files the core wrote.


In-process client/server (no network)

A teamserver is a team.Client of itself with no configuration. teamserver.Self() returns an in-memory teamclient whose Users()/VersionServer() are answered directly by the server — no transport, TLS or tokens involved:

teamserver, _ := server.New("myapp", server.WithInMemory())
self := teamserver.Self(client.WithInMemory())

// Exercise the core directly.
cfg, _ := teamserver.UserCreate("alice", "localhost", 0)
users, _ := self.Users() // routed straight through the server, no dialer

This is the fastest way to test core logic (user lifecycle, PKI, config) because there is no handler to register and no connection to establish.

Note: Self() with no dialer uses the server itself as the team.Client backend (via an implicit WithTeamClient(server)). Pass client.WithDialer(...) only when you want the self-client to go through a real transport.


Exercising a real transport in memory

To test the actual RPC path — middleware, auth interceptors, serialization — without a network, use a bufconn-style backend. The gRPC example ships one: grpc.NewClientFrom(server) builds an in-memory dialer bound to a given listener.

gts := grpcserver.NewListener()
ts, _ := server.New("myapp", server.WithHandler(gts), server.WithInMemory())

gtc := grpcserver.NewClientFrom(gts)          // in-memory (bufconn) dialer, no TLS
self := ts.Self(client.WithDialer(gtc))

if err := ts.Serve(self); err != nil { /* ... */ } // serve default listener + connect self
defer self.Disconnect()

users, _ := self.Users() // now really goes over gRPC/bufconn

Serve(self) starts the default in-memory listener and connects the self-client in one call. This is exactly the shape of the repository's own authenticate_test.go. The in-memory path skips TLS and token auth; see Writing a Transport → In-memory vs remote.


Testing patterns

Drawn from the library's own tests (server/*_test.go, top-level *_test.go, internal/{certs,db}/*_test.go, log/*_test.go):

  • Isolate the home dir. Pass server.WithHomeDirectory(t.TempDir()) (or WithInMemory()) so tests never collide with a real ~/.<app>/. The encryption-at-rest test uses t.TempDir() precisely because it needs a real file to inspect on disk.
  • Bootstrap without a transport. The user primitives only need the DB + certificates. The test helper calls New(..., WithInMemory()) then the unexported init() to force that bootstrap; from an external test package, ts.Serve(self) (or the first UserCreate) does the same via the public API.
  • Deterministic identity round-trips. UserCreateAuthenticate maps a token back to its user; assert two users get distinct tokens and certs, and that UserDelete then makes Authenticate return ErrUnauthenticated (revocation invalidates the cache, not just the DB row — authenticate once before deleting to prove it).
  • PKI assertions. UsersTLSConfig() should report RequireAndVerifyClientCert, TLS 1.3 minimum, exactly one server certificate and non-nil CA pools. UsersGetCA/UsersSaveCA round-trip the whole CA.
  • Reject garbage safely. Empty and well-formed-but-unknown tokens must both be rejected without leaking an identity (user == nil, non-nil error).
  • No os.Exit. The generated commands never call os.Exit, so you can invoke the cobra tree in a test and inspect returned errors and output via cmd.OutOrStdout().

A minimal test server

The helper the repo uses (server/users_test.go) is a good template for an internal test; the public-API equivalent for an external _test package is shown alongside:

// Internal test (package server): New + unexported init() bootstraps DB + certs.
func newTestServer(t *testing.T) *Server {
    t.Helper()
    ts, err := New("usertest", WithInMemory())
    if err != nil {
        t.Fatalf("server.New: %v", err)
    }
    if err := ts.init(); err != nil {
        t.Fatalf("server.init: %v", err)
    }
    return ts
}

// External test (package myapp_test): drive the public API instead.
ts, _ := server.New("usertest", server.WithInMemory())
if _, err := ts.UserCreate("alice", "localhost", 31337); err != nil { // triggers init lazily
    t.Fatal(err)
}

Logging in tests

  • Silence output with WithNoLogs(true), or capture it with WithLogger(handler) where the handler writes to a bytes.Buffer you later assert on.
  • For an in-memory core that still needs a file logger, open a file on Server.Filesystem() and pass the writer to log.New(...) — see Logging → in-memory logging.

Related: Architecture → · Server API → · Logging → · Writing a Transport → · Configuration →

Clone this wiki locally