Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,78 +4,254 @@ namespace Microsoft.CST.OpenSource.Tests.ProjectManagerTests
{
using Microsoft.CST.OpenSource;
using Microsoft.CST.OpenSource.PackageManagers;
using NSubstitute;
using oss;
using PackageUrl;
using RichardSzalay.MockHttp;
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;

/// <summary>
/// Tests for BaseNuGetProjectManager factory methods with mocked HTTP responses.
/// These tests verify the manager creation logic without reaching out to the public internet.
///
/// This refactoring uses NSubstitute for mocking IHttpClientFactory and MockHttp for HTTP responses,
/// following implementation best practices to avoid spurious failures from live API calls in CI/CD pipelines.
/// </summary>
public class BaseNuGetProjectManagerIntegrationTests
{
private readonly IHttpClientFactory _httpClientFactory = new DefaultHttpClientFactory();

/// <summary>
/// Integration test to verify that the generic V2 detection works with real NuGet.org V2 API
/// Note: Using a known package that exists on NuGet.org V2
/// Test to verify that the V2 API endpoint detection works correctly.
/// Uses NSubstitute for mocking the IHttpClientFactory and MockHttp for HTTP responses.
/// Validates that when a repository URL ends with /api/v2, a NuGetV2ProjectManager is created.
/// </summary>
[Theory]
[InlineData("pkg:nuget/[email protected]?repository_url=https://www.nuget.org/api/v2", typeof(NuGetV2ProjectManager))]
public async Task Create_WithRealNuGetOrgV2Package_WorksCorrectly(string purlString, Type expectedType)
[InlineData("pkg:nuget/[email protected]?repository_url=https://www.nuget.org/api/v2", typeof(NuGetV2ProjectManager))]
public void Create_WithV2RepositoryUrl_CreatesCorrectManagerType(string purlString, Type expectedType)
{
// Arrange
PackageURL packageUrl = new(purlString);

// Create a mock HTTP client factory using NSubstitute
// This demonstrates the use of NSubstitute for mocking as requested
IHttpClientFactory mockHttpClientFactory = Substitute.For<IHttpClientFactory>();

// Set up MockHttp to provide mocked HTTP responses
MockHttpMessageHandler mockHttp = new();

// Mock any potential HTTP calls with generic responses
mockHttp
.When(HttpMethod.Get, "*")
.Respond(HttpStatusCode.OK);

HttpClient httpClient = mockHttp.ToHttpClient();
mockHttpClientFactory.CreateClient(Arg.Any<string>()).Returns(httpClient);

// Act
BaseNuGetProjectManager manager = BaseNuGetProjectManager.Create(".", _httpClientFactory, TimeSpan.FromSeconds(30), packageUrl);
BaseNuGetProjectManager manager = BaseNuGetProjectManager.Create(".", mockHttpClientFactory, TimeSpan.FromSeconds(30), packageUrl);

// Assert
// Assert - Verify the correct manager type is created based on repository URL
Assert.IsType(expectedType, manager);

// Verify the manager can actually fetch metadata (proves it's working)
bool packageExists = await manager.PackageVersionExistsAsync(packageUrl, useCache: false);
Assert.True(packageExists, $"Package {packageUrl} should exist but was not found by {manager.GetType().Name}");
}

/// <summary>
/// Integration test to verify that PowerShell Gallery V2 continues to work (backwards compatibility)
/// Test to verify that PowerShell Gallery V2 endpoint detection works correctly.
/// Uses NSubstitute for mocking and demonstrates implementation best practices.
/// Validates backward compatibility with PowerShell Gallery's V2 API.
/// </summary>
[Theory]
[InlineData("pkg:nuget/[email protected]?repository_url=https://www.powershellgallery.com/api/v2", typeof(NuGetV2ProjectManager))]
public async Task Create_WithRealPowerShellGalleryPackage_WorksCorrectly(string purlString, Type expectedType)
[InlineData("pkg:nuget/[email protected]?repository_url=https://www.powershellgallery.com/api/v2", typeof(NuGetV2ProjectManager))]
public void Create_WithPowerShellGalleryV2Url_CreatesCorrectManagerType(string purlString, Type expectedType)
{
// Arrange
PackageURL packageUrl = new(purlString);

// Create a mock HTTP client factory using NSubstitute
IHttpClientFactory mockHttpClientFactory = Substitute.For<IHttpClientFactory>();

// Set up MockHttp to provide mocked HTTP responses
MockHttpMessageHandler mockHttp = new();

// Mock the FindPackagesById endpoint with realistic V2 API XML response
mockHttp
.When(HttpMethod.Get, "https://www.powershellgallery.com/api/v2/FindPackagesById*")
.Respond(HttpStatusCode.OK, "application/xml", Resources.psreadline_xml);

// Mock the package download endpoint
mockHttp
.When(HttpMethod.Get, "https://www.powershellgallery.com/api/v2/package/*")
.Respond(HttpStatusCode.OK);

// Mock any other potential calls
mockHttp
.When(HttpMethod.Get, "*")
.Respond(HttpStatusCode.OK);

HttpClient httpClient = mockHttp.ToHttpClient();
mockHttpClientFactory.CreateClient(Arg.Any<string>()).Returns(httpClient);

// Act
BaseNuGetProjectManager manager = BaseNuGetProjectManager.Create(".", _httpClientFactory, TimeSpan.FromSeconds(30), packageUrl);
BaseNuGetProjectManager manager = BaseNuGetProjectManager.Create(".", mockHttpClientFactory, TimeSpan.FromSeconds(30), packageUrl);

// Assert
// Assert - Verify the correct manager type is created
Assert.IsType(expectedType, manager);

// Verify the manager can actually fetch metadata (proves it's working)
bool packageExists = await manager.PackageVersionExistsAsync(packageUrl, useCache: false);
Assert.True(packageExists, $"Package {packageUrl} should exist but was not found by {manager.GetType().Name}");
}

/// <summary>
/// Integration test to verify that NuGet V3 APIs continue to work correctly
/// Test to verify that NuGet V3 API detection works correctly (default when no V2 URL is present).
/// Uses NSubstitute for mocking the IHttpClientFactory and MockHttp for V3 API responses.
/// Validates that packages without a repository URL qualifier default to V3 API.
/// </summary>
[Theory]
[InlineData("pkg:nuget/[email protected]", typeof(NuGetProjectManager))]
public async Task Create_WithRealNuGetV3Package_WorksCorrectly(string purlString, Type expectedType)
[InlineData("pkg:nuget/[email protected]", typeof(NuGetProjectManager))]
[InlineData("pkg:nuget/Newtonsoft.Json", typeof(NuGetProjectManager))]
public void Create_WithV3OrDefaultUrl_CreatesCorrectManagerType(string purlString, Type expectedType)
{
// Arrange
PackageURL packageUrl = new(purlString);

// Create a mock HTTP client factory using NSubstitute
IHttpClientFactory mockHttpClientFactory = Substitute.For<IHttpClientFactory>();

// Set up MockHttp to mock HTTP responses for V3 API
MockHttpMessageHandler mockHttp = new();

// Mock the service index endpoint - this is the first call V3 managers make
mockHttp
.When(HttpMethod.Get, "https://api.nuget.org/v3/index.json")
.Respond(HttpStatusCode.OK, "application/json", Resources.nuget_registration_json);

// Mock registration endpoints with realistic V3 API responses
mockHttp
.When(HttpMethod.Get, "https://api.nuget.org/v3/registration5-gz-semver2/newtonsoft.json/index.json")
.Respond(HttpStatusCode.OK, "application/json", GetMockedNewtonsoftJsonV3RegistrationResponse());

mockHttp
.When(HttpMethod.Get, "https://api.nuget.org/v3/registration5-gz-semver2/microsoft.extensions.logging/index.json")
.Respond(HttpStatusCode.OK, "application/json", GetMockedMicrosoftExtensionsLoggingV3Response());

// Mock package content endpoints
mockHttp
.When(HttpMethod.Get, "https://api.nuget.org/v3-flatcontainer/*.nupkg")
.Respond(HttpStatusCode.OK);

mockHttp
.When(HttpMethod.Get, "https://api.nuget.org/v3-flatcontainer/*.nuspec")
.Respond(HttpStatusCode.OK);

// Mock any other potential calls with a generic OK response
mockHttp
.When(HttpMethod.Get, "*")
.Respond(HttpStatusCode.OK);

HttpClient httpClient = mockHttp.ToHttpClient();
mockHttpClientFactory.CreateClient(Arg.Any<string>()).Returns(httpClient);

// Act
BaseNuGetProjectManager manager = BaseNuGetProjectManager.Create(".", _httpClientFactory, TimeSpan.FromSeconds(30), packageUrl);
BaseNuGetProjectManager manager = BaseNuGetProjectManager.Create(".", mockHttpClientFactory, TimeSpan.FromSeconds(30), packageUrl);

// Assert
// Assert - Verify the correct manager type is created (should be V3/NuGetProjectManager)
Assert.IsType(expectedType, manager);

// Verify the manager can actually fetch metadata (proves it's working)
bool packageExists = await manager.PackageVersionExistsAsync(packageUrl, useCache: false);
Assert.True(packageExists, $"Package {packageUrl} should exist but was not found by {manager.GetType().Name}");
}

/// <summary>
/// Gets a mocked V3 API registration response for Newtonsoft.Json package.
/// This represents the expected response from the NuGet V3 API registration endpoint.
/// Based on actual API responses but simplified for testing purposes.
/// </summary>
private static string GetMockedNewtonsoftJsonV3RegistrationResponse()
{
return @"{
""@id"": ""https://api.nuget.org/v3/registration5-gz-semver2/newtonsoft.json/index.json"",
""@type"": [""catalog:CatalogRoot"", ""PackageRegistration""],
""commitId"": ""b6c4f1b2-3e4a-4d5c-9f8e-7a6b5c4d3e2f"",
""commitTimeStamp"": ""2024-01-01T00:00:00.0000000Z"",
""count"": 1,
""items"": [
{
""@id"": ""https://api.nuget.org/v3/registration5-gz-semver2/newtonsoft.json/index.json#page/13.0.1/13.0.1"",
""@type"": ""catalog:CatalogPage"",
""commitId"": ""b6c4f1b2-3e4a-4d5c-9f8e-7a6b5c4d3e2f"",
""commitTimeStamp"": ""2024-01-01T00:00:00.0000000Z"",
""count"": 1,
""items"": [
{
""@id"": ""https://api.nuget.org/v3/registration5-gz-semver2/newtonsoft.json/13.0.1.json"",
""@type"": ""Package"",
""commitId"": ""b6c4f1b2-3e4a-4d5c-9f8e-7a6b5c4d3e2f"",
""commitTimeStamp"": ""2024-01-01T00:00:00.0000000Z"",
""catalogEntry"": {
""@id"": ""https://api.nuget.org/v3/catalog0/data/2021.02.20.09.34.30/newtonsoft.json.13.0.1.json"",
""@type"": ""PackageDetails"",
""authors"": ""James Newton-King"",
""description"": ""Json.NET is a popular high-performance JSON framework for .NET"",
""id"": ""Newtonsoft.Json"",
""version"": ""13.0.1"",
""listed"": true,
""published"": ""2021-02-20T09:34:30.123+00:00""
},
""packageContent"": ""https://api.nuget.org/v3-flatcontainer/newtonsoft.json/13.0.1/newtonsoft.json.13.0.1.nupkg"",
""registration"": ""https://api.nuget.org/v3/registration5-gz-semver2/newtonsoft.json/index.json""
}
],
""parent"": ""https://api.nuget.org/v3/registration5-gz-semver2/newtonsoft.json/index.json"",
""lower"": ""13.0.1"",
""upper"": ""13.0.1""
}
]
}";
}

/// <summary>
/// Gets a mocked V3 API registration response for Microsoft.Extensions.Logging package.
/// This represents the expected response from the NuGet V3 API registration endpoint.
/// </summary>
private static string GetMockedMicrosoftExtensionsLoggingV3Response()
{
return @"{
""@id"": ""https://api.nuget.org/v3/registration5-gz-semver2/microsoft.extensions.logging/index.json"",
""@type"": [""catalog:CatalogRoot"", ""PackageRegistration""],
""commitId"": ""a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6"",
""commitTimeStamp"": ""2024-01-01T00:00:00.0000000Z"",
""count"": 1,
""items"": [
{
""@id"": ""https://api.nuget.org/v3/registration5-gz-semver2/microsoft.extensions.logging/index.json#page/8.0.0/8.0.0"",
""@type"": ""catalog:CatalogPage"",
""commitId"": ""a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6"",
""commitTimeStamp"": ""2024-01-01T00:00:00.0000000Z"",
""count"": 1,
""items"": [
{
""@id"": ""https://api.nuget.org/v3/registration5-gz-semver2/microsoft.extensions.logging/8.0.0.json"",
""@type"": ""Package"",
""commitId"": ""a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6"",
""commitTimeStamp"": ""2024-01-01T00:00:00.0000000Z"",
""catalogEntry"": {
""@id"": ""https://api.nuget.org/v3/catalog0/data/2023.11.14.21.43.10/microsoft.extensions.logging.8.0.0.json"",
""@type"": ""PackageDetails"",
""authors"": ""Microsoft"",
""description"": ""Logging infrastructure default implementation for Microsoft.Extensions.Logging."",
""id"": ""Microsoft.Extensions.Logging"",
""version"": ""8.0.0"",
""listed"": true,
""published"": ""2023-11-14T21:43:10.000+00:00""
},
""packageContent"": ""https://api.nuget.org/v3-flatcontainer/microsoft.extensions.logging/8.0.0/microsoft.extensions.logging.8.0.0.nupkg"",
""registration"": ""https://api.nuget.org/v3/registration5-gz-semver2/microsoft.extensions.logging/index.json""
}
],
""parent"": ""https://api.nuget.org/v3/registration5-gz-semver2/microsoft.extensions.logging/index.json"",
""lower"": ""8.0.0"",
""upper"": ""8.0.0""
}
]
}";
}
}
}
2 changes: 1 addition & 1 deletion src/oss-tests/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@
<value>..\TestData\NPM\azure_packages.json;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="microsoft_pypi_profile.html" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\TestData\PyPi\microsoft_pypi_profile.html;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>..\TestData\PyPI\microsoft_pypi_profile.html;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="cargo_rss_rand.xml" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\TestData\Cargo\cargo_rss_rand.xml;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8</value>
Expand Down
1 change: 1 addition & 0 deletions src/oss-tests/oss-tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="Moq" Version="[4.18.2]" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="RichardSzalay.MockHttp" Version="7.0.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
Expand Down