PhoenixとElixirを使用してブログエンジンを作成する/パート2。





翻訳者から:「 エリクサーとフェニックスは、最新のウェブ開発がどこに進んでいるかの良い例です。 すでにこれらのツールは、Webアプリケーションのリアルタイムテクノロジーへの質の高いアクセスを提供します。 対話性が向上したサイト、マルチユーザーブラウザーゲーム、マイクロサービスは、これらの技術がうまく機能する分野です。 以下は、Phoenixフレームワークでの開発の詳細な側面を説明する一連の11の記事の翻訳です。ブログエンジンのような些細なことのように思えます。 しかし、急いでつまずかないでください。特に記事がエリクサーに注意を払うか、彼のフォロワーになるように促す場合、それは本当に面白いでしょう。



このパートでは、ブログの基礎を完成させ、テストをさらに掘り下げ、最終的に承認を追加します。 少し遅れて申し訳ありませんが、明確なスケジュールを厳守するか、スケジュールを早めます。



現時点では、アプリケーションは以下に基づいています。





いくつかのバグを修正します



最初の部分を実行した場合、Elixir / Phoenixで動作するブログエンジンがある程度機能するはずです。 あなたが私のような人であれば、そのような一見小さな仕事でも興奮し、より速く前進するようになり、コードをさらに磨きたいという欲求を引き起こします。



作業の進捗状況を追跡する場合は、 Githubのリポジトリにすべてのコードをアップロードしました。



最初のバグは、 http:// localhost:4000 / sessions / newのアドレスに移動し、[ 送信 ]ボタンをクリックすることで簡単に再現できます。 次のようなエラーメッセージが表示されます。



nil given for :username, comparison with nil is forbidden as it always evaluates to false. Pass a full query expression and use is_nil/1 instead.
      
      





SessionControllerの create関数を見ると、何が問題なのかすぐにわかります。



 def create(conn, %{"user" => user_params}) do user = Repo.get_by(User, username: user_params["username"]) user |> sign_in(user_params["password"], conn) end
      
      





そのため、 ユーザー名の代わりに空の値を含む(または何も含まない)文字列をパラメーターで送信すると、エラーが発生します。 すぐに修正しましょう。 幸いなことに、これは、 guard節パターンマッチングを使用して簡単に実行できます。 現在の作成関数を次のものに置き換えます。



 def create(conn, %{"user" => %{"username" => username, "password" => password}}) when not is_nil(username) and not is_nil(password) do user = Repo.get_by(User, username: username) sign_in(user, password, conn) end def create(conn, _) do failed_login(conn) end
      
      





2番目の作成関数のparams引数をアンダースコアに置き換えます。どこでも使用する必要がないためです。 また、 failed_login関数も参照します。これはプライベートとして追加する必要があります。 web / controllers / session_controller.exファイルで、Comeoninインポートを変更します。



 import Comeonin.Bcrypt, only: [checkpw: 2, dummy_checkpw: 0]
      
      





誰もユーザーを列挙するだけではタイムアタックを開始できないように、 dummy_checkpw()を呼び出す必要があります。 次に、 failed_login関数を追加します。



 defp failed_login(conn) do dummy_checkpw() conn |> put_session(:current_user, nil) |> put_flash(:error, "Invalid username/password combination!") |> redirect(to: page_path(conn, :index)) |> halt() end
      
      





繰り返しますが、上部のdummy_checkpw()の呼び出しに注意してください ! また、 current_userセッションをクリアし、ユーザーに間違ったログインとパスワードを知らせるフラッシュメッセージを設定し、メインページにリダイレクトします。 最後に、 halt関数を呼び出します。これは、ダブルレンダリングの問題に対する合理的な防御です。 そして、すべての同様のコードを新しい関数の呼び出しに置き換えます。



 defp sign_in(user, _password, conn) when is_nil(user) do failed_login(conn) end defp sign_in(user, password, conn) do if checkpw(password, user.password_digest) do conn |> put_session(:current_user, %{id: user.id, username: user.username}) |> put_flash(:info, "Sign in successful!") |> redirect(to: page_path(conn, :index)) else failed_login(conn) end end
      
      





