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,326 @@
1
+ require('../../index.js');
2
+ const chai = require('chai');
3
+ const sinon = require('sinon');
4
+ const forge = require('node-forge');
5
+ const { cassPromisify } = require('../com/eduworks/ec/promises/helpers.js');
6
+ const EcRemote = require('../com/eduworks/ec/remote/EcRemote.js');
7
+ const EcRemoteIdentityManager = require('../org/cassproject/ebac/identity/remote/EcRemoteIdentityManager.js');
8
+ const EcIdentityManager = require('../org/cassproject/ebac/identity/EcIdentityManager.js');
9
+ const EcIdentity = require('../org/cassproject/ebac/identity/EcIdentity.js');
10
+ const EcContact = require('../org/cassproject/ebac/identity/EcContact.js');
11
+
12
+ const assert = chai.assert;
13
+
14
+ const SALTS = {
15
+ usernameSalt: 'u'.repeat(16), usernameIterations: 1000.7, usernameLength: 64,
16
+ passwordSalt: 'p'.repeat(16), passwordIterations: 1000, passwordLength: 64,
17
+ secretSalt: 's'.repeat(16), secretIterations: 1000,
18
+ signatureSheetHashAlgorithm: 'SHA-256',
19
+ };
20
+
21
+ function serverReturns(payload) {
22
+ return (server, service, s, f) => cassPromisify(Promise.resolve(payload), s, f);
23
+ }
24
+ function postReturns(payload) {
25
+ return (server, service, fd, s, f) => cassPromisify(Promise.resolve(payload), s, f);
26
+ }
27
+ function loggedIn() {
28
+ const rim = new EcRemoteIdentityManager();
29
+ rim.server = 'http://id/';
30
+ rim.configure('u'.repeat(16), 2, 16, 'p'.repeat(16), 2, 16, 's'.repeat(16), 2);
31
+ rim.startLogin('user', 'pass');
32
+ return rim;
33
+ }
34
+ const fakeIdentity = (name, ppk) => Object.assign(new EcIdentity(), { displayName: name, ppk });
35
+ const fakeContact = (name, pk) => Object.assign(new EcContact(), { displayName: name, pk });
36
+ const fakePpk = (n) => ({ toPem: () => 'PPK' + n, toPk: () => ({ toPem: () => 'PK' + n }) });
37
+ const fakePk = (n) => ({ toPem: () => 'PK' + n });
38
+ const isNode = typeof process !== 'undefined' && process.versions?.node != null && typeof window === 'undefined';
39
+
40
+ // EcIdentityManager.addContact saves contacts to localStorage without a guard.
41
+ let createdStorage = false;
42
+ let usingStorage = false;
43
+ let savedContacts;
44
+ function useStorage() {
45
+ usingStorage = true;
46
+ if (typeof localStorage === 'undefined') {
47
+ global.localStorage = {};
48
+ createdStorage = true;
49
+ }
50
+ savedContacts = localStorage["contacts"];
51
+ }
52
+ function restoreStorage() {
53
+ if (!usingStorage) return;
54
+ usingStorage = false;
55
+ if (createdStorage) delete global.localStorage;
56
+ else if (savedContacts === undefined) delete localStorage["contacts"];
57
+ else localStorage["contacts"] = savedContacts;
58
+ createdStorage = false;
59
+ }
60
+
61
+ describe('EcRemoteIdentityManager flows', () => {
62
+ afterEach(() => sinon.restore());
63
+
64
+ describe('configureFromServer', () => {
65
+ it('reads salts and iterations from the server', async () => {
66
+ const get = sinon.stub(EcRemote, 'getExpectingObject').callsFake(serverReturns(SALTS));
67
+ const rim = new EcRemoteIdentityManager();
68
+ rim.server = 'http://id/';
69
+ const r = await rim.configureFromServer();
70
+ assert.strictEqual(r, SALTS);
71
+ assert.deepEqual(get.firstCall.args.slice(0, 2), ['http://id/', 'sky/id/salts']);
72
+ assert.isTrue(rim.configured);
73
+ assert.equal(rim.usernameIterations, 1000);
74
+ assert.equal(rim.usernameWidth, 64);
75
+ assert.equal(rim.passwordSalt, 'p'.repeat(16));
76
+ assert.equal(rim.secretIterations, 1000);
77
+ assert.equal(rim.signatureSheetHashAlgorithm, 'SHA-256');
78
+ });
79
+ const bad = [
80
+ ['usernameSalt', 'short', 'Insufficient length on Username Salt'],
81
+ ['usernameIterations', 999, 'Insufficient iterations on Username Hash'],
82
+ ['usernameLength', 32, 'Username Hash required to be length 64.'],
83
+ ['passwordSalt', 'short', 'Insufficient length on Password Salt'],
84
+ ['passwordIterations', 999, 'Insufficient iterations on Password Hash'],
85
+ ['passwordLength', 32, 'Password Hash required to be length 64.'],
86
+ ['secretSalt', 'short', 'Insufficient length on Secret Salt'],
87
+ ['secretIterations', 999, 'Insufficient iterations on Secret Hash'],
88
+ ];
89
+ for (const [field, value, message] of bad) {
90
+ it('rejects weak ' + field, async () => {
91
+ sinon.stub(EcRemote, 'getExpectingObject').callsFake(serverReturns({ ...SALTS, [field]: value }));
92
+ const rim = new EcRemoteIdentityManager();
93
+ const e = await rim.configureFromServer().catch((x) => x);
94
+ assert.instanceOf(e, Error);
95
+ assert.equal(e.message, message);
96
+ assert.isFalse(rim.configured);
97
+ });
98
+ }
99
+ it('marks itself unconfigured when the server fails', async () => {
100
+ sinon.stub(EcRemote, 'getExpectingObject').callsFake((server, service, s, f) => cassPromisify(Promise.reject('down'), s, f));
101
+ const rim = new EcRemoteIdentityManager();
102
+ rim.configured = true;
103
+ let failed = null;
104
+ await rim.configureFromServer(null, (e) => { failed = e; });
105
+ assert.instanceOf(failed, Error);
106
+ assert.equal(failed.message, 'down');
107
+ assert.isFalse(rim.configured);
108
+ });
109
+ });
110
+
111
+ describe('fetch', () => {
112
+ beforeEach(useStorage);
113
+ afterEach(restoreStorage);
114
+ (isNode ? it : it.skip)('fails on contacts in Node when there is no localStorage', async () => {
115
+ restoreStorage();
116
+ const rim = loggedIn();
117
+ sinon.stub(EcRemote, 'postExpectingObject').callsFake(postReturns({ contacts: [{ k: 1 }] }));
118
+ sinon.stub(EcContact, 'fromEncryptedContact').returns(fakeContact('k', fakePk(1)));
119
+ const e = await rim.fetch().catch((x) => x);
120
+ assert.instanceOf(e, ReferenceError);
121
+ assert.match(e.message, /localStorage/);
122
+ });
123
+ it('fails when not configured or not logged in', () => {
124
+ const failure = sinon.spy();
125
+ const rim = new EcRemoteIdentityManager();
126
+ assert.isUndefined(rim.fetch(null, failure));
127
+ assert.equal(failure.firstCall.args[0], 'Remote Identity not configured.');
128
+ rim.configured = true;
129
+ assert.isUndefined(rim.fetch(null, failure));
130
+ assert.equal(failure.secondCall.args[0], 'Please log in before performing this operation.');
131
+ });
132
+ it('builds an identity manager from the credentials and contacts', async () => {
133
+ const rim = loggedIn();
134
+ const post = sinon.stub(EcRemote, 'postExpectingObject').callsFake(postReturns({
135
+ pad: 'PAD', token: 'TOKEN', lastLogin: 42,
136
+ credentials: [{ c: 1 }, { c: 2 }],
137
+ contacts: [{ k: 1 }],
138
+ }));
139
+ const fromCredential = sinon.stub(EcIdentity, 'fromCredential').callsFake(async (c) => fakeIdentity('id' + c.c, fakePpk(c.c)));
140
+ const fromContact = sinon.stub(EcContact, 'fromEncryptedContact').callsFake((c) => fakeContact('k' + c.k, fakePk(10 + c.k)));
141
+ const commit = sinon.stub(rim, 'commit').resolves('committed');
142
+ let seen = null;
143
+ const eim = await rim.fetch((x) => { seen = x; return x; }, null);
144
+ assert.strictEqual(seen, eim);
145
+ assert.instanceOf(eim, EcIdentityManager);
146
+ assert.deepEqual(post.firstCall.args.slice(0, 2), ['http://id/', 'sky/id/login']);
147
+ const request = JSON.parse(post.firstCall.args[2].get('credentialRequest'));
148
+ assert.equal(request.username, rim.usernameWithSalt);
149
+ assert.equal(request.password, rim.passwordWithSalt);
150
+ assert.equal(rim.pad, 'PAD');
151
+ assert.equal(rim.token, 'TOKEN');
152
+ assert.deepEqual(eim.ids.map((i) => i.displayName), ['id1', 'id2']);
153
+ assert.deepEqual(eim.ids.map((i) => i.lastLogin), [42, 42]);
154
+ assert.deepEqual(eim.contacts.map((i) => i.displayName), ['k1']);
155
+ assert.deepEqual(fromCredential.firstCall.args, [{ c: 1 }, rim.secretWithSalt, 'http://id/']);
156
+ assert.deepEqual(fromContact.firstCall.args, [{ k: 1 }, rim.secretWithSalt, 'http://id/']);
157
+ assert.equal(commit.callCount, 0);
158
+ });
159
+ it('retries with the empty-password secret and then commits', async () => {
160
+ const rim = loggedIn();
161
+ const report = sinon.stub(global.auditLogger, 'report');
162
+ sinon.stub(EcRemote, 'postExpectingObject').callsFake(postReturns({
163
+ credentials: [{ c: 1 }, { c: 2 }], contacts: [{ k: 1 }, { k: 2 }],
164
+ }));
165
+ const alt = forge.util.encode64(forge.pkcs5.pbkdf2('', rim.secretSalt, rim.secretIterations, 32));
166
+ const fromCredential = sinon.stub(EcIdentity, 'fromCredential').callsFake(async (c, secret) => {
167
+ if (secret == alt) return fakeIdentity('alt' + c.c, fakePpk(c.c));
168
+ if (c.c == 1) throw new Error('bad secret');
169
+ return fakeIdentity('noKey', null);
170
+ });
171
+ const fromContact = sinon.stub(EcContact, 'fromEncryptedContact').callsFake((c, secret) => {
172
+ if (secret == alt) return fakeContact('alt' + c.k, fakePk(10 + c.k));
173
+ if (c.k == 1) throw new Error('bad contact');
174
+ return null;
175
+ });
176
+ const commit = sinon.stub(rim, 'commit').resolves('committed');
177
+ const eim = await rim.fetch();
178
+ assert.deepEqual(eim.ids.map((i) => i.displayName), ['alt1', 'alt2']);
179
+ assert.deepEqual(eim.contacts.map((i) => i.displayName), ['alt1', 'alt2']);
180
+ assert.equal(fromCredential.callCount, 4);
181
+ assert.equal(fromContact.callCount, 4);
182
+ assert.deepEqual(fromContact.getCall(1).args, [{ k: 1 }, alt, 'http://id/']);
183
+ assert.equal(report.callCount, 2);
184
+ assert.equal(report.firstCall.args[2], 'EcRemoIdenManaFetch');
185
+ assert.equal(report.firstCall.args[3].message, 'bad secret');
186
+ assert.equal(commit.callCount, 1);
187
+ assert.deepEqual(commit.firstCall.args, [null, null, eim]);
188
+ });
189
+ it('commits when only contacts needed the retry', async () => {
190
+ const rim = loggedIn();
191
+ sinon.stub(EcRemote, 'postExpectingObject').callsFake(postReturns({ contacts: [{ k: 1 }] }));
192
+ const alt = forge.util.encode64(forge.pkcs5.pbkdf2('', rim.secretSalt, rim.secretIterations, 32));
193
+ sinon.stub(EcContact, 'fromEncryptedContact').callsFake((c, secret) => secret == alt ? fakeContact('alt', fakePk(1)) : fakeContact('none', null));
194
+ const commit = sinon.stub(rim, 'commit').resolves();
195
+ const eim = await rim.fetch();
196
+ assert.deepEqual(eim.ids, []);
197
+ assert.equal(eim.contacts.length, 1);
198
+ assert.equal(commit.callCount, 1);
199
+ });
200
+ it('does not commit when nothing needed the retry, even without credentials or contacts', async () => {
201
+ const rim = loggedIn();
202
+ sinon.stub(EcRemote, 'postExpectingObject').callsFake(postReturns({}));
203
+ const commit = sinon.stub(rim, 'commit').resolves();
204
+ const eim = await rim.fetch();
205
+ assert.deepEqual(eim.ids, []);
206
+ assert.deepEqual(eim.contacts, []);
207
+ assert.equal(commit.callCount, 0);
208
+ });
209
+ it('fails when a credential has no key even with the retry', async () => {
210
+ const rim = loggedIn();
211
+ sinon.stub(EcRemote, 'postExpectingObject').callsFake(postReturns({ credentials: [{ c: 1 }] }));
212
+ sinon.stub(EcIdentity, 'fromCredential').resolves(fakeIdentity('none', null));
213
+ let failed = null;
214
+ await rim.fetch(null, (e) => { failed = e; });
215
+ assert.equal(failed.message, 'Could not retreive encrypted ppks from credential packet.');
216
+ });
217
+ it('fails when a contact has no key even with the retry', async () => {
218
+ const rim = loggedIn();
219
+ sinon.stub(EcRemote, 'postExpectingObject').callsFake(postReturns({ contacts: [{ k: 1 }] }));
220
+ sinon.stub(EcContact, 'fromEncryptedContact').returns(fakeContact('none', null));
221
+ const e = await rim.fetch().catch((x) => x);
222
+ assert.equal(e.message, 'Could not retreive encrypted pks from credential packet.');
223
+ });
224
+ it('reports and rethrows a failed login', async () => {
225
+ const rim = loggedIn();
226
+ const report = sinon.stub(global.auditLogger, 'report');
227
+ sinon.stub(EcRemote, 'postExpectingObject').callsFake((server, service, fd, s, f) => cassPromisify(Promise.reject('denied'), s, f));
228
+ const e = await rim.fetch().catch((x) => x);
229
+ assert.instanceOf(e, Error);
230
+ assert.equal(e.message, 'denied');
231
+ assert.deepEqual(report.firstCall.args.slice(2), ['EcRemoIdenManaFetch', 'denied']);
232
+ });
233
+ });
234
+
235
+ describe('sendCredentials', () => {
236
+ function fakeEim() {
237
+ const eim = new EcIdentityManager();
238
+ const mine = Object.assign(new EcIdentity(), { source: null });
239
+ const same = Object.assign(new EcIdentity(), { source: 'http://id/' });
240
+ const other = Object.assign(new EcIdentity(), { source: 'http://elsewhere/' });
241
+ sinon.stub(mine, 'toCredential').resolves('cred-mine');
242
+ sinon.stub(same, 'toCredential').resolves('cred-same');
243
+ sinon.stub(other, 'toCredential').resolves('cred-other');
244
+ eim.ids.push(mine, other, same);
245
+ const c1 = Object.assign(new EcContact(), { source: null });
246
+ const c2 = Object.assign(new EcContact(), { source: 'http://elsewhere/' });
247
+ sinon.stub(c1, 'toEncryptedContact').resolves('contact-1');
248
+ sinon.stub(c2, 'toEncryptedContact').resolves('contact-2');
249
+ eim.contacts.push(c2, c1);
250
+ return { eim, mine, other, c1, c2 };
251
+ }
252
+ it('rejects when not configured or not logged in', async () => {
253
+ const rim = new EcRemoteIdentityManager();
254
+ let e = await rim.sendCredentials(null, null, 'svc', new EcIdentityManager()).catch((x) => x);
255
+ assert.equal(e.message, 'Remote Identity not configured.');
256
+ rim.configured = true;
257
+ e = await rim.sendCredentials(null, null, 'svc', new EcIdentityManager()).catch((x) => x);
258
+ assert.equal(e.message, 'Please log in before performing this operation.');
259
+ });
260
+ for (const [method, service] of [['commit', 'sky/id/commit'], ['create', 'sky/id/create']]) {
261
+ it(method + ' posts this server\'s credentials and contacts with a signature sheet', async () => {
262
+ const rim = loggedIn();
263
+ rim.pad = 'PAD';
264
+ rim.token = 'TOKEN';
265
+ rim.signatureSheetHashAlgorithm = 'ALG';
266
+ const { eim, mine, other, c1, c2 } = fakeEim();
267
+ const sheet = sinon.stub(eim, 'signatureSheet').callsFake((d, server, s, f) => cassPromisify(Promise.resolve('SHEET'), s, f));
268
+ const post = sinon.stub(EcRemote, 'postExpectingString').callsFake(postReturns('OK'));
269
+ let seen = null;
270
+ const r = await rim[method]((x) => { seen = x; return 'cb'; }, null, eim);
271
+ assert.equal(r, 'cb');
272
+ assert.equal(seen, 'OK');
273
+ assert.deepEqual(sheet.firstCall.args.slice(0, 2), [60000, 'http://id/']);
274
+ assert.equal(sheet.firstCall.args[4], 'ALG');
275
+ assert.deepEqual(post.firstCall.args.slice(0, 2), ['http://id/', service]);
276
+ const fd = post.firstCall.args[2];
277
+ assert.equal(fd.get('signatureSheet'), 'SHEET');
278
+ const commit = JSON.parse(fd.get('credentialCommit'));
279
+ assert.equal(commit.username, rim.usernameWithSalt);
280
+ assert.equal(commit.password, rim.passwordWithSalt);
281
+ assert.equal(commit.token, 'TOKEN');
282
+ assert.equal(commit.credentials.pad, 'PAD');
283
+ assert.deepEqual(commit.credentials.credentials, ['cred-mine', 'cred-same']);
284
+ assert.deepEqual(commit.credentials.contacts, ['contact-1']);
285
+ assert.equal(mine.source, 'http://id/');
286
+ assert.equal(other.source, 'http://elsewhere/');
287
+ assert.equal(c1.source, 'http://id/');
288
+ assert.equal(c2.source, 'http://elsewhere/');
289
+ assert.equal(mine.toCredential.firstCall.args[0], rim.secretWithSalt);
290
+ assert.equal(c1.toEncryptedContact.firstCall.args[0], rim.secretWithSalt);
291
+ assert.equal(other.toCredential.callCount, 0);
292
+ });
293
+ }
294
+ it('uses the default identity manager when none is given', async () => {
295
+ const rim = loggedIn();
296
+ const { eim } = fakeEim();
297
+ sinon.replace(EcIdentityManager, 'default', eim);
298
+ sinon.stub(eim, 'signatureSheet').callsFake((d, server, s, f) => cassPromisify(Promise.resolve('SHEET'), s, f));
299
+ sinon.stub(EcRemote, 'postExpectingString').callsFake(postReturns('OK'));
300
+ assert.equal(await rim.commit(), 'OK');
301
+ assert.equal(eim.signatureSheet.callCount, 1);
302
+ });
303
+ it('rethrows post failures as errors', async () => {
304
+ const rim = loggedIn();
305
+ const eim = new EcIdentityManager();
306
+ sinon.stub(eim, 'signatureSheet').callsFake((d, server, s, f) => cassPromisify(Promise.resolve('SHEET'), s, f));
307
+ sinon.stub(EcRemote, 'postExpectingString').callsFake((server, service, fd, s, f) => cassPromisify(Promise.reject('nope'), s, f));
308
+ const e = await rim.commit(null, null, eim).catch((x) => x);
309
+ assert.instanceOf(e, Error);
310
+ assert.equal(e.message, 'nope');
311
+ });
312
+ it('rethrows signature sheet failures as errors', async () => {
313
+ const rim = loggedIn();
314
+ const eim = new EcIdentityManager();
315
+ sinon.stub(eim, 'signatureSheet').callsFake((d, server, s, f) => cassPromisify(Promise.reject('nosheet'), s, f));
316
+ const e = await rim.commit(null, null, eim).catch((x) => x);
317
+ assert.equal(e.message, 'nosheet');
318
+ });
319
+ });
320
+
321
+ it('splicePasswords interleaves any number of passwords', () => {
322
+ const rim = new EcRemoteIdentityManager();
323
+ assert.equal(rim.splicePasswords([]), '');
324
+ assert.equal(rim.splicePasswords(['ab', '', 'wxyz', '1']), 'aw1bxyz');
325
+ });
326
+ });