0
votes

I am trying to respond with JSON after validating a GET request against a JWT token using Guardian.

my test:

  describe "Show user if authenticated" do
    setup %{conn: conn} do
      {:ok, %User{} = user} = Riders.create_user(@create_attrs)
      {:ok, jwt, claims} = Bikefit.Guardian.encode_and_sign(user)
      IO.inspect claims
      conn = put_req_header(conn, "authorization", "Bearer #{jwt}")
      {:ok, conn: conn, user: user}
    end

    test  "and return user if token is valid", %{conn: conn, user: user} do
      email = user.email
      conn = get conn, current_user_path(conn, :current)
      response = json_response(conn, 200)
      IO.puts "---------- test response--------"
      IO.inspect response
      assert %{"email" => email} = response
    end
  end

My controller:

defmodule BikefitWeb.UserController do
  use BikefitWeb, :controller

  alias Bikefit.Riders
  alias Bikefit.Riders.User

  plug Guardian.Plug.EnsureAuthenticated, handler: BikefitWeb.AuthController

  action_fallback BikefitWeb.FallbackController

  def current(conn, _params) do
    user = conn |> Guardian.Plug.current_resource

    conn
    |> render(BikefitWeb.UserView, "show.json-api", user: user)
  end
end

my view:

defmodule BikefitWeb.UserView do
  use BikefitWeb, :view
  alias BikefitWeb.UserView

  def render("show.json-api", %{user: user}) do
    %{data: render_one(user, UserView, "user.json")}
  end

  def render("user.json",  %{user: user}) do
    %{id: user.id,
      email: user.email,
      auth_provider: user.auth_provider}
  end
end

my router:

defmodule BikefitWeb.Router do
  use BikefitWeb, :router

  pipeline :api do
    plug :accepts, ["json", "json-api"]
    plug JaSerializer.Deserializer
  end

  pipeline :api_auth do
    plug :accepts, ["json", "json-api"]
    plug Guardian.Plug.Pipeline, 
          module: Bikefit.Guardian, 
          error_handler: BikefitWeb.AuthErrorHandler
    plug Guardian.Plug.VerifyHeader, realm: "Bearer"
    plug Guardian.Plug.LoadResource
    plug JaSerializer.Deserializer
  end

  scope "/api/v1", BikefitWeb do
    pipe_through :api_auth

    resources "/users", UserController, except: [:new, :edit]
    get "/user/current", UserController, :current, as: :current_user
    delete "/logout", UserController, :delete
  end
end

