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>
98 lines
2.9 KiB
Go
98 lines
2.9 KiB
Go
package federation_test
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/dariuszjaniak/warp/server/internal/federation"
|
|
)
|
|
|
|
func TestGateFollowsTheSwitch(t *testing.T) {
|
|
var enabled atomic.Bool
|
|
gate := federation.NewGate(enabled.Load, nil)
|
|
|
|
handler := gate.Protect(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
|
|
rec := httptest.NewRecorder()
|
|
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/_matrix/federation/v1/version", nil))
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("status = %d, want 404 while federation is off", rec.Code)
|
|
}
|
|
|
|
enabled.Store(true)
|
|
rec = httptest.NewRecorder()
|
|
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/_matrix/federation/v1/version", nil))
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200 once federation is on", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestNilCallbackMeansDisabled(t *testing.T) {
|
|
gate := federation.NewGate(nil, nil)
|
|
if gate.Enabled() {
|
|
t.Error("a gate without a switch must be closed")
|
|
}
|
|
}
|
|
|
|
func TestPolicy(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
allowlist []string
|
|
denylist []string
|
|
server string
|
|
want bool
|
|
}{
|
|
{"empty lists allow anything", nil, nil, "matrix.org", true},
|
|
{"denylist blocks", nil, []string{"evil.example"}, "evil.example", false},
|
|
{"denylist is case insensitive", nil, []string{"evil.example"}, "Evil.Example", false},
|
|
{"allowlist admits members", []string{"a.example"}, nil, "a.example", true},
|
|
{"allowlist excludes everyone else", []string{"a.example"}, nil, "b.example", false},
|
|
{"denylist wins over allowlist", []string{"a.example"}, []string{"a.example"}, "a.example", false},
|
|
{"empty server name is refused", nil, nil, "", false},
|
|
{"whitespace is trimmed", []string{"a.example"}, nil, " a.example ", true},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
p := federation.NewPolicy(tc.allowlist, tc.denylist)
|
|
if got := p.Allowed(tc.server); got != tc.want {
|
|
t.Errorf("Allowed(%q) = %v, want %v", tc.server, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPolicyReplace(t *testing.T) {
|
|
p := federation.NewPolicy([]string{"a.example"}, nil)
|
|
if !p.Allowed("a.example") {
|
|
t.Fatal("a.example should be allowed")
|
|
}
|
|
|
|
p.Replace(nil, []string{"a.example"})
|
|
if p.Allowed("a.example") {
|
|
t.Error("after Replace, a.example should be denied")
|
|
}
|
|
|
|
allow, deny := p.Lists()
|
|
if len(allow) != 0 || len(deny) != 1 || deny[0] != "a.example" {
|
|
t.Errorf("Lists() = %v, %v", allow, deny)
|
|
}
|
|
}
|
|
|
|
func TestRejectClientRequestExplainsItself(t *testing.T) {
|
|
err := federation.RejectClientRequest("joining a room on another server")
|
|
if err == nil {
|
|
t.Fatal("expected an error")
|
|
}
|
|
msg := err.Error()
|
|
for _, want := range []string{"M_FORBIDDEN", "Federation is disabled", "joining a room"} {
|
|
if !strings.Contains(msg, want) {
|
|
t.Errorf("error %q should mention %q", msg, want)
|
|
}
|
|
}
|
|
}
|