Mapping from models to path helpers

I’d like to exploit the regularity of router.ex's resources to write tests expressed in terms of what the page lets people do to resources. Leaving syntax aside for the moment, that means assertions like:

  • the layout gives the user a quick link to the :index page for Eecrit.Animal.
  • the page lets the user see the :show page for this particular %Eecrit.Animal{...}
  • there is no :delete link for any Eecrit.Animal.
  • there’s a way to let the user :index Eecrit.Animals with include_out_of_service set. (That is, the path looks like "/animals&include_out_of_service=true".)

… and so on. This is pretty straightforward, except: how do you go from :index and Eecrit.Animal to Eecrit.Router.Helpers.animal_path?

Is that data preserved anywhere when the path helpers are generated? Can it be accessed at test-time?

Note: I’m not a huge fan of unit tests for views. But figuring out the details of doing this has taught me, a Phoenix and Elixir novice, a good deal. And, if I published the result in hex, it could lower the cost of testing the “outgoing paths” from views enough to make it worthwhile for others.

For what it’s worth, I’m looking at syntax like this:

    html
    |> allows_index!(Eecrit.Animal, "View all animals")
    |> allows_show!(%Eecrit.Animal{...}, "Show")
    |> disallows_any_delete!(Eecrit.Animal)
    |> allows_index!({Eecrit.Animal, include_out_of_service: true}, "Include animals already out of service")

Say your router is MyServer.Router.

You want to get all of the route information for whatever, thus do:

MyServer.Router.__routes__()

This returns a list of Phoenix.Router.Route structures. This is an internal implementation detail but should be easily accessed to get all(?) the information you want. :slight_smile:

Doesn’t have a link to the model module name. :frowning:

The module that handles the route (the Controller, like IndexController or so) should be on the :plug key on the %Phoenix.Router.Route{} structure. What else were you needing? :slight_smile:

If by ‘model’ you mean like the database model? Those have absolutely nothing to do with the Controllers or Routes at all so there is no mapping there. :slight_smile:

You could always add such information to your controllers that would allow you to make such a mapping though. :wink:

I meant the database model. There is a connection: the original mix phoenix.gen.html. (I realize it was a faint hope.)

Ah, no connection between those no. The mix phoenix.gen.html just happens to make a default controller that gives a REST-style interface to a single model, but those are almost always edited entirely because there are often multiple model interactions, perhaps it ends up having no model, it is all in the access. The generator is just something that fills in some (eex internal) templates. You could add such a function or attribute to your controllers defining what models they use though. :slight_smile:

… and so on. This is pretty straightforward, except: how do you go from :index and Eecrit.Animal to Eecrit.Router.Helpers.animal_path?

There isn’t anything that does it today. Assuming that’s the question you want answered, you could create a protocol that converts any data structure to a path. There are a couple options, you can either have a single function in the protoco that receives the data structure + atom or multiple actions:

defprotocol Routeable do
  def to_path(data, action)
end

Now you can implement it as:

import Eecript.Router.Helpers

defimpl Routeable, for: Eecript.Animal do
  def to_path(%Eecript.Animal{}, :insert) do
    animal_path(...)
  end
end

Now, by dispatching the protocol, you know what is allowed and what is not. You could pass extra parameters, like the current user, to raise based if the user has certain permissions or not. This would allow you to centralize all the permission logic in the protocol, which you could test in isolation, and you should be golden as long as you use the protocol in your views.

Keep in mind though that, even with the above, you can still accidentally expose a DELETE endpoint in your router. The only way to protect against those is by doing a request and asserting it is a 404 (or by introspecting the router).

2 Likes