Files
Warp/server/internal/matrix/roomversion/roomversion_test.go
T
tajniak81andClaude Opus 5.5 2fbd5f53ce
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
Phase 2: the event core
Implements PLAN.md Phase 2: the library every later phase is built on,
as pure code under server/internal/matrix with no HTTP and no storage.

canonicaljson: the canonical JSON encoding, hand-written because
encoding/json escapes <, > and & and accepts floats. Integers only, in
[-(2^53)+1, 2^53-1]; lone surrogate escapes rejected rather than turned
into U+FFFD.

signing: the Signing JSON algorithm alongside the existing key handling,
and one lenient base64 decoder shared by the packages above it.

id: identifier grammars as they appear inside events, deliberately laxer
than userapi's account rules because rooms carry historical user IDs.

roomversion: the registry. Versions 3 to 12 are supported, each written
out in full as a Rules value with the flags the other packages read.
Versions 1 and 2 are known but refused: their event format and state
resolution v1 wait for federation. createRoom will default to 11, as the
spec asks for a while after 1.16, with 12 available.

event: PDU parsing and validation, content and reference hashes, event
IDs, the redaction algorithm for every supported version, signing and
verification, and a Builder.

auth: the authorisation rules in spec order, power levels with their
defaults, v12 creators with infinite power, the auth events selection
algorithm, rule 3's auth_events checks, and the separate test for
whether a redaction applies. Comments use the room version 12 rule
numbering, with the version 11 numbers given where they differ.

stateres: state resolution v2, and version 12's amendment (an empty
starting state and the conflicted state subgraph).

Tested against the spec's own vectors byte for byte: the canonical JSON
examples, both JSON signing vectors and both event signing vectors.
State resolution ports scenarios from Synapse's suite. canonicaljson
and event.Parse have fuzz targets.

The capabilities endpoint now advertises the registry instead of a
hard-coded "11". docs/event-core.md describes the packages and the
known gaps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 22:23:35 +02:00

235 lines
7.5 KiB
Go

