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,497 @@
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
+
12
+ // recast() normally expands and compacts through remote JSON-LD contexts. These tests replace it
13
+ // with an identity transform so the importer's own logic runs without the network.
14
+ function stubNetwork(terms, onRecast) {
15
+ sinon.stub(EcRemote, 'getExpectingObject').resolves(terms || {});
16
+ sinon.stub(EcRepository, 'get').resolves(null);
17
+ sinon.stub(EcLinkedData.prototype, 'recast').callsFake(async function (translationContext, targetContext) {
18
+ this.context = targetContext;
19
+ if (onRecast) onRecast(this);
20
+ return this;
21
+ });
22
+ }
23
+
24
+ function importCsv(csv, opts) {
25
+ opts = opts || {};
26
+ return new Promise((resolve) => {
27
+ const notes = [];
28
+ CTDLASNCSVImport.importFrameworksAndCompetencies(opts.repo || repo, csv,
29
+ (frameworks, competencies, relations) => resolve({ frameworks, competencies, relations, notes }),
30
+ (e) => { if (Array.isArray(e)) resolve({ errors: e, notes }); else notes.push(e); },
31
+ opts.ceo || null, opts.endpoint || null, new EcIdentityManager(), opts.collections, opts.skip, opts.validationRules, opts.duplicateChecks);
32
+ });
33
+ }
34
+
35
+ const csvOf = (rows) => rows.map(r => r.join(",")).join("\n");
36
+
37
+ const FW_HEADER = ["@type", "@id", "ceterms:ctid", "ceasn:name", "ceasn:competencyText", "ceasn:isTopChildOf", "ceasn:isChildOf", "ceasn:broadAlignment", "ceterms:versionIdentifier", "ceasn:dateCreated"];
38
+
39
+ describe('CTDLASNCSVImport', () => {
40
+ let savedTemplates;
41
+ beforeEach(() => {
42
+ savedTemplates = [EcFramework.template, EcCompetency.template];
43
+ });
44
+ afterEach(() => {
45
+ sinon.restore();
46
+ EcFramework.template = savedTemplates[0];
47
+ EcCompetency.template = savedTemplates[1];
48
+ });
49
+
50
+ describe('analyzeFile', () => {
51
+ const analyze = (csv) => new Promise((resolve) => {
52
+ const failures = [];
53
+ CTDLASNCSVImport.analyzeFile(csv, (f, c, k, d) => resolve({ f, c, k, d, failures }), (e) => {
54
+ failures.push(e);
55
+ if (e !== "Invalid file") resolve({ failures });
56
+ });
57
+ });
58
+
59
+ it('counts types and lists rows that only differ by @id and CTID', async () => {
60
+ const csv = csvOf([
61
+ ["@type", "@id", "ceterms:ctid", "ceasn:competencyText", "ceasn:codedNotation"],
62
+ [" ceasn:CompetencyFramework ", "http://f/1", "ce-1", "", ""],
63
+ ["ceasn:Competency", "http://c/1", "ce-2", "Same", "A"],
64
+ ["ceasn:Competency", "http://c/2", "ce-3", "Same", "A"],
65
+ ["ceasn:Competency", "http://c/3", "ce-4", "Same", "A"],
66
+ ["ceasn:Competency", "http://c/4", "ce-5", "Other", "B"],
67
+ ["ceterms:Collection", "http://k/1", "ce-6", "", ""],
68
+ ["sample row", "", "", "", ""],
69
+ [""]
70
+ ]);
71
+ const { f, c, k, d, failures } = await analyze(csv);
72
+ assert.deepEqual(failures, ["Invalid file"]);
73
+ assert.equal(f, 1);
74
+ assert.equal(c, 4);
75
+ assert.equal(k, 1);
76
+ // The first duplicate is listed once even though it has two copies.
77
+ assert.deepEqual(d.map(x => [x.ctid, x.line]), [["ce-2", 2], ["ce-3", 3], ["ce-4", 4]]);
78
+ assert.deepEqual(d.map(x => [x.competencyText, x.codedNotation]), [["Same", "A"], ["Same", "A"], ["Same", "A"]]);
79
+ });
80
+
81
+ it('reports unknown types', async () => {
82
+ const { failures } = await analyze(csvOf([["@type", "@id"], ["ceasn:Nope", "http://x"]]));
83
+ assert.deepEqual(failures, ["Invalid file", "Found unknown type:ceasn:Nope"]);
84
+ });
85
+ });
86
+
87
+ describe('getIdFromCtid', () => {
88
+ const CTX = "https://schema.cassproject.org/0.4/";
89
+ it('appends the CTID to an endpoint, without the ce- prefix when the endpoint has one', () => {
90
+ assert.equal(CTDLASNCSVImport.getIdFromCtid("ce-1234", "http://ep.test/resources/", repo, CTX, "Competency", "id"), "http://ep.test/resources/ce-1234");
91
+ assert.equal(CTDLASNCSVImport.getIdFromCtid("ce-1234", "http://ep.test/resources/ce-", repo, CTX, "Competency", "id"), "http://ep.test/resources/ce-1234");
92
+ });
93
+
94
+ it('builds a versioned id for the id key and short ids for references', () => {
95
+ const id = CTDLASNCSVImport.getIdFromCtid("ce-1234", null, repo, CTX, "Competency", "id");
96
+ assert.isTrue(id.startsWith(SERVER + "data/schema.cassproject.org.0.4.Competency/1234/"));
97
+ assert.equal(CTDLASNCSVImport.getIdFromCtid("ce-1234", null, repo, CTX, "Competency", "ceasn:isPartOf"), SERVER + "data/schema.cassproject.org.0.4.Framework/1234");
98
+ assert.equal(CTDLASNCSVImport.getIdFromCtid("ce-1234", null, repo, CTX, "Concept", "ceasn:isTopChildOf"), SERVER + "data/schema.cassproject.org.0.4.ConceptScheme/1234");
99
+ assert.equal(CTDLASNCSVImport.getIdFromCtid("ce-1234", null, repo, CTX, "Framework", "ceasn:hasChild"), SERVER + "data/schema.cassproject.org.0.4.Competency/1234");
100
+ assert.equal(CTDLASNCSVImport.getIdFromCtid("ce-1234", null, repo, CTX, "ConceptScheme", "skos:hasTopConcept"), SERVER + "data/schema.cassproject.org.0.4.Concept/1234");
101
+ assert.equal(CTDLASNCSVImport.getIdFromCtid("ce-1234", null, repo, CTX, "Competency", "ceasn:broadAlignment"), SERVER + "data/schema.cassproject.org.0.4.Competency/1234");
102
+ assert.equal(CTDLASNCSVImport.getIdFromCtid("ce-1234", null, repo, CTX, "Thing", "ceasn:isPartOf"), SERVER + "data/schema.cassproject.org.0.4.Thing/1234");
103
+ });
104
+ });
105
+
106
+ describe('cleanUpTranslator', () => {
107
+ it('splits piped values, trims them and resolves CTIDs except in ceterms:ctid', () => {
108
+ const t = {
109
+ type: "ceasn:Competency",
110
+ "ceasn:broadAlignment": "ce-1| http://x/2 ",
111
+ "ceterms:ctid": "ce-9",
112
+ "ceterms:keyword": "a|b",
113
+ "ceasn:isChildOf": "ce-3",
114
+ "ceasn:comment": ["ce-4", "plain"],
115
+ "ceasn:empty": "",
116
+ "ceasn:blank": " ",
117
+ " ceasn:padded ": "v"
118
+ };
119
+ CTDLASNCSVImport.cleanUpTranslator(t, "http://ep.test/", repo);
120
+ assert.deepEqual(t, {
121
+ type: "ceasn:Competency",
122
+ "ceasn:broadAlignment": ["http://ep.test/ce-1", "http://x/2"],
123
+ "ceterms:ctid": "ce-9",
124
+ "ceterms:keyword": ["a", "b"],
125
+ "ceasn:isChildOf": "http://ep.test/ce-3",
126
+ "ceasn:comment": ["http://ep.test/ce-4", "plain"],
127
+ "ceasn:padded": "v"
128
+ });
129
+ });
130
+
131
+ it('keeps a piped ceterms:ctid unresolved', () => {
132
+ const t = { type: "ceasn:Competency", "ceterms:ctid": "ce-1|ce-2" };
133
+ CTDLASNCSVImport.cleanUpTranslator(t, "http://ep.test/", repo);
134
+ assert.deepEqual(t["ceterms:ctid"], ["ce-1", "ce-2"]);
135
+ });
136
+ });
137
+
138
+ describe('createRelations', () => {
139
+ const run = (value, endpoint) => {
140
+ const e = { id: "http://c/1", "ceasn:isPartOf": "http://f/1", "ceasn:broadAlignment": value };
141
+ const relations = [];
142
+ const relationById = {};
143
+ const frameworks = { "http://f/1": { relation: [] } };
144
+ CTDLASNCSVImport.createRelations(e, "ceasn:broadAlignment", "narrows", repo, null, new EcIdentity(), relations, relationById, frameworks, endpoint);
145
+ return { e, relations, relationById, frameworks };
146
+ };
147
+
148
+ it('wraps a single value in an array', () => {
149
+ const { e, relations, frameworks } = run("http://c/2");
150
+ assert.deepEqual(e["ceasn:broadAlignment"], ["http://c/2"]);
151
+ assert.deepEqual(relations.map(r => [r.source, r.target, r.relationType]), [["http://c/1", "http://c/2", "narrows"]]);
152
+ assert.notEqual(relations[0].id, relations[0].shortId());
153
+ assert.deepEqual(frameworks["http://f/1"].relation, [relations[0].shortId()]);
154
+ });
155
+
156
+ it('creates one relation per array entry, resolving CTIDs, with short ids for an endpoint', () => {
157
+ const { relations, relationById } = run(["http://c/2", "ce-3"], "http://ep.test/");
158
+ assert.deepEqual(relations.map(r => r.target), ["http://c/2", "http://ep.test/ce-3"]);
159
+ for (const r of relations) {
160
+ assert.equal(r.id, r.shortId());
161
+ assert.equal(relationById[r.shortId()], r);
162
+ }
163
+ });
164
+
165
+ it('treats a numeric value as an array length and then fails on the empty slots', () => {
166
+ const e = { id: "http://c/1", "ceasn:isPartOf": "http://f/1", "ceasn:broadAlignment": 2 };
167
+ assert.throws(() => CTDLASNCSVImport.createRelations(e, "ceasn:broadAlignment", "narrows", repo, null, new EcIdentity(), [], {}, { "http://f/1": { relation: [] } }, null), TypeError);
168
+ assert.lengthOf(e["ceasn:broadAlignment"], 2);
169
+ assert.isFalse(0 in e["ceasn:broadAlignment"]);
170
+ });
171
+ });
172
+
173
+ describe('validateFrameworkHierarchy', () => {
174
+ const rules = {
175
+ "ceasn:Competency": { requiredProperties: ["ceasn:isPartOf"], childProperties: ["ceasn:isChildOf", "ceasn:hasChild"], errorMessage: "custom" },
176
+ "ceasn:CompetencyFramework": { requiredProperties: ["ceasn:hasTopChild"] }
177
+ };
178
+
179
+ it('requires rules', () => {
180
+ assert.equal(CTDLASNCSVImport.validateFrameworkHierarchy([], null), 'Hierarchy rules not defined.');
181
+ });
182
+
183
+ it('ignores types that are not present', () => {
184
+ assert.isNull(CTDLASNCSVImport.validateFrameworkHierarchy([null, { "@type": "x" }], rules));
185
+ });
186
+
187
+ it('passes when a row of the type has a required property', () => {
188
+ assert.isNull(CTDLASNCSVImport.validateFrameworkHierarchy([{ "@type": "ceasn:Competency", "ceasn:isPartOf": "x" }], { "ceasn:Competency": rules["ceasn:Competency"] }));
189
+ });
190
+
191
+ it('passes when any typed row has a child property', () => {
192
+ assert.isNull(CTDLASNCSVImport.validateFrameworkHierarchy([{ "@type": "ceasn:Competency" }, { "@type": "other", "ceasn:hasChild": "y" }], { "ceasn:Competency": rules["ceasn:Competency"] }));
193
+ });
194
+
195
+ it('ignores child properties on rows without a type', () => {
196
+ const data = [{ "@type": "ceasn:Competency" }, { "ceasn:hasChild": "y" }, null, { "@type": "", "ceasn:hasChild": "y" }];
197
+ assert.equal(CTDLASNCSVImport.validateFrameworkHierarchy(data, { "ceasn:Competency": rules["ceasn:Competency"] }), "custom");
198
+ });
199
+
200
+ it('ignores required properties on rows of another type', () => {
201
+ const data = [{ "@type": "ceasn:CompetencyFramework" }, { "@type": "other", "ceasn:hasTopChild": "x" }];
202
+ assert.equal(CTDLASNCSVImport.validateFrameworkHierarchy(data, rules), "CSV must contain hierarchy properties for ceasn:CompetencyFramework");
203
+ });
204
+
205
+ it('checks every type in order and returns the first failure', () => {
206
+ const data = [{ "@type": "ceasn:Competency" }, { "@type": "ceasn:CompetencyFramework" }];
207
+ assert.equal(CTDLASNCSVImport.validateFrameworkHierarchy(data, rules), "custom");
208
+ data[0]["ceasn:isPartOf"] = "x";
209
+ assert.equal(CTDLASNCSVImport.validateFrameworkHierarchy(data, rules), "CSV must contain hierarchy properties for ceasn:CompetencyFramework");
210
+ });
211
+
212
+ it('works with rules that only have one kind of property', () => {
213
+ assert.isNull(CTDLASNCSVImport.validateFrameworkHierarchy([{ "@type": "t", c: 1 }], { t: { childProperties: ["c"] } }));
214
+ assert.equal(CTDLASNCSVImport.validateFrameworkHierarchy([{ "@type": "t", c: 1 }], { t: {} }), "CSV must contain hierarchy properties for t");
215
+ });
216
+
217
+ it('keeps scanning after one property is found and throws on non-iterable rules', () => {
218
+ const data = [{ "@type": "other", c: 1 }, { "@type": "t" }];
219
+ assert.throws(() => CTDLASNCSVImport.validateFrameworkHierarchy(data, { t: { childProperties: ["c"], requiredProperties: 5 } }), TypeError);
220
+ assert.isNull(CTDLASNCSVImport.validateFrameworkHierarchy([{ "@type": "t", r: 1, c: 1 }, { "@type": "t" }], { t: { childProperties: ["c"], requiredProperties: ["r"], x: 5 } }));
221
+ });
222
+ });
223
+
224
+ describe('importFrameworksAndCompetencies', () => {
225
+ const frameworkCsv = () => csvOf([
226
+ FW_HEADER,
227
+ ["ceasn:CompetencyFramework", "http://fw.test/F1", "ce-f1", "Framework", "", "", "", "", "1.0~Version~http://t", ""],
228
+ ["ceasn:Competency", "http://fw.test/C1", "ce-c1", "", "Comp one", "http://fw.test/F1", "", "http://other.test/X", "", ""],
229
+ ["ceasn:Competency", "http://fw.test/C2", "ce-c2", "", "Comp two", "", "http://fw.test/C1", "http://other.test/Y|http://other.test/Z", "", "2020-01-01"]
230
+ ]);
231
+
232
+ it('imports frameworks, competencies and relations', async () => {
233
+ stubNetwork();
234
+ const { frameworks, competencies, relations, notes } = await importCsv(frameworkCsv());
235
+ assert.deepEqual(notes, ["Invalid file"]);
236
+ assert.lengthOf(frameworks, 1);
237
+ const fw = frameworks[0];
238
+ assert.equal(fw.id, "http://fw.test/F1");
239
+ assert.deepEqual(fw.competency, ["http://fw.test/C1", "http://fw.test/C2"]);
240
+ assert.deepEqual(fw["ceterms:versionIdentifier"], [{
241
+ "ceterms:identifierValueCode": "1.0",
242
+ "ceterms:identifierTypeName": { "@language": "en-us", "@value": "Version" },
243
+ "ceterms:identifierType": "http://t"
244
+ }]);
245
+ assert.deepEqual(competencies.map(c => c.id), ["http://fw.test/C1", "http://fw.test/C2"]);
246
+ assert.deepEqual(relations.map(r => [r.source, r.relationType, r.target]), [
247
+ ["http://fw.test/C1", "narrows", "http://other.test/X"],
248
+ ["http://fw.test/C2", "narrows", "http://fw.test/C1"],
249
+ ["http://fw.test/C2", "narrows", "http://other.test/Y"],
250
+ ["http://fw.test/C2", "narrows", "http://other.test/Z"]
251
+ ]);
252
+ assert.deepEqual(fw.relation, relations.map(r => r.shortId()));
253
+ for (const r of relations) {
254
+ assert.isTrue(r.id.startsWith(SERVER));
255
+ assert.notEqual(r.id, r.shortId());
256
+ }
257
+ assert.isUndefined(fw["schema:dateCreated"]);
258
+ assert.isUndefined(competencies[0]["schema:dateCreated"]);
259
+ });
260
+
261
+ it('renames columns through the terms map', async () => {
262
+ stubNetwork({ "ceasn:competencyText": "ceasn:renamedText" });
263
+ const { competencies } = await importCsv(frameworkCsv());
264
+ assert.equal(competencies[0]["ceasn:renamedText"], "Comp one");
265
+ assert.isUndefined(competencies[0]["ceasn:competencyText"]);
266
+ });
267
+
268
+ it('applies template owners and creation dates', async () => {
269
+ stubNetwork();
270
+ EcFramework.template = { "schema:dateCreated": "TEMPLATE", "ceasn:extra": "E" };
271
+ EcCompetency.template = { "@owner": "OWNER", "schema:dateCreated": "TEMPLATE" };
272
+ const { frameworks, competencies } = await importCsv(frameworkCsv());
273
+ assert.equal(frameworks[0]["ceasn:extra"], "E");
274
+ assert.match(frameworks[0]["schema:dateCreated"], /^\d{4}-\d\d-\d\dT/);
275
+ assert.equal(competencies[0].owner, "OWNER");
276
+ assert.match(competencies[0]["schema:dateCreated"], /^\d{4}-\d\d-\d\dT/);
277
+ // A row with its own creation date keeps it.
278
+ assert.isUndefined(competencies[1]["schema:dateCreated"]);
279
+ });
280
+
281
+ it('leaves dates alone when the templates have no creation date', async () => {
282
+ stubNetwork();
283
+ EcFramework.template = {};
284
+ EcCompetency.template = {};
285
+ const { frameworks, competencies } = await importCsv(frameworkCsv());
286
+ assert.isUndefined(frameworks[0]["schema:dateCreated"]);
287
+ assert.isUndefined(competencies[0]["schema:dateCreated"]);
288
+ assert.isUndefined(competencies[0].owner);
289
+ });
290
+
291
+ it('creates short relation ids for an endpoint', async () => {
292
+ stubNetwork();
293
+ const { relations } = await importCsv(frameworkCsv(), { endpoint: "http://ep.test/" });
294
+ for (const r of relations) {
295
+ assert.isTrue(r.id.startsWith("http://ep.test/"));
296
+ assert.equal(r.id, r.shortId());
297
+ }
298
+ });
299
+
300
+ it('drops version identifiers that are not strings, leaving a hole', async () => {
301
+ stubNetwork(null, (t) => {
302
+ if (t.id === "http://fw.test/F1") t["ceterms:versionIdentifier"] = ["1.0~V", 5];
303
+ });
304
+ const { frameworks } = await importCsv(frameworkCsv());
305
+ const v = frameworks[0]["ceterms:versionIdentifier"];
306
+ assert.lengthOf(v, 2);
307
+ assert.isFalse(1 in v);
308
+ assert.deepEqual(v[0], { "ceterms:identifierValueCode": "1.0", "ceterms:identifierTypeName": { "@language": "en-us", "@value": "V" }, "ceterms:identifierType": undefined });
309
+ });
310
+
311
+ it('skips sample and instruction rows and reports missing fields', async () => {
312
+ stubNetwork();
313
+ const csv = csvOf([
314
+ ["@type", "@id", "ceasn:name"],
315
+ ["Sample row", "", ""],
316
+ ["INSTRUCTIONS", "", ""],
317
+ ["", "", "no type"],
318
+ ["ceasn:Competency", "bad", ""],
319
+ ["ceasn:Unknown", "http://x/1", ""]
320
+ ]);
321
+ const { errors } = await importCsv(csv);
322
+ assert.deepEqual(errors, [
323
+ "Row 4: is missing an @id",
324
+ "Row 4: is missing a @type",
325
+ "Row 5: @id must be a valid URI or start with 'ce-'",
326
+ "Row 6: Found unknown type:ceasn:Unknown"
327
+ ]);
328
+ });
329
+
330
+ it('fails on a CSV that ends with a newline (cassproject/cass-npm#542)', async () => {
331
+ stubNetwork();
332
+ const { errors } = await importCsv(frameworkCsv() + "\n");
333
+ assert.deepEqual(errors, ["Row 5: is missing an @id", "Row 5: is missing a @type"]);
334
+ });
335
+
336
+ it('applies required-property and registry-url validation rules', async () => {
337
+ stubNetwork();
338
+ const validationRules = {
339
+ validateHierarchy: false,
340
+ requiredProps: { "ceasn:CompetencyFramework": ["ceasn:name"], "ceasn:Competency": ["ceasn:competencyText"] },
341
+ urlRules: { fields: { "ceasn:CompetencyFramework": ["ceasn:publisher"], "ceasn:Competency": ["ceasn:creator"] }, allowedOrigins: ["https://ok.test"] }
342
+ };
343
+ const csv = csvOf([
344
+ ["@type", "@id", "ceasn:name", "ceasn:competencyText", "ceasn:publisher", "ceasn:creator", "ceasn:isTopChildOf"],
345
+ ["ceasn:CompetencyFramework", "http://f/1", "", "", "", "", ""],
346
+ ["ceasn:CompetencyFramework", "http://f/2", "Two", "", "https://bad.test/x", "", ""],
347
+ ["ceasn:CompetencyFramework", "http://f/3", "Three", "", "https://ok.test/x", "", ""],
348
+ ["ceasn:Competency", "http://c/1", "", "", "", "", "http://f/3"],
349
+ ["ceasn:Competency", "http://c/2", "", "Text", "", "https://bad.test/y", "http://f/3"]
350
+ ]);
351
+ const { errors } = await importCsv(csv, { validationRules });
352
+ assert.deepEqual(errors, [
353
+ "Row 2: Missing required properties for ceasn:CompetencyFramework: ceasn:name",
354
+ "Row 3: ceasn:publisher must be a CTID or a credential registry URL for this environment (got: https://bad.test/x)",
355
+ "Row 5: Missing required properties for ceasn:Competency: ceasn:competencyText",
356
+ "Row 6: ceasn:creator must be a CTID or a credential registry URL for this environment (got: https://bad.test/y)"
357
+ ]);
358
+ });
359
+
360
+ it('passes when the validation rules are satisfied', async () => {
361
+ stubNetwork();
362
+ const validationRules = {
363
+ hierarchyRules: { "ceasn:Competency": { requiredProperties: ["ceasn:isTopChildOf"] } },
364
+ requiredProps: { "ceasn:Competency": ["ceasn:competencyText"] },
365
+ urlRules: { fields: {}, allowedOrigins: ["https://ok.test"] }
366
+ };
367
+ const { competencies } = await importCsv(frameworkCsv(), { validationRules });
368
+ assert.lengthOf(competencies, 2);
369
+ });
370
+
371
+ it('skips rows by CTID and replaces skipped CTIDs in the remaining rows', async () => {
372
+ stubNetwork();
373
+ const csv = csvOf([
374
+ ["@type", "@id", "ceterms:CTID", "ceasn:competencyText", "ceasn:isTopChildOf", "ceasn:comment"],
375
+ ["ceasn:CompetencyFramework", "http://fw.test/F1", "ce-f1", "", "", ""],
376
+ ["ceasn:Competency", "http://fw.test/aaa", "ce-aaa", "A", "http://fw.test/F1", "see aaa"],
377
+ ["ceasn:Competency", "http://fw.test/bbb", "ce-bbb", "B", "http://fw.test/F1", "see bbb"],
378
+ ["ceasn:Competency", "http://fw.test/ccc", "ce-ccc", "C", "http://fw.test/F1", "see bbb"]
379
+ ]);
380
+ const { competencies } = await importCsv(csv, { skip: [{ ctid: "ce-bbb", replaceWith: "ce-zzz" }, "ce-aaa"] });
381
+ assert.deepEqual(competencies.map(c => c.id), ["http://fw.test/ccc"]);
382
+ assert.equal(competencies[0]["ceasn:comment"], "see zzz");
383
+ assert.equal(competencies[0]["ceterms:ctid"], "ce-ccc");
384
+ });
385
+
386
+ it('does not skip a row whose CTID matches a falsy skip entry', async () => {
387
+ stubNetwork();
388
+ const csv = csvOf([
389
+ ["@type", "@id", "ceterms:ctid", "ceasn:competencyText", "ceasn:isTopChildOf"],
390
+ ["ceasn:CompetencyFramework", "http://fw.test/F1", "ce-f1", "", ""],
391
+ ["ceasn:Competency", "http://fw.test/aaa", "", "A", "http://fw.test/F1"]
392
+ ]);
393
+ const { competencies } = await importCsv(csv, { skip: ["", { ctid: "ce-zzz" }] });
394
+ assert.deepEqual(competencies.map(c => c.id), ["http://fw.test/aaa"]);
395
+ });
396
+
397
+ it('reports competencies whose id already exists as another type', async () => {
398
+ stubNetwork();
399
+ EcRepository.get.resolves({ type: "Competency" });
400
+ const { errors } = await importCsv(frameworkCsv());
401
+ assert.equal(errors[0], "Row 2: http://fw.test/F1 already exists as a Competency");
402
+ });
403
+
404
+ it('passes duplicate-check results through', async () => {
405
+ stubNetwork();
406
+ const seen = [];
407
+ const check = async (r, ids, fws, ctids) => { seen.push([r, ids, fws.length, ctids]); return ["dup"]; };
408
+ const { errors } = await importCsv(frameworkCsv(), { duplicateChecks: [check] });
409
+ assert.deepEqual(errors, ["dup"]);
410
+ assert.equal(seen[0][0], repo);
411
+ assert.deepEqual(seen[0][1], ["http://fw.test/C1", "http://fw.test/C2"]);
412
+ assert.equal(seen[0][2], 1);
413
+ assert.deepEqual(seen[0][3], { "http://fw.test/C1": "ce-c1", "http://fw.test/C2": "ce-c2" });
414
+ });
415
+ });
416
+
417
+ describe('importCollectionsAndCompetencies', () => {
418
+ const header = ["@type", "@id", "ceterms:ctid", "ceasn:name", "ceasn:competencyText", "ceterms:isMemberOf", "ceasn:broadAlignment"];
419
+ const collectionCsv = () => csvOf([
420
+ header,
421
+ ["ceterms:Collection", "http://col.test/K1", "ce-k1", "Collection", "", "", ""],
422
+ ["ceasn:Competency", "http://col.test/C1", "ce-c1", "", "One", "http://col.test/K1", "http://other.test/X"],
423
+ ["ceasn:Competency", "http://col.test/C2", "ce-c2", "", "Two", "http://col.test/K9", ""]
424
+ ]);
425
+
426
+ it('imports collections, members and relations', async () => {
427
+ stubNetwork();
428
+ const { frameworks, competencies, relations } = await importCsv(collectionCsv(), { collections: true });
429
+ assert.lengthOf(frameworks, 1);
430
+ assert.equal(frameworks[0].subType, "Collection");
431
+ assert.deepEqual(frameworks[0].competency, ["http://col.test/C1"]);
432
+ assert.deepEqual(competencies.map(c => c.id), ["http://col.test/C1", "http://col.test/C2"]);
433
+ assert.deepEqual(relations.map(r => [r.source, r.target]), [["http://col.test/C1", "http://other.test/X"]]);
434
+ assert.deepEqual(frameworks[0].relation, [relations[0].shortId()]);
435
+ // Neither the known collection (no owner) nor the unknown one gives the members an owner.
436
+ assert.isUndefined(competencies[0].owner);
437
+ assert.isUndefined(competencies[1].owner);
438
+ });
439
+
440
+ it('applies template owners and creation dates', async () => {
441
+ stubNetwork();
442
+ EcFramework.template = { "@owner": ["TEMPLATE"], "schema:dateCreated": "TEMPLATE" };
443
+ EcCompetency.template = { "@owner": "OWNER", "schema:dateCreated": "TEMPLATE" };
444
+ const { frameworks, competencies } = await importCsv(collectionCsv(), { collections: true });
445
+ assert.deepEqual(frameworks[0].owner, ["TEMPLATE"]);
446
+ assert.match(frameworks[0]["schema:dateCreated"], /^\d{4}-\d\d-\d\dT/);
447
+ assert.equal(competencies[0].owner, "OWNER");
448
+ assert.match(competencies[0]["schema:dateCreated"], /^\d{4}-\d\d-\d\dT/);
449
+ });
450
+
451
+ it('leaves dates alone when the templates have no creation date', async () => {
452
+ stubNetwork();
453
+ EcFramework.template = {};
454
+ EcCompetency.template = {};
455
+ const { frameworks, competencies } = await importCsv(collectionCsv(), { collections: true });
456
+ assert.isUndefined(frameworks[0]["schema:dateCreated"]);
457
+ assert.isUndefined(competencies[0]["schema:dateCreated"]);
458
+ });
459
+
460
+ it('applies required-property and registry-url validation rules', async () => {
461
+ stubNetwork();
462
+ const validationRules = {
463
+ validateHierarchy: false,
464
+ requiredProps: { "ceterms:Collection": ["ceasn:name"], "ceterms:Collection:Competency": ["ceasn:competencyText"] },
465
+ urlRules: { fields: { "ceterms:Collection": ["ceasn:publisher"], "ceasn:Competency": ["ceasn:creator"] }, allowedOrigins: ["https://ok.test"] }
466
+ };
467
+ const csv = csvOf([
468
+ ["@type", "@id", "ceasn:name", "ceasn:competencyText", "ceasn:publisher", "ceasn:creator", "ceterms:isMemberOf"],
469
+ ["ceterms:Collection", "http://k/1", "", "", "", "", ""],
470
+ ["ceterms:Collection", "http://k/2", "Two", "", "https://bad.test/x", "", ""],
471
+ ["ceasn:Competency", "http://c/1", "", "", "", "", "http://k/2"],
472
+ ["ceasn:Competency", "http://c/2", "", "Text", "", "https://bad.test/y", "http://k/2"]
473
+ ]);
474
+ const { errors } = await importCsv(csv, { collections: true, validationRules });
475
+ assert.deepEqual(errors, [
476
+ "Row 2: Missing required properties for ceterms:Collection: ceasn:name",
477
+ "Row 3: ceasn:publisher must be a CTID or a credential registry URL for this environment (got: https://bad.test/x)",
478
+ "Row 4: Missing required properties for ceterms:Collection:Competency: ceasn:competencyText",
479
+ "Row 5: ceasn:creator must be a CTID or a credential registry URL for this environment (got: https://bad.test/y)"
480
+ ]);
481
+ });
482
+
483
+ it('skips rows by CTID, including a falsy-entry edge case', async () => {
484
+ stubNetwork();
485
+ let r = await importCsv(collectionCsv(), { collections: true, skip: [{ ctid: "ce-c1" }] });
486
+ assert.deepEqual(r.competencies.map(c => c.id), ["http://col.test/C2"]);
487
+ r = await importCsv(collectionCsv(), { collections: true, skip: ["ce-c2"] });
488
+ assert.deepEqual(r.competencies.map(c => c.id), ["http://col.test/C1"]);
489
+ const csv = csvOf([
490
+ ["@type", "@id", "ceterms:ctid", "ceasn:competencyText", "ceterms:isMemberOf"],
491
+ ["ceasn:Competency", "http://col.test/C1", "", "One", "http://col.test/K1"]
492
+ ]);
493
+ r = await importCsv(csv, { collections: true, skip: ["", { ctid: "ce-zzz" }] });
494
+ assert.deepEqual(r.competencies.map(c => c.id), ["http://col.test/C1"]);
495
+ });
496
+ });
497
+ });
@@ -0,0 +1,107 @@
1
+ require('../../index.js');
2
+ const chai = require('chai');
3
+ const Ebac = require('../org/cassproject/schema/general/Ebac.js');
4
+
5
+ const assert = chai.assert;
6
+
7
+ const classes = {
8
+ EbacContact: require('../com/eduworks/schema/ebac/EbacContact.js'),
9
+ EbacContactGrant: require('../com/eduworks/schema/ebac/EbacContactGrant.js'),
10
+ EbacCredential: require('../com/eduworks/schema/ebac/EbacCredential.js'),
11
+ EbacCredentialCommit: require('../com/eduworks/schema/ebac/EbacCredentialCommit.js'),
12
+ EbacCredentialRequest: require('../com/eduworks/schema/ebac/EbacCredentialRequest.js'),
13
+ EbacCredentials: require('../com/eduworks/schema/ebac/EbacCredentials.js'),
14
+ EbacEncryptedSecret: require('../com/eduworks/schema/ebac/EbacEncryptedSecret.js'),
15
+ EbacEncryptedValue: require('../com/eduworks/schema/ebac/EbacEncryptedValue.js'),
16
+ };
17
+ const EbacSignature = require('../com/eduworks/schema/ebac/EbacSignature.js');
18
+
19
+ const SCHEMA = 'http://schema.eduworks.com/ebac/0.1/';
20
+
21
+ function legacy(C, type) {
22
+ const o = new C();
23
+ o.type = type;
24
+ delete o['@context'];
25
+ delete o['@type'];
26
+ return o;
27
+ }
28
+
29
+ describe('Ebac schema classes', () => {
30
+ for (const [name, C] of Object.entries(classes)) {
31
+ describe(name, () => {
32
+ it('getTypes lists the types newest first', () => {
33
+ const a = new C().getTypes();
34
+ assert.deepEqual(a, [C.TYPE_0_4, C.TYPE_0_3, C.TYPE_0_2, C.TYPE_0_1]);
35
+ assert.notStrictEqual(new C().getTypes(), a);
36
+ });
37
+ it('upgrades a 0.1 object, copying @schema into a missing @context', () => {
38
+ const o = legacy(C, C.TYPE_0_1);
39
+ o['@schema'] = SCHEMA;
40
+ o.upgrade();
41
+ assert.equal(o['@context'], SCHEMA);
42
+ assert.equal(o.context, Ebac.context_0_4);
43
+ assert.equal(o.getFullType(), C.TYPE_0_4);
44
+ assert.equal(o.getFullType(), C.TYPE_0_4);
45
+ });
46
+ it('upgrade keeps an existing @context', () => {
47
+ const o = legacy(C, C.TYPE_0_1);
48
+ o['@schema'] = SCHEMA;
49
+ o['@context'] = 'keep';
50
+ o.upgrade();
51
+ assert.equal(o['@context'], 'keep');
52
+ assert.equal(o.getFullType(), C.TYPE_0_4);
53
+ });
54
+ it('upgrade without @schema leaves @context unset', () => {
55
+ const o = legacy(C, C.TYPE_0_1);
56
+ o.upgrade();
57
+ assert.isUndefined(o['@context']);
58
+ assert.equal(o.getFullType(), C.TYPE_0_4);
59
+ });
60
+ it('upgrades 0.3 objects to 0.4', () => {
61
+ const o = new C();
62
+ o.setContextAndType(Ebac.context_0_3, C.TYPE_0_3);
63
+ o.upgrade();
64
+ assert.equal(o.context, Ebac.context_0_4);
65
+ assert.equal(o.getFullType(), C.TYPE_0_4);
66
+ });
67
+ });
68
+ }
69
+
70
+ it('EbacContact 0.1 and 0.2 types are the same IRI', () => {
71
+ assert.equal(classes.EbacContact.TYPE_0_1, classes.EbacContact.TYPE_0_2);
72
+ });
73
+
74
+ describe('EbacSignature', () => {
75
+ it('getTypes lists the types newest first', () => {
76
+ assert.deepEqual(new EbacSignature().getTypes(), [EbacSignature.TYPE_0_4, EbacSignature.TYPE_0_3, EbacSignature.TYPE_0_2, EbacSignature.TYPE_0_1]);
77
+ });
78
+ it('upgrade does not change legacy types', () => {
79
+ const o = legacy(EbacSignature, EbacSignature.TYPE_0_1);
80
+ o['@schema'] = SCHEMA;
81
+ o.upgrade();
82
+ assert.equal(o.type, EbacSignature.TYPE_0_1);
83
+ assert.isUndefined(o['@context']);
84
+ });
85
+ });
86
+
87
+ describe('EbacEncryptedValue.copyFrom', () => {
88
+ const C = classes.EbacEncryptedValue;
89
+ it('replaces all fields and strips the leading @ from keys', () => {
90
+ const v = new C();
91
+ v.extra = 1;
92
+ v.copyFrom({ "@type": "EncryptedValue", "@context": Ebac.context_0_4, payload: "p", "@owner": ["a"], "x@y": 2 });
93
+ assert.notProperty(v, 'extra');
94
+ assert.notProperty(v, 'iv');
95
+ assert.equal(v.type, "EncryptedValue");
96
+ assert.equal(v.context, Ebac.context_0_4);
97
+ assert.equal(v.payload, "p");
98
+ assert.deepEqual(v.owner, ["a"]);
99
+ assert.equal(v.xy, 2);
100
+ assert.deepEqual(Object.keys(v).sort(), ['context', 'owner', 'payload', 'type', 'xy']);
101
+ });
102
+ it('throws for an incompatible type', () => {
103
+ const v = new C();
104
+ assert.throws(() => v.copyFrom({ "@type": "Thing", "@context": "https://schema.org/" }), Error, "Incompatible type: https://schema.org/Thing");
105
+ });
106
+ });
107
+ });