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
POST /keys/query for a remote user who shares a room with a local user is
now answered from a cache, fetched whole with GET /user/devices/{userId}
on a miss. Incoming m.device_list_update EDUs are applied when their
prev_id matches the cached stream_id; anything else marks the copy stale
and it is fetched again in the background, and a stale copy is never
served. m.signing_key_update replaces the cached cross-signing keys. A
user's copy is dropped once no local user shares a room with them, by a
keyserver listener on the roomserver's emit stage.
Migration 0009 adds keyserver_remote_users, keyserver_remote_device_keys
and keyserver_remote_cross_signing_keys for sqlite and postgres.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
186 lines
7.6 KiB
Go
186 lines
7.6 KiB
Go
package storage_test
|
|
|
|
import (
|
|
"slices"
|
|
"testing"
|
|
|
|
"github.com/dariuszjaniak/warp/server/internal/storage"
|
|
)
|
|
|
|
const carol = "@carol:remote.example"
|
|
|
|
func TestRemoteKeyCache(t *testing.T) {
|
|
for _, driver := range testDrivers {
|
|
t.Run(driver, func(t *testing.T) {
|
|
ctx := t.Context()
|
|
db := migratedKeysDB(t, driver)
|
|
keys := db.Keys()
|
|
|
|
if _, ok, err := keys.RemoteKeys(ctx, carol); ok || err != nil {
|
|
t.Fatalf("RemoteKeys before anything was cached = %v, %v", ok, err)
|
|
}
|
|
|
|
snapshot := storage.RemoteKeys{
|
|
UserID: carol, StreamID: 5,
|
|
Devices: []storage.RemoteDevice{
|
|
{DeviceID: "C2", JSON: []byte(`{"d":2}`)},
|
|
{DeviceID: "C1", DisplayName: "phone", JSON: []byte(`{"d":1}`)},
|
|
},
|
|
CrossSigning: map[string][]byte{
|
|
storage.CrossSigningMaster: []byte(`{"m":1}`),
|
|
storage.CrossSigningSelfSigning: []byte(`{"s":1}`),
|
|
},
|
|
}
|
|
if stored, err := keys.SetRemoteKeys(ctx, snapshot); !stored || err != nil {
|
|
t.Fatalf("SetRemoteKeys = %v, %v", stored, err)
|
|
}
|
|
got, ok, err := keys.RemoteKeys(ctx, carol)
|
|
if err != nil || !ok {
|
|
t.Fatalf("RemoteKeys = %v, %v", ok, err)
|
|
}
|
|
if got.StreamID != 5 || got.Stale || len(got.Devices) != 2 ||
|
|
got.Devices[0].DeviceID != "C1" || got.Devices[0].DisplayName != "phone" || string(got.Devices[1].JSON) != `{"d":2}` ||
|
|
string(got.CrossSigning[storage.CrossSigningMaster]) != `{"m":1}` || len(got.CrossSigning) != 2 {
|
|
t.Errorf("RemoteKeys = %+v", got)
|
|
}
|
|
|
|
// Caching moves nothing in the device list stream: that is the
|
|
// keyserver's call, per update.
|
|
if pos, _ := keys.DeviceListPosition(ctx, carol); pos != 0 {
|
|
t.Errorf("caching moved the device list to %d", pos)
|
|
}
|
|
|
|
apply := func(u storage.RemoteDeviceUpdate) storage.RemoteUpdateResult {
|
|
t.Helper()
|
|
u.UserID = carol
|
|
result, err := keys.UpdateRemoteDevice(ctx, u)
|
|
if err != nil {
|
|
t.Fatalf("UpdateRemoteDevice(%+v): %v", u, err)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Following on, a repeat, and a deletion.
|
|
if r := apply(storage.RemoteDeviceUpdate{DeviceID: "C3", JSON: []byte(`{"d":3}`), StreamID: 6, PrevIDs: []int64{5}}); r != storage.RemoteUpdateApplied {
|
|
t.Errorf("an update that follows on = %v", r)
|
|
}
|
|
if r := apply(storage.RemoteDeviceUpdate{DeviceID: "C3", JSON: []byte(`{"d":3}`), StreamID: 6, PrevIDs: []int64{5}}); r != storage.RemoteUpdateDuplicate {
|
|
t.Errorf("the same update again = %v", r)
|
|
}
|
|
if r := apply(storage.RemoteDeviceUpdate{DeviceID: "C2", Deleted: true, StreamID: 7, PrevIDs: []int64{4, 6}}); r != storage.RemoteUpdateApplied {
|
|
t.Errorf("a deletion naming the position held among others = %v", r)
|
|
}
|
|
got, _, _ = keys.RemoteKeys(ctx, carol)
|
|
var ids []string
|
|
for _, d := range got.Devices {
|
|
ids = append(ids, d.DeviceID)
|
|
}
|
|
if got.StreamID != 7 || !slices.Equal(ids, []string{"C1", "C3"}) {
|
|
t.Errorf("after the updates: position %d, devices %v", got.StreamID, ids)
|
|
}
|
|
|
|
// A gap marks the copy stale at the newest position heard of.
|
|
if r := apply(storage.RemoteDeviceUpdate{DeviceID: "C4", JSON: []byte(`{}`), StreamID: 12, PrevIDs: []int64{11}}); r != storage.RemoteUpdateStale {
|
|
t.Errorf("an update after a gap = %v", r)
|
|
}
|
|
got, _, _ = keys.RemoteKeys(ctx, carol)
|
|
if !got.Stale || got.StreamID != 12 || len(got.Devices) != 2 {
|
|
t.Errorf("after the gap: %+v", got)
|
|
}
|
|
if r := apply(storage.RemoteDeviceUpdate{DeviceID: "C4", JSON: []byte(`{}`), StreamID: 13, PrevIDs: []int64{12}}); r != storage.RemoteUpdateStale {
|
|
t.Errorf("an update to a stale copy = %v", r)
|
|
}
|
|
if stored, _ := keys.SetRemoteCrossSigningKeys(ctx, carol, map[string][]byte{storage.CrossSigningMaster: []byte(`{}`)}); stored {
|
|
t.Error("cross-signing keys were written into a stale copy")
|
|
}
|
|
|
|
// A snapshot older than the position held is not stored; one at
|
|
// or past it replaces the copy whole.
|
|
if stored, _ := keys.SetRemoteKeys(ctx, storage.RemoteKeys{UserID: carol, StreamID: 10}); stored {
|
|
t.Error("a snapshot older than the copy was stored")
|
|
}
|
|
if stored, _ := keys.SetRemoteKeys(ctx, storage.RemoteKeys{
|
|
UserID: carol, StreamID: 13, Devices: []storage.RemoteDevice{{DeviceID: "C4", JSON: []byte(`{"d":4}`)}},
|
|
}); !stored {
|
|
t.Error("a current snapshot was not stored")
|
|
}
|
|
got, _, _ = keys.RemoteKeys(ctx, carol)
|
|
if got.Stale || got.StreamID != 13 || len(got.Devices) != 1 || len(got.CrossSigning) != 0 {
|
|
t.Errorf("after the resync: %+v", got)
|
|
}
|
|
|
|
// Cross-signing keys are replaced as a set.
|
|
if stored, err := keys.SetRemoteCrossSigningKeys(ctx, carol, map[string][]byte{
|
|
storage.CrossSigningMaster: []byte(`{"m":2}`), storage.CrossSigningSelfSigning: nil,
|
|
}); !stored || err != nil {
|
|
t.Fatalf("SetRemoteCrossSigningKeys = %v, %v", stored, err)
|
|
}
|
|
got, _, _ = keys.RemoteKeys(ctx, carol)
|
|
if string(got.CrossSigning[storage.CrossSigningMaster]) != `{"m":2}` || len(got.CrossSigning) != 1 {
|
|
t.Errorf("cross-signing keys = %v", got.CrossSigning)
|
|
}
|
|
|
|
// An update about a user with nothing cached leaves a stale
|
|
// marker, and a snapshot from before the update loses to it.
|
|
const dave = "@dave:remote.example"
|
|
if r, _ := keys.UpdateRemoteDevice(ctx, storage.RemoteDeviceUpdate{UserID: dave, DeviceID: "V1", StreamID: 3}); r != storage.RemoteUpdateNotCached {
|
|
t.Errorf("an update about an uncached user = %v", r)
|
|
}
|
|
marker, ok, _ := keys.RemoteKeys(ctx, dave)
|
|
if !ok || !marker.Stale || marker.StreamID != 3 {
|
|
t.Errorf("the marker = %+v, %v", marker, ok)
|
|
}
|
|
if stored, _ := keys.SetRemoteKeys(ctx, storage.RemoteKeys{UserID: dave, StreamID: 2}); stored {
|
|
t.Error("a snapshot from before the update was stored")
|
|
}
|
|
if stored, _ := keys.SetRemoteCrossSigningKeys(ctx, "@nobody:remote.example", map[string][]byte{}); stored {
|
|
t.Error("cross-signing keys were cached for a user with no copy")
|
|
}
|
|
|
|
users, err := keys.RemoteKeyUsers(ctx)
|
|
if err != nil || !slices.Equal(users, []string{carol, dave}) {
|
|
t.Errorf("RemoteKeyUsers = %v, %v", users, err)
|
|
}
|
|
if err := keys.DeleteRemoteKeys(ctx, []string{carol, dave}); err != nil {
|
|
t.Fatalf("DeleteRemoteKeys: %v", err)
|
|
}
|
|
if _, ok, _ := keys.RemoteKeys(ctx, carol); ok {
|
|
t.Error("a dropped copy is still there")
|
|
}
|
|
if users, _ := keys.RemoteKeyUsers(ctx); len(users) != 0 {
|
|
t.Errorf("RemoteKeyUsers after dropping everybody = %v", users)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRemoteKeyCacheFirstUpdate(t *testing.T) {
|
|
for _, driver := range testDrivers {
|
|
t.Run(driver, func(t *testing.T) {
|
|
ctx := t.Context()
|
|
keys := migratedKeysDB(t, driver).Keys()
|
|
|
|
// A user whose server had never moved their list is cached at
|
|
// position 0, and the first update, with no prev_id, follows on.
|
|
if _, err := keys.SetRemoteKeys(ctx, storage.RemoteKeys{UserID: carol}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r, err := keys.UpdateRemoteDevice(ctx, storage.RemoteDeviceUpdate{UserID: carol, DeviceID: "C1", JSON: []byte(`{}`), StreamID: 1})
|
|
if err != nil || r != storage.RemoteUpdateApplied {
|
|
t.Errorf("the first update = %v, %v", r, err)
|
|
}
|
|
// A later one with no prev_id does not.
|
|
r, _ = keys.UpdateRemoteDevice(ctx, storage.RemoteDeviceUpdate{UserID: carol, DeviceID: "C1", JSON: []byte(`{}`), StreamID: 2})
|
|
if r != storage.RemoteUpdateStale {
|
|
t.Errorf("a later update with no prev_id = %v", r)
|
|
}
|
|
// Nor does one that says a device changed without its keys.
|
|
_, _ = keys.SetRemoteKeys(ctx, storage.RemoteKeys{UserID: carol, StreamID: 2})
|
|
r, _ = keys.UpdateRemoteDevice(ctx, storage.RemoteDeviceUpdate{UserID: carol, DeviceID: "C1", StreamID: 3, PrevIDs: []int64{2}})
|
|
if r != storage.RemoteUpdateStale {
|
|
t.Errorf("an update with no keys = %v", r)
|
|
}
|
|
})
|
|
}
|
|
}
|