mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-22 21:14:51 +03:00
785 lines
19 KiB
Go
785 lines
19 KiB
Go
package utils
|
||
|
||
import (
|
||
"testing"
|
||
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
func TestMatchCallbackURL(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
pattern string
|
||
input string
|
||
shouldMatch bool
|
||
}{
|
||
// Basic matching
|
||
{
|
||
"exact match",
|
||
"https://example.com/callback",
|
||
"https://example.com/callback",
|
||
true,
|
||
},
|
||
{
|
||
"no match",
|
||
"https://example.org/callback",
|
||
"https://example.com/callback",
|
||
false,
|
||
},
|
||
|
||
// Scheme
|
||
{
|
||
"scheme mismatch",
|
||
"https://example.com/callback",
|
||
"http://example.com/callback",
|
||
false,
|
||
},
|
||
{
|
||
"wildcard scheme",
|
||
"*://example.com/callback",
|
||
"https://example.com/callback",
|
||
true,
|
||
},
|
||
|
||
// Hostname
|
||
{
|
||
"hostname mismatch",
|
||
"https://example.com/callback",
|
||
"https://malicious.com/callback",
|
||
false,
|
||
},
|
||
{
|
||
"wildcard subdomain",
|
||
"https://*.example.com/callback",
|
||
"https://subdomain.example.com/callback",
|
||
true,
|
||
},
|
||
{
|
||
"partial wildcard in hostname prefix",
|
||
"https://app*.example.com/callback",
|
||
"https://app1.example.com/callback",
|
||
true,
|
||
},
|
||
{
|
||
"partial wildcard in hostname suffix",
|
||
"https://*-prod.example.com/callback",
|
||
"https://api-prod.example.com/callback",
|
||
true,
|
||
},
|
||
{
|
||
"partial wildcard in hostname middle",
|
||
"https://app-*-server.example.com/callback",
|
||
"https://app-staging-server.example.com/callback",
|
||
true,
|
||
},
|
||
{
|
||
"subdomain wildcard doesn't match domain hijack attempt",
|
||
"https://*.example.com/callback",
|
||
"https://malicious.site?url=abc.example.com/callback",
|
||
false,
|
||
},
|
||
{
|
||
"hostname mismatch with confusable characters",
|
||
"https://example.com/callback",
|
||
"https://examp1e.com/callback",
|
||
false,
|
||
},
|
||
{
|
||
"hostname mismatch with homograph attack",
|
||
"https://example.com/callback",
|
||
"https://еxample.com/callback",
|
||
false,
|
||
},
|
||
|
||
// Port
|
||
{
|
||
"port mismatch",
|
||
"https://example.com:8080/callback",
|
||
"https://example.com:9090/callback",
|
||
false,
|
||
},
|
||
{
|
||
"wildcard port",
|
||
"https://example.com:*/callback",
|
||
"https://example.com:8080/callback",
|
||
true,
|
||
},
|
||
{
|
||
"partial wildcard in port prefix",
|
||
"https://example.com:80*/callback",
|
||
"https://example.com:8080/callback",
|
||
true,
|
||
},
|
||
|
||
// Path
|
||
{
|
||
"path mismatch",
|
||
"https://example.com/callback",
|
||
"https://example.com/other",
|
||
false,
|
||
},
|
||
{
|
||
"wildcard path segment",
|
||
"https://example.com/api/*/callback",
|
||
"https://example.com/api/v1/callback",
|
||
true,
|
||
},
|
||
{
|
||
"wildcard entire path",
|
||
"https://example.com/*",
|
||
"https://example.com/callback",
|
||
true,
|
||
},
|
||
{
|
||
"partial wildcard in path prefix",
|
||
"https://example.com/test*",
|
||
"https://example.com/testcase",
|
||
true,
|
||
},
|
||
{
|
||
"partial wildcard in path suffix",
|
||
"https://example.com/*-callback",
|
||
"https://example.com/oauth-callback",
|
||
true,
|
||
},
|
||
{
|
||
"partial wildcard in path middle",
|
||
"https://example.com/api-*-v1/callback",
|
||
"https://example.com/api-internal-v1/callback",
|
||
true,
|
||
},
|
||
{
|
||
"multiple partial wildcards in path",
|
||
"https://example.com/*/test*/callback",
|
||
"https://example.com/v1/testing/callback",
|
||
true,
|
||
},
|
||
{
|
||
"multiple wildcard segments in path",
|
||
"https://example.com/**/callback",
|
||
"https://example.com/api/v1/foo/bar/callback",
|
||
true,
|
||
},
|
||
{
|
||
"multiple wildcard segments in path",
|
||
"https://example.com/**/v1/**/callback",
|
||
"https://example.com/api/v1/foo/bar/callback",
|
||
true,
|
||
},
|
||
{
|
||
"partial wildcard matching full path segment",
|
||
"https://example.com/foo-*",
|
||
"https://example.com/foo-bar",
|
||
true,
|
||
},
|
||
|
||
// Credentials
|
||
{
|
||
"username mismatch",
|
||
"https://user:pass@example.com/callback",
|
||
"https://admin:pass@example.com/callback",
|
||
false,
|
||
},
|
||
{
|
||
"missing credentials",
|
||
"https://user:pass@example.com/callback",
|
||
"https://example.com/callback",
|
||
false,
|
||
},
|
||
{
|
||
"unexpected credentials",
|
||
"https://example.com/callback",
|
||
"https://user:pass@example.com/callback",
|
||
false,
|
||
},
|
||
{
|
||
"wildcard password",
|
||
"https://user:*@example.com/callback",
|
||
"https://user:secret123@example.com/callback",
|
||
true,
|
||
},
|
||
{
|
||
"partial wildcard in username",
|
||
"https://admin*:pass@example.com/callback",
|
||
"https://admin123:pass@example.com/callback",
|
||
true,
|
||
},
|
||
{
|
||
"partial wildcard in password",
|
||
"https://user:pass*@example.com/callback",
|
||
"https://user:password123@example.com/callback",
|
||
true,
|
||
},
|
||
{
|
||
"wildcard password doesn't allow domain hijack",
|
||
"https://user:*@example.com/callback",
|
||
"https://user:password@malicious.site#example.com/callback",
|
||
false,
|
||
},
|
||
{
|
||
"credentials with @ in password trying to hijack hostname",
|
||
"https://user:pass@example.com/callback",
|
||
"https://user:pass@evil.com@example.com/callback",
|
||
false,
|
||
},
|
||
|
||
// Query parameters
|
||
{
|
||
"extra query parameter",
|
||
"https://example.com/callback?code=*",
|
||
"https://example.com/callback?code=abc123&extra=value",
|
||
false,
|
||
},
|
||
{
|
||
"missing query parameter",
|
||
"https://example.com/callback?code=*&state=*",
|
||
"https://example.com/callback?code=abc123",
|
||
false,
|
||
},
|
||
{
|
||
"query parameter after fragment",
|
||
"https://example.com/callback?code=123",
|
||
"https://example.com/callback#section?code=123",
|
||
false,
|
||
},
|
||
{
|
||
"query parameter name mismatch",
|
||
"https://example.com/callback?code=*",
|
||
"https://example.com/callback?token=abc123",
|
||
false,
|
||
},
|
||
{
|
||
"wildcard query parameter",
|
||
"https://example.com/callback?code=*",
|
||
"https://example.com/callback?code=abc123",
|
||
true,
|
||
},
|
||
{
|
||
"multiple query parameters",
|
||
"https://example.com/callback?code=*&state=*",
|
||
"https://example.com/callback?code=abc123&state=xyz789",
|
||
true,
|
||
},
|
||
{
|
||
"query parameters in different order",
|
||
"https://example.com/callback?state=*&code=*",
|
||
"https://example.com/callback?code=abc123&state=xyz789",
|
||
true,
|
||
},
|
||
{
|
||
"exact query parameter value",
|
||
"https://example.com/callback?mode=production",
|
||
"https://example.com/callback?mode=production",
|
||
true,
|
||
},
|
||
{
|
||
"query parameter value mismatch",
|
||
"https://example.com/callback?mode=production",
|
||
"https://example.com/callback?mode=development",
|
||
false,
|
||
},
|
||
{
|
||
"mixed exact and wildcard query parameters",
|
||
"https://example.com/callback?mode=production&code=*",
|
||
"https://example.com/callback?mode=production&code=abc123",
|
||
true,
|
||
},
|
||
{
|
||
"mixed exact and wildcard with wrong exact value",
|
||
"https://example.com/callback?mode=production&code=*",
|
||
"https://example.com/callback?mode=development&code=abc123",
|
||
false,
|
||
},
|
||
{
|
||
"multiple values for same parameter",
|
||
"https://example.com/callback?param=*¶m=*",
|
||
"https://example.com/callback?param=value1¶m=value2",
|
||
true,
|
||
},
|
||
{
|
||
"unexpected query parameters",
|
||
"https://example.com/callback",
|
||
"https://example.com/callback?extra=value",
|
||
false,
|
||
},
|
||
{
|
||
"query parameter with redirect to external site",
|
||
"https://example.com/callback?code=*",
|
||
"https://example.com/callback?code=123&redirect=https://evil.com",
|
||
false,
|
||
},
|
||
{
|
||
"open redirect via encoded URL in query param",
|
||
"https://example.com/callback?state=*",
|
||
"https://example.com/callback?state=abc&next=//evil.com",
|
||
false,
|
||
},
|
||
|
||
// Fragment
|
||
{
|
||
"fragment ignored when both pattern and input have fragment",
|
||
"https://example.com/callback#fragment",
|
||
"https://example.com/callback#fragment",
|
||
true,
|
||
},
|
||
{
|
||
"fragment ignored when pattern has fragment but input doesn't",
|
||
"https://example.com/callback#fragment",
|
||
"https://example.com/callback",
|
||
true,
|
||
},
|
||
{
|
||
"fragment ignored when input has fragment but pattern doesn't",
|
||
"https://example.com/callback",
|
||
"https://example.com/callback#section",
|
||
true,
|
||
},
|
||
|
||
// Path traversal and injection attempts
|
||
{
|
||
"path traversal attempt",
|
||
"https://example.com/callback",
|
||
"https://example.com/../admin/callback",
|
||
false,
|
||
},
|
||
{
|
||
"backslash instead of forward slash",
|
||
"https://example.com/callback",
|
||
"https://example.com\\callback",
|
||
false,
|
||
},
|
||
{
|
||
"double slash in hostname (protocol smuggling)",
|
||
"https://example.com/callback",
|
||
"https://example.com//evil.com/callback",
|
||
false,
|
||
},
|
||
{
|
||
"CRLF injection attempt in path",
|
||
"https://example.com/callback",
|
||
"https://example.com/callback%0d%0aLocation:%20https://evil.com",
|
||
false,
|
||
},
|
||
{
|
||
"null byte injection",
|
||
"https://example.com/callback",
|
||
"https://example.com/callback%00.evil.com",
|
||
false,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
matches, err := matchCallbackURL(tt.pattern, tt.input)
|
||
require.NoError(t, err, tt.name)
|
||
assert.Equal(t, tt.shouldMatch, matches, tt.name)
|
||
|
||
}
|
||
}
|
||
|
||
func TestGetCallbackURLFromList_LoopbackSpecialHandling(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
urls []string
|
||
inputCallbackURL string
|
||
expectedURL string
|
||
expectMatch bool
|
||
}{
|
||
{
|
||
name: "127.0.0.1 with dynamic port - exact match",
|
||
urls: []string{"http://127.0.0.1/callback"},
|
||
inputCallbackURL: "http://127.0.0.1:8080/callback",
|
||
expectedURL: "http://127.0.0.1:8080/callback",
|
||
expectMatch: true,
|
||
},
|
||
{
|
||
name: "127.0.0.1 with different port",
|
||
urls: []string{"http://127.0.0.1/callback"},
|
||
inputCallbackURL: "http://127.0.0.1:9999/callback",
|
||
expectedURL: "http://127.0.0.1:9999/callback",
|
||
expectMatch: true,
|
||
},
|
||
{
|
||
name: "IPv6 loopback with dynamic port",
|
||
urls: []string{"http://[::1]/callback"},
|
||
inputCallbackURL: "http://[::1]:8080/callback",
|
||
expectedURL: "http://[::1]:8080/callback",
|
||
expectMatch: true,
|
||
},
|
||
{
|
||
name: "IPv6 loopback without brackets in input",
|
||
urls: []string{"http://[::1]/callback"},
|
||
inputCallbackURL: "http://::1:8080/callback",
|
||
expectedURL: "http://::1:8080/callback",
|
||
expectMatch: true,
|
||
},
|
||
{
|
||
name: "localhost with dynamic port",
|
||
urls: []string{"http://localhost/callback"},
|
||
inputCallbackURL: "http://localhost:8080/callback",
|
||
expectedURL: "http://localhost:8080/callback",
|
||
expectMatch: true,
|
||
},
|
||
{
|
||
name: "https loopback doesn't trigger special handling",
|
||
urls: []string{"https://127.0.0.1/callback"},
|
||
inputCallbackURL: "https://127.0.0.1:8080/callback",
|
||
expectedURL: "",
|
||
expectMatch: false,
|
||
},
|
||
{
|
||
name: "loopback with path match",
|
||
urls: []string{"http://127.0.0.1/auth/*"},
|
||
inputCallbackURL: "http://127.0.0.1:3000/auth/callback",
|
||
expectedURL: "http://127.0.0.1:3000/auth/callback",
|
||
expectMatch: true,
|
||
},
|
||
{
|
||
name: "loopback with path mismatch",
|
||
urls: []string{"http://127.0.0.1/callback"},
|
||
inputCallbackURL: "http://127.0.0.1:8080/different",
|
||
expectedURL: "",
|
||
expectMatch: false,
|
||
},
|
||
{
|
||
name: "non-loopback IP",
|
||
urls: []string{"http://192.168.1.1/callback"},
|
||
inputCallbackURL: "http://192.168.1.1:8080/callback",
|
||
expectedURL: "",
|
||
expectMatch: false,
|
||
},
|
||
{
|
||
name: "wildcard matches loopback",
|
||
urls: []string{"*"},
|
||
inputCallbackURL: "http://127.0.0.1:8080/callback",
|
||
expectedURL: "http://127.0.0.1:8080/callback",
|
||
expectMatch: true,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
result, err := GetCallbackURLFromList(tt.urls, tt.inputCallbackURL)
|
||
require.NoError(t, err)
|
||
if tt.expectMatch {
|
||
assert.Equal(t, tt.expectedURL, result)
|
||
} else {
|
||
assert.Empty(t, result)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestGetCallbackURLFromList_MultiplePatterns(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
urls []string
|
||
inputCallbackURL string
|
||
expectedURL string
|
||
expectMatch bool
|
||
}{
|
||
{
|
||
name: "matches first pattern",
|
||
urls: []string{
|
||
"https://example.com/callback",
|
||
"https://example.org/callback",
|
||
},
|
||
inputCallbackURL: "https://example.com/callback",
|
||
expectedURL: "https://example.com/callback",
|
||
expectMatch: true,
|
||
},
|
||
{
|
||
name: "matches second pattern",
|
||
urls: []string{
|
||
"https://example.com/callback",
|
||
"https://example.org/callback",
|
||
},
|
||
inputCallbackURL: "https://example.org/callback",
|
||
expectedURL: "https://example.org/callback",
|
||
expectMatch: true,
|
||
},
|
||
{
|
||
name: "matches none",
|
||
urls: []string{
|
||
"https://example.com/callback",
|
||
"https://example.org/callback",
|
||
},
|
||
inputCallbackURL: "https://malicious.com/callback",
|
||
expectedURL: "",
|
||
expectMatch: false,
|
||
},
|
||
{
|
||
name: "matches wildcard pattern",
|
||
urls: []string{
|
||
"https://example.com/callback",
|
||
"https://*.example.org/callback",
|
||
},
|
||
inputCallbackURL: "https://subdomain.example.org/callback",
|
||
expectedURL: "https://subdomain.example.org/callback",
|
||
expectMatch: true,
|
||
},
|
||
{
|
||
name: "empty pattern list",
|
||
urls: []string{},
|
||
inputCallbackURL: "https://example.com/callback",
|
||
expectedURL: "",
|
||
expectMatch: false,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
result, err := GetCallbackURLFromList(tt.urls, tt.inputCallbackURL)
|
||
require.NoError(t, err)
|
||
if tt.expectMatch {
|
||
assert.Equal(t, tt.expectedURL, result)
|
||
} else {
|
||
assert.Empty(t, result)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestMatchPath(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
pattern string
|
||
input string
|
||
shouldMatch bool
|
||
}{
|
||
// Exact matches
|
||
{
|
||
name: "exact match",
|
||
pattern: "/callback",
|
||
input: "/callback",
|
||
shouldMatch: true,
|
||
},
|
||
{
|
||
name: "exact mismatch",
|
||
pattern: "/callback",
|
||
input: "/other",
|
||
shouldMatch: false,
|
||
},
|
||
{
|
||
name: "empty paths",
|
||
pattern: "",
|
||
input: "",
|
||
shouldMatch: true,
|
||
},
|
||
|
||
// Single wildcard (*)
|
||
{
|
||
name: "single wildcard matches segment",
|
||
pattern: "/api/*/callback",
|
||
input: "/api/v1/callback",
|
||
shouldMatch: true,
|
||
},
|
||
{
|
||
name: "single wildcard doesn't match multiple segments",
|
||
pattern: "/api/*/callback",
|
||
input: "/api/v1/v2/callback",
|
||
shouldMatch: false,
|
||
},
|
||
{
|
||
name: "single wildcard at end",
|
||
pattern: "/callback/*",
|
||
input: "/callback/test",
|
||
shouldMatch: true,
|
||
},
|
||
{
|
||
name: "single wildcard at start",
|
||
pattern: "/*/callback",
|
||
input: "/api/callback",
|
||
shouldMatch: true,
|
||
},
|
||
{
|
||
name: "multiple single wildcards",
|
||
pattern: "/*/test/*",
|
||
input: "/api/test/callback",
|
||
shouldMatch: true,
|
||
},
|
||
{
|
||
name: "partial wildcard prefix",
|
||
pattern: "/test*",
|
||
input: "/testing",
|
||
shouldMatch: true,
|
||
},
|
||
{
|
||
name: "partial wildcard suffix",
|
||
pattern: "/*-callback",
|
||
input: "/oauth-callback",
|
||
shouldMatch: true,
|
||
},
|
||
{
|
||
name: "partial wildcard middle",
|
||
pattern: "/api-*-v1",
|
||
input: "/api-internal-v1",
|
||
shouldMatch: true,
|
||
},
|
||
|
||
// Double wildcard (**)
|
||
{
|
||
name: "double wildcard matches multiple segments",
|
||
pattern: "/api/**/callback",
|
||
input: "/api/v1/v2/v3/callback",
|
||
shouldMatch: true,
|
||
},
|
||
{
|
||
name: "double wildcard matches single segment",
|
||
pattern: "/api/**/callback",
|
||
input: "/api/v1/callback",
|
||
shouldMatch: true,
|
||
},
|
||
{
|
||
name: "double wildcard doesn't match when pattern has extra slashes",
|
||
pattern: "/api/**/callback",
|
||
input: "/api/callback",
|
||
shouldMatch: false,
|
||
},
|
||
{
|
||
name: "double wildcard at end",
|
||
pattern: "/api/**",
|
||
input: "/api/v1/v2/callback",
|
||
shouldMatch: true,
|
||
},
|
||
{
|
||
name: "double wildcard in middle",
|
||
pattern: "/api/**/v2/**/callback",
|
||
input: "/api/v1/v2/v3/v4/callback",
|
||
shouldMatch: true,
|
||
},
|
||
|
||
// Complex patterns
|
||
{
|
||
name: "mix of single and double wildcards",
|
||
pattern: "/*/api/**/callback",
|
||
input: "/app/api/v1/v2/callback",
|
||
shouldMatch: true,
|
||
},
|
||
{
|
||
name: "wildcard with special characters",
|
||
pattern: "/callback-*",
|
||
input: "/callback-123",
|
||
shouldMatch: true,
|
||
},
|
||
{
|
||
name: "path with query-like string (no special handling)",
|
||
pattern: "/callback?code=*",
|
||
input: "/callback?code=abc",
|
||
shouldMatch: true,
|
||
},
|
||
|
||
// Edge cases
|
||
{
|
||
name: "single wildcard matches empty segment",
|
||
pattern: "/api/*/callback",
|
||
input: "/api//callback",
|
||
shouldMatch: true,
|
||
},
|
||
{
|
||
name: "pattern longer than input",
|
||
pattern: "/api/v1/callback",
|
||
input: "/api",
|
||
shouldMatch: false,
|
||
},
|
||
{
|
||
name: "input longer than pattern",
|
||
pattern: "/api",
|
||
input: "/api/v1/callback",
|
||
shouldMatch: false,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
matches, err := matchPath(tt.pattern, tt.input)
|
||
require.NoError(t, err)
|
||
assert.Equal(t, tt.shouldMatch, matches)
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestSplitParts(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
input string
|
||
expectedParts []string
|
||
expectedPath string
|
||
}{
|
||
{
|
||
name: "simple https URL",
|
||
input: "https://example.com/callback",
|
||
expectedParts: []string{"https", "example", "com"},
|
||
expectedPath: "/callback",
|
||
},
|
||
{
|
||
name: "URL with port",
|
||
input: "https://example.com:8080/callback",
|
||
expectedParts: []string{"https", "example", "com", "8080"},
|
||
expectedPath: "/callback",
|
||
},
|
||
{
|
||
name: "URL with subdomain",
|
||
input: "https://api.example.com/callback",
|
||
expectedParts: []string{"https", "api", "example", "com"},
|
||
expectedPath: "/callback",
|
||
},
|
||
{
|
||
name: "URL with credentials",
|
||
input: "https://user:pass@example.com/callback",
|
||
expectedParts: []string{"https", "user", "pass", "example", "com"},
|
||
expectedPath: "/callback",
|
||
},
|
||
{
|
||
name: "URL without path",
|
||
input: "https://example.com",
|
||
expectedParts: []string{"https", "example", "com"},
|
||
expectedPath: "",
|
||
},
|
||
{
|
||
name: "URL with deep path",
|
||
input: "https://example.com/api/v1/callback",
|
||
expectedParts: []string{"https", "example", "com"},
|
||
expectedPath: "/api/v1/callback",
|
||
},
|
||
{
|
||
name: "URL with path and query",
|
||
input: "https://example.com/callback?code=123",
|
||
expectedParts: []string{"https", "example", "com"},
|
||
expectedPath: "/callback?code=123",
|
||
},
|
||
{
|
||
name: "URL with trailing slash",
|
||
input: "https://example.com/",
|
||
expectedParts: []string{"https", "example", "com"},
|
||
expectedPath: "/",
|
||
},
|
||
{
|
||
name: "URL with multiple subdomains",
|
||
input: "https://api.v1.staging.example.com/callback",
|
||
expectedParts: []string{"https", "api", "v1", "staging", "example", "com"},
|
||
expectedPath: "/callback",
|
||
},
|
||
{
|
||
name: "URL with port and credentials",
|
||
input: "https://user:pass@example.com:8080/callback",
|
||
expectedParts: []string{"https", "user", "pass", "example", "com", "8080"},
|
||
expectedPath: "/callback",
|
||
},
|
||
{
|
||
name: "scheme with authority separator but no slash",
|
||
input: "http://example.com",
|
||
expectedParts: []string{"http", "example", "com"},
|
||
expectedPath: "",
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
parts, path := splitParts(tt.input)
|
||
assert.Equal(t, tt.expectedParts, parts, "parts mismatch")
|
||
assert.Equal(t, tt.expectedPath, path, "path mismatch")
|
||
})
|
||
}
|
||
}
|