Skip to content

Commit ab332c7

Browse files
test: add unit and integration tests for Backstage plugin (#6)
Co-authored-by: Claude Opus 4.5 <[email protected]>
1 parent 9568e53 commit ab332c7

File tree

3 files changed

+278
-0
lines changed

3 files changed

+278
-0
lines changed

.github/workflows/ci.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,22 @@ jobs:
6363

6464
- name: Build package
6565
run: yarn build:all
66+
67+
test:
68+
name: Test
69+
runs-on: ubuntu-latest
70+
steps:
71+
- name: Checkout code
72+
uses: actions/checkout@v4
73+
74+
- name: Setup Node.js
75+
uses: actions/setup-node@v4
76+
with:
77+
node-version: '22'
78+
cache: 'yarn'
79+
80+
- name: Install dependencies
81+
run: yarn install --frozen-lockfile
82+
83+
- name: Run tests
84+
run: yarn test --coverage --ci

src/api/FlagsmithClient.test.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { FlagsmithClient } from './FlagsmithClient';
2+
3+
describe('FlagsmithClient', () => {
4+
const baseUrl = 'http://localhost:7007/api/proxy/flagsmith';
5+
let client: FlagsmithClient;
6+
let mockFetch: jest.Mock;
7+
8+
beforeEach(() => {
9+
mockFetch = jest.fn();
10+
client = new FlagsmithClient(
11+
{ getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7007/api/proxy') },
12+
{ fetch: mockFetch },
13+
);
14+
});
15+
16+
const mockOk = (data: unknown) => ({ ok: true, json: async () => data });
17+
const mockError = (status: string) => ({ ok: false, statusText: status });
18+
19+
describe('getOrganizations', () => {
20+
it('fetches organizations', async () => {
21+
const org = { id: 1, name: 'Test Org', created_date: '2024-01-01' };
22+
mockFetch.mockResolvedValue(mockOk({ results: [org] }));
23+
24+
const result = await client.getOrganizations();
25+
26+
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/organisations/`);
27+
expect(result).toEqual([org]);
28+
});
29+
30+
it('throws on error', async () => {
31+
mockFetch.mockResolvedValue(mockError('Unauthorized'));
32+
await expect(client.getOrganizations()).rejects.toThrow('Failed to fetch organizations: Unauthorized');
33+
});
34+
});
35+
36+
describe('getProjectsInOrg', () => {
37+
it('fetches projects', async () => {
38+
const project = { id: 1, name: 'Project', organisation: 1, created_date: '2024-01-01' };
39+
mockFetch.mockResolvedValue(mockOk({ results: [project] }));
40+
41+
const result = await client.getProjectsInOrg(1);
42+
43+
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/organisations/1/projects/`);
44+
expect(result).toEqual([project]);
45+
});
46+
});
47+
48+
describe('getProject', () => {
49+
it('fetches single project', async () => {
50+
const project = { id: 123, name: 'Project', organisation: 1, created_date: '2024-01-01' };
51+
mockFetch.mockResolvedValue(mockOk(project));
52+
53+
const result = await client.getProject(123);
54+
55+
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/projects/123/`);
56+
expect(result).toEqual(project);
57+
});
58+
});
59+
60+
describe('getProjectFeatures', () => {
61+
it('fetches features', async () => {
62+
const features = [{ id: 1, name: 'feature', created_date: '2024-01-01', project: 1 }];
63+
mockFetch.mockResolvedValue(mockOk({ results: features }));
64+
65+
const result = await client.getProjectFeatures('123');
66+
67+
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/projects/123/features/`);
68+
expect(result).toEqual(features);
69+
});
70+
});
71+
72+
describe('getProjectEnvironments', () => {
73+
it('fetches environments', async () => {
74+
const envs = [{ id: 1, name: 'Dev', api_key: 'key', project: 123 }];
75+
mockFetch.mockResolvedValue(mockOk({ results: envs }));
76+
77+
const result = await client.getProjectEnvironments(123);
78+
79+
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/projects/123/environments/`);
80+
expect(result).toEqual(envs);
81+
});
82+
});
83+
84+
describe('getUsageData', () => {
85+
it('fetches usage data', async () => {
86+
const usage = [{ flags: 100, identities: 50, traits: 25, environment_document: 10, day: '2024-01-01', labels: {} }];
87+
mockFetch.mockResolvedValue(mockOk(usage));
88+
89+
const result = await client.getUsageData(1);
90+
91+
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/organisations/1/usage-data/`);
92+
expect(result).toEqual(usage);
93+
});
94+
95+
it('includes project_id filter', async () => {
96+
mockFetch.mockResolvedValue(mockOk([]));
97+
await client.getUsageData(1, 123);
98+
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/organisations/1/usage-data/?project_id=123`);
99+
});
100+
});
101+
102+
describe('getFeatureVersions', () => {
103+
it('fetches versions', async () => {
104+
const versions = [{ uuid: 'v1', is_live: true, published: true }];
105+
mockFetch.mockResolvedValue(mockOk({ results: versions }));
106+
107+
const result = await client.getFeatureVersions(1, 100);
108+
109+
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/environments/1/features/100/versions/`);
110+
expect(result).toEqual(versions);
111+
});
112+
});
113+
114+
describe('getFeatureStates', () => {
115+
it('fetches states', async () => {
116+
const states = [{ id: 1, enabled: true, feature_segment: null }];
117+
mockFetch.mockResolvedValue(mockOk(states));
118+
119+
const result = await client.getFeatureStates(1, 100, 'uuid');
120+
121+
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/environments/1/features/100/versions/uuid/featurestates/`);
122+
expect(result).toEqual(states);
123+
});
124+
});
125+
126+
describe('getFeatureDetails', () => {
127+
it('combines versions and states', async () => {
128+
const versions = [{ uuid: 'v1', is_live: true, published: true }];
129+
const states = [
130+
{ id: 1, enabled: true, feature_segment: null },
131+
{ id: 2, enabled: true, feature_segment: { segment: 1, priority: 1 } },
132+
];
133+
134+
mockFetch
135+
.mockResolvedValueOnce(mockOk({ results: versions }))
136+
.mockResolvedValueOnce(mockOk(states));
137+
138+
const result = await client.getFeatureDetails(1, 100);
139+
140+
expect(result.liveVersion).toEqual(versions[0]);
141+
expect(result.featureState).toEqual(states);
142+
expect(result.segmentOverrides).toBe(1);
143+
});
144+
145+
it('returns nulls when no live version', async () => {
146+
mockFetch.mockResolvedValue(mockOk({ results: [] }));
147+
148+
const result = await client.getFeatureDetails(1, 100);
149+
150+
expect(result.liveVersion).toBeNull();
151+
expect(result.featureState).toBeNull();
152+
expect(result.segmentOverrides).toBe(0);
153+
});
154+
});
155+
});

src/utils/flagHelpers.test.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import {
2+
getFeatureEnvStatus,
3+
buildEnvStatusTooltip,
4+
calculateFeatureStats,
5+
paginate,
6+
} from './flagHelpers';
7+
import { FlagsmithFeature, FlagsmithEnvironment } from '../api/FlagsmithClient';
8+
9+
describe('flagHelpers', () => {
10+
describe('getFeatureEnvStatus', () => {
11+
const feature: FlagsmithFeature = {
12+
id: 1,
13+
name: 'test',
14+
created_date: '2024-01-01',
15+
project: 1,
16+
default_enabled: true,
17+
environment_state: [
18+
{ id: 1, enabled: true },
19+
{ id: 2, enabled: false },
20+
],
21+
};
22+
23+
it('returns status from environment_state', () => {
24+
expect(getFeatureEnvStatus(feature, 1)).toBe(true);
25+
expect(getFeatureEnvStatus(feature, 2)).toBe(false);
26+
});
27+
28+
it('falls back to default_enabled when env not found', () => {
29+
expect(getFeatureEnvStatus(feature, 999)).toBe(true);
30+
});
31+
32+
it('falls back to default_enabled when environment_state is null', () => {
33+
const f = { ...feature, environment_state: null };
34+
expect(getFeatureEnvStatus(f, 1)).toBe(true);
35+
});
36+
37+
it('returns false when no environment_state and no default_enabled', () => {
38+
const f = { id: 1, name: 'test', created_date: '2024-01-01', project: 1 };
39+
expect(getFeatureEnvStatus(f, 1)).toBe(false);
40+
});
41+
});
42+
43+
describe('buildEnvStatusTooltip', () => {
44+
const feature: FlagsmithFeature = {
45+
id: 1,
46+
name: 'test',
47+
created_date: '2024-01-01',
48+
project: 1,
49+
environment_state: [
50+
{ id: 1, enabled: true },
51+
{ id: 2, enabled: false },
52+
],
53+
};
54+
const envs: FlagsmithEnvironment[] = [
55+
{ id: 1, name: 'Dev', api_key: 'k1', project: 1 },
56+
{ id: 2, name: 'Prod', api_key: 'k2', project: 1 },
57+
];
58+
59+
it('builds tooltip with all environments', () => {
60+
expect(buildEnvStatusTooltip(feature, envs)).toBe('Dev: On • Prod: Off');
61+
});
62+
63+
it('returns empty string for empty environments', () => {
64+
expect(buildEnvStatusTooltip(feature, [])).toBe('');
65+
});
66+
});
67+
68+
describe('calculateFeatureStats', () => {
69+
it('counts enabled and disabled features', () => {
70+
const features: FlagsmithFeature[] = [
71+
{ id: 1, name: 'f1', created_date: '2024-01-01', project: 1, default_enabled: true },
72+
{ id: 2, name: 'f2', created_date: '2024-01-01', project: 1, default_enabled: false },
73+
{ id: 3, name: 'f3', created_date: '2024-01-01', project: 1, default_enabled: true },
74+
];
75+
76+
const stats = calculateFeatureStats(features);
77+
78+
expect(stats.enabledCount).toBe(2);
79+
expect(stats.disabledCount).toBe(1);
80+
});
81+
82+
it('returns zeros for empty array', () => {
83+
expect(calculateFeatureStats([])).toEqual({ enabledCount: 0, disabledCount: 0 });
84+
});
85+
});
86+
87+
describe('paginate', () => {
88+
const items = ['a', 'b', 'c', 'd', 'e'];
89+
90+
it('returns correct page', () => {
91+
expect(paginate(items, 0, 2)).toEqual({ paginatedItems: ['a', 'b'], totalPages: 3 });
92+
expect(paginate(items, 1, 2)).toEqual({ paginatedItems: ['c', 'd'], totalPages: 3 });
93+
expect(paginate(items, 2, 2)).toEqual({ paginatedItems: ['e'], totalPages: 3 });
94+
});
95+
96+
it('handles empty array', () => {
97+
expect(paginate([], 0, 10)).toEqual({ paginatedItems: [], totalPages: 0 });
98+
});
99+
100+
it('handles out of bounds page', () => {
101+
expect(paginate(items, 10, 2).paginatedItems).toEqual([]);
102+
});
103+
});
104+
});

0 commit comments

Comments
 (0)