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.
- package/package.json +47 -50
- 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/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/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,961 @@
|
|
|
1
|
+
// Pins EcRepository behaviour without a server by stubbing EcRemote and the
|
|
2
|
+
// identity manager. Written before refactoring EcRepository for SonarCloud.
|
|
3
|
+
require('../../index.js');
|
|
4
|
+
const chai = require('chai');
|
|
5
|
+
const sinon = require('sinon');
|
|
6
|
+
const EcRepository = require('../org/cassproject/ebac/repository/EcRepository.js');
|
|
7
|
+
const EcRemote = require('../com/eduworks/ec/remote/EcRemote.js');
|
|
8
|
+
const EcRemoteLinkedData = require('../org/cassproject/schema/general/EcRemoteLinkedData.js');
|
|
9
|
+
const EcEncryptedValue = require('../org/cassproject/ebac/repository/EcEncryptedValue.js');
|
|
10
|
+
const EcCrypto = require('../com/eduworks/ec/crypto/EcCrypto.js');
|
|
11
|
+
|
|
12
|
+
const assert = chai.assert;
|
|
13
|
+
|
|
14
|
+
const SERVER = 'https://repo.example/api/';
|
|
15
|
+
const NOT_LOCATED = 'Could not locate object. May be due to EcRepository.alwaysTryUrl flag.';
|
|
16
|
+
const NOT_FOUND = 'Object not found or you did not supply sufficient permissions to access the object.';
|
|
17
|
+
|
|
18
|
+
// A fake signer, so nothing needs real keys.
|
|
19
|
+
const eim = {
|
|
20
|
+
eimId: 'unit-eim',
|
|
21
|
+
ids: [],
|
|
22
|
+
signatureSheet: async () => '[]',
|
|
23
|
+
sign: async (d) => d
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function thing(id, name) {
|
|
27
|
+
const t = new EcRemoteLinkedData('https://schema.org/', 'Thing');
|
|
28
|
+
t.id = id;
|
|
29
|
+
if (name) t.name = name;
|
|
30
|
+
return t;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function repo(server, fields) {
|
|
34
|
+
return Object.assign({ selectedServer: server, timeOffset: 0, signatureSheetHashAlgorithm: null }, fields);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Settles a promise and reports how, so tests can pin rejections exactly.
|
|
38
|
+
async function settle(p) {
|
|
39
|
+
try {
|
|
40
|
+
return { value: await p };
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return { error: e };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const STATICS = ['repos', 'alwaysTryUrl', 'unsigned', 'caching', 'cachingL2', 'cachingSearch'];
|
|
47
|
+
let saved;
|
|
48
|
+
function saveStatics() {
|
|
49
|
+
saved = {};
|
|
50
|
+
for (const s of STATICS) saved[s] = EcRepository[s];
|
|
51
|
+
EcRepository.repos = [];
|
|
52
|
+
EcRepository.alwaysTryUrl = false;
|
|
53
|
+
EcRepository.unsigned = false;
|
|
54
|
+
EcRepository.caching = false;
|
|
55
|
+
EcRepository.cachingL2 = false;
|
|
56
|
+
EcRepository.cachingSearch = false;
|
|
57
|
+
}
|
|
58
|
+
function restoreStatics() {
|
|
59
|
+
for (const s of STATICS) EcRepository[s] = saved[s];
|
|
60
|
+
EcRepository.clearCache();
|
|
61
|
+
EcRepository.fetching = {};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe('EcRepository (unit)', () => {
|
|
65
|
+
beforeEach(saveStatics);
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
sinon.restore();
|
|
68
|
+
restoreStatics();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('escapeSearch', () => {
|
|
72
|
+
const cases = [
|
|
73
|
+
['plain', 'plain'],
|
|
74
|
+
['', ''],
|
|
75
|
+
['a-b', 'a\\-b'],
|
|
76
|
+
['a=b', 'a\\=b'],
|
|
77
|
+
['a&&b', 'a\\&&b'],
|
|
78
|
+
// "||" is escaped, then each "|" is escaped again.
|
|
79
|
+
['a||b', 'a\\\\|\\|b'],
|
|
80
|
+
['a|b', 'a\\|b'],
|
|
81
|
+
['<>', '\\<\\>'],
|
|
82
|
+
['(x)', '\\(x\\)'],
|
|
83
|
+
['{x}', '\\{x\\}'],
|
|
84
|
+
['[x]', '\\[x\\]'],
|
|
85
|
+
['a^b', 'a\\^b'],
|
|
86
|
+
['say "hi"', 'say \\"hi\\"'],
|
|
87
|
+
['~*?', '\\~\\*\\?'],
|
|
88
|
+
['a:b', 'a\\:b'],
|
|
89
|
+
['a/b', 'a\\/b'],
|
|
90
|
+
['a+b', 'a\\+b'],
|
|
91
|
+
['back\\slash', 'back\\\\slash'],
|
|
92
|
+
['a--b', 'a\\-\\-b'],
|
|
93
|
+
['all -=&&||<>|(){}[]^"~*?:/+\\', 'all \\-\\=\\&&\\\\|\\|\\<\\>\\|\\(\\)\\{\\}\\[\\]\\^\\"\\~\\*\\?\\:\\/\\+\\\\']
|
|
94
|
+
];
|
|
95
|
+
for (const [input, expected] of cases)
|
|
96
|
+
it(`escapes ${JSON.stringify(input)}`, () => {
|
|
97
|
+
assert.strictEqual(EcRepository.escapeSearch(input), expected);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('history', () => {
|
|
102
|
+
beforeEach(() => { EcRepository.unsigned = true; });
|
|
103
|
+
|
|
104
|
+
it('rejects bad URLs synchronously', () => {
|
|
105
|
+
assert.throws(() => EcRepository.history(null), 'URL is null. Cannot EcRepository.history');
|
|
106
|
+
assert.throws(() => EcRepository.history('ftp://x'), 'URL does not begin with http. Cannot EcRepository.history');
|
|
107
|
+
});
|
|
108
|
+
it('maps results to EcRemoteLinkedData', async () => {
|
|
109
|
+
sinon.stub(EcRemote, 'getExpectingObject').resolves([{ '@id': 'https://x/1', '@type': 'Thing', '@context': 'https://schema.org/' }]);
|
|
110
|
+
const r = await EcRepository.history('https://x/1', null, eim);
|
|
111
|
+
assert.lengthOf(r, 1);
|
|
112
|
+
assert.instanceOf(r[0], EcRemoteLinkedData);
|
|
113
|
+
assert.equal(r[0].id, 'https://x/1');
|
|
114
|
+
assert.equal(EcRemote.getExpectingObject.firstCall.args[0], 'https://x/1?history=true');
|
|
115
|
+
});
|
|
116
|
+
for (const [name, err] of [['a not-located Error', new Error('x ' + NOT_LOCATED)], ['a not-found string', NOT_FOUND]])
|
|
117
|
+
it(`resolves null for ${name}`, async () => {
|
|
118
|
+
sinon.stub(EcRemote, 'getExpectingObject').rejects(err);
|
|
119
|
+
assert.isNull(await EcRepository.history('https://x/1', null, eim));
|
|
120
|
+
});
|
|
121
|
+
it('rethrows other errors, null and objects without toString', async () => {
|
|
122
|
+
const other = new Error('boom');
|
|
123
|
+
sinon.stub(EcRemote, 'getExpectingObject').returns(Promise.reject(other));
|
|
124
|
+
assert.strictEqual((await settle(EcRepository.history('https://x/1', null, eim))).error, other);
|
|
125
|
+
EcRemote.getExpectingObject.returns(Promise.reject(null));
|
|
126
|
+
const r = await settle(EcRepository.history('https://x/1', null, eim));
|
|
127
|
+
assert.property(r, 'error');
|
|
128
|
+
assert.isNull(r.error);
|
|
129
|
+
const bare = Object.create(null);
|
|
130
|
+
EcRemote.getExpectingObject.returns(Promise.reject(bare));
|
|
131
|
+
assert.strictEqual((await settle(EcRepository.history('https://x/1', null, eim))).error, bare);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('get', () => {
|
|
136
|
+
it('rejects bad URLs', async () => {
|
|
137
|
+
assert.equal((await settle(EcRepository.get(null))).error.message, 'URL is null. Cannot EcRepository.get');
|
|
138
|
+
assert.equal((await settle(EcRepository.get('ftp://x/1'))).error.message, 'URL does not begin with http. Cannot EcRepository.get ftp://x/1');
|
|
139
|
+
});
|
|
140
|
+
it('accepts an upper-case scheme', async () => {
|
|
141
|
+
EcRepository.alwaysTryUrl = true;
|
|
142
|
+
EcRepository.unsigned = true;
|
|
143
|
+
const t = { '@id': 'HTTP://x/api/data/t/1', '@type': 'Thing', '@context': 'https://schema.org/' };
|
|
144
|
+
sinon.stub(EcRemote, 'getExpectingObject').resolves(t);
|
|
145
|
+
const r = await EcRepository.get('HTTP://x/api/data/t/1', null, null, null, eim);
|
|
146
|
+
assert.equal(r.id, 'HTTP://x/api/data/t/1');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('when the URL is not tried directly and there are several repositories', () => {
|
|
150
|
+
beforeEach(() => {
|
|
151
|
+
EcRepository.repos = [repo('https://a.example/api/'), repo('https://b.example/api/')];
|
|
152
|
+
});
|
|
153
|
+
for (const [name, err] of [['not-located', new Error(NOT_LOCATED)], ['not-found', new Error(NOT_FOUND)]])
|
|
154
|
+
it(`resolves null when find rejects with ${name}`, async () => {
|
|
155
|
+
sinon.stub(EcRepository, 'find').rejects(err);
|
|
156
|
+
assert.isNull(await EcRepository.get('https://c.example/api/data/x/1', null, null, null, eim));
|
|
157
|
+
assert.equal(EcRepository.find.firstCall.args[1], NOT_LOCATED);
|
|
158
|
+
});
|
|
159
|
+
it('rethrows other errors from find', async () => {
|
|
160
|
+
const err = new Error('boom');
|
|
161
|
+
sinon.stub(EcRepository, 'find').returns(Promise.reject(err));
|
|
162
|
+
assert.strictEqual((await settle(EcRepository.get('https://c.example/api/data/x/1', null, null, null, eim))).error, err);
|
|
163
|
+
});
|
|
164
|
+
it('throws a TypeError when find rejects with null', async () => {
|
|
165
|
+
sinon.stub(EcRepository, 'find').returns(Promise.reject(null));
|
|
166
|
+
assert.instanceOf((await settle(EcRepository.get('https://c.example/api/data/x/1', null, null, null, eim))).error, TypeError);
|
|
167
|
+
});
|
|
168
|
+
it('resolves what find resolves', async () => {
|
|
169
|
+
const t = thing('https://c.example/api/data/x/1');
|
|
170
|
+
sinon.stub(EcRepository, 'find').resolves(t);
|
|
171
|
+
assert.strictEqual(await EcRepository.get('https://c.example/api/data/x/1', null, null, null, eim), t);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('when the request fails', () => {
|
|
176
|
+
beforeEach(() => {
|
|
177
|
+
EcRepository.alwaysTryUrl = true;
|
|
178
|
+
EcRepository.unsigned = true;
|
|
179
|
+
});
|
|
180
|
+
describe('without a repository', () => {
|
|
181
|
+
for (const [name, err] of [['not-located', new Error(NOT_LOCATED)], ['not-found', NOT_FOUND]])
|
|
182
|
+
it(`resolves null when find rejects with ${name}`, async () => {
|
|
183
|
+
sinon.stub(EcRemote, 'getExpectingObject').rejects(new Error('offline'));
|
|
184
|
+
sinon.stub(EcRepository, 'find').returns(Promise.reject(err));
|
|
185
|
+
assert.isNull(await EcRepository.get('https://x/api/data/t/1', null, null, null, eim));
|
|
186
|
+
assert.equal(EcRepository.find.firstCall.args[1].message, 'offline');
|
|
187
|
+
});
|
|
188
|
+
it('rethrows other errors from find', async () => {
|
|
189
|
+
const err = new Error('boom');
|
|
190
|
+
sinon.stub(EcRemote, 'getExpectingObject').rejects(new Error('offline'));
|
|
191
|
+
sinon.stub(EcRepository, 'find').returns(Promise.reject(err));
|
|
192
|
+
assert.strictEqual((await settle(EcRepository.get('https://x/api/data/t/1', null, null, null, eim))).error, err);
|
|
193
|
+
});
|
|
194
|
+
it('throws a TypeError when find rejects with undefined', async () => {
|
|
195
|
+
sinon.stub(EcRemote, 'getExpectingObject').rejects(new Error('offline'));
|
|
196
|
+
sinon.stub(EcRepository, 'find').returns(Promise.reject(undefined));
|
|
197
|
+
assert.instanceOf((await settle(EcRepository.get('https://x/api/data/t/1', null, null, null, eim))).error, TypeError);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
describe('with a repository', () => {
|
|
201
|
+
const r = repo('https://x/api/');
|
|
202
|
+
for (const msg of [NOT_LOCATED, NOT_FOUND])
|
|
203
|
+
it(`resolves null for "${msg.slice(0, 20)}..."`, async () => {
|
|
204
|
+
sinon.stub(EcRemote, 'getExpectingObject').returns(Promise.reject(msg));
|
|
205
|
+
assert.isNull(await EcRepository.get('https://x/api/data/t/1', null, null, r, eim));
|
|
206
|
+
});
|
|
207
|
+
it('rejects with an Error built from any other error', async () => {
|
|
208
|
+
sinon.stub(EcRemote, 'getExpectingObject').returns(Promise.reject('offline'));
|
|
209
|
+
const res = await settle(EcRepository.get('https://x/api/data/t/1', null, null, r, eim));
|
|
210
|
+
assert.instanceOf(res.error, Error);
|
|
211
|
+
assert.equal(res.error.message, 'offline');
|
|
212
|
+
});
|
|
213
|
+
it('caches a miss as null when caching', async () => {
|
|
214
|
+
EcRepository.caching = true;
|
|
215
|
+
sinon.stub(EcRemote, 'getExpectingObject').returns(Promise.reject(NOT_FOUND));
|
|
216
|
+
assert.isNull(await EcRepository.get('https://x/api/data/t/1', null, null, r, eim));
|
|
217
|
+
assert.strictEqual(EcRepository.cache['https://x/api/data/t/1'], null);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('setOffset', () => {
|
|
224
|
+
it('uses the time offset of the last repository whose server the URL contains', () => {
|
|
225
|
+
EcRepository.repos = [repo('https://a/api/', { timeOffset: 5 }), repo('https://b/api/', { timeOffset: 7 }), repo('https://a/api/', { timeOffset: 9 })];
|
|
226
|
+
assert.equal(EcRepository.setOffset('https://a/api/data/1'), 9);
|
|
227
|
+
assert.equal(EcRepository.setOffset('https://b/api/data/1'), 7);
|
|
228
|
+
assert.equal(EcRepository.setOffset('https://c/api/data/1'), 0);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('shouldTryUrl', () => {
|
|
233
|
+
it('decides from the URL, the flag and the repositories', () => {
|
|
234
|
+
assert.isFalse(EcRepository.shouldTryUrl(null));
|
|
235
|
+
assert.isTrue(EcRepository.shouldTryUrl('https://x'));
|
|
236
|
+
EcRepository.repos = [repo(null)];
|
|
237
|
+
assert.isTrue(EcRepository.shouldTryUrl('https://x'));
|
|
238
|
+
EcRepository.repos = [repo(null), repo('https://a/api/')];
|
|
239
|
+
assert.isFalse(EcRepository.shouldTryUrl('https://x'));
|
|
240
|
+
EcRepository.alwaysTryUrl = true;
|
|
241
|
+
assert.isTrue(EcRepository.shouldTryUrl('https://x'));
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('_save', () => {
|
|
246
|
+
it('drops empty reader and owner lists, keeps others, and signs', async () => {
|
|
247
|
+
const saveWithout = sinon.stub(EcRepository, '_saveWithoutSigning').resolves('saved');
|
|
248
|
+
const sign = sinon.spy(eim, 'sign');
|
|
249
|
+
const a = thing('https://x/api/data/t/1');
|
|
250
|
+
a.reader = [];
|
|
251
|
+
a.owner = [];
|
|
252
|
+
assert.equal(await EcRepository._save(a, null, null, null, eim), 'saved');
|
|
253
|
+
assert.notProperty(a, 'reader');
|
|
254
|
+
assert.notProperty(a, 'owner');
|
|
255
|
+
const b = thing('https://x/api/data/t/2');
|
|
256
|
+
b.reader = ['r'];
|
|
257
|
+
b.owner = null;
|
|
258
|
+
await EcRepository._save(b, null, null, null, eim);
|
|
259
|
+
assert.deepEqual(b.reader, ['r']);
|
|
260
|
+
assert.property(b, 'owner');
|
|
261
|
+
assert.isNull(b.owner);
|
|
262
|
+
assert.isTrue(sign.calledTwice);
|
|
263
|
+
assert.strictEqual(saveWithout.firstCall.args[0], a);
|
|
264
|
+
});
|
|
265
|
+
it('throws a string for invalid data', () => {
|
|
266
|
+
const bad = thing(null);
|
|
267
|
+
let thrown;
|
|
268
|
+
try { EcRepository._save(bad, null, null, null, eim); } catch (e) { thrown = e; }
|
|
269
|
+
assert.strictEqual(thrown, 'Cannot save data. It is missing a vital component.');
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe('_saveWithoutSigning', () => {
|
|
274
|
+
let post;
|
|
275
|
+
beforeEach(() => {
|
|
276
|
+
post = sinon.stub(EcRemote, 'postExpectingString').resolves('ok');
|
|
277
|
+
});
|
|
278
|
+
const r = repo(SERVER);
|
|
279
|
+
|
|
280
|
+
async function save(data, repoArg) {
|
|
281
|
+
const stamp = sinon.spy(data, 'updateTimestamp');
|
|
282
|
+
await EcRepository._saveWithoutSigning(data, null, null, repoArg, eim);
|
|
283
|
+
return { stamped: stamp.called, url: post.firstCall.args[0], service: post.firstCall.args[1], fd: post.firstCall.args[2] };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
it('posts objects on the repository to their own id, re-stamped', async () => {
|
|
287
|
+
EcRepository.repos = [r];
|
|
288
|
+
const d = thing(SERVER + 'data/schema.org.Thing/abc/123');
|
|
289
|
+
const res = await save(d, r);
|
|
290
|
+
assert.isTrue(res.stamped);
|
|
291
|
+
assert.equal(res.url, d.id);
|
|
292
|
+
assert.notEqual(d.id, SERVER + 'data/schema.org.Thing/abc/123');
|
|
293
|
+
assert.equal(res.fd.get('data'), d.toJson());
|
|
294
|
+
assert.equal(res.fd.get('signatureSheet'), '[]');
|
|
295
|
+
});
|
|
296
|
+
it('registers foreign objects under an md5 id without re-stamping', async () => {
|
|
297
|
+
EcRepository.repos = [r];
|
|
298
|
+
const d = thing('https://other.example/things/1');
|
|
299
|
+
const res = await save(d, r);
|
|
300
|
+
assert.isFalse(res.stamped);
|
|
301
|
+
assert.equal(res.url, SERVER + 'data/schema.org.Thing/' + EcCrypto.md5('https://other.example/things/1'));
|
|
302
|
+
});
|
|
303
|
+
it('treats the same host over another scheme as local for stamping, but registers it', async () => {
|
|
304
|
+
EcRepository.repos = [r];
|
|
305
|
+
const d = thing('http://repo.example/api/things/1');
|
|
306
|
+
const res = await save(d, r);
|
|
307
|
+
assert.isTrue(res.stamped);
|
|
308
|
+
assert.equal(res.url, SERVER + 'data/schema.org.Thing/' + EcCrypto.md5(d.shortId()));
|
|
309
|
+
});
|
|
310
|
+
it('posts to the id when there is no repository', async () => {
|
|
311
|
+
EcRepository.repos = [r];
|
|
312
|
+
const d = thing('https://other.example/things/one');
|
|
313
|
+
const res = await save(d, null);
|
|
314
|
+
assert.isTrue(res.stamped);
|
|
315
|
+
assert.equal(res.url, 'https://other.example/things/one');
|
|
316
|
+
});
|
|
317
|
+
it('posts to the id when alwaysTryUrl is set', async () => {
|
|
318
|
+
EcRepository.repos = [r];
|
|
319
|
+
EcRepository.alwaysTryUrl = true;
|
|
320
|
+
const d = thing('https://other.example/things/one');
|
|
321
|
+
const res = await save(d, r);
|
|
322
|
+
assert.isTrue(res.stamped);
|
|
323
|
+
assert.equal(res.url, 'https://other.example/things/one');
|
|
324
|
+
});
|
|
325
|
+
it('re-stamps a trailing number as a version before posting', async () => {
|
|
326
|
+
EcRepository.repos = [r];
|
|
327
|
+
const d = thing('https://other.example/things/1');
|
|
328
|
+
const res = await save(d, null);
|
|
329
|
+
assert.isTrue(res.stamped);
|
|
330
|
+
assert.match(d.id, /^https:\/\/other\.example\/things\/\d{13}$/);
|
|
331
|
+
assert.equal(res.url, d.id);
|
|
332
|
+
});
|
|
333
|
+
it('reports malformed data to failure and carries on, or throws a string', async () => {
|
|
334
|
+
const d = thing('https://x/api/data/t/1');
|
|
335
|
+
sinon.stub(d, 'invalid').returns(true);
|
|
336
|
+
const failure = sinon.spy();
|
|
337
|
+
await EcRepository._saveWithoutSigning(d, null, failure, null, eim);
|
|
338
|
+
assert.isTrue(failure.calledOnceWith('Data is malformed.'));
|
|
339
|
+
assert.isTrue(post.calledOnce);
|
|
340
|
+
let thrown;
|
|
341
|
+
try { EcRepository._saveWithoutSigning(d, null, null, null, eim); } catch (e) { thrown = e; }
|
|
342
|
+
assert.strictEqual(thrown, 'Data is malformed.');
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
describe('deleteRegistered', () => {
|
|
347
|
+
it('deletes by guid on this server or its proxy, otherwise by md5', async () => {
|
|
348
|
+
const del = sinon.stub(EcRemote, '_delete').resolves('deleted');
|
|
349
|
+
const r = new EcRepository();
|
|
350
|
+
r.selectedServer = SERVER;
|
|
351
|
+
await r.deleteRegistered(thing(SERVER + 'data/schema.org.Thing/abc'), null, null, eim);
|
|
352
|
+
assert.equal(del.lastCall.args[0], SERVER + 'data/schema.org.Thing/abc');
|
|
353
|
+
await r.deleteRegistered(thing('https://other.example/things/1'), null, null, eim);
|
|
354
|
+
assert.equal(del.lastCall.args[0], SERVER + 'data/schema.org.Thing/' + EcCrypto.md5('https://other.example/things/1'));
|
|
355
|
+
r.selectedServerProxy = 'https://proxy.example/';
|
|
356
|
+
await r.deleteRegistered(thing('https://proxy.example/things/zz'), null, null, eim);
|
|
357
|
+
assert.equal(del.lastCall.args[0], SERVER + 'data/schema.org.Thing/zz');
|
|
358
|
+
await r.deleteRegistered(thing(SERVER + 'data/schema.org.Thing/abc'), null, null, eim);
|
|
359
|
+
assert.equal(del.lastCall.args[0], SERVER + 'data/schema.org.Thing/' + EcCrypto.md5(SERVER + 'data/schema.org.Thing/abc'));
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe('multiput', () => {
|
|
364
|
+
it('drops empty reader and owner lists, re-stamps local objects and posts them together', async () => {
|
|
365
|
+
const post = sinon.stub(EcRemote, 'postExpectingString').resolves('ok');
|
|
366
|
+
const r = new EcRepository();
|
|
367
|
+
r.selectedServer = SERVER;
|
|
368
|
+
const local = thing(SERVER + 'data/schema.org.Thing/abc/123');
|
|
369
|
+
local.reader = [];
|
|
370
|
+
local.owner = [];
|
|
371
|
+
const foreign = thing('https://other.example/things/1');
|
|
372
|
+
foreign.reader = ['r'];
|
|
373
|
+
const sameHost = thing('http://repo.example/api/things/2');
|
|
374
|
+
const stamps = [local, foreign, sameHost].map((d) => sinon.spy(d, 'updateTimestamp'));
|
|
375
|
+
assert.equal(await r.multiput([local, foreign, sameHost], null, null, eim), 'ok');
|
|
376
|
+
assert.deepEqual(stamps.map((x) => x.called), [true, false, true]);
|
|
377
|
+
assert.notProperty(local, 'reader');
|
|
378
|
+
assert.notProperty(local, 'owner');
|
|
379
|
+
assert.deepEqual(foreign.reader, ['r']);
|
|
380
|
+
assert.equal(post.firstCall.args[0], SERVER);
|
|
381
|
+
assert.equal(post.firstCall.args[1], 'sky/repo/multiPut');
|
|
382
|
+
const sent = JSON.parse(post.firstCall.args[2].get('data'));
|
|
383
|
+
assert.deepEqual(sent.map((x) => x['@id']), [local.id, foreign.id, sameHost.id]);
|
|
384
|
+
});
|
|
385
|
+
it('throws for invalid data', () => {
|
|
386
|
+
const r = new EcRepository();
|
|
387
|
+
r.selectedServer = SERVER;
|
|
388
|
+
assert.throws(() => r.multiput([thing(null)], null, null, eim), 'Cannot save data. It is missing a vital component.');
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
describe('precache', () => {
|
|
393
|
+
let r, post;
|
|
394
|
+
const obj = (id, extra) => Object.assign({ '@id': id, '@type': 'Thing', '@context': 'https://schema.org/' }, extra);
|
|
395
|
+
beforeEach(() => {
|
|
396
|
+
r = new EcRepository();
|
|
397
|
+
r.selectedServer = SERVER;
|
|
398
|
+
post = sinon.stub(EcRemote, 'postExpectingObject');
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('rejects without urls', async () => {
|
|
402
|
+
assert.equal((await settle(r.precache(null, null, null, eim))).error.message, 'urls not defined.');
|
|
403
|
+
});
|
|
404
|
+
it('fetches objects, caches them under every id and returns those requested', async () => {
|
|
405
|
+
const local = SERVER + 'data/schema.org.Thing/abc';
|
|
406
|
+
const foreign = 'https://other.example/things/one';
|
|
407
|
+
post.resolves([obj(local), obj(foreign)]);
|
|
408
|
+
const out = await r.precache([local, foreign, SERVER + 'data/schema.org.Thing/missing'], null, null, eim);
|
|
409
|
+
assert.deepEqual(out.map((x) => x.id), [local, foreign]);
|
|
410
|
+
assert.equal(post.firstCall.args[1], 'sky/repo/multiGet');
|
|
411
|
+
assert.deepEqual(JSON.parse(post.firstCall.args[2].get('data')), ['data/schema.org.Thing/abc', 'data//' + EcCrypto.md5(foreign), 'data/schema.org.Thing/missing']);
|
|
412
|
+
assert.equal(post.firstCall.args[2].get('signatureSheet'), '[]');
|
|
413
|
+
assert.isNull(post.firstCall.args[2].get('ids'));
|
|
414
|
+
assert.sameMembers(Object.keys(EcRepository.cacheBacking), [
|
|
415
|
+
local,
|
|
416
|
+
SERVER + 'data/abc',
|
|
417
|
+
foreign,
|
|
418
|
+
EcRemoteLinkedData.veryShortId(SERVER, EcCrypto.md5(foreign)),
|
|
419
|
+
EcRemoteLinkedData.veryShortId(SERVER, 'one')
|
|
420
|
+
]);
|
|
421
|
+
});
|
|
422
|
+
it('calls success twice for fetched objects, the second time with its own result', async () => {
|
|
423
|
+
const local = SERVER + 'data/schema.org.Thing/abc';
|
|
424
|
+
post.resolves([obj(local)]);
|
|
425
|
+
const success = sinon.stub();
|
|
426
|
+
success.onFirstCall().returns('first');
|
|
427
|
+
success.onSecondCall().returns('second');
|
|
428
|
+
assert.equal(await r.precache([local], success, null, eim), 'second');
|
|
429
|
+
assert.deepEqual(success.firstCall.args[0].map((x) => x.id), [local]);
|
|
430
|
+
assert.equal(success.secondCall.args[0], 'first');
|
|
431
|
+
});
|
|
432
|
+
it('requests a trailing number as a version and caches the result only under its id', async () => {
|
|
433
|
+
const foreign = 'https://other.example/things/1';
|
|
434
|
+
post.resolves([obj(foreign)]);
|
|
435
|
+
const out = await r.precache([foreign], null, null, eim);
|
|
436
|
+
assert.deepEqual(out.map((x) => x.id), [foreign]);
|
|
437
|
+
assert.deepEqual(JSON.parse(post.firstCall.args[2].get('data')), ['data//' + EcCrypto.md5(foreign) + '/1']);
|
|
438
|
+
assert.deepEqual(Object.keys(EcRepository.cacheBacking), [foreign]);
|
|
439
|
+
});
|
|
440
|
+
it('keeps versioned objects only under their versioned id', async () => {
|
|
441
|
+
const versioned = SERVER + 'data/schema.org.Thing/abc/123';
|
|
442
|
+
post.resolves([obj(versioned)]);
|
|
443
|
+
const out = await r.precache([versioned], null, null, eim);
|
|
444
|
+
assert.deepEqual(out.map((x) => x.id), [versioned]);
|
|
445
|
+
assert.deepEqual(JSON.parse(post.firstCall.args[2].get('data')), ['data/schema.org.Thing/abc/123']);
|
|
446
|
+
assert.equal(EcRepository.cache[versioned].id, versioned);
|
|
447
|
+
assert.isUndefined(EcRepository.cache[SERVER + 'data/schema.org.Thing/abc']);
|
|
448
|
+
});
|
|
449
|
+
it('skips what is already cached when caching, and does not post when everything is', async () => {
|
|
450
|
+
EcRepository.caching = true;
|
|
451
|
+
const a = SERVER + 'data/schema.org.Thing/a';
|
|
452
|
+
const b = SERVER + 'data/schema.org.Thing/b';
|
|
453
|
+
EcRepository.cache[a] = thing(a);
|
|
454
|
+
EcRepository.cache[b] = null;
|
|
455
|
+
const out = await r.precache([a, b], null, null, eim);
|
|
456
|
+
assert.deepEqual(out.map((x) => x.id), [a]);
|
|
457
|
+
assert.isFalse(post.called);
|
|
458
|
+
const c = SERVER + 'data/schema.org.Thing/c';
|
|
459
|
+
post.resolves([obj(c)]);
|
|
460
|
+
const out2 = await r.precache([a, c], null, null, eim);
|
|
461
|
+
assert.deepEqual(out2.map((x) => x.id), [a, c]);
|
|
462
|
+
assert.deepEqual(JSON.parse(post.firstCall.args[2].get('data')), ['data/schema.org.Thing/c']);
|
|
463
|
+
});
|
|
464
|
+
it('asks for ids under L2 caching, then fetches the ids it is given', async () => {
|
|
465
|
+
EcRepository.cachingL2 = true;
|
|
466
|
+
const a = SERVER + 'data/schema.org.Thing/a';
|
|
467
|
+
post.onFirstCall().resolves([a + '/1']);
|
|
468
|
+
post.onSecondCall().resolves([obj(a + '/1')]);
|
|
469
|
+
const success = sinon.stub().returnsArg(0);
|
|
470
|
+
const out = await r.precache([a], success, null, eim);
|
|
471
|
+
assert.equal(post.firstCall.args[2].get('ids'), 'true');
|
|
472
|
+
assert.isNull(post.secondCall.args[2].get('ids'));
|
|
473
|
+
assert.deepEqual(JSON.parse(post.secondCall.args[2].get('data')), ['data/schema.org.Thing/a/1']);
|
|
474
|
+
assert.deepEqual(out.map((x) => x.id), [a + '/1']);
|
|
475
|
+
assert.isTrue(success.calledOnce);
|
|
476
|
+
});
|
|
477
|
+
it('makes a second attempt for ids whose objects did not come back', async () => {
|
|
478
|
+
const a = SERVER + 'data/schema.org.Thing/a';
|
|
479
|
+
const other = 'https://other.example/x/9';
|
|
480
|
+
post.onCall(0).resolves([other + '/5']);
|
|
481
|
+
post.onCall(1).resolves([]);
|
|
482
|
+
post.onCall(2).resolves([obj(other)]);
|
|
483
|
+
const out = await r.precache([a], null, null, eim);
|
|
484
|
+
// The retry asks for the same versioned id again, and finds nothing under it.
|
|
485
|
+
assert.deepEqual(post.getCalls().map((c) => JSON.parse(c.args[2].get('data'))), [
|
|
486
|
+
['data/schema.org.Thing/a'],
|
|
487
|
+
['data//' + EcCrypto.md5(other + '/5') + '/5'],
|
|
488
|
+
['data//' + EcCrypto.md5(other + '/5') + '/5']
|
|
489
|
+
]);
|
|
490
|
+
assert.deepEqual(out, []);
|
|
491
|
+
});
|
|
492
|
+
it('retries only the ids that no returned object matches', async () => {
|
|
493
|
+
const a = SERVER + 'data/schema.org.Thing/a';
|
|
494
|
+
const b = SERVER + 'data/schema.org.Thing/b';
|
|
495
|
+
const other = 'https://other.example/x/9/5';
|
|
496
|
+
const pre = sinon.stub(r, 'precache');
|
|
497
|
+
pre.callThrough();
|
|
498
|
+
post.onCall(0).resolves([a, b, other]);
|
|
499
|
+
post.onCall(1).resolves([obj(a)]);
|
|
500
|
+
post.onCall(2).resolves([]);
|
|
501
|
+
const out = await r.precache([a], null, null, eim);
|
|
502
|
+
assert.deepEqual(out.map((x) => x.id), [a]);
|
|
503
|
+
// a was requested and found; b and the foreign id were neither, so both are retried.
|
|
504
|
+
assert.deepEqual(pre.getCall(2).args[0], [b, other]);
|
|
505
|
+
});
|
|
506
|
+
it('posts unsigned when the repository is unsigned', async () => {
|
|
507
|
+
EcRepository.unsigned = true;
|
|
508
|
+
const sig = sinon.spy(eim, 'signatureSheet');
|
|
509
|
+
post.resolves([]);
|
|
510
|
+
await r.precache([SERVER + 'data/schema.org.Thing/a'], null, null, eim);
|
|
511
|
+
assert.isFalse(sig.called);
|
|
512
|
+
assert.isNull(post.firstCall.args[2].get('signatureSheet'));
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
describe('multidelete', () => {
|
|
517
|
+
it('rejects without urls', async () => {
|
|
518
|
+
const r = new EcRepository();
|
|
519
|
+
assert.equal((await settle(r.multidelete(null, null, null, eim))).error.message, 'urls not defined.');
|
|
520
|
+
});
|
|
521
|
+
it('posts the short forms of the URLs, signed unless unsigned', async () => {
|
|
522
|
+
const post = sinon.stub(EcRemote, 'postExpectingObject').resolves('gone');
|
|
523
|
+
const sig = sinon.spy(eim, 'signatureSheet');
|
|
524
|
+
const r = new EcRepository();
|
|
525
|
+
r.selectedServer = SERVER;
|
|
526
|
+
const local = SERVER + 'data/schema.org.Thing/abc';
|
|
527
|
+
const foreign = 'https://other.example/things/one';
|
|
528
|
+
assert.equal(await r.multidelete([local, foreign, thing(SERVER + 'data/schema.org.Thing/obj/9')], null, null, eim), 'gone');
|
|
529
|
+
assert.equal(post.firstCall.args[1], 'sky/repo/multiDelete');
|
|
530
|
+
assert.deepEqual(JSON.parse(post.firstCall.args[2].get('data')), ['data/schema.org.Thing/abc', 'data//' + EcCrypto.md5(foreign), 'data/schema.org.Thing/obj']);
|
|
531
|
+
assert.equal(post.firstCall.args[2].get('signatureSheet'), '[]');
|
|
532
|
+
assert.isTrue(sig.calledOnce);
|
|
533
|
+
EcRepository.unsigned = true;
|
|
534
|
+
await r.multidelete([local], null, null, eim);
|
|
535
|
+
assert.isTrue(sig.calledOnce);
|
|
536
|
+
assert.isNull(post.secondCall.args[2].get('signatureSheet'));
|
|
537
|
+
});
|
|
538
|
+
it('drops every cached form of the URLs when caching', async () => {
|
|
539
|
+
sinon.stub(EcRemote, 'postExpectingObject').resolves('gone');
|
|
540
|
+
EcRepository.caching = true;
|
|
541
|
+
const r = new EcRepository();
|
|
542
|
+
r.selectedServer = SERVER;
|
|
543
|
+
const url = 'https://other.example/things/one';
|
|
544
|
+
const keys = [url, EcRemoteLinkedData.veryShortId(SERVER, 'one'), EcRemoteLinkedData.veryShortId(SERVER, EcCrypto.md5(url)), 'other'];
|
|
545
|
+
for (const k of keys) EcRepository.cache[k] = thing(k);
|
|
546
|
+
await r.multidelete([url], null, null, eim);
|
|
547
|
+
assert.deepEqual(Object.keys(EcRepository.cacheBacking), ['other']);
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
describe('multiget', () => {
|
|
552
|
+
it('precaches, then gets each URL and drops what is missing', async () => {
|
|
553
|
+
const r = new EcRepository();
|
|
554
|
+
r.selectedServer = SERVER;
|
|
555
|
+
sinon.stub(r, 'precache').resolves([]);
|
|
556
|
+
const a = thing(SERVER + 'a');
|
|
557
|
+
sinon.stub(EcRepository, 'get').callsFake(async (url) => (url.endsWith('a') ? a : null));
|
|
558
|
+
const success = sinon.stub().returnsArg(0);
|
|
559
|
+
const out = await r.multiget([SERVER + 'a', SERVER + 'b'], success, null, eim);
|
|
560
|
+
assert.deepEqual(out, [a]);
|
|
561
|
+
assert.isTrue(success.calledOnceWith([a]));
|
|
562
|
+
assert.deepEqual(EcRepository.get.getCalls().map((c) => [c.args[0], c.args[3]]), [[SERVER + 'a', r], [SERVER + 'b', r]]);
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
describe('searchWithParams', () => {
|
|
567
|
+
let r, post;
|
|
568
|
+
const obj = (id) => ({ '@id': id, '@type': 'Thing', '@context': 'https://schema.org/' });
|
|
569
|
+
beforeEach(() => {
|
|
570
|
+
r = new EcRepository();
|
|
571
|
+
r.selectedServer = SERVER;
|
|
572
|
+
post = sinon.stub(EcRemote, 'postExpectingObject');
|
|
573
|
+
});
|
|
574
|
+
it('maps objects, calls eachSuccess and caches them when caching', async () => {
|
|
575
|
+
EcRepository.caching = true;
|
|
576
|
+
post.resolves([obj(SERVER + 'data/schema.org.Thing/a')]);
|
|
577
|
+
const each = sinon.spy();
|
|
578
|
+
const out = await r.searchWithParams('q', { size: 5 }, each, null, null, eim);
|
|
579
|
+
assert.deepEqual(out.map((x) => x.id), [SERVER + 'data/schema.org.Thing/a']);
|
|
580
|
+
assert.isTrue(each.calledOnce);
|
|
581
|
+
assert.equal(post.firstCall.args[1], 'sky/repo/search');
|
|
582
|
+
assert.equal(post.firstCall.args[2].get('data'), 'q');
|
|
583
|
+
assert.equal(post.firstCall.args[2].get('searchParams'), '{"size":5}');
|
|
584
|
+
assert.equal(post.firstCall.args[2].get('signatureSheet'), '[]');
|
|
585
|
+
assert.equal(EcRepository.cache[EcRemoteLinkedData.veryShortId(SERVER, 'a')].id, SERVER + 'data/schema.org.Thing/a');
|
|
586
|
+
});
|
|
587
|
+
it('rejects with a string when the server returns nothing', async () => {
|
|
588
|
+
post.resolves(null);
|
|
589
|
+
assert.strictEqual((await settle(r.searchWithParams('q', null, null, null, null, eim))).error, 'Error in search. See HTTP request for more details.');
|
|
590
|
+
});
|
|
591
|
+
it('signs unless unsigned is set on the repository or the call', async () => {
|
|
592
|
+
const sig = sinon.spy(eim, 'signatureSheet');
|
|
593
|
+
post.resolves([]);
|
|
594
|
+
await r.searchWithParams('q', { unsigned: true }, null, null, null, eim);
|
|
595
|
+
EcRepository.unsigned = true;
|
|
596
|
+
await r.searchWithParams('q', {}, null, null, null, eim);
|
|
597
|
+
assert.isFalse(sig.called);
|
|
598
|
+
assert.equal(post.firstCall.args[2].get('signatureSheet'), '');
|
|
599
|
+
EcRepository.unsigned = false;
|
|
600
|
+
await r.searchWithParams('q', { unsigned: 'true' }, null, null, null, eim);
|
|
601
|
+
assert.isTrue(sig.calledOnce);
|
|
602
|
+
});
|
|
603
|
+
it('requests ids under L2 caching and precaches them', async () => {
|
|
604
|
+
EcRepository.cachingL2 = true;
|
|
605
|
+
post.resolves([SERVER + 'data/schema.org.Thing/a/1', 'https://other.example/x/2/3']);
|
|
606
|
+
const found = thing(SERVER + 'data/schema.org.Thing/a/1');
|
|
607
|
+
const second = thing('https://other.example/x/2');
|
|
608
|
+
const pre = sinon.stub(r, 'precache');
|
|
609
|
+
pre.onFirstCall().resolves([found]);
|
|
610
|
+
pre.onSecondCall().resolves([second]);
|
|
611
|
+
const each = sinon.spy();
|
|
612
|
+
const out = await r.searchWithParams('q', null, each, null, null, eim);
|
|
613
|
+
assert.equal(post.firstCall.args[2].get('searchParams'), '{"ids":true}');
|
|
614
|
+
assert.deepEqual(pre.firstCall.args.slice(0, 5), [[SERVER + 'data/schema.org.Thing/a/1', 'https://other.example/x/2/3'], null, null, eim, true]);
|
|
615
|
+
// A non-repository id is retried as is; trimVersionFromUrl leaves it alone.
|
|
616
|
+
assert.deepEqual(pre.secondCall.args[0], ['https://other.example/x/2/3']);
|
|
617
|
+
assert.deepEqual(out, [found, second]);
|
|
618
|
+
assert.isTrue(each.calledOnceWith(found));
|
|
619
|
+
});
|
|
620
|
+
it('answers repeated searches from the search cache', async () => {
|
|
621
|
+
EcRepository.cachingSearch = true;
|
|
622
|
+
post.resolves([obj(SERVER + 'data/schema.org.Thing/a')]);
|
|
623
|
+
const first = await r.searchWithParams('q', null, null, null, null, eim);
|
|
624
|
+
const each = sinon.spy();
|
|
625
|
+
const second = await r.searchWithParams('q', null, each, null, null, eim);
|
|
626
|
+
assert.strictEqual(second, first);
|
|
627
|
+
assert.isTrue(post.calledOnce);
|
|
628
|
+
assert.isTrue(each.calledOnce);
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
describe('searchParamProps', () => {
|
|
633
|
+
const pk = (pem) => ({ ppk: { toPk: () => ({ toPem: () => pem }) } });
|
|
634
|
+
it('copies paging and type parameters', () => {
|
|
635
|
+
const r = new EcRepository();
|
|
636
|
+
const props = {};
|
|
637
|
+
assert.equal(r.searchParamProps('q', { start: 1, size: 2, types: 't', sort: 's', track_scores: true, index_hint: 'h', other: 1 }, props, eim), 'q');
|
|
638
|
+
assert.deepEqual(props, { start: 1, size: 2, types: 't', sort: 's', track_scores: true, index_hint: 'h' });
|
|
639
|
+
});
|
|
640
|
+
it('adds ownership clauses', () => {
|
|
641
|
+
const r = new EcRepository();
|
|
642
|
+
assert.equal(r.searchParamProps('q', { ownership: 'public' }, {}, eim), '(q) AND (_missing_:owner) AND (_missing_:@owner)');
|
|
643
|
+
assert.equal(r.searchParamProps('(q)', { ownership: 'owned' }, {}, eim), '(q) AND (_exists_:owner OR _exists_:@owner)');
|
|
644
|
+
assert.equal(r.searchParamProps('(a) OR (b', { ownership: 'other' }, {}, eim), '((a) OR (b)');
|
|
645
|
+
const me = Object.assign({}, eim, { ids: [pk('PEM1'), pk('PEM2')] });
|
|
646
|
+
assert.equal(r.searchParamProps('q', { ownership: 'me' }, {}, me), '(q) AND (\\*owner:"PEM1" OR \\*owner:"PEM2")');
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
describe('auto-detection', () => {
|
|
651
|
+
let hadWindow, oldWindow;
|
|
652
|
+
before(function () {
|
|
653
|
+
// These tests swap in a fake window, which a real browser (Cypress) does not allow.
|
|
654
|
+
if (typeof document !== 'undefined') this.skip();
|
|
655
|
+
});
|
|
656
|
+
beforeEach(() => {
|
|
657
|
+
sinon.stub(console, 'trace');
|
|
658
|
+
hadWindow = 'window' in global;
|
|
659
|
+
oldWindow = global.window;
|
|
660
|
+
});
|
|
661
|
+
afterEach(() => {
|
|
662
|
+
if (hadWindow) global.window = oldWindow;
|
|
663
|
+
else delete global.window;
|
|
664
|
+
});
|
|
665
|
+
function fakeWindow(protocol) {
|
|
666
|
+
global.window = {
|
|
667
|
+
location: { protocol, host: 'cass.example:3000', hostname: 'cass.example', pathname: '/app/page' },
|
|
668
|
+
document: {
|
|
669
|
+
createElement: () => {
|
|
670
|
+
const a = {};
|
|
671
|
+
Object.defineProperty(a, 'href', { set(v) { const u = new URL(v); a.host = u.host; a.pathname = u.pathname; } });
|
|
672
|
+
return a;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
it('tries every host, prefix and protocol in order until one answers', () => {
|
|
679
|
+
fakeWindow('http:');
|
|
680
|
+
const r = new EcRepository();
|
|
681
|
+
const guesses = [];
|
|
682
|
+
sinon.stub(r, 'autoDetectRepositoryActual').callsFake((g) => { guesses.push(g); return false; });
|
|
683
|
+
r.autoDetectRepository();
|
|
684
|
+
// 8 host forms x 6 prefixes x 2 protocols for an http: page.
|
|
685
|
+
assert.equal(guesses.length, 96);
|
|
686
|
+
assert.deepEqual(guesses.slice(0, 4), ['http://cass.example:3000/app/api/', 'https://cass.example:3000/app/api/', 'http://cass.example:3000/app/api/custom/', 'https://cass.example:3000/app/api/custom/']);
|
|
687
|
+
assert.deepEqual(guesses.slice(-2), ['http://cass.service.example:8080/api/custom/', 'https://cass.service.example:8080/api/custom/']);
|
|
688
|
+
const hit = 'https://cass.example:3000/app/api/custom/';
|
|
689
|
+
guesses.length = 0;
|
|
690
|
+
r.autoDetectRepositoryActual.callsFake((g) => { guesses.push(g); return g === hit; });
|
|
691
|
+
r.autoDetectRepository();
|
|
692
|
+
assert.equal(guesses[guesses.length - 1], hit);
|
|
693
|
+
assert.equal(guesses.length, 4);
|
|
694
|
+
});
|
|
695
|
+
it('starts from the selected server when there is one', () => {
|
|
696
|
+
fakeWindow('https:');
|
|
697
|
+
const r = new EcRepository();
|
|
698
|
+
r.selectedServer = 'https://chosen.example/x/api/';
|
|
699
|
+
const guesses = [];
|
|
700
|
+
sinon.stub(r, 'autoDetectRepositoryActual').callsFake((g) => { guesses.push(g); return false; });
|
|
701
|
+
r.autoDetectRepository();
|
|
702
|
+
assert.deepEqual(guesses.slice(0, 3), ['https://chosen.example/x/api/', 'https://chosen.example/app/api/', 'https://chosen.example/app/api/custom/']);
|
|
703
|
+
assert.equal(guesses.length, 7);
|
|
704
|
+
});
|
|
705
|
+
it('throws without a window when a server is selected', () => {
|
|
706
|
+
delete global.window;
|
|
707
|
+
const r = new EcRepository();
|
|
708
|
+
r.selectedServer = SERVER;
|
|
709
|
+
assert.throws(() => r.autoDetectRepository(), ReferenceError);
|
|
710
|
+
});
|
|
711
|
+
it('probes asynchronously, then tries custom endpoints, then gives up', () => {
|
|
712
|
+
fakeWindow('https:');
|
|
713
|
+
const clock = sinon.useFakeTimers();
|
|
714
|
+
const r = new EcRepository();
|
|
715
|
+
const guesses = [];
|
|
716
|
+
sinon.stub(r, 'autoDetectRepositoryActualAsync').callsFake((g) => guesses.push(g));
|
|
717
|
+
const failure = sinon.spy();
|
|
718
|
+
r.autoDetectRepositoryAsync(null, failure);
|
|
719
|
+
assert.isFalse(r.autoDetectFound);
|
|
720
|
+
// An https: page only tries https.
|
|
721
|
+
assert.equal(guesses.length, 32);
|
|
722
|
+
assert.deepEqual(guesses.slice(0, 2), ['https://cass.example:3000/app/api/', 'https://cass.example:3000/']);
|
|
723
|
+
clock.tick(5000);
|
|
724
|
+
// Each of the 32 probes then schedules every custom endpoint, each with its own give-up timer.
|
|
725
|
+
assert.equal(guesses.length, 32 + 512);
|
|
726
|
+
assert.equal(guesses[32], 'https://cass.example:3000/app/api/custom/');
|
|
727
|
+
clock.tick(5000);
|
|
728
|
+
assert.equal(failure.callCount, 512);
|
|
729
|
+
assert.isTrue(failure.alwaysCalledWith('Could not find service.'));
|
|
730
|
+
clock.restore();
|
|
731
|
+
});
|
|
732
|
+
it('probes both protocols asynchronously from an http page', () => {
|
|
733
|
+
fakeWindow('http:');
|
|
734
|
+
const clock = sinon.useFakeTimers();
|
|
735
|
+
try {
|
|
736
|
+
const r = new EcRepository();
|
|
737
|
+
const guesses = [];
|
|
738
|
+
sinon.stub(r, 'autoDetectRepositoryActualAsync').callsFake((g) => guesses.push(g));
|
|
739
|
+
r.autoDetectRepositoryAsync(null, sinon.spy());
|
|
740
|
+
assert.equal(guesses.length, 64);
|
|
741
|
+
assert.deepEqual(guesses.slice(0, 2), ['http://cass.example:3000/app/api/', 'https://cass.example:3000/app/api/']);
|
|
742
|
+
} finally {
|
|
743
|
+
clock.restore();
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
it('falls back to both protocols without a location, then fails reading it', () => {
|
|
747
|
+
global.window = { location: null };
|
|
748
|
+
const r = new EcRepository();
|
|
749
|
+
assert.throws(() => r.autoDetectRepositoryAsync(null, sinon.spy()), TypeError);
|
|
750
|
+
});
|
|
751
|
+
it('stops the async fallbacks once something answers', () => {
|
|
752
|
+
fakeWindow('https:');
|
|
753
|
+
const clock = sinon.useFakeTimers();
|
|
754
|
+
const r = new EcRepository();
|
|
755
|
+
const guesses = [];
|
|
756
|
+
sinon.stub(r, 'autoDetectRepositoryActualAsync').callsFake((g) => guesses.push(g));
|
|
757
|
+
const failure = sinon.spy();
|
|
758
|
+
r.autoDetectRepositoryAsync(null, failure);
|
|
759
|
+
r.autoDetectFound = true;
|
|
760
|
+
clock.tick(20000);
|
|
761
|
+
assert.equal(guesses.length, 32);
|
|
762
|
+
assert.isFalse(failure.called);
|
|
763
|
+
clock.restore();
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
describe('single probes', () => {
|
|
767
|
+
const NOW = 1700000000000;
|
|
768
|
+
let clock;
|
|
769
|
+
beforeEach(() => { clock = sinon.useFakeTimers({ now: NOW }); });
|
|
770
|
+
afterEach(() => clock.restore());
|
|
771
|
+
|
|
772
|
+
for (const method of ['autoDetectRepositoryActual', 'autoDetectRepositoryActualAsync']) {
|
|
773
|
+
it(`${method} accepts a pong and records the time offset`, () => {
|
|
774
|
+
const r = new EcRepository();
|
|
775
|
+
const success = sinon.spy();
|
|
776
|
+
sinon.stub(EcRemote, 'getExpectingObject').callsFake((g, svc, ok) => ok({ ping: 'pong', time: NOW + 50 }));
|
|
777
|
+
assert.isTrue(r[method]('https://g.example/api/', success));
|
|
778
|
+
assert.equal(r.selectedServer, 'https://g.example/api/');
|
|
779
|
+
assert.equal(r.timeOffset, 50);
|
|
780
|
+
assert.equal(EcRemote.getExpectingObject.firstCall.args[1], 'ping');
|
|
781
|
+
if (method.endsWith('Async')) assert.isTrue(success.calledOnce);
|
|
782
|
+
});
|
|
783
|
+
it(`${method} accepts "pong" in a failure body and ignores other replies`, () => {
|
|
784
|
+
const r = new EcRepository();
|
|
785
|
+
const success = sinon.spy();
|
|
786
|
+
const replies = [null, '', { indexOf: () => { throw new Error('x'); } }, 'nope', 'pong!'];
|
|
787
|
+
sinon.stub(EcRemote, 'getExpectingObject').callsFake((g, svc, ok, fail) => fail(replies.shift()));
|
|
788
|
+
for (let i = 0; i < 4; i++) {
|
|
789
|
+
assert.isFalse(r[method]('https://g.example/api/', success));
|
|
790
|
+
assert.isFalse(r.autoDetectFound);
|
|
791
|
+
}
|
|
792
|
+
assert.isTrue(r[method]('https://g.example/api/', success));
|
|
793
|
+
assert.equal(r.selectedServer, 'https://g.example/api/');
|
|
794
|
+
});
|
|
795
|
+
it(`${method} ignores empty guesses and swallows request errors`, () => {
|
|
796
|
+
const r = new EcRepository();
|
|
797
|
+
sinon.stub(EcRemote, 'getExpectingObject').throws(new Error('offline'));
|
|
798
|
+
assert.isFalse(r[method]('', null));
|
|
799
|
+
assert.isFalse(r[method](null, null));
|
|
800
|
+
assert.isFalse(EcRemote.getExpectingObject.called);
|
|
801
|
+
assert.isFalse(r[method]('https://g.example/api/', null));
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
it('autoDetectRepositoryActualAsync reports success only once', () => {
|
|
805
|
+
const r = new EcRepository();
|
|
806
|
+
const success = sinon.spy();
|
|
807
|
+
sinon.stub(EcRemote, 'getExpectingObject').callsFake((g, svc, ok) => ok({ ping: 'pong' }));
|
|
808
|
+
r.autoDetectRepositoryActualAsync('https://g1.example/api/', success);
|
|
809
|
+
r.autoDetectRepositoryActualAsync('https://g2.example/api/', success);
|
|
810
|
+
assert.isTrue(success.calledOnce);
|
|
811
|
+
assert.equal(r.selectedServer, 'https://g1.example/api/');
|
|
812
|
+
});
|
|
813
|
+
it('autoDetectRepositoryActual restores the request timeout', () => {
|
|
814
|
+
const r = new EcRepository();
|
|
815
|
+
const old = EcRemote.timeout;
|
|
816
|
+
let during;
|
|
817
|
+
sinon.stub(EcRemote, 'getExpectingObject').callsFake(() => { during = EcRemote.timeout; });
|
|
818
|
+
r.autoDetectRepositoryActual('https://g.example/api/');
|
|
819
|
+
assert.equal(during, 500);
|
|
820
|
+
assert.equal(EcRemote.timeout, old);
|
|
821
|
+
});
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
describe('fetchServerAdminKeys', () => {
|
|
826
|
+
it('copies the keys and hands them to success', async () => {
|
|
827
|
+
const r = new EcRepository();
|
|
828
|
+
r.selectedServer = SERVER;
|
|
829
|
+
const keys = ['k1', 'k2'];
|
|
830
|
+
sinon.stub(EcRemote, 'getExpectingObject').callsFake((s, svc, ok) => Promise.resolve(ok(keys)));
|
|
831
|
+
const success = sinon.spy();
|
|
832
|
+
await r.fetchServerAdminKeys(success);
|
|
833
|
+
assert.deepEqual(r.adminKeys, keys);
|
|
834
|
+
assert.notStrictEqual(r.adminKeys, keys);
|
|
835
|
+
assert.isTrue(success.calledOnceWith(keys));
|
|
836
|
+
assert.equal(EcRemote.getExpectingObject.firstCall.args[1], 'sky/admin');
|
|
837
|
+
assert.deepEqual(await r.fetchServerAdminKeys(), keys);
|
|
838
|
+
r.selectedServer = 'https://repo.example/api';
|
|
839
|
+
await r.fetchServerAdminKeys();
|
|
840
|
+
assert.equal(EcRemote.getExpectingObject.lastCall.args[1], '/sky/admin');
|
|
841
|
+
});
|
|
842
|
+
it('passes failures on, or throws them as Errors', () => {
|
|
843
|
+
const r = new EcRepository();
|
|
844
|
+
r.selectedServer = SERVER;
|
|
845
|
+
sinon.stub(EcRemote, 'getExpectingObject').callsFake((s, svc, ok, fail) => fail('nope'));
|
|
846
|
+
const failure = sinon.spy();
|
|
847
|
+
r.fetchServerAdminKeys(null, failure);
|
|
848
|
+
assert.isTrue(failure.calledOnceWith('nope'));
|
|
849
|
+
assert.throws(() => r.fetchServerAdminKeys(), 'nope');
|
|
850
|
+
});
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
describe('searchAs', () => {
|
|
854
|
+
it('decrypts results, drops ones that fail, and copies them into new objects', async () => {
|
|
855
|
+
const results = [thing('https://x/1'), thing('https://x/2'), thing('https://x/3')];
|
|
856
|
+
const r = { searchWithParams: sinon.stub().resolves(results) };
|
|
857
|
+
sinon.stub(EcEncryptedValue, 'fromEncryptedValue').callsFake(async (p1) => {
|
|
858
|
+
if (p1.id.endsWith('2')) throw new Error('cannot decrypt');
|
|
859
|
+
if (p1.id.endsWith('3')) return null;
|
|
860
|
+
return p1;
|
|
861
|
+
});
|
|
862
|
+
sinon.stub(console, 'log');
|
|
863
|
+
const factory = () => new EcRemoteLinkedData('https://schema.org/', 'Thing');
|
|
864
|
+
const out = await EcRepository.searchAs(r, 'name:x', factory, null, null, null, eim);
|
|
865
|
+
assert.deepEqual(out.map((x) => x.id), ['https://x/1']);
|
|
866
|
+
assert.instanceOf(out[0], EcRemoteLinkedData);
|
|
867
|
+
assert.notStrictEqual(out[0], results[0]);
|
|
868
|
+
assert.isTrue(console.log.calledWith('SearchAs error: Error: cannot decrypt'));
|
|
869
|
+
const [query, paramObj] = r.searchWithParams.firstCall.args;
|
|
870
|
+
assert.equal(query, '(name:x) AND ' + factory().getSearchStringByType());
|
|
871
|
+
assert.equal(paramObj.index_hint, '*thing,*encryptedvalue');
|
|
872
|
+
await EcRepository.searchAs(r, '', factory, null, null, null, eim);
|
|
873
|
+
assert.equal(r.searchWithParams.lastCall.args[0], factory().getSearchStringByType());
|
|
874
|
+
});
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
describe('negotiateTimeOffset', () => {
|
|
878
|
+
it('applies the ping settings and builds the key forwarding table', async () => {
|
|
879
|
+
const clock = sinon.useFakeTimers({ now: 1700000000000 });
|
|
880
|
+
try {
|
|
881
|
+
const r = new EcRepository();
|
|
882
|
+
r.selectedServer = SERVER;
|
|
883
|
+
sinon.stub(EcRemote, 'getExpectingObject').resolves({ ping: 'pong', time: 1700000000250, plugins: '["p1"]', postMaxSize: 99, signatureSheetHashAlgorithm: 'SHA-256' });
|
|
884
|
+
sinon.stub(r, 'buildKeyForwardingTable').resolves('built');
|
|
885
|
+
const login = sinon.spy();
|
|
886
|
+
const oldPlugins = EcRepository.defaultPlugins;
|
|
887
|
+
assert.equal(await r.negotiateTimeOffset(null, null, login), 'built');
|
|
888
|
+
assert.equal(r.timeOffset, 250);
|
|
889
|
+
assert.equal(r.postMaxSize, 99);
|
|
890
|
+
assert.equal(r.signatureSheetHashAlgorithm, 'SHA-256');
|
|
891
|
+
assert.deepEqual(EcRepository.defaultPlugins, ['p1']);
|
|
892
|
+
assert.isTrue(login.calledOnce);
|
|
893
|
+
EcRepository.defaultPlugins = oldPlugins;
|
|
894
|
+
EcRemote.getExpectingObject.resolves({ ping: 'nope' });
|
|
895
|
+
assert.isUndefined(await r.negotiateTimeOffset());
|
|
896
|
+
} finally {
|
|
897
|
+
clock.restore();
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
describe('buildKeyForwardingTable', () => {
|
|
903
|
+
it('adds every rekey request found, in order', async () => {
|
|
904
|
+
const order = [];
|
|
905
|
+
const reqs = [1, 2, 3].map((n) => ({ addRekeyRequestToForwardingTable: async () => { order.push(n); } }));
|
|
906
|
+
sinon.stub(EcRepository, 'searchAs').resolves(reqs);
|
|
907
|
+
const r = new EcRepository();
|
|
908
|
+
const success = sinon.spy();
|
|
909
|
+
await r.buildKeyForwardingTable(success, null, eim);
|
|
910
|
+
assert.deepEqual(order, [1, 2, 3]);
|
|
911
|
+
assert.isTrue(success.calledOnce);
|
|
912
|
+
assert.strictEqual(EcRepository.searchAs.firstCall.args[0], r);
|
|
913
|
+
assert.equal(EcRepository.searchAs.firstCall.args[1], '*');
|
|
914
|
+
assert.deepEqual(EcRepository.searchAs.firstCall.args[5], { size: 10000 });
|
|
915
|
+
});
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
describe('find', () => {
|
|
919
|
+
it('returns the search result whose id or short id is the URL', async () => {
|
|
920
|
+
const url = 'https://a/api/data/t/1';
|
|
921
|
+
const exact = thing(url);
|
|
922
|
+
const r = repo('https://a/api/', { search: sinon.stub().resolves([thing('https://a/api/data/t/2'), exact]) });
|
|
923
|
+
EcRepository.repos = [r];
|
|
924
|
+
assert.strictEqual(await EcRepository.find(url, 'err', {}, 0, null, null, eim), exact);
|
|
925
|
+
assert.equal(r.search.firstCall.args[0], '@id:"' + url + '"');
|
|
926
|
+
const versioned = thing(url + '/123');
|
|
927
|
+
r.search.resolves([versioned]);
|
|
928
|
+
assert.strictEqual(await EcRepository.find(url, 'err', {}, 0, null, null, eim), versioned);
|
|
929
|
+
});
|
|
930
|
+
it('caches the match when caching', async () => {
|
|
931
|
+
EcRepository.caching = true;
|
|
932
|
+
const url = 'https://a/api/data/t/1';
|
|
933
|
+
const exact = thing(url);
|
|
934
|
+
EcRepository.repos = [repo('https://a/api/', { search: sinon.stub().resolves([exact]) })];
|
|
935
|
+
await EcRepository.find(url, 'err', {}, 0, null, null, eim);
|
|
936
|
+
assert.strictEqual(EcRepository.cache[url], exact);
|
|
937
|
+
});
|
|
938
|
+
it('moves to the next repository when nothing matches, then rejects with the error', async () => {
|
|
939
|
+
const a = repo('https://a/api/', { search: sinon.stub().resolves([thing('https://a/api/data/t/2')]) });
|
|
940
|
+
const b = repo('https://b/api/', { search: sinon.stub().resolves([]) });
|
|
941
|
+
const c = repo('https://c/api/', { search: sinon.stub().resolves(null) });
|
|
942
|
+
EcRepository.repos = [a, b, c];
|
|
943
|
+
const res = await settle(EcRepository.find('https://z/api/data/t/1', 'not here', {}, 0, null, null, eim));
|
|
944
|
+
// Each repository passed through wraps the error in another Error.
|
|
945
|
+
assert.equal(res.error.message, 'Error: Error: Error: not here');
|
|
946
|
+
assert.isTrue(a.search.calledOnce && b.search.calledOnce && c.search.calledOnce);
|
|
947
|
+
});
|
|
948
|
+
it('skips repositories already searched and those without a server', async () => {
|
|
949
|
+
const a = repo('https://a/api/', { search: sinon.stub().resolves([]) });
|
|
950
|
+
EcRepository.repos = [repo(null), a, a];
|
|
951
|
+
await settle(EcRepository.find('https://z/1', 'e', {}, 0, null, null, eim));
|
|
952
|
+
assert.isTrue(a.search.calledOnce);
|
|
953
|
+
});
|
|
954
|
+
it('carries a search failure on as the error', async () => {
|
|
955
|
+
const a = repo('https://a/api/', { search: sinon.stub().rejects('search broke') });
|
|
956
|
+
EcRepository.repos = [a];
|
|
957
|
+
const res = await settle(EcRepository.find('https://z/1', 'e', {}, 0, null, null, eim));
|
|
958
|
+
assert.equal(res.error.message, 'search broke');
|
|
959
|
+
});
|
|
960
|
+
});
|
|
961
|
+
});
|