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,439 @@
1
+ require('../../index.js');
2
+ const chai = require('chai');
3
+ const sinon = require('sinon');
4
+ const EcIdentityManager = require('../org/cassproject/ebac/identity/EcIdentityManager.js');
5
+ const EcIdentity = require('../org/cassproject/ebac/identity/EcIdentity.js');
6
+ const EcContact = require('../org/cassproject/ebac/identity/EcContact.js');
7
+ const EcContactGrant = require('../org/cassproject/ebac/identity/EcContactGrant.js');
8
+ const EcPpk = require('../com/eduworks/ec/crypto/EcPpk.js');
9
+ const EcPk = require('../com/eduworks/ec/crypto/EcPk.js');
10
+ const EcPpkFacade = require('../com/eduworks/ec/crypto/EcPpkFacade.js');
11
+ const EcRsaOaep = require('../com/eduworks/ec/crypto/EcRsaOaep.js');
12
+ const EcRsaOaepAsyncWorker = require('../com/eduworks/ec/crypto/EcRsaOaepAsyncWorker.js');
13
+
14
+ const assert = chai.assert;
15
+ const isNode = typeof process !== 'undefined' && process.versions?.node != null && typeof window === 'undefined';
16
+
17
+ const PEM_A = "-----BEGIN RSA PRIVATE KEY-----MIIEowIBAAKCAQEAzzy0+Hau1QfZoZo/cUUaEdqWHoN3f+BezVUM8DZpxWrrnsKhKSV+ieEHZRduVM4LfxeByWhbzjkAvE8IWbQIGOA60UhZSjgjuSpNtlHRgmommkBtkvMN65TQwfuD6Z1GnPrE+T2PciKOPiiD5/5836zadSn4tMrM52jMZbUS/zi8P61y9TZejXI1KCgKYoi7po91eXmklTN192nG7iiOoUxz2TQFHG8VxqRi2UcBS+WuFEvLOayvCH5aZpZvN9GlR3EdbrCcZkDLFF9gict9Z3LbSWAe+0/RejgCe24MpfQsWnPNl/9ZSiFG7mbeH6G1hZl0Hahr+xBKrqdNMGfk8wIDAQABAoIBAGDPFB8pmhyMMT9Lxi1MwZPrj8srUOyczMyQ2Z1xXdHKU81zUEPfbxBL3d98pZ7aLgat3YRPyus4N77AEL+O2WV9ghx39rY0KYP7Xhg4em241tWiGU3XWv5dI3Qq4YGFZhI5F0DzYApKWDZzSMNcX8tBfT/UXoleX4AOtcUhZIFykUd0bgcIA93lHaLnL5r+CyuUcYaLmzR7imbGO18SAEuTIRcOExmcX1NLKrCQwfreYEVb87GAddfWefCw2SkwkyMXeQSJzPKEzSHTMGZr51eUkDjhFIiDOcE1jIDabN9auVIGH18Pu/1d3OX+41hQ6Kz8bmuACo7Yr6HjRxfcwkECgYEA6y2W/7Qqny75uMpAgJyd/BGeNcXNv9p5uOo+iUAzEcUeEQ1hmvkpW9AsZf+MS3BIJXXScafZF9zn1fOURaw9aE0se2fhM/odiFE/FdgtWYwGxVJalK85g33v1lphc5Zp88SyOt8O+hFah+viPIHwVT+W+KE/s1FtrawyiD+9KOECgYEA4ZXS36EGCYXyiCguPgVjCHvvZEB6BElZNaLO9Sd0Vbz82k2Y80JTFtuqJqVFdH70OpRar7GM4zxxYkw8a8VyGiUIwJkAx+vRYkPBOSmCIS+EziYsBK+DJkEffntzUWSAFtA16L1pZL1joVFfav6Hs0qbnFcra27VCj6t7FLUJFMCgYA8hfUQRWuan9pAhjxnXOcDkf0nVYqHz0/4GMCTTeRWtGXyeYEfAvRz3nUuor+WQoUurs3D1jB2hbFcTF1iONts1j+WWruIRtwG0vHrS0EQhIrRiUoqbZXdkzF/9q6Rsqn9xCciI3mVPZYQ2NHWRJJ3PfA6eDUOjuws/93ZVp31wQKBgCnswmg1cP3MGysa7J0yOxmUjwU+xm6NgzroqzX8wy48FXItAcQUC4UwBgtwo9vmzFUmxUPp55lD3n66VHBjHbWmNF43EWIWfa+ZkAujnz+zqSb3CS9Fv0YYzFWhc+komHWk9J/ws+m5GQ8eJHGqJGY583m26abpzkY9J+3H5w1pAoGBAIuF5suYbI2fmXnRlJWgVTy7UHub4exvssSxSoHZzjGH8gAlcqduTKjSq7A5JqAWwMPBSN7uoXgITWzVenV86WOTG6+YuWFpjmCdrQ2wNuj6di02KEBGSQzUa1xclumnArDywr6A5NKc8ZdmXuD3BnsBFgF75rrE/WRCKwvMdE55-----END RSA PRIVATE KEY-----";
18
+ const PEM_B = "-----BEGIN RSA PRIVATE KEY-----MIIEogIBAAKCAQEAr8oXR8sAu5zJvWk0BRbL/yFfz7zTv76nlwk498FqfyVtfnL13CAiwKWqDx6aNvlYXEVFvjMIclMkIFVqHFJRFBiVEQpuP//Ltd4WKzsRWlWWAGvNUfrgKMip62fZHQstC5MJUSDyCd3ljZe/1HjLFrvDsvjJ3o+GPOaJaHtlkz+Y0K7GrbBM4AzduzowNgcsHbmJ154QTTN6uw4ke1BAf3dRZ7ysyDKonJEtQGVGWvmYOOm5gwf5CHIqx7zp+zDROxzXvFHu0Wevh2WSzPO8u5cW3INe7kjgsG/mQm9QUW3m0+HT/wMh4WOTaR5yBwYMt7akC9Xs9zl2Vus80QQ4+QIDAQABAoIBABoX5VzCxVkT7N4wv4gB2kYErjQn00ajBj1lIQ+lpik5ZQdgCR6jpoTyEkwsPrfqSzNnFPMmoBpYPQSPqTCLB330KCxfX/4f+h0E2kWQfwLpsVpephuFIcDsCEzpHVEyV31gVgG04jiUt35NCCyBwaOeO25Vf6GJrGNd1QetSWaRymB6tLHGyJAQoLEl40XHizRDMDcOiN5SBH9rgP4e1tCNxPhIqHl9Pmzif/39TXBYInY990zLUPGd2VN+2kZ4G9XFxchvloze+eyfv14ghZ3AeUXytd0esTj8sEW5Y0zKLDLyxujsrHGRVFIl3yTpJJxRHik64uwz2AHbrzSCYAECgYEA97obu9d9A1goQvzvTpdIjfEGVS6eNe1ruEUtVGgdP0Xucy10sZZPhGjfL2ww2Ls1AMxJChNpbGkqb0X8ObQk2GOty7wavpplr+1ACvSVHCj8qJ81AJquxSNFF4l9ymTOr1mgatM3swxAs8TKOoDQ+tYb0cnJMinAfDFrdXXaKwUCgYEAtaj3iaqHpY+msJdh7EKrpQWVnTuPA0yL5V64YYGE+uzITTnsK/r//Fs/9+va3UPU8E8kihmn3zquLms4v0udmHnq9AxI5jPqPZzW5jeHyrkuCQSR2Nt7ahYQdWVSILWH0oAtXKcRm4KrI8LMLtHHWHl0OtPqucYi+Lu3wDFwQGUCgYAry2kPwj7FkpY7kX+1AyW+gje+Y2lw2zJJ92DWJTnWb8SB9C1HH+2a2Pdr6NKR3ja2l2bqMxhWJBluZo76nJBO+QBGnlCUFbOYcHb7+NVLjytrnAnAu5feHigeyx3D5CJt+YhUmlakfqwJm+ashQ1s6uEsK2I4Tg1LZ2k58MXLCQKBgAd+C4+a//M9JIPeCoTjDw9EVrJgdXqOWqrAKFylKeXrhCtjOzFpLcuru8yu5pz/ojWzHe+wNTNIyUxS0claBgr25FDifNM5yux1notqOOiMkLCAhr9p9gIfHNiqsCN3OT3eWYCbx2chkYttClzX/B/TCo8ulFzLJkAaBe8gWt/dAoGAaA+XFXbS0PWNVfeYD4WAip6UgY1OxoM/BeQsTQr4IyDv1FCeRY+qE5tYwacfz8FNZr6TbPsFhj2K/RD6dhjgHw+qo+wOsRErQJ6neXmF7gvjFrDgvrsvzQrJLVinZzlZ0tfDlCD6EmjonBJDuUKVEU/KznAfX7dDqMReiWz2PYY=-----END RSA PRIVATE KEY-----";
19
+
20
+ const ppkA = EcPpk.fromPem(PEM_A);
21
+ const ppkB = EcPpk.fromPem(PEM_B);
22
+ const pkA = ppkA.toPk();
23
+ const pkB = ppkB.toPk();
24
+
25
+ function identity(ppk, name) {
26
+ const i = new EcIdentity();
27
+ i.ppk = ppk;
28
+ i.displayName = name;
29
+ i.source = 'src-' + name;
30
+ return i;
31
+ }
32
+ function contact(pk, name) {
33
+ const c = new EcContact();
34
+ c.pk = pk;
35
+ c.displayName = name;
36
+ c.source = 'src-' + name;
37
+ return c;
38
+ }
39
+
40
+ // readContacts/saveContacts use localStorage without a guard; give Node a plain object for it.
41
+ let createdStorage = false;
42
+ let savedStorage = null;
43
+ function useStorage() {
44
+ if (typeof localStorage === 'undefined') {
45
+ global.localStorage = {};
46
+ createdStorage = true;
47
+ }
48
+ savedStorage = { contacts: localStorage["contacts"], identities: localStorage["identities"] };
49
+ return localStorage;
50
+ }
51
+ function restoreStorage() {
52
+ if (savedStorage == null) return;
53
+ if (createdStorage) {
54
+ savedStorage = null;
55
+ delete global.localStorage;
56
+ createdStorage = false;
57
+ return;
58
+ }
59
+ for (const k of ['contacts', 'identities']) {
60
+ if (savedStorage[k] === undefined) delete localStorage[k];
61
+ else localStorage[k] = savedStorage[k];
62
+ }
63
+ savedStorage = null;
64
+ }
65
+
66
+ describe('EcIdentityManager key handling', () => {
67
+ afterEach(() => {
68
+ sinon.restore();
69
+ restoreStorage();
70
+ });
71
+
72
+ it('starts empty', () => {
73
+ const eim = new EcIdentityManager();
74
+ assert.deepEqual(eim.ids, []);
75
+ assert.deepEqual(eim.contacts, []);
76
+ assert.isString(eim.eimId);
77
+ assert.isFalse(eim.signatureSheetCaching);
78
+ assert.notEqual(new EcIdentityManager().eimId, eim.eimId);
79
+ });
80
+
81
+ describe('local storage', () => {
82
+ it('readContacts does nothing without stored contacts', () => {
83
+ const store = useStorage();
84
+ delete store["contacts"];
85
+ const eim = new EcIdentityManager();
86
+ eim.readContacts();
87
+ assert.deepEqual(eim.contacts, []);
88
+ });
89
+ it('readContacts adds stored contacts that are not already known', () => {
90
+ const store = useStorage();
91
+ store["contacts"] = JSON.stringify([
92
+ { displayName: 'A', pk: pkA.toPem(), source: 's1' },
93
+ { displayName: 'B', pk: pkB.toPem(), source: 's2' },
94
+ { displayName: 'B again', pk: pkB.toPem(), source: 's3' },
95
+ ]);
96
+ const eim = new EcIdentityManager();
97
+ const existing = contact(pkA, 'mine');
98
+ eim.contacts.push(existing);
99
+ eim.readContacts();
100
+ assert.equal(eim.contacts.length, 2);
101
+ assert.strictEqual(eim.contacts[0], existing);
102
+ assert.instanceOf(eim.contacts[1], EcContact);
103
+ assert.equal(eim.contacts[1].displayName, 'B');
104
+ assert.equal(eim.contacts[1].source, 's2');
105
+ assert.equal(eim.contacts[1].pk.toPem(), pkB.toPem());
106
+ });
107
+ it('saveContacts writes name, key and source', () => {
108
+ const store = useStorage();
109
+ const eim = new EcIdentityManager();
110
+ eim.contacts.push(contact(pkA, 'A'), contact(pkB, 'B'));
111
+ eim.saveContacts();
112
+ assert.deepEqual(JSON.parse(store["contacts"]), [
113
+ { displayName: 'A', pk: pkA.toPem(), source: 'src-A' },
114
+ { displayName: 'B', pk: pkB.toPem(), source: 'src-B' },
115
+ ]);
116
+ eim.contacts = [];
117
+ eim.saveContacts();
118
+ assert.equal(store["contacts"], '[]');
119
+ });
120
+ (isNode ? it : it.skip)('readIdentities and saveIdentities do nothing without localStorage', () => {
121
+ const eim = new EcIdentityManager();
122
+ eim.ids.push(identity(ppkA, 'A'));
123
+ eim.readIdentities();
124
+ eim.saveIdentities();
125
+ assert.equal(eim.ids.length, 1);
126
+ assert.equal(typeof localStorage, 'undefined');
127
+ });
128
+ it('readIdentities adds stored identities that are not already known', () => {
129
+ const store = useStorage();
130
+ delete store["identities"];
131
+ const eim = new EcIdentityManager();
132
+ eim.readIdentities();
133
+ assert.deepEqual(eim.ids, []);
134
+ store["identities"] = JSON.stringify([
135
+ { displayName: 'A', ppk: PEM_A, source: 's1' },
136
+ { displayName: 'B', ppk: PEM_B, source: 's2' },
137
+ { displayName: 'B again', ppk: PEM_B, source: 's3' },
138
+ ]);
139
+ const existing = identity(ppkA, 'mine');
140
+ eim.ids.push(existing);
141
+ eim.readIdentities();
142
+ assert.equal(eim.ids.length, 2);
143
+ assert.strictEqual(eim.ids[0], existing);
144
+ assert.instanceOf(eim.ids[1], EcIdentity);
145
+ assert.equal(eim.ids[1].displayName, 'B');
146
+ assert.equal(eim.ids[1].source, 's2');
147
+ assert.equal(eim.ids[1].ppk.toPem(), PEM_B);
148
+ });
149
+ it('saveIdentities writes name, key and source', () => {
150
+ const store = useStorage();
151
+ const eim = new EcIdentityManager();
152
+ eim.ids.push(identity(ppkA, 'A'), identity(ppkB, 'B'));
153
+ eim.saveIdentities();
154
+ assert.deepEqual(JSON.parse(store["identities"]), [
155
+ { displayName: 'A', ppk: PEM_A, source: 'src-A' },
156
+ { displayName: 'B', ppk: PEM_B, source: 'src-B' },
157
+ ]);
158
+ });
159
+ });
160
+
161
+ describe('adding identities and contacts', () => {
162
+ it('addIdentity skips duplicates and fires the hook for new ones', () => {
163
+ const eim = new EcIdentityManager();
164
+ const hook = sinon.spy();
165
+ eim.onIdentityChanged = hook;
166
+ const a = identity(ppkA, 'A');
167
+ eim.addIdentity(a);
168
+ eim.addIdentity(identity(ppkA, 'A2'));
169
+ eim.addIdentity(identity(ppkB, 'B'));
170
+ assert.equal(eim.ids.length, 2);
171
+ assert.strictEqual(eim.ids[0], a);
172
+ assert.equal(hook.callCount, 2);
173
+ assert.strictEqual(hook.firstCall.args[0], a);
174
+ });
175
+ it('addIdentityQuietly skips duplicates and fires no hook', () => {
176
+ const eim = new EcIdentityManager();
177
+ const hook = sinon.spy();
178
+ eim.onIdentityChanged = hook;
179
+ eim.addIdentityQuietly(identity(ppkA, 'A'));
180
+ eim.addIdentityQuietly(identity(ppkA, 'A2'));
181
+ eim.addIdentityQuietly(identity(ppkB, 'B'));
182
+ assert.equal(eim.ids.length, 2);
183
+ assert.equal(eim.ids[0].displayName, 'A');
184
+ assert.equal(hook.callCount, 0);
185
+ });
186
+ it('addContact renames a matching identity and contact, and adds new contacts', () => {
187
+ const store = useStorage();
188
+ const eim = new EcIdentityManager();
189
+ const idHook = sinon.spy();
190
+ const contactHook = sinon.spy();
191
+ eim.onIdentityChanged = idHook;
192
+ eim.onContactChanged = contactHook;
193
+ const id = identity(ppkA, 'me');
194
+ eim.ids.push(id);
195
+ const existing = contact(pkB, 'old');
196
+ eim.contacts.push(existing);
197
+
198
+ eim.addContact(contact(pkA, 'renamed me'));
199
+ assert.equal(id.displayName, 'renamed me');
200
+ assert.equal(idHook.callCount, 1);
201
+ assert.strictEqual(idHook.firstCall.args[0], id);
202
+ assert.equal(eim.contacts.length, 2);
203
+ assert.equal(contactHook.callCount, 1);
204
+
205
+ eim.addContact(contact(pkB, 'new name'));
206
+ assert.equal(existing.displayName, 'new name');
207
+ assert.equal(eim.contacts.length, 2);
208
+ assert.equal(contactHook.callCount, 2);
209
+ assert.strictEqual(contactHook.lastCall.args[0], existing);
210
+ assert.equal(JSON.parse(store["contacts"]).length, 2);
211
+ });
212
+ it('addContactQuietly renames matches and adds new contacts without hooks', () => {
213
+ const eim = new EcIdentityManager();
214
+ const idHook = sinon.spy();
215
+ const contactHook = sinon.spy();
216
+ eim.onIdentityChanged = idHook;
217
+ eim.onContactChanged = contactHook;
218
+ const id = identity(ppkA, 'me');
219
+ eim.ids.push(id);
220
+ const existing = contact(pkB, 'old');
221
+ eim.contacts.push(existing);
222
+ eim.addContactQuietly(contact(pkA, 'renamed me'));
223
+ assert.equal(id.displayName, 'renamed me');
224
+ assert.equal(eim.contacts.length, 2);
225
+ eim.addContactQuietly(contact(pkB, 'new name'));
226
+ assert.equal(existing.displayName, 'new name');
227
+ assert.equal(eim.contacts.length, 2);
228
+ assert.equal(idHook.callCount, 0);
229
+ assert.equal(contactHook.callCount, 0);
230
+ });
231
+ });
232
+
233
+ describe('lookups', () => {
234
+ it('getPpk, getContact and getIdentity find by key or return null', () => {
235
+ const eim = new EcIdentityManager();
236
+ const a = identity(ppkA, 'A');
237
+ const cb = contact(pkB, 'B');
238
+ eim.ids.push(a);
239
+ eim.contacts.push(cb);
240
+ assert.strictEqual(eim.getPpk(pkA), ppkA);
241
+ assert.isNull(eim.getPpk(pkB));
242
+ assert.strictEqual(eim.getContact(pkB), cb);
243
+ assert.isNull(eim.getContact(pkA));
244
+ assert.strictEqual(eim.getIdentity(pkA), a);
245
+ assert.isNull(eim.getIdentity(pkB));
246
+ });
247
+ it('myIdentitiesSearchString escapes the field prefix', () => {
248
+ const eim = new EcIdentityManager();
249
+ assert.equal(eim.myIdentitiesSearchString(), '');
250
+ eim.ids.push(identity(ppkA, 'A'), identity(ppkB, 'B'));
251
+ const a = pkA.toPem(), b = pkB.toPem();
252
+ assert.equal(eim.myIdentitiesSearchString(),
253
+ '\\*reader:"' + a + '" OR \\*owner:"' + a + '" OR \\*reader:"' + b + '" OR \\*owner:"' + b + '"');
254
+ });
255
+ it('getMyPks returns public keys, or [] when ids is null', () => {
256
+ const eim = new EcIdentityManager();
257
+ eim.ids.push(identity(ppkA, 'A'), identity(ppkB, 'B'));
258
+ assert.deepEqual(eim.getMyPks().map((x) => x.toPem()), [pkA.toPem(), pkB.toPem()]);
259
+ eim.ids = null;
260
+ assert.deepEqual(eim.getMyPks(), []);
261
+ });
262
+ });
263
+
264
+ describe('signatures', () => {
265
+ let fips;
266
+ beforeEach(() => {
267
+ fips = isNode ? process.env.FIPS : undefined;
268
+ });
269
+ afterEach(() => {
270
+ if (!isNode) return;
271
+ if (fips === undefined) delete process.env.FIPS;
272
+ else process.env.FIPS = fips;
273
+ });
274
+
275
+ it('createSignature returns null for a facade', () => {
276
+ const eim = new EcIdentityManager();
277
+ assert.isNull(eim.createSignature(1000, 'srv', new EcPpkFacade(pkA)));
278
+ });
279
+ it('createSignature signs with SHA-1 or SHA-256', async () => {
280
+ sinon.useFakeTimers({ now: 1000000, toFake: ['Date'] });
281
+ if (isNode) sinon.stub(require('crypto'), 'getFips').returns(0);
282
+ const sign = sinon.stub(EcRsaOaepAsyncWorker, 'sign').resolves('SIG1');
283
+ const sign256 = sinon.stub(EcRsaOaepAsyncWorker, 'signSha256').resolves('SIG256');
284
+ const eim = new EcIdentityManager();
285
+ let s = await eim.createSignature(5000, 'srv', ppkA);
286
+ assert.equal(sign.callCount, 1);
287
+ assert.strictEqual(sign.firstCall.args[0], ppkA);
288
+ const signed = JSON.parse(sign.firstCall.args[1]);
289
+ assert.equal(signed.expiry, 1005000);
290
+ assert.equal(signed.server, 'srv');
291
+ assert.equal(signed['@owner'], pkA.toPem());
292
+ assert.notProperty(signed, '@signature');
293
+ assert.equal(s['@signature'], 'SIG1');
294
+ assert.equal(s.expiry, 1005000);
295
+ assert.notProperty(s, 'signature');
296
+ assert.notProperty(s, 'owner');
297
+
298
+ s = await eim.createSignature(5000, 'srv', ppkA, 'SHA-256');
299
+ assert.equal(sign256.callCount, 1);
300
+ assert.equal(s['@signatureSha256'], 'SIG256');
301
+ assert.notProperty(s, '@signature');
302
+ s = await eim.createSignature(5000, 'srv', ppkA, 'SHA-512');
303
+ assert.equal(sign.callCount, 2);
304
+ });
305
+ (isNode ? it : it.skip)('createSignature forces SHA-256 when OpenSSL is in FIPS mode and FIPS was not requested', async () => {
306
+ const getFips = sinon.stub(require('crypto'), 'getFips').returns(1);
307
+ const sign = sinon.stub(EcRsaOaepAsyncWorker, 'sign').resolves('SIG1');
308
+ const sign256 = sinon.stub(EcRsaOaepAsyncWorker, 'signSha256').resolves('SIG256');
309
+ const eim = new EcIdentityManager();
310
+ delete process.env.FIPS;
311
+ assert.equal((await eim.createSignature(5000, 'srv', ppkA))['@signatureSha256'], 'SIG256');
312
+ process.env.FIPS = '1';
313
+ assert.equal((await eim.createSignature(5000, 'srv', ppkA))['@signature'], 'SIG1');
314
+ delete process.env.FIPS;
315
+ getFips.returns(0);
316
+ assert.equal((await eim.createSignature(5000, 'srv', ppkA))['@signature'], 'SIG1');
317
+ assert.equal(sign.callCount, 2);
318
+ assert.equal(sign256.callCount, 1);
319
+ });
320
+ it('signatureSheet drops empty signatures and joins the rest', async () => {
321
+ const eim = new EcIdentityManager();
322
+ eim.ids.push(identity(ppkA, 'A'), identity(ppkB, 'B'));
323
+ const create = sinon.stub(eim, 'createSignature');
324
+ create.onCall(0).resolves({ s: 'a' });
325
+ create.onCall(1).returns(null);
326
+ let seen = null;
327
+ const r = await eim.signatureSheet(1000, 'srv', (x) => { seen = x; return x; }, null, 'ALG');
328
+ assert.equal(r, '[{"s":"a"}]');
329
+ assert.equal(seen, r);
330
+ assert.deepEqual(create.firstCall.args, [1000, 'srv', ppkA, 'ALG']);
331
+ assert.deepEqual(eim.signatureSheetCache, {});
332
+ });
333
+ it('signatureSheet caches per server while the cache is fresh', async () => {
334
+ const clock = sinon.useFakeTimers({ now: 1000000, toFake: ['Date'] });
335
+ const eim = new EcIdentityManager();
336
+ eim.signatureSheetCaching = true;
337
+ eim.ids.push(identity(ppkA, 'A'));
338
+ const create = sinon.stub(eim, 'createSignature').callsFake(async (d) => ({ d }));
339
+ assert.equal(await eim.signatureSheet(1000, 'srv'), '[{"d":21000}]');
340
+ assert.deepEqual(eim.signatureSheetCache['srv'], [1021000, '[{"d":21000}]']);
341
+ assert.equal(await eim.signatureSheet(1000, 'srv'), '[{"d":21000}]');
342
+ assert.equal(create.callCount, 1);
343
+ clock.tick(20000);
344
+ assert.equal(await eim.signatureSheet(1000, 'srv'), '[{"d":21000}]');
345
+ assert.equal(create.callCount, 2);
346
+ assert.deepEqual(eim.signatureSheetCache['srv'], [1041000, '[{"d":21000}]']);
347
+ await eim.signatureSheet(1000, 'other');
348
+ assert.equal(create.callCount, 3);
349
+ });
350
+ it('signatureSheetFor signs only with keys it owns', async () => {
351
+ const eim = new EcIdentityManager();
352
+ eim.ids.push(identity(ppkA, 'A'));
353
+ const create = sinon.stub(eim, 'createSignature').callsFake(async (d, srv, ppk) => ({ d, k: ppk === ppkA }));
354
+ const r = await eim.signatureSheetFor([pkB.toPem(), pkA.toPem()], 1000, 'srv', null, null, 'ALG');
355
+ assert.equal(r, '[{"d":1000,"k":true}]');
356
+ assert.equal(create.callCount, 1);
357
+ assert.equal(create.firstCall.args[3], 'ALG');
358
+ create.resolves(null);
359
+ assert.equal(await eim.signatureSheetFor([pkA.toPem()], 1000, 'srv'), '[]');
360
+ assert.equal(await eim.signatureSheetFor([], 1000, 'srv'), '[]');
361
+ });
362
+ it('signatureSheetFor caches per server while the cache is fresh', async () => {
363
+ const clock = sinon.useFakeTimers({ now: 1000000, toFake: ['Date'] });
364
+ const eim = new EcIdentityManager();
365
+ eim.signatureSheetCaching = true;
366
+ eim.ids.push(identity(ppkA, 'A'));
367
+ const create = sinon.stub(eim, 'createSignature').callsFake(async (d) => ({ d }));
368
+ assert.equal(await eim.signatureSheetFor([pkA.toPem()], 1000, 'srv'), '[{"d":21000}]');
369
+ assert.deepEqual(eim.signatureSheetCache['srv'], [1021000, '[{"d":21000}]']);
370
+ let seen = null;
371
+ assert.equal(await eim.signatureSheetFor([pkB.toPem()], 1000, 'srv', (x) => { seen = x; return 'cb'; }), 'cb');
372
+ assert.equal(seen, '[{"d":21000}]');
373
+ assert.equal(create.callCount, 1);
374
+ clock.tick(20000);
375
+ await eim.signatureSheetFor([pkA.toPem()], 1000, 'srv');
376
+ assert.equal(create.callCount, 2);
377
+ assert.deepEqual(eim.signatureSheetCache['srv'], [1041000, '[{"d":21000}]']);
378
+ });
379
+ it('sign signs with owned owner keys and clears empty signature lists', async () => {
380
+ const eim = new EcIdentityManager();
381
+ eim.ids.push(identity(ppkA, 'A'));
382
+ const d = new EcRemoteLinkedData('http://ctx/', 'Thing');
383
+ d.owner = [pkB.toPem(), pkA.toPem()];
384
+ const signWith = sinon.stub(d, 'signWith').resolves('SIG');
385
+ assert.strictEqual(await eim.sign(d), d);
386
+ assert.deepEqual(d.signatureSha256, ['SIG']);
387
+ assert.equal(signWith.callCount, 1);
388
+ assert.strictEqual(signWith.firstCall.args[0], ppkA);
389
+
390
+ signWith.resolves(undefined);
391
+ d.signature = ['old'];
392
+ await eim.sign(d);
393
+ assert.notProperty(d, 'signatureSha256');
394
+ assert.notProperty(d, 'signature');
395
+
396
+ const e = new EcRemoteLinkedData('http://ctx/', 'Thing');
397
+ e.signature = ['old'];
398
+ delete e.owner;
399
+ await eim.sign(e);
400
+ assert.notProperty(e, 'signatureSha256');
401
+ assert.notProperty(e, 'signature');
402
+ });
403
+ });
404
+ });
405
+
406
+ describe('EcContactGrant.valid', () => {
407
+ afterEach(() => sinon.restore());
408
+ function grant(verified, invalid) {
409
+ const g = new EcContactGrant();
410
+ g.responseToken = 'token';
411
+ g.responseSignature = 'sig';
412
+ sinon.stub(g, 'verify').returns(verified);
413
+ sinon.stub(g, 'invalid').returns(invalid);
414
+ return g;
415
+ }
416
+ it('is false when the grant does not verify or is invalid', () => {
417
+ const verify = sinon.stub(EcRsaOaep, 'verify').returns(true);
418
+ const eim = new EcIdentityManager();
419
+ eim.ids.push(identity(ppkA, 'A'));
420
+ assert.isFalse(grant(false, false).valid(eim));
421
+ assert.isFalse(grant(true, true).valid(eim));
422
+ assert.equal(verify.callCount, 0);
423
+ });
424
+ it('is false with no identities', () => {
425
+ sinon.stub(EcRsaOaep, 'verify').returns(true);
426
+ assert.isFalse(grant(true, false).valid(new EcIdentityManager()));
427
+ });
428
+ it('checks the response signature against every identity', () => {
429
+ const verify = sinon.stub(EcRsaOaep, 'verify').callsFake((pk) => pk.toPem() == pkA.toPem());
430
+ const eim = new EcIdentityManager();
431
+ eim.ids.push(identity(ppkA, 'A'), identity(ppkB, 'B'));
432
+ assert.isTrue(grant(true, false).valid(eim));
433
+ assert.equal(verify.callCount, 2);
434
+ assert.deepEqual(verify.firstCall.args.slice(1), ['token', 'sig']);
435
+ assert.equal(verify.secondCall.args[0].toPem(), pkB.toPem());
436
+ eim.ids.shift();
437
+ assert.isFalse(grant(true, false).valid(eim));
438
+ });
439
+ });
@@ -0,0 +1,68 @@
1
+ require('../../index.js');
2
+ const chai = require('chai');
3
+ const EcOrganization = require('../org/schema/EcOrganization.js');
4
+ const EcPpk = require('../com/eduworks/ec/crypto/EcPpk.js');
5
+ const EcIdentity = require('../org/cassproject/ebac/identity/EcIdentity.js');
6
+ const EcIdentityManager = require('../org/cassproject/ebac/identity/EcIdentityManager.js');
7
+ const EcEncryptedValue = require('../org/cassproject/ebac/repository/EcEncryptedValue.js');
8
+
9
+ const assert = chai.assert;
10
+
11
+ describe('EcOrganization.rekeyAndSave', function () {
12
+ this.timeout(60000);
13
+ // rekeyAndSave reads the current key set without passing its eim along, so the
14
+ // owner has to be in the default identity manager.
15
+ const eim = EcIdentityManager.default;
16
+ let org, oldKey, saved, owner;
17
+ const repo = {
18
+ selectedServer: 'https://localhost/api/',
19
+ saveTo: async (o, success) => {
20
+ saved.push({ id: o.id, keySet: JSON.parse(JSON.stringify(o[EcOrganization.ORG_PPK_SET_KEY] ?? null)) });
21
+ if (success) await success();
22
+ }
23
+ };
24
+
25
+ before(async () => {
26
+ owner = new EcIdentity();
27
+ owner.ppk = await EcPpk.generateKeyAsync();
28
+ eim.addIdentity(owner);
29
+ oldKey = await EcPpk.generateKeyAsync();
30
+ org = new EcOrganization();
31
+ org.generateId(repo.selectedServer);
32
+ org.addOwner(owner.ppk.toPk());
33
+ org[EcOrganization.ORG_PPK_SET_KEY] = await EcEncryptedValue.encryptValue(
34
+ org.ppkListToPemArrayString([oldKey]), EcOrganization.ORG_PPK_SET_KEY, org.owner, org.reader);
35
+ saved = [];
36
+ await org.rekeyAndSave(null, null, repo, eim);
37
+ // Give any work the method left running a chance to finish before checking.
38
+ await new Promise((resolve) => setTimeout(resolve, 500));
39
+ });
40
+
41
+ after(() => {
42
+ eim.ids = eim.ids.filter((i) => i !== owner && i.displayName !== 'Organization Rekey New Key');
43
+ });
44
+
45
+ it('saves the organization, then the rekey request', () => {
46
+ assert.equal(saved.length, 2);
47
+ assert.equal(saved[0].id, org.id);
48
+ assert.notEqual(saved[1].id, org.id);
49
+ });
50
+
51
+ it('saves the key set encrypted', () => {
52
+ assert.isFalse(Array.isArray(saved[0].keySet), 'saved a plaintext key list');
53
+ assert.equal(saved[0].keySet['@type'] ?? saved[0].keySet.type, 'EncryptedValue');
54
+ });
55
+
56
+ it('keeps the key set encrypted afterwards', () => {
57
+ const keySet = org[EcOrganization.ORG_PPK_SET_KEY];
58
+ assert.isFalse(Array.isArray(keySet), 'key set was replaced by a plaintext key list');
59
+ assert.equal(keySet['@type'] ?? keySet.type, 'EncryptedValue');
60
+ });
61
+
62
+ it('adds the new key after the old one', async () => {
63
+ const keys = await org.getOrgKeys(eim);
64
+ assert.equal(keys.length, 2);
65
+ assert.equal(keys[0].toPem(), oldKey.toPem());
66
+ assert.isTrue(eim.ids.some((i) => i.ppk.toPem() === keys[1].toPem()), 'new key is not the one added to the identity manager');
67
+ });
68
+ });
@@ -0,0 +1,155 @@
1
+ require('../../index.js');
2
+ const chai = require('chai');
3
+ const sinon = require('sinon');
4
+ const EcRemote = require('../com/eduworks/ec/remote/EcRemote.js');
5
+
6
+ const assert = chai.assert;
7
+ const isNode = typeof process !== 'undefined' && process.versions?.node != null && typeof window === 'undefined';
8
+
9
+ const routes = {
10
+ 'GET /json': [200, 'application/json; charset=utf-8', '{"a":1}'],
11
+ 'GET /text-json': [200, 'text/plain', '{"b":2}'],
12
+ 'GET /text': [200, 'text/plain', 'hello'],
13
+ 'GET /err-json': [500, 'application/json', '{"x":1}'],
14
+ 'GET /err-text': [500, 'text/plain', 'nope'],
15
+ 'GET /err-empty': [500, 'text/plain', ''],
16
+ 'POST /post-json': [200, 'application/json', '{"posted":true}'],
17
+ 'POST /post-text': [200, 'text/plain', 'plain'],
18
+ 'POST /post-err': [400, 'text/plain', ''],
19
+ 'DELETE /del-json': [200, 'application/json', '{"deleted":1}'],
20
+ 'DELETE /del-text-json': [200, 'text/plain', '[1,2]'],
21
+ 'DELETE /del-text': [200, 'text/plain', 'gone'],
22
+ 'DELETE /del-err': [500, 'application/json', '{"z":1}'],
23
+ };
24
+
25
+ // DNS lookups for the log lines finish after the request, so find the call for this URL.
26
+ async function callFor(report, url) {
27
+ let found = null;
28
+ await waitFor(() => (found = report.getCalls().find((c) => c.args.includes(url))) != null);
29
+ return found.args;
30
+ }
31
+
32
+ async function waitFor(fn) {
33
+ for (let i = 0; i < 200; i++) {
34
+ if (fn()) return;
35
+ await new Promise((r) => setTimeout(r, 10));
36
+ }
37
+ throw new Error("timed out waiting");
38
+ }
39
+
40
+ (isNode ? describe : describe.skip)('EcRemote responses', () => {
41
+ let server, base, closedBase;
42
+ before(async () => {
43
+ const http = require('http');
44
+ server = http.createServer((req, res) => {
45
+ const r = routes[req.method + ' ' + req.url];
46
+ req.resume();
47
+ if (r == null) { res.writeHead(404); res.end(); return; }
48
+ res.writeHead(r[0], { 'content-type': r[1] });
49
+ res.end(r[2]);
50
+ });
51
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
52
+ base = 'http://127.0.0.1:' + server.address().port + '/';
53
+ const probe = http.createServer();
54
+ await new Promise((resolve) => probe.listen(0, '127.0.0.1', resolve));
55
+ closedBase = 'http://127.0.0.1:' + probe.address().port + '/';
56
+ await new Promise((resolve) => probe.close(resolve));
57
+ });
58
+ after(() => server.close());
59
+ afterEach(() => sinon.restore());
60
+
61
+ it('GET parses JSON and JSON-looking text, and returns other text as is', async () => {
62
+ assert.deepEqual(await EcRemote.getExpectingObject(base, 'json'), { a: 1 });
63
+ assert.deepEqual(await EcRemote.getExpectingString(base, 'text-json'), { b: 2 });
64
+ assert.strictEqual(await EcRemote.getExpectingString(base, 'text'), 'hello');
65
+ });
66
+ it('GET logs the resolved address on success', async () => {
67
+ const report = sinon.stub(global.auditLogger, 'report');
68
+ await EcRemote.getExpectingString(base, 'text');
69
+ const args = await callFor(report, base + 'text');
70
+ assert.equal(args[1], global.auditLogger.Severity.INFO);
71
+ assert.equal(args[2], 'EcRemoteGetExpectString');
72
+ assert.equal(args[3], '127.0.0.1');
73
+ assert.equal(args[4], base + 'text');
74
+ });
75
+ it('GET errors use the body, or the status text when there is none', async () => {
76
+ const report = sinon.stub(global.auditLogger, 'report');
77
+ let e = await EcRemote.getExpectingString(base, 'err-json').catch((x) => x);
78
+ assert.instanceOf(e, Error);
79
+ assert.equal(e.message, '[object Object]');
80
+ e = await EcRemote.getExpectingString(base, 'err-text').catch((x) => x);
81
+ assert.equal(e.message, 'nope');
82
+ e = await EcRemote.getExpectingString(base, 'err-empty').catch((x) => x);
83
+ assert.equal(e.message, 'Internal Server Error');
84
+ await callFor(report, base + 'err-empty');
85
+ });
86
+ it('GET failures are logged with the resolved address and rethrown', async () => {
87
+ const report = sinon.stub(global.auditLogger, 'report');
88
+ let failed = null;
89
+ const r = await EcRemote.getExpectingString(closedBase, 'x', null, (e) => { failed = e; return 'handled'; });
90
+ assert.equal(r, 'handled');
91
+ assert.instanceOf(failed, Error);
92
+ const args = await callFor(report, closedBase + 'x');
93
+ assert.equal(args[1], global.auditLogger.Severity.INFO);
94
+ assert.deepEqual(args.slice(2), ['EcRemoteGetExpectString', '127.0.0.1', closedBase + 'x', failed]);
95
+ });
96
+ it('POST parses JSON and returns text', async () => {
97
+ const report = sinon.stub(global.auditLogger, 'report');
98
+ const fd = new EcRemote.FormData();
99
+ fd.append('a', 'b');
100
+ assert.deepEqual(await EcRemote.postExpectingObject(base, 'post-json', fd), { posted: true });
101
+ assert.strictEqual(await EcRemote.postExpectingString(base, 'post-text', fd), 'plain');
102
+ const e = await EcRemote.postExpectingString(base, 'post-err', fd).catch((x) => x);
103
+ assert.equal(e.message, 'Bad Request');
104
+ await callFor(report, base + 'post-err');
105
+ });
106
+ it('POST failures are logged with the resolved address and rethrown', async () => {
107
+ const report = sinon.stub(global.auditLogger, 'report');
108
+ const e = await EcRemote.postExpectingString(closedBase, 'x', new EcRemote.FormData()).catch((x) => x);
109
+ assert.instanceOf(e, Error);
110
+ const args = await callFor(report, closedBase + 'x');
111
+ assert.equal(args[1], global.auditLogger.Severity.ERROR);
112
+ assert.deepEqual(args.slice(2), ['EcRemotePostInner', '127.0.0.1', closedBase + 'x', null, e]);
113
+ });
114
+ it('DELETE parses JSON and JSON-looking text, and returns other text as is', async () => {
115
+ assert.deepEqual(await EcRemote._delete(base + 'del-json', 'sheet'), { deleted: 1 });
116
+ assert.deepEqual(await EcRemote._delete(base + 'del-text-json', 'sheet'), [1, 2]);
117
+ assert.strictEqual(await EcRemote._delete(base + 'del-text', 'sheet'), 'gone');
118
+ });
119
+ it('DELETE errors use the status text and are logged with the resolved address', async () => {
120
+ const report = sinon.stub(global.auditLogger, 'report');
121
+ let e = await EcRemote._delete(base + 'del-err', 'sheet').catch((x) => x);
122
+ assert.equal(e.message, 'Internal Server Error');
123
+ assert.deepEqual((await callFor(report, base + 'del-err')).slice(2), ['EcRemoteDelete', '127.0.0.1', base + 'del-err', 'sheet', e]);
124
+ e = await EcRemote._delete(closedBase + 'x', 'sheet').catch((x) => x);
125
+ assert.instanceOf(e, Error);
126
+ const args = await callFor(report, closedBase + 'x');
127
+ assert.equal(args[1], global.auditLogger.Severity.INFO);
128
+ assert.deepEqual(args.slice(2), ['EcRemoteDelete', '127.0.0.1', closedBase + 'x', 'sheet', e]);
129
+ });
130
+ });
131
+
132
+ (isNode ? describe : describe.skip)('EcRemote.upgradeHttpToHttps', () => {
133
+ function withWindow(protocol, fn) {
134
+ global.window = { location: { protocol } };
135
+ try {
136
+ return fn();
137
+ } finally {
138
+ delete global.window;
139
+ }
140
+ }
141
+ it('leaves URLs alone without a window', () => {
142
+ assert.equal(EcRemote.upgradeHttpToHttps('http://a/b'), 'http://a/b');
143
+ });
144
+ it('upgrades http URLs on an https page', () => {
145
+ assert.equal(withWindow('https:', () => EcRemote.upgradeHttpToHttps('http://a/http:')), 'https://a/http:');
146
+ assert.equal(withWindow('https:', () => EcRemote.upgradeHttpToHttps('https://a/b')), 'https://a/b');
147
+ assert.equal(withWindow('https:', () => EcRemote.upgradeHttpToHttps('//a/b')), '//a/b');
148
+ });
149
+ it('leaves URLs that mention the page protocol alone', () => {
150
+ assert.equal(withWindow('https:', () => EcRemote.upgradeHttpToHttps('http://a/?u=https:')), 'http://a/?u=https:');
151
+ });
152
+ it('leaves URLs alone on an http page', () => {
153
+ assert.equal(withWindow('http:', () => EcRemote.upgradeHttpToHttps('http://a/b')), 'http://a/b');
154
+ });
155
+ });