cassproject 5.0.22 → 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 (105) hide show
  1. package/package.json +47 -50
  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/forgeAsyncNode.js +8 -8
  13. package/src/com/eduworks/ec/graph/EcDirectedGraph.js +1 -2
  14. package/src/com/eduworks/ec/graph/EcFrameworkGraph.js +18 -24
  15. package/src/com/eduworks/ec/promises/helpers.js +1 -3
  16. package/src/com/eduworks/ec/remote/EcRemote.js +9 -9
  17. package/src/com/eduworks/ec/task/EcAsyncTask.js +5 -20
  18. package/src/com/eduworks/ec/task/EcAsyncTaskManager.js +3 -4
  19. package/src/com/eduworks/ec/task/EcAsyncTaskWorker.js +9 -11
  20. package/src/com/eduworks/schema/cfd/competency/CfdFramework.js +36 -37
  21. package/src/com/eduworks/schema/ebac/EbacContact.js +3 -9
  22. package/src/com/eduworks/schema/ebac/EbacContactGrant.js +3 -9
  23. package/src/com/eduworks/schema/ebac/EbacCredential.js +3 -9
  24. package/src/com/eduworks/schema/ebac/EbacCredentialCommit.js +3 -9
  25. package/src/com/eduworks/schema/ebac/EbacCredentialRequest.js +3 -9
  26. package/src/com/eduworks/schema/ebac/EbacCredentials.js +3 -9
  27. package/src/com/eduworks/schema/ebac/EbacEncryptedSecret.js +3 -9
  28. package/src/com/eduworks/schema/ebac/EbacEncryptedValue.js +5 -12
  29. package/src/com/eduworks/schema/ebac/EbacSignature.js +1 -6
  30. package/src/index/env.js +6 -3
  31. package/src/index/polyfills.js +13 -27
  32. package/src/org/angles/schema/angles/VideoStory.js +1 -1
  33. package/src/org/cass/competency/EcCompetency.js +1 -2
  34. package/src/org/cass/competency/EcDirectory.js +1 -2
  35. package/src/org/cass/competency/EcFramework.js +9 -12
  36. package/src/org/cass/exporter/CSVExport.js +12 -13
  37. package/src/org/cass/importer/ASNImport.js +3 -6
  38. package/src/org/cass/importer/CSVImport.js +9 -17
  39. package/src/org/cass/importer/CTDLASNCSVConceptImport.js +25 -121
  40. package/src/org/cass/importer/CTDLASNCSVImport.js +88 -133
  41. package/src/org/cass/importer/FrameworkImport.js +16 -30
  42. package/src/org/cass/importer/MedbiqImport.js +3 -4
  43. package/src/org/cass/importer/TabStructuredImport.js +6 -6
  44. package/src/org/cass/profile/EcAssertion.js +40 -51
  45. package/src/org/cassproject/ebac/identity/EcContactGrant.js +2 -2
  46. package/src/org/cassproject/ebac/identity/EcIdentityManager.js +56 -62
  47. package/src/org/cassproject/ebac/identity/remote/EcRemoteIdentityManager.js +51 -62
  48. package/src/org/cassproject/ebac/identity/remote/OAuth2FileBasedRemoteIdentityManager.js +56 -70
  49. package/src/org/cassproject/ebac/identity/remote/RemoteIdentityManagerInterface.js +3 -3
  50. package/src/org/cassproject/ebac/repository/EcEncryptedValue.js +51 -44
  51. package/src/org/cassproject/ebac/repository/EcRepository.js +228 -301
  52. package/src/org/cassproject/general/repository/GeneralFile.js +8 -9
  53. package/src/org/cassproject/schema/cass/competency/Competency.js +10 -11
  54. package/src/org/cassproject/schema/cass/competency/Framework.js +10 -11
  55. package/src/org/cassproject/schema/cass/competency/Level.js +10 -11
  56. package/src/org/cassproject/schema/cass/competency/Relation.js +10 -11
  57. package/src/org/cassproject/schema/cass/competency/RollupRule.js +7 -7
  58. package/src/org/cassproject/schema/cass/profile/Assertion.js +20 -27
  59. package/src/org/cassproject/schema/cass/profile/AssertionEnvelope.js +3 -5
  60. package/src/org/cassproject/schema/general/EcRemoteLinkedData.js +41 -44
  61. package/src/org/json/ld/EcLinkedData.js +10 -12
  62. package/src/org/schema/EcOrganization.js +10 -10
  63. package/src/org/schema/EcQuestion.js +8 -8
  64. package/src/org/w3/skos/Concept.js +6 -6
  65. package/src/org/w3/skos/ConceptScheme.js +6 -6
  66. package/src/org/w3/skos/EcConcept.js +1 -2
  67. package/src/org/w3/skos/EcConceptScheme.js +1 -2
  68. package/src/test/0.start.test.js +1 -1
  69. package/src/test/0.testLibraries.test.js +172 -0
  70. package/src/test/1.ASNImport.test.js +122 -0
  71. package/src/test/1.CSVExport.test.js +57 -0
  72. package/src/test/1.CSVImport.test.js +185 -0
  73. package/src/test/1.CTDLASNCSVConceptImport.test.js +348 -0
  74. package/src/test/1.CTDLASNCSVImport.import.test.js +497 -0
  75. package/src/test/1.Ebac.schema.test.js +107 -0
  76. package/src/test/1.EcAssertion.decrypt.test.js +98 -0
  77. package/src/test/1.EcAssertion.pinned.test.js +160 -0
  78. package/src/test/1.EcCrypto.formats.test.js +293 -0
  79. package/src/test/1.EcCrypto.generateUUID.test.js +47 -0
  80. package/src/test/1.EcEncryptedValue.helpers.test.js +236 -0
  81. package/src/test/1.EcHelpers.misc.test.js +259 -0
  82. package/src/test/1.EcIdentityManager.keys.test.js +439 -0
  83. package/src/test/1.EcOrganization.rekey.test.js +68 -0
  84. package/src/test/1.EcRemote.responses.test.js +155 -0
  85. package/src/test/1.EcRemoteIdentityManager.flows.test.js +326 -0
  86. package/src/test/1.EcRemoteLinkedData.pinned.test.js +473 -0
  87. package/src/test/1.EcRepository.l0.test.js +14 -5
  88. package/src/test/1.EcRepository.l1.test.js +16 -7
  89. package/src/test/1.EcRepository.l2.test.js +20 -11
  90. package/src/test/1.EcRepository.unit.test.js +961 -0
  91. package/src/test/1.EcRsaOaep.test.js +2 -12
  92. package/src/test/1.EcRsaOaepAsync.test.js +2 -12
  93. package/src/test/1.EcRsaOaepAsyncWorker.test.js +2 -12
  94. package/src/test/1.EcRsaOaepAsyncWorker.workers.test.js +147 -0
  95. package/src/test/1.FrameworkImport.test.js +170 -0
  96. package/src/test/1.MedbiqImport.test.js +92 -0
  97. package/src/test/1.OAuth2FileBasedRemoteIdentityManager.test.js +399 -0
  98. package/src/test/1.TabStructuredImport.test.js +61 -0
  99. package/src/test/1.polyfills.test.js +89 -0
  100. package/src/test/1.schema.competency.pinned.test.js +384 -0
  101. package/src/test/3.EcFrameworkGraph.test.js +3 -2
  102. package/src/test/4.schema.test.js +33 -28
  103. package/src/test/4.skyId.test.js +4 -2
  104. package/src/test/cypressValue.js +9 -0
  105. package/src/test/2.EcCompetency.test.js +0 -17
