AngularJSのInterceptorを利用する

# Interceptor
Interceptorを使用すると、サーバに引き渡される前のリクエストの前処理や、リクエストを行ったアプリケーションコードに引き渡される前のレスポンスの前処理、さらにはグローバルのエラー処理を定義することができるようです。
https://docs.angularjs.org/api/ng/service/$http #Interceptors

Interceptorを利用して、リクエストからレスポンスまでの時間の測定と、レスポンスのエラーハンドリングにトライしてみました。

```lang:フォルダ構成
├── angular
│   ├── angular-cookies.js
│   ├── angular-resource.js
│   ├── angular-route.js
│   ├── angular-sanitize.js
│   └── angular.js
├── app
│   ├── controllers.js
│   ├── services.js
│   └── configurations.js ← 新規追加
└── index.html
```

サンプルソースはAngularJSの開発者ガイドから拝借。
（YahooファイナンスAPIで取得した各国の為替レートを入力した数量・金額に掛けて一覧表示するもの）
https://docs.angularjs.org/guide/concepts

```html:index.html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Try Interceptor</title>
    <script src="angular/angular.js"></script>
    <script src="angular/angular-resource.js"></script>
    <script src="angular/angular-cookies.js"></script>
    <script src="angular/angular-sanitize.js"></script>
    <script src="angular/angular-route.js"></script>
    <script src="app/app.js"></script>
    <script src="app/controllers.js"></script>
    <script src="app/services.js"></script>
  </head>
  <body ng-app="myapp.controllers">
    <h1>AngularJs Sample</h1>
    <div class="container" ng-controller="SampleController">
      <b>Invoice:</b>
      <div>
        Quantity: <input type="number" ng-model="qty" required />
      </div>
      <div>
        Costs: <input type="number" ng-model="cost" required /><br />
        <select ng-model="inCurr">
          <option ng-repeat="c in currencies">{{c}}</option>
        </select>
      </div>
      <div>
        <b>Total:</b>
        <span ng-repeat="c in currencies">
          {{total(c) | currency:c }}
        </span><br/>
        <button class="btn" ng-click="pay()">Pay</button>
      </div>
    </div>
</body>
</html>
```
```js:controllers.js
angular.module('myapp.controllers', ['myapp.services'])
.controller('SampleController', function($scope, SampleService) {
  $scope.qty = 1;
  $scope.cost = 2;
  $scope.inCurr = 'JPY';
  $scope.currencies = SampleService.currencies;
  $scope.total = function (outCurr) {
    return SampleService.convert($scope.qty * $scope.cost, $scope.inCurr, outCurr);
  };
  $scope.pay = function () {
    SampleService.pay($scope.qty, $scope.incurr);
  };
});
```
```js:services.js
angular.module('myapp.services', [])
.factory('SampleService', function ($http) {
  var YAHOO_FINANCE_URL_PATTERN = 
    'http://query.yahooapis.com/v1/public/yql?q=select * from '+
      'yahoo.finance.xchange where pair in ("PAIRS")&format=json&'+
        'env=store://datatables.org/alltableswithkeys&callback=JSON_CALLBACK';
  var currencies = ['USD', 'EUR', 'CNY', 'JPY'];
  var usdToForeignRates = {};
  refresh();
  return {
    currencies: currencies,
    convert: convert,
    refresh: refresh
  };
  function convert(amount, inCurr, outCurr) {
    return amount * usdToForeignRates[outCurr] * 1 / usdToForeignRates[inCurr];
  };

  function refresh() {
    var url = YAHOO_FINANCE_URL_PATTERN.replace('PAIRS', 'USD' + currencies.join('","USD'));
    return $http.jsonp(url).success(
      function(data, status, headers, config) {
        var time = config.responseTimestamp - config.requestTimestamp;
        console.log('The request took ' + (time / 1000) + ' seconds.');
        var newUsdToForeignRates = {};
        angular.forEach(data.query.results.rate, function(rate) {
          var currency = rate.id.substring(3,6);
          newUsdToForeignRates[currency] = window.parseFloat(rate.Rate);
        });
        usdToForeignRates = newUsdToForeignRates;
      }
    );
  };

  function pay(qty, incurr) {
    $http.post("/test", {qty: qty, incurr: incurr});
  }
});
```
Interceptorの処理はconfigurations.jsで行いました。

```js:configurations.js
angular.module('myapp.configurations', ['myapp.controllers'])
.config(function($httpProvider) {
  $httpProvider.interceptors.push(
    function ($q, $rootScope) {
      return {
        request: function(config) {
          config.requestTimestamp = new Date().getTime();
          return config;
        },
        response: function(response) {
          response.config.responseTimestamp = new Date().getTime();
          return response;
        },
        responseError: function(rejection) {
          if (500 == rejection.status) {
            alert('System Error!');
          }
          return $q.reject(rejection);
        }
      };
    }
  );
});
```
Interceptorの定義は`$httpProvider`がもつ配列`interceptors`にfactoryをpushするだけで行えます。

