-
Notifications
You must be signed in to change notification settings - Fork 2
Testing and In Memory
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
- In-process client/server (no network)
- Exercising a real transport in memory
- Testing patterns
- A minimal test server
- Logging in tests
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 setsnoLogs, 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.
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 dialerThis 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 theteam.Clientbackend (via an implicitWithTeamClient(server)). Passclient.WithDialer(...)only when you want the self-client to go through a real transport.
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/bufconnServe(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.
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())(orWithInMemory()) so tests never collide with a real~/.<app>/. The encryption-at-rest test usest.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 unexportedinit()to force that bootstrap; from an external test package,ts.Serve(self)(or the firstUserCreate) does the same via the public API. -
Deterministic identity round-trips.
UserCreate→Authenticatemaps a token back to its user; assert two users get distinct tokens and certs, and thatUserDeletethen makesAuthenticatereturnErrUnauthenticated(revocation invalidates the cache, not just the DB row — authenticate once before deleting to prove it). -
PKI assertions.
UsersTLSConfig()should reportRequireAndVerifyClientCert, TLS 1.3 minimum, exactly one server certificate and non-nil CA pools.UsersGetCA/UsersSaveCAround-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 callos.Exit, so you can invoke the cobra tree in a test and inspect returned errors and output viacmd.OutOrStdout().
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)
}-
Silence output with
WithNoLogs(true), or capture it withWithLogger(handler)where the handler writes to abytes.Bufferyou later assert on. - For an in-memory core that still needs a file logger, open a file on
Server.Filesystem()and pass the writer tolog.New(...)— see Logging → in-memory logging.
Related: Architecture → · Server API → · Logging → · Writing a Transport → · Configuration →