@natlibfi/melinda-rest-api-commons 6.0.1-alpha.1 → 6.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,197 @@
1
+ // Edge-case tests for the MARC fix helpers.
2
+
3
+ import assert from 'node:assert';
4
+ import {test} from 'node:test';
5
+ import {MarcRecord} from '@natlibfi/marc-record';
6
+ import {fixRecord} from '../../src/helpers/fix.js';
7
+ import {BIB_STRIP_F884S_SETTINGS, BIB_F035_TO_SID} from '../../src/helpers/fix-constants.js';
8
+
9
+ // a Melinda f884 (tag 884, $5 MELINDA, $a + $g present) that
10
+ // has NO $k subfield. findSource/findHash used to call `sfK.replace(...)` on
11
+ // undefined and crash the whole fixRecord call. Now a missing $k is treated
12
+ // as an empty string: the field gets NO_SOURCE (source) and the all-zero
13
+ // sentinel (hash), and the merged date range is applied to its $g.
14
+ test('fixRecord handles Melinda f884 without $k subfield without crashing', () => {
15
+ const record = new MarcRecord({
16
+ leader: '03085cam a22006858i 4500',
17
+ fields: [
18
+ {tag: '001', value: '000000001'},
19
+ {tag: '003', value: 'FI-MELINDA'},
20
+ {tag: '005', value: '20220818041047.0'},
21
+ {
22
+ // Melinda f884 WITHOUT $k
23
+ tag: '884',
24
+ ind1: ' ',
25
+ ind2: ' ',
26
+ subfields: [
27
+ {code: 'a', value: 'ONIX3 to MARC transformation'},
28
+ {code: 'g', value: '20220101'},
29
+ {code: '5', value: 'MELINDA'}
30
+ ]
31
+ },
32
+ {
33
+ // A second Melinda f884 with $k so that the
34
+ // "not enough Melinda f884s" early return does not trigger
35
+ tag: '884',
36
+ ind1: ' ',
37
+ ind2: ' ',
38
+ subfields: [
39
+ {code: 'a', value: 'ONIX3 to MARC transformation'},
40
+ {code: 'g', value: '20000718'},
41
+ {
42
+ code: 'k',
43
+ value: 'SRC_4:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
44
+ },
45
+ {code: '5', value: 'MELINDA'}
46
+ ]
47
+ }
48
+ ]
49
+ }, {subfieldValues: false});
50
+
51
+ // Fixed behaviour: fixRecord must not throw
52
+ const result = fixRecord(record, BIB_STRIP_F884S_SETTINGS);
53
+
54
+ // Both Melinda f884s survive (they are not identical, so uniq keeps both)
55
+ const f884s = result.fields.filter((f) => f.tag === '884');
56
+ assert.equal(f884s.length, 2);
57
+
58
+ // The field WITHOUT $k still has its $a, $g and $5 MELINDA
59
+ const noK = f884s.find((f) => !f.subfields.some((s) => s.code === 'k'));
60
+ assert.ok(noK, 'field without $k should survive');
61
+ assert.deepEqual(
62
+ noK.subfields.map((s) => s.code).sort(),
63
+ ['5', 'a', 'g']
64
+ );
65
+ const gNoK = noK.subfields.find((s) => s.code === 'g').value;
66
+ const aNoK = noK.subfields.find((s) => s.code === 'a').value;
67
+ assert.equal(aNoK, 'ONIX3 to MARC transformation');
68
+ assert.equal(gNoK, '20220101');
69
+
70
+ // The field WITH $k keeps its $a, $g, $k and $5 MELINDA
71
+ const withK = f884s.find((f) => f.subfields.some((s) => s.code === 'k'));
72
+ assert.ok(withK, 'field with $k should survive');
73
+ assert.deepEqual(
74
+ withK.subfields.map((s) => s.code).sort(),
75
+ ['5', 'a', 'g', 'k']
76
+ );
77
+ assert.equal(withK.subfields.find((s) => s.code === 'g').value, '20000718');
78
+ assert.equal(withK.subfields.find((s) => s.code === 'k').value,
79
+ 'SRC_4:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
80
+ });
81
+
82
+ // stripF884s early return: fewer than 2 Melinda f884s (0 or 1) means the
83
+ // record is returned unchanged (the merge/uniq pipeline never runs).
84
+ test('stripF884s returns the record unchanged when it has no Melinda f884s', () => {
85
+ const record = new MarcRecord({
86
+ leader: '03085cam a22006858i 4500',
87
+ fields: [
88
+ {tag: '001', value: '000000001'},
89
+ {tag: '035', ind1: ' ', ind2: ' ', subfields: [{code: 'a', value: '(FI-BTJ)12345'}]}
90
+ ]
91
+ }, {subfieldValues: false});
92
+
93
+ const result = fixRecord(record, BIB_STRIP_F884S_SETTINGS);
94
+ assert.deepStrictEqual(result, record.toObject());
95
+ });
96
+
97
+ test('stripF884s returns the record unchanged when it has a single Melinda f884', () => {
98
+ const record = new MarcRecord({
99
+ leader: '03085cam a22006858i 4500',
100
+ fields: [
101
+ {tag: '001', value: '000000001'},
102
+ {
103
+ // a single Melinda f884: fewer than 2, so the field must survive
104
+ // untouched (no date-range merge, no removeFields/insertFields)
105
+ tag: '884',
106
+ ind1: ' ',
107
+ ind2: ' ',
108
+ subfields: [
109
+ {code: 'a', value: 'ONIX3 to MARC transformation'},
110
+ {code: 'g', value: '20220101'},
111
+ {
112
+ code: 'k',
113
+ value: 'SRC_4:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
114
+ },
115
+ {code: '5', value: 'MELINDA'}
116
+ ]
117
+ }
118
+ ]
119
+ }, {subfieldValues: false});
120
+
121
+ const result = fixRecord(record, BIB_STRIP_F884S_SETTINGS);
122
+ assert.deepStrictEqual(result, record.toObject());
123
+ });
124
+
125
+ // generateMissingSIDs: an existing SID field is only counted when its $b is
126
+ // in fSIDFilters AND it has a $c. Anything else maps to undefined and is
127
+ // filtered out, so the SID is (re)generated from the f035.
128
+ test('fixRecord ignores an existing SID field whose $b is not in fSIDFilters and generates the SID from the f035', () => {
129
+ const record = new MarcRecord({
130
+ leader: '03085cam a22006858i 4500',
131
+ fields: [
132
+ {tag: '001', value: '000000001'},
133
+ {tag: '035', ind1: ' ', ind2: ' ', subfields: [{code: 'a', value: '(FI-BTJ)12345'}]},
134
+ {
135
+ // $b 'FI-KV' is NOT in fSIDFilters -> the field must be ignored,
136
+ // so the f035 (FI-BTJ)12345 still yields a generated SID
137
+ tag: 'SID',
138
+ ind1: ' ',
139
+ ind2: ' ',
140
+ subfields: [
141
+ {code: 'c', value: '12345'},
142
+ {code: 'b', value: 'FI-KV'}
143
+ ]
144
+ }
145
+ ]
146
+ }, {subfieldValues: false});
147
+
148
+ const result = fixRecord(record, {generateMissingSIDs: BIB_F035_TO_SID});
149
+ const sids = result.fields.filter((f) => f.tag === 'SID');
150
+ assert.equal(sids.length, 2);
151
+ const generated = sids.find((f) => f.subfields.some((s) => s.code === 'b' && s.value === 'FI-BTJ'));
152
+ assert.ok(generated, 'a new SID with $b FI-BTJ should have been generated');
153
+ assert.equal(generated.subfields.find((s) => s.code === 'c').value, '12345');
154
+ });
155
+
156
+ test('fixRecord ignores an existing SID field with a matching $b but missing $c and generates the SID from the f035', () => {
157
+ const record = new MarcRecord({
158
+ leader: '03085cam a22006858i 4500',
159
+ fields: [
160
+ {tag: '001', value: '000000001'},
161
+ {tag: '035', ind1: ' ', ind2: ' ', subfields: [{code: 'a', value: '(FI-BTJ)12345'}]},
162
+ {
163
+ // $b 'FI-BTJ' IS in fSIDFilters, but there is no $c -> the field
164
+ // must be ignored, so the f035 still yields a generated SID
165
+ tag: 'SID',
166
+ ind1: ' ',
167
+ ind2: ' ',
168
+ subfields: [
169
+ {code: 'b', value: 'FI-BTJ'}
170
+ ]
171
+ }
172
+ ]
173
+ }, {subfieldValues: false});
174
+
175
+ const result = fixRecord(record, {generateMissingSIDs: BIB_F035_TO_SID});
176
+ const sids = result.fields.filter((f) => f.tag === 'SID');
177
+ assert.equal(sids.length, 2);
178
+ const generated = sids.find((f) => f.subfields.some((s) => s.code === 'c'));
179
+ assert.ok(generated, 'a generated SID with $c should exist');
180
+ assert.equal(generated.subfields.find((s) => s.code === 'b').value, 'FI-BTJ');
181
+ assert.equal(generated.subfields.find((s) => s.code === 'c').value, '12345');
182
+ });
183
+
184
+ test('fixRecord adds no SID when the f035 $a matches no f035Filter', () => {
185
+ const record = new MarcRecord({
186
+ leader: '03085cam a22006858i 4500',
187
+ fields: [
188
+ {tag: '001', value: '000000001'},
189
+ // $a does not match any f035Filter in BIB_F035_TO_SID, so
190
+ // f035ToSidInfo maps the field to undefined and nothing is generated
191
+ {tag: '035', ind1: ' ', ind2: ' ', subfields: [{code: 'a', value: '(NOT-IN-FILTERS)999'}]}
192
+ ]
193
+ }, {subfieldValues: false});
194
+
195
+ const result = fixRecord(record, {generateMissingSIDs: BIB_F035_TO_SID});
196
+ assert.equal(result.fields.filter((f) => f.tag === 'SID').length, 0);
197
+ });
@@ -22,10 +22,28 @@ generateTests({
22
22
  });
23
23
 
24
24
  function callback({getFixture, settings = undefined}) {
25
- if (settings !== undefined && fixSettings[settings] === undefined) {
25
+ if (
26
+ settings !== undefined
27
+ && typeof settings !== 'object'
28
+ && fixSettings[settings] === undefined
29
+ ) {
26
30
  throw new Error(`There are no settings ${settings} defined!`);
27
31
  }
28
- const runSettings = fixSettings[settings] || settings;
32
+ let runSettings = fixSettings[settings] || settings;
33
+ // metadata.json is plain JSON, so f035Filters may contain string
34
+ // patterns which fix.js cannot use as-is; convert them to RegExp
35
+ if (runSettings && runSettings.generateMissingSIDs) {
36
+ runSettings = {
37
+ ...runSettings,
38
+ generateMissingSIDs: {
39
+ ...runSettings.generateMissingSIDs,
40
+ f035Filters: (runSettings.generateMissingSIDs.f035Filters || []).map(
41
+ filter => (typeof filter === 'string' ? new RegExp(filter) : filter)
42
+ ),
43
+ fSIDFilters: runSettings.generateMissingSIDs.fSIDFilters
44
+ }
45
+ };
46
+ }
29
47
 
30
48
  const record = new MarcRecord(getFixture('record.json'), {subfieldValues: false});
31
49
  const expectedRecord = getFixture('expectedRecord.json');
@@ -31,7 +31,9 @@ describe('services/validate', () => {
31
31
  ]
32
32
  });
33
33
  const {_validationOptions, ...expectedRecordData} = expectedRecord;
34
- assert.deepStrictEqual(result, {...rest, record: new MarcRecord(expectedRecordData, _validationOptions)});
34
+ const {record: actualRecord, ...actualRest} = result;
35
+ assert.deepStrictEqual(actualRest, rest);
36
+ assert.deepStrictEqual(actualRecord.toObject(), new MarcRecord(expectedRecordData, _validationOptions).toObject());
35
37
  assert.equal(result.failed, false);
36
38
  });
