Merge "[CORD-1133] E2E GUI Tests"
diff --git a/Dockerfile.xos-gui-extension-builder b/Dockerfile.xos-gui-extension-builder
index ea41050..f04fa3e 100644
--- a/Dockerfile.xos-gui-extension-builder
+++ b/Dockerfile.xos-gui-extension-builder
@@ -1,6 +1,6 @@
 # To build use: docker build -t xosproject/xos-gui-extension-builder .
 
-FROM node:argon
+FROM node:7.9.0
 
 # Set environment vars
 ENV CODE_SOURCE .
diff --git a/conf/browsersync.conf.js b/conf/browsersync.conf.js
index 391f688..3f262ba 100644
--- a/conf/browsersync.conf.js
+++ b/conf/browsersync.conf.js
@@ -1,6 +1,7 @@
 const conf = require('./gulp.conf');
 const proxy = require('./proxy').proxy;
 const extensionsProxy = require('./proxy').extensionsProxy;
+const socketProxy = require('./proxy').socketProxy;
 
 module.exports = function () {
   return {
@@ -10,9 +11,12 @@
         conf.paths.src
       ],
       middleware: function(req, res, next){
-        if (req.url.indexOf('xosapi') !== -1 || req.url.indexOf('socket.io') !== -1) {
+        if (req.url.indexOf('xosapi') !== -1) {
           proxy.web(req, res);
         }
+        else if (req.url.indexOf('socket.io') !== -1) {
+          socketProxy.web(req, res);
+        }
         else if (req.url.indexOf('extensions') !== -1) {
           extensionsProxy.web(req, res);
         }
diff --git a/conf/proxy.js b/conf/proxy.js
index 8af184d..d45a27b 100644
--- a/conf/proxy.js
+++ b/conf/proxy.js
@@ -3,21 +3,34 @@
 const target = process.env.PROXY || '192.168.46.100';
 
 const proxy = httpProxy.createProxyServer({
-  target: `http://${target}:9101`
+  target: `http://${target}`
 });
 
 const extensionsProxy = httpProxy.createProxyServer({
   target: `http://${target}/xos/`
 });
 
+const socketProxy = httpProxy.createProxyServer({
+  target: `http://${target}/`
+});
+
 proxy.on('error', function(error, req, res) {
-  res.writeHead(500, {
-    'Content-Type': 'text/plain'
-  });
+  res.writeHead(500, {'Content-Type': 'text/plain'});
   console.error('[Proxy]', error);
 });
 
+extensionsProxy.on('error', function(error, req, res) {
+  res.writeHead(500, {'Content-Type': 'text/plain'});
+  console.error('[extensionsProxy]', error);
+});
+
+socketProxy.on('error', function(error, req, res) {
+  res.writeHead(500, {'Content-Type': 'text/plain'});
+  console.error('[socketProxy]', error);
+});
+
 module.exports = {
   proxy,
-  extensionsProxy
+  extensionsProxy,
+  socketProxy
 };
\ No newline at end of file
diff --git a/package.json b/package.json
index 40be132..2307f81 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,5 @@
 {
-  "version": "2.0.0",
+  "version": "3.0.0",
   "dependencies": {
     "angular": "1.6.3",
     "angular-animate": "1.6.3",
diff --git a/src/app/core/loader/loader.spec.ts b/src/app/core/loader/loader.spec.ts
index 1bcff9c..d4b88dd 100644
--- a/src/app/core/loader/loader.spec.ts
+++ b/src/app/core/loader/loader.spec.ts
@@ -3,6 +3,7 @@
 import {xosLoader} from './loader';
 
 let loaded = true;
+let authenticated = true;
 
 const MockConfig = {
   lastVisitedUrl: '/test'
@@ -17,6 +18,15 @@
   onboard: null
 };
 
+const MockAuth = {
+  isAuthenticated: jasmine.createSpy('isAuthenticated')
+    .and.callFake(() => authenticated)
+};
+
+const MockState = {
+  go: jasmine.createSpy('state.go')
+};
+
 describe('The XosLoader component', () => {
   beforeEach(() => {
     angular
@@ -24,6 +34,8 @@
       .value('XosConfig', MockConfig)
       .value('XosModelDiscoverer', MockDiscover)
       .value('XosOnboarder', MockOnboarder)
+      .value('AuthService', MockAuth)
+      .value('$state', MockState)
       .component('xosLoader', xosLoader);
     angular.mock.module('loader');
   });
@@ -99,6 +111,24 @@
     });
   });
 
+  describe('when user is not authenticated', () => {
+
+    beforeEach(() => {
+      loaded = false;
+      authenticated = false;
+      compileElement();
+      isolatedScope.run();
+    });
+
+    it('should redirect to the login page', () => {
+      expect(MockState.go).toHaveBeenCalledWith('xos.login');
+    });
+
+    afterEach(() => {
+      authenticated = true;
+    });
+  });
+
   describe('when models are not loaded', () => {
 
     beforeEach(() => {
diff --git a/src/app/core/loader/loader.ts b/src/app/core/loader/loader.ts
index 07a9875..f12b17b 100644
--- a/src/app/core/loader/loader.ts
+++ b/src/app/core/loader/loader.ts
@@ -1,23 +1,26 @@
 import {IXosModelDiscovererService} from '../../datasources/helpers/model-discoverer.service';
 import {IXosOnboarder} from '../../extender/services/onboard.service';
+import {IXosAuthService} from '../../datasources/rest/auth.rest';
 class LoaderCtrl {
   static $inject = [
     '$log',
     '$rootScope',
     '$location',
     '$timeout',
+    '$state',
+    'AuthService',
     'XosConfig',
     'XosModelDiscoverer',
     `XosOnboarder`
   ];
 
-  public aaaaa = 'ciao';
-
   constructor (
     private $log: ng.ILogService,
     private $rootScope: ng.IScope,
     private $location: ng.ILocationService,
     private $timeout: ng.ITimeoutService,
+    private $state: ng.ui.IStateService,
+    private XosAuthService: IXosAuthService,
     private XosConfig: any,
     private XosModelDiscoverer: IXosModelDiscovererService,
     private XosOnboarder: IXosOnboarder
@@ -30,6 +33,9 @@
     if (this.XosModelDiscoverer.areModelsLoaded()) {
       this.moveOnTo(this.XosConfig.lastVisitedUrl);
     }
+    else if (!this.XosAuthService.isAuthenticated()) {
+      this.$state.go('xos.login');
+    }
     else {
       this.XosModelDiscoverer.discover()
       // NOTE loading XOS Models
@@ -48,7 +54,6 @@
         })
         .finally(() => {
           // NOTE it is in a timeout as the searchService is loaded after that
-          // we navigate to another page
           this.$timeout(() => {
             this.$rootScope.$emit('xos.core.modelSetup');
           }, 500);
diff --git a/src/app/core/services/helpers/config.helpers.ts b/src/app/core/services/helpers/config.helpers.ts
index 1cd9e60..c495ba5 100644
--- a/src/app/core/services/helpers/config.helpers.ts
+++ b/src/app/core/services/helpers/config.helpers.ts
@@ -269,10 +269,6 @@
 
       item.$save()
         .then((res) => {
-          if (res.status === 403 || res.status === 405 || res.status === 404 || res.status === 500) {
-            // TODO understand why 405 does not go directly in catch (it may be related to ng-rest-gw)
-            throw new Error();
-          }
           formCfg.feedback = {
             show: true,
             message: `${model.name} succesfully saved`,
@@ -282,8 +278,13 @@
           this.toastr.success(`${model.name} succesfully saved`);
         })
         .catch(err => {
-          // TODO keep the edited model
-          this.toastr.error(`Error while saving ${model.name}`);
+          formCfg.feedback = {
+            show: true,
+            message: `Error while saving ${model.name}: ${err.error}. ${err.specific_error || ''}`,
+            type: 'danger',
+            closeBtn: true
+          };
+          this.toastr.error(err.specific_error || '', `Error while saving ${model.name}: ${err.error}`);
         });
     };
 
diff --git a/src/app/datasources/websocket/global.ts b/src/app/datasources/websocket/global.ts
index a820269..7e5dad7 100644
--- a/src/app/datasources/websocket/global.ts
+++ b/src/app/datasources/websocket/global.ts
@@ -23,27 +23,38 @@
     '$log'
   ];
 
+
   private _events: Subject<IWSEvent> = new Subject<IWSEvent>();
-    private socket;
-    constructor(
-      private AppConfig: IXosAppConfig,
-      private $log: ng.ILogService
-    ) {
-      this.socket = io(this.AppConfig.websocketClient);
-      this.socket.on('event', (data: IWSEvent): void => {
-          this.$log.debug(`[WebSocket] Received Event for: ${data.model} [${data.msg.pk}]`);
-          this._events.next(data);
+  private socket;
+  constructor(
+    private AppConfig: IXosAppConfig,
+    private $log: ng.ILogService
+  ) {
+    // NOTE list of field that are not useful to the UI
+    const ignoredFields: string[] = ['created', 'updated', 'backend_register'];
 
-          // NOTE update observers of parent classes
-          if (data.msg.object.class_names && angular.isString(data.msg.object.class_names)) {
-            const models = data.msg.object.class_names.split(',');
-            _.forEach(models, (m: string) => {
-              data.model = m;
-              this._events.next(data);
-            });
-          }
+    this.socket = io(this.AppConfig.websocketClient);
+    this.socket.on('event', (data: IWSEvent): void => {
+        this.$log.debug(`[WebSocket] Received Event for: ${data.model} [${data.msg.pk}]`);
 
-        });
+        if (data.msg.changed_fields.length === 0 || _.intersection(data.msg.changed_fields, ignoredFields).length === data.msg.changed_fields.length) {
+          // NOTE means that the only updated fields does not change anything in the UI, so don't send events around
+          this.$log.debug(`[WebSocket] Ignoring Event for: ${data.model} [${data.msg.pk}]`);
+          return;
+        }
+
+        this._events.next(data);
+
+        // NOTE update observers of parent classes
+        if (data.msg.object.class_names && angular.isString(data.msg.object.class_names)) {
+          const models = data.msg.object.class_names.split(',');
+          _.forEach(models, (m: string) => {
+            data.model = m;
+            this._events.next(data);
+          });
+        }
+
+      });
     }
     list() {
       return this._events.asObservable();
diff --git a/src/interceptors.ts b/src/interceptors.ts
index 8a11d08..a1b5df8 100644
--- a/src/interceptors.ts
+++ b/src/interceptors.ts
@@ -11,17 +11,16 @@
     switch (res.status) {
       case -1:
       case 401:
-      case 500:
         $cookies.remove('sessionid', {path: '/'});
         $state.go('login');
         return $q.reject(res);
       default:
-        return res;
+        return $q.reject(res);
     }
   };
 
   return {
-    response: checkLogin,
+    // response: checkLogin,
     responseError: checkLogin
   };
 }
@@ -39,7 +38,7 @@
   };
 }
 
-export function NoHyperlinksInterceptor() {
+export function NoHyperlinksInterceptor($q: ng.IQService) {
   return {
     request: (req) => {
       if (req.url.indexOf('.html') === -1) {
@@ -68,6 +67,9 @@
         res.data = res.data;
       }
       return res;
+    },
+    responseError: (res) => {
+      return $q.reject(res.data);
     }
   };
 }
diff --git a/typings.json b/typings.json
index a830275..709ff96 100644
--- a/typings.json
+++ b/typings.json
@@ -5,11 +5,12 @@
     "angular-mocks": "github:DefinitelyTyped/DefinitelyTyped/angularjs/angular-mocks.d.ts#dc9dabe74a5be62613b17a3605309783a12ff28a",
     "angular-resource": "registry:dt/angular-resource#1.5.0+20161114123626",
     "angular-ui-router": "registry:dt/angular-ui-router#1.1.5+20160707113237",
-    "es6-shim": "registry:dt/es6-shim#0.31.2+20160602141504",
+    "es6-shim": "registry:dt/es6-shim#0.31.2+20160726072212",
     "jasmine": "github:DefinitelyTyped/DefinitelyTyped/jasmine/jasmine.d.ts#dc9dabe74a5be62613b17a3605309783a12ff28a",
     "jasmine-jquery": "registry:dt/jasmine-jquery#1.5.8+20161128184045",
     "jquery": "registry:dt/jquery#1.10.0+20161119044246",
     "require": "registry:dt/require#2.1.20+20160316155526",
+    "rx": "npm:rx/ts/rx.all.d.ts",
     "socket.io-client": "registry:dt/socket.io-client#1.4.4+20160317120654"
   },
   "dependencies": {