0
0
mirror of https://github.com/renovatebot/renovate.git synced 2024-12-22 13:38:32 +00:00
renovatebot_renovate/lib/modules/datasource/maven/index.spec.ts

835 lines
23 KiB
TypeScript

import { HeadObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { mockClient } from 'aws-sdk-client-mock';
import { GoogleAuth as _googleAuth } from 'google-auth-library';
import { DateTime } from 'luxon';
import type { Release, ReleaseResult } from '..';
import { getPkgReleases } from '..';
import { Fixtures } from '../../../../test/fixtures';
import * as httpMock from '../../../../test/http-mock';
import { mocked } from '../../../../test/util';
import { EXTERNAL_HOST_ERROR } from '../../../constants/error-messages';
import * as hostRules from '../../../util/host-rules';
import { id as versioning } from '../../versioning/maven';
import { postprocessRelease } from '../postprocess-release';
import { MAVEN_REPO } from './common';
import { MavenDatasource } from '.';
const googleAuth = mocked(_googleAuth);
jest.mock('google-auth-library');
const datasource = MavenDatasource.id;
const baseUrl = 'https://repo.maven.apache.org/maven2';
const baseUrlCustom = 'https://custom.registry.renovatebot.com';
const arRegistry = 'maven.pkg.dev/some-project/some-repository';
const baseUrlAR = `artifactregistry://${arRegistry}`;
const baseUrlARHttps = `https://${arRegistry}`;
interface SnapshotOpts {
version: string;
meta?: string;
}
interface MockOpts {
dep?: string;
base?: string;
meta?: string | null;
pom?: string | null;
latest?: string;
snapshots?: SnapshotOpts[] | null;
}
function mockGenericPackage(opts: MockOpts = {}) {
const {
dep = 'org.example:package',
base = baseUrl,
latest = '2.0.0',
snapshots,
} = opts;
const meta =
opts.meta === undefined ? Fixtures.get('metadata.xml') : opts.meta;
const pom = opts.pom === undefined ? Fixtures.get('pom.xml') : opts.pom;
const scope = httpMock.scope(base);
const [group, artifact] = dep.split(':');
const packagePath = `${group.replace(/\./g, '/')}/${artifact}`;
if (meta) {
scope.get(`/${packagePath}/maven-metadata.xml`).reply(200, meta);
}
if (pom) {
if (latest.endsWith('-SNAPSHOT')) {
const [major, minor, patch] = latest
.replace('-SNAPSHOT', '')
.split('.')
.map((x) => parseInt(x, 10))
.map((x) => (x < 10 ? `0${x}` : `${x}`));
scope
.get(
`/${packagePath}/${latest}/${artifact}-${latest.replace(
'-SNAPSHOT',
'',
)}-20200101.${major}${minor}${patch}-${parseInt(patch, 10)}.pom`,
)
.reply(200, pom);
} else {
scope
.get(`/${packagePath}/${latest}/${artifact}-${latest}.pom`)
.reply(200, pom);
}
}
if (snapshots) {
snapshots.forEach((snapshot) => {
if (snapshot.meta) {
scope
.get(`/${packagePath}/${snapshot.version}/maven-metadata.xml`)
.reply(200, snapshot.meta);
} else {
scope
.get(`/${packagePath}/${snapshot.version}/maven-metadata.xml`)
.reply(404, '');
}
});
}
}
function get(
packageName = 'org.example:package',
...registryUrls: string[]
): Promise<ReleaseResult | null> {
const conf = { versioning, datasource, packageName };
return getPkgReleases(registryUrls ? { ...conf, registryUrls } : conf);
}
describe('modules/datasource/maven/index', () => {
beforeEach(() => {
hostRules.add({
hostType: datasource,
matchHost: 'custom.registry.renovatebot.com',
token: '123test',
});
});
afterEach(() => {
hostRules.clear();
});
it('returns null when metadata is not found', async () => {
httpMock
.scope(baseUrl)
.get('/org/example/package/maven-metadata.xml')
.reply(404);
const res = await get();
expect(res).toBeNull();
});
it('returns releases', async () => {
mockGenericPackage();
const res = await get();
expect(res).toMatchSnapshot();
});
it('returns releases when only snapshot', async () => {
const meta = Fixtures.get('metadata-snapshot-version.xml');
mockGenericPackage({
meta: Fixtures.get('metadata-snapshot-only.xml'),
latest: '1.0.3-SNAPSHOT',
snapshots: [
{
version: '1.0.3-SNAPSHOT',
meta,
},
],
});
const res = await get();
expect(res).toEqual({
display: 'org.example:package',
group: 'org.example',
homepage: 'https://package.example.org/about',
name: 'package',
packageScope: 'org.example',
registryUrl: 'https://repo.maven.apache.org/maven2',
releases: [{ version: '1.0.3-SNAPSHOT' }],
});
});
it('handles invalid snapshot', async () => {
const meta = Fixtures.get('metadata-snapshot-version-invalid.xml');
httpMock
.scope(MAVEN_REPO)
.get('/org/example/package/1.0.3-SNAPSHOT/package-1.0.3-SNAPSHOT.pom')
.reply(200, meta);
mockGenericPackage({
meta: Fixtures.get('metadata-snapshot-only.xml'),
pom: null,
latest: '1.0.3-SNAPSHOT',
snapshots: [
{
version: '1.0.3-SNAPSHOT',
meta,
},
],
});
const res = await get();
expect(res).toEqual({
display: 'org.example:package',
group: 'org.example',
name: 'package',
packageScope: 'org.example',
registryUrl: 'https://repo.maven.apache.org/maven2',
releases: [{ version: '1.0.3-SNAPSHOT' }],
});
});
it('returns releases from custom repository', async () => {
mockGenericPackage({ base: baseUrlCustom });
const res = await get('org.example:package', baseUrlCustom);
expect(res).toMatchSnapshot();
});
it('falls back to next registry url', async () => {
mockGenericPackage();
httpMock
.scope('https://failed_repo')
.get('/org/example/package/maven-metadata.xml')
.reply(404);
httpMock
.scope('https://unauthorized_repo')
.get('/org/example/package/maven-metadata.xml')
.reply(403);
httpMock
.scope('https://empty_repo')
.get('/org/example/package/maven-metadata.xml')
.reply(200, 'non-sense');
httpMock
.scope('https://unknown_error')
.get('/org/example/package/maven-metadata.xml')
.replyWithError('unknown');
const res = await get(
'org.example:package',
'https://failed_repo/',
'https://unauthorized_repo/',
'https://empty_repo',
'https://unknown_error',
baseUrl,
);
expect(res).toMatchSnapshot();
});
it('throws EXTERNAL_HOST_ERROR for 50x', async () => {
httpMock
.scope(baseUrl)
.get('/org/example/package/maven-metadata.xml')
.reply(503);
await expect(get()).rejects.toThrow(EXTERNAL_HOST_ERROR);
});
it('ignores unsupported protocols', async () => {
const base = baseUrl.replace('https', 'http');
mockGenericPackage({ base });
const res = await get(
'org.example:package',
'ftp://protocol_error_repo',
base,
);
expect(res?.releases).toMatchSnapshot();
});
it('skips registry with invalid metadata structure', async () => {
mockGenericPackage();
httpMock
.scope('https://invalid_metadata_repo')
.get('/org/example/package/maven-metadata.xml')
.reply(200, Fixtures.get('metadata-invalid.xml'));
const res = await get(
'org.example:package',
'https://invalid_metadata_repo',
baseUrl,
);
expect(res).toMatchSnapshot();
});
it('skips registry with invalid XML', async () => {
mockGenericPackage();
httpMock
.scope('https://invalid_metadata_repo')
.get('/org/example/package/maven-metadata.xml')
.reply(200, '###');
const res = await get(
'org.example:package',
'https://invalid_metadata_repo',
baseUrl,
);
expect(res).toMatchSnapshot();
});
it('handles optional slash at the end of registry url', async () => {
mockGenericPackage();
const resA = await get('org.example:package', baseUrl.replace(/\/+$/, ''));
mockGenericPackage();
const resB = await get('org.example:package', baseUrl.replace(/\/*$/, '/'));
expect(resA).not.toBeNull();
expect(resB).not.toBeNull();
expect(resA?.releases).toEqual(resB?.releases);
});
it('returns null for invalid registryUrls', async () => {
const res = await get(
'org.example:package',
'${project.baseUri}../../repository/',
);
expect(res).toBeNull();
});
it('supports scm.url values prefixed with "scm:"', async () => {
const pom = Fixtures.get('pom.scm-prefix.xml');
mockGenericPackage({ pom });
const res = await get();
expect(res?.sourceUrl).toBe('https://github.com/example/test');
});
it('removes authentication header after redirect', async () => {
const frontendHost = 'frontend_for_private_s3_repository';
const frontendUrl = `https://${frontendHost}/maven2`;
const backendUrl = 'https://private_s3_repository/maven2';
const metadataPath = '/org/example/package/maven-metadata.xml';
const pomfilePath = '/org/example/package/2.0.0/package-2.0.0.pom';
const queryStr = '?X-Amz-Algorithm=AWS4-HMAC-SHA256';
hostRules.add({
hostType: datasource,
matchHost: frontendHost,
username: 'username',
password: 'password',
timeout: 20000,
});
httpMock
.scope(frontendUrl)
.get(metadataPath)
.basicAuth({ user: 'username', pass: 'password' })
.reply(302, '', {
Location: `${backendUrl}${metadataPath}${queryStr}`,
})
.get(pomfilePath)
.basicAuth({ user: 'username', pass: 'password' })
.reply(302, '', {
Location: `${backendUrl}${pomfilePath}${queryStr}`,
});
httpMock
.scope(backendUrl, { badheaders: ['authorization'] })
.get(`${metadataPath}${queryStr}`)
.reply(200, Fixtures.get('metadata.xml'))
.get(`${pomfilePath}${queryStr}`)
.reply(200, Fixtures.get('pom.xml'));
const res = await get('org.example:package', frontendUrl);
expect(res).toMatchSnapshot();
});
it('supports artifactregistry urls with auth', async () => {
const pomfilePath = '/org/example/package/2.0.0/package-2.0.0.pom';
hostRules.clear();
httpMock
.scope(baseUrlARHttps)
.get('/org/example/package/maven-metadata.xml')
.matchHeader(
'authorization',
'Basic b2F1dGgyYWNjZXNzdG9rZW46c29tZS10b2tlbg==',
)
.reply(200, Fixtures.get('metadata.xml'));
httpMock
.scope(baseUrlARHttps)
.get(pomfilePath)
.matchHeader(
'authorization',
'Basic b2F1dGgyYWNjZXNzdG9rZW46c29tZS10b2tlbg==',
)
.reply(200, Fixtures.get('pom.xml'));
googleAuth.mockImplementation(
jest.fn().mockImplementation(() => ({
getAccessToken: jest.fn().mockResolvedValue('some-token'),
})),
);
const res = await get('org.example:package', baseUrlAR);
expect(res).toEqual({
display: 'org.example:package',
group: 'org.example',
homepage: 'https://package.example.org/about',
name: 'package',
packageScope: 'org.example',
registryUrl:
'artifactregistry://maven.pkg.dev/some-project/some-repository',
releases: [
{ version: '0.0.1' },
{ version: '1.0.0' },
{ version: '1.0.1' },
{ version: '1.0.2' },
{ version: '1.0.3-SNAPSHOT' },
{ version: '1.0.4-SNAPSHOT' },
{ version: '1.0.5-SNAPSHOT' },
{ version: '2.0.0' },
],
isPrivate: true,
});
expect(googleAuth).toHaveBeenCalledTimes(2);
});
it('supports artifactregistry urls without auth', async () => {
const pomfilePath = '/org/example/package/2.0.0/package-2.0.0.pom';
hostRules.clear();
httpMock
.scope(baseUrlARHttps)
.get('/org/example/package/maven-metadata.xml')
.reply(200, Fixtures.get('metadata.xml'));
httpMock
.scope(baseUrlARHttps)
.get(pomfilePath)
.reply(200, Fixtures.get('pom.xml'));
googleAuth.mockImplementation(
jest.fn().mockImplementation(() => ({
getAccessToken: jest.fn().mockResolvedValue(undefined),
})),
);
const res = await get('org.example:package', baseUrlAR);
expect(res).toEqual({
display: 'org.example:package',
group: 'org.example',
homepage: 'https://package.example.org/about',
name: 'package',
packageScope: 'org.example',
registryUrl:
'artifactregistry://maven.pkg.dev/some-project/some-repository',
releases: [
{ version: '0.0.1' },
{ version: '1.0.0' },
{ version: '1.0.1' },
{ version: '1.0.2' },
{ version: '1.0.3-SNAPSHOT' },
{ version: '1.0.4-SNAPSHOT' },
{ version: '1.0.5-SNAPSHOT' },
{ version: '2.0.0' },
],
isPrivate: true,
});
expect(googleAuth).toHaveBeenCalledTimes(2);
});
describe('fetching parent info', () => {
const parentPackage = {
dep: 'org.example:parent',
meta: null,
pom: Fixtures.get('parent-scm-homepage/pom.xml'),
latest: '1.0.0',
};
it('should get source and homepage from parent', async () => {
mockGenericPackage({
meta: Fixtures.get('child-no-info/meta.xml'),
pom: Fixtures.get('child-no-info/pom.xml'),
latest: '2.0.0',
});
mockGenericPackage(parentPackage);
const res = await get();
expect(res).toMatchObject({
sourceUrl: 'https://github.com/parent-scm/parent',
homepage: 'https://parent-home.example.com',
});
});
it('should deal with missing parent fields', async () => {
mockGenericPackage({
meta: Fixtures.get('child-empty/meta.xml'),
pom: Fixtures.get('child-empty/pom.xml'),
latest: '2.0.0',
});
const res = await get();
expect(res).toMatchObject({
display: 'org.example:package',
group: 'org.example',
name: 'package',
});
expect(res).not.toHaveProperty('homepage');
expect(res).not.toHaveProperty('sourceUrl');
});
it('should deal with circular hierarchy', async () => {
const parentPom = Fixtures.get('child-parent-cycle/parent.pom.xml');
const parentPomMock = {
dep: 'org.example:parent',
meta: null,
pom: parentPom,
latest: '2.0.0',
};
const childMeta = Fixtures.get('child-parent-cycle/child.meta.xml');
const childPom = Fixtures.get('child-parent-cycle/child.pom.xml');
const childPomMock = {
dep: 'org.example:child',
meta: null,
pom: childPom,
latest: '2.0.0',
};
mockGenericPackage({
...childPomMock,
meta: childMeta,
});
mockGenericPackage(parentPomMock);
mockGenericPackage(childPomMock);
mockGenericPackage(parentPomMock);
mockGenericPackage(childPomMock);
mockGenericPackage(parentPomMock);
const res = await get('org.example:child');
expect(res).toMatchObject({
homepage: 'https://parent-home.example.com',
});
});
it('should get source from own pom and homepage from parent', async () => {
mockGenericPackage({
meta: Fixtures.get('child-scm/meta.xml'),
pom: Fixtures.get('child-scm/pom.xml'),
latest: '2.0.0',
});
mockGenericPackage(parentPackage);
const res = await get();
expect(res).toMatchObject({
sourceUrl: 'https://github.com/child-scm/child',
homepage: 'https://parent-home.example.com',
});
});
it('should get homepage from own pom and source from parent', async () => {
mockGenericPackage({
meta: Fixtures.get('child-url/meta.xml'),
pom: Fixtures.get('child-url/pom.xml'),
latest: '2.0.0',
});
mockGenericPackage(parentPackage);
const res = await get();
expect(res).toMatchObject({
sourceUrl: 'https://github.com/parent-scm/parent',
homepage: 'https://child-home.example.com',
});
});
it('should get homepage and source from own pom', async () => {
mockGenericPackage({
meta: Fixtures.get('child-all-info/meta.xml'),
pom: Fixtures.get('child-all-info/pom.xml'),
latest: '2.0.0',
});
const res = await get();
expect(res).toMatchObject({
sourceUrl: 'https://github.com/child-scm/child',
homepage: 'https://child-home.example.com',
});
});
it('should be able to detect git@github.com:child-scm as valid sourceUrl', async () => {
mockGenericPackage({
meta: Fixtures.get('child-scm-gitatcolon/meta.xml'),
pom: Fixtures.get('child-scm-gitatcolon/pom.xml'),
latest: '2.0.0',
});
const res = await get();
expect(res).toMatchObject({
sourceUrl: 'https://github.com/child-scm/child',
});
});
it('should be able to detect git@github.com/child-scm as valid sourceUrl', async () => {
mockGenericPackage({
meta: Fixtures.get('child-scm-gitatslash/meta.xml'),
pom: Fixtures.get('child-scm-gitatslash/pom.xml'),
latest: '2.0.0',
});
const res = await get();
expect(res).toMatchObject({
sourceUrl: 'https://github.com/child-scm/child',
});
});
it('should be able to detect git://@github.com/child-scm as valid sourceUrl', async () => {
mockGenericPackage({
meta: Fixtures.get('child-scm-gitprotocol/meta.xml'),
pom: Fixtures.get('child-scm-gitprotocol/pom.xml'),
latest: '2.0.0',
});
const res = await get();
expect(res).toMatchObject({
sourceUrl: 'https://github.com/child-scm/child',
});
});
});
describe('post-fetch release validation', () => {
it('returns null for 404', async () => {
httpMock
.scope(MAVEN_REPO)
.head('/foo/bar/1.2.3/bar-1.2.3.pom')
.reply(404);
const res = await postprocessRelease(
{ datasource, packageName: 'foo:bar', registryUrl: MAVEN_REPO },
{ version: '1.2.3' },
);
expect(res).toBeNull();
});
it('returns null for unknown error', async () => {
httpMock
.scope(MAVEN_REPO)
.head('/foo/bar/1.2.3/bar-1.2.3.pom')
.replyWithError('unknown error');
const res = await postprocessRelease(
{ datasource, packageName: 'foo:bar', registryUrl: MAVEN_REPO },
{ version: '1.2.3' },
);
expect(res).toBeNull();
});
it('returns original value for 200 response', async () => {
httpMock
.scope(MAVEN_REPO)
.head('/foo/bar/1.2.3/bar-1.2.3.pom')
.reply(200);
const releaseOrig: Release = { version: '1.2.3' };
const res = await postprocessRelease(
{ datasource, packageName: 'foo:bar', registryUrl: MAVEN_REPO },
releaseOrig,
);
expect(res).toBe(releaseOrig);
});
it('returns original value for 200 response with versionOrig', async () => {
httpMock
.scope(MAVEN_REPO)
.head('/foo/bar/1.2.3/bar-1.2.3.pom')
.reply(200);
const releaseOrig: Release = { version: '1.2', versionOrig: '1.2.3' };
const res = await postprocessRelease(
{ datasource, packageName: 'foo:bar', registryUrl: MAVEN_REPO },
releaseOrig,
);
expect(res).toBe(releaseOrig);
});
it('returns original value for invalid configs', async () => {
const releaseOrig: Release = { version: '1.2.3' };
expect(
await postprocessRelease(
{ datasource, registryUrl: MAVEN_REPO },
releaseOrig,
),
).toBe(releaseOrig);
expect(
await postprocessRelease(
{ datasource, packageName: 'foo:bar' },
releaseOrig,
),
).toBe(releaseOrig);
});
it('adds releaseTimestamp', async () => {
httpMock
.scope(MAVEN_REPO)
.head('/foo/bar/1.2.3/bar-1.2.3.pom')
.reply(200, '', { 'Last-Modified': '2024-01-01T00:00:00.000Z' });
const res = await postprocessRelease(
{ datasource, packageName: 'foo:bar', registryUrl: MAVEN_REPO },
{ version: '1.2.3' },
);
expect(res).toEqual({
version: '1.2.3',
releaseTimestamp: '2024-01-01T00:00:00.000Z',
});
});
describe('S3', () => {
const s3mock = mockClient(S3Client);
afterEach(() => {
s3mock.reset();
});
it('checks package', async () => {
s3mock
.on(HeadObjectCommand, {
Bucket: 'bucket',
Key: 'foo/bar/1.2.3/bar-1.2.3.pom',
})
.resolvesOnce({});
const res = await postprocessRelease(
{ datasource, packageName: 'foo:bar', registryUrl: 's3://bucket' },
{ version: '1.2.3' },
);
expect(res).toEqual({ version: '1.2.3' });
});
it('supports timestamp', async () => {
s3mock
.on(HeadObjectCommand, {
Bucket: 'bucket',
Key: 'foo/bar/1.2.3/bar-1.2.3.pom',
})
.resolvesOnce({
LastModified: DateTime.fromISO(
'2024-01-01T00:00:00.000Z',
).toJSDate(),
});
const res = await postprocessRelease(
{ datasource, packageName: 'foo:bar', registryUrl: 's3://bucket' },
{ version: '1.2.3' },
);
expect(res).toEqual({
version: '1.2.3',
releaseTimestamp: '2024-01-01T00:00:00.000Z',
});
});
it('returns null for deleted object', async () => {
s3mock
.on(HeadObjectCommand, {
Bucket: 'bucket',
Key: 'foo/bar/1.2.3/bar-1.2.3.pom',
})
.resolvesOnce({ DeleteMarker: true });
const releaseOrig = { version: '1.2.3' };
const res = await postprocessRelease(
{ datasource, packageName: 'foo:bar', registryUrl: 's3://bucket' },
releaseOrig,
);
expect(res).toBeNull();
});
it('returns null for NotFound response', async () => {
s3mock
.on(HeadObjectCommand, {
Bucket: 'bucket',
Key: 'foo/bar/1.2.3/bar-1.2.3.pom',
})
.rejectsOnce('NotFound');
const releaseOrig = { version: '1.2.3' };
const res = await postprocessRelease(
{ datasource, packageName: 'foo:bar', registryUrl: 's3://bucket' },
releaseOrig,
);
expect(res).toBeNull();
});
it('returns null for NoSuchKey response', async () => {
s3mock
.on(HeadObjectCommand, {
Bucket: 'bucket',
Key: 'foo/bar/1.2.3/bar-1.2.3.pom',
})
.rejectsOnce('NoSuchKey');
const releaseOrig = { version: '1.2.3' };
const res = await postprocessRelease(
{ datasource, packageName: 'foo:bar', registryUrl: 's3://bucket' },
releaseOrig,
);
expect(res).toBeNull();
});
it('returns null for unknown error', async () => {
s3mock
.on(HeadObjectCommand, {
Bucket: 'bucket',
Key: 'foo/bar/1.2.3/bar-1.2.3.pom',
})
.rejectsOnce('Unknown');
const releaseOrig = { version: '1.2.3' };
const res = await postprocessRelease(
{ datasource, packageName: 'foo:bar', registryUrl: 's3://bucket' },
releaseOrig,
);
expect(res).toBeNull();
});
});
});
});