Skip to content

Commit

Permalink
Add and make require_tld=true the default for URL constraint
Browse files Browse the repository at this point in the history
  • Loading branch information
Blacksmoke16 committed Dec 16, 2024
1 parent d6aefc3 commit fc4506d
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 7 deletions.
38 changes: 33 additions & 5 deletions src/components/validator/spec/constraints/url_validator_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ struct URLValidatorTest < AVD::Spec::ConstraintValidatorTestCase

@[DataProvider("valid_urls")]
def test_valid_urls(value : String) : Nil
self.validator.validate value, self.new_constraint
self.validator.validate value, self.new_constraint require_tld: false
self.assert_no_violation
end

@[DataProvider("valid_urls")]
@[DataProvider("valid_relative_urls")]
def test_valid_relative_urls(value : String) : Nil
self.validator.validate value, self.new_constraint relative_protocol: true
self.validator.validate value, self.new_constraint relative_protocol: true, require_tld: false
self.assert_no_violation
end

Expand Down Expand Up @@ -118,14 +118,14 @@ struct URLValidatorTest < AVD::Spec::ConstraintValidatorTestCase

@[DataProvider("invalid_urls")]
def test_invalid_urls(value : String) : Nil
self.validator.validate value, self.new_constraint message: "my_message"
self.validator.validate value, self.new_constraint message: "my_message", require_tld: false
self.assert_violation "my_message", CONSTRAINT::INVALID_URL_ERROR, value
end

@[DataProvider("invalid_urls")]
@[DataProvider("invalid_relative_urls")]
def test_invalid_relative_urls(value : String) : Nil
self.validator.validate value, self.new_constraint message: "my_message", relative_protocol: true
self.validator.validate value, self.new_constraint message: "my_message", relative_protocol: true, require_tld: false
self.assert_violation "my_message", CONSTRAINT::INVALID_URL_ERROR, value
end

Expand Down Expand Up @@ -176,7 +176,7 @@ struct URLValidatorTest < AVD::Spec::ConstraintValidatorTestCase

@[DataProvider("valid_custom_urls")]
def test_custom_protocols_are_valid(value : String) : Nil
self.validator.validate value, self.new_constraint protocols: ["ftp", "file", "git"]
self.validator.validate value, self.new_constraint protocols: ["ftp", "file", "git"], require_tld: false
self.assert_no_violation
end

Expand All @@ -188,6 +188,34 @@ struct URLValidatorTest < AVD::Spec::ConstraintValidatorTestCase
}
end

@[TestWith(
{"https://aaa", true, false},
{"https://aaa", false, true},
{"https://localhost", true, false},
{"https://localhost", false, true},
{"http://127.0.0.1", false, true},
{"http://127.0.0.1", true, false},
{"http://user.pass@local", false, true},
{"http://user.pass@local", true, false},
{"https://example.com", true, true},
{"https://example.com", false, true},
{"http://foo/bar.png", false, true},
{"http://foo/bar.png", true, false},
{"https://example.com.org", true, true},
{"https://example.com.org", false, true},
)]
def test_require_tld(value : String, require_tld : Bool, is_valid : Bool) : Nil
self.validator.validate value, self.new_constraint require_tld: require_tld, tld_message: "my_message"

if is_valid
self.assert_no_violation
else
self
.build_violation("my_message", CONSTRAINT::MISSING_TLD_ERROR, value)
.assert_violation
end
end

private def create_validator : AVD::ConstraintValidatorInterface
CONSTRAINT::Validator.new
end
Expand Down
41 changes: 39 additions & 2 deletions src/components/validator/src/constraints/url.cr
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,29 @@
#
# If `true` the protocol is considered optional.
#
# ### require_tld
#
# **Type:** `Bool` **Default:** `true`
#
# The [URL spec](https://datatracker.ietf.org/doc/html/rfc1738) considers URLs like `https://aaa` or `https://foobar` to be valid
# However, this is most likely not desirable for most use cases.
# As such, this argument defaults to `true` and can be used to require that the host part of the URL will have to include a TLD (top-level domain name).
# E.g. `https://example.com` is valid but `https://example` is not.
#
# NOTE: This constraint does _NOT_ validate that the provided TLD is a valid one according to the [official list](https://en.wikipedia.org/wiki/List_of_Internet_top-level_domains).
#
# ### tld_message
#
# **Type:** `String` **Default:** `This URL is missing a top-level domain.`
#
# The message that will be shown if `#require_tld?` is `true` and the URL does not contain at least one TLD.
#
# #### Placeholders
#
# The following placeholders can be used in this message:
#
# * `{{ value }}` - The current (invalid) value.
#
# ### message
#
# **Type:** `String` **Default:** `This value is not a valid URL.`
Expand Down Expand Up @@ -57,17 +80,23 @@
# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.
class Athena::Validator::Constraints::URL < Athena::Validator::Constraint
INVALID_URL_ERROR = "e87ceba6-a896-4906-9957-b102045272ee"
MISSING_TLD_ERROR = "4507f4cc-90fd-4616-989b-2166fc0d1083"

@@error_names = {
INVALID_URL_ERROR => "INVALID_URL_ERROR",
MISSING_TLD_ERROR => "MISSING_TLD_ERROR",
}

getter protocols : Array(String)
getter? relative_protocol : Bool
getter? require_tld : Bool
getter tld_message : String

def initialize(
@protocols : Array(String) = ["http", "https"],
@relative_protocol : Bool = false,
@require_tld : Bool = true,
@tld_message : String = "This URL is missing a top-level domain.",
message : String = "This value is not a valid URL.",
groups : Array(String) | String | Nil = nil,
payload : Hash(String, String)? = nil,
Expand All @@ -81,9 +110,17 @@ class Athena::Validator::Constraints::URL < Athena::Validator::Constraint
value = value.to_s

return if value.nil? || value.empty?
return if value.matches? self.pattern(constraint)
unless value.matches? self.pattern(constraint)
self.context.add_violation constraint.message, INVALID_URL_ERROR, value
end

return unless constraint.require_tld?
return unless url_host = URI.parse(value).host

self.context.add_violation constraint.message, INVALID_URL_ERROR, value
# URL with a TLD must include at least a `.`, but cannot be an IP address
if !url_host.includes?('.') || Socket::IPAddress.valid?(url_host)
self.context.add_violation constraint.tld_message, MISSING_TLD_ERROR, value
end
end

def pattern(constraint : AVD::Constraints::URL) : ::Regex
Expand Down

0 comments on commit fc4506d

Please sign in to comment.