@@ -0,0 +1,236 @@
1
+ require('../../index.js');
2
+ const chai = require('chai');
3
+ const sinon = require('sinon');
4
+ const EcEncryptedValue = require('../org/cassproject/ebac/repository/EcEncryptedValue.js');
5
+ const EcRsaOaepAsyncWorker = require('../com/eduworks/ec/crypto/EcRsaOaepAsyncWorker.js');
6
+ const EcAesCtrAsyncWorker = require('../com/eduworks/ec/crypto/EcAesCtrAsyncWorker.js');
7
+ const EcPpk = require('../com/eduworks/ec/crypto/EcPpk.js');
8
+ const EcPk = require('../com/eduworks/ec/crypto/EcPk.js');
9
+
10
+ const assert = chai.assert;
11
+
12
+ const PEM = "-----BEGIN RSA PRIVATE KEY-----MIIEpAIBAAKCAQEAz4BiFucFE9bNcKfGD+e6aPRHl402YM4Z6nrurDRNlnwsWpsCoZasPLkjC314pVtHAI2duZo+esGKDloBsiLxASRJo3R2XiXVh2Y8U1RcHA5mWL4tMG5UY2d0libpNEHbHPNBmooVYpA2yhxN/vGibIk8x69uZWxJcFOxOg6zWG8EjF8UMgGnRCVSMTY3THhTlfZ0cGUzvrfb7OvHUgdCe285XkmYkj/V9P/m7hbWoOyJAJSTOm4/s6fIKpl72lblfN7bKaxTCsJp6/rQdmUeo+PIaa2lDOfo7dWbuTMcqkZ93kispNfYYhsEGUGlCsrrVWhlve8MenO4GdLsFP+HRwIDAQABAoIBAGaQpOuBIYde44lNxJ7UAdYi+Mg2aqyK81Btl0/TQo6hriLTAAfzPAt/z4y8ZkgFyCDD3zSAw2VWCPFzF+d/UfUohKWgyWlb9iHJLQRbbHQJwhkXV6raviesWXpmnVrROocizkie/FcNxac9OmhL8+cGJt7lHgJP9jTpiW6TGZ8ZzM8KBH2l80x9AWdvCjsICuPIZRjc706HtkKZzTROtq6Z/F4Gm0uWRnwAZrHTRpnh8qjtdBLYFrdDcUoFtzOM6UVRmocTfsNe4ntPpvwY2aGTWY7EmTj1kteMJ+fCQFIS+KjyMWQHsN8yQNfD5/j2uv6/BdSkO8uorGSJT6DwmTECgYEA8ydoQ4i58+A1udqA+fujM0Zn46++NTehFe75nqIt8rfQgoduBam3lE5IWj2U2tLQeWxQyr1ZJkLbITtrAI3PgfMnuFAii+cncwFo805Fss/nbKx8K49vBuCEAq3MRhLjWy3ZvIgUHj67jWvl50dbNqc7TUguxhS4BxGr/cPPkP0CgYEA2nbJPGzSKhHTETL37NWIUAdU9q/6NVRISRRXeRqZYwE1VPzs2sIUxA8zEDBHX7OtvCKzvZy1Lg5Unx1nh4nCEVkbW/8npLlRG2jOcZJF6NRfhzwLz3WMIrP6j9SmjJaB+1mnrTjfsg36tDEPDjjJLjJHCx9z/qRJh1v4bh4aPpMCgYACG31T2IOEEZVlnvcvM3ceoqWT25oSbAEBZ6jSLyWmzOEJwJK7idUFfAg0gAQiQWF9K+snVqzHIB02FIXA43nA7pKRjmA+RiqZXJHEShFgk1y2HGiXGA8mSBvcyhTTJqbBy4vvjl5eRLzrZNwBPSUVPC3PZajCHrvZk9WhxWivIQKBgQCzCu1MH2dy4R7ZlqsIJ8zKweeJMZpfQI7pjclO0FTrhh7+Yzd+5db9A/P2jYrBTVHSwaILgTYf49DIguHJfEZXz26TzB7iapqlWxTukVHISt1ryPNo+E58VoLAhChnSiaHJ+g7GESE+d4A9cAACNwgh0YgQIvhIyW70M1e+j7KDwKBgQDQSBLFDFmvvTP3sIRAr1+0OZWd1eRcwdhs0U9GwootoCoUP/1Y64pqukT6B9oIB/No9Nyn8kUX3/ZDtCslaGKEUGMJXQ4hc5J+lq0tSi9ZWBdhqOuMPEfUF3IxW+9yeILP4ppUBn1m5MVOWg5CvuuEeCmy4bhMaUErUlHZ78t5cA==-----END RSA PRIVATE KEY-----";
13
+ const PK_A = EcPpk.fromPem(PEM).toPk().toPem();
14
+ // Any parseable public keys will do here; encryption is stubbed and keyed by PEM.
15
+ const PK_B = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzzy0+Hau1QfZoZo/cUUaEdqWHoN3f+BezVUM8DZpxWrrnsKhKSV+ieEHZRduVM4LfxeByWhbzjkAvE8IWbQIGOA60UhZSjgjuSpNtlHRgmommkBtkvMN65TQwfuD6Z1GnPrE+T2PciKOPiiD5/5836zadSn4tMrM52jMZbUS/zi8P61y9TZejXI1KCgKYoi7po91eXmklTN192nG7iiOoUxz2TQFHG8VxqRi2UcBS+WuFEvLOayvCH5aZpZvN9GlR3EdbrCcZkDLFF9gict9Z3LbSWAe+0/RejgCe24MpfQsWnPNl/9ZSiFG7mbeH6G1hZl0Hahr+xBKrqdNMGfk8wIDAQAB-----END PUBLIC KEY-----";
16
+ const PK_C = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr8oXR8sAu5zJvWk0BRbL/yFfz7zTv76nlwk498FqfyVtfnL13CAiwKWqDx6aNvlYXEVFvjMIclMkIFVqHFJRFBiVEQpuP//Ltd4WKzsRWlWWAGvNUfrgKMip62fZHQstC5MJUSDyCd3ljZe/1HjLFrvDsvjJ3o+GPOaJaHtlkz+Y0K7GrbBM4AzduzowNgcsHbmJ154QTTN6uw4ke1BAf3dRZ7ysyDKonJEtQGVGWvmYOOm5gwf5CHIqx7zp+zDROxzXvFHu0Wevh2WSzPO8u5cW3INe7kjgsG/mQm9QUW3m0+HT/wMh4WOTaR5yBwYMt7akC9Xs9zl2Vus80QQ4+QIDAQAB-----END PUBLIC KEY-----";
17
+
18
+ function typed(context, type) {
19
+ const v = new EcEncryptedValue();
20
+ v.encryptedContext = context;
21
+ v.encryptedType = type;
22
+ return v;
23
+ }
24
+
25
+ describe('EcEncryptedValue helpers', () => {
26
+ afterEach(() => sinon.restore());
27
+
28
+ describe('getEncryptedFullType / getEncryptedDottedType', () => {
29
+ it('returns the type when there is no context or the type is already a URL', () => {
30
+ assert.equal(typed(null, 'Thing').getEncryptedFullType(), 'Thing');
31
+ assert.isUndefined(typed(undefined, undefined).getEncryptedFullType());
32
+ assert.equal(typed('http://ctx/', 'https://other/Thing').getEncryptedFullType(), 'https://other/Thing');
33
+ assert.equal(typed('http://ctx/', 'xhttpy').getEncryptedFullType(), 'xhttpy');
34
+ });
35
+ it('joins a string context and the type', () => {
36
+ assert.equal(typed('http://ctx', 'Thing').getEncryptedFullType(), 'http://ctx/Thing');
37
+ assert.equal(typed('http://ctx/', 'Thing').getEncryptedFullType(), 'http://ctx/Thing');
38
+ });
39
+ it('resolves prefixes and @vocab from an object context', () => {
40
+ assert.equal(typed({ ce: 'https://ce.org/terms' }, 'ce:Thing').getEncryptedFullType(), 'https://ce.org/terms/Thing');
41
+ assert.equal(typed({ ce: 'https://ce.org/terms/' }, 'ce:Thing').getEncryptedFullType(), 'https://ce.org/terms/Thing');
42
+ assert.equal(typed({ '@vocab': 'https://v.org' }, 'Thing').getEncryptedFullType(), 'https://v.org/Thing');
43
+ assert.throws(() => typed({ a: 'b' }, 'Thing').getEncryptedFullType(), TypeError);
44
+ });
45
+ it('dots the full type', () => {
46
+ assert.equal(typed('https://schema.cassproject.org/0.4/', 'Competency').getEncryptedDottedType(), 'schema.cassproject.org.0.4.Competency');
47
+ assert.equal(typed('http://a/b/', 'C').getEncryptedDottedType(), 'a.b.C');
48
+ assert.equal(typed(null, 'x/y/z').getEncryptedDottedType(), 'x.y.z');
49
+ assert.equal(typed(null, 'http://a/https://b/c').getEncryptedDottedType(), 'a.b.c');
50
+ });
51
+ });
52
+
53
+ describe('isAnEncrypted', () => {
54
+ it('compares the whole type or its last path segment', () => {
55
+ assert.isFalse(new EcEncryptedValue().isAnEncrypted('http://a/Thing'));
56
+ assert.isTrue(typed(null, 'Thing').isAnEncrypted('http://a/Thing'));
57
+ assert.isTrue(typed(null, 'http://a/Thing').isAnEncrypted('http://a/Thing'));
58
+ assert.isTrue(typed(null, 'Thing').isAnEncrypted('Thing'));
59
+ assert.isFalse(typed(null, 'Thing').isAnEncrypted('http://a/Thing/'));
60
+ assert.isFalse(typed(null, 'a').isAnEncrypted('http://a/Thing'));
61
+ });
62
+ });
63
+
64
+ describe('encryptValueActual', () => {
65
+ let reuse, map;
66
+ beforeEach(() => {
67
+ reuse = EcEncryptedValue.secretReuse;
68
+ map = EcEncryptedValue.secretReuseMap;
69
+ EcEncryptedValue.secretReuseMap = {};
70
+ sinon.useFakeTimers({ now: 1000000, toFake: ['Date'] });
71
+ sinon.stub(EcAesCtrAsyncWorker, 'encrypt').resolves('CIPHER');
72
+ sinon.stub(EcRsaOaepAsyncWorker, 'encrypt').callsFake(async (pk, json) => 'E(' + pk.toPem().length + ')' + json);
73
+ });
74
+ afterEach(() => {
75
+ EcEncryptedValue.secretReuse = reuse;
76
+ EcEncryptedValue.secretReuseMap = map;
77
+ });
78
+ const secretJson = (s) => JSON.stringify({ v: s });
79
+
80
+ it('encrypts the payload and the secret for each owner and reader', async () => {
81
+ EcEncryptedValue.secretReuse = false;
82
+ const v = new EcEncryptedValue();
83
+ let seen = null;
84
+ const r = await EcEncryptedValue.encryptValueActual(v, 'text', 'IV', 'SECRET', 'ID', [PK_A], [PK_B], (x) => { seen = x; return x; }, null);
85
+ assert.strictEqual(r, v);
86
+ assert.strictEqual(seen, v);
87
+ assert.deepEqual(EcAesCtrAsyncWorker.encrypt.firstCall.args, ['text', 'SECRET', 'IV']);
88
+ assert.equal(v.payload, 'CIPHER');
89
+ assert.equal(v.iv, 'IV');
90
+ assert.equal(v.id, 'ID');
91
+ assert.deepEqual(v.owner, [PK_A]);
92
+ assert.deepEqual(v.reader, [PK_B]);
93
+ const calls = EcRsaOaepAsyncWorker.encrypt.getCalls();
94
+ assert.deepEqual(calls.map((c) => c.args[0].toPem()), [PK_A, PK_B]);
95
+ assert.include(calls[0].args[1], '"SECRET"');
96
+ assert.deepEqual(v.secret, calls.map((c) => 'E(' + c.args[0].toPem().length + ')' + c.args[1]));
97
+ assert.deepEqual(EcEncryptedValue.secretReuseMap, {});
98
+ });
99
+ it('handles missing owners and readers', async () => {
100
+ EcEncryptedValue.secretReuse = false;
101
+ const v = await EcEncryptedValue.encryptValueActual(new EcEncryptedValue(), 'text', 'IV', 'SECRET', 'ID', null, null);
102
+ assert.isNull(v.owner);
103
+ assert.isNull(v.reader);
104
+ assert.deepEqual(v.secret, []);
105
+ });
106
+ it('without a reuse entry encrypts the secret every time', async () => {
107
+ EcEncryptedValue.secretReuse = true;
108
+ await EcEncryptedValue.encryptValueActual(new EcEncryptedValue(), 't', 'IV', 'S', 'ID', [PK_A], null);
109
+ await EcEncryptedValue.encryptValueActual(new EcEncryptedValue(), 't', 'IV', 'S', 'ID', [PK_A], null);
110
+ assert.equal(EcRsaOaepAsyncWorker.encrypt.callCount, 2);
111
+ assert.deepEqual(EcEncryptedValue.secretReuseMap, {});
112
+ });
113
+ it('reuses the encrypted secret for a fresh entry with the same secret', async () => {
114
+ EcEncryptedValue.secretReuse = true;
115
+ const key = [PK_A, PK_B].join(',');
116
+ const entry = { secret: 'S', expires: 1000001 };
117
+ EcEncryptedValue.secretReuseMap[key] = entry;
118
+ const v1 = await EcEncryptedValue.encryptValueActual(new EcEncryptedValue(), 't', 'IV', 'S', 'ID', [PK_A], [PK_B]);
119
+ assert.deepEqual(Object.keys(entry.encryptedSecret), [PK_A, PK_B]);
120
+ assert.instanceOf(entry.encryptedSecret[PK_A], Promise);
121
+ const v2 = await EcEncryptedValue.encryptValueActual(new EcEncryptedValue(), 't2', 'IV2', 'S', 'ID2', [PK_A], [PK_B]);
122
+ assert.equal(EcRsaOaepAsyncWorker.encrypt.callCount, 2);
123
+ assert.deepEqual(v2.secret, v1.secret);
124
+ assert.strictEqual(EcEncryptedValue.secretReuseMap[key], entry);
125
+ });
126
+ it('treats missing owners or readers as empty when looking up a reuse entry', async () => {
127
+ EcEncryptedValue.secretReuse = true;
128
+ const entryB = { secret: 'S', expires: 2000000 };
129
+ EcEncryptedValue.secretReuseMap[PK_B] = entryB;
130
+ let v = await EcEncryptedValue.encryptValueActual(new EcEncryptedValue(), 't', 'IV', 'S', 'ID', null, [PK_B]);
131
+ assert.isNull(v.owner);
132
+ assert.deepEqual(Object.keys(entryB.encryptedSecret), [PK_B]);
133
+ const entryA = { secret: 'S', expires: 2000000, encryptedSecret: { [PK_A]: Promise.resolve('CACHED') } };
134
+ EcEncryptedValue.secretReuseMap[PK_A] = entryA;
135
+ v = await EcEncryptedValue.encryptValueActual(new EcEncryptedValue(), 't', 'IV', 'S', 'ID', [PK_A], null);
136
+ assert.isNull(v.reader);
137
+ assert.deepEqual(v.secret, ['CACHED']);
138
+ assert.equal(EcRsaOaepAsyncWorker.encrypt.callCount, 1);
139
+ });
140
+ it('drops a reuse entry with another secret or that has expired', async () => {
141
+ EcEncryptedValue.secretReuse = true;
142
+ EcEncryptedValue.secretReuseMap[PK_A] = { secret: 'OTHER', expires: 2000000 };
143
+ let v = await EcEncryptedValue.encryptValueActual(new EcEncryptedValue(), 't', 'IV', 'S', 'ID', [PK_A], null);
144
+ assert.notProperty(EcEncryptedValue.secretReuseMap, PK_A);
145
+ assert.equal(v.secret.length, 1);
146
+ EcEncryptedValue.secretReuseMap[PK_A] = { secret: 'S', expires: 1000000 };
147
+ v = await EcEncryptedValue.encryptValueActual(new EcEncryptedValue(), 't', 'IV', 'S', 'ID', [PK_A], null);
148
+ assert.notProperty(EcEncryptedValue.secretReuseMap, PK_A);
149
+ assert.equal(EcRsaOaepAsyncWorker.encrypt.callCount, 2);
150
+ });
151
+ it('drops a mismatched entry once and encrypts for every key', async () => {
152
+ EcEncryptedValue.secretReuse = true;
153
+ const key = [PK_A, PK_B].join(',');
154
+ EcEncryptedValue.secretReuseMap[key] = { secret: 'S', expires: 1000001 };
155
+ const v = await EcEncryptedValue.encryptValueActual(new EcEncryptedValue(), 't', 'IV', 'OTHER', 'ID', [PK_A], [PK_B]);
156
+ assert.notProperty(EcEncryptedValue.secretReuseMap, key);
157
+ assert.equal(v.secret.length, 2);
158
+ assert.equal(EcRsaOaepAsyncWorker.encrypt.callCount, 2);
159
+ });
160
+ it('rejects when the payload cannot be encrypted', async () => {
161
+ EcAesCtrAsyncWorker.encrypt.rejects(new Error('aes'));
162
+ let failed = null;
163
+ await EcEncryptedValue.encryptValueActual(new EcEncryptedValue(), 't', 'IV', 'S', 'ID', [PK_A], null, null, (e) => { failed = e; });
164
+ assert.equal(failed.message, 'aes');
165
+ });
166
+ });
167
+
168
+ describe('addReader / removeReader', () => {
169
+ function value() {
170
+ const v = new EcEncryptedValue();
171
+ v.owner = [PK_A];
172
+ v.reader = [PK_B];
173
+ v.secret = ['sA', 'sB'];
174
+ return v;
175
+ }
176
+ const secret = { toEncryptableJson: () => 'JSON' };
177
+
178
+ it('addReader rejects with a string when the secret is unknown', async () => {
179
+ const v = value();
180
+ sinon.stub(v, 'decryptSecret').resolves(null);
181
+ const e = await v.addReader(EcPk.fromPem(PK_C)).catch((x) => x);
182
+ assert.strictEqual(e, "Cannot add a Reader if you don't know the secret");
183
+ assert.deepEqual(v.reader, [PK_B, PK_C]);
184
+ assert.deepEqual(v.secret, ['sA', 'sB']);
185
+ });
186
+ it('addReader encrypts the secret for the new reader', async () => {
187
+ const v = value();
188
+ const eim = {};
189
+ const decrypt = sinon.stub(v, 'decryptSecret').resolves(secret);
190
+ const encrypt = sinon.stub(EcRsaOaepAsyncWorker, 'encrypt').resolves('sC');
191
+ const pkC = EcPk.fromPem(PK_C);
192
+ const r = await v.addReader(pkC, eim);
193
+ assert.isUndefined(r);
194
+ assert.strictEqual(decrypt.firstCall.args[0], eim);
195
+ assert.strictEqual(encrypt.firstCall.args[0], pkC);
196
+ assert.equal(encrypt.firstCall.args[1], 'JSON');
197
+ assert.deepEqual(v.reader, [PK_B, PK_C]);
198
+ assert.deepEqual(v.secret, ['sA', 'sB', 'sC']);
199
+ await v.addReader(pkC, eim);
200
+ assert.deepEqual(v.secret, ['sA', 'sB', 'sC']);
201
+ });
202
+ it('addReader rejects when encryption fails', async () => {
203
+ const v = value();
204
+ sinon.stub(v, 'decryptSecret').resolves(secret);
205
+ sinon.stub(EcRsaOaepAsyncWorker, 'encrypt').rejects(new Error('rsa'));
206
+ const e = await v.addReader(EcPk.fromPem(PK_C)).catch((x) => x);
207
+ assert.equal(e.message, 'rsa');
208
+ });
209
+ it('removeReader re-encrypts the secret for the remaining owners and readers', async () => {
210
+ const v = value();
211
+ v.reader = [PK_B, PK_C];
212
+ sinon.stub(v, 'decryptSecret').resolves(secret);
213
+ sinon.stub(EcRsaOaepAsyncWorker, 'encrypt').callsFake(async (pk) => 'for ' + pk.toPem().length + pk.toPem().slice(-30));
214
+ const r = await v.removeReader(EcPk.fromPem(PK_B));
215
+ assert.isUndefined(r);
216
+ assert.deepEqual(v.reader, [PK_C]);
217
+ assert.deepEqual(v.secret, [PK_A, PK_C].map((p) => 'for ' + p.length + p.slice(-30)));
218
+ });
219
+ it('removeReader with no readers re-encrypts for owners only', async () => {
220
+ const v = value();
221
+ v.reader = null;
222
+ sinon.stub(v, 'decryptSecret').resolves(secret);
223
+ sinon.stub(EcRsaOaepAsyncWorker, 'encrypt').resolves('x');
224
+ await v.removeReader(EcPk.fromPem(PK_B));
225
+ assert.isNull(v.reader);
226
+ assert.deepEqual(v.secret, ['x']);
227
+ });
228
+ it('removeReader rejects when the secret is unknown', async () => {
229
+ const v = value();
230
+ sinon.stub(v, 'decryptSecret').resolves(null);
231
+ const e = await v.removeReader(EcPk.fromPem(PK_B)).catch((x) => x);
232
+ assert.instanceOf(e, TypeError);
233
+ assert.deepEqual(v.secret, []);
234
+ });
235
+ });
236
+ });
@@ -0,0 +1,259 @@
1
+ require("../../index.js");
2
+ const { expect } = require("chai");
3
+ const sinon = require("sinon");
4
+ const EcArray = require("../com/eduworks/ec/array/EcArray.js");
5
+ const EcObject = require("../com/eduworks/ec/array/EcObject.js");
6
+ const EcAsyncHelper = require("../com/eduworks/ec/array/EcAsyncHelper.js");
7
+ const helpers = require("../com/eduworks/ec/promises/helpers.js");
8
+ const EcDirectedGraph = require("../com/eduworks/ec/graph/EcDirectedGraph.js");
9
+ const EcFrameworkGraph = require("../com/eduworks/ec/graph/EcFrameworkGraph.js");
10
+ const EcAsyncTask = require("../com/eduworks/ec/task/EcAsyncTask.js");
11
+ const EcAsyncTaskManager = require("../com/eduworks/ec/task/EcAsyncTaskManager.js");
12
+ const EcAsyncTaskWorker = require("../com/eduworks/ec/task/EcAsyncTaskWorker.js");
13
+ const Task = require("../com/eduworks/ec/task/Task.js");
14
+
15
+ describe("Helper classes (pinned behaviour)", () => {
16
+ describe("EcArray.isArray / EcObject.isObject type tags", () => {
17
+ it("classifies values by their Object.prototype.toString tag", () => {
18
+ expect(EcArray.isArray([])).to.equal(true);
19
+ expect(EcArray.isArray(new Array(3))).to.equal(true);
20
+ expect(EcArray.isArray({ length: 0 })).to.equal(false);
21
+ expect(EcArray.isArray(null)).to.equal(false);
22
+ expect(EcArray.isArray(undefined)).to.equal(false);
23
+ expect(EcArray.isArray("[]")).to.equal(false);
24
+ expect(EcObject.isObject({})).to.equal(true);
25
+ expect(EcObject.isObject([])).to.equal(false);
26
+ expect(EcObject.isObject(null)).to.equal(false);
27
+ expect(EcObject.isObject(undefined)).to.equal(false);
28
+ expect(EcObject.isObject("x")).to.equal(false);
29
+ expect(EcObject.isObject(new Date())).to.equal(true);
30
+ });
31
+ });
32
+
33
+ describe("EcAsyncHelper.eachSet", () => {
34
+ it("replaces values and drops falsy results before calling after", (done) => {
35
+ let arr = [1, 2, 3, 4, 5];
36
+ let results = { 1: "a", 2: 0, 3: null, 4: "", 5: "e" };
37
+ new EcAsyncHelper().eachSet(arr, (item, cb) => cb(results[item]), (out) => {
38
+ try {
39
+ expect(out).to.deep.equal(["a", "e"]);
40
+ expect(arr).to.deep.equal(["a", 0, null, "", "e"]);
41
+ done();
42
+ } catch (e) {
43
+ done(e);
44
+ }
45
+ });
46
+ });
47
+ it("calls after immediately with the same array when empty", () => {
48
+ let arr = [];
49
+ let got = null;
50
+ new EcAsyncHelper().eachSet(arr, () => { }, (out) => got = out);
51
+ expect(got).to.equal(arr);
52
+ });
53
+ });
54
+
55
+ describe("promises/helpers", () => {
56
+ it("cassReturnNullAsPromise resolves null", async () => {
57
+ expect(await helpers.cassReturnNullAsPromise()).to.equal(null);
58
+ });
59
+ it("cassReturnNullAsPromise passes null to success and returns its result", async () => {
60
+ let success = sinon.stub().returns("ok");
61
+ let failure = sinon.spy();
62
+ expect(await helpers.cassReturnNullAsPromise(success, failure)).to.equal("ok");
63
+ expect(success.calledOnceWithExactly(null)).to.equal(true);
64
+ expect(failure.called).to.equal(false);
65
+ });
66
+ it("cassReturnNullAsPromise with only failure resolves null", async () => {
67
+ let failure = sinon.spy();
68
+ expect(await helpers.cassReturnNullAsPromise(null, failure)).to.equal(null);
69
+ expect(failure.called).to.equal(false);
70
+ });
71
+ it("cassReturnNullAsPromise resolves asynchronously", () => {
72
+ let success = sinon.spy();
73
+ helpers.cassReturnNullAsPromise(success);
74
+ expect(success.called).to.equal(false);
75
+ });
76
+ });
77
+
78
+ describe("EcDirectedGraph.getIncidentVertices", () => {
79
+ it("returns source then destination, de-duplicated", () => {
80
+ let graph = new EcDirectedGraph();
81
+ let v1 = { equals: (v) => v === v1 };
82
+ let v2 = { equals: (v) => v === v2 };
83
+ let e1 = { equals: (e) => e === e1 };
84
+ let e2 = { equals: (e) => e === e2 };
85
+ graph.addEdge(e1, v2, v1);
86
+ graph.addEdge(e2, v1, v1);
87
+ expect(graph.getIncidentVertices(e1)).to.deep.equal([v2, v1]);
88
+ expect(graph.getIncidentVertices(e2)).to.deep.equal([v1]);
89
+ expect(graph.getIncidentVertices({ equals: () => false })).to.deep.equal([]);
90
+ });
91
+ });
92
+
93
+ describe("EcFrameworkGraph.processAssertionsBooleanPerAssertion", () => {
94
+ let graph, msc, comp;
95
+ let mkAssertion = (id, registration) => ({ shortId: () => id, registration });
96
+ beforeEach(() => {
97
+ graph = new EcFrameworkGraph();
98
+ msc = {};
99
+ comp = { shortId: () => "c1", equals: (o) => o === comp };
100
+ sinon.stub(graph, "getMetaStateCompetency").returns(msc);
101
+ sinon.stub(graph, "getOutEdges").returns([]);
102
+ sinon.stub(graph, "getInEdges").returns([]);
103
+ });
104
+ afterEach(() => sinon.restore());
105
+
106
+ it("returns early if the competency was already visited", async () => {
107
+ let visited = [comp];
108
+ await graph.processAssertionsBooleanPerAssertion([mkAssertion("a")], true, comp, visited);
109
+ expect(visited).to.deep.equal([comp]);
110
+ expect(graph.getMetaStateCompetency.called).to.equal(false);
111
+ });
112
+ for (let negative of [true, false]) {
113
+ let field = negative ? "negativeAssertion" : "positiveAssertion";
114
+ it(`adds assertions to ${field} on the first visit, regardless of registration`, async () => {
115
+ msc.positiveAssertion = [mkAssertion("p", "r1")];
116
+ msc.negativeAssertion = [mkAssertion("n", "r2")];
117
+ let visited = [];
118
+ await graph.processAssertionsBooleanPerAssertion(
119
+ [mkAssertion("a", "r1"), mkAssertion("b", "r2"), mkAssertion("a")],
120
+ negative, comp, visited);
121
+ expect(visited).to.deep.equal([comp]);
122
+ expect(msc[field].map((a) => a.shortId())).to.deep.equal(negative ? ["n", "a", "b"] : ["p", "a", "b"]);
123
+ });
124
+ it(`skips ${field} assertions whose registration is already present when propagated`, async () => {
125
+ msc.positiveAssertion = [mkAssertion("p", "r1")];
126
+ msc.negativeAssertion = [mkAssertion("n", "r2")];
127
+ let visited = [{ equals: () => false }];
128
+ await graph.processAssertionsBooleanPerAssertion(
129
+ [mkAssertion("a", "r1"), mkAssertion("b", "r2"), mkAssertion("c", "r3"), mkAssertion("d", "r3"), mkAssertion("e")],
130
+ negative, comp, visited);
131
+ // "d" is skipped because "c" (same registration) was added to the array earlier in the loop.
132
+ let expected = negative ? ["n", "c", "e"] : ["p", "c", "e"];
133
+ expect(msc[field].map((a) => a.shortId())).to.deep.equal(expected);
134
+ });
135
+ it(`tolerates a null meta state (${field})`, async () => {
136
+ graph.getMetaStateCompetency.returns(null);
137
+ await graph.processAssertionsBooleanPerAssertion([mkAssertion("a", "r1")], negative, comp, [{ equals: () => false }]);
138
+ });
139
+ }
140
+ it("negative: propagates outward over out-edges, then inward over in-edges", async () => {
141
+ let calls = [];
142
+ let target = { id: "t" }, source = { id: "s" };
143
+ graph.getOutEdges.returns([{ target: "T" }]);
144
+ graph.getInEdges.returns([{ source: "S" }]);
145
+ sinon.stub(graph, "getCompetency").callsFake(async (id) => id == "T" ? target : source);
146
+ sinon.stub(graph, "processAssertionBooleanOutward").callsFake(async (al, c) => calls.push(["out", al, c]));
147
+ sinon.stub(graph, "processAssertionBooleanInward").callsFake(async (al, c) => calls.push(["in", al, c]));
148
+ await graph.processAssertionsBooleanPerAssertion([], true, comp, []);
149
+ expect(calls).to.deep.equal([["out", { target: "T" }, target], ["in", { source: "S" }, source]]);
150
+ });
151
+ it("positive: in-edges outward first, then out-edges outward for IMPLIES and inward otherwise", async () => {
152
+ let calls = [];
153
+ graph.getInEdges.returns([{ source: "S" }]);
154
+ graph.getOutEdges.returns([{ target: "T1", relationType: Relation.IMPLIES }, { target: "T2", relationType: Relation.NARROWS }]);
155
+ sinon.stub(graph, "getCompetency").callsFake(async (id) => ({ id }));
156
+ sinon.stub(graph, "processAssertionBooleanOutward").callsFake(async (al, c, as, neg) => calls.push(["out", c.id, neg]));
157
+ sinon.stub(graph, "processAssertionBooleanInward").callsFake(async (al, c, as, neg) => calls.push(["in", c.id, neg]));
158
+ await graph.processAssertionsBooleanPerAssertion([], false, comp, []);
159
+ expect(calls).to.deep.equal([["out", "S", false], ["out", "T1", false], ["in", "T2", false]]);
160
+ });
161
+ });
162
+
163
+ describe("EcAsyncTask", () => {
164
+ for (let n = 0; n <= 4; n++) {
165
+ it(`invoke${n} passes ${n} arguments plus a completion callback, with this = task`, () => {
166
+ let seen = null;
167
+ let task = new EcAsyncTask(function (...args) {
168
+ seen = { self: this, args };
169
+ return "ret" + n;
170
+ });
171
+ let args = [1, 2, 3, 4].slice(0, n);
172
+ expect(task.doTask(...args)).to.equal("ret" + n);
173
+ expect(seen.self).to.equal(task);
174
+ expect(seen.args.slice(0, n)).to.deep.equal(args);
175
+ expect(seen.args.length).to.equal(n + 1);
176
+ expect(task.getIsStarted()).to.equal(true);
177
+ expect(task.getIsComplete()).to.equal(false);
178
+ seen.args[n].call({});
179
+ expect(task.getIsComplete()).to.equal(true);
180
+ expect(task.stop).to.be.instanceOf(Date);
181
+ });
182
+ }
183
+ it("doTask runs only once and returns undefined for more than 4 arguments", () => {
184
+ let job = sinon.stub().returns(1);
185
+ let task = new EcAsyncTask(job);
186
+ expect(task.doTask(1, 2, 3, 4, 5)).to.equal(undefined);
187
+ expect(job.called).to.equal(false);
188
+ expect(task.doTask()).to.equal(undefined);
189
+ });
190
+ it("constructor collects non-null params (param3 is guarded by param2)", () => {
191
+ expect(new EcAsyncTask(null, 1, 2, 3, 4).getArguments()).to.deep.equal([1, 2, 3, 4]);
192
+ expect(new EcAsyncTask(null, 1, null, 3, null).getArguments()).to.deep.equal([1]);
193
+ });
194
+ });
195
+
196
+ describe("EcAsyncTaskManager / EcAsyncTaskWorker", () => {
197
+ let clock, savedLastFrame, savedHandle;
198
+ beforeEach(() => {
199
+ savedLastFrame = Task.lastFrame;
200
+ savedHandle = Task.updateFrameHandle;
201
+ clock = sinon.useFakeTimers({ now: 1000000 });
202
+ });
203
+ afterEach(() => {
204
+ clock.restore();
205
+ Task.lastFrame = savedLastFrame;
206
+ Task.updateFrameHandle = savedHandle;
207
+ });
208
+ it("runs assigned tasks on the worker interval and cleans them up", () => {
209
+ let ran = [];
210
+ let mgr = new EcAsyncTaskManager(1);
211
+ expect(mgr.workerList.length).to.equal(1);
212
+ let t1 = new EcAsyncTask(function (a, done) { ran.push(a); done(); }, "x");
213
+ let t2 = new EcAsyncTask(function (a, b, done) { ran.push(a + b); done(); }, "y", "z");
214
+ let ret = mgr.addTasks([t1, t2]);
215
+ expect(Object.values(ret)).to.deep.equal([t1, t2]);
216
+ expect(mgr.workerList[0].currentTask).to.equal(t1);
217
+ expect(mgr.workerList[0].assignedList).to.deep.equal([t2]);
218
+ Task.lastFrame = Date.now();
219
+ clock.tick(100);
220
+ expect(ran).to.deep.equal(["x"]);
221
+ expect(mgr.workerList[0].currentTask).to.equal(t2);
222
+ clock.tick(100);
223
+ expect(ran).to.deep.equal(["x", "yz"]);
224
+ clock.tick(2500);
225
+ expect(Object.keys(mgr.taskMap)).to.deep.equal([]);
226
+ });
227
+ it("cleanupTasks completes started tasks that exceeded their timeout", () => {
228
+ let mgr = new EcAsyncTaskManager(0);
229
+ let task = new EcAsyncTask(function () { });
230
+ task.setTimeout(5);
231
+ mgr.addTask(task);
232
+ task.doTask();
233
+ expect(task.getIsComplete()).to.equal(false);
234
+ clock.tick(5000);
235
+ mgr.cleanupTasks();
236
+ expect(Object.keys(mgr.taskMap).length).to.equal(1);
237
+ expect(task.getIsComplete()).to.equal(false);
238
+ clock.tick(1);
239
+ mgr.cleanupTasks();
240
+ expect(task.getIsComplete()).to.equal(true);
241
+ expect(Object.keys(mgr.taskMap).length).to.equal(0);
242
+ });
243
+ it("the manager's interval calls cleanupTasks on itself", () => {
244
+ let mgr = new EcAsyncTaskManager(0);
245
+ let spy = sinon.spy(mgr, "cleanupTasks");
246
+ clock.tick(2500);
247
+ expect(spy.calledOnce).to.equal(true);
248
+ expect(spy.firstCall.thisValue).to.equal(mgr);
249
+ });
250
+ it("the worker's interval calls run on itself", () => {
251
+ let worker = new EcAsyncTaskWorker();
252
+ let spy = sinon.spy(worker, "run");
253
+ clock.tick(100);
254
+ expect(spy.calledOnce).to.equal(true);
255
+ expect(spy.firstCall.thisValue).to.equal(worker);
256
+ clearInterval(worker.intervalHandler);
257
+ });
258
+ });
259
+ });