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>
150 lines
4.3 KiB
Go
150 lines
4.3 KiB
Go
package storage_test
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"testing"
|
|
|
|
"github.com/dariuszjaniak/warp/server/internal/storage"
|
|
_ "github.com/dariuszjaniak/warp/server/internal/storage/drivers"
|
|
)
|
|
|
|
// openTestDB opens each driver that can run without external services. The
|
|
// postgres driver joins in when WARP_TEST_POSTGRES_DSN is set, which is how
|
|
// CI runs the same tests against both engines.
|
|
func openTestDB(t *testing.T, driver string) storage.Database {
|
|
t.Helper()
|
|
|
|
cfg := storage.Config{Driver: driver, MaxOpenConns: 4, MaxIdleConns: 2}
|
|
switch driver {
|
|
case storage.DriverSQLite:
|
|
path := filepath.ToSlash(filepath.Join(t.TempDir(), "warp.db"))
|
|
cfg.ConnectionString = "file:" + path + "?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)"
|
|
case storage.DriverPostgres:
|
|
dsn := os.Getenv("WARP_TEST_POSTGRES_DSN")
|
|
if dsn == "" {
|
|
t.Skip("set WARP_TEST_POSTGRES_DSN to test the postgres driver")
|
|
}
|
|
cfg.ConnectionString = dsn
|
|
default:
|
|
t.Fatalf("no test setup for driver %q", driver)
|
|
}
|
|
|
|
db, err := storage.Open(t.Context(), cfg, nil)
|
|
if err != nil {
|
|
t.Fatalf("Open(%s): %v", driver, err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
return db
|
|
}
|
|
|
|
// testDrivers are run by every storage test.
|
|
var testDrivers = []string{storage.DriverSQLite, storage.DriverPostgres}
|
|
|
|
func TestDriversRegistered(t *testing.T) {
|
|
got := storage.Drivers()
|
|
for _, want := range []string{storage.DriverSQLite, storage.DriverPostgres, storage.DriverPocketBase} {
|
|
if !slices.Contains(got, want) {
|
|
t.Errorf("driver %q is not registered (have %v)", want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOpenUnknownDriver(t *testing.T) {
|
|
_, err := storage.Open(t.Context(), storage.Config{Driver: "mysql"}, nil)
|
|
if err == nil {
|
|
t.Fatal("expected an error for an unknown driver")
|
|
}
|
|
}
|
|
|
|
func TestPocketBaseIsNotImplemented(t *testing.T) {
|
|
_, err := storage.Open(t.Context(), storage.Config{
|
|
Driver: storage.DriverPocketBase,
|
|
ConnectionString: "./pb_data",
|
|
}, nil)
|
|
if !errors.Is(err, storage.ErrNotImplemented) {
|
|
t.Fatalf("expected ErrNotImplemented, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMigrateAndSettings(t *testing.T) {
|
|
for _, driver := range testDrivers {
|
|
t.Run(driver, func(t *testing.T) {
|
|
ctx := t.Context()
|
|
db := openTestDB(t, driver)
|
|
|
|
pending, err := db.PendingMigrations(ctx)
|
|
if err != nil {
|
|
t.Fatalf("PendingMigrations: %v", err)
|
|
}
|
|
if len(pending) == 0 {
|
|
t.Skip("database already migrated by a previous run")
|
|
}
|
|
|
|
if err := db.Migrate(ctx); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
// Migrating twice must be a no-op, not an error: it runs on
|
|
// every boot.
|
|
if err := db.Migrate(ctx); err != nil {
|
|
t.Fatalf("second Migrate: %v", err)
|
|
}
|
|
pending, err = db.PendingMigrations(ctx)
|
|
if err != nil {
|
|
t.Fatalf("PendingMigrations: %v", err)
|
|
}
|
|
if len(pending) != 0 {
|
|
t.Errorf("after Migrate, still pending: %v", pending)
|
|
}
|
|
|
|
settings := db.Settings()
|
|
if _, found, err := settings.Get(ctx, "federation.enabled"); err != nil {
|
|
t.Fatalf("Get: %v", err)
|
|
} else if found {
|
|
t.Fatal("expected no stored value on a fresh database")
|
|
}
|
|
|
|
if err := settings.Set(ctx, "federation.enabled", "true"); err != nil {
|
|
t.Fatalf("Set: %v", err)
|
|
}
|
|
if err := settings.Set(ctx, "federation.enabled", "false"); err != nil {
|
|
t.Fatalf("Set (overwrite): %v", err)
|
|
}
|
|
value, found, err := settings.Get(ctx, "federation.enabled")
|
|
if err != nil || !found {
|
|
t.Fatalf("Get after Set: value=%q found=%v err=%v", value, found, err)
|
|
}
|
|
if value != "false" {
|
|
t.Errorf("value = %q, want %q (the upsert must overwrite)", value, "false")
|
|
}
|
|
|
|
all, err := settings.GetAll(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetAll: %v", err)
|
|
}
|
|
if all["federation.enabled"] != "false" {
|
|
t.Errorf("GetAll = %v", all)
|
|
}
|
|
|
|
if err := settings.Delete(ctx, "federation.enabled"); err != nil {
|
|
t.Fatalf("Delete: %v", err)
|
|
}
|
|
if err := settings.Delete(ctx, "federation.enabled"); err != nil {
|
|
t.Errorf("deleting an absent key must not fail: %v", err)
|
|
}
|
|
if _, found, _ := settings.Get(ctx, "federation.enabled"); found {
|
|
t.Error("value survived Delete")
|
|
}
|
|
|
|
if err := db.Ping(ctx); err != nil {
|
|
t.Errorf("Ping: %v", err)
|
|
}
|
|
if db.Driver() != driver {
|
|
t.Errorf("Driver() = %q, want %q", db.Driver(), driver)
|
|
}
|
|
})
|
|
}
|
|
}
|