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
129 changes: 129 additions & 0 deletions src/ImageBuilder.Tests/Commands/BuildOptionsBindingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.CommandLine;
using System.CommandLine.Parsing;
using Microsoft.DotNet.ImageBuilder.Commands;
using Shouldly;
using Xunit;

namespace Microsoft.DotNet.ImageBuilder.Tests.Commands;

public class BuildOptionsBindingTests
{
[Fact]
public void RegistryCredentials_SingleCredential()
{
string[] args = ["--registry-creds", "mcr.microsoft.com=myuser;mypass"];

BuildOptions options = ParseAndBind<BuildOptions>(args);

options.CredentialsOptions.Credentials.ShouldContainKey("mcr.microsoft.com");
options.CredentialsOptions.Credentials["mcr.microsoft.com"].Username.ShouldBe("myuser");
options.CredentialsOptions.Credentials["mcr.microsoft.com"].Password.ShouldBe("mypass");
}

[Fact]
public void RegistryCredentials_MultipleCredentials()
{
string[] args =
[
"--registry-creds", "reg1.io=user1;pass1",
"--registry-creds", "reg2.io=user2;pass2",
];

BuildOptions options = ParseAndBind<BuildOptions>(args);

options.CredentialsOptions.Credentials.Count.ShouldBe(2);
options.CredentialsOptions.Credentials["reg1.io"].Username.ShouldBe("user1");
options.CredentialsOptions.Credentials["reg2.io"].Password.ShouldBe("pass2");
}

[Fact]
public void BuildArgs_ParsesDictionaryValues()
{
string[] args =
[
"--build-arg", "SDK_VERSION=8.0",
"--build-arg", "RUNTIME=aspnet",
];

BuildOptions options = ParseAndBind<BuildOptions>(args);

options.BuildArgs.ShouldContainKeyAndValue("SDK_VERSION", "8.0");
options.BuildArgs.ShouldContainKeyAndValue("RUNTIME", "aspnet");
}

[Fact]
public void Variables_ParsesDictionaryValues()
{
string[] args =
[
"--var", "branch=main",
"--var", "version=8.0",
];

BuildOptions options = ParseAndBind<BuildOptions>(args);

options.Variables.ShouldContainKeyAndValue("branch", "main");
options.Variables.ShouldContainKeyAndValue("version", "8.0");
}

[Fact]
public void BooleanFlags_DefaultToFalse()
{
BuildOptions options = ParseAndBind<BuildOptions>([]);

options.IsPushEnabled.ShouldBeFalse();
options.NoCache.ShouldBeFalse();
options.IsRetryEnabled.ShouldBeFalse();
options.IsSkipPullingEnabled.ShouldBeFalse();
options.IsDryRun.ShouldBeFalse();
}

[Fact]
public void BooleanFlags_SetWhenPresent()
{
string[] args = ["--push", "--no-cache", "--retry"];

BuildOptions options = ParseAndBind<BuildOptions>(args);

options.IsPushEnabled.ShouldBeTrue();
options.NoCache.ShouldBeTrue();
options.IsRetryEnabled.ShouldBeTrue();
}

[Fact]
public void ManifestOption_DefaultsToManifestJson()
{
BuildOptions options = ParseAndBind<BuildOptions>([]);

options.Manifest.ShouldBe("manifest.json");
}

[Fact]
public void ManifestOption_OverriddenWhenSpecified()
{
string[] args = ["--manifest", "custom-manifest.json"];

BuildOptions options = ParseAndBind<BuildOptions>(args);

options.Manifest.ShouldBe("custom-manifest.json");
}

/// <summary>
/// Parses the given args using the real CLI command shape for <typeparamref name="TOptions"/>
/// and binds the result — the same code path used by the production CLI.
/// </summary>
private static TOptions ParseAndBind<TOptions>(string[] args)
where TOptions : Options, new()
{
TOptions options = new();
Command command = new("test", "test");
command.AddOptions(options);

ParseResult parseResult = command.Parse(args);
options.Bind(parseResult);
return options;
}
}
128 changes: 128 additions & 0 deletions src/ImageBuilder.Tests/Commands/GitOptionsBindingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.CommandLine;
using System.CommandLine.Parsing;
using System.Linq;
using Microsoft.DotNet.ImageBuilder.Commands;
using Shouldly;
using Xunit;

namespace Microsoft.DotNet.ImageBuilder.Tests.Commands;

