import { codeBlock } from 'common-tags'; import { Fixtures } from '~test/fixtures.ts'; import { fs, logger } from '~test/util.ts'; import { extractAllPackageFiles, extractExtensions, extractPackage, extractRegistries, resolveParents, } from './extract.ts'; vi.mock('../../../util/fs/index.ts'); const simpleContent = Fixtures.get('simple.pom.xml'); const mirrorSettingsContent = Fixtures.get('mirror.settings.xml'); const parentPomContent = Fixtures.get('parent.pom.xml'); const childPomContent = Fixtures.get('child.pom.xml'); const profileSettingsContent = Fixtures.get('profile.settings.xml'); describe('modules/manager/maven/extract', () => { describe('extractPackage', () => { it('returns null for invalid XML', () => { expect(extractPackage('', 'some-file', {})).toBeNull(); expect(extractPackage('invalid xml content', 'some-file', {})).toBeNull(); expect(extractPackage('', 'some-file', {})).toBeNull(); expect(extractPackage('', 'some-file', {})).toBeNull(); }); it('extract dependencies from any XML position', () => { const res = extractPackage(simpleContent, 'some-file', {}); expect(res).toMatchObject({ datasource: 'maven', deps: [ { datasource: 'maven', depName: 'org.example:parent', currentValue: '42', depType: 'parent', fileReplacePosition: 186, registryUrls: [ 'https://maven.atlassian.com/content/repositories/atlassian-public/', ], }, { datasource: 'maven', depName: 'org.example:foo', currentValue: '0.0.1', depType: 'compile', fileReplacePosition: 905, registryUrls: [ 'https://maven.atlassian.com/content/repositories/atlassian-public/', ], }, { datasource: 'maven', depName: 'org.example:bar', currentValue: '1.0.0', depType: 'compile', fileReplacePosition: 1093, registryUrls: [ 'https://maven.atlassian.com/content/repositories/atlassian-public/', ], }, { datasource: 'maven', depName: 'org.apache.maven.plugins:maven-release-plugin', currentValue: '2.4.2', depType: 'build', fileReplacePosition: 1347, registryUrls: [ 'https://maven.atlassian.com/content/repositories/atlassian-public/', ], }, { datasource: 'maven', depName: 'org.apache.maven.scm:maven-scm-provider-gitexe', currentValue: '1.8.1', depType: 'build', fileReplacePosition: 1545, registryUrls: [ 'https://maven.atlassian.com/content/repositories/atlassian-public/', ], }, { datasource: 'maven', depName: 'org.example:extension-artefact', currentValue: '1.0', depType: 'build', fileReplacePosition: 2276, registryUrls: [ 'https://maven.atlassian.com/content/repositories/atlassian-public/', ], }, { datasource: 'maven', depName: 'org.example:${artifact-id-placeholder}', currentValue: '0.0.1', depType: 'compile', fileReplacePosition: 2484, registryUrls: [ 'https://maven.atlassian.com/content/repositories/atlassian-public/', ], }, { datasource: 'maven', depName: '${group-id-placeholder}:baz', currentValue: '0.0.1', depType: 'compile', fileReplacePosition: 2634, registryUrls: [ 'https://maven.atlassian.com/content/repositories/atlassian-public/', ], }, { datasource: 'maven', depName: '${quuxGroup}:${quuxId}', currentValue: '${quuxVersion}', depType: 'compile', fileReplacePosition: 2779, registryUrls: [ 'https://maven.atlassian.com/content/repositories/atlassian-public/', ], }, { datasource: 'maven', depName: '${quuxGroup}:${quuxId}-test', currentValue: '${quuxVersion}', depType: 'compile', fileReplacePosition: 2938, registryUrls: [ 'https://maven.atlassian.com/content/repositories/atlassian-public/', ], }, { datasource: 'maven', depName: 'org.example:quuz', currentValue: '1.2.3', depType: 'test', fileReplacePosition: 3086, registryUrls: [ 'https://maven.atlassian.com/content/repositories/atlassian-public/', ], }, { datasource: 'maven', depName: 'org.example:quuuz', currentValue: "it's not a version", depType: 'compile', fileReplacePosition: 3252, registryUrls: [ 'https://maven.atlassian.com/content/repositories/atlassian-public/', ], }, { datasource: 'maven', depName: 'org.example:hard-range', currentValue: '[1.0.0]', depType: 'compile', fileReplacePosition: 3410, registryUrls: [ 'https://maven.atlassian.com/content/repositories/atlassian-public/', ], }, { datasource: 'maven', depName: 'org.example:optional', currentValue: '1.0.0', depType: 'optional', fileReplacePosition: 3555, registryUrls: [ 'https://maven.atlassian.com/content/repositories/atlassian-public/', ], }, { datasource: 'maven', depName: 'org.example:relocation-artifact', currentValue: '1.0', fileReplacePosition: 3787, registryUrls: [ 'https://maven.atlassian.com/content/repositories/atlassian-public/', ], }, { datasource: 'maven', depName: 'org.example:profile-artifact', currentValue: '${profile-placeholder}', depType: 'compile', fileReplacePosition: 4119, registryUrls: [ 'https://maven.atlassian.com/content/repositories/atlassian-public/', ], }, { datasource: 'maven', depName: 'org.example:profile-build-artefact', currentValue: '2.17', depType: 'build', fileReplacePosition: 4375, registryUrls: [ 'https://maven.atlassian.com/content/repositories/atlassian-public/', ], }, { datasource: 'maven', depName: 'org.apache.maven.plugins:maven-checkstyle-plugin', currentValue: '2.17', depType: 'build', fileReplacePosition: 4769, registryUrls: [ 'https://maven.atlassian.com/content/repositories/atlassian-public/', ], }, ], mavenProps: { quuxGroup: { fileReplacePosition: 631, packageFile: 'some-file', val: 'org.example', }, quuxId: { fileReplacePosition: 667, packageFile: 'some-file', val: 'quux', }, quuxVersion: { fileReplacePosition: 698, packageFile: 'some-file', val: '1.2.3.4', }, }, packageFile: 'some-file', packageFileVersion: '0.0.1', parent: '../pom.xml', }); }); it('extract dependencies with windows line endings', () => { extractPackage( ' \r\n', 'some-file', {}, ); expect(logger.logger.warn).toHaveBeenCalledWith( 'Your pom.xml contains windows line endings. This is not supported and may result in parsing issues.', ); }); it('tries minimum manifests', () => { const res = extractPackage( Fixtures.get('minimum.pom.xml'), 'some-file', {}, ); expect(res).toEqual({ datasource: 'maven', deps: [], mavenProps: {}, packageFile: 'some-file', packageFileVersion: '1', }); }); it('tries minimum snapshot manifests', () => { const res = extractPackage( Fixtures.get(`minimum_snapshot.pom.xml`), 'some-file', {}, ); expect(res).toEqual({ datasource: 'maven', deps: [], mavenProps: {}, packageFile: 'some-file', packageFileVersion: '0.0.1-SNAPSHOT', }); }); it('extracts builder and buildpack images from spring-boot plugin', () => { const res = extractPackage( Fixtures.get('full_cnb.pom.xml'), 'full_cnb.pom.xml', {}, ); expect(res?.deps).toEqual([ { currentValue: '3.2.2', datasource: 'maven', depName: 'org.springframework.boot:spring-boot-starter-parent', depType: 'parent', fileReplacePosition: 404, registryUrls: [], }, { autoReplaceStringTemplate: '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', currentValue: '0.4.316', datasource: 'docker', depName: 'paketobuildpacks/builder-jammy-base', packageName: 'paketobuildpacks/builder-jammy-base', replaceString: 'paketobuildpacks/builder-jammy-base:0.4.316', fileReplacePosition: 1273, }, { autoReplaceStringTemplate: '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', currentValue: '0.0.28', datasource: 'docker', depName: 'paketobuildpacks/run-noble-full', packageName: 'paketobuildpacks/run-noble-full', replaceString: 'paketobuildpacks/run-noble-full:0.0.28', fileReplacePosition: 1343, }, { autoReplaceStringTemplate: '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', currentValue: '6.1.1', datasource: 'buildpacks-registry', packageName: 'paketo-buildpacks/nodejs', fileReplacePosition: 1430, }, { autoReplaceStringTemplate: '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', currentValue: '1.8.0', datasource: 'docker', depName: 'gcr.io/paketo-buildpacks/nodejs', fileReplacePosition: 1566, packageName: 'gcr.io/paketo-buildpacks/nodejs', replaceString: 'gcr.io/paketo-buildpacks/nodejs:1.8.0', }, { autoReplaceStringTemplate: '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', currentDigest: 'sha256:2c27cd0b4482a4aa5aeb38104f6d934511cd87c1af34a10d1d6cdf2d9d16f138', currentValue: '2.22.1', datasource: 'docker', depName: 'docker.io/paketobuildpacks/python', fileReplacePosition: 1634, packageName: 'docker.io/paketobuildpacks/python', replaceString: 'docker.io/paketobuildpacks/python:2.22.1@sha256:2c27cd0b4482a4aa5aeb38104f6d934511cd87c1af34a10d1d6cdf2d9d16f138', }, { autoReplaceStringTemplate: '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', currentDigest: 'sha256:080f4cfa5c8fe43837b2b83f69ae16e320ea67c051173e4934a015590b2ca67a', datasource: 'docker', depName: 'docker.io/paketobuildpacks/ruby', fileReplacePosition: 1795, packageName: 'docker.io/paketobuildpacks/ruby', replaceString: 'docker.io/paketobuildpacks/ruby@sha256:080f4cfa5c8fe43837b2b83f69ae16e320ea67c051173e4934a015590b2ca67a', }, { autoReplaceStringTemplate: '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', currentValue: '12.1.0', datasource: 'docker', depName: 'paketobuildpacks/java', packageName: 'paketobuildpacks/java', replaceString: 'paketobuildpacks/java:12.1.0', fileReplacePosition: 2001, }, ]); }); it('extracts only builder if defaults are used in spring-boot plugin', () => { const res = extractPackage( Fixtures.get('basic_cnb.pom.xml'), 'basic_cnb.pom.xml', {}, ); expect(res?.deps).toEqual([ { currentValue: '3.2.2', datasource: 'maven', depName: 'org.springframework.boot:spring-boot-starter-parent', depType: 'parent', fileReplacePosition: 404, registryUrls: [], }, { autoReplaceStringTemplate: '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', currentValue: '0.4.316', datasource: 'docker', depName: 'paketobuildpacks/builder-jammy-base', packageName: 'paketobuildpacks/builder-jammy-base', replaceString: 'paketobuildpacks/builder-jammy-base:0.4.316', fileReplacePosition: 1273, }, ]); }); it('returns no buildpack dependencies when image tag is missing in spring boot plugin configuration', () => { const res = extractPackage( Fixtures.get('empty_cnb.pom.xml'), 'empty_cnb.pom.xml', {}, ); expect(res?.deps).toEqual([]); }); it('returns no buildpack dependencies when dependencies are invalid in spring boot plugin', () => { const res = extractPackage( Fixtures.get('invalid_cnb.pom.xml'), 'invalid_cnb.pom.xml', {}, ); expect(res?.deps).toEqual([]); }); }); describe('resolveParents', () => { it('should apply props recursively', () => { const packages = extractPackage( Fixtures.get('recursive_props.pom.xml'), 'some-file', {}, ); const [{ deps }] = resolveParents([packages!]); expect(deps).toMatchObject([ { depName: 'com.sksamuel.scapegoat:scalac-scapegoat-plugin_2.13.7', currentValue: '1.4.11', }, ]); }); it('should apply props multiple times', () => { const packages = extractPackage( Fixtures.get('multiple_usages_props.pom.xml'), 'some-file', {}, ); const [{ deps }] = resolveParents([packages!]); expect(deps).toMatchObject([ { depName: 'org.apache.lucene:lucene-core-1.2.3.1.2.3', currentValue: '1.2.3', }, ]); }); it('should detect props infinitely recursing props', () => { const packages = extractPackage( Fixtures.get('infinite_recursive_props.pom.xml'), 'some-file', {}, ); const [{ deps }] = resolveParents([packages!]); expect(deps).toMatchObject([ { depName: 'org.apache.lucene:lucene-core', currentValue: '${foo}', skipReason: 'recursive-placeholder', }, { depName: 'org.apache.lucene:lucene-core-${var1}', currentValue: '1.2', skipReason: 'recursive-placeholder', }, ]); }); }); describe('extractRegistries', () => { it('returns null for invalid XML', () => { expect(extractRegistries('')).toBeEmptyArray(); expect(extractRegistries('invalid xml content')).toBeEmptyArray(); expect(extractRegistries('')).toBeEmptyArray(); expect(extractRegistries('')).toBeEmptyArray(); }); it('extract registries from a simple mirror settings file', () => { const res = extractRegistries(mirrorSettingsContent); expect(res).toStrictEqual([ 'https://artifactory.company.com/artifactory/my-maven-repo', ]); }); it('extract registries from a simple profile settings file', () => { const res = extractRegistries(profileSettingsContent); expect(res).toStrictEqual([ 'https://repo.adobe.com/nexus/content/groups/public', ]); }); it('extract registries from a complex profile settings file', () => { const res = extractRegistries(Fixtures.get('complex.settings.xml')); expect(res).toStrictEqual([ 'https://artifactory.company.com/artifactory/my-maven-repo', 'https://repo.adobe.com/nexus/content/groups/public', 'https://repo.adobe.com/v2/nexus/content/groups/public', 'https://repo.adobe.com/v3/nexus/content/groups/public', 'https://repo.adobe.com/v4/nexus/content/groups/public', ]); }); it('extract registries from a settings file that uses a newer schema', () => { const settingsUpdatedContent = codeBlock` Test-Internal-repository Proxy Repository Manager https://proxy-repo.com/artifactory/apache-maven central `; const res = extractRegistries(settingsUpdatedContent); expect(res).toStrictEqual([ 'https://proxy-repo.com/artifactory/apache-maven', ]); }); }); describe('extractExtensions', () => { it('returns null for invalid xml files', () => { expect(extractExtensions('', '.mvn/extensions.xml')).toBeNull(); expect( extractExtensions('invalid xml content', '.mvn/extensions.xml'), ).toBeNull(); expect( extractExtensions('', '.mvn/extensions.xml'), ).toBeNull(); expect( extractExtensions('', '.mvn/extensions.xml'), ).toBeNull(); expect( extractExtensions( '', '.mvn/extensions.xml', ), ).toBeNull(); }); }); describe('extractAllPackageFiles', () => { it('should return empty if package has no content', async () => { fs.readLocalFile.mockResolvedValueOnce(''); const res = await extractAllPackageFiles({}, ['random.pom.xml']); expect(res).toBeEmptyArray(); }); it('should return empty for packages with invalid content', async () => { fs.readLocalFile.mockResolvedValueOnce('invalid content'); const res = await extractAllPackageFiles({}, ['random.pom.xml']); expect(res).toBeEmptyArray(); }); it('should return packages with urls from a settings file', async () => { fs.readLocalFile .mockResolvedValueOnce(mirrorSettingsContent) .mockResolvedValueOnce(simpleContent); const res = await extractAllPackageFiles({}, [ 'mirror.settings.xml', 'simple.pom.xml', ]); const urls = [ 'https://artifactory.company.com/artifactory/my-maven-repo', 'https://maven.atlassian.com/content/repositories/atlassian-public/', 'https://repo.maven.apache.org/maven2', ]; for (const packageFile of res) { for (const dep of packageFile.deps) { expect(dep.registryUrls).toStrictEqual(urls); } } }); it('should include registryUrls from parent pom files', async () => { fs.readLocalFile .mockResolvedValueOnce(parentPomContent) .mockResolvedValueOnce(childPomContent); const res = await extractAllPackageFiles({}, [ 'parent.pom.xml', 'child.pom.xml', ]); const unorderedUrls = new Set([ 'https://repo.maven.apache.org/maven2', 'http://example.com/', 'http://example.com/nexus/xyz', ]); for (const packageFile of res) { for (const dep of packageFile.deps) { const depUrls = new Set(dep.registryUrls); expect(depUrls).toStrictEqual(unorderedUrls); } } expect(res).toMatchObject([ { datasource: 'maven', deps: [ { currentValue: '42', datasource: 'maven', depName: 'org.example:child', depType: 'parent', fileReplacePosition: 185, registryUrls: [ 'http://example.com/nexus/xyz', 'http://example.com/', 'https://repo.maven.apache.org/maven2', ], }, { currentValue: '1.2.3.4', datasource: 'maven', depName: 'org.example:quux', depType: 'compile', editFile: 'parent.pom.xml', fileReplacePosition: 470, sharedVariableName: 'quuxVersion', registryUrls: [ 'http://example.com/', 'http://example.com/nexus/xyz', 'https://repo.maven.apache.org/maven2', ], }, ], packageFile: 'parent.pom.xml', }, { datasource: 'maven', deps: [ { currentValue: '42', datasource: 'maven', depName: 'org.example:parent', depType: 'parent', fileReplacePosition: 186, registryUrls: [ 'http://example.com/', 'http://example.com/nexus/xyz', 'https://repo.maven.apache.org/maven2', ], }, { currentValue: '0.0.1', datasource: 'maven', depName: 'org.example:foo', depType: 'compile', fileReplacePosition: 806, registryUrls: [ 'http://example.com/', 'http://example.com/nexus/xyz', 'https://repo.maven.apache.org/maven2', ], }, { currentValue: '1.0.0', datasource: 'maven', depName: 'org.example:bar', depType: 'compile', fileReplacePosition: 954, registryUrls: [ 'http://example.com/', 'http://example.com/nexus/xyz', 'https://repo.maven.apache.org/maven2', ], }, { currentValue: '2.4.2', datasource: 'maven', depName: 'org.apache.maven.plugins:maven-release-plugin', depType: 'build', fileReplacePosition: 1188, registryUrls: [ 'http://example.com/', 'http://example.com/nexus/xyz', 'https://repo.maven.apache.org/maven2', ], }, { currentValue: '1.8.1', datasource: 'maven', depName: 'org.apache.maven.scm:maven-scm-provider-gitexe', depType: 'build', fileReplacePosition: 1386, registryUrls: [ 'http://example.com/', 'http://example.com/nexus/xyz', 'https://repo.maven.apache.org/maven2', ], }, { currentValue: '0.0.1', datasource: 'maven', depName: 'org.example:${artifact-id-placeholder}', depType: 'compile', fileReplacePosition: 2131, registryUrls: [ 'http://example.com/', 'http://example.com/nexus/xyz', 'https://repo.maven.apache.org/maven2', ], skipReason: 'name-placeholder', }, { currentValue: '0.0.1', datasource: 'maven', depName: '${group-id-placeholder}:baz', depType: 'compile', fileReplacePosition: 2281, registryUrls: [ 'http://example.com/', 'http://example.com/nexus/xyz', 'https://repo.maven.apache.org/maven2', ], skipReason: 'name-placeholder', }, { currentValue: '1.2.3', datasource: 'maven', depName: 'org.example:quuz', depType: 'compile', fileReplacePosition: 2574, registryUrls: [ 'http://example.com/', 'http://example.com/nexus/xyz', 'https://repo.maven.apache.org/maven2', ], }, { currentValue: "it's not a version", datasource: 'maven', depName: 'org.example:quuuz', depType: 'compile', fileReplacePosition: 2714, registryUrls: [ 'http://example.com/', 'http://example.com/nexus/xyz', 'https://repo.maven.apache.org/maven2', ], }, { currentValue: '[1.0.0]', datasource: 'maven', depName: 'org.example:hard-range', depType: 'compile', fileReplacePosition: 2872, registryUrls: [ 'http://example.com/', 'http://example.com/nexus/xyz', 'https://repo.maven.apache.org/maven2', ], }, { currentValue: '${profile-placeholder}', datasource: 'maven', depName: 'org.example:profile-artifact', depType: 'compile', fileReplacePosition: 3134, registryUrls: [ 'http://example.com/', 'http://example.com/nexus/xyz', 'https://repo.maven.apache.org/maven2', ], skipReason: 'version-placeholder', }, { currentValue: '2.17', datasource: 'maven', depName: 'org.apache.maven.plugins:maven-checkstyle-plugin', depType: 'build', fileReplacePosition: 3410, registryUrls: [ 'http://example.com/', 'http://example.com/nexus/xyz', 'https://repo.maven.apache.org/maven2', ], }, ], packageFile: 'child.pom.xml', packageFileVersion: '0.0.1', }, ]); }); it('should include registryUrls in the correct order', async () => { fs.readLocalFile .mockResolvedValueOnce(simpleContent) .mockResolvedValueOnce(profileSettingsContent); const res = await extractAllPackageFiles({}, [ 'simple.pom.xml', 'profile.settings.xml', ]); const urls = [ 'https://repo.adobe.com/nexus/content/groups/public', 'https://maven.atlassian.com/content/repositories/atlassian-public/', 'https://repo.maven.apache.org/maven2', ]; for (const packageFile of res) { for (const dep of packageFile.deps) { expect(dep.registryUrls).toStrictEqual(urls); } } }); it('should return package files info', async () => { fs.readLocalFile.mockResolvedValueOnce(simpleContent); const res = await extractAllPackageFiles({}, ['random.pom.xml']); expect(res).toMatchObject([ { deps: [ { depName: 'org.example:parent', currentValue: '42' }, { depName: 'org.example:foo', currentValue: '0.0.1' }, { depName: 'org.example:bar', currentValue: '1.0.0' }, { depName: 'org.apache.maven.plugins:maven-release-plugin', currentValue: '2.4.2', }, { depName: 'org.apache.maven.scm:maven-scm-provider-gitexe', currentValue: '1.8.1', }, { depName: 'org.example:extension-artefact', currentValue: '1.0', }, { depName: 'org.example:${artifact-id-placeholder}', skipReason: 'name-placeholder', }, { depName: '${group-id-placeholder}:baz', skipReason: 'name-placeholder', }, { depName: 'org.example:quux', currentValue: '1.2.3.4', sharedVariableName: 'quuxVersion', }, { depName: 'org.example:quux-test', currentValue: '1.2.3.4', sharedVariableName: 'quuxVersion', }, { depName: 'org.example:quuz', currentValue: '1.2.3', }, { depName: 'org.example:quuuz', currentValue: "it's not a version", }, { depName: 'org.example:hard-range', currentValue: '[1.0.0]' }, { depName: 'org.example:optional', currentValue: '1.0.0', }, { depName: 'org.example:relocation-artifact', currentValue: '1.0', }, { depName: 'org.example:profile-artifact', currentValue: '${profile-placeholder}', skipReason: 'version-placeholder', }, { depName: 'org.example:profile-build-artefact', currentValue: '2.17', }, { depName: 'org.apache.maven.plugins:maven-checkstyle-plugin', currentValue: '2.17', }, ], packageFile: 'random.pom.xml', }, ]); }); it('should extract from .mvn/extensions.xml file', async () => { fs.readLocalFile.mockResolvedValueOnce(codeBlock` io.jenkins.tools.incrementals git-changelist-maven-extension 1.6 `); const res = await extractAllPackageFiles({}, ['.mvn/extensions.xml']); expect(res).toMatchObject([ { packageFile: '.mvn/extensions.xml', deps: [ { datasource: 'maven', depName: 'io.jenkins.tools.incrementals:git-changelist-maven-extension', currentValue: '1.6', depType: 'build', fileReplacePosition: 372, registryUrls: ['https://repo.maven.apache.org/maven2'], }, ], }, ]); }); it('should extract from pom.template.xml file', async () => { fs.readLocalFile.mockResolvedValueOnce(codeBlock` 4.0.0 org.example template-project 1.0.0 UTF-8 2021.1.12 {{scala_version}} {{scala_binary_version}} org.springframework.boot spring-boot-starter-web 3.2.0 org.junit.jupiter junit-jupiter 5.10.1 test org.example templated-dep \${templateVersion} org.scala-lang scala-library \${scala.version} `); const res = await extractAllPackageFiles({}, ['pom.template.xml']); expect(res).toMatchObject([ { packageFile: 'pom.template.xml', deps: [ { datasource: 'maven', depName: 'org.springframework.boot:spring-boot-starter-web', currentValue: '3.2.0', depType: 'compile', registryUrls: ['https://repo.maven.apache.org/maven2'], }, { datasource: 'maven', depName: 'org.junit.jupiter:junit-jupiter', currentValue: '5.10.1', depType: 'test', registryUrls: ['https://repo.maven.apache.org/maven2'], }, { datasource: 'maven', depName: 'org.example:templated-dep', currentValue: '${templateVersion}', depType: 'compile', skipReason: 'version-placeholder', registryUrls: ['https://repo.maven.apache.org/maven2'], }, { datasource: 'maven', depName: 'org.scala-lang:scala-library', currentValue: '{{scala_version}}', depType: 'compile', skipReason: 'version-placeholder', registryUrls: ['https://repo.maven.apache.org/maven2'], }, ], }, ]); }); it('should return empty array if extensions file is invalid or empty', async () => { fs.readLocalFile .mockResolvedValueOnce('') .mockResolvedValueOnce('invalid xml content'); await expect( extractAllPackageFiles({}, [ '.mvn/extensions.xml', 'grp/.mvn/extensions.xml', ]), ).resolves.toBeEmptyArray(); }); describe('root pom handling', () => { it('should skip root pom.xml', async () => { fs.readLocalFile.mockResolvedValueOnce(codeBlock` 4.0.0 org.example root 1.0.0 `); fs.readLocalFile.mockResolvedValueOnce(codeBlock` org.example root 1.0.0 4.0.0 org.example child `); const res = await extractAllPackageFiles({}, [ 'pom.xml', 'foo.bar/pom.xml', ]); expect(res).toMatchObject([ { packageFile: 'pom.xml', deps: [] }, { packageFile: 'foo.bar/pom.xml', deps: [{ depName: 'org.example:root', depType: 'parent-root' }], }, ]); }); it('should skip root pom.xml when it has an external parent', async () => { fs.readLocalFile.mockResolvedValueOnce(codeBlock` 4.0.0 org.example root 1.0.0 org.acme external-parent 1.0.0 `); fs.readLocalFile.mockResolvedValueOnce(codeBlock` org.example root 1.0.0 4.0.0 org.example child `); const res = await extractAllPackageFiles({}, [ 'pom.xml', 'foo.bar/pom.xml', ]); expect(res).toMatchObject([ { packageFile: 'pom.xml', deps: [{ depName: 'org.acme:external-parent', depType: 'parent' }], }, { packageFile: 'foo.bar/pom.xml', deps: [{ depName: 'org.example:root', depType: 'parent-root' }], }, ]); }); it('handles cross-referencing', async () => { fs.readLocalFile.mockResolvedValueOnce(codeBlock` 4.0.0 org.example foo 1.0.0 org.example bar 1.0.0 `); fs.readLocalFile.mockResolvedValueOnce(codeBlock` 4.0.0 org.example bar 1.0.0 org.example foo 1.0.0 `); const res = await extractAllPackageFiles({}, ['foo.xml', 'bar.xml']); expect(res).toMatchObject([ { packageFile: 'foo.xml', deps: [{ depName: 'org.example:bar' }] }, { packageFile: 'bar.xml', deps: [{ depName: 'org.example:foo' }] }, ]); const [foo, bar] = res; expect(foo.deps[0].skipReason).toBeUndefined(); expect(bar.deps[0].skipReason).toBeUndefined(); }); }); }); });