PhonenixによるWeb開発 - 雛形テンプレート作りとCIによるコードレビューとテスト実行まで -

## はじめに

以前までのPhoenixに関する記事は以下の２つです。

* [PhoenixによるWeb開発 - Guardianによるログイン認証の実装まで -](https://qiita.com/himrock922/items/eafd03d4880f9ec55272)
* [https://qiita.com/himrock922/items/80f33046239a2d178e7e](https://qiita.com/himrock922/items/80f33046239a2d178e7e)

もう少しPhoenixによるWebアプリケーション開発に慣れたいので、今回は実際にmixコマンドを用いてWebリソースを生成するための方法とコードレビュー、テスト実行まで纏めたものを整理します。

## Phoenixのテンプレートをカスタマイズ - UI上でログインしているかどうか確認することができる -

まずはデフォルトの`layout/app.html.eex`をカスタマイズしてみましょう。

PhoenixのデフォルトのCSSフレームワークですが、[milligram](https://milligram.io/)というものを使っている模様です。

layout/app.html.eexのヘッダ部分を変えてみます。

```exs:layout/app.html.eex
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>DailyReport · Phoenix Framework</title>
    <link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
  </head>
  <body>
    <header>
      <%= render DailyReportWeb.SharedView, "header.html", assigns %>
    </header>
    <main role="main" class="container">
      <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
      <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
      <%= render @view_module, @view_template, assigns %>
    </main>
    <script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
  </body>
</html>
```

ヘッダー部分をpartialとして分けたいため、別ページとして作成することにしました。

```bash
% mkdir shared
% vim shared/header.html.eex
```

```eex:shared/header.html.eex
<section class="container">
  <nav role="navigation">
    <ul>
      <%= if logged_in?(@conn) do %>
        <li><a class="button" href="<%= Routes.session_path(@conn, :logout) %>">ログアウト</a></li>
      <% end %>
    </ul>
  </nav>
</section>
```

header.html.eexの中でlogged_inというメソッドを用意します。中身は自分で作成しますが、logged_inは現在ログイン中かどうか確認するためのメソッドの予定です。
さて、実際の中身をどうするかについては、[(1)](https://ruby-rails.hatenadiary.com/entry/20151013/1444662887)
[(2)](https://nabinno.github.io/f/2018/05/20/%E9%80%A3%E8%BC%89_rails2phoenix_2.html)の参考文献を踏まえて考えました。今回は、Viewから実行するメソッドなためController側で行うよりもModel側で実装することに決めました。

```elixir:lib/daily_report/user_manager/guardian.ex
  def current_user(conn), do: Guardian.Plug.current_resource(conn, [])

  def logged_in?(conn), do: Guardian.Plug.authenticated?(conn, [])
```

そしてこれをhelperメソッドとして扱えるようにview側でimportします。

```elixir:lib/daily_report_web.ex
  def view do
    quote do
      import DailyReport.UserManager.Guardian, only: [current_user: 1, logged_in?: 1]
    end
  end
```

```elixir:lib/daily_report/user_manager/guardian.ex
defmodule DailyReport.UserManager.Guardian do
  def current_user(conn), do: Guardian.Plug.current_resource(conn, [])

  def logged_in?(conn), do: Guardian.Plug.authenticated?(conn, [])
end
```

Guardian.Plugモジュールにより、認証に関係する関数が提供されるため、上記２つの関数を使うことにしました。

## 雛形テンプレートの作成

### ユーザの投稿記事機能を作る

ログインした後の最初のページは、自分が投稿した記事の一覧を閲覧できるようなページにしたいので、一旦User以外のリソース、投稿記事に対応するReportモデルを作ることとしました。
なお、PhoenixでもRailsのようにscaffoldのような雛形のテンプレートを作成することができます。

```bash
docker exec daily_report_web_1 mix phx.gen.html Articles Article article title:string body:text
```

また、User : Articlesの関係を1対N対応にしたいため、migrationファイルやモデル構造の書き換えを行います。

```elixir:lib/daily_report/articles/article.ex
defmodule DailyReport.Articles.Article do
  use Ecto.Schema
  import Ecto.Changeset

  schema "article" do
    field :body, :string
    field :title, :string
    belongs_to :user, DailyReport.UserManager.User
    timestamps()
  end

  @doc false
  def changeset(article, attrs) do
    article
    |> cast(attrs, [:title, :body])
    |> validate_required([:title, :body])
  end
end
```

```elixir:lib/daily_report/user_manager/user.ex
defmodule DailyReport.UserManager.User do
  use Ecto.Schema
  import Ecto.Changeset
  alias DailyReport.UserManager.User

  schema "users" do
    field :password, :string
    field :username, :string

    timestamps()
    has_many :articles, DailyReport.Articles.Article
  end
```

```elixir:priv/repo/migrations/20200222064817_create_article.exs
defmodule DailyReport.Repo.Migrations.CreateArticle do
  use Ecto.Migration

  def change do
    create table(:article) do
      add :title, :string
      add :body, :text
      add :user_id, references(:users)
      timestamps()
    end

  end
end
```


```bash
% docker exec daily_report_web_1 mix ecto.migrate                                      (git)-[feature/add_daily_report]

06:56:20.864 [info]  == Running 20200222064817 DailyReport.Repo.Migrations.CreateArticle.change/0 forward

06:56:20.898 [info]  create table article

06:56:20.982 [info]  == Migrated 20200222064817 in 0.0s
```

```elixir:lib/daily_report_web/router.ex
  scope "/", DailyReportWeb do
    pipe_through [:browser, :auth, :ensure_auth]
    resources "/article", ArticleController
  end
```

ログイン成功後、及びloginページに遷移した時にartice/index.htmlにリダイレクトできるように修正します。

```elixir:lib/daily_report_web/controllers/session_controller.ex

  def new(conn, _) do
    changeset = UserManager.change_user(%User{})
    maybe_user = Guardian.Plug.current_resource(conn)
    if maybe_user do
      redirect(conn, to: "/article")
    else
      render(conn, "new.html", changeset: changeset, action: Routes.session_path(conn, :login))
    end
  end

  defp login_reply({:ok, user}, conn) do
    conn
    |> put_flash(:info, "DailyReportにログインしました!")
    |> Guardian.Plug.sign_in(user)
    |> redirect(to: "/article")
  end
```

以上で、ログイン時に投稿記事一覧にリダイレクトすることが可能となりました。

![投稿記事一覧.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144991/7781ec21-4701-49c7-b8e6-bb19f6c206a2.png)

## TravisCIの設定

雛形テンプレート作成を学んだので、ここから本格的にコードレビューやテスト実行をCIサービスで行えるようにしていきます。CIサービスはパブリックリポジトリであれば無料で使えるTravis CIを利用します。

```yaml:.travis.yml
language: elixir
sudo: false
otp_release:
  - 22.2.1
elixir:
  - 1.9.4
services:
  - postgresql
addons:
  postgresql: "10"
  apt:
    packages:
    - postgresql-10
    - postgresql-client-10
env:
  global:
  - PGPORT=5432
  - MIX_ENV=test
cache:
  directories:
    - _build
    - deps
before_script:
  - psql -c 'create database travis_ci_test;' -U postgres
  - cp config/travis.exs config/test.exs
  - mix do ecto.create, ecto.migrate
script:
  - mix credo --strict
  - mix dialyzer --halt-exit-status
  - mix test
```

今回はDBの設定も必要となるので、必要に応じてpostgresqlに接続するconfigファイルも生成しました。

```elixir:config/travis.exs
use Mix.Config

# Configure your database
config :daily_report, DailyReport.Repo,
  username: "postgres",
  password: "",
  database: "travis_ci_test",
  hostname: "localhost",
  pool: Ecto.Adapters.SQL.Sandbox

# We don't run a server during test. If one is required,
# you can enable the server option below.
config :daily_report, DailyReportWeb.Endpoint,
  http: [port: 5432],
  server: false

# Print only warnings and errors during test
config :logger, level: :warn
```

### CIによるコードレビュー

コードレビュー関係のツールには`credo`[(5)](https://github.com/rrrene/credo)と`dialyxir`[(6)](https://github.com/jeremyjh/dialyxir)を利用しました。この辺りは機械的でも良いからレビュー案が欲しいので詳しく調べるのは一旦省略。
なお、ここの設定部分に関しては[(7)](https://qiita.com/ak-ymst/items/1a973dd4b21c3d5ea4e1)
を参考にしました。

### CIによるテスト実行

前述のコードレビューと同時に、CIによるテストも行います。
テストのテンプレートに関しては`phx.gen.html`タスクコマンドで概ねなものは作ってくれます。
ただし、articleページに関しては、Guardianによるログイン認証を先に行わないと閲覧できないステートにしているため、テストでも予めアクセスする前にユーザログインを済ましておく必要があります。

Phoenixのテストでは、Tag付けによって、条件分岐をすることができるので、conn_case.exを[(9)](https://medium.com/@simon.strom/how-to-test-controller-authenticated-by-guardian-in-elixir-phoenix-b9bfa141ed4)のようにカスタマイズすることで、sign_inしたい時とそうでない時で分岐をすることとしました。

```elixir:test/support/conn_case.ex
defmodule DailyReportWeb.ConnCase do
  @moduledoc """
  This module defines the test case to be used by
  tests that require setting up a connection.

  Such tests rely on `Phoenix.ConnTest` and also
  import other functionality to make it easier
  to build common data structures and query the data layer.

  Finally, if the test case interacts with the database,
  we enable the SQL sandbox, so changes done to the database
  are reverted at the end of every test. If you are using
  PostgreSQL, you can even run database tests asynchronously
  by setting `use DailyReportWeb.ConnCase, async: true`, although
  this option is not recommendded for other databases.
  """
  use ExUnit.CaseTemplate
  alias DailyReport.{UserManager, UserManager.Guardian}

  using do
    quote do
      # Import conveniences for testing with connections
      use Phoenix.ConnTest
      alias DailyReportWeb.Router.Helpers, as: Routes
      # The default endpoint for testing
      @endpoint DailyReportWeb.Endpoint
    end
  end

  @default_opts [
    store: :cookie,
    key: "secretkey",
    encryption_salt: "encrypted cookie salt",
    signing_salt: "signing salt"
  ]
  @signing_opts Plug.Session.init(Keyword.put(@default_opts, :encrypt, false))
  @valid_user %{password: "some password", username: "some username"}

  setup tags do
    :ok = Ecto.Adapters.SQL.Sandbox.checkout(DailyReport.Repo)

    unless tags[:async] do
      Ecto.Adapters.SQL.Sandbox.mode(DailyReport.Repo, {:shared, self()})
    end

    {conn} = if tags[:authenticated] do
      {:ok, user} = UserManager.create_user(@valid_user)

      conn = Phoenix.ConnTest.build_conn()
      |> Plug.Session.call(@signing_opts)
      |> Plug.Conn.fetch_session
      |> Guardian.Plug.sign_in(user)
      {conn}
    else
      {Phoenix.ConnTest.build_conn()}
    end
  
    {:ok, conn: conn}
  end
end
```

このタグ名を`test`シナリオを実行する前にタグ付けすることで、sign_inの挙動を付与することができます。

```elixir:test/controllers/article_controller_test.exs
defmodule DailyReportWeb.ArticleControllerTest do
  use DailyReportWeb.ConnCase
  alias DailyReport.Articles
  @create_attrs %{body: "some body", title: "some title"}
  @update_attrs %{body: "some updated body", title: "some updated title"}
  @invalid_attrs %{body: nil, title: nil}
  def fixture(:article) do
    {:ok, article} = Articles.create_article(@create_attrs)
    article
  end

  describe "index" do
    @tag :authenticated
    test "lists all article", %{conn: conn} do
      conn = get(conn, Routes.article_path(conn, :index))
      assert html_response(conn, 200) =~ "Listing Article"
    end
  end

  describe "new article" do
    @tag :authenticated
    test "renders form", %{conn: conn} do
      conn = get(conn, Routes.article_path(conn, :new))
      assert html_response(conn, 200) =~ "New Article"
    end
  end

  describe "create article" do
    @tag :authenticated
    test "redirects to show when data is valid", %{conn: conn} do
      conn = post(conn, Routes.article_path(conn, :create), article: @create_attrs)

      assert %{id: id} = redirected_params(conn)
      assert redirected_to(conn) == Routes.article_path(conn, :show, id)

      conn = get(conn, Routes.article_path(conn, :show, id))
      assert html_response(conn, 200) =~ "Show Article"
    end

    @tag :authenticated
    test "renders errors when data is invalid", %{conn: conn} do
      conn = post(conn, Routes.article_path(conn, :create), article: @invalid_attrs)
      assert html_response(conn, 200) =~ "New Article"
    end
  end

  describe "edit article" do
    setup [:create_article]
    @tag :authenticated
    test "renders form for editing chosen article", %{conn: conn, article: article} do
      conn = get(conn, Routes.article_path(conn, :edit, article))
      assert html_response(conn, 200) =~ "Edit Article"
    end
  end

  describe "update article" do
    setup [:create_article]
    @tag :authenticated
    test "redirects when data is valid", %{conn: conn, article: article} do
      conn = put(conn, Routes.article_path(conn, :update, article), article: @update_attrs)
      assert redirected_to(conn) == Routes.article_path(conn, :show, article)

      conn = get(conn, Routes.article_path(conn, :show, article))
      assert html_response(conn, 200) =~ "some updated body"
    end

    @tag :authenticated
    test "renders errors when data is invalid", %{conn: conn, article: article} do
      conn = put(conn, Routes.article_path(conn, :update, article), article: @invalid_attrs)
      assert html_response(conn, 200) =~ "Edit Article"
    end
  end

  describe "delete article" do
    setup [:create_article]
    @tag :authenticated
    test "deletes chosen article", %{conn: conn, article: article} do
      conn = delete(conn, Routes.article_path(conn, :delete, article))
      assert redirected_to(conn) == Routes.article_path(conn, :index)
      assert_error_sent 404, fn ->
        get(conn, Routes.article_path(conn, :show, article))
      end
    end
  end

  defp create_article(_) do
    article = fixture(:article)
    {:ok, article: article}
  end
end
```

以上やcredo,dialyzerの設定でignoreなテストを設定すればテストがすべてパスします。
![スクリーンショット 2020-02-24 22.13.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144991/99115ec9-a3ed-6c66-bf71-d13115cb1469.png)

## まとめ

まだ、本格的にElixirのコードを書いた訳じゃありませんが、一旦これでPhoenixによるWeb開発の設定やテンプレート作りは一先ず終わったような気がします。
ここからは、本格的にゴリゴリElixirのコードを書いていき、まとめたものを執筆していこうと思います。
それでは。

## 参考文献

1. [https://ruby-rails.hatenadiary.com/entry/20151013/1444662887](https://ruby-rails.hatenadiary.com/entry/20151013/1444662887)
2. [https://nabinno.github.io/f/2018/05/20/%E9%80%A3%E8%BC%89_rails2phoenix_2.html](https://nabinno.github.io/f/2018/05/20/%E9%80%A3%E8%BC%89_rails2phoenix_2.html)
3. [https://hexdocs.pm/guardian/Guardian.Plug.html](https://hexdocs.pm/guardian/Guardian.Plug.html)
4. [https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Html.html](https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Html.html)
5. [https://github.com/rrrene/credo](https://github.com/rrrene/credo)
6. [https://github.com/jeremyjh/dialyxir](https://github.com/jeremyjh/dialyxir)
7. [https://medium.com/@devenney/phoenix-framework-travis-ci-a46a0dbfecd7](https://medium.com/@devenney/phoenix-framework-travis-ci-a46a0dbfecd7)
8. [https://qiita.com/ak-ymst/items/1a973dd4b21c3d5ea4e1](https://qiita.com/ak-ymst/items/1a973dd4b21c3d5ea4e1)
9. [https://medium.com/@simon.strom/how-to-test-controller-authenticated-by-guardian-in-elixir-phoenix-b9bfa141ed4](https://medium.com/@simon.strom/how-to-test-controller-authenticated-by-guardian-in-elixir-phoenix-b9bfa141ed4)