Files
Warp/server/internal/settings/settings_test.go
T
tajniak81andClaude Opus 5 b4fb60a8ab
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
Phase 0: bootstrap the Warp homeserver
Scaffolds the server module and everything Phase 0 of PLAN.md calls for:
a binary that boots, serves its discovery endpoints and nothing else yet.

- config: one YAML file plus per-field WARP_* environment overrides,
  unknown fields rejected, validation that reports every problem at once
- logging and metrics: slog, a Prometheus registry, per-route HTTP
  instrumentation labelled by pattern rather than path
- httputil: the Matrix error envelope, error-returning handlers, CORS,
  request logging, panic recovery, and a router over http.ServeMux
- endpoints: /_matrix/client/versions, /.well-known/matrix/client,
  /.well-known/matrix/server (gated), /healthz, /metrics, and
  M_UNRECOGNIZED for everything else
- storage: driver registry, shared database/sql base, checksummed
  migration runner, sqlite and postgres drivers, pocketbase stub
- settings: server_settings table behind a cached service, so the
  federation and registration switches change without a restart
- federation: the gate that makes federation a runtime mode
- cmd/warp: serve, migrate, generate-keys, version
- Makefile, golangci-lint v2, GitHub Actions CI (Linux and Windows tests,
  race on Linux, storage tests against PostgreSQL, lint, boot smoke test),
  Dockerfile, example config and docs

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 17:01:13 +02:00

201 lines
5.1 KiB
Go

package settings_test
import (
"context"
"errors"
"sync"
"testing"
"github.com/dariuszjaniak/warp/server/internal/settings"
)
// memStore is an in-memory storage.SettingsStore.
type memStore struct {
mu sync.Mutex
values map[string]string
failSet error
}
func newMemStore(initial map[string]string) *memStore {
values := map[string]string{}
for k, v := range initial {
values[k] = v
}
return &memStore{values: values}
}
func (m *memStore) Get(_ context.Context, key string) (string, bool, error) {
m.mu.Lock()
defer m.mu.Unlock()
v, ok := m.values[key]
return v, ok, nil
}
func (m *memStore) GetAll(context.Context) (map[string]string, error) {
m.mu.Lock()
defer m.mu.Unlock()
out := map[string]string{}
for k, v := range m.values {
out[k] = v
}
return out, nil
}
func (m *memStore) Set(_ context.Context, key, value string) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.failSet != nil {
return m.failSet
}
m.values[key] = value
return nil
}
func (m *memStore) Delete(_ context.Context, key string) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.values, key)
return nil
}
func togglableDefaults() settings.Defaults {
return settings.Defaults{
FederationEnabled: false,
FederationAllowRuntimeToggle: true,
RegistrationEnabled: false,
RegistrationAllowRuntimeToggle: true,
}
}
func TestStoredValueOverridesConfig(t *testing.T) {
store := newMemStore(map[string]string{settings.KeyFederationEnabled: "true"})
svc, err := settings.New(t.Context(), store, togglableDefaults(), nil)
if err != nil {
t.Fatalf("New: %v", err)
}
if !svc.FederationEnabled() {
t.Error("the persisted value should win over the config default")
}
}
func TestPinnedSettingIgnoresStoredValue(t *testing.T) {
store := newMemStore(map[string]string{settings.KeyFederationEnabled: "true"})
defaults := togglableDefaults()
defaults.FederationAllowRuntimeToggle = false
svc, err := settings.New(t.Context(), store, defaults, nil)
if err != nil {
t.Fatalf("New: %v", err)
}
if svc.FederationEnabled() {
t.Error("with allow_runtime_toggle false the config file is authoritative")
}
if err := svc.SetFederationEnabled(t.Context(), true); !errors.Is(err, settings.ErrNotToggleable) {
t.Errorf("SetFederationEnabled = %v, want ErrNotToggleable", err)
}
if _, found, _ := store.Get(t.Context(), settings.KeyFederationEnabled); !found {
t.Error("the stored value should be kept for when the toggle is re-enabled")
}
}
func TestUnparseableStoredValueFallsBackToConfig(t *testing.T) {
store := newMemStore(map[string]string{settings.KeyRegistrationEnabled: "maybe"})
defaults := togglableDefaults()
defaults.RegistrationEnabled = true
svc, err := settings.New(t.Context(), store, defaults, nil)
if err != nil {
t.Fatalf("New: %v", err)
}
if !svc.RegistrationEnabled() {
t.Error("a corrupt stored value must fall back to the config value, not to false")
}
}
func TestSetPersistsAndNotifies(t *testing.T) {
store := newMemStore(nil)
svc, err := settings.New(t.Context(), store, togglableDefaults(), nil)
if err != nil {
t.Fatal(err)
}
var got []settings.Snapshot
svc.Subscribe(func(s settings.Snapshot) { got = append(got, s) })
if err := svc.SetFederationEnabled(t.Context(), true); err != nil {
t.Fatalf("SetFederationEnabled: %v", err)
}
if !svc.FederationEnabled() {
t.Error("the cached value was not updated")
}
if v, _, _ := store.Get(t.Context(), settings.KeyFederationEnabled); v != "true" {
t.Errorf("stored value = %q, want \"true\"", v)
}
if len(got) != 1 || !got[0].FederationEnabled {
t.Fatalf("subscriber saw %v, want one snapshot with federation on", got)
}
// Setting the same value again changes nothing and must not notify.
if err := svc.SetFederationEnabled(t.Context(), true); err != nil {
t.Fatal(err)
}
if len(got) != 1 {
t.Errorf("a no-op change notified subscribers: %v", got)
}
}
func TestSetKeepsCacheOnStoreFailure(t *testing.T) {
store := newMemStore(nil)
store.failSet = errors.New("disk on fire")
svc, err := settings.New(t.Context(), store, togglableDefaults(), nil)
if err != nil {
t.Fatal(err)
}
if err := svc.SetFederationEnabled(t.Context(), true); err == nil {
t.Fatal("expected the store failure to surface")
}
if svc.FederationEnabled() {
t.Error("the cached value must not change when persistence fails")
}
}
func TestSnapshotIsConsistent(t *testing.T) {
store := newMemStore(map[string]string{
settings.KeyFederationEnabled: "true",
settings.KeyRegistrationEnabled: "true",
})
svc, err := settings.New(t.Context(), store, togglableDefaults(), nil)
if err != nil {
t.Fatal(err)
}
snap := svc.Snapshot()
if !snap.FederationEnabled || !snap.RegistrationEnabled {
t.Errorf("Snapshot() = %+v, want both enabled", snap)
}
}
func TestConcurrentReadsAndWrites(t *testing.T) {
store := newMemStore(nil)
svc, err := settings.New(t.Context(), store, togglableDefaults(), nil)
if err != nil {
t.Fatal(err)
}
var wg sync.WaitGroup
for i := range 8 {
wg.Add(1)
go func() {
defer wg.Done()
if i%2 == 0 {
_ = svc.SetFederationEnabled(t.Context(), i%4 == 0)
return
}
_ = svc.FederationEnabled()
_ = svc.Snapshot()
}()
}
wg.Wait()
}