これらの編集は、既存のすべての奇妙なログインバグを処理する必要があります。これにより、追加したユーザーに投稿を関連付けることができます。



移行を追加



最初に、 投稿テーブルのユーザーテーブルへのリンクを追加します。 これを行うには、 ecto-generatorを使用して、移行を作成します。



 $ mix ecto.gen.migration add_user_id_to_posts
      
      





結論:



 Compiling 1 file (.ex) * creating priv/repo/migrations * creating priv/repo/migrations/20160720211140_add_user_id_to_posts.exs
      
      





作成したファイルを開くと、その中には何も表示されません。 したがって、次のコードを変更関数に追加します。



 def change do alter table(:posts) do add :user_id, references(:users) end create index(:posts, [:user_id]) end
      
      





これにより、ユーザーテーブルを参照するuser_id列とそのインデックスが追加されますmix ecto.migrate



を実行し、モデルの編集を開始します。



投稿とユーザーを結び付けます



web / models / post.exファイルを開いて、 ユーザーモデルへのリンクを追加しましょう。 投稿スキーム内に、次の行を配置します。



 belongs_to :user, Pxblog.User
      
      





Postモデルを指すフィードバックをUserモデルに追加する必要があります。 web / models / user.exファイルusersスキーマ内に、次の行を配置します。



 has_many :posts, Pxblog.Post
      
      





また、 投稿コントローラーを開き、投稿をユーザーに直接関連付ける必要があります。



方法を変える



ユーザー内の投稿を指定してルーターを更新することから始めましょう。 これを行うには、 web / router.exファイルを開き、パス/ユーザー/投稿を次のように置き換えます



 resources "/users", UserController do resources "/posts", PostController end
      
      





コントローラーを修正します



mix phoenix.routes



を今すぐ実行しようとすると、エラーが発生します。 これが標準です! パスの構造を変更したため、 post_pathヘルパーは失われました。この新しいバージョンはuser_post_pathと呼ばれ、接続されたリソースを参照します。 ネストされたヘルパーを使用すると、別のリソースを必要とするリソース(投稿にユーザーが必要など)で表されるパスにアクセスできます。



したがって、通常のpost_pathヘルパーがある場合、次のように呼び出します。



 post_path(conn, :show, post)
      
      





connオブジェクトは接続オブジェクト、atom :showは参照しているアクションです。3番目の引数はモデルまたはオブジェクト識別子のいずれかです。 ここから、そうする機会があります。



 post_path(conn, :show, 1)
      
      





同時に、ネストされたリソースがある場合、ヘルパーはrouteファイルの変更とともに変更されます。 私たちの場合:



 user_post_path(conn, :show, user, post)
      
      





3番目の引数は外部リソースを表し、ネストされた各引数が次に来ることに注意してください。



エラーが発生する理由がわかったので、エラーを修正できます。 各コントローラーアクションで、要求されたユーザーにアクセスする必要があります。 それを取得する最良の方法は、プラグインを使用することです。 これを行うには、 web / controllers / post_controller.exファイルを開き、一番上の新しいプラグインへの呼び出しを追加します。



 plug :assign_user
      
      





そして、それを少し下に書きます。



 defp assign_user(conn, _opts) do case conn.params do %{"user_id" => user_id} -> user = Repo.get(Pxblog.User, user_id) assign(conn, :user, user) _ -> conn end end
      
      





そして、どこでもpost_pathuser_post_pathに置き換えます



 def create(conn, %{"post" => post_params}) do changeset = Post.changeset(%Post{}, post_params) case Repo.insert(changeset) do {:ok, _post} -> conn |> put_flash(:info, "Post created successfully.") |> redirect(to: user_post_path(conn, :index, conn.assigns[:user])) {:error, changeset} -> render(conn, "new.html", changeset: changeset) end end def update(conn, %{"id" => id, "post" => post_params}) do post = Repo.get!(Post, id) changeset = Post.changeset(post, post_params) case Repo.update(changeset) do {:ok, post} -> conn |> put_flash(:info, "Post updated successfully.") |> redirect(to: user_post_path(conn, :show, conn.assigns[:user], post)) {:error, changeset} -> render(conn, "edit.html", post: post, changeset: changeset) end end def delete(conn, %{"id" => id}) do post = Repo.get!(Post, id) #    delete! (  ),     #      (  ). Repo.delete!(post) conn |> put_flash(:info, "Post deleted successfully.") |> redirect(to: user_post_path(conn, :index, conn.assigns[:user])) end
      
      





