diff --git a/src/lib/bypasser/key.ts b/src/lib/bypasser/key.ts index 36cd1db..5bc93ac 100644 --- a/src/lib/bypasser/key.ts +++ b/src/lib/bypasser/key.ts @@ -3,11 +3,11 @@ */ export default class Key { key: string; - value?: boolean; + value: boolean; constructor(key: string) { this.key = key; - this.value = undefined; + this.value = false; } /** @@ -34,7 +34,7 @@ export default class Key { /** * Return current value */ - getValue(): boolean | undefined { + getValue(): boolean { return this.value; } } diff --git a/src/lib/remote.ts b/src/lib/remote.ts index de9b8fd..fe6dcbd 100644 --- a/src/lib/remote.ts +++ b/src/lib/remote.ts @@ -18,6 +18,22 @@ const getHeader = (token: string | undefined) => { }; }; +const trySilent = (options: SwitcherOptions, retryOptions: RetryOptions) => { + if (options && 'silentMode' in options) { + if (options.silentMode) { + const expirationTime = new DateMoment(new Date()) + .add(retryOptions.retryTime, retryOptions.retryDurationIn).getDate(); + + return { + data: { + token: 'SILENT', + exp: expirationTime.getTime() / 1000, + }, + }; + } + } +}; + export const getEntry = (input?: string[][]) => { if (!input) { return undefined; @@ -45,19 +61,7 @@ export const checkAPIHealth = async (url: string, options: SwitcherOptions, retr throw new ApiConnectionError('API is offline'); } } catch (_e) { - if (options && 'silentMode' in options) { - if (options.silentMode) { - const expirationTime = new DateMoment(new Date()) - .add(retryOptions.retryTime, retryOptions.retryDurationIn).getDate(); - - return { - data: { - token: 'SILENT', - exp: expirationTime.getTime() / 1000, - }, - }; - } - } + return trySilent(options, retryOptions); } }; @@ -78,7 +82,11 @@ export const checkCriteria = async ( }, ); - return response.json(); + if (response.status == 200) { + return response.json(); + } + + throw new Error(`[checkCriteria] failed with status ${response.status}`); } catch (e) { throw new CriteriaError( e.errno ? getConnectivityError(e.errno) : e.message, @@ -101,7 +109,11 @@ export const auth = async (context: SwitcherContext) => { }, }); - return response.json(); + if (response.status == 200) { + return response.json(); + } + + throw new Error(`[auth] failed with status ${response.status}`); } catch (e) { throw new AuthError(e.errno ? getConnectivityError(e.errno) : e.message); } @@ -145,7 +157,11 @@ export const checkSnapshotVersion = async ( headers: getHeader(token), }); - return response.json(); + if (response.status == 200) { + return response.json(); + } + + throw new Error(`[checkSnapshotVersion] failed with status ${response.status}`); } catch (e) { throw new SnapshotServiceError( e.errno ? getConnectivityError(e.errno) : e.message, @@ -182,7 +198,11 @@ export const resolveSnapshot = async ( headers: getHeader(token), }); - return JSON.stringify(await response.json(), null, 4); + if (response.status == 200) { + return JSON.stringify(await response.json(), null, 4); + } + + throw new Error(`[resolveSnapshot] failed with status ${response.status}`); } catch (e) { throw new SnapshotServiceError( e.errno ? getConnectivityError(e.errno) : e.message, diff --git a/src/switcher-client.ts b/src/switcher-client.ts index ee7daba..f27d9ae 100644 --- a/src/switcher-client.ts +++ b/src/switcher-client.ts @@ -205,12 +205,20 @@ export class Switcher { if (Switcher._options.offline && Switcher._snapshot) { checkSwitchers(Switcher._snapshot, switcherKeys); } else { - await Switcher._auth(); - await services.checkSwitchers( - Switcher._context.url || '', - Switcher._context.token, - switcherKeys, - ); + try { + await Switcher._auth(); + await services.checkSwitchers( + Switcher._context.url || '', + Switcher._context.token, + switcherKeys, + ); + } catch (e) { + if (Switcher._options.silentMode) { + checkSwitchers(Switcher._snapshot!, switcherKeys); + } else { + throw e; + } + } } } @@ -384,7 +392,7 @@ export class Switcher { * @param input * @param showReason Display details when using ExecutionLogger */ - async isItOn(key?: string, input?: string[][], showReason = false) { + async isItOn(key?: string, input?: string[][], showReason = false): Promise { let result; this._validateArgs(key, input); @@ -398,11 +406,19 @@ export class Switcher { if (Switcher._options.offline) { result = await this._executeOfflineCriteria(); } else { - await this.validate(); - if (Switcher._context.token === 'SILENT') { - result = await this._executeOfflineCriteria(); - } else { - result = await this._executeOnlineCriteria(showReason); + try { + await this.validate(); + if (Switcher._context.token === 'SILENT') { + result = await this._executeOfflineCriteria(); + } else { + result = await this._executeOnlineCriteria(showReason); + } + } catch (e) { + if (Switcher._options.silentMode) { + return this._executeOfflineCriteria(); + } + + throw e; } } diff --git a/test/switcher-integrated.test.ts b/test/switcher-integrated.test.ts index 484f818..aa13d21 100644 --- a/test/switcher-integrated.test.ts +++ b/test/switcher-integrated.test.ts @@ -56,6 +56,20 @@ describe('Integrated test - Switcher:', function () { assertTrue(await switcher.isItOn()); }); + it('should NOT be valid - API returned 429 (too many requests)', async function () { + // given API responding properly + given('GET@/check', null); + given('POST@/criteria/auth', null, 429); + + // test + Switcher.buildContext(contextSettings); + const switcher = Switcher.factory(); + + await assertRejects(async () => + await switcher.isItOn('FLAG_1'), + Error, 'Something went wrong: [auth] failed with status 429'); + }); + it('should be valid - throttle', async function () { // given API responding properly given('GET@/check', null); @@ -94,6 +108,84 @@ describe('Integrated test - Switcher:', function () { }); }); + describe('check fail response (e2e):', function () { + + let contextSettings: SwitcherContext; + + afterAll(function() { + Switcher.unloadSnapshot(); + }); + + beforeEach(function() { + tearDown(); + Switcher.setTestEnabled(); + + contextSettings = { + url: 'http://localhost:3000', + apiKey: '[apiKey]', + domain: '[domain]', + component: '[component]', + environment: 'default' + }; + }); + + it('should NOT be valid - API returned 429 (too many requests) at checkHealth/auth', async function () { + // given API responding properly + given('GET@/check', null, 429); + given('POST@/criteria/auth', { error: 'Too many requests' }, 429); + + // test + Switcher.buildContext(contextSettings); + const switcher = Switcher.factory(); + + await assertRejects(async () => + await switcher.isItOn('FLAG_1'), + Error, 'Something went wrong: [auth] failed with status 429'); + }); + + it('should NOT be valid - API returned 429 (too many requests) at checkCriteria', async function () { + // given API responding properly + given('GET@/check', null); + given('POST@/criteria/auth', generateAuth('[auth_token]', 5)); + given('POST@/criteria', { error: 'Too many requests' }, 429); + + // test + Switcher.buildContext(contextSettings); + const switcher = Switcher.factory(); + + await assertRejects(async () => + await switcher.isItOn('FLAG_1'), + Error, 'Something went wrong: [checkCriteria] failed with status 429'); + }); + + it('should use silent mode when fail to check switchers', async function() { + //given + given('GET@/check', null, 429); + + //test + Switcher.buildContext(contextSettings, { silentMode: true }); + await assertRejects(async () => + await Switcher.checkSwitchers(['FEATURE01', 'FEATURE02']), + Error, 'Something went wrong: [FEATURE01,FEATURE02] not found'); + + await Switcher.checkSwitchers(['FF2FOR2021', 'FF2FOR2021']); + }); + + it('should use silent mode when fail to check criteria', async function () { + // given API responding properly + given('GET@/check', null); + given('POST@/criteria/auth', generateAuth('[auth_token]', 5)); + given('POST@/criteria', { error: 'Too many requests' }, 429); + + // test + Switcher.buildContext(contextSettings, { silentMode: true }); + const switcher = Switcher.factory(); + + assertTrue(await switcher.isItOn('FF2FOR2022')); + }); + + }); + describe('check criteria:', function () { beforeEach(function() { diff --git a/test/switcher-snapshot.test.ts b/test/switcher-snapshot.test.ts index 9acca6e..ae0d512 100644 --- a/test/switcher-snapshot.test.ts +++ b/test/switcher-snapshot.test.ts @@ -41,6 +41,35 @@ describe('E2E test - Switcher offline - Snapshot:', function () { Deno.removeSync('generated-snapshots/', { recursive: true }); }); + it('should NOT update snapshot - Too many requests at checkSnapshotVersion', testSettings, async function () { + //give + given('POST@/criteria/auth', generateAuth(token, 5)); + given('GET@/criteria/snapshot_check/:version', null, 429); + + //test + Switcher.setTestEnabled(); + await Switcher.loadSnapshot(); + await assertRejects(async () => + await Switcher.checkSnapshot(), + Error, 'Something went wrong: [checkSnapshotVersion] failed with status 429'); + }); + + it('should NOT update snapshot - Too many requests at resolveSnapshot', testSettings, async function () { + //given + given('POST@/criteria/auth', generateAuth(token, 5)); + given('GET@/criteria/snapshot_check/:version', generateStatus(false)); // Snapshot outdated + given('POST@/graphql', null, 429); + + //test + Switcher.buildContext(contextSettings, { + snapshotLocation: 'generated-snapshots/' + }); + + await assertRejects(async () => + await Switcher.loadSnapshot(), + Error, 'Something went wrong: [resolveSnapshot] failed with status 429'); + }); + it('should update snapshot', testSettings, async function () { await delay(2000);