-
Notifications
You must be signed in to change notification settings - Fork 31
feat: adding shopify oxygen server sdk #991
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 8 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
163b9da
chore: initial commit for Oxygen SDK
joker23 783e9d9
chore: adding pollInterval default to exceed max request time
joker23 8f0a8ce
chore:[sdk-1557] implementing builtin caching for requests
joker23 6acd9f9
chore: functional refactor
joker23 a253d70
chore: lint refactor
joker23 cb11082
test: Oxygen SDK unit tests
joker23 4539307
chore: lint
joker23 1455104
chore: addressing PR comments
joker23 8bf4b39
doc: adding README and CHANGELOG docs
joker23 d1377ba
chore: fixing the platform info name to match package
joker23 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| # Local module builds | ||
| *.tgz |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| # TODO |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,203 @@ | ||
| import { LDClient, LDContext } from '@launchdarkly/js-server-sdk-common'; | ||
|
|
||
| import { init, OxygenLDOptions } from '../src/index'; | ||
| import { setupTestEnvironment } from './setup'; | ||
|
|
||
| const sdkKey = 'test-sdk-key'; | ||
| const flagKey1 = 'testFlag1'; | ||
| const flagKey2 = 'testFlag2'; | ||
| const flagKey3 = 'testFlag3'; | ||
| const context: LDContext = { kind: 'user', key: 'test-user-key-1' }; | ||
|
|
||
| describe('Shopify Oxygen SDK', () => { | ||
| describe('initialization tests', () => { | ||
| beforeEach(async () => { | ||
| await setupTestEnvironment(); | ||
| }); | ||
|
|
||
| it('will initialize successfully with default options', async () => { | ||
| const ldClient = init(sdkKey); | ||
| await ldClient.waitForInitialization(); | ||
| expect(ldClient).toBeDefined(); | ||
| ldClient.close(); | ||
| }); | ||
|
|
||
| it('will initialize successfully with custom options', async () => { | ||
| const ldClient = init(sdkKey, { | ||
| sendEvents: false, | ||
| cache: { | ||
| enabled: false, | ||
| }, | ||
| } as OxygenLDOptions); | ||
| await ldClient.waitForInitialization(); | ||
| expect(ldClient).toBeDefined(); | ||
| ldClient.close(); | ||
| }); | ||
|
|
||
| it('will fail to initialize if there is no SDK key', () => { | ||
| expect(() => init(null as any)).toThrow(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('polling tests', () => { | ||
| beforeEach(async () => { | ||
| await setupTestEnvironment(); | ||
| }); | ||
|
|
||
| describe('without caching', () => { | ||
| let ldClient: LDClient; | ||
|
|
||
| beforeEach(async () => { | ||
| // Ensure fetch is set up before creating client | ||
| ldClient = init(sdkKey, { | ||
| cache: { | ||
| enabled: false, | ||
| }, | ||
| } as OxygenLDOptions); | ||
| await ldClient.waitForInitialization(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| if (ldClient) { | ||
| ldClient.close(); | ||
| } | ||
| }); | ||
|
|
||
| it('Should not cache any requests', async () => { | ||
| await ldClient.variation(flagKey1, context, false); | ||
| await ldClient.allFlagsState(context); | ||
| await ldClient.variationDetail(flagKey3, context, false); | ||
| expect(caches.open).toHaveBeenCalledTimes(0); | ||
| }); | ||
|
|
||
| describe('flags', () => { | ||
| it('variation default', async () => { | ||
| const value = await ldClient.variation(flagKey1, context, false); | ||
|
|
||
| expect(value).toBeTruthy(); | ||
|
|
||
| expect(caches.open).toHaveBeenCalledTimes(0); | ||
| }); | ||
|
|
||
| it('variation default rollout', async () => { | ||
| const contextWithEmail = { ...context, email: '[email protected]' }; | ||
| const value = await ldClient.variation(flagKey2, contextWithEmail, false); | ||
| const detail = await ldClient.variationDetail(flagKey2, contextWithEmail, false); | ||
|
|
||
| expect(detail).toEqual({ | ||
| reason: { kind: 'FALLTHROUGH' }, | ||
| value: true, | ||
| variationIndex: 0, | ||
| }); | ||
| expect(value).toBeTruthy(); | ||
|
|
||
| expect(caches.open).toHaveBeenCalledTimes(0); | ||
| }); | ||
|
|
||
| it('rule match', async () => { | ||
| const contextWithEmail = { ...context, email: '[email protected]' }; | ||
| const value = await ldClient.variation(flagKey1, contextWithEmail, false); | ||
| const detail = await ldClient.variationDetail(flagKey1, contextWithEmail, false); | ||
|
|
||
| expect(detail).toEqual({ | ||
| reason: { kind: 'RULE_MATCH', ruleId: 'rule1', ruleIndex: 0 }, | ||
| value: false, | ||
| variationIndex: 1, | ||
| }); | ||
| expect(value).toBeFalsy(); | ||
|
|
||
| expect(caches.open).toHaveBeenCalledTimes(0); | ||
| }); | ||
|
|
||
| it('fallthrough', async () => { | ||
| const contextWithEmail = { ...context, email: '[email protected]' }; | ||
| const value = await ldClient.variation(flagKey1, contextWithEmail, false); | ||
| const detail = await ldClient.variationDetail(flagKey1, contextWithEmail, false); | ||
|
|
||
| expect(detail).toEqual({ | ||
| reason: { kind: 'FALLTHROUGH' }, | ||
| value: true, | ||
| variationIndex: 0, | ||
| }); | ||
| expect(value).toBeTruthy(); | ||
|
|
||
| expect(caches.open).toHaveBeenCalledTimes(0); | ||
| }); | ||
|
|
||
| it('allFlags fallthrough', async () => { | ||
| const allFlags = await ldClient.allFlagsState(context); | ||
|
|
||
| expect(allFlags).toBeDefined(); | ||
| expect(allFlags.toJSON()).toEqual({ | ||
| $flagsState: { | ||
| testFlag1: { debugEventsUntilDate: 2000, variation: 0, version: 2 }, | ||
| testFlag2: { debugEventsUntilDate: 2000, variation: 0, version: 2 }, | ||
| testFlag3: { debugEventsUntilDate: 2000, variation: 0, version: 2 }, | ||
| }, | ||
| $valid: true, | ||
| testFlag1: true, | ||
| testFlag2: true, | ||
| testFlag3: true, | ||
| }); | ||
|
|
||
| expect(caches.open).toHaveBeenCalledTimes(0); | ||
| }); | ||
| }); | ||
|
|
||
| describe('segments', () => { | ||
| it('segment by country', async () => { | ||
| const contextWithCountry = { ...context, country: 'australia' }; | ||
| const value = await ldClient.variation(flagKey3, contextWithCountry, false); | ||
| const detail = await ldClient.variationDetail(flagKey3, contextWithCountry, false); | ||
|
|
||
| expect(detail).toEqual({ | ||
| reason: { kind: 'RULE_MATCH', ruleId: 'rule1', ruleIndex: 0 }, | ||
| value: false, | ||
| variationIndex: 1, | ||
| }); | ||
| expect(value).toBeFalsy(); | ||
|
|
||
| expect(caches.open).toHaveBeenCalledTimes(0); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe('with caching', () => { | ||
| let ldClient: LDClient; | ||
|
|
||
| beforeEach(async () => { | ||
| // Ensure fetch is set up before creating client | ||
| ldClient = init(sdkKey); | ||
| await ldClient.waitForInitialization(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| if (ldClient) { | ||
| ldClient.close(); | ||
| } | ||
| }); | ||
|
|
||
| it('will cache across multiple variation calls', async () => { | ||
| await ldClient.variation(flagKey1, context, false); | ||
| await ldClient.variation(flagKey2, context, false); | ||
|
|
||
| // Should only fetch once due to caching | ||
| expect(caches.open).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it('will cache across multiple allFlags calls', async () => { | ||
| await ldClient.allFlagsState(context); | ||
| await ldClient.allFlagsState(context); | ||
|
|
||
| expect(caches.open).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it('will cache between allFlags and variation', async () => { | ||
| await ldClient.variation(flagKey1, context, false); | ||
| await ldClient.allFlagsState(context); | ||
|
|
||
| expect(caches.open).toHaveBeenCalledTimes(1); | ||
| }); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| /* eslint-disable import/no-extraneous-dependencies */ | ||
| import { jest } from '@jest/globals'; | ||
|
|
||
| import * as allFlagsSegments from './testData.json'; | ||
|
|
||
| // @ts-ignore | ||
| global.setInterval = () => ({}) as any; | ||
|
|
||
| // @ts-ignore | ||
| global.clearInterval = () => {}; | ||
|
|
||
| // @ts-ignore | ||
| global.setTimeout = () => ({}) as any; | ||
|
|
||
| // @ts-ignore | ||
| global.clearTimeout = () => {}; | ||
|
|
||
| // Setup test environment with mocks | ||
| export const setupTestEnvironment = async () => { | ||
| // Setup Cache API mock | ||
| const matchFn = jest.fn(); | ||
| // @ts-ignore - Mock implementation | ||
| matchFn.mockResolvedValue(undefined); | ||
| const putFn = jest.fn(); | ||
| // @ts-ignore - Mock implementation | ||
| putFn.mockResolvedValue(undefined); | ||
| const mockCache = { | ||
| match: matchFn as any, | ||
| put: putFn as any, | ||
| }; | ||
|
|
||
| const openFn = jest.fn(); | ||
| // @ts-ignore - Mock implementation | ||
| openFn.mockResolvedValue(mockCache); | ||
| // @ts-ignore - Mock Cache API for testing | ||
| global.caches = { | ||
| open: openFn as any, | ||
| }; | ||
|
|
||
| // @ts-ignore - Mock implementation | ||
| global.fetch = jest.fn<typeof fetch>((url: string) => { | ||
| // Match any URL containing /sdk/latest-all which should be the only URL that we are interested in. | ||
| if (url.includes('/sdk/latest-all') || url.endsWith('/sdk/latest-all')) { | ||
| const jsonFn = jest.fn(); | ||
| // @ts-ignore - Mock implementation | ||
| jsonFn.mockResolvedValue(allFlagsSegments); | ||
| const textFn = jest.fn(); | ||
| // @ts-ignore - Mock implementation | ||
| textFn.mockResolvedValue(JSON.stringify(allFlagsSegments)); | ||
| const arrayBufferFn = jest.fn(); | ||
| // @ts-ignore - Mock implementation | ||
| arrayBufferFn.mockResolvedValue(new ArrayBuffer(0)); | ||
| const mockResponse: any = { | ||
| ok: true, | ||
| status: 200, | ||
| statusText: 'OK', | ||
| headers: new Headers({ 'Content-Type': 'application/json' }), | ||
| json: jsonFn, | ||
| text: textFn, | ||
| arrayBuffer: arrayBufferFn, | ||
| clone: jest.fn().mockReturnThis(), | ||
| }; | ||
| return Promise.resolve(mockResponse); | ||
| } | ||
|
|
||
| const jsonFn = jest.fn(); | ||
| // @ts-ignore - Mock implementation | ||
| jsonFn.mockResolvedValue({}); | ||
| const textFn = jest.fn(); | ||
| // @ts-ignore - Mock implementation | ||
| textFn.mockResolvedValue(''); | ||
| const arrayBufferFn = jest.fn(); | ||
| // @ts-ignore - Mock implementation | ||
| arrayBufferFn.mockResolvedValue(new ArrayBuffer(0)); | ||
| const mockResponse: any = { | ||
| ok: true, | ||
| status: 200, | ||
| statusText: 'OK', | ||
| headers: new Headers(), | ||
| json: jsonFn, | ||
| text: textFn, | ||
| arrayBuffer: arrayBufferFn, | ||
| }; | ||
| return Promise.resolve(mockResponse); | ||
| }); | ||
| }; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.