From aee5db19ee11e0c925bc0ff19e5746e9ac115582 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Mon, 10 Nov 2025 17:18:46 -0300 Subject: [PATCH 1/5] fix: angular fetch response statuses --- utils/angular-fetch.ts | 49 ++++++++++++++++-------------------------- 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/utils/angular-fetch.ts b/utils/angular-fetch.ts index d7edf2f1..a4b29e20 100644 --- a/utils/angular-fetch.ts +++ b/utils/angular-fetch.ts @@ -1,37 +1,26 @@ export default (angularHttpClient:any)=> (url: string, params: { headers: Record, method: "GET" | "POST" | "PUT", body: string }) => { const {headers, method, body} = params + const options = { headers, observe: 'response' as const, responseType: 'text' as const } return new Promise((resolve) => { + const handleResponse = (res:any) => resolve({ + status: res.status, + ok: true, + headers: { get: (name:string) => res.headers?.get(name) }, + text: () => Promise.resolve(res.body) + }) + const handleError = (err:any) => resolve({ + status: err.status || 500, + ok: false, + headers: { get: (name:string) => err.headers?.get(name) || null }, + text: () => Promise.resolve(err.error || err.message || 'Unknown error') + }) switch (method) { - case "GET": { - return angularHttpClient.get(url, { - headers, - }).subscribe((v:string) => { - resolve({ - ok: true, - text: () => Promise.resolve(v) - }) - }) - } - case "POST": { - return angularHttpClient.post(url, body, { - headers, - }).subscribe((v:string) => { - resolve({ - ok: true, - text: () => Promise.resolve(v) - }) - }) - } - case "PUT": { - return angularHttpClient.post(url, body, { - headers, - }).subscribe((v:string) => { - resolve({ - ok: true, - text: () => Promise.resolve(v) - }) - }) - } + case "GET": + return angularHttpClient.get(url, options).subscribe(handleResponse, handleError) + case "POST": + return angularHttpClient.post(url, body, options).subscribe(handleResponse, handleError) + case "PUT": + return angularHttpClient.put(url, body, options).subscribe(handleResponse, handleError) } }) } From 9ccd4251e02335f98863d1af869aef99f3fc4500 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Mon, 10 Nov 2025 17:19:00 -0300 Subject: [PATCH 2/5] tests: add tests for angular fetch --- test/angular-fetch.test.ts | 90 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 test/angular-fetch.test.ts diff --git a/test/angular-fetch.test.ts b/test/angular-fetch.test.ts new file mode 100644 index 00000000..f27e4821 --- /dev/null +++ b/test/angular-fetch.test.ts @@ -0,0 +1,90 @@ +import angularFetch from '../utils/angular-fetch'; +import { createFlagsmithInstance } from '../lib/flagsmith'; +import MockAsyncStorage from './mocks/async-storage-mock'; +import { environmentID } from './test-constants'; +import { promises as fs } from 'fs'; + +describe('Angular HttpClient Fetch Adapter', () => { + it('should return response with status property', async () => { + const mockAngularHttpClient = { + get: jest.fn().mockReturnValue({ + subscribe: (onSuccess: any, onError: any) => { + onSuccess({ + status: 200, + body: JSON.stringify({ flags: [] }), + headers: { get: (name: string) => null } + }); + } + }) + }; + + const fetchAdapter = angularFetch(mockAngularHttpClient); + const response: any = await fetchAdapter('https://api.example.com/flags', { + headers: { 'Content-Type': 'application/json' }, + method: 'GET', + body: '' + }); + + expect(response.status).toBe(200); + expect(response.ok).toBe(true); + }); + + it('should handle errors with status property and proper error messages', async () => { + const mockAngularHttpClient = { + get: jest.fn().mockReturnValue({ + subscribe: (onSuccess: any, onError: any) => { + onError({ + status: 401, + error: 'Unauthorized', + headers: { get: (name: string) => null } + }); + } + }) + }; + + const fetchAdapter = angularFetch(mockAngularHttpClient); + const response: any = await fetchAdapter('https://api.example.com/flags', { + headers: { 'Content-Type': 'application/json' }, + method: 'GET', + body: '' + }); + + expect(response.status).toBe(401); + expect(response.ok).toBe(false); + + const errorText = await response.text(); + expect(errorText).toBe('Unauthorized'); + }); + + it('should initialize Flagsmith successfully with Angular HttpClient', async () => { + const mockAngularHttpClient = { + get: jest.fn().mockReturnValue({ + subscribe: async (onSuccess: any) => { + const body = await fs.readFile('./test/data/flags.json', 'utf8'); + onSuccess({ + status: 200, + body, + headers: { get: (name: string) => null } + }); + } + }) + }; + + const flagsmith = createFlagsmithInstance(); + const fetchAdapter = angularFetch(mockAngularHttpClient); + const AsyncStorage = new MockAsyncStorage(); + + // @ts-ignore + flagsmith.canUseStorage = true; + + await expect( + flagsmith.init({ + evaluationContext: { environment: { apiKey: environmentID } }, + fetch: fetchAdapter, + AsyncStorage + }) + ).resolves.not.toThrow(); + + expect(flagsmith.hasFeature('hero')).toBe(true); + }); +}); From 1c3624ce980fbbeb8891c88ba194ffac960ce43d Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Wed, 12 Nov 2025 13:33:56 -0300 Subject: [PATCH 3/5] refactor: handle unsupported methods --- utils/angular-fetch.ts | 49 ++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/utils/angular-fetch.ts b/utils/angular-fetch.ts index a4b29e20..6be64e1c 100644 --- a/utils/angular-fetch.ts +++ b/utils/angular-fetch.ts @@ -1,26 +1,33 @@ -export default (angularHttpClient:any)=> (url: string, params: { headers: Record, method: "GET" | "POST" | "PUT", body: string }) => { - const {headers, method, body} = params - const options = { headers, observe: 'response' as const, responseType: 'text' as const } +export default (angularHttpClient: any) => (url: string, params: { + headers: Record, + method: "GET" | "POST" | "PUT", + body?: string +}) => { + const { headers, method, body } = params; + const options = { headers, observe: 'response', responseType: 'text' }; + + const toFetchResponse = (response: any, ok: boolean) => { + const { status, headers, body, error, message } = response; + return { + status: status ?? (ok ? 200 : 500), + ok, + headers: { get: (name: string) => headers?.get?.(name) ?? null }, + text: () => Promise.resolve(body ?? error ?? message ?? ''), + }; + }; + return new Promise((resolve) => { - const handleResponse = (res:any) => resolve({ - status: res.status, - ok: true, - headers: { get: (name:string) => res.headers?.get(name) }, - text: () => Promise.resolve(res.body) - }) - const handleError = (err:any) => resolve({ - status: err.status || 500, - ok: false, - headers: { get: (name:string) => err.headers?.get(name) || null }, - text: () => Promise.resolve(err.error || err.message || 'Unknown error') - }) + const onNext = (res: any) => resolve(toFetchResponse(res, res.status >= 200 && res.status < 300)); + const onError = (err: any) => resolve(toFetchResponse(err, false)); switch (method) { case "GET": - return angularHttpClient.get(url, options).subscribe(handleResponse, handleError) + return angularHttpClient.get(url, options).subscribe(onNext, onError); case "POST": - return angularHttpClient.post(url, body, options).subscribe(handleResponse, handleError) + return angularHttpClient.post(url, body ?? '', options).subscribe(onNext, onError); case "PUT": - return angularHttpClient.put(url, body, options).subscribe(handleResponse, handleError) - } - }) -} + return angularHttpClient.post(url, body ?? '', options).subscribe(onNext, onError); + default: + return onError({ status: 405, message: `Unsupported method: ${method}` }); + } + }); +}; From 6b7760eed46a3cbba0606ccc469c14859551bd16 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Wed, 12 Nov 2025 17:20:56 -0300 Subject: [PATCH 4/5] tests: add more tests to improve test coverage --- test/angular-fetch.test.ts | 239 +++++++++++++++++++++++++++++++++++++ utils/angular-fetch.ts | 6 +- 2 files changed, 242 insertions(+), 3 deletions(-) diff --git a/test/angular-fetch.test.ts b/test/angular-fetch.test.ts index f27e4821..5424669b 100644 --- a/test/angular-fetch.test.ts +++ b/test/angular-fetch.test.ts @@ -87,4 +87,243 @@ describe('Angular HttpClient Fetch Adapter', () => { expect(flagsmith.hasFeature('hero')).toBe(true); }); + + it('should handle POST requests correctly', async () => { + const mockAngularHttpClient = { + post: jest.fn().mockReturnValue({ + subscribe: (onSuccess: any, onError: any) => { + onSuccess({ + status: 201, + body: JSON.stringify({ success: true }), + headers: { get: (name: string) => null } + }); + } + }) + }; + + const fetchAdapter = angularFetch(mockAngularHttpClient); + const response: any = await fetchAdapter('https://api.example.com/create', { + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + body: JSON.stringify({ data: 'test' }) + }); + + expect(mockAngularHttpClient.post).toHaveBeenCalledWith( + 'https://api.example.com/create', + JSON.stringify({ data: 'test' }), + { headers: { 'Content-Type': 'application/json' }, observe: 'response', responseType: 'text' } + ); + expect(response.status).toBe(201); + expect(response.ok).toBe(true); + }); + + it('should handle PUT requests correctly', async () => { + const mockAngularHttpClient = { + post: jest.fn().mockReturnValue({ + subscribe: (onSuccess: any, onError: any) => { + onSuccess({ + status: 200, + body: JSON.stringify({ updated: true }), + headers: { get: (name: string) => null } + }); + } + }) + }; + + const fetchAdapter = angularFetch(mockAngularHttpClient); + const response: any = await fetchAdapter('https://api.example.com/update', { + headers: { 'Content-Type': 'application/json' }, + method: 'PUT', + body: JSON.stringify({ data: 'updated' }) + }); + + expect(mockAngularHttpClient.post).toHaveBeenCalledWith( + 'https://api.example.com/update', + JSON.stringify({ data: 'updated' }), + { headers: { 'Content-Type': 'application/json' }, observe: 'response', responseType: 'text' } + ); + expect(response.status).toBe(200); + expect(response.ok).toBe(true); + }); + + it('should retrieve headers correctly', async () => { + const mockAngularHttpClient = { + get: jest.fn().mockReturnValue({ + subscribe: (onSuccess: any, onError: any) => { + onSuccess({ + status: 200, + body: 'test', + headers: { + get: (name: string) => { + if (name === 'Content-Type') return 'application/json'; + if (name === 'X-Custom-Header') return 'custom-value'; + return null; + } + } + }); + } + }) + }; + + const fetchAdapter = angularFetch(mockAngularHttpClient); + const response: any = await fetchAdapter('https://api.example.com/test', { + headers: {}, + method: 'GET', + body: '' + }); + + expect(response.headers.get('Content-Type')).toBe('application/json'); + expect(response.headers.get('X-Custom-Header')).toBe('custom-value'); + expect(response.headers.get('Non-Existent')).toBe(null); + }); + + it('should handle different error status codes correctly', async () => { + const testCases = [ + { status: 400, expectedOk: false, description: 'Bad Request' }, + { status: 403, expectedOk: false, description: 'Forbidden' }, + { status: 404, expectedOk: false, description: 'Not Found' }, + { status: 500, expectedOk: false, description: 'Internal Server Error' }, + { status: 503, expectedOk: false, description: 'Service Unavailable' } + ]; + + for (const testCase of testCases) { + const mockAngularHttpClient = { + get: jest.fn().mockReturnValue({ + subscribe: (_onSuccess: any, onError: any) => { + onError({ + status: testCase.status, + error: testCase.description, + headers: { get: (_name: string) => null } + }); + } + }) + }; + + const fetchAdapter = angularFetch(mockAngularHttpClient); + const response: any = await fetchAdapter('https://api.example.com/test', { + headers: {}, + method: 'GET', + body: '' + }); + + expect(response.status).toBe(testCase.status); + expect(response.ok).toBe(testCase.expectedOk); + const errorText = await response.text(); + expect(errorText).toBe(testCase.description); + } + }); + + it('should handle 3xx redirect status codes', async () => { + const mockAngularHttpClient = { + get: jest.fn().mockReturnValue({ + subscribe: (onSuccess: any, onError: any) => { + onSuccess({ + status: 301, + body: 'Moved Permanently', + headers: { get: (name: string) => null } + }); + } + }) + }; + + const fetchAdapter = angularFetch(mockAngularHttpClient); + const response: any = await fetchAdapter('https://api.example.com/redirect', { + headers: {}, + method: 'GET', + body: '' + }); + + expect(response.status).toBe(301); + expect(response.ok).toBe(false); // 3xx should have ok: false + }); + + it('should use fallback status codes when status is missing', async () => { + // Test success case without status + const mockSuccessClient = { + get: jest.fn().mockReturnValue({ + subscribe: (onSuccess: any, _onError: any) => { + onSuccess({ + body: 'success', + headers: { get: (name: string) => null } + }); + } + }) + }; + + const fetchAdapter1 = angularFetch(mockSuccessClient); + const response1: any = await fetchAdapter1('https://api.example.com/test', { + headers: {}, + method: 'GET', + body: '' + }); + + expect(response1.status).toBe(200); // Defaults to 200 for success + expect(response1.ok).toBe(true); + + // Test error case without status + const mockErrorClient = { + get: jest.fn().mockReturnValue({ + subscribe: (_onSuccess: any, onError: any) => { + onError({ + message: 'Network error', + headers: { get: (name: string) => null } + }); + } + }) + }; + + const fetchAdapter2 = angularFetch(mockErrorClient); + const response2: any = await fetchAdapter2('https://api.example.com/test', { + headers: {}, + method: 'GET', + body: '' + }); + + expect(response2.status).toBe(500); // Defaults to 500 for errors + expect(response2.ok).toBe(false); + }); + + it('should use fallback error messages when error and message are missing', async () => { + const mockAngularHttpClient = { + get: jest.fn().mockReturnValue({ + subscribe: (_onSuccess: any, onError: any) => { + onError({ + status: 500, + headers: { get: (_name: string) => null } + }); + } + }) + }; + + const fetchAdapter = angularFetch(mockAngularHttpClient); + const response: any = await fetchAdapter('https://api.example.com/test', { + headers: {}, + method: 'GET', + body: '' + }); + + const errorText = await response.text(); + expect(errorText).toBe(''); // Falls back to empty string + }); + + it('should handle unsupported HTTP methods with 405 status', async () => { + const mockAngularHttpClient = { + get: jest.fn(), + post: jest.fn(), + put: jest.fn() + }; + + const fetchAdapter = angularFetch(mockAngularHttpClient); + const response: any = await fetchAdapter('https://api.example.com/test', { + headers: {}, + method: 'DELETE' as any, // Using unsupported method + body: '' + }); + + expect(response.status).toBe(405); + expect(response.ok).toBe(false); + const errorText = await response.text(); + expect(errorText).toContain('Unsupported method'); + expect(errorText).toContain('DELETE'); + }); }); diff --git a/utils/angular-fetch.ts b/utils/angular-fetch.ts index 6be64e1c..209eb813 100644 --- a/utils/angular-fetch.ts +++ b/utils/angular-fetch.ts @@ -6,7 +6,7 @@ export default (angularHttpClient: any) => (url: string, params: { const { headers, method, body } = params; const options = { headers, observe: 'response', responseType: 'text' }; - const toFetchResponse = (response: any, ok: boolean) => { + const buildResponse = (response: any, ok: boolean) => { const { status, headers, body, error, message } = response; return { status: status ?? (ok ? 200 : 500), @@ -17,8 +17,8 @@ export default (angularHttpClient: any) => (url: string, params: { }; return new Promise((resolve) => { - const onNext = (res: any) => resolve(toFetchResponse(res, res.status >= 200 && res.status < 300)); - const onError = (err: any) => resolve(toFetchResponse(err, false)); + const onNext = (res: any) => resolve(buildResponse(res, res.status ? res.status >= 200 && res.status < 300 : true)); + const onError = (err: any) => resolve(buildResponse(err, false)); switch (method) { case "GET": return angularHttpClient.get(url, options).subscribe(onNext, onError); From 9556b10f941ede6344d6f68c15d92a8aca1ac56b Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Mon, 17 Nov 2025 09:05:04 -0300 Subject: [PATCH 5/5] fix: stringify JSON objects in angular-fetch text() method --- test/angular-fetch.test.ts | 94 ++++++++++++++++++++++++++++++++++++++ utils/angular-fetch.ts | 5 +- 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/test/angular-fetch.test.ts b/test/angular-fetch.test.ts index 5424669b..6e7455ba 100644 --- a/test/angular-fetch.test.ts +++ b/test/angular-fetch.test.ts @@ -326,4 +326,98 @@ describe('Angular HttpClient Fetch Adapter', () => { expect(errorText).toContain('Unsupported method'); expect(errorText).toContain('DELETE'); }); + + it('should stringify JSON objects in text() method', async () => { + // Test case 1: body contains a JSON object + const mockAngularHttpClient1 = { + get: jest.fn().mockReturnValue({ + subscribe: (onSuccess: any, onError: any) => { + onSuccess({ + status: 200, + body: { flags: [], message: 'Success' }, + headers: { get: (name: string) => null } + }); + } + }) + }; + + const fetchAdapter1 = angularFetch(mockAngularHttpClient1); + const response1: any = await fetchAdapter1('https://api.example.com/test', { + headers: {}, + method: 'GET', + body: '' + }); + + const text1 = await response1.text(); + expect(text1).toBe(JSON.stringify({ flags: [], message: 'Success' })); + + // Test case 2: error contains a JSON object + const mockAngularHttpClient2 = { + get: jest.fn().mockReturnValue({ + subscribe: (onSuccess: any, onError: any) => { + onError({ + status: 400, + error: { code: 'INVALID_REQUEST', details: 'Bad data' }, + headers: { get: (name: string) => null } + }); + } + }) + }; + + const fetchAdapter2 = angularFetch(mockAngularHttpClient2); + const response2: any = await fetchAdapter2('https://api.example.com/test', { + headers: {}, + method: 'GET', + body: '' + }); + + const text2 = await response2.text(); + expect(text2).toBe(JSON.stringify({ code: 'INVALID_REQUEST', details: 'Bad data' })); + + // Test case 3: body contains a string (should not stringify) + const mockAngularHttpClient3 = { + get: jest.fn().mockReturnValue({ + subscribe: (onSuccess: any, onError: any) => { + onSuccess({ + status: 200, + body: 'plain text response', + headers: { get: (name: string) => null } + }); + } + }) + }; + + const fetchAdapter3 = angularFetch(mockAngularHttpClient3); + const response3: any = await fetchAdapter3('https://api.example.com/test', { + headers: {}, + method: 'GET', + body: '' + }); + + const text3 = await response3.text(); + expect(text3).toBe('plain text response'); + + // Test case 4: message contains a string (should not stringify) + const mockAngularHttpClient4 = { + get: jest.fn().mockReturnValue({ + subscribe: (onSuccess: any, onError: any) => { + onError({ + status: 500, + message: 'Internal Server Error', + headers: { get: (name: string) => null } + }); + } + }) + }; + + const fetchAdapter4 = angularFetch(mockAngularHttpClient4); + const response4: any = await fetchAdapter4('https://api.example.com/test', { + headers: {}, + method: 'GET', + body: '' + }); + + const text4 = await response4.text(); + expect(text4).toBe('Internal Server Error'); + }); }); diff --git a/utils/angular-fetch.ts b/utils/angular-fetch.ts index 209eb813..8780c431 100644 --- a/utils/angular-fetch.ts +++ b/utils/angular-fetch.ts @@ -12,7 +12,10 @@ export default (angularHttpClient: any) => (url: string, params: { status: status ?? (ok ? 200 : 500), ok, headers: { get: (name: string) => headers?.get?.(name) ?? null }, - text: () => Promise.resolve(body ?? error ?? message ?? ''), + text: () => { + const value = body ?? error ?? message ?? ''; + return Promise.resolve(typeof value !== 'string' ? JSON.stringify(value) : value); + }, }; };