Routes.Helper.path/3 is undefined error in test

I’m having some further problems with the generated tests for my controller.

I ran

mix phx.gen.json Propsects Person persons {with my data parameters} --web Prospects

and the migration, schema and controller files (with tests) were generated. I added the resource to my Router and ran mix.phx.routes to verify with the following results:

mix phx.routes
page_path GET / ScandimensionWeb.PageController :index
admin_path GET /admin ScandimensionWeb.AdminController :index
user_path GET /api/users ScandimensionWeb.UserController :index
user_path GET /api/users/:id ScandimensionWeb.UserController :show
user_path POST /api/users ScandimensionWeb.UserController :create
user_path PATCH /api/users/:id ScandimensionWeb.UserController :update
PUT /api/users/:id ScandimensionWeb.UserController :update
user_path DELETE /api/users/:id ScandimensionWeb.UserController :delete
user_path POST /api/users/sign_in ScandimensionWeb.UserController :sign_in
person_path GET /prospects/persons ScandimensionWeb.PersonController :index
person_path GET /prospects/persons/:id/edit ScandimensionWeb.PersonController :edit
person_path GET /prospects/persons/new ScandimensionWeb.PersonController :new
person_path GET /prospects/persons/:id ScandimensionWeb.PersonController :show
person_path POST /prospects/persons ScandimensionWeb.PersonController :create
person_path PATCH /prospects/persons/:id ScandimensionWeb.PersonController :update
PUT /prospects/persons/:id ScandimensionWeb.PersonController :update
person_path DELETE /prospects/persons/:id ScandimensionWeb.PersonController :delete
websocket WS /socket/websocket ScandimensionWeb.UserSocket

When I run mix test, the new PersonControllerTest fails on each call to:

Routes.prospects_person_path(conn, :index))

with the following error message:

