Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tests for pull request #39 Added license handling using Github API when possible #41

Merged
merged 6 commits into from
Nov 26, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
113 changes: 113 additions & 0 deletions CycloneDX.Tests/GithubServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// This file is part of the CycloneDX Tool for .NET
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Copyright (c) Steve Springett. All Rights Reserved.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Xunit;
using RichardSzalay.MockHttp;
using CycloneDX.Models;
using CycloneDX.Services;

namespace CycloneDX.Tests
{
public class GithubServiceTests
{
[Fact]
public async Task GitLicence_FromMasterBranch()
{
var mockResponseContent = @"{
""license"": {
""spdx_id"": ""LicenseSpdxId"",
""name"": ""Test License"",
}
}";
var mockHttp = new MockHttpMessageHandler();
mockHttp.When("https://api.github.com/repos/CycloneDX/cyclonedx-dotnet/license?ref=master")
.Respond("application/json", mockResponseContent);
var client = mockHttp.ToHttpClient();
var githubService = new GithubService(client);

var license = await githubService.GetLicenseAsync("https://github.com/CycloneDX/cyclonedx-dotnet/blob/master/LICENSE");

Assert.Equal("LicenseSpdxId", license.Id);
Assert.Equal("Test License", license.Name);
}

[Fact]
public async Task GitLicence_FromVersionTag()
{
var mockResponseContent = @"{
""license"": {
""spdx_id"": ""LicenseSpdxId"",
""name"": ""Test License"",
}
}";
var mockHttp = new MockHttpMessageHandler();
mockHttp.When("https://api.github.com/repos/CycloneDX/cyclonedx-dotnet/license?ref=v1.2.3")
.Respond("application/json", mockResponseContent);
var client = mockHttp.ToHttpClient();
var githubService = new GithubService(client);

var license = await githubService.GetLicenseAsync("https://github.com/CycloneDX/cyclonedx-dotnet/blob/v1.2.3/LICENSE");

Assert.Equal("LicenseSpdxId", license.Id);
Assert.Equal("Test License", license.Name);
}

[Fact]
public async Task GitLicence_FromMarkdownExtensionLicense()
{
var mockResponseContent = @"{
""license"": {
""spdx_id"": ""LicenseSpdxId"",
""name"": ""Test License"",
}
}";
var mockHttp = new MockHttpMessageHandler();
mockHttp.When("https://api.github.com/repos/CycloneDX/cyclonedx-dotnet/license?ref=master")
.Respond("application/json", mockResponseContent);
var client = mockHttp.ToHttpClient();
var githubService = new GithubService(client);

var license = await githubService.GetLicenseAsync("https://github.com/CycloneDX/cyclonedx-dotnet/blob/master/LICENSE.md");

Assert.Equal("LicenseSpdxId", license.Id);
Assert.Equal("Test License", license.Name);
}

[Fact]
public async Task GitLicence_FromGithubUserContent()
{
var mockResponseContent = @"{
""license"": {
""spdx_id"": ""LicenseSpdxId"",
""name"": ""Test License"",
}
}";
var mockHttp = new MockHttpMessageHandler();
mockHttp.When("https://api.github.com/repos/CycloneDX/cyclonedx-dotnet/license?ref=master")
.Respond("application/json", mockResponseContent);
var client = mockHttp.ToHttpClient();
var githubService = new GithubService(client);

var license = await githubService.GetLicenseAsync("https://raw.githubusercontent.com/CycloneDX/cyclonedx-dotnet/master/LICENSE");

Assert.Equal("LicenseSpdxId", license.Id);
Assert.Equal("Test License", license.Name);
}
}
}
64 changes: 51 additions & 13 deletions CycloneDX.Tests/NugetServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Xunit;
using Moq;
using RichardSzalay.MockHttp;
using CycloneDX.Models;
using CycloneDX.Services;

namespace CycloneDX.Tests
Expand All @@ -40,7 +42,7 @@ public async Task GetComponent_ReturnsName()
mockHttp.When("https://api.nuget.org/v3-flatcontainer/PackageName/1.2.3/PackageName.nuspec")
.Respond("application/xml", mockResponseContent);
var client = mockHttp.ToHttpClient();
var nugetService = new NugetService(client);
var nugetService = new NugetService(client, new Mock<IGithubService>().Object);

