Files
Warp/server/internal/setup/phase10_test.go
T
tajniak81andClaude Opus 5.5 a47e15cc1c
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 / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
CI / Admin panel (push) Canceled after 0s
CI / Web client (push) Canceled after 0s
CI / Desktop client (ubuntu-24.04) (push) Canceled after 0s
CI / Desktop client (windows-latest) (push) Canceled after 0s
PocketBase as a standalone database server
The pocketbase driver no longer embeds PocketBase: Warp keeps its data as
PocketBase collections on a server of its own and reaches it only through
the REST API, the way the postgres driver reaches PostgreSQL.
database.connection_string is the server's URL, and Warp signs in as the
superuser in WARP_PB_SUPERUSER_EMAIL / WARP_PB_SUPERUSER_PASSWORD (or a
WARP_PB_SUPERUSER_FILE of the two).

Every SQL table is a superuser-only collection of the same name and
columns, created by warp migrate. Reads go through PocketBase's SQL
endpoint with the SQL drivers' queries, via a read-only database/sql
driver that inlines arguments, moves long ones into warp_params and pages
past 1000 rows. Writes are record operations in batch requests under one
write lock in the Warp process. Media is a protected file field in
warp_media, local or in the S3 bucket Warp writes into PocketBase's
settings. Search matches word prefixes with LIKE. The storage tests run
against an in-process PocketBase and pass as on SQLite.

Docker: the PocketBase stacks get a pocketbase service running
tajniak81/pocketbase-docker-root (WARP_PB_IMAGE), with a pb-data volume
and its admin UI on loopback; the AIO images run that PocketBase binary
as a process beside the homeserver, making up a superuser when none is
set. The .env examples, READMEs, docs and warp.yaml.example follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 16:31:08 +01:00

173 lines
6.5 KiB
Go

