Explorar o código

PROD-1221: optIn/optOut validation improvements

 * Reject opts for: Invalid event, old version of event, cancelled/completed event.
Blake Schneider %!s(int64=5) %!d(string=hai) anos
pai
achega
a6dc3d938e

+ 1 - 1
__tests__/unit/modules/nantum-responses.js

@@ -4,7 +4,7 @@ const sampleEvent1 = {
   event_identifier: 'a2fa542eca8d4e829ff5c0f0c8e68710',
   client_id: 'D8:1D:4B:20:5A:65:4C:50:32:FA',
   test_event: false,
-  event_mod_number: 0,
+  event_mod_number: 2,
   offLine: false,
   dr_mode_data: {
     operation_mode_value: 'NORMAL',

+ 39 - 1
__tests__/unit/processes/event.spec.js

@@ -5,7 +5,11 @@ const { v4 } = require('uuid');
 const sinon = require('sinon');
 const rewire = require('rewire');
 
-const { requestEvent1, createdEvent1 } = require('../xml/event/js-requests');
+const {
+  requestEvent1,
+  createdEvent1,
+  createdEventInvalidEventId1,
+} = require('../xml/event/js-requests');
 
 const { oadrPoll1 } = require('../xml/poll/js-requests');
 
@@ -101,5 +105,39 @@ describe('Event', function() {
       );
       expect(pollResponse2).to.be.undefined;
     });
+
+    it('should fail when an invalid eventId is optedIn', async () => {
+      const venId = oadrPoll1.venId;
+      const commonName = v4()
+        .replace(/-/g, '')
+        .substring(0, 12);
+
+      const response = await rewired.updateOptType(
+        createdEventInvalidEventId1,
+        commonName,
+        venId,
+      );
+      expect(response.responseCode).to.eql('454');
+      expect(response.responseDescription).to.eql(
+        'Event response references invalid event',
+      );
+    });
+
+    it('should fail when an old modificationNumber is specified', async () => {
+      const venId = oadrPoll1.venId;
+      const commonName = v4()
+        .replace(/-/g, '')
+        .substring(0, 12);
+
+      const response = await rewired.updateOptType(
+        createdEventInvalidEventId1,
+        commonName,
+        venId,
+      );
+      expect(response.responseCode).to.eql('454');
+      expect(response.responseDescription).to.eql(
+        'Event response references invalid event',
+      );
+    });
   });
 });

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1 - 1
__tests__/unit/xml/event/created-event.spec.js


+ 38 - 0
__tests__/unit/xml/event/js-requests.js