var component = await nugetService.GetComponentAsync("PackageName", "1.2.3");

Expand All @@ -60,7 +62,7 @@ public async Task GetComponent_ReturnsVersion()
mockHttp.When("https://api.nuget.org/v3-flatcontainer/PackageName/1.2.3/PackageName.nuspec")
.Respond("application/xml", mockResponseContent);
var client = mockHttp.ToHttpClient();
var nugetService = new NugetService(httpClient: client);
var nugetService = new NugetService(client, new Mock<IGithubService>().Object);

var component = await nugetService.GetComponentAsync("PackageName", "1.2.3");

Expand All @@ -81,7 +83,7 @@ public async Task GetComponent_ReturnsPurl()
mockHttp.When("https://api.nuget.org/v3-flatcontainer/PackageName/1.2.3/PackageName.nuspec")
.Respond("application/xml", mockResponseContent);
var client = mockHttp.ToHttpClient();
var nugetService = new NugetService(httpClient: client);
var nugetService = new NugetService(client, new Mock<IGithubService>().Object);

var component = await nugetService.GetComponentAsync("PackageName", "1.2.3");

Expand All @@ -101,7 +103,7 @@ public async Task GetComponent_ReturnsPublisher()
mockHttp.When("https://api.nuget.org/v3-flatcontainer/PackageName/1.2.3/PackageName.nuspec")
.Respond("application/xml", mockResponseContent);
var client = mockHttp.ToHttpClient();
var nugetService = new NugetService(httpClient: client);
var nugetService = new NugetService(client, new Mock<IGithubService>().Object);

var component = await nugetService.GetComponentAsync("PackageName", "1.2.3");

Expand All @@ -121,7 +123,7 @@ public async Task GetComponent_ReturnsCopyright()
mockHttp.When("https://api.nuget.org/v3-flatcontainer/PackageName/1.2.3/PackageName.nuspec")
.Respond("application/xml", mockResponseContent);
var client = mockHttp.ToHttpClient();
var nugetService = new NugetService(httpClient: client);
var nugetService = new NugetService(client, new Mock<IGithubService>().Object);

var component = await nugetService.GetComponentAsync("PackageName", "1.2.3");

Expand All @@ -141,7 +143,7 @@ public async Task GetComponent_ReturnsDescription()
mockHttp.When("https://api.nuget.org/v3-flatcontainer/PackageName/1.2.3/PackageName.nuspec")
.Respond("application/xml", mockResponseContent);
var client = mockHttp.ToHttpClient();
var nugetService = new NugetService(httpClient: client);
var nugetService = new NugetService(client, new Mock<IGithubService>().Object);

var component = await nugetService.GetComponentAsync("PackageName", "1.2.3");

Expand All @@ -161,7 +163,7 @@ public async Task GetComponent_WithoutSummaryReturns_NugetDescription()
mockHttp.When("https://api.nuget.org/v3-flatcontainer/PackageName/1.2.3/PackageName.nuspec")
.Respond("application/xml", mockResponseContent);
var client = mockHttp.ToHttpClient();
var nugetService = new NugetService(httpClient: client);
var nugetService = new NugetService(client, new Mock<IGithubService>().Object);

var component = await nugetService.GetComponentAsync("PackageName", "1.2.3");

Expand All @@ -181,7 +183,7 @@ public async Task GetComponent_WithoutDescription_ReturnsNugetTitle()
mockHttp.When("https://api.nuget.org/v3-flatcontainer/PackageName/1.2.3/PackageName.nuspec")
.Respond("application/xml", mockResponseContent);
var client = mockHttp.ToHttpClient();
var nugetService = new NugetService(httpClient: client);
var nugetService = new NugetService(client, new Mock<IGithubService>().Object);

var component = await nugetService.GetComponentAsync("PackageName", "1.2.3");

Expand All @@ -201,7 +203,7 @@ public async Task GetComponent_ReturnsLicense()
mockHttp.When("https://api.nuget.org/v3-flatcontainer/PackageName/1.2.3/PackageName.nuspec")
.Respond("application/xml", mockResponseContent);
var client = mockHttp.ToHttpClient();
var nugetService = new NugetService(httpClient: client);
var nugetService = new NugetService(client, new Mock<IGithubService>().Object);

var component = await nugetService.GetComponentAsync("PackageName", "1.2.3");

