Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"packages/sdk/server-ai/examples/vercel-ai",
"packages/telemetry/browser-telemetry",
"contract-tests",
"packages/sdk/combined-browser"
"packages/sdk/combined-browser",
"packages/sdk/shopify-oxygen"
],
"private": true,
"scripts": {
Expand Down
2 changes: 2 additions & 0 deletions packages/sdk/shopify-oxygen/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Local module builds
*.tgz
1 change: 1 addition & 0 deletions packages/sdk/shopify-oxygen/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# TODO
203 changes: 203 additions & 0 deletions packages/sdk/shopify-oxygen/__tests__/index.test.ts
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);
});
});
});
});
86 changes: 86 additions & 0 deletions packages/sdk/shopify-oxygen/__tests__/setup.ts
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);
});
};
Loading