テンプレートを整理する



コントローラーがエラーメッセージの出力を停止したので、今度はテンプレートを作成します。 任意のコントローラーアクションからアクセスできるプラグインを実装することで、短い道を歩みました。 接続オブジェクトでassign関数を使用して、テンプレートで使用できる変数を定義します。 次に、テンプレートを少し変更して、post_pathヘルパーをuser_post_pathに置き換え、アクション名の後の次の引数がユーザー識別子であることを確認します。 web / templates / post / index.html.eexファイルで、次のように書きます:



 <h2>Listing posts</h2> <table class="table"> <thead> <tr> <th>Title</th> <th>Body</th> <th></th> </tr> </thead> <tbody> <%= for post <- @posts do %> <tr> <td><%= post.title %></td> <td><%= post.body %></td> <td class="text-right"> <%= link "Show", to: user_post_path(@conn, :show, @user, post), class: "btn btn-default btn-xs" %> <%= link "Edit", to: user_post_path(@conn, :edit, @user, post), class: "btn btn-default btn-xs" %> <%= link "Delete", to: user_post_path(@conn, :delete, @user, post), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger btn-xs" %> </td> </tr> <% end %> </tbody> </table> <%= link "New post", to: user_post_path(@conn, :new, @user) %>
      
      





web / templates / post / show.html.eexファイルで:



 <h2>Show post</h2> <ul> <li> <strong>Title:</strong> <%= @post.title %> </li> <li> <strong>Body:</strong> <%= @post.body %> </li> </ul> <%= link "Edit", to: user_post_path(@conn, :edit, @user, @post) %> <%= link "Back", to: user_post_path(@conn, :index, @user) %>
      
      





web / templates / post / new.html.eexファイルで



 <h2>New post</h2> <%= render "form.html", changeset: @changeset, action: user_post_path(@conn, :create, @user) %> <%= link "Back", to: user_post_path(@conn, :index, @user) %>
      
      





web / templates / post / edit.html.eexファイルで



 <h2>Edit post</h2> <%= render "form.html", changeset: @changeset, action: user_post_path(@conn, :update, @user, @post) %> <%= link "Back", to: user_post_path(@conn, :index, @user) %>
      
      





さて、ヘルスチェックとして、 mix phoenix.routes



を実行すると、パスの出力とコンパイルの成功が表示されるはずです!



 Compiling 14 files (.ex) page_path GET / Pxblog.PageController :index user_path GET /users Pxblog.UserController :index user_path GET /users/:id/edit Pxblog.UserController :edit user_path GET /users/new Pxblog.UserController :new user_path GET /users/:id Pxblog.UserController :show user_path POST /users Pxblog.UserController :create user_path PATCH /users/:id Pxblog.UserController :update PUT /users/:id Pxblog.UserController :update user_path DELETE /users/:id Pxblog.UserController :delete user_post_path GET /users/:user_id/posts Pxblog.PostController :index user_post_path GET /users/:user_id/posts/:id/edit Pxblog.PostController :edit user_post_path GET /users/:user_id/posts/new Pxblog.PostController :new user_post_path GET /users/:user_id/posts/:id Pxblog.PostController :show user_post_path POST /users/:user_id/posts Pxblog.PostController :create user_post_path PATCH /users/:user_id/posts/:id Pxblog.PostController :update PUT /users/:user_id/posts/:id Pxblog.PostController :update user_post_path DELETE /users/:user_id/posts/:id Pxblog.PostController :delete session_path GET /sessions/new Pxblog.SessionController :new session_path POST /sessions Pxblog.SessionController :create session_path DELETE /sessions/:id Pxblog.SessionController :delete
      
      





