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
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>
173 lines
6.5 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|