Added tests

Change-Id: I493675212f4b1548b32a6d92ce3664d184bc0e04
diff --git a/conf/karma-auto.conf.js b/conf/karma-auto.conf.js
index c0b9509..e66de9c 100644
--- a/conf/karma-auto.conf.js
+++ b/conf/karma-auto.conf.js
@@ -1,4 +1,5 @@
 const conf = require('./gulp.conf');
+const pkg = require('../package.json');
 
 module.exports = function (config) {
   const configuration = {
@@ -10,7 +11,8 @@
       outputDir: 'test-reports'
     },
     browsers: [
-      'PhantomJS'
+      'PhantomJS',
+      'Chrome'
     ],
     frameworks: [
       'jasmine',
@@ -46,6 +48,7 @@
       require('karma-junit-reporter'),
       require('karma-coverage'),
       require('karma-phantomjs-launcher'),
+      require('karma-chrome-launcher'),
       require('karma-phantomjs-shim'),
       require('karma-ng-html2js-preprocessor'),
       require('karma-webpack'),
diff --git a/package.json b/package.json
index 982fb96..ff66a45 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,7 @@
     "json-loader": "^0.5.4",
     "karma": "^1.3.0",
     "karma-angular-filesort": "^1.0.0",
+    "karma-chrome-launcher": "^2.0.0",
     "karma-coverage": "^1.1.1",
     "karma-es6-shim": "^1.0.0",
     "karma-jasmine": "^1.0.2",
diff --git a/src/app/core/services/navigation.spec.ts b/src/app/core/services/navigation.spec.ts
new file mode 100644
index 0000000..1096ec7
--- /dev/null
+++ b/src/app/core/services/navigation.spec.ts
@@ -0,0 +1,47 @@
+import * as angular from 'angular';
+import 'angular-mocks';
+import 'angular-ui-router';
+import {xosCore} from '../index';
+import {IXosNavigationService, IXosNavigationRoute} from './navigation';
+
+let service: IXosNavigationService;
+
+const defaultRoutes: IXosNavigationRoute[] = [
+  {label: 'Home', state: 'xos.dashboard'}
+];
+
+describe('The Navigation service', () => {
+
+  beforeEach(angular.mock.module(xosCore));
+
+  beforeEach(angular.mock.inject((
+    NavigationService: IXosNavigationService,
+  ) => {
+    service = NavigationService;
+  }));
+
+  it('should return navigation routes', () => {
+    expect(service.query()).toEqual(defaultRoutes);
+  });
+
+  it('should add a route', () => {
+    const testRoutes: IXosNavigationRoute[] = [
+      {label: 'TestState', state: 'xos.test'},
+      {label: 'TestUrl', url: 'test'}
+    ];
+    service.add(testRoutes[0]);
+    service.add(testRoutes[1]);
+    expect(service.query()).toEqual(defaultRoutes.concat(testRoutes));
+  });
+
+  it('should not add route that have both url and state', () => {
+    function wrapper() {
+      service.add({
+        label: 'Fail',
+        url: 'f',
+        state: 'f'
+      });
+    }
+    expect(wrapper).toThrowError('[XosNavigation] You can\'t provide both state and url');
+  });
+});
diff --git a/src/app/core/services/navigation.ts b/src/app/core/services/navigation.ts
index 8cb3b66..04eb575 100644
--- a/src/app/core/services/navigation.ts
+++ b/src/app/core/services/navigation.ts
@@ -26,6 +26,9 @@
   }
 
   add(route: IXosNavigationRoute) {
+    if (angular.isDefined(route.state) && angular.isDefined(route.url)) {
+      throw new Error('[XosNavigation] You can\'t provide both state and url');
+    }
     this.routes.push(route);
   }
 }
