cassproject 5.0.20 → 5.0.23

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.
Files changed (107) hide show
  1. package/package.json +49 -52
  2. package/src/com/eduworks/ec/array/EcArray.js +1 -1
  3. package/src/com/eduworks/ec/array/EcAsyncHelper.js +1 -1
  4. package/src/com/eduworks/ec/array/EcObject.js +1 -1
  5. package/src/com/eduworks/ec/crypto/EcAes.js +1 -1
  6. package/src/com/eduworks/ec/crypto/EcAesCtrAsync.js +20 -22
  7. package/src/com/eduworks/ec/crypto/EcCrypto.js +8 -4
  8. package/src/com/eduworks/ec/crypto/EcPk.js +7 -7
  9. package/src/com/eduworks/ec/crypto/EcPpk.js +7 -7
  10. package/src/com/eduworks/ec/crypto/EcRsaOaepAsync.js +20 -19
  11. package/src/com/eduworks/ec/crypto/EcRsaOaepAsyncWorker.js +7 -6
  12. package/src/com/eduworks/ec/crypto/forgeAsync.js +1 -9
  13. package/src/com/eduworks/ec/crypto/forgeAsyncNode.js +8 -8
  14. package/src/com/eduworks/ec/graph/EcDirectedGraph.js +1 -2
  15. package/src/com/eduworks/ec/graph/EcFrameworkGraph.js +18 -24
  16. package/src/com/eduworks/ec/promises/helpers.js +1 -3
  17. package/src/com/eduworks/ec/remote/EcRemote.js +9 -9
  18. package/src/com/eduworks/ec/task/EcAsyncTask.js +5 -20
  19. package/src/com/eduworks/ec/task/EcAsyncTaskManager.js +3 -4
  20. package/src/com/eduworks/ec/task/EcAsyncTaskWorker.js +9 -11
  21. package/src/com/eduworks/schema/cfd/competency/CfdFramework.js +36 -37
  22. package/src/com/eduworks/schema/ebac/EbacContact.js +3 -9
  23. package/src/com/eduworks/schema/ebac/EbacContactGrant.js +3 -9
  24. package/src/com/eduworks/schema/ebac/EbacCredential.js +3 -9
  25. package/src/com/eduworks/schema/ebac/EbacCredentialCommit.js +3 -9
  26. package/src/com/eduworks/schema/ebac/EbacCredentialRequest.js +3 -9
  27. package/src/com/eduworks/schema/ebac/EbacCredentials.js +3 -9
  28. package/src/com/eduworks/schema/ebac/EbacEncryptedSecret.js +3 -9
  29. package/src/com/eduworks/schema/ebac/EbacEncryptedValue.js +5 -12
  30. package/src/com/eduworks/schema/ebac/EbacSignature.js +1 -6
  31. package/src/index/env.js +6 -3
  32. package/src/index/polyfills.js +13 -27
  33. package/src/org/angles/schema/angles/VideoStory.js +1 -1
  34. package/src/org/cass/competency/EcCompetency.js +1 -2
  35. package/src/org/cass/competency/EcDirectory.js +1 -2
  36. package/src/org/cass/competency/EcFramework.js +9 -12
  37. package/src/org/cass/exporter/CSVExport.js +12 -13
  38. package/src/org/cass/importer/ASNImport.js +3 -6
  39. package/src/org/cass/importer/CSVImport.js +9 -17
  40. package/src/org/cass/importer/CTDLASNCSVConceptImport.js +25 -121
  41. package/src/org/cass/importer/CTDLASNCSVImport.js +88 -133
  42. package/src/org/cass/importer/FrameworkImport.js +16 -30
  43. package/src/org/cass/importer/MedbiqImport.js +3 -4
  44. package/src/org/cass/importer/TabStructuredImport.js +6 -6
  45. package/src/org/cass/profile/EcAssertion.js +40 -51
  46. package/src/org/cassproject/ebac/identity/EcContactGrant.js +2 -2
  47. package/src/org/cassproject/ebac/identity/EcIdentityManager.js +56 -62
  48. package/src/org/cassproject/ebac/identity/EcRekeyRequest.js +3 -2
  49. package/src/org/cassproject/ebac/identity/remote/EcRemoteIdentityManager.js +51 -62
  50. package/src/org/cassproject/ebac/identity/remote/OAuth2FileBasedRemoteIdentityManager.js +56 -70
  51. package/src/org/cassproject/ebac/identity/remote/RemoteIdentityManagerInterface.js +3 -3
  52. package/src/org/cassproject/ebac/repository/EcEncryptedValue.js +51 -44
  53. package/src/org/cassproject/ebac/repository/EcRepository.js +228 -301
  54. package/src/org/cassproject/general/repository/GeneralFile.js +8 -9
  55. package/src/org/cassproject/schema/cass/competency/Competency.js +10 -11
  56. package/src/org/cassproject/schema/cass/competency/Framework.js +10 -11
  57. package/src/org/cassproject/schema/cass/competency/Level.js +10 -11
  58. package/src/org/cassproject/schema/cass/competency/Relation.js +10 -11
  59. package/src/org/cassproject/schema/cass/competency/RollupRule.js +7 -7
  60. package/src/org/cassproject/schema/cass/profile/Assertion.js +20 -27
  61. package/src/org/cassproject/schema/cass/profile/AssertionEnvelope.js +3 -5
  62. package/src/org/cassproject/schema/general/EcRemoteLinkedData.js +41 -44
  63. package/src/org/json/ld/EcLinkedData.js +10 -12
  64. package/src/org/schema/EcOrganization.js +10 -10
  65. package/src/org/schema/EcQuestion.js +8 -8
  66. package/src/org/w3/skos/Concept.js +6 -6
  67. package/src/org/w3/skos/ConceptScheme.js +6 -6
  68. package/src/org/w3/skos/EcConcept.js +1 -2
  69. package/src/org/w3/skos/EcConceptScheme.js +1 -2
  70. package/src/test/0.start.test.js +1 -1
  71. package/src/test/0.testLibraries.test.js +172 -0
  72. package/src/test/1.ASNImport.test.js +122 -0
  73. package/src/test/1.CSVExport.test.js +57 -0
  74. package/src/test/1.CSVImport.test.js +185 -0
  75. package/src/test/1.CTDLASNCSVConceptImport.test.js +348 -0
  76. package/src/test/1.CTDLASNCSVImport.import.test.js +497 -0
  77. package/src/test/1.Ebac.schema.test.js +107 -0
  78. package/src/test/1.EcAssertion.decrypt.test.js +98 -0
  79. package/src/test/1.EcAssertion.pinned.test.js +160 -0
  80. package/src/test/1.EcCrypto.formats.test.js +293 -0
  81. package/src/test/1.EcCrypto.generateUUID.test.js +47 -0
  82. package/src/test/1.EcEncryptedValue.helpers.test.js +236 -0
  83. package/src/test/1.EcHelpers.misc.test.js +259 -0
  84. package/src/test/1.EcIdentityManager.keys.test.js +439 -0
  85. package/src/test/1.EcOrganization.rekey.test.js +68 -0
  86. package/src/test/1.EcRemote.responses.test.js +155 -0
  87. package/src/test/1.EcRemoteIdentityManager.flows.test.js +326 -0
  88. package/src/test/1.EcRemoteLinkedData.pinned.test.js +473 -0
  89. package/src/test/1.EcRepository.l0.test.js +14 -5
  90. package/src/test/1.EcRepository.l1.test.js +16 -7
  91. package/src/test/1.EcRepository.l2.test.js +20 -11
  92. package/src/test/1.EcRepository.unit.test.js +961 -0
  93. package/src/test/1.EcRsaOaep.test.js +2 -12
  94. package/src/test/1.EcRsaOaepAsync.test.js +2 -12
  95. package/src/test/1.EcRsaOaepAsyncWorker.test.js +2 -12
  96. package/src/test/1.EcRsaOaepAsyncWorker.workers.test.js +147 -0
  97. package/src/test/1.FrameworkImport.test.js +170 -0
  98. package/src/test/1.MedbiqImport.test.js +92 -0
  99. package/src/test/1.OAuth2FileBasedRemoteIdentityManager.test.js +399 -0
  100. package/src/test/1.TabStructuredImport.test.js +61 -0
  101. package/src/test/1.polyfills.test.js +89 -0
  102. package/src/test/1.schema.competency.pinned.test.js +384 -0
  103. package/src/test/3.EcFrameworkGraph.test.js +3 -2
  104. package/src/test/4.schema.test.js +33 -28
  105. package/src/test/4.skyId.test.js +4 -2
  106. package/src/test/cypressValue.js +9 -0
  107. package/src/test/2.EcCompetency.test.js +0 -17
