Skip to content

Commit 4efe8c0

Browse files
test: add unit tests for VersionManager, ReleaseProcessor, and ReleaseIndexBuilder
1 parent 611c052 commit 4efe8c0

File tree

4 files changed

+4128
-131
lines changed

4 files changed

+4128
-131
lines changed
Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
1+
jest.mock("@octokit/rest", () => {
2+
return {
3+
Octokit: jest.fn().mockImplementation(() => ({
4+
rest: {}
5+
}))
6+
};
7+
});
8+
9+
const {
10+
VersionManager,
11+
ReleaseProcessor,
12+
ReleaseIndexBuilder,
13+
} = require('./build-releases');
14+
15+
describe('VersionManager', () => {
16+
let vm;
17+
18+
beforeEach(() => {
19+
const latestPattern = /^(\d{2})\.(\d{1,2})-(\d+)$/;
20+
const minStableVersion = '1.0.0';
21+
vm = new VersionManager(latestPattern, minStableVersion);
22+
});
23+
24+
describe('detectChannel', () => {
25+
it('should detect latest channel for YY.MM-patch format', () => {
26+
expect(vm.detectChannel('25.04-1')).toBe('latest');
27+
expect(vm.detectChannel('25.1-5')).toBe('latest');
28+
expect(vm.detectChannel('24.12-10')).toBe('latest');
29+
});
30+
31+
it('should detect stable channel for semver format', () => {
32+
expect(vm.detectChannel('1.0.0')).toBe('stable');
33+
expect(vm.detectChannel('1.2.3')).toBe('stable');
34+
expect(vm.detectChannel('2.0.0-beta.1')).toBe('stable');
35+
expect(vm.detectChannel('0.24.7')).toBe('stable');
36+
});
37+
38+
it('should treat invalid month as stable', () => {
39+
expect(vm.detectChannel('25.13-1')).toBe('stable'); // month 13
40+
expect(vm.detectChannel('25.0-1')).toBe('stable'); // month 0
41+
});
42+
43+
it('should handle edge cases', () => {
44+
expect(vm.detectChannel('invalid')).toBe('stable');
45+
expect(vm.detectChannel('')).toBe('stable');
46+
});
47+
});
48+
49+
describe('normalizeVersion', () => {
50+
it('should normalize latest channel versions', () => {
51+
expect(vm.normalizeVersion('25.04-1', 'latest')).toBe('25.4.1');
52+
expect(vm.normalizeVersion('25.1-5', 'latest')).toBe('25.1.5');
53+
expect(vm.normalizeVersion('24.12-10', 'latest')).toBe('24.12.10');
54+
});
55+
56+
it('should not modify stable channel versions', () => {
57+
expect(vm.normalizeVersion('1.0.0', 'stable')).toBe('1.0.0');
58+
expect(vm.normalizeVersion('1.2.3', 'stable')).toBe('1.2.3');
59+
expect(vm.normalizeVersion('2.0.0-beta.1', 'stable')).toBe('2.0.0-beta.1');
60+
});
61+
62+
it('should handle invalid latest format', () => {
63+
expect(vm.normalizeVersion('invalid', 'latest')).toBe('invalid');
64+
expect(vm.normalizeVersion('25-04-1', 'latest')).toBe('25-04-1');
65+
});
66+
});
67+
68+
describe('isValidChannelVersion', () => {
69+
it('should validate stable versions >= 1.0.0', () => {
70+
expect(vm.isValidChannelVersion('1.0.0', 'stable')).toBe(true);
71+
expect(vm.isValidChannelVersion('1.2.3', 'stable')).toBe(true);
72+
expect(vm.isValidChannelVersion('2.0.0', 'stable')).toBe(true);
73+
});
74+
75+
it('should reject stable versions < 1.0.0', () => {
76+
expect(vm.isValidChannelVersion('0.9.9', 'stable')).toBe(false);
77+
expect(vm.isValidChannelVersion('0.24.7', 'stable')).toBe(false);
78+
expect(vm.isValidChannelVersion('0.1.0', 'stable')).toBe(false);
79+
});
80+
81+
it('should validate latest channel versions', () => {
82+
expect(vm.isValidChannelVersion('25.4.1', 'latest')).toBe(true);
83+
expect(vm.isValidChannelVersion('24.12.10', 'latest')).toBe(true);
84+
});
85+
86+
it('should reject invalid semver', () => {
87+
expect(vm.isValidChannelVersion('invalid', 'stable')).toBe(false);
88+
expect(vm.isValidChannelVersion('1.2', 'stable')).toBe(false);
89+
expect(vm.isValidChannelVersion('', 'stable')).toBe(false);
90+
});
91+
});
92+
93+
describe('compareVersions', () => {
94+
it('should compare versions correctly', () => {
95+
expect(vm.compareVersions('1.2.3', '1.2.2')).toBeGreaterThan(0);
96+
expect(vm.compareVersions('1.2.2', '1.2.3')).toBeLessThan(0);
97+
expect(vm.compareVersions('1.2.3', '1.2.3')).toBe(0);
98+
});
99+
});
100+
});
101+
102+
describe('ReleaseProcessor', () => {
103+
let processor;
104+
let mockVersionManager;
105+
106+
beforeEach(() => {
107+
mockVersionManager = {
108+
detectChannel: jest.fn((v) => (v.includes('-') ? 'latest' : 'stable')),
109+
normalizeVersion: jest.fn((v) => v),
110+
isValidChannelVersion: jest.fn(() => true),
111+
compareVersions: jest.fn((a, b) => a.localeCompare(b)),
112+
minStableVersion: '1.0.0',
113+
};
114+
115+
processor = new ReleaseProcessor(mockVersionManager, '### Security Fixes:');
116+
});
117+
118+
describe('processReleases', () => {
119+
it('should process valid releases', () => {
120+
const rawReleases = [
121+
{
122+
tag_name: '1.0.0',
123+
draft: false,
124+
prerelease: false,
125+
body: 'Release notes',
126+
published_at: '2025-01-01',
127+
html_url: 'https://github.com/test/test/releases/tag/1.0.0',
128+
created_at: '2025-01-01',
129+
},
130+
{
131+
tag_name: '25.04-1',
132+
draft: false,
133+
prerelease: false,
134+
body: 'Release notes',
135+
published_at: '2025-04-01',
136+
html_url: 'https://github.com/test/test/releases/tag/25.04-1',
137+
created_at: '2025-04-01',
138+
},
139+
];
140+
141+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
142+
const result = processor.processReleases(rawReleases);
143+
144+
expect(result.releases).toHaveLength(2);
145+
expect(result.channels.stable).toHaveLength(1);
146+
expect(result.channels.latest).toHaveLength(1);
147+
consoleSpy.mockRestore();
148+
});
149+
150+
it('should skip draft and prerelease', () => {
151+
const rawReleases = [
152+
{ tag_name: '1.0.0', draft: true, prerelease: false },
153+
{ tag_name: '1.0.1', draft: false, prerelease: true },
154+
];
155+
156+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
157+
const result = processor.processReleases(rawReleases);
158+
159+
expect(result.releases).toHaveLength(0);
160+
consoleSpy.mockRestore();
161+
});
162+
163+
it('should detect security fixes', () => {
164+
const rawReleases = [
165+
{
166+
tag_name: '1.0.0',
167+
draft: false,
168+
prerelease: false,
169+
body: '### Security Fixes:\n- Fixed vulnerability',
170+
published_at: '2025-01-01',
171+
html_url: 'https://github.com/test/test/releases/tag/1.0.0',
172+
created_at: '2025-01-01',
173+
},
174+
];
175+
176+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
177+
const result = processor.processReleases(rawReleases);
178+
179+
expect(result.releases[0].hasSecurityFixes).toBe(true);
180+
consoleSpy.mockRestore();
181+
});
182+
183+
it('should skip invalid versions', () => {
184+
mockVersionManager.isValidChannelVersion.mockReturnValue(false);
185+
186+
const rawReleases = [
187+
{
188+
tag_name: '0.9.0',
189+
draft: false,
190+
prerelease: false,
191+
body: 'Release',
192+
published_at: '2025-01-01',
193+
html_url: 'https://github.com/test/test/releases/tag/0.9.0',
194+
created_at: '2025-01-01',
195+
},
196+
];
197+
198+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
199+
const result = processor.processReleases(rawReleases);
200+
201+
expect(result.releases).toHaveLength(0);
202+
consoleSpy.mockRestore();
203+
});
204+
});
205+
206+
describe('buildChannelData', () => {
207+
it('should build channel data with sorted releases', async () => {
208+
const releases = [
209+
{ version: '1.0.0', normalized: '1.0.0', hasSecurityFixes: false },
210+
{ version: '1.0.2', normalized: '1.0.2', hasSecurityFixes: true },
211+
{ version: '1.0.1', normalized: '1.0.1', hasSecurityFixes: false },
212+
];
213+
214+
const mockGithubClient = {
215+
getAppVersionFromChart: jest.fn().mockResolvedValue('1.2.3'),
216+
};
217+
218+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
219+
const result = await processor.buildChannelData(
220+
releases,
221+
'stable',
222+
mockGithubClient,
223+
10
224+
);
225+
226+
expect(result.releases[0].version).toBe('1.0.2'); // Sorted desc
227+
expect(result.latestWithSecurityFixes).toBe('1.0.2');
228+
expect(result.latestRelease.version).toBe('1.0.2');
229+
consoleSpy.mockRestore();
230+
});
231+
232+
it('should mark upgrade available correctly', async () => {
233+
const releases = [
234+
{ version: '1.0.2', normalized: '1.0.2', hasSecurityFixes: false },
235+
{ version: '1.0.1', normalized: '1.0.1', hasSecurityFixes: false },
236+
];
237+
238+
const mockGithubClient = {
239+
getAppVersionFromChart: jest.fn().mockResolvedValue('1.2.3'),
240+
};
241+
242+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
243+
const result = await processor.buildChannelData(
244+
releases,
245+
'stable',
246+
mockGithubClient,
247+
10
248+
);
249+
250+
expect(result.releases[0].upgradeAvailable).toBe(false); // Latest
251+
expect(result.releases[1].upgradeAvailable).toBe(true); // Not latest
252+
consoleSpy.mockRestore();
253+
});
254+
255+
it('should mark security vulnerabilities correctly', async () => {
256+
const releases = [
257+
{ version: '1.0.3', normalized: '1.0.3', hasSecurityFixes: false },
258+
{ version: '1.0.2', normalized: '1.0.2', hasSecurityFixes: true },
259+
{ version: '1.0.1', normalized: '1.0.1', hasSecurityFixes: false },
260+
];
261+
262+
const mockGithubClient = {
263+
getAppVersionFromChart: jest.fn().mockResolvedValue('1.2.3'),
264+
};
265+
266+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
267+
const result = await processor.buildChannelData(
268+
releases,
269+
'stable',
270+
mockGithubClient,
271+
10
272+
);
273+
274+
expect(result.releases[0].hasSecurityVulnerabilities).toBe(false);
275+
expect(result.releases[1].hasSecurityVulnerabilities).toBe(false);
276+
expect(result.releases[2].hasSecurityVulnerabilities).toBe(true);
277+
consoleSpy.mockRestore();
278+
});
279+
280+
it('should limit releases to maxReleases', async () => {
281+
const releases = Array(20)
282+
.fill(0)
283+
.map((_, i) => ({
284+
version: `1.0.${i}`,
285+
normalized: `1.0.${i}`,
286+
hasSecurityFixes: false,
287+
}));
288+
289+
const mockGithubClient = {
290+
getAppVersionFromChart: jest.fn().mockResolvedValue('1.2.3'),
291+
};
292+
293+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
294+
const result = await processor.buildChannelData(
295+
releases,
296+
'stable',
297+
mockGithubClient,
298+
5
299+
);
300+
301+
expect(result.releases).toHaveLength(5);
302+
consoleSpy.mockRestore();
303+
});
304+
});
305+
});
306+
307+
describe('ReleaseIndexBuilder', () => {
308+
let builder;
309+
let mockConfig;
310+
let mockGithubClient;
311+
let mockReleaseProcessor;
312+
let mockVersionManager;
313+
314+
beforeEach(() => {
315+
mockConfig = {
316+
owner: 'test-owner',
317+
repo: 'test-repo',
318+
maxGithubReleases: 1000,
319+
maxReleasesPerChannel: 10,
320+
};
321+
322+
mockGithubClient = {
323+
fetchReleases: jest.fn(),
324+
};
325+
326+
mockReleaseProcessor = {
327+
processReleases: jest.fn(),
328+
buildChannelData: jest.fn(),
329+
};
330+
331+
mockVersionManager = {};
332+
333+
builder = new ReleaseIndexBuilder(
334+
mockConfig,
335+
mockGithubClient,
336+
mockReleaseProcessor,
337+
mockVersionManager
338+
);
339+
});
340+
341+
describe('buildIndexObject', () => {
342+
it('should build correct index structure', () => {
343+
const releases = [
344+
{ version: '1.0.0' },
345+
{ version: '25.04-1' },
346+
];
347+
348+
const stable = {
349+
releases: [{ version: '1.0.0' }],
350+
latestRelease: { version: '1.0.0' },
351+
latestWithSecurityFixes: null,
352+
};
353+
354+
const latest = {
355+
releases: [{ version: '25.04-1' }],
356+
latestRelease: { version: '25.04-1' },
357+
latestWithSecurityFixes: null,
358+
};
359+
360+
const index = builder.buildIndexObject(releases, stable, latest);
361+
362+
expect(index).toHaveProperty('generatedAt');
363+
expect(index.repository).toBe('test-owner/test-repo');
364+
expect(index.channels.stable).toEqual(stable);
365+
expect(index.channels.latest).toEqual(latest);
366+
expect(index.stats.totalReleases).toBe(2);
367+
});
368+
});
369+
});
370+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
testEnvironment: 'node',
3+
testTimeout: 60000,
4+
forceExit: true,
5+
};

0 commit comments

Comments
 (0)