Implements PLAN.md Phase 8. internal/pushrules is the rule language: the spec's default ruleset, all six condition kinds, glob and word-boundary matching, intentional mentions switching off the legacy body rules, and rule ordering as Synapse has it. internal/pushserver keeps the rules, pushers and notifications and talks to push gateways. Push rules are the m.push_rules account data, written only by the push server; reads keep the user's own rules and the enabled/actions of each default and take the rest from the current defaults, so new default rules reach every user without a migration. PUT account_data/m.push_rules is refused with 405. The push server listens on the roomserver's emit stage and evaluates each event on its own worker for every local member and an invite's target. Notifications and per-room unread counts are stored in migration 0010; counts are a seventh sync token stream, so /sync reports unread_notifications and picks up count changes made after the event or by a receipt from another device. m.read and m.read.private receipts clear the counts; leaving a room clears it. Pushers (http kind only) push through a bounded sender pool with retries, honour event_id_only, drop push keys the gateway rejects, and go away with their device. GET /notifications pages the history. New config section push: enabled, include_content, gateway_timeout, notification_retention. Account data writes now wake the user's blocked syncs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
113 lines
3.4 KiB
Go
113 lines
3.4 KiB
Go
package syncapi_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/dariuszjaniak/warp/server/internal/syncapi"
|
|
)
|
|
|
|
func TestTokenRoundTrips(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
token syncapi.Token
|
|
want string
|
|
}{
|
|
{"the initial token", syncapi.Token{}, "s0"},
|
|
{"events only", syncapi.Token{PDU: 12}, "s12"},
|
|
{"events and receipts", syncapi.Token{PDU: 12, Receipt: 3}, "s12_3"},
|
|
{
|
|
"every stream",
|
|
syncapi.Token{PDU: 1, Receipt: 2, ToDevice: 3, AccountData: 4, Presence: 5, DeviceList: 6, Notification: 7},
|
|
"s1_2_3_4_5_6_7",
|
|
},
|
|
{
|
|
// A Phase 7 token, from before the notification stream.
|
|
"six streams",
|
|
syncapi.Token{PDU: 1, Receipt: 2, ToDevice: 3, AccountData: 4, Presence: 5, DeviceList: 6},
|
|
"s1_2_3_4_5_6",
|
|
},
|
|
{
|
|
// /messages pages back into history backfilled from other
|
|
// servers, which is numbered downwards from -1.
|
|
"a position inside backfilled history",
|
|
syncapi.Token{PDU: -7},
|
|
"s-7",
|
|
},
|
|
{
|
|
// A zero in the middle has to be written, because the
|
|
// positions are read by place.
|
|
"a gap in the middle",
|
|
syncapi.Token{PDU: 1, Presence: 5},
|
|
"s1_0_0_0_5",
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := tc.token.String(); got != tc.want {
|
|
t.Fatalf("String() = %q, want %q", got, tc.want)
|
|
}
|
|
parsed, err := syncapi.ParseToken(tc.want)
|
|
if err != nil {
|
|
t.Fatalf("ParseToken(%q): %v", tc.want, err)
|
|
}
|
|
if parsed != tc.token {
|
|
t.Errorf("ParseToken(%q) = %+v, want %+v", tc.want, parsed, tc.token)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseTokenAcceptsAnEmptyString(t *testing.T) {
|
|
// /sync without ?since= and /messages without ?from= both mean "from
|
|
// the beginning"; refusing the empty string would make every caller
|
|
// special-case it.
|
|
parsed, err := syncapi.ParseToken("")
|
|
if err != nil {
|
|
t.Fatalf("ParseToken(\"\"): %v", err)
|
|
}
|
|
if !parsed.IsInitial() {
|
|
t.Errorf("ParseToken(\"\") = %+v, want the initial token", parsed)
|
|
}
|
|
}
|
|
|
|
func TestParseTokenAcceptsAShortToken(t *testing.T) {
|
|
// A token a client stored before a stream existed must go on working:
|
|
// the stream it does not mention reads as "seen nothing", so the
|
|
// client is sent that stream in full once and is in step afterwards.
|
|
parsed, err := syncapi.ParseToken("s7")
|
|
if err != nil {
|
|
t.Fatalf("ParseToken(\"s7\"): %v", err)
|
|
}
|
|
if parsed.PDU != 7 {
|
|
t.Errorf("PDU = %d, want 7", parsed.PDU)
|
|
}
|
|
if parsed.Receipt != 0 || parsed.ToDevice != 0 || parsed.AccountData != 0 || parsed.Presence != 0 {
|
|
t.Errorf("the streams a short token omits must read as zero: %+v", parsed)
|
|
}
|
|
}
|
|
|
|
func TestParseTokenRejectsNonsense(t *testing.T) {
|
|
for _, raw := range []string{
|
|
"nonsense", // no prefix
|
|
"t1_2", // a prefix this build does not write
|
|
"s", // a prefix and nothing else
|
|
"s1_-1", // a negative position outside the event stream
|
|
"sx", // not a number
|
|
"s1_", // a trailing separator
|
|
"s1_2_3_4_5_6_7_8", // more positions than there are streams
|
|
"s1.5",
|
|
} {
|
|
if _, err := syncapi.ParseToken(raw); err == nil {
|
|
t.Errorf("ParseToken(%q) was accepted", raw)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseTokenIsNotFooledByOverflow(t *testing.T) {
|
|
// A position larger than an int64 is not a position this server ever
|
|
// handed out, and silently wrapping it would mean a client being sent
|
|
// the whole of every room again.
|
|
if _, err := syncapi.ParseToken("s99999999999999999999"); err == nil {
|
|
t.Error("a position that does not fit in an int64 was accepted")
|
|
}
|
|
}
|