Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
28 changes: 28 additions & 0 deletions packages/sdk/react-native/src/RNOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { LDOptions } from '@launchdarkly/js-client-sdk-common';

export default interface RNOptions extends LDOptions {
/**
* Some platforms (windows, web, mac, linux) can continue executing code
* in the background.
*
* Defaults to false.
*/
readonly runInBackground?: boolean;

/**
* Enable handling of network availability. When this is true the
* connection state will automatically change when network
* availability changes.
*
* Defaults to true.
*/
readonly automaticNetworkHandling?: boolean;

/**
* Enable handling associated with transitioning between the foreground
* and background.
*
* Defaults to true.
*/
readonly automaticBackgroundHandling?: boolean;
}
333 changes: 313 additions & 20 deletions packages/sdk/react-native/src/ReactNativeLDClient.test.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,325 @@
import { AutoEnvAttributes, type LDContext } from '@launchdarkly/js-client-sdk-common';
import { AutoEnvAttributes, LDLogger, Response } from '@launchdarkly/js-client-sdk-common';

import createPlatform from './platform';
import PlatformCrypto from './platform/crypto';
import PlatformEncoding from './platform/PlatformEncoding';
import PlatformInfo from './platform/PlatformInfo';
import PlatformStorage from './platform/PlatformStorage';
import ReactNativeLDClient from './ReactNativeLDClient';

describe('ReactNativeLDClient', () => {
let ldc: ReactNativeLDClient;
function mockResponse(value: string, statusCode: number) {
const response: Response = {
headers: {
get: jest.fn(),
keys: jest.fn(),
values: jest.fn(),
entries: jest.fn(),
has: jest.fn(),
},
status: statusCode,
text: () => Promise.resolve(value),
json: () => Promise.resolve(JSON.parse(value)),
};
return Promise.resolve(response);
}

beforeEach(() => {
ldc = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, { sendEvents: false });
/**
* Mocks basicPlatform fetch. Returns the fetch jest.Mock object.
* @param remoteJson
* @param statusCode
*/
function mockFetch(value: string, statusCode: number = 200) {
const f = jest.fn();
f.mockResolvedValue(mockResponse(value, statusCode));
return f;
}

jest.mock('./platform', () => ({
__esModule: true,
default: jest.fn((logger: LDLogger) => ({
crypto: new PlatformCrypto(),
info: new PlatformInfo(logger),
requests: {
createEventSource: jest.fn(),
fetch: jest.fn(),
},
encoding: new PlatformEncoding(),
storage: new PlatformStorage(logger),
})),
}));

const createMockEventSource = (streamUri: string = '', options: any = {}) => ({
streamUri,
options,
onclose: jest.fn(),
addEventListener: jest.fn(),
close: jest.fn(),
});

it('uses correct default diagnostic url', () => {
const mockedFetch = jest.fn();
const logger: LDLogger = {
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
};
(createPlatform as jest.Mock).mockReturnValue({
crypto: new PlatformCrypto(),
info: new PlatformInfo(logger),
requests: {
createEventSource: jest.fn(),
fetch: mockedFetch,
},
encoding: new PlatformEncoding(),
storage: new PlatformStorage(logger),
});
const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled);

expect(mockedFetch).toHaveBeenCalledWith(
'https://events.launchdarkly.com/mobile/events/diagnostic',
expect.anything(),
);
client.close();
});

it('uses correct default analytics event url', async () => {
const mockedFetch = mockFetch('{"flagA": true}', 200);
const logger: LDLogger = {
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
};
(createPlatform as jest.Mock).mockReturnValue({
crypto: new PlatformCrypto(),
info: new PlatformInfo(logger),
requests: {
createEventSource: createMockEventSource,
fetch: mockedFetch,
},
encoding: new PlatformEncoding(),
storage: new PlatformStorage(logger),
});
const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, {
diagnosticOptOut: true,
initialConnectionMode: 'polling',
});
await client.identify({ kind: 'user', key: 'bob' });
await client.flush();

expect(mockedFetch).toHaveBeenCalledWith(
'https://events.launchdarkly.com/mobile',
expect.anything(),
);
});

it('uses correct default polling url', async () => {
const mockedFetch = mockFetch('{"flagA": true}', 200);
const logger: LDLogger = {
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
};
(createPlatform as jest.Mock).mockReturnValue({
crypto: new PlatformCrypto(),
info: new PlatformInfo(logger),
requests: {
createEventSource: jest.fn(),
fetch: mockedFetch,
},
encoding: new PlatformEncoding(),
storage: new PlatformStorage(logger),
});
const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, {
diagnosticOptOut: true,
sendEvents: false,
initialConnectionMode: 'polling',
automaticBackgroundHandling: false,
});
await client.identify({ kind: 'user', key: 'bob' });

const regex = /https:\/\/clientsdk\.launchdarkly\.com\/msdk\/evalx\/contexts\/.*/;
expect(mockedFetch).toHaveBeenCalledWith(expect.stringMatching(regex), expect.anything());
});

