package url import ( "strings" "testing" ) func TestNormalize(t *testing.T) { tests := []struct { name string input string want string }{ { name: "empty string", input: "", want: "", }, { name: "http scheme unchanged", input: "http://example.com", want: "http://example.com", }, { name: "https scheme unchanged", input: "https://example.com", want: "https://example.com", }, { name: "file scheme unchanged", input: "file:///path/to/file.html", want: "file:///path/to/file.html", }, { name: "dumb scheme unchanged", input: "dumb://history", want: "dumb://history", }, { name: "about scheme unchanged", input: "about:blank", want: "about:blank", }, { name: "mailto scheme unchanged", input: "mailto:foo@example.com", want: "mailto:foo@example.com", }, { name: "magnet scheme unchanged", input: "magnet:?xt=urn:btih:123", want: "magnet:?xt=urn:btih:123", }, { name: "obsidian scheme unchanged", input: "obsidian://open?vault=foo.bar", want: "obsidian://open?vault=foo.bar", }, { name: "domain gets https", input: "example.com", want: "https://example.com", }, { name: "domain with path gets https", input: "example.com/path", want: "https://example.com/path", }, { name: "search query unchanged", input: "hello world", want: "hello world", }, { name: "single word unchanged", input: "hello", want: "hello", }, { name: "localhost", input: "localhost", want: "http://localhost", }, { name: "localhost with port", input: "localhost:5173", want: "http://localhost:5173", }, { name: "localhost with alt port", input: "localhost:8080", want: "http://localhost:8080", }, { name: "localhost with path", input: "localhost/api/v1", want: "http://localhost/api/v1", }, { name: "localhost.com is a domain not localhost", input: "localhost.com", want: "https://localhost.com", }, { name: "localhostevil.com is a domain not localhost", input: "localhostevil.com", want: "https://localhostevil.com", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := Normalize(tt.input) if got != tt.want { t.Errorf("Normalize(%q) = %q, want %q", tt.input, got, tt.want) } }) } } func TestNormalize_PathLikeInputsArePure(t *testing.T) { tests := []struct { name string input string }{ {name: "absolute path", input: "/tmp/example.html"}, {name: "relative path with dot", input: "./example.html"}, {name: "relative parent path", input: "../example.html"}, {name: "home path", input: "~/example.html"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := Normalize(tt.input); got != tt.input { t.Errorf("Normalize(%q) = %q, want unchanged", tt.input, got) } }) } } func TestTrimLeadingSpacesIfURL(t *testing.T) { tests := []struct { name string input string want string }{ { name: "single space before https URL", input: " https://example.com", want: "https://example.com", }, { name: "multiple spaces before https URL", input: " https://example.com", want: "https://example.com", }, { name: "tab before URL", input: "\thttps://example.com", want: "https://example.com", }, { name: "mixed whitespace before URL", input: " \t https://example.com", want: "https://example.com", }, { name: "space before domain", input: " example.com", want: "example.com", }, { name: "spaces before localhost", input: " localhost:8080", want: "localhost:8080", }, { name: "no leading space URL unchanged", input: "https://example.com", want: "https://example.com", }, { name: "search query with leading space unchanged", input: " hello world", want: " hello world", }, { name: "single word with leading space unchanged", input: " hello", want: " hello", }, { name: "empty string unchanged", input: "", want: "", }, { name: "only spaces unchanged", input: " ", want: " ", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := TrimLeadingSpacesIfURL(tt.input) if got != tt.want { t.Errorf("TrimLeadingSpacesIfURL(%q) = %q, want %q", tt.input, got, tt.want) } }) } } func TestLooksLikeURL(t *testing.T) { tests := []struct { name string input string want bool }{ { name: "empty string", input: "", want: false, }, { name: "http URL", input: "http://example.com", want: true, }, { name: "https URL", input: "https://example.com", want: true, }, { name: "file URL", input: "file:///path/to/file", want: true, }, { name: "dumb URL", input: "dumb://history", want: true, }, { name: "obsidian URL", input: "obsidian://open?vault=foo.bar", want: true, }, { name: "about URL", input: "about:blank", want: true, }, { name: "spotify URL", input: "spotify://track/123", want: true, }, { name: "mailto URL", input: "mailto:foo@example.com", want: true, }, { name: "magnet URL", input: "magnet:?xt=urn:btih:123", want: true, }, { name: "vscode URL", input: "vscode://file/tmp/demo", want: true, }, { name: "domain", input: "example.com", want: true, }, { name: "domain with path", input: "example.com/path", want: true, }, { name: "search query", input: "hello world", want: false, }, { name: "single word", input: "hello", want: false, }, { name: "localhost", input: "localhost", want: true, }, { name: "localhost with port", input: "localhost:5173", want: true, }, { name: "localhost with alt port", input: "localhost:8080", want: true, }, { name: "localhost with path", input: "localhost/api/v1", want: true, }, { name: "localhost.com is a domain not localhost", input: "localhost.com", want: true, }, { name: "localhostevil.com is a domain not localhost", input: "localhostevil.com", want: true, }, { name: "ipv4 with port", input: "100.64.0.10:3000", want: true, }, { name: "ipv4", input: "127.0.0.1", want: true, }, { name: "ipv6 bracketed", input: "[::1]", want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := LooksLikeURL(tt.input) if got != tt.want { t.Errorf("LooksLikeURL(%q) = %v, want %v", tt.input, got, tt.want) } }) } } func TestNormalize_IPAddressGetsHTTPByDefault(t *testing.T) { tests := []struct { name string input string want string }{ {name: "tailnet ipv4", input: "100.64.0.10", want: "http://100.64.0.10"}, {name: "tailnet ipv4 with port", input: "100.64.0.10:8080", want: "http://100.64.0.10:8080"}, {name: "ipv4 with path", input: "100.64.0.10/api", want: "http://100.64.0.10/api"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := Normalize(tt.input) if got != tt.want { t.Errorf("Normalize(%q) = %q, want %q", tt.input, got, tt.want) } }) } } func TestSanitizeDomainForPNGPathKeysDoNotCollide(t *testing.T) { pathKey := SanitizeDomainForPNG("example.com/a/b") underscoreKey := SanitizeDomainForPNG("example.com/a_b") if pathKey == underscoreKey { t.Fatalf("path key filename collision: %q", pathKey) } if pathKey != "example.com%2Fa%2Fb.png" { t.Fatalf("path key filename = %q, want example.com%%2Fa%%2Fb.png", pathKey) } if underscoreKey != "example.com%2Fa_b.png" { t.Fatalf("underscore key filename = %q, want example.com%%2Fa_b.png", underscoreKey) } } func TestSanitizeDomainForLongPathKeysUsesBoundedDeterministicFilenames(t *testing.T) { longKeyA := "example.com/" + strings.Repeat("very-long-segment/", 24) + "alpha" longKeyB := "example.com/" + strings.Repeat("very-long-segment/", 24) + "beta" pngA := SanitizeDomainForPNG(longKeyA) pngB := SanitizeDomainForPNG(longKeyB) if len(pngA) > 240 { t.Fatalf("png filename too long: len=%d name=%q", len(pngA), pngA) } if pngA == pngB { t.Fatalf("distinct long keys collided: %q", pngA) } if !strings.HasSuffix(pngA, ".png") { t.Fatalf("png filename missing suffix: %q", pngA) } ico := SanitizeDomainForFilename(longKeyA) if len(ico) > 240 { t.Fatalf("ico filename too long: len=%d name=%q", len(ico), ico) } if !strings.HasSuffix(ico, ".ico") { t.Fatalf("ico filename missing suffix: %q", ico) } sized := SanitizeDomainForPNGSized(longKeyA, 32) if len(sized) > 240 { t.Fatalf("sized png filename too long: len=%d name=%q", len(sized), sized) } if !strings.HasSuffix(sized, ".32.png") { t.Fatalf("sized png filename missing suffix: %q", sized) } } func TestIsExternalScheme(t *testing.T) { tests := []struct { name string input string want bool }{ { name: "empty string", input: "", want: false, }, { name: "http scheme", input: "http://example.com", want: false, }, { name: "https scheme", input: "https://example.com", want: false, }, { name: "file scheme", input: "file:///path/to/file.html", want: false, }, { name: "dumb scheme", input: "dumb://history", want: false, }, { name: "about scheme", input: "about:blank", want: false, }, { name: "data scheme", input: "data:text/plain;base64,SGVsbG8gV29ybGQ==", want: false, }, { name: "blob scheme", input: "blob:https://example.com/blob-id", want: false, }, { name: "javascript scheme", input: "javascript:alert('hello')", want: false, }, { name: "vscode scheme", input: "vscode://open", want: true, }, { name: "spotify scheme", input: "spotify:track:123", want: true, }, { name: "steam scheme", input: "steam://run/123", want: true, }, { name: "uppercase HTTP scheme", input: "HTTP://example.com", want: false, }, { name: "uppercase ABOUT scheme", input: "ABOUT:blank", want: false, }, { name: "uppercase VSCODE scheme", input: "VSCODE://open", want: true, }, { name: "geo scheme", input: "geo:37.786971,-122.399677", want: true, }, { name: "xmpp scheme", input: "xmpp:user@example.com", want: true, }, { name: "malformed URL", input: "://invalid", want: false, }, { name: "no scheme", input: "example.com", want: false, }, { name: "localhost port is not external scheme", input: "localhost:5173", want: false, }, { name: "scheme only", input: "custom://", want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := IsExternalScheme(tt.input) if got != tt.want { t.Errorf("IsExternalScheme(%q) = %v, want %v", tt.input, got, tt.want) } }) } } func TestExtractOrigin_ValidURI(t *testing.T) { tests := []struct { name string uri string expected string }{ { name: "https with port", uri: "https://example.com:8443/path?query=1", expected: "https://example.com:8443", }, { name: "https without port", uri: "https://example.com/path", expected: "https://example.com", }, { name: "http", uri: "http://localhost:8080/app", expected: "http://localhost:8080", }, { name: "uppercase host", uri: "https://EXAMPLE.COM/path", expected: "https://example.com", }, { name: "uppercase scheme", uri: "HTTPS://example.com/path", expected: "https://example.com", }, { name: "https default port omitted", uri: "https://example.com:443/path", expected: "https://example.com", }, { name: "http default port omitted", uri: "http://example.com:80/path", expected: "http://example.com", }, { name: "https explicit non-default port kept", uri: "https://example.com:8080/path", expected: "https://example.com:8080", }, { name: "mixed case with default port", uri: "HTTPS://EXAMPLE.COM:443/path", expected: "https://example.com", }, { name: "ipv6 with explicit port", uri: "https://[::1]:8443/path", expected: "https://[::1]:8443", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { origin, err := ExtractOrigin(tt.uri) if err != nil { t.Errorf("ExtractOrigin(%q) returned error: %v", tt.uri, err) return } if origin != tt.expected { t.Errorf("ExtractOrigin(%q) = %q, want %q", tt.uri, origin, tt.expected) } }) } } func TestExtractOrigin_InvalidURI(t *testing.T) { tests := []struct { name string uri string }{ {name: "empty", uri: ""}, {name: "no scheme", uri: "example.com"}, {name: "no host", uri: "https://"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { origin, err := ExtractOrigin(tt.uri) if err == nil { t.Errorf("ExtractOrigin(%q) should return error, got origin: %q", tt.uri, origin) } }) } } func TestCanonicalDomain(t *testing.T) { tests := []struct { name string input string want string }{ {name: "empty", input: "", want: ""}, {name: "whitespace only", input: " ", want: ""}, {name: "url", input: "https://www.example.com/path", want: "example.com"}, {name: "url with port", input: "https://www.example.com:8443/path", want: "example.com:8443"}, {name: "url with userinfo", input: "https://user:pass@example.com/path", want: "example.com"}, {name: "url with userinfo and port", input: "https://user:pass@example.com:8443/path", want: "example.com:8443"}, {name: "domain", input: "www.example.com", want: "example.com"}, {name: "domain with path", input: "www.example.com/path?q=1", want: "example.com"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := CanonicalDomain(tt.input); got != tt.want { t.Errorf("CanonicalDomain(%q) = %q, want %q", tt.input, got, tt.want) } }) } } func TestDisplayDomain(t *testing.T) { tests := []struct { name string input string want string }{ {name: "empty", input: "", want: ""}, {name: "whitespace only", input: " ", want: ""}, {name: "url with port", input: "https://www.example.com:8443/path", want: "example.com"}, {name: "url without port", input: "https://www.example.com/path", want: "example.com"}, {name: "url with userinfo", input: "https://user:pass@example.com/path", want: "example.com"}, {name: "no www prefix", input: "https://docs.example.com/path", want: "docs.example.com"}, {name: "host only", input: "www.example.org:9443/path?q=1", want: "example.org"}, {name: "invalid url", input: "%zz", want: "%zz"}, {name: "non-http scheme", input: "ftp://www.example.net:21/file", want: "example.net"}, {name: "ipv6 url", input: "https://[2001:db8::1]:8443/path", want: "2001:db8::1"}, {name: "uppercase host", input: "https://WWW.EXAMPLE.COM/path", want: "example.com"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := DisplayDomain(tt.input); got != tt.want { t.Errorf("DisplayDomain(%q) = %q, want %q", tt.input, got, tt.want) } }) } }