diff --git a/src/app/core/services/page-title.spec.ts b/src/app/core/services/page-title.spec.ts
new file mode 100644
index 0000000..4ff2be6
--- /dev/null
+++ b/src/app/core/services/page-title.spec.ts
@@ -0,0 +1,31 @@
+import * as angular from 'angular';
+import 'angular-mocks';
+import 'angular-ui-router';
+import {xosCore} from '../index';
+import {IXosPageTitleService} from './page-title';
+import IWindowService = angular.IWindowService;
+import {StyleConfig} from '../../config/style.config';
+
+let service: IXosPageTitleService, $window: IWindowService;
+describe('The PageTitle service', () => {
+
+  beforeEach(angular.mock.module(xosCore));
+
+  beforeEach(angular.mock.inject((
+    PageTitle: IXosPageTitleService,
+    _$window_: IWindowService
+  ) => {
+    service = PageTitle;
+    $window = _$window_;
+  }));
+
+  it('should get the page title', () => {
+    $window.document.title = 'test';
+    expect(service.get()).toEqual('test');
+  });
+
+  it('should set a page title', () => {
+    service.set('sample');
+    expect($window.document.title).toEqual(`${StyleConfig.projectName} - sample`);
+  });
+});
diff --git a/src/app/core/services/page-title.ts b/src/app/core/services/page-title.ts
index 6f7e0c3..fb1e5a7 100644
--- a/src/app/core/services/page-title.ts
+++ b/src/app/core/services/page-title.ts
@@ -11,7 +11,6 @@
     private $window: angular.IWindowService,
     private $transitions: any // missing definition
   ) {
-    console.log('page title');
     this.$transitions.onSuccess({ to: '**' }, (transtion) => {
       this.set(this.formatStateName(transtion.$to().name));
     });
diff --git a/src/app/core/services/runtime-states.spec.ts b/src/app/core/services/runtime-states.spec.ts
new file mode 100644
index 0000000..5dd44db
--- /dev/null
+++ b/src/app/core/services/runtime-states.spec.ts
@@ -0,0 +1,30 @@
+import * as angular from 'angular';
+import 'angular-mocks';
+import 'angular-ui-router';
+import {xosCore} from '../index';
+import {IRuntimeStatesService} from './runtime-states';
+
+let service: IRuntimeStatesService, $state: ng.ui.IStateService;
+
+describe('The Navigation service', () => {
+
+  beforeEach(angular.mock.module(xosCore));
+
+  beforeEach(angular.mock.inject((
+    RuntimeStates: IRuntimeStatesService,
+    _$state_: ng.ui.IStateService
+  ) => {
+    service = RuntimeStates;
+    $state = _$state_;
+  }));
+
+  it('should add a state', () => {
+    service.addState('testState', {
+      url: 'test-state',
+      template: 'test-state'
+    });
+
+    expect($state.get('testState').url).toEqual('test-state');
+    expect($state.get('testState').template).toEqual('test-state');
+  });
+});
diff --git a/src/app/datasources/index.ts b/src/app/datasources/index.ts
index 936920e..c82f317 100644
--- a/src/app/datasources/index.ts
+++ b/src/app/datasources/index.ts
@@ -1,4 +1,3 @@
-import {CoreRest} from './rest/core.rest';
 import {ModelRest} from './rest/model.rest';
 import {AuthService} from './rest/auth.rest';
 import {WebSocketEvent} from './websocket/global';
@@ -10,8 +9,7 @@
 export const xosDataSources = 'xosDataSources';
 
 angular
-  .module('xosDataSources', ['ngCookies'])
-  .service('CoreRest', CoreRest)
+  .module('xosDataSources', ['ngCookies', 'ngResource'])
   .service('ModelRest', ModelRest)
   .service('AuthService', AuthService)
   .service('WebSocket', WebSocketEvent);
diff --git a/src/app/datasources/rest/auth.rest.spec.ts b/src/app/datasources/rest/auth.rest.spec.ts
new file mode 100644
index 0000000..85aefb4
--- /dev/null
+++ b/src/app/datasources/rest/auth.rest.spec.ts
@@ -0,0 +1,84 @@
+import * as angular from 'angular';
+import 'angular-mocks';
+import 'angular-resource';
+import 'angular-cookies';
+import {xosDataSources} from '../index';
+import {AppConfig} from '../../config/app.config';
+import {IXosAuthService} from './auth.rest';
+
+let service: IXosAuthService;
+let httpBackend: ng.IHttpBackendService;
+let $scope;
+let $cookies;
+
+describe('The AuthService service', () => {
+
+  beforeEach(angular.mock.module(xosDataSources));
+
+  beforeEach(() => {
+    angular.mock.module(xosDataSources);
+  });
+
+
+  beforeEach(angular.mock.inject((
+    AuthService: IXosAuthService,
+    $httpBackend: ng.IHttpBackendService,
+    _$rootScope_: ng.IRootScopeService,
+    _$cookies_: ng.cookies.ICookiesService
+  ) => {
+    service = AuthService;
+    httpBackend = $httpBackend;
+    $scope = _$rootScope_;
+    $cookies = _$cookies_;
+  }));
+
+  describe('when logging in', () => {
+    beforeEach(() => {
+      httpBackend.expectPOST(`${AppConfig.apiEndpoint}/utility/login/`)
+        .respond({
+          user: JSON.stringify({usernane: 'test@xos.org'}),
+          xoscsrftoken: 'token',
+          xossessionid: 'session'
+        });
+    });
+    it('should store user auth in cookies', (done) => {
+      service.login({username: 'test', password: 'xos'})
+        .then((res) => {
+          expect($cookies.get('xoscsrftoken')).toEqual('token');
+          expect($cookies.get('xossessionid')).toEqual('session');
+          expect($cookies.get('xosuser')).toEqual(JSON.stringify({usernane: 'test@xos.org'}));
+          done();
+        })
+        .catch(e => {
+          done(e);
+        });
+      $scope.$apply();
+      httpBackend.flush();
+    });
+  });
+
+  describe('when logging out', () => {
+    beforeEach(() => {
+      httpBackend.expectPOST(`${AppConfig.apiEndpoint}/utility/logout/`)
+        .respond({
+          user: JSON.stringify({usernane: 'test@xos.org'}),
+          xoscsrftoken: 'token',
+          xossessionid: 'session'
+        });
+    });
+    it('should remove user auth from cookies', (done) => {
+      service.logout()
+        .then((res) => {
+          expect($cookies.get('xoscsrftoken')).toEqual(null);
+          expect($cookies.get('xossessionid')).toEqual(null);
+          expect($cookies.get('xosuser')).toEqual(null);
+          done();
+        })
+        .catch(e => {
+          done(e);
+        });
+      $scope.$apply();
+      httpBackend.flush();
+    });
+  });
+});
diff --git a/src/app/datasources/rest/auth.rest.ts b/src/app/datasources/rest/auth.rest.ts
index a94599a..f71d295 100644
--- a/src/app/datasources/rest/auth.rest.ts
+++ b/src/app/datasources/rest/auth.rest.ts
@@ -12,6 +12,11 @@
     xossessionid: string;
   };
 }
