Ember: 1.0.0-rc.6
Ember-Data: e999edb (2013-07-06 06:03:59 -0700)
I make a REST call (POST) to login a user.
Server response is ok.
I need the ID from the server, but i only got the ID with "setTimeout".
I think this is not the right way.
What is my mistake?
Within Controller i call:
var login = App.Login.createRecord(this.getProperties("email", "password"));
login.on("didCreate", function(record) {
console.log(record.get("id")); // ID is null
console.log(record.get("email"));
});
setTimeout(function() {
console.log(login.get("id")); // ID is available
console.log(login.get("email"));
}, 500);
DS.defaultStore.commit();
You're right -- there's a bug in ember-data where the materializeData event, which basically sets the id and unwraps the server response doesn't happen until AFTER the didCreate callback. So what's happening is that in your login.on("didCreate" ....) callback, the record still hasn't materialized yet.
This seems to still be an issue -- see this thread for more information: https://github.com/emberjs/data/issues/405#issuecomment-17107045
Workaround
Your work around is fine, but an easier (cleaner?) one is to wrap your callback actions in a Ember.run.next:
login.on("didCreate", function(record) {
Ember.run.next(function() {
console.log(record.get("id")); // ID should be set
console.log(record.get("email"));
}
});
This way, at least you don't need to deal with the timeout.
I believe this works by delaying the actions until the next run loop, and by then the materialization should have already happened. More on the Ember run loop
Source: https://github.com/emberjs/data/issues/405#issuecomment-18726035
Related
Stumped on a couple failures and want to know if I'm understanding Mirage correctly:
1.In ember-cli-mirage, am I correct that the server response I define should reflect what my actual server is returning? For example:
this.get('/athletes', function(db, request) {
let athletes = db.athletes || [];
return {
athletes: athletes,
meta: { count: athletes.length }
}
});
I am using custom serializers and the above matches the format of my server response for a get request on this route, however, on two tests I'm getting two failures with this error: normalizeResponse must return a valid JSON API document: meta must be an object
2.Is mirage enforcing the json:api format, and is it doing so because of the way I'm setting up the tests?
For example, I have several tests that visit the above /athletes route, yet my failures occur when I use an async call like below. I would love to know the appropriate way to correctly overwrite the server response behavior, as well as why the normalizeResponse error appears in the console for 2 tests but only causes the one below to fail.
test('contact params not sent with request after clicking .showglobal', function(assert) {
assert.expect(2);
let done = assert.async();
server.createList('athlete', 10);
//perform a search, which shows all 10 athletes
visit('/athletes');
fillIn('.search-inner input', "c");
andThen(() => {
server.get('/athletes', (db, request) => {
assert.notOk(params.hasOwnProperty("contacts"));
done();
});
//get global athletes, which I thought would now be intercepted by the server.get call defined within the andThen block
click('button.showglobal');
});
});
Result:
✘ Error: Assertion Failed: normalizeResponse must return a valid JSON API document:
* meta must be an object
expected true
I tried changing my server response to a json:api format as suggested in the last example here but this looks nothing like my actual server response and causes my tests to fail since my app doesn't parse a payload with this structure. Any tips or advice must appreciated.
You are correct. Are the failures happening for the mock you've shown above? It looks to me like that would always return meta as an object, so verify the response is what you think it should be by looking in the console after the request is made.
If you'd like to see responses during a test, enter server.logging = true in your test:
test('I can view the photos', function() {
server.logging = true;
server.createList('photo', 10);
visit('/');
andThen(function() {
equal( find('img').length, 10 );
});
});
No, Mirage is agnostic about your particular backend, though it does come with some defaults. Again I would try enabling server.logging here to debug your tests.
Also, when writing asserts against the mock server, define the route handlers at the beginning of the test, as shown in the example from the docs.
I was able to get my second test to pass based on Sam's advice. My confusion was how to assert against the request params for a route that I have to visit and perform actions on. I was having to visit /athletes, click on different buttons, and each of these actions was sending separate requests (and params) to the /athletes route. That's is why I was trying to redefine the route handler within the andThen block (i.e. after I had already visited the route using the route definition in my mirage/config file).
Not in love with my solution, but the way I handled it was to move my assertion out of route handler and instead assign the value of the request to a top-level variable. That way, in my final andThen() block, I was able to assert against the last call to the /athletes route.
assert.expect(1);
//will get assigned the value of 'request' on each server call
let athletesRequest;
//override server response defined in mirage/config in order to
//capture and assert against request/response after user actions
server.get('athletes', (db, request) => {
let athletes = db.athletes || [];
athletesRequest = request;
return {
athletes: athletes,
meta: { count: athletes.length }
};
});
//sends request to /athletes
visit('/athletes');
andThen(() => {
//sends request to /athletes
fillIn('.search-inner input', "ab");
andThen(function() {
//sends (final) request to /athletes
click('button.search');
andThen(function() {
//asserts against /athletes request made on click('button.search') assert.notOk(athletesRequest.queryParams.hasOwnProperty("contact"));
});
});
});
I'm still getting console errors related to meta is not an object, but they are not preventing tests from passing. Using the server.logging = true allowed me to see that meta is indeed an object in all FakeServer responses.
Thanks again to Sam for the advice. server.logging = true and pauseTest() make acceptance tests a lot easier to troubleshoot.
I see this question is being ask all over again still don't find solution that works for such a trivial task.
This url displays a list of navigations tabs for workspaces.
http://localhost:4200/users/1/workspaces
Each of tab resolves to
http://localhost:4200/users/1/workspaces/:wid
Also on the I have a button that suppose to create a new workspace as well as new tab.
Here how controller for looks:
export default Ember.Controller.extend({
actions: {
newWorkspace: function () {
this.get('currentModel').reload();
var self = this;
var onFail = function() {
// deal with the failure here
};
var onSuccess = function(workspace) {
self.transitionToRoute('dashboard.workspaces.workspace', workspace.id);
};
this.store.createRecord('workspace', {
title: 'Rails is Omakase'
}).save().then(onSuccess, onFail);
}
}
});
When I click on button I see in ember inspector new record indeed created as well as url redirected to id that represents newly created workspace.
My question is how to force model/template to reload. I have already killed 5h trying model.reload() etc. Everything seem not supported no longer. Please please help.
UPDATE
When adding onSuccess
model.pushObject(post);
throws Uncaught TypeError: internalModel.getRecord is not a function
I believe you should call this.store.find('workspace', workspace.id) for Ember Data 1.12.x or earlier. For 1.13 and 2.0 there are more complicated hooks that determine whether or not the browser should query the server again or use a cached value; in that case, call this.store.findRecord('workspace', workspace.id, { reload: true }).
I do not know if this help. I had a similar problem. My action was performed in the route. Refresh function took care of everything.
When I request a single resource that returns a 404 (or 403 for that matter) Ember Data is creating a local record.
For example, I load my app from scratch at /items/123. The adapter does a request for GET /items/123 which results in a 404 but now I have an item record in my local store with id=123. All attributes are undefined expect where the model defines default values.
Also, all model flags are false except isValid which is true.
Is this excepted behaviour? It seems strange that the local record gets created even though the server is saying that it doesn't exist (or that the user is not allowed to see it).
Details
I'm running Ember 1.8.1 and Ember Data 1.0.0-beta.11.
Here's what I have for Routes—pretty basic stuff:
// itemsRoute, TOP LEVEL
model: function() {
return this.store.find('item');
}
// itemRoute, CHILD LEVEL
model: function(params) {
return this.store.find('item', params.item_id); // <-- this returns 404
},
actions: {
error: function(err) {
this.replaceWith('items'); // jump out to main list view
return true; // ensure error bubbles up
}
}
The rejection in the model hook is working because the error action is triggered and I'm redirected out the top level /items view. But I still end up with a local record for an item that doesn't exist.
After thinking about this a little more... try altering your error handler in itemRoute from...
actions: {
error: function(err) {
this.replaceWith('items'); // jump out to main list view
return true; // ensure error bubbles up
}
}
to...
actions: {
error: function(err) {
this.transitionTo('items'); // jump out to main list view
return false; // Stop event bubbling - we've handled it.
//You could also console.log parts or all of your error
//You can return true too if you want to keep the event bubbling.
}
}
I don't have a concrete reason for why this would be yet. Waiting to hear back from you if it worked.
This is most definitely a bug. It seems this was fixed a while back but as was reintroduced by a change on March 14.
I've submitted a bug report https://github.com/emberjs/data/issues/3085
My UserManager service automatically fires a $http POST every hour to refresh the user access token.
I'm trying to mock that call to verify that the token is being refreshed, but when I try to flush the $httpbackend I get an error saying 'No pending requests to flush' even though I know that the refresh function has been called (added a console.log just to verify).
Either the fact that the function is being called through a setTimeOut is effecting the $httpbackend or I'm missing something else.
Code attached bellow:
describe("UserManager Service Testing", function() {
beforeEach(angular.mock.module('WebApp'));
beforeEach(inject(function($httpBackend) {
window.apiUrls = jQuery.parseJSON('{"cover_art": "http://myrockpack.com/ws/cover_art/?locale=en-us", "channel_search_terms": "http://myrockpack.com/ws/complete/channels/?locale=en-us", "register": "http://myrockpack.com/ws/register/", "categories": "http://myrockpack.com/ws/categories/?locale=en-us", "reset_password": "http://myrockpack.com/ws/reset-password/", "share_url": "http://myrockpack.com/ws/share/link/", "video_search": "http://myrockpack.com/ws/search/videos/?locale=en-us", "channel_search": "http://myrockpack.com/ws/search/channels/?locale=en-us", "video_search_terms": "http://myrockpack.com/ws/complete/videos/?locale=en-us", "popular_channels": "http://myrockpack.com/ws/channels/?locale=en-us", "popular_videos": "http://myrockpack.com/ws/videos/?locale=en-us", "login": "http://myrockpack.com/ws/login/", "login_register_external": "http://myrockpack.com/ws/login/external/", "refresh_token": "http://myrockpack.com/ws/token/"}');
var mockLogin = {"token_type":"Bearer","user_id":"oCRwcy5MRIiWmsJjvbFbHA","access_token":"752a4f939662846a787a1474ad17ffddcd816dc7AAFB1G7HvgH-0qAkcHMuTESIlprCY72xWxyiuhySCpFJxKVYqOx9W7Gt","resource_url":"http://myrockpack.com/ws/oCRwcy5MRIiWmsJjvbFbHA/","expires_in":2,"refresh_token":"fa2f47f3590240e4bdfdbde03bf8042d"}
var refreshToken = {"token_type":"Bearer","user_id":"CeGfSz6dQW2ga2P2tKb3Bg","access_token":"ef235de46dba53ba69ed049f57496ec902da5d28AAFB1HdeTE-2vwnhn0s-nUFtoGtj9rSm9waiuhySCpFJxKVYqOx9W7Gt","resource_url":"http://myrockpack.com/ws/CeGfSz6dQW2ga2P2tKb3Bg/","expires_in":3600,"refresh_token":"873e06747d964a0d80f79181c98aceac"};
$httpBackend.when('POST', window.apiUrls.refresh_token).respond(refreshToken);
$httpBackend.when('POST', window.apiUrls.login).respond(mockLogin);
}));
it('UserManager should refresh the token after 2 seconds', inject(function(UserManager, $httpBackend) {
UserManager.oauth.Login('gtest','qweqwe');
$httpBackend.flush();
waits(4000);
$httpBackend.flush();
expect(UserManager.oauth.credentials.access_token).toEqual('ef235de46dba53ba69ed049f57496ec902da5d28AAFB1HdeTE-2vwnhn0s-nUFtoGtj9rSm9waiuhySCpFJxKVYqOx9W7Gt');
}));
});
There is a discussion about this here. "Since promises are async you need to do $rootScope.$digest() in your tests to get them going"
add this before your $httpBackend.flush():
if(!$rootScope.$$phase) {
$rootScope.$apply();
}
so your code becomes:
it('UserManager should refresh the token after 2 seconds', inject(function(UserManager, $httpBackend) {
UserManager.oauth.Login('gtest','qweqwe');
if(!$rootScope.$$phase) {
$rootScope.$apply();
}
$httpBackend.flush();
expect(UserManager.oauth.credentials.access_token).toEqual('ef235de46dba53ba69ed049f57496ec902da5d28AAFB1HdeTE-2vwnhn0s-nUFtoGtj9rSm9waiuhySCpFJxKVYqOx9W7Gt');
}));
I guess you have to wrap the code below the waits inside a runs(...) block. Otherwise your code is executed immediately before the waiting has been finished
waits(4000);
runs(function() {
$httpBackend.flush();
expect(UserManager.oauth.credentials.access_token).toEqual('ef235de46dba53ba69ed049f57496ec902 da5d28AAFB1HdeTE-2vwnhn0s-nUFtoGtj9rSm9waiuhySCpFJxKVYqOx9W7Gt');
});
It's described in the jasmine docs as well: https://github.com/pivotal/jasmine/wiki/Asynchronous-specs
Your first $httpBackend.flush() flushes all the pending requests that you defined in your beforeEach().
When you call $httpBackend.flush() a second time, there are no pending requests, so you get the message.
You need to add another event(s) after the first flush.
Untested, but that's my theory.
So with my jasmine tests all the time sometimes I have had to use regex to make the url a little less specific, most of this has been with angular wanting to strip trailing slashes or other stupid little things. Such as the request url wouldn't find the expect request but the regex below would.
https://qa.com/sessions/5cdec5bde6a242dca2cf5dd0ff7be2c9
/(qa.com\/sessions\/5cdec5bde6a242dca2cf5dd0ff7be2c9)/g
I am building an ember.js application and am hung up on authentication. The json rest backend is rails. Every request is authenticated using a session cookie (warden).
When a user first navigates to the application root rails redirects to a login page. Once the session is authorized the ember.js app is loaded. Once loaded the ember.js app makes requests to the backend using ember-data RESTadapter and the session for authorization.
The problem is the session will expire after a predetermined amount of time. Many times when this happens the ember.js app is still loaded. So all requests to the backend return a 401 {not autorized} response.
To fix this problem I am thinking the ember.js app needs to notify the user with a login modal every time a 401 {not autorized} response is returned from the server.
Does anyone know how to listen for a 401 {not autorized} response and allow the user to re-login without losing any changes or state.
I have seen other approaches such as token authorization but I am concerned with the security implications.
Anybody have a working solution to this problem?
As of the current version of Ember Data (1.0 beta) you can override the ajaxError method of DS.RESTAdapter:
App.ApplicationAdapter = DS.RESTAdapter.extend({
ajaxError: function(jqXHR) {
var error = this._super(jqXHR);
if (jqXHR && jqXHR.status === 401) {
#handle the 401 error
}
return error;
}
});
Note that you should call #_super, especially if you are overriding one of the more complex adapters like DS.ActiveModelAdapter, which handles 422 Unprocessable Entity.
AFAIK this is not addressed by the current implementation of ember-data and the ember-data README states that "Handle error states" is on the Roadmap.
For the time being, you can implement your own error handling adapter. Take a look at the implementation of the DS.RestAdapter . By using that as a starter, it should not be too difficult to add error handling in there (e.g simply add an error function to the the data hash that is passed to the jQuery.ajax call).
For those willing to accept a solution that does lose changes and state you can register a jQuery ajaxError handler to redirect to a login page.
$(document).ajaxError(function(event, jqXHR, ajaxSettings, thrownError) {
// You should include additional conditions to the if statement so that this
// only triggers when you're absolutely certain it should
if (jqXHR.status === 401) {
document.location.href = '/users/sign_in';
}
});
This code will get triggered anytime any jQuery ajax request completes with an error.
Of course you would never actually use such a solution as it creates an incredibly poor user experience. The user is yanked away from what they're doing and they lose all state. What you'd really do is render a LoginView, probably inside of a modal.
An additional nicety of this solution is that it works even if you occasionally make requests to your server outside of ember-data. The danger is if jQuery is being used to load data from other sources or if you already have some 401 error handling built-in elsewhere. You'll want to add appropriate conditions to the if statement above to ensure things are triggered only when you're absolutely certain they should.
It's not addressed by ember-data (and probably won't be), but you can reopen the DS class and extend the ajax method.
It looks like this:
ajax: function(url, type, hash) {
hash.url = url;
hash.type = type;
hash.dataType = 'json';
hash.contentType = 'application/json; charset=utf-8';
hash.context = this;
if (hash.data && type !== 'GET') {
hash.data = JSON.stringify(hash.data);
}
jQuery.ajax(hash);
},
You can rewrite it with something like this (disclaimer: untested, probably won't work):
DS.reopen({
ajax: function(url, type, hash) {
var originalError = hash.error;
hash.error = function(xhr) {
if (xhr.status == 401) {
var payload = JSON.parse(xhr.responseText);
//Check for your API's errorCode, if applicable, or just remove this conditional entirely
if (payload.errorCode === 'USER_LOGIN_REQUIRED') {
//Show your login modal here
App.YourModal.create({
//Your modal's callback will process the original call
callback: function() {
hash.error = originalError;
DS.ajax(url, type, hash);
}
}).show();
return;
}
}
originalError.call(hash.context, xhr);
};
//Let ember-data's ajax method handle the call
this._super(url, type, hash);
}
});
What we're doing here is essentially deferring the call that received the 401 and are preserving the request to be called again when login is complete. The modal's ajax call with have the original error applied to it from the original ajax call's hash, so the original error would still work as long as it's defined :-)
This is a modified implementation of something we're using with our own data-persistence library, so your implementation might vary a bit, but the same concept should work for ember-data.