public class GitOptionsBindingTests
{
[Fact]
public void WithGitHubAuth_IsRequired_ProducesErrorWhenNoAuthProvided()
{
GitOptionsBuilder builder = GitOptionsBuilder.Build()
.WithGitHubAuth(isRequired: true);

ParseResult parseResult = ParseGitOptions(builder, []);

parseResult.Errors.ShouldNotBeEmpty(
"Expected a validation error when no GitHub auth is provided but isRequired is true");
}

[Fact]
public void WithGitHubAuth_IsRequired_NoErrorWhenTokenProvided()
{
GitOptionsBuilder builder = GitOptionsBuilder.Build()
.WithGitHubAuth(isRequired: true);

ParseResult parseResult = ParseGitOptions(builder, ["--gh-token", "my-pat"]);

parseResult.Errors.ShouldBeEmpty();
}

[Fact]
public void WithGitHubAuth_IsRequired_NoErrorWhenAppAuthProvided()
{
GitOptionsBuilder builder = GitOptionsBuilder.Build()
.WithGitHubAuth(isRequired: true);

ParseResult parseResult = ParseGitOptions(builder,
[
"--gh-private-key", "base64key",
"--gh-app-client-id", "my-client-id",
]);

parseResult.Errors.ShouldBeEmpty();
}

[Fact]
public void WithGitHubAuth_NotRequired_NoErrorWhenNoAuthProvided()
{
GitOptionsBuilder builder = GitOptionsBuilder.Build()
.WithGitHubAuth(isRequired: false);

ParseResult parseResult = ParseGitOptions(builder, []);

parseResult.Errors.ShouldBeEmpty();
}

[Fact]
public void WithGitHubAuth_CustomDescription_AppliedToTokenOption()
{
string customDescription = "Custom auth description";

GitOptionsBuilder builder = GitOptionsBuilder.Build()
.WithGitHubAuth(description: customDescription);

Command command = BuildCommand(builder);

Option? tokenOption = command.Options
.FirstOrDefault(o => o.Name == "--gh-token");

tokenOption.ShouldNotBeNull();
tokenOption.Description.ShouldBe(customDescription);
}

[Fact]
public void WithGitHubAuth_DefaultDescription_UsedWhenNoneProvided()
{
GitOptionsBuilder builder = GitOptionsBuilder.Build()
.WithGitHubAuth();

Command command = BuildCommand(builder);

Option? tokenOption = command.Options
.FirstOrDefault(o => o.Name == "--gh-token");

tokenOption.ShouldNotBeNull();
tokenOption.Description.ShouldBe("GitHub Personal Access Token (PAT)");
}

/// <summary>
/// Builds a command with the options and validators from the given <see cref="GitOptionsBuilder"/>
/// and parses the provided args.
/// </summary>
private static ParseResult ParseGitOptions(GitOptionsBuilder builder, string[] args)
{
Command command = BuildCommand(builder);
return command.Parse(args);
}

private static Command BuildCommand(GitOptionsBuilder builder)
{
Command command = new("test", "test");

foreach (Option option in builder.GetCliOptions())
{
command.Add(option);
}

foreach (Argument argument in builder.GetCliArguments())
{
command.Add(argument);
}

foreach (Action<CommandResult> validator in builder.GetValidators())
{
command.Validators.Add(validator);
}

return command;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.CommandLine;
using System.CommandLine.Parsing;
using Microsoft.DotNet.ImageBuilder.Commands;
using Shouldly;
using Xunit;

namespace Microsoft.DotNet.ImageBuilder.Tests.Commands;

public class ServiceConnectionOptionsBindingTests
{
[Fact]
public void ValidFormat_ParsesCorrectly()
{
string[] args =
[
"--storage-service-connection", "my-tenant:my-client:my-connection-id",
];

BuildOptions options = ParseAndBind<BuildOptions>(args);

options.StorageServiceConnection.ShouldNotBeNull();
options.StorageServiceConnection.TenantId.ShouldBe("my-tenant");
options.StorageServiceConnection.ClientId.ShouldBe("my-client");
options.StorageServiceConnection.Id.ShouldBe("my-connection-id");
}

[Theory]
[InlineData("invalid")]
[InlineData("only:two")]
[InlineData("a:b:c:d")]
public void InvalidFormat_ProducesParseError(string invalidValue)
{
string[] args = ["--storage-service-connection", invalidValue];

BuildOptions options = new();
Command command = new("test", "test");
command.AddOptions(options);

ParseResult parseResult = command.Parse(args);

parseResult.Errors.ShouldNotBeEmpty(
$"Expected a parse error for invalid service connection format '{invalidValue}'");
}

private static TOptions ParseAndBind<TOptions>(string[] args)
where TOptions : Options, new()
{
TOptions options = new();
Command command = new("test", "test");
command.AddOptions(options);

ParseResult parseResult = command.Parse(args);
options.Bind(parseResult);
return options;
}
}
2 changes: 1 addition & 1 deletion src/ImageBuilder.Tests/QueueBuildCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -698,7 +698,7 @@ private static bool FilterBuildToParameters(string buildParametersJson, string p
IList<string> paths = pathString
.Split(" ", StringSplitOptions.RemoveEmptyEntries)
.Select(p => p.Trim().Trim('\''))
.Except([ CliHelper.FormatAlias(DockerfileFilterOptionsBuilder.PathOptionName) ])
.Except([ CliHelper.FormatAlias(DockerfileFilterOptions.PathOptionName) ])
.ToList();
return CompareLists(expectedPaths, paths);
}
Expand Down
54 changes: 54 additions & 0 deletions src/ImageBuilder.Tests/Signing/SignImagesOptionsBindingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.CommandLine;
using System.CommandLine.Parsing;
using Microsoft.DotNet.ImageBuilder.Commands;
using Microsoft.DotNet.ImageBuilder.Commands.Signing;
using Shouldly;
using Xunit;

namespace Microsoft.DotNet.ImageBuilder.Tests.Signing;

public class SignImagesOptionsBindingTests
{
[Fact]
public void RegistryOverride_BoundFromCliArgs()
{
string[] args =
[
"image-info.json",
"--registry-override", "myregistry.azurecr.io",
];

SignImagesOptions options = ParseAndBind<SignImagesOptions>(args);

options.RegistryOverride.Registry.ShouldBe("myregistry.azurecr.io");
}

[Fact]
public void RepoPrefix_BoundFromCliArgs()
{
string[] args =
[
"image-info.json",
"--repo-prefix", "public/",
];

SignImagesOptions options = ParseAndBind<SignImagesOptions>(args);

options.RegistryOverride.RepoPrefix.ShouldBe("public/");
}

private static TOptions ParseAndBind<TOptions>(string[] args)
where TOptions : Options, new()
{
TOptions options = new();
Command command = new("test", "test");
command.AddOptions(options);

ParseResult parseResult = command.Parse(args);
options.Bind(parseResult);
return options;
}
}
Loading