How to start with testing


#1

I’d like to implement tests in the existing app and don’t know how and where to start from :frowning: The app is using ember-simple-auth with Implicit Grant authentication:

  • it hits a corporate authentication gate to enter username and password: https://company-gateway/authorization.oauth2?client_id=xxxx&redirect_uri=http://localhost:4200/callback&response_type=token&scope=profile%20openid
  • If all is correct, it’s redirect to the client callback with an API Management token:
HTTP 302 Redirect
Location https://myapp/callback#access_token=2YotnFZFEjr1zCsicMWpAA&type=Bearer&expire_in=3600&state=myAppRandomState
  • it hits the back-end users/me end-point with Authorization: Bearer <token> in the header to grab user data.

All the routes on Ember side are protected and there is no login page. The index route looks like that:

#routes/index.js

import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import config from '../config/environment';
import isFastBoot from 'ember-simple-auth/utils/is-fastboot';
import UnauthenticatedRouteMixin from 'ember-simple-auth/mixins/unauthenticated-route-mixin';

export default Route.extend(UnauthenticatedRouteMixin, {
  session:      service('session'),
   _isFastBoot: isFastBoot(),

  beforeModel: function() {
    if (this.get('session.isAuthenticated')) {
      this.transitionTo('dashboard');
    } else {
      if (!this.get('_isFastBoot')) {
        let oauthUrl = config.oauthUrl;
        let clientId = config.clientID;
        let redirectURI = `${window.location.origin}/callback`;
        let responseType = `token`;
        let scope = `profile%20openid`;
        window.location.replace(oauthUrl
                              + `?client_id=${clientId}`
                              + `&redirect_uri=${redirectURI}`
                              + `&response_type=${responseType}`
                              + `&scope=${scope}`
        );
      }
    }
  }
});

And the application route is defined as follows:

#routes/application.js

import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin';
import moment from 'moment';

