mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-25 23:11:31 +00:00
71d4f95ecc
Co-Authored-By: Claude Opus 5.5 <michael.kriese+claude-code@mend.io> Co-Authored-By: Claude Sonnet 5 <michael.kriese+claude-code@mend.io>
1567 lines
50 KiB
TypeScript
1567 lines
50 KiB
TypeScript
import { isFunction } from '@sindresorhus/is';
|
|
import fs from 'fs-extra';
|
|
import { logger } from '~test/util.ts';
|
|
import { GlobalConfig } from '../../config/global.ts';
|
|
import {
|
|
EXTERNAL_HOST_ERROR,
|
|
HOST_BLOCKED,
|
|
HOST_DISABLED,
|
|
} from '../../constants/error-messages.ts';
|
|
import { ExternalHostError } from '../../types/errors/external-host-error.ts';
|
|
import type { HostRule } from '../../types/index.ts';
|
|
import * as memCache from '../../util/cache/memory/index.ts';
|
|
import * as _packageCache from '../../util/cache/package/index.ts';
|
|
import { sanitizeKey } from '../../util/cache/sanitize-key.ts';
|
|
import * as hostRules from '../../util/host-rules.ts';
|
|
import datasources from './api.ts';
|
|
import { getDefaultVersioning } from './common.ts';
|
|
import { Datasource } from './datasource.ts';
|
|
import {
|
|
getDatasourceList,
|
|
getDatasources,
|
|
getDefaultConfig,
|
|
getDigest,
|
|
getPkgReleases,
|
|
supportsDigests,
|
|
} from './index.ts';
|
|
import type {
|
|
DigestConfig,
|
|
GetPkgReleasesConfig,
|
|
GetReleasesConfig,
|
|
ReleaseResult,
|
|
} from './types.ts';
|
|
|
|
const datasource = 'dummy';
|
|
const packageName = 'package';
|
|
|
|
type RegistriesMock = Record<
|
|
string,
|
|
ReleaseResult | (() => ReleaseResult) | null
|
|
>;
|
|
const defaultRegistriesMock: RegistriesMock = {
|
|
'https://reg1.com': { releases: [{ version: '1.2.3' }] },
|
|
};
|
|
|
|
class DummyDatasource extends Datasource {
|
|
override defaultVersioning = 'python';
|
|
override getDefaultRegistryUrls(_packageName: string): string[] | undefined {
|
|
return ['https://reg1.com'];
|
|
}
|
|
private registriesMock: RegistriesMock;
|
|
|
|
constructor(registriesMock: RegistriesMock = defaultRegistriesMock) {
|
|
super(datasource);
|
|
this.registriesMock = registriesMock;
|
|
}
|
|
|
|
override getReleases({
|
|
registryUrl,
|
|
}: GetReleasesConfig): Promise<ReleaseResult | null> {
|
|
const fn = this.registriesMock[registryUrl!];
|
|
if (isFunction(fn)) {
|
|
return Promise.resolve(fn());
|
|
}
|
|
return Promise.resolve(fn ?? null);
|
|
}
|
|
}
|
|
|
|
class DummyDatasource2 extends Datasource {
|
|
override getDefaultRegistryUrls(_packageName: string) {
|
|
return ['https://reg1.com'];
|
|
}
|
|
private registriesMock: RegistriesMock;
|
|
|
|
constructor(registriesMock: RegistriesMock = defaultRegistriesMock) {
|
|
super(datasource);
|
|
this.registriesMock = registriesMock;
|
|
}
|
|
|
|
override getReleases({
|
|
registryUrl,
|
|
}: GetReleasesConfig): Promise<ReleaseResult | null> {
|
|
const fn = this.registriesMock[registryUrl!];
|
|
if (isFunction(fn)) {
|
|
return Promise.resolve(fn());
|
|
}
|
|
return Promise.resolve(fn ?? null);
|
|
}
|
|
}
|
|
|
|
class DummyDatasource3 extends Datasource {
|
|
override supportsCustomRegistry(_packageName: string) {
|
|
return false;
|
|
}
|
|
override getDefaultRegistryUrls(_packageName: string): string[] | undefined {
|
|
return ['https://reg1.com'];
|
|
}
|
|
private registriesMock: RegistriesMock;
|
|
|
|
constructor(registriesMock: RegistriesMock = defaultRegistriesMock) {
|
|
super(datasource);
|
|
this.registriesMock = registriesMock;
|
|
}
|
|
|
|
override getReleases({
|
|
registryUrl,
|
|
}: GetReleasesConfig): Promise<ReleaseResult | null> {
|
|
const fn = this.registriesMock[registryUrl!];
|
|
if (isFunction(fn)) {
|
|
return Promise.resolve(fn());
|
|
}
|
|
return Promise.resolve(fn ?? null);
|
|
}
|
|
}
|
|
|
|
class DummyDatasource4 extends DummyDatasource3 {
|
|
override getDefaultRegistryUrls(_packageName: string): string[] | undefined {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
vi.mock('./metadata-manual.ts', () => ({
|
|
manualChangelogUrls: {
|
|
dummy: {
|
|
package: 'https://foo.bar/package/CHANGELOG.md',
|
|
},
|
|
},
|
|
manualSourceUrls: {
|
|
dummy: {
|
|
package: 'https://foo.bar/package',
|
|
},
|
|
},
|
|
}));
|
|
|
|
vi.mock('../../util/cache/package/index.ts');
|
|
const packageCache = vi.mocked(_packageCache);
|
|
|
|
describe('modules/datasource/index', () => {
|
|
it('preserves representative function-only registry defaults', () => {
|
|
const expected = {
|
|
docker: ['https://index.docker.io'],
|
|
maven: ['https://repo.maven.apache.org/maven2'],
|
|
'dotnet-version': [
|
|
'https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/releases-index.json',
|
|
],
|
|
'python-version': ['https://www.python.org/api/v2/downloads/release'],
|
|
'terraform-provider': [
|
|
'https://registry.terraform.io',
|
|
'https://releases.hashicorp.com',
|
|
],
|
|
'bitbucket-tags': ['https://bitbucket.org'],
|
|
'gitea-tags': ['https://gitea.com'],
|
|
'gitea-releases': ['https://gitea.com'],
|
|
'forgejo-tags': ['https://code.forgejo.org'],
|
|
'forgejo-releases': ['https://code.forgejo.org'],
|
|
};
|
|
for (const [id, urls] of Object.entries(expected)) {
|
|
const definition = getDatasources().get(id);
|
|
expect(definition).toBeDefined();
|
|
expect(definition!.getDefaultRegistryUrls('example')).toEqual(urls);
|
|
}
|
|
});
|
|
|
|
it('selects Java registry defaults and custom support by package', () => {
|
|
const java = getDatasources().get('java-version')!;
|
|
expect(java.getDefaultRegistryUrls('java-jdk')).toEqual([
|
|
'https://api.adoptium.net/',
|
|
]);
|
|
expect(java.supportsCustomRegistry('java-jdk')).toBe(false);
|
|
expect(java.getDefaultRegistryUrls('oracle-graalvm-jdk')).toEqual([
|
|
'https://mise-java.jdx.dev/',
|
|
]);
|
|
expect(java.supportsCustomRegistry('oracle-graalvm-jdk')).toBe(true);
|
|
});
|
|
|
|
it('invokes accessors for every registered datasource', () => {
|
|
for (const [id, definition] of getDatasources()) {
|
|
const urls = definition.getDefaultRegistryUrls(id);
|
|
expect(urls === undefined || Array.isArray(urls)).toBe(true);
|
|
expect(typeof definition.supportsCustomRegistry(id)).toBe('boolean');
|
|
}
|
|
});
|
|
|
|
afterEach(() => {
|
|
datasources.delete(datasource);
|
|
});
|
|
|
|
describe('getDefaultVersioning()', () => {
|
|
it('returns semver if undefined', () => {
|
|
expect(getDefaultVersioning(undefined)).toBe('semver-coerced');
|
|
});
|
|
});
|
|
|
|
describe('getDefaultConfig()', () => {
|
|
it('returns the defaultConfig of a datasource', async () => {
|
|
class DummyDatasourceWithDefaultConfig extends DummyDatasource {
|
|
override defaultConfig = { commitMessageTopic: 'Dummy {{depName}}' };
|
|
}
|
|
datasources.set(datasource, new DummyDatasourceWithDefaultConfig());
|
|
|
|
await expect(getDefaultConfig(datasource)).resolves.toEqual({
|
|
commitMessageTopic: 'Dummy {{depName}}',
|
|
});
|
|
});
|
|
|
|
it('returns an empty object for a datasource without defaults', async () => {
|
|
datasources.set(datasource, new DummyDatasource());
|
|
|
|
await expect(getDefaultConfig(datasource)).resolves.toEqual({});
|
|
});
|
|
|
|
it('returns an empty object for an unknown datasource', async () => {
|
|
await expect(
|
|
getDefaultConfig('some-unknown-datasource'),
|
|
).resolves.toEqual({});
|
|
});
|
|
});
|
|
|
|
describe('Validations', () => {
|
|
it('returns datasources', async () => {
|
|
expect(getDatasources()).toBeDefined();
|
|
|
|
const managerList = (
|
|
await fs.readdir(import.meta.dirname, { withFileTypes: true })
|
|
)
|
|
.filter(
|
|
(dirent) => dirent.isDirectory() && !dirent.name.startsWith('_'),
|
|
)
|
|
.map((dirent) => dirent.name)
|
|
.sort();
|
|
expect(getDatasourceList()).toEqual(managerList);
|
|
});
|
|
|
|
it('has consistent metadata', () => {
|
|
// Fields whose contract (DatasourceApi) requires them to be instance
|
|
// fields. Declaring one as a class `static` only is invisible to the
|
|
// registry (registry-URL resolution, caching) and to the docs
|
|
// generator, both of which read instances.
|
|
const metadataNames = [
|
|
'customRegistrySupport',
|
|
'registryStrategy',
|
|
'defaultRegistryUrls',
|
|
'defaultVersioning',
|
|
'defaultConfig',
|
|
'releaseTimestampSupport',
|
|
'releaseTimestampNote',
|
|
'sourceUrlSupport',
|
|
'sourceUrlNote',
|
|
'caching',
|
|
];
|
|
|
|
for (const [name, ds] of getDatasources()) {
|
|
expect(ds.id).toBe(name);
|
|
expect(isFunction(ds.getReleases)).toBeTrue();
|
|
|
|
const ownInstanceProps = Object.getOwnPropertyNames(ds);
|
|
const ownStaticProps = Object.getOwnPropertyNames(ds.constructor);
|
|
for (const metadataName of metadataNames) {
|
|
const accessorName =
|
|
metadataName === 'defaultRegistryUrls'
|
|
? 'getDefaultRegistryUrls'
|
|
: undefined;
|
|
const isStaticOnly =
|
|
ownStaticProps.includes(metadataName) &&
|
|
!ownInstanceProps.includes(metadataName) &&
|
|
!(accessorName && isFunction(ds[accessorName]));
|
|
expect(isStaticOnly).toBeFalse();
|
|
}
|
|
|
|
expect(!ds.releaseTimestampNote || ds.releaseTimestampSupport).toBe(
|
|
true,
|
|
);
|
|
expect(!ds.sourceUrlNote || ds.sourceUrlSupport !== 'none').toBe(true);
|
|
}
|
|
});
|
|
|
|
it('returns null for null datasource', async () => {
|
|
await expect(
|
|
getPkgReleases({
|
|
datasource: null as never, // #22198
|
|
packageName: 'some/dep',
|
|
}),
|
|
).resolves.toBeNull();
|
|
});
|
|
|
|
it('returns null for no packageName', async () => {
|
|
datasources.set(datasource, new DummyDatasource());
|
|
await expect(
|
|
getPkgReleases({
|
|
datasource,
|
|
packageName: null as never, // #22198
|
|
}),
|
|
).resolves.toBeNull();
|
|
});
|
|
|
|
it('returns null for unknown datasource', async () => {
|
|
await expect(
|
|
getPkgReleases({
|
|
datasource: 'some-unknown-datasource',
|
|
packageName: 'some/dep',
|
|
}),
|
|
).resolves.toBeNull();
|
|
});
|
|
|
|
it('ignores and warns for disabled custom registryUrls', async () => {
|
|
class TestDatasource extends DummyDatasource {
|
|
override supportsCustomRegistry(_packageName: string) {
|
|
return false;
|
|
}
|
|
}
|
|
datasources.set(datasource, new TestDatasource());
|
|
const registryUrls = [
|
|
'https://foo.bar',
|
|
'https://user:s3cr3t@foo.bar/private',
|
|
'not a url',
|
|
];
|
|
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryUrls,
|
|
});
|
|
|
|
// logged without credentials
|
|
expect(logger.logger.warn).toHaveBeenCalledWith(
|
|
{
|
|
datasource: 'dummy',
|
|
registryUrls: [
|
|
'https://foo.bar',
|
|
'https://foo.bar/private',
|
|
'not a url',
|
|
],
|
|
defaultRegistryUrls: undefined,
|
|
},
|
|
'Custom registries are not allowed for this datasource and will be ignored',
|
|
);
|
|
expect(res).toMatchObject({ releases: [{ version: '1.2.3' }] });
|
|
});
|
|
|
|
it('skips empty entries when warning for disabled custom registryUrls', async () => {
|
|
class TestDatasource extends DummyDatasource {
|
|
override supportsCustomRegistry(_packageName: string) {
|
|
return false;
|
|
}
|
|
}
|
|
datasources.set(datasource, new TestDatasource());
|
|
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryUrls: [null as never, 'https://user:s3cr3t@foo.bar'],
|
|
defaultRegistryUrls: [undefined as never, 'https://foo.bar/default'],
|
|
additionalRegistryUrls: [null as never, 'https://foo.bar/additional'],
|
|
});
|
|
|
|
expect(logger.logger.warn).toHaveBeenCalledWith(
|
|
{
|
|
datasource: 'dummy',
|
|
registryUrls: ['https://foo.bar'],
|
|
defaultRegistryUrls: ['https://foo.bar/default'],
|
|
additionalRegistryUrls: ['https://foo.bar/additional'],
|
|
},
|
|
'Custom registries are not allowed for this datasource and will be ignored',
|
|
);
|
|
expect(res).toMatchObject({ releases: [{ version: '1.2.3' }] });
|
|
});
|
|
});
|
|
|
|
describe('Digest', () => {
|
|
it('returns if digests are supported', () => {
|
|
datasources.set(datasource, new DummyDatasource());
|
|
expect(supportsDigests(datasource)).toBeFalse();
|
|
});
|
|
|
|
it('returns value if defined', async () => {
|
|
class TestDatasource extends DummyDatasource {
|
|
override getDigest(): Promise<string> {
|
|
return Promise.resolve('123');
|
|
}
|
|
}
|
|
datasources.set(datasource, new TestDatasource());
|
|
|
|
expect(supportsDigests(datasource)).toBeTrue();
|
|
await expect(getDigest({ datasource, packageName })).resolves.toBe('123');
|
|
});
|
|
|
|
it('returns replacementName if defined', async () => {
|
|
class TestDatasource extends DummyDatasource {
|
|
override getDigest(
|
|
config: DigestConfig,
|
|
newValue?: string,
|
|
): Promise<string> {
|
|
return Promise.resolve(config.packageName);
|
|
}
|
|
}
|
|
datasources.set(datasource, new TestDatasource());
|
|
|
|
await expect(
|
|
getDigest({
|
|
datasource,
|
|
packageName: 'pkgName',
|
|
replacementName: 'replacement',
|
|
}),
|
|
).resolves.toBe('replacement');
|
|
});
|
|
});
|
|
|
|
describe('Metadata', () => {
|
|
beforeEach(() => {
|
|
datasources.set(datasource, new DummyDatasource());
|
|
});
|
|
|
|
it('adds changelogUrl', async () => {
|
|
await expect(
|
|
getPkgReleases({ datasource, packageName }),
|
|
).resolves.toMatchObject({
|
|
changelogUrl: 'https://foo.bar/package/CHANGELOG.md',
|
|
});
|
|
});
|
|
|
|
it('adds sourceUrl', async () => {
|
|
await expect(
|
|
getPkgReleases({ datasource, packageName }),
|
|
).resolves.toMatchObject({
|
|
sourceUrl: 'https://foo.bar/package',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Packages', () => {
|
|
describe('registryUrls with credentials', () => {
|
|
afterEach(() => {
|
|
hostRules.clear();
|
|
GlobalConfig.reset();
|
|
});
|
|
|
|
it('moves URL credentials into a host rule for the host type of the datasource', async () => {
|
|
const sbtPackage = datasources.get('sbt-package')!;
|
|
vi.spyOn(sbtPackage, 'getReleases').mockResolvedValueOnce({
|
|
releases: [{ version: '1.2.3' }],
|
|
});
|
|
|
|
await getPkgReleases({
|
|
datasource: 'sbt-package',
|
|
packageName: 'org.example:lib',
|
|
registryUrls: ['https://user:pass@repo.example.com/maven'],
|
|
});
|
|
|
|
expect(
|
|
hostRules.find({
|
|
hostType: 'sbt',
|
|
url: 'https://repo.example.com/maven/org',
|
|
}),
|
|
).toMatchObject({ username: 'user', password: 'pass' });
|
|
expect(
|
|
hostRules.find({
|
|
hostType: 'sbt-package',
|
|
url: 'https://repo.example.com/maven/org',
|
|
}),
|
|
).toEqual({});
|
|
});
|
|
|
|
it('moves URL credentials into a host rule before the lookup', async () => {
|
|
class CachingDatasource extends DummyDatasource {
|
|
override caching = true;
|
|
}
|
|
datasources.set(datasource, new CachingDatasource());
|
|
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryUrls: ['https://user:pass@reg1.com'],
|
|
});
|
|
|
|
expect(res).toMatchObject({
|
|
registryUrl: 'https://reg1.com',
|
|
releases: [{ version: '1.2.3' }],
|
|
});
|
|
expect(
|
|
hostRules.find({ hostType: datasource, url: 'https://reg1.com' }),
|
|
).toMatchObject({ username: 'user', password: 'pass' });
|
|
expect(
|
|
hostRules.find({ hostType: 'npm', url: 'https://reg1.com' }),
|
|
).toEqual({});
|
|
expect(hostRules.getAll()).toBeEmptyArray();
|
|
expect(packageCache.set).toHaveBeenCalledExactlyOnceWith(
|
|
'datasource-releases-dummy',
|
|
'https://reg1.com:package',
|
|
expect.any(Object),
|
|
15,
|
|
);
|
|
expect(sanitizeKey('https://reg1.com:package')).toMatch(
|
|
/^https:\/\/sha256-[\da-f]{16}@reg1\.com:package$/,
|
|
);
|
|
});
|
|
|
|
it('caches the release metadata URLs without URL credentials', async () => {
|
|
class CachingDatasource extends DummyDatasource {
|
|
override caching = true;
|
|
}
|
|
datasources.set(
|
|
datasource,
|
|
new CachingDatasource({
|
|
'https://reg1.com': {
|
|
sourceUrl: 'https://user:secret@git.example.com/org/repo',
|
|
homepage: 'https://user:secret@example.com/home',
|
|
changelogUrl: 'https://user:secret@example.com/CHANGELOG.md',
|
|
dependencyUrl: 'https://user:secret@example.com/dep',
|
|
releases: [
|
|
{
|
|
version: '1.2.3',
|
|
changelogUrl: 'https://user:secret@example.com/1.2.3.md',
|
|
sourceUrl: 'https://user:secret@git.example.com/org/fork',
|
|
downloadUrl: 'https://user:secret@example.com/1.2.3.tgz',
|
|
checksumUrl: 'https://user:secret@example.com/1.2.3.sha256',
|
|
registryUrl: 'https://user:secret@mirror.example.com',
|
|
},
|
|
{ version: '1.2.4' },
|
|
],
|
|
},
|
|
}),
|
|
);
|
|
// the cached value as it is at the time it is cached, before the lookup adds its metadata
|
|
let cached: unknown;
|
|
packageCache.set.mockImplementationOnce((_namespace, _key, value) => {
|
|
cached = structuredClone(value);
|
|
return Promise.resolve();
|
|
});
|
|
|
|
await getPkgReleases({ datasource, packageName });
|
|
|
|
expect(packageCache.set).toHaveBeenCalledOnce();
|
|
expect(cached).toEqual({
|
|
sourceUrl: 'https://git.example.com/org/repo',
|
|
homepage: 'https://example.com/home',
|
|
changelogUrl: 'https://example.com/CHANGELOG.md',
|
|
dependencyUrl: 'https://example.com/dep',
|
|
registryUrl: 'https://reg1.com',
|
|
releases: [
|
|
{
|
|
version: '1.2.3',
|
|
changelogUrl: 'https://example.com/1.2.3.md',
|
|
sourceUrl: 'https://git.example.com/org/fork',
|
|
downloadUrl: 'https://example.com/1.2.3.tgz',
|
|
checksumUrl: 'https://example.com/1.2.3.sha256',
|
|
registryUrl: 'https://mirror.example.com',
|
|
},
|
|
{ version: '1.2.4' },
|
|
],
|
|
});
|
|
});
|
|
|
|
it('returns an uncached private result without URL credentials', async () => {
|
|
class CachingDatasource extends DummyDatasource {
|
|
override caching = true;
|
|
}
|
|
datasources.set(
|
|
datasource,
|
|
new CachingDatasource({
|
|
'https://reg1.com': {
|
|
isPrivate: true,
|
|
homepage: 'https://user:secret@example.com/home',
|
|
releases: [
|
|
{
|
|
version: '1.2.3',
|
|
downloadUrl: 'https://user:secret@example.com/1.2.3.tgz',
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
);
|
|
|
|
const res = await getPkgReleases({ datasource, packageName });
|
|
|
|
expect(packageCache.set).not.toHaveBeenCalled();
|
|
expect(res).toMatchObject({
|
|
homepage: 'https://example.com/home',
|
|
releases: [
|
|
{
|
|
version: '1.2.3',
|
|
downloadUrl: 'https://example.com/1.2.3.tgz',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('returns the result of a lookup without registry URL without URL credentials', async () => {
|
|
class NoRegistryDatasource extends Datasource {
|
|
constructor() {
|
|
super(datasource);
|
|
}
|
|
|
|
override getReleases(): Promise<ReleaseResult | null> {
|
|
return Promise.resolve({
|
|
releases: [
|
|
{
|
|
version: '1.2.3',
|
|
sourceUrl: 'https://user:secret@git.example.com/org/repo',
|
|
changelogUrl: 'https://user:secret@example.com/1.2.3.md',
|
|
},
|
|
],
|
|
});
|
|
}
|
|
}
|
|
datasources.set(datasource, new NoRegistryDatasource());
|
|
|
|
const res = await getPkgReleases({ datasource, packageName });
|
|
|
|
expect(res).toMatchObject({
|
|
releases: [
|
|
{
|
|
version: '1.2.3',
|
|
sourceUrl: 'https://git.example.com/org/repo',
|
|
changelogUrl: 'https://example.com/1.2.3.md',
|
|
},
|
|
],
|
|
});
|
|
expect(res?.registryUrl).toBeUndefined();
|
|
});
|
|
|
|
it('uses a separate cache key per detached URL credentials', async () => {
|
|
GlobalConfig.set({ cachePrivatePackages: true });
|
|
datasources.set(datasource, new DummyDatasource());
|
|
|
|
hostRules.detachUrlCredentials('https://user:first@reg1.com/', [
|
|
datasource,
|
|
]);
|
|
await getPkgReleases({ datasource, packageName });
|
|
const first = sanitizeKey(packageCache.get.mock.calls[0][1]);
|
|
hostRules.clear();
|
|
hostRules.detachUrlCredentials('https://user:second@reg1.com/', [
|
|
datasource,
|
|
]);
|
|
await getPkgReleases({ datasource, packageName });
|
|
const second = sanitizeKey(packageCache.get.mock.calls[1][1]);
|
|
|
|
expect(first).toMatch(
|
|
/^https:\/\/sha256-[\da-f]{16}@reg1\.com:package$/,
|
|
);
|
|
expect(second).toMatch(
|
|
/^https:\/\/sha256-[\da-f]{16}@reg1\.com:package$/,
|
|
);
|
|
expect(first).not.toBe(second);
|
|
expect(`${first}${second}`).not.toContain('first');
|
|
expect(`${first}${second}`).not.toContain('second');
|
|
});
|
|
|
|
it('uses a separate cache key for credentials of registryUrls with a path', async () => {
|
|
GlobalConfig.set({ cachePrivatePackages: true });
|
|
datasources.set(
|
|
datasource,
|
|
new DummyDatasource({
|
|
'https://reg1.com/npm': { releases: [{ version: '1.2.3' }] },
|
|
}),
|
|
);
|
|
|
|
await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryUrls: ['https://user:pass@reg1.com/npm/'],
|
|
});
|
|
|
|
expect(packageCache.get).toHaveBeenCalledExactlyOnceWith(
|
|
'datasource-releases-dummy',
|
|
'https://reg1.com/npm:package',
|
|
);
|
|
expect(sanitizeKey('https://reg1.com/npm:package')).toMatch(
|
|
/^https:\/\/sha256-[\da-f]{16}@reg1\.com\/npm:package$/,
|
|
);
|
|
});
|
|
|
|
it.each`
|
|
rule | trusted
|
|
${undefined} | ${false}
|
|
${{ matchHost: 'reg1.com', token: 'admin' }} | ${true}
|
|
${{ matchHost: 'https://reg1.com', username: 'user', password: 'pass' }} | ${false}
|
|
`(
|
|
'keeps the cache key for host rule $rule (trusted: $trusted)',
|
|
async ({ rule, trusted }: { rule?: HostRule; trusted: boolean }) => {
|
|
GlobalConfig.set({ cachePrivatePackages: true });
|
|
datasources.set(datasource, new DummyDatasource());
|
|
if (rule) {
|
|
hostRules.add(rule, { trusted });
|
|
}
|
|
|
|
await getPkgReleases({ datasource, packageName });
|
|
|
|
expect(packageCache.get).toHaveBeenCalledExactlyOnceWith(
|
|
'datasource-releases-dummy',
|
|
'https://reg1.com:package',
|
|
);
|
|
},
|
|
);
|
|
|
|
it('keeps the credentials of a host rule scoped to the datasource', async () => {
|
|
hostRules.add(
|
|
{ hostType: datasource, matchHost: 'reg1.com', token: 'admin' },
|
|
{ trusted: true },
|
|
);
|
|
|
|
await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryUrls: ['https://user:pass@reg1.com'],
|
|
});
|
|
|
|
expect(hostRules.getAll()).toHaveLength(1);
|
|
expect(
|
|
hostRules.find({ hostType: datasource, url: 'https://reg1.com' }),
|
|
).toMatchObject({ token: 'admin' });
|
|
});
|
|
|
|
it('keeps the credentials of a host rule for the datasource without matchHost', async () => {
|
|
hostRules.add(
|
|
{ hostType: datasource, token: 'admin' },
|
|
{ trusted: true },
|
|
);
|
|
|
|
await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryUrls: ['https://user:pass@reg1.com'],
|
|
});
|
|
|
|
expect(hostRules.getAll()).toHaveLength(1);
|
|
expect(
|
|
hostRules.find({ hostType: datasource, url: 'https://reg1.com' }),
|
|
).toEqual({ token: 'admin' });
|
|
});
|
|
});
|
|
|
|
describe('registry caching', () => {
|
|
beforeEach(() => memCache.init());
|
|
afterEach(() => memCache.reset());
|
|
|
|
it.each(['defaultRegistryUrls', 'additionalRegistryUrls'])(
|
|
'keeps releases separate for different %s',
|
|
async (registryOption) => {
|
|
const firstRegistry = vi.fn(() => ({
|
|
releases: [{ version: '1.0.0' }],
|
|
}));
|
|
const secondRegistry = vi.fn(() => ({
|
|
releases: [{ version: '2.0.0' }],
|
|
}));
|
|
datasources.set(
|
|
datasource,
|
|
new DummyDatasource({
|
|
'https://reg2.com': firstRegistry,
|
|
'https://reg3.com': secondRegistry,
|
|
}),
|
|
);
|
|
const firstConfig = {
|
|
datasource,
|
|
packageName,
|
|
registryStrategy: 'merge',
|
|
[registryOption]: ['https://reg2.com'],
|
|
} satisfies GetPkgReleasesConfig;
|
|
const secondConfig = {
|
|
...firstConfig,
|
|
[registryOption]: ['https://reg3.com'],
|
|
};
|
|
|
|
const [first, second, repeated] = await Promise.all([
|
|
getPkgReleases(firstConfig),
|
|
getPkgReleases(secondConfig),
|
|
getPkgReleases(secondConfig),
|
|
]);
|
|
|
|
expect(first).toMatchObject({
|
|
releases: [{ version: '1.0.0' }],
|
|
registryUrl: 'https://reg2.com',
|
|
});
|
|
expect(second).toMatchObject({
|
|
releases: [{ version: '2.0.0' }],
|
|
registryUrl: 'https://reg3.com',
|
|
});
|
|
expect(repeated).toEqual(second);
|
|
expect(firstRegistry).toHaveBeenCalledTimes(1);
|
|
expect(secondRegistry).toHaveBeenCalledTimes(1);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('supports defaultRegistryUrls parameter', async () => {
|
|
const registries: RegistriesMock = {
|
|
'https://foo.bar': { releases: [{ version: '0.0.1' }] },
|
|
};
|
|
datasources.set(datasource, new DummyDatasource(registries));
|
|
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
defaultRegistryUrls: ['https://foo.bar'],
|
|
});
|
|
expect(res).toMatchObject({ releases: [{ version: '0.0.1' }] });
|
|
});
|
|
|
|
it('defaultRegistryUrls function works', async () => {
|
|
datasources.set(datasource, new DummyDatasource2());
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
});
|
|
expect(res).toMatchObject({
|
|
releases: [{ version: '1.2.3' }],
|
|
registryUrl: 'https://reg1.com',
|
|
});
|
|
});
|
|
|
|
it('defaultRegistryUrls function with customRegistrySupport works', async () => {
|
|
datasources.set(datasource, new DummyDatasource3());
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
});
|
|
expect(res).toMatchObject({
|
|
releases: [{ version: '1.2.3' }],
|
|
registryUrl: 'https://reg1.com',
|
|
});
|
|
});
|
|
|
|
it('supports datasource objects with function-only registry accessors', async () => {
|
|
datasources.set(datasource, {
|
|
id: datasource,
|
|
supportsCustomRegistry: () => true,
|
|
releaseTimestampSupport: false,
|
|
sourceUrlSupport: 'none',
|
|
registryStrategy: 'first',
|
|
getDefaultRegistryUrls: () => ['https://function-registry.com'],
|
|
getReleases: ({ registryUrl }) =>
|
|
Promise.resolve(
|
|
registryUrl === 'https://function-registry.com'
|
|
? { releases: [{ version: '2.0.0' }] }
|
|
: null,
|
|
),
|
|
postprocessRelease: (_config, release) => Promise.resolve(release),
|
|
});
|
|
const res = await getPkgReleases({ datasource, packageName });
|
|
expect(res).toMatchObject({ releases: [{ version: '2.0.0' }] });
|
|
});
|
|
|
|
it('handles datasource objects without default registries', async () => {
|
|
datasources.set(datasource, {
|
|
id: datasource,
|
|
supportsCustomRegistry: () => true,
|
|
releaseTimestampSupport: false,
|
|
sourceUrlSupport: 'none',
|
|
registryStrategy: 'first',
|
|
getDefaultRegistryUrls: () => undefined,
|
|
getReleases: vi.fn(),
|
|
postprocessRelease: (_config, release) => Promise.resolve(release),
|
|
});
|
|
await expect(
|
|
getPkgReleases({ datasource, packageName }),
|
|
).resolves.toBeNull();
|
|
});
|
|
|
|
// for coverage
|
|
it('undefined defaultRegistryUrls with customRegistrySupport works', async () => {
|
|
datasources.set(datasource, new DummyDatasource4());
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
});
|
|
expect(res).toBeNull();
|
|
});
|
|
|
|
it('applies extractVersion', async () => {
|
|
const registries: RegistriesMock = {
|
|
'https://reg1.com': {
|
|
releases: [{ version: 'v4.3.143' }, { version: 'rc4.3.143' }],
|
|
},
|
|
};
|
|
datasources.set(datasource, new DummyDatasource(registries));
|
|
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
extractVersion: '^(?<version>v\\d+\\.\\d+)',
|
|
versioning: 'loose',
|
|
});
|
|
expect(res).toMatchObject({ releases: [{ version: 'v4.3' }] });
|
|
});
|
|
|
|
it('trims sourceUrl', async () => {
|
|
datasources.set(
|
|
datasource,
|
|
new DummyDatasource({
|
|
'https://reg1.com': {
|
|
sourceUrl: ' https://abc.com ',
|
|
releases: [{ version: '1.0.0' }],
|
|
},
|
|
}),
|
|
);
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName: 'foobar',
|
|
});
|
|
expect(res).toMatchObject({ sourceUrl: 'https://abc.com' });
|
|
});
|
|
|
|
it('massages sourceUrl', async () => {
|
|
datasources.set(
|
|
datasource,
|
|
new DummyDatasource({
|
|
'https://reg1.com': {
|
|
sourceUrl: 'scm:git@github.com:Jasig/cas.git',
|
|
releases: [{ version: '1.0.0' }],
|
|
},
|
|
}),
|
|
);
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName: 'foobar',
|
|
});
|
|
expect(res).toMatchObject({ sourceUrl: 'https://github.com/Jasig/cas' });
|
|
});
|
|
|
|
it('applies replacements', async () => {
|
|
datasources.set(datasource, new DummyDatasource());
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
replacementName: 'def',
|
|
replacementVersion: '2.0.0',
|
|
});
|
|
expect(res).toMatchObject({
|
|
replacementName: 'def',
|
|
replacementVersion: '2.0.0',
|
|
});
|
|
});
|
|
|
|
describe('Registry strategies', () => {
|
|
describe('first', () => {
|
|
class FirstRegistryDatasource extends DummyDatasource {
|
|
override readonly registryStrategy = 'first';
|
|
}
|
|
|
|
it('returns value from single registry', async () => {
|
|
datasources.set(datasource, new FirstRegistryDatasource());
|
|
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryUrls: ['https://reg1.com'],
|
|
});
|
|
|
|
expect(res).toMatchObject({
|
|
releases: [{ version: '1.2.3' }],
|
|
registryUrl: 'https://reg1.com',
|
|
});
|
|
expect(logger.logger.warn).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('warns and returns first result', async () => {
|
|
const registries: RegistriesMock = {
|
|
'https://reg1.com': { releases: [{ version: '1.0.0' }] },
|
|
'https://reg2.com': { releases: [{ version: '2.0.0' }] },
|
|
'https://reg3.com': null,
|
|
};
|
|
const registryUrls = Object.keys(registries);
|
|
datasources.set(datasource, new FirstRegistryDatasource(registries));
|
|
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryUrls,
|
|
});
|
|
|
|
expect(res).toMatchObject({
|
|
releases: [{ version: '1.0.0' }],
|
|
registryUrl: 'https://reg1.com',
|
|
});
|
|
|
|
expect(logger.logger.warn).toHaveBeenCalledWith(
|
|
{
|
|
datasource: 'dummy',
|
|
packageName: 'package',
|
|
registryUrls,
|
|
},
|
|
'Excess registryUrls found for datasource lookup - using first configured only',
|
|
);
|
|
});
|
|
|
|
it('warns and returns first null', async () => {
|
|
const registries: RegistriesMock = {
|
|
'https://reg1.com': null,
|
|
'https://reg2.com': { releases: [{ version: '1.2.3' }] },
|
|
};
|
|
const registryUrls = Object.keys(registries);
|
|
datasources.set(datasource, new FirstRegistryDatasource(registries));
|
|
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryUrls,
|
|
});
|
|
|
|
expect(res).toBeNull();
|
|
|
|
expect(logger.logger.warn).toHaveBeenCalledWith(
|
|
{ datasource, packageName, registryUrls },
|
|
'Excess registryUrls found for datasource lookup - using first configured only',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('merge', () => {
|
|
class MergeRegistriesDatasource extends DummyDatasource {
|
|
override readonly registryStrategy = 'merge';
|
|
override caching = true;
|
|
override getDefaultRegistryUrls(_packageName: string) {
|
|
return ['https://reg1.com', 'https://reg2.com'];
|
|
}
|
|
}
|
|
|
|
const registries: RegistriesMock = {
|
|
'https://reg1.com': () => ({
|
|
releases: [{ version: '1.0.0' }],
|
|
tags: { release: '2.0.0' },
|
|
}),
|
|
'https://reg2.com': () => ({
|
|
releases: [{ version: '1.1.0' }],
|
|
tags: { latest: '1.1.0', release: '1.1.0' },
|
|
}),
|
|
'https://reg3.com': () => {
|
|
throw new ExternalHostError(new Error());
|
|
},
|
|
'https://reg4.com': () => {
|
|
throw new Error('a');
|
|
},
|
|
'https://reg5.com': () => {
|
|
throw new Error('b');
|
|
},
|
|
// for coverage
|
|
'https://reg6.com': null,
|
|
// has the same result as reg1 url, to test de-deplication of releases
|
|
'https://reg7.com': () => ({
|
|
releases: [{ version: '1.0.0' }],
|
|
tags: { latest: '1.2.0.0', release: '2.1.0' },
|
|
}),
|
|
};
|
|
|
|
beforeEach(() => {
|
|
datasources.set(
|
|
datasource,
|
|
new MergeRegistriesDatasource(registries),
|
|
);
|
|
});
|
|
|
|
it('merges custom defaultRegistryUrls and returns success', async () => {
|
|
const res = await getPkgReleases({ datasource, packageName });
|
|
|
|
expect(res).toMatchObject({
|
|
releases: [
|
|
{ registryUrl: 'https://reg1.com', version: '1.0.0' },
|
|
{ registryUrl: 'https://reg2.com', version: '1.1.0' },
|
|
],
|
|
tags: {
|
|
latest: '1.1.0',
|
|
release: '2.0.0',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('ignores custom defaultRegistryUrls if registryUrls are set', async () => {
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
defaultRegistryUrls: ['https://reg3.com'],
|
|
registryUrls: ['https://reg1.com', 'https://reg2.com'],
|
|
});
|
|
|
|
expect(res).toMatchObject({
|
|
releases: [
|
|
{ registryUrl: 'https://reg1.com', version: '1.0.0' },
|
|
{ registryUrl: 'https://reg2.com', version: '1.1.0' },
|
|
],
|
|
});
|
|
});
|
|
|
|
it('merges registries and returns success', async () => {
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryUrls: ['https://reg1.com', 'https://reg2.com'],
|
|
});
|
|
expect(res).toMatchObject({
|
|
releases: [
|
|
{ registryUrl: 'https://reg1.com', version: '1.0.0' },
|
|
{ registryUrl: 'https://reg2.com', version: '1.1.0' },
|
|
],
|
|
});
|
|
});
|
|
|
|
it('filters out duplicate releases', async () => {
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryUrls: ['https://reg1.com', 'https://reg7.com'],
|
|
});
|
|
expect(res).toMatchObject({
|
|
releases: [
|
|
{ registryUrl: 'https://reg1.com', version: '1.0.0' },
|
|
// { registryUrl: 'https://reg2.com', version: '1.0.0' },
|
|
],
|
|
tags: {
|
|
latest: '1.2.0.0',
|
|
release: '2.1.0',
|
|
},
|
|
});
|
|
});
|
|
|
|
describe('Cache', () => {
|
|
afterAll(() => {
|
|
GlobalConfig.reset();
|
|
});
|
|
|
|
class CachingDatasource extends DummyDatasource {
|
|
override caching = true;
|
|
}
|
|
|
|
it('returns cached result on cache hit', async () => {
|
|
const cachedResult: ReleaseResult = {
|
|
releases: [{ version: '0.0.1' }],
|
|
};
|
|
packageCache.get.mockResolvedValue(cachedResult);
|
|
datasources.set(datasource, new CachingDatasource());
|
|
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryUrls: ['https://reg1.com'],
|
|
});
|
|
expect(res).toMatchObject({ releases: [{ version: '0.0.1' }] });
|
|
expect(packageCache.set).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('caches by default', async () => {
|
|
const registries = {
|
|
'https://reg1.com': {
|
|
releases: [{ version: '0.0.1' }, { version: '0.0.2' }],
|
|
},
|
|
} satisfies RegistriesMock;
|
|
datasources.set(datasource, new CachingDatasource(registries));
|
|
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryUrls: ['https://reg1.com'],
|
|
});
|
|
expect(res).toMatchObject({
|
|
releases: [{ version: '0.0.1' }, { version: '0.0.2' }],
|
|
});
|
|
expect(packageCache.set).toHaveBeenCalledExactlyOnceWith(
|
|
'datasource-releases-dummy',
|
|
'https://reg1.com:package',
|
|
{
|
|
changelogUrl: 'https://foo.bar/package/CHANGELOG.md',
|
|
registryUrl: 'https://reg1.com',
|
|
releases: [{ version: '0.0.1' }, { version: '0.0.2' }],
|
|
sourceUrl: 'https://foo.bar/package',
|
|
},
|
|
15,
|
|
);
|
|
});
|
|
|
|
it('skips cache when isPrivate=true', async () => {
|
|
const registries = {
|
|
'https://reg1.com': {
|
|
isPrivate: true,
|
|
releases: [{ version: '0.0.1' }, { version: '0.0.2' }],
|
|
},
|
|
} satisfies RegistriesMock;
|
|
datasources.set(datasource, new CachingDatasource(registries));
|
|
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryUrls: ['https://reg1.com'],
|
|
});
|
|
expect(res).toMatchObject({
|
|
releases: [{ version: '0.0.1' }, { version: '0.0.2' }],
|
|
});
|
|
expect(packageCache.set).not.toHaveBeenCalledExactlyOnceWith();
|
|
});
|
|
|
|
it('forces cache via GlobalConfig', async () => {
|
|
GlobalConfig.set({ cachePrivatePackages: true });
|
|
const registries = {
|
|
'https://reg1.com': {
|
|
isPrivate: true,
|
|
releases: [{ version: '0.0.1' }, { version: '0.0.2' }],
|
|
},
|
|
} satisfies RegistriesMock;
|
|
datasources.set(datasource, new CachingDatasource(registries));
|
|
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryUrls: ['https://reg1.com'],
|
|
});
|
|
expect(res).toMatchObject({
|
|
releases: [{ version: '0.0.1' }, { version: '0.0.2' }],
|
|
});
|
|
expect(packageCache.set).toHaveBeenCalledExactlyOnceWith(
|
|
'datasource-releases-dummy',
|
|
expect.any(String),
|
|
expect.any(Object),
|
|
expect.any(Number),
|
|
);
|
|
});
|
|
|
|
it('does not cache a HOST_BLOCKED result', async () => {
|
|
const registries: RegistriesMock = {
|
|
'https://reg1.com': () => {
|
|
throw new ExternalHostError(new Error(HOST_BLOCKED));
|
|
},
|
|
};
|
|
datasources.set(datasource, new CachingDatasource(registries));
|
|
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryUrls: ['https://reg1.com'],
|
|
});
|
|
|
|
expect(res).toBeNull();
|
|
expect(packageCache.set).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
it('keeps merged results when a registry throws ExternalHostError', async () => {
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryUrls: [
|
|
'https://reg1.com',
|
|
'https://reg2.com',
|
|
'https://reg3.com',
|
|
],
|
|
});
|
|
expect(res).toMatchObject({
|
|
releases: [
|
|
{ registryUrl: 'https://reg1.com', version: '1.0.0' },
|
|
{ registryUrl: 'https://reg2.com', version: '1.1.0' },
|
|
],
|
|
});
|
|
});
|
|
|
|
it('returns results found after an ExternalHostError', async () => {
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryUrls: ['https://reg3.com', 'https://reg1.com'],
|
|
});
|
|
expect(res).toMatchObject({
|
|
registryUrl: 'https://reg1.com',
|
|
releases: [{ version: '1.0.0' }],
|
|
});
|
|
});
|
|
|
|
it('aborts on ExternalHostError when no registry returned releases', async () => {
|
|
await expect(
|
|
getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryUrls: ['https://reg3.com'],
|
|
}),
|
|
).rejects.toThrow(EXTERNAL_HOST_ERROR);
|
|
});
|
|
|
|
it('merges registries and returns null for error', async () => {
|
|
await expect(
|
|
getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryUrls: ['https://reg4.com', 'https://reg5.com'],
|
|
}),
|
|
).resolves.toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('hunt', () => {
|
|
class HuntRegistriyDatasource extends DummyDatasource {
|
|
override readonly registryStrategy = 'hunt';
|
|
}
|
|
|
|
it('returns first successful result', async () => {
|
|
const registries: RegistriesMock = {
|
|
'https://reg1.com': null,
|
|
'https://reg2.com': () => {
|
|
throw new Error('unknown');
|
|
},
|
|
'https://reg3.com': { releases: [{ version: '1.0.0' }] },
|
|
'https://reg4.com': { releases: [{ version: '2.0.0' }] },
|
|
'https://reg5.com': { releases: [{ version: '3.0.0' }] },
|
|
};
|
|
const registryUrls = Object.keys(registries);
|
|
datasources.set(datasource, new HuntRegistriyDatasource(registries));
|
|
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryUrls,
|
|
});
|
|
|
|
expect(res).toMatchObject({
|
|
registryUrl: 'https://reg3.com',
|
|
releases: [{ version: '1.0.0' }],
|
|
});
|
|
});
|
|
|
|
it('returns null for HOST_DISABLED', async () => {
|
|
const registries: RegistriesMock = {
|
|
'https://reg1.com': () => {
|
|
throw new ExternalHostError(new Error(HOST_DISABLED));
|
|
},
|
|
'https://reg2.com': { releases: [{ version: '1.0.0' }] },
|
|
};
|
|
const registryUrls = Object.keys(registries);
|
|
datasources.set(datasource, new HuntRegistriyDatasource(registries));
|
|
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryUrls,
|
|
});
|
|
|
|
expect(res).toBeNull();
|
|
});
|
|
|
|
it('returns null for HOST_BLOCKED', async () => {
|
|
const registries: RegistriesMock = {
|
|
'https://reg1.com': () => {
|
|
throw new ExternalHostError(new Error(HOST_BLOCKED));
|
|
},
|
|
'https://reg2.com': { releases: [{ version: '1.0.0' }] },
|
|
};
|
|
const registryUrls = Object.keys(registries);
|
|
datasources.set(datasource, new HuntRegistriyDatasource(registries));
|
|
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryUrls,
|
|
});
|
|
|
|
expect(res).toBeNull();
|
|
});
|
|
|
|
it('aborts on ExternalHostError', async () => {
|
|
const registries: RegistriesMock = {
|
|
'https://reg1.com': () => {
|
|
throw new ExternalHostError(new Error('something unknown'));
|
|
},
|
|
'https://reg2.com': { releases: [{ version: '1.0.0' }] },
|
|
};
|
|
const registryUrls = Object.keys(registries);
|
|
datasources.set(datasource, new HuntRegistriyDatasource(registries));
|
|
|
|
await expect(
|
|
getPkgReleases({ datasource, packageName, registryUrls }),
|
|
).rejects.toThrow(EXTERNAL_HOST_ERROR);
|
|
});
|
|
|
|
it('returns null if no releases are found', async () => {
|
|
const registries: RegistriesMock = {
|
|
'https://reg1.com': () => {
|
|
throw Object.assign(new Error(), { statusCode: '404' });
|
|
},
|
|
'https://reg2.com': () => {
|
|
throw Object.assign(new Error(), { statusCode: '401' });
|
|
},
|
|
'https://reg3.com': () => {
|
|
throw Object.assign(new Error(), { statusCode: '403' });
|
|
},
|
|
'https://reg4.com': () => {
|
|
throw new Error('b');
|
|
},
|
|
'https://reg5.com': () => {
|
|
throw Object.assign(new Error(), { code: '403' });
|
|
},
|
|
};
|
|
const registryUrls = Object.keys(registries);
|
|
datasources.set(datasource, new HuntRegistriyDatasource(registries));
|
|
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryUrls,
|
|
});
|
|
|
|
expect(res).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('relaseConstraintFiltering', () => {
|
|
it('keeps all releases by default', async () => {
|
|
const registries = {
|
|
'https://foo.bar': {
|
|
releases: [
|
|
{
|
|
version: '0.0.1',
|
|
constraints: {
|
|
python: ['2.7'],
|
|
},
|
|
},
|
|
{
|
|
version: '0.0.2',
|
|
},
|
|
],
|
|
},
|
|
} satisfies RegistriesMock;
|
|
datasources.set(datasource, new DummyDatasource(registries));
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
defaultRegistryUrls: ['https://foo.bar'],
|
|
});
|
|
expect(res).toMatchObject({
|
|
releases: [{ version: '0.0.1' }, { version: '0.0.2' }],
|
|
});
|
|
});
|
|
|
|
it('keeps all releases if constraints is set but no value defined for constraintsFiltering', async () => {
|
|
const registries = {
|
|
'https://foo.bar': {
|
|
releases: [
|
|
{
|
|
version: '0.0.1',
|
|
constraints: {
|
|
python: ['2.7'],
|
|
},
|
|
},
|
|
{
|
|
version: '0.0.2',
|
|
},
|
|
],
|
|
},
|
|
} satisfies RegistriesMock;
|
|
datasources.set(datasource, new DummyDatasource(registries));
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
defaultRegistryUrls: ['https://foo.bar'],
|
|
constraints: {
|
|
python: '2.7.0',
|
|
},
|
|
});
|
|
expect(res).toMatchObject({
|
|
releases: [{ version: '0.0.1' }, { version: '0.0.2' }],
|
|
});
|
|
});
|
|
|
|
it('filters releases if value is strict', async () => {
|
|
const registries = {
|
|
'https://foo.bar': {
|
|
releases: [
|
|
{
|
|
version: '0.0.5',
|
|
constraints: {
|
|
python: ['>= 3.0.0, < 4.0'],
|
|
},
|
|
},
|
|
{
|
|
version: '0.0.4',
|
|
constraints: {
|
|
python: ['>= 2.7, < 4.0'],
|
|
},
|
|
},
|
|
{
|
|
version: '0.0.3',
|
|
constraints: {
|
|
python: ['>= 2.7, < 3.0'],
|
|
},
|
|
},
|
|
{
|
|
version: '0.0.2',
|
|
constraints: {
|
|
python: ['2.7'],
|
|
},
|
|
},
|
|
{
|
|
version: '0.0.1',
|
|
constraints: {
|
|
python: ['1.0'],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
} satisfies RegistriesMock;
|
|
datasources.set(datasource, new DummyDatasource(registries));
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
defaultRegistryUrls: ['https://foo.bar'],
|
|
constraints: { python: '>= 2.7, < 3.0' },
|
|
constraintsFiltering: 'strict',
|
|
});
|
|
expect(res).toMatchObject({
|
|
releases: [
|
|
{ version: '0.0.2' },
|
|
{ version: '0.0.3' },
|
|
{ version: '0.0.4' },
|
|
],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('overruled by package config', () => {
|
|
beforeEach(() => {
|
|
datasources.set(
|
|
datasource,
|
|
new DummyDatasource({
|
|
'https://reg1.com': { releases: [{ version: '0.0.1' }] },
|
|
'https://reg2.com': { releases: [{ version: '0.0.2' }] },
|
|
'https://reg3.com': { releases: [{ version: '0.0.3' }] },
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('first', async () => {
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryStrategy: 'first',
|
|
defaultRegistryUrls: ['https://reg1.com', 'https://reg2.com'],
|
|
});
|
|
expect(res).toMatchObject({
|
|
releases: [{ version: '0.0.1' }],
|
|
});
|
|
});
|
|
it('hunt', async () => {
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryStrategy: 'hunt',
|
|
defaultRegistryUrls: ['https://foo.bar', 'https://reg1.com'],
|
|
});
|
|
expect(res).toMatchObject({
|
|
releases: [{ version: '0.0.1' }],
|
|
});
|
|
});
|
|
it('merge', async () => {
|
|
const res = await getPkgReleases({
|
|
datasource,
|
|
packageName,
|
|
registryStrategy: 'merge',
|
|
defaultRegistryUrls: [
|
|
'https://reg1.com',
|
|
'https://reg2.com',
|
|
'https://reg3.com',
|
|
],
|
|
});
|
|
expect(res).toMatchObject({
|
|
releases: [
|
|
{ version: '0.0.1' },
|
|
{ version: '0.0.2' },
|
|
{ version: '0.0.3' },
|
|
],
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|