package validators import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestDomainValidator_SafeHostname(t *testing.T) { type testCase struct { description string options DomainValidatorOptions input string expected string errorFunc func(t *testing.T, e error) } tests := []testCase{ { description: "Empty url fails", errorFunc: func(t *testing.T, e error) { assert.ErrorContains(t, e, "url cannot be empty") }, }, { description: "URL without host fails", input: "/foo", errorFunc: func(t *testing.T, e error) { assert.ErrorContains(t, e, "missing host in url") }, }, { description: "Invalid url fails", input: "foo:foo", errorFunc: func(t *testing.T, e error) { assert.ErrorContains(t, e, "failed to parse host") }, }, { description: "With scheme and invalid url should fail", options: DomainValidatorOptions{WithScheme: true, AllowedSchemes: []string{"https"}}, input: "https://foo:foo", errorFunc: func(t *testing.T, e error) { assert.ErrorContains(t, e, "failed to parse input url") }, }, { description: "Scheme disabled with scheme should pass", input: "https://example.com", expected: "example.com", }, { description: "Domain without scheme should parse if scheme is disabled", input: "example.com", expected: "example.com", }, { description: "Domain without scheme should not parse if scheme is enabled", options: DomainValidatorOptions{WithScheme: true}, input: "example.com", errorFunc: func(t *testing.T, e error) { assert.ErrorContains(t, e, "invalid scheme") }, }, { description: "Domain with scheme and disallowed scheme should fail", options: DomainValidatorOptions{WithScheme: true, AllowedSchemes: []string{"https"}}, input: "foo://example.com", errorFunc: func(t *testing.T, e error) { assert.ErrorContains(t, e, "invalid scheme") }, }, { description: "Domain with scheme and allowed scheme should pass", options: DomainValidatorOptions{WithScheme: true, AllowedSchemes: []string{"https"}}, input: "https://example.com", expected: "example.com", }, { description: "Domain should get lowercased", input: "EXAMPLE.COM", expected: "example.com", }, { description: "DNS dot should be removed", input: "example.com.", expected: "example.com", }, { description: "IPv4 address should fail", input: "127.0.0.1", errorFunc: func(t *testing.T, e error) { assert.ErrorContains(t, e, "ip addresses are not supported") }, }, { description: "IPv6 address should fail", input: "[::1]", errorFunc: func(t *testing.T, e error) { assert.ErrorContains(t, e, "ip addresses are not supported") }, }, { description: "Domains with unicode characters should be allowed", input: "bücher.example.com", expected: "xn--bcher-kva.example.com", }, { description: "Invalid IDNA domain should fail", input: "ab--cd.example.com", errorFunc: func(t *testing.T, e error) { assert.ErrorContains(t, e, "invalid label") }, }, { description: "With port enabled without any port should work", options: DomainValidatorOptions{WithPort: true}, input: "example.com", expected: "example.com", }, } for _, test := range tests { t.Run(test.description, func(t *testing.T) { v := NewDomainValidator(test.options) res, err := v.SafeHostname(test.input) if test.errorFunc != nil { test.errorFunc(t, err) return } require.NoError(t, err) assert.Equal(t, test.expected, res) }) } } func TestDomainValidator_Validate(t *testing.T) { type testCase struct { description string options DomainValidatorOptions expected string actual string errorFunc func(t *testing.T, e error) } tests := []testCase{ { description: "Invalid expected domain fails checks", expected: "foo:foo", actual: "bar.com", errorFunc: func(t *testing.T, e error) { assert.ErrorContains(t, e, "failed to parse host:") }, }, { description: "Invalid check domain fails checks", expected: "example.com", actual: "foo:foo", errorFunc: func(t *testing.T, e error) { assert.ErrorContains(t, e, "failed to parse host:") }, }, { description: "Valid domains with non-matching schemes should fail", options: DomainValidatorOptions{WithScheme: true, AllowedSchemes: []string{"https", "http"}}, expected: "https://example.com", actual: "http://example.com", errorFunc: func(t *testing.T, e error) { assert.ErrorIs(t, e, ErrSchemeMismatch) }, }, { description: "Valid domains with matching schemes should pass", options: DomainValidatorOptions{WithScheme: true, AllowedSchemes: []string{"https", "http"}}, expected: "https://example.com", actual: "https://example.com", }, { description: "Port validation with ports enabled and empty ports should work", options: DomainValidatorOptions{WithPort: true}, expected: "example.com", actual: "example.com", }, { description: "Port validation should pass with same port", options: DomainValidatorOptions{WithPort: true}, expected: "example.com:8080", actual: "example.com:8080", }, { description: "Port enabled with scheme and matching port should pass", options: DomainValidatorOptions{WithPort: true}, expected: "https://example.com:443", actual: "https://example.com:443", }, { description: "Failure to format expected domain should fail", expected: "ab--cd.example.com", actual: "example.com", errorFunc: func(t *testing.T, e error) { assert.ErrorContains(t, e, "idna: invalid label") }, }, { description: "Failure to format check domain should fail", expected: "example.com", actual: "ab--cd.example.com", errorFunc: func(t *testing.T, e error) { assert.ErrorContains(t, e, "idna: invalid label") }, }, { description: "Valid domains with matching schemes and ports should pass", options: DomainValidatorOptions{WithScheme: true, AllowedSchemes: []string{"https", "http"}, WithPort: true}, expected: "https://example.com:8080", actual: "https://example.com:8080", }, { description: "Valid domains with matching schemes should pass", options: DomainValidatorOptions{WithScheme: true, AllowedSchemes: []string{"https", "http"}}, expected: "https://example.com", actual: "https://example.com", }, { description: "Valid domains with non matching ports should fail", options: DomainValidatorOptions{WithPort: true}, expected: "example.com:8080", actual: "example.com:8085", errorFunc: func(t *testing.T, e error) { assert.ErrorIs(t, e, ErrPortMismatch) }, }, { description: "Valid domains without ports or schemes should pass", actual: "example.com", expected: "example.com", }, { description: "Unicode valid domains should pass", expected: "xn--bcher-kva.example.com", actual: "bücher.example.com", }, { description: "Unicode valid domains should pass (reverse)", expected: "bücher.example.com", actual: "xn--bcher-kva.example.com", }, { description: "Non matching hostnames should fail", expected: "example.com", actual: "foo.com", errorFunc: func(t *testing.T, e error) { assert.ErrorIs(t, e, ErrHostnameMismatch) }, }, } for _, test := range tests { t.Run(test.description, func(t *testing.T) { v := NewDomainValidator(test.options) err := v.Validate(test.expected, test.actual) if test.errorFunc != nil { test.errorFunc(t, err) return } require.NoError(t, err) }) } }