Expand All @@ -225,7 +227,7 @@ public async Task GetComponent_ReturnsLicenseUrl()
mockHttp.When("https://api.nuget.org/v3-flatcontainer/PackageName/1.2.3/PackageName.nuspec")
.Respond("application/xml", mockResponseContent);
var client = mockHttp.ToHttpClient();
var nugetService = new NugetService(httpClient: client);
var nugetService = new NugetService(client, new Mock<IGithubService>().Object);

var component = await nugetService.GetComponentAsync("PackageName", "1.2.3");

Expand All @@ -250,7 +252,7 @@ public async Task GetComponent_ReturnsDependencies()
mockHttp.When("https://api.nuget.org/v3-flatcontainer/PackageName/1.2.3/PackageName.nuspec")
.Respond("application/xml", mockResponseContent);
var client = mockHttp.ToHttpClient();
var nugetService = new NugetService(httpClient: client);
var nugetService = new NugetService(client, new Mock<IGithubService>().Object);

var component = await nugetService.GetComponentAsync("PackageName", "1.2.3");
var orderedDependencies = new List<Models.NugetPackage>(component.Dependencies);
Expand Down Expand Up @@ -278,7 +280,7 @@ public async Task GetImaginaryComponent_ReturnsComponent()
mockHttp.When("https://api.nuget.org/v3-flatcontainer/PackageName/1.2.3/PackageName.nuspec")
.Respond(System.Net.HttpStatusCode.NotFound);
var client = mockHttp.ToHttpClient();
var nugetService = new NugetService(httpClient: client);
var nugetService = new NugetService(client, new Mock<IGithubService>().Object);

var component = await nugetService.GetComponentAsync("PackageName", "1.2.3");

Expand All @@ -291,11 +293,47 @@ public async Task GetImplicitVersionComponent_ReturnsNull()
{
var mockHttp = new MockHttpMessageHandler();
var client = mockHttp.ToHttpClient();
var nugetService = new NugetService(httpClient: client);
var nugetService = new NugetService(client, new Mock<IGithubService>().Object);

var component = await nugetService.GetComponentAsync("PackageName", "");

Assert.Null(component);
}

