Skip to content

Commit

Permalink
Merge pull request #59 from Shaderbug/checksum-validation
Browse files Browse the repository at this point in the history
feat: implement checksum validation
  • Loading branch information
definitelyokay committed May 2, 2024
2 parents d119ad6 + 0b1053b commit 205bffd
Show file tree
Hide file tree
Showing 15 changed files with 651 additions and 16 deletions.
7 changes: 7 additions & 0 deletions GodotEnv.Tests/Chickensoft.GodotEnv.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,11 @@
<ProjectReference Include="../GodotEnv/Chickensoft.GodotEnv.csproj" />
</ItemGroup>

<ItemGroup>
<None Remove="src\features\godot\domain\data\godot-4.3-dev5.json" />
<EmbeddedResource Include="src\features\godot\domain\data\godot-4.3-dev5.json" />
<None Remove="src\features\godot\domain\data\godot-1.1-stable.json" />
<EmbeddedResource Include="src\features\godot\domain\data\godot-1.1-stable.json" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Chickensoft.GodotEnv.Features.Addons.Commands;
using Chickensoft.GodotEnv.Features.Godot.Domain;
using CliFx.Infrastructure;
using Common.Models;
using Common.Utilities;
using Features.Addons.Commands;
using Features.Godot.Domain;
using Moq;
using Shouldly;
using Xunit;
Expand Down
276 changes: 276 additions & 0 deletions GodotEnv.Tests/src/features/godot/domain/GodotChecksumClientTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
namespace Chickensoft.GodotEnv.Tests.features.godot.domain;

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using Chickensoft.GodotEnv.Features.Godot.Domain;
using Chickensoft.GodotEnv.Features.Godot.Models;
using Common.Clients;
using Moq;
using Xunit;

