package setup_test import ( "net/http" "net/url" "os" "path/filepath" "strings" "testing" "time" "github.com/dariuszjaniak/warp/server/internal/config" ) // The Phase 10 tests: the PocketBase driver and the new endpoints, through // the whole stack. // TestPocketBaseServer runs a server on the pocketbase driver: accounts, // rooms, search and media, with the media landing in PocketBase's own // filesystem rather than under media.base_path. func TestPocketBaseServer(t *testing.T) { dir := t.TempDir() dataDir := filepath.Join(dir, "pb_data") mediaDir := filepath.Join(dir, "media") srv := newSigningServer(t, func(cfg *config.Config) { cfg.Database.Driver = "pocketbase" cfg.Database.ConnectionString = dataDir 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(dataDir, "storage", "warp_media"), 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) } }) } }