Skip to content

Commit f1f6557

Browse files
committed
test: add comprehensive unit tests for enhanced GitHub release methods with glob pattern support
1 parent 2a429cd commit f1f6557

1 file changed

Lines changed: 328 additions & 1 deletion

File tree

test/unit/releases-github.test.mts

Lines changed: 328 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,57 @@
22
* @fileoverview Unit tests for GitHub release download utilities.
33
*/
44

5-
import { describe, expect, it } from 'vitest'
5+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
66

77
import picomatch from 'picomatch'
88

99
import {
10+
downloadReleaseAsset,
1011
getAuthHeaders,
12+
getLatestRelease,
13+
getReleaseAssetUrl,
1114
SOCKET_BTM_REPO,
1215
} from '@socketsecurity/lib/releases/github'
1316

17+
import type { HttpResponse } from '@socketsecurity/lib/http-request'
18+
import { httpDownload, httpRequest } from '@socketsecurity/lib/http-request'
19+
20+
// Mock httpRequest and httpDownload modules.
21+
vi.mock('@socketsecurity/lib/http-request')
22+
23+
/**
24+
* Create a mock HttpResponse object for testing.
25+
*
26+
* @param body - Response body as Buffer
27+
* @param ok - Whether the request was successful
28+
* @param status - HTTP status code
29+
* @returns Complete mock HttpResponse object
30+
*/
31+
function createMockHttpResponse(
32+
body: Buffer,
33+
ok: boolean,
34+
status: number,
35+
): HttpResponse {
36+
return {
37+
arrayBuffer: () => {
38+
const slice = body.buffer.slice(
39+
body.byteOffset,
40+
body.byteOffset + body.byteLength,
41+
)
42+
return slice as ArrayBuffer
43+
},
44+
body,
45+
headers: {},
46+
json<T = unknown>(): T {
47+
return JSON.parse(body.toString('utf8')) as T
48+
},
49+
ok,
50+
status,
51+
statusText: ok ? 'OK' : 'Error',
52+
text: () => body.toString('utf8'),
53+
}
54+
}
55+
1456
describe('releases/github', () => {
1557
describe('SOCKET_BTM_REPO', () => {
1658
it('should export socket-btm repository config', () => {
@@ -178,4 +220,289 @@ describe('releases/github', () => {
178220
expect(isMatch('YOGA-SYNC-abc.MJS')).toBe(false)
179221
})
180222
})
223+
224+
describe('getLatestRelease', () => {
225+
const mockReleases = [
226+
{
227+
assets: [
228+
{ name: 'yoga-sync-20260107-abc123.mjs' },
229+
{ name: 'yoga-layout-20260107-abc123.mjs' },
230+
],
231+
tag_name: 'yoga-layout-20260107-abc123',
232+
},
233+
{
234+
assets: [
235+
{ name: 'models-20260106-def456.tar.gz' },
236+
{ name: 'models-embeddings-20260106-def456.bin' },
237+
],
238+
tag_name: 'models-20260106-def456',
239+
},
240+
{
241+
assets: [{ name: 'node-darwin-arm64' }, { name: 'node-linux-x64' }],
242+
tag_name: 'node-smol-20260105-ghi789',
243+
},
244+
]
245+
246+
beforeEach(() => {
247+
vi.mocked(httpRequest).mockResolvedValue(
248+
createMockHttpResponse(
249+
Buffer.from(JSON.stringify(mockReleases)),
250+
true,
251+
200,
252+
),
253+
)
254+
})
255+
256+
afterEach(() => {
257+
vi.clearAllMocks()
258+
})
259+
260+
it('should find latest release by prefix without asset pattern', async () => {
261+
const tag = await getLatestRelease('yoga-layout-', SOCKET_BTM_REPO, {
262+
quiet: true,
263+
})
264+
expect(tag).toBe('yoga-layout-20260107-abc123')
265+
})
266+
267+
it('should find latest release by prefix with matching asset pattern', async () => {
268+
const tag = await getLatestRelease('yoga-layout-', SOCKET_BTM_REPO, {
269+
assetPattern: 'yoga-sync-*.mjs',
270+
quiet: true,
271+
})
272+
expect(tag).toBe('yoga-layout-20260107-abc123')
273+
})
274+
275+
it('should skip release without matching asset when pattern provided', async () => {
276+
const tag = await getLatestRelease('node-smol-', SOCKET_BTM_REPO, {
277+
assetPattern: '*.tar.gz',
278+
quiet: true,
279+
})
280+
expect(tag).toBeNull()
281+
})
282+
283+
it('should match asset with brace expansion pattern', async () => {
284+
const tag = await getLatestRelease('models-', SOCKET_BTM_REPO, {
285+
assetPattern: 'models-{embeddings,data}-*.{bin,dat}',
286+
quiet: true,
287+
})
288+
expect(tag).toBe('models-20260106-def456')
289+
})
290+
291+
it('should match asset with RegExp pattern', async () => {
292+
const tag = await getLatestRelease('models-', SOCKET_BTM_REPO, {
293+
assetPattern: /^models-\d{8}-.+\.tar\.gz$/,
294+
quiet: true,
295+
})
296+
expect(tag).toBe('models-20260106-def456')
297+
})
298+
299+
it('should return null when no releases match prefix', async () => {
300+
const tag = await getLatestRelease('nonexistent-', SOCKET_BTM_REPO, {
301+
quiet: true,
302+
})
303+
expect(tag).toBeNull()
304+
})
305+
})
306+
307+
describe('getReleaseAssetUrl', () => {
308+
const mockRelease = {
309+
assets: [
310+
{
311+
browser_download_url:
312+
'https://github.com/test/repo/releases/download/v1.0.0/yoga-sync-20260107-abc123.mjs',
313+
name: 'yoga-sync-20260107-abc123.mjs',
314+
},
315+
{
316+
browser_download_url:
317+
'https://github.com/test/repo/releases/download/v1.0.0/yoga-layout-20260107-abc123.mjs',
318+
name: 'yoga-layout-20260107-abc123.mjs',
319+
},
320+
{
321+
browser_download_url:
322+
'https://github.com/test/repo/releases/download/v1.0.0/models-data.tar.gz',
323+
name: 'models-data.tar.gz',
324+
},
325+
],
326+
tag_name: 'v1.0.0',
327+
}
328+
329+
beforeEach(() => {
330+
vi.mocked(httpRequest).mockResolvedValue(
331+
createMockHttpResponse(
332+
Buffer.from(JSON.stringify(mockRelease)),
333+
true,
334+
200,
335+
),
336+
)
337+
})
338+
339+
afterEach(() => {
340+
vi.clearAllMocks()
341+
})
342+
343+
it('should get asset URL with exact name', async () => {
344+
const url = await getReleaseAssetUrl(
345+
'v1.0.0',
346+
'yoga-sync-20260107-abc123.mjs',
347+
SOCKET_BTM_REPO,
348+
{ quiet: true },
349+
)
350+
expect(url).toBe(
351+
'https://github.com/test/repo/releases/download/v1.0.0/yoga-sync-20260107-abc123.mjs',
352+
)
353+
})
354+
355+
it('should get asset URL with wildcard pattern', async () => {
356+
const url = await getReleaseAssetUrl(
357+
'v1.0.0',
358+
'yoga-sync-*.mjs',
359+
SOCKET_BTM_REPO,
360+
{ quiet: true },
361+
)
362+
expect(url).toBe(
363+
'https://github.com/test/repo/releases/download/v1.0.0/yoga-sync-20260107-abc123.mjs',
364+
)
365+
})
366+
367+
it('should get asset URL with brace expansion', async () => {
368+
const url = await getReleaseAssetUrl(
369+
'v1.0.0',
370+
'yoga-{sync,layout}-*.mjs',
371+
SOCKET_BTM_REPO,
372+
{ quiet: true },
373+
)
374+
expect(url).toBe(
375+
'https://github.com/test/repo/releases/download/v1.0.0/yoga-sync-20260107-abc123.mjs',
376+
)
377+
})
378+
379+
it('should get asset URL with RegExp pattern', async () => {
380+
const url = await getReleaseAssetUrl(
381+
'v1.0.0',
382+
/^models-.+\.tar\.gz$/,
383+
SOCKET_BTM_REPO,
384+
{ quiet: true },
385+
)
386+
expect(url).toBe(
387+
'https://github.com/test/repo/releases/download/v1.0.0/models-data.tar.gz',
388+
)
389+
})
390+
391+
it('should get asset URL with prefix/suffix object pattern', async () => {
392+
const url = await getReleaseAssetUrl(
393+
'v1.0.0',
394+
{ prefix: 'models-', suffix: '.tar.gz' },
395+
SOCKET_BTM_REPO,
396+
{ quiet: true },
397+
)
398+
expect(url).toBe(
399+
'https://github.com/test/repo/releases/download/v1.0.0/models-data.tar.gz',
400+
)
401+
})
402+
403+
it('should throw error when pattern does not match any asset', async () => {
404+
await expect(
405+
getReleaseAssetUrl('v1.0.0', 'nonexistent-*.xyz', SOCKET_BTM_REPO, {
406+
quiet: true,
407+
}),
408+
).rejects.toThrow('Asset nonexistent-*.xyz not found in release v1.0.0')
409+
}, 40_000)
410+
})
411+
412+
describe('downloadReleaseAsset', () => {
413+
const mockRelease = {
414+
assets: [
415+
{
416+
browser_download_url:
417+
'https://github.com/test/repo/releases/download/v1.0.0/yoga-sync-abc.mjs',
418+
name: 'yoga-sync-abc.mjs',
419+
},
420+
{
421+
browser_download_url:
422+
'https://github.com/test/repo/releases/download/v1.0.0/models-data.tar.gz',
423+
name: 'models-data.tar.gz',
424+
},
425+
],
426+
tag_name: 'v1.0.0',
427+
}
428+
429+
beforeEach(() => {
430+
vi.mocked(httpRequest).mockResolvedValue(
431+
createMockHttpResponse(
432+
Buffer.from(JSON.stringify(mockRelease)),
433+
true,
434+
200,
435+
),
436+
)
437+
vi.mocked(httpDownload).mockResolvedValue(undefined)
438+
})
439+
440+
afterEach(() => {
441+
vi.clearAllMocks()
442+
})
443+
444+
it('should download asset with exact name', async () => {
445+
await downloadReleaseAsset(
446+
'v1.0.0',
447+
'yoga-sync-abc.mjs',
448+
'/tmp/output.mjs',
449+
SOCKET_BTM_REPO,
450+
{ quiet: true },
451+
)
452+
453+
expect(httpDownload).toHaveBeenCalledWith(
454+
'https://github.com/test/repo/releases/download/v1.0.0/yoga-sync-abc.mjs',
455+
'/tmp/output.mjs',
456+
expect.objectContaining({
457+
progressInterval: 10,
458+
retries: 2,
459+
retryDelay: 5000,
460+
}),
461+
)
462+
})
463+
464+
it('should download asset with wildcard pattern', async () => {
465+
await downloadReleaseAsset(
466+
'v1.0.0',
467+
'yoga-*.mjs',
468+
'/tmp/output.mjs',
469+
SOCKET_BTM_REPO,
470+
{ quiet: true },
471+
)
472+
473+
expect(httpDownload).toHaveBeenCalledWith(
474+
'https://github.com/test/repo/releases/download/v1.0.0/yoga-sync-abc.mjs',
475+
'/tmp/output.mjs',
476+
expect.any(Object),
477+
)
478+
})
479+
480+
it('should download asset with brace expansion', async () => {
481+
await downloadReleaseAsset(
482+
'v1.0.0',
483+
'{yoga,models}-*.{mjs,tar.gz}',
484+
'/tmp/output',
485+
SOCKET_BTM_REPO,
486+
{ quiet: true },
487+
)
488+
489+
expect(httpDownload).toHaveBeenCalledWith(
490+
'https://github.com/test/repo/releases/download/v1.0.0/yoga-sync-abc.mjs',
491+
'/tmp/output',
492+
expect.any(Object),
493+
)
494+
})
495+
496+
it('should throw error when pattern does not match', async () => {
497+
await expect(
498+
downloadReleaseAsset(
499+
'v1.0.0',
500+
'nonexistent-*.xyz',
501+
'/tmp/output.xyz',
502+
SOCKET_BTM_REPO,
503+
{ quiet: true },
504+
),
505+
).rejects.toThrow('Asset nonexistent-*.xyz not found in release v1.0.0')
506+
}, 40_000)
507+
})
181508
})

0 commit comments

Comments
 (0)