Files
Warp/server/internal/matrix/id/id_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

185 lines
5.5 KiB
Go

package id
import (
"errors"
"strings"
"testing"
)
func TestSplitUserID(t *testing.T) {
cases := []struct {
in string
localpart string
serverName string
}{
{"@alice:warp.test", "alice", "warp.test"},
{"@alice:warp.test:8448", "alice", "warp.test:8448"},
// Historical IDs: characters the current grammar forbids still have
// to be accepted, because rooms are full of them.
{"@Alice:warp.test", "Alice", "warp.test"},
{"@a b:warp.test", "a b", "warp.test"},
{"@😀:warp.test", "😀", "warp.test"},
}
for _, tc := range cases {
localpart, serverName, err := SplitUserID(tc.in)
if err != nil {
t.Errorf("SplitUserID(%q) = %v", tc.in, err)
continue
}
if localpart != tc.localpart || serverName != tc.serverName {
t.Errorf("SplitUserID(%q) = %q, %q; want %q, %q",
tc.in, localpart, serverName, tc.localpart, tc.serverName)
}
}
}
func TestValidateUserIDRejects(t *testing.T) {
cases := map[string]string{
"": "must not be empty",
"alice:warp.test": "does not start with",
"#alice:warp.test": "does not start with",
"@alice": "no server name",
"@:warp.test": "empty localpart",
"@alice:": "empty server name",
"@alice\x00:warp.test": "NUL",
"@" + strings.Repeat("a", 255): "the limit is",
}
for in, want := range cases {
err := ValidateUserID(in)
if err == nil {
t.Errorf("ValidateUserID(%q) was accepted, want an error", in)
continue
}
if !strings.Contains(err.Error(), want) {
t.Errorf("ValidateUserID(%q) = %q, want it to mention %q", in, err, want)
}
if !errors.Is(err, ErrInvalidID) {
t.Errorf("ValidateUserID(%q) does not wrap ErrInvalidID: %v", in, err)
}
}
}
func TestUserIDLengthLimit(t *testing.T) {
// Exactly at the limit is allowed; one byte over is not.
atLimit := "@" + strings.Repeat("a", MaxLength-len("@:warp.test")) + ":warp.test"
if len(atLimit) != MaxLength {
t.Fatalf("the test built a %d-byte ID, want %d", len(atLimit), MaxLength)
}
if err := ValidateUserID(atLimit); err != nil {
t.Errorf("a %d-byte user ID was rejected: %v", MaxLength, err)
}
if err := ValidateUserID(atLimit + "a"); err == nil {
t.Error("a user ID one byte over the limit was accepted")
}
}
func TestServerNameOf(t *testing.T) {
got, err := ServerNameOf("@alice:warp.test")
if err != nil {
t.Fatal(err)
}
if got != "warp.test" {
t.Errorf("ServerNameOf = %q, want warp.test", got)
}
if _, err := ServerNameOf("nonsense"); err == nil {
t.Error("ServerNameOf accepted a string that is not a user ID")
}
}
func TestSameServer(t *testing.T) {
cases := []struct {
a, b string
want bool
}{
{"@alice:warp.test", "@bob:warp.test", true},
{"@alice:warp.test", "@alice:warp.test", true},
{"@alice:warp.test", "@bob:other.test", false},
// Server names are case-sensitive by spec, and the rules that turn
// on a domain match must not be widened by a capital letter.
{"@alice:warp.test", "@bob:Warp.test", false},
{"@alice:warp.test", "not-a-user-id", false},
{"", "", false},
}
for _, tc := range cases {
if got := SameServer(tc.a, tc.b); got != tc.want {
t.Errorf("SameServer(%q, %q) = %v, want %v", tc.a, tc.b, got, tc.want)
}
}
}
func TestValidateRoomID(t *testing.T) {
valid := []string{
"!abc:warp.test",
// From room version 12 a room ID is a hash with no domain at all.
"!Nhcu5BS-UMnFX7hBVfVSoXiD7OgH6iRT-xyIuqDnpYQ",
}
for _, s := range valid {
if err := ValidateRoomID(s); err != nil {
t.Errorf("ValidateRoomID(%q) = %v", s, err)
}
}
invalid := map[string]string{
"": "must not be empty",
"abc:warp.test": "does not start with",
"@abc:warp.test": "does not start with",
"!": "only a sigil",
"!abc\x00:warp.test": "NUL",
"!" + strings.Repeat("a", 255): "the limit is",
}
for s, want := range invalid {
err := ValidateRoomID(s)
if err == nil {
t.Errorf("ValidateRoomID(%q) was accepted, want an error", s)
continue
}
if !strings.Contains(err.Error(), want) {
t.Errorf("ValidateRoomID(%q) = %q, want it to mention %q", s, err, want)
}
}
}
func TestServerNameOfRoomID(t *testing.T) {
got, err := ServerNameOfRoomID("!abc:warp.test")
if err != nil {
t.Fatal(err)
}
if got != "warp.test" {
t.Errorf("ServerNameOfRoomID = %q, want warp.test", got)
}
// A version 12 room ID names no server, and saying so is the point:
// the caller must only ask in the versions where there is an answer.
if _, err := ServerNameOfRoomID("!Nhcu5BS-UMnFX7hBVfVSoXiD7OgH6iRT"); err == nil {
t.Error("ServerNameOfRoomID found a server in a room ID that has none")
}
}
func TestValidateEventID(t *testing.T) {
valid := []string{"$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg", "$abc:warp.test"}
for _, s := range valid {
if err := ValidateEventID(s); err != nil {
t.Errorf("ValidateEventID(%q) = %v", s, err)
}
}
invalid := map[string]string{
"": "must not be empty",
"abc": "does not start with",
"!abc": "does not start with",
"$": "only a sigil",
"$abc\x00": "NUL",
"$" + strings.Repeat("a", 255): "the limit is",
}
for s, want := range invalid {
err := ValidateEventID(s)
if err == nil {
t.Errorf("ValidateEventID(%q) was accepted, want an error", s)
continue
}
if !strings.Contains(err.Error(), want) {
t.Errorf("ValidateEventID(%q) = %q, want it to mention %q", s, err, want)
}
}
}