@vida-global/core 2.5.0 → 3.0.1
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/README.md +1 -0
- package/index.js +4 -0
- package/lib/activeRecord/baseRecord.js +2 -2
- package/lib/search/README.md +232 -0
- package/lib/search/abstractSearchClient.js +188 -0
- package/lib/search/errors.js +31 -0
- package/lib/search/index.js +16 -0
- package/lib/search/openSearchClient.js +319 -0
- package/lib/search/query.js +163 -0
- package/lib/server/authorizer.js +42 -0
- package/lib/server/index.js +4 -2
- package/lib/server/serverController.js +37 -3
- package/package.json +2 -1
- package/test/activeRecord/baseRecord.test.js +22 -0
- package/test/activeRecord/db/schema.test.js +15 -0
- package/test/search/abstractSearchClient.test.js +354 -0
- package/test/search/errors.test.js +41 -0
- package/test/search/helpers/searchClient.js +137 -0
- package/test/search/openSearchClient.test.js +514 -0
- package/test/search/query.test.js +286 -0
- package/test/search/serializer.test.js +202 -0
- package/test/server/authorizer.test.js +105 -0
- package/test/server/helpers/authorizer.js +19 -0
- package/test/server/serverController.test.js +77 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
const { BulkWriteError, UnsupportedOperationError } = require('../../lib/search/errors');
|
|
2
|
+
const { OpenSearchClient } = require('../../lib/search/openSearchClient');
|
|
3
|
+
const TestHelpers = require('@vida-global/test-helpers');
|
|
4
|
+
const {
|
|
5
|
+
MOCK_ENDPOINT,
|
|
6
|
+
MOCK_INDEX,
|
|
7
|
+
MOCK_MAPPING,
|
|
8
|
+
buildHit,
|
|
9
|
+
buildOpenSearchClient,
|
|
10
|
+
buildSearchResponse,
|
|
11
|
+
buildUnsupportedError
|
|
12
|
+
} = require('./helpers/searchClient');
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
const randomString = () => TestHelpers.Faker.Text.randomString();
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
describe('OpenSearchClient', () => {
|
|
19
|
+
let client;
|
|
20
|
+
let index;
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
client = buildOpenSearchClient();
|
|
25
|
+
index = MOCK_INDEX;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
jest.clearAllMocks();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
describe('#endpoint', () => {
|
|
35
|
+
it('is not implemented until a subclass declares it', () => {
|
|
36
|
+
class BareClient extends OpenSearchClient {}
|
|
37
|
+
|
|
38
|
+
expect(() => new BareClient().endpoint).toThrow('Not Implemented');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
describe('index names', () => {
|
|
44
|
+
it('writes to the plain index when there is no suffix', () => {
|
|
45
|
+
expect(client.writeIndex).toBe(index);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
it('writes to the suffixed index when there is a suffix', () => {
|
|
50
|
+
expect(buildOpenSearchClient({indexSuffix: '2026-10'}).writeIndex)
|
|
51
|
+
.toBe(`${index}-2026-10`);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
it('reads across every suffixed index through the wildcard', () => {
|
|
56
|
+
expect(client.wildcardIndex).toBe(`${index}-*`);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
describe('#_execute', () => {
|
|
62
|
+
it('searches the write index by default', async () => {
|
|
63
|
+
const body = {query: {}};
|
|
64
|
+
|
|
65
|
+
await client._execute(body, {});
|
|
66
|
+
|
|
67
|
+
expect(client.openSearchDouble.search).toHaveBeenCalledTimes(1);
|
|
68
|
+
expect(client.openSearchDouble.search).toHaveBeenCalledWith({index, body});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
it('searches the wildcard index when asked to', async () => {
|
|
73
|
+
const body = {query: {}};
|
|
74
|
+
|
|
75
|
+
await client._execute(body, {indexWildcard: true});
|
|
76
|
+
|
|
77
|
+
expect(client.openSearchDouble.search)
|
|
78
|
+
.toHaveBeenCalledWith({index: `${index}-*`, body});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
describe('response hooks', () => {
|
|
84
|
+
const response = buildSearchResponse([buildHit('a', {name: 'first'}, 9.5)], 57);
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
it('reads the hit container off the response', () => {
|
|
88
|
+
expect(client._hitsFromResponse(response)).toBe(response.body.hits);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
it('reads the exact total off the response', () => {
|
|
93
|
+
expect(client._totalFromResponse(response)).toBe(57);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
it('reports zero when the response carries no total', () => {
|
|
98
|
+
expect(client._totalFromResponse({body: {hits: {hits: []}}})).toBe(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
it.each([
|
|
103
|
+
['_idFromHit', 'a'],
|
|
104
|
+
['_scoreFromHit', 9.5],
|
|
105
|
+
['_documentFromHit', {name: 'first'}],
|
|
106
|
+
])('%s reads its field off the hit', (hook, expected) => {
|
|
107
|
+
expect(client[hook](response.body.hits.hits[0])).toEqual(expected);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
it('shapes a whole response into the standard envelope', () => {
|
|
112
|
+
expect(client._processResponse(response)).toEqual({
|
|
113
|
+
total: 57,
|
|
114
|
+
results: [{id: 'a', score: 9.5, document: {name: 'first'}}]
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
describe('#_serializeConditions', () => {
|
|
121
|
+
const serialize = (build) => client._serializeConditions(build(client.query()).conditions);
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
it('always asks OpenSearch for an exact total', () => {
|
|
125
|
+
expect(serialize(query => query).track_total_hits).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
it('puts and conditions into must and not conditions into must_not', () => {
|
|
130
|
+
const body = serialize(query => query
|
|
131
|
+
.equals('status', 'success')
|
|
132
|
+
.not({operator: 'exists', field: 'deletedAt'}));
|
|
133
|
+
|
|
134
|
+
expect(body.query).toEqual({
|
|
135
|
+
bool: {
|
|
136
|
+
must: [{term: {status: {value: 'success'}}}],
|
|
137
|
+
must_not: [{exists: {field: 'deletedAt'}}]
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
it('wraps or conditions in a should group that still filters', () => {
|
|
144
|
+
const body = serialize(query => query
|
|
145
|
+
.equals('status', 'success')
|
|
146
|
+
.or({operator: 'equals', field: 'from', value: 'x'},
|
|
147
|
+
{operator: 'equals', field: 'to', value: 'x'}));
|
|
148
|
+
|
|
149
|
+
expect(body.query.bool.must).toEqual([
|
|
150
|
+
{term: {status: {value: 'success'}}},
|
|
151
|
+
{
|
|
152
|
+
bool: {
|
|
153
|
+
should: [
|
|
154
|
+
{term: {from: {value: 'x'}}},
|
|
155
|
+
{term: {to: {value: 'x'}}}
|
|
156
|
+
],
|
|
157
|
+
minimum_should_match: 1
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
it('drops the bool wrapper around a lone clause', () => {
|
|
165
|
+
expect(serialize(query => query.exists('summary')).query)
|
|
166
|
+
.toEqual({exists: {field: 'summary'}});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
it('keeps the bool wrapper once there is more than one clause', () => {
|
|
171
|
+
const body = serialize(query => query.exists('summary').exists('message'));
|
|
172
|
+
|
|
173
|
+
expect(body.query.bool.must).toHaveLength(2);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
it('matches everything when no condition was added', () => {
|
|
178
|
+
expect(serialize(query => query).query).toEqual({bool: {must: [], must_not: []}});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
it('serializes a nested sub-query tree', () => {
|
|
183
|
+
const subQuery = client.query().matches('addresses.address', 'home');
|
|
184
|
+
const body = serialize(query => query.nested('addresses', subQuery));
|
|
185
|
+
|
|
186
|
+
expect(body.query).toEqual({
|
|
187
|
+
nested: {
|
|
188
|
+
path: 'addresses',
|
|
189
|
+
query: {match: {'addresses.address': {query: 'home', fuzziness: 'AUTO'}}}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
it.each([
|
|
196
|
+
['equals', query => query.equals('status', 'ok'), {term: {status: {value: 'ok'}}}],
|
|
197
|
+
['in', query => query.in('roomId', ['a', 'b']), {terms: {roomId: ['a', 'b']}}],
|
|
198
|
+
['range', query => query.range('ts', {gte: 1, lte: 9}), {range: {ts: {gte: 1, lte: 9}}}],
|
|
199
|
+
['exists', query => query.exists('summary'), {exists: {field: 'summary'}}],
|
|
200
|
+
['missing', query => query.missing('summary'), {bool: {must_not: {exists: {field: 'summary'}}}}],
|
|
201
|
+
['phrase', query => query.phrase('message', 'hi'), {match_phrase: {message: {query: 'hi'}}}],
|
|
202
|
+
])('serializes a %s leaf', (_operator, build, expected) => {
|
|
203
|
+
expect(serialize(build).query).toEqual(expected);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
it('serializes a matches leaf with its fuzziness', () => {
|
|
208
|
+
const body = serialize(query => query.matches('message', 'hi', {fuzziness: 2}));
|
|
209
|
+
|
|
210
|
+
expect(body.query).toEqual({match: {message: {query: 'hi', fuzziness: 2}}});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
it('serializes a fullText leaf by gluing each boost onto its field name', () => {
|
|
215
|
+
const body = serialize(query => query.fullText('hi', {
|
|
216
|
+
fields: ['message', 'summary'],
|
|
217
|
+
boost: {summary: 5}
|
|
218
|
+
}));
|
|
219
|
+
|
|
220
|
+
expect(body.query).toEqual({
|
|
221
|
+
query_string: {query: 'hi', fields: ['message^1', 'summary^5']}
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
it('wraps a fullText leaf in asterisks only when the caller asks', () => {
|
|
227
|
+
const body = serialize(query => query.fullText('hi', {
|
|
228
|
+
fields: ['message'],
|
|
229
|
+
wildcard: true
|
|
230
|
+
}));
|
|
231
|
+
|
|
232
|
+
expect(body.query).toEqual({
|
|
233
|
+
query_string: {query: '*hi*', fields: ['message^1']}
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
it('maps paging, score floor and sort onto the request body', () => {
|
|
239
|
+
const body = serialize(query => query
|
|
240
|
+
.limit(50)
|
|
241
|
+
.offset(100)
|
|
242
|
+
.minScore(5)
|
|
243
|
+
.sort('timestamp', 'desc')
|
|
244
|
+
.sort('score', 'asc'));
|
|
245
|
+
|
|
246
|
+
expect(body.size).toBe(50);
|
|
247
|
+
expect(body.from).toBe(100);
|
|
248
|
+
expect(body.min_score).toBe(5);
|
|
249
|
+
expect(body.sort).toEqual([
|
|
250
|
+
{timestamp: {order: 'desc'}},
|
|
251
|
+
{score: {order: 'asc'}}
|
|
252
|
+
]);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
it('leaves paging keys out when they were never set', () => {
|
|
257
|
+
const body = serialize(query => query.equals('status', 'ok'));
|
|
258
|
+
|
|
259
|
+
expect(body).not.toHaveProperty('size');
|
|
260
|
+
expect(body).not.toHaveProperty('from');
|
|
261
|
+
expect(body).not.toHaveProperty('min_score');
|
|
262
|
+
expect(body).not.toHaveProperty('sort');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
it('keeps an explicit zero offset', () => {
|
|
267
|
+
expect(serialize(query => query.offset(0)).from).toBe(0);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
describe('#_upsert', () => {
|
|
273
|
+
it('updates the document in place and creates it when it is new', async () => {
|
|
274
|
+
const id = randomString();
|
|
275
|
+
const document = {name: randomString()};
|
|
276
|
+
|
|
277
|
+
await client._upsert(id, document);
|
|
278
|
+
|
|
279
|
+
expect(client.openSearchDouble.update).toHaveBeenCalledTimes(1);
|
|
280
|
+
expect(client.openSearchDouble.update).toHaveBeenCalledWith({
|
|
281
|
+
id,
|
|
282
|
+
index,
|
|
283
|
+
body: {doc: document, doc_as_upsert: true}
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
it('turns a collection refusal into UnsupportedOperationError', async () => {
|
|
289
|
+
const refusal = buildUnsupportedError();
|
|
290
|
+
client.openSearchDouble.update.mockRejectedValue(refusal);
|
|
291
|
+
|
|
292
|
+
await expect(client._upsert('a', {})).rejects.toThrow(UnsupportedOperationError);
|
|
293
|
+
await expect(client._upsert('a', {})).rejects.toThrow(/upsert is not supported/);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
it('keeps the refusal as the cause', async () => {
|
|
298
|
+
const refusal = buildUnsupportedError();
|
|
299
|
+
client.openSearchDouble.update.mockRejectedValue(refusal);
|
|
300
|
+
|
|
301
|
+
await client._upsert('a', {}).catch((error) => {
|
|
302
|
+
expect(error.cause).toBe(refusal);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
it('lets every other error through untouched', async () => {
|
|
308
|
+
const failure = Object.assign(new Error('gateway timeout'), {statusCode: 504});
|
|
309
|
+
client.openSearchDouble.update.mockRejectedValue(failure);
|
|
310
|
+
|
|
311
|
+
await expect(client._upsert('a', {})).rejects.toBe(failure);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
describe('#_isUnsupportedOperation', () => {
|
|
317
|
+
it.each([
|
|
318
|
+
[400, 'Document ID is not supported for this collection', true],
|
|
319
|
+
[400, 'mapper_parsing_exception', false],
|
|
320
|
+
[404, 'index is not supported', false],
|
|
321
|
+
[503, 'service unavailable', false],
|
|
322
|
+
])('reads a %i "%s" as %s', (statusCode, message, expected) => {
|
|
323
|
+
const error = Object.assign(new Error(message), {statusCode});
|
|
324
|
+
|
|
325
|
+
expect(client._isUnsupportedOperation(error)).toBe(expected);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
it('reads the status out of the error meta when it is not on the error', () => {
|
|
330
|
+
const error = Object.assign(new Error('not supported'), {meta: {statusCode: 400}});
|
|
331
|
+
|
|
332
|
+
expect(client._isUnsupportedOperation(error)).toBe(true);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
describe('#_append', () => {
|
|
338
|
+
it('writes the document without an id', async () => {
|
|
339
|
+
const document = {name: randomString()};
|
|
340
|
+
|
|
341
|
+
await client._append(document);
|
|
342
|
+
|
|
343
|
+
expect(client.openSearchDouble.index).toHaveBeenCalledTimes(1);
|
|
344
|
+
expect(client.openSearchDouble.index).toHaveBeenCalledWith({index, body: document});
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
describe('#_delete', () => {
|
|
350
|
+
it('deletes the document by id', async () => {
|
|
351
|
+
const id = randomString();
|
|
352
|
+
|
|
353
|
+
await client._delete(id);
|
|
354
|
+
|
|
355
|
+
expect(client.openSearchDouble.delete).toHaveBeenCalledTimes(1);
|
|
356
|
+
expect(client.openSearchDouble.delete).toHaveBeenCalledWith({id, index});
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
describe('#_bulkUpsert', () => {
|
|
362
|
+
const documents = [
|
|
363
|
+
{id: 'a', document: {name: 'first'}},
|
|
364
|
+
{id: 'b', document: {name: 'second'}}
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
it('sends one action pair per document', async () => {
|
|
369
|
+
client.openSearchDouble.bulk.mockResolvedValue({body: {errors: false}});
|
|
370
|
+
|
|
371
|
+
await client._bulkUpsert(documents);
|
|
372
|
+
|
|
373
|
+
expect(client.openSearchDouble.bulk).toHaveBeenCalledWith({
|
|
374
|
+
body: [
|
|
375
|
+
{update: {_id: 'a', _index: index}},
|
|
376
|
+
{doc: {name: 'first'}, doc_as_upsert: true},
|
|
377
|
+
{update: {_id: 'b', _index: index}},
|
|
378
|
+
{doc: {name: 'second'}, doc_as_upsert: true}
|
|
379
|
+
]
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
it('throws with every failed id when the response reports errors', async () => {
|
|
385
|
+
client.openSearchDouble.bulk.mockResolvedValue({
|
|
386
|
+
body: {
|
|
387
|
+
errors: true,
|
|
388
|
+
items: [
|
|
389
|
+
{update: {_id: 'a'}},
|
|
390
|
+
{update: {_id: 'b', error: {reason: 'mapper_parsing_exception'}}}
|
|
391
|
+
]
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
await expect(client._bulkUpsert(documents)).rejects.toThrow(BulkWriteError);
|
|
396
|
+
|
|
397
|
+
await client._bulkUpsert(documents).catch((error) => {
|
|
398
|
+
expect(error.failures).toEqual([{id: 'b', reason: 'mapper_parsing_exception'}]);
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
it('turns a collection refusal into UnsupportedOperationError', async () => {
|
|
404
|
+
client.openSearchDouble.bulk.mockRejectedValue(buildUnsupportedError());
|
|
405
|
+
|
|
406
|
+
await expect(client._bulkUpsert(documents))
|
|
407
|
+
.rejects.toThrow(/bulkUpsert is not supported/);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
describe('#syncIndex', () => {
|
|
413
|
+
it('creates the index and then pushes the mapping when it is missing', async () => {
|
|
414
|
+
client.openSearchDouble.indices.exists.mockResolvedValue({body: false});
|
|
415
|
+
|
|
416
|
+
await client.syncIndex();
|
|
417
|
+
|
|
418
|
+
expect(client.openSearchDouble.indices.create).toHaveBeenCalledWith({index});
|
|
419
|
+
expect(client.openSearchDouble.indices.putMapping)
|
|
420
|
+
.toHaveBeenCalledWith({index, body: MOCK_MAPPING});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
it('only pushes the mapping when the index already exists', async () => {
|
|
425
|
+
client.openSearchDouble.indices.exists.mockResolvedValue({body: true});
|
|
426
|
+
|
|
427
|
+
await client.syncIndex();
|
|
428
|
+
|
|
429
|
+
expect(client.openSearchDouble.indices.create).not.toHaveBeenCalled();
|
|
430
|
+
expect(client.openSearchDouble.indices.putMapping).toHaveBeenCalledTimes(1);
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
describe('#shouldRetry', () => {
|
|
436
|
+
const errorWithStatus = (statusCode) => Object.assign(new Error('boom'), {statusCode});
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
it('never retries when numRetries is zero', () => {
|
|
440
|
+
expect(client.shouldRetry(1, errorWithStatus(503))).toBe(false);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
it.each([
|
|
445
|
+
[500, true],
|
|
446
|
+
[503, true],
|
|
447
|
+
[599, true],
|
|
448
|
+
[404, false],
|
|
449
|
+
[429, false],
|
|
450
|
+
])('retries a %i response: %s', (statusCode, expected) => {
|
|
451
|
+
const retryingClient = buildOpenSearchClient({numRetries: 3});
|
|
452
|
+
|
|
453
|
+
expect(retryingClient.shouldRetry(1, errorWithStatus(statusCode))).toBe(expected);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
it('reads the status out of the error meta when it is not on the error', () => {
|
|
458
|
+
const retryingClient = buildOpenSearchClient({numRetries: 3});
|
|
459
|
+
const error = Object.assign(new Error('boom'), {meta: {statusCode: 502}});
|
|
460
|
+
|
|
461
|
+
expect(retryingClient.shouldRetry(1, error)).toBe(true);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
it('stops once the attempts are used up', () => {
|
|
466
|
+
const retryingClient = buildOpenSearchClient({numRetries: 1});
|
|
467
|
+
|
|
468
|
+
expect(retryingClient.shouldRetry(2, errorWithStatus(503))).toBe(false);
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
describe('client configuration', () => {
|
|
474
|
+
it('builds the client once and reuses it', async () => {
|
|
475
|
+
await client._delete('a');
|
|
476
|
+
await client._delete('b');
|
|
477
|
+
|
|
478
|
+
expect(client.clientClassDouble).toHaveBeenCalledTimes(1);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
it('points the client at the endpoint and spreads in the signer', async () => {
|
|
483
|
+
await client._delete('a');
|
|
484
|
+
|
|
485
|
+
expect(client.clientClassDouble).toHaveBeenCalledWith({
|
|
486
|
+
Connection: 'signed-connection',
|
|
487
|
+
node: MOCK_ENDPOINT
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
it('signs for the serverless collection service', async () => {
|
|
493
|
+
await client._delete('a');
|
|
494
|
+
|
|
495
|
+
expect(client.signerDouble).toHaveBeenCalledWith(expect.objectContaining({
|
|
496
|
+
service: 'aoss'
|
|
497
|
+
}));
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
it('resolves credentials from the environment when the signer asks', async () => {
|
|
502
|
+
process.env.OPENSEARCH_AWS_KEY = 'key';
|
|
503
|
+
process.env.OPENSEARCH_AWS_SECRET = 'secret';
|
|
504
|
+
|
|
505
|
+
await expect(client.getCredentials()).resolves.toEqual({
|
|
506
|
+
accessKeyId: 'key',
|
|
507
|
+
secretAccessKey: 'secret'
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
delete process.env.OPENSEARCH_AWS_KEY;
|
|
511
|
+
delete process.env.OPENSEARCH_AWS_SECRET;
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
});
|