mirror of
https://github.com/renovatebot/renovate.git
synced 2026-11-01 18:19:51 +00:00
795d083264
* feat(datasource): add APK datasource support * remove missing manager link for now * lint docs * update tests * replace binary fixtures * fix coverage * Update lib/modules/datasource/apk/readme.md Co-authored-by: Jamie Tanna <github@jamietanna.co.uk> * Apply suggestion from @viceice Co-authored-by: Michael Kriese <michael.kriese@gmx.de> * use tar instead of tar-stream * use codeBlock * add ignore back * make method private * use an async extract instead of list * add usage example * stream apkindex download and parse line-by-line * refactor(datasource): address review comments on APK datasource Co-authored-by: Cursor <cursoragent@cursor.com> * chore: sync pnpm-lock.yaml after merge Co-authored-by: Cursor <cursoragent@cursor.com> * test: remove ignored lines As they seem to be covered, anyway. * fixup! Merge remote-tracking branch 'origin/HEAD' into apk-datasource * fixup! fixup! Merge remote-tracking branch 'origin/HEAD' into apk-datasource * lint: use `node:` prefix * Update lib/modules/datasource/apk/index.ts Co-authored-by: Michael Kriese <michael.kriese@gmx.de> * refactor(datasource/apk): use asTimestamp millis instead of new Date Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(datasource/apk): rename uncached method to _getPackages Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(datasource/apk): encode registry URL parts as query parameters Co-authored-by: Cursor <cursoragent@cursor.com> * fix(datasource/apk): reject invalid registry URLs, skip caching empty indexes Co-authored-by: Cursor <cursoragent@cursor.com> * fix(datasource/apk): throw on unparseable registry URL and drop unset sourceUrl support Co-authored-by: Cursor <cursoragent@cursor.com> * docs(datasource/apk): show how registry URL parameters build the index URLs Co-authored-by: Cursor <cursoragent@cursor.com> * Apply suggestions from code review Co-authored-by: RahulGautamSingh <rahultesnik@gmail.com> * fix(datasource/apk): keep credentials and port in component URLs Co-authored-by: Cursor <cursoragent@cursor.com> * fix(datasource/apk): extract APKINDEX by basename and key packages by name Co-authored-by: Cursor <cursoragent@cursor.com> * fix(datasource/apk): report a version served by two components only once Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Jamie Tanna <github@jamietanna.co.uk> Co-authored-by: Michael Kriese <michael.kriese@gmx.de> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Jamie Tanna <jamie.tanna@mend.io> Co-authored-by: RahulGautamSingh <rahultesnik@gmail.com>
784 lines
23 KiB
TypeScript
784 lines
23 KiB
TypeScript
import { promisify } from 'node:util';
|
|
import * as zlib from 'node:zlib';
|
|
import { codeBlock } from 'common-tags';
|
|
import { Header, Pack, ReadEntry } from 'tar';
|
|
import type { DirectoryResult } from 'tmp-promise';
|
|
import { dir } from 'tmp-promise';
|
|
import { vi } from 'vitest';
|
|
import * as httpMock from '~test/http-mock.ts';
|
|
import { GlobalConfig } from '../../../config/global.ts';
|
|
import { EXTERNAL_HOST_ERROR } from '../../../constants/error-messages.ts';
|
|
import { getPkgReleases } from '../index.ts';
|
|
import type { GetPkgReleasesConfig } from '../types.ts';
|
|
import { ApkDatasource } from './index.ts';
|
|
import * as parser from './parser.ts';
|
|
|
|
const gzip = promisify(zlib.gzip);
|
|
|
|
async function createTarGz(
|
|
entries: { name: string; content: string }[],
|
|
): Promise<Buffer> {
|
|
const pack = new Pack({ gzip: true });
|
|
|
|
for (const entry of entries) {
|
|
const data = Buffer.from(entry.content, 'utf8');
|
|
const header = new Header({
|
|
path: entry.name,
|
|
size: data.length,
|
|
type: 'File',
|
|
});
|
|
const readEntry = new ReadEntry(header);
|
|
readEntry.end(data);
|
|
pack.add(readEntry);
|
|
}
|
|
|
|
pack.end();
|
|
|
|
const chunks: Buffer[] = [];
|
|
for await (const chunk of pack) {
|
|
chunks.push(chunk);
|
|
}
|
|
return Buffer.concat(chunks);
|
|
}
|
|
|
|
const nginxApkIndex = [
|
|
'P:nginx',
|
|
'V:1.24.0-r16',
|
|
'U:https://www.nginx.org/',
|
|
't:1714170434',
|
|
].join('\n');
|
|
|
|
describe('modules/datasource/apk/index', () => {
|
|
let apkIndexArchive: Buffer;
|
|
let cacheDir: DirectoryResult | null;
|
|
|
|
beforeAll(async () => {
|
|
apkIndexArchive = await createTarGz([
|
|
{ name: 'APKINDEX', content: nginxApkIndex },
|
|
]);
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
cacheDir = await dir({ unsafeCleanup: true });
|
|
GlobalConfig.set({ cacheDir: cacheDir.path });
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await cacheDir?.cleanup();
|
|
cacheDir = null;
|
|
});
|
|
|
|
const apkDatasource = new ApkDatasource();
|
|
|
|
it('should export ApkDatasource', () => {
|
|
expect(ApkDatasource).toBeDefined();
|
|
});
|
|
|
|
it('should have correct id', () => {
|
|
expect(ApkDatasource.id).toBe('apk');
|
|
});
|
|
|
|
it('should have default registry URLs', () => {
|
|
expect(apkDatasource.defaultRegistryUrls).toEqual([
|
|
'https://dl-cdn.alpinelinux.org/alpine?branch=latest-stable&components=main&arch=x86_64',
|
|
]);
|
|
});
|
|
|
|
it('should support custom registries', () => {
|
|
expect(apkDatasource.customRegistrySupport).toBe(true);
|
|
});
|
|
|
|
describe('getReleases', () => {
|
|
it('should return null for unknown package', async () => {
|
|
const config: GetPkgReleasesConfig = {
|
|
datasource: 'apk',
|
|
packageName: 'unknown-package',
|
|
};
|
|
httpMock
|
|
.scope('https://dl-cdn.alpinelinux.org')
|
|
.get('/alpine/latest-stable/main/x86_64/APKINDEX.tar.gz')
|
|
.reply(200, apkIndexArchive);
|
|
|
|
const res = await getPkgReleases(config);
|
|
expect(res).toBeNull();
|
|
});
|
|
|
|
it('should use default registry URLs', async () => {
|
|
const config: GetPkgReleasesConfig = {
|
|
datasource: 'apk',
|
|
packageName: 'nginx',
|
|
};
|
|
|
|
httpMock
|
|
.scope('https://dl-cdn.alpinelinux.org')
|
|
.get('/alpine/latest-stable/main/x86_64/APKINDEX.tar.gz')
|
|
.reply(200, apkIndexArchive);
|
|
|
|
const res = await getPkgReleases(config);
|
|
expect(res).toEqual({
|
|
homepage: 'https://www.nginx.org/',
|
|
registryUrl:
|
|
'https://dl-cdn.alpinelinux.org/alpine?branch=latest-stable&components=main&arch=x86_64',
|
|
releases: [
|
|
{
|
|
version: '1.24.0-r16',
|
|
releaseTimestamp: '2024-04-26T22:27:14.000Z',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should use custom registry URLs', async () => {
|
|
const config: GetPkgReleasesConfig = {
|
|
datasource: 'apk',
|
|
packageName: 'nginx',
|
|
registryUrls: [
|
|
'https://dl-cdn.alpinelinux.org/alpine?branch=v3.19&components=main&arch=x86_64',
|
|
],
|
|
};
|
|
|
|
httpMock
|
|
.scope('https://dl-cdn.alpinelinux.org')
|
|
.get('/alpine/v3.19/main/x86_64/APKINDEX.tar.gz')
|
|
.reply(200, apkIndexArchive);
|
|
|
|
const res = await getPkgReleases(config);
|
|
expect(res).toEqual({
|
|
homepage: 'https://www.nginx.org/',
|
|
registryUrl:
|
|
'https://dl-cdn.alpinelinux.org/alpine?branch=v3.19&components=main&arch=x86_64',
|
|
releases: [
|
|
{
|
|
version: '1.24.0-r16',
|
|
releaseTimestamp: '2024-04-26T22:27:14.000Z',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should aggregate releases across components', async () => {
|
|
const communityArchive = await createTarGz([
|
|
{
|
|
name: 'APKINDEX',
|
|
content: ['P:nginx', 'V:1.26.2-r0', 't:1747894670'].join('\n'),
|
|
},
|
|
]);
|
|
|
|
httpMock
|
|
.scope('https://dl-cdn.alpinelinux.org')
|
|
.get('/alpine/v3.19/main/x86_64/APKINDEX.tar.gz')
|
|
.reply(200, apkIndexArchive)
|
|
.get('/alpine/v3.19/community/x86_64/APKINDEX.tar.gz')
|
|
.reply(200, communityArchive);
|
|
|
|
const res = await apkDatasource.getReleases({
|
|
packageName: 'nginx',
|
|
registryUrl:
|
|
'https://dl-cdn.alpinelinux.org/alpine?branch=v3.19&components=main,community&arch=x86_64',
|
|
});
|
|
|
|
expect(res).toEqual({
|
|
homepage: 'https://www.nginx.org/',
|
|
registryUrl:
|
|
'https://dl-cdn.alpinelinux.org/alpine?branch=v3.19&components=main,community&arch=x86_64',
|
|
releases: [
|
|
{
|
|
version: '1.24.0-r16',
|
|
releaseTimestamp: '2024-04-26T22:27:14.000Z',
|
|
},
|
|
{
|
|
version: '1.26.2-r0',
|
|
releaseTimestamp: '2025-05-22T06:17:50.000Z',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should report a version served by two components only once', async () => {
|
|
const communityArchive = await createTarGz([
|
|
{
|
|
name: 'APKINDEX',
|
|
content: ['P:nginx', 'V:1.24.0-r16', 't:1714170434'].join('\n'),
|
|
},
|
|
]);
|
|
|
|
httpMock
|
|
.scope('https://dl-cdn.alpinelinux.org')
|
|
.get('/alpine/v3.19/main/x86_64/APKINDEX.tar.gz')
|
|
.reply(200, apkIndexArchive)
|
|
.get('/alpine/v3.19/community/x86_64/APKINDEX.tar.gz')
|
|
.reply(200, communityArchive);
|
|
|
|
const res = await apkDatasource.getReleases({
|
|
packageName: 'nginx',
|
|
registryUrl:
|
|
'https://dl-cdn.alpinelinux.org/alpine?branch=v3.19&components=main,community&arch=x86_64',
|
|
});
|
|
|
|
expect(res).toEqual({
|
|
homepage: 'https://www.nginx.org/',
|
|
registryUrl:
|
|
'https://dl-cdn.alpinelinux.org/alpine?branch=v3.19&components=main,community&arch=x86_64',
|
|
releases: [
|
|
{
|
|
version: '1.24.0-r16',
|
|
releaseTimestamp: '2024-04-26T22:27:14.000Z',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should skip components which do not contain the package', async () => {
|
|
const communityArchive = await createTarGz([
|
|
{
|
|
name: 'APKINDEX',
|
|
content: ['P:docker', 'V:27.3.1-r0', 't:1747894670'].join('\n'),
|
|
},
|
|
]);
|
|
|
|
httpMock
|
|
.scope('https://dl-cdn.alpinelinux.org')
|
|
.get('/alpine/v3.19/main/x86_64/APKINDEX.tar.gz')
|
|
.reply(200, apkIndexArchive)
|
|
.get('/alpine/v3.19/community/x86_64/APKINDEX.tar.gz')
|
|
.reply(200, communityArchive);
|
|
|
|
const res = await apkDatasource.getReleases({
|
|
packageName: 'docker',
|
|
registryUrl:
|
|
'https://dl-cdn.alpinelinux.org/alpine?branch=v3.19&components=main,community&arch=x86_64',
|
|
});
|
|
|
|
expect(res).toEqual({
|
|
registryUrl:
|
|
'https://dl-cdn.alpinelinux.org/alpine?branch=v3.19&components=main,community&arch=x86_64',
|
|
releases: [
|
|
{
|
|
version: '27.3.1-r0',
|
|
releaseTimestamp: '2025-05-22T06:17:50.000Z',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should skip components which cannot be fetched', async () => {
|
|
httpMock
|
|
.scope('https://dl-cdn.alpinelinux.org')
|
|
.get('/alpine/v3.19/testing/x86_64/APKINDEX.tar.gz')
|
|
.reply(404)
|
|
.get('/alpine/v3.19/main/x86_64/APKINDEX.tar.gz')
|
|
.reply(200, apkIndexArchive);
|
|
|
|
const res = await apkDatasource.getReleases({
|
|
packageName: 'nginx',
|
|
registryUrl:
|
|
'https://dl-cdn.alpinelinux.org/alpine?branch=v3.19&components=testing,main&arch=x86_64',
|
|
});
|
|
|
|
expect(res).toEqual({
|
|
homepage: 'https://www.nginx.org/',
|
|
registryUrl:
|
|
'https://dl-cdn.alpinelinux.org/alpine?branch=v3.19&components=testing,main&arch=x86_64',
|
|
releases: [
|
|
{
|
|
version: '1.24.0-r16',
|
|
releaseTimestamp: '2024-04-26T22:27:14.000Z',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should use the homepage of the first version which has one', async () => {
|
|
const archive = await createTarGz([
|
|
{
|
|
name: 'APKINDEX',
|
|
content: codeBlock`
|
|
P:bash
|
|
V:5.3-r1
|
|
t:1752770415
|
|
|
|
P:bash
|
|
V:5.3-r2
|
|
U:https://www.gnu.org/software/bash/
|
|
t:1752888034
|
|
`,
|
|
},
|
|
]);
|
|
|
|
httpMock
|
|
.scope('https://packages.wolfi.dev')
|
|
.get('/os/x86_64/APKINDEX.tar.gz')
|
|
.reply(200, archive);
|
|
|
|
const res = await apkDatasource.getReleases({
|
|
packageName: 'bash',
|
|
registryUrl: 'https://packages.wolfi.dev/os?arch=x86_64',
|
|
});
|
|
|
|
expect(res).toEqual({
|
|
homepage: 'https://www.gnu.org/software/bash/',
|
|
registryUrl: 'https://packages.wolfi.dev/os?arch=x86_64',
|
|
releases: [
|
|
{ version: '5.3-r1', releaseTimestamp: '2025-07-17T16:40:15.000Z' },
|
|
{ version: '5.3-r2', releaseTimestamp: '2025-07-19T01:20:34.000Z' },
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should use the homepage of a later component when the first has none', async () => {
|
|
const mainArchive = await createTarGz([
|
|
{
|
|
name: 'APKINDEX',
|
|
content: ['P:bash', 'V:5.3-r1', 't:1752770415'].join('\n'),
|
|
},
|
|
]);
|
|
const communityArchive = await createTarGz([
|
|
{
|
|
name: 'APKINDEX',
|
|
content: [
|
|
'P:bash',
|
|
'V:5.3-r2',
|
|
'U:https://www.gnu.org/software/bash/',
|
|
't:1752888034',
|
|
].join('\n'),
|
|
},
|
|
]);
|
|
|
|
httpMock
|
|
.scope('https://dl-cdn.alpinelinux.org')
|
|
.get('/alpine/v3.19/main/x86_64/APKINDEX.tar.gz')
|
|
.reply(200, mainArchive)
|
|
.get('/alpine/v3.19/community/x86_64/APKINDEX.tar.gz')
|
|
.reply(200, communityArchive);
|
|
|
|
const res = await apkDatasource.getReleases({
|
|
packageName: 'bash',
|
|
registryUrl:
|
|
'https://dl-cdn.alpinelinux.org/alpine?branch=v3.19&components=main,community&arch=x86_64',
|
|
});
|
|
|
|
expect(res).toEqual({
|
|
homepage: 'https://www.gnu.org/software/bash/',
|
|
registryUrl:
|
|
'https://dl-cdn.alpinelinux.org/alpine?branch=v3.19&components=main,community&arch=x86_64',
|
|
releases: [
|
|
{ version: '5.3-r1', releaseTimestamp: '2025-07-17T16:40:15.000Z' },
|
|
{ version: '5.3-r2', releaseTimestamp: '2025-07-19T01:20:34.000Z' },
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should throw for a registry URL without arch', async () => {
|
|
await expect(
|
|
apkDatasource.getReleases({
|
|
packageName: 'nginx',
|
|
registryUrl: 'https://dl-cdn.alpinelinux.org/alpine?components=main',
|
|
}),
|
|
).rejects.toThrow('Invalid apk repo URL');
|
|
});
|
|
});
|
|
|
|
describe('getReleases with real APK data', () => {
|
|
it('should parse multiple versions of bash package', async () => {
|
|
const indexContent = codeBlock`
|
|
C:Q1vtlRfMK8sXD8JDnaOMI5kCrJG9A=
|
|
P:bash
|
|
V:5.3-r1
|
|
A:x86_64
|
|
S:756553
|
|
I:1585105
|
|
T:GNU bourne again shell
|
|
L:GPL-3.0-or-later
|
|
o:bash
|
|
m:wolfi
|
|
t:1752770415
|
|
c:e71f2e8133acebc89937730af9d9ec874acacc00
|
|
D:merged-bin so:ld-linux-x86-64.so.2 so:libc.so.6 so:libtinfo.so.6 wolfi-baselayout
|
|
p:cmd:bash=5.3-r1
|
|
|
|
C:Q1jngl4lAcPFLwCTlbCwNp+10Usd8=
|
|
P:bash
|
|
V:5.3-r2
|
|
A:x86_64
|
|
S:756557
|
|
I:1585129
|
|
T:GNU bourne again shell
|
|
L:GPL-3.0-or-later
|
|
o:bash
|
|
m:wolfi
|
|
t:1752888034
|
|
c:b96768fe1c8d6dbd5bcf00b324fa94288a2b8eff
|
|
D:merged-bin so:ld-linux-x86-64.so.2 so:libc.so.6 so:libtinfo.so.6 wolfi-baselayout
|
|
p:cmd:bash=5.3-r2
|
|
|
|
C:Q1iOqqGFee8KnAMAiigIso6EN+u7o=
|
|
P:bash
|
|
V:5.3-r3
|
|
A:x86_64
|
|
S:757735
|
|
I:1585231
|
|
T:GNU bourne again shell
|
|
L:GPL-3.0-or-later
|
|
o:bash
|
|
m:wolfi
|
|
t:1754953964
|
|
c:57df7e0f0fda63de7a35cf4da489dd46a2d4bcb3
|
|
D:merged-bin so:ld-linux-x86-64.so.2 so:libc.so.6 so:libtinfo.so.6 wolfi-baselayout
|
|
p:cmd:bash=5.3-r3
|
|
`;
|
|
|
|
const apkDatasource = new ApkDatasource();
|
|
const tarGzBuffer = await createTarGz([
|
|
{ name: 'APKINDEX', content: indexContent },
|
|
]);
|
|
|
|
httpMock
|
|
.scope('https://packages.wolfi.dev')
|
|
.get('/os/x86_64/APKINDEX.tar.gz')
|
|
.reply(200, tarGzBuffer);
|
|
|
|
const result = await apkDatasource.getReleases({
|
|
packageName: 'bash',
|
|
registryUrl: 'https://packages.wolfi.dev/os?arch=x86_64',
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
homepage: undefined, // No URL field in the test data
|
|
registryUrl: 'https://packages.wolfi.dev/os?arch=x86_64',
|
|
releases: [
|
|
{
|
|
version: '5.3-r1',
|
|
releaseTimestamp: '2025-07-17T16:40:15.000Z',
|
|
},
|
|
{
|
|
version: '5.3-r2',
|
|
releaseTimestamp: '2025-07-19T01:20:34.000Z',
|
|
},
|
|
{
|
|
version: '5.3-r3',
|
|
releaseTimestamp: '2025-08-11T23:12:44.000Z',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should handle packages with URL field', async () => {
|
|
const indexContent = codeBlock`
|
|
C:Q1sUvhopwmAmHD4IGg4MsoPPo2veQ=
|
|
P:nginx
|
|
V:1.28.0-r3
|
|
A:x86_64
|
|
S:520401
|
|
I:1183347
|
|
T:HTTP and reverse proxy server (stable version)
|
|
U:https://www.nginx.org/
|
|
L:BSD-2-Clause
|
|
o:nginx
|
|
m:Jakub Jirutka <jakub@jirutka.cz>
|
|
t:1747894670
|
|
c:7094d681a90006e7599ce94b8b2aed5c27b53c1d
|
|
D:/bin/sh so:libc.musl-x86_64.so.1 so:libcrypto.so.3 so:libpcre2-8.so.0 so:libssl.so.3 so:libz.so.1
|
|
p:cmd:nginx=1.28.0-r3
|
|
`;
|
|
|
|
const apkDatasource = new ApkDatasource();
|
|
const tarGzBuffer = await createTarGz([
|
|
{ name: 'APKINDEX', content: indexContent },
|
|
]);
|
|
|
|
httpMock
|
|
.scope('https://dl-cdn.alpinelinux.org')
|
|
.get('/alpine/latest-stable/main/x86_64/APKINDEX.tar.gz')
|
|
.reply(200, tarGzBuffer);
|
|
|
|
const result = await apkDatasource.getReleases({
|
|
packageName: 'nginx',
|
|
registryUrl:
|
|
'https://dl-cdn.alpinelinux.org/alpine?branch=latest-stable&components=main&arch=x86_64',
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
homepage: 'https://www.nginx.org/',
|
|
registryUrl:
|
|
'https://dl-cdn.alpinelinux.org/alpine?branch=latest-stable&components=main&arch=x86_64',
|
|
releases: [
|
|
{
|
|
version: '1.28.0-r3',
|
|
releaseTimestamp: '2025-05-22T06:17:50.000Z',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should return null for unknown package', async () => {
|
|
const indexContent = codeBlock`
|
|
C:Q1vtlRfMK8sXD8JDnaOMI5kCrJG9A=
|
|
P:bash
|
|
V:5.3-r1
|
|
A:x86_64
|
|
S:756553
|
|
I:1585105
|
|
T:GNU bourne again shell
|
|
L:GPL-3.0-or-later
|
|
o:bash
|
|
m:wolfi
|
|
t:1752770415
|
|
c:e71f2e8133acebc89937730af9d9ec874acacc00
|
|
D:merged-bin so:ld-linux-x86-64.so.2 so:libc.so.6 so:libtinfo.so.6 wolfi-baselayout
|
|
p:cmd:bash=5.3-r1
|
|
`;
|
|
|
|
const apkDatasource = new ApkDatasource();
|
|
const tarGzBuffer = await createTarGz([
|
|
{ name: 'APKINDEX', content: indexContent },
|
|
]);
|
|
|
|
httpMock
|
|
.scope('https://packages.wolfi.dev')
|
|
.get('/os/x86_64/APKINDEX.tar.gz')
|
|
.reply(200, tarGzBuffer);
|
|
|
|
const result = await apkDatasource.getReleases({
|
|
packageName: 'unknown-package',
|
|
registryUrl: 'https://packages.wolfi.dev/os?arch=x86_64',
|
|
});
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should extract the index from an archive laid out like a real one', async () => {
|
|
const tarGzBuffer = await createTarGz([
|
|
{
|
|
name: '.SIGN.RSA.alpine-devel@lists.alpinelinux.org-6165ee59.rsa.pub',
|
|
content: 'signature',
|
|
},
|
|
{ name: 'DESCRIPTION', content: 'alpine v3.19 main' },
|
|
{ name: './APKINDEX', content: nginxApkIndex },
|
|
]);
|
|
|
|
httpMock
|
|
.scope('https://dl-cdn.alpinelinux.org')
|
|
.get('/alpine/v3.19/main/x86_64/APKINDEX.tar.gz')
|
|
.reply(200, tarGzBuffer);
|
|
|
|
const result = await apkDatasource.getReleases({
|
|
packageName: 'nginx',
|
|
registryUrl:
|
|
'https://dl-cdn.alpinelinux.org/alpine?branch=v3.19&components=main&arch=x86_64',
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
homepage: 'https://www.nginx.org/',
|
|
registryUrl:
|
|
'https://dl-cdn.alpinelinux.org/alpine?branch=v3.19&components=main&arch=x86_64',
|
|
releases: [
|
|
{
|
|
version: '1.24.0-r16',
|
|
releaseTimestamp: '2024-04-26T22:27:14.000Z',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should handle packages without buildDate', async () => {
|
|
const indexContent = codeBlock`
|
|
C:Q1vtlRfMK8sXD8JDnaOMI5kCrJG9A=
|
|
P:minimal-package
|
|
V:1.0.0
|
|
A:x86_64
|
|
S:1000
|
|
I:2000
|
|
T:Minimal package without build date
|
|
L:MIT
|
|
o:minimal-package
|
|
m:test@example.com
|
|
c:abc123def456
|
|
`;
|
|
|
|
const apkDatasource = new ApkDatasource();
|
|
const tarGzBuffer = await createTarGz([
|
|
{ name: 'APKINDEX', content: indexContent },
|
|
]);
|
|
|
|
httpMock
|
|
.scope('https://packages.wolfi.dev')
|
|
.get('/os/x86_64/APKINDEX.tar.gz')
|
|
.reply(200, tarGzBuffer);
|
|
|
|
const result = await apkDatasource.getReleases({
|
|
packageName: 'minimal-package',
|
|
registryUrl: 'https://packages.wolfi.dev/os?arch=x86_64',
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
homepage: undefined,
|
|
registryUrl: 'https://packages.wolfi.dev/os?arch=x86_64',
|
|
releases: [
|
|
{
|
|
version: '1.0.0',
|
|
releaseTimestamp: undefined,
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('error handling', () => {
|
|
it('should throw ExternalHostError for 429', async () => {
|
|
httpMock
|
|
.scope('https://packages.wolfi.dev')
|
|
.get('/os/x86_64/APKINDEX.tar.gz')
|
|
.reply(429);
|
|
|
|
await expect(
|
|
apkDatasource.getReleases({
|
|
packageName: 'bash',
|
|
registryUrl: 'https://packages.wolfi.dev/os?arch=x86_64',
|
|
}),
|
|
).rejects.toThrow(EXTERNAL_HOST_ERROR);
|
|
});
|
|
|
|
it('should throw ExternalHostError for 5xx', async () => {
|
|
httpMock
|
|
.scope('https://packages.wolfi.dev')
|
|
.get('/os/x86_64/APKINDEX.tar.gz')
|
|
.reply(502);
|
|
|
|
await expect(
|
|
apkDatasource.getReleases({
|
|
packageName: 'bash',
|
|
registryUrl: 'https://packages.wolfi.dev/os?arch=x86_64',
|
|
}),
|
|
).rejects.toThrow(EXTERNAL_HOST_ERROR);
|
|
});
|
|
|
|
it('should return null for 404', async () => {
|
|
httpMock
|
|
.scope('https://packages.wolfi.dev')
|
|
.get('/os/x86_64/APKINDEX.tar.gz')
|
|
.reply(404);
|
|
|
|
const result = await apkDatasource.getReleases({
|
|
packageName: 'bash',
|
|
registryUrl: 'https://packages.wolfi.dev/os?arch=x86_64',
|
|
});
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should return null for 401', async () => {
|
|
httpMock
|
|
.scope('https://packages.wolfi.dev')
|
|
.get('/os/x86_64/APKINDEX.tar.gz')
|
|
.reply(401);
|
|
|
|
const result = await apkDatasource.getReleases({
|
|
packageName: 'bash',
|
|
registryUrl: 'https://packages.wolfi.dev/os?arch=x86_64',
|
|
});
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should throw ExternalHostError for 503', async () => {
|
|
httpMock
|
|
.scope('https://packages.wolfi.dev')
|
|
.get('/os/x86_64/APKINDEX.tar.gz')
|
|
.reply(503);
|
|
|
|
await expect(
|
|
apkDatasource.getReleases({
|
|
packageName: 'bash',
|
|
registryUrl: 'https://packages.wolfi.dev/os?arch=x86_64',
|
|
}),
|
|
).rejects.toThrow(EXTERNAL_HOST_ERROR);
|
|
});
|
|
|
|
it('should throw ExternalHostError for 500', async () => {
|
|
httpMock
|
|
.scope('https://packages.wolfi.dev')
|
|
.get('/os/x86_64/APKINDEX.tar.gz')
|
|
.reply(500);
|
|
|
|
await expect(
|
|
apkDatasource.getReleases({
|
|
packageName: 'bash',
|
|
registryUrl: 'https://packages.wolfi.dev/os?arch=x86_64',
|
|
}),
|
|
).rejects.toThrow(EXTERNAL_HOST_ERROR);
|
|
});
|
|
|
|
it('should handle extraction errors gracefully', async () => {
|
|
const apkDatasource = new ApkDatasource();
|
|
const tarGzBuffer = await createTarGz([
|
|
{ name: 'APKINDEX', content: 'P:bash\nV:5.0.0\n' },
|
|
]);
|
|
|
|
httpMock
|
|
.scope('https://packages.wolfi.dev')
|
|
.get('/os/x86_64/APKINDEX.tar.gz')
|
|
.reply(200, tarGzBuffer);
|
|
|
|
const parseSpy = vi
|
|
.spyOn(parser, 'parseApkIndexFile')
|
|
.mockRejectedValue(new Error('Parse failed'));
|
|
|
|
const result = await apkDatasource.getReleases({
|
|
packageName: 'bash',
|
|
registryUrl: 'https://packages.wolfi.dev/os?arch=x86_64',
|
|
});
|
|
|
|
expect(result).toBeNull();
|
|
parseSpy.mockRestore();
|
|
});
|
|
|
|
it('should handle invalid gzip data', async () => {
|
|
httpMock
|
|
.scope('https://packages.wolfi.dev')
|
|
.get('/os/x86_64/APKINDEX.tar.gz')
|
|
.reply(200, Buffer.from('not-gzip-data'));
|
|
|
|
const result = await new ApkDatasource().getReleases({
|
|
packageName: 'bash',
|
|
registryUrl: 'https://packages.wolfi.dev/os?arch=x86_64',
|
|
});
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should handle tar archive without APKINDEX file', async () => {
|
|
const tarGzBuffer = await createTarGz([
|
|
{ name: 'DESCRIPTION', content: 'not the index file' },
|
|
]);
|
|
|
|
httpMock
|
|
.scope('https://packages.wolfi.dev')
|
|
.get('/os/x86_64/APKINDEX.tar.gz')
|
|
.reply(200, tarGzBuffer);
|
|
|
|
const result = await new ApkDatasource().getReleases({
|
|
packageName: 'bash',
|
|
registryUrl: 'https://packages.wolfi.dev/os?arch=x86_64',
|
|
});
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should handle corrupt tar data', async () => {
|
|
const corruptTarGz = await gzip(Buffer.alloc(512, 0xff));
|
|
|
|
httpMock
|
|
.scope('https://packages.wolfi.dev')
|
|
.get('/os/x86_64/APKINDEX.tar.gz')
|
|
.reply(200, corruptTarGz);
|
|
|
|
const result = await new ApkDatasource().getReleases({
|
|
packageName: 'bash',
|
|
registryUrl: 'https://packages.wolfi.dev/os?arch=x86_64',
|
|
});
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
});
|