📜 ⬆️ ⬇️

Creating a blog engine with Phoenix and Elixir / Part 2. Authorization



From the translator: “ Elixir and Phoenix are a great example of where modern web development is heading. Already, these tools provide high-quality access to real-time technologies for web applications. Sites with increased interactivity, multiplayer browser games, microservices - those areas in which these technologies will serve a good service. The following is a translation of a series of 11 articles that describe in detail the aspects of development on the Phoenix framework that would seem such a trivial thing as a blog engine. But do not hurry to sulk, it will be really interesting, especially if the articles encourage you to pay attention to the Elixir or become its followers.

In this part, we will refine the basis for the blog, dive a little deeper into testing and finally add authorization. I apologize for the slight delay, then I will try to keep to a clear schedule, or go ahead of the curve! "

At the moment, our application is based on:
')

Fix some bugs


If you followed the first part, you should have a somewhat functioning blogging engine running on Elixir / Phoenix. If you are like me, then even such a seemingly small part of the work done will excite and make you move further, causing a desire to polish the code even more.

If you want to follow the progress of the work, I poured all the code for you into the repository on Github .

The first bug is fairly easy to reproduce by going to http: // localhost: 4000 / sessions / new and clicking the Submit button. You should see an error message similar to:

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 take a look at the create function in SessionController, it becomes immediately clear what's the matter.

 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 in the parameters instead of username a string containing an empty value (or nothing), then we get an error. Let's quickly fix it. Fortunately, this is done easily using 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 don't need to use it anywhere. We also refer to the failed_login function, which needs to be added as a private one. In the web / controllers / session_controller.ex file, change the Comeonin import:

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

We need to call dummy_checkpw () so that no one can carry out an attack on time by simply iterating over the users. Next, we add the 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, notice the dummy_checkpw () call above! We also clear our current_user session, set a flash message telling the user to enter a wrong username and password and redirect back to the main page. Finally, we call the halt function, which is a reasonable defense against double-rendering problems. And then we replace all similar code with calls of our 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 

These edits should take care of all existing strange bugs with the entrance, so that we can move on to associate posts with users adding them.

Add migration


Let's start by adding a link to the users table to the posts table. To do this, we will create a migration through the Ecto-generator :

 $ mix ecto.gen.migration add_user_id_to_posts 

Conclusion:

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

If we open the file we just created, we won't see anything in it. So add the following code to the change function:

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

This will add a user_id column that refers to the user table, as well as an index for it. mix ecto.migrate and start editing our models.

Associate posts with users


Let's open the web / models / post.ex file and add a link to the User model. Inside the posts scheme we place the line:

 belongs_to :user, Pxblog.User 

We need to add feedback to the User model that points back to the Post model. Inside the users schema in the web / models / user.ex file we place the line:

 has_many :posts, Pxblog.Post 

We also need to open the Posts controller and directly link posts to users.

We change ways


Let's start with updating the router by specifying posts within users. To do this, open the web / router.ex file and replace the / users and / posts paths with:

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

We fix the controller


If we try to execute the mix phoenix.routes right now, we get an error. This is the norm! Since we changed the structure of the paths, the post_path helper, the new version of which is called user_post_path and refers to the embedded resource, is lost. Nested helpers allow us to gain access to the paths represented by resources that require the availability of another resource (for example, posts require a user).

So, if we have the usual post_path helper, we call it this way:

 post_path(conn, :show, post) 

The conn object is a connection object, an atom : show is an action to which we refer, and the third argument can be either a model or an object identifier. From here we have the opportunity to do so:

 post_path(conn, :show, 1) 

At the same time, if we have a nested resource, helpers will change along with changing our routes file. In our case:

 user_post_path(conn, :show, user, post) 

Please note that the third argument now represents an external resource, and each nested one comes next.

Now that we understand why errors occur, we can fix them. We need to have access to the requested user in each of the controller actions. The best way to get it is to use the plug. To do this, open the web / controllers / post_controller.ex file and at the very top add a call to the new plug:

 plug :assign_user 

And we will write it a little lower:

 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 replace post_path with user_post_path everywhere :

 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 

Putting in order templates


Our controller stopped “spitting out” the error message, so now let's work on our templates. We went a short way by implementing the plug-in, which is accessible from any controller action. Using the assign function on the connection object, we define a variable with which we can work in the template. Now we will change the templates a bit, replacing post_path helper with user_post_path and making sure that the argument following the name of the action is the user ID. In the web / templates / post / index.html.eex file we will write:

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

In the web / templates / post / show.html.eex file:

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

In the web / templates / post / new.html.eex file :

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

In the web / templates / post / edit.html.eex file :

 <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 health check, if we run mix phoenix.routes , we should see the output of the paths and the 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 

We connect the remaining parts to the controller


Now, all we need is to finish the work on the controller to use the new associations. We iex -S mix start by launching the interactive console with the iex -S mix command to learn a little about how to select user posts. But before that we need to set up a list of standard imports / aliases that will be loaded each time the iex console is loaded inside our project. Create a new .iex.exs file in the project root (note the point at the beginning of the file name) and fill it with the following content:

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

Now, when running iex, we don’t need to do anything like this every 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 

Now we need to have at least one user in the repository. If not, add it. Then we can 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 [] 

While we have not created a single post for this user, so it is logical to get an empty list here. We used the assoc function from ecto to get the request linking posts with the user. We can also do 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 

This creates a query with an inner join instead of a direct condition for a selection by user ID. Pay particular attention to what the queries generated in both cases look like. It is very useful to understand the SQL that is created “behind the scenes” whenever you work with the code that generates queries.

