Skip to content

Commit

Permalink
chore: Refactor hook execution. (#417)
Browse files Browse the repository at this point in the history
This code lifts the hook running code out of the client.
  • Loading branch information
kinyoklion authored Apr 2, 2024
1 parent 669acb8 commit e27f298
Show file tree
Hide file tree
Showing 5 changed files with 605 additions and 426 deletions.
227 changes: 3 additions & 224 deletions packages/shared/sdk-server/__tests__/LDClient.hooks.test.ts
Original file line number Diff line number Diff line change
@@ -1,78 +1,14 @@
import { basicPlatform } from '@launchdarkly/private-js-mocks';

import { integrations, LDClientImpl, LDEvaluationDetail, LDMigrationStage } from '../src';
import { LDClientImpl, LDMigrationStage } from '../src';
import Reasons from '../src/evaluation/Reasons';
import TestData from '../src/integrations/test_data/TestData';
import TestLogger, { LogLevel } from './Logger';
import { TestHook } from './hooks/TestHook';
import TestLogger from './Logger';
import makeCallbacks from './makeCallbacks';

const defaultUser = { kind: 'user', key: 'user-key' };

type EvalCapture = {
method: string;
hookContext: integrations.EvaluationSeriesContext;
hookData: integrations.EvaluationSeriesData;
detail?: LDEvaluationDetail;
};

class TestHook implements integrations.Hook {
captureBefore: EvalCapture[] = [];
captureAfter: EvalCapture[] = [];

getMetadataImpl: () => integrations.HookMetadata = () => ({ name: 'LaunchDarkly Test Hook' });

getMetadata(): integrations.HookMetadata {
return this.getMetadataImpl();
}

verifyBefore(
hookContext: integrations.EvaluationSeriesContext,
data: integrations.EvaluationSeriesData,
) {
expect(this.captureBefore).toHaveLength(1);
expect(this.captureBefore[0].hookContext).toEqual(hookContext);
expect(this.captureBefore[0].hookData).toEqual(data);
}

verifyAfter(
hookContext: integrations.EvaluationSeriesContext,
data: integrations.EvaluationSeriesData,
detail: LDEvaluationDetail,
) {
expect(this.captureAfter).toHaveLength(1);
expect(this.captureAfter[0].hookContext).toEqual(hookContext);
expect(this.captureAfter[0].hookData).toEqual(data);
expect(this.captureAfter[0].detail).toEqual(detail);
}

beforeEvalImpl: (
hookContext: integrations.EvaluationSeriesContext,
data: integrations.EvaluationSeriesData,
) => integrations.EvaluationSeriesData = (_hookContext, data) => data;

afterEvalImpl: (
hookContext: integrations.EvaluationSeriesContext,
data: integrations.EvaluationSeriesData,
detail: LDEvaluationDetail,
) => integrations.EvaluationSeriesData = (_hookContext, data, _detail) => data;

beforeEvaluation?(
hookContext: integrations.EvaluationSeriesContext,
data: integrations.EvaluationSeriesData,
): integrations.EvaluationSeriesData {
this.captureBefore.push({ method: 'beforeEvaluation', hookContext, hookData: data });
return this.beforeEvalImpl(hookContext, data);
}
afterEvaluation?(
hookContext: integrations.EvaluationSeriesContext,
data: integrations.EvaluationSeriesData,
detail: LDEvaluationDetail,
): integrations.EvaluationSeriesData {
this.captureAfter.push({ method: 'afterEvaluation', hookContext, hookData: data, detail });
return this.afterEvalImpl(hookContext, data, detail);
}
}

describe('given an LDClient with test data', () => {
let client: LDClientImpl;
let td: TestData;
Expand Down Expand Up @@ -409,105 +345,6 @@ describe('given an LDClient with test data', () => {
},
);
});

it('propagates data between stages', async () => {
testHook.beforeEvalImpl = (
_hookContext: integrations.EvaluationSeriesContext,
data: integrations.EvaluationSeriesData,
) => ({
...data,
added: 'added data',
});
await client.variation('flagKey', defaultUser, false);

testHook.verifyAfter(
{
flagKey: 'flagKey',
context: { ...defaultUser },
defaultValue: false,
method: 'LDClient.variation',
},
{ added: 'added data' },
{
value: false,
reason: { kind: 'ERROR', errorKind: 'FLAG_NOT_FOUND' },
variationIndex: null,
},
);
});

it('handles an exception thrown in beforeEvaluation', async () => {
testHook.beforeEvalImpl = (
_hookContext: integrations.EvaluationSeriesContext,
_data: integrations.EvaluationSeriesData,
) => {
throw new Error('bad hook');
};
await client.variation('flagKey', defaultUser, false);
logger.expectMessages([
{
level: LogLevel.Error,
matches:
/An error was encountered in "beforeEvaluation" of the "LaunchDarkly Test Hook" hook: Error: bad hook/,
},
]);
});

it('handles an exception thrown in afterEvaluation', async () => {
testHook.afterEvalImpl = () => {
throw new Error('bad hook');
};
await client.variation('flagKey', defaultUser, false);
logger.expectMessages([
{
level: LogLevel.Error,
matches:
/An error was encountered in "afterEvaluation" of the "LaunchDarkly Test Hook" hook: Error: bad hook/,
},
]);
});

it('handles exception getting the hook metadata', async () => {
testHook.getMetadataImpl = () => {
throw new Error('bad hook');
};
await client.variation('flagKey', defaultUser, false);

logger.expectMessages([
{
level: LogLevel.Error,
matches: /Exception thrown getting metadata for hook. Unable to get hook name./,
},
]);
});

it('uses unknown name when the name cannot be accessed', async () => {
testHook.beforeEvalImpl = (
_hookContext: integrations.EvaluationSeriesContext,
_data: integrations.EvaluationSeriesData,
) => {
throw new Error('bad hook');
};
testHook.getMetadataImpl = () => {
throw new Error('bad hook');
};
testHook.afterEvalImpl = () => {
throw new Error('bad hook');
};
await client.variation('flagKey', defaultUser, false);
logger.expectMessages([
{
level: LogLevel.Error,
matches:
/An error was encountered in "afterEvaluation" of the "unknown hook" hook: Error: bad hook/,
},
{
level: LogLevel.Error,
matches:
/An error was encountered in "beforeEvaluation" of the "unknown hook" hook: Error: bad hook/,
},
]);
});
});

