NatLibFi/marc-record-js

View on GitHub
src/index.spec.js

Summary

Maintainability
F
3 days
Test Coverage
/* eslint-disable max-lines */
/* eslint-disable max-statements */
/* eslint-disable complexity */
/* eslint-disable no-unused-vars */

import generateTests from '@natlibfi/fixugen';
import {READERS} from '@natlibfi/fixura';
import {expect, assert} from 'chai';
import createDebugLogger from 'debug';
import {MarcRecord} from '.';

const debug = createDebugLogger('@natlibfi/marc-record/index.spec.js'); // <---

/******************************************************************************
 *
 * Short guide to make generated test cases:
 *
 * ----------------------------------------------------------------------------
 *
 * Test case directory structure:
 *
 *    test-fixtures/index/MyTest/01/
 *
 *        metadata.json   - Test specifications
 *        input.json      - if input record is not specified in metadata.json
 *        result.json     - if expected output record is not specified in metadata.json
 *
 * ----------------------------------------------------------------------------
 *
 * metadata.json:
 *
 *    description: string
 *
 *      Test description.
 *
 *    skip: Set true to skip this particular case.
 *
 *    only: Set true to run only this case.
 *
 * Specifying input record for test case:
 *
 *    input: object / array of strings
 *
 *      Either object suitable for MarcRecord constructor, or a list of
 *      strings joined with newlines to be used with MarcRecord.fromString.
 *
 *    noinput: [optional] true/false
 *
 *      Some test cases have no sensible input records, for example cases that test
 *      MarcRecord constructor. You can omit "input" field by setting "noinput: true".
 *
 * Specifying expected result of test case:
 *
 *    result: object / array of strings
 *
 *      Record to compare the modifications made to input. Similar to input record,
 *      you can specify the result either as object for MacrRecord(), or as a list
 *      of strings for MarcRecord.fromString()
 *
 *    immutable: [optional] true/false
 *
 *      If your test case should not modify the input, set "immutable: true"
 *      to omit expected result record. In this case, result record is compared
 *      to input record.
 *
 * Specifying operations performed in test case:
 *
 *    operations: array of objects
 *
 *      Operations performed to input record. The exact syntax is found from
 *      runOperation() function below. In general, names match to function names,
 *      but arguments may have special cases to feed internal values to functions.
 *
 *      Operations are pairs of name and args:
 *
 *          operations: [
 *            { name: "myFunc", args { ... } },
 *            { name: "myFunc", args { ... } },
 *            ...
 *          ]
 *
 *      Args are parsed in runOperation() function. You can add new operations
 *      to that specific function, as well as argument parsing for it.
 *
 *    returns: [optional] object / array
 *
 *      In some tests, you are interested in the return values of the operations,
 *      not the modifications in input record. If "returns" field is present,
 *      the return value of last operation is compared to it.
 *
 *      For example, you may check return values of MarcRecord.get() or
 *      MarcRecord.toString(), and expect the input record stays immutable.
 *
 *    throws: [optional] string
 *
 *      In some cases, you are not interested in return values, but how the
 *      operation fails. I
 *
 * ----------------------------------------------------------------------------
 *
 * Consult test case descriptions in test-fixtures/index/ directory tree.
 *
 * In case your test case is not suitable for automated generation, you can
 * add it in mocha way as usual (consult the end of this file for examples).
 *
 ******************************************************************************/

