Skip to content

Commit

Permalink
Added switcher.defaultResult() to handle panic events (#72)
Browse files Browse the repository at this point in the history
* Added switcher.defaultResult() to handle panic events

* chore: fixed lint issue

* chore: fixed test resource leak settings

* chore: disable test matrix
  • Loading branch information
petruki committed May 12, 2024
1 parent 4dd26a8 commit f671d0b
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 67 deletions.
34 changes: 17 additions & 17 deletions .github/workflows/master.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,23 +44,23 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

test-matrix:
name: Test Matrix - Deno ${{ matrix.deno-version }} on ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
deno-version: [v1.40.0, v1.43.x]
os: [ ubuntu-latest, windows-latest ]
runs-on: ${{ matrix.os }}
if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
# test-matrix:
# name: Test Matrix - Deno ${{ matrix.deno-version }} on ${{ matrix.os }}
# strategy:
# fail-fast: false
# matrix:
# deno-version: [v1.43.x]
# os: [ ubuntu-latest, windows-latest ]
# runs-on: ${{ matrix.os }}
# if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"

steps:
- name: Git checkout
uses: actions/checkout@v4
# steps:
# - name: Git checkout
# uses: actions/checkout@v4

- name: Setup Deno ${{ matrix.deno-version }}
uses: denoland/setup-deno@v1
with:
deno-version: ${{ matrix.deno-version }}
# - name: Setup Deno ${{ matrix.deno-version }}
# uses: denoland/setup-deno@v1
# with:
# deno-version: ${{ matrix.deno-version }}

- run: deno task test
# - run: deno task test
95 changes: 62 additions & 33 deletions src/switcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class Switcher {
private _nextRun = 0;
private _input?: string[][];
private _key = '';
private _defaultResult: boolean | undefined;
private _forceRemote = false;
private _showDetail = false;

Expand Down Expand Up @@ -74,27 +75,28 @@ export class Switcher {
return this._showDetail ? response : response.result;
}

// verify if query from snapshot
if (Client.options.local && !this._forceRemote) {
result = await this._executeLocalCriteria();
} else {
try {
await this.validate();
if (Auth.getToken() === 'SILENT') {
result = await this._executeLocalCriteria();
} else {
result = await this._executeRemoteCriteria();
}
} catch (err) {
this._notifyError(err);
try {
// verify if query from snapshot
if (Client.options.local && !this._forceRemote) {
return await this._executeLocalCriteria();
}

if (Client.options.silentMode) {
Auth.updateSilentToken();
return this._executeLocalCriteria();
}
// otherwise, execute remote criteria or local snapshot when silent mode is enabled
await this.validate();
if (Auth.getToken() === 'SILENT') {
result = await this._executeLocalCriteria();
} else {
result = await this._executeRemoteCriteria();
}
} catch (err) {
this._notifyError(err);

throw err;
if (Client.options.silentMode) {
Auth.updateSilentToken();
return this._executeLocalCriteria();
}

throw err;
}

return result;
Expand Down Expand Up @@ -135,6 +137,14 @@ export class Switcher {
return this;
}

/**
* Define a default result when the client enters in panic mode
*/
defaultResult(defaultResult: boolean): this {
this._defaultResult = defaultResult;
return this;
}

/**
* Adds a strategy for validation
*/
Expand Down Expand Up @@ -203,11 +213,15 @@ export class Switcher {
let responseCriteria: ResultDetail;

if (this._useSync()) {
responseCriteria = await remote.checkCriteria(
this._key,
this._input,
this._showDetail,
);
try {
responseCriteria = await remote.checkCriteria(
this._key,
this._input,
this._showDetail,
);
} catch (err) {
responseCriteria = this.getDefaultResultOrThrow(err);
}

if (Client.options.logger && this._key) {
ExecutionLogger.add(responseCriteria, this._key, this._input);
Expand Down Expand Up @@ -260,17 +274,18 @@ export class Switcher {
}
}

async _executeLocalCriteria(): Promise<
boolean | {
result: boolean;
reason: string;
async _executeLocalCriteria(): Promise<boolean | ResultDetail> {
let response: ResultDetail;

try {
response = await checkCriteriaLocal(
Client.snapshot,
util.get(this._key, ''),
util.get(this._input, []),
);
} catch (err) {
response = this.getDefaultResultOrThrow(err);
}
> {
const response = await checkCriteriaLocal(
Client.snapshot,
util.get(this._key, ''),
util.get(this._input, []),
);

if (Client.options.logger) {
ExecutionLogger.add(response, this._key, this._input);
Expand All @@ -293,6 +308,20 @@ export class Switcher {
return this._delay == 0 || !ExecutionLogger.getExecution(this._key, this._input);
}

private getDefaultResultOrThrow(err: Error): ResultDetail {
if (this._defaultResult === undefined) {
throw err;
}

const response = {
result: this._defaultResult,
reason: 'Default result',
};

this._notifyError(err);
return response;
}

/**
* Return switcher key
*/
Expand Down
6 changes: 5 additions & 1 deletion test/playground/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,17 @@ const _testThrottledAPICall = async () => {
setupSwitcher(false);

await Client.checkSwitchers([SWITCHER_KEY]);
Client.subscribeNotifyError((error) => console.log(error));

switcher = Client.getSwitcher();
switcher.throttle(1000);

setInterval(async () => {
const time = Date.now();
const result = await switcher.isItOn(SWITCHER_KEY);
const result = await switcher
.detail()
.isItOn(SWITCHER_KEY);

console.log(`- ${Date.now() - time} ms - ${JSON.stringify(result)}`);
}, 1000);

Expand Down
11 changes: 10 additions & 1 deletion test/switcher-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ describe('E2E test - Client local:', function () {
assertEquals(metadata, { value: 'something' });
});

it('should be valid assuming unknown key to be true', testSettings, async function () {
it('should be valid assuming unknown key to be true and throw error when forgetting', testSettings, async function () {
await switcher
.checkValue('Japan')
.checkNetwork('10.0.0.3')
Expand Down Expand Up @@ -268,4 +268,13 @@ describe('E2E test - Client local:', function () {
assertEquals(error?.message, 'Something went wrong: It was not possible to load the file at //somewhere/');
});

it('should not throw error when a default result is provided', testSettings, async function () {
Client.buildContext({ url, apiKey, domain, component, environment }, {
local: true
});

const switcher = Client.getSwitcher('UNKNOWN_FEATURE').defaultResult(true);
assertTrue(await switcher.isItOn());
});

});
45 changes: 30 additions & 15 deletions test/switcher-functional.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe('Integrated test - Client:', function () {
});

it('should be valid', async function () {
// given API responding properly
// given API responses
given('POST@/criteria/auth', generateAuth('[auth_token]', 5));
given('POST@/criteria', generateResult(true));

Expand All @@ -53,8 +53,23 @@ describe('Integrated test - Client:', function () {
assertTrue(await switcher.isItOn());
});

it('should NOT throw error when default result is provided using remote', async function () {
// given API responses
given('POST@/criteria/auth', generateAuth('[auth_token]', 5));
given('POST@/criteria', { message: 'ERROR' }, 404);

// test
let asyncErrorMessage = null;
Client.buildContext(contextSettings);
Client.subscribeNotifyError((error) => asyncErrorMessage = error.message);
const switcher = Client.getSwitcher().defaultResult(true);

assertTrue(await switcher.isItOn('UNKNOWN_FEATURE'));
assertEquals(asyncErrorMessage, 'Something went wrong: [checkCriteria] failed with status 404');
});

it('should NOT be valid - API returned 429 (too many requests)', async function () {
// given API responding properly
// given API responses
given('POST@/criteria/auth', undefined, 429);

// test
Expand All @@ -67,7 +82,7 @@ describe('Integrated test - Client:', function () {
});

it('should be valid - throttle', async function () {
// given API responding properly
// given API responses
given('POST@/criteria/auth', generateAuth('[auth_token]', 5));
given('POST@/criteria', generateResult(true));

Expand All @@ -88,7 +103,7 @@ describe('Integrated test - Client:', function () {
});

it('should be valid - throttle - with details', async function () {
// given API responding properly
// given API responses
given('POST@/criteria/auth', generateAuth('[auth_token]', 5));
given('POST@/criteria', generateResult(true));

Expand All @@ -106,7 +121,7 @@ describe('Integrated test - Client:', function () {
});

it('should renew token when using throttle', async function () {
// given API responding properly
// given API responses
// first API call
given('POST@/criteria/auth', generateAuth('[auth_token]', 1));
given('POST@/criteria', generateResult(true)); // before token expires
Expand Down Expand Up @@ -149,7 +164,7 @@ describe('Integrated test - Client:', function () {
});

it('should not crash when async checkCriteria fails', async function () {
// given API responding properly
// given API responses
// first API call
given('POST@/criteria/auth', generateAuth('[auth_token]', 1));
given('POST@/criteria', generateResult(true)); // before token expires
Expand Down Expand Up @@ -199,7 +214,7 @@ describe('Integrated test - Client:', function () {
});

it('should return false - same switcher return false when remote', async function () {
// given API responding properly
// given API responses
given('POST@/criteria/auth', generateAuth('[auth_token]', 5));
given('POST@/criteria', generateResult(false));

Expand All @@ -215,7 +230,7 @@ describe('Integrated test - Client:', function () {
});

it('should return true - including reason and metadata', async function () {
// given API responding properly
// given API responses
given('POST@/criteria/auth', generateAuth('[auth_token]', 5));
given('POST@/criteria', generateDetailedResult({
result: true,
Expand Down Expand Up @@ -269,7 +284,7 @@ describe('Integrated test - Client:', function () {
});

it('should NOT be valid - API returned 429 (too many requests) at checkHealth/auth', async function () {
// given API responding properly
// given API responses
given('POST@/criteria/auth', { error: 'Too many requests' }, 429);

// test
Expand All @@ -282,7 +297,7 @@ describe('Integrated test - Client:', function () {
});

it('should NOT be valid - API returned 429 (too many requests) at checkCriteria', async function () {
// given API responding properly
// given API responses
given('POST@/criteria/auth', generateAuth('[auth_token]', 5));
given('POST@/criteria', { error: 'Too many requests' }, 429);

Expand All @@ -305,7 +320,7 @@ describe('Integrated test - Client:', function () {
});

it('should use silent mode when fail to check criteria', async function () {
// given API responding properly
// given API responses
given('POST@/criteria/auth', generateAuth('[auth_token]', 5));
given('POST@/criteria', { error: 'Too many requests' }, 429);

Expand All @@ -329,7 +344,7 @@ describe('Integrated test - Client:', function () {
});

it('should be valid', async function () {
// given API responding properly
// given API responses
given('POST@/criteria/auth', generateAuth('[auth_token]', 5));
given('POST@/criteria', generateResult(true));

Expand Down Expand Up @@ -402,7 +417,7 @@ describe('Integrated test - Client:', function () {
});

it('should renew the token after expiration', async function () {
// given API responding properly
// given API responses
given('POST@/criteria/auth', generateAuth('[auth_token]', 1));

Client.buildContext(contextSettings);
Expand Down Expand Up @@ -432,7 +447,7 @@ describe('Integrated test - Client:', function () {
});

it('should be valid - when sending key without calling prepare', async function () {
// given API responding properly
// given API responses
given('POST@/criteria/auth', generateAuth('[auth_token]', 5));
given('POST@/criteria', generateResult(true));

Expand All @@ -446,7 +461,7 @@ describe('Integrated test - Client:', function () {
});

it('should be valid - when preparing key and sending input strategy afterwards', async function () {
// given API responding properly
// given API responses
given('POST@/criteria/auth', generateAuth('[auth_token]', 5));
given('POST@/criteria', generateResult(true));

Expand Down

0 comments on commit f671d0b

Please sign in to comment.