Home Manual Reference Source Test

test/src/lib/server/AtviseFile.spec.js

import { Buffer } from 'buffer';
import { stub, spy } from 'sinon';
import File from 'vinyl';
import { DataType, VariantArrayType, NodeClass } from 'node-opcua';
import expect from '../../../expect';
import AtviseFile from '../../../../src/lib/server/AtviseFile';
import AtviseTypes from '../../../../src/lib/server/Types';
import NodeId from '../../../../src/lib/model/opcua/NodeId';

/** @test {AtviseFile} */
describe('AtviseFile', function () {
  /** @test {AtviseFile#constructor} */
  describe('#constructor', function () {
    it('should create a vinyl instance', function () {
      const file = new AtviseFile();

      expect(file, 'to be a', File);
    });
  });

  const tests = [
    {
      name: 'should store variables with their data type as an extension',
      nodeId: new NodeId('ns=1;s=AGENT.OBJECTS.Test'),
      dataType: DataType.UInt16,
      typeDefinition: new NodeId(NodeId.NodeIdType.NUMERIC, 62, 0),
      arrayType: VariantArrayType.Scalar,
      filePath: 'AGENT/OBJECTS/Test.uint16',
    },
    {
      name: 'should store variable arrays with their data type as an extension',
      nodeId: new NodeId('ns=1;s=AGENT.OBJECTS.Test'),
      dataType: DataType.UInt16,
      typeDefinition: new NodeId(NodeId.NodeIdType.NUMERIC, 62, 0),
      arrayType: VariantArrayType.Array,
      filePath: 'AGENT/OBJECTS/Test.uint16.array',
    },
    {
      name: 'should store variable matrices with their data type as an extension',
      nodeId: new NodeId('ns=1;s=AGENT.OBJECTS.Test'),
      dataType: DataType.UInt16,
      typeDefinition: new NodeId(NodeId.NodeIdType.NUMERIC, 62, 0),
      arrayType: VariantArrayType.Matrix,
      filePath: 'AGENT/OBJECTS/Test.uint16.matrix',
    },
    {
      name: 'should store property variables with their data type as an extension',
      nodeId: new NodeId('ns=1;s=AGENT.OBJECTS.Test.property'),
      dataType: DataType.UInt16,
      typeDefinition: new NodeId(NodeId.NodeIdType.NUMERIC, 68, 0),
      arrayType: VariantArrayType.Scalar,
      filePath: 'AGENT/OBJECTS/Test/property.prop.uint16',
    },
    {
      name: 'should store html help documents with a ".help.html" extension',
      nodeId: new NodeId('ns=1;s=AGENT.DISPLAYS.Test.en'),
      dataType: DataType.ByteString,
      typeDefinition: new NodeId('VariableTypes.ATVISE.HtmlHelp'),
      arrayType: VariantArrayType.Scalar,
      filePath: 'AGENT/DISPLAYS/Test/en.help.html',
    },
    {
      name: 'should store quickdynamics with a ".qd.xml" extension',
      nodeId: new NodeId('ns=1;s=SYSTEM.LIBRARY.PROJECT.QUICKDYNAMICS.Test'),
      dataType: DataType.XmlElement,
      typeDefinition: new NodeId('VariableTypes.ATVISE.QuickDynamic'),
      arrayType: VariantArrayType.Scalar,
      filePath: 'SYSTEM/LIBRARY/PROJECT/QUICKDYNAMICS/Test.qd.xml',
    },
    {
      name: 'should store scripts with a ".script.xml" extension',
      nodeId: new NodeId('ns=1;s=SYSTEM.LIBRARY.PROJECT.SERVERSCRIPTS.Test'),
      dataType: DataType.XmlElement,
      typeDefinition: new NodeId('VariableTypes.ATVISE.ScriptCode'),
      arrayType: VariantArrayType.Scalar,
      filePath: 'SYSTEM/LIBRARY/PROJECT/SERVERSCRIPTS/Test.script.xml',
    },
    {
      name: 'should store displays with a ".display.xml" extension',
      nodeId: new NodeId('ns=1;s=AGENT.DISPLAYS.Test'),
      dataType: DataType.XmlElement,
      typeDefinition: new NodeId('VariableTypes.ATVISE.Display'),
      arrayType: VariantArrayType.Scalar,
      filePath: 'AGENT/DISPLAYS/Test.display.xml',
    },
    {
      name: 'should store translation tables with a ".locs.xml" extension',
      nodeId: new NodeId('ns=1;s=SYSTEM.LIBRARY.PROJECT.de'),
      dataType: DataType.XmlElement,
      typeDefinition: new NodeId('VariableTypes.ATVISE.TranslationTable'),
      arrayType: VariantArrayType.Scalar,
      filePath: 'SYSTEM/LIBRARY/PROJECT/de.locs.xml',
    },
    {
      name: 'should store custom resources with their original extension',
      nodeId: new NodeId('ns=1;s=SYSTEM.LIBRARY.PROJECT.RESOURCES/Test.md'),
      dataType: DataType.ByteString,
      typeDefinition: new NodeId('VariableTypes.ATVISE.Resource.OctetStream'),
      arrayType: VariantArrayType.Scalar,
      filePath: 'SYSTEM/LIBRARY/PROJECT/RESOURCES/Test.md',
    },
  ].concat(
    AtviseTypes.filter((t) => t.constructor.name === 'AtviseResourceType').map((t) => ({
      name: `should store ${t.typeDefinition.value} resources with their original extension`,
      nodeId: new NodeId(`ns=1;s=SYSTEM.LIBRARY.PROJECT.RESOURCES/Test.${t.identifier}`),
      dataType: DataType.ByteString,
      typeDefinition: new NodeId(t.typeDefinition.value),
      arrayType: VariantArrayType.Scalar,
      filePath: `SYSTEM/LIBRARY/PROJECT/RESOURCES/Test.${t.identifier}`,
    }))
  );

  /** @test {AtviseFile.pathForReadResult} */
  describe('.pathForReadResult', function () {
    it('should store non-variables as .{nodeClass}.json', function () {
      expect(
        AtviseFile.pathForReadResult({
          nodeId: new NodeId('Path.To.Node'),
          nodeClass: NodeClass.Object,
        }),
        'to equal',
        'Path/To/Node/.Object.json'
      );
    });

    tests.forEach((test) => {
      it(test.name, function () {
        expect(
          AtviseFile.pathForReadResult({
            nodeId: test.nodeId,
            nodeClass: NodeClass.Variable,
            value: {
              $dataType: test.dataType,
              $arrayType: test.arrayType,
            },
            references: {
              HasTypeDefinition: [test.typeDefinition],
            },
          }),
          'to equal',
          test.filePath
        );
      });
    });

    it('should store custom typed variables with a ".var" extension', function () {
      expect(
        AtviseFile.pathForReadResult({
          nodeId: new NodeId('AGENT.OBJECTS.CustomVar'),
          nodeClass: NodeClass.Variable,
          value: {
            $dataType: DataType.Boolean,
            $arrayType: VariantArrayType.Scalar,
          },
          references: {
            HasTypeDefinition: [new NodeId('VariableTypes.Project.CustomType')],
          },
        }),
        'to equal',
        'AGENT/OBJECTS/CustomVar.var.bool'
      );
    });
  });

  /** @test {AtviseFile.encodeValue} */
  describe('.encodeValue', function () {
    it('should return empty buffer for null', function () {
      expect(AtviseFile.encodeValue({ value: null }), 'to equal', Buffer.from(''));
    });

    it('should store timestamp as string for DateTime values', function () {
      const now = new Date();

      expect(
        AtviseFile.encodeValue({ value: now }, DataType.DateTime, VariantArrayType.Scalar),
        'to equal',
        Buffer.from(now.toJSON())
      );
    });

    it('should store JSON encoded bytes for UInt64 values', function () {
      expect(
        AtviseFile.encodeValue({ value: [1, 2] }, DataType.UInt64, VariantArrayType.Scalar),
        'to equal',
        Buffer.from(JSON.stringify([1, 2], null, '  '))
      );
    });

    it('should use trimmed string value if no special encoder is used', function () {
      const value = 'string\n ';

      expect(
        AtviseFile.encodeValue({ value }, DataType.String, VariantArrayType.Scalar),
        'to equal',
        Buffer.from('string')
      );
    });

    it('should convert typed array', function () {
      expect(
        AtviseFile.encodeValue(
          { value: new Uint16Array([1, 2]) },
          DataType.UInt16,
          VariantArrayType.Array
        ).toString(),
        'to equal',
        JSON.stringify([1, 2], null, '  ')
      );
    });

    context('with an array passed', function () {
      it('should JSON encode standard values', function () {
        const value = ['test', 'another'];

        return expect(
          AtviseFile.encodeValue({ value }, DataType.String, VariantArrayType.Array),
          'to equal',
          Buffer.from(JSON.stringify(value, null, '  '))
        );
      });

      it('should JSON encode special encoded values', function () {
        const value = [[0, 1]];

        return expect(
          AtviseFile.encodeValue({ value }, DataType.Int64, VariantArrayType.Array),
          'to equal',
          Buffer.from(JSON.stringify([[0, 1]], null, '  '))
        );
      });

      it('should JSON encode null values', function () {
        const value = [null];

        return expect(
          AtviseFile.encodeValue({ value }, DataType.String, VariantArrayType.Array),
          'to equal',
          Buffer.from(JSON.stringify(value, null, '  '))
        );
      });
    });
  });

  /** @test {AtviseFile.decodeValue} */
  describe('.decodeValue', function () {
    it('should forward null', function () {
      expect(AtviseFile.decodeValue(null), 'to equal', null);
    });

    function testDecoderForDataType(dataType, rawValue, expectedValue) {
      it(`decoder for ${dataType} should work`, function () {
        expect(
          AtviseFile.decodeValue(Buffer.from(rawValue), dataType, VariantArrayType.Scalar),
          'to satisfy',
          expectedValue
        );
      });
    }

    const now = new Date();
    now.setMilliseconds(0);

    [
      [DataType.Boolean, 'false', false],
      [DataType.Boolean, 'true', true],
      [DataType.String, 'test', 'test'],
      [DataType.NodeId, 'ns=1;s=AGENT.DISPLAYS.Main', new NodeId('AGENT.DISPLAYS.Main')],
      [DataType.DateTime, now.toString(), now],
      ...[
        // Long int types
        DataType.Int64,
        DataType.UInt64,
      ].map((type) => [type, JSON.stringify([1, 2], null, '  '), [1, 2]]),
      ...[
        // Int types
        DataType.SByte,
        DataType.Byte,
        DataType.Int16,
        DataType.UInt16,
        DataType.Int32,
        DataType.UInt32,
      ].map((type) => [type, '13', 13]),
      ...[
        // float types
        DataType.Float,
        DataType.Double,
      ].map((type) => [type, '13.5', 13.5]),
    ].forEach((t) => testDecoderForDataType(...t));

    it('should forward binary buffer for ByteString', function () {
      const buffer = Buffer.from('test');
      expect(
        AtviseFile.decodeValue(buffer, DataType.ByteString, VariantArrayType.Scalar),
        'to equal',
        buffer
      );
    });

    it("should throw if an array variable's value is scalar", function () {
      const value = 24;
      return expect(
        () => AtviseFile.decodeValue(value, DataType.Int16, VariantArrayType.Array),
        'to throw',
        /not an array/i
      );
    });

    context('with an array passed', function () {
      it('should JSON decode standard values', function () {
        const value = ['<xml />', '<xml />'];
        const buffer = Buffer.from(JSON.stringify(value));
        expect(
          AtviseFile.decodeValue(buffer, DataType.XmlElement, VariantArrayType.Array),
          'to equal',
          value
        );
      });

      it('should JSON decode special encoded values', function () {
        const value = [[0, 1]];
        const buffer = Buffer.from(JSON.stringify(value));
        expect(
          AtviseFile.decodeValue(buffer, DataType.UInt64, VariantArrayType.Array),
          'to equal',
          [[0, 1]]
        );
      });

      it('should JSON decode null values', function () {
        const value = [null];
        const buffer = Buffer.from(JSON.stringify(value));
        expect(
          AtviseFile.decodeValue(buffer, DataType.String, VariantArrayType.Array),
          'to equal',
          value
        );
      });
    });
  });

  /** @test {AtviseFile.normalizeMtime} */
  describe('.normalizeMtime', function () {
    it('should return original without milliseconds', function () {
      const org = new Date();
      org.setMilliseconds(0);

      expect(AtviseFile.normalizeMtime(org), 'to equal', org);
    });

    it('should remove milliseconds if provided', function () {
      const org = new Date();
      org.setMilliseconds(500);

      expect(AtviseFile.normalizeMtime(org).getMilliseconds(), 'to equal', 0);
    });
  });

  /** @test {AtviseFile.fromReadResult} */
  describe('.fromReadResult', function () {
    it('should fail for variable without value', function () {
      expect(
        () =>
          AtviseFile.fromReadResult({
            nodeClass: NodeClass.Variable,
          }),
        'to throw',
        'no value'
      );
    });

    it('should return a new instance with valid readResult', function () {
      const nodeId = new NodeId('AGENT.DISPLAYS.Main');

      expect(
        AtviseFile.fromReadResult({
          nodeId,
          nodeClass: NodeClass.Variable,
          value: {
            value: '<svg></svg>',
            $dataType: DataType.XmlElement,
            $arrayType: VariantArrayType.Scalar,
          },
          references: {
            HasTypeDefinition: [new NodeId('VariableTypes.ATVISE.Display')],
          },
          mtime: new Date(),
        }),
        'to be a',
        AtviseFile
      );
    });

    it('should use undefined as mtime if not provided', function () {
      const nodeId = new NodeId('AGENT.DISPLAYS.Main');

      const file = AtviseFile.fromReadResult({
        nodeId,
        nodeClass: NodeClass.Variable,
        value: {
          value: '<svg></svg>',
          $dataType: DataType.XmlElement,
          $arrayType: VariantArrayType.Scalar,
        },
        references: {
          HasTypeDefinition: [new NodeId('VariableTypes.ATVISE.Display')],
        },
      });

      expect(file, 'to be a', AtviseFile);
      expect(file.stat.mtime, 'to be', undefined);
    });

    it('should store JSON-encoded references if not a variable-node', function () {
      const nodeId = new NodeId('AGENT');
      const references = {
        HasTypeDefinition: [new NodeId('ns=1;s=ObjectTypes.ATVISE.Server.Local')],
      };

      const file = AtviseFile.fromReadResult({
        nodeId,
        nodeClass: NodeClass.Object,
        references,
      });

      expect(file, 'to be a', AtviseFile);
      expect(file._dataType, 'to be', undefined);
      expect(file._arrayType, 'to be', undefined);
      expect(file.stat.mtime, 'to be', undefined);
      expect(JSON.parse(file.contents.toString()), 'to equal', {
        references: {
          HasTypeDefinition: [references.HasTypeDefinition[0].toString()],
        },
      });
    });

    it('should sort references in JSON file', function () {
      const nodeId = new NodeId('AGENT');
      const references = {
        HasTypeDefinition: [new NodeId('ns=1;s=ObjectTypes.ATVISE.Server.Local')],
        toParent: 'HasComponent',
        HasModellingRule: [new NodeId('ns=1;i=78')],
      };

      const file = AtviseFile.fromReadResult({
        nodeId,
        nodeClass: NodeClass.Object,
        references,
      });

      expect(
        file.contents.toString(),
        'to equal',
        `{
  "references": {
    "HasModellingRule": [
      "ns=1;i=78"
    ],
    "HasTypeDefinition": [
      "ns=1;s=ObjectTypes.ATVISE.Server.Local"
    ],
    "toParent": "HasComponent"
  }
}`
      );
    });
  });

  /** @test {AtviseFile#_getMetadata} */
  describe('#_getMetadata', function () {
    tests.forEach((test) => {
      it(test.name, function () {
        const file = new AtviseFile({ path: test.filePath });

        expect(() => file._getMetadata(), 'not to throw');
        if (test.dataType) {
          expect(file._dataType, 'to equal', test.dataType);
        }
        expect(file._arrayType, 'to equal', test.arrayType);
        expect(file._references, 'to be an object');
        expect(file._references, 'to have property', 'HasTypeDefinition');
        expect(file._references.HasTypeDefinition[0], 'to equal', test.typeDefinition);
      });
    });

    it('should use dirname extensions if filename has no extensions', function () {
      const file = new AtviseFile({ path: 'dir.display/file' });
      expect(() => file._getMetadata(), 'not to throw');
      expect(
        file._references.HasTypeDefinition[0],
        'to equal',
        new NodeId('VariableTypes.ATVISE.Display')
      );
    });

    it('should not get tripped up by multiple dots in dirname if filename has no extensions', function () {
      const file = new AtviseFile({ path: 'dir.with.multiple.dots.display/file' });
      expect(() => file._getMetadata(), 'not to throw');
      expect(
        file._references.HasTypeDefinition[0],
        'to equal',
        new NodeId('VariableTypes.ATVISE.Display')
      );
    });

    it('should parse contents for non-variable nodes', function () {
      const references = {
        HasTypeDefinition: ['ns=1;s=Type.Definition'],
      };

      const file = new AtviseFile({
        path: './path/to/object/.Object.json',
        contents: Buffer.from(JSON.stringify({ references })),
      });

      expect(() => file._getMetadata(), 'not to throw');
      expect(
        file._references.HasTypeDefinition[0],
        'to equal',
        new NodeId('ns=1;s=Type.Definition')
      );
    });
  });

  function testMetaGetter(name) {
    beforeEach(() => stub(AtviseFile.prototype, '_getMetadata').callsFake(() => {}));
    afterEach(() => AtviseFile.prototype._getMetadata.restore());

    it('should call _getMetadata if not present', function () {
      const file = new AtviseFile({ path: 'path' });
      expect(file[`_${name}`], 'to be', undefined);

      const val = file[name];

      expect(val, 'to be', undefined);
      expect(AtviseFile.prototype._getMetadata.calledOnce, 'to be', true);
    });

    it('should return stored value if present', function () {
      const value = 'value';
      const file = new AtviseFile({
        path: 'path',
        [`_${name}`]: value,
      });

      expect(file[name], 'to be', value);
      expect(AtviseFile.prototype._getMetadata, 'was not called');
    });
  }

  /** @test {AtviseFile#dataType} */
  describe('#nodeClass', function () {
    testMetaGetter('nodeClass');
  });

  /** @test {AtviseFile#dataType} */
  describe('#dataType', function () {
    testMetaGetter('dataType');
  });

  /** @test {AtviseFile#arrayType} */
  describe('#arrayType', function () {
    testMetaGetter('arrayType');
  });

  /** @test {AtviseFile#typeDefinition} */
  describe('#typeDefinition', function () {
    it('should call _getMetadata if not present', function () {
      const file = new AtviseFile({
        path: 'path',
      });
      expect(file._references, 'to be', undefined);

      spy(file, '_getMetadata');

      expect(() => file.typeDefinition, 'not to throw');

      expect(file._getMetadata, 'was called once');
    });

    it('should return stored value if present', function () {
      const value = 'value';
      const file = new AtviseFile({
        path: 'path',
        _references: {
          HasTypeDefinition: [value],
        },
      });

      spy(file, '_getMetadata');

      expect(file.typeDefinition, 'to be', value);
      expect(file._getMetadata, 'was not called');
    });

    it('should default to ns=0;i=0 for non-variable nodes', function () {
      const file = new AtviseFile({
        path: 'path/.Object.json',
        contents: Buffer.from(JSON.stringify({ references: {} })),
      });

      expect(file.typeDefinition, 'to equal', new NodeId('ns=0;i=0'));
    });
  });

  /** @test {AtviseFile#isDisplay} */
  describe('#isDisplay', function () {
    it('should return true for AtviseFiles with correct TypeDefinition', function () {
      expect(
        new AtviseFile({
          path: './src/test/path',
          _references: {
            HasTypeDefinition: [new NodeId('VariableTypes.ATVISE.Display')],
          },
        }).isDisplay,
        'to be true'
      );
    });
  });

  /** @test {AtviseFile#isScript} */
  describe('#isScript', function () {
    it('should return true for AtviseFiles with correct TypeDefinition', function () {
      expect(
        new AtviseFile({
          path: './src/test/path',
          _references: {
            HasTypeDefinition: [new NodeId('VariableTypes.ATVISE.ScriptCode')],
          },
        }).isScript,
        'to be true'
      );
    });
  });

  /** @test {AtviseFile#isQuickDynamic} */
  describe('#isQuickDynamic', function () {
    it('should return true for AtviseFiles with correct TypeDefinition', function () {
      expect(
        new AtviseFile({
          path: './src/test/path',
          _references: {
            HasTypeDefinition: [new NodeId('VariableTypes.ATVISE.QuickDynamic')],
          },
        }).isQuickDynamic,
        'to be true'
      );
    });
  });

  /** @test {AtivseFile#value} */
  describe('#value', function () {
    const val = Buffer.from('test');

    before(() => {
      stub(AtviseFile, 'decodeValue').callsFake(() => true);
      stub(AtviseFile, 'encodeValue').callsFake(() => val);
    });

    after(() => {
      AtviseFile.decodeValue.restore();
      AtviseFile.encodeValue.restore();
    });

    context('when used as getter', function () {
      it('should return decodedValue', function () {
        const file = new AtviseFile({ path: 'path.ext' });

        expect(file.value, 'to equal', true);
        expect(AtviseFile.decodeValue.calledOnce, 'to be true');
      });
    });

    context('when used as setter', function () {
      it('should set encoded value as contents', function () {
        const file = new AtviseFile({ path: 'path.ext' });
        file.value = 13;

        expect(AtviseFile.encodeValue.calledOnce, 'to be true');
        expect(file.contents, 'to equal', val);
      });
    });
  });

  /** @test {AtivseFile#createNodeValue} */
  describe('#createNodeValue', function () {
    it('should return #value for non-datetime nodes', function () {
      expect(
        new AtviseFile({
          path: 'AGENT/OBJECTS/Test.bool',
          contents: Buffer.from('true'),
        }).createNodeValue,
        'to equal',
        true
      );
    });

    it('should return timestamp for datetime nodes', function () {
      const date = new Date();
      date.setMilliseconds(0);

      expect(
        new AtviseFile({
          path: 'AGENT/OBJECTS/Test.datetime',
          contents: Buffer.from(date.toString()),
        }).createNodeValue,
        'to equal',
        date.valueOf()
      );
    });
  });

  /** @test {AtviseFile#nodeId} */
  describe('#nodeId', function () {
    it('should return id for directory with non-variable file', function () {
      expect(
        new AtviseFile({
          path: 'SYSTEM/LIBRARY/PROJECT/.Object.json',
          contents: Buffer.from(JSON.stringify({})),
        }).nodeId.value,
        'to equal',
        'SYSTEM.LIBRARY.PROJECT'
      );
    });

    it('should keep extensions for resources', function () {
      expect(
        new AtviseFile({ path: 'SYSTEM/LIBRARY/PROJECT/RESOURCES/example.js' }).nodeId.value,
        'to equal',
        'SYSTEM.LIBRARY.PROJECT.RESOURCES/example.js'
      );
    });

    it('should remove extension for non-atvise types', function () {
      expect(
        new AtviseFile({ path: 'AGENT/OBJECTS/Test.bool' }).nodeId.value,
        'to equal',
        'AGENT.OBJECTS.Test'
      );
    });
  });

  /** @test {AtviseFile#clone} */
  describe('#clone', function () {
    it('should return a file again', function () {
      expect(
        new AtviseFile({
          path: 'path/to/name.display.xml',
          _arrayType: VariantArrayType.Matrix,
        }).clone(),
        'to be a',
        AtviseFile
      );
    });

    it('should return file with the same array type', function () {
      expect(
        new AtviseFile({
          path: 'path/to/name.display.xml',
          _arrayType: VariantArrayType.Matrix,
        }).clone()._arrayType,
        'to equal',
        VariantArrayType.Matrix
      );
    });
  });

  /** @test {AtviseFile.read} */
  describe('.read', function () {
    it('should fail without path', function () {
      return expect(AtviseFile.read(), 'to be rejected with', 'options.path is required');
    });

    it('should forward read errors', function () {
      return expect(
        AtviseFile.read({
          path: 'does/not/exist',
        }),
        'to be rejected with',
        /no such file/
      );
    });

    it('should return AtviseFile if read succeeds', function () {
      return expect(
        AtviseFile.read({
          path: `${__filename}`,
        }),
        'when fulfilled',
        'to be a',
        AtviseFile
      );
    });
  });
});