@@ -20,6 +20,42 @@ const createdEvent1 = {
       responseRequestId: '336f7e47b92eefe985ec',
       optType: 'optIn',
       eventId: 'a2fa542eca8d4e829ff5c0f0c8e68710',
+      modificationNumber: 2,
+    },
+  ],
+};
+
+const createdEventInvalidEventId1 = {
+  _type: 'oadrCreatedEvent',
+  responseCode: '200',
+  responseDescription: 'OK',
+  responseRequestId: '336f7e47b92eefe985ec',
+  venId: 'D8:1D:4B:20:5A:65:4C:50:32:FA',
+  eventResponses: [
+    {
+      responseCode: '200',
+      responseDescription: 'OK',
+      responseRequestId: '336f7e47b92eefe985ec',
+      optType: 'optIn',
+      eventId: 'invalid',
+      modificationNumber: 0,
+    },
+  ],
+};
+
+const createdEventOldModificationNumber1 = {
+  _type: 'oadrCreatedEvent',
+  responseCode: '200',
+  responseDescription: 'OK',
+  responseRequestId: '336f7e47b92eefe985ec',
+  venId: 'D8:1D:4B:20:5A:65:4C:50:32:FA',
+  eventResponses: [
+    {
+      responseCode: '200',
+      responseDescription: 'OK',
+      responseRequestId: '336f7e47b92eefe985ec',
+      optType: 'optIn',
+      eventId: 'a2fa542eca8d4e829ff5c0f0c8e68710',
       modificationNumber: 0,
     },
   ],
@@ -27,5 +63,7 @@ const createdEvent1 = {
 
 module.exports = {
   createdEvent1,
+  createdEventInvalidEventId1,
+  createdEventOldModificationNumber1,
   requestEvent1,
 };

+ 1 - 1
__tests__/unit/xml/event/js-responses.js

@@ -488,7 +488,7 @@ const generatedFromNantumEvent1 = {
     {
       eventDescriptor: {
         eventId: 'a2fa542eca8d4e829ff5c0f0c8e68710',
-        modificationNumber: 0,
+        modificationNumber: 2,
         marketContext: 'http://MarketContext1',
         createdDateTime: '2020-04-14T16:06:39.000Z',
         eventStatus: 'far',

+ 1 - 1
__tests__/unit/xml/event/xml-requests.js

@@ -29,7 +29,7 @@ const createdEvent1Xml = `<oadr2b:oadrPayload xmlns:oadr2b="http://openadr.org/o
       <ei:optType>optIn</ei:optType>
       <ei:qualifiedEventID>
        <ei:eventID>a2fa542eca8d4e829ff5c0f0c8e68710</ei:eventID>
-       <ei:modificationNumber>0</ei:modificationNumber>
+       <ei:modificationNumber>2</ei:modificationNumber>
       </ei:qualifiedEventID>
      </ei:eventResponse>
     </ei:eventResponses>

+ 57 - 20
processes/event.js

@@ -167,6 +167,35 @@ async function retrieveEvents(
   };
 }
 
+/* qualifiedEvent is the combination of eventId & modificationNumber */
+function eventResponseMatchesValidEvent(eventResponse, oadrEvents) {
+  return (
+    oadrEvents.filter(oadrEvent => {
+      return (
+        oadrEvent.eventDescriptor.eventId === eventResponse.eventId &&
+        oadrEvent.eventDescriptor.modificationNumber ===
+          eventResponse.modificationNumber &&
+        oadrEvent.eventDescriptor.status !== 'cancelled' &&
+        oadrEvent.eventDescriptor.status !== 'completed'
+      );
+    }).length > 0
+  );
+}
+
+async function validateEventResponses(venId, eventResponses) {
+  const event = await nantum.fetchEvent(venId);
+  const oadrEvents = convertToOadrEvents(event);
+  const staleResponses = eventResponses.filter(
+    eventResponse => !eventResponseMatchesValidEvent(eventResponse, oadrEvents),
+  );
+
+  if (staleResponses.length > 0) {
+    const error = new Error('Event response references invalid event');
+    error.responseCode = '454';
+    throw error;
+  }
+}
+
 async function updateOptType(
   oadrCreatedEvent,
   clientCertificateCn,
@@ -184,27 +213,35 @@ async function updateOptType(
 
   let opted = await nantum.fetchOpted(requestVenId);
 
-  //TODO: more validation: VEN may opt into an event that doesn't exist. VEN may opt into an old version of an event
-  // (modificationNumber doesn't match). May opt into a completed event. Indicate error(s) to client.
-
-  for (const eventResponse of oadrCreatedEvent.eventResponses) {
-    // remove existing opts for this eventId
-    opted = [
-      ...opted.filter(optedItem => optedItem.eventId !== eventResponse.eventId),
-    ];
-    opted.push({
-      eventId: eventResponse.eventId,
-      modificationNumber: eventResponse.modificationNumber,
-      optType: eventResponse.optType,
-    });
-  }
-  await nantum.updateOpted(requestVenId, opted);
+  try {
+    await validateEventResponses(requestVenId, oadrCreatedEvent.eventResponses);
+    for (const eventResponse of oadrCreatedEvent.eventResponses) {
+      // remove existing opts for this eventId
+      opted = [
+        ...opted.filter(
+          optedItem => optedItem.eventId !== eventResponse.eventId,
+        ),
+      ];
+      opted.push({
+        eventId: eventResponse.eventId,
+        modificationNumber: eventResponse.modificationNumber,
+        optType: eventResponse.optType,
+      });
+    }
+    await nantum.updateOpted(requestVenId, opted);
 
-  return {
-    responseCode: '200',
-    responseDescription: 'OK',
-    venId: clientCertificateFingerprint,
-  };
+    return {
+      responseCode: '200',
+      responseDescription: 'OK',
+      venId: clientCertificateFingerprint,
+    };
+  } catch (e) {
+    return {
+      responseCode: e.responseCode || '454',
+      responseDescription: e.message || 'Invalid event response received',
+      venId: clientCertificateFingerprint,
+    };
+  }
 }
 
 async function filterOutAcknowledgedEvents(venId, events) {