Brandon Richey

@diamondgfx

Writing a Blog Engine in Phoenix and Elixir: Part 2, Authorization

Last Updated At: 07/21/2016

Previous Post in this series

Current Versions:

As of the time of writing this, the current versions of our applications are:

  • Elixir: v1.3.1
  • Phoenix: v1.2.0
  • Ecto: v2.0.2
  • Comeonin: v2.5.2

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 http://localhost:4000/sessions/new and just hit submit. You should see an error message that looks like the following:

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 create function in SessionController it’s pretty clear what’s happening here.

def create(conn, %{"user" => user_params}) do
user = 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 create function with the following:

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

We replace the params argument in the second create function with an underscore since we do not need to use the results from it anywhere. We also added a reference to this failed_login function, so let’s add it as a private function. In web/controllers/session_controller.ex and modify the Comeonin import statement at the top:

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 failed_login function:

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

Again, note that call to dummy_checkpw() 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.

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) 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

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 posts table to include a reference to the users table.

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 change function to contain the following:

def change do
alter table(:posts) do
add :user_id, references(:users)
end
create 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 mix ecto.migrate and start modifying our models now.

Associating posts with users

Let’s open up web/models/post.ex and add a reference to the User model.

Under the “posts” schema, add the following:

belongs_to :user, Pxblog.User

We’ll add an inverse relationship to the User model pointing back to the Post model. Under the “users” schema in web/models/user.ex, add the following:

has_many :posts, Pxblog.Post

We’ll need to also open up the Posts controller and associating our posts with users.

Modifying our routes

First, we’ll update the router to point to posts underneath a user. Open up web/router.ex and we’ll change the routes around a little.

We’ll change the “/users” and “/posts” routes slightly. Remove the two and replace instead with this block:

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

Fixing up our controller

If we try to run mix phoenix.routes right now, we’ll get an error. That’s okay, though! Since we changed our route structure, we lost the post_path helper, which has instead been replaced with the nested resource version of user_post_path. Nested helpers allow us to access routes that represent resources that require another resource to be present (such as posts underneath a user).

So, if we have a regular post_path helper, we call it this way:

post_path(conn, :show, post)

The conn is our connection object, the :show 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:

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) do
case conn.params do
%{"user_id" => user_id} ->
user = Repo.get(Pxblog.User, user_id)
assign(conn, :user, user)
_ ->
conn
end
end

And then everywhere that post_path shows up, we’ll replace it with user_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)
  # 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 assign 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 post_path helper, we need to update that to user_post_path and make sure the first argument after the action is the user’s id.

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 mix phoenix.routes, we should see output and a successful compilation!

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

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 iex -S mix 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 .iex.exs (note the period at the start of the file name) and populate it with the following contents:

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

Now when iex starts up, we won’t have to do something like the following every single time:

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

Moving on, and running iex -S mix: we should already have at least one User in our repo. If not, please create one first. Then we’ll run:

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 assoc function from Ecto to give us a query that linked posts to a user. We also could have done the following:

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 preload function when we’re fetching posts to preload the users as well, such as the following:

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 Ecto’s build_assoc function. build_assoc 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.

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 (web/controllers/post_controller.ex) 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:

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

We can then visit the posts index for user 1 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.

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

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) do
changeset =
conn.assigns[:user]
|> build_assoc(:posts)
|> Post.changeset()
render(conn, "new.html", changeset: changeset)
end

We take our user 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):

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

And then modify our show, edit, update, and delete actions:

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)
  # 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) 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

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 mix test to figure out where we’re at. Odds are you’re going to see an error message like the one below:

** (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 post_path to user_post_path 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 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

There’s a lot going on here already. The first thing we’ve done is added a call to a create_user function that we need to write. We need some test helpers, so we’re going to add functions to handle these. Our create_user function just inserts a sample user into our Repo, so that’s why we’re pattern matching {:ok, user} from that function call.

Next, we have the conn = build_conn() call, which you’ve seen before. We then pipe the resulting conn into this login_user 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 need 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!

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} do
conn = 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 :conn and :user instead of just :conn. This ensures that we’re exposing the :user key that we were working with in our setup block. Other than that, we’ve just changed the post_path helper call to user_post_path 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:

$ 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} do
conn = 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} 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

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 post_path calls.

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

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} do
post = 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) do
changeset =
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} 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

When you’ve modified everything, you should be able to run mix test and get green tests!

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} 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

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} 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

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 authorize_user plug! Save this file and run mix test and we’ll wait for the results:

.......................................
Finished in 0.4 seconds
39 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

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:

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!

Hacker Noon is how hackers start their afternoons. We’re a part of the @AMIfamily. We are now accepting submissions and happy to discuss advertising & sponsorship opportunities.
To learn more, read our about page, like/message us on Facebook, or simply, tweet/DM @HackerNoon.
If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories. Until next time, don’t take the realities of the world for granted!

More by Brandon Richey

Topics of interest

More Related Stories