export default Route.extend(ApplicationRouteMixin, {
  currentUser: service(),
  intl: service(),
  constants: service(),

  beforeModel() {
    let locale = this.figureOutLocale();
    this.intl.setLocale(locale);
    moment.locale(locale);
    return this._loadCurrentUser();
  },

  sessionAuthenticated() {
    this._super(...arguments);
    this._loadCurrentUser();
  },

  _loadCurrentUser() {
    return this.get('currentUser').loadCurrentUser().catch(() => this.get('session').invalidate());
  },
...

And, finally, current-user service looks liek that:

#services/current-user.js

import RSVP from 'rsvp';
import Service, { inject as service } from '@ember/service';

export default Service.extend({
  session:        service(),
  store:          service(),
  flashMessages:  service(),
  intl:           service(),
  currentShop:    service(),
  shopService:    service(),

  async loadCurrentUser() {
    this.get('flashMessages').clearMessages();
    if (this.get('session.isAuthenticated')) {
      let user = await this.get('store').queryRecord('user', { me: true });
      if (user) {
        this.get('flashMessages').success(this.get('intl').t('flash.signed_in'));
        this.get('currentShop').setShop(user.get('shop'));
        this.set('user', user);
        this.get('shopService').loadShops();
        return user;
      } else {
        this._respondWithError();
      }

    } else {
      this.get('flashMessages').info(this.get('intl').t('flash.signed_off'));
      return RSVP.resolve();
    }
  },

  _respondWithError() {
    this.get('flashMessages').danger(this.get('intl').t('flash.authentication.error'));
    return RSVP.reject();
  }
});

I’m stuck with Mirage and have no idea how to proceed in such a case. Any ideas to put me on the right way :slight_smile: ?

Thanks a lot !


#2

In short, I’ll have to provide a token for every request. ESA supplies some testing helpers for acceptance tests. I tried one as follows:

#tests/acceptance/dashboard-test.js

import { module, test } from 'qunit';
import { visit, currentURL } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import { authenticateSession } from 'ember-simple-auth/test-support';
import setupMirageTest from 'ember-cli-mirage/test-support/setup-mirage';

module('Acceptance | Dashboard', function(hooks) {
  setupApplicationTest(hooks);
  setupMirageTest(hooks);

  test('Authenticated users can visit /dashboard', async function(assert) {
     let shop = this.server.create('shop');
    this.server.create('user', { shop });
    await authenticateSession({
      token: 'abcdDEF',
      token_type: 'Bearer'
    });

    await visit('/dashboard');

    assert.equal(currentURL(), '/dashboard', 'user is on dashboard page');
  });
});

but it fails with 'Error: Browser timeout exceeded: 10s':

ember test tests/acceptance/dashboard-test.js

Built project successfully. Stored in "/Users/Serguei/projects/decastore/decastore-front/tmp/class-tests_dist-pCh6z5j4.tmp".
ok 1 Chrome 71.0 - [2 ms] - ESLint | mirage: mirage/config.js
ok 2 Chrome 71.0 - [0 ms] - ESLint | mirage: mirage/scenarios/default.js
ok 3 Chrome 71.0 - [0 ms] - ESLint | mirage: mirage/serializers/application.js
not ok 4 Chrome - [undefined ms] - error
    ---
        message: >
            Error: Browser timeout exceeded: 10s
            Error while executing test: Acceptance | Dashboard: Authenticated users can visit /dashboard
            Stderr: 
             [0208/095941.065778:ERROR:gpu_process_transport_factory.cc(967)] Lost UI shared context.
...

I defined the only end-point in mirage/config.js- dashboard:

...
this.get('/dashboard');

I suppose that I’ll also have to add users/me end-point to Mirage config.js file. But what should I put if the response send by users/me is the following JSON:

{"data":{"id":"2","type":"users","attributes":{"first_name":"XXXX","last_name":"XXX","username":"RRRRRRR"},"relationships":{"shop":{"data":{"id":"879","type":"shops"}}}},"included":[{"id":"879","type":"shops","attributes":{"category":"AZERTYU","fax":null,"identifier":927,"leader":"SDFGHJK","modified_by":"TYUIOP","name":"惠山店","opening_date":"2014-12-26","phone":"0","status":"open","updated_at":"2018-12-14T14:27:01.148Z"},"relationships":{"country":{"data":{"id":"14","type":"countries"}}}}]}

I have an impression that I create a User with Mirage factory but when mocking the authentication response, the token is wrong and the backend can’t figure out how to decode the fake token and find a User to return back from users/me end-point.

Here is how mirage config.js is defined:

this.get('/dashboard');
this.get('/users/me', (schema) => {
    return schema.users.first;
});
this.get('/shops');

What am I missing ?


#3

@belgoros that error doesn’t seem specifically related to your mirage config or authentication (though I can’t rule it out completely). Based on a little googling it seems like it might be more testem/ember-qunit related. Try checking out these issues and see if you can find anything:

ember qunit issue #1

ember qunit issue #2

testem issue

FWIW you shouldn’t need the “dashboard” endpoint in your mirage config (unless you have an ember model called “dashboard” or you need a backend endpoint for dashboard requests) but that probably isn’t causing any issues so that’s unrelated, just noting.


#4

Hi @belgoros,

Are you using the ember-simple-auth-token addon as well? You’ll probably need to tell the addon not to create a timer to refresh the token. The testing framework waits until the app ‘settles’ before moving on - that is there should be no active timers.

From https://github.com/jpadilla/ember-simple-auth-token -

For acceptance testing, token refresh must be disabled to allow the test to exit. Therefore, the following configuration should be set:

// config/environment.js
if (environment == 'test) {
  ENV['ember-simple-auth-token'] = {
    refreshAccessTokens: false,
    tokenExpirationInvalidateSession: false,
  };
}

#5

Thank you, Dan, for your response. After chatting on ec-mirage and topic-testingat Discord, it seems like the problem is related to the use of window.location.replace() in my index route. As suggested by folks, I tried to use ember-window-mock add-on but still getting the test failed:


#6

ah well that’s at least progress! So it looks like the redirect from index -> dashboard (if authenticated) isn’t working… maybe try putting a breakpoint in routes/index.js at the top of the beforeModel hook and seeing if the session still isn’t authenticated properly? Otherwise I’d expect it to do the redirect and pass…

My guess is what’s happening is that ESA still doesn’t think it’s authenticated so when you visit('dashboard') it kicks you back to index, which never redirects back to dashboard.


#7

I’d say, the app is working pretty well, where I’m stuck is in this first and trivial test :slight_smile:


#8

After putting a break point in beforeModel hook in index route, the User is not authenticated and I pass into the else block. The questions I have for the moment are:

  • should I reset the url in my test as window.location.replace('/'); or else ?
  • should I declare/copy ENV variables values from development section in environment.js to test? If not, the OAuth URL will contain undefined for client_id, redirect_url, etc.. It means that ember-window-mock does not reset to blank ?

More of that, if I check the assert:

assert.equal(window.location.href, '/dashboard', 'user is on dashboard page');

the error is different:

user is on dashboard page@ 189 ms
Expected: 	
"/dashboard"
Result: 	
"https://corporate-gateway-url?client_id=XXX&redirect_uri=http://localhost:7357/callback&response_type=token&scope=profile%20openid"
Diff: 	
"https://corporate-gateway-url?client_id=XXXX&redirect_uri=http://localhost:7357/callback&response_type=token&scope=profile%20openid"

compared to the use of currentURL() helper:

assert.equal(currentURL(), '/dashboard', 'user is on dashboard page');
|Expected:|"/dashboard"|
|Result:|"/"|
|Diff:|"/<del>dashboard</del>"|
|Source:|at Object.&lt;anonymous&gt; (http://localhost:7357/assets/tests.js:23:14)|

It is really weird and frustrating…


#9

So ideally I think you’d want this test to skip anything related to your implicit grant auth, but it doesn’t seem like it is. Makes me think the issue is with this part somehow:

    await authenticateSession({
      token: 'abcdDEF',
      token_type: 'Bearer'
    });

If your app doesn’t think that it’s authenticated it will (rightfully) fail the test because it will try to redirect to the auth page. AFAIK the default implicit grant authenticator requires the token be called “access_token” so maybe try this:

    await authenticateSession({
      access_token: 'abcdDEF',
      token_type: 'Bearer'
    });

?


#10

Hi Dan ! Thank you for your response. Yes, as all my routes are protected except index route. In my acceptance tests I’d like to ensure that a user is logged in before running the tests. So in BDD style, it would be alike:

Given a User is logged in
When I visit the products page
Then I should see a products list

I digged deeper and found that, the value to use was access_token.

It was proven by displaying the value this.get('session.data.authenticated') in the console:

this.get('session.data.authenticated');
=>  {authenticator: "authenticator:oauth2-implicit-grant", access_token: "eyJhbGciOiJSUzI1NiIsImtpZCI6Ik1BSU4ifQ.eyJzY29wZSI…", token_type: "Bearer", expires_in: "7199"}

Unfortunately, changing for access_token:

#tests/acceptance/dashboard-test.js

import { module, test } from 'qunit';
import { visit, currentURL } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import { authenticateSession } from 'ember-simple-auth/test-support';
import setupMirageTest from 'ember-cli-mirage/test-support/setup-mirage';
import { setupWindowMock } from 'ember-window-mock';

module('Acceptance | Dashboard', function(hooks) {
  setupWindowMock(hooks);
  setupApplicationTest(hooks);
  setupMirageTest(hooks);


  test('Authenticated users can visit /dashboard', async function(assert) {
    let shop = this.server.create('shop');
    this.server.create('user', { shop });

    await authenticateSession({
      access_token: 'abcdDEF',
      token_type: 'Bearer'
    });

    await visit('/dashboard');

    assert.equal(currentURL(), '/dashboard', 'user is on dashboard page');
  });
});

hasn’t solved the problem and the value of this.get('session.isAuthenticated') in index.js route is just empty and I’m still redirected to the corporate login page. Maybe, that’s the problem, - because I’ll have to enter username and password on that page and I just can’t mock this behavior. So ESA is waiting to be authenticated and after a timeout happens, the value of this.get('session.isAuthenticated') is not changed and stays false:

#routes/index.js

export default Route.extend(UnauthenticatedRouteMixin, {
  session:      service('session'),
   _isFastBoot: isFastBoot(),

  beforeModel: function() {
    console.log("++++++++++++ is Authenticated: " + this.get('session.isAuthenticated'));
    if (this.get('session.isAuthenticated')) {
      this.transitionTo('dashboard');
    } else {
      if (!this.get('_isFastBoot')) {
        let oauthUrl = config.oauthUrl;
        let clientId = config.clientID;
        let redirectURI = `${window.location.origin}/callback`;
        let responseType = `token`;
        let scope = `profile%20openid`;
        window.location.replace(oauthUrl
                              + `?client_id=${clientId}`
                              + `&redirect_uri=${redirectURI}`
                              + `&response_type=${responseType}`
                              + `&scope=${scope}`
        );
      }
    }
  }
});

#11

Hi @mikeburg ! Thank you for your response. No, I’m not using ember-simple-auth-token.


#12

I also added missing this._super(...arguments) to beforeModel hook, as it is suggested in ESA docs, but still no success. I opened an issue at ESA repo, may some of their folks could figure out what am I missing.