[Fact]
public async Task GetComponent_WithGithubLicense_ReturnsGithubLicense()
{
var mockResponseContent = @"<?xml version=""1.0"" encoding=""utf-8""?>
<package xmlns=""http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"">
<metadata>
<licenseUrl>https://www.example.com/license</licenseUrl>
</metadata>
</package>";
var mockHttp = new MockHttpMessageHandler();
mockHttp.When("https://api.nuget.org/v3-flatcontainer/PackageName/1.2.3/PackageName.nuspec")
.Respond("application/xml", mockResponseContent);
var client = mockHttp.ToHttpClient();
var mockGithubService = new Mock<IGithubService>();
mockGithubService
.Setup(service => service.GetLicenseAsync(It.IsAny<string>()))
.ReturnsAsync(new Models.License
{
Id = "TestLicenseId",
Name = "Test License",
Url = "https://www.example.com/LICENSE"
});
var nugetService = new NugetService(
client,
mockGithubService.Object);

var component = await nugetService.GetComponentAsync("PackageName", "1.2.3");

Assert.Collection(component.Licenses,
item => {
Assert.Equal("TestLicenseId", item.Id);
Assert.Equal("Test License", item.Name);
Assert.Equal("https://www.example.com/LICENSE", item.Url);
});
}
}
}
3 changes: 2 additions & 1 deletion CycloneDX/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ async Task<int> OnExecute() {

// instantiate services
var fileDiscoveryService = new FileDiscoveryService(Program.fileSystem);
var nugetService = new NugetService(Program.httpClient, baseUrl);
var githubService = new GithubService(Program.httpClient);
var nugetService = new NugetService(Program.httpClient, githubService, baseUrl);
var packagesFileService = new PackagesFileService(Program.fileSystem);
var projectFileService = new ProjectFileService(Program.fileSystem);
var solutionFileService = new SolutionFileService(Program.fileSystem);
Expand Down
44 changes: 23 additions & 21 deletions CycloneDX/Services/GithubService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
using CycloneDX.Models;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
Expand All @@ -27,54 +28,55 @@ namespace CycloneDX.Services

public interface IGithubService
{
Task<License> GetLicenseAsync(string licenseUrl, string version);
Task<License> GetLicenseAsync(string licenseUrl);
}

class GithubService : IGithubService
public class GithubService : IGithubService
{

private string _baseUrl;
private string _baseUrl = "https://api.github.com/";
private HttpClient _httpClient;
private Regex _repositoryIdRegex = new Regex(@"^https?\:\/\/((github\.com)|(raw\.githubusercontent\.com))\/(?<repositoryId>[^\/]+\/[^\/]+)\/(blob\/)?master\/LICENSE.*$");
private List<Regex> _githubRepositoryRegexes = new List<Regex>
{
new Regex(@"^https?\:\/\/github\.com\/(?<repositoryId>[^\/]+\/[^\/]+)\/blob\/(?<refSpec>[^\/]+)\/LICENSE(.md)?$"),
new Regex(@"^https?\:\/\/raw\.githubusercontent\.com\/(?<repositoryId>[^\/]+\/[^\/]+)\/(?<refSpec>[^\/]+)\/LICENSE(.md)?$"),
Copy link
Contributor

@ST-Apps ST-Apps Nov 20, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% sure about this, but I think I encountered some LICENSE.txt files in the past, so we may want to allow some other extensions too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly the documentation states it needs to be LICENSE or LICENSE.md. But it also supports LICENSE.txt. Good pick up @ST-Apps.

But what doesn't work, unfortunately, is the ref parameter. Although it's supported, and the response includes the ref in returned urls, the license object is whatever is in master.

I've raised a support request with github about this. But in the meantime I think we'll just return license information when master is referenced. I think that will cover off almost all cases.

};

public GithubService(HttpClient httpClient, string baseUrl = null)
public GithubService(HttpClient httpClient)
{
_httpClient = httpClient;
_baseUrl = baseUrl ?? "https://api.github.com/";
}

/// <summary>
/// Tries to get a license from GitHub.
/// </summary>
/// <param name="licenseUrl">URL for the license file. Supporting both github.com and raw.githubusercontent.com URLs.</param>
/// <param name="version">Version number for the current package.</param>
/// <returns></returns>
public async Task<License> GetLicenseAsync(string licenseUrl, string version)
public async Task<License> GetLicenseAsync(string licenseUrl)
{
if (licenseUrl == null) return null;

// Detect correct repository id starting from URL
var match = _repositoryIdRegex.Match(licenseUrl);
Match match = null;

foreach(var regex in _githubRepositoryRegexes)
{
match = regex.Match(licenseUrl);
if (match.Success) break;
}

// License is not on GitHub, we need to abort
if (!match.Success) return null;
var repositoryId = match.Groups["repositoryId"];
var refSpec = match.Groups["refSpec"];

Console.WriteLine($"Retrieving GitHub license for repository {repositoryId} and version v{version}");
Console.WriteLine($"Retrieving GitHub license for repository {repositoryId} and ref {refSpec}");

// Try getting license for the specified version
var githubLicense = await GetLicenseAsync($"{_baseUrl}repos/{repositoryId}/license?ref=v{version}");
if (githubLicense == null)
{
// Fallback on master version
Console.WriteLine($"No license found on GitHub for repository {repositoryId} and version v{version}, trying to get one from master");

githubLicense = await GetLicenseAsync($"{_baseUrl}repos/{repositoryId}/license");
}
var githubLicense = await GetGithubLicenseAsync($"{_baseUrl}repos/{repositoryId}/license?ref={refSpec}");

// If license is still null, this means we didn't get it neither from ref or from master
if (githubLicense == null) {
Console.WriteLine($"No license found on GitHub for repository {repositoryId}");
Console.WriteLine($"No license found on GitHub for repository {repositoryId} using ref {refSpec}");

return null;
}
Expand All @@ -93,7 +95,7 @@ public async Task<License> GetLicenseAsync(string licenseUrl, string version)
/// </summary>
/// <param name="githubLicenseUrl"></param>
/// <returns></returns>
private async Task<GithubLicenseRoot> GetLicenseAsync(string githubLicenseUrl)
private async Task<GithubLicenseRoot> GetGithubLicenseAsync(string githubLicenseUrl)
{
var githubLicenseRequestMessage = new HttpRequestMessage(HttpMethod.Get, githubLicenseUrl);

Expand Down
Loading