Как использовать соединение с сессией в Phoenix?

У меня есть штепсель аутентификации, и я хочу протестировать свои контроллеры. Проблема в том, что линия в этой вилке

user_id = get_session(conn, :user_id)

И это всегда ноль, когда я использую этот метод (раньше я использовал грязный хак, но я больше не хочу этого делать):

  @session  Plug.Session.init([
    store:            :cookie,
    key:              "_app",
    encryption_salt:  "secret",
    signing_salt:     "secret",
    encrypt:          false
  ])

user = MyApp.Factory.create(:user)

conn()
|> put_req_header("accept", "application/vnd.api+json")
|> put_req_header("content-type", "application/vnd.api+json")
|> Map.put(:secret_key_base, String.duplicate("abcdefgh", 8))
|> Plug.Session.call(@session)
|> fetch_session
|> put_session(:user_id, user.id)

Я отправляю запрос на исправление, используя этот conn, и его сеанс user_id равен нулю. Результаты IO.puts conn в моем плагине:

%Plug.Conn{adapter: {Plug.Adapters.Test.Conn, :...}, assigns: %{},
 before_send: [#Function<0.111117999/1 in Plug.Session.before_send/2>,
  #Function<0.110103833/1 in JaSerializer.ContentTypeNegotiation.set_content_type/2>,
  #Function<1.55011211/1 in Plug.Logger.call/2>,
  #Function<0.111117999/1 in Plug.Session.before_send/2>], body_params: %{},
 cookies: %{}, halted: false, host: "www.example.com", method: "PATCH",
 owner: #PID<0.349.0>,
 params: %{"data" => %{"attributes" => %{"action" => "start"}}, "id" => "245"},
 path_info: ["api", "tasks", "245"], peer: {{127, 0, 0, 1}, 111317}, port: 80,
 private: %{MyApp.Router => {[], %{}}, :phoenix_endpoint => MyApp.Endpoint,
   :phoenix_format => "json-api", :phoenix_pipelines => [:api],
   :phoenix_recycled => true,
   :phoenix_route => #Function<4.15522358/1 in MyApp.Router.match_route/4>,
   :phoenix_router => MyApp.Router, :plug_session => %{},
   :plug_session_fetch => :done, :plug_session_info => :write,
   :plug_skip_csrf_protection => true}, query_params: %{}, query_string: "",
 remote_ip: {127, 0, 0, 1}, req_cookies: %{},
 req_headers: [{"accept", "application/vnd.api+json"},
  {"content-type", "application/vnd.api+json"}], request_path: "/api/tasks/245",
 resp_body: nil, resp_cookies: %{},
 resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"},
  {"x-request-id", "d00tun3s9d7fo2ah2klnhafvt3ks4pbj"}], scheme: :http,
 script_name: [],
 secret_key_base: "npvJ1fWodIYzJ2eNnJmC5b1LecCTsveK4/mj7akuBaLdeAr2KGH4gwohwHsz8Ony",
 state: :unset, status: nil}

Что мне нужно сделать, чтобы решить эту проблему и хорошо проверить аутентификацию?

ОБНОВЛЕНИЕ Разъем аутентификации

defmodule MyApp.Plug.Authenticate do
  import Plug.Conn
  import Phoenix.Controller

  def init(default), do: default

  def call(conn, _) do
    IO.puts inspect get_session(conn, :user_id)
    IO.puts conn
    user_id = get_session(conn, :user_id)

    if user_id do
      current_user = MyApp.Repo.get(MyApp.Task, user_id)
      assign(conn, :current_user, current_user)
    else
      conn
      |> put_status(401)
      |> json(%{})
      |> halt
    end
  end
end

роутер (отсюда вырезал некоторые детали):

defmodule MyApp.Router do
  use MyApp.Web, :router

  pipeline :api do
    plug :accepts, ["json-api"] # this line and 3 below are under JaSerializer package responsibility
    plug JaSerializer.ContentTypeNegotiation
    plug JaSerializer.Deserializer
    plug :fetch_session
    plug MyApp.Plug.Authenticate # this one
  end

  scope "/api", MyApp do
    pipe_through :api

    # tasks
    resources "/tasks", TaskController, only: [:show, :update]
  end
end

