1+ import * as td from 'testdouble' ;
2+
13import ApiEndpoints from './api-endpoints' ;
2- import { BASE_URL as DEFAULT_BASE_URL } from './constants' ;
4+ import { BASE_URL as DEFAULT_BASE_URL , DEFAULT_EVENT_DOMAIN } from './constants' ;
35import EnhancedSdkToken from './enhanced-sdk-token' ;
46
57describe ( 'ApiEndpoints' , ( ) => {
68 it ( 'should append query parameters to the URL' , ( ) => {
79 const apiEndpoints = new ApiEndpoints ( {
8- baseUrl : 'https ://api.example.com' ,
10+ baseUrl : 'http ://api.example.com' ,
911 queryParams : {
1012 apiKey : '12345' ,
1113 sdkVersion : 'foobar' ,
1214 sdkName : 'ExampleSDK' ,
1315 } ,
1416 } ) ;
1517 expect ( apiEndpoints . endpoint ( '/data' ) . toString ( ) ) . toEqual (
16- 'https ://api.example.com/data?apiKey=12345&sdkVersion=foobar&sdkName=ExampleSDK' ,
18+ 'http ://api.example.com/data?apiKey=12345&sdkVersion=foobar&sdkName=ExampleSDK' ,
1719 ) ;
1820 expect ( apiEndpoints . ufcEndpoint ( ) . toString ( ) ) . toEqual (
19- 'https ://api.example.com/flag-config/v1/config?apiKey=12345&sdkVersion=foobar&sdkName=ExampleSDK' ,
21+ 'http ://api.example.com/flag-config/v1/config?apiKey=12345&sdkVersion=foobar&sdkName=ExampleSDK' ,
2022 ) ;
2123 } ) ;
2224
@@ -55,7 +57,7 @@ describe('ApiEndpoints', () => {
5557 // This token has cs=test-subdomain
5658 const sdkToken = 'abc.Y3M9dGVzdC1zdWJkb21haW4=' ;
5759 const endpoints = new ApiEndpoints ( { sdkToken : new EnhancedSdkToken ( sdkToken ) } ) ;
58- expect ( endpoints . getEffectiveBaseUrl ( ) ) . toBe ( 'https://test-subdomain.fscdn.eppo.cloud/api' ) ;
60+ expect ( endpoints . endpoint ( '/data' ) ) . toBe ( 'https://test-subdomain.fscdn.eppo.cloud/api/data ' ) ;
5961 } ) ;
6062
6163 it ( 'should prefer custom baseUrl over SDK token subdomain' , ( ) => {
@@ -66,20 +68,21 @@ describe('ApiEndpoints', () => {
6668 baseUrl : customBaseUrl ,
6769 sdkToken : new EnhancedSdkToken ( sdkToken ) ,
6870 } ) ;
69- expect ( endpoints . getEffectiveBaseUrl ( ) ) . toBe ( customBaseUrl ) ;
71+
72+ expect ( endpoints . endpoint ( '' ) ) . toContain ( customBaseUrl ) ;
7073 } ) ;
7174
7275 it ( 'should fallback to DEFAULT_BASE_URL when SDK token has no subdomain' , ( ) => {
7376 // This token has no cs parameter
7477 const sdkToken = 'abc.ZWg9ZXZlbnQtaG9zdG5hbWU=' ;
7578 const endpoints = new ApiEndpoints ( { sdkToken : new EnhancedSdkToken ( sdkToken ) } ) ;
76- expect ( endpoints . getEffectiveBaseUrl ( ) ) . toBe ( DEFAULT_BASE_URL ) ;
79+ expect ( endpoints . endpoint ( '' ) . startsWith ( DEFAULT_BASE_URL ) ) . toBeTruthy ( ) ;
7780 } ) ;
7881
7982 it ( 'should fallback to DEFAULT_BASE_URL when SDK token is invalid' , ( ) => {
8083 const invalidToken = new EnhancedSdkToken ( 'invalid-token' ) ;
8184 const endpoints = new ApiEndpoints ( { sdkToken : invalidToken } ) ;
82- expect ( endpoints . getEffectiveBaseUrl ( ) ) . toBe ( DEFAULT_BASE_URL ) ;
85+ expect ( endpoints . endpoint ( '' ) . startsWith ( DEFAULT_BASE_URL ) ) . toBeTruthy ( ) ;
8386 } ) ;
8487 } ) ;
8588
@@ -133,6 +136,37 @@ describe('ApiEndpoints', () => {
133136 } ) ;
134137 } ) ;
135138
139+ describe ( 'Event Url generation' , ( ) => {
140+ const hostnameToken = new EnhancedSdkToken (
141+ 'zCsQuoHJxVPp895.ZWg9MTIzNDU2LmUudGVzdGluZy5lcHBvLmNsb3Vk' ,
142+ ) ;
143+ const mockedToken = td . object < EnhancedSdkToken > ( ) ;
144+ beforeAll ( ( ) => {
145+ td . when ( mockedToken . isValid ( ) ) . thenReturn ( true ) ;
146+ } ) ;
147+
148+ it ( 'should decode the event ingestion hostname from the SDK key' , ( ) => {
149+ const endpoints = new ApiEndpoints ( { sdkToken : hostnameToken } ) ;
150+ const hostname = endpoints . eventIngestionEndpoint ( ) ;
151+ expect ( hostname ) . toEqual ( 'https://123456.e.testing.eppo.cloud/v0/i' ) ;
152+ } ) ;
153+
154+ it ( 'should decode strings with non URL-safe characters' , ( ) => {
155+ // this is not a really valid ingestion URL, but it's useful for testing the decoder
156+ td . when ( mockedToken . getEventIngestionHostname ( ) ) . thenReturn ( '12 3456/.e.testing.eppo.cloud' ) ;
157+ const endpoints = new ApiEndpoints ( { sdkToken : mockedToken } ) ;
158+ const hostname = endpoints . eventIngestionEndpoint ( ) ;
159+ expect ( hostname ) . toEqual ( 'https://12 3456/.e.testing.eppo.cloud/v0/i' ) ;
160+ } ) ;
161+
162+ it ( "should return null if the SDK key doesn't contain the event ingestion hostname" , ( ) => {
163+ td . when ( mockedToken . isValid ( ) ) . thenReturn ( false ) ;
164+ const endpoints = new ApiEndpoints ( { sdkToken : mockedToken } ) ;
165+ const hostname = endpoints . eventIngestionEndpoint ( ) ;
166+ expect ( hostname ) . toBeNull ( ) ;
167+ } ) ;
168+ } ) ;
169+
136170 describe ( 'Query parameter handling' , ( ) => {
137171 it ( 'should append query parameters to endpoint URLs' , ( ) => {
138172 const queryParams = { apiKey : 'test-key' , sdkName : 'js-sdk' , sdkVersion : '1.0.0' } ;
@@ -161,3 +195,110 @@ describe('ApiEndpoints', () => {
161195 } ) ;
162196 } ) ;
163197} ) ;
198+
199+ describe ( 'ApiEndpoints - Additional Tests' , ( ) => {
200+ describe ( 'URL normalization' , ( ) => {
201+ it ( 'should preserve different protocol types' , ( ) => {
202+ // We can test this indirectly through the endpoint method
203+ const httpEndpoints = new ApiEndpoints ( { baseUrl : 'http://example.com' } ) ;
204+ const httpsEndpoints = new ApiEndpoints ( { baseUrl : 'https://example.com' } ) ;
205+ const protocolRelativeEndpoints = new ApiEndpoints ( { baseUrl : '//example.com' } ) ;
206+
207+ expect ( httpEndpoints . endpoint ( 'test' ) ) . toEqual ( 'http://example.com/test' ) ;
208+ expect ( httpsEndpoints . endpoint ( 'test' ) ) . toEqual ( 'https://example.com/test' ) ;
209+ expect ( protocolRelativeEndpoints . endpoint ( 'test' ) ) . toEqual ( '//example.com/test' ) ;
210+ } ) ;
211+
212+ it ( 'should add https:// to URLs without protocols' , ( ) => {
213+ const endpoints = new ApiEndpoints ( { baseUrl : 'example.com' } ) ;
214+ expect ( endpoints . endpoint ( 'test' ) ) . toEqual ( 'https://example.com/test' ) ;
215+ } ) ;
216+
217+ it ( 'should handle multiple slashes' , ( ) => {
218+ const endpoints = new ApiEndpoints ( { baseUrl : 'example.com/' } ) ;
219+ expect ( endpoints . endpoint ( '/test' ) ) . toEqual ( 'https://example.com/test' ) ;
220+ } ) ;
221+ } ) ;
222+
223+ describe ( 'Subdomain handling' , ( ) => {
224+ it ( 'should correctly integrate subdomain with base URLs containing paths' , ( ) => {
225+ const sdkToken = new EnhancedSdkToken ( 'abc.Y3M9dGVzdC1zdWJkb21haW4=' ) ; // cs=test-subdomain
226+ const endpoints = new ApiEndpoints ( {
227+ sdkToken,
228+ defaultUrl : 'example.com/api/v2' ,
229+ } ) ;
230+
231+ expect ( endpoints . endpoint ( '' ) ) . toContain ( 'https://test-subdomain.example.com/api/v2' ) ;
232+ } ) ;
233+
234+ it ( 'should handle subdomains with special characters' , ( ) => {
235+ // Encode a token with cs=test-sub.domain-special
236+ const sdkToken = new EnhancedSdkToken ( 'abc.Y3M9dGVzdC1zdWIuZG9tYWluLXNwZWNpYWw=' ) ;
237+ const endpoints = new ApiEndpoints ( { sdkToken } ) ;
238+
239+ // The implementation should handle this correctly, but this is what we'd expect
240+ expect ( endpoints . endpoint ( '' ) ) . toContain ( 'test-sub.domain-special' ) ;
241+ } ) ;
242+ } ) ;
243+
244+ describe ( 'Event ingestion endpoint' , ( ) => {
245+ it ( 'should use subdomain with DEFAULT_EVENT_DOMAIN when hostname is not available' , ( ) => {
246+ // Create a mock token with only a subdomain
247+ const mockToken = {
248+ isValid : ( ) => true ,
249+ getEventIngestionHostname : ( ) => null ,
250+ getSubdomain : ( ) => 'test-subdomain' ,
251+ } as EnhancedSdkToken ;
252+
253+ const endpoints = new ApiEndpoints ( { sdkToken : mockToken } ) ;
254+ expect ( endpoints . eventIngestionEndpoint ( ) ) . toEqual (
255+ `https://test-subdomain.${ DEFAULT_EVENT_DOMAIN } /v0/i` ,
256+ ) ;
257+ } ) ;
258+
259+ it ( 'should prioritize hostname over subdomain if both are available' , ( ) => {
260+ // Create a mock token with both hostname and subdomain
261+ const mockToken = {
262+ isValid : ( ) => true ,
263+ getEventIngestionHostname : ( ) => 'event-host.example.com' ,
264+ getSubdomain : ( ) => 'test-subdomain' ,
265+ } as EnhancedSdkToken ;
266+
267+ const endpoints = new ApiEndpoints ( { sdkToken : mockToken } ) ;
268+ expect ( endpoints . eventIngestionEndpoint ( ) ) . toEqual ( 'https://event-host.example.com/v0/i' ) ;
269+ } ) ;
270+
271+ it ( 'should return null when token is valid but no hostname or subdomain is available' , ( ) => {
272+ // Create a mock token with neither hostname nor subdomain
273+ const mockToken = {
274+ isValid : ( ) => true ,
275+ getEventIngestionHostname : ( ) => null ,
276+ getSubdomain : ( ) => null ,
277+ } as EnhancedSdkToken ;
278+
279+ const endpoints = new ApiEndpoints ( { sdkToken : mockToken } ) ;
280+ expect ( endpoints . eventIngestionEndpoint ( ) ) . toBeNull ( ) ;
281+ } ) ;
282+ } ) ;
283+
284+ describe ( 'Edge cases and error handling' , ( ) => {
285+ it ( 'should handle extremely long subdomains' , ( ) => {
286+ const longSubdomain = 'a' . repeat ( 100 ) ;
287+ const mockToken = {
288+ isValid : ( ) => true ,
289+ getSubdomain : ( ) => longSubdomain ,
290+ } as EnhancedSdkToken ;
291+
292+ const endpoints = new ApiEndpoints ( { sdkToken : mockToken } ) ;
293+ expect ( endpoints . endpoint ( '' ) ) . toContain ( longSubdomain ) ;
294+ } ) ;
295+
296+ it ( 'should handle unusual base URL formats' , ( ) => {
297+ const endpoints = new ApiEndpoints ( {
298+ baseUrl : 'https://@:example.com:8080/path?query=value#fragment' ,
299+ } ) ;
300+ // The exact handling will depend on implementation details, but it shouldn't throw
301+ expect ( ( ) => endpoints . endpoint ( 'test' ) ) . not . toThrow ( ) ;
302+ } ) ;
303+ } ) ;
304+ } ) ;
0 commit comments