|
1 | | -import { AutoEnvAttributes, type LDContext } from '@launchdarkly/js-client-sdk-common'; |
| 1 | +import { AutoEnvAttributes, LDLogger, Response } from '@launchdarkly/js-client-sdk-common'; |
2 | 2 |
|
| 3 | +import createPlatform from './platform'; |
| 4 | +import PlatformCrypto from './platform/crypto'; |
| 5 | +import PlatformEncoding from './platform/PlatformEncoding'; |
| 6 | +import PlatformInfo from './platform/PlatformInfo'; |
| 7 | +import PlatformStorage from './platform/PlatformStorage'; |
3 | 8 | import ReactNativeLDClient from './ReactNativeLDClient'; |
4 | 9 |
|
5 | | -describe('ReactNativeLDClient', () => { |
6 | | - let ldc: ReactNativeLDClient; |
| 10 | +function mockResponse(value: string, statusCode: number) { |
| 11 | + const response: Response = { |
| 12 | + headers: { |
| 13 | + get: jest.fn(), |
| 14 | + keys: jest.fn(), |
| 15 | + values: jest.fn(), |
| 16 | + entries: jest.fn(), |
| 17 | + has: jest.fn(), |
| 18 | + }, |
| 19 | + status: statusCode, |
| 20 | + text: () => Promise.resolve(value), |
| 21 | + json: () => Promise.resolve(JSON.parse(value)), |
| 22 | + }; |
| 23 | + return Promise.resolve(response); |
| 24 | +} |
7 | 25 |
|
8 | | - beforeEach(() => { |
9 | | - ldc = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, { sendEvents: false }); |
| 26 | +/** |
| 27 | + * Mocks basicPlatform fetch. Returns the fetch jest.Mock object. |
| 28 | + * @param remoteJson |
| 29 | + * @param statusCode |
| 30 | + */ |
| 31 | +function mockFetch(value: string, statusCode: number = 200) { |
| 32 | + const f = jest.fn(); |
| 33 | + f.mockResolvedValue(mockResponse(value, statusCode)); |
| 34 | + return f; |
| 35 | +} |
| 36 | + |
| 37 | +jest.mock('./platform', () => ({ |
| 38 | + __esModule: true, |
| 39 | + default: jest.fn((logger: LDLogger) => ({ |
| 40 | + crypto: new PlatformCrypto(), |
| 41 | + info: new PlatformInfo(logger), |
| 42 | + requests: { |
| 43 | + createEventSource: jest.fn(), |
| 44 | + fetch: jest.fn(), |
| 45 | + }, |
| 46 | + encoding: new PlatformEncoding(), |
| 47 | + storage: new PlatformStorage(logger), |
| 48 | + })), |
| 49 | +})); |
| 50 | + |
| 51 | +const createMockEventSource = (streamUri: string = '', options: any = {}) => ({ |
| 52 | + streamUri, |
| 53 | + options, |
| 54 | + onclose: jest.fn(), |
| 55 | + addEventListener: jest.fn(), |
| 56 | + close: jest.fn(), |
| 57 | +}); |
| 58 | + |
| 59 | +it('uses correct default diagnostic url', () => { |
| 60 | + const mockedFetch = jest.fn(); |
| 61 | + const logger: LDLogger = { |
| 62 | + error: jest.fn(), |
| 63 | + warn: jest.fn(), |
| 64 | + info: jest.fn(), |
| 65 | + debug: jest.fn(), |
| 66 | + }; |
| 67 | + (createPlatform as jest.Mock).mockReturnValue({ |
| 68 | + crypto: new PlatformCrypto(), |
| 69 | + info: new PlatformInfo(logger), |
| 70 | + requests: { |
| 71 | + createEventSource: jest.fn(), |
| 72 | + fetch: mockedFetch, |
| 73 | + }, |
| 74 | + encoding: new PlatformEncoding(), |
| 75 | + storage: new PlatformStorage(logger), |
| 76 | + }); |
| 77 | + const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled); |
| 78 | + |
| 79 | + expect(mockedFetch).toHaveBeenCalledWith( |
| 80 | + 'https://events.launchdarkly.com/mobile/events/diagnostic', |
| 81 | + expect.anything(), |
| 82 | + ); |
| 83 | + client.close(); |
| 84 | +}); |
| 85 | + |
| 86 | +it('uses correct default analytics event url', async () => { |
| 87 | + const mockedFetch = mockFetch('{"flagA": true}', 200); |
| 88 | + const logger: LDLogger = { |
| 89 | + error: jest.fn(), |
| 90 | + warn: jest.fn(), |
| 91 | + info: jest.fn(), |
| 92 | + debug: jest.fn(), |
| 93 | + }; |
| 94 | + (createPlatform as jest.Mock).mockReturnValue({ |
| 95 | + crypto: new PlatformCrypto(), |
| 96 | + info: new PlatformInfo(logger), |
| 97 | + requests: { |
| 98 | + createEventSource: createMockEventSource, |
| 99 | + fetch: mockedFetch, |
| 100 | + }, |
| 101 | + encoding: new PlatformEncoding(), |
| 102 | + storage: new PlatformStorage(logger), |
| 103 | + }); |
| 104 | + const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, { |
| 105 | + diagnosticOptOut: true, |
| 106 | + initialConnectionMode: 'polling', |
| 107 | + }); |
| 108 | + await client.identify({ kind: 'user', key: 'bob' }); |
| 109 | + await client.flush(); |
| 110 | + |
| 111 | + expect(mockedFetch).toHaveBeenCalledWith( |
| 112 | + 'https://events.launchdarkly.com/mobile', |
| 113 | + expect.anything(), |
| 114 | + ); |
| 115 | +}); |
| 116 | + |
| 117 | +it('uses correct default polling url', async () => { |
| 118 | + const mockedFetch = mockFetch('{"flagA": true}', 200); |
| 119 | + const logger: LDLogger = { |
| 120 | + error: jest.fn(), |
| 121 | + warn: jest.fn(), |
| 122 | + info: jest.fn(), |
| 123 | + debug: jest.fn(), |
| 124 | + }; |
| 125 | + (createPlatform as jest.Mock).mockReturnValue({ |
| 126 | + crypto: new PlatformCrypto(), |
| 127 | + info: new PlatformInfo(logger), |
| 128 | + requests: { |
| 129 | + createEventSource: jest.fn(), |
| 130 | + fetch: mockedFetch, |
| 131 | + }, |
| 132 | + encoding: new PlatformEncoding(), |
| 133 | + storage: new PlatformStorage(logger), |
| 134 | + }); |
| 135 | + const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, { |
| 136 | + diagnosticOptOut: true, |
| 137 | + sendEvents: false, |
| 138 | + initialConnectionMode: 'polling', |
| 139 | + automaticBackgroundHandling: false, |
| 140 | + }); |
| 141 | + await client.identify({ kind: 'user', key: 'bob' }); |
| 142 | + |
| 143 | + const regex = /https:\/\/clientsdk\.launchdarkly\.com\/msdk\/evalx\/contexts\/.*/; |
| 144 | + expect(mockedFetch).toHaveBeenCalledWith(expect.stringMatching(regex), expect.anything()); |
| 145 | +}); |
| 146 | + |
| 147 | +it('uses correct default streaming url', (done) => { |
| 148 | + const mockedCreateEventSource = jest.fn(); |
| 149 | + const logger: LDLogger = { |
| 150 | + error: jest.fn(), |
| 151 | + warn: jest.fn(), |
| 152 | + info: jest.fn(), |
| 153 | + debug: jest.fn(), |
| 154 | + }; |
| 155 | + (createPlatform as jest.Mock).mockReturnValue({ |
| 156 | + crypto: new PlatformCrypto(), |
| 157 | + info: new PlatformInfo(logger), |
| 158 | + requests: { |
| 159 | + createEventSource: mockedCreateEventSource, |
| 160 | + fetch: jest.fn(), |
| 161 | + }, |
| 162 | + encoding: new PlatformEncoding(), |
| 163 | + storage: new PlatformStorage(logger), |
| 164 | + }); |
| 165 | + const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, { |
| 166 | + diagnosticOptOut: true, |
| 167 | + sendEvents: false, |
| 168 | + initialConnectionMode: 'streaming', |
| 169 | + automaticBackgroundHandling: false, |
10 | 170 | }); |
11 | 171 |
|
12 | | - test('constructing a new client', () => { |
13 | | - expect(ldc.highTimeoutThreshold).toEqual(15); |
14 | | - expect(ldc.sdkKey).toEqual('mobile-key'); |
15 | | - expect(ldc.config.serviceEndpoints).toEqual({ |
16 | | - analyticsEventPath: '/mobile', |
17 | | - diagnosticEventPath: '/mobile/events/diagnostic', |
18 | | - events: 'https://events.launchdarkly.com', |
19 | | - includeAuthorizationHeader: true, |
20 | | - polling: 'https://clientsdk.launchdarkly.com', |
21 | | - streaming: 'https://clientstream.launchdarkly.com', |
| 172 | + client |
| 173 | + .identify({ kind: 'user', key: 'bob' }, { timeout: 0 }) |
| 174 | + .then(() => {}) |
| 175 | + .catch(() => {}) |
| 176 | + .then(() => { |
| 177 | + const regex = /https:\/\/clientstream\.launchdarkly\.com\/meval\/.*/; |
| 178 | + expect(mockedCreateEventSource).toHaveBeenCalledWith( |
| 179 | + expect.stringMatching(regex), |
| 180 | + expect.anything(), |
| 181 | + ); |
| 182 | + done(); |
22 | 183 | }); |
| 184 | +}); |
| 185 | + |
| 186 | +it('includes authorization header for polling', async () => { |
| 187 | + const mockedFetch = mockFetch('{"flagA": true}', 200); |
| 188 | + const logger: LDLogger = { |
| 189 | + error: jest.fn(), |
| 190 | + warn: jest.fn(), |
| 191 | + info: jest.fn(), |
| 192 | + debug: jest.fn(), |
| 193 | + }; |
| 194 | + (createPlatform as jest.Mock).mockReturnValue({ |
| 195 | + crypto: new PlatformCrypto(), |
| 196 | + info: new PlatformInfo(logger), |
| 197 | + requests: { |
| 198 | + createEventSource: jest.fn(), |
| 199 | + fetch: mockedFetch, |
| 200 | + }, |
| 201 | + encoding: new PlatformEncoding(), |
| 202 | + storage: new PlatformStorage(logger), |
| 203 | + }); |
| 204 | + const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, { |
| 205 | + diagnosticOptOut: true, |
| 206 | + sendEvents: false, |
| 207 | + initialConnectionMode: 'polling', |
| 208 | + automaticBackgroundHandling: false, |
| 209 | + }); |
| 210 | + await client.identify({ kind: 'user', key: 'bob' }); |
| 211 | + |
| 212 | + expect(mockedFetch).toHaveBeenCalledWith( |
| 213 | + expect.anything(), |
| 214 | + expect.objectContaining({ |
| 215 | + headers: expect.objectContaining({ authorization: 'mobile-key' }), |
| 216 | + }), |
| 217 | + ); |
| 218 | +}); |
| 219 | + |
| 220 | +it('includes authorization header for streaming', (done) => { |
| 221 | + const mockedCreateEventSource = jest.fn(); |
| 222 | + const logger: LDLogger = { |
| 223 | + error: jest.fn(), |
| 224 | + warn: jest.fn(), |
| 225 | + info: jest.fn(), |
| 226 | + debug: jest.fn(), |
| 227 | + }; |
| 228 | + (createPlatform as jest.Mock).mockReturnValue({ |
| 229 | + crypto: new PlatformCrypto(), |
| 230 | + info: new PlatformInfo(logger), |
| 231 | + requests: { |
| 232 | + createEventSource: mockedCreateEventSource, |
| 233 | + fetch: jest.fn(), |
| 234 | + }, |
| 235 | + encoding: new PlatformEncoding(), |
| 236 | + storage: new PlatformStorage(logger), |
| 237 | + }); |
| 238 | + const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, { |
| 239 | + diagnosticOptOut: true, |
| 240 | + sendEvents: false, |
| 241 | + initialConnectionMode: 'streaming', |
| 242 | + automaticBackgroundHandling: false, |
| 243 | + }); |
| 244 | + |
| 245 | + client |
| 246 | + .identify({ kind: 'user', key: 'bob' }, { timeout: 0 }) |
| 247 | + .then(() => {}) |
| 248 | + .catch(() => {}) |
| 249 | + .then(() => { |
| 250 | + expect(mockedCreateEventSource).toHaveBeenCalledWith( |
| 251 | + expect.anything(), |
| 252 | + expect.objectContaining({ |
| 253 | + headers: expect.objectContaining({ authorization: 'mobile-key' }), |
| 254 | + }), |
| 255 | + ); |
| 256 | + done(); |
| 257 | + }); |
| 258 | +}); |
| 259 | + |
| 260 | +it('includes authorization header for events', async () => { |
| 261 | + const mockedFetch = mockFetch('{"flagA": true}', 200); |
| 262 | + const logger: LDLogger = { |
| 263 | + error: jest.fn(), |
| 264 | + warn: jest.fn(), |
| 265 | + info: jest.fn(), |
| 266 | + debug: jest.fn(), |
| 267 | + }; |
| 268 | + (createPlatform as jest.Mock).mockReturnValue({ |
| 269 | + crypto: new PlatformCrypto(), |
| 270 | + info: new PlatformInfo(logger), |
| 271 | + requests: { |
| 272 | + createEventSource: jest.fn(), |
| 273 | + fetch: mockedFetch, |
| 274 | + }, |
| 275 | + encoding: new PlatformEncoding(), |
| 276 | + storage: new PlatformStorage(logger), |
| 277 | + }); |
| 278 | + const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, { |
| 279 | + diagnosticOptOut: true, |
| 280 | + initialConnectionMode: 'polling', |
23 | 281 | }); |
| 282 | + await client.identify({ kind: 'user', key: 'bob' }); |
| 283 | + await client.flush(); |
24 | 284 |
|
25 | | - test('createStreamUriPath', () => { |
26 | | - const context: LDContext = { kind: 'user', key: 'test-user-key-1' }; |
| 285 | + expect(mockedFetch).toHaveBeenCalledWith( |
| 286 | + expect.anything(), |
| 287 | + expect.objectContaining({ |
| 288 | + headers: expect.objectContaining({ authorization: 'mobile-key' }), |
| 289 | + }), |
| 290 | + ); |
| 291 | +}); |
| 292 | + |
| 293 | +it('identify with too high of a timeout', () => { |
| 294 | + const logger: LDLogger = { |
| 295 | + error: jest.fn(), |
| 296 | + warn: jest.fn(), |
| 297 | + info: jest.fn(), |
| 298 | + debug: jest.fn(), |
| 299 | + }; |
| 300 | + const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, { |
| 301 | + sendEvents: false, |
| 302 | + initialConnectionMode: 'offline', |
| 303 | + logger, |
| 304 | + }); |
| 305 | + client.identify({ key: 'potato', kind: 'user' }, { timeout: 16 }); |
| 306 | + expect(logger.warn).toHaveBeenCalledWith( |
| 307 | + 'The identify function was called with a timeout greater than 15 seconds. We recommend a timeout of less than 15 seconds.', |
| 308 | + ); |
| 309 | +}); |
27 | 310 |
|
28 | | - expect(ldc.createStreamUriPath(context)).toEqual( |
29 | | - '/meval/eyJraW5kIjoidXNlciIsImtleSI6InRlc3QtdXNlci1rZXktMSJ9', |
30 | | - ); |
| 311 | +it('identify timeout equal to threshold', () => { |
| 312 | + const logger: LDLogger = { |
| 313 | + error: jest.fn(), |
| 314 | + warn: jest.fn(), |
| 315 | + info: jest.fn(), |
| 316 | + debug: jest.fn(), |
| 317 | + }; |
| 318 | + const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, { |
| 319 | + sendEvents: false, |
| 320 | + initialConnectionMode: 'offline', |
| 321 | + logger, |
31 | 322 | }); |
| 323 | + client.identify({ key: 'potato', kind: 'user' }, { timeout: 15 }); |
| 324 | + expect(logger.warn).not.toHaveBeenCalled(); |
32 | 325 | }); |
0 commit comments