[CORD-2647] Handling deletion for _decl model

Change-Id: Iedbf09a59907ed85009e1e9946e6994a34ab6adf
diff --git a/src/app/datasources/index.ts b/src/app/datasources/index.ts
index d5869b5..45f2561 100644
--- a/src/app/datasources/index.ts
+++ b/src/app/datasources/index.ts
@@ -18,7 +18,7 @@
 
 import {ModelRest} from './rest/model.rest';
 import {AuthService} from './rest/auth.rest';
-import {WebSocketEvent} from './websocket/global';
+import {SocketIoService, WebSocketEvent} from './websocket/global';
 import {XosModelStore} from './stores/model.store';
 import {StoreHelpers} from './helpers/store.helpers';
 import {SynchronizerStore} from './stores/synchronizer.store';
@@ -34,6 +34,7 @@
   .module(xosDataSources, ['ngCookies', 'ngResource', xosCore])
   .service('ModelRest', ModelRest)
   .service('AuthService', AuthService)
+  .service('SocketIo', SocketIoService)
   .service('WebSocket', WebSocketEvent)
   .service('XosModeldefsCache', XosModeldefsCache)
   .service('StoreHelpers', StoreHelpers)
diff --git a/src/app/datasources/websocket/global.spec.ts b/src/app/datasources/websocket/global.spec.ts
new file mode 100644
index 0000000..7adaf07
--- /dev/null
+++ b/src/app/datasources/websocket/global.spec.ts
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import * as angular from 'angular';
+import 'angular-mocks';
+import {IWSEvent, IWSEventService, WebSocketEvent} from './global';
+
+const MockAppCfg = {
+  apiEndpoint: 'http://xos-test:3000/api',
+  websocketClient: 'http://xos-test:3000'
+};
+
+class MockSocket {
+  private cbs = {};
+
+  public on(event: string, cb: any) {
+    this.cbs[event] = cb;
+  }
+
+  public send(evt: string, data: any) {
+    const cb = this.cbs[evt];
+    cb(data);
+  }
+
+  public clean() {
+    this.cbs = {};
+  }
+}
+
+describe('The WebSocket service', () => {
+
+  let service, observable, mockWS;
+
+  beforeEach(() => {
+
+    mockWS = new MockSocket();
+
+    angular
+      .module('WebSocketEvent', [])
+      .service('WebSocket', WebSocketEvent)
+      .constant('AppConfig', MockAppCfg)
+      .value('SocketIo', {socket: mockWS});
+
+    angular.mock.module('WebSocketEvent');
+  });
+
+  beforeEach(angular.mock.inject((
+    WebSocket: IWSEventService
+  ) => {
+    service = WebSocket;
+    observable = service['_events'];
+
+    spyOn(observable, 'next');
+  }));
+
+  afterEach(() => {
+    mockWS.clean();
+    observable.next.calls.reset();
+  });
+
+  it('should have a list method', () => {
+    expect(service.list).toBeDefined();
+  });
+
+  describe('the update event', () => {
+    it('should update the base class', () => {
+      const data: IWSEvent = {
+        model: 'Test',
+        msg: {
+          pk: 1,
+          changed_fields: ['name'],
+          object: {}
+        }
+      };
+      mockWS.send('update', data);
+      expect(observable.next).toHaveBeenCalledWith(data);
+      expect(observable.next.calls.count()).toBe(1);
+    });
+
+    it('should not update the class if the changed_fields are not useful to the UI', () => {
+      const data: IWSEvent = {
+        model: 'Test',
+        msg: {
+          pk: 1,
+          changed_fields: ['created', 'updated', 'backend_register', 'backend_status', 'policy_status'],
+          object: {}
+        }
+      };
+      mockWS.send('update', data);
+      expect(observable.next).not.toHaveBeenCalled();
+    });
+
+    it('should update parent classes (if any)', () => {
+      const data: IWSEvent = {
+        model: 'ONOSApp',
+        msg: {
+          pk: 1,
+          changed_fields: ['name'],
+          object: {
+            class_names: 'ONOSApp,ONOSApp_decl,ServiceInstance,XOSBase,Model,PlModelMixIn,AttributeMixin,object'
+          }
+        }
+      };
+      mockWS.send('update', data);
+      expect(observable.next).toHaveBeenCalledWith(data);
+      const siEvent = data;
+      siEvent.model = 'ServiceInstance';
+      siEvent.skip_notification = true;
+      expect(observable.next).toHaveBeenCalledWith(siEvent);
+      expect(observable.next.calls.count()).toBe(2);
+    });
+  });
+
+  describe('the remove event', () => {
+    it('should trigger the remove event', () => {
+      const data: IWSEvent = {
+        model: 'Test',
+        msg: {
+          pk: 1,
+          changed_fields: [],
+          object: {}
+        },
+      };
+      mockWS.send('remove', data);
+      expect(observable.next).toHaveBeenCalledWith(data);
+    });
+
+    it('should update parent classes (if any)', () => {
+      const data: IWSEvent = {
+        model: 'ONOSApp',
+        msg: {
+          pk: 1,
+          changed_fields: ['name'],
+          object: {
+            class_names: 'ONOSApp,ONOSApp_decl,ServiceInstance,XOSBase,Model,PlModelMixIn,AttributeMixin,object'
+          }
+        }
+      };
+      mockWS.send('remove', data);
+      expect(observable.next).toHaveBeenCalledWith(data);
+      const siEvent = data;
+      siEvent.model = 'ServiceInstance';
+      siEvent.skip_notification = true;
+      expect(observable.next).toHaveBeenCalledWith(siEvent);
+      expect(observable.next.calls.count()).toBe(2);
+    });
+
+    it('should update derived class if the original is _decl', () => {
+      const data: IWSEvent = {
+        model: 'ONOSApp_decl',
+        msg: {
+          pk: 1,
+          changed_fields: ['name'],
+          object: {
+            class_names: 'ONOSApp_decl,ServiceInstance,XOSBase,Model,PlModelMixIn,AttributeMixin,object'
+          }
+        }
+      };
+      mockWS.send('remove', data);
+
+      const nextData = angular.copy(data);
+      nextData.model = 'ONOSApp';
+      expect(observable.next).toHaveBeenCalledWith(nextData);
+      const declEvent = nextData;
+      declEvent.model = 'ServiceInstance';
+      declEvent.skip_notification = true;
+      expect(observable.next).toHaveBeenCalledWith(declEvent);
+      expect(observable.next.calls.count()).toBe(2);
+    });
+  });
+});
diff --git a/src/app/datasources/websocket/global.ts b/src/app/datasources/websocket/global.ts
index 8774173..54c7209 100644
--- a/src/app/datasources/websocket/global.ts
+++ b/src/app/datasources/websocket/global.ts
@@ -36,10 +36,11 @@
   list(): Observable<IWSEvent>;
 }
 
