package userapi_test import ( "errors" "strings" "testing" "github.com/dariuszjaniak/warp/server/internal/userapi" ) func TestNormaliseLocalpart(t *testing.T) { for input, want := range map[string]string{ "alice": "alice", "Alice": "alice", "USER-UPPER": "user-upper", "": "", } { if got := userapi.NormaliseLocalpart(input); got != want { t.Errorf("NormaliseLocalpart(%q) = %q, want %q", input, got, want) } } } func TestValidateLocalpart(t *testing.T) { // The grammar the Client-Server API gives for a user ID localpart. for _, localpart := range []string{ "alice", "user-upper", "a", "0", "chrtestuserq", "chrtestuser3", "chrtestuser.", "chrtestuser_", "chrtestuser=", "chrtestuser-", "chrtestuser/", "chrtestuser+", } { if err := userapi.ValidateLocalpart(localpart, "warp.example"); err != nil { t.Errorf("ValidateLocalpart(%q) = %v, want nil", localpart, err) } } for name, localpart := range map[string]string{ "empty": "", "uppercase": "Alice", "space": "a b", "exclamation": "a!b", "quote": `a"b`, "colon": "a:b", "question": "a?b", "backslash": `a\b`, "at sign": "a@b", "bracket": "a[b", "brace": "a{b", "pipe": "a|b", "pound": "a£b", "accent": "aéb", "newline": "a\nb", "apostrophe": "a'b", "comma": "a,b", } { t.Run(name, func(t *testing.T) { if err := userapi.ValidateLocalpart(localpart, "warp.example"); !errors.Is(err, userapi.ErrInvalidLocalpart) { t.Errorf("ValidateLocalpart(%q) = %v, want ErrInvalidLocalpart", localpart, err) } }) } } // TestValidateLocalpartLengthDependsOnTheServerName: the 255-byte limit is // on the whole user ID, so a long server name leaves less room for a name. func TestValidateLocalpartLengthDependsOnTheServerName(t *testing.T) { const shortServer = "a.example" // "@" + localpart + ":" + server must be at most 255 bytes. longest := strings.Repeat("x", userapi.MaxUserIDLength-len("@:"+shortServer)) if err := userapi.ValidateLocalpart(longest, shortServer); err != nil { t.Errorf("a localpart exactly at the limit was rejected: %v", err) } if err := userapi.ValidateLocalpart(longest+"x", shortServer); !errors.Is(err, userapi.ErrInvalidLocalpart) { t.Errorf("a localpart one byte over the limit = %v, want ErrInvalidLocalpart", err) } // The same localpart on a longer server name no longer fits. if err := userapi.ValidateLocalpart(longest, "a-much-longer-server-name.example"); err == nil { t.Error("the limit must count the server name") } } func TestParseLocalUserID(t *testing.T) { api, _ := newTestAPI(t, nil) for name, tc := range map[string]struct { input string want string }{ "full user ID": {"@alice:warp.example", "alice"}, "bare localpart": {"alice", "alice"}, "uppercase localpart": {"@ALICE:warp.example", "alice"}, "uppercase bare": {"Alice", "alice"}, "uppercase server name": {"@alice:WARP.Example", "alice"}, } { t.Run(name, func(t *testing.T) { got, err := api.ParseLocalUserID(tc.input) if err != nil { t.Fatalf("ParseLocalUserID(%q) = %v", tc.input, err) } if got != tc.want { t.Errorf("ParseLocalUserID(%q) = %q, want %q", tc.input, got, tc.want) } }) } // A user ID on another server is its own case: it is well formed, and // federation is what would change the answer. if _, err := api.ParseLocalUserID("@bob:other.example"); !errors.Is(err, userapi.ErrForeignServer) { t.Errorf("a remote user ID = %v, want ErrForeignServer", err) } for name, input := range map[string]string{ "empty": "", "no server name": "@alice", "empty server": "@alice:", "illegal rune": "@not valid:warp.example", "bare illegal": "not valid", } { t.Run(name, func(t *testing.T) { if _, err := api.ParseLocalUserID(input); err == nil { t.Errorf("ParseLocalUserID(%q) returned no error", input) } }) } } func TestValidateDeviceID(t *testing.T) { for _, deviceID := range []string{"ABCDEFGHIJ", "my_device_id", "device with spaces", "日本語"} { if err := userapi.ValidateDeviceID(deviceID); err != nil { t.Errorf("ValidateDeviceID(%q) = %v, want nil (device IDs are opaque)", deviceID, err) } } for name, deviceID := range map[string]string{ "empty": "", "too long": strings.Repeat("x", userapi.MaxDeviceIDLength+1), "newline": "a\nb", "control character": "a\x00b", } { t.Run(name, func(t *testing.T) { if err := userapi.ValidateDeviceID(deviceID); !errors.Is(err, userapi.ErrInvalidDeviceID) { t.Errorf("ValidateDeviceID(%q) = %v, want ErrInvalidDeviceID", deviceID, err) } }) } } func TestGenerateDeviceID(t *testing.T) { seen := map[string]bool{} for range 100 { id, err := userapi.GenerateDeviceID() if err != nil { t.Fatalf("GenerateDeviceID: %v", err) } if err := userapi.ValidateDeviceID(id); err != nil { t.Fatalf("a generated device ID is invalid: %v", err) } if seen[id] { t.Fatalf("GenerateDeviceID repeated %q within 100 calls", id) } seen[id] = true } } func TestTokensAreDistinctAndHashed(t *testing.T) { access, err := userapi.GenerateAccessToken() if err != nil { t.Fatal(err) } refresh, err := userapi.GenerateRefreshToken() if err != nil { t.Fatal(err) } // The prefixes exist so that a client sending the wrong one gets a // clear rejection, and so a token is recognisable in a log. if !strings.HasPrefix(access, "warp_at_") { t.Errorf("access token %q has no recognisable prefix", access) } if !strings.HasPrefix(refresh, "warp_rt_") { t.Errorf("refresh token %q has no recognisable prefix", refresh) } hash := userapi.HashToken(access) if hash == access || len(hash) != 64 { t.Errorf("HashToken(%q) = %q, want a 64-character hex digest", access, hash) } if userapi.HashToken(access) != hash { t.Error("HashToken is not deterministic") } if userapi.HashToken(refresh) == hash { t.Error("two different tokens hashed the same") } second, err := userapi.GenerateAccessToken() if err != nil { t.Fatal(err) } if second == access { t.Error("two generated access tokens are identical") } }