No function clause matching in Ecto.build_assoc/3

Hi all
I have a video controller that looks as follow:

defmodule Rumbl.VideoController do
  use Rumbl.Web, :controller

  alias Rumbl.Video
  alias Rumbl.Category

  plug :load_categories when action in [:new, :create, :edit, :update]

  defp load_categories(conn, _) do
    query =
      Category
      |> Category.alphabetical
      |> Category.names_and_ids
    categories = Repo.all query
    assign(conn, :categories, categories)
  end

  def action(conn, _) do
    apply(__MODULE__, action_name(conn),
          [conn, conn.params, conn.assigns.current_user])
  end

  def index(conn, _params, user) do
    videos = Repo.all(user_videos(user))

    render(conn, "index.html", videos: videos)
  end

  def show(conn, %{"id" => id}, user) do
    video = Repo.get!(user_videos(user), id)
    render(conn, "show.html", video: video)
  end

  def new(conn, _params, user) do
    changeset =
      user
      |> build_assoc(:videos)
      |> Video.changeset()

    render(conn, "new.html", changeset: changeset)
  end

  def create(conn, %{"video" => video_params}, user) do
    changeset =
      user
      |> build_assoc(:videos)
      |> Video.changeset(video_params)

    case Repo.insert(changeset) do
      {:ok, _video} ->
        conn
        |> put_flash(:info, "Video created successfully.")
        |> redirect(to: video_path(conn, :index))
      {:error, changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

  def edit(conn, %{"id" => id}, user) do
    video = Repo.get!(user_videos(user), id)
    changeset = Video.changeset(video)
    render(conn, "edit.html", video: video, changeset: changeset)
  end

  def update(conn, %{"id" => id, "video" => video_params}, user) do
    video = Repo.get!(user_videos(user), id)
    changeset = Video.changeset(video, video_params)

    case Repo.update(changeset) do
      {:ok, video} ->
        conn
        |> put_flash(:info, "Video updated successfully.")
        |> redirect(to: video_path(conn, :show, video))
      {:error, changeset} ->
        render(conn, "edit.html", video: video, changeset: changeset)
    end
  end

  def delete(conn, %{"id" => id}, user) do
    video = Repo.get!(user_videos(user), id)
    Repo.delete!(video)

    conn
    |> put_flash(:info, "Video deleted successfully.")
    |> redirect(to: video_path(conn, :index))
  end

  defp user_videos(user) do
    assoc(user, :videos)
  end
end

Then I wrote a test for controller

defmodule Rumbl.VideoControllerTest do
  use Rumbl.ConnCase

  test "requires user authentication on all actions", %{conn: conn} do
    Enum.each([
      get(conn, video_path(conn, :new)),
      get(conn, video_path(conn, :index)),
      get(conn, video_path(conn, :show, "123")),
      get(conn, video_path(conn, :edit, "123")),
      put(conn, video_path(conn, :update, "123", %{})),
      post(conn, video_path(conn, :create, %{})),
      delete(conn, video_path(conn, :delete, "123")),
    ], fn conn ->
      assert html_response(conn, 302)
      assert conn.halted
    end)
  end
end

and try to test it, but I’ve got error message

  1) test requires user authentication on all actions (Rumbl.VideoControllerTest)
     test/controllers/video_controller_test.exs:4
     ** (FunctionClauseError) no function clause matching in Ecto.build_assoc/3
     stacktrace:
       (ecto) lib/ecto.ex:460: Ecto.build_assoc(nil, :videos, %{})
       (rumbl) web/controllers/video_controller.ex:40: Rumbl.VideoController.new/3
       (rumbl) web/controllers/video_controller.ex:1: Rumbl.VideoController.action/2
       (rumbl) web/controllers/video_controller.ex:1: Rumbl.VideoController.phoenix_controller_pipeline/2
       (rumbl) lib/rumbl/endpoint.ex:1: Rumbl.Endpoint.instrument/4
       (rumbl) lib/phoenix/router.ex:261: Rumbl.Router.dispatch/2
       (rumbl) web/router.ex:1: Rumbl.Router.do_call/2
       (rumbl) lib/rumbl/endpoint.ex:1: Rumbl.Endpoint.phoenix_pipeline/1
       (rumbl) lib/rumbl/endpoint.ex:1: Rumbl.Endpoint.call/2
       (phoenix) lib/phoenix/test/conn_test.ex:224: Phoenix.ConnTest.dispatch/5
       test/controllers/video_controller_test.exs:6: (test)

.....

Finished in 0.1 seconds
9 tests, 1 failure

I could not find out, what is wrong with new function.

Thanks

1 Like

It seems like your user is nil in the new action and you pass nil to build_assoc.

How does the code look that assigns the current user to conn?

EDIT:

Actually, I know what the code looks like since it comes from the Phoenix book, right?

The relevant part is this:

  def call(conn, repo) do
     user_id = get_session(conn, :user_id)
    cond do
      user = conn.assigns[:current_user] ->
        put_current_user(conn, user)
      user = user_id && repo.get(Rumbl.User, user_id) ->
        put_current_user(conn, user)
      true ->
        assign(conn, :current_user, nil)
    end
  end

You can see that it falls back to setting the :current_user to nil when a user is not found.

1 Like

@Linuus is right, and the source of nil is the first line of your test:

1 Like

Yes you are right. The code comes from phoenix book. So do I have to implement the call function?

1 Like

As you created the action(conn, _), all of your actions inside that controller became action/3 and the third argument is the user.

This is way here you are passing “123” as user id:
get(conn, video_path(conn, :show, "123"))

So, to fix your error, just pass the fake user id in the 2 first lines:
get(conn, video_path(conn, :new, "123")),
get(conn, video_path(conn, :index, "123")),

1 Like

This is not correct. The id passed there is the video id and not the user id so it won’t help. The user ID comes from the session.

I would create a separate plug which checks if conn.assigns.current_user is nil and if so halt and return status code 401.

(I’m on my phone at the moment so I can’t write example code at the moment :slight_smile: )

1 Like

Yes It does not work at all. I try it now.

1 Like

Yes, @Linuus, you are right, sorry about consfusing.

But @kostonstyle, what do you want to test here?

Your test description says: “requires user authentication on all actions”. If you want to test that the connection will be halted when no user is authenticated, then you need to use a plug that requires authentication.

If you are using the Phoenix Book, you can see an example of it on Page 95:
pipe_through [:browser, :authenticate_user]

If you are following the book, you should have it as a function plug in your web/controllers/auth.ex

1 Like

Just for other people that might end up here after a google search, the answer is here.

From your router.ex file, remove the resources "/videos" from / scope (it’s repeated on both / and manage scopes).

8 Likes

that worked for me. Thanks @arashm