残りの部品をコントローラーに接続します



これで、必要なのは、新しい関連付けを使用するためにコントローラーで作業を完了することだけです。 iex -S mix



コマンドを使用して対話型コンソールを起動し、ユーザーの投稿を選択する方法について少し学習します。 ただし、その前に、iexコンソールがプロジェクト内に読み込まれるたびに読み込まれる標準のインポート/エイリアスのリストを設定する必要があります。 プロジェクトルートに新しい.iex.exsファイルを作成し(ファイル名の先頭にあるドットに注意してください)、次の内容を入力します。



 import Ecto.Query alias Pxblog.User alias Pxblog.Post alias Pxblog.Repo import Ecto
      
      





今、iexを起動するとき、毎回このようなことをする必要はありません:



 iex(1)> import Ecto.Query nil iex(2)> alias Pxblog.User nil iex(3)> alias Pxblog.Post nil iex(4)> alias Pxblog.Repo nil iex(5)> import Ecto nil
      
      





次に、リポジトリに少なくとも1人のユーザーが必要です。 そうでない場合は、追加します。 その後、次を実行できます。



 iex(8)> user = Repo.get(User, 1) [debug] SELECT u0."id", u0."username", u0."email", u0."password_digest", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [1] OK query=8.2ms %Pxblog.User{__meta__: #Ecto.Schema.Metadata<:loaded>, email: "test", id: 1, inserted_at: #Ecto.DateTime<2015-10-06T17:47:07Z>, password: nil, password_confirmation: nil, password_digest: "$2b$12$pV/XBBCRl0RQhadQd9Y4mevOy5y0j4bCC/LjGgx7VJMosRdwme22a", posts: #Ecto.Association.NotLoaded<association :posts is not loaded>, updated_at: #Ecto.DateTime<2015-10-06T17:47:07Z>, username: "test"} iex(10)> Repo.all(assoc(user, :posts)) [debug] SELECT p0."id", p0."title", p0."body", p0."user_id", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 WHERE (p0."user_id" IN ($1)) [1] OK query=3.5ms []
      
      





これまでのところ、このユーザーの投稿を1つも作成していないため、ここで空のリストを取得するのが論理的です。 Ectoのassoc関数を使用して、投稿をユーザーにリンクするリクエストを取得しました。 次のこともできます。



 iex(14)> Repo.all from p in Post, ...(14)> join: u in assoc(p, :user), ...(14)> select: p [debug] SELECT p0."id", p0."title", p0."body", p0."user_id", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 INNER JOIN "users" AS u1 ON u1."id" = p0."user_id" [] OK query=0.9ms
      
      





ここでは、ユーザーIDで選択するための直接条件ではなく、内部結合を使用して要求が作成されます。 両方の場合に生成されるクエリがどのように見えるかに特に注意してください。 クエリを生成するコードを操作するときはいつでも、「舞台裏」で作成されるSQLを理解することは非常に役立ちます。



以下に示すように、ユーザーをプリロードするために投稿を取得するときにプリロード機能を使用することもできます。



 iex(18)> Repo.all(from u in User, preload: [:posts]) [debug] SELECT u0."id", u0."username", u0."email", u0."password_digest", u0."inserted_at", u0."updated_at" FROM "users" AS u0 [] OK query=0.9ms [debug] SELECT p0."id", p0."title", p0."body", p0."user_id", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 WHERE (p0."user_id" IN ($1)) ORDER BY p0."user_id" [1] OK query=0.8ms iex(20)> Repo.all(from p in Post, preload: [:user]) [debug] SELECT p0."id", p0."title", p0."body", p0."user_id", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 [] OK query=0.8ms []
      
      





リクエストに手を加えることができるように、投稿を追加する必要があります。 そのため、このためにbuild_assocと呼ばれるEcto関数を使用します。 この関数は、アソシエーションを追加したいモデルの最初の引数と、アトムの形のアソシエーション自体を取ります。



 iex(1)> user = Repo.get(User, 1) iex(2)> post = build_assoc(user, :posts, %{title: "Test Title", body: "Test Body"}) iex(3)> Repo.insert(post) iex(4)> posts = Repo.all(from p in Post, preload: [:user])
      
      





そして今、最後のリクエストを完了したら、次の出力を取得する必要があります。



 iex(4)> posts = Repo.all(from p in Post, preload: [:user]) [debug] SELECT p0."id", p0."title", p0."body", p0."user_id", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 [] OK query=0.7ms [debug] SELECT u0."id", u0."username", u0."email", u0."password_digest", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" IN ($1)) [1] OK query=0.7ms [%Pxblog.Post{__meta__: #Ecto.Schema.Metadata<:loaded>, body: "Test Body", id: 1, inserted_at: #Ecto.DateTime<2015-10-06T18:06:20Z>, title: "Test Title", updated_at: #Ecto.DateTime<2015-10-06T18:06:20Z>, user: %Pxblog.User{__meta__: #Ecto.Schema.Metadata<:loaded>, email: "test", id: 1, inserted_at: #Ecto.DateTime<2015-10-06T17:47:07Z>, password: nil, password_confirmation: nil, password_digest: "$2b$12$pV/XBBCRl0RQhadQd9Y4mevOy5y0j4bCC/LjGgx7VJMosRdwme22a", posts: #Ecto.Association.NotLoaded<association :posts is not loaded>, updated_at: #Ecto.DateTime<2015-10-06T17:47:07Z>, username: "test"}, user_id: 1}]
      
      





そして、最初の結果をすばやく確認します。



 iex(5)> post = List.first posts %Pxblog.Post{__meta__: #Ecto.Schema.Metadata<:loaded>, body: "Test Body", id: 1, inserted_at: #Ecto.DateTime<2015-10-06T18:06:20Z>, title: "Test Title", updated_at: #Ecto.DateTime<2015-10-06T18:06:20Z>, user: %Pxblog.User{__meta__: #Ecto.Schema.Metadata<:loaded>, email: "test", id: 1, inserted_at: #Ecto.DateTime<2015-10-06T17:47:07Z>, password: nil, password_confirmation: nil, password_digest: "$2b$12$pV/XBBCRl0RQhadQd9Y4mevOy5y0j4bCC/LjGgx7VJMosRdwme22a", posts: #Ecto.Association.NotLoaded<association :posts is not loaded>, updated_at: #Ecto.DateTime<2015-10-06T17:47:07Z>, username: "test"}, user_id: 1} iex(6)> post.title "Test Title" iex(7)> post.user.username "test"
      
      





かっこいい! 私たちの実験は私たちが期待したものを正確に示したので、コントローラー(ファイルweb / controllers / post_controller.ex )に戻ってコードの編集を開始ます。 インデックスアクションでは、ユーザーに関連するすべての投稿を取得します。 それから始めましょう:



 def index(conn, _params) do posts = Repo.all(assoc(conn.assigns[:user], :posts)) render(conn, "index.html", posts: posts) end
      
      





これで、最初のユーザーの投稿のリストを見ることができます! しかし、存在しないユーザーの投稿のリストを取得しようとすると、UXが悪いというエラーメッセージが表示されるので、 assign_userプラグインを整理しましょう。



 defp assign_user(conn, _opts) do case conn.params do %{"user_id" => user_id} -> case Repo.get(Pxblog.User, user_id) do nil -> invalid_user(conn) user -> assign(conn, :user, user) end _ -> invalid_user(conn) end end defp invalid_user(conn) do conn |> put_flash(:error, "Invalid user!") |> redirect(to: page_path(conn, :index)) |> halt end
      
      





これで、存在しないユーザーの投稿のリストを開くと、素敵なフラッシュメッセージが表示され、親切にpage_pathにリダイレクトされます 。 次に、 新しいアクションを変更する必要があります。



 def new(conn, _params) do changeset = conn.assigns[:user] |> build_assoc(:posts) |> Post.changeset() render(conn, "new.html", changeset: changeset) end
      
      





ユーザーモデルを取得し、それをbuild_assoc関数に渡し、投稿を作成する必要があると言ってから、結果の空のモデルをPost.changeset関数に渡して空のリビジョンを取得します。 createメソッドについても同じ方法で行います( post_paramsの追加を除く ):



 def create(conn, %{"post" => post_params}) do changeset = conn.assigns[:user] |> build_assoc(:posts) |> Post.changeset(post_params) case Repo.insert(changeset) do {:ok, _post} -> conn |> put_flash(:info, "Post created successfully.") |> redirect(to: user_post_path(conn, :index, conn.assigns[:user])) {:error, changeset} -> render(conn, "new.html", changeset: changeset) end end
      
      





そして、 showeditupdate 、およびdeleteのアクションを変更します



 def show(conn, %{"id" => id}) do post = Repo.get!(assoc(conn.assigns[:user], :posts), id) render(conn, "show.html", post: post) end def edit(conn, %{"id" => id}) do post = Repo.get!(assoc(conn.assigns[:user], :posts), id) changeset = Post.changeset(post) render(conn, "edit.html", post: post, changeset: changeset) end def update(conn, %{"id" => id, "post" => post_params}) do post = Repo.get!(assoc(conn.assigns[:user], :posts), id) changeset = Post.changeset(post, post_params) case Repo.update(changeset) do {:ok, post} -> conn |> put_flash(:info, "Post updated successfully.") |> redirect(to: user_post_path(conn, :show, conn.assigns[:user], post)) {:error, changeset} -> render(conn, "edit.html", post: post, changeset: changeset) end end def delete(conn, %{"id" => id}) do post = Repo.get!(assoc(conn.assigns[:user], :posts), id) #    delete! (  ),     #      (  ). Repo.delete!(post) conn |> put_flash(:info, "Post deleted successfully.") |> redirect(to: user_post_path(conn, :index, conn.assigns[:user])) end
      
      





すべてのテストを実行した後、すべてが機能することを確認する必要があります。 それを除いて...すべてのユーザーは、自分が望むユーザーの下で新しい投稿を削除/編集/作成することができます!



ユーザーによる投稿の作成を制限します



このようなセキュリティホールのあるブログエンジンはリリースできません。 受信したユーザーが現在のユーザーでもあることを保証する別のプラグインを追加して、これを修正しましょう。



web / controllers / post_controller.exファイルの最後に新しい関数を追加します



 defp authorize_user(conn, _opts) do user = get_session(conn, :current_user) if user && Integer.to_string(user.id) == conn.params["user_id"] do conn else conn |> put_flash(:error, "You are not authorized to modify that post!") |> redirect(to: page_path(conn, :index)) |> halt() end end
      
      





そして一番上に、プラグイン呼び出しを追加します:



 plug :authorize_user when action in [:new, :create, :update, :edit, :delete]
      
      





これですべてがうまくいくはずです! 投稿するには、ユーザーを登録する必要があります。その後、ユーザーのみと作業します。 残っているのは、テストスイートを更新してこれらの変更を処理するだけです。 開始するには、 混合テストを実行して現在の状況を評価するだけです。 ほとんどの場合、次のエラーが表示されます。



 ** (CompileError) test/controllers/post_controller_test.exs:14: function post_path/2 undefined (stdlib) lists.erl:1337: :lists.foreach/2 (stdlib) erl_eval.erl:669: :erl_eval.do_apply/6 (elixir) lib/code.ex:363: Code.require_file/2 (elixir) lib/kernel/parallel_require.ex:50: anonymous fn/4 in Kernel.ParallelRequire.spawn_requires/5
      
      





残念ながら、各post_path呼び出しを再びuser_post_pathに変更する必要があります。 これを行うには、テストを根本的に変更する必要があります。 test / controllers / post_controller_text.exsファイルに設定ブロックを追加することから始めます



 alias Pxblog.User setup do {:ok, user} = create_user conn = build_conn() |> login_user(user) {:ok, conn: conn, user: user} end defp create_user do User.changeset(%User{}, %{email: "test@test.com", username: "test", password: "test", password_confirmation: "test"}) |> Repo.insert end defp login_user(conn, user) do post conn, session_path(conn, :create), user: %{username: user.username, password: user.password} end
      
      





ここでは多くのことが行われています。 最初にしたことは、作成する必要があるcreate_user関数への呼び出しを追加することでした。 テストにはヘルパーが必要なので、追加しましょう。 create_user関数は単純にテストユーザーをRepoに追加するため、この関数を呼び出すときにパターンマッチング{:ok、user}を使用します。



次に、 前述のようにconn = build_conn()を呼び出します。 次に、 connの結果をlogin_user関数に渡します。 すべての基本的な投稿アクションにはユーザーが必要なため、これにより投稿がログイン機能に接続されます。 connを返し、個々のテストに持ち込む必要があることを理解することは非常に重要です。 そうしないと、ユーザーはログインしたままになりません。



最後に、その関数の戻り値を標準値の戻り値okおよび:connに変更しましたが、辞書に別のエントリuserも含めます 。 変更する最初のテストを見てみましょう。



 test "lists all entries on index", %{conn: conn, user: user} do conn = get conn, user_post_path(conn, :index, user) assert html_response(conn, 200) =~ "Listing posts" end
      
      





testメソッドの2番目の引数を変更して、パターンマッチングを使用して、key :connに加えてkey :userを含む辞書を取得することに注意してください。 これにより、 セットアップブロックで作業するユーザーキーを使用することが保証されます 。 さらに、 post_pathヘルパーの呼び出しをuser_post_pathに変更し、3番目の引数を持つユーザーを追加しました。 このテストのみを直接実行します。 これを行うには、タグを指定するか、次のようにコマンドを実行して目的の行の番号を指定します。



 $ mix test test/controller/post_controller_test.exs:[line number]
      
      





テストが緑色に変わるはずです! いいね! この部分を変更しましょう:



 test "renders form for new resources", %{conn: conn, user: user} do conn = get conn, user_post_path(conn, :new, user) assert html_response(conn, 200) =~ "New post" end
      
      





ここでは、 セットアップハンドラとパスを変更する以外に新しいものはありません。



 test "creates resource and redirects when data is valid", %{conn: conn, user: user} do conn = post conn, user_post_path(conn, :create, user), post: @valid_attrs assert redirected_to(conn) == user_post_path(conn, :index, user) assert Repo.get_by(assoc(user, :posts), @valid_attrs) end
      
      





ユーザーに関連付けられたすべての投稿を受信する必要があることを忘れないでください。 したがって、すべての呼び出しをpost_pathに変更します。



 test "does not create resource and renders errors when data is invalid", %{conn: conn, user: user} do conn = post conn, user_post_path(conn, :create, user), post: @invalid_attrs assert html_response(conn, 200) =~ "New post" end
      
      





別のわずかに変更されたテスト。 見るべきものは何もないので、次に興味深いものに移りましょう。 ユーザーアソシエーションに属する投稿を作成/受信することを思い出してください。そこで、 「shows selected resource」テストの変更に進みます。



 test "shows chosen resource", %{conn: conn, user: user} do post = build_post(user) conn = get conn, user_post_path(conn, :show, user, post) assert html_response(conn, 200) =~ "Show post" end
      
      





以前は、単純なRepo.insert! %Post{}



を使用して投稿を追加しましたRepo.insert! %Post{}



Repo.insert! %Post{}



。 これはもう機能しないので、正しい関連付けで作成する必要があります。 この行は残りのテストで非常に頻繁に使用されるため、その使用を容易にするヘルパーを作成します。



 defp build_post(user) do changeset = user |> build_assoc(:posts) |> Post.changeset(@valid_attrs) Repo.insert!(changeset) end
      
      





このメソッドは、ユーザーに関連付けられた有効な投稿モデルを作成し、データベースに挿入します。Repo.insert!に注意してください戻らない{:[OK]ザ・モデルを}、およびモデル自体を返します!



変更したテストに戻りましょう。残りのテストをレイアウトし、すべてのテストに合格するまで対応する変更を1つずつ繰り返します。



 test "renders page not found when id is nonexistent", %{conn: conn, user: user} do assert_raise Ecto.NoResultsError, fn -> get conn, user_post_path(conn, :show, user, -1) end end test "renders form for editing chosen resource", %{conn: conn, user: user} do post = build_post(user) conn = get conn, user_post_path(conn, :edit, user, post) assert html_response(conn, 200) =~ "Edit post" end test "updates chosen resource and redirects when data is valid", %{conn: conn, user: user} do post = build_post(user) conn = put conn, user_post_path(conn, :update, user, post), post: @valid_attrs assert redirected_to(conn) == user_post_path(conn, :show, user, post) assert Repo.get_by(Post, @valid_attrs) end test "does not update chosen resource and renders errors when data is invalid", %{conn: conn, user: user} do post = build_post(user) conn = put conn, user_post_path(conn, :update, user, post), post: %{"body" => nil} assert html_response(conn, 200) =~ "Edit post" end test "deletes chosen resource", %{conn: conn, user: user} do post = build_post(user) conn = delete conn, user_post_path(conn, :delete, user, post) assert redirected_to(conn) == user_post_path(conn, :index, user) refute Repo.get(Post, post.id) end
      
      





それらをすべて修正したら、mix testコマンド実行して、グリーンテストを取得できます!



最後に、ユーザーの検索と承認を処理するためのプラグインなどの新しいコードを作成し、成功したケースを非常によくテストしましたが、負のケースのテストも追加する必要があります。存在しないユーザーからの投稿にアクセスしようとすると何が起こるかをテストすることから始めます。



 test "redirects when the specified user does not exist", %{conn: conn} do conn = get conn, user_post_path(conn, :index, -1) assert get_flash(conn, :error) == "Invalid user!" assert redirected_to(conn) == page_path(conn, :index) assert conn.halted end
      
      





ここでは使用しないため、セットアップブロックのサンプルと比較して:userは含めませんでした。接続が最後に閉じることも確認します。そして最後に、誰かの投稿を編集しようとするテストを書く必要があります。







 test "redirects when trying to edit a post for a different user", %{conn: conn, user: user} do other_user = User.changeset(%User{}, %{email: "test2@test.com", username: "test2", password: "test", password_confirmation: "test"}) |> Repo.insert! post = build_post(user) conn = get conn, user_post_path(conn, :edit, other_user, post) assert get_flash(conn, :error) == "You are not authorized to modify that post!" assert redirected_to(conn) == page_path(conn, :index) assert conn.halted end
      
      





悪いユーザーになる別のユーザーを作成し、彼をRepoに追加します。次に、最初のユーザーの投稿の編集アクションにアクセスしようとしますこれにより、authorize_userプラグインのマイナスのケースが機能しますファイルを保存し、コマンドmix test



実行して結果を待ちます:



 ....................................... Finished in 0.4 seconds 39 tests, 0 failures Randomized with seed 102543
      
      





行くぞ!私たちはたくさんやった!しかし、今では機能的な(そしてより安全な)ブログがあり、ユーザー向けの投稿が作成されています。そして、我々はまだ良いテストカバレッジを持っています!休憩する時間です。管理者の役割、コメント、Markdownサポートを追加することで、この一連のトレーニング資料を継続し、最終的にライブコメントシステムでチャンネルに侵入します!



翻訳者からの重要な結論



私はこの記事とシリーズ全体の翻訳の両方を翻訳する素晴らしい仕事をしました。私が今し続けていること。したがって、記事自体またはRuNetでElixirを普及させる努力が気に入った場合は、プラス、コメント、再投稿で記事をサポートしてください。これは私個人にとっても、エリクサーコミュニティ全体にとっても非常に重要です。



シリーズの他の記事



  1. エントリー
  2. ログイン
  3. 役割を追加
  4. コントローラーで役割を処理します
  5. ExMachinaを接続します
  6. マークダウンのサポート
  7. コメントを追加
  8. コメントで終了
  9. チャンネル
  10. チャネルテスト
  11. おわりに




すべての不正確さ、エラー、不十分な翻訳については、個人的なメッセージで書いてください、私はすぐにそれを修正します。事前に感謝します。



All Articles