Problem
I have a /login route that uses ember-simple-auth to implement authentication. During testing ember-cli-mirage is used to mock the backend. The user logs in by providing their email address and password.
In total I have 4 acceptance tests for this route, similar to the test below:
test('should show error message for invalid email', function(assert) {
visit('/login');
fillIn('input#email', 'invalid-email');
fillIn('input#password', 'invalid-password');
click('button.button');
andThen(function() {
assert.equal(find('div.notification').text(), "Invalid email/password");
});
});
When I run the tests using ember t only the first test in the file fails. If I comment this test out, the next one fails, and so on. If I run the tests in server mode with ember t -s the same test fails; however, when I press enter to re-run the tests, all the tests pass.
The failure message is always the same, shown below:
not ok 7 PhantomJS 2.1 - Acceptance | login: should show error message for invalid email
---
actual: >
expected: >
Invalid email/password
stack: >
http://localhost:7357/assets/tests.js:22:19
andThen#http://localhost:7357/assets/vendor.js:48231:41
http://localhost:7357/assets/vendor.js:48174:24
isolate#http://localhost:7357/assets/vendor.js:49302:30
http://localhost:7357/assets/vendor.js:49258:23
tryCatch#http://localhost:7357/assets/vendor.js:68726:20
invokeCallback#http://localhost:7357/assets/vendor.js:68738:21
publish#http://localhost:7357/assets/vendor.js:68709:21
http://localhost:7357/assets/vendor.js:48192:24
invoke#http://localhost:7357/assets/vendor.js:10892:18
flush#http://localhost:7357/assets/vendor.js:10960:15
flush#http://localhost:7357/assets/vendor.js:11084:20
end#http://localhost:7357/assets/vendor.js:11154:28
run#http://localhost:7357/assets/vendor.js:11277:19
run#http://localhost:7357/assets/vendor.js:32073:32
http://localhost:7357/assets/vendor.js:48783:24
Log: |
After all the tests have run, test emits an exception:
# tests 60
# pass 59
# skip 0
# fail 1
Not all tests passed.
Error: Not all tests passed.
at EventEmitter.getExitCode (/home/jon/projects/jonblack/wishlist-web/node_modules/testem/lib/app.js:434:15)
at EventEmitter.exit (/home/jon/projects/jonblack/wishlist-web/node_modules/testem/lib/app.js:189:23)
at /home/jon/projects/jonblack/wishlist-web/node_modules/testem/lib/app.js:103:14
at tryCatcher (/home/jon/projects/jonblack/wishlist-web/node_modules/testem/node_modules/bluebird/js/release/util.js:16:23)
at Promise._settlePromiseFromHandler (/home/jon/projects/jonblack/wishlist-web/node_modules/testem/node_modules/bluebird/js/release/promise.js:510:31)
at Promise._settlePromise (/home/jon/projects/jonblack/wishlist-web/node_modules/testem/node_modules/bluebird/js/release/promise.js:567:18)
at Promise._settlePromise0 (/home/jon/projects/jonblack/wishlist-web/node_modules/testem/node_modules/bluebird/js/release/promise.js:612:10)
at Promise._settlePromises (/home/jon/projects/jonblack/wishlist-web/node_modules/testem/node_modules/bluebird/js/release/promise.js:691:18)
at Async._drainQueue (/home/jon/projects/jonblack/wishlist-web/node_modules/testem/node_modules/bluebird/js/release/async.js:138:16)
at Async._drainQueues (/home/jon/projects/jonblack/wishlist-web/node_modules/testem/node_modules/bluebird/js/release/async.js:148:10)
at Immediate.Async.drainQueues (/home/jon/projects/jonblack/wishlist-web/node_modules/testem/node_modules/bluebird/js/release/async.js:17:14)
at runCallback (timers.js:637:20)
at tryOnImmediate (timers.js:610:5)
at processImmediate [as _immediateCallback] (timers.js:582:5)
It seems odd that this is emitted for tests failing rather than just reporting the test failure, so perhaps it's related.
Running the tests in Firefox and Chromium work, as does running the application in development mode and logging in manually. The problem is limited to phantomjs.
I have other acceptance tests for another route and these all pass. It seems limited to the /login route, suggesting that it is possibly related to authentication.
Debugging
I've tried debugging by adding pauseTest() to the test and "phantomjs_debug_port": 9000 to testem.js but both Firefox and Chromium do nothing when I use the debug console. This might be my lack of experience debugging phantomjs, but I would at least expect it to give me an error - it literally does nothing.
It feels as though there is a timing issue between phantomjs and something, possible ember-simple-auth, in my Ember app.
I'm not that experienced debugging phantomjs problems nor Ember acceptance test failures, so any help is appreciated.
Versions
ember-cli 2.10.0
ember-simple-auth 1.1.0
ember-cli-mirage 0.2.4
Update 1
The button is inside a login-form component:
<form {{action 'login' on='submit'}}>
<p class="control has-icon">
{{input value=email id='email' placeholder='email' class='input'}}
<i class="fa fa-envelope"></i>
</p>
<p class="control has-icon">
{{input value=password id='password' placeholder='password'
type='password' class='input'}}
<i class="fa fa-lock"></i>
</p>
<p class="control">
<button class="button is-success" disabled={{isDisabled}}>Log In</button>
</p>
</form>
The component's login action just calls the passed in login handler:
import Ember from 'ember';
export default Ember.Component.extend({
email: "",
password: "",
isDisabled: Ember.computed('email', 'password', function() {
return this.get('email') === "" || this.get('password') === "";
}),
actions: {
login() {
var email = this.get('email');
var password = this.get('password');
this.attrs.login(email, password);
}
}
});
Which is the authenticate method in the login controller:
import Ember from 'ember';
export default Ember.Controller.extend({
session: Ember.inject.service(),
actions: {
authenticate(email, password) {
this.get('session').authenticate('authenticator:oauth2', email, password).catch((data) => {
this.set('errors', data['errors']);
});
}
}
});
Update 2
As suggested by Daniel I added a delay to the test:
test('should show error message for invalid email', function(assert) {
visit('/login');
fillIn('input#email', 'invalid-email');
fillIn('input#password', 'invalid-password');
click('button.button');
andThen(function() {
Ember.run.later(this, function() {
assert.equal(find('div.notification').text(), "Invalid email/password");
}, 0);
});
});
Using only Ember.run.later the test still failed, but putting that inside the andThen causes it to pass. Have you noticed the bizarre part? The delay is 0 milliseconds.
I still want to find an explanation for this because I don't trust that this will run the same on whatever machine the tests run on.
Update 3
Today I had a surprise: suddenly the tests were working again!
I added a new route with acceptance tests. The route itself is an authenticated route, so the tests use the authenticateSession test helper from ember-simple-auth to authenticate.
when I remove the tests that use this helper, the error returns!.
I'm not sure what this means. It feels like the issue is with ember-simple-auth, but it might also be a giant coincidence that the helper resolves another timing issue.
Down the rabbit hole we go...
Update 4
Below is the configuration for the auth endpoints in ember-cli-mirage:
this.post('/token', function({db}, request) {
var data = parsePostData(request.requestBody);
if (data.grant_type === 'password') {
// Lookup user in the mirage db
var users = db.users.where({ email: data.username });
if (users.length !== 1) {
return new Mirage.Response(400, {'Content-Type': 'application/json'}, {
errors: [{
id: 'invalid_login',
status: '400',
title: 'Invalid email/password',
}]
});
}
var user = users[0];
// Check password
if (data.password === user.password) {
if (!user.active) {
return new Mirage.Response(400, {'Content-Type': 'application/json'}, {
errors: [{
id: 'inactive_user',
status: '400',
title: 'Inactive user',
}]
});
} else {
return new Mirage.Response(200, {
'Content-Type': 'application/json'
}, {
access_token: 'secret token!',
user_id: user.id
});
}
} else {
return new Mirage.Response(400, {'Content-Type': 'application/json'}, {
errors: [{
id: 'invalid_login',
status: '400',
title: 'Invalid email/password',
}]
});
}
} else {
return new Mirage.Response(400, {'Content-Type': 'application/json'}, {
errors: [{
id: 'invalid_grant_type',
status: '400',
title: 'Invalid grant type',
}]
});
}
});
this.post('/revoke', function(db, request) {
var data = parsePostData(request.requestBody);
if (data.token_type_hint === 'access_token' ||
data.token_type_hint === 'refresh_token') {
return new Mirage.Response(200, {'Content-Type': 'application/json'});
} else {
return new Mirage.Response(400, {'Content-Type': 'application/json'},
{error: 'unsupported_token_type'});
}
});
Update 5
Here's my config/environment.js file:
/* jshint node: true */
module.exports = function(environment) {
var ENV = {
modulePrefix: 'wishlist-web',
environment: environment,
rootURL: '/',
locationType: 'auto',
EmberENV: {
FEATURES: {
},
EXTEND_PROTOTYPES: {
// Prevent Ember Data from overriding Date.parse.
Date: false
}
},
APP: {
}
};
if (environment === 'development') {
}
if (environment === 'test') {
// Testem prefers this...
ENV.locationType = 'none';
// keep test console output quieter
ENV.APP.LOG_ACTIVE_GENERATION = false;
ENV.APP.LOG_VIEW_LOOKUPS = false;
ENV.APP.rootElement = '#ember-testing';
}
if (environment === 'production') {
ENV.ServerTokenEndpoint = 'http://localhost:9292/token';
ENV.ServerTokenRevocationEndpoint = 'http://localhost:9292/revoke';
ENV.ApiHost = 'http://localhost:9292';
}
return ENV;
};
You have few things to try here to debug this issue.
You could remove {{isDisabled}} from button to make sure it's not disabled when you try to click it.
Use setTimeout instead of andThen and see if it's timing issue.
Replace authenticate action code with nothing, to make sure it isn't causing your test to fail.
You could also rewrite test to put your assert.ok after some event in JavaScript. For example you could mock authenticate action or observer errors property. You can do this by using lookups or registers in acceptance environment - tests from one of my Ember CLI addons could help you - ember-link-action/tests/acceptance/link-action-test.js.
Edit
Having seen what worked for you experience tells me that you should try 2 things.
For this code:
andThen(function() {
Ember.run.later(this, function() {
assert.equal(find('div.notification').text(), "Invalid email/password");
}, 0);
});
You could try Ember.run.scheduleOnce('afterRender', this, () => { ... assert here } instead of using Ember.run.later. Or you could try using just Ember.run instead of Ember.run.later.
Conclusion: The key to fixing this issue could be putting your assertion in Ember Run Loop.
I would assume that the error you're seeing (Invalid email/password) is the (mock) server response and indicates something is wrong with either the mock or the credentials you're using in the test.
I'd also not use mirage for mocking the authentication request. mirage (just like Jason API) is resource based and not something that's well suited for authentication.
Related
I am trying to send post data to a django Restful API using vuejs. here is the code I have so far:
<script>
import axios from 'axios'
import VueCookies from 'vue-cookies'
//3RD ATTEMPT
VueCookies.set("csrftoken","00000000000000000000000000000000");
// # is an alias to /src
export default {
name: "Signup",
components: {},
data: () => {
},
methods: {
sendData(){
// 2ND ATTEMPT
// $cookies.set("csrftoken", "00000000000000000000000000000000");
axios({
method: 'post', //you can set what request you want to be
url: 'https://localhost:8000/indy/signup/',
data: {
csrfmiddlewaretoken: "00000000000000000000000000000000",
first_name: "wade",
last_name: "king",
email: "wade%40mail.com",
password1: "05470a5bfe",
password2: "05470a5bfe"
},
// 1ST ATTEMPT
// headers: {
// Cookie: "csrftoken= 00000000000000000000000000000000"
// },
withCredentials: true
})
}
}
</script>
I have a button which executes the sendData() method on a click. The code uses the axios library to send a post request to the django API running on http://localhost:800/indy/signup/
The problem with just sending a post request to the API is that it will get blocked in order to prevent Cross Site Response Forgery (CSRF), I dont quite understand CSRF but I know if the csrftoken is set as a cookie and has the same value as the csrfmiddlewaretoken then the post should go through to the API.
You can see my attempts to set the cookie in the code I provided
1ST ATTEMPT)
headers: {
Cookie: "csrftoken= 00000000000000000000000000000000"
},
Here I'm trying to set the cookie directly in the header. When I click send I get an error in my browser console saying refused to set unsafe header "Cookie"
2ND ATTEMPT)
$cookies.set("csrftoken", "00000000000000000000000000000000");
Here I'm trying to set the cookie using the vue-cookies module. When i click send I get the following error, net::ERR_SSL_PROTOCOL_ERROR
3RD ATTEMPT)
VueCookies.set("csrftoken","00000000000000000000000000000000");
Here I'm trying to set a global cookie using the vue-cookies module. When I click send I get the same error as attempt 2
IMPORTANT:
However when I send post data to the API from my terminal using the following curl command, it works perfectly
curl -s -D - -o /dev/null \
-H 'Cookie: csrftoken= 00000000000000000000000000000000' \
--data 'csrfmiddlewaretoken=00000000000000000000000000000000&first_name=wade&last_name=king&email=wade%40mail.com&password1=05470a5bfe&password2=05470a5bfe' \
http://localhost:8000/indy/signup/
my main question is How can I replicate this curl request using vuejs? I've looked all over on line and none of the tutorials deal with setting cookies.
I posted this question some time ago, I have managed to work around it by running the vue frontend on the same network as the django backend. Follow this tutorial for instructions: integrating vuejs and django
Once I had the application set up I was able to set the cookies much more cleanly using :
axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
Here is my login page for example
<template>
<div class = "container">
<h2>Sign In</h2>
<b-form v-on:submit.prevent="submit()">
<b-form-group id="signin" label="">
<!-- dynamic error message -->
<p class="loginErr" v-if="logErr">Incorrect Username or Password</p>
<b-form-input
id="signin-email"
v-model="username"
placeholder="Email"
required
></b-form-input>
<b-form-input
id="signin-password"
v-model="password"
placeholder="Password"
required
type="password"
></b-form-input>
</b-form-group>
<b-button v-if="!loading" type="submit" variant="primary">Submit</b-button>
<b-spinner v-if="loading"></b-spinner>
</b-form>
</div>
</template>
<script>
import axios from 'axios'
import Vue from 'vue'
export default {
data: ()=>{
return{
loading: false,
logErr: false,
username:'',
password:'',
next: '%2Findy%2Fprofile%2F'
}
},
created: function(){
},
methods: {
submit(){
var vm = this;
vm.loading = true;
var dataStr = 'username='+vm.username+'&password='+vm.password
//set the csrf tokens so django doesn't get fussy when we post
axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
axios.post('http://localhost:8000/api/signin/', dataStr)
.then(function (response) {
vm.loading = false;
//determine if indy accepts the login request
var res = response.data
console.log(response.data)
if(!res.login){
vm.logErr = true;
}else{
vm.redirect('home');
}
})
.catch(function (error) {
//currentObj.output = error;
});
},
redirect(path) {
this.$router.push('/' + path);
}
}
}
</script>
<style>
.loginErr{
color: orange;
}
</style>
I'm trying to test a service that I've created that makes an API call with vue-resource. The service should make the call and update the component with the new data, however in my tests it doesn't register the updated value. I'm using the same setup as the vue-cli webpack example and have based my auth service off this repo (which unfortunately doesn't have any tests)
my service:
export default {
login(context, creds){
context.$http.post(LOGIN_URL, creds)
.then((response) => {
//do something else here
}, (response) => {
context.error = response.data.error
}
}
}
my test:
import Vue from 'vue'
import VueResource from 'vue-resource'
Vue.use(VueResource)
import Auth from 'src/auth'
describe('Auth', () => {
it('should throw an error on unsuccessful login', (done) => {
//intercept the api call and force an invalid response
Vue.http.interceptors.unshift((request, next) => {
next(request.respondWidth({error: 'some error'}, {status: 401}));
});
const vm = new Vue({
data: {
error: ''
}
}).$mount()
Auth.login(vm, {email: 'test#test.com', pass: 'test'} )
//this always fails
expect(vm.error).to.equal('some error')
//ive also tried:
vm.$nextTick(() => {
expect(vm.error).to.equal('some error')
//done()
});
//undo our interceptor
Vue.http.interceptors.shift()
}
}
When I run the test it fails because it's expecting '' to equal 'some error'.
My suspicions are around the fact that vue-resource is using promises.
After reading through some of the Vue.js tests I found my answer. Instead of using vm.$nextTick I did the following:
setTimeout(function(){
expect(vm.error).to.equal('something')
done()
}, 0)
I would like to display an error message when the server responses with record not found.
The model in the route handler:
model: function(userLoginToken) {
var userLoginToken= this.store.createRecord('userLoginToken');
return userLoginToken;
},
The action:
actions: {
sendOTP: function(userLoginToken) {
var thisObject = this;
var model=this.currentModel;
this.store.findRecord('user-login-token', userLoginToken.get('mobileNumber')).then(function(response) {
//thisObject.get('controller').set('model', response);
},
function(error) {
//thisObject.get('controller').set('model', error);
//alert("model======== "+model.get('errors'));
});
},
The template is not displaying any error message.
The template:
{{#each model.errors.messages as |message|}}
<div class="errors">
{{message}}
</div>
{{/each}}
Unfortunately, the error message doesn't appear.
Ember depends on an DS.error object, in order to get errors from your models the response has to fulfill the requirements. In order to get Ember to recognize an valid error, in Ember 2.x the error code MUST be 422 and has to follow jsonapi http://jsonapi.org/format/#errors-processing
If you want to catch the errors from the backend response you have to use the catch method:
this.store.findRecord('user-login-token', userLoginToken.get('mobileNumber'))
.then(success => {
// Do whatever you need when the response success
})
.catch(failure => {
// Do whatever you need when the response fails
})
},
For catching the errors automatically as you are doing in your template, your backend needs to response in the right way. I would suggest you to read the answer for this SO question.
I've got an ember-cli app that's using Ember Data and I'm trying to write an acceptance test that covers the failure case of submitting a form to ask a question. In the test, I'm mocking the response with Pretender to return an errors object, and then asserting that the user is shown a message that lets them know that their submission failed.
The actual assertion that I've written, which checks for an error message to be displayed, is passing. The issue is that I'm also receiving a failure for Error: The backend rejected the commit because it was invalid: {title: can't be blank, body: can't be blank}.
Is there a way to silence this error during tests? Am I approaching this incorrectly? It's not actually an error in this case. The backend should reject the commit, because that's what I'm trying to cover.
Here's the test:
test('errors are displayed when asking question fails', function() {
server.post('/api/v1/questions', function(request) {
var errors = {
title: ["can't be blank"],
body: ["can't be blank"],
};
return jsonResponse(422, { errors: errors });
});
authenticateSession();
visit('/questions/new');
click('button:contains("Ask")');
andThen(function() {
ok(hasContent('There were some errors with your question.'),
'Error message displayed');
});
});
And the relevant action that's being triggered:
save: function(model) {
var _this = this;
model.save().then(function() {
_this.transitionTo('questions.show', model);
}, function() {
_this.wuphf.danger('There were some errors with your question.');
});
},
And the failure that's popping up:
I am converting from Ember data 0.13 to 1.0.0 Beta 1. In 0.13, I was using the becameError and becameInvalid states to know whether there was a problem when saving a record.
In 1.0.0 there is no longer a transaction and you need to use the save promise to handle errors. See below:
save: function() {
this.get('model').save().then(function () {
alert("Record saved");
}, function () {
alert("Problem");
});
},
In the above, I want to make a distinction between validation errors and all the rest (just as it was before in 0.13 with becameError and becameInvalid).
Is there a way to access the error object and how to read the validation errors included in the json response ? Before this was via this.get('content.errors') ...
Hoep somebody can help
Marc
Three steps:
Return errors in a proper format. If it Rails application, then:
\# Rails controller, update function
format.json { render json: {errors: #post.errors.messages}, status: :unprocessable_entity }
Set errors in promise
// app.js
save: function() {
self = this;
this.get('model').save().then(function () {
alert("Record saved");
}, function (response) {
self.set('errors', response.responseJSON.errors);
});
}
Display errors in a handlebar template
<\!-- index.html -->
{{input type="text" value=title}}<span class="alert-error">{{errors.title}}</span>
Not sure if this helps you substitute the bacameInvalid and becameError since states are now being removed, but you could try this as a catchall workaround:
Ember.RSVP.configure('onerror', function(error) {
console.log(error.message);
console.log(error.stack);
});
Hope it helps.