Files
pocket-id-pocket-id/backend/internal/utils/testing/actors.go

83 lines
2.5 KiB
Go
Raw Normal View History

//go:build unit
// This file is only imported by unit tests
package testing
import (
"context"
"net"
"testing"
"time"
"github.com/italypaleale/francis/components/standalone"
"github.com/italypaleale/francis/host/local"
"github.com/stretchr/testify/require"
)
// testActorHostPSK is the runtime pre-shared key for the test actor host
// It only needs to be a stable value of at least 32 bytes, since the test host never talks to another host
const testActorHostPSK = "pocket-id-test-actor-host-psk-32bytes"
// NewActorHostForTest starts a single-host Francis cluster backed by the in-memory provider, runs it, and waits until it is ready to serve invocations
// The register callback, if not nil, runs after the host is created but before it starts, so callers can register actors with host.RegisterActor/host.RegisterBuiltInActor (must be called before the host is running)
// The host is stopped when the test ends
// The in-memory provider keeps no state on disk, so the test never touches a real database
func NewActorHostForTest(t *testing.T, register func(t *testing.T, h *local.Host)) *local.Host {
t.Helper()
hostOpts := []local.HostOption{
local.WithAddress(freeLoopbackAddr(t)),
local.WithRuntimePSKs([]byte(testActorHostPSK)),
local.WithStandaloneMemoryProvider(standalone.StandaloneMemoryOptions{}),
local.WithShutdownGracePeriod(time.Second),
}
h, err := local.NewHost(hostOpts...)
require.NoError(t, err)
// Register built-in actors before the host starts
if register != nil {
register(t, h)
}
// Run the host in the background and stop it when the test ends
ctx, cancel := context.WithCancel(context.Background())
errCh := make(chan error, 1)
go func() {
errCh <- h.Run(ctx)
}()
t.Cleanup(func() {
cancel()
<-errCh
})
// Wait until the host is ready before returning, so callers can invoke actors immediately
select {
case <-h.Ready():
case err = <-errCh:
t.Fatalf("actor host stopped before becoming ready: %v", err)
case <-time.After(10 * time.Second):
t.Fatal("timed out waiting for the actor host to become ready")
}
return h
}
// freeLoopbackAddr reserves a free loopback port and returns its address
// The port is released before returning, so the actor host can bind it
func freeLoopbackAddr(t *testing.T) string {
t.Helper()
var lc net.ListenConfig
lis, err := lc.Listen(t.Context(), "tcp", "127.0.0.1:0")
require.NoError(t, err)
addr := lis.Addr().String()
err = lis.Close()
require.NoError(t, err)
return addr
}