The output from the test:

   ** (FunctionClauseError) no function clause matching in Plug.Conn.resp/3

     The following arguments were given to Plug.Conn.resp/3:

         # 1
         %Plug.Conn{adapter: {Plug.Adapters.Test.Conn, :...}, 
  assigns: %{layout: false, user: %Bikefit.Riders.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
  auth_provider: "some auth_provider", email: "some email", id: 272, inserted_at: ~N[2018-02-24 06:28:20.876426], 
  password: nil, password_hash: nil, updated_at: ~N[2018-02-24 06:28:20.876442]}}, 
  before_send: [#Function<1.42514850/1 in Plug.Logger.call/2>], body_params: %{}, 
  cookies: %Plug.Conn.Unfetched{aspect: :cookies}, halted: false, host: "www.example.com", method: "GET", owner: #PID<0.418.0>, 
  params: %{}, path_info: ["api", "v1", "user", "current"], path_params: %{}, peer: {{127, 0, 0, 1}, 111317}, 
  port: 80, private: %{BikefitWeb.Router => {[], %{}}, 
  :guardian_default_claims => %{"aud" => "Bikefit", "exp" => 1522045700, "iat" => 1519453700, "iss" => "Bikefit", "jti" => "5f8312a9-8f49-4f0a-9654-968882cd3f90", "nbf" => 1519453699, "sub" => "some email", "typ" => "access"}, 
  :guardian_default_resource => %Bikefit.Riders.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, auth_provider: "some auth_provider", email: "some email", id: 272, inserted_at: ~N[2018-02-24 06:28:20.876426], password: nil, password_hash: nil, updated_at: ~N[2018-02-24 06:28:20.876442]}, 
  :guardian_default_token => "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJCaWtlZml0IiwiZXhwIjoxNTIyMDQ1NzAwLCJpYXQiOjE1MTk0NTM3MDAsImlzcyI6IkJpa2VmaXQiLCJqdGkiOiI1ZjgzMTJhOS04ZjQ5LTRmMGEtOTY1NC05Njg4ODJjZDNmOTAiLCJuYmYiOjE1MTk0NTM2OTksInN1YiI6InNvbWUgZW1hac3MifQ.gt7B7itXs8HFWEzFwQxa5LJaDzSKkF1b2C4BDxw28nP3Q4_cHDi-PexZQDh8BbjD363qFKK9p9jvoQqqe9yx8A", :guardian_error_handler => BikefitWeb.AuthErrorHandler, 
 :guardian_module => Bikefit.Guardian, :phoenix_action => :current, 
 :phoenix_controller => BikefitWeb.UserController, :phoenix_endpoint => BikefitWeb.Endpoint, 
 :phoenix_format => "json", :phoenix_layout => {BikefitWeb.LayoutView, :app}, :phoenix_pipelines => [:api_auth], 
 :phoenix_recycled => true, :phoenix_router => BikefitWeb.Router, :phoenix_template => "show.json-api", 
 :phoenix_view => BikefitWeb.UserView, :plug_session_fetch => #Function<1.45862765/1 in Plug.Session.fetch_session/1>, 
 :plug_skip_csrf_protection => true}, query_params: %{}, query_string: "", remote_ip: {127, 0, 0, 1}, 
 req_cookies: %Plug.Conn.Unfetched{aspect: :cookies}, req_headers: [{"accept", "application/json"}, 
 {"authorization", "Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJCaWtlZml0IiwiZXhwIjoxNTIyMDQ1NzAwLCJpYXQiOjE1MTk0NTM3MDAsImlzcyI6IkJpa2VmaXQRmMGEtOTY1NC05Njg4ODJjZDNmOTAiLCJuYmYiOjE1MTk0NTM2OTksInN1YiI6InNvbWUgZW1haWwiLCJ0eXAiOiJhY2Nlc3MifQ.gt7B7itXs8HFWEzFwQxa5LJaDzSKkF1b2C4BDxw28nP3Q4_cHDi-PexZQDh8BbjD363qFKK9p9jvoQqqe9yx8A"}], 
 request_path: "/api/v1/user/current", resp_body: nil, resp_cookies: %{}, 
 resp_headers: [{"content-type", "application/vnd.api+json; charset=utf-8"}, 
 {"cache-control", "max-age=0, private, must-revalidate"}, {"x-request-id", "44e2jir0js16jn0pe2pf8qgbksc9346s"}], 
 scheme: :http, script_name: [], secret_key_base: "UD4qdbi6YOBRbCrf", 
 state: :unset, status: nil}

         # 2
         200

         # 3
         %{data: %{auth_provider: "some auth_provider", email: "some email", id: 272}}

     Attempted function clauses (showing 3 out of 3):

         def resp(%Plug.Conn{state: state}, status, _body) when not(state === :set or (state === :set_chunked or (state === :set_file or state === :unset)))
         def resp(%Plug.Conn{}, _status, nil)
         def resp(%Plug.Conn{} = conn, status, body) when is_binary(body) or is_list(body)

     code: conn = get conn, current_user_path(conn, :current)
     stacktrace:
       (plug) lib/plug/conn.ex:505: Plug.Conn.resp/3
       (plug) lib/plug/conn.ex:495: Plug.Conn.send_resp/3
       (bikefit) lib/bikefit_web/controllers/user_controller.ex:1: BikefitWeb.UserController.action/2
       (bikefit) lib/bikefit_web/controllers/user_controller.ex:1: BikefitWeb.UserController.phoenix_controller_pipeline/2
       (bikefit) lib/bikefit_web/endpoint.ex:1: BikefitWeb.Endpoint.instrument/4
       (phoenix) lib/phoenix/router.ex:278: Phoenix.Router.__call__/1
       (bikefit) lib/bikefit_web/endpoint.ex:1: BikefitWeb.Endpoint.plug_builder_call/2
       (bikefit) lib/bikefit_web/endpoint.ex:1: BikefitWeb.Endpoint.call/2
       (phoenix) lib/phoenix/test/conn_test.ex:224: Phoenix.ConnTest.dispatch/5
       test/bikefit_web/controllers/user_controller_test.exs:63: (test)

I think the problem is that conn's state: :unset is causing the problem on the function clause match, but I'm not sure why this is happening as I think my controllers and views are matching the tutorials. Any ideas?

2
If one expects a json response, one should use json not render: render(conn, BikefitWeb.UserView, "show.json-api", user: user)json(conn, BikefitWeb.UserView, "show.json-api", user: user).Aleksei Matiushkin
json\4 doesn't seem to exist in the Phoenix.Controller module. Only json\2, which does not invoke the View. The Phoenix guide to controller testing shows using render to respond with JSON, but I can't figure out what I have done wrong.CHsurfer
resp receives %{data: render_one(user, UserView, "user.json")} while it expects either a binary or a list. Try to convert this map to JSON string manually then, using any JSON serializer.Aleksei Matiushkin
Ok, some progress. By changing the view function from "show.json-api" to "show.json" (and adjusting the UserController to call this view) it seem to be working as expected. For some reason, calling the view "show.json-api" prevented the automatic serialization of the JSON returned by the view. Your last comment caused me to think about why the JSON was not being serialized and using 'json-api' was the only difference compared to the phoenix 'testing controller' docs, so thanks a lot for your comments.CHsurfer
If anyone can post an answer explaining why using 'json-api' results in unserialized json being sent to the Plug.resp, I'll accept that answer.CHsurfer

2 Answers

4
votes

You need to tell Phoenix how to encode different formats via the format_encoders configuration. By default it knows how to encode .json but nothing about .json-api, that's why they were not encoded by default.

Renaming it .json or adding a new entry to format encoders should be enough to fix it.

0
votes

i stumbled upon the same error with a different issue:

i was trying to render an xml-layout in my controller

got rid of it with "put_root_layout(false)"

  def general_feed_xml(conn, _params) do
    conn
    |> put_resp_content_type("text/xml")
    |> put_root_layout(false)
    |> render("general_feed.xml", [])
  end

if it helps anyone.