0

I'm writing a controller function where it will check for a condition ( keyword to be valid ) before either to render a json object or error object if failed.

router.ex

scope "/api", DongNghiaWeb do
    pipe_through :api
    scope "/tim_kiem" do
      get "/tu/:word"   , APIWordController, :search_word
[...]

word_controller.ex

def search_word(conn, %{"word" => word}) do
  conn
  |> check_invalid_keyword(word)
  |> render("search.json", words: word |> String.trim |> Words.suggest)
end

defp check_invalid_keyword(conn, keyword) do
  unless Words.keyword_valid?(String.trim(keyword)) do
    conn
    |> put_status(400)
    |> json(%{
      error: "Invalid keyword"
    })
  end
  conn
end

word_controller_test.ex

test "response error when word is not valid", %{conn: conn} do
  response = get(conn,api_word_path(conn, :search_word, "a3d"))
    |> json_response(400)
  assert response["error"] == "Invalid keyword"
end

When running mix test, the results will be like so :

** (RuntimeError) expected response with status 400, got: 200, with body: {"data":[]}

But when I try testing with a REST client ( Insomnia, for example ), the json will return to be { error : "Invalid keyword" } just fine.

1 Answers1

3

Your code is writing the response to the connection twice when Words.keyword_valid?(String.trim(keyword)) is falsy.

The first write happens when you call |> json(...) and the second one when you call render. Your code does not prevent render being called when json has already been called. The browser connection ends after the first write, so you see the right output with Insomnia but the testing setup uses the last response written to the connection.

Fixing this requires a bit of restructuring of your code. Here's how I would do it:

def search_word(conn, %{"word" => word}) do
  if Words.keyword_valid?(String.trim(word)) do
    conn
    |> render("search.json", words: word |> String.trim() |> Words.suggest())
  else
    conn
    |> put_status(400)
    |> json(%{
      error: "Invalid keyword"
    })
  end
end

Edit: here's one way to do what you requested in the comment below:

def search_word(conn, %{"word" => word}) do
  with {:ok, conn} <- check_invalid_keyword(conn, word) do
    conn
    |> render("search.json", words: word |> String.trim() |> Words.suggest())
  end
end

def check_invalid_keyword(conn, keyword) do
  if Words.keyword_valid?(String.trim(keyword)) do
    {:ok, conn}
  else
    conn
    |> put_status(400)
    |> json(%{
      error: "Invalid keyword"
    })
  end
end

When the keyword is invalid, the return value fails to match the with clause and is returned as is. If it was valid, the do block gets executed.

Dogbert
  • 212,659
  • 41
  • 396
  • 397
  • Thanks ! But because I'm repeating the `check_invalid_keyword` for multiple function in the controller, which is why I wrote it as a separate function. How would I achieve it here ? While your code answered my question, it doesn't really resolve my problem – Sơn Đỗ Đình Thy Jul 21 '18 at 10:38
  • By "repeating", you mean like `conn |> check_invalid_keyword(word) |> check_invalid_keyword(word2) |> check_invalid_keyword(word3) |> render(...)`? – Dogbert Jul 21 '18 at 10:52
  • No I mean in function `show(conn, etc. )` and `search(conn, etc. )` will both need to use the check – Sơn Đỗ Đình Thy Jul 21 '18 at 10:54
  • 1
    Ah. I've added a solution for that. Let me know if it works! – Dogbert Jul 21 '18 at 10:59