Test for URL validation

Hello, I am newbie in Elixir. I have URL validation from this gist.

  def validate_url(changeset, field, opts \\ []) do
    validate_change changeset, field, fn _, value ->
      case URI.parse(value) do
        %URI{scheme: nil} -> "is missing a scheme (e.g. https)"
        %URI{host: nil} -> "is missing a host"
        %URI{host: host} ->
          case :inet.gethostbyname(Kernel.to_charlist host) do
            {:ok, _} -> nil
            {:error, _} -> "invalid host"
          end
      end
      |> case do
        error when is_binary(error) -> [{field, Keyword.get(opts, :message, error)}]
        _ -> []
      end
    end
  end

How can I write a correct test please?

I found some test example but I think I will need something different for this function.

defmodule ValidateTest do
  use ExUnit.Case

  test "validate_url" do
    assert Validate.validate_url("https://example.com/bar/123") == :ok
    assert Validate.validate_url("https://example.com/bar/123/../456") == {:error, :path_cannot_contain_parent_reference}
    assert Validate.validate_url("1.2.3.4") == {:error, :no_host_info}
    assert Validate.validate_url("https://1.2.3.4") == {:error, :no_path_info}
    assert Validate.validate_url("http://1.2.3.4") == {:error, :no_path_info}
    assert Validate.validate_url("http://example.com/bar/123") == {:error, :other_error}
    assert Validate.validate_url(123) == {:error, :other_error}
  end

  test "valid_url?" do
    assert Validate.valid_url?("https://example.com/bar/123")
    refute Validate.valid_url?("https://example.com/bar/123/../456")
  end
end

Thanks a lof for any help! :slight_smile:

1 Like

To test this function as-is, you’d need to make an Ecto.Changeset with the correct shape and then call validate_url with the right arguments.

BUT

That’s a lot of machinery to involve just to put a string in the right place. Hard-to-test functions are frequently a sign that code could be structured better. For instance, you could extract the “is this string a valid URL” part out from the changeset-handling part:

def validate_url(changeset, field, opts \\ []) do
  validate_change changeset, field, fn _, value ->
    case check_url(value) do
      error when is_binary(error) -> [{field, Keyword.get(opts, :message, error)}]
      _ -> []
    end
  end
end

def check_url(value) do
  case URI.parse(value) do
    %URI{scheme: nil} -> "is missing a scheme (e.g. https)"
    %URI{host: nil} -> "is missing a host"
    %URI{host: host} ->
      case :inet.gethostbyname(Kernel.to_charlist host) do
        {:ok, _} -> nil
        {:error, _} -> "invalid host"
      end
  end
end

You could also clean up check_url a little - returning nil on success is somewhat unusual.

def validate_url(changeset, field, opts \\ []) do
  validate_change changeset, field, fn _, value ->
    case check_url(value) do
      :ok -> []
      {:error, error} -> [{field, Keyword.get(opts, :message, error)}]
    end
  end
end

def check_url(value) do
  case URI.parse(value) do
    %URI{scheme: nil} -> "is missing a scheme (e.g. https)"
    %URI{host: nil} -> "is missing a host"
    %URI{host: host} ->
      case :inet.gethostbyname(Kernel.to_charlist host) do
        {:ok, _} -> nil
        {:error, _} -> "invalid host"
      end
  end
end

This will let you write detailed tests for check_url without setup hassles (like in your second code block), then a single test that uses validate_url in context, to verify that it handles the error case - presumably the tests for SomeSchema.changeset.

2 Likes

Thank you a lot for answer. :slightly_smiling_face:
Ok, so now I have this code:

def validate_url(changeset, field, opts \\ []) do
  validate_change changeset, field, fn _, value ->
    case check_url(value) do
      :ok -> []
      {:error, error} -> [{field, Keyword.get(opts, :message, error)}]
    end
  end
end

def check_url(value) do
  case URI.parse(value) do
    %URI{scheme: nil} -> "is missing a scheme (e.g. https)"
    %URI{host: nil} -> "is missing a host"
    %URI{host: host} -> validate_host(host)
  end
 end
end

  def validate_host(host) do
    case :inet.gethostbyname(Kernel.to_charlist(host)) do
      {:ok, _} -> nil
      {:error, _} -> "invalid host"
    end
  end
end

That’s what I call in this changesets:

  def changeset(site, attrs) do
    site
    |> cast(attrs, [:name, :url, :page_counts, :user_id, :site_config_id])
    |> validate_required([:name, :url, :page_counts, :user_id])
    |> MyApp.EctoHelpers.validate_url(:url)
  end

#and this

def changeset(site_config, attrs) do
    site_config
    |> cast(attrs, [:lang, :privacy_policy_url, :logo, :color, :user_id])
    |> validate_required([:lang, :privacy_policy_url, :logo, :color, :user_id])
    |> MyApp.EctoHelpers.validate_url(:privacy_policy_url)
  end

Interestingly, if I only comment out the changeset with the privacy policy(validate_url(:privacy_policy_url)), my tests pass. And validate_utl(url) can be left as is.
So I thought there was already a test somewhere for the url, but I just couldn’t find it. (As a newbie I still don’t understand every part of the code in detail)
So I wanted to write my own test for validation in addition to the automatically generated tests.
Unfortunately, I’ve only modified the generated tests so far, so I’m having trouble understanding the principle of how to put this test together correctly.
I originally thought that as the automated test verifies the creation of the page with valid data, I would similarly create something to validate the URL.

    test "create_site/1 with valid data creates a site" do
      user = user_fixture()

      valid_attrs = %{
        name: "some name",
        page_counts: 42,
        url: "https://some_url.com",
        user_id: user.id
      }

      assert {:ok, %Site{} = site} = Sites.create_site(valid_attrs)
      assert site.name == "some name"
      assert site.page_counts == 42
      assert site.url == "https://some_url.com"
      assert site.user_id == user.id
    end

Then I came across the method of validation viz. first comment and thought I might write something like this:

  test "validate_url" do
    assert MyApp.EctoHelpers.validate_url("https://example.com/bar/123") == :ok
    assert MyApp.EctoHelpers.validate_url("https://example.com/bar/123/../456") == {:error, ???}
    assert MyApp.EctoHelpers.validate_url("1.2.3.4") == {:error, ???}
    assert MyApp.EctoHelpers.validate_url("https://1.2.3.4") == {:error, ???}
    assert MyApp.EctoHelpers.validate_url("http://1.2.3.4") == {:error, ???}
    assert MyApp.EctoHelpers.validate_url("http://example.com/bar/123") == {:error, ???}
    assert MyApp.EctoHelpers.validate_url(123) == {:error, ???}
  end

  test "valid_url?" do
    assert MyApp.EctoHelpers.validate_url?("https://example.com/bar/123")
    refute MyApp.EctoHelpers.validate_url?("https://example.com/bar/123/../456")
  end

So what should I write instead ??? or how should i write functional test?

Thank you very much for your help and patience. And I apologize for any stupidity in the question. :slight_smile:

Finally, I needed to add this invalid URL test to the generated tests, and then the tests passed without errors.

 test "update_site_config/2 with invalid url" do
       site_config = site_config_fixture(user_fixture())

       assert {:error, %Ecto.Changeset{}} =
                Sites.update_site_config(site_config, %{privacy_policy_url: "bad example"})
     end