@@ -0,0 +1,185 @@
1
+ require('../../index.js');
2
+ const chai = require('chai');
3
+ const sinon = require('sinon');
4
+ const EcRepository = require('../org/cassproject/ebac/repository/EcRepository.js');
5
+
6
+ const assert = chai.assert;
7
+
8
+ const SERVER = "http://repo.test/api/";
9
+
10
+ const savingRepo = (saved, selectedServer) => ({ selectedServer: selectedServer || SERVER, saveTo: (o, s) => { saved.push(o); s("ok"); } });
11
+
12
+ describe('CSVImport', () => {
13
+ afterEach(() => sinon.restore());
14
+
15
+ describe('transformId', () => {
16
+ const transform = (oldId) => {
17
+ const c = new EcCompetency();
18
+ CSVImport.transformId(oldId, c, SERVER);
19
+ return c.id;
20
+ };
21
+
22
+ it('keeps ids that contain http in any case', () => {
23
+ assert.equal(transform("http://x/1"), "http://x/1");
24
+ assert.equal(transform("HTTPS://X/1"), "HTTPS://X/1");
25
+ assert.equal(transform("urn:http-thing"), "urn:http-thing");
26
+ });
27
+
28
+ it('assigns a server id for other values', () => {
29
+ assert.isTrue(transform("abc").startsWith(SERVER + "data/schema.cassproject.org.0.4.Competency/abc/"));
30
+ assert.isTrue(transform("").startsWith(SERVER + "data/"));
31
+ assert.isTrue(transform(null).startsWith(SERVER + "data/"));
32
+ });
33
+ });
34
+
35
+ describe('importCompetencies', () => {
36
+ const run = (csv, opts, relationsCsv) => new Promise((resolve, reject) => {
37
+ opts = Object.assign({ nameIndex: 0, descriptionIndex: 1, scopeIndex: -1, idIndex: -1, sourceIndex: 0, relationTypeIndex: 1, destIndex: 2 }, opts);
38
+ CSVImport.importCompetencies(csv, opts.serverUrl || SERVER, null, opts.nameIndex, opts.descriptionIndex, opts.scopeIndex, opts.idIndex,
39
+ relationsCsv || null, opts.sourceIndex, opts.relationTypeIndex, opts.destIndex,
40
+ (c, r) => resolve({ c, r }), reject, () => { }, opts.uniquify, opts.repo);
41
+ });
42
+
43
+ it('copies extra columns, skipping dotted, @ and blank ones', async () => {
44
+ const csv = "name,description,owner,ex:extra,a.b,@x,\nOne,D1,pk1,E1,ab,x,y\n,skip,,,,,\n";
45
+ const { c, r } = await run(csv, { repo: savingRepo([]) });
46
+ assert.lengthOf(c, 1);
47
+ assert.equal(c[0].name, "One");
48
+ assert.equal(c[0].description, "D1");
49
+ assert.deepEqual(c[0].owner, ["pk1"]);
50
+ assert.equal(c[0]["ex:extra"], "E1");
51
+ assert.isUndefined(c[0]["a.b"]);
52
+ assert.isUndefined(c[0]["@x"]);
53
+ assert.deepEqual(r, []);
54
+ });
55
+
56
+ it('generates versioned ids when the repository server contains the server url', async () => {
57
+ const { c } = await run("name,description\nOne,D1", { repo: savingRepo([], SERVER + "more") });
58
+ assert.isTrue(c[0].id.startsWith(SERVER));
59
+ assert.notEqual(c[0].id, c[0].shortId());
60
+ });
61
+
62
+ it('generates short ids when the repository server does not contain the server url', async () => {
63
+ const { c } = await run("name,description\nOne,D1", { serverUrl: "http://other.test/", repo: savingRepo([]) });
64
+ assert.isTrue(c[0].id.startsWith("http://other.test/"));
65
+ assert.equal(c[0].id, c[0].shortId());
66
+ });
67
+
68
+ it('generates versioned ids and saves through EcRepository without a repository', async () => {
69
+ const save = sinon.stub(EcRepository, 'save').callsFake((o, s) => s("ok"));
70
+ const { c } = await run("name,description\nOne,D1", {});
71
+ assert.notEqual(c[0].id, c[0].shortId());
72
+ assert.isTrue(save.calledOnce);
73
+ });
74
+
75
+ it('uses the id column unless uniquify is set', async () => {
76
+ const { c } = await run("name,description,id\nOne,D1,http://old/1\nTwo,D2,two", { idIndex: 2, repo: savingRepo([]) });
77
+ assert.equal(c[0].id, "http://old/1");
78
+ assert.isTrue(c[1].id.startsWith(SERVER + "data/schema.cassproject.org.0.4.Competency/two/"));
79
+ assert.equal(CSVImport.importCsvLookup["http://old/1"], "http://old/1");
80
+ assert.equal(CSVImport.importCsvLookup["One"], "http://old/1");
81
+
82
+ const u = await run("name,description,id\nOne,D1,http://old/1", { idIndex: 2, uniquify: true, repo: savingRepo([]) });
83
+ assert.notEqual(u.c[0].id, "http://old/1");
84
+ assert.equal(CSVImport.importCsvLookup["http://old/1"], u.c[0].shortId());
85
+ });
86
+
87
+ it('imports relations, resolving names and old ids through the lookup', async () => {
88
+ const saved = [];
89
+ const { c, r } = await run("name,description\nParent,P\nChild,C", { repo: savingRepo(saved) },
90
+ "source,type,target\nChild,narrows,Parent\n,,\nChild,requires,http://elsewhere/x");
91
+ assert.lengthOf(r, 2);
92
+ assert.deepEqual(r.map(x => [x.source, x.relationType, x.target]), [
93
+ [c[1].shortId(), "narrows", c[0].shortId()],
94
+ [c[1].shortId(), "requires", "http://elsewhere/x"]
95
+ ]);
96
+ for (const x of r) {
97
+ assert.isTrue(x.id.startsWith(SERVER));
98
+ assert.notEqual(x.id, x.shortId());
99
+ }
100
+ });
101
+
102
+ it('imports relations with short ids when the repository server does not contain the server url', async () => {
103
+ const { r } = await run("name,description\nParent,P\nChild,C", { serverUrl: "http://other.test/", repo: savingRepo([]) },
104
+ "source,type,target\nChild,narrows,Parent");
105
+ assert.isTrue(r[0].id.startsWith("http://other.test/"));
106
+ assert.equal(r[0].id, r[0].shortId());
107
+ });
108
+
109
+ it('imports relations with versioned ids without a repository', async () => {
110
+ sinon.stub(EcRepository, 'save').callsFake((o, s) => s("ok"));
111
+ const { r } = await run("name,description\nParent,P\nChild,C", {}, "source,type,target\nChild,narrows,Parent");
112
+ assert.notEqual(r[0].id, r[0].shortId());
113
+ });
114
+ });
115
+
116
+ describe('transformReferences', () => {
117
+ it('throws on string values because Object.toString is Function.prototype.toString', () => {
118
+ CSVImport.importCsvLookup = { old: "new" };
119
+ assert.throws(() => CSVImport.transformReferences({ a: "old" }), TypeError);
120
+ assert.throws(() => CSVImport.transformReferences({ a: { b: 1 } }), TypeError);
121
+ });
122
+
123
+ it('skips null values and leaves function values alone', () => {
124
+ CSVImport.importCsvLookup = { old: "new" };
125
+ const fn = function old() { };
126
+ const data = { a: null, b: undefined, c: fn };
127
+ CSVImport.transformReferences(data);
128
+ assert.deepEqual(data, { a: null, b: undefined, c: fn });
129
+ });
130
+
131
+ it('replaces a function value whose source text names String and is in the lookup', () => {
132
+ const fn = function () { return String; };
133
+ CSVImport.importCsvLookup = {};
134
+ CSVImport.importCsvLookup[fn] = "replaced";
135
+ const data = { c: fn };
136
+ CSVImport.transformReferences(data);
137
+ assert.equal(data.c, "replaced");
138
+ });
139
+ });
140
+
141
+ describe('importData', () => {
142
+ const run = (csv, idIndex, repo, serverUrl) => {
143
+ const captured = [];
144
+ sinon.stub(CSVImport, 'transformReferences');
145
+ sinon.stub(CSVImport, 'saveTransformedData').callsFake((data) => captured.push(data));
146
+ const failures = [];
147
+ CSVImport.importData(csv, serverUrl || SERVER, null, null, (e) => failures.push(e), null, idIndex, "https://schema.cassproject.org/0.4", "Thing", repo);
148
+ return { captured, failures };
149
+ };
150
+
151
+ it('expands dotted columns into nested objects and copies the rest', () => {
152
+ const { captured, failures } = run("@id,name,a.b,a.c.d,x.\nhttp://f/1,N,B,D,X", -1, null);
153
+ assert.deepEqual(failures, []);
154
+ assert.lengthOf(captured, 1);
155
+ const d = captured[0];
156
+ assert.equal(d.name, "N");
157
+ assert.deepEqual(d.a, { b: "B", c: { d: "D" } });
158
+ assert.deepEqual(d.x, { "": "X" });
159
+ });
160
+
161
+ it('generates versioned ids when the repository server contains the server url', () => {
162
+ const { captured } = run("name\nN", -1, { selectedServer: SERVER });
163
+ assert.isTrue(captured[0].id.startsWith(SERVER));
164
+ assert.notEqual(captured[0].id, captured[0].shortId());
165
+ });
166
+
167
+ it('generates short ids when the repository server does not contain the server url', () => {
168
+ const { captured } = run("name\nN", -1, { selectedServer: SERVER }, "http://other.test/");
169
+ assert.isTrue(captured[0].id.startsWith("http://other.test/"));
170
+ assert.equal(captured[0].id, captured[0].shortId());
171
+ });
172
+
173
+ it('generates versioned ids without a repository and maps the file id', () => {
174
+ const { captured } = run("@id,name\nhttp://f/1/123,N", undefined, null);
175
+ assert.notEqual(captured[0].id, captured[0].shortId());
176
+ assert.equal(CSVImport.importCsvLookup["http://f/1/123"], captured[0].shortId());
177
+ });
178
+
179
+ it('uses the id column when an id index is given', () => {
180
+ const { captured } = run("name,id\nN,http://old/1", 1, null);
181
+ assert.equal(captured[0].id, "http://old/1");
182
+ assert.equal(CSVImport.importCsvLookup["http://old/1"], "http://old/1");
183
+ });
184
+ });
185
+ });
@@ -0,0 +1,348 @@
1
+ require('../../index.js');
2
+ const chai = require('chai');
3
+ const sinon = require('sinon');
4
+ const EcRepository = require('../org/cassproject/ebac/repository/EcRepository.js');
5
+ const EcRemote = require('../com/eduworks/ec/remote/EcRemote.js');
6
+
7
+ const assert = chai.assert;
8
+
9
+ const SERVER = "http://repo.test/api/";
10
+ const repo = { selectedServer: SERVER };
11
+ const isNode = typeof process === 'object' && process.versions != null && process.versions.node != null && typeof window === 'undefined';
12
+
13
+ // recast() normally expands and compacts through remote JSON-LD contexts. Most tests replace it
14
+ // with an identity transform so the importer's own logic runs without the network.
15
+ function stubNetwork(terms, onRecast) {
16
+ sinon.stub(EcRemote, 'getExpectingObject').resolves(terms || {});
17
+ sinon.stub(EcRepository, 'get').resolves(null);
18
+ sinon.stub(EcLinkedData.prototype, 'recast').callsFake(async function (translationContext, targetContext) {
19
+ this.context = targetContext;
20
+ if (onRecast) onRecast(this);
21
+ return this;
22
+ });
23
+ }
24
+
25
+ function importCsv(csv, opts) {
26
+ opts = opts || {};
27
+ return new Promise((resolve) => {
28
+ const notes = [];
29
+ CTDLASNCSVConceptImport.importFrameworksAndCompetencies(repo, csv,
30
+ (schemes, concepts) => resolve({ schemes, concepts, notes }),
31
+ (e) => { if (Array.isArray(e)) resolve({ errors: e, notes }); else notes.push(e); },
32
+ null, opts.endpoint || null, new EcIdentityManager(), opts.progressions, opts.validationRules, opts.duplicateChecks);
33
+ });
34
+ }
35
+
36
+ const csvOf = (rows) => rows.map(r => r.join(",")).join("\n");
37
+
38
+ const conceptCsv = () => csvOf([
39
+ ["@type", "@id", "ceterms:ctid", "ceasn:name", "skos:prefLabel", "skos:topConceptOf", "skos:inScheme", "ceterms:versionIdentifier", "ceasn:dateCreated", "skos:broader"],
40
+ ["skos:ConceptScheme", "http://cs.test/S1", "ce-s1", "Scheme", "", "", "", "1.0~V~http://t", "", ""],
41
+ ["skos:ConceptScheme", "http://cs.test/S2", "ce-s2", "Scheme two", "", "", "", "", "2020-01-01", ""],
42
+ ["skos:Concept", "http://cs.test/K1", "ce-k1", "", "One", "http://cs.test/S1", "", "", "", ""],
43
+ ["skos:Concept", "http://cs.test/K2", "", "", "Two", "", "http://cs.test/S2", "", "2020-01-01", "http://cs.test/K1"],
44
+ ["skos:Concept", "http://cs.test/K3", "ce-k3", "", "Three", "http://cs.test/S1", "http://cs.test/S1", "", "", ""],
45
+ ["skos:Concept", "http://cs.test/K4", "ce-k4", "", "Four", "http://cs.test/S9", "http://cs.test/S9", "", "", ""]
46
+ ]);
47
+
48
+ const progressionCsv = () => csvOf([
49
+ ["@type", "@id", "ceasn:name", "skos:prefLabel", "skos:topConceptOf", "skos:inScheme", "ceasn:dateCreated", "skos:broader"],
50
+ ["asn:ProgressionModel", "http://pm.test/M1", "Model", "", "", "", "", ""],
51
+ ["asn:ProgressionModel", "http://pm.test/M2", "Model two", "", "", "", "2020-01-01", ""],
52
+ ["asn:ProgressionLevel", "http://pm.test/L1", "", "One", "http://pm.test/M1", "", "", ""],
53
+ ["asn:ProgressionLevel", "http://pm.test/L2", "", "Two", "", "http://pm.test/M2", "2020-01-01", "http://pm.test/L1"],
54
+ ["asn:ProgressionLevel", "http://pm.test/L3", "", "Three", "http://pm.test/M1", "http://pm.test/M1", "", ""],
55
+ ["asn:ProgressionLevel", "http://pm.test/L4", "", "Four", "http://pm.test/M9", "http://pm.test/M9", "", ""]
56
+ ]);
57
+
58
+ describe('CTDLASNCSVConceptImport', () => {
59
+ let savedTemplates;
60
+ beforeEach(() => {
61
+ savedTemplates = [EcConceptScheme.template, EcConcept.template];
62
+ });
63
+ afterEach(() => {
64
+ sinon.restore();
65
+ EcConceptScheme.template = savedTemplates[0];
66
+ EcConcept.template = savedTemplates[1];
67
+ });
68
+
69
+ describe('concept schemes', () => {
70
+ it('imports schemes and concepts and links top concepts', async () => {
71
+ stubNetwork();
72
+ const { schemes, concepts, notes } = await importCsv(conceptCsv());
73
+ assert.deepEqual(notes, ["Invalid file"]);
74
+ assert.deepEqual(schemes.map(s => s.id), ["http://cs.test/S1", "http://cs.test/S2"]);
75
+ assert.deepEqual(concepts.map(c => c.id), ["http://cs.test/K1", "http://cs.test/K2", "http://cs.test/K3", "http://cs.test/K4"]);
76
+ assert.deepEqual(schemes[0]["skos:hasTopConcept"], ["http://cs.test/K1", "http://cs.test/K3"]);
77
+ // skos:inScheme also adds the concept to the scheme's top concepts.
78
+ assert.deepEqual(schemes[1]["skos:hasTopConcept"], ["http://cs.test/K2"]);
79
+ assert.deepEqual(concepts[1]["skos:broader"], ["http://cs.test/K1"]);
80
+ assert.deepEqual(schemes[0]["ceasn:name"], { "en-US": "Scheme" });
81
+ assert.deepEqual(concepts[0]["skos:prefLabel"], { "en-US": "One" });
82
+ assert.deepEqual(schemes[0]["ceterms:versionIdentifier"], [{
83
+ "ceterms:identifierValueCode": "1.0",
84
+ "ceterms:identifierTypeName": { "@language": "en-us", "@value": "V" },
85
+ "ceterms:identifierType": "http://t"
86
+ }]);
87
+ assert.isUndefined(schemes[0]["schema:dateCreated"]);
88
+ assert.isUndefined(concepts[0]["schema:dateCreated"]);
89
+ assert.isUndefined(schemes[0].subType);
90
+ });
91
+
92
+ it('applies template owners and creation dates', async () => {
93
+ stubNetwork();
94
+ EcConceptScheme.template = { "@owner": ["T"], "schema:dateCreated": "TEMPLATE", "ceasn:extra": "E" };
95
+ EcConcept.template = { "@owner": "OWNER", "schema:dateCreated": "TEMPLATE" };
96
+ const { schemes, concepts } = await importCsv(conceptCsv());
97
+ assert.deepEqual(schemes[0].owner, ["T"]);
98
+ assert.equal(schemes[0]["ceasn:extra"], "E");
99
+ assert.match(schemes[0]["schema:dateCreated"], /^\d{4}-\d\d-\d\dT/);
100
+ // A row with its own creation date keeps the template value.
101
+ assert.equal(schemes[1]["schema:dateCreated"], "TEMPLATE");
102
+ assert.equal(concepts[0].owner, "OWNER");
103
+ assert.match(concepts[0]["schema:dateCreated"], /^\d{4}-\d\d-\d\dT/);
104
+ assert.isUndefined(concepts[1]["schema:dateCreated"]);
105
+ });
106
+
107
+ it('leaves owners and dates alone when the templates do not set them', async () => {
108
+ stubNetwork();
109
+ EcConceptScheme.template = {};
110
+ EcConcept.template = {};
111
+ const { schemes, concepts } = await importCsv(conceptCsv());
112
+ assert.isUndefined(schemes[0]["schema:dateCreated"]);
113
+ assert.isUndefined(concepts[0].owner);
114
+ assert.isUndefined(concepts[0]["schema:dateCreated"]);
115
+ });
116
+
117
+ it('drops version identifiers that are not strings, leaving a hole', async () => {
118
+ stubNetwork(null, (t) => {
119
+ if (t.id === "http://cs.test/S1") t["ceterms:versionIdentifier"] = ["1.0~V", 5];
120
+ });
121
+ const { schemes } = await importCsv(conceptCsv());
122
+ const v = schemes[0]["ceterms:versionIdentifier"];
123
+ assert.lengthOf(v, 2);
124
+ assert.isFalse(1 in v);
125
+ assert.equal(v[0]["ceterms:identifierValueCode"], "1.0");
126
+ });
127
+
128
+ it('renames columns through the terms map', async () => {
129
+ stubNetwork({ "skos:prefLabel": "skos:altLabel" });
130
+ const { concepts } = await importCsv(conceptCsv());
131
+ assert.deepEqual(concepts[0]["skos:altLabel"], { "en-US": "One" });
132
+ assert.isUndefined(concepts[0]["skos:prefLabel"]);
133
+ });
134
+
135
+ it('reports missing fields, unknown types and missing ids', async () => {
136
+ stubNetwork(null, (t) => {
137
+ if (t.id === "http://cs.test/noid") delete t.id;
138
+ });
139
+ const csv = csvOf([
140
+ ["@type", "@id", "ceasn:name"],
141
+ ["Sample", "x", ""],
142
+ ["", "", ""],
143
+ ["skos:Concept", "bad", ""],
144
+ ["skos:Other", "http://x/1", ""],
145
+ ["skos:Concept", "http://cs.test/noid", ""]
146
+ ]);
147
+ const { errors } = await importCsv(csv);
148
+ assert.deepEqual(errors, [
149
+ // The @id is checked before sample rows are skipped.
150
+ "Row 2: @id must be a valid URI or start with 'ce-'",
151
+ "Row 3: is missing an @id",
152
+ "Row 3: is missing a @type",
153
+ "Row 4: @id must be a valid URI or start with 'ce-'",
154
+ "Row 5: Found unknown type: skos:Other",
155
+ "Row 6: Concept is missing an id"
156
+ ]);
157
+ });
158
+
159
+ it('applies required-property and registry-url validation rules', async () => {
160
+ stubNetwork();
161
+ const validationRules = {
162
+ validateHierarchy: false,
163
+ requiredProps: { "skos:ConceptScheme": ["ceasn:name"], "skos:Concept": ["skos:prefLabel"] },
164
+ urlRules: { fields: { "skos:ConceptScheme": ["ceasn:publisher"], "skos:Concept": ["ceasn:creator"] }, allowedOrigins: ["https://ok.test"] }
165
+ };
166
+ const csv = csvOf([
167
+ ["@type", "@id", "ceasn:name", "skos:prefLabel", "ceasn:publisher", "ceasn:creator"],
168
+ ["skos:ConceptScheme", "http://s/1", "", "", "", ""],
169
+ ["skos:ConceptScheme", "http://s/2", "Two", "", "https://bad.test/x", ""],
170
+ ["skos:Concept", "http://c/1", "", "", "", ""],
171
+ ["skos:Concept", "http://c/2", "", "Label", "", "https://bad.test/y"],
172
+ ["skos:Concept", "http://c/3", "", "Label", "", "https://ok.test/y"]
173
+ ]);
174
+ const { errors } = await importCsv(csv, { validationRules });
175
+ assert.deepEqual(errors, [
176
+ "Row 2: Missing required properties for skos:ConceptScheme: ceasn:name",
177
+ "Row 3: ceasn:publisher must be a CTID or a credential registry URL for this environment (got: https://bad.test/x)",
178
+ "Row 4: Missing required properties for skos:Concept: skos:prefLabel",
179
+ "Row 5: ceasn:creator must be a CTID or a credential registry URL for this environment (got: https://bad.test/y)"
180
+ ]);
181
+ });
182
+
183
+ it('runs hierarchy validation unless it is turned off', async () => {
184
+ stubNetwork();
185
+ const hierarchyRules = { "skos:Concept": { requiredProperties: ["skos:nope"], errorMessage: "no hierarchy" } };
186
+ let r = await importCsv(conceptCsv(), { validationRules: { hierarchyRules } });
187
+ assert.deepEqual(r.errors, ["no hierarchy"]);
188
+ r = await importCsv(conceptCsv(), { validationRules: { hierarchyRules: { "skos:Concept": { requiredProperties: ["skos:topConceptOf"] } } } });
189
+ assert.lengthOf(r.concepts, 4);
190
+ });
191
+
192
+ it('passes duplicate-check results through', async () => {
193
+ stubNetwork();
194
+ const seen = [];
195
+ const check = async (r, ids, schemes, ctids) => { seen.push([r, ids, schemes.length, ctids]); return ["dup"]; };
196
+ const { errors } = await importCsv(conceptCsv(), { duplicateChecks: [check] });
197
+ assert.deepEqual(errors, ["dup"]);
198
+ assert.equal(seen[0][0], repo);
199
+ assert.deepEqual(seen[0][1], ["http://cs.test/K1", "http://cs.test/K2", "http://cs.test/K3", "http://cs.test/K4"]);
200
+ assert.equal(seen[0][2], 2);
201
+ assert.deepEqual(seen[0][3], { "http://cs.test/K1": "ce-k1", "http://cs.test/K2": "http://cs.test/K2", "http://cs.test/K3": "ce-k3", "http://cs.test/K4": "ce-k4" });
202
+ });
203
+ });
204
+
205
+ describe('progressions', () => {
206
+ it('imports progression models and levels and links top levels', async () => {
207
+ stubNetwork();
208
+ const { schemes, concepts } = await importCsv(progressionCsv(), { progressions: true });
209
+ assert.deepEqual(schemes.map(s => [s.id, s.subType]), [["http://pm.test/M1", "Progression"], ["http://pm.test/M2", "Progression"]]);
210
+ assert.deepEqual(concepts.map(c => [c.id, c.subType]), [
211
+ ["http://pm.test/L1", "Progression"], ["http://pm.test/L2", "Progression"], ["http://pm.test/L3", "Progression"], ["http://pm.test/L4", "Progression"]
212
+ ]);
213
+ assert.deepEqual(schemes[0]["skos:hasTopConcept"], ["http://pm.test/L1", "http://pm.test/L3"]);
214
+ assert.deepEqual(schemes[1]["skos:hasTopConcept"], ["http://pm.test/L2"]);
215
+ assert.deepEqual(concepts[1]["skos:broader"], ["http://pm.test/L1"]);
216
+ assert.isUndefined(schemes[0]["schema:dateCreated"]);
217
+ });
218
+
219
+ it('applies template owners and creation dates', async () => {
220
+ stubNetwork();
221
+ EcConceptScheme.template = { "@owner": ["T"], "schema:dateCreated": "TEMPLATE" };
222
+ EcConcept.template = { "@owner": "OWNER", "schema:dateCreated": "TEMPLATE" };
223
+ const { schemes, concepts } = await importCsv(progressionCsv(), { progressions: true });
224
+ assert.deepEqual(schemes[0].owner, ["T"]);
225
+ assert.match(schemes[0]["schema:dateCreated"], /^\d{4}-\d\d-\d\dT/);
226
+ // A row with its own creation date keeps the template value.
227
+ assert.equal(schemes[1]["schema:dateCreated"], "TEMPLATE");
228
+ assert.equal(concepts[0].owner, "OWNER");
229
+ assert.match(concepts[0]["schema:dateCreated"], /^\d{4}-\d\d-\d\dT/);
230
+ assert.isUndefined(concepts[1]["schema:dateCreated"]);
231
+ });
232
+
233
+ it('leaves owners and dates alone when the templates do not set them', async () => {
234
+ stubNetwork();
235
+ EcConceptScheme.template = {};
236
+ EcConcept.template = {};
237
+ const { schemes, concepts } = await importCsv(progressionCsv(), { progressions: true });
238
+ assert.isUndefined(schemes[0]["schema:dateCreated"]);
239
+ assert.isUndefined(concepts[0].owner);
240
+ assert.isUndefined(concepts[0]["schema:dateCreated"]);
241
+ });
242
+
243
+ it('applies required-property and registry-url validation rules', async () => {
244
+ stubNetwork();
245
+ const validationRules = {
246
+ validateHierarchy: false,
247
+ requiredProps: { "asn:ProgressionModel": ["ceasn:name"], "asn:ProgressionLevel": ["skos:prefLabel"] },
248
+ urlRules: { fields: { "asn:ProgressionModel": ["ceasn:publisher"], "asn:ProgressionLevel": ["ceasn:creator"] }, allowedOrigins: ["https://ok.test"] }
249
+ };
250
+ const csv = csvOf([
251
+ ["@type", "@id", "ceasn:name", "skos:prefLabel", "ceasn:publisher", "ceasn:creator"],
252
+ ["asn:ProgressionModel", "http://m/1", "", "", "", ""],
253
+ ["asn:ProgressionModel", "http://m/2", "Two", "", "https://bad.test/x", ""],
254
+ ["asn:ProgressionLevel", "http://l/1", "", "", "", ""],
255
+ ["asn:ProgressionLevel", "http://l/2", "", "Label", "", "https://bad.test/y"],
256
+ ["asn:ProgressionLevel", "http://l/3", "", "Label", "", ""],
257
+ ["Instructions", "", "", "", "", ""],
258
+ ["asn:Other", "x", "", "", "", ""],
259
+ ["", "", "", "", "", ""]
260
+ ]);
261
+ const { errors } = await importCsv(csv, { progressions: true, validationRules });
262
+ assert.deepEqual(errors, [
263
+ "Row 2: Missing required properties for asn:ProgressionModel: ceasn:name",
264
+ "Row 3: ceasn:publisher must be a CTID or a credential registry URL for this environment (got: https://bad.test/x)",
265
+ "Row 4: Missing required properties for asn:ProgressionLevel: skos:prefLabel",
266
+ "Row 5: ceasn:creator must be a CTID or a credential registry URL for this environment (got: https://bad.test/y)",
267
+ "Row 7: is missing an @id",
268
+ "Row 8: Found unknown type: asn:Other",
269
+ "Row 9: is missing an @id",
270
+ "Row 9: is missing a @type"
271
+ ]);
272
+ });
273
+
274
+ it('does not pass duplicate checks through to the progression import', async () => {
275
+ stubNetwork();
276
+ const check = sinon.stub().resolves(["dup"]);
277
+ const { schemes } = await importCsv(progressionCsv(), { progressions: true, duplicateChecks: [check] });
278
+ assert.lengthOf(schemes, 2);
279
+ assert.isFalse(check.called);
280
+ });
281
+
282
+ it('runs duplicate checks when called directly', async () => {
283
+ stubNetwork();
284
+ const check = sinon.stub().resolves(["dup"]);
285
+ const errors = await new Promise((resolve) => {
286
+ CTDLASNCSVConceptImport.importProgressions(repo, progressionCsv(), () => resolve(null), resolve, null, null, new EcIdentityManager(), null, [check]);
287
+ });
288
+ assert.deepEqual(errors, ["dup"]);
289
+ assert.deepEqual(check.firstCall.args[1], ["http://pm.test/L1", "http://pm.test/L2", "http://pm.test/L3", "http://pm.test/L4"]);
290
+ });
291
+ });
292
+
293
+ describe('validateConceptHierarchy', () => {
294
+ const rules = {
295
+ "skos:Concept": { requiredProperties: ["skos:topConceptOf"], childProperties: ["skos:broader", "skos:narrower"], errorMessage: "custom" },
296
+ "skos:ConceptScheme": { requiredProperties: ["skos:hasTopConcept"] }
297
+ };
298
+
299
+ it('requires rules', () => {
300
+ assert.equal(CTDLASNCSVConceptImport.validateConceptHierarchy([], undefined), 'Hierarchy rules not defined.');
301
+ });
302
+
303
+ it('ignores types that are not present', () => {
304
+ assert.isNull(CTDLASNCSVConceptImport.validateConceptHierarchy([null, { "@type": "x" }], rules));
305
+ });
306
+
307
+ it('passes on a required property of the type or a child property of any typed row', () => {
308
+ assert.isNull(CTDLASNCSVConceptImport.validateConceptHierarchy([{ "@type": "skos:Concept", "skos:topConceptOf": "x" }], { "skos:Concept": rules["skos:Concept"] }));
309
+ assert.isNull(CTDLASNCSVConceptImport.validateConceptHierarchy([{ "@type": "skos:Concept" }, { "@type": "o", "skos:narrower": "y" }], { "skos:Concept": rules["skos:Concept"] }));
310
+ });
311
+
312
+ it('ignores rows without a type and required properties on other types', () => {
313
+ const data = [{ "@type": "skos:Concept" }, { "skos:broader": "y" }, null, { "@type": "o", "skos:topConceptOf": "x" }];
314
+ assert.equal(CTDLASNCSVConceptImport.validateConceptHierarchy(data, { "skos:Concept": rules["skos:Concept"] }), "custom");
315
+ });
316
+
317
+ it('returns the default message for the first failing type', () => {
318
+ const data = [{ "@type": "skos:Concept", "skos:broader": "y" }, { "@type": "skos:ConceptScheme" }];
319
+ assert.equal(CTDLASNCSVConceptImport.validateConceptHierarchy(data, rules), "CSV must contain hierarchy properties for skos:ConceptScheme");
320
+ });
321
+ });
322
+
323
+ (isNode ? describe : describe.skip)('JSON-LD document cache', () => {
324
+ it('loads each remote context once and reuses it', async () => {
325
+ // Serves stand-in contexts through jsonld's node document loader. The importer caches them
326
+ // for the rest of the process, so no other test may rely on the real contexts.
327
+ // Node's own createRequire keeps this Node-only client (and its node: imports) out of the Cypress
328
+ // build, where this test is skipped anyway.
329
+ const { httpClient } = module.constructor.createRequire(__filename)('@digitalbazaar/http-client');
330
+ const calls = [];
331
+ sinon.stub(httpClient, 'get').callsFake(async (url) => {
332
+ calls.push(url);
333
+ return { status: 200, headers: { get: () => null }, data: { "@context": { "@vocab": "http://vocab.test/" } } };
334
+ });
335
+ sinon.stub(EcRemote, 'getExpectingObject').resolves({});
336
+ const csv = csvOf([["@type", "@id", "ceasn:name"], ["skos:ConceptScheme", "http://cs.test/S1", "Scheme"], ["skos:Concept", "http://cs.test/K1", ""]]);
337
+ let r = await importCsv(csv);
338
+ assert.deepEqual(r.schemes.map(s => [s.id, s.type]), [["http://cs.test/S1", "ConceptScheme"]]);
339
+ assert.deepEqual(r.concepts.map(c => c.id), ["http://cs.test/K1"]);
340
+ assert.isAtMost(calls.length, 2);
341
+ assert.isTrue(calls.every(u => u === "https://schema.cassproject.org/0.4/jsonld1.1/ceasn2cassConcepts.json" || u === "https://schema.cassproject.org/0.4/skos"));
342
+ const before = calls.length;
343
+ r = await importCsv(csv);
344
+ assert.lengthOf(r.concepts, 1);
345
+ assert.equal(calls.length, before);
346
+ });
347
+ });
348
+ });