public class GodotChecksumClientTest {
private static readonly string GODOT_4_3_DEV_5_MACOS_CHECKSUM = "0fdd44c725980c463d86b14aeb47fc41a35ff9005e9df9a9c821168b21d60f845d80313e93c892565daadef04d02c6f6fbb6a9d9a26374db9caa8cd4d9354d7c";

private static readonly string GODOT_ENV_STRING_CHECKSUM =
"3a2e1fa23f9e99ff976803d6fb1283707e015b7904040f604a6a10240c1eba138c6feed88e9ec5db72aee81c6f9b1eba99292e346eab054004e2427a4d4b39b8";

private static string GetChecksumFileUrl(string version) =>
$"https://raw.githubusercontent.com/godotengine/godot-builds/main/releases/godot-{version}.json";

public static IEnumerable<object[]> CorrectChecksumUrlRequestedTestData() {
yield return [new SemanticVersion("1", "2", "3"), false, GetChecksumFileUrl("1.2.3-stable")];
yield return [new SemanticVersion("1", "0", "0"), false, GetChecksumFileUrl("1.0-stable")];
yield return [new SemanticVersion("4", "0", "0", "alpha14"), false, GetChecksumFileUrl("4.0-alpha14")];
yield return [new SemanticVersion("4", "2", "2", "rc1"), false, GetChecksumFileUrl("4.2.2-rc1")];
}

[Theory]
[MemberData(nameof(CorrectChecksumUrlRequestedTestData))]
public async Task CorrectChecksumUrlRequested(
SemanticVersion version,
bool isDotNetVersion,
string expectedChecksumUrl
) {
var archive = new GodotCompressedArchive(
string.Empty,
string.Empty,
version,
isDotNetVersion,
string.Empty
);

var networkClient = new Mock<INetworkClient>();
networkClient.Setup(
client => client.WebRequestGetAsync(
It.IsAny<string>()
))
.ThrowsAsync(new HttpRequestException());

var platform = new Mock<IGodotEnvironment>();

var checksumClient = new GodotChecksumClient(networkClient.Object, platform.Object);

await Assert.ThrowsAsync<HttpRequestException>(async () => await checksumClient.GetExpectedChecksumForArchive(archive));

networkClient.Verify(nc => nc.WebRequestGetAsync(expectedChecksumUrl), Times.Once);
}

public static IEnumerable<object[]> CorrectlyParsedJsonTestData() {
yield return [
false,
"Godot_v4.3-dev5_macos.universal.zip",
GODOT_4_3_DEV_5_MACOS_CHECKSUM
];

yield return [
true,
"Godot_v4.3-dev5_mono_macos.universal.zip",
"18790956c8c12be4458c47aa3b682ccfce4430fc43bd740372940cb5f294988035d2d709af8f05b59db6d0e8f9e36fb998e2b1105608f89d32b6cfef3f77ed36"
];

yield return [
true,
"Godot_v4.3-dev5_mono_win64.zip",
"c53b87f8f5369059fd729605a0e508123289fa02e1ffca2dc53fd97245bc78ade667346856505b821c75821d8720380fcf5e0d337a38bd030e8e05c6858305db"
];

yield return [
false,
"Godot_v4.3-dev5_linux.x86_64.zip",
"800e272ffb8ba92b535f6b17ffe7578273d9fd0b9e56d2b14d1db2eddbdffa3822be8e3f3e76775f1d9c940520a553a41ba1e2f3eb00e49992d03be090a7a022"
];
}


[Theory]
[MemberData(nameof(CorrectlyParsedJsonTestData))]
public async void CorrectlyParsedJson(
bool isDotnetVersion,
string filename,
string expectedChecksum
) {
var networkClient = await GetMockChecksumFileNetworkClient("godot-4.3-dev5.json");

var archive = new GodotCompressedArchive(
string.Empty,
string.Empty,
new SemanticVersion("4", "3", "0", "dev5"),
isDotnetVersion,
string.Empty
);

var platform = new Mock<IGodotEnvironment>();
platform.Setup(
platform => platform.GetInstallerFilename(
It.IsAny<SemanticVersion>(),
It.IsAny<bool>()
)
).Returns(filename);

var checksumClient = new GodotChecksumClient(networkClient.Object, platform.Object);

var checksumFromClient = await checksumClient.GetExpectedChecksumForArchive(archive);

Assert.Equal(expectedChecksum, checksumFromClient);
}

/// <summary>
/// At the time of implementation (2024-04-29) there was no checksum data published
/// for versions below 3.2.2-beta1. This test uses the empty release data for
/// Godot v1.1 to verify that a correct exception is raised.
/// </summary>
[Fact]
public async Task MissingVersionDataRaisesMissingChecksumException() {
const string testDataFilename = "godot-1.1-stable.json";
const string downloadFileName = "Godot_v1.1_stable_win64.exe.zip";
var networkClient = await GetMockChecksumFileNetworkClient(testDataFilename);

var archive = new GodotCompressedArchive(
string.Empty,
downloadFileName,
new SemanticVersion("1", "1", "0"),
false,
string.Empty
);

var platform = new Mock<IGodotEnvironment>();
platform.Setup(
platform => platform.GetInstallerFilename(
It.IsAny<SemanticVersion>(),
It.IsAny<bool>()
)
).Returns(downloadFileName);

var checksumClient = new GodotChecksumClient(networkClient.Object, platform.Object);

var ex = await Assert.ThrowsAsync<MissingChecksumException>(
async () => await checksumClient.GetExpectedChecksumForArchive(archive)
);

var ex2 = await Assert.ThrowsAsync<MissingChecksumException>(
async () => await checksumClient.VerifyArchiveChecksum(archive)
);

Assert.Equal(ex.Message, $"File checksum for {downloadFileName} not present");
Assert.Equal(ex2.Message, $"File checksum for {downloadFileName} not present");

}

/// <summary>
/// Creates a new Mock instance of INetworkClient that returns a JSON response
/// with the contents of a given embedded resource in the data directory to any
/// request.
///
/// If you want to add another resource, you will have to configure it to be an
/// embedded resource.
/// </summary>
/// <param name="responseFilename">Filename from data directory whose </param>
/// <returns>A INetworkClient returning the JSON contents to any request.</returns>
/// <exception cref="FileNotFoundException">Thrown if the embedded resource cannot be found.</exception>
private static async Task<Mock<INetworkClient>> GetMockChecksumFileNetworkClient(string responseFilename) {
var resourceName = $"Chickensoft.GodotEnv.Tests.src.features.godot.domain.data.{responseFilename}";
string godotReleaseJson;
await using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName)
?? throw new FileNotFoundException("Failed to get test release JSON file.")) {
using (var reader = new StreamReader(stream))
{
godotReleaseJson = await reader.ReadToEndAsync();
}
}

var networkClient = new Mock<INetworkClient>();
networkClient.Setup(
client => client.WebRequestGetAsync(
It.IsAny<string>()
))
.ReturnsAsync(() => {
var response = new HttpResponseMessage(HttpStatusCode.OK);
response.Content = new StringContent(godotReleaseJson);
return response;
});
return networkClient;
}

