Files
renovate/lib/modules/datasource/deb/index.spec.ts
Felipe Santos 392105b7c6 chore(oxlint): enable unicorn plugin and prefer-node-protocol (#43283)
chore(oxlint): enable unicorn plugin and prefer-node-protocol
2026-05-29 06:55:58 +00:00

443 lines
13 KiB
TypeScript

import { createReadStream } from 'node:fs';
import { copyFile, stat } from 'fs-extra';
import type { DirectoryResult } from 'tmp-promise';
import { dir } from 'tmp-promise';
import upath from 'upath';
import { Fixtures } from '~test/fixtures.ts';
import * as httpMock from '~test/http-mock.ts';
import { fs } from '~test/util.ts';
import { GlobalConfig } from '../../../config/global.ts';
import { hashStream, toSha256 } from '../../../util/hash.ts';
import { getPkgReleases } from '../index.ts';
import type { GetPkgReleasesConfig } from '../types.ts';
import { cacheSubDir } from './common.ts';
import { DebDatasource } from './index.ts';
import {
getBaseSuiteUrl,
getComponentUrl,
getPackageUrl,
getRegistryUrl,
} from './url.ts';
vi.unmock('../../../util/mutex.ts');
const debBaseUrl = 'http://deb.debian.org';
describe('modules/datasource/deb/index', () => {
const fixturePackagesArchivePath = Fixtures.getPath(`Packages.gz`);
const fixturePackagesArchivePath2 = Fixtures.getPath(`Packages2.gz`);
const fixturePackagesPath = Fixtures.getPath(`Packages`);
let fixturePackagesArchiveHash: string;
let fixturePackagesArchiveHash2: string;
let debDatasource: DebDatasource;
let cacheDir: DirectoryResult | null;
let cfg: GetPkgReleasesConfig;
let extractionFolder: string;
let extractedPackageFile: string;
beforeEach(async () => {
debDatasource = new DebDatasource();
cacheDir = await dir({ unsafeCleanup: true });
GlobalConfig.set({ cacheDir: cacheDir.path });
extractionFolder = await fs.ensureCacheDir(cacheSubDir);
extractedPackageFile = upath.join(
extractionFolder,
`${toSha256(getComponentUrl(debBaseUrl, 'stable', 'non-free', 'amd64'))}.txt`,
);
cfg = {
datasource: 'deb',
packageName: 'album',
registryUrls: [
getRegistryUrl(debBaseUrl, 'stable', ['non-free'], 'amd64'),
],
};
fixturePackagesArchiveHash = await computeFileChecksum(
fixturePackagesArchivePath,
);
fixturePackagesArchiveHash2 = await computeFileChecksum(
fixturePackagesArchivePath2,
);
});
afterEach(async () => {
await cacheDir?.cleanup();
cacheDir = null;
});
describe('getReleases', () => {
it('returns a valid version for the package `album` and does not require redownload', async () => {
await copyFile(fixturePackagesPath, extractedPackageFile);
const stats = await stat(extractedPackageFile);
const ts = stats.ctime;
httpMock
.scope(debBaseUrl, {
// ensure the rest call sets the correct request headers
reqheaders: {
'if-modified-since': ts.toUTCString(),
},
})
.head(getPackageUrl('', 'stable', 'non-free', 'amd64'))
.reply(304);
const res = await getPkgReleases(cfg);
expect(res).toEqual({
homepage: 'http://marginalhacks.com/Hacks/album',
registryUrl:
'http://deb.debian.org/debian?suite=stable&components=non-free&binaryArch=amd64',
releases: [
{
version: '4.15-1',
},
],
});
});
describe('parsing of registry url', () => {
it('returns null when registry url misses components', async () => {
cfg.registryUrls = [
`${debBaseUrl}/debian?suite=stable&binaryArch=amd64`,
];
const res = await getPkgReleases(cfg);
expect(res).toBeNull();
});
it('returns null when registry url misses binaryArch', async () => {
cfg.registryUrls = [
`${debBaseUrl}/debian?suite=stable&components=non-free`,
];
const res = await getPkgReleases(cfg);
expect(res).toBeNull();
});
it('returns null when registry url misses suite or release', async () => {
cfg.registryUrls = [
`${debBaseUrl}/debian?components=non-free&binaryArch=amd64`,
];
const res = await getPkgReleases(cfg);
expect(res).toBeNull();
});
});
describe('without local version', () => {
beforeEach(() => {
mockHttpCalls(
'stable',
'non-free',
'amd64',
false,
fixturePackagesArchivePath,
fixturePackagesArchiveHash,
);
});
it('returns a valid version for the package `album`', async () => {
const res = await getPkgReleases(cfg);
expect(res).toEqual({
homepage: 'http://marginalhacks.com/Hacks/album',
registryUrl:
'http://deb.debian.org/debian?suite=stable&components=non-free&binaryArch=amd64',
releases: [
{
version: '4.15-1',
},
],
});
});
it('returns a valid version for the package `album` if release is used in the registryUrl', async () => {
cfg.registryUrls = [
getRegistryUrl(debBaseUrl, 'stable', ['non-free'], 'amd64'),
];
const res = await getPkgReleases(cfg);
expect(res).toEqual({
homepage: 'http://marginalhacks.com/Hacks/album',
registryUrl:
'http://deb.debian.org/debian?suite=stable&components=non-free&binaryArch=amd64',
releases: [
{
version: '4.15-1',
},
],
});
});
it('returns null for an unknown package', async () => {
cfg.packageName = 'you-will-never-find-me';
const res = await getPkgReleases(cfg);
expect(res).toBeNull();
});
describe('with two components', () => {
beforeEach(() => {
httpMock
.scope(debBaseUrl)
.get(getPackageUrl('', 'stable', 'non-free-second', 'amd64'))
.replyWithFile(200, fixturePackagesArchivePath2);
mockFetchInReleaseContent(
fixturePackagesArchiveHash2,
'stable',
'non-free-second',
'amd64',
);
cfg.registryUrls = [
getRegistryUrl(
debBaseUrl,
'stable',
['non-free', 'non-free-second'],
'amd64',
),
];
});
it('returns two releases for `album` which is the same across the components', async () => {
const res = await getPkgReleases(cfg);
expect(res).toEqual({
homepage: 'http://marginalhacks.com/Hacks/album',
registryUrl:
'http://deb.debian.org/debian?suite=stable&components=non-free,non-free-second&binaryArch=amd64',
releases: [
{
version: '4.14-1',
},
{
version: '4.15-1',
},
],
});
});
it('returns two releases for `album` which has different metadata across the components', async () => {
cfg.packageName = 'album';
const res = await getPkgReleases(cfg);
expect(res).toEqual({
homepage: 'http://marginalhacks.com/Hacks/album',
registryUrl:
'http://deb.debian.org/debian?suite=stable&components=non-free,non-free-second&binaryArch=amd64',
releases: [
{
version: '4.14-1',
},
{
version: '4.15-1',
},
],
});
});
});
});
describe('without server response', () => {
beforeEach(() => {
httpMock
.scope(debBaseUrl)
.get(getPackageUrl('', 'stable', 'non-free', 'amd64'))
.reply(404);
});
it('returns null for the package', async () => {
cfg.packageName = 'you-will-never-find-me';
const res = await getPkgReleases(cfg);
expect(res).toBeNull();
});
});
it('supports specifying a custom binary arch', async () => {
httpMock
.scope(debBaseUrl)
.get(getPackageUrl('', 'stable', 'non-free', 'riscv'))
.replyWithFile(200, fixturePackagesArchivePath);
mockFetchInReleaseContent(
fixturePackagesArchiveHash,
'stable',
'non-free',
'riscv',
);
cfg.registryUrls = [
getRegistryUrl(debBaseUrl, 'stable', ['non-free'], 'riscv'),
];
const res = await getPkgReleases(cfg);
expect(res).toEqual({
homepage: 'http://marginalhacks.com/Hacks/album',
registryUrl:
'http://deb.debian.org/debian?suite=stable&components=non-free&binaryArch=riscv',
releases: [
{
version: '4.15-1',
},
],
});
});
it('should not lead to a race condition on parallel lookups', async () => {
const packages = [
'album',
'album-data',
'alien-arena-data',
'amiwm',
'arb',
'arb-common',
'libfaac-dev',
'amoeba-data',
];
for (let i = 0; i < packages.length; i++) {
// first call doesn't include a http head call, since the file doesn't exists locally yet
// the package index is downloaded every time since the http head call returns 200
mockHttpCalls(
'stable',
'non-free',
'amd64',
!!i,
fixturePackagesArchivePath,
fixturePackagesArchiveHash,
);
}
const results = await Promise.all(
packages.map((packageName) => getPkgReleases({ ...cfg, packageName })),
);
for (const result of results) {
expect(result?.releases?.length).toBe(1);
}
});
});
describe('parseExtractedPackageIndex', () => {
it('should parse the extracted package', async () => {
await copyFile(fixturePackagesPath, extractedPackageFile);
const parsedPackages = await debDatasource.parseExtractedPackageIndex(
extractedPackageFile,
new Date(),
);
expect(parsedPackages).toEqual({
album: [
{
Homepage: 'http://marginalhacks.com/Hacks/album',
Package: 'album',
Version: '4.15-1',
},
],
'album-data': [
{
Homepage: 'http://marginalhacks.com/Hacks/album',
Package: 'album-data',
Version: '4.05-7.2',
},
{
Homepage: 'http://marginalhacks.com/Hacks/album',
Package: 'album-data',
Version: '4.05-7.3',
},
],
'alien-arena-data': [
{
Homepage: 'https://martianbackup.com',
Package: 'alien-arena-data',
Version: '7.71.3+ds-1',
},
],
});
});
});
});
/**
* Mocks several http calls for the in parallel lookup test
*
* - Mocks the http get call for the Package Index file
* - Mocks the http get call for the InRelease file
* - Mocks the http head call for Package Index file (returns 200)
*
* @param release - The release name (e.g., 'bullseye').
* @param component - The component name (e.g., 'main').
* @param arch - The architecture (e.g., 'amd64').
* @param checkIfModified - whether it should mock the http head call of the Package Index file
* @param packageArchivePath - path to package index
* @param packagesArchiveHash - sha256 hash of package
*/
function mockHttpCalls(
release: string,
component: string,
arch: string,
checkIfModified: boolean,
packageArchivePath: string,
packagesArchiveHash: string,
) {
httpMock
.scope(debBaseUrl)
.get(getPackageUrl('', release, component, arch))
.replyWithFile(200, packageArchivePath);
mockFetchInReleaseContent(packagesArchiveHash, release, component, arch);
if (checkIfModified) {
httpMock
.scope(debBaseUrl)
.head(getPackageUrl('', release, component, arch))
.reply(200);
}
}
/**
* Mocks the response for fetching the InRelease file content.
*
* This function sets up a mock HTTP response for a specific InRelease file request. The content includes a SHA256 checksum
* entry for a package index file. It allows simulating both successful and error responses.
*
* @param packageIndexHash - The SHA256 checksum hash of the package index file.
* @param release - The release name (e.g., 'bullseye').
* @param component - The component name (e.g., 'main').
* @param arch - The architecture (e.g., 'amd64').
* @param error - Optional flag to simulate an error response (default is false).
*/
export function mockFetchInReleaseContent(
packageIndexHash: string,
release: string,
component: string,
arch: string,
error = false,
) {
const packageIndexPath = `${component}/binary-${arch}/Packages.gz`;
const content = `SHA256:
3957f28db16e3f28c7b34ae84f1c929c567de6970f3f1b95dac9b498dd80fe63 738242 contrib/Contents-all
${packageIndexHash} 1234 ${packageIndexPath}
`;
const mockCall = httpMock
.scope(debBaseUrl)
.get(
`${getBaseSuiteUrl(getComponentUrl('', release, component, arch))}/InRelease`,
);
if (error) {
mockCall.replyWithError('Unexpected Error');
} else {
mockCall.reply(200, content);
}
}
/**
* Computes the SHA256 checksum of a specified file.
*
* @param filePath - path of the file
* @returns resolves to the SHA256 checksum
*/
export function computeFileChecksum(filePath: string): Promise<string> {
const stream = createReadStream(filePath);
return hashStream(stream, 'sha256');
}