In this exercise, we will be adding redirection in the UI flow based on whether the user is logged in or not.
Specifically, users should be able to access the login
route only when they are not logged in. And similarly, users should be able to access the teams
route only when they are logged in.
So let's get started.
First let's enhance the auth
service, so that we can easily tell if there's a logged in user, and implement a logout
action. This auth
service is defined at app/services/auth.js
.
Import the action decorator at the top of the file:
import { action } from '@ember/object';
Then add a getter-based property, isAuthenticated
that will be true
if the user is logged in and false
otherwise. Add an action called logout
that clears the user ID we write to localStorage on login, and sends the user to the /login
page.
get isAuthenticated() {
return !!this.currentUserId;
}
@action
logout() {
window.localStorage.removeItem(AUTH_KEY);
this.router.transitionTo('login');
}
Remember the StubbedAuthService
that we just added? It needs to have the same interface as the real auth service. Let’s add isAuthenticated
and logout
to StubbedAuthService
too:
get isAuthenticated() {
return !!this.currentUserId;
}
@action
logout() {
this.currentUserId = null;
this.router.transitionTo('login');
}
We also need to import action
at the top of the file:
import { action } from '@ember/object';
In the login route, present at app/routes/login.js
, add a beforeModel
hook to check whether the user is authenticated, and either redirect the user or not.
In the login route, import the auth
service, since its not already available(injected).
import { inject as service } from '@ember/service';
import AuthService from 'shlack/services/auth';
And, in the same route file(login
route), inject the imported auth
service and add the following beforeModel
hook to handle redirection.
/**
* @type {AuthService}
*/
@service auth;
async beforeModel(transition) {
await super.beforeModel(transition);
if (this.auth.isAuthenticated) {
this.transitionTo('teams');
}
}
The jsdoc comment above, for AuthService
, improves the developer experience when editing the code (that is, this.auth.*
will have a nice autocomplete).
Now let's move on to teams
route defined at app/routes/teams.js
.
The login
route should have a similar beforeModel
hook, but note that the validation logic is flipped in this case. We redirect to the /login
page only if the user is logged out.
In the teams
route, import the auth
service, since its not already available(injected).
// app/routes/teams.js
import { inject as service } from '@ember/service';
import AuthService from 'shlack/services/auth';
And, in the same route file(app/routes/teams.js
), inject the imported auth
service and add the beforeModel
hook to check if the user is logged in.
/**
* @type {AuthService}
*/
@service auth;
async beforeModel(transition) {
await super.beforeModel(transition);
if (!this.auth.isAuthenticated) {
this.transitionTo('login');
}
}
The jsdoc comment above, for AuthService
, improves the developer experience when editing the code (that is, this.auth.*
will have a nice autocomplete).
Now that we have the javascript part of our component in place, let's work on the matching handlebars template.
In app/templates/components/team-sidebar.hbs
, replace the LinkTo
component with a plain old HTML button
element with an onclick handler that will trigger the logout
action, that you defined in app/services/auth.js
.
- <LinkTo @route='login' {{! destination route }}
- @tagName="button" {{! use <button> instead of <a> }}
- class="text-white rounded bg-grey-darker hover:bg-red-darker p-2 team-sidebar__logout-button" {{! HTML classes}}
+ <button {{on "click" this.auth.logout}} class="text-white rounded bg-grey-darker hover:bg-red-darker p-2 team-sidebar__logout-button"
+ >
Logout
- </LinkTo>
+ </button>
Now that we have the implementation in place for our redirect logic, let's add some acceptance tests for the same.
Reasons why acceptance tests are preferred (over unit tests or an integration tests):
-
Rather than just testing individual modules in isolation, we need to test and verify that features (in this case, conditional redirection) work as expected from an end user's perspective, when its part of the entire application.
-
The need to mimic user interactions with the application, and verify things work as expected.
In tests/acceptance/login-test.js
, import the StubbedAuthService
service:
import StubbedAuthService from '../test-helpers/auth-service';
Then, inject the auth service inside the beforeEach
hooks for test setup.
hooks.beforeEach(function () {
this.owner.register('service:auth', StubbedAuthService);
});
Modify the test with label, starting logged out, then logging in
as follows:
test('starting logged out, then logging in', async function (assert) {
const auth = this.owner.lookup('service:auth');
auth.currentUserId = null;
await visit('/login');
assert.equal(currentURL(), '/login');
await fillIn('select', '1');
await click('form input[type="submit"]');
assert.equal(currentURL(), '/teams');
});
Then add a test for the use case when the user is already logged in
, as follows:
test('already logged in', async function (assert) {
const auth = this.owner.lookup('service:auth');
auth.currentUserId = '1';
await visit('/login');
assert.equal(currentURL(), '/teams');
});
Now let's add acceptance tests to test when users are logged out. The test file is present at tests/acceptance/logout-test.js
.
And as in the previous test, first import the StubbedAuthService
service:
import StubbedAuthService from '../test-helpers/auth-service';
For test with label, 'visiting /teams', remove the definition.
And add a test with label, visiting /teams while logged in, and then logging out
.
Modify the beforeEach
in the same way we did for the previous test.
hooks.beforeEach(function () {
this.owner.register('service:auth', StubbedAuthService);
});
Then add the test for accessing teams
route while being logging in, and then logging out:
test('visiting /teams while logged in, and then logging out', async function (assert) {
const auth = this.owner.lookup('service:auth');
auth.currentUserId = '1';
await visit('/teams'); // Go to a URL
assert.equal(currentURL(), '/teams'); // Make sure we've arrived
await click('.team-sidebar__logout-button'); // Click a button
assert.equal(currentURL(), '/login'); // Make sure we're now at /login
});
And finally, let's add a test for the use case when visiting /teams while logged out
.
test('visiting /teams while logged out', async function (assert) {
const auth = this.owner.lookup('service:auth');
auth.currentUserId = null;
await visit('/teams');
assert.equal(currentURL(), '/login');
});