はじめに
以前までのPhoenixに関する記事は以下の2つです。
もう少しPhoenixによるWebアプリケーション開発に慣れたいので、今回は実際にmixコマンドを用いてWebリソースを生成するための方法とコードレビュー、テスト実行まで纏めたものを整理します。
Phoenixのテンプレートをカスタマイズ - UI上でログインしているかどうか確認することができる -
まずはデフォルトのlayout/app.html.eex
をカスタマイズしてみましょう。
PhoenixのデフォルトのCSSフレームワークですが、milligramというものを使っている模様です。
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として分けたいため、別ページとして作成することにしました。
% mkdir shared
% vim 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)
(2)の参考文献を踏まえて考えました。今回は、Viewから実行するメソッドなためController側で行うよりもModel側で実装することに決めました。
def current_user(conn), do: Guardian.Plug.current_resource(conn, [])
def logged_in?(conn), do: Guardian.Plug.authenticated?(conn, [])
そしてこれをhelperメソッドとして扱えるようにview側でimportします。
def view do
quote do
import DailyReport.UserManager.Guardian, only: [current_user: 1, logged_in?: 1]
end
end
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モジュールにより、認証に関係する関数が提供されるため、上記2つの関数を使うことにしました。
雛形テンプレートの作成
ユーザの投稿記事機能を作る
ログインした後の最初のページは、自分が投稿した記事の一覧を閲覧できるようなページにしたいので、一旦User以外のリソース、投稿記事に対応するReportモデルを作ることとしました。
なお、PhoenixでもRailsのようにscaffoldのような雛形のテンプレートを作成することができます。
docker exec daily_report_web_1 mix phx.gen.html Articles Article article title:string body:text
また、User : Articlesの関係を1対N対応にしたいため、migrationファイルやモデル構造の書き換えを行います。
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
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
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
% 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
scope "/", DailyReportWeb do
pipe_through [:browser, :auth, :ensure_auth]
resources "/article", ArticleController
end
ログイン成功後、及びloginページに遷移した時にartice/index.htmlにリダイレクトできるように修正します。
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
以上で、ログイン時に投稿記事一覧にリダイレクトすることが可能となりました。
TravisCIの設定
雛形テンプレート作成を学んだので、ここから本格的にコードレビューやテスト実行をCIサービスで行えるようにしていきます。CIサービスはパブリックリポジトリであれば無料で使えるTravis CIを利用します。
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ファイルも生成しました。
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)とdialyxir
(6)を利用しました。この辺りは機械的でも良いからレビュー案が欲しいので詳しく調べるのは一旦省略。
なお、ここの設定部分に関しては(7)
を参考にしました。
CIによるテスト実行
前述のコードレビューと同時に、CIによるテストも行います。
テストのテンプレートに関してはphx.gen.html
タスクコマンドで概ねなものは作ってくれます。
ただし、articleページに関しては、Guardianによるログイン認証を先に行わないと閲覧できないステートにしているため、テストでも予めアクセスする前にユーザログインを済ましておく必要があります。
Phoenixのテストでは、Tag付けによって、条件分岐をすることができるので、conn_case.exを(9)のようにカスタマイズすることで、sign_inしたい時とそうでない時で分岐をすることとしました。
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の挙動を付与することができます。
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なテストを設定すればテストがすべてパスします。
まとめ
まだ、本格的にElixirのコードを書いた訳じゃありませんが、一旦これでPhoenixによるWeb開発の設定やテンプレート作りは一先ず終わったような気がします。
ここからは、本格的にゴリゴリElixirのコードを書いていき、まとめたものを執筆していこうと思います。
それでは。
参考文献
- https://ruby-rails.hatenadiary.com/entry/20151013/1444662887
- https://nabinno.github.io/f/2018/05/20/%E9%80%A3%E8%BC%89_rails2phoenix_2.html
- https://hexdocs.pm/guardian/Guardian.Plug.html
- https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Html.html
- https://github.com/rrrene/credo
- https://github.com/jeremyjh/dialyxir
- https://medium.com/@devenney/phoenix-framework-travis-ci-a46a0dbfecd7
- https://qiita.com/ak-ymst/items/1a973dd4b21c3d5ea4e1
- https://medium.com/@simon.strom/how-to-test-controller-authenticated-by-guardian-in-elixir-phoenix-b9bfa141ed4