Skip to content

Commit 5501efa

Browse files
talissoncostaclaude
andcommitted
test(api): add unit tests for FlagsmithClient
Add tests for all API client methods with mocked fetch responses and error handling. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent c35a527 commit 5501efa

File tree

1 file changed

+357
-0
lines changed

1 file changed

+357
-0
lines changed

src/api/FlagsmithClient.test.ts

Lines changed: 357 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
import { FlagsmithClient } from './FlagsmithClient';
2+
import {
3+
mockOrganization,
4+
mockProject,
5+
mockEnvironments,
6+
mockFeatures,
7+
mockUsageData,
8+
mockFeatureVersions,
9+
mockFeatureStates,
10+
} from '../__tests__/fixtures';
11+
12+
describe('FlagsmithClient', () => {
13+
let client: FlagsmithClient;
14+
let mockDiscoveryApi: { getBaseUrl: jest.Mock };
15+
let mockFetchApi: { fetch: jest.Mock };
16+
17+
beforeEach(() => {
18+
mockDiscoveryApi = {
19+
getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7007/api/proxy'),
20+
};
21+
mockFetchApi = {
22+
fetch: jest.fn(),
23+
};
24+
client = new FlagsmithClient(mockDiscoveryApi, mockFetchApi);
25+
});
26+
27+
afterEach(() => {
28+
jest.clearAllMocks();
29+
});
30+
31+
describe('getOrganizations', () => {
32+
it('fetches organizations successfully', async () => {
33+
mockFetchApi.fetch.mockResolvedValue({
34+
ok: true,
35+
json: async () => ({ results: [mockOrganization] }),
36+
});
37+
38+
const result = await client.getOrganizations();
39+
40+
expect(mockDiscoveryApi.getBaseUrl).toHaveBeenCalledWith('proxy');
41+
expect(mockFetchApi.fetch).toHaveBeenCalledWith(
42+
'http://localhost:7007/api/proxy/flagsmith/organisations/',
43+
);
44+
expect(result).toEqual([mockOrganization]);
45+
});
46+
47+
it('handles response without results wrapper', async () => {
48+
mockFetchApi.fetch.mockResolvedValue({
49+
ok: true,
50+
json: async () => [mockOrganization],
51+
});
52+
53+
const result = await client.getOrganizations();
54+
55+
expect(result).toEqual([mockOrganization]);
56+
});
57+
58+
it('throws error on failed response', async () => {
59+
mockFetchApi.fetch.mockResolvedValue({
60+
ok: false,
61+
statusText: 'Unauthorized',
62+
});
63+
64+
await expect(client.getOrganizations()).rejects.toThrow(
65+
'Failed to fetch organizations: Unauthorized',
66+
);
67+
});
68+
});
69+
70+
describe('getProjectsInOrg', () => {
71+
it('fetches projects for organization', async () => {
72+
mockFetchApi.fetch.mockResolvedValue({
73+
ok: true,
74+
json: async () => ({ results: [mockProject] }),
75+
});
76+
77+
const result = await client.getProjectsInOrg(1);
78+
79+
expect(mockFetchApi.fetch).toHaveBeenCalledWith(
80+
'http://localhost:7007/api/proxy/flagsmith/organisations/1/projects/',
81+
);
82+
expect(result).toEqual([mockProject]);
83+
});
84+
85+
it('throws error on failed response', async () => {
86+
mockFetchApi.fetch.mockResolvedValue({
87+
ok: false,
88+
statusText: 'Not Found',
89+
});
90+
91+
await expect(client.getProjectsInOrg(999)).rejects.toThrow(
92+
'Failed to fetch projects: Not Found',
93+
);
94+
});
95+
});
96+
97+
describe('getProject', () => {
98+
it('fetches single project', async () => {
99+
mockFetchApi.fetch.mockResolvedValue({
100+
ok: true,
101+
json: async () => mockProject,
102+
});
103+
104+
const result = await client.getProject(123);
105+
106+
expect(mockFetchApi.fetch).toHaveBeenCalledWith(
107+
'http://localhost:7007/api/proxy/flagsmith/projects/123/',
108+
);
109+
expect(result).toEqual(mockProject);
110+
});
111+
112+
it('throws error on failed response', async () => {
113+
mockFetchApi.fetch.mockResolvedValue({
114+
ok: false,
115+
statusText: 'Not Found',
116+
});
117+
118+
await expect(client.getProject(999)).rejects.toThrow(
119+
'Failed to fetch project: Not Found',
120+
);
121+
});
122+
});
123+
124+
describe('getProjectFeatures', () => {
125+
it('fetches features for project', async () => {
126+
mockFetchApi.fetch.mockResolvedValue({
127+
ok: true,
128+
json: async () => ({ results: mockFeatures }),
129+
});
130+
131+
const result = await client.getProjectFeatures('123');
132+
133+
expect(mockFetchApi.fetch).toHaveBeenCalledWith(
134+
'http://localhost:7007/api/proxy/flagsmith/projects/123/features/',
135+
);
136+
expect(result).toEqual(mockFeatures);
137+
});
138+
139+
it('handles response without results wrapper', async () => {
140+
mockFetchApi.fetch.mockResolvedValue({
141+
ok: true,
142+
json: async () => mockFeatures,
143+
});
144+
145+
const result = await client.getProjectFeatures('123');
146+
147+
expect(result).toEqual(mockFeatures);
148+
});
149+
150+
it('throws error on failed response', async () => {
151+
mockFetchApi.fetch.mockResolvedValue({
152+
ok: false,
153+
statusText: 'Internal Server Error',
154+
});
155+
156+
await expect(client.getProjectFeatures('123')).rejects.toThrow(
157+
'Failed to fetch features: Internal Server Error',
158+
);
159+
});
160+
});
161+
162+
describe('getProjectEnvironments', () => {
163+
it('fetches environments for project', async () => {
164+
mockFetchApi.fetch.mockResolvedValue({
165+
ok: true,
166+
json: async () => ({ results: mockEnvironments }),
167+
});
168+
169+
const result = await client.getProjectEnvironments(123);
170+
171+
expect(mockFetchApi.fetch).toHaveBeenCalledWith(
172+
'http://localhost:7007/api/proxy/flagsmith/projects/123/environments/',
173+
);
174+
expect(result).toEqual(mockEnvironments);
175+
});
176+
177+
it('throws error on failed response', async () => {
178+
mockFetchApi.fetch.mockResolvedValue({
179+
ok: false,
180+
statusText: 'Forbidden',
181+
});
182+
183+
await expect(client.getProjectEnvironments(123)).rejects.toThrow(
184+
'Failed to fetch environments: Forbidden',
185+
);
186+
});
187+
});
188+
189+
describe('getUsageData', () => {
190+
it('fetches usage data for organization', async () => {
191+
mockFetchApi.fetch.mockResolvedValue({
192+
ok: true,
193+
json: async () => mockUsageData,
194+
});
195+
196+
const result = await client.getUsageData(1);
197+
198+
expect(mockFetchApi.fetch).toHaveBeenCalledWith(
199+
'http://localhost:7007/api/proxy/flagsmith/organisations/1/usage-data/',
200+
);
201+
expect(result).toEqual(mockUsageData);
202+
});
203+
204+
it('fetches usage data with project filter', async () => {
205+
mockFetchApi.fetch.mockResolvedValue({
206+
ok: true,
207+
json: async () => mockUsageData,
208+
});
209+
210+
const result = await client.getUsageData(1, 123);
211+
212+
expect(mockFetchApi.fetch).toHaveBeenCalledWith(
213+
'http://localhost:7007/api/proxy/flagsmith/organisations/1/usage-data/?project_id=123',
214+
);
215+
expect(result).toEqual(mockUsageData);
216+
});
217+
218+
it('throws error on failed response', async () => {
219+
mockFetchApi.fetch.mockResolvedValue({
220+
ok: false,
221+
statusText: 'Bad Request',
222+
});
223+
224+
await expect(client.getUsageData(1)).rejects.toThrow(
225+
'Failed to fetch usage data: Bad Request',
226+
);
227+
});
228+
});
229+
230+
describe('getFeatureVersions', () => {
231+
it('fetches feature versions', async () => {
232+
mockFetchApi.fetch.mockResolvedValue({
233+
ok: true,
234+
json: async () => ({ results: mockFeatureVersions }),
235+
});
236+
237+
const result = await client.getFeatureVersions(1, 100);
238+
239+
expect(mockFetchApi.fetch).toHaveBeenCalledWith(
240+
'http://localhost:7007/api/proxy/flagsmith/environments/1/features/100/versions/',
241+
);
242+
expect(result).toEqual(mockFeatureVersions);
243+
});
244+
245+
it('throws error on failed response', async () => {
246+
mockFetchApi.fetch.mockResolvedValue({
247+
ok: false,
248+
statusText: 'Not Found',
249+
});
250+
251+
await expect(client.getFeatureVersions(1, 999)).rejects.toThrow(
252+
'Failed to fetch feature versions: Not Found',
253+
);
254+
});
255+
});
256+
257+
describe('getFeatureStates', () => {
258+
it('fetches feature states for a version', async () => {
259+
mockFetchApi.fetch.mockResolvedValue({
260+
ok: true,
261+
json: async () => mockFeatureStates,
262+
});
263+
264+
const result = await client.getFeatureStates(1, 100, 'version-uuid-1');
265+
266+
expect(mockFetchApi.fetch).toHaveBeenCalledWith(
267+
'http://localhost:7007/api/proxy/flagsmith/environments/1/features/100/versions/version-uuid-1/featurestates/',
268+
);
269+
expect(result).toEqual(mockFeatureStates);
270+
});
271+
272+
it('throws error on failed response', async () => {
273+
mockFetchApi.fetch.mockResolvedValue({
274+
ok: false,
275+
statusText: 'Internal Server Error',
276+
});
277+
278+
await expect(
279+
client.getFeatureStates(1, 100, 'invalid-uuid'),
280+
).rejects.toThrow('Failed to fetch feature states: Internal Server Error');
281+
});
282+
});
283+
284+
describe('getFeatureDetails', () => {
285+
it('fetches and combines feature details', async () => {
286+
// First call: getFeatureVersions
287+
mockFetchApi.fetch.mockResolvedValueOnce({
288+
ok: true,
289+
json: async () => ({ results: mockFeatureVersions }),
290+
});
291+
// Second call: getFeatureStates
292+
mockFetchApi.fetch.mockResolvedValueOnce({
293+
ok: true,
294+
json: async () => mockFeatureStates,
295+
});
296+
297+
const result = await client.getFeatureDetails(1, 100);
298+
299+
expect(mockFetchApi.fetch).toHaveBeenCalledTimes(2);
300+
expect(result.liveVersion).toEqual(mockFeatureVersions[0]); // The one with is_live: true
301+
expect(result.featureState).toEqual(mockFeatureStates);
302+
expect(result.segmentOverrides).toBe(1); // One state has feature_segment
303+
});
304+
305+
it('returns null liveVersion when no live version exists', async () => {
306+
const nonLiveVersions = [
307+
{ ...mockFeatureVersions[1] }, // is_live: false
308+
];
309+
310+
mockFetchApi.fetch.mockResolvedValueOnce({
311+
ok: true,
312+
json: async () => ({ results: nonLiveVersions }),
313+
});
314+
315+
const result = await client.getFeatureDetails(1, 100);
316+
317+
expect(mockFetchApi.fetch).toHaveBeenCalledTimes(1);
318+
expect(result.liveVersion).toBeNull();
319+
expect(result.featureState).toBeNull();
320+
expect(result.segmentOverrides).toBe(0);
321+
});
322+
323+
it('counts segment overrides correctly', async () => {
324+
const statesWithMultipleSegments = [
325+
{ ...mockFeatureStates[0], feature_segment: null },
326+
{ ...mockFeatureStates[1], feature_segment: { segment: 1, priority: 1 } },
327+
{ id: 3, enabled: true, feature_segment: { segment: 2, priority: 2 } },
328+
];
329+
330+
mockFetchApi.fetch.mockResolvedValueOnce({
331+
ok: true,
332+
json: async () => ({ results: mockFeatureVersions }),
333+
});
334+
mockFetchApi.fetch.mockResolvedValueOnce({
335+
ok: true,
336+
json: async () => statesWithMultipleSegments,
337+
});
338+
339+
const result = await client.getFeatureDetails(1, 100);
340+
341+
expect(result.segmentOverrides).toBe(2);
342+
});
343+
344+
it('handles empty versions list', async () => {
345+
mockFetchApi.fetch.mockResolvedValueOnce({
346+
ok: true,
347+
json: async () => ({ results: [] }),
348+
});
349+
350+
const result = await client.getFeatureDetails(1, 100);
351+
352+
expect(result.liveVersion).toBeNull();
353+
expect(result.featureState).toBeNull();
354+
expect(result.segmentOverrides).toBe(0);
355+
});
356+
});
357+
});

0 commit comments

Comments
 (0)