Last Updated At: 07/20/2016
As of the time of writing this, the current versions of our applications are:
If you are reading this and these are not the latest, let me know and I’ll update this tutorial accordingly.
The best instructions for installing Phoenix can be found on the Phoenix website.
We need to start by using the Phoenix mix task to create a new project, which we will call “pxblog”. We do this using the mix phoenix.new [project] [command]. Answer Y to all questions, since we do want to use phoenix.js and the other front-end requirements.
mix phoenix.new pxblog
Output:
* creating pxblog/config/config.exs...
Fetch and install dependencies? [Yn] y* running mix deps.get* running npm install && node node_modules/brunch/bin/brunch build
We are all set! Run your Phoenix application:
$ cd pxblog$ mix phoenix.server
You can also run your app inside IEx (Interactive Elixir) as:
$ iex -S mix phoenix.server
Before moving on, configure your database in config/dev.exs and run:
$ mix ecto.create
We should see a bunch of output indicating that our project has been completed and the initial work is done.
The mix ecto.create step may fail if we have not set up our postgres database correctly or configured our application to use the right credentials. If you open up config/dev.exs, you should see some configuration details at the bottom of the file:
# Configure your databaseconfig :pxblog, Pxblog.Repo,adapter: Ecto.Adapters.Postgres,username: "postgres",password: "postgres",database: "pxblog_dev",hostname: "localhost",pool_size: 10
Just change the username and password to a role that has the correct database creation permissions.
When we have that working, we’ll start up the server just to make sure everything is fine.
$ iex -S mix phoenix.server
We should now be able to visit http://localhost:4000/ and see the “Welcome to Phoenix!” page. Now that we have a good baseline, let’s add our basic scaffold for creating posts, since this is after all a blogging engine.
The first thing we’ll do is utilize one of Phoenix’s generators to build us not only the Ecto model and migration, but the UI scaffolding to handle the CRUD (Create, Read, Update, Delete) operations for our Post object. Since this is a very, very simple blog engine right now, let’s just stick with a title and a body; the title will be a string and the body will be text. The syntax for these generators is pretty straightforward:
mix phoenix.gen.html [Model Name] [Table Name] [Column Name:Column Type]…
$ mix phoenix.gen.html Post posts title:string body:text
Output:
* creating web/controllers/post_controller.ex...
Add the resource to your browser scope in web/router.ex:
resources "/posts", PostController
Remember to update your repository by running migrations:
$ mix ecto.migrate
Next, to make this scaffold accessible (and stop Elixir from complaining), we’re going to open up web/router.ex, and add the following to our root scope (the “/” scope, using the :browser pipeline):
resources "/posts", PostController
Finally, we’re going to make sure that our database has this new migration loaded by calling mix ecto.migrate.
Output:
Compiling 9 files (.ex)Generated pxblog app
15:52:20.004 [info] == Running Pxblog.Repo.Migrations.CreatePost.change/0 forward
15:52:20.004 [info] create table posts
15:52:20.019 [info] == Migrated in 0.0s
Finally, let’s restart our server, and then visit http://localhost:4000/posts and we should see the “Listing posts” header with a table containing the columns in our object.
Mess around a bit and you should be able to create new posts, edit posts, and delete posts. Pretty cool for very little work!
The beautiful thing of working with the scaffolds at the start is that it will create the baseline tests for us from the start. We don’t even really need to modify much yet since we haven’t really changed any of the scaffolds, but let’s analyze what was created for us so that we can be better prepared to write our own tests later.
First, we’ll open up test/models/post_test.exs and take a look:
defmodule Pxblog.PostTest douse Pxblog.ModelCase
alias Pxblog.Post
@valid_attrs %{body: "some content", title: "some content"}@invalid_attrs %{}
test "changeset with valid attributes" dochangeset = Post.changeset(%Post{}, @valid_attrs)assert changeset.valid?end
test "changeset with invalid attributes" dochangeset = Post.changeset(%Post{}, @invalid_attrs)refute changeset.valid?endend
Let’s take this apart and understand what’s going on.
defmodule Pxblog.PostTest do
Clearly, we need to define our Test module using our application’s namespace.
use Pxblog.ModelCase
Next, we tell this module that it is going to be using the functions and DSL introduced by the ModelCase macro set.
alias Pxblog.Post
Now make sure that the test has visibility into the model itself.
@valid_attrs %{body: “some content”, title: “some content”}
Set up some basic valid attributes that would be able to create a successful changeset. This just provides a module-level variable that we can pull from every time we want to be able to create a valid model.
@invalid_attrs %{}
Like the above, but creating, unsurprisingly, an invalid attribute set.
test "changeset with valid attributes" dochangeset = Post.changeset(%Post{}, @valid_attrs)assert changeset.valid?end
Now, we create our test by giving it a string-based name using the “test” function. Inside of our function body, we first create a changeset from the Post model (given it a blank struct and the list of valid parameters). We then assert that the changeset is valid, since that is what we’re expecting with the @valid_attrs variable.
test "changeset with invalid attributes" dochangeset = Post.changeset(%Post{}, @invalid_attrs)refute changeset.valid?end
Finally, we check against creating a changeset with an invalid parameter list, and instead of asserting the changeset is valid, we perform the reverse operation. refute is essentially assert not true.
This is a pretty good example of how to write a model test file. Now let’s take a look at the controller tests.
Let’s take a look at the top, since that all looks roughly the same:
defmodule Pxblog.PostControllerTest douse Pxblog.ConnCase
alias Pxblog.Post@valid_attrs %{body: "some content", title: "some content"}@invalid_attrs %{}
The first change set can see is Pxblog.ConnCase; we’re relying on the DSL that is exposed for controller-level tests. Other than that, the rest of the lines should be pretty familiar.
Let’s take a look at the first test:
test "lists all entries on index", %{conn: conn} doconn = get conn, post_path(conn, :index)assert html_response(conn, 200) =~ "Listing posts"end
Here, we grab the “conn” variable that is going to be sent via a setup block in ConnCase. I’ll explain this later. The next step is for us to call the appropriate verb to hit the expected route, which in our case is a get request against our index action.
We then assert that the response of this action returns HTML with a status of 200 (“ok”) and contains the phrase “Listing posts.”
test "renders form for new resources", %{conn: conn} doconn = get conn, post_path(conn, :new)assert html_response(conn, 200) =~ "New post"end
The next test is basically the same, but we’re just validating the “new” action instead. Simple stuff.
test "creates resource and redirects when data is valid", %{conn: conn} doconn = post conn, post_path(conn, :create), post: @valid_attrsassert redirected_to(conn) == post_path(conn, :index)assert Repo.get_by(Post, @valid_attrs)end
Now, we’re doing something new. First, this time we’re posting to the post_path helper with our list of valid parameters. We’re expecting to get redirected to the index route for the post resource. redirected_to takes in a connection object as an argument, since we need to see where that connection object was redirected to.
Finally, we assert that the object represented by those valid parameters are inserted into the database successfully through querying our Ecto Repo, looking for a Post model that matches our @valid_attrs parameters.
Now, we want to test the negative path for creating a new Post.
test "does not create resource and renders errors when data is invalid", %{conn: conn} doconn = post conn, post_path(conn, :create), post: @invalid_attrsassert html_response(conn, 200) =~ "New post"end
So, we post to the same create path but with our invalid_attrs parameter list, and we assert that it renders out the New Post form again.
test "shows chosen resource", %{conn: conn} dopost = Repo.insert! %Post{}conn = get conn, post_path(conn, :show, post)assert html_response(conn, 200) =~ "Show post"end
To test our show action, we make sure that we create a Post model to work with. We then call our get function to the post_path helper, and we make sure it returns the appropriate resource. However, if we attempt to grab a show path to a resource that does not exist, we do the following:
test "renders page not found when id is nonexistent", %{conn: conn} doassert_error_sent 404, fn ->get conn, post_path(conn, :show, -1)endend
We see a new pattern here, but one that is actually pretty simple to digest. We expect that if we fetch a resource that does not exist, that we should receive a 404. We then pass it an anonymous function which contains the code we want to execute that should return that error. Simple!
The rest of the tests are just repeats of the above for each path, with the exception of our delete action. Let’s take a look:
test "deletes chosen resource", %{conn: conn} dopost = Repo.insert! %Post{}conn = delete conn, post_path(conn, :delete, post)assert redirected_to(conn) == post_path(conn, :index)refute Repo.get(Post, post.id)end
Here, most of what we see is the same, with the exception of using our delete verb. We assert that we redirect out of the delete page back to the index, but we do something new here: we refute that the Post object exists anymore. Assert and Refute are truthy, so the existence of an object at all will work with an Assert and cause a Refute to fail.
We didn’t add any code to our view, so we don’t do anything with our PostView module.
We’re going to follow almost the exact same steps we followed with creating our Post object to create our User object, except with a few different columns. First, we’ll run:
$ mix phoenix.gen.html User users username:string email:string password_digest:string
Output:
* creating web/controllers/user_controller.ex...
Add the resource to your browser scope in web/router.ex:
resources "/users", UserController
Remember to update your repository by running migrations:
$ mix ecto.migrate
Next, we’ll open up web/router.ex, and add the following to our browser scope again:
resources "/users", UserController
The syntax here is defining a standard resourceful route where the first argument is the URL and the second is the controller’s class name. We’ll then run mix ecto.migrate
Output:
Compiling 11 files (.ex)Generated pxblog app
16:02:03.987 [info] == Running Pxblog.Repo.Migrations.CreateUser.change/0 forward
16:02:03.987 [info] create table users
16:02:03.996 [info] == Migrated in 0.0s
And finally, restart the server and check out http://localhost:4000/users. We can now independently create Posts and Users! Unfortunately, this isn’t a very useful blog yet. After all, we can create users (anyone can, in fact), but we can’t even log in. Plus, password digests aren’t coming from any encryption algorithm; the user is just creating them and we’re storing them in plain text! No bueno!
We’ll make this look more like a real user registration screen instead.
Our tests for the user stuff looks exactly the same as what was auto-generated for our Posts, so we’ll leave those alone until we start modifying the logic (IE, right now!)
When we visit /users/new, we see three fields: Username, Email, and PasswordDigest. But when you register on other sites, you would enter a password and a password confirmation! How can we correct this?
In web/templates/user/form.html.eex, delete the following lines:
<div class=”form-group”><%= label f, :password_digest, class: “control-label” %><%= text_input f, :password_digest, class: “form-control” %><%= error_tag f, :password_digest %></div>
And add in its place:
<div class="form-group"><%= label f, :password, "Password", class: "control-label" %><%= password_input f, :password, class: "form-control" %><%= error_tag f, :password %></div>
<div class="form-group"><%= label f, :password_confirmation, "Password Confirmation", class: "control-label" %><%= password_input f, :password_confirmation, class: "form-control" %><%= error_tag f, :password_confirmation %></div>
Refresh the page (should happen automatically), enter user details, hit submit.
Error:
Oops, something went wrong! Please check the errors below.
This is because we’re creating a password and password confirmation but nothing is being done to create the actual password_digest. Let’s write some code to do this. First, we’re going to modify the actual schema to do something new:
In web/models/user.ex:
schema "users" dofield :username, :stringfield :email, :stringfield :password_digest, :string
timestamps
field :password, :string, virtual: truefield :password_confirmation, :string, virtual: trueend
Note the addition of the two fields, :password and :password_confirmation. We’re declaring these as virtual fields, as these do not actually exist in our database but need to exist as properties in our User struct. This also allows us to apply transformations in our changeset function.
We then modify the list of required fields and casted fields to include :password and :password_confirmation.
def changeset(struct, params \\ %{}) dostruct|> cast(params, [:username, :email, :password, :password_confirmation])|> validate_required([:username, :email, :password, :password_confirmation])end
If you run test/models/user_test.exs at this point, you’ll notice that our “changeset with valid attributes” test is now failing. That’s because we made password and password_confirmation required but did not update our @valid_attrs map to include either. Let’s change that line to:
@valid_attrs %{email: "[email protected]", password: "test1234", password_confirmation: "test1234", username: "testuser"}
Our model tests should be right back to passing! We also need our controller tests passing. In test/controllers/user_controller_test.exs, we’ll make some modifications. First, we’ll distinguish between valid creation attributes and valid searching attributes:
@valid_create_attrs %{email: "[email protected]", password: "test1234", password_confirmation: "test1234", username: "testuser"}@valid_attrs %{email: "[email protected]", username: "testuser"}
Then we’ll modify our creation test:
test "creates resource and redirects when data is valid", %{conn: conn} doconn = post conn, user_path(conn, :create), user: @valid_create_attrsassert redirected_to(conn) == user_path(conn, :index)assert Repo.get_by(User, @valid_attrs)end
And our update test:
test "updates chosen resource and redirects when data is valid", %{conn: conn} douser = Repo.insert! %User{}conn = put conn, user_path(conn, :update, user), user: @valid_create_attrsassert redirected_to(conn) == user_path(conn, :show, user)assert Repo.get_by(User, @valid_attrs)end
With our tests back to green, we need to modify the changeset function to change our password into a password digest on the fly:
def changeset(struct, params \\ %{}) dostruct|> cast(params, [:username, :email, :password, :password_confirmation])|> validate_required([:username, :email, :password, :password_confirmation])|> hash_passwordend
defp hash_password(changeset) dochangeset|> put_change(:password_digest, "ABCDE")end
Right now we’re just stubbing out the behavior of our hashing function. The first step is to make sure that we can modify our changeset as we go along. Let’s verify this behavior first. Go back to http://localhost:4000/users in our browser, click on “New user”, and create a new user with any details. When we hit the index page again, we should expect to see the user created with a password_digest value of “ABCDE”.
And run our tests again for this file. Our tests are passing, but we haven’t added any tests for this new hash_password work. Let’s add a test in our suite of model tests that will add a test on password digest:
test "password_digest value gets set to a hash" dochangeset = User.changeset(%User{}, @valid_attrs)assert get_change(changeset, :password_digest) == "ABCDE"end
This is a great step forward, but not terribly great for security! Let’s modify our hashes to be real password hashes with Bcrypt, courtesy of the comeonin library.
First, open up mix.exs and add :comeonin to our list of applications:
def application do[mod: {Pxblog, []},applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, :gettext,:phoenix_ecto, :postgrex, :comeonin]]end
And we’ll also need to modify our deps definition:
defp deps do[{:phoenix, "~> 1.2.0"},{:phoenix_pubsub, "~> 1.0"},{:phoenix_ecto, "~> 3.0"},{:postgrex, ">= 0.0.0"},{:phoenix_html, "~> 2.6"},{:phoenix_live_reload, "~> 1.0", only: :dev},{:gettext, "~> 0.11"},{:cowboy, "~> 1.0"},{:comeonin, "~> 2.3"}]end
Same here, note the addition of {:comeonin, “~> 2.3”}. Now, let’s shut down the server we’ve been running and run mix deps.get. If all goes well (it should!), then now you should be able to rerun iex -S mix phoenix.server to restart your server.
Our old hash_password method is neat, but we need it to actually hash our password. Since we’ve added the comeonin library, which provides us a nice Bcrypt module with a hashpwsalt method, so let’s import that into our User model.
In web/models/user.ex, add the following line to the top just under our use Pxblog.Web, :model line:
import Comeonin.Bcrypt, only: [hashpwsalt: 1]
What we’re doing here is pulling in the Bcrypt module under the Comeonin namespace and importing the hashpwsalt method with an arity of 1. And now we’re going to modify our hash_password method to work.
defp hash_password(changeset) doif password = get_change(changeset, :password) dochangeset|> put_change(:password_digest, hashpwsalt(password))elsechangesetendend
Let’s try creating a user again! This time, after entering in our data for username, email, password, and password confirmation, we should see an encrypted digest show up in the password_digest field!
Now, we’re going to want to work on the hash_password function that we added. The first thing we’re going to want to do is change the configuration for our testing environment to make sure our tests don’t slow down dramatically when working with our password encryption. Open up config/test.exs and add the following to the bottom:
config :comeonin, bcrypt_log_rounds: 4
This will configure ComeOnIn when it’s in our test environment to not try too hard to encrypt our password. Since this is only for tests, we don’t need anything super secure and would prefer sanity and speed instead! In config/prod.exs, we’ll want to instead change that to:
config :comeonin, bcrypt_log_rounds: 14
Let’s write a test for the comeonin call. We’ll make it a little less specific; we just want to verify the encryption. In test/models/user_test.exs:
test "password_digest value gets set to a hash" dochangeset = User.changeset(%User{}, @valid_attrs)assert Comeonin.Bcrypt.checkpw(@valid_attrs.password, Ecto.Changeset.get_change(changeset, :password_digest))end
For some more test coverage, let’s add a case to handle if the password = get_change() line is not hit:
test "password_digest value does not get set if password is nil" dochangeset = User.changeset(%User{}, %{email: "[email protected]", password: nil, password_confirmation: nil, username: "test"})refute Ecto.Changeset.get_change(changeset, :password_digest)end
Since assert/refute use truthiness, we can see if this block of code leaves password_digest blank, which it does! We’re doing a good job of covering our work with specs!
Let’s add a new controller, SessionController and an accompanying view, SessionView. We’ll start simple and build our way up to a better implementation over time.
Create web/controllers/session_controller.ex:
defmodule Pxblog.SessionController douse Pxblog.Web, :controller
def new(conn, _params) dorender conn, "new.html"endend
Create web/views/session_view.ex:
defmodule Pxblog.SessionView douse Pxblog.Web, :viewend
Create web/templates/session/new.html.eex:
<h2>Login</h2>
And finally, let’s update the router to include this new controller. Add the following line to our “/” scope:
resources "/sessions", SessionController, only: [:new]
The only route that we want to expose for the time being is new, so we’re going to limit it just to that. Again, we want to keep things simple and build up from a stable foundation.
Now let’s visit http://localhost:4000/sessions/new and we should expect to see the Phoenix framework header and the “Login” header.
Let’s give it a real form. Create web/templates/session/form.html.eex:
<%= form_for @changeset, @action, fn f -> %><%= if f.errors != [] do %><div class="alert alert-danger"><p>Oops, something went wrong! Please check the errors below:</p><ul><%= for {attr, message} <- f.errors do %><li><%= humanize(attr) %> <%= message %></li><% end %></ul></div><% end %>
<div class="form-group"><label>Username</label><%= text_input f, :username, class: "form-control" %></div>
<div class="form-group"><label>Password</label><%= password_input f, :password, class: "form-control" %></div>
<div class="form-group"><%= submit "Submit", class: "btn btn-primary" %></div><% end %>
And modify web/templates/session/new.html.eex to call our new form by adding one line:
<%= render "form.html", changeset: @changeset, action: session_path(@conn, :create) %>
The autoreloading will end up displaying an error page right now because we haven’t actually defined @changeset, which as you may guess needs to be a changeset. Since we’re working with the member object, which has username and password fields already on it, let’s use that!
In web/controllers/session_controller.ex, we need to alias the User model to be able to use it further. At the top of our class, under our use Pxblog.Web, :controller line, add the following:
alias Pxblog.User
And in the new function, modify the call to render as follows:
render conn, "new.html", changeset: User.changeset(%User{})
We need to pass it the connection, the template we’re rendering (minus the eex), and a list of additional variables that should be exposed to our templates. In this case, we want to expose @changeset, so we specify changeset: here, and we give it the Ecto changeset for the User with a blank User struct. (%User{} is a User Struct with no values set)
Refresh now and we get a different error message that should resemble the following:
No helper clause for Pxblog.Router.Helpers.session_path/2 defined for action :create.The following session_path actions are defined under your router:*:new
In our form, we are referencing a route that doesn’t actually exist yet! We are using the session_path helper, passing it the @conn object, but then specifying the :create path which we’ve not created yet.
We’ve gotten part of the way there. Now let’s make it so we can actually post our login details and set the session.
Let’s update our routes to allow posting to create.
In web/router.ex, change our reference to SessionController to also include :create.
resources "/sessions", SessionController, only: [:new, :create]
In web/controllers/session_controller.ex, we need to import a new function, checkpw from Comeonin’s Bcrypt module. We do this via the following line:
import Comeonin.Bcrypt, only: [checkpw: 2]
This line is saying “Import from the Comeonin.Bcrypt module, but only the checkpw function with an arity of 2”. And then let’s add a scrub_params plug to deal with User data. Before our functions, we’ll add:
plug :scrub_params, "user" when action in [:create]
“scrub_params” is a special function that cleans up any user input; in the case where something is passed in as a blank string, for example, scrub_params will convert it into a nil value instead to avoid creating entries in your database that have empty strings.
And let’s add our function to handle the create post. We’re going to add this to the bottom of our SessionController module. There’s going to be a lot of code here, so we’ll take it piece by piece.
In web/controllers/session_controller.ex:
def create(conn, %{"user" => user_params}) doRepo.get_by(User, username: user_params["username"])|> sign_in(user_params["password"], conn)end
The first bit of this code, Repo.get_by(User, username: user_params[“username”]) pulls the first applicable User from our Ecto Repo that has a matching username, or will otherwise return nil.
Here is some output to verify this behavior:
iex(3)> Repo.get_by(User, username: "flibbity")[debug] SELECT u0."id", u0."username", u0."email", u0."password_digest", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."username" = $1) ["flibbity"] OK query=0.7msnil
iex(4)> Repo.get_by(User, username: "test")[debug] SELECT u0."id", u0."username", u0."email", u0."password_digest", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."username" = $1) ["test"] OK query=0.8ms%Pxblog.User{__meta__: %Ecto.Schema.Metadata{source: "users", state: :loaded},email: "test", id: 15,inserted_at: %Ecto.DateTime{day: 24, hour: 19, min: 6, month: 6, sec: 14,usec: 0, year: 2015}, password: nil, password_confirmation: nil,password_digest: "$2b$12$RRkTZiUoPVuIHMCJd7yZUOnAptSFyM9Hw3Aa88ik4erEsXTZQmwu2",updated_at: %Ecto.DateTime{day: 24, hour: 19, min: 6, month: 6, sec: 14,usec: 0, year: 2015}, username: "test"}
We then take the user, and chain that user into a sign_in method. We haven’t written that yet, so let’s do so!
defp sign_in(user, password, conn) when is_nil(user) doconn|> put_flash(:error, "Invalid username/password combination!")|> redirect(to: page_path(conn, :index))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))elseconn|> put_session(:current_user, nil)|> put_flash(:error, "Invalid username/password combination!")|> redirect(to: page_path(conn, :index))endend
The first thing to notice is the order that these methods are defined in. The first of these methods has a guard clause attached to it, so that method will only be executed when that guard clause is true, so if the user is nil, we redirect back to the index of the page (root) path with an appropriate flash message.
The second method will get called if the guard clause is false and will handle all other scenarios. We check the result of that checkpw function, and if it is true, we set the user to the current_user session variable and redirect with a success message. Otherwise, we clear out the current user session, set an error message, and redirect back to the root.
If we return to our login page http://localhost:4000/sessions/new, we should be able to test out login with a valid set of credentials and invalid credentials and see the appropriate error messages!
We need to write some specs for this controller, as well. We’ll create test/controllers/session_controller_test.exs and fill it with the following:
defmodule Pxblog.SessionControllerTest douse Pxblog.ConnCasealias Pxblog.User
setup doUser.changeset(%User{}, %{username: "test", password: "test", password_confirmation: "test", email: "[email protected]"})|> Repo.insert{:ok, conn: build_conn()}end
test "shows the login form", %{conn: conn} doconn = get conn, session_path(conn, :new)assert html_response(conn, 200) =~ "Login"end
test "creates a new user session for a valid user", %{conn: conn} doconn = post conn, session_path(conn, :create), user: %{username: "test", password: "test"}assert get_session(conn, :current_user)assert get_flash(conn, :info) == "Sign in successful!"assert redirected_to(conn) == page_path(conn, :index)end
test "does not create a session with a bad login", %{conn: conn} doconn = post conn, session_path(conn, :create), user: %{username: "test", password: "wrong"}refute get_session(conn, :current_user)assert get_flash(conn, :error) == "Invalid username/password combination!"assert redirected_to(conn) == page_path(conn, :index)end
test "does not create a session if user does not exist", %{conn: conn} doconn = post conn, session_path(conn, :create), user: %{username: "foo", password: "wrong"}assert get_flash(conn, :error) == "Invalid username/password combination!"assert redirected_to(conn) == page_path(conn, :index)endend
We start off with our standard setup block, and write a pretty standard assertion for a get request. The creation bits are where this starts to get more interesting:
test "creates a new user session for a valid user", %{conn: conn} doconn = post conn, session_path(conn, :create), user: %{username: "test", password: "test"}assert get_session(conn, :current_user)assert get_flash(conn, :info) == "Sign in successful!"assert redirected_to(conn) == page_path(conn, :index)end
The first bit here is posting to our session creation path. We then verify that we set the current_user session variable, the flash message for the action, and finally, we assert where we are being redirected to.
The other two calls are just using the same sort of assertions (and in one case, a refutation) to make sure we’re testing all of the paths that the sign_in function can hit. Again, very simple stuff!
Let’s modify our layout to display a message or a link depending on if the member is logged in or not.
In web/views/layout_view.ex, let’s write a helper that will make it easy for us to access the user information, so we’ll add:
def current_user(conn) doPlug.Conn.get_session(conn, :current_user)end
Let’s write a test to make sure this works.
In web/templates/layout/app.html.eex, instead of the “Get Started” link, let’s do the following:
<li><%= if user = current_user(@conn) do %>Logged in as<strong><%= user.username %></strong><br><%= link "Log out", to: session_path(@conn, :delete, user.id), method: :delete %><% else %><%= link "Log in", to: session_path(@conn, :new) %><% end %></li>
Again, let’s step through this piece by piece. One of the first things we need to do is figure out who the current user is, assuming they’re logged in. We’re going with a simple first, refactor later approach, so for right now we’re going to just set a user object from the session right in our template. get_session is part of the Plug.Conn object. If the user exists (this takes advantage of Elixir’s Ruby-like truthiness values in that nil will return false here.)
If the user is logged in, we’ll also want to provide a logout link as well. Even though this does not exist yet, it will eventually need to exist, so for right now we’re going to send it along. We’ll treat a session like a resource, so to logout, we’ll “delete” the session, so we’ll provide a link to it here.
We also want to output the current user’s username. We’re storing the user struct in the :current_user session variable, so we can just access the username as user.username.
If we could not find the user, then we’ll just provide the login link. Again, we’re treating sessions like a resource here, so “new” will provide the appropriate route to create a new session.
You probably noticed that when everything refreshed, we’re getting another error message about a missing matching function clause. Let’s add our delete route as well to keep Phoenix happy!
In web/router.ex, we’ll modify our “sessions” route to also allow :delete:
resources "/sessions", SessionController, only: [:new, :create, :delete]
And let’s modify the controller as well. In web/controllers/session_controller.ex, add the following:
def delete(conn, _params) doconn|> delete_session(:current_user)|> put_flash(:info, "Signed out successfully!")|> redirect(to: page_path(conn, :index))end
Since we’re just deleting the :current_user key, we don’t actually care what the params are, so we mark those as unused with an underscore. We set a flash message to make the UI a little more clear to the user and redirect back to our root route.
We can now log in, log out, and see login failures too! Things are shaping up for the best! But first, we need to write some tests. We’ll start with the tests for our LayoutView.
defmodule Pxblog.LayoutViewTest douse Pxblog.ConnCase, async: true
alias Pxblog.LayoutViewalias Pxblog.User
setup doUser.changeset(%User{}, %{username: "test", password: "test", password_confirmation: "test", email: "[email protected]"})|> Repo.insert{:ok, conn: build_conn()}end
test "current user returns the user in the session", %{conn: conn} doconn = post conn, session_path(conn, :create), user: %{username: "test", password: "test"}assert LayoutView.current_user(conn)end
test "current user returns nothing if there is no user in the session", %{conn: conn} douser = Repo.get_by(User, %{username: "test"})conn = delete conn, session_path(conn, :delete, user)refute LayoutView.current_user(conn)endend
Let’s analyze. The first thing we’re going to do is alias the LayoutView and User modules so that we can shorten some of our code. Next, in our setup block, we create a User and throw it into the database. We then return our standard {:ok, conn: build_conn()} tuple.
We then write our first test by logging in to our session create action and asserting that, after logging in, the LayoutView.current_user function returns some data.
We then write for our negative case; we explicitly delete the session and refute that a user is returned out of our current_user call. We also updated our SessionController by adding a delete action, so we need to get our tests all set.
test "deletes the user session", %{conn: conn} douser = Repo.get_by(User, %{username: "test"})conn = delete conn, session_path(conn, :delete, user)refute get_session(conn, :current_user)assert get_flash(conn, :info) == "Signed out successfully!"assert redirected_to(conn) == page_path(conn, :index)end
We make sure that current_user from the session is blank, then we check the flash message and make sure we get redirected out!
One thing to note is that you may end up hitting an error when you’re trying to compile the assets with brunch. The error message I got was:
16 Dec 23:30:20 — error: Compiling of ‘web/static/js/app.js’ failed. Couldn’t find preset “es2015” relative to directory “web/static/js” ; Compiling of ‘web/static/js/socket.js’ failed. Couldn’t find preset “es2015” relative to directory “web/static/js” ; Compiling of ‘deps/phoenix/web/static/js/phoenix.js’ failed. Couldn’t find preset “es2015” relative to directory “deps/phoenix/web/static/js” ; Compiling of ‘deps/phoenix_html/web/static/js/phoenix_html.js’ failed. Couldn’t find preset “es2015” relative to directory “deps/phoenix_html/web/static/js”
You can fix this with installing babel-preset-es2015 with NPM. I ran the following command:
npm install -g babel-preset-es2015
Now if you start up the server you should see all of the assets correctly compiled in!
Writing a Blog Engine in Phoenix and Elixir: Part 2, Authorization_Last Updated At: 07/21/2016_medium.com
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:
Phoenix Web Development | PACKT Books_Learn to build a high-performance functional prototype of a voting web application from scratch using Elixir and…_www.packtpub.com
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!