person asiniy    schedule 01.07.2016    source источник
comment
(Чтобы напечатать conn, используйте IO.inspect conn.)   -  person Dogbert    schedule 01.07.2016
comment
@Dogbert спасибо, я знаю :) Как решить мою проблему?   -  person asiniy    schedule 01.07.2016
comment
Можете ли вы опубликовать свой плагин полной аутентификации?   -  person TheAnh    schedule 01.07.2016
comment
@asiniy, не могли бы вы добавить и содержимое вашего router.ex? Не могли бы вы также указать фактический результат IO.inspect conn в вопросе? Текущий вывод крайне нечитабелен, так как он находится в одной строке. IO.inspect должен дать вам красиво оформленный вывод.   -  person Dogbert    schedule 01.07.2016
comment
@Dogbert, спасибо за IO.inspect подсказку. Добавлен роутер   -  person asiniy    schedule 01.07.2016
comment
извините за поздний ответ. Вы просто хотите проверить свою аутентификацию, верно?   -  person TheAnh    schedule 08.07.2016


Ответы (3)


Сейчас существует ...

Plug Test init_test_session / 2

conn = Plug.Test.init_test_session(conn, user_id: user.id)
person sweeneydavidj    schedule 13.04.2018

Решить эту проблему гораздо проще, если вообще пропустить сеансы в тестах. Идея состоит в том, чтобы назначить current_user непосредственно соединению в тестах и ​​в плагине аутентификации - пропустить выборку пользователя из сеанса, когда установлено current_user assign. Это, очевидно, оставляет сам плагин аутентификации непроверенным, но его тестирование должно быть намного проще, чем прохождение всего стека.

# in the authentication plug
def call(%{assigns: %{current_user: user}} = conn, opts) when user != nil do
  conn
end
def call(conn, opts) do
  # handle fetching user from session
end

Это позволяет вам просто выполнять assign(conn, :current_user, user) в тестах для проверки подлинности соединения.

person michalmuskala    schedule 03.07.2016
comment
Не думаю, что это хорошая идея. Кстати, я уже видел это решение. - person asiniy; 04.07.2016
comment
Я бы сказал, что это стандартный способ решения этой проблемы, который вы найдете в большинстве мест. Я бы ожидал, что прямое управление сеансом может не сработать так, как ожидалось, потому что сеансы обычно включают в себя обратный путь к серверу и обратно, чтобы вступить в силу. - person michalmuskala; 05.07.2016

Поскольку вы вызываете сеанс до fetch_session/2, поэтому в вашем плагине аутентификации get_session/2 вернет nil

Давайте изменим ваш плагин аутентификации, чтобы провести тест:

defmodule MyApp.Plug.Authenticate do
  import Plug.Conn
  import Phoenix.Controller
  alias MyApp.{Repo, User}

  def init(opts), do: opts

  def call(conn, _opts) do
    if user = get_user(conn) do
      assign(conn, :current_user, user)
    else
      conn
      |> put_status(401)
      |> put_flash(:error, "You must be logged in!")
      |> halt
    end
  end

  def get_user(conn) do
    case conn.assigns[:current_user] do
      nil ->
        case get_session(conn, :user_id) do
          id -> fetch_user(id)
          nil -> nil
        end
      user -> user
    end
  end

  defp fetch_user(id), do: Repo.get!(User, id)
end

Теперь вы можете проверить свою вилку следующим образом:

defmodule MyApp.Plug.AuthenticateTest do
  use ExUnit.Case, async: true
  use Plug.Test
  import Phoenix.ConnTest
  alias MyApp.Plug.Authenticate

  @endpoint MyApp.Endpoint

  @session  Plug.Session.init([
    store:            :cookie,
    key:              "_app",
    encryption_salt:  "secret",
    signing_salt:     "secret",
    encrypt:          false
  ])

  setup do
    user = MyApp.Factory.create(:user)

    conn = build_conn()
    |> put_req_header("accept", "application/vnd.api+json")
    |> put_req_header("content-type", "application/vnd.api+json")
    |> Map.put(:secret_key_base, String.duplicate("abcdefgh", 8))
    |> Plug.Session.call(@session)
    |> fetch_session
    |> put_session(:user_id, user.id)

    {:ok, conn: conn, user: user}
  end

  test "get_user returns where it is set in session", %{conn: conn, user: user} do
    assert Authenticate.get_user(conn) == user
  end
end

И, наконец, вы можете протестировать свой контроллер следующим образом:

setup do
    user = MyApp.Factory.create(:user)

    {:ok, user: user}
  end

  test "GET /login", %{user: user} do
    conn = build_conn()
    |> assign(:current_user, user)
    |> get("/login")

    assert html_response(conn, 200) =~ "Successfull login"
  end

Есть похожий вопрос вроде этого:

как я могу установить сеанс в настройке, когда я тестирую действие Phoenix, для которого требуется user_id в сеансе?

А лучший способ, когда вы хотите, чтобы пользователь вводил для теста, - это сохранить его в conn.private и прочитать его из приватного в вашем плагине аутентификации. Вы должны взглянуть, чтобы увидеть изменение. Надеюсь, это вам поможет!

person TheAnh    schedule 08.07.2016