2
votes

So I had a situation where we had to hit a "restful" service not under our control, where in order to get json back from the service on a GET call, we have to pass Content-Type="application/json" in the header. Only problem is that Angular strips the Content-Type from request headers on a GET. I found a blog post that suggested using a decorator on $httpBackend that allows us to intercept the call before it is sent and add back the content type:

angular
.module('MyApp')
.decorator('$httpBackend', [
  '$delegate', function($delegate) {
      return function() {
          var contentType, headers;
          headers = arguments[4];
          contentType = headers != null ? headers['X-Force-Content-Type'] : null;
          if (contentType != null && headers['Content-Type'] == null)
            headers['Content-Type'] = contentType;

          return $delegate.apply(null, arguments);
      };
  }]);

so, that works beautifully! Now our problem is that it has broken all unit tests where we used the mock $httpBackend service. The only error we get is "undefined".

Ex. unit test method:

it('should return service.model.error if service returns an exception code from EndProject', 
 inject(function($httpBackend) {
      var mockResponse = sinon.stub({ 'exception': 'Unable to retrieve service data' });
      $httpBackend.whenPUT(this.endProjectUrl).respond(mockResponse);
      var data;
      this.service.EndProject().then(function(fetchedData) {
           data = fetchedData;
      });

      $httpBackend.flush();
      expect(data.error.state).toBe(true);
      expect(data.error.message).toEqual('Unable to retrieve service data');
 }));

PhantomJS 2.1.1 (Mac OS X 0.0.0) projectService EndProject should return service.model.error if service returns an exception code from EndProject FAILED undefined /Users/mlm1205/Documents/THDSource/bolt-projects/html_app/src/app/components/services/project/projectService.spec.js:213:41 invoke@/Users/mlm1205/Documents/THDSource/bolt-projects/html_app/bower_components/angular/angular.js:4560:22 workFn@/Users/mlm1205/Documents/THDSource/bolt-projects/html_app/bower_components/angular-mocks/angular-mocks.js:2518:26

2

2 Answers

1
votes

The listed decorator covers simple monkey-patching scenario where patched function isn't a constructor and has no static properties and methods.

This is true for $httpBackend in ng module, it is just a factory function with no extra properies.

This is not true for ngMock and ngMockE2E modules that override $httpBackend and have static methods, at least some of them are documented.

This means that generally safe recipe (it doesn't cover non-enumerable and inherited properties) for monkey-patching a factory function is

app.decorator('$httpBackend', ['$delegate', function ($delegate) {
    var $httpBackend = function () {
        ...
        return $delegate.apply(null, arguments);
    };
    angular.extend($httpBackend, $delegate);
    return $httpBackend;
}]);

Regardless of that, it is a good habit to modularize the app to the level where units can be tested in isolation with no excessive moving parts (this issue is an expressive example why this is important). It is convenient to have app (bootstrapped in production), app.e2e (bootstrapped in e2e tests), app.common (common denominator), app.unitA (loaded in app.common and can be loaded separately in unit test), etc.

Most of application-wide code (config and run blocks, routing) may be moved to separate modules and loaded only in modules that directly depend on them. Unless this is a spec that tests decorator unit itself, decorator module shouldn't be loaded.

Also notice that Chrome may offer superior experience than PhantomJS when debugging spec errors.

0
votes

While I marked estus's answer as the solution, based purely on what my question was...in the end, ultimately it wasn't the end result we went with. In a case of not seeing the forest through the trees, the simplest solution was to add an empty data element to the $http call's config. I had tried it before and it didn't work (or so it seemed), but after playing with it again, it did in fact work and we were able to remove the decorator from the application.

return $http.get(getItemInformationUrl + params, { dataType: 'json', data: '', headers: {'Content-Type': 'application/json'} }).then(getItemInformationCompleted).catch(getItemInformationFailed);