import { codeBlock } from 'common-tags'; import upath from 'upath'; import { Fixtures } from '~test/fixtures.ts'; import { fs, logger } from '~test/util.ts'; import { GlobalConfig } from '../../../config/global.ts'; import type { InternalGlobalConfigOptions, RepoGlobalConfig, } from '../../../config/types.ts'; import { DotnetVersionDatasource } from '../../datasource/dotnet-version/index.ts'; import type { ExtractConfig } from '../types.ts'; import { extractPackageFile } from './index.ts'; import * as nugetExtractUtil from './util.ts'; const config: ExtractConfig = {}; const adminConfig: RepoGlobalConfig & InternalGlobalConfigOptions = { localDir: upath.resolve('lib/modules/manager/nuget/__fixtures__'), }; describe('modules/manager/nuget/extract', () => { describe('extractPackageFile()', () => { beforeEach(() => { GlobalConfig.set(adminConfig); }); afterEach(() => { GlobalConfig.reset(); }); it('returns null for invalid csproj', async () => { await expect( extractPackageFile( '\uFEFF invalid xml>', 'bogus', config, ), ).resolves.toBeNull(); expect(logger.logger.debug).toHaveBeenCalledWith( expect.anything(), 'Failed to parse XML', ); }); it('returns null if not xml', async () => { await expect( extractPackageFile( codeBlock` org.apache.curator:* =4.3.0 org.apache.hadoop:*=3.1.4 org.apache.hadoop.thirdparty:* = 1.1.0 `, 'versions.props', config, ), ).resolves.toBeNull(); expect(logger.logger.debug).toHaveBeenCalledWith( 'NuGet: Skipping versions.props as it is not XML', ); }); it('extracts package version dependency', async () => { const packageFile = 'with-centralized-package-versions/Directory.Packages.props'; const sample = Fixtures.get(packageFile); const res = await extractPackageFile(sample, packageFile, config); expect(res?.deps).toEqual([ { currentValue: '4.5.0', datasource: 'nuget', depName: 'Autofac', depType: 'nuget', }, ]); }); it('extracts package file version', async () => { const packageFile = 'sample.csproj'; const sample = Fixtures.get(packageFile); vi.spyOn(fs, 'localPathExists').mockResolvedValueOnce(true); const res = await extractPackageFile(sample, packageFile, config); expect(res?.packageFileVersion).toBe('0.1.0'); expect(res?.lockFiles).toEqual(['packages.lock.json']); }); it('does not fail on package file without version', async () => { const packageFile = 'single-project-file/single.csproj'; const sample = Fixtures.get(packageFile); const res = await extractPackageFile(sample, packageFile, config); expect(res?.packageFileVersion).toBeUndefined(); }); it('extracts all dependencies', async () => { const packageFile = 'sample.csproj'; const sample = Fixtures.get(packageFile); const res = await extractPackageFile(sample, packageFile, config); expect(res?.deps).toMatchObject([ { currentValue: '1.0.0', depName: 'My.Package' }, { currentValue: '1.0.0', depName: 'Microsoft.VisualStudio.Web.CodeGeneration.Tools', }, { depName: 'NotUpdatable3', skipReason: 'invalid-version' }, { currentValue: '[1.2.3, 3.2.1)', depName: 'NotUpdatable3' }, { currentValue: '[1.2.3, 3.2.1]', depName: 'NotUpdatable3' }, { currentValue: '(1.2.3, 3.2.1)', depName: 'NotUpdatable3' }, { currentValue: '(1.2.3,)', depName: 'NotUpdatable2' }, { currentValue: '[,1.2.3)', depName: 'NotUpdatable1' }, { currentValue: '[1.2.3,)', depName: 'Range3' }, { currentValue: '[1.2.3,]', depName: 'Range2' }, { currentValue: '[1.2.3]', depName: 'Range1' }, { currentValue: '3.1.0.5', depName: 'Stateless' }, { currentValue: '2.1.0', depName: 'Serilog.Sinks.Literate' }, { currentValue: '1.4.0', depName: 'Serilog.Extensions.Logging' }, { currentValue: '2.4.0', depName: 'Serilog' }, { currentValue: '10.0.2', depName: 'Newtonsoft.Json' }, { currentValue: '1.1.2', depName: 'Microsoft.Extensions.Logging.Debug', }, { currentValue: '1.1.2', depName: 'Microsoft.Extensions.Configuration.Json', }, { currentValue: '1.1.2', depName: 'Microsoft.AspNetCore.Server.Kestrel', }, { currentValue: '1.1.3', depName: 'Microsoft.AspNetCore.Mvc.Core' }, { currentValue: '$(UnknownVariable)', depName: 'Microsoft.AspNetCore.Hosting', skipReason: 'contains-variable', }, { currentValue: '4.5.0', depName: 'Autofac.Extensions.DependencyInjection', sharedVariableName: 'AutofacVersion', }, { currentValue: '4.5.0', depName: 'Autofac', sharedVariableName: 'AutofacVersion', }, ]); }); it('extracts msbuild sdk from the Sdk attr of Project element', async () => { const packageFile = 'sample.csproj'; const sample = ` net7.0 $(MSBuildThisFileDirectory)\tdlib.native.nuspec version=$(PackageVersion) `; const res = await extractPackageFile(sample, packageFile, config); expect(res?.deps).toEqual([ { depName: 'Microsoft.Build.NoTargets', depType: 'msbuild-sdk', currentValue: '3.4.0', datasource: 'nuget', }, ]); expect(res?.deps).toHaveLength(1); }); it('does not extract msbuild sdk from the Sdk attr of Project element if version is missing', async () => { const packageFile = 'sample.csproj'; const sample = ` net7.0 $(MSBuildThisFileDirectory)\tdlib.native.nuspec version=$(PackageVersion) `; const res = await extractPackageFile(sample, packageFile, config); expect(res).toBeNull(); }); it('extracts msbuild sdk from the Sdk element', async () => { const packageFile = 'sample.csproj'; const sample = ` net7.0 $(MSBuildThisFileDirectory)\tdlib.native.nuspec version=$(PackageVersion) `; const res = await extractPackageFile(sample, packageFile, config); expect(res?.deps).toEqual([ { depName: 'Microsoft.Build.NoTargets', depType: 'msbuild-sdk', currentValue: '3.4.0', datasource: 'nuget', }, ]); expect(res?.deps).toHaveLength(1); }); it('extracts msbuild sdk from sqlproj Sdk element', async () => { const packageFile = 'sample.sqlproj'; const sample = codeBlock` `; const res = await extractPackageFile(sample, packageFile, config); expect(res?.deps).toEqual([ { depName: 'Microsoft.Build.Sql', depType: 'msbuild-sdk', currentValue: '0.1.19-preview', datasource: 'nuget', }, ]); }); it('does not extract msbuild sdk from the Sdk element if version is missing', async () => { const packageFile = 'sample.csproj'; const sample = ` net7.0 $(MSBuildThisFileDirectory)\tdlib.native.nuspec version=$(PackageVersion) `; const res = await extractPackageFile(sample, packageFile, config); expect(res).toBeNull(); }); it('extracts msbuild sdk from the Import element', async () => { const packageFile = 'sample.csproj'; const sample = ` net7.0 $(MSBuildThisFileDirectory)\tdlib.native.nuspec version=$(PackageVersion) `; const res = await extractPackageFile(sample, packageFile, config); expect(res?.deps).toEqual([ { depName: 'My.Custom.Sdk', depType: 'msbuild-sdk', currentValue: '1.2.3', datasource: 'nuget', }, ]); expect(res?.deps).toHaveLength(1); }); it('does not set versioning on PackageReference dependencies', async () => { const packageFile = 'sample.csproj'; const sample = codeBlock` `; const res = await extractPackageFile(sample, packageFile, config); expect(res?.deps).toEqual([ { currentValue: '4.5.0', datasource: 'nuget', depName: 'Autofac', depType: 'nuget', }, ]); }); it('does not extract msbuild sdk from the Import element if version is missing', async () => { const packageFile = 'sample.csproj'; const sample = ` net7.0 $(MSBuildThisFileDirectory)\tdlib.native.nuspec version=$(PackageVersion) `; const res = await extractPackageFile(sample, packageFile, config); expect(res).toBeNull(); }); it('extracts dependency with lower-case Version attribute', async () => { const contents = codeBlock` net8.0 `; const res = await extractPackageFile(contents, 'some.csproj', config); expect(res?.deps).toHaveLength(1); }); it('extracts all dependencies from global packages file', async () => { const packageFile = 'packages.props'; const sample = Fixtures.get(packageFile); const res = await extractPackageFile(sample, packageFile, config); expect(res?.deps).toMatchObject([ { currentValue: '1.0.0', depName: 'Microsoft.VisualStudio.Web.CodeGeneration.Tools', }, { currentValue: '[1.2.3, 3.2.1)', depName: 'NotUpdatable3' }, { currentValue: '[1.2.3, 3.2.1]', depName: 'NotUpdatable3' }, { currentValue: '(1.2.3, 3.2.1)', depName: 'NotUpdatable3' }, { currentValue: '(1.2.3,)', depName: 'NotUpdatable2' }, { currentValue: '[,1.2.3)', depName: 'NotUpdatable1' }, { currentValue: '[1.2.3,)', depName: 'Range3' }, { currentValue: '[1.2.3,]', depName: 'Range2' }, { currentValue: '[1.2.3]', depName: 'Range1' }, { currentValue: '3.1.0.5', depName: 'Stateless' }, { currentValue: '2.1.0', depName: 'Serilog.Sinks.Literate' }, { currentValue: '1.4.0', depName: 'Serilog.Extensions.Logging' }, { currentValue: '2.4.0', depName: 'Serilog' }, { currentValue: '10.0.2', depName: 'Newtonsoft.Json' }, { currentValue: '1.1.2', depName: 'Microsoft.Extensions.Logging.Debug', }, { currentValue: '1.1.2', depName: 'Microsoft.Extensions.Configuration.Json', }, { currentValue: '1.1.2', depName: 'Microsoft.AspNetCore.Server.Kestrel', }, { currentValue: '1.1.3', depName: 'Microsoft.AspNetCore.Mvc.Core' }, { currentValue: '1.1.2', depName: 'Microsoft.AspNetCore.Hosting' }, { currentValue: '4.1.0', depName: 'Autofac.Extensions.DependencyInjection', }, { currentValue: '4.5.0', depName: 'Autofac' }, { currentValue: '2.0.0', depName: 'Roslynator.Analyzers' }, ]); }); it('extracts ContainerBaseImage', async () => { const contents = codeBlock` 0.1.0 mcr.microsoft.com/dotnet/runtime:7.0.10 `; await expect( extractPackageFile(contents, contents, config), ).resolves.toEqual({ deps: [ { autoReplaceStringTemplate: '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', depName: 'mcr.microsoft.com/dotnet/runtime', packageName: 'mcr.microsoft.com/dotnet/runtime', depType: 'docker', datasource: 'docker', currentValue: '7.0.10', replaceString: 'mcr.microsoft.com/dotnet/runtime:7.0.10', }, ], packageFileVersion: '0.1.0', }); }); it('extracts ContainerBaseImage with pinned digest', async () => { const contents = codeBlock` 0.1.0 mcr.microsoft.com/dotnet/runtime:7.0.10@sha256:181067029e094856691ee1ce3782ea3bd3fda01bb5b6d19411d0f673cab1ab19 `; await expect( extractPackageFile(contents, contents, config), ).resolves.toEqual({ deps: [ { autoReplaceStringTemplate: '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', depName: 'mcr.microsoft.com/dotnet/runtime', packageName: 'mcr.microsoft.com/dotnet/runtime', depType: 'docker', datasource: 'docker', currentValue: '7.0.10', currentDigest: 'sha256:181067029e094856691ee1ce3782ea3bd3fda01bb5b6d19411d0f673cab1ab19', replaceString: 'mcr.microsoft.com/dotnet/runtime:7.0.10@sha256:181067029e094856691ee1ce3782ea3bd3fda01bb5b6d19411d0f673cab1ab19', }, ], packageFileVersion: '0.1.0', }); }); it('considers NuGet.config', async () => { const packageFile = 'with-config-file/with-config-file.csproj'; const contents = Fixtures.get(packageFile); await expect( extractPackageFile(contents, packageFile, config), ).resolves.toEqual({ deps: [ { currentValue: '4.5.0', datasource: 'nuget', depName: 'Autofac', depType: 'nuget', registryUrls: [ 'https://api.nuget.org/v3/index.json#protocolVersion=3', 'https://contoso.com/packages/', ], }, ], packageFileVersion: '0.1.0', }); }); it('considers lower-case nuget.config', async () => { const packageFile = 'with-lower-case-config-file/with-lower-case-config-file.csproj'; const contents = Fixtures.get(packageFile); await expect( extractPackageFile(contents, packageFile, config), ).resolves.toEqual({ deps: [ { currentValue: '4.5.0', datasource: 'nuget', depName: 'Autofac', depType: 'nuget', registryUrls: [ 'https://api.nuget.org/v3/index.json#protocolVersion=3', 'https://contoso.com/packages/', ], }, ], packageFileVersion: '0.1.0', }); }); it('considers pascal-case NuGet.Config', async () => { const packageFile = 'with-pascal-case-config-file/with-pascal-case-config-file.csproj'; const contents = Fixtures.get(packageFile); await expect( extractPackageFile(contents, packageFile, config), ).resolves.toEqual({ deps: [ { currentValue: '4.5.0', datasource: 'nuget', depName: 'Autofac', depType: 'nuget', registryUrls: [ 'https://api.nuget.org/v3/index.json#protocolVersion=3', 'https://contoso.com/packages/', ], }, ], packageFileVersion: '0.1.0', }); }); it('handles malformed NuGet.config', async () => { const packageFile = 'with-malformed-config-file/with-malformed-config-file.csproj'; const contents = Fixtures.get(packageFile); await expect( extractPackageFile(contents, packageFile, config), ).resolves.toEqual({ deps: [ { currentValue: '4.5.0', datasource: 'nuget', depName: 'Autofac', depType: 'nuget', }, ], packageFileVersion: '0.1.0', }); }); it('handles NuGet.config without package sources', async () => { const packageFile = 'without-package-sources/without-package-sources.csproj'; const contents = Fixtures.get(packageFile); await expect( extractPackageFile(contents, packageFile, config), ).resolves.toEqual({ deps: [ { currentValue: '4.5.0', datasource: 'nuget', depName: 'Autofac', depType: 'nuget', }, ], packageFileVersion: '0.1.0', }); }); it('handles NuGet.config with whitespaces in package source keys', async () => { const packageFile = 'with-whitespaces/with-whitespaces.csproj'; const contents = Fixtures.get(packageFile); await expect( extractPackageFile(contents, packageFile, config), ).resolves.toEqual({ deps: [ { currentValue: '12.0.3', datasource: 'nuget', depName: 'Newtonsoft.Json', depType: 'nuget', registryUrls: [ 'https://api.nuget.org/v3/index.json#protocolVersion=3', 'https://my.myget.org/F/my/auth/guid/api/v3/index.json', ], }, ], }); }); it('ignores local feed in NuGet.config', async () => { const packageFile = 'with-local-feed-in-config-file/with-local-feed-in-config-file.csproj'; const contents = Fixtures.get(packageFile); await expect( extractPackageFile(contents, packageFile, config), ).resolves.toEqual({ deps: [ { currentValue: '4.5.0', datasource: 'nuget', depName: 'Autofac', depType: 'nuget', registryUrls: ['https://contoso.com/packages/'], }, ], packageFileVersion: '0.1.0', }); }); it('extracts registry URLs independently', async () => { const packageFile = 'multiple-package-files/one/one.csproj'; const contents = Fixtures.get(packageFile); const otherPackageFile = 'multiple-package-files/two/two.csproj'; const otherContents = Fixtures.get(otherPackageFile); await expect( extractPackageFile(contents, packageFile, config), ).resolves.toEqual({ deps: [ { currentValue: '4.5.0', datasource: 'nuget', depName: 'Autofac', depType: 'nuget', registryUrls: [ 'https://api.nuget.org/v3/index.json', 'https://example.org/one', ], }, ], packageFileVersion: '0.1.0', }); await expect( extractPackageFile(otherContents, otherPackageFile, config), ).resolves.toEqual({ deps: [ { currentValue: '4.5.0', datasource: 'nuget', depName: 'Autofac', depType: 'nuget', registryUrls: [ 'https://api.nuget.org/v3/index.json', 'https://example.org/two', ], }, ], packageFileVersion: '0.2.0', }); }); it('extracts msbuild-sdks from global.json', async () => { const packageFile = 'msbuild-sdk-files/global.json'; const contents = Fixtures.get(packageFile); await expect( extractPackageFile(contents, packageFile, config), ).resolves.toEqual({ deps: [ { currentValue: '5.0.302', depName: 'dotnet-sdk', depType: 'dotnet-sdk', datasource: DotnetVersionDatasource.id, }, { currentValue: '0.2.0', datasource: 'nuget', depName: 'YoloDev.Sdk', depType: 'msbuild-sdk', }, ], extractedConstraints: { 'dotnet-sdk': '5.0.302' }, }); }); it('extracts dotnet-sdk from global.json', async () => { const packageFile = 'msbuild-sdk-files/global.1.json'; const contents = Fixtures.get(packageFile); await expect( extractPackageFile(contents, 'global.json', config), ).resolves.toEqual({ deps: [ { currentValue: '5.0.302', depName: 'dotnet-sdk', depType: 'dotnet-sdk', datasource: DotnetVersionDatasource.id, }, ], extractedConstraints: { 'dotnet-sdk': '5.0.302' }, }); }); it('handles malformed global.json', async () => { const packageFile = 'msbuild-sdk-files/invalid-json/global.json'; const contents = Fixtures.get(packageFile); await expect( extractPackageFile(contents, packageFile, config), ).resolves.toBeNull(); }); it('handles not-a-nuget global.json', async () => { const packageFile = 'msbuild-sdk-files/not-nuget/global.json'; const contents = Fixtures.get(packageFile); await expect( extractPackageFile(contents, packageFile, config), ).resolves.toBeNull(); }); describe('.config/dotnet-tools.json', () => { const packageFile = '.config/dotnet-tools.json'; const contents = Fixtures.get('dotnet-tools.json'); it('works', async () => { await expect( extractPackageFile(contents, packageFile, config), ).resolves.toEqual({ deps: [ { currentValue: '2.0.0', datasource: 'nuget', depName: 'minver-cli', depType: 'nuget', versioning: 'semver', }, ], }); }); it('with-config', async () => { await expect( extractPackageFile( contents, `with-config-file/${packageFile}`, config, ), ).resolves.toEqual({ deps: [ { currentValue: '2.0.0', datasource: 'nuget', depName: 'minver-cli', depType: 'nuget', registryUrls: [ 'https://api.nuget.org/v3/index.json#protocolVersion=3', 'https://contoso.com/packages/', ], versioning: 'semver', }, ], }); }); it('wrong version', async () => { await expect( extractPackageFile( contents.replace('"version": 1,', '"version": 2,'), packageFile, config, ), ).resolves.toBeNull(); }); it('returns null for no deps', async () => { await expect( extractPackageFile('{"version": 1}', packageFile, config), ).resolves.toBeNull(); }); it('does not throw', async () => { await expect( extractPackageFile('{{', packageFile, config), ).resolves.toBeNull(); }); }); describe('single-csharp-file', () => { it('reads sdk and package directives', async () => { const packageFile = 'single-csharp-file/singlefile.cs'; const contents = codeBlock` #:sdk Some.Sdk@6.0.0 #:package Some.NuGet.Package@3.0.1 // https://devblogs.microsoft.com/dotnet/announcing-dotnet-run-app/ Console.WriteLine("Hello World!"); `; await expect( extractPackageFile(contents, packageFile, config), ).resolves.toEqual({ deps: [ { datasource: 'nuget', currentValue: '6.0.0', depName: 'Some.Sdk', depType: 'msbuild-sdk', }, { datasource: 'nuget', currentValue: '3.0.1', depName: 'Some.NuGet.Package', depType: 'nuget', }, ], }); }); }); describe('single-csharp-file-nuget', () => { it('calls applyRegistries to honor nuget.config files if present', async () => { const packageFile = 'single-csharp-file-nuget/singlefile.cs'; const contents = codeBlock` #:sdk Some.Sdk@6.0.0 #:package Some.NuGet.Package@3.0.1 // https://devblogs.microsoft.com/dotnet/announcing-dotnet-run-app/ Console.WriteLine("Hello World!"); `; const applyRegistriesSpy = vi .spyOn(nugetExtractUtil, 'applyRegistries') .mockImplementation((deps: any) => deps); await extractPackageFile(contents, packageFile, config); expect(applyRegistriesSpy).toHaveBeenCalled(); }); }); }); });