We can also use the preload function when fetching posts to preload users as well, as shown below:

 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 add posts to make it possible to tinker with requests. So for this we are going to use the Ecto function called build_assoc . This function takes as the first argument the model for which we want to add an association, and the second - the association itself as an atom.

 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 now, having completed the last query, 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 just quickly check the first result:

 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 showed exactly what we expected, so let's go back to the controller ( web / controllers / post_controller.ex file ) and begin to edit the code. In the index action, we want to receive all posts related to the user. Let's start with it:

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

Now we can go see the list of posts for the first user! But if we try to get a list of posts for a user that doesn't exist, we will get an error message, which is a bad UX, so let's tidy up our assign_user plugin :

 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 open the list of posts for a non-existent user, we will receive a nice flash message and will be kindly redirected to page_path . Next, we need to change the 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 the user model, pass it to the build_assoc function, say we need to create a post, and then pass the resulting empty model to the Post.changeset function to get an empty revision. We will go the same way for the create method (except for adding 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 

And then change the 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) #    delete! (  ),     #      (  ). Repo.delete!(post) conn |> put_flash(:info, "Post deleted successfully.") |> redirect(to: user_post_path(conn, :index, conn.assigns[:user])) end 

After running all the tests, we should see that everything works. Except that ... any user has the ability to delete / edit / create a new post under any user he wants!

We limit the creation of posts by users


We cannot release a blog engine with such a security hole. Let's fix this by adding another plugin that ensures that the resulting user is also the current user.

Add a new function to the end of the web / controllers / post_controller.ex file :

 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 very top, add a call to the plug:

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

Now everything should work fine! Users must be registered to leave posts, and then work only with them. All we have to do is update the test suite to handle these changes, and everything will be ready. To start, just run the mix test to evaluate the current situation. Most likely you will see this error:

 ** (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 need to change each post_path call to user_post_path again. And in order to do this, we need to radically change our tests. Let's start by adding a settings block to the file 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 is a lot going on here. The first thing we did was add a call to the create_user function, which we need to write. We need some helpers for the tests, so let's add them. The create_user function simply adds a test user to the Repo , which is why we use the {: ok, user} pattern matching when calling this function.

Next we call conn = build_conn () , as well as before. Then we pass the result of conn to the login_user function. This connects posts with our login function, since all basic actions with posts require a user. It is very important to understand that we need to return conn and carry it with us to each individual test. If we do not do this, the user will not be logged in.

Finally, we changed the return of that function to returning the standard values : ok and : conn , but now we will also include another entry : user in the dictionary. Let's take a look at the first test that will change:

 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 

Please note that we have changed the second argument of the test method so that using pattern matching we can obtain a dictionary containing, in addition to the key : conn , also the key : user . This ensures that we use the key : user , which we work with in the setup block. In addition, we changed the post_path helper call to user_post_path and added a user with the third argument. Let's run this test right now. This can be done by specifying a tag, or by specifying the number of the desired line by executing the command as follows:

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

Our test should turn green! Sumptuously! Now let's change this piece:

 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 

There is nothing new here, except for changing the setup handler and the path, so go ahead.

 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 

Don't forget that we had to receive every post associated with a user, so we’ll change all 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 slightly modified test. There is nothing to watch, so let's move on to the next more interesting one. Recall again that we create / get posts belonging to a user association, so proceed to changing the “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 

Earlier we added posts using simple Repo.insert! %Post{} Repo.insert! %Post{} . This will no longer work, so now we need to create them with the correct association. Since this line is used quite often in the remaining tests, we will write a helper to facilitate its use.

 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 associated with the user, and then inserts it into the database. Please note that Repo.insert! returns not {: ok, model} , but returns the model itself!

Let us return to our test, which we have changed. I want to lay out the rest of the tests, and you just repeat the appropriate changes one by one, until all the tests begin to pass.

 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 fix them all, you can run the mix test command and get green tests!

Finally, we wrote a bit of new code, such as plugins for processing user searches and authorization, and we tested the successful cases fairly well, but we need to add tests for negative cases. We will start with a test of what will happen when we try to gain access to posts of 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 did not include : user in comparison with the sample from the setup block , since we do not use it here. We also check that the connection is closed at the end.

Finally, we need to write a test in which we will 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 who will become our bad user and add it to Repo . Then we try to access the edit action for the post of our first user. This will make the negative case of our plugin authorize_user work ! Save the file, run the command mix testand wait for the results:

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

That's it!Done a lot! But now we have a functional (and more secure blog) with posts created by users. And we still have good test coverage! It's time to take a break. We will continue this series of training materials by adding the role of administrator, comments, support for Markdown, and finally we will break into channels with a live commenting system!

An important conclusion from the translator


I have done a great job of translating both this article and the translation of the entire series. What I continue to do now. Therefore, if you liked the article itself or initiatives in popularizing Elixir in runet, please support the article with pluses, comments and reposts. This is incredibly important both for me personally and for the whole Elixir community as a whole.

Other series articles


  1. Introduction
  2. Authorization
  3. Adding Roles
  4. Process roles in controllers
  5. We connect ExMachina
  6. Markdown support
  7. Add comments
  8. We finish with comments
  9. Channels
  10. Channel testing
  11. Conclusion


About all inaccuracies, errors, poor translation, please write personal messages, I will promptly correct. Thanks in advance to everyone involved.

Source: https://habr.com/ru/post/313482/


All Articles