UNPKG

@elastic.io/maester-client

Version:
417 lines (395 loc) 17.6 kB
/* eslint-disable no-unused-expressions */ import nock from 'nock'; import sinon from 'sinon'; import getStream from 'get-stream'; import { expect } from 'chai'; import { ObjectStorage, StorageClient } from '../src'; import { encryptStream, decryptStream, zip, unzip, streamFromObject } from './helpers'; import { PotentiallyConsumedStreamError } from '../src/errors'; import { RETRIES_COUNT } from '../src/interfaces'; describe('Object Storage', () => { const config = { uri: 'https://ma.es.ter', jwtSecret: 'jwt', userAgent: 'userAgent' }; const objectStorage = new ObjectStorage(config); const postData = { test: 'test' }; const createdObjWithQueryField = { contentType: 'application/json', createdAt: 1622811501107, objectId: '2bd48165-119f-489d-8842-8d07b2c7cc1b', metadata: {}, queriableFields: { demosearchfield: 'qwerty', }, }; const responseData = { contentLength: 'meta.contentLength', contentType: 'meta.contentType', createdAt: 'meta.createdAt', md5: 'meta.md5Hash', objectId: 'obj.id', metadata: 'meta.userMetadata', }; let finalReqCfg; afterEach(sinon.restore); before(() => { process.env.ELASTICIO_FLOW_ID = 'flow_id'; process.env.ELASTICIO_STEP_ID = 'step_id'; }); describe('basic', () => { describe('data mode', () => { describe('should getAllByParams', () => { beforeEach(async () => { finalReqCfg = sinon.stub(StorageClient.prototype, <any>'requestRetry').callsFake(async () => ( { data: streamFromObject([createdObjWithQueryField, createdObjWithQueryField]) } )); }); it('should getAllByParams', async () => { const objectStorage2 = new ObjectStorage({ ...config, msgId: 'msgId' }); const result = await objectStorage2.getAllByParams({ foo: 'bar' }); expect(result).to.deep.equal([createdObjWithQueryField, createdObjWithQueryField]); const { firstArg, lastArg } = finalReqCfg.getCall(0); expect(lastArg).to.be.deep.equal({}); expect(firstArg.getFreshStream).to.be.equal(undefined); expect(firstArg.axiosReqConfig).to.deep.equal({ method: 'get', url: '/objects', responseType: 'stream', params: { foo: 'bar' }, headers: { Authorization: 'Bearer jwt', 'User-Agent': 'userAgent axios/^1.8.2', 'x-request-id': 'f:flow_id;s:step_id;m:msgId', } }); }); }); describe('should getById (stream)', () => { beforeEach(async () => { finalReqCfg = sinon.stub(StorageClient.prototype, <any>'requestRetry').callsFake(async () => ({ data: streamFromObject({ q: 'i`m a stream' }) })); }); it('should getById (stream)', async () => { const { data } = await objectStorage.getOne('objectId', { responseType: 'stream' }); const streamAsJSON = await getStream(data); expect(JSON.parse(streamAsJSON)).to.be.deep.equal({ q: 'i`m a stream' }); const { firstArg, lastArg } = finalReqCfg.getCall(0); expect(lastArg).to.be.deep.equal({}); expect(firstArg.getFreshStream).to.be.equal(undefined); expect(firstArg.axiosReqConfig).to.deep.equal({ method: 'get', url: '/objects/objectId', responseType: 'stream', params: {}, headers: { Authorization: 'Bearer jwt', 'User-Agent': 'userAgent axios/^1.8.2', 'x-request-id': 'f:flow_id;s:step_id;m:', } }); }); }); describe('should getById (json)', () => { beforeEach(async () => { finalReqCfg = sinon.stub(StorageClient.prototype, <any>'requestRetry').callsFake(async () => ({ data: streamFromObject({ q: 'i`m a stream' }) })); }); it('should getById (json)', async () => { const { data } = await objectStorage.getOne('objectId', { responseType: 'json' }); expect(data).to.be.deep.equal({ q: 'i`m a stream' }); const { firstArg, lastArg } = finalReqCfg.getCall(0); expect(lastArg).to.be.deep.equal({}); expect(firstArg.getFreshStream).to.be.equal(undefined); expect(firstArg.axiosReqConfig).to.deep.equal({ method: 'get', url: '/objects/objectId', responseType: 'stream', params: {}, headers: { Authorization: 'Bearer jwt', 'User-Agent': 'userAgent axios/^1.8.2', 'x-request-id': 'f:flow_id;s:step_id;m:', } }); }); }); describe('should getById (arraybuffer)', () => { beforeEach(async () => { finalReqCfg = sinon.stub(StorageClient.prototype, <any>'requestRetry').callsFake(async () => ({ data: streamFromObject({ q: 'i`m a stream' }) })); }); it('should getById (arraybuffer)', async () => { const { data } = await objectStorage.getOne('objectId', { responseType: 'arraybuffer' }); const encodedResult = Buffer.from(JSON.stringify({ q: 'i`m a stream' }), 'binary').toString('base64'); expect(data.toString('base64')).to.be.equal(encodedResult); const { firstArg, lastArg } = finalReqCfg.getCall(0); expect(lastArg).to.be.deep.equal({}); expect(firstArg.getFreshStream).to.be.equal(undefined); expect(firstArg.axiosReqConfig).to.deep.equal({ method: 'get', url: '/objects/objectId', responseType: 'stream', params: {}, headers: { Authorization: 'Bearer jwt', 'User-Agent': 'userAgent axios/^1.8.2', 'x-request-id': 'f:flow_id;s:step_id;m:', } }); }); }); }); describe('stream mode', () => { it(`should fail after ${RETRIES_COUNT.defaultValue} get retries`, async () => { const objectStorageCalls = nock(config.uri) .matchHeader('authorization', `Bearer ${config.jwtSecret}`) .get('/objects/1') .times(RETRIES_COUNT.defaultValue) .reply(500); await expect(objectStorage.getOne('1')).to.be.rejectedWith('Server error during request'); expect(objectStorageCalls.isDone()).to.be.true; }); it('should retry get request on errors', async () => { const objectStorageCalls = nock(config.uri) .matchHeader('authorization', `Bearer ${config.jwtSecret}`) .get('/objects/1') .reply(500) .get('/objects/1') .reply(200, streamFromObject(responseData)); const { data } = await objectStorage.getOne('1', { responseType: 'json' }); expect(objectStorageCalls.isDone()).to.be.true; expect(data).to.be.deep.equal(responseData); }); it('should throw an error on post request connection error', async () => { const objectStorageCalls = nock(config.uri) .matchHeader('authorization', `Bearer ${config.jwtSecret}`) .post('/objects') .times(3) .replyWithError({ code: 'ECONNREFUSED' }); await expect(objectStorage.add(postData, {})).to.be.rejectedWith('Server error during request'); expect(objectStorageCalls.isDone()).to.be.true; }); it('should throw an error immediately on post request http error', async () => { const objectStorageCalls = nock(config.uri) .matchHeader('authorization', `Bearer ${config.jwtSecret}`) .post('/objects') .reply(409); await expect(objectStorage.add(postData, {})).to.be.rejectedWith('Request failed with status code 409'); expect(objectStorageCalls.isDone()).to.be.true; }); it('should post successfully', async () => { const objectStorageCalls = nock(config.uri) .matchHeader('authorization', `Bearer ${config.jwtSecret}`) .post('/objects') .reply(200); const objectId = await objectStorage.add(postData, {}); expect(objectStorageCalls.isDone()).to.be.true; expect(objectId).to.match(/^[0-9a-z-]+$/); }); }); }); describe('custom headers', () => { it('should post successfully', async () => { const objectStorageCalls = nock(config.uri) .matchHeader('authorization', `Bearer ${config.jwtSecret}`) .matchHeader('content-type', 'some-type') .matchHeader('x-eio-ttl', '1') .matchHeader('x-meta-k', 'v') .matchHeader('x-query-k', 'v') .post('/objects') .reply(200, streamFromObject({ objectId: 'dfsf-2dasd3-dsf2l' })); const response = await objectStorage.add(postData, { headers: { 'content-type': 'some-type', 'x-eio-ttl': 1, 'x-meta-k': 'v', 'x-query-k': 'v', } }); expect(response).to.be.equal('dfsf-2dasd3-dsf2l'); expect(objectStorageCalls.isDone()).to.be.true; }); it('should put successfully', async () => { const objectStorageCalls = nock(config.uri) .matchHeader('authorization', `Bearer ${config.jwtSecret}`) .matchHeader('content-type', 'some-type') .matchHeader('x-eio-ttl', '1') .matchHeader('x-meta-k', 'v') .matchHeader('x-query-k', 'v') .put('/objects/dfsf-2dasd3-dsf2l') .reply(200, 'response'); const response = await objectStorage.update('dfsf-2dasd3-dsf2l', postData, { headers: { 'content-type': 'some-type', 'x-eio-ttl': 1, 'x-meta-k': 'v', 'x-query-k': 'v', } }); expect(response).to.be.equal('response'); expect(objectStorageCalls.isDone()).to.be.true; }); }); describe('middlewares + zip/unzip and encrypt/decrypt', () => { describe('stream mode', () => { it(`should fail after ${RETRIES_COUNT.defaultValue} get retries`, async () => { const objectStorageWithMiddlewares = new ObjectStorage(config); objectStorageWithMiddlewares.use(encryptStream, decryptStream); objectStorageWithMiddlewares.use(zip, unzip); const objectStorageWithMiddlewaresCalls = nock(config.uri) .matchHeader('authorization', `Bearer ${config.jwtSecret}`) .get('/objects/1') .times(3) .replyWithError({ code: 'ETIMEDOUT' }); await expect(objectStorageWithMiddlewares.getOne('1')).to.be.rejectedWith('Server error during request'); expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; }); it('should retry get request on errors', async () => { const objectStorageWithMiddlewares = new ObjectStorage(config); objectStorageWithMiddlewares.use(encryptStream, decryptStream); objectStorageWithMiddlewares.use(zip, unzip); const responseStream = streamFromObject(responseData).pipe(encryptStream()).pipe(zip()); const objectStorageWithMiddlewaresCalls = nock(config.uri) .matchHeader('authorization', `Bearer ${config.jwtSecret}`) .get('/objects/1') .reply(500) .get('/objects/1') .reply(200, responseStream); const { data } = await objectStorageWithMiddlewares.getOne('1', { responseType: 'stream' }); const result = await getStream(data); expect(result).to.be.deep.equal(JSON.stringify(responseData)); expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; }); it('should throw an error on post request connection error', async () => { const objectStorageWithMiddlewares = new ObjectStorage(config); objectStorageWithMiddlewares.use(encryptStream, decryptStream); objectStorageWithMiddlewares.use(zip, unzip); const objectStorageWithMiddlewaresCalls = nock(config.uri) .matchHeader('authorization', `Bearer ${config.jwtSecret}`) .post('/objects') .times(3) .replyWithError({ code: 'ECONNREFUSED' }); await expect(objectStorageWithMiddlewares.add(postData, {})).to.be.rejectedWith('Server error during request'); expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; }); it('should throw an error on post request http error', async () => { const objectStorageWithMiddlewares = new ObjectStorage(config); objectStorageWithMiddlewares.use(encryptStream, decryptStream); objectStorageWithMiddlewares.use(zip, unzip); const objectStorageWithMiddlewaresCalls = nock(config.uri) .matchHeader('authorization', `Bearer ${config.jwtSecret}`) .post('/objects') .reply(409); await expect(objectStorageWithMiddlewares.add(postData, {})).to.be.rejectedWith('Request failed with status code 409'); expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; }); it('should post successfully', async () => { const objectStorageWithMiddlewares = new ObjectStorage(config); objectStorageWithMiddlewares.use(encryptStream, decryptStream); objectStorageWithMiddlewares.use(zip, unzip); const objectStorageWithMiddlewaresCalls = nock(config.uri) .matchHeader('authorization', `Bearer ${config.jwtSecret}`) .post('/objects') .reply(200, streamFromObject({ objectId: 'dfsf-2dasd3-dsf2l' })); const response = await objectStorageWithMiddlewares.add(postData, {}); expect(response).to.be.equal('dfsf-2dasd3-dsf2l'); expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; }); }); }); describe('configure ReqOptions', () => { describe('configure ReqOptions', () => { beforeEach(async () => { finalReqCfg = sinon.spy(StorageClient.prototype, <any>'requestRetry'); }); it('configure ReqOptions use defaultValue', async () => { const objectStorageCalls = nock(config.uri) .matchHeader('authorization', `Bearer ${config.jwtSecret}`) .get('/objects/1') .times(RETRIES_COUNT.defaultValue) .replyWithError({ code: 'ETIMEDOUT' }); const retryOptions = { retriesCount: 10, requestTimeout: 1 }; await expect(objectStorage.getOne('1', { retryOptions })).to.be.rejectedWith('Server error during request'); expect(objectStorageCalls.isDone()).to.be.true; const { lastArg } = finalReqCfg.getCall(0); expect(lastArg).to.be.deep.equal(retryOptions); }); it('configure ReqOptions', async () => { const objectStorageCalls = nock(config.uri) .matchHeader('authorization', `Bearer ${config.jwtSecret}`) .get('/objects/1') .times(4) .replyWithError({ code: 'ETIMEDOUT' }) .get('/objects/1') .reply(200, streamFromObject({ objectId: '234-sdf' })); const retryOptions = { retriesCount: 4, requestTimeout: 1 }; const { data } = await objectStorage.getOne('1', { retryOptions }); expect(data).to.be.deep.equal({ objectId: '234-sdf' }); expect(objectStorageCalls.isDone()).to.be.true; const { lastArg } = finalReqCfg.getCall(0); expect(lastArg).to.be.deep.equal(retryOptions); }); }); }); describe('PotentiallyConsumedStreamError', () => { describe('on put', () => { it('should fail put request when the same stream returned on retry', async () => { const objectStorageCalls = nock(config.uri) .matchHeader('authorization', `Bearer ${config.jwtSecret}`) .put('/objects/1') .reply(500); const sameStream = streamFromObject(postData); let err: Error; try { await objectStorage.update('1', async () => sameStream); } catch (e) { err = e; } expect(objectStorageCalls.isDone()).to.be.true; expect(err).to.be.instanceOf(PotentiallyConsumedStreamError); }); it('should retry', async () => { const objectStorageCalls = nock(config.uri) .matchHeader('authorization', `Bearer ${config.jwtSecret}`) .put('/objects/1') .reply(500) .put('/objects/1') .reply(500) .put('/objects/1') .reply(200); await objectStorage.update('1', async () => streamFromObject(postData)); expect(objectStorageCalls.isDone()).to.be.true; }); }); describe('on post', () => { it('should fail post request when the same stream returned on retry', async () => { const objectStorageCalls = nock(config.uri) .matchHeader('authorization', `Bearer ${config.jwtSecret}`) .post('/objects') .reply(500); const sameStream = streamFromObject(postData); let err: Error; try { await objectStorage.add(async () => sameStream); } catch (e) { err = e; } expect(objectStorageCalls.isDone()).to.be.true; expect(err).to.be.instanceOf(PotentiallyConsumedStreamError); }); it('should retry', async () => { const objectStorageCalls = nock(config.uri) .matchHeader('authorization', `Bearer ${config.jwtSecret}`) .post('/objects') .reply(500) .post('/objects') .reply(500) .post('/objects') .reply(200); await objectStorage.add(async () => streamFromObject(postData)); expect(objectStorageCalls.isDone()).to.be.true; }); }); }); });