mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-08-20 11:56:23 +00:00
129 lines
4.2 KiB
Go
129 lines
4.2 KiB
Go
//go:build unit
|
|
|
|
// This file is only imported by unit tests
|
|
|
|
package testing
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/italypaleale/francis/components/standalone"
|
|
"github.com/italypaleale/francis/host/local"
|
|
"github.com/quic-go/quic-go"
|
|
"github.com/quic-go/quic-go/http3"
|
|
"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)
|
|
// Any extra options are appended last, so they override the defaults set here, which lets a test reproduce a production host setting such as the alarm poll interval
|
|
// 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), extraOpts ...local.HostOption) *local.Host {
|
|
t.Helper()
|
|
|
|
address := freeLoopbackUDPAddr(t)
|
|
hostOpts := []local.HostOption{
|
|
local.WithAddress(address),
|
|
local.WithRuntimePSKs([]byte(testActorHostPSK)),
|
|
local.WithStandaloneMemoryProvider(standalone.StandaloneMemoryOptions{}),
|
|
local.WithShutdownGracePeriod(time.Second),
|
|
}
|
|
hostOpts = append(hostOpts, extraOpts...)
|
|
|
|
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")
|
|
}
|
|
|
|
// Francis signals host readiness before starting the peer server, so wait for a remote TLS response before a fast test can trigger cleanup
|
|
waitForActorHostPeerServer(t, address, errCh)
|
|
|
|
return h
|
|
}
|
|
|
|
// waitForActorHostPeerServer waits until the WebTransport listener has passed the startup point that races with shutdown
|
|
func waitForActorHostPeerServer(t *testing.T, address string, errCh <-chan error) {
|
|
t.Helper()
|
|
|
|
// The probe intentionally omits the Francis client certificate because a remote TLS rejection is enough to prove the peer server is accepting connections
|
|
//nolint:gosec
|
|
tlsConfig := &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
NextProtos: []string{http3.NextProtoH3},
|
|
}
|
|
deadline := time.Now().Add(10 * time.Second)
|
|
|
|
for time.Now().Before(deadline) {
|
|
probeCtx, probeCancel := context.WithTimeout(t.Context(), 200*time.Millisecond)
|
|
conn, err := quic.DialAddr(probeCtx, address, tlsConfig, &quic.Config{})
|
|
probeCancel()
|
|
if conn != nil {
|
|
_ = conn.CloseWithError(0, "readiness probe complete")
|
|
return
|
|
}
|
|
|
|
var transportErr *quic.TransportError
|
|
if errors.As(err, &transportErr) && transportErr.Remote {
|
|
return
|
|
}
|
|
|
|
select {
|
|
case runErr := <-errCh:
|
|
t.Fatalf("actor host stopped before its peer server became ready: %v", runErr)
|
|
case <-time.After(10 * time.Millisecond):
|
|
}
|
|
}
|
|
|
|
t.Fatalf("timed out waiting for actor host peer server %s", address)
|
|
}
|
|
|
|
// freeLoopbackUDPAddr reserves a free loopback UDP port and returns its address
|
|
// The port is released before returning, so the actor host can bind it
|
|
func freeLoopbackUDPAddr(t *testing.T) string {
|
|
t.Helper()
|
|
|
|
var lc net.ListenConfig
|
|
lis, err := lc.ListenPacket(t.Context(), "udp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
|
|
addr := lis.LocalAddr().String()
|
|
err = lis.Close()
|
|
require.NoError(t, err)
|
|
|
|
return addr
|
|
}
|