-export class WebSocketEvent {
+export class WebSocketEvent implements IWSEventService {
 
   static $inject = [
     'AppConfig',
+    'SocketIo',
     '$log'
   ];
 
@@ -48,18 +49,26 @@
   private socket;
   constructor(
     private AppConfig: IXosAppConfig,
+    private SocketIo: any,
     private $log: ng.ILogService
   ) {
     // NOTE list of field that are not useful to the UI
-    const ignoredFields: string[] = ['created', 'updated', 'backend_register'];
+    const ignoredFields: string[] = ['created', 'updated', 'backend_register', 'backend_status', 'policy_status'];
 
-    this.socket = io(this.AppConfig.websocketClient);
+    this.socket = this.SocketIo.socket;
 
     this.socket.on('remove', (data: IWSEvent): void => {
       this.$log.info(`[WebSocket] Received Remove Event for: ${data.model} [${data.msg.pk}]`, data);
+
+      if (data.model.indexOf('_decl') > -1) {
+        // the GUI doesn't know about _decl models,
+        // send the event for the actual model
+        data.model = data.model.replace('_decl', '');
+      }
       this._events.next(data);
 
-      // TODO update observers of parent classes
+      // NOTE update observers of parent classes
+      this.updateParentClasses(data);
     });
 
     this.socket.on('update', (data: IWSEvent): void => {
@@ -74,22 +83,57 @@
         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(',');
-          let event: IWSEvent = angular.copy(data);
-          _.forEach(models, (m: string) => {
-            // send event only if the parent class is not the same as the model class
-            if (event.model !== m && m !== 'object') {
-              event.model = m;
-              event.skip_notification = true;
-              this._events.next(event);
-            }
-          });
-        }
+        this.updateParentClasses(data);
 
       });
     }
-    list() {
+
+    public list() {
       return this._events.asObservable();
     }
+
+    private updateParentClasses(data: IWSEvent) {
+      if (data.msg.object.class_names && angular.isString(data.msg.object.class_names)) {
+        const models = data.msg.object.class_names.split(',');
+        let event: IWSEvent = angular.copy(data);
+        _.forEach(models, (m: string) => {
+
+          switch (m) {
+            case 'object':
+            case 'XOSBase':
+            case 'Model':
+            case 'PlModelMixIn':
+            case 'AttributeMixin':
+              // do not send events for classes that we don't care about
+              break;
+            default:
+              if (m.indexOf('_decl') > -1 || event.model === m) {
+                // do not send events for _decl classes
+                // or if the parent class is the same as the model class
+                return;
+              }
+
+              event.model = m;
+              event.skip_notification = true;
+              this._events.next(event);
+          }
+        });
+      }
+    }
+}
+
+export class SocketIoService {
+
+  static $inject = [
+    'AppConfig'
+  ];
+
+  public socket;
+
+  constructor(
+    private AppConfig: IXosAppConfig,
+    private $log: ng.ILogService
+  ) {
+    this.socket = io(this.AppConfig.websocketClient);
+  }
 }