package setup_test
import (
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/dariuszjaniak/warp/server/internal/config"
"github.com/dariuszjaniak/warp/server/internal/storage/pocketbase/pbtest"
)
// The Phase 10 tests: the PocketBase driver and the new endpoints, through
// the whole stack.
// TestPocketBaseServer runs a server on the pocketbase driver, against a
// PocketBase server of its own: accounts, rooms, search and media, with the
// media landing in PocketBase's storage rather than under media.base_path.
func TestPocketBaseServer(t *testing.T) {
pb := pbtest.Start(t)
mediaDir := filepath.Join(t.TempDir(), "media")
srv := newSigningServer(t, func(cfg *config.Config) {
cfg.Database.Driver = "pocketbase"
cfg.Database.ConnectionString = pb.URL
cfg.Database.PocketBase.SuperuserEmail = pbtest.SuperuserEmail
cfg.Database.PocketBase.SuperuserPassword = pbtest.SuperuserPassword
cfg.Registration.Enabled = true
cfg.ClientAPI.RateLimit.Enabled = false
cfg.Media.BasePath = mediaDir
})
health := get(t, srv, http.MethodGet, "/healthz")
if health.Code != http.StatusOK || !strings.Contains(health.Body.String(), `"database":"pocketbase"`) {
t.Fatalf("healthz: %d %s", health.Code, health.Body.String())
}
token := registerThrough(t, srv, "alice")
rec := post(t, srv, "/_matrix/client/v3/createRoom", map[string]any{"name": "On PocketBase"}, token)
if rec.Code != http.StatusOK {
t.Fatalf("createRoom: %d %s", rec.Code, rec.Body.String())
}
roomID := decode[map[string]string](t, rec)["room_id"]
rec = send(t, srv, http.MethodPut,
"/_matrix/client/v3/rooms/"+url.PathEscape(roomID)+"/send/m.room.message/t1",
"application/json", []byte(`{"msgtype":"m.text","body":"stored in a pocket"}`), token, nil)
if rec.Code != http.StatusOK {
t.Fatalf("send: %d %s", rec.Code, rec.Body.String())
}
rec = post(t, srv, "/_matrix/client/v3/search", map[string]any{
"search_categories": map[string]any{"room_events": map[string]any{"search_term": "pocket"}},
}, token)
if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "stored in a pocket") {
t.Errorf("search: %d %s", rec.Code, rec.Body.String())
}
server, id := uploadThrough(t, srv, token, "text/plain", "note.txt", []byte("pocket media"))
rec = getWithToken(t, srv, "/_matrix/client/v1/media/download/"+server+"/"+id, token)
if rec.Code != http.StatusOK || rec.Body.String() != "pocket media" {
t.Fatalf("download: %d %q", rec.Code, rec.Body.String())
}
var found bool
_ = filepath.WalkDir(filepath.Join(pb.DataDir, "storage"), func(_ string, d os.DirEntry, err error) error {
if err == nil && !d.IsDir() {
found = true
}
return nil
})
if !found {
t.Error("the upload is not in PocketBase's storage directory")
}
if _, err := os.Stat(mediaDir); !os.IsNotExist(err) {
t.Errorf("media.base_path was used: %v", err)
}
}
func TestVersionsAdvertisesSlidingSync(t *testing.T) {
srv := newTestServer(t, nil)
rec := get(t, srv, http.MethodGet, "/_matrix/client/versions")
features := decode[map[string]any](t, rec)["unstable_features"].(map[string]any)
if features["org.matrix.simplified_msc3575"] != true {
t.Errorf("unstable_features = %v", features)
}
}
func TestPhase10RoutesAreWiredUp(t *testing.T) {
srv := newSigningServer(t, func(c *config.Config) {
c.Registration.Enabled = true
c.ClientAPI.RateLimit.Enabled = false
})
token := registerThrough(t, srv, "alice")
rec := post(t, srv, "/_matrix/client/v3/createRoom", map[string]any{
"creation_content": map[string]any{"type": "m.space"},
}, token)
roomID := decode[map[string]string](t, rec)["room_id"]
escaped := url.PathEscape(roomID)
for _, path := range []string{
"/_matrix/client/v1/rooms/" + escaped + "/hierarchy",
"/_matrix/client/v1/rooms/" + escaped + "/threads",
"/_matrix/client/v1/room_summary/" + escaped,
} {
if rec := getWithToken(t, srv, path, token); rec.Code != http.StatusOK {
t.Errorf("%s: %d %s", path, rec.Code, rec.Body.String())
}
}
rec = post(t, srv, "/_matrix/client/unstable/org.matrix.simplified_msc3575/sync", map[string]any{
"lists": map[string]any{"all": map[string]any{"ranges": [][2]int{{0, 5}}}},
}, token)
if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), `"pos"`) {
t.Errorf("sliding sync: %d %s", rec.Code, rec.Body.String())
}
}
func TestServerMetrics(t *testing.T) {
srv := newSigningServer(t, nil)
token := registerThrough(t, srv, "alice")
if rec := post(t, srv, "/_matrix/client/v3/createRoom", map[string]any{}, token); rec.Code != http.StatusOK {
t.Fatalf("createRoom: %d", rec.Code)
}
body := get(t, srv, http.MethodGet, "/metrics").Body.String()
for _, want := range []string{
`warp_events_persisted_total{kind="state",origin="local"}`,
"warp_sync_waiting 0",
"warp_sync_sliding_connections 0",
"warp_federation_enabled 0",
"go_sql_open_connections",
} {
if !strings.Contains(body, want) {
t.Errorf("/metrics has no %s", want)
}
}
}
// TestIdleSyncLongPolls is the regression test for two ways an idle
// /sync came back at once instead of waiting, which made every client's
// long poll a busy loop: push rules sent again on every sync of a server
// with no account data written yet, and an all-zero token taken for a
// first sync on a server whose streams had not moved at all.
func TestIdleSyncLongPolls(t *testing.T) {
for _, withRoom := range []bool{false, true} {
t.Run(map[bool]string{false: "empty server", true: "with a room"}[withRoom], func(t *testing.T) {
srv := newSigningServer(t, nil)
token := registerThrough(t, srv, "alice")
if withRoom {
if rec := post(t, srv, "/_matrix/client/v3/createRoom", map[string]any{}, token); rec.Code != http.StatusOK {
t.Fatalf("createRoom: %d", rec.Code)
}
}
first := decode[map[string]any](t, getWithToken(t, srv, "/_matrix/client/v3/sync", token))
since, _ := first["next_batch"].(string)
body := decode[map[string]any](t, getWithToken(t, srv, "/_matrix/client/v3/sync?timeout=0&since="+url.QueryEscape(since), token))
accountData, _ := body["account_data"].(map[string]any)
if events, _ := accountData["events"].([]any); len(events) != 0 {
t.Errorf("an idle incremental sync sent account data again: %d events", len(events))
}
start := time.Now()
getWithToken(t, srv, "/_matrix/client/v3/sync?timeout=1000&since="+url.QueryEscape(since), token)
if waited := time.Since(start); waited < 900*time.Millisecond {
t.Errorf("an idle sync returned after %s instead of waiting out its timeout", waited)
}
})
}
}