37
39
  });
@@ -53,7 +55,9 @@ describe('services/validate', () => {
53
55
  ]
54
56
  });
55
57
  const {_validationOptions, ...expectedRecordData} = expectedRecord;
56
- assert.deepStrictEqual(result, {...rest, record: new MarcRecord(expectedRecordData, _validationOptions)});
58
+ const {record: actualRecord, ...actualRest} = result;
59
+ assert.deepStrictEqual(actualRest, rest);
60
+ assert.deepStrictEqual(actualRecord.toObject(), new MarcRecord(expectedRecordData, _validationOptions).toObject());
57
61
  assert.equal(result.failed, true);
58
62
  });
59
63
  });
@@ -89,9 +89,11 @@ async function callback({
89
89
  const opResult = await mongoOperator.checkAndSetState(params);
90
90
  debug(`checkAndSetState result: ${JSON.stringify(opResult)} (it should be: ${JSON.stringify(expectedOpResult)})}`);
91
91
 
92
+ // checkAndSetState now returns a discriminated result
93
+ // {status: 'updated' | 'timeout' | 'unchanged', queueItem} - compare status
92
94
  // eslint-disable-next-line functional/no-conditional-statements
93
95
  if (expectedOpResult !== undefined) {
94
- assert.deepStrictEqual(opResult, expectedOpResult);
96
+ assert.deepStrictEqual(opResult.status, expectedOpResult);
95
97
  }
96
98
  await compareToFirstDbEntry({mongoFixtures, expectedResult, expectModificationTime, formatDates: true});
97
99
 
@@ -89,9 +89,11 @@ async function callback({
89
89
  const opResult = await mongoOperator.checkAndSetImportJobState(params);
90
90
  debug(`checkAndSetImportJobState result: ${JSON.stringify(opResult)} (it should be: ${JSON.stringify(expectedOpResult)})}`);
91
91
 
92
+ // checkAndSetImportJobState now returns a discriminated result
93
+ // {status: 'updated' | 'timeout' | 'unchanged', queueItem} - compare status
92
94
  // eslint-disable-next-line functional/no-conditional-statements
93
95
  if (expectedOpResult !== undefined) {
94
- assert.deepStrictEqual(opResult, expectedOpResult);
96
+ assert.deepStrictEqual(opResult.status, expectedOpResult);
95
97
  }
96
98
  await compareToFirstDbEntry({mongoFixtures, expectedResult, expectModificationTime, formatDates: true});
97
99
  } catch (error) {