浏览代码

PROD-1221: oadrCancelRegistration implementation

Blake Schneider 5 年之前
父节点
当前提交
a1986913d5

+ 6 - 0
__tests__/integration/ven-registration.spec.js

@@ -40,6 +40,12 @@ describe('VEN registration', function() {
       expect(queryResponse.venId).to.be.a('string');
     });
 
+    it('should successfully cancel that registration', async () => {
+      const cancelResponse = await ven.cancelRegistration();
+      expect(cancelResponse.registrationId).to.be.a('string');
+      expect(cancelResponse.venId).to.be.a('string');
+    });
+
     after(async () => {
       await app.stop();
     });

+ 101 - 5
__tests__/unit/processes/registration.spec.js

@@ -5,6 +5,7 @@ const { v4 } = require('uuid');
 const { sequelize } = require('../../../db');
 
 const {
+  cancelParty,
   query,
   registerParty,
 } = require('../../../processes/registration');
@@ -19,7 +20,7 @@ describe('VEN registration', function() {
 
     let venId, commonName, registrationResponse;
 
-    before(async () => {
+    beforeEach(async () => {
       venId = v4().replace(/-/g, '').substring(0, 20).toUpperCase().match(/.{2}/g).join(':');
       const requestId = v4().replace(/-/g, '');
       commonName = v4().replace(/-/g, '').substring(0, 12);
@@ -63,7 +64,7 @@ describe('VEN registration', function() {
       }
 
       expect(exception).is.an('error');
-      expect(exception.message).to.eql('VenID does not match certificate.');
+      expect(exception.message).to.eql('VenID does not match certificate');
     });
 
     it('rejects registration when common name changes', async () => {
@@ -88,7 +89,7 @@ describe('VEN registration', function() {
       }
 
       expect(exception).is.an('error');
-      expect(exception.message).to.eql('Client certificate CN mismatch.');
+      expect(exception.message).to.eql('Client certificate CN mismatch');
     });
 
     it('rejects registration with existing common name but different venId', async () => {
@@ -114,7 +115,7 @@ describe('VEN registration', function() {
       }
 
       expect(exception).is.an('error');
-      expect(exception.message).to.eql('Ven already exists with that CN.');
+      expect(exception.message).to.eql('Ven already exists with that CN');
     });
   });
 
@@ -122,7 +123,7 @@ describe('VEN registration', function() {
 
     let venId, commonName, queryResponse;
 
-    before(async () => {
+    beforeEach(async () => {
       venId = v4().replace(/-/g, '').substring(0, 20).toUpperCase().match(/.{2}/g).join(':');
       const requestId = v4().replace(/-/g, '');
       commonName = v4().replace(/-/g, '').substring(0, 12);
@@ -162,4 +163,99 @@ describe('VEN registration', function() {
       expect(queryResponse.venId).to.eql(venId);
     });
   });
+
+  describe('cancelParty', function() {
+
+    let venId, commonName, registrationResponse;
+
+    beforeEach(async () => {
+      venId = v4().replace(/-/g, '').substring(0, 20).toUpperCase().match(/.{2}/g).join(':');
+      const requestId = v4().replace(/-/g, '');
+      commonName = v4().replace(/-/g, '').substring(0, 12);
+      const request = {
+        requestId: requestId,
+        venId: venId,
+        oadrProfileName: '2.0b',
+        oadrTransportName: 'simplehttp',
+        oadrReportOnly: false,
+        oadrXmlSignature: false,
+        oadrVenName: `VEN ${commonName}`,
+        oadrHttpPullModel: true
+      };
+      registrationResponse = await registerParty(request, commonName, venId);
+    });
+
+    it('successfully cancels an existing registration', async () => {
+      const cancelRequestId = v4().replace(/-/g, '');
+      const cancelRequest = {
+        requestId: cancelRequestId,
+        registrationId: registrationResponse.registrationId,
+        venId: venId
+      };
+
+      const cancelResponse = await cancelParty(cancelRequest, commonName, venId);
+      expect(cancelResponse.responseCode).to.eql('200');
+      expect(cancelResponse.responseDescription).to.eql('OK');
+      expect(cancelResponse.responseRequestId).to.eql(cancelRequestId);
+      expect(cancelResponse.venId).to.eql(venId);
+    });
+
+    it('fails if no registrationId is specified', async () => {
+      const cancelRequestId = v4().replace(/-/g, '');
+      const cancelRequest = {
+        requestId: cancelRequestId,
+        venId: venId
+      };
+
+      let error;
+      try {
+        await cancelParty(cancelRequest, commonName, venId);
+      } catch (e) {
+        error = e;
+      }
+      expect(error).to.be.an('error');
+      expect(error.message).to.eql('No registrationID in request');
+    });
+
+    it('fails if venID does not match certificate', async () => {
+      const otherVenId = v4().replace(/-/g, '');
+      const cancelRequestId = v4().replace(/-/g, '');
+      const cancelRequest = {
+        requestId: cancelRequestId,
+        registrationId: registrationResponse.registrationId,
+        venId: venId
+      };
+
+      let error;
+      try {
+        await cancelParty(cancelRequest, commonName, otherVenId);
+      } catch (e) {
+        error = e;
+      }
+      expect(error).to.be.an('error');
+      expect(error.message).to.eql('VenID does not match certificate');
+    });
+
+    it('fails if no current registration to cancel', async () => {
+      const cancelRequestId = v4().replace(/-/g, '');
+      const cancelRequest = {
+        requestId: cancelRequestId,
+        registrationId: registrationResponse.registrationId,
+        venId: venId
+      };
+
+      // first cancellation
+      await cancelParty(cancelRequest, commonName, venId);
+
+      let error;
+      try {
+        // second cancellation
+        await cancelParty(cancelRequest, commonName, venId);
+      } catch (e) {
+        error = e;
+      }
+      expect(error).to.be.an('error');
+      expect(error.message).to.eql('No current registration for VenID');
+    });
+  });
 });

文件差异内容过多而无法显示
+ 42 - 0
__tests__/unit/xml/register-party/cancel-party-registration.spec.js


文件差异内容过多而无法显示
+ 44 - 0
__tests__/unit/xml/register-party/canceled-party-registration.spec.js


文件差异内容过多而无法显示
+ 13 - 0
__tests__/unit/xml/register-party/create-party-registration.spec.js


文件差异内容过多而无法显示
+ 2 - 2
__tests__/unit/xml/register-party/created-party-registration.spec.js


+ 8 - 0
__tests__/unit/xml/register-party/js-requests.js

@@ -13,12 +13,20 @@ const createPartyRegistration1 = {
   oadrHttpPullModel: true
 };
 
+const cancelPartyRegistration1 = {
+  _type: 'oadrCancelPartyRegistration',
+  requestId: '223344',
+  registrationId: '3bd3c02dc6965c8b9240',
+  venId: '3f59d85fbdf3997dbeb1'
+};
+
 const queryRegistration1 = {
   _type: 'oadrQueryRegistration',
   requestId: '12345'
 };
 
 module.exports = {
+  cancelPartyRegistration1,
   createPartyRegistration1,
   queryRegistration1
 };

+ 9 - 0
__tests__/unit/xml/register-party/js-responses.js

@@ -10,6 +10,15 @@ const createdPartyRegistration1 = {
   pollFreqDuration: 'PT10S'
 };
 
+const canceledPartyRegistration1 = {
+  responseCode: '200',
+  responseDescription: 'OK',
+  responseRequestId: '334455',
+  registrationId: '3bd3c02dc6965c8b9240',
+  venId: '3f59d85fbdf3997dbeb1'
+};
+
 module.exports = {
+  canceledPartyRegistration1,
   createdPartyRegistration1,
 };

文件差异内容过多而无法显示
+ 11 - 29
__tests__/unit/xml/register-party/xml-requests.js


文件差异内容过多而无法显示
+ 48 - 0
__tests__/unit/xml/register-party/xml-responses.js


+ 29 - 2
client/ven.js

@@ -9,9 +9,17 @@ const {
 } = require('../xml/register-party/query-registration');
 
 const {
+  serialize: serializeCancelPartyRegistration,
+} = require('../xml/register-party/cancel-party-registration');
+
+const {
   parse: parseCreatedPartyRegistration,
 } = require('../xml/register-party/created-party-registration');
 
+const {
+  parse: parseCanceledPartyRegistration,
+} = require('../xml/register-party/canceled-party-registration');
+
 const axios = require('axios');
 const { escape } = require('querystring');
 
@@ -38,7 +46,7 @@ class Ven {
       requestId: '2233',
     };
 
-    const createdResponse = this.makeRequest(
+    const createdResponse = await this.makeRequest(
       'EiRegisterParty',
       message,
       serializeQueryRegistration,
@@ -50,6 +58,25 @@ class Ven {
     return createdResponse;
   }
 
+  async cancelRegistration() {
+    const message = {
+      requestId: '2233',
+      registrationId: this.registrationId,
+      venId: this.venId,
+    };
+
+    const cancelledResponse = await this.makeRequest(
+      'EiRegisterParty',
+      message,
+      serializeCancelPartyRegistration,
+      parseCanceledPartyRegistration,
+    );
+
+    // track registrationId for subsequent requests
+    this.registrationId = undefined;
+    return cancelledResponse;
+  }
+
   async register() {
     const message = {
       requestId: '2233',
@@ -63,7 +90,7 @@ class Ven {
       oadrHttpPullModel: true,
     };
 
-    const createdResponse = this.makeRequest(
+    const createdResponse = await this.makeRequest(
       'EiRegisterParty',
       message,
       serializeCreatePartyRegistration,

+ 73 - 9
processes/registration.js

@@ -20,20 +20,20 @@ async function registerParty(
   const requestVenId = obj.venId;
 
   if (!requestVenId) {
-    const error = new Error('No VenID in request.');
+    const error = new Error('No VenID in request');
     error.responseCode = 452;
     throw error;
   }
 
   if (requestVenId !== clientCertificateFingerprint) {
     // as per certification item #512, venId MUST be case-sensitive
-    const error = new Error('VenID does not match certificate.');
+    const error = new Error('VenID does not match certificate');
     error.responseCode = 452;
     throw error;
   }
 
   if (!clientCertificateCn) {
-    const error = new Error('Could not determine CN from client certificate.');
+    const error = new Error('Could not determine CN from client certificate');
     error.responseCode = 452;
     throw error;
   }
@@ -49,14 +49,14 @@ async function registerParty(
 
   if (existingDbRecordByVenId) {
     if (existingDbRecordByVenId.common_name !== clientCertificateCn) {
-      const error = new Error('Client certificate CN mismatch.');
+      const error = new Error('Client certificate CN mismatch');
       error.responseCode = 452;
       throw error;
     }
     registrationId = existingDbRecordByVenId.data.registrationId;
     venId = existingDbRecordByVenId.ven_id;
   } else if (existingDbRecordByCommonName) {
-    const error = new Error('Ven already exists with that CN.');
+    const error = new Error('Ven already exists with that CN');
     error.responseCode = 452;
     throw error;
   } else {
@@ -73,7 +73,7 @@ async function registerParty(
 
   return {
     responseRequestId: obj.requestId || '',
-    responseCode: 200,
+    responseCode: '200',
     responseDescription: 'OK',
     registrationId: registrationId,
     venId: venId,
@@ -97,21 +97,21 @@ async function query(obj, clientCertificateCn, clientCertificateFingerprint) {
 
   if (existingDbRecordByVenId) {
     if (existingDbRecordByVenId.common_name !== clientCertificateCn) {
-      const error = new Error('Client certificate CN mismatch.');
+      const error = new Error('Client certificate CN mismatch');
       error.responseCode = 452;
       throw error;
     }
     registrationId = existingDbRecordByVenId.data.registrationId;
     venId = existingDbRecordByVenId.ven_id;
   } else if (existingDbRecordByCommonName) {
-    const error = new Error('Ven already exists with that CN.');
+    const error = new Error('Ven already exists with that CN');
     error.responseCode = 452;
     throw error;
   }
 
   return {
     responseRequestId: obj.requestId || '',
-    responseCode: 200,
+    responseCode: '200',
     responseDescription: 'OK',
     registrationId: registrationId,
     venId: venId,
@@ -120,7 +120,71 @@ async function query(obj, clientCertificateCn, clientCertificateFingerprint) {
   };
 }
 
+async function cancelParty(
+  obj,
+  clientCertificateCn,
+  clientCertificateFingerprint,
+) {
+  logger.info(
+    'cancelParty',
+    obj,
+    clientCertificateCn,
+    clientCertificateFingerprint,
+  );
+
+  const registrationId = obj.registrationId;
+  if (!registrationId) {
+    const error = new Error('No registrationID in request');
+    error.responseCode = 452;
+    throw error;
+  }
+
+  const requestVenId = obj.venId;
+
+  if (requestVenId && requestVenId !== clientCertificateFingerprint) {
+    // as per certification item #512, venId MUST be case-sensitive
+    const error = new Error('VenID does not match certificate');
+    error.responseCode = 452;
+    throw error;
+  }
+
+  const venId = clientCertificateFingerprint;
+
+  if (!clientCertificateCn) {
+    const error = new Error('Could not determine CN from client certificate');
+    error.responseCode = 452;
+    throw error;
+  }
+
+  const existingDbRecordByVenId = await Ven.findOne({
+    where: { ven_id: venId },
+  });
+
+  if (existingDbRecordByVenId == null) {
+    const error = new Error('No current registration for VenID');
+    error.responseCode = 452;
+    throw error;
+  }
+
+  if (existingDbRecordByVenId.data.registrationId !== registrationId) {
+    const error = new Error('Incorrect registrationID for VenID');
+    error.responseCode = 452;
+    throw error;
+  }
+
+  await existingDbRecordByVenId.destroy();
+
+  return {
+    responseRequestId: obj.requestId || '',
+    responseCode: '200',
+    responseDescription: 'OK',
+    registrationId: registrationId,
+    venId: venId,
+  };
+}
+
 module.exports = {
+  cancelParty,
   query,
   registerParty,
 };

+ 11 - 3
server/controllers/register-party.js

@@ -2,9 +2,11 @@
 
 const logger = require('../../logger');
 const { parse } = require('../../xml/register-party');
-const { serialize } = require('../../xml/register-party/created-party-registration');
+const { serialize: serializeCreatedPartyRegistration } = require('../../xml/register-party/created-party-registration');
+const { serialize: serializeCanceledPartyRegistration } = require('../../xml/register-party/canceled-party-registration');
 
 const {
+  cancelParty,
   query,
   registerParty,
 } = require('../../processes/registration');
@@ -13,22 +15,28 @@ exports.postController = async (req, res) => {
   const xmlRequest = req.body;
   let parsedRequest;
   let xmlResponse;
+  let serialize;
 
   try {
     parsedRequest = await parse(xmlRequest);
     let response;
     switch(parsedRequest._type) {
       case 'oadrCreatePartyRegistration':
+        serialize = serializeCreatedPartyRegistration;
         response = await registerParty(parsedRequest, req.clientCertificateCn, req.clientCertificateFingerprint);
-        xmlResponse = serialize(response);
+        break;
+      case 'oadrCancelPartyRegistration':
+        serialize = serializeCanceledPartyRegistration;
+        response = await cancelParty(parsedRequest, req.clientCertificateCn, req.clientCertificateFingerprint);
         break;
       case 'oadrQueryRegistration':
+        serialize = serializeCreatedPartyRegistration;
         response = await query(parsedRequest, req.clientCertificateCn, req.clientCertificateFingerprint);
-        xmlResponse = serialize(response);
         break;
       default:
         throw new Error(`Unknown _type: ${parsedRequest._type}`);
     }
+    xmlResponse = serialize(response);
   } catch (e) {
     logger.warn('Error occurred processing', parsedRequest, e);
     const responseRequestId = (parsedRequest != null) ? parsedRequest.requestId : '';

+ 67 - 0
xml/register-party/cancel-party-registration.js

@@ -0,0 +1,67 @@
+'use strict';
+
+const { parseXML, childAttr, required } = require('../parser');
+const { create, fragment } = require('xmlbuilder2');
+
+const oadrPayloadNs = 'http://www.w3.org/2000/09/xmldsig#';
+const oadrNs = 'http://openadr.org/oadr-2.0b/2012/07';
+const energyInteropNs = 'http://docs.oasis-open.org/ns/energyinterop/201110';
+const energyInteropPayloadsNs =
+  'http://docs.oasis-open.org/ns/energyinterop/201110/payloads';
+const calendarNs = 'urn:ietf:params:xml:ns:icalendar-2.0';
+
+async function parse(input) {
+  const json = await parseXML(input);
+  const o =
+    json['oadrPayload']['$$']['oadrSignedObject'][0]['$$'][
+      'oadrCancelPartyRegistration'
+    ][0]['$$'];
+
+  const result = {
+    _type: 'oadrCancelPartyRegistration',
+    requestId: required(childAttr(o, 'requestID'), 'requestID'),
+    registrationId: required(childAttr(o, 'registrationID'), 'registrationID'),
+  };
+
+  const venId = childAttr(o, 'venID');
+  if (venId != null) result.venId = venId;
+
+  return result;
+}
+
+function serialize(obj) {
+  const venId =
+    obj.venId != null
+      ? fragment()
+        .ele(energyInteropNs, 'ei:venID')
+        .txt(obj.venId)
+      : fragment();
+
+  const doc = create({
+    namespaceAlias: {
+      ns: oadrPayloadNs,
+      oadr2b: oadrNs,
+      ei: energyInteropNs,
+      pyld: energyInteropPayloadsNs,
+      cal: calendarNs,
+    },
+  })
+    .ele('@oadr2b', 'oadr2b:oadrPayload')
+    .ele('oadr2b:oadrSignedObject')
+    .ele('oadr2b:oadrCancelPartyRegistration')
+    .att(energyInteropNs, 'ei:schemaVersion', '2.0b')
+    .ele('@pyld', 'pyld:requestID')
+    .txt(obj.requestId)
+    .up()
+    .ele('@ei', 'ei:registrationID')
+    .txt(obj.registrationId)
+    .up()
+    .import(venId)
+    .doc();
+  return doc.end({ headless: true, prettyPrint: false });
+}
+
+module.exports = {
+  parse,
+  serialize,
+};

+ 122 - 0
xml/register-party/canceled-party-registration.js

@@ -0,0 +1,122 @@
+'use strict';
+
+const { parseXML, childAttr, required } = require('../parser');
+const { create, fragment } = require('xmlbuilder2');
+
+const oadrPayloadNs = 'http://www.w3.org/2000/09/xmldsig#';
+const oadrNs = 'http://openadr.org/oadr-2.0b/2012/07';
+const energyInteropNs = 'http://docs.oasis-open.org/ns/energyinterop/201110';
+const energyInteropPayloadsNs =
+  'http://docs.oasis-open.org/ns/energyinterop/201110/payloads';
+
+function parseEiResponse(response) {
+  return {
+    code: required(childAttr(response, 'responseCode'), 'responseCode'),
+    description: childAttr(response, 'responseDescription'),
+    requestId: required(childAttr(response, 'requestID'), 'requestID'),
+  };
+}
+
+async function parse(input) {
+  const json = await parseXML(input);
+  const o =
+    json['oadrPayload']['$$']['oadrSignedObject'][0]['$$'][
+      'oadrCanceledPartyRegistration'
+    ][0]['$$'];
+
+  const { code, description, requestId } = parseEiResponse(
+    o['eiResponse'][0]['$$'],
+  );
+
+  const result = {
+    responseCode: code,
+    responseDescription: description,
+    responseRequestId: requestId,
+  };
+
+  if (code < 200 || code >= 300) {
+    return result;
+  }
+
+  const registrationId = childAttr(o, 'registrationID');
+  if (registrationId != null) result.registrationId = registrationId;
+
+  const venId = childAttr(o, 'venID');
+  if (venId != null) result.venId = venId;
+
+  return result;
+}
+
+function serializeEiResponse(code, description, requestId) {
+  const descriptionFrag =
+    description != null
+      ? fragment()
+        .ele(energyInteropNs, 'ei:responseDescription')
+        .txt(description)
+      : fragment();
+  return fragment()
+    .ele(energyInteropNs, 'ei:responseCode')
+    .txt(code)
+    .up()
+    .import(descriptionFrag)
+    .ele(energyInteropPayloadsNs, 'pyld:requestID')
+    .txt(requestId)
+    .up();
+}
+
+function validate(obj) {
+  if (!obj.responseCode) {
+    throw new Error('Missing responseCode');
+  }
+  if (!obj.responseRequestId) {
+    throw new Error('Missing responseRequestId');
+  }
+}
+
+function serialize(obj) {
+  validate(obj);
+
+  const registrationId =
+    obj.registrationId != null
+      ? fragment()
+        .ele(energyInteropNs, 'ei:registrationID')
+        .txt(obj.registrationId)
+      : fragment();
+  const venId =
+    obj.venId != null
+      ? fragment()
+        .ele(energyInteropNs, 'ei:venID')
+        .txt(obj.venId)
+      : fragment();
+
+  const doc = create({
+    namespaceAlias: {
+      ns: oadrPayloadNs,
+      oadr2b: oadrNs,
+      ei: energyInteropNs,
+      pyld: energyInteropPayloadsNs
+    },
+  })
+    .ele('@oadr2b', 'oadr2b:oadrPayload')
+    .ele('oadr2b:oadrSignedObject')
+    .ele('oadr2b:oadrCanceledPartyRegistration')
+    .att('@ei', 'ei:schemaVersion', '2.0b')
+    .ele('@ei', 'ei:eiResponse')
+    .import(
+      serializeEiResponse(
+        obj.responseCode,
+        obj.responseDescription,
+        obj.responseRequestId,
+      ),
+    )
+    .up()
+    .import(registrationId)
+    .import(venId)
+    .doc();
+  return doc.end({ headless: true, prettyPrint: false });
+}
+
+module.exports = {
+  parse,
+  serialize,
+};

+ 10 - 11
xml/register-party/created-party-registration.js

@@ -1,4 +1,3 @@
-/* eslint-disable indent */
 'use strict';
 
 const { parseXML, childAttr, required } = require('../parser');
@@ -69,8 +68,8 @@ function serializeEiResponse(code, description, requestId) {
   const descriptionFrag =
     description != null
       ? fragment()
-          .ele(energyInteropNs, 'ei:responseDescription')
-          .txt(description)
+        .ele(energyInteropNs, 'ei:responseDescription')
+        .txt(description)
       : fragment();
   return fragment()
     .ele(energyInteropNs, 'ei:responseCode')
@@ -85,8 +84,8 @@ function serializeEiResponse(code, description, requestId) {
 function serializeDuration(duration) {
   return duration != null
     ? fragment()
-        .ele(calendarNs, 'cal:duration')
-        .txt(duration)
+      .ele(calendarNs, 'cal:duration')
+      .txt(duration)
     : fragment();
 }
 
@@ -105,20 +104,20 @@ function serialize(obj) {
   const registrationId =
     obj.registrationId != null
       ? fragment()
-          .ele(energyInteropNs, 'ei:registrationID')
-          .txt(obj.registrationId)
+        .ele(energyInteropNs, 'ei:registrationID')
+        .txt(obj.registrationId)
       : fragment();
   const venId =
     obj.venId != null
       ? fragment()
-          .ele(energyInteropNs, 'ei:venID')
-          .txt(obj.venId)
+        .ele(energyInteropNs, 'ei:venID')
+        .txt(obj.venId)
       : fragment();
   const vtnId =
     obj.vtnId != null
       ? fragment()
-          .ele(energyInteropNs, 'ei:vtnID')
-          .txt(obj.vtnId)
+        .ele(energyInteropNs, 'ei:vtnID')
+        .txt(obj.vtnId)
       : fragment();
 
   const doc = create({

+ 6 - 0
xml/register-party/index.js

@@ -4,14 +4,20 @@ const { parseXML } = require('../parser');
 
 const { parse: parseCreatePartyRegistration } = require('./create-party-registration');
 const { parse: parseQueryRegistration } = require('./query-registration');
+const { parse: parseCancelPartyRegistration } = require('./cancel-party-registration');
 
 async function parse(input) {
   const json = await parseXML(input);
   const o = json['oadrPayload']['$$']['oadrSignedObject'][0]['$$'];
+
   if (o['oadrCreatePartyRegistration']) {
     return await parseCreatePartyRegistration(input);
   }
 
+  if (o['oadrCancelPartyRegistration']) {
+    return await parseCancelPartyRegistration(input);
+  }
+
   if (o['oadrQueryRegistration']) {
     return await parseQueryRegistration(input);
   }