it('can add a hook after initialization', async () => {
Expand Down Expand Up @@ -555,61 +392,3 @@ it('can add a hook after initialization', async () => {
},
);
});

it('executes hook stages in the specified order', async () => {
const beforeCalledOrder: string[] = [];
const afterCalledOrder: string[] = [];

const hookA = new TestHook();
hookA.beforeEvalImpl = (_context, data) => {
beforeCalledOrder.push('a');
return data;
};

hookA.afterEvalImpl = (_context, data, _detail) => {
afterCalledOrder.push('a');
return data;
};

const hookB = new TestHook();
hookB.beforeEvalImpl = (_context, data) => {
beforeCalledOrder.push('b');
return data;
};
hookB.afterEvalImpl = (_context, data, _detail) => {
afterCalledOrder.push('b');
return data;
};

const hookC = new TestHook();
hookC.beforeEvalImpl = (_context, data) => {
beforeCalledOrder.push('c');
return data;
};

hookC.afterEvalImpl = (_context, data, _detail) => {
afterCalledOrder.push('c');
return data;
};

const logger = new TestLogger();
const td = new TestData();
const client = new LDClientImpl(
'sdk-key',
basicPlatform,
{
updateProcessor: td.getFactory(),
sendEvents: false,
logger,
hooks: [hookA, hookB],
},
makeCallbacks(true),
);

await client.waitForInitialization();
client.addHook(hookC);
await client.variation('flagKey', defaultUser, false);

expect(beforeCalledOrder).toEqual(['a', 'b', 'c']);
expect(afterCalledOrder).toEqual(['c', 'b', 'a']);
});
Loading

0 comments on commit e27f298

Please sign in to comment.