it('uses correct default streaming url', (done) => {
const mockedCreateEventSource = jest.fn();
const logger: LDLogger = {
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
};
(createPlatform as jest.Mock).mockReturnValue({
crypto: new PlatformCrypto(),
info: new PlatformInfo(logger),
requests: {
createEventSource: mockedCreateEventSource,
fetch: jest.fn(),
},
encoding: new PlatformEncoding(),
storage: new PlatformStorage(logger),
});
const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, {
diagnosticOptOut: true,
sendEvents: false,
initialConnectionMode: 'streaming',
automaticBackgroundHandling: false,
});

test('constructing a new client', () => {
expect(ldc.highTimeoutThreshold).toEqual(15);
expect(ldc.sdkKey).toEqual('mobile-key');
expect(ldc.config.serviceEndpoints).toEqual({
analyticsEventPath: '/mobile',
diagnosticEventPath: '/mobile/events/diagnostic',
events: 'https://events.launchdarkly.com',
includeAuthorizationHeader: true,
polling: 'https://clientsdk.launchdarkly.com',
streaming: 'https://clientstream.launchdarkly.com',
client
.identify({ kind: 'user', key: 'bob' }, { timeout: 0 })
.then(() => {})
.catch(() => {})
.then(() => {
const regex = /https:\/\/clientstream\.launchdarkly\.com\/meval\/.*/;
expect(mockedCreateEventSource).toHaveBeenCalledWith(
expect.stringMatching(regex),
expect.anything(),
);
done();
});
});

it('includes authorization header for polling', async () => {
const mockedFetch = mockFetch('{"flagA": true}', 200);
const logger: LDLogger = {
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
};
(createPlatform as jest.Mock).mockReturnValue({
crypto: new PlatformCrypto(),
info: new PlatformInfo(logger),
requests: {
createEventSource: jest.fn(),
fetch: mockedFetch,
},
encoding: new PlatformEncoding(),
storage: new PlatformStorage(logger),
});
const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, {
diagnosticOptOut: true,
sendEvents: false,
initialConnectionMode: 'polling',
automaticBackgroundHandling: false,
});
await client.identify({ kind: 'user', key: 'bob' });

expect(mockedFetch).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
headers: expect.objectContaining({ authorization: 'mobile-key' }),
}),
);
});

it('includes authorization header for streaming', (done) => {
const mockedCreateEventSource = jest.fn();
const logger: LDLogger = {
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
};
(createPlatform as jest.Mock).mockReturnValue({
crypto: new PlatformCrypto(),
info: new PlatformInfo(logger),
requests: {
createEventSource: mockedCreateEventSource,
fetch: jest.fn(),
},
encoding: new PlatformEncoding(),
storage: new PlatformStorage(logger),
});
const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, {
diagnosticOptOut: true,
sendEvents: false,
initialConnectionMode: 'streaming',
automaticBackgroundHandling: false,
});

client
.identify({ kind: 'user', key: 'bob' }, { timeout: 0 })
.then(() => {})
.catch(() => {})
.then(() => {
expect(mockedCreateEventSource).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
headers: expect.objectContaining({ authorization: 'mobile-key' }),
}),
);
done();
});
});

it('includes authorization header for events', async () => {
const mockedFetch = mockFetch('{"flagA": true}', 200);
const logger: LDLogger = {
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
};
(createPlatform as jest.Mock).mockReturnValue({
crypto: new PlatformCrypto(),
info: new PlatformInfo(logger),
requests: {
createEventSource: jest.fn(),
fetch: mockedFetch,
},
encoding: new PlatformEncoding(),
storage: new PlatformStorage(logger),
});
const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, {
diagnosticOptOut: true,
initialConnectionMode: 'polling',
});
await client.identify({ kind: 'user', key: 'bob' });
await client.flush();

test('createStreamUriPath', () => {
const context: LDContext = { kind: 'user', key: 'test-user-key-1' };
expect(mockedFetch).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
headers: expect.objectContaining({ authorization: 'mobile-key' }),
}),
);
});

it('identify with too high of a timeout', () => {
const logger: LDLogger = {
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
};
const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, {
sendEvents: false,
initialConnectionMode: 'offline',
logger,
});
client.identify({ key: 'potato', kind: 'user' }, { timeout: 16 });
expect(logger.warn).toHaveBeenCalledWith(
'The identify function was called with a timeout greater than 15 seconds. We recommend a timeout of less than 15 seconds.',
);
});

expect(ldc.createStreamUriPath(context)).toEqual(
'/meval/eyJraW5kIjoidXNlciIsImtleSI6InRlc3QtdXNlci1rZXktMSJ9',
);
it('identify timeout equal to threshold', () => {
const logger: LDLogger = {
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
};
const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, {
sendEvents: false,
initialConnectionMode: 'offline',
logger,
});
client.identify({ key: 'potato', kind: 'user' }, { timeout: 15 });
expect(logger.warn).not.toHaveBeenCalled();
});
Loading