package roomversion
import (
"errors"
"strings"
"testing"
)
func TestGet(t *testing.T) {
t.Run("a supported version", func(t *testing.T) {
rules, err := Get(V11)
if err != nil {
t.Fatalf("Get(11): %v", err)
}
if rules.Version != V11 {
t.Errorf("Version = %q, want 11", rules.Version)
}
if !rules.Supported {
t.Error("the rules we were handed say they are not supported")
}
})
t.Run("a known but unsupported version", func(t *testing.T) {
_, err := Get(V1)
if !errors.Is(err, ErrUnsupportedVersion) {
t.Errorf("Get(1) = %v, want ErrUnsupportedVersion", err)
}
})
t.Run("an unknown version", func(t *testing.T) {
_, err := Get("99")
if !errors.Is(err, ErrUnknownVersion) {
t.Errorf("Get(99) = %v, want ErrUnknownVersion", err)
}
})
}
func TestMustGetPanicsOnAnUnsupportedVersion(t *testing.T) {
defer func() {
if recover() == nil {
t.Error("MustGet did not panic on an unsupported version")
}
}()
MustGet(V1)
}
func TestDefaultIsSupported(t *testing.T) {
if _, err := Get(Default); err != nil {
t.Fatalf("the default room version %q is not usable: %v", Default, err)
}
if !IsSupported(Default) {
t.Errorf("IsSupported(%q) = false", Default)
}
}
func TestSupported(t *testing.T) {
supported := Supported()
if len(supported) == 0 {
t.Fatal("no room versions are supported")
}
for _, v := range supported {
if !IsSupported(v) {
t.Errorf("Supported() lists %q but IsSupported says no", v)
}
}
for _, v := range []Version{V1, V2} {
if IsSupported(v) {
t.Errorf("IsSupported(%q) = true, but the event format for it is not implemented", v)
}
}
if s := SupportedString(); !strings.Contains(s, string(Default)) {
t.Errorf("SupportedString() = %q, want it to include the default", s)
}
}
func TestKnownListsEveryVersion(t *testing.T) {
known := Known()
if len(known) < 12 {
t.Errorf("Known() returned %d versions, want every version from 1 to 12", len(known))
}
// Known hands out a copy, so a caller cannot edit the registry.
known[0].Supported = !known[0].Supported
if Known()[0].Supported == known[0].Supported {
t.Error("Known() returns the registry itself rather than a copy")
}
}
// TestFlagsAreMonotonic checks the flags that a later room version never
// takes back. Each of these was added once and kept, so a version where
// one is off after a version where it is on is a transcription error in
// the table — the kind of mistake that would otherwise only show up as a
// room whose history stops verifying.
func TestFlagsAreMonotonic(t *testing.T) {
flags := map[string]func(Rules) bool{
"StrictCanonicalJSON": func(r Rules) bool { return r.StrictCanonicalJSON },
"EnforceSigningKeyValidity": func(r Rules) bool { return r.EnforceSigningKeyValidity },
"KnockJoinRule": func(r Rules) bool { return r.KnockJoinRule },
"RestrictedJoinRule": func(r Rules) bool { return r.RestrictedJoinRule },
"KnockRestrictedJoinRule": func(r Rules) bool { return r.KnockRestrictedJoinRule },
"RequireIntegerPowerLevels": func(r Rules) bool { return r.RequireIntegerPowerLevels },
"RedactsInContent": func(r Rules) bool { return r.RedactsInContent },
"RedactionKeepsWholeCreateContent": func(r Rules) bool { return r.RedactionKeepsWholeCreateContent },
"RedactionKeepsRedacts": func(r Rules) bool { return r.RedactionKeepsRedacts },
"RedactionKeepsPowerLevelsInvite": func(r Rules) bool { return r.RedactionKeepsPowerLevelsInvite },
"RedactionKeepsThirdPartyInviteSigned": func(r Rules) bool { return r.RedactionKeepsThirdPartyInviteSigned },
"RedactionKeepsJoinAuthorisedViaUsersServer": func(r Rules) bool { return r.RedactionKeepsJoinAuthorisedViaUsersServer },
"RedactionKeepsJoinRulesAllow": func(r Rules) bool { return r.RedactionKeepsJoinRulesAllow },
"RoomIDIsCreateEventID": func(r Rules) bool { return r.RoomIDIsCreateEventID },
"CreateEventExcludedFromAuthEvents": func(r Rules) bool { return r.CreateEventExcludedFromAuthEvents },
"CreatorsHaveInfinitePower": func(r Rules) bool { return r.CreatorsHaveInfinitePower },
"AdditionalCreators": func(r Rules) bool { return r.AdditionalCreators },
}
known := Known()
for name, get := range flags {
seen := false
for _, rules := range known {
if get(rules) {
seen = true
continue
}
if seen {
t.Errorf("%s is on before room version %s and off again at it", name, rules.Version)
break
}
}
if !seen {
t.Errorf("%s is never on in any room version", name)
}
}
}
// TestFlagsThatWereDropped covers the other direction: the four pieces of
// behaviour a later room version took away.
func TestFlagsThatWereDropped(t *testing.T) {
cases := []struct {
name string
get func(Rules) bool
lastOnFor Version
}{
{"AliasesEventAuth", func(r Rules) bool { return r.AliasesEventAuth }, V5},
{"RedactionKeepsAliases", func(r Rules) bool { return r.RedactionKeepsAliases }, V5},
{"CreateEventRequiresCreator", func(r Rules) bool { return r.CreateEventRequiresCreator }, V10},
{"RedactionProtectsLegacyKeys", func(r Rules) bool { return r.RedactionProtectsLegacyKeys }, V10},
}
known := Known()
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
past := false
for _, rules := range known {
want := !past
if got := tc.get(rules); got != want {
t.Errorf("room version %s has %s = %v, want %v", rules.Version, tc.name, got, want)
}
if rules.Version == tc.lastOnFor {
past = true
}
}
})
}
}
func TestEventIDFormatByVersion(t *testing.T) {
cases := map[Version]EventIDFormat{
V1: EventIDFormatExplicit,
V2: EventIDFormatExplicit,
V3: EventIDFormatSHA256,
V4: EventIDFormatSHA256URLSafe,
V11: EventIDFormatSHA256URLSafe,
V12: EventIDFormatSHA256URLSafe,
}
for _, rules := range Known() {
want, ok := cases[rules.Version]
if !ok {
continue
}
if rules.EventIDFormat != want {
t.Errorf("room version %s has event ID format %d, want %d", rules.Version, rules.EventIDFormat, want)
}
}
}
func TestStateResByVersion(t *testing.T) {
cases := map[Version]StateResVersion{
V1: StateResV1,
V2: StateResV2,
V11: StateResV2,
V12: StateResV2Subgraph,
}
for _, rules := range Known() {
want, ok := cases[rules.Version]
if !ok {
continue
}
if rules.StateRes != want {
t.Errorf("room version %s uses state resolution %d, want %d", rules.Version, rules.StateRes, want)
}
}
}
func TestValidate(t *testing.T) {
valid := []string{"1", "11", "12", "org.example.room-version.1", "a.b-c", strings.Repeat("a", 32)}
for _, s := range valid {
if err := Validate(s); err != nil {
t.Errorf("Validate(%q) = %v, want it to be accepted", s, err)
}
}
invalid := map[string]string{
"": "empty",
strings.Repeat("a", 33): "the limit is",
"V11": "contains",
"11_beta": "contains",
"org.example/room": "contains",
"room version 11": "contains",
}
for s, want := range invalid {
err := Validate(s)
if err == nil {
t.Errorf("Validate(%q) was accepted, want an error", s)
continue
}
if !strings.Contains(err.Error(), want) {
t.Errorf("Validate(%q) = %q, want it to mention %q", s, err, want)
}
}
}
func TestRulesString(t *testing.T) {
if got := MustGet(V11).String(); got != "11" {
t.Errorf("String() = %q, want 11", got)
}
}