07/21/2016 Last Updated At: Previous Post in this series _Last Updated At: 07/20/2016_medium.com Writing a Blog Engine in Phoenix and Elixir: Part 1 Current Versions: As of the time of writing this, the current versions of our applications are: : v1.3.1 Elixir v1.2.0 Phoenix: v2.0.2 Ecto: v2.5.2 Comeonin: If you are reading this and these are not the latest, let me know and I’ll update this tutorial accordingly. Fixing up some bugs If you’ve been following along, you should have a (somewhat) functional blog engine running in Elixir/Phoenix. If you’re anything like me, this sort of thing makes you giddy and you can’t wait to press ahead and have something even more polished! If you want to check your progress, I’ve thrown all of the work we’ve put together online a . github repo The first bug that’s pretty easy to reproduce is to go to and just hit submit. You should see an error message that looks like the following: 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. If we look at the function in it’s pretty clear what’s happening here. create SessionController def create(conn, %{"user" => user_params}) douser = Repo.get_by(User, username: user_params["username"])user|> sign_in(user_params["password"], conn)end So if we send across a parameter string that includes a blank value (or no value) for username, we’ll hit this error. Let’s fix this up quickly; thankfully, this is pretty easy to fix up with guard clauses and pattern matching. Replace the current function with the following: create def create(conn, %{"user" => %{"username" => username, "password" => password}})when not is_nil(username) and not is_nil(password) douser = Repo.get_by(User, username: username)sign_in(user, password, conn)end def create(conn, _) dofailed_login(conn)end We replace the params argument in the second function with an underscore since we do not need to use the results from it anywhere. We also added a reference to this function, so let’s add it as a private function. In and modify the Comeonin import statement at the top: create failed_login web/controllers/session_controller.ex import Comeonin.Bcrypt, only: [checkpw: 2, dummy_checkpw: 0] We need to call out to dummy_checkpw() so that someone cannot just iterate over usernames with bad passwords and check to see if the timing changes when usernames don’t exist in our system. Then, we add our function: failed_login defp failed_login(conn) dodummy_checkpw()conn|> put_session(:current_user, nil)|> put_flash(:error, "Invalid username/password combination!")|> redirect(to: page_path(conn, :index))|> halt()end Again, note that call to at the top! We also clear out the current_user session, set a flash message indicating that the user entered an invalid combination of usernames/passwords, and redirect back to the main index and halt. The call to halt here is sanity against double render issues. dummy_checkpw() And then everywhere we have any code that was doing any of the work above, we can replace that old code with calls to the new function. defp sign_in(user, _password, conn) when is_nil(user) dofailed_login(conn)end defp sign_in(user, password, conn) doif checkpw(password, user.password_digest) doconn|> put_session(:current_user, %{id: user.id, username: user.username})|> put_flash(:info, "Sign in successful!")|> redirect(to: page_path(conn, :index))elsefailed_login(conn)endend That should take care of any existing odd bugs with logins, so we can move on to making our posts associated with the current logged in user. Adding our migration The first thing we need to do to be able to associate posts with users is modify the table to include a reference to the table. posts users First, we’ll use Ecto’s migration generator to create a new migration. $ mix ecto.gen.migration add_user_id_to_posts And we should see some output letting us know that it worked successfully. Compiling 1 file (.ex)* creating priv/repo/migrations* creating priv/repo/migrations/20160720211140_add_user_id_to_posts.exs If we open up the file, however, there won’t be anything there yet, so we’ll add in the details. We’ll change the function to contain the following: change def change doalter table(:posts) doadd :user_id, references(:users)endcreate index(:posts, [:user_id])end This will add the user_id column referencing the users table. It also sets up an index on the user_id column on the posts table. We’ll run and start modifying our models now. mix ecto.migrate Associating posts with users Let’s open up and add a reference to the model. web/models/post.ex User Under the “posts” schema, add the following: belongs_to :user, Pxblog.User We’ll add an inverse relationship to the model pointing back to the model. Under the “users” schema in , add the following: User Post web/models/user.ex has_many :posts, Pxblog.Post We’ll need to also open up the controller and associating our posts with users. Posts Modifying our routes First, we’ll update the router to point to posts underneath a user. Open up and we’ll change the routes around a little. web/router.ex We’ll change the “/users” and “/posts” routes slightly. Remove the two and replace instead with this block: resources "/users", UserController doresources "/posts", PostControllerend Fixing up our controller If we try to run right now, we’ll get an error. That’s okay, though! Since we changed our route structure, we lost the helper, which has instead been replaced with the nested resource version of . Nested helpers allow us to access routes that represent resources that require another resource to be present (such as posts underneath a user). mix phoenix.routes post_path user_post_path So, if we have a regular helper, we call it this way: post_path post_path(conn, :show, post) The is our connection object, the is what action we’re linking to, and the third argument can either be a model or the id for the object. We could also do: conn :show post_path(conn, :show, 1) However, when we have a nested resource, the helpers have to change with the modification to our routes file. Given that posts are now nested under users, our routes change too: user_post_path(conn, :show, user, post) Notice that the third argument changes to the top level of our nested resource, and each additional resource follows in order. Given our new understanding of this, it’s pretty clear where this is throwing us errors, so we’re going to want to clean this up. We’re going to want to expose the requested user to all of our controller actions. The best way for us to do this is via a plug, so we’ll open up : web/controllers/post_controller.ex At the top, we’ll add a new plug call. plug :assign_user And then at the bottom we’ll write up our assign_user plug: defp assign_user(conn, _opts) docase conn.params do%{"user_id" => user_id} ->user = Repo.get(Pxblog.User, user_id)assign(conn, :user, user)_ ->connendend And then everywhere that shows up, we’ll replace it with : post_path user_post_path def create(conn, %{"post" => post_params}) dochangeset = 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)endend def update(conn, %{"id" => id, "post" => post_params}) dopost = 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)endend def delete(conn, %{"id" => id}) dopost = Repo.get!(Post, id) Here we use delete! (with a bang) because we expect it to always work (and if it does not, it will raise). Repo.delete!(post) conn|> put_flash(:info, "Post deleted successfully.")|> redirect(to: user_post_path(conn, :index, conn.assigns[:user]))end Fixing up our templates Our controller is no longer spitting out an error message, so now we’ll work on our templates. We took a bit of a shortcut by implementing a plug that affected all of our controller actions. By using the function on our connection object, we’ve exposed a variable that we can work with in our templates. We’ll have to modify quite a few templates, because everywhere we’re using the link helper with our helper, we need to update that to user_post_path and make sure the first argument after the action is the user’s id. assign 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) %> Now, as a sanity check, if we run , we should see output and a successful compilation! mix phoenix.routes Compiling 14 files (.ex)page_path GET / Pxblog.PageController :indexuser_path GET /users Pxblog.UserController :indexuser_path GET /users/:id/edit Pxblog.UserController :edituser_path GET /users/new Pxblog.UserController :newuser_path GET /users/:id Pxblog.UserController :showuser_path POST /users Pxblog.UserController :createuser_path PATCH /users/:id Pxblog.UserController :updatePUT /users/:id Pxblog.UserController :updateuser_path DELETE /users/:id Pxblog.UserController :deleteuser_post_path GET /users/:user_id/posts Pxblog.PostController :indexuser_post_path GET /users/:user_id/posts/:id/edit Pxblog.PostController :edituser_post_path GET /users/:user_id/posts/new Pxblog.PostController :newuser_post_path GET /users/:user_id/posts/:id Pxblog.PostController :showuser_post_path POST /users/:user_id/posts Pxblog.PostController :createuser_post_path PATCH /users/:user_id/posts/:id Pxblog.PostController :updatePUT /users/:user_id/posts/:id Pxblog.PostController :updateuser_post_path DELETE /users/:user_id/posts/:id Pxblog.PostController :deletesession_path GET /sessions/new Pxblog.SessionController :newsession_path POST /sessions Pxblog.SessionController :createsession_path DELETE /sessions/:id Pxblog.SessionController :delete Hooking it all up in the controller Now, all we need to do is finish hooking up our controller to use the new associations. First, we’ll fire up so we can learn a bit about how to fetch a user’s posts. Before we do that, though, it’ll help us out a little bit to set up a list of standard imports/aliases that will get loaded every time we load up an iex prompt inside of our project. Create a new file in the project root called (note the period at the start of the file name) and populate it with the following contents: iex -S mix .iex.exs import Ecto.Queryalias Pxblog.Useralias Pxblog.Postalias Pxblog.Repoimport Ecto Now when iex starts up, we won’t have to do something like the following every single time: iex(1)> import Ecto.Queryniliex(2)> alias Pxblog.Userniliex(3)> alias Pxblog.Postniliex(4)> alias Pxblog.Reponiliex(5)> import Ectonil Moving on, and running we should already have at least one User in our repo. If not, please create one first. Then we’ll run: iex -S mix: 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[] We haven’t created any posts associated with a user yet, so it makes sense that we’re getting a blank list there. We used the function from to give us a query that linked posts to a user. We also could have done the following: assoc Ecto 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 Which instead would’ve given us a query with an inner join instead of a straight where clause on the user id. Take special care to look at the query that is being generated in both cases; it’s very handy to understand the SQL being generated behind the scenes any time you’re working with code that generates queries. We could also use the function when we’re fetching posts to preload the users as well, such as the following: preload 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[] We need to give ourselves to work with for messing around with these queries, so we’re going use function. takes the model we want to add an association to as the first argument, the association we want to hook into as an atom for the second argument, and the parameters for the third argument. Ecto’s build_assoc build_assoc 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]) And on the last command, we should get the following output: 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}] And we’ll just check the first result quickly: 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" Cool! Our experiment is doing precisely what we expect, so let’s go back to our controller ( and start fixing up the code. For the index, action, we want all of the posts associated with a user, so let’s fix up the code. We’ll start with our index action: web/controllers/post_controller.ex) def index(conn, _params) doposts = Repo.all(assoc(conn.assigns[:user], :posts))render(conn, "index.html", posts: posts)end We can then visit the and see a list of posts show up! But if we try to visit a post index for a user that doesn’t exist, we get an error message, which is not a great user experience, so let’s clean up our assign user plug. posts index for user 1 defp assign_user(conn, _opts) docase conn.params do%{"user_id" => user_id} ->case Repo.get(Pxblog.User, user_id) donil -> invalid_user(conn)user -> assign(conn, :user, user)end_ -> invalid_user(conn)endend defp invalid_user(conn) doconn|> put_flash(:error, "Invalid user!")|> redirect(to: page_path(conn, :index))|> haltend Now, when we visit a post index for a user that doesn’t exist, we get a nice flash message and are kindly redirected back to the page path! Next, we need to change our new action: def new(conn, _params) dochangeset =conn.assigns[:user]|> build_assoc(:posts)|> Post.changeset()render(conn, "new.html", changeset: changeset)end We take our model, pipe that into Ecto’s build_assoc function, tell it we need to build a post, and then pipe the resulting blank model into the Post.changeset function to get a blank changeset. We’ll follow the same pattern for our create method (except with our post_params intact): user def create(conn, %{"post" => post_params}) dochangeset =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)endend And then modify our show, edit, update, and delete actions: def show(conn, %{"id" => id}) dopost = Repo.get!(assoc(conn.assigns[:user], :posts), id)render(conn, "show.html", post: post)end def edit(conn, %{"id" => id}) dopost = 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}) dopost = 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)endend def delete(conn, %{"id" => id}) dopost = Repo.get!(assoc(conn.assigns[:user], :posts), id) Here we use delete! (with a bang) because we expect it to always work (and if it does not, it will raise). Repo.delete!(post) conn|> put_flash(:info, "Post deleted successfully.")|> redirect(to: user_post_path(conn, :index, conn.assigns[:user]))end And when we test it all out, we should just see everything working! Except…any user can delete/edit/create new posts underneath any user id they want! Restricting posting to users We can’t very well release a blog engine that has a security hole like this, now can we? Let’s fix it by adding another plug that makes sure the current user is the same as the fetched user. At the bottom, we’ll add a new function to : web/controllers/post_controller.ex defp authorize_user(conn, _opts) douser = get_session(conn, :current_user)if user && Integer.to_string(user.id) == conn.params["user_id"] doconnelseconn|> put_flash(:error, "You are not authorized to modify that post!")|> redirect(to: page_path(conn, :index))|> halt()endend And at the top, we’ll add the plug call: plug :authorize_user when action in [:new, :create, :update, :edit, :delete] Now everything should be working just fine! Users have to be logged in to post and can only mess around with their own posts. All we need is to update our test suite to handle these changes and we should be all set. Let’s start by just running to figure out where we’re at. Odds are you’re going to see an error message like the one below: mix test ** (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 Unfortunately, we have to go in and change every instance of to again. And, in order to do so, we’re going to need to change our tests pretty drastically. We’ll start by adding a setup block to : post_path user_post_path test/controllers/post_controller_text.exs alias Pxblog.User setup do{:ok, user} = create_userconn = build_conn()|> login_user(user){:ok, conn: conn, user: user}end defp create_user doUser.changeset(%User{}, %{email: "test@test.com", username: "test", password: "test", password_confirmation: "test"})|> Repo.insertend defp login_user(conn, user) dopost conn, session_path(conn, :create), user: %{username: user.username, password: user.password}end There’s a lot going on here already. The first thing we’ve done is added a call to a function that we need to write. We need some test helpers, so we’re going to add functions to handle these. Our function just inserts a sample user into our Repo, so that’s why we’re pattern matching from that function call. create_user create_user {:ok, user} Next, we have the call, which you’ve seen before. We then pipe the resulting conn into this function. This connection posts to our login function, since all of our major post actions require a logged in user. One thing that’s very important here: we to return the conn and carry it with us into each test. If we don’t do that, the user will not stay logged in! conn = build_conn() login_user need Finally, we changed our return of that function to return the same standard :ok and :conn values, but now we also include one more :user entry into the dict. Let’s take a look at the first test we have to modify: test "lists all entries on index", %{conn: conn, user: user} doconn = get conn, user_post_path(conn, :index, user)assert html_response(conn, 200) =~ "Listing posts"end Notice we changed the second argument to our “test” method to instead pattern match to a map containing the keys and instead of just . This ensures that we’re exposing the key that we were working with in our setup block. Other than that, we’ve just changed the helper call to and added the user as our third argument. Run this test explicitly now; you can target this test with a tag or by specifying the line number by running the command as: :conn :user :conn :user post_path user_post_path $ mix test test/controller/post_controller_test.exs:[line number] Our test should now be green! Great! But let’s keep modifying these: test "renders form for new resources", %{conn: conn, user: user} doconn = get conn, user_post_path(conn, :new, user)assert html_response(conn, 200) =~ "New post"end Nothing new here other than the change to the setup handler and the user post path, so we’ll move on. test "creates resource and redirects when data is valid", %{conn: conn, user: user} doconn = post conn, user_post_path(conn, :create, user), post: @valid_attrsassert redirected_to(conn) == user_post_path(conn, :index, user)assert Repo.get_by(assoc(user, :posts), @valid_attrs)end Remember that we had to fetch each post by the user’s association, so we want to make sure we’re doing that here and changing all of the calls. post_path test "does not create resource and renders errors when data is invalid", %{conn: conn, user: user} doconn = post conn, user_post_path(conn, :create, user), post: @invalid_attrsassert html_response(conn, 200) =~ "New post"end Another easily modified test, so we’ll move to the next one where it’s more interesting. Remember again that we have to build/fetch all of our Posts underneath a user association, so we’re going to modify our “shows chosen resource” test: test "shows chosen resource", %{conn: conn, user: user} dopost = build_post(user)conn = get conn, user_post_path(conn, :show, user, post)assert html_response(conn, 200) =~ "Show post"end Previously, we were setting post to a simple Repo.insert! %Post{}. That won’t work for us anymore as we need to build it with the appropriate association. Since this line appears pretty frequently in the remaining tests, we’ll write a helper method to make this simpler for us. defp build_post(user) dochangeset =user|> build_assoc(:posts)|> Post.changeset(@valid_attrs)Repo.insert!(changeset)end This method creates a valid post model under our user association, and then inserts it into the database. Note that Repo.insert! does not return {:ok, model}, instead it just returns the model! Going back to our test that we were modifying, the rest is pretty boiler-plate. I’m going to post the rest of the tests below; you’ll just be repeating the same modifications over and over until they’re all taken care of. test "renders page not found when id is nonexistent", %{conn: conn, user: user} doassert_raise Ecto.NoResultsError, fn ->get conn, user_post_path(conn, :show, user, -1)endend test "renders form for editing chosen resource", %{conn: conn, user: user} dopost = 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} dopost = build_post(user)conn = put conn, user_post_path(conn, :update, user, post), post: @valid_attrsassert 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} dopost = 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} dopost = 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 When you’ve modified everything, you should be able to run and get green tests! mix test Finally, we wrote some new code as plugs to handle user lookup and authorization, and we’ve tested the positive cases pretty well, but we should also add tests for the negative cases as well. We’ll start with a test for what happens when we try to access the posts listing for a user that does not exist: test "redirects when the specified user does not exist", %{conn: conn} doconn = 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.haltedend We don’t have to include :user in our pattern match from the setup block here since we’re not using it anyways. We’re additionally asserting that the connection is halted at the end of it all. Finally, we need to write a test for when we try to edit someone else’s post. test "redirects when trying to edit a post for a different user", %{conn: conn, user: user} doother_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.haltedend We create another user to act as our bad user and insert it into the Repo. Then, we attempt to access the edit action for the post under our first user. This will trigger the negative case of our plug! Save this file and run and we’ll wait for the results: authorize_user mix test ....................................... Finished in 0.4 seconds39 tests, 0 failures Randomized with seed 102543 Whew! That was a lot! But, we now have a functional (and more protected blog), with posts being created under our users and we still have some good test coverage! Take a break, relax, go play some Monster Hunter Generations (or at least, that’s my plan)! We’ll continue this series of tutorials with adding admin roles, comments, Markdown support, and finally we’ll break into channels with a live commenting system! If you’re interested in learning how to debug this application in development mode, then read more about debugging a Phoenix application here! If you want to continue on the tutorial, then let’s move on! Next post in this series _Latest Update: 01/26/2016_medium.com Writing a Blog Engine in Phoenix and Elixir: Part 3, Adding Roles to our Models Check out my new book! Hey everyone! If you liked what you read here and want to learn more with me, check out my new book on Elixir and Phoenix web development: _Learn to build a high-performance functional prototype of a voting web application from scratch using Elixir and…_www.packtpub.com Phoenix Web Development | PACKT Books I’m really excited to finally be bringing this project to the world! It’s written in the same style as my other tutorials where we will be building the scaffold of a full project from start to finish, even covering some of the trickier topics like file uploads, Twitter/Google OAuth logins, and APIs! is how hackers start their afternoons. We’re a part of the family. We are now and happy to opportunities. Hacker Noon @AMI accepting submissions discuss advertising & sponsorship To learn more, , , or simply, read our about page like/message us on Facebook tweet/DM @HackerNoon. If you enjoyed this story, we recommend reading our and . Until next time, don’t take the realities of the world for granted! latest tech stories trending tech stories