@elastic.io/object-storage-client
Advanced tools
Comparing version 0.0.2-dev to 0.0.3-dev1
/// <reference types="node" /> | ||
import { Readable, Duplex } from 'stream'; | ||
interface JWTPayload { | ||
[index: string]: string; | ||
} | ||
import { StorageClientConfig, JWTPayload } from './storage-client'; | ||
export interface ObjectDTO { | ||
@@ -15,13 +13,10 @@ stream?: Readable; | ||
private reverses; | ||
constructor(config: { | ||
uri: string; | ||
jwtSecret?: string; | ||
}); | ||
constructor(config: StorageClientConfig); | ||
use(forward: TransformMiddleware, reverse: TransformMiddleware): ObjectStorage; | ||
getAsJSON(objectId: string, jwtPayloadOrToken?: JWTPayload | string): Promise<any>; | ||
getAsStream(objectId: string, jwtPayloadOrToken?: JWTPayload | string): Promise<ObjectDTO>; | ||
addAsStream(stream: Readable, jwtPayloadOrToken?: JWTPayload | string): Promise<string>; | ||
getAsJSON(objectId: string, jwtPayload?: JWTPayload): Promise<any>; | ||
getAsStream(objectId: string, jwtPayload?: JWTPayload): Promise<ObjectDTO>; | ||
deleteOne(objectId: string, jwtPayload?: JWTPayload): Promise<void>; | ||
addAsStream(stream: Readable, jwtPayload?: JWTPayload): Promise<string>; | ||
private applyMiddlewares; | ||
addAsJSON(data: any, jwtPayloadOrToken: JWTPayload | string): Promise<string>; | ||
addAsJSON(data: any, jwtPayload?: JWTPayload): Promise<string>; | ||
} | ||
export {}; |
@@ -10,6 +10,5 @@ "use strict"; | ||
; | ||
; | ||
class ObjectStorage { | ||
constructor(config) { | ||
this.client = new storage_client_1.default({ uri: config.uri, jwtSecret: config.jwtSecret }); | ||
this.client = new storage_client_1.default(config); | ||
this.forwards = []; | ||
@@ -24,15 +23,18 @@ this.reverses = []; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
async getAsJSON(objectId, jwtPayloadOrToken) { | ||
const objectDTO = await this.getAsStream(objectId, jwtPayloadOrToken); | ||
async getAsJSON(objectId, jwtPayload) { | ||
const objectDTO = await this.getAsStream(objectId, jwtPayload); | ||
const data = await get_stream_1.default(objectDTO.stream); | ||
return JSON.parse(data); | ||
} | ||
async getAsStream(objectId, jwtPayloadOrToken) { | ||
const res = await this.client.readStream(objectId, jwtPayloadOrToken); | ||
async getAsStream(objectId, jwtPayload) { | ||
const res = await this.client.readStream(objectId, { jwtPayload }); | ||
const resultStream = this.applyMiddlewares(res.data, this.reverses); | ||
return { stream: resultStream, headers: res.headers }; | ||
} | ||
async addAsStream(stream, jwtPayloadOrToken) { | ||
async deleteOne(objectId, jwtPayload) { | ||
await this.client.deleteOne(objectId, { jwtPayload }); | ||
} | ||
async addAsStream(stream, jwtPayload) { | ||
const resultStream = this.applyMiddlewares(stream, this.forwards); | ||
const res = await this.client.writeStream(resultStream, jwtPayloadOrToken); | ||
const res = await this.client.writeStream(resultStream, { jwtPayload }); | ||
return res.data.objectId; | ||
@@ -46,3 +48,3 @@ } | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
async addAsJSON(data, jwtPayloadOrToken) { | ||
async addAsJSON(data, jwtPayload) { | ||
const dataString = JSON.stringify(data); | ||
@@ -52,5 +54,5 @@ const dataStream = new stream_1.Readable(); | ||
dataStream.push(null); | ||
return this.addAsStream(dataStream, jwtPayloadOrToken); | ||
return this.addAsStream(dataStream, jwtPayload); | ||
} | ||
} | ||
exports.default = ObjectStorage; |
/// <reference types="node" /> | ||
import { AxiosResponse } from 'axios'; | ||
import { Readable } from 'stream'; | ||
interface JWTPayload { | ||
declare const AXIOS_CONFIG_NAMESPACE = "object-storage-client"; | ||
export interface JWTPayload { | ||
[index: string]: string; | ||
} | ||
export interface RequestOptions { | ||
maxAttempts?: number; | ||
delay?: number; | ||
onResponse?: (err: Error, res: AxiosResponse) => boolean; | ||
export interface ObjectOptions { | ||
ttl: number; | ||
} | ||
export interface StorageClientRequestConfig { | ||
jwtPayload?: JWTPayload; | ||
objectOptions?: ObjectOptions; | ||
retryDelay?: number; | ||
retryCount?: number; | ||
} | ||
export interface StorageClientConfig { | ||
uri: string; | ||
jwtSecret?: string; | ||
jwtToken?: string; | ||
defaultRequestConfig?: StorageClientRequestConfig; | ||
} | ||
declare module 'axios' { | ||
interface AxiosRequestConfig { | ||
[AXIOS_CONFIG_NAMESPACE]?: StorageClientRequestConfig; | ||
} | ||
} | ||
export default class StorageClient { | ||
private api; | ||
private readonly jwtSecret?; | ||
private readonly api; | ||
private static readonly DEFAULT_RETRY_DELAY; | ||
private static readonly DEFAULT_RETRY_COUNT; | ||
private static httpAgent; | ||
private static httpsAgent; | ||
constructor(config: { | ||
uri: string; | ||
jwtSecret?: string; | ||
}); | ||
private requestRetry; | ||
private getHeaders; | ||
readStream(objectId: string, jwtPayloadOrToken?: JWTPayload | string): Promise<AxiosResponse>; | ||
writeStream(stream: Readable, jwtPayloadOrToken?: JWTPayload | string): Promise<AxiosResponse>; | ||
constructor(config: StorageClientConfig); | ||
readStream(objectId: string, config?: StorageClientRequestConfig): Promise<AxiosResponse>; | ||
writeStream(stream: Readable, config?: StorageClientRequestConfig): Promise<AxiosResponse>; | ||
deleteOne(objectId: string, config?: StorageClientRequestConfig): Promise<AxiosResponse>; | ||
private static retryCondition; | ||
private static createApiClient; | ||
} | ||
export {}; |
@@ -5,69 +5,119 @@ "use strict"; | ||
}; | ||
var __importStar = (this && this.__importStar) || function (mod) { | ||
if (mod && mod.__esModule) return mod; | ||
var result = {}; | ||
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; | ||
result["default"] = mod; | ||
return result; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const axios_1 = __importDefault(require("axios")); | ||
const axios_retry_1 = __importStar(require("axios-retry")); | ||
const http_1 = __importDefault(require("http")); | ||
const https_1 = __importDefault(require("https")); | ||
const logger_1 = __importDefault(require("./logger")); | ||
const jsonwebtoken_1 = require("jsonwebtoken"); | ||
const util_1 = require("util"); | ||
const logger_1 = __importDefault(require("./logger")); | ||
const AXIOS_CONFIG_NAMESPACE = 'object-storage-client'; | ||
const AXIOS_RETRY_CONFIG_NAMESPACE = 'axios-retry'; | ||
var ObjectHeaders; | ||
(function (ObjectHeaders) { | ||
ObjectHeaders["ttl"] = "x-eio-ttl"; | ||
})(ObjectHeaders || (ObjectHeaders = {})); | ||
; | ||
; | ||
class StorageClient { | ||
constructor(config) { | ||
this.api = axios_1.default.create({ | ||
baseURL: `${config.uri}/`, | ||
const { uri, jwtSecret, defaultRequestConfig, jwtToken } = config; | ||
this.api = StorageClient.createApiClient(jwtSecret, jwtToken, uri, defaultRequestConfig); | ||
} | ||
async readStream(objectId, config) { | ||
const res = await this.api.get(`/objects/${objectId}`, { | ||
responseType: 'stream', | ||
[AXIOS_CONFIG_NAMESPACE]: config | ||
}); | ||
return res; | ||
} | ||
async writeStream(stream, config) { | ||
const res = await this.api.post(`/objects`, stream, { | ||
headers: { | ||
'content-type': 'application/octet-stream' | ||
}, | ||
[AXIOS_CONFIG_NAMESPACE]: config | ||
}); | ||
return res; | ||
} | ||
async deleteOne(objectId, config) { | ||
const res = await this.api.delete(`/objects/${objectId}`, { | ||
[AXIOS_CONFIG_NAMESPACE]: config | ||
}); | ||
return res; | ||
} | ||
static retryCondition(err) { | ||
if (axios_retry_1.isNetworkError(err)) { | ||
logger_1.default.warn('Error during object request: %s', err); | ||
return true; | ||
} | ||
const { response } = err; | ||
if (!response) { | ||
return false; | ||
} | ||
if (response.status === 429 || (response.status >= 500 && response.status <= 599)) { | ||
logger_1.default.warn('Error during object request: %s', `${response.status} (${response.statusText})`); | ||
return true; | ||
} | ||
return false; | ||
} | ||
static createApiClient(jwtSecret, jwtToken, uri, requestConfig) { | ||
if (!jwtSecret && !jwtToken) { | ||
throw new Error('Neither JWT token passed, nor JWT secret provided.'); | ||
} | ||
const api = axios_1.default.create({ | ||
baseURL: `${uri}/`, | ||
httpAgent: StorageClient.httpAgent, | ||
httpsAgent: StorageClient.httpsAgent, | ||
validateStatus: null, | ||
maxContentLength: Infinity, | ||
maxRedirects: 0 | ||
maxContentLength: Infinity | ||
}); | ||
this.jwtSecret = config.jwtSecret; | ||
} | ||
async requestRetry(request, { maxAttempts = 3, delay = 100, onResponse } = {}) { | ||
let attempts = 0; | ||
let res; | ||
let err; | ||
while (attempts < maxAttempts) { | ||
err = null; | ||
res = null; | ||
attempts++; | ||
try { | ||
res = await request(); | ||
if (jwtToken) { | ||
api.defaults.headers.common.Authorization = `Bearer ${jwtToken}`; | ||
} | ||
// pls do not move axios interceptors init order, | ||
// since middleware below sets values for retry meddileware, which sould be next | ||
api.interceptors.request.use(async (config) => { | ||
const { [AXIOS_CONFIG_NAMESPACE]: cfg } = config; | ||
if (!cfg) { | ||
return config; | ||
} | ||
catch (e) { | ||
err = e; | ||
let newConfig = Object.assign({}, config, { [AXIOS_RETRY_CONFIG_NAMESPACE]: config[AXIOS_RETRY_CONFIG_NAMESPACE] || {} }); | ||
const { jwtPayload, retryDelay, retryCount, objectOptions } = cfg; | ||
if (jwtPayload) { | ||
if (!jwtSecret) { | ||
throw new Error('JWT secret is not provided during initialization, can not encode JWT payload'); | ||
} | ||
const token = await util_1.promisify(jsonwebtoken_1.sign)(jwtPayload, jwtSecret); | ||
newConfig.headers.Authorization = `Bearer ${token}`; | ||
} | ||
if (onResponse && onResponse(err, res)) { | ||
continue; | ||
; | ||
if (typeof retryDelay === 'number') { | ||
newConfig['axios-retry'].retryDelay = () => retryDelay; | ||
} | ||
// last attempt error should not be logged | ||
if ((err || res.status >= 400) && attempts < maxAttempts) { | ||
logger_1.default.warn('Error during object request: %s', err || `${res.status} (${res.statusText})`); | ||
await new Promise((resolve) => setTimeout(resolve, delay)); | ||
continue; | ||
if (typeof retryCount === 'number') { | ||
newConfig['axios-retry'].retries = retryCount; | ||
} | ||
break; | ||
} | ||
if (err || res.status >= 400) { | ||
throw err || new Error(`HTTP error during object request: ${res.status} (${res.statusText})`); | ||
} | ||
return res; | ||
if (objectOptions) { | ||
newConfig.headers[ObjectHeaders.ttl] = objectOptions.ttl; | ||
} | ||
return newConfig; | ||
}); | ||
axios_retry_1.default(api, { | ||
retries: requestConfig && typeof requestConfig.retryCount === 'number' | ||
? requestConfig.retryCount | ||
: this.DEFAULT_RETRY_COUNT, | ||
retryDelay: () => requestConfig && typeof requestConfig.retryDelay === 'number' | ||
? requestConfig.retryDelay | ||
: this.DEFAULT_RETRY_DELAY, | ||
retryCondition: StorageClient.retryCondition | ||
}); | ||
return api; | ||
} | ||
async getHeaders(jwtPayloadOrToken, override) { | ||
if (typeof jwtPayloadOrToken !== 'string' && !this.jwtSecret) { | ||
throw new Error('Neither JWT token passed, nor JWT secret provided during initialization'); | ||
} | ||
const token = typeof jwtPayloadOrToken === 'string' | ||
? jwtPayloadOrToken | ||
: await util_1.promisify(jsonwebtoken_1.sign)(jwtPayloadOrToken, this.jwtSecret); | ||
return Object.assign({ Authorization: `Bearer ${token}` }, override); | ||
} | ||
async readStream(objectId, jwtPayloadOrToken) { | ||
const res = await this.requestRetry(async () => this.api.get(`/objects/${objectId}`, { responseType: 'stream', headers: await this.getHeaders(jwtPayloadOrToken) })); | ||
return res; | ||
} | ||
async writeStream(stream, jwtPayloadOrToken) { | ||
const res = await this.requestRetry(async () => this.api.post(`/objects`, stream, { headers: await this.getHeaders(jwtPayloadOrToken, { 'content-type': 'application/octet-stream' }) })); | ||
return res; | ||
} | ||
} | ||
@@ -74,0 +124,0 @@ StorageClient.httpAgent = new http_1.default.Agent({ keepAlive: true }); |
{ | ||
"name": "@elastic.io/object-storage-client", | ||
"version": "0.0.2-dev", | ||
"version": "0.0.3-dev1", | ||
"description": "Elastic.io Message Store Client", | ||
"main": "dist/index.js", | ||
"engines": { | ||
"node": ">= 10" | ||
}, | ||
"scripts": { | ||
@@ -18,3 +15,4 @@ "lint": "eslint --ext .ts .", | ||
"@elastic.io/bunyan-logger": "1.0.5", | ||
"axios": "0.19.0", | ||
"axios": "0.21.0", | ||
"axios-retry": "3.1.9", | ||
"get-stream": "5.1.0", | ||
@@ -21,0 +19,0 @@ "jsonwebtoken": "8.5.1", |
@@ -11,9 +11,5 @@ import nock from 'nock'; | ||
import { streamResponse, encryptStream, decryptStream, zip, unzip } from './helpers'; | ||
import { JWTPayload } from '../src/storage-client'; | ||
describe('Object Storage', () => { | ||
const config = { | ||
uri: 'https://ma.es.ter', | ||
jwtSecret: 'jwt' | ||
}; | ||
const postData = { test: 'test' }; | ||
@@ -44,247 +40,664 @@ | ||
function authHeaderMatch (jwtPayload?: { [index: string]: string }) { | ||
return (val: string) => { | ||
const decoded = verify(val.split(' ')[1], config.jwtSecret); | ||
if (jwtPayload) { | ||
expect(decoded).to.deep.include(jwtPayload); | ||
} | ||
return decoded; | ||
}; | ||
} | ||
it('should throw exception if neither jwt secret, nor jwt token provided', async () => { | ||
let err; | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
let client: ObjectStorage; // eslint-disable-line no-unused-vars | ||
try { | ||
client = new ObjectStorage({ uri: '' }); | ||
} catch (e) { | ||
err = e; | ||
} | ||
describe('basic', () => { | ||
describe('data mode', () => { | ||
it('should fail after 3 retries', async () => { | ||
const log = sinon.stub(logging, 'warn'); | ||
const objectStorage = new ObjectStorage(config); | ||
expect(err.toString()).to.include('JWT'); | ||
}); | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ETIMEDOUT' }) | ||
.get('/objects/1') | ||
.reply(404) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ENOTFOUND' }); | ||
describe('when jwt payload provided', () => { | ||
const config = { | ||
uri: 'https://ma.es.ter', | ||
jwtSecret: 'jwt' | ||
}; | ||
let err; | ||
try { | ||
await objectStorage.getAsJSON('1', {}); | ||
} catch (e) { | ||
err = e; | ||
function authHeaderMatch(jwtPayload?: JWTPayload) { | ||
return (val: string) => { | ||
const decoded = verify(val.split(' ')[1], config.jwtSecret); | ||
if (jwtPayload) { | ||
expect(decoded).to.deep.include(jwtPayload); | ||
} | ||
return decoded; | ||
}; | ||
} | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(err.code).to.be.equal('ENOTFOUND'); | ||
expect(log.getCall(1).args[1].toString()).to.include('404'); | ||
expect(log.callCount).to.be.equal(2); | ||
}); | ||
describe('basic', () => { | ||
describe('data mode', () => { | ||
it('should fail after 3 retries', async () => { | ||
const log = sinon.stub(logging, 'warn'); | ||
const objectStorage = new ObjectStorage(config); | ||
it('should retry get request 3 times on errors', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ETIMEDOUT' }) | ||
.get('/objects/1') | ||
.reply(429) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ECONNRESET' }) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'EHOSTUNREACH' }); | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.get('/objects/1') | ||
.reply(500) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ECONNRESET' }) | ||
.get('/objects/1') | ||
.reply(200, streamResponse(responseData)); | ||
let err; | ||
try { | ||
await objectStorage.getAsJSON('1', {}); | ||
} catch (e) { | ||
err = e; | ||
} | ||
const out = await objectStorage.getAsJSON('1', {}); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(err.code).to.be.equal('EHOSTUNREACH'); | ||
expect(log.getCall(1).args[1].toString()).to.include('429'); | ||
expect(log.callCount).to.be.equal(4); | ||
}); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(out).to.be.deep.equal(responseData); | ||
}); | ||
it('should retry get request 3 times on errors', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
it('should retry post request 3 times on errors', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.get('/objects/1') | ||
.reply(500) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ECONNRESET' }) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'EHOSTUNREACH' }) | ||
.get('/objects/1') | ||
.reply(200, streamResponse(responseData)); | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.post('/objects') | ||
.replyWithError({ code: 'ECONNREFUSED' }) | ||
.post('/objects') | ||
.reply(400) | ||
.post('/objects') | ||
.reply(200, responseData); | ||
const out = await objectStorage.getAsJSON('1', {}); | ||
await objectStorage.addAsJSON(postData, {}); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(out).to.be.deep.equal(responseData); | ||
}); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
}); | ||
it('should retry post request 3 times on errors', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
it('should accept jwt token on add', async () => { | ||
const objectStorage = new ObjectStorage({ uri: config.uri }); | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.post('/objects') | ||
.replyWithError({ code: 'ECONNREFUSED' }) | ||
.post('/objects') | ||
.reply(429) | ||
.post('/objects') | ||
.replyWithError({ code: 'EHOSTUNREACH' }) | ||
.post('/objects') | ||
.reply(200, responseData); | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch(jwtPayload)) | ||
.post('/objects') | ||
.reply(200); | ||
await objectStorage.addAsJSON(postData, {}); | ||
const objectId = await objectStorage.addAsJSON(postData, sign(jwtPayload, config.jwtSecret)); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
}); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(objectId).to.match(/^[0-9a-z-]+$/); | ||
}); | ||
it('should accept jwt token on add', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
it('should accept jwt token on get', async () => { | ||
const objectStorage = new ObjectStorage({ uri: config.uri }); | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch(jwtPayload)) | ||
.post('/objects') | ||
.reply(200); | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch(jwtPayload)) | ||
.get('/objects/1') | ||
.reply(200, streamResponse(responseData)); | ||
const objectId = await objectStorage.addAsJSON(postData, jwtPayload); | ||
const out = await objectStorage.getAsJSON('1', sign(jwtPayload, config.jwtSecret)); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(objectId).to.match(/^[0-9a-z-]+$/); | ||
}); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(out).to.be.deep.equal(responseData); | ||
it('should accept jwt token on get', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch(jwtPayload)) | ||
.get('/objects/1') | ||
.reply(200, streamResponse(responseData)); | ||
const out = await objectStorage.getAsJSON('1', jwtPayload); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(out).to.be.deep.equal(responseData); | ||
}); | ||
it('should accept jwt token on delete', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch(jwtPayload)) | ||
.delete('/objects/1') | ||
.reply(204); | ||
await objectStorage.deleteOne('1', jwtPayload); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
}); | ||
it('should throw exception if no jwt secret provided but with JWT payload', async () => { | ||
const objectStorage = new ObjectStorage({ uri: config.uri, jwtToken: 'token' }); | ||
let err; | ||
try { | ||
await objectStorage.getAsJSON('1', {}); | ||
} catch (e) { | ||
err = e; | ||
} | ||
expect(err.toString()).to.include('JWT'); | ||
}); | ||
}); | ||
it('should throw exception if neither jwt secret, nor jwt token provided', async () => { | ||
const objectStorage = new ObjectStorage({ uri: config.uri }); | ||
describe('stream mode', () => { | ||
it('should fail after 3 get retries', async () => { | ||
const log = sinon.stub(logging, 'warn'); | ||
const objectStorage = new ObjectStorage(config); | ||
let err; | ||
try { | ||
await objectStorage.getAsJSON('1', {}); | ||
} catch (e) { | ||
err = e; | ||
} | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ETIMEDOUT' }) | ||
.get('/objects/1') | ||
.reply(429) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ECONNRESET' }) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'EHOSTUNREACH' }); | ||
expect(err.toString()).to.include('JWT'); | ||
let err; | ||
try { | ||
await objectStorage.getAsStream('1', {}); | ||
} catch (e) { | ||
err = e; | ||
} | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(err.code).to.be.equal('EHOSTUNREACH'); | ||
expect(log.getCall(1).args[1].toString()).to.include('429'); | ||
expect(log.callCount).to.be.equal(4); | ||
}); | ||
it('should retry get request on errors', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.get('/objects/1') | ||
.reply(500) | ||
.get('/objects/1') | ||
.reply(200, streamResponse(responseData)); | ||
const response = await objectStorage.getAsStream('1', {}); | ||
const out = JSON.parse(await getStream(response.stream)); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(out).to.be.deep.equal(responseData); | ||
}); | ||
it('should throw an error on post request connection error', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.post('/objects') | ||
.replyWithError({ code: 'ECONNREFUSED' }) | ||
.post('/objects') | ||
.replyWithError({ code: 'ECONNRESET' }) | ||
.post('/objects') | ||
.replyWithError({ code: 'ETIMEDOUT' }) | ||
.post('/objects') | ||
.replyWithError({ code: 'EHOSTUNREACH' }); | ||
let err; | ||
try { | ||
await objectStorage.addAsStream(postStream, {}); | ||
} catch (e) { | ||
err = e; | ||
} | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(err.code).to.be.equal('EHOSTUNREACH'); | ||
}); | ||
it('should throw an error on post request http error', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.post('/objects') | ||
.reply(500) | ||
.post('/objects') | ||
.reply(503) | ||
.post('/objects') | ||
.reply(502) | ||
.post('/objects') | ||
.reply(429); | ||
let err; | ||
try { | ||
await objectStorage.addAsStream(postStream, {}); | ||
} catch (e) { | ||
err = e; | ||
} | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(err.toString()).to.include('429'); | ||
}); | ||
it('should post successfully', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.post('/objects') | ||
.reply(200); | ||
const objectId = await objectStorage.addAsStream(postStream, {}); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(objectId).to.match(/^[0-9a-z-]+$/); | ||
}); | ||
it('should use valid jwt token', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch(jwtPayload)) | ||
.post('/objects') | ||
.reply(200); | ||
const objectId = await objectStorage.addAsStream(postStream, jwtPayload); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(objectId).to.match(/^[0-9a-z-]+$/); | ||
}); | ||
}); | ||
}); | ||
describe('stream mode', () => { | ||
it('should fail after 3 get retries', async () => { | ||
const log = sinon.stub(logging, 'warn'); | ||
const objectStorage = new ObjectStorage(config); | ||
describe('middlewares + zip/unzip and encrypt/decrypt', () => { | ||
describe('data mode', () => { | ||
it('should fail after 3 retries', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const log = sinon.stub(logging, 'warn'); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ETIMEDOUT' }) | ||
.get('/objects/1') | ||
.reply(429) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'EHOSTUNREACH' }) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ECONNREFUSED' }); | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ETIMEDOUT' }) | ||
.get('/objects/1') | ||
.reply(404) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ENOTFOUND' }); | ||
let err; | ||
try { | ||
await objectStorageWithMiddlewares.getAsJSON('1', {}); | ||
} catch (e) { | ||
err = e; | ||
} | ||
let err; | ||
try { | ||
await objectStorage.getAsStream('1', {}); | ||
} catch (e) { | ||
err = e; | ||
} | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(err.code).to.be.equal('ECONNREFUSED'); | ||
expect(log.getCall(1).args[1].toString()).to.include('429'); | ||
expect(log.callCount).to.be.equal(4); | ||
}); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(err.code).to.be.equal('ENOTFOUND'); | ||
expect(log.getCall(1).args[1].toString()).to.include('404'); | ||
expect(log.callCount).to.be.equal(2); | ||
}); | ||
it('should retry get request 3 times on errors', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.get('/objects/1') | ||
.reply(500) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ECONNRESET' }) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ECONNREFUSED' }) | ||
.get('/objects/1') | ||
.reply(200, () => { | ||
const stream = streamResponse(responseData)(); | ||
return stream.pipe(encryptStream()).pipe(zip()); | ||
}); | ||
it('should retry get request on errors', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
const out = await objectStorageWithMiddlewares.getAsJSON('1', {}); | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.get('/objects/1') | ||
.reply(500) | ||
.get('/objects/1') | ||
.reply(200, streamResponse(responseData)); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(out).to.be.deep.equal(responseData); | ||
}); | ||
const response = await objectStorage.getAsStream('1', {}); | ||
it('should retry post request 3 times on errors', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.post('/objects') | ||
.replyWithError({ code: 'ECONNREFUSED' }) | ||
.post('/objects') | ||
.replyWithError({ code: 'ECONNRESET' }) | ||
.post('/objects') | ||
.reply(429) | ||
.post('/objects') | ||
.reply(200, responseData); | ||
const out = JSON.parse(await getStream(response.stream)); | ||
await objectStorageWithMiddlewares.addAsJSON(postData, {}); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(out).to.be.deep.equal(responseData); | ||
}); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
}); | ||
it('should throw an error on put request connection error', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
it('should accept jwt token on add', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch(jwtPayload)) | ||
.post('/objects') | ||
.reply(200); | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.post('/objects') | ||
.replyWithError({ code: 'ECONNREFUSED' }) | ||
.post('/objects') | ||
.replyWithError({ code: 'ECONNREFUSED' }) | ||
.post('/objects') | ||
.replyWithError({ code: 'ECONNREFUSED' }); | ||
const objectId = await objectStorageWithMiddlewares.addAsJSON(postData, jwtPayload); | ||
let err; | ||
try { | ||
await objectStorage.addAsStream(postStream, {}); | ||
} catch (e) { | ||
err = e; | ||
} | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(objectId).to.match(/^[0-9a-z-]+$/); | ||
}); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(err.code).to.be.equal('ECONNREFUSED'); | ||
}); | ||
it('should accept jwt token on get', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
it('should throw an error on put request http error', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch(jwtPayload)) | ||
.get('/objects/1') | ||
.reply(200, () => { | ||
const stream = streamResponse(responseData)(); | ||
return stream.pipe(encryptStream()).pipe(zip()); | ||
}); | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.post('/objects') | ||
.reply(409) | ||
.post('/objects') | ||
.reply(409) | ||
.post('/objects') | ||
.reply(409); | ||
const out = await objectStorageWithMiddlewares.getAsJSON('1', jwtPayload); | ||
let err; | ||
try { | ||
await objectStorage.addAsStream(postStream, {}); | ||
} catch (e) { | ||
err = e; | ||
} | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(err.toString()).to.include('409'); | ||
}); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(out).to.be.deep.equal(responseData); | ||
}); | ||
it('should put successfully', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
it('should add 2 objects successfully', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch(jwtPayload)) | ||
.post('/objects') | ||
.reply(200, { objectId: '1' }) | ||
.post('/objects') | ||
.reply(200, { objectId: '2' }); | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.post('/objects') | ||
.reply(200); | ||
const objectIdFirst = await objectStorageWithMiddlewares.addAsJSON(postData, jwtPayload); | ||
const objectIdSecond = await objectStorageWithMiddlewares.addAsJSON(postData, jwtPayload); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(objectIdFirst).to.be.equal('1'); | ||
expect(objectIdSecond).to.be.equal('2'); | ||
}); | ||
const objectId = await objectStorage.addAsStream(postStream, {}); | ||
it('should get 2 objects successfully', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(objectId).to.match(/^[0-9a-z-]+$/); | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch(jwtPayload)) | ||
.get('/objects/1') | ||
.reply(200, () => { | ||
const stream = streamResponse(responseData)(); | ||
return stream.pipe(encryptStream()).pipe(zip()); | ||
}) | ||
.get('/objects/2') | ||
.reply(200, () => { | ||
const stream = streamResponse(responseData)(); | ||
return stream.pipe(encryptStream()).pipe(zip()); | ||
}); | ||
const outFirst = await objectStorageWithMiddlewares.getAsJSON('1', jwtPayload); | ||
const outSecond = await objectStorageWithMiddlewares.getAsJSON('2', jwtPayload); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(outFirst).to.be.deep.equal(responseData); | ||
expect(outSecond).to.be.deep.equal(responseData); | ||
}); | ||
it('should throw exception if no jwt secret provided but with JWT payload', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage({ uri: config.uri, jwtToken: 'token' }); | ||
let err; | ||
try { | ||
await objectStorageWithMiddlewares.getAsJSON('1', {}); | ||
} catch (e) { | ||
err = e; | ||
} | ||
expect(err.toString()).to.include('JWT'); | ||
}); | ||
}); | ||
it('should use valid jwt token', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
describe('stream mode', () => { | ||
it('should fail after 3 get retries', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const log = sinon.stub(logging, 'warn'); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ETIMEDOUT' }) | ||
.get('/objects/1') | ||
.reply(429) | ||
.get('/objects/1') | ||
.reply(500) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'EHOSTUNREACH' }); | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch(jwtPayload)) | ||
.post('/objects') | ||
.reply(200); | ||
let err; | ||
try { | ||
await objectStorageWithMiddlewares.getAsStream('1', {}); | ||
} catch (e) { | ||
err = e; | ||
} | ||
const objectId = await objectStorage.addAsStream(postStream, jwtPayload); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(err.code).to.be.equal('EHOSTUNREACH'); | ||
expect(log.getCall(1).args[1].toString()).to.include('429'); | ||
expect(log.callCount).to.be.equal(4); | ||
}); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(objectId).to.match(/^[0-9a-z-]+$/); | ||
it('should retry get request on errors', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.get('/objects/1') | ||
.reply(500) | ||
.get('/objects/1') | ||
.reply(429) | ||
.get('/objects/1') | ||
.reply(200, () => { | ||
const stream = streamResponse(responseData)(); | ||
return stream.pipe(encryptStream()).pipe(zip()); | ||
}); | ||
const response = await objectStorageWithMiddlewares.getAsStream('1', {}); | ||
const out = JSON.parse(await getStream(response.stream)); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(out).to.be.deep.equal(responseData); | ||
}); | ||
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) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.post('/objects') | ||
.replyWithError({ code: 'ECONNREFUSED' }) | ||
.post('/objects') | ||
.replyWithError({ code: 'ETIMEDOUT' }) | ||
.post('/objects') | ||
.replyWithError({ code: 'ECONNRESET' }) | ||
.post('/objects') | ||
.replyWithError({ code: 'ECONNREFUSED' }); | ||
let err; | ||
try { | ||
await objectStorageWithMiddlewares.addAsStream(postStream, {}); | ||
} catch (e) { | ||
err = e; | ||
} | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(err.code).to.be.equal('ECONNREFUSED'); | ||
}); | ||
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) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.post('/objects') | ||
.reply(502) | ||
.post('/objects') | ||
.reply(503) | ||
.post('/objects') | ||
.reply(500) | ||
.post('/objects') | ||
.reply(429); | ||
let err; | ||
try { | ||
await objectStorageWithMiddlewares.addAsStream(postStream, {}); | ||
} catch (e) { | ||
err = e; | ||
} | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(err.toString()).to.include('429'); | ||
}); | ||
it('should post successfully', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.post('/objects') | ||
.reply(200, { objectId: '1' }); | ||
const objectId = await objectStorageWithMiddlewares.addAsStream(postStream, {}); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(objectId).to.be.equal('1'); | ||
}); | ||
it('should add 2 objects successfully', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch(jwtPayload)) | ||
.post('/objects') | ||
.reply(200, { objectId: '1' }) | ||
.post('/objects') | ||
.reply(200, { objectId: '2' }); | ||
const objectIdFirst = await objectStorageWithMiddlewares.addAsStream(postStream, jwtPayload); | ||
const objectIdSecond = await objectStorageWithMiddlewares.addAsStream(postStream, jwtPayload); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(objectIdFirst).to.be.equal('1'); | ||
expect(objectIdSecond).to.be.equal('2'); | ||
}); | ||
it('should get 2 objects successfully', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch(jwtPayload)) | ||
.get('/objects/1') | ||
.reply(200, () => { | ||
const stream = streamResponse(responseData)(); | ||
return stream.pipe(encryptStream()).pipe(zip()); | ||
}) | ||
.get('/objects/2') | ||
.reply(200, () => { | ||
const stream = streamResponse(responseData)(); | ||
return stream.pipe(encryptStream()).pipe(zip()); | ||
}); | ||
const outStreamFirst = await objectStorageWithMiddlewares.getAsStream('1', jwtPayload); | ||
const outFirst = JSON.parse(await getStream(outStreamFirst.stream)); | ||
const outStreamSecond = await objectStorageWithMiddlewares.getAsStream('2', jwtPayload); | ||
const outSecond = JSON.parse(await getStream(outStreamSecond.stream)); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(outFirst).to.be.deep.equal(responseData); | ||
expect(outSecond).to.be.deep.equal(responseData); | ||
}); | ||
it('should use valid jwt token', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch(jwtPayload)) | ||
.post('/objects') | ||
.reply(200); | ||
const objectId = await objectStorageWithMiddlewares.addAsStream(postStream, jwtPayload); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(objectId).to.match(/^[0-9a-z-]+$/); | ||
}); | ||
}); | ||
@@ -294,354 +707,607 @@ }); | ||
describe('middlewares + zip/unzip and encrypt/decrypt', () => { | ||
describe('data mode', () => { | ||
it('should fail after 3 retries', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const log = sinon.stub(logging, 'warn'); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ETIMEDOUT' }) | ||
.get('/objects/1') | ||
.reply(404) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ENOTFOUND' }); | ||
describe('when jwt token provided', () => { | ||
const jwtSecret = 'jwt'; | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const token = sign(jwtPayload, jwtSecret); | ||
const config = { | ||
uri: 'https://ma.es.ter', | ||
jwtToken: token | ||
}; | ||
let err; | ||
try { | ||
await objectStorageWithMiddlewares.getAsJSON('1', {}); | ||
} catch (e) { | ||
err = e; | ||
} | ||
describe('basic', () => { | ||
describe('data mode', () => { | ||
it('should fail after 3 retries', async () => { | ||
const log = sinon.stub(logging, 'warn'); | ||
const objectStorage = new ObjectStorage(config); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(err.code).to.be.equal('ENOTFOUND'); | ||
expect(log.getCall(1).args[1].toString()).to.include('404'); | ||
expect(log.callCount).to.be.equal(2); | ||
}); | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ETIMEDOUT' }) | ||
.get('/objects/1') | ||
.reply(429) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ECONNRESET' }) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'EHOSTUNREACH' }); | ||
it('should retry get request 3 times on errors', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.get('/objects/1') | ||
.reply(500) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ECONNRESET' }) | ||
.get('/objects/1') | ||
.reply(200, () => { | ||
const stream = streamResponse(responseData)(); | ||
return stream.pipe(encryptStream()).pipe(zip()); | ||
}); | ||
let err; | ||
try { | ||
await objectStorage.getAsJSON('1'); | ||
} catch (e) { | ||
err = e; | ||
} | ||
const out = await objectStorageWithMiddlewares.getAsJSON('1', {}); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(err.code).to.be.equal('EHOSTUNREACH'); | ||
expect(log.getCall(1).args[1].toString()).to.include('429'); | ||
expect(log.callCount).to.be.equal(4); | ||
}); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(out).to.be.deep.equal(responseData); | ||
}); | ||
it('should retry get request 3 times on errors', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
it('should retry post request 3 times on errors', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.post('/objects') | ||
.replyWithError({ code: 'ECONNREFUSED' }) | ||
.post('/objects') | ||
.reply(400) | ||
.post('/objects') | ||
.reply(200, responseData); | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.get('/objects/1') | ||
.reply(500) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ECONNRESET' }) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'EHOSTUNREACH' }) | ||
.get('/objects/1') | ||
.reply(200, streamResponse(responseData)); | ||
await objectStorageWithMiddlewares.addAsJSON(postData, {}); | ||
const out = await objectStorage.getAsJSON('1'); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
}); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(out).to.be.deep.equal(responseData); | ||
}); | ||
it('should accept jwt token on add', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch(jwtPayload)) | ||
.post('/objects') | ||
.reply(200); | ||
it('should retry post request 3 times on errors', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
const objectId = await objectStorageWithMiddlewares.addAsJSON(postData, sign(jwtPayload, config.jwtSecret)); | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.post('/objects') | ||
.replyWithError({ code: 'ECONNREFUSED' }) | ||
.post('/objects') | ||
.reply(429) | ||
.post('/objects') | ||
.replyWithError({ code: 'EHOSTUNREACH' }) | ||
.post('/objects') | ||
.reply(200, responseData); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(objectId).to.match(/^[0-9a-z-]+$/); | ||
}); | ||
await objectStorage.addAsJSON(postData); | ||
it('should accept jwt token on get', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
}); | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch(jwtPayload)) | ||
.get('/objects/1') | ||
.reply(200, () => { | ||
const stream = streamResponse(responseData)(); | ||
return stream.pipe(encryptStream()).pipe(zip()); | ||
}); | ||
it('should accept jwt token on add', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
const out = await objectStorageWithMiddlewares.getAsJSON('1', sign(jwtPayload, config.jwtSecret)); | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.post('/objects') | ||
.reply(200); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(out).to.be.deep.equal(responseData); | ||
}); | ||
const objectId = await objectStorage.addAsJSON(postData); | ||
it('should add 2 objects successfully', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch(jwtPayload)) | ||
.post('/objects') | ||
.reply(200, { objectId: '1' }) | ||
.post('/objects') | ||
.reply(200, { objectId: '2' }); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(objectId).to.match(/^[0-9a-z-]+$/); | ||
}); | ||
const objectIdFirst = await objectStorageWithMiddlewares.addAsJSON(postData, sign(jwtPayload, config.jwtSecret)); | ||
const objectIdSecond = await objectStorageWithMiddlewares.addAsJSON(postData, sign(jwtPayload, config.jwtSecret)); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(objectIdFirst).to.be.equal('1'); | ||
expect(objectIdSecond).to.be.equal('2'); | ||
}); | ||
it('should accept jwt token on get', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
it('should get 2 objects successfully', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.get('/objects/1') | ||
.reply(200, streamResponse(responseData)); | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch(jwtPayload)) | ||
.get('/objects/1') | ||
.reply(200, () => { | ||
const stream = streamResponse(responseData)(); | ||
return stream.pipe(encryptStream()).pipe(zip()); | ||
}) | ||
.get('/objects/2') | ||
.reply(200, () => { | ||
const stream = streamResponse(responseData)(); | ||
return stream.pipe(encryptStream()).pipe(zip()); | ||
}); | ||
const out = await objectStorage.getAsJSON('1'); | ||
const outFirst = await objectStorageWithMiddlewares.getAsJSON('1', sign(jwtPayload, config.jwtSecret)); | ||
const outSecond = await objectStorageWithMiddlewares.getAsJSON('2', sign(jwtPayload, config.jwtSecret)); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(out).to.be.deep.equal(responseData); | ||
}); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(outFirst).to.be.deep.equal(responseData); | ||
expect(outSecond).to.be.deep.equal(responseData); | ||
it('should accept jwt token on delete', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.delete('/objects/1') | ||
.reply(204); | ||
await objectStorage.deleteOne('1'); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
}); | ||
}); | ||
it('should throw exception if neither jwt secret, nor jwt token provided', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage({ uri: config.uri }); | ||
describe('stream mode', () => { | ||
it('should fail after 3 get retries', async () => { | ||
const log = sinon.stub(logging, 'warn'); | ||
const objectStorage = new ObjectStorage(config); | ||
let err; | ||
try { | ||
await objectStorageWithMiddlewares.getAsJSON('1', {}); | ||
} catch (e) { | ||
err = e; | ||
} | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ETIMEDOUT' }) | ||
.get('/objects/1') | ||
.reply(429) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ECONNRESET' }) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'EHOSTUNREACH' }); | ||
expect(err.toString()).to.include('JWT'); | ||
let err; | ||
try { | ||
await objectStorage.getAsStream('1'); | ||
} catch (e) { | ||
err = e; | ||
} | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(err.code).to.be.equal('EHOSTUNREACH'); | ||
expect(log.getCall(1).args[1].toString()).to.include('429'); | ||
expect(log.callCount).to.be.equal(4); | ||
}); | ||
it('should retry get request on errors', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.get('/objects/1') | ||
.reply(500) | ||
.get('/objects/1') | ||
.reply(200, streamResponse(responseData)); | ||
const response = await objectStorage.getAsStream('1'); | ||
const out = JSON.parse(await getStream(response.stream)); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(out).to.be.deep.equal(responseData); | ||
}); | ||
it('should throw an error on post request connection error', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.post('/objects') | ||
.replyWithError({ code: 'ECONNREFUSED' }) | ||
.post('/objects') | ||
.replyWithError({ code: 'ECONNRESET' }) | ||
.post('/objects') | ||
.replyWithError({ code: 'ETIMEDOUT' }) | ||
.post('/objects') | ||
.replyWithError({ code: 'EHOSTUNREACH' }); | ||
let err; | ||
try { | ||
await objectStorage.addAsStream(postStream); | ||
} catch (e) { | ||
err = e; | ||
} | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(err.code).to.be.equal('EHOSTUNREACH'); | ||
}); | ||
it('should throw an error on post request http error', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.post('/objects') | ||
.reply(500) | ||
.post('/objects') | ||
.reply(503) | ||
.post('/objects') | ||
.reply(502) | ||
.post('/objects') | ||
.reply(429); | ||
let err; | ||
try { | ||
await objectStorage.addAsStream(postStream); | ||
} catch (e) { | ||
err = e; | ||
} | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(err.toString()).to.include('429'); | ||
}); | ||
it('should post successfully', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.post('/objects') | ||
.reply(200); | ||
const objectId = await objectStorage.addAsStream(postStream); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(objectId).to.match(/^[0-9a-z-]+$/); | ||
}); | ||
it('should use valid jwt token', async () => { | ||
const objectStorage = new ObjectStorage(config); | ||
const objectStorageCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.post('/objects') | ||
.reply(200); | ||
const objectId = await objectStorage.addAsStream(postStream); | ||
expect(objectStorageCalls.isDone()).to.be.true; | ||
expect(objectId).to.match(/^[0-9a-z-]+$/); | ||
}); | ||
}); | ||
}); | ||
describe('stream mode', () => { | ||
it('should fail after 3 get retries', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const log = sinon.stub(logging, 'warn'); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ETIMEDOUT' }) | ||
.get('/objects/1') | ||
.reply(404) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ENOTFOUND' }); | ||
describe('middlewares + zip/unzip and encrypt/decrypt', () => { | ||
describe('data mode', () => { | ||
it('should fail after 3 retries', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const log = sinon.stub(logging, 'warn'); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ETIMEDOUT' }) | ||
.get('/objects/1') | ||
.reply(429) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'EHOSTUNREACH' }) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ECONNREFUSED' }); | ||
let err; | ||
try { | ||
await objectStorageWithMiddlewares.getAsStream('1', {}); | ||
} catch (e) { | ||
err = e; | ||
} | ||
let err; | ||
try { | ||
await objectStorageWithMiddlewares.getAsJSON('1'); | ||
} catch (e) { | ||
err = e; | ||
} | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(err.code).to.be.equal('ENOTFOUND'); | ||
expect(log.getCall(1).args[1].toString()).to.include('404'); | ||
expect(log.callCount).to.be.equal(2); | ||
}); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(err.code).to.be.equal('ECONNREFUSED'); | ||
expect(log.getCall(1).args[1].toString()).to.include('429'); | ||
expect(log.callCount).to.be.equal(4); | ||
}); | ||
it('should retry get request on errors', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.get('/objects/1') | ||
.reply(500) | ||
.get('/objects/1') | ||
.reply(200, () => { | ||
const stream = streamResponse(responseData)(); | ||
return stream.pipe(encryptStream()).pipe(zip()); | ||
}); | ||
it('should retry get request 3 times on errors', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.get('/objects/1') | ||
.reply(500) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ECONNRESET' }) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ECONNREFUSED' }) | ||
.get('/objects/1') | ||
.reply(200, () => { | ||
const stream = streamResponse(responseData)(); | ||
return stream.pipe(encryptStream()).pipe(zip()); | ||
}); | ||
const response = await objectStorageWithMiddlewares.getAsStream('1', {}); | ||
const out = await objectStorageWithMiddlewares.getAsJSON('1'); | ||
const out = JSON.parse(await getStream(response.stream)); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(out).to.be.deep.equal(responseData); | ||
}); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(out).to.be.deep.equal(responseData); | ||
}); | ||
it('should throw an error on put request connection error', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.post('/objects') | ||
.replyWithError({ code: 'ECONNREFUSED' }) | ||
.post('/objects') | ||
.replyWithError({ code: 'ECONNREFUSED' }) | ||
.post('/objects') | ||
.replyWithError({ code: 'ECONNREFUSED' }); | ||
it('should retry post request 3 times on errors', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.post('/objects') | ||
.replyWithError({ code: 'ECONNREFUSED' }) | ||
.post('/objects') | ||
.replyWithError({ code: 'ECONNRESET' }) | ||
.post('/objects') | ||
.reply(429) | ||
.post('/objects') | ||
.reply(200, responseData); | ||
let err; | ||
try { | ||
await objectStorageWithMiddlewares.addAsStream(postStream, {}); | ||
} catch (e) { | ||
err = e; | ||
} | ||
await objectStorageWithMiddlewares.addAsJSON(postData); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(err.code).to.be.equal('ECONNREFUSED'); | ||
}); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
}); | ||
it('should throw an error on put request http error', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.post('/objects') | ||
.reply(409) | ||
.post('/objects') | ||
.reply(409) | ||
.post('/objects') | ||
.reply(409); | ||
it('should accept jwt token on add', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.post('/objects') | ||
.reply(200); | ||
let err; | ||
try { | ||
await objectStorageWithMiddlewares.addAsStream(postStream, {}); | ||
} catch (e) { | ||
err = e; | ||
} | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(err.toString()).to.include('409'); | ||
}); | ||
const objectId = await objectStorageWithMiddlewares.addAsJSON(postData); | ||
it('should post successfully', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch()) | ||
.post('/objects') | ||
.reply(200, { objectId: '1' }); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(objectId).to.match(/^[0-9a-z-]+$/); | ||
}); | ||
const objectId = await objectStorageWithMiddlewares.addAsStream(postStream, {}); | ||
it('should accept jwt token on get', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(objectId).to.be.equal('1'); | ||
}); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.get('/objects/1') | ||
.reply(200, () => { | ||
const stream = streamResponse(responseData)(); | ||
return stream.pipe(encryptStream()).pipe(zip()); | ||
}); | ||
it('should add 2 objects successfully', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch(jwtPayload)) | ||
.post('/objects') | ||
.reply(200, { objectId: '1' }) | ||
.post('/objects') | ||
.reply(200, { objectId: '2' }); | ||
const out = await objectStorageWithMiddlewares.getAsJSON('1'); | ||
const objectIdFirst = await objectStorageWithMiddlewares.addAsStream(postStream, sign(jwtPayload, config.jwtSecret)); | ||
const objectIdSecond = await objectStorageWithMiddlewares.addAsStream(postStream, sign(jwtPayload, config.jwtSecret)); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(objectIdFirst).to.be.equal('1'); | ||
expect(objectIdSecond).to.be.equal('2'); | ||
}); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(out).to.be.deep.equal(responseData); | ||
}); | ||
it('should get 2 objects successfully', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
it('should add 2 objects successfully', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.post('/objects') | ||
.reply(200, { objectId: '1' }) | ||
.post('/objects') | ||
.reply(200, { objectId: '2' }); | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch(jwtPayload)) | ||
.get('/objects/1') | ||
.reply(200, () => { | ||
const stream = streamResponse(responseData)(); | ||
return stream.pipe(encryptStream()).pipe(zip()); | ||
}) | ||
.get('/objects/2') | ||
.reply(200, () => { | ||
const stream = streamResponse(responseData)(); | ||
return stream.pipe(encryptStream()).pipe(zip()); | ||
}); | ||
const objectIdFirst = await objectStorageWithMiddlewares.addAsJSON(postData); | ||
const objectIdSecond = await objectStorageWithMiddlewares.addAsJSON(postData); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(objectIdFirst).to.be.equal('1'); | ||
expect(objectIdSecond).to.be.equal('2'); | ||
}); | ||
const outStreamFirst = await objectStorageWithMiddlewares.getAsStream('1', sign(jwtPayload, config.jwtSecret)); | ||
const outFirst = JSON.parse(await getStream(outStreamFirst.stream)); | ||
const outStreamSecond = await objectStorageWithMiddlewares.getAsStream('2', sign(jwtPayload, config.jwtSecret)); | ||
const outSecond = JSON.parse(await getStream(outStreamSecond.stream)); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(outFirst).to.be.deep.equal(responseData); | ||
expect(outSecond).to.be.deep.equal(responseData); | ||
it('should get 2 objects successfully', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.get('/objects/1') | ||
.reply(200, () => { | ||
const stream = streamResponse(responseData)(); | ||
return stream.pipe(encryptStream()).pipe(zip()); | ||
}) | ||
.get('/objects/2') | ||
.reply(200, () => { | ||
const stream = streamResponse(responseData)(); | ||
return stream.pipe(encryptStream()).pipe(zip()); | ||
}); | ||
const outFirst = await objectStorageWithMiddlewares.getAsJSON('1'); | ||
const outSecond = await objectStorageWithMiddlewares.getAsJSON('2'); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(outFirst).to.be.deep.equal(responseData); | ||
expect(outSecond).to.be.deep.equal(responseData); | ||
}); | ||
}); | ||
it('should use valid jwt token', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch(jwtPayload)) | ||
.post('/objects') | ||
.reply(200); | ||
describe('stream mode', () => { | ||
it('should fail after 3 get retries', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const log = sinon.stub(logging, 'warn'); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ETIMEDOUT' }) | ||
.get('/objects/1') | ||
.reply(429) | ||
.get('/objects/1') | ||
.reply(500) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'EHOSTUNREACH' }); | ||
const objectId = await objectStorageWithMiddlewares.addAsStream(postStream, jwtPayload); | ||
let err; | ||
try { | ||
await objectStorageWithMiddlewares.getAsStream('1'); | ||
} catch (e) { | ||
err = e; | ||
} | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(objectId).to.match(/^[0-9a-z-]+$/); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(err.code).to.be.equal('EHOSTUNREACH'); | ||
expect(log.getCall(1).args[1].toString()).to.include('429'); | ||
expect(log.callCount).to.be.equal(4); | ||
}); | ||
it('should retry get request on errors', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.get('/objects/1') | ||
.reply(500) | ||
.get('/objects/1') | ||
.reply(429) | ||
.get('/objects/1') | ||
.reply(200, () => { | ||
const stream = streamResponse(responseData)(); | ||
return stream.pipe(encryptStream()).pipe(zip()); | ||
}); | ||
const response = await objectStorageWithMiddlewares.getAsStream('1'); | ||
const out = JSON.parse(await getStream(response.stream)); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(out).to.be.deep.equal(responseData); | ||
}); | ||
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) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.post('/objects') | ||
.replyWithError({ code: 'ECONNREFUSED' }) | ||
.post('/objects') | ||
.replyWithError({ code: 'ETIMEDOUT' }) | ||
.post('/objects') | ||
.replyWithError({ code: 'ECONNRESET' }) | ||
.post('/objects') | ||
.replyWithError({ code: 'ECONNREFUSED' }); | ||
let err; | ||
try { | ||
await objectStorageWithMiddlewares.addAsStream(postStream); | ||
} catch (e) { | ||
err = e; | ||
} | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(err.code).to.be.equal('ECONNREFUSED'); | ||
}); | ||
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) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.post('/objects') | ||
.reply(502) | ||
.post('/objects') | ||
.reply(503) | ||
.post('/objects') | ||
.reply(500) | ||
.post('/objects') | ||
.reply(429); | ||
let err; | ||
try { | ||
await objectStorageWithMiddlewares.addAsStream(postStream); | ||
} catch (e) { | ||
err = e; | ||
} | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(err.toString()).to.include('429'); | ||
}); | ||
it('should post successfully', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.post('/objects') | ||
.reply(200, { objectId: '1' }); | ||
const objectId = await objectStorageWithMiddlewares.addAsStream(postStream); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(objectId).to.be.equal('1'); | ||
}); | ||
it('should add 2 objects successfully', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.post('/objects') | ||
.reply(200, { objectId: '1' }) | ||
.post('/objects') | ||
.reply(200, { objectId: '2' }); | ||
const objectIdFirst = await objectStorageWithMiddlewares.addAsStream(postStream); | ||
const objectIdSecond = await objectStorageWithMiddlewares.addAsStream(postStream); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(objectIdFirst).to.be.equal('1'); | ||
expect(objectIdSecond).to.be.equal('2'); | ||
}); | ||
it('should get 2 objects successfully', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.get('/objects/1') | ||
.reply(200, () => { | ||
const stream = streamResponse(responseData)(); | ||
return stream.pipe(encryptStream()).pipe(zip()); | ||
}) | ||
.get('/objects/2') | ||
.reply(200, () => { | ||
const stream = streamResponse(responseData)(); | ||
return stream.pipe(encryptStream()).pipe(zip()); | ||
}); | ||
const outStreamFirst = await objectStorageWithMiddlewares.getAsStream('1'); | ||
const outFirst = JSON.parse(await getStream(outStreamFirst.stream)); | ||
const outStreamSecond = await objectStorageWithMiddlewares.getAsStream('2'); | ||
const outSecond = JSON.parse(await getStream(outStreamSecond.stream)); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(outFirst).to.be.deep.equal(responseData); | ||
expect(outSecond).to.be.deep.equal(responseData); | ||
}); | ||
it('should use valid jwt token', async () => { | ||
const objectStorageWithMiddlewares = new ObjectStorage(config); | ||
objectStorageWithMiddlewares.use(encryptStream, decryptStream); | ||
objectStorageWithMiddlewares.use(zip, unzip); | ||
const objectStorageWithMiddlewaresCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', `Bearer ${token}`) | ||
.post('/objects') | ||
.reply(200); | ||
const objectId = await objectStorageWithMiddlewares.addAsStream(postStream); | ||
expect(objectStorageWithMiddlewaresCalls.isDone()).to.be.true; | ||
expect(objectId).to.match(/^[0-9a-z-]+$/); | ||
}); | ||
}); | ||
@@ -648,0 +1314,0 @@ }); |
@@ -63,9 +63,11 @@ import nock from 'nock'; | ||
.get('/objects/1') | ||
.reply(404) | ||
.reply(429) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'ENOTFOUND' }); | ||
.reply(500) | ||
.get('/objects/1') | ||
.replyWithError({ code: 'EHOSTUNREACH' }); | ||
let err; | ||
try { | ||
await storageClient.readStream('1', {}); | ||
await storageClient.readStream('1', { jwtPayload: {} }); | ||
} catch (e) { | ||
@@ -75,5 +77,5 @@ err = e; | ||
expect(storageClientCalls.isDone()).to.be.true; | ||
expect(err.code).to.be.equal('ENOTFOUND'); | ||
expect(log.getCall(1).args[1].toString()).to.include('404'); | ||
expect(log.callCount).to.be.equal(2); | ||
expect(err.code).to.be.equal('EHOSTUNREACH'); | ||
expect(log.getCall(1).args[1].toString()).to.include('429'); | ||
expect(log.callCount).to.be.equal(4); | ||
}); | ||
@@ -92,5 +94,7 @@ | ||
.get('/objects/1') | ||
.reply(429) | ||
.get('/objects/1') | ||
.reply(200, streamResponse(data)); | ||
const response = await storageClient.readStream('1', {}); | ||
const response = await storageClient.readStream('1', { jwtPayload: {} }); | ||
@@ -112,7 +116,9 @@ expect(storageClientCalls.isDone()).to.be.true; | ||
.post('/objects') | ||
.reply(400) | ||
.reply(429) | ||
.post('/objects') | ||
.reply(500) | ||
.post('/objects') | ||
.reply(200, responseData); | ||
const response = await storageClient.writeStream(putStream, {}); | ||
const response = await storageClient.writeStream(putStream, { jwtPayload: {} }); | ||
expect(response.data).to.be.deep.equal(responseData); | ||
@@ -122,4 +128,4 @@ expect(storageClientCalls.isDone()).to.be.true; | ||
it('should accept jwt token on add', async () => { | ||
const storageClient = new StorageClient({ uri: config.uri }); | ||
it('should accept jwt payload on add', async () => { | ||
const storageClient = new StorageClient(config); | ||
@@ -133,3 +139,3 @@ const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const response = await storageClient.writeStream(putStream, sign(jwtPayload, config.jwtSecret)); | ||
const response = await storageClient.writeStream(putStream, { jwtPayload }); | ||
expect(storageClientCalls.isDone()).to.be.true; | ||
@@ -140,5 +146,22 @@ expect(response.data).to.be.deep.equal(responseData); | ||
it('should accept jwt token on get', async () => { | ||
const storageClient = new StorageClient({ uri: config.uri }); | ||
it('should use default token on add', async () => { | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const jwtToken = sign(jwtPayload, config.jwtSecret); | ||
const storageClient = new StorageClient({ ...config, jwtToken: jwtToken }); | ||
const storageClientCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch(jwtPayload)) | ||
.post('/objects') | ||
.reply(200, responseData); | ||
const response = await storageClient.writeStream(putStream); | ||
expect(storageClientCalls.isDone()).to.be.true; | ||
expect(response.data).to.be.deep.equal(responseData); | ||
expect(storageClientCalls.isDone()).to.be.true; | ||
}); | ||
it('should accept jwt payload on get', async () => { | ||
const storageClient = new StorageClient(config); | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
@@ -151,3 +174,3 @@ const storageClientCalls = nock(config.uri) | ||
const response = await storageClient.readStream('1', sign(jwtPayload, config.jwtSecret)); | ||
const response = await storageClient.readStream('1', { jwtPayload }); | ||
expect(storageClientCalls.isDone()).to.be.true; | ||
@@ -159,8 +182,56 @@ expect(response.data).to.be.instanceOf(Readable); | ||
it('should throw exception if neither jwt secret, nor jwt token provided', async () => { | ||
const storageClient = new StorageClient({ uri: config.uri }); | ||
it('should use default token on get', async () => { | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const jwtToken = sign(jwtPayload, config.jwtSecret); | ||
const storageClient = new StorageClient({ ...config, jwtToken: jwtToken }); | ||
const storageClientCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch(jwtPayload)) | ||
.get('/objects/1') | ||
.reply(200, streamResponse(data)); | ||
const response = await storageClient.readStream('1'); | ||
expect(storageClientCalls.isDone()).to.be.true; | ||
expect(response.data).to.be.instanceOf(Readable); | ||
const resultData = JSON.parse(await getStream(response.data)); | ||
expect(resultData).to.be.deep.equal(data); | ||
}); | ||
it('should accept jwt payload on delete', async () => { | ||
const storageClient = new StorageClient(config); | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const storageClientCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch(jwtPayload)) | ||
.delete('/objects/1') | ||
.reply(200, streamResponse(data)); | ||
const response = await storageClient.deleteOne('1', { jwtPayload }); | ||
expect(storageClientCalls.isDone()).to.be.true; | ||
expect(response.data).to.be.deep.equal(data); | ||
}); | ||
it('should use default token on delete', async () => { | ||
const jwtPayload = { tenantId: '12', contractId: '1' }; | ||
const jwtToken = sign(jwtPayload, config.jwtSecret); | ||
const storageClient = new StorageClient({ ...config, jwtToken: jwtToken }); | ||
const storageClientCalls = nock(config.uri) | ||
// @ts-ignore: Nock .d.ts are outdated. | ||
.matchHeader('authorization', authHeaderMatch(jwtPayload)) | ||
.delete('/objects/1') | ||
.reply(200, streamResponse(data)); | ||
const response = await storageClient.deleteOne('1'); | ||
expect(storageClientCalls.isDone()).to.be.true; | ||
expect(response.data).to.be.deep.equal(data); | ||
}); | ||
it('should throw exception if no jwt secret, nor jwt token provided', async () => { | ||
let err; | ||
try { | ||
await storageClient.readStream('1', {}); | ||
// eslint-disable-next-line no-new | ||
new StorageClient({ uri: config.uri }); | ||
} catch (e) { | ||
@@ -172,2 +243,41 @@ err = e; | ||
}); | ||
it('should throw exception if no jwt secret, but JWT payload provided on get', async () => { | ||
const client = new StorageClient({ uri: config.uri, jwtToken: 'token' }); | ||
let err; | ||
try { | ||
// eslint-disable-next-line no-new | ||
await client.readStream('1', { jwtPayload: {} }); | ||
} catch (e) { | ||
err = e; | ||
} | ||
expect(err.toString()).to.include('JWT'); | ||
}); | ||
it('should throw exception if no jwt secret, but JWT payload provided on write', async () => { | ||
const client = new StorageClient({ uri: config.uri, jwtToken: 'token' }); | ||
let err; | ||
try { | ||
// eslint-disable-next-line no-new | ||
await client.writeStream(putStream, { jwtPayload: {} }); | ||
} catch (e) { | ||
err = e; | ||
} | ||
expect(err.toString()).to.include('JWT'); | ||
}); | ||
it('should throw exception if no jwt secret, but JWT payload provided on delete', async () => { | ||
const client = new StorageClient({ uri: config.uri, jwtToken: 'token' }); | ||
let err; | ||
try { | ||
// eslint-disable-next-line no-new | ||
await client.deleteOne('1', { jwtPayload: {} }); | ||
} catch (e) { | ||
err = e; | ||
} | ||
expect(err.toString()).to.include('JWT'); | ||
}); | ||
}); |
import { Readable, Duplex } from 'stream'; | ||
import getStream from 'get-stream'; | ||
import StorageClient from './storage-client'; | ||
import StorageClient, { StorageClientConfig, JWTPayload } from './storage-client'; | ||
interface JWTPayload { [index: string]: string }; | ||
export interface ObjectDTO { | ||
@@ -20,4 +18,4 @@ stream?: Readable; | ||
public constructor(config: {uri: string; jwtSecret?: string }) { | ||
this.client = new StorageClient({ uri: config.uri, jwtSecret: config.jwtSecret }); | ||
public constructor(config: StorageClientConfig) { | ||
this.client = new StorageClient(config); | ||
this.forwards = []; | ||
@@ -34,4 +32,4 @@ this.reverses = []; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
public async getAsJSON(objectId: string, jwtPayloadOrToken?: JWTPayload | string): Promise<any> { | ||
const objectDTO = await this.getAsStream(objectId, jwtPayloadOrToken); | ||
public async getAsJSON(objectId: string, jwtPayload?: JWTPayload): Promise<any> { | ||
const objectDTO = await this.getAsStream(objectId, jwtPayload); | ||
const data = await getStream(objectDTO.stream); | ||
@@ -41,4 +39,4 @@ return JSON.parse(data); | ||
public async getAsStream(objectId: string, jwtPayloadOrToken?: JWTPayload | string): Promise<ObjectDTO> { | ||
const res = await this.client.readStream(objectId, jwtPayloadOrToken); | ||
public async getAsStream(objectId: string, jwtPayload?: JWTPayload): Promise<ObjectDTO> { | ||
const res = await this.client.readStream(objectId, { jwtPayload }); | ||
const resultStream = this.applyMiddlewares(res.data, this.reverses); | ||
@@ -48,5 +46,9 @@ return { stream: resultStream, headers: res.headers }; | ||
public async addAsStream(stream: Readable, jwtPayloadOrToken?: JWTPayload | string): Promise<string> { | ||
public async deleteOne(objectId: string, jwtPayload?: JWTPayload): Promise<void> { | ||
await this.client.deleteOne(objectId, { jwtPayload }); | ||
} | ||
public async addAsStream(stream: Readable, jwtPayload?: JWTPayload): Promise<string> { | ||
const resultStream = this.applyMiddlewares(stream, this.forwards); | ||
const res = await this.client.writeStream(resultStream, jwtPayloadOrToken); | ||
const res = await this.client.writeStream(resultStream, { jwtPayload }); | ||
return res.data.objectId; | ||
@@ -62,3 +64,3 @@ } | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
public async addAsJSON(data: any, jwtPayloadOrToken: JWTPayload | string): Promise<string> { | ||
public async addAsJSON(data: any, jwtPayload?: JWTPayload): Promise<string> { | ||
const dataString = JSON.stringify(data); | ||
@@ -68,4 +70,4 @@ const dataStream = new Readable(); | ||
dataStream.push(null); | ||
return this.addAsStream(dataStream, jwtPayloadOrToken); | ||
return this.addAsStream(dataStream, jwtPayload); | ||
} | ||
} |
@@ -1,89 +0,189 @@ | ||
import axios, { AxiosInstance, AxiosResponse } from 'axios'; | ||
import axios, { AxiosInstance, AxiosResponse, AxiosRequestConfig, AxiosError } from 'axios'; | ||
import axiosRetry, { isNetworkError } from 'axios-retry'; | ||
import http from 'http'; | ||
import https from 'https'; | ||
import log from './logger'; | ||
import { sign } from 'jsonwebtoken'; | ||
import { promisify } from 'util'; | ||
import { Readable } from 'stream'; | ||
import logger from './logger'; | ||
interface JWTPayload { [index: string]: string }; | ||
const AXIOS_CONFIG_NAMESPACE = 'object-storage-client'; | ||
const AXIOS_RETRY_CONFIG_NAMESPACE = 'axios-retry'; | ||
export interface RequestOptions { | ||
maxAttempts?: number; | ||
delay?: number; | ||
onResponse?: (err: Error, res: AxiosResponse) => boolean; | ||
enum ObjectHeaders { | ||
ttl = 'x-eio-ttl' | ||
} | ||
interface RequestHeaders { [index: string]: string | number } | ||
export interface JWTPayload { [index: string]: string }; | ||
export interface ObjectOptions { | ||
ttl: number; | ||
}; | ||
export interface StorageClientRequestConfig { | ||
jwtPayload?: JWTPayload; | ||
objectOptions?: ObjectOptions; | ||
retryDelay?: number; | ||
retryCount?: number; | ||
} | ||
export interface StorageClientConfig { | ||
uri: string; | ||
jwtSecret?: string; | ||
jwtToken?: string; | ||
defaultRequestConfig?: StorageClientRequestConfig; | ||
} | ||
declare module 'axios' { | ||
interface AxiosRequestConfig { | ||
[AXIOS_CONFIG_NAMESPACE]?: StorageClientRequestConfig; | ||
} | ||
} | ||
export default class StorageClient { | ||
private api: AxiosInstance; | ||
private readonly jwtSecret?: string; | ||
private readonly api: AxiosInstance; | ||
private static readonly DEFAULT_RETRY_DELAY: 100; | ||
private static readonly DEFAULT_RETRY_COUNT: 3; | ||
private static httpAgent = new http.Agent({ keepAlive: true }); | ||
private static httpsAgent = new https.Agent({ keepAlive: true }); | ||
public constructor(config: { uri: string; jwtSecret?: string }) { | ||
this.api = axios.create({ | ||
baseURL: `${config.uri}/`, | ||
public constructor(config: StorageClientConfig) { | ||
const { uri, jwtSecret, defaultRequestConfig, jwtToken } = config; | ||
this.api = StorageClient.createApiClient(jwtSecret, jwtToken, uri, defaultRequestConfig); | ||
} | ||
public async readStream( | ||
objectId: string, | ||
config?: StorageClientRequestConfig | ||
): Promise<AxiosResponse> { | ||
const res = await this.api.get( | ||
`/objects/${objectId}`, | ||
{ | ||
responseType: 'stream', | ||
[AXIOS_CONFIG_NAMESPACE]: config | ||
}); | ||
return res; | ||
} | ||
public async writeStream( | ||
stream: Readable, | ||
config?: StorageClientRequestConfig | ||
): Promise<AxiosResponse> { | ||
const res = await this.api.post( | ||
`/objects`, | ||
stream, | ||
{ | ||
headers: { | ||
'content-type': 'application/octet-stream' | ||
}, | ||
[AXIOS_CONFIG_NAMESPACE]: config | ||
}); | ||
return res; | ||
} | ||
public async deleteOne( | ||
objectId: string, | ||
config?: StorageClientRequestConfig | ||
): Promise<AxiosResponse> { | ||
const res = await this.api.delete( | ||
`/objects/${objectId}`, | ||
{ | ||
[AXIOS_CONFIG_NAMESPACE]: config | ||
}); | ||
return res; | ||
} | ||
private static retryCondition(err: AxiosError) { | ||
if (isNetworkError(err)) { | ||
logger.warn('Error during object request: %s', err); | ||
return true; | ||
} | ||
const { response } = err; | ||
if (!response) { | ||
return false; | ||
} | ||
if (response.status === 429 || (response.status >= 500 && response.status <= 599)) { | ||
logger.warn('Error during object request: %s', `${response.status} (${response.statusText})`); | ||
return true; | ||
} | ||
return false; | ||
} | ||
private static createApiClient( | ||
jwtSecret: string, | ||
jwtToken: string, | ||
uri: string, | ||
requestConfig: StorageClientRequestConfig | ||
): AxiosInstance { | ||
if (!jwtSecret && !jwtToken) { | ||
throw new Error('Neither JWT token passed, nor JWT secret provided.'); | ||
} | ||
const api = axios.create({ | ||
baseURL: `${uri}/`, | ||
httpAgent: StorageClient.httpAgent, | ||
httpsAgent: StorageClient.httpsAgent, | ||
validateStatus: null, | ||
maxContentLength: Infinity, | ||
maxRedirects: 0 | ||
maxContentLength: Infinity | ||
}); | ||
this.jwtSecret = config.jwtSecret; | ||
} | ||
private async requestRetry(request: () => Promise<AxiosResponse>, { maxAttempts = 3, delay = 100, onResponse }: RequestOptions = {}): Promise<AxiosResponse> { | ||
let attempts = 0; | ||
let res; | ||
let err; | ||
while (attempts < maxAttempts) { | ||
err = null; | ||
res = null; | ||
attempts++; | ||
try { | ||
res = await request(); | ||
} catch (e) { | ||
err = e; | ||
if (jwtToken) { | ||
api.defaults.headers.common.Authorization = `Bearer ${jwtToken}`; | ||
} | ||
// pls do not move axios interceptors init order, | ||
// since middleware below sets values for retry meddileware, which sould be next | ||
api.interceptors.request.use(async (config) => { | ||
const { [AXIOS_CONFIG_NAMESPACE]: cfg } = config; | ||
if (!cfg) { | ||
return config; | ||
} | ||
if (onResponse && onResponse(err, res)) { | ||
continue; | ||
let newConfig = { | ||
...config, | ||
[AXIOS_RETRY_CONFIG_NAMESPACE]: config[AXIOS_RETRY_CONFIG_NAMESPACE] || {} | ||
}; | ||
const { jwtPayload, retryDelay, retryCount, objectOptions } = cfg; | ||
if (jwtPayload) { | ||
if (!jwtSecret) { | ||
throw new Error('JWT secret is not provided during initialization, can not encode JWT payload'); | ||
} | ||
const token = await promisify(sign)(jwtPayload, jwtSecret); | ||
newConfig.headers.Authorization = `Bearer ${token}`; | ||
}; | ||
if (typeof retryDelay === 'number') { | ||
newConfig['axios-retry'].retryDelay = () => retryDelay; | ||
} | ||
// last attempt error should not be logged | ||
if ((err || res.status >= 400) && attempts < maxAttempts) { | ||
log.warn('Error during object request: %s', err || `${res.status} (${res.statusText})`); | ||
await new Promise((resolve): NodeJS.Timeout => setTimeout(resolve, delay)); | ||
continue; | ||
if (typeof retryCount === 'number') { | ||
newConfig['axios-retry'].retries = retryCount; | ||
} | ||
break; | ||
} | ||
if (err || res.status >= 400) { | ||
throw err || new Error(`HTTP error during object request: ${res.status} (${res.statusText})`); | ||
} | ||
return res; | ||
} | ||
private async getHeaders(jwtPayloadOrToken: JWTPayload | string, override?: { [index: string]: string }) { | ||
if (typeof jwtPayloadOrToken !== 'string' && !this.jwtSecret) { | ||
throw new Error('Neither JWT token passed, nor JWT secret provided during initialization'); | ||
} | ||
const token = typeof jwtPayloadOrToken === 'string' | ||
? jwtPayloadOrToken | ||
: await promisify(sign)(jwtPayloadOrToken, this.jwtSecret); | ||
return { Authorization: `Bearer ${token}`, ...override }; | ||
} | ||
if (objectOptions) { | ||
newConfig.headers[ObjectHeaders.ttl] = objectOptions.ttl; | ||
} | ||
public async readStream(objectId: string, jwtPayloadOrToken?: JWTPayload | string): Promise<AxiosResponse> { | ||
const res = await this.requestRetry( | ||
async (): Promise<AxiosResponse> => this.api.get(`/objects/${objectId}`, { responseType: 'stream', headers: await this.getHeaders(jwtPayloadOrToken) }) | ||
return newConfig; | ||
}); | ||
axiosRetry( | ||
api, | ||
{ | ||
retries: requestConfig && typeof requestConfig.retryCount === 'number' | ||
? requestConfig.retryCount | ||
: this.DEFAULT_RETRY_COUNT, | ||
retryDelay: () => requestConfig && typeof requestConfig.retryDelay === 'number' | ||
? requestConfig.retryDelay | ||
: this.DEFAULT_RETRY_DELAY, | ||
retryCondition: StorageClient.retryCondition | ||
} | ||
); | ||
return res; | ||
} | ||
public async writeStream(stream: Readable, jwtPayloadOrToken?: JWTPayload | string): Promise<AxiosResponse> { | ||
const res = await this.requestRetry( | ||
async (): Promise<AxiosResponse> => this.api.post(`/objects`, stream, { headers: await this.getHeaders(jwtPayloadOrToken, { 'content-type': 'application/octet-stream' }) }) | ||
); | ||
return res; | ||
return api; | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
96671
1902
2
6
+ Addedaxios-retry@3.1.9
+ Addedaxios@0.21.0(transitive)
+ Addedaxios-retry@3.1.9(transitive)
+ Addedfollow-redirects@1.15.9(transitive)
+ Addedis-retry-allowed@1.2.0(transitive)
- Removedaxios@0.19.0(transitive)
- Removedfollow-redirects@1.5.10(transitive)
- Removedis-buffer@2.0.5(transitive)
Updatedaxios@0.21.0