** (UndefinedFunctionError) function ScandimensionWeb.Router.Helpers.prospects_person_path/2 is undefined or private
code: conn = post(conn, Routes.prospects_person_path(conn, :create), person: @create_attrs)
stacktrace:
(scandimension) ScandimensionWeb.Router.Helpers.prospects_person_path(%Plug.Conn{adapter: {Plug.Adapters.Test.Conn, :…}, assigns: %{}, before_send: [], body_params: %Plug.Conn.Unfetched{aspect: :body_params}, cookies: %Plug.Conn.Unfetched{aspect: :cookies}, halted: false, host: “www.example.com”, method: “GET”, owner: #PID<0.439.0>, params: %Plug.Conn.Unfetched{aspect: :params}, path_info: [], path_params: %{}, port: 80, private: %{phoenix_recycled: true, plug_skip_csrf_protection: true}, query_params: %Plug.Conn.Unfetched{aspect: :query_params}, query_string: “”, remote_ip: {127, 0, 0, 1}, req_cookies: %Plug.Conn.Unfetched{aspect: :cookies}, req_headers: [{“accept”, “application/json”}], request_path: “/”, resp_body: nil, resp_cookies: %{}, resp_headers: [{“cache-control”, “max-age=0, private, must-revalidate”}], scheme: :http, script_name: [], secret_key_base: nil, state: :unset, status: nil}, :create)
test/scandimension_web/controllers/prospects/person_controller_test.exs:43: (test)

I checked the helpers via iex:

iex(4)> ScandimensionWeb.Router.Helpers.
admin_path/2 admin_path/3 admin_url/2 admin_url/3
page_path/2 page_path/3 page_url/2 page_url/3
path/2 person_path/2 person_path/3 person_path/4
person_url/2 person_url/3 person_url/4 static_path/2
static_url/2 url/1 user_path/2 user_path/3
user_path/4 user_url/2 user_url/3 user_url/4

And agree that prospects_person_path is not defined, but I’m not certain how to fix this.

Any guidance?

Thanks,

You’ll need to scope the prospects path. If you show your router someone could probably help further. But to get phoenix to pick up the name you’ll probably want to do some route nesting.

I’m going guess something like this might help you out:

scope "/prospects", ScandimensionWeb, as: "prospects" do
  resources "/persons", PersonController
end

You can also nest the resource.

scope "/", ScandimensionWeb do
  resources "/prospects", ProspectsController do
    resources "/persons", PersonController
  end
end

https://hexdocs.pm/phoenix/routing.html#nested-resources

Is that wanted to mix Propsects with Prospects? or is it a typo?

Is that wanted to mix Propsects with Prospects? or is it a typo?

Yes, that was a typo as I created the post.

The original Router file is:

defmodule ScandimensionWeb.Router do
  use ScandimensionWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json"]
    plug :fetch_session
  end

  pipeline :api_auth do
    plug :ensure_authenticated
  end

  scope "/", ScandimensionWeb do
    pipe_through :browser

    get "/", PageController, :index
    get "/admin", AdminController, :index
    
  end

  # Other scopes may use custom stacks.
  scope "/api", ScandimensionWeb do
    pipe_through :api
    resources "/users", UserController, except: [:new, :edit]
    post "/users/sign_in", UserController, :sign_in
  end

  scope "/prospects", ScandimensionWeb do
    pipe_through :api
    resources "/persons", PersonController
  end

  #plug function
  defp ensure_authenticated(conn, _opts) do
    current_user_id = get_session(conn, :current_user_id)

    if current_user_id do
      conn
    else
      conn
      |> put_status(:unauthorized)
      |> put_view(ScandimensionWeb.ErrorView)
      |> render("401.json", message: "Unauthenticated user")
      |> halt()
    end
  end

end

When I change the scope statement (as recommended by mikemccall) to:

  scope "/prospects", ScandimensionWeb, as: "prospects" do
    pipe_through :api
    resources "/persons", PersonController
  end

I get a different failure on the same six lines in the test:

** (UndefinedFunctionError) function ScandimensionWeb.PersonController.init/1 is undefined (module ScandimensionWeb.PersonController is not available)
code: conn = put(conn, Routes.prospects_person_path(conn, :update, person), person: @invalid_attrs)
stacktrace:
ScandimensionWeb.PersonController.init(:update)
(phoenix) lib/phoenix/router.ex:275: Phoenix.Router.call/1
(scandimension) lib/scandimension_web/endpoint.ex:1: ScandimensionWeb.Endpoint.plug_builder_call/2
(scandimension) lib/scandimension_web/endpoint.ex:1: ScandimensionWeb.Endpoint.call/2
(phoenix) lib/phoenix/test/conn_test.ex:235: Phoenix.ConnTest.dispatch/5
test/scandimension_web/controllers/prospects/person_controller_test.exs:86: (test)

But now see the helper routes:

iex(3)> ScandimensionWeb.Router.Helpers.
admin_path/2 admin_path/3
admin_url/2 admin_url/3
page_path/2 page_path/3
page_url/2 page_url/3
path/2 prospects_person_path/2
prospects_person_path/3 prospects_person_path/4
prospects_person_url/2 prospects_person_url/3
prospects_person_url/4 static_path/2
static_url/2 url/1
user_path/2 user_path/3
user_path/4 user_url/2
user_url/3 user_url/4

person_controller.ex is:

defmodule ScandimensionWeb.Prospects.PersonController do
  use ScandimensionWeb, :controller

  alias Scandimension.Prospects
  alias Scandimension.Prospects.Person

  action_fallback ScandimensionWeb.FallbackController

  def index(conn, _params) do
    persons = Prospects.list_persons()
    render(conn, "index.json", persons: persons)
  end

  def create(conn, %{"person" => person_params}) do
    with {:ok, %Person{} = person} <- Prospects.create_person(person_params) do
      conn
      |> put_status(:created)
      |> put_resp_header("location", Routes.prospects_person_path(conn, :show, person))
      |> render("show.json", person: person)
    end
  end

  def show(conn, %{"id" => id}) do
    person = Prospects.get_person!(id)
    render(conn, "show.json", person: person)
  end

  def update(conn, %{"id" => id, "person" => person_params}) do
    person = Prospects.get_person!(id)

    with {:ok, %Person{} = person} <- Prospects.update_person(person, person_params) do
      render(conn, "show.json", person: person)
    end
  end

  def delete(conn, %{"id" => id}) do
    person = Prospects.get_person!(id)

    with {:ok, %Person{}} <- Prospects.delete_person(person) do
      send_resp(conn, :no_content, "")
    end
  end
end

Using --web will namespace controller…

it will be ScandimensionWeb.Prospects.PersonController.

Try to add an alias

alias ScandimensionWeb.Prospects.PersonController

alias ScandimensionWeb.Prospects.PersonController

Same result

** (UndefinedFunctionError) function ScandimensionWeb.ScandimensionWeb.Prospects.PersonController.init/1 is undefined (module ScandimensionWeb.ScandimensionWeb.Prospects.PersonController is not available)

Ok, try this one…

alias Prospects.PersonController

Nope…

** (UndefinedFunctionError) function ScandimensionWeb.ScandimensionWeb.Prospects.PersonController.init/1 is undefined (module ScandimensionWeb.ScandimensionWeb.Prospects.PersonController is not available)

The router needs to know about the namespace.

Currently you have

scope "/prospects", ScandimensionWeb, as: "prospects" do
    pipe_through :api
    resources "/persons", PersonController
  end

And this gets you the route helper :+1:

But if you look at it, it is namespaced to ScandimensionWeb.PersonController when it needs to be ScandimensionWeb.Prospects.PersonController in regards to the controller module.

defmodule ScandimensionWeb.Prospects.PersonController do
  use ScandimensionWeb, :controller
  ...
end

Try

scope "/prospects", ScandimensionWeb, as: "prospects" do
    pipe_through :api
    resources "/persons", Prospects.PersonController
  end

or

scope "/", ScandimensionWeb do
  scope "/prospects", Prospects, as: "prospects" do
    pipe_through :api
    resources "/persons", PersonController
  end
end

Adding the namespace for the resources did the trick. Thank you!

1 Like

Adding this alias in the router should also have been working :slight_smile:

alias Prospects.PersonController

True, but from what I can tell using the alias with scope will always append. That is why @ChipCoons got this error.

** (UndefinedFunctionError) function ScandimensionWeb.ScandimensionWeb.Prospects.PersonController.init/1 is undefined (module ScandimensionWeb.ScandimensionWeb.Prospects.PersonController is not available)

Hence the double ScandimensionWeb.

Another option would be leaving the alias out of the scope declaration such as:

alias ScandimensionWeb.Prospects.PersonController
...
scope path: "/prospects", as: "prospects" do
   pipe_through :api
   resources "/persons", PersonController
end

Which I think is what you were getting at? There’s definitely more than one way to skin a cat with the phoenix router.

I guess technically this works too:

scope "/prospects", ScandimensionWeb, as: "prospects" do
    pipe_through :api
    alias Prospects.PersonController
    resources "/persons", PersonController
  end

But I’m not sure I’m a fan of nesting the alias.

1 Like

I actually decided for this app that namespacing was not needed, and reverted back to an earlier version and re-ran the mix generate command without the --Web flag for name spacing. Thank you all for your assistance. I’m sure I’ll run across something else as I try to pick up Phoenix and Elixir.

1 Like