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.
- package/package.json +49 -52
- package/src/com/eduworks/ec/array/EcArray.js +1 -1
- package/src/com/eduworks/ec/array/EcAsyncHelper.js +1 -1
- package/src/com/eduworks/ec/array/EcObject.js +1 -1
- package/src/com/eduworks/ec/crypto/EcAes.js +1 -1
- package/src/com/eduworks/ec/crypto/EcAesCtrAsync.js +20 -22
- package/src/com/eduworks/ec/crypto/EcCrypto.js +8 -4
- package/src/com/eduworks/ec/crypto/EcPk.js +7 -7
- package/src/com/eduworks/ec/crypto/EcPpk.js +7 -7
- package/src/com/eduworks/ec/crypto/EcRsaOaepAsync.js +20 -19
- package/src/com/eduworks/ec/crypto/EcRsaOaepAsyncWorker.js +7 -6
- package/src/com/eduworks/ec/crypto/forgeAsync.js +1 -9
- package/src/com/eduworks/ec/crypto/forgeAsyncNode.js +8 -8
- package/src/com/eduworks/ec/graph/EcDirectedGraph.js +1 -2
- package/src/com/eduworks/ec/graph/EcFrameworkGraph.js +18 -24
- package/src/com/eduworks/ec/promises/helpers.js +1 -3
- package/src/com/eduworks/ec/remote/EcRemote.js +9 -9
- package/src/com/eduworks/ec/task/EcAsyncTask.js +5 -20
- package/src/com/eduworks/ec/task/EcAsyncTaskManager.js +3 -4
- package/src/com/eduworks/ec/task/EcAsyncTaskWorker.js +9 -11
- package/src/com/eduworks/schema/cfd/competency/CfdFramework.js +36 -37
- package/src/com/eduworks/schema/ebac/EbacContact.js +3 -9
- package/src/com/eduworks/schema/ebac/EbacContactGrant.js +3 -9
- package/src/com/eduworks/schema/ebac/EbacCredential.js +3 -9
- package/src/com/eduworks/schema/ebac/EbacCredentialCommit.js +3 -9
- package/src/com/eduworks/schema/ebac/EbacCredentialRequest.js +3 -9
- package/src/com/eduworks/schema/ebac/EbacCredentials.js +3 -9
- package/src/com/eduworks/schema/ebac/EbacEncryptedSecret.js +3 -9
- package/src/com/eduworks/schema/ebac/EbacEncryptedValue.js +5 -12
- package/src/com/eduworks/schema/ebac/EbacSignature.js +1 -6
- package/src/index/env.js +6 -3
- package/src/index/polyfills.js +13 -27
- package/src/org/angles/schema/angles/VideoStory.js +1 -1
- package/src/org/cass/competency/EcCompetency.js +1 -2
- package/src/org/cass/competency/EcDirectory.js +1 -2
- package/src/org/cass/competency/EcFramework.js +9 -12
- package/src/org/cass/exporter/CSVExport.js +12 -13
- package/src/org/cass/importer/ASNImport.js +3 -6
- package/src/org/cass/importer/CSVImport.js +9 -17
- package/src/org/cass/importer/CTDLASNCSVConceptImport.js +25 -121
- package/src/org/cass/importer/CTDLASNCSVImport.js +88 -133
- package/src/org/cass/importer/FrameworkImport.js +16 -30
- package/src/org/cass/importer/MedbiqImport.js +3 -4
- package/src/org/cass/importer/TabStructuredImport.js +6 -6
- package/src/org/cass/profile/EcAssertion.js +40 -51
- package/src/org/cassproject/ebac/identity/EcContactGrant.js +2 -2
- package/src/org/cassproject/ebac/identity/EcIdentityManager.js +56 -62
- package/src/org/cassproject/ebac/identity/EcRekeyRequest.js +3 -2
- package/src/org/cassproject/ebac/identity/remote/EcRemoteIdentityManager.js +51 -62
- package/src/org/cassproject/ebac/identity/remote/OAuth2FileBasedRemoteIdentityManager.js +56 -70
- package/src/org/cassproject/ebac/identity/remote/RemoteIdentityManagerInterface.js +3 -3
- package/src/org/cassproject/ebac/repository/EcEncryptedValue.js +51 -44
- package/src/org/cassproject/ebac/repository/EcRepository.js +228 -301
- package/src/org/cassproject/general/repository/GeneralFile.js +8 -9
- package/src/org/cassproject/schema/cass/competency/Competency.js +10 -11
- package/src/org/cassproject/schema/cass/competency/Framework.js +10 -11
- package/src/org/cassproject/schema/cass/competency/Level.js +10 -11
- package/src/org/cassproject/schema/cass/competency/Relation.js +10 -11
- package/src/org/cassproject/schema/cass/competency/RollupRule.js +7 -7
- package/src/org/cassproject/schema/cass/profile/Assertion.js +20 -27
- package/src/org/cassproject/schema/cass/profile/AssertionEnvelope.js +3 -5
- package/src/org/cassproject/schema/general/EcRemoteLinkedData.js +41 -44
- package/src/org/json/ld/EcLinkedData.js +10 -12
- package/src/org/schema/EcOrganization.js +10 -10
- package/src/org/schema/EcQuestion.js +8 -8
- package/src/org/w3/skos/Concept.js +6 -6
- package/src/org/w3/skos/ConceptScheme.js +6 -6
- package/src/org/w3/skos/EcConcept.js +1 -2
- package/src/org/w3/skos/EcConceptScheme.js +1 -2
- package/src/test/0.start.test.js +1 -1
- package/src/test/0.testLibraries.test.js +172 -0
- package/src/test/1.ASNImport.test.js +122 -0
- package/src/test/1.CSVExport.test.js +57 -0
- package/src/test/1.CSVImport.test.js +185 -0
- package/src/test/1.CTDLASNCSVConceptImport.test.js +348 -0
- package/src/test/1.CTDLASNCSVImport.import.test.js +497 -0
- package/src/test/1.Ebac.schema.test.js +107 -0
- package/src/test/1.EcAssertion.decrypt.test.js +98 -0
- package/src/test/1.EcAssertion.pinned.test.js +160 -0
- package/src/test/1.EcCrypto.formats.test.js +293 -0
- package/src/test/1.EcCrypto.generateUUID.test.js +47 -0
- package/src/test/1.EcEncryptedValue.helpers.test.js +236 -0
- package/src/test/1.EcHelpers.misc.test.js +259 -0
- package/src/test/1.EcIdentityManager.keys.test.js +439 -0
- package/src/test/1.EcOrganization.rekey.test.js +68 -0
- package/src/test/1.EcRemote.responses.test.js +155 -0
- package/src/test/1.EcRemoteIdentityManager.flows.test.js +326 -0
- package/src/test/1.EcRemoteLinkedData.pinned.test.js +473 -0
- package/src/test/1.EcRepository.l0.test.js +14 -5
- package/src/test/1.EcRepository.l1.test.js +16 -7
- package/src/test/1.EcRepository.l2.test.js +20 -11
- package/src/test/1.EcRepository.unit.test.js +961 -0
- package/src/test/1.EcRsaOaep.test.js +2 -12
- package/src/test/1.EcRsaOaepAsync.test.js +2 -12
- package/src/test/1.EcRsaOaepAsyncWorker.test.js +2 -12
- package/src/test/1.EcRsaOaepAsyncWorker.workers.test.js +147 -0
- package/src/test/1.FrameworkImport.test.js +170 -0
- package/src/test/1.MedbiqImport.test.js +92 -0
- package/src/test/1.OAuth2FileBasedRemoteIdentityManager.test.js +399 -0
- package/src/test/1.TabStructuredImport.test.js +61 -0
- package/src/test/1.polyfills.test.js +89 -0
- package/src/test/1.schema.competency.pinned.test.js +384 -0
- package/src/test/3.EcFrameworkGraph.test.js +3 -2
- package/src/test/4.schema.test.js +33 -28
- package/src/test/4.skyId.test.js +4 -2
- package/src/test/cypressValue.js +9 -0
- package/src/test/2.EcCompetency.test.js +0 -17
|
@@ -0,0 +1,399 @@
|
|
|
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
|
+
const EcIdentityManager = require('../org/cassproject/ebac/identity/EcIdentityManager.js');
|
|
6
|
+
const EcIdentity = require('../org/cassproject/ebac/identity/EcIdentity.js');
|
|
7
|
+
const EcContact = require('../org/cassproject/ebac/identity/EcContact.js');
|
|
8
|
+
const OAuth2 = require('../org/cassproject/ebac/identity/remote/OAuth2FileBasedRemoteIdentityManager.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
|
+
|
|
14
|
+
// A synchronous stand-in for hello.js: api() answers from `routes` and records each request.
|
|
15
|
+
function fakeHello(routes = {}) {
|
|
16
|
+
const hello = {
|
|
17
|
+
calls: [],
|
|
18
|
+
handlers: {},
|
|
19
|
+
failures: [],
|
|
20
|
+
init: sinon.spy(),
|
|
21
|
+
logout: sinon.spy(),
|
|
22
|
+
getAuthResponse: sinon.stub().returns({ access_token: 'TOKEN' }),
|
|
23
|
+
on(event, cb) { hello.handlers[event] = cb; },
|
|
24
|
+
login(server, o) {
|
|
25
|
+
hello.calls.push(['login', server, o]);
|
|
26
|
+
return { fail(f) { hello.failures.push(['login', f]); } };
|
|
27
|
+
},
|
|
28
|
+
api(path, method, o) {
|
|
29
|
+
hello.calls.push(['api', path, method, o]);
|
|
30
|
+
const response = routes[method + ' ' + path];
|
|
31
|
+
return {
|
|
32
|
+
then(cb) {
|
|
33
|
+
cb(typeof response === 'function' ? response(o) : response);
|
|
34
|
+
return { fail(f) { hello.failures.push([path, f]); } };
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
return hello;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const saved = {};
|
|
43
|
+
function setGlobal(name, value) {
|
|
44
|
+
if (!(name in saved)) saved[name] = { had: name in global, value: global[name] };
|
|
45
|
+
global[name] = value;
|
|
46
|
+
}
|
|
47
|
+
function restoreGlobals() {
|
|
48
|
+
for (const [name, s] of Object.entries(saved)) {
|
|
49
|
+
if (s.had) global[name] = s.value;
|
|
50
|
+
else delete global[name];
|
|
51
|
+
delete saved[name];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function waitFor(fn) {
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
let n = 0;
|
|
58
|
+
const check = () => {
|
|
59
|
+
if (fn()) resolve();
|
|
60
|
+
else if (n++ > 200) reject(new Error('timed out'));
|
|
61
|
+
else setTimeout(check, 5);
|
|
62
|
+
};
|
|
63
|
+
check();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function manager(hello) {
|
|
68
|
+
setGlobal('hello', hello);
|
|
69
|
+
sinon.stub(EcRemote, 'getExpectingObject');
|
|
70
|
+
const m = new OAuth2();
|
|
71
|
+
EcRemote.getExpectingObject.restore();
|
|
72
|
+
return m;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe('OAuth2FileBasedRemoteIdentityManager', () => {
|
|
76
|
+
afterEach(() => {
|
|
77
|
+
sinon.restore();
|
|
78
|
+
restoreGlobals();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('constructor', () => {
|
|
82
|
+
const config = { googleScope: 'files', nested: { a: [1, 2] } };
|
|
83
|
+
function construct(hello, respond) {
|
|
84
|
+
setGlobal('hello', hello);
|
|
85
|
+
const get = sinon.stub(EcRemote, 'getExpectingObject').callsFake(respond);
|
|
86
|
+
const m = new OAuth2();
|
|
87
|
+
assert.deepEqual(get.firstCall.args.slice(0, 2), ['', 'hello.json']);
|
|
88
|
+
return m;
|
|
89
|
+
}
|
|
90
|
+
it('loads hello.json, keeps a copy of it and calls initialized()', () => {
|
|
91
|
+
const hello = fakeHello();
|
|
92
|
+
const initialized = sinon.spy();
|
|
93
|
+
setGlobal('initialized', initialized);
|
|
94
|
+
const m = construct(hello, (s, svc, ok) => ok(config));
|
|
95
|
+
assert.deepEqual(m.configuration, config);
|
|
96
|
+
assert.notStrictEqual(m.configuration, config);
|
|
97
|
+
assert.notStrictEqual(m.configuration.nested, config.nested);
|
|
98
|
+
assert.strictEqual(hello.init.firstCall.args[0], config);
|
|
99
|
+
assert.isTrue(m.oauthEnabled);
|
|
100
|
+
assert.equal(initialized.callCount, 1);
|
|
101
|
+
assert.isFalse(OAuth2.oauthEnabled);
|
|
102
|
+
});
|
|
103
|
+
it('disables OAuth when initialized() is missing', () => {
|
|
104
|
+
const hello = fakeHello();
|
|
105
|
+
setGlobal('initialized', undefined);
|
|
106
|
+
delete global.initialized;
|
|
107
|
+
const m = construct(hello, (s, svc, ok) => ok(config));
|
|
108
|
+
assert.deepEqual(m.configuration, config);
|
|
109
|
+
assert.equal(hello.init.callCount, 1);
|
|
110
|
+
assert.isFalse(m.oauthEnabled);
|
|
111
|
+
});
|
|
112
|
+
it('disables OAuth when hello.init throws', () => {
|
|
113
|
+
const hello = fakeHello();
|
|
114
|
+
hello.init = sinon.stub().throws(new Error('bad config'));
|
|
115
|
+
const initialized = sinon.spy();
|
|
116
|
+
setGlobal('initialized', initialized);
|
|
117
|
+
const m = construct(hello, (s, svc, ok) => ok('just text'));
|
|
118
|
+
assert.equal(m.configuration, 'just text');
|
|
119
|
+
assert.isFalse(m.oauthEnabled);
|
|
120
|
+
assert.equal(initialized.callCount, 0);
|
|
121
|
+
});
|
|
122
|
+
it('disables OAuth when hello.json cannot be loaded', () => {
|
|
123
|
+
const m = construct(fakeHello(), (s, svc, ok, fail) => fail('404'));
|
|
124
|
+
assert.isNull(m.configuration);
|
|
125
|
+
assert.isFalse(m.oauthEnabled);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('simple members', () => {
|
|
130
|
+
it('isGlobal, configureFromServer, clear, setDefaultIdentityManagementServer and changePassword', () => {
|
|
131
|
+
const hello = fakeHello();
|
|
132
|
+
const m = manager(hello);
|
|
133
|
+
assert.isTrue(m.isGlobal());
|
|
134
|
+
m.global = false;
|
|
135
|
+
assert.isFalse(m.isGlobal());
|
|
136
|
+
const success = sinon.spy();
|
|
137
|
+
m.configureFromServer(success, null);
|
|
138
|
+
assert.deepEqual(success.firstCall.args, [null]);
|
|
139
|
+
OAuth2.oauthEnabled = true;
|
|
140
|
+
m.clear();
|
|
141
|
+
assert.isFalse(OAuth2.oauthEnabled);
|
|
142
|
+
assert.equal(hello.logout.callCount, 0);
|
|
143
|
+
m.setDefaultIdentityManagementServer('google');
|
|
144
|
+
assert.equal(m.server, 'google');
|
|
145
|
+
m.clear();
|
|
146
|
+
assert.deepEqual(hello.logout.firstCall.args, ['google', null]);
|
|
147
|
+
assert.isFalse(m.changePassword('a', 'b', 'c'));
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
for (const method of ['fetch', 'create']) {
|
|
152
|
+
describe(method, () => {
|
|
153
|
+
function start(folders) {
|
|
154
|
+
const hello = fakeHello({ 'get google/me/folders': { data: folders } });
|
|
155
|
+
const m = manager(hello);
|
|
156
|
+
m.server = 'google';
|
|
157
|
+
m.configuration = { googleScope: 'drive' };
|
|
158
|
+
for (const name of ['hookIdentityManagerIdentities', 'readIdentityFiles', 'hookIdentityManagerContacts', 'readContactFiles', 'createIdentityFolder', 'createContactFolder'])
|
|
159
|
+
sinon.stub(m, name);
|
|
160
|
+
const success = () => { };
|
|
161
|
+
const failure = () => { };
|
|
162
|
+
m[method](success, failure);
|
|
163
|
+
return { hello, m, success, failure };
|
|
164
|
+
}
|
|
165
|
+
it('logs in and reads the CASS folders', () => {
|
|
166
|
+
const { hello, m, success, failure } = start([
|
|
167
|
+
{ name: 'CASS Identities', id: 'I' }, { name: 'Other', id: 'X' }, { name: 'CASS Contacts', id: 'C' },
|
|
168
|
+
]);
|
|
169
|
+
const expected = method == 'fetch' ? { scope: 'drive', display: 'page' } : { scope: 'drive' };
|
|
170
|
+
assert.deepEqual(hello.calls[0], ['login', 'google', expected]);
|
|
171
|
+
assert.strictEqual(hello.failures[0][1], failure);
|
|
172
|
+
const response = { network: 'google' };
|
|
173
|
+
hello.handlers['auth.login'](response);
|
|
174
|
+
assert.strictEqual(m.oauthLoginResponse, response);
|
|
175
|
+
assert.equal(m.network, 'google');
|
|
176
|
+
assert.deepEqual(hello.calls[1], ['api', 'google/me/folders', 'get', {}]);
|
|
177
|
+
assert.deepEqual(m.hookIdentityManagerIdentities.firstCall.args, ['I']);
|
|
178
|
+
assert.deepEqual(m.readIdentityFiles.firstCall.args, ['I', success, failure]);
|
|
179
|
+
assert.deepEqual(m.hookIdentityManagerContacts.firstCall.args, ['C']);
|
|
180
|
+
assert.deepEqual(m.readContactFiles.firstCall.args, ['C', success, failure]);
|
|
181
|
+
assert.equal(m.createIdentityFolder.callCount, 0);
|
|
182
|
+
assert.equal(m.createContactFolder.callCount, 0);
|
|
183
|
+
assert.deepEqual(hello.failures[1], ['google/me/folders', failure]);
|
|
184
|
+
});
|
|
185
|
+
it('creates missing folders', () => {
|
|
186
|
+
const { hello, m, success } = start([]);
|
|
187
|
+
hello.handlers['auth.login']({ network: 'google' });
|
|
188
|
+
assert.deepEqual(m.createIdentityFolder.firstCall.args, [success]);
|
|
189
|
+
assert.equal(m.createContactFolder.callCount, 1);
|
|
190
|
+
assert.equal(m.readIdentityFiles.callCount, 0);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
describe('folders', () => {
|
|
196
|
+
it('createContactFolder posts the folder and hooks contacts to it', () => {
|
|
197
|
+
const hello = fakeHello({ 'post net/me/folders': { id: 'NEW' } });
|
|
198
|
+
const m = manager(hello);
|
|
199
|
+
m.network = 'net';
|
|
200
|
+
sinon.stub(m, 'hookIdentityManagerContacts');
|
|
201
|
+
m.createContactFolder();
|
|
202
|
+
assert.deepEqual(hello.calls[0], ['api', 'net/me/folders', 'post', { name: 'CASS Contacts' }]);
|
|
203
|
+
assert.deepEqual(m.hookIdentityManagerContacts.firstCall.args, ['NEW']);
|
|
204
|
+
});
|
|
205
|
+
it('createIdentityFolder posts the folder, hooks identities and reports success', () => {
|
|
206
|
+
const r = { id: 'NEW' };
|
|
207
|
+
const hello = fakeHello({ 'post net/me/folders': r });
|
|
208
|
+
const m = manager(hello);
|
|
209
|
+
m.network = 'net';
|
|
210
|
+
sinon.stub(m, 'hookIdentityManagerIdentities');
|
|
211
|
+
const success = sinon.spy();
|
|
212
|
+
m.createIdentityFolder(success);
|
|
213
|
+
assert.deepEqual(hello.calls[0], ['api', 'net/me/folders', 'post', { name: 'CASS Identities' }]);
|
|
214
|
+
assert.deepEqual(m.hookIdentityManagerIdentities.firstCall.args, ['NEW']);
|
|
215
|
+
assert.strictEqual(success.firstCall.args[0], r);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe('writing files', () => {
|
|
220
|
+
it('writeIdentityFiles writes each identity and then reports success', async () => {
|
|
221
|
+
const m = manager(fakeHello());
|
|
222
|
+
const eim = new EcIdentityManager();
|
|
223
|
+
eim.ids.push({ n: 1 }, { n: 2 });
|
|
224
|
+
sinon.stub(m, 'writeIdentityFile').callsFake((folder, identity, done) => done());
|
|
225
|
+
let result = null;
|
|
226
|
+
m.writeIdentityFiles('F', (r) => { result = r; }, eim);
|
|
227
|
+
await waitFor(() => result != null);
|
|
228
|
+
assert.strictEqual(result, eim.ids);
|
|
229
|
+
assert.deepEqual(m.writeIdentityFile.getCalls().map((c) => [c.args[0], c.args[1].n]), [['F', 1], ['F', 2]]);
|
|
230
|
+
});
|
|
231
|
+
it('writeIdentityFile posts new files and puts known ones', () => {
|
|
232
|
+
const hello = fakeHello({ 'post net/me/files': { id: 'ID1' }, 'put net/me/files': { id: 'ID2' } });
|
|
233
|
+
const m = manager(hello);
|
|
234
|
+
m.network = 'net';
|
|
235
|
+
setGlobal('stringToFile', (content, name, type) => ({ content, name, type }));
|
|
236
|
+
const identity = { ppk: { toPem: () => 'PEM' }, displayName: 'Me' };
|
|
237
|
+
const done = sinon.spy();
|
|
238
|
+
m.writeIdentityFile('F', identity, done);
|
|
239
|
+
const o = hello.calls[0][3];
|
|
240
|
+
assert.deepEqual(hello.calls[0].slice(0, 3), ['api', 'net/me/files', 'post']);
|
|
241
|
+
assert.equal(o.parent, 'F');
|
|
242
|
+
assert.equal(o.name, 'Me.pem');
|
|
243
|
+
assert.deepEqual(o.file, [{ content: 'PEM', name: 'Me.pem', type: 'text/plain' }]);
|
|
244
|
+
assert.equal(identity.id, 'ID1');
|
|
245
|
+
assert.equal(done.callCount, 1);
|
|
246
|
+
m.writeIdentityFile('F', identity, null);
|
|
247
|
+
assert.equal(hello.calls[1][2], 'put');
|
|
248
|
+
assert.notProperty(hello.calls[1][3], 'parent');
|
|
249
|
+
assert.equal(hello.calls[1][3].id, 'ID1');
|
|
250
|
+
assert.equal(identity.id, 'ID2');
|
|
251
|
+
});
|
|
252
|
+
it('writeContactFiles writes each contact', () => {
|
|
253
|
+
const m = manager(fakeHello());
|
|
254
|
+
const eim = new EcIdentityManager();
|
|
255
|
+
const a = {}, b = {};
|
|
256
|
+
eim.contacts.push(a, b);
|
|
257
|
+
sinon.stub(m, 'writeContactFile');
|
|
258
|
+
m.writeContactFiles('F', eim);
|
|
259
|
+
assert.deepEqual(m.writeContactFile.getCalls().map((c) => c.args), [['F', a], ['F', b]]);
|
|
260
|
+
});
|
|
261
|
+
it('writeContactFile posts new files and puts known ones', () => {
|
|
262
|
+
const hello = fakeHello({ 'post net/me/files': { id: 'ID1' }, 'put net/me/files': { id: 'ID2' } });
|
|
263
|
+
const m = manager(hello);
|
|
264
|
+
m.network = 'net';
|
|
265
|
+
setGlobal('stringToFile', (content, name, type) => ({ content, name, type }));
|
|
266
|
+
const c = { pk: { toPem: () => 'PK' }, displayName: 'You' };
|
|
267
|
+
m.writeContactFile('F', c);
|
|
268
|
+
assert.equal(hello.calls[0][2], 'post');
|
|
269
|
+
assert.equal(hello.calls[0][3].parent, 'F');
|
|
270
|
+
assert.deepEqual(hello.calls[0][3].file, [{ content: 'PK', name: 'You.pem', type: 'text/plain' }]);
|
|
271
|
+
assert.equal(c.id, 'ID1');
|
|
272
|
+
m.writeContactFile('F', c);
|
|
273
|
+
assert.equal(hello.calls[1][2], 'put');
|
|
274
|
+
assert.equal(c.id, 'ID2');
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe('reading files', () => {
|
|
279
|
+
const files = { data: [
|
|
280
|
+
{ name: 'Bob.pem.pem', id: '1', downloadUrl: 'http://dl/1?x=1' },
|
|
281
|
+
{ name: 'x\\.pemy.pem', id: '2', downloadUrl: 'http://dl/2?x=2' },
|
|
282
|
+
] };
|
|
283
|
+
it('readIdentityFiles downloads each key into the identity manager', async () => {
|
|
284
|
+
const hello = fakeHello({ 'get net/me/files': files });
|
|
285
|
+
const m = manager(hello);
|
|
286
|
+
m.network = 'net';
|
|
287
|
+
const pk = EcPpk.fromPem(PEM);
|
|
288
|
+
const get = sinon.stub(EcRemote, 'getExpectingString').callsFake((s, url, ok) => ok(PEM));
|
|
289
|
+
const eim = new EcIdentityManager();
|
|
290
|
+
let result = null;
|
|
291
|
+
m.readIdentityFiles('F', (r) => { result = r; }, null, eim);
|
|
292
|
+
assert.deepEqual(hello.calls[0], ['api', 'net/me/files', 'get', { parent: 'F' }]);
|
|
293
|
+
await waitFor(() => result != null);
|
|
294
|
+
assert.strictEqual(result, eim);
|
|
295
|
+
assert.deepEqual(get.getCalls().map((c) => c.args[1]), ['http://dl/1?x=1&access_token=TOKEN', 'http://dl/2?x=2&access_token=TOKEN']);
|
|
296
|
+
assert.equal(hello.getAuthResponse.firstCall.args[0], 'net');
|
|
297
|
+
assert.equal(eim.ids.length, 1);
|
|
298
|
+
assert.instanceOf(eim.ids[0], EcIdentity);
|
|
299
|
+
assert.equal(eim.ids[0].displayName, 'Bob.pem');
|
|
300
|
+
assert.equal(eim.ids[0].source, 'google');
|
|
301
|
+
assert.equal(eim.ids[0].id, '1');
|
|
302
|
+
assert.strictEqual(eim.ids[0].ppk, pk);
|
|
303
|
+
});
|
|
304
|
+
it('readIdentityFiles strips the literal "\\.pem" text from names', async () => {
|
|
305
|
+
const hello = fakeHello({ 'get net/me/files': { data: [files.data[1]] } });
|
|
306
|
+
const m = manager(hello);
|
|
307
|
+
m.network = 'net';
|
|
308
|
+
sinon.stub(EcRemote, 'getExpectingString').callsFake((s, url, ok) => ok(PEM));
|
|
309
|
+
const eim = new EcIdentityManager();
|
|
310
|
+
let result = null;
|
|
311
|
+
m.readIdentityFiles('F', (r) => { result = r; }, null, eim);
|
|
312
|
+
await waitFor(() => result != null);
|
|
313
|
+
assert.equal(eim.ids[0].displayName, 'xy');
|
|
314
|
+
});
|
|
315
|
+
it('readContactFiles downloads each key into the contacts', async () => {
|
|
316
|
+
const hello = fakeHello({ 'get net/me/files': files });
|
|
317
|
+
const m = manager(hello);
|
|
318
|
+
m.network = 'net';
|
|
319
|
+
const failure = () => { };
|
|
320
|
+
const pkPem = EcPpk.fromPem(PEM).toPk().toPem();
|
|
321
|
+
const get = sinon.stub(EcRemote, 'getExpectingString').callsFake((s, url, ok) => ok(pkPem));
|
|
322
|
+
const eim = new EcIdentityManager();
|
|
323
|
+
let done = false;
|
|
324
|
+
m.readContactFiles('F', (r) => { done = r === null; }, failure, eim);
|
|
325
|
+
await waitFor(() => done);
|
|
326
|
+
assert.strictEqual(get.firstCall.args[3], failure);
|
|
327
|
+
assert.equal(eim.contacts.length, 1);
|
|
328
|
+
assert.instanceOf(eim.contacts[0], EcContact);
|
|
329
|
+
// Both files hold the same key, so the second renames the first.
|
|
330
|
+
assert.equal(eim.contacts[0].displayName, 'xy');
|
|
331
|
+
assert.equal(eim.contacts[0].source, 'google');
|
|
332
|
+
assert.equal(eim.contacts[0].id, '1');
|
|
333
|
+
});
|
|
334
|
+
it('read reports success at once when the folder is empty', () => {
|
|
335
|
+
const hello = fakeHello({ 'get net/me/files': { data: [] } });
|
|
336
|
+
const m = manager(hello);
|
|
337
|
+
m.network = 'net';
|
|
338
|
+
const eim = new EcIdentityManager();
|
|
339
|
+
const s1 = sinon.spy(), s2 = sinon.spy();
|
|
340
|
+
m.readIdentityFiles('F', s1, null, eim);
|
|
341
|
+
m.readContactFiles('F', s2, null, eim);
|
|
342
|
+
assert.strictEqual(s1.firstCall.args[0], eim);
|
|
343
|
+
assert.deepEqual(s2.firstCall.args, [null]);
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
describe('hooks', () => {
|
|
348
|
+
it('hookIdentityManagerIdentities writes changed identities to the folder', () => {
|
|
349
|
+
const m = manager(fakeHello());
|
|
350
|
+
sinon.stub(m, 'writeIdentityFile');
|
|
351
|
+
const eim = new EcIdentityManager();
|
|
352
|
+
m.hookIdentityManagerIdentities('F', eim);
|
|
353
|
+
const who = {};
|
|
354
|
+
eim.onIdentityChanged(who);
|
|
355
|
+
assert.deepEqual(m.writeIdentityFile.firstCall.args, ['F', who, null]);
|
|
356
|
+
});
|
|
357
|
+
it('hookIdentityManagerContacts writes changed contacts to the folder', () => {
|
|
358
|
+
const m = manager(fakeHello());
|
|
359
|
+
sinon.stub(m, 'writeContactFile');
|
|
360
|
+
const eim = new EcIdentityManager();
|
|
361
|
+
m.hookIdentityManagerContacts('F', eim);
|
|
362
|
+
const who = {};
|
|
363
|
+
eim.onContactChanged(who);
|
|
364
|
+
assert.deepEqual(m.writeContactFile.firstCall.args, ['F', who]);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
describe('commit', () => {
|
|
369
|
+
it('fails without a login', () => {
|
|
370
|
+
const hello = fakeHello();
|
|
371
|
+
hello.getAuthResponse.returns(null);
|
|
372
|
+
const m = manager(hello);
|
|
373
|
+
m.server = 'google';
|
|
374
|
+
const failure = sinon.spy();
|
|
375
|
+
m.commit(null, failure);
|
|
376
|
+
assert.deepEqual(failure.firstCall.args, ['Please login again.']);
|
|
377
|
+
assert.equal(hello.calls.length, 0);
|
|
378
|
+
});
|
|
379
|
+
it('writes identities and contacts into their folders', () => {
|
|
380
|
+
const hello = fakeHello({ 'get net/me/folders': { data: [
|
|
381
|
+
{ name: 'CASS Contacts', id: 'C' }, { name: 'Junk', id: 'J' }, { name: 'CASS Identities', id: 'I' },
|
|
382
|
+
] } });
|
|
383
|
+
const m = manager(hello);
|
|
384
|
+
m.server = 'google';
|
|
385
|
+
m.network = 'net';
|
|
386
|
+
sinon.stub(m, 'writeIdentityFiles');
|
|
387
|
+
sinon.stub(m, 'writeContactFiles');
|
|
388
|
+
const success = () => { };
|
|
389
|
+
const failure = () => { };
|
|
390
|
+
m.commit(success, failure);
|
|
391
|
+
assert.equal(hello.getAuthResponse.firstCall.args[0], 'google');
|
|
392
|
+
assert.deepEqual(hello.calls[0], ['api', 'net/me/folders', 'get', { network: 'net' }]);
|
|
393
|
+
assert.deepEqual(m.writeIdentityFiles.firstCall.args, ['I', success]);
|
|
394
|
+
assert.deepEqual(m.writeContactFiles.firstCall.args, ['C']);
|
|
395
|
+
assert.equal(m.writeIdentityFiles.callCount, 1);
|
|
396
|
+
assert.deepEqual(hello.failures[0], ['net/me/folders', failure]);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
require('../../index.js');
|
|
2
|
+
const chai = require('chai');
|
|
3
|
+
|
|
4
|
+
const assert = chai.assert;
|
|
5
|
+
|
|
6
|
+
const SERVER = "http://repo.test/api/";
|
|
7
|
+
|
|
8
|
+
describe('TabStructuredImport.importCompetencies', () => {
|
|
9
|
+
const run = async (text, serverUrl, repo, hashNameForId) => {
|
|
10
|
+
let out = null;
|
|
11
|
+
await TabStructuredImport.importCompetencies(text, serverUrl, null, (c, a) => { out = { c, a }; }, null, null, repo, hashNameForId);
|
|
12
|
+
return out;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
it('builds one competency per distinct line and a narrows alignment to the indented parent', async () => {
|
|
16
|
+
const { c, a } = await run("Parent\n\tChild\n\tSibling\n\t\tGrandchild", SERVER, { selectedServer: SERVER }, true);
|
|
17
|
+
assert.deepEqual(c.map(x => x.getName()), ["Parent", "\tChild", "\tSibling", "\t\tGrandchild"]);
|
|
18
|
+
assert.equal(c[0].shortId(), SERVER + "data/schema.cassproject.org.0.4.Competency/" + EcCrypto.md5("Parent"));
|
|
19
|
+
assert.lengthOf(a, 3);
|
|
20
|
+
assert.deepEqual(a.map(x => [x.source, x.target, x.relationType]), [
|
|
21
|
+
[c[1].shortId(), c[0].shortId(), EcAlignment.NARROWS],
|
|
22
|
+
[c[2].shortId(), c[0].shortId(), EcAlignment.NARROWS],
|
|
23
|
+
[c[3].shortId(), c[2].shortId(), EcAlignment.NARROWS]
|
|
24
|
+
]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('reuses the first competency with the same trimmed name', async () => {
|
|
28
|
+
const { c, a } = await run("A\n\tB\nC\n\tB", SERVER, { selectedServer: SERVER }, true);
|
|
29
|
+
assert.deepEqual(c.map(x => x.getName()), ["A", "\tB", "C"]);
|
|
30
|
+
// The second "B" reuses the first one and gains a second parent.
|
|
31
|
+
assert.deepEqual(a.map(x => [x.source, x.target]), [
|
|
32
|
+
[c[1].shortId(), c[0].shortId()],
|
|
33
|
+
[c[1].shortId(), c[2].shortId()]
|
|
34
|
+
]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('does not align a competency to itself when the parent has the same name', async () => {
|
|
38
|
+
const { c, a } = await run("A\n\tA", SERVER, { selectedServer: SERVER }, true);
|
|
39
|
+
assert.lengthOf(c, 1);
|
|
40
|
+
assert.lengthOf(a, 0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('generates versioned ids when the server matches the repository', async () => {
|
|
44
|
+
const { c, a } = await run("A\n\tB", SERVER, { selectedServer: SERVER }, false);
|
|
45
|
+
assert.notEqual(c[0].id, c[0].shortId());
|
|
46
|
+
assert.notEqual(a[0].id, a[0].shortId());
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('generates short ids when the server differs from the repository', async () => {
|
|
50
|
+
const { c, a } = await run("A\n\tB", "http://other.test/", { selectedServer: SERVER }, false);
|
|
51
|
+
assert.equal(c[0].id, c[0].shortId());
|
|
52
|
+
assert.isTrue(c[0].id.startsWith("http://other.test/"));
|
|
53
|
+
assert.equal(a[0].id, a[0].shortId());
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('counts leading tabs and spaces', () => {
|
|
57
|
+
assert.equal(TabStructuredImport.tabs("\t x"), 3);
|
|
58
|
+
assert.equal(TabStructuredImport.tabs(" "), 3);
|
|
59
|
+
assert.equal(TabStructuredImport.tabs("x\t"), 0);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
require('../../index.js');
|
|
2
|
+
const chai = require('chai');
|
|
3
|
+
const sinon = require('sinon');
|
|
4
|
+
const EcRepository = require('../org/cassproject/ebac/repository/EcRepository.js');
|
|
5
|
+
const EcRemoteLinkedData = require('../org/cassproject/schema/general/EcRemoteLinkedData.js');
|
|
6
|
+
|
|
7
|
+
const assert = chai.assert;
|
|
8
|
+
|
|
9
|
+
describe('Number polyfills', () => {
|
|
10
|
+
it('Number.parseInt and Number.parseFloat are the global functions', () => {
|
|
11
|
+
assert.strictEqual(Number.parseInt, parseInt);
|
|
12
|
+
assert.strictEqual(Number.parseFloat, parseFloat);
|
|
13
|
+
});
|
|
14
|
+
it('intValue, shortValue, longValue and byteValue truncate like parseInt', () => {
|
|
15
|
+
for (const fn of ['intValue', 'shortValue', 'longValue', 'byteValue']) {
|
|
16
|
+
assert.strictEqual((3.7)[fn](), 3, fn);
|
|
17
|
+
assert.strictEqual((-3.7)[fn](), -3, fn);
|
|
18
|
+
assert.strictEqual((1e21)[fn](), 1, fn); // parseInt("1e+21")
|
|
19
|
+
assert.isNaN((NaN)[fn](), fn);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
it('floatValue and doubleValue parse like parseFloat', () => {
|
|
23
|
+
for (const fn of ['floatValue', 'doubleValue']) {
|
|
24
|
+
assert.strictEqual((3.5)[fn](), 3.5, fn);
|
|
25
|
+
assert.strictEqual((1e21)[fn](), 1e21, fn);
|
|
26
|
+
assert.isNaN((NaN)[fn](), fn);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
it('Number.parseShort, parseLong, parseByte and parseDouble parse strings', () => {
|
|
30
|
+
assert.strictEqual(Number.parseShort('12px'), 12);
|
|
31
|
+
assert.strictEqual(Number.parseLong('-0x1F'), -31);
|
|
32
|
+
assert.strictEqual(Number.parseByte('08'), 8);
|
|
33
|
+
assert.isNaN(Number.parseByte('px'));
|
|
34
|
+
assert.strictEqual(Number.parseDouble('1.5e3x'), 1500);
|
|
35
|
+
assert.isNaN(Number.parseDouble(''));
|
|
36
|
+
});
|
|
37
|
+
it('Number.prototype.isNaN reports NaN', () => {
|
|
38
|
+
assert.isTrue((NaN).isNaN());
|
|
39
|
+
assert.isTrue(Number.prototype.isNaN.call(NaN));
|
|
40
|
+
assert.isFalse((5).isNaN());
|
|
41
|
+
assert.isFalse((Infinity).isNaN());
|
|
42
|
+
assert.isFalse(Number.prototype.isNaN.call(0));
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('String polyfills', () => {
|
|
47
|
+
it('trim removes leading and trailing whitespace', () => {
|
|
48
|
+
assert.equal(' \t a b \n\r '.trim(), 'a b');
|
|
49
|
+
assert.equal(''.trim(), '');
|
|
50
|
+
assert.equal(' '.trim(), '');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('EcRemoteLinkedData.getVersionFromUrl', () => {
|
|
55
|
+
it('parses a numeric last path segment', () => {
|
|
56
|
+
assert.strictEqual(EcRemoteLinkedData.getVersionFromUrl('https://x/api/data/a/b/123'), 123);
|
|
57
|
+
assert.strictEqual(EcRemoteLinkedData.getVersionFromUrl('https://x/api/data/a/b/0012'), 12);
|
|
58
|
+
assert.strictEqual(EcRemoteLinkedData.getVersionFromUrl('https://x/api/data/a/b/99999999999999999999'), 100000000000000000000);
|
|
59
|
+
});
|
|
60
|
+
it('returns null otherwise', () => {
|
|
61
|
+
assert.isNull(EcRemoteLinkedData.getVersionFromUrl('https://x/api/data/a/b'));
|
|
62
|
+
assert.isNull(EcRemoteLinkedData.getVersionFromUrl('https://x/api/data/a/b/12a'));
|
|
63
|
+
assert.isNull(EcRemoteLinkedData.getVersionFromUrl(null));
|
|
64
|
+
assert.isNull(EcRemoteLinkedData.getVersionFromUrl(''));
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('EcRepository.find counter guard', () => {
|
|
69
|
+
let repos;
|
|
70
|
+
before(() => {
|
|
71
|
+
repos = EcRepository.repos;
|
|
72
|
+
// A repository with no server makes find move on to the next counter.
|
|
73
|
+
EcRepository.repos = [{ selectedServer: null }];
|
|
74
|
+
});
|
|
75
|
+
after(() => {
|
|
76
|
+
EcRepository.repos = repos;
|
|
77
|
+
});
|
|
78
|
+
afterEach(() => sinon.restore());
|
|
79
|
+
|
|
80
|
+
for (const [counter, calls] of [[0, 2], ['0', 2], [undefined, 1], [null, 1], [NaN, 1], ['abc', 1], [{}, 1], [5, 1]]) {
|
|
81
|
+
it(`counter ${typeof counter === 'string' ? `'${counter}'` : String(counter)} makes ${calls} call(s) and finds nothing`, async () => {
|
|
82
|
+
const spy = sinon.spy(EcRepository, 'find');
|
|
83
|
+
let error;
|
|
84
|
+
await EcRepository.find('https://x/api/data/missing', 'not found', {}, counter).catch((e) => { error = e; });
|
|
85
|
+
assert.equal(error && error.message, 'not found');
|
|
86
|
+
assert.equal(spy.callCount, calls);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
});
|