describe('index', () => {

  beforeEach(() => {
    // Reset global validation options before each case
    MarcRecord.setValidationOptions({});
  });

  //***************************************************************************
  //
  // Generate tests which use operations table in metadata to perform operations
  // to input record, and check it equals to result record after operations.
  //
  //***************************************************************************

  generateTests({
    callback: doTest,
    path: [__dirname, '..', 'test-fixtures', 'index'],
    useMetadataFile: true,
    recurse: true,
    fixura: {
      reader: READERS.JSON,
      failWhenNotFound: true
    }
  });

  function doTest(metadata) {

    // Get input & expected output
    const {getFixture} = metadata;
    const {input, result, immutable, noinput, validationOptions} = metadata;

    // if !noinput and we have input in metadata we use it, otherwise we get it from file input.json
    const inputRecord = noinput ? null : getRecord(input, 'input.json');
    const record = inputRecord ? MarcRecord.clone(inputRecord, validationOptions) : null;

    // Operations may lead to record validation errors after changes. We don't want to
    // get those errors when reading the expected result record, so we turn off
    // global validation checks temporarily.

    MarcRecord.setValidationOptions({fields: false, subfields: false, subfieldValues: false});
    const outputRecord = immutable ? inputRecord : getRecord(result, 'result.json');
    MarcRecord.setValidationOptions({});

    // Get operations
    const {operations, returns, throws} = metadata;

    checkResults(operations, throws, returns);
    expect(record).to.eql(outputRecord);

    return;

    //---------------------------------------------------------------------------

    function checkResults(operations, throws, returns) {
      //debug(`Returns: ${returns} ${result}`);
      if (throws) {
        try {
          return runOps();
        } catch (e) {
          expect(e).to.have.property('message');
          expect(e).to.have.property('validationResults');
          expect(e.message).to.match(new RegExp(`^${throws}`, 'u'));
        }
        return;
      }
      const result = runOps();
      if (returns === undefined) {
        return;
      }
      expect(result).to.eql(returns);

      function runOps() {
        return operations.reduce((_, op) => runOperation(op), record);
      }
    }

    //---------------------------------------------------------------------------

    function getRecord(fromMeta, filename) {
      const data = fromMeta || getFixture(filename);

      if (Array.isArray(data)) {
        const text = data.join('\n');
        return MarcRecord.fromString(text, validationOptions);
      }
      return new MarcRecord(data, validationOptions);
    }

    //---------------------------------------------------------------------------

    function runOperation(op) {
      const {name, args} = op;

      //-------------------------------------------------------------------------
      if (name === 'nop') {
        return record;
      }

      //-------------------------------------------------------------------------
      if (name === 'insertField') {
        expect(record.insertField(args) === record);
        return record;
      }

      //-------------------------------------------------------------------------
      if (name === 'insertFields') {
        expect(record.insertFields(args) === record);
        return record;
      }

      //-------------------------------------------------------------------------
      if (name === 'appendField') {
        expect(record.appendField(args) === record);
        return record;
      }

      //-------------------------------------------------------------------------
      if (name === 'appendFields') {
        expect(record.appendFields(args) === record);
        return record;
      }

      //-------------------------------------------------------------------------
      if (name === 'removeField') {
        const what = (function (args) {
          const {string, field, regexp, index} = args;

          if (string || field) {
            return string || field;
          }

          if (regexp) {
            return new RegExp(regexp, 'u');
          }

          if (index !== undefined) {
            return record.fields[index];
          }

          throw new Error(`No arg for ${name}(): ${JSON.stringify(args, null, 2)}`);
        }(args));

        expect(record.removeField(what) === record);
        return record;
      }

      //-------------------------------------------------------------------------
      if (name === 'removeFields') {
        const what = (function (args) {
          const {getRegExp} = args;

          if (getRegExp) {
            return record.get(new RegExp(getRegExp, 'u'));
          }

          throw new Error(`No arg for ${name}(): ${JSON.stringify(args, null, 2)}`);
        }(args));

        expect(record.removeFields(what) === record);
        return record;
      }

      //-------------------------------------------------------------------------
      if (name === 'removeSubfield') {
        const field = record.fields[args.field];
        const subfield = field.subfields[args.subfield];
        expect(record.removeSubfield(subfield, field) === record);
        return record;
      }

      //-------------------------------------------------------------------------
      if (['get', 'pop'].includes(name)) {
        const what = (function (args) {
          const {string, regexp} = args;

          if (string) {
            return string;
          }
          if (regexp) {
            return new RegExp(regexp, 'u');
          }

          throw new Error(`No arg for ${name}(): ${JSON.stringify(args, null, 2)}`);
        }(args));

        if (name === 'pop') {
          return record.pop(what); // eslint-disable-line functional/immutable-data
        }
        return record.get(what);
      }

      //-------------------------------------------------------------------------
      if (name === 'getControlfields') {
        return record.getControlfields();
      }

      //-------------------------------------------------------------------------
      if (name === 'getDatafields') {
        return record.getDatafields();
      }

      //-------------------------------------------------------------------------
      if (name === 'getValidationOptions') {
        return MarcRecord.getValidationOptions();
      }

      //-------------------------------------------------------------------------
      if (name === 'setValidationOptions') {
        return MarcRecord.setValidationOptions(args);
      }

      //-------------------------------------------------------------------------
      if (name === 'MarcRecord') {
        const {leader, fields, validationOptions} = args ?? {};
        const object = args && {leader, fields};
        //debug(`Object: ${JSON.stringify(object, null, 2)}`);

        const created = new MarcRecord(object, validationOptions);
        expect(created).to.be.an('object');
        expect(object === undefined || created.fields !== object.fields);
        //debug(`Created: ${JSON.stringify(created, null, 2)}`);
        return created;
      }

      //-------------------------------------------------------------------------
      if (name === 'clone') {
        const {validationOptions} = args ?? {};
        const cloned = MarcRecord.clone(record, validationOptions);

        // Expect cloned record to be deeply cloned, and still being identical
        expect(record._validationOptions !== cloned._validationOptions);
        expect(record.fields !== cloned.fields);
        expect(record.leader !== cloned.leader);
        expect(record.equalsTo(cloned) === true);
        return cloned;
      }

      //-------------------------------------------------------------------------
      if (name === 'toString') {
        return record.toString().split('\n');
      }

      //-------------------------------------------------------------------------
      if (name === 'toObject') {
        return record.toObject();
      }

      //-------------------------------------------------------------------------
      if (name === 'equalsTo') {
        const what = (function (args) {
          const {self, clone, string, object} = args;

          if (self) {
            return record;
          }
          if (clone) {
            return MarcRecord.clone(record);
          }
          if (string) {
            return MarcRecord.fromString(string.join('\n'));
          }
          if (object) {
            return object;
          }

          const {leader, fields, validationOptions} = args;
          return new MarcRecord({leader, fields}, validationOptions);
        }(args));

        //debug(`Record: ${JSON.stringify(record, null, 2)}`);
        //debug(`What: ${JSON.stringify(what, null, 2)}`);

        const result = record.equalsTo(what);
        expect(MarcRecord.isEqual(record, what) === result);
        return result;
      }

      //-------------------------------------------------------------------------
      if (name === 'getFields') {
        const {tag, value} = args;

        const fields = record.getFields(tag, value);
        expect(record.containsFieldWithValue(tag, value)).eql(fields.length > 0);
        return fields;
      }

      //-------------------------------------------------------------------------
      if (name === 'isTypeOfMaterial') {
        const {target} = args;

        // console.info(`TARGET: '${target}'\n${record.toString()}`); // eslint-disable-line no-console
        if (target === 'BK') { // Book
          return record.isBK();
        }
        if (target === 'CF') { // Computer File
          return record.isCF();
        }
        if (target === 'CR') { // Continuing Resource
          return record.isCR();
        }
        if (target === 'MP') { // Map
          return record.isMP();
        }
        if (target === 'MU') { // Music
          return record.isMU();
        }
        if (target === 'MX') { // Mixed
          return record.isMX();
        }
        if (target === 'VM') { // Visual Material
          return record.isVM();
        }

        return false;
      }

      //-------------------------------------------------------------------------
      if (name === 'getTypeOfMaterial') {
        return record.getTypeOfMaterial();
      }

      //-------------------------------------------------------------------------
      throw new Error(`Invalid operation: ${name}`);
    }
  }
});