+
+export interface IXosAuthService {
+  login(data: IAuthRequestData): Promise<any>;
+  logout(): Promise<any>;
+}
 export class AuthService {
 
 
@@ -38,4 +43,22 @@
       });
     return d.promise;
   }
+
+  public logout(): Promise<any> {
+    const d = this.$q.defer();
+    this.$http.post(`${AppConfig.apiEndpoint}/utility/login/`, {
+      xoscsrftoken: this.$cookies.get('xoscsrftoken'),
+      xossessionid: this.$cookies.get('xossessionid')
+    })
+      .then(() => {
+        this.$cookies.remove('xoscsrftoken');
+        this.$cookies.remove('xossessionid');
+        this.$cookies.remove('xosuser');
+        d.resolve();
+      })
+      .catch(e => {
+        d.reject(e);
+      });
+    return d.promise;
+  }
 }
diff --git a/src/app/datasources/rest/core.rest.ts b/src/app/datasources/rest/core.rest.ts
deleted file mode 100644
index 2c10e10..0000000
--- a/src/app/datasources/rest/core.rest.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import {AppConfig} from '../../config/app.config';
-export class CoreRest {
-
-  /** @ngInject */
-  constructor(
-    private $http: angular.IHttpService,
-    private $q: angular.IQService
-  ) {
-  }
-
-  public query(): Promise<any> {
-    const d = this.$q.defer();
-    this.$http.get(`${AppConfig.apiEndpoint}/core/`)
-      .then(res => d.resolve(res.data))
-      .catch(d.reject);
-    return d.promise;
-  }
-}
diff --git a/src/app/datasources/rest/model.rest.spec.ts b/src/app/datasources/rest/model.rest.spec.ts
new file mode 100644
index 0000000..f57c0c8
--- /dev/null
+++ b/src/app/datasources/rest/model.rest.spec.ts
@@ -0,0 +1,76 @@
+import * as angular from 'angular';
+import 'angular-mocks';
+import 'angular-resource';
+import 'angular-cookies';
+import {IXosResourceService} from './model.rest';
+import {xosDataSources} from '../index';
+import {AppConfig} from '../../config/app.config';
+
+let service: IXosResourceService;
+let resource: ng.resource.IResourceClass<any>;
+let httpBackend: ng.IHttpBackendService;
+let $resource;
+let $scope;
+
+describe('The ModelRest service', () => {
+
+  beforeEach(angular.mock.module(xosDataSources));
+
+  beforeEach(() => {
+    angular.mock.module(xosDataSources);
+  });
+
+
+  beforeEach(angular.mock.inject((
+    ModelRest: IXosResourceService,
+    $httpBackend: ng.IHttpBackendService,
+    _$resource_: ng.resource.IResourceService,
+    _$rootScope_: ng.IRootScopeService
+  ) => {
+    service = ModelRest;
+    httpBackend = $httpBackend;
+    $resource = _$resource_;
+    $scope = _$rootScope_;
+  }));
+
+  it('should return a resource based on the URL', () => {
+    resource = service.getResource('/core/test');
+    expect(resource.constructor).toEqual($resource.constructor);
+  });
+
+  it('should have a query method', (done) => {
+    httpBackend.expectGET(`${AppConfig.apiEndpoint}/core/test`)
+      .respond([
+        {status: 'ok'}
+      ]);
+    resource = service.getResource('/core/test');
+    resource.query().$promise
+      .then((res) => {
+        expect(res[0].status).toEqual('ok');
+        done();
+      })
+      .catch(e => {
+        done(e);
+      });
+    $scope.$apply();
+    httpBackend.flush();
+  });
+
+  it('should have a get method', (done) => {
+    httpBackend.expectGET(`${AppConfig.apiEndpoint}/core/test/1`)
+      .respond([
+        {status: 'ok'}
+      ]);
+    resource = service.getResource('/core/test');
+    resource.get({id: 1}).$promise
+      .then((res) => {
+        expect(res[0].status).toEqual('ok');
+        done();
+      })
+      .catch(e => {
+        done(e);
+      });
+    $scope.$apply();
+    httpBackend.flush();
+  });
+});
diff --git a/src/app/datasources/rest/model.rest.ts b/src/app/datasources/rest/model.rest.ts
index 45431d4..908ca0f 100644
--- a/src/app/datasources/rest/model.rest.ts
+++ b/src/app/datasources/rest/model.rest.ts
@@ -16,6 +16,6 @@
   }
 
   public getResource(url: string): ng.resource.IResourceClass<ng.resource.IResource<any>> {
-    return this.resource = this.$resource(`${AppConfig.apiEndpoint}${url}`);
+    return this.resource = this.$resource(`${AppConfig.apiEndpoint}${url}/:id`);
   }
 }