[Fact]
public async void VerifyChecksumComputation() {
var tempFileName = Path.GetTempFileName();

try {
await using (var writer = File.CreateText(tempFileName)) {
await writer.WriteAsync("GodotEnv");
}

var dummyArchive = new GodotCompressedArchive(
"TestFilename",
Path.GetFileName(tempFileName),
new SemanticVersion("1", "0", "0"),
true,
Path.GetDirectoryName(tempFileName) ?? "/"
);

var networkClient = new Mock<INetworkClient>();
var platform = new Mock<IGodotEnvironment>();

var checksumClient = new GodotChecksumClient(networkClient.Object, platform.Object);

var computed = await checksumClient.ComputeChecksumOfArchive(dummyArchive);

var expected =
"3a2e1fa23f9e99ff976803d6fb1283707e015b7904040f604a6a10240c1eba138c6feed88e9ec5db72aee81c6f9b1eba99292e346eab054004e2427a4d4b39b8";

Assert.Equal(expected, computed);
}
finally {
File.Delete(tempFileName);
}
}

[Fact]
public async void IncorrectChecksumThrowsChecksumMismatchException() {
var archiveDirectory = Path.Join(Path.GetTempPath(), "GodotEnvTest" + Guid.NewGuid());
Directory.CreateDirectory(archiveDirectory);

var archiveFileName = "Godot_v4.3-dev5_macos.universal.zip";
var archivePath = Path.Join(archiveDirectory, archiveFileName);

try {
await using (var writer = File.CreateText(archivePath)) {
await writer.WriteAsync("GodotEnv");
}

var networkClient = await GetMockChecksumFileNetworkClient("godot-4.3-dev5.json");

var archive = new GodotCompressedArchive(
string.Empty,
archiveFileName,
new SemanticVersion("4", "3", "0", "dev5"),
false,
Path.GetDirectoryName(archivePath) ?? "/"
);

var platform = new Mock<IGodotEnvironment>();
platform.Setup(
platform => platform.GetInstallerFilename(
It.IsAny<SemanticVersion>(),
It.IsAny<bool>()
)
).Returns(archiveFileName);

var checksumClient = new GodotChecksumClient(networkClient.Object, platform.Object);

var ex = await Assert.ThrowsAsync<ChecksumMismatchException>(
async () => await checksumClient.VerifyArchiveChecksum(archive)
);

Assert.Equal(ex.Message, $"Expected: {GODOT_4_3_DEV_5_MACOS_CHECKSUM}, Actual: {GODOT_ENV_STRING_CHECKSUM}");
}
finally {
File.Delete(archivePath);
}
}
}
15 changes: 8 additions & 7 deletions GodotEnv.Tests/src/features/godot/domain/GodotRepositoryTest.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
namespace Chickensoft.GodotEnv.Tests;

namespace Chickensoft.GodotEnv.Tests.Features.Godot.Domain;

using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Chickensoft.GodotEnv.Common.Clients;
using Chickensoft.GodotEnv.Common.Models;
using Chickensoft.GodotEnv.Common.Utilities;
using Chickensoft.GodotEnv.Features.Godot.Domain;
using Chickensoft.GodotEnv.Features.Godot.Models;
using CliFx.Infrastructure;
using Common.Clients;
using Common.Models;
using Downloader;
using Features.Godot.Domain;
using Features.Godot.Models;
using Moq;
using Xunit;

Expand Down Expand Up @@ -48,6 +47,7 @@ public class GodotRepositoryTest {
.Returns(Task.CompletedTask);

var platform = new Mock<GodotEnvironment>(fileClient.Object, computer.Object);
var checksumClient = new Mock<IGodotChecksumClient>();

var godotRepo = new GodotRepository(
config: new ConfigFile { GodotInstallationsPath = "INSTALLATION_PATH" },
Expand All @@ -56,7 +56,8 @@ public class GodotRepositoryTest {
zipClient: zipClient.Object,
platform: platform.Object,
environmentVariableClient: environmentVariableClient.Object,
processRunner: processRunner.Object
processRunner: processRunner.Object,
checksumClient: checksumClient.Object
);

var executionContext = new Mock<IExecutionContext>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "1.1",
"version": "1.1",
"status": "stable",
"release_date": 1432159200,
"git_reference": "1.1-stable",

"files": [
]
}

0 comments on commit 205bffd

Please sign in to comment.