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) } } }