`interceptors`にpushするfactoryに定義できるfunctionは以下の４つ。

* `request` : サーバに引き渡される前のリクエストの前処理を定義
* `response` : リクエストを行ったアプリケーションコードに引き渡される前のレスポンスの前処理
* `requestError` : リクエストでエラーが起きた際の処理を定義
* `responseError` : レスポンスでエラーが起きた際の処理を定義

(see https://docs.angularjs.org/api/ng/service/$http #Interceptors)

本稿ではリクエストからレスポンスまでの時間の測定と、レスポンスのエラーハンドリングを行うため、`request`、`response`と`responseError`を定義しました。

`request`、`response`では処理時の時間を算出するように、
`responseError`はHTTPステータスコード500 の場合に処理をするようにしています。

# 実行してみる
リクエストからレスポンスまでにかかる時間はコンソールに出力されるはずなので確認してみると、
![console](https://qiita-image-store.s3.amazonaws.com/0/50783/cfdb7ccd-c9b3-bb95-4098-9cf04b2e7272.png)
と表示されていることがわかります。

# エラーハンドリングのテストをする
エラーハンドリングの確認はサーバーサイドを書いてもいいのですが、面倒なのでテストで確認してみます。

基本的なテストの書き方は以下を参照してください。
[Jasmineのみを使用したAngularJSのテスト](http://qiita.com/uggds/items/5d0c643c187c14a1591b)

AngularJSではバックエンドの振る舞いをMock化する$httpBackendを提供しています。
https://code.angularjs.org/1.2.26/docs/api/ngMock/service/$httpBackend
これを利用して`SampleService`の`pay`メソッドで行っている`$http.post`のサーバーサイドをMock化しました。
※SampleService読み込み時に`$http.jsonp`でyahooapiにアクセスしてしまうのでこれもMock化しています。

```js:sampleServiceSpec.js
'use strict';

describe('Service: SampleService', function () {

  beforeEach(module('myapp.services'));

  var $httpBackend,
      SampleService ;

  var YAHOO_FINANCE_URL_PATTERN = 
    'http://query.yahooapis.com/v1/public/yql?q=select * from '+
      'yahoo.finance.xchange where pair in ("PAIRS")&format=json&'+
        'env=store://datatables.org/alltableswithkeys&callback=JSON_CALLBACK';
  
  var alert_msg;

  beforeEach(inject(function($injector) {

    // Set up the mock http service responses
    $httpBackend = $injector.get('$httpBackend');

    var currencies = ['USD', 'EUR', 'CNY', 'JPY'];
    var url = YAHOO_FINANCE_URL_PATTERN.replace('PAIRS', 'USD' + currencies.join('","USD'));
    $httpBackend.when('JSONP', url).respond(
      {query: {results: {rate: [
        {id: 'HOGE', rate: 1.0}
      ]}}}
    );

    alert_msg = '_defalut_';
    spyOn(window, 'alert').and.callFake(function(msg) {
      alert_msg = msg;
    });

    SampleService = $injector.get('SampleService');
  }));

  it('should success, when pay() is called', function() {
    expect(alert_msg).toEqual('_defalut_');
    var data = {qty: 1, cost: 1000};
    $httpBackend.when('POST', '/test', data).respond(200, '');
    SampleService.pay(1, 1000).success(function() {
       expect(true).toBe(true);
     }).error(function() {
       expect(false).toBe(true);
     });
    $httpBackend.flush();
    expect(alert_msg).toEqual('_defalut_');
  });

  it('should fail, when pay() is called', function() {
    expect(alert_msg).toEqual('_defalut_');
    var data = {qty: 1, cost: 1000};
    $httpBackend.when('POST', '/test', data).respond(400, '');

    SampleService.pay(1, 1000).success(function() {
      expect(false).toBe(true);
    }).error(function() {
      expect(true).toBe(true);
    });
    $httpBackend.flush();
    expect(alert_msg).toEqual('_defalut_');
  });

  it('should fail and emit alert message for status code 500, when pay() is called', function() {
    expect(alert_msg).toEqual('_defalut_');
    var data = {qty: 1, cost: 1000};
    $httpBackend.when('POST', '/test', data).respond(500, '');

    SampleService.pay(1, 1000).success(function() {
      expect(false).toBe(true);
    }).error(function() {
      expect(true).toBe(true);
    });
    $httpBackend.flush();
    expect(alert_msg).toEqual('System Error!');
  });
});
```

テストケースは３つ。

1. ステータスコード200の正常系
2. ステータスコード400の異常系
3. ステータスコード500の異常系

alertメッセージの中身を確認しています。

# 参考
・[Interceptors in AngularJS and Useful Examples](http://www.webdeveasy.com/interceptors-in-angularjs-and-useful-examples/)
・[AngularJS を本気でつかうための tips](http://blog.64p.org/entry/2013/12/10/155533)