diff --git a/src/app/datasources/rest/modeldefs.rest.spec.ts b/src/app/datasources/rest/modeldefs.rest.spec.ts
new file mode 100644
index 0000000..6469cca
--- /dev/null
+++ b/src/app/datasources/rest/modeldefs.rest.spec.ts
@@ -0,0 +1,49 @@
+import * as angular from 'angular';
+import 'angular-mocks';
+import 'angular-resource';
+import 'angular-cookies';
+import {xosDataSources} from '../index';
+import {AppConfig} from '../../config/app.config';
+import {IModeldefsService} from './modeldefs.rest';
+
+let service: IModeldefsService;
+let httpBackend: ng.IHttpBackendService;
+let $scope;
+
+describe('The ModelDefs service', () => {
+
+  beforeEach(angular.mock.module(xosDataSources));
+
+  beforeEach(() => {
+    angular.mock.module(xosDataSources);
+  });
+
+
+  beforeEach(angular.mock.inject((
+    ModelDefs: IModeldefsService,
+    $httpBackend: ng.IHttpBackendService,
+    _$resource_: ng.resource.IResourceService,
+    _$rootScope_: ng.IRootScopeService
+  ) => {
+    service = ModelDefs;
+    httpBackend = $httpBackend;
+    $scope = _$rootScope_;
+  }));
+
+  it('should have a get method', (done) => {
+    httpBackend.expectGET(`${AppConfig.apiEndpoint}/utility/modeldefs/`)
+      .respond([
+        {name: 'ok'}
+      ]);
+    service.get()
+      .then((res) => {
+        expect(res[0].name).toEqual('ok');
+        done();
+      })
+      .catch(e => {
+        done(e);
+      });
+    $scope.$apply();
+    httpBackend.flush();
+  });
+});
diff --git a/src/app/datasources/stores/model.store.spec.ts b/src/app/datasources/stores/model.store.spec.ts
new file mode 100644
index 0000000..48798df
--- /dev/null
+++ b/src/app/datasources/stores/model.store.spec.ts
@@ -0,0 +1,117 @@
+import * as angular from 'angular';
+import 'angular-mocks';
+import 'angular-resource';
+import {IModelStoreService, ModelStore} from './model.store';
+import {Subject} from 'rxjs';
+import {IWSEvent} from '../websocket/global';
+import {StoreHelpers} from '../helpers/store.helpers';
+import {ModelRest} from '../rest/model.rest';
+import {AppConfig} from '../../config/app.config';
+
+let service: IModelStoreService;
+let httpBackend: ng.IHttpBackendService;
+let $scope;
+let WebSocket;
+
+class MockWs {
+  private _list;
+  constructor() {
+    this._list = new Subject<IWSEvent>();
+  }
+  list() {
+    return this._list.asObservable();
+  }
+
+  next(event: IWSEvent) {
+    this._list.next(event);
+  }
+}
+
+const queryData = [
+  {id: 1, name: 'foo'},
+  {id: 1, name: 'bar'}
+];
+
+describe('The ModelStore service', () => {
+
+  beforeEach(() => {
+    angular
+      .module('ModelStore', ['ngResource'])
+      .service('WebSocket', MockWs)
+      .service('StoreHelpers', StoreHelpers) // TODO mock
+      .service('ModelRest', ModelRest) // TODO mock
+      .service('ModelStore', ModelStore);
+
+    angular.mock.module('ModelStore');
+  });
+
+  beforeEach(angular.mock.inject((
+    ModelStore: IModelStoreService,
+    $httpBackend: ng.IHttpBackendService,
+    _$rootScope_: ng.IRootScopeService,
+    _WebSocket_: any
+  ) => {
+    service = ModelStore;
+    httpBackend = $httpBackend;
+    $scope = _$rootScope_;
+    WebSocket = _WebSocket_;
+
+    // ModelRest will call the backend
+    httpBackend.expectGET(`${AppConfig.apiEndpoint}/core/tests`)
+      .respond(queryData);
+  }));
+
+  it('should return an Observable', () => {
+    expect(typeof service.query('test').subscribe).toBe('function');
+  });
+
+  it('the first event should be the resource response', (done) => {
+    let event = 0;
+    service.query('test')
+      .subscribe(collection => {
+        event++;
+        if (event === 2) {
+          expect(collection[0].id).toEqual(queryData[0].id);
+          expect(collection[1].id).toEqual(queryData[1].id);
+          done();
+        }
+      });
+    $scope.$apply();
+    httpBackend.flush();
+  });
+
+  describe('when a web-socket event is received for that model', () => {
+    it('should update the collection', (done) => {
+      let event = 0;
+      service.query('test')
+        .subscribe(
+          collection => {
+            event++;
+            if (event === 3) {
+              expect(collection[0].id).toEqual(queryData[0].id);
+              expect(collection[1].id).toEqual(queryData[1].id);
+              expect(collection[2].id).toEqual(3);
+              expect(collection[2].name).toEqual('baz');
+              done();
+            }
+          },
+          err => {
+            console.log(err);
+            done(err);
+          }
+        );
+      window.setTimeout(() => {
+        WebSocket.next({
+          model: 'test',
+          msg: {
+            changed_fields: ['id'],
+            object: {id: 3, name: 'baz'},
+            pk: 3
+          }
+        });
+      }, 1);
+      $scope.$apply();
+      httpBackend.flush();
+    });
+  });
+});
diff --git a/src/app/datasources/stores/model.store.ts b/src/app/datasources/stores/model.store.ts
index f31d571..d306f9d 100644
--- a/src/app/datasources/stores/model.store.ts
+++ b/src/app/datasources/stores/model.store.ts
@@ -11,11 +11,11 @@
 
 export class ModelStore {
   static $inject = ['WebSocket', 'StoreHelpers', 'ModelRest'];
-  private _slices: BehaviorSubject<any[]> = new BehaviorSubject([]);
+  private _collection: BehaviorSubject<any[]> = new BehaviorSubject([]);
   constructor(
     private webSocket: IWSEventService,
     private storeHelpers: IStoreHelpersService,
-    private sliceService: IXosResourceService
+    private ModelRest: IXosResourceService
   ) {
   }
 
@@ -25,18 +25,19 @@
       .filter((e: IWSEvent) => e.model === model)
       .subscribe(
         (event: IWSEvent) => {
-          this.storeHelpers.updateCollection(event, this._slices);
-        }
+          this.storeHelpers.updateCollection(event, this._collection);
+        },
+        err => console.error
       );
-    return this._slices.asObservable();
+    return this._collection.asObservable();
   }
 
   private loadInitialData(model: string) {
     const endpoint = `/core/${model.toLowerCase()}s/`;
-    this.sliceService.getResource(endpoint).query().$promise
+    this.ModelRest.getResource(endpoint).query().$promise
       .then(
         res => {
-          this._slices.next(res);
+          this._collection.next(res);
         },
         err => console.log(`Error retrieving ${model}`, err)
       );
diff --git a/src/app/datasources/websocket/global.ts b/src/app/datasources/websocket/global.ts
index aaf142a..89a8607 100644
--- a/src/app/datasources/websocket/global.ts
+++ b/src/app/datasources/websocket/global.ts
@@ -19,7 +19,6 @@
   private _events: Subject<IWSEvent> = new Subject<IWSEvent>();
     private socket;
     constructor() {
-      console.log('socket.io');
       this.socket = io(AppConfig.websocketClient);
       this.socket.on('event', (data: IWSEvent): void => {
           this._events.next(data);
diff --git a/src/app/main.spec.ts b/src/app/main.spec.ts
index 2409163..4ac2d04 100644
--- a/src/app/main.spec.ts
+++ b/src/app/main.spec.ts
@@ -12,7 +12,7 @@
     angular.mock.module('app');
   });
 
-  it('should render the header, title, techs and footer', angular.mock.inject(($rootScope: ng.IRootScopeService, $compile: ng.ICompileService) => {
+  it('should render the header and footer', angular.mock.inject(($rootScope: ng.IRootScopeService, $compile: ng.ICompileService) => {
     const element = $compile('<app></app>')($rootScope);
     $rootScope.$digest();
     expect(element.find('xos-header').length).toEqual(1);
diff --git a/src/index.spec.js b/src/index.spec.js
index 3aaa277..8822bb5 100644
--- a/src/index.spec.js
+++ b/src/index.spec.js
@@ -2,4 +2,4 @@
 context.keys().forEach(function(f) {
   // console.log(f);
   context(f);
-});
+});
\ No newline at end of file