From b07234a060167d2acd163acc40432662cd63b480 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Sat, 7 Sep 2024 12:04:15 +0200 Subject: [PATCH 1/4] refactor: Align IImage and DockerImage impl with DSL --- .../Builders/ContainerBuilder`3.cs | 2 +- .../DockerRegistryAuthenticationProvider.cs | 1 - ...tlessUnixEndpointAuthenticationProvider.cs | 3 +- src/Testcontainers/Images/DockerImage.cs | 120 +++++++++++------- .../Images/FutureDockerImage.cs | 24 +++- src/Testcontainers/Images/IImage.cs | 21 ++- src/Testcontainers/Images/MatchImage.cs | 37 ++++-- .../Fixtures/Images/HealthCheckFixture.cs | 6 +- ...ockerRegistryAuthenticationProviderTest.cs | 10 +- .../Unit/Images/TestcontainersImageTest.cs | 5 +- 10 files changed, 153 insertions(+), 76 deletions(-) diff --git a/src/Testcontainers/Builders/ContainerBuilder`3.cs b/src/Testcontainers/Builders/ContainerBuilder`3.cs index a41a049b2..5bc8509eb 100644 --- a/src/Testcontainers/Builders/ContainerBuilder`3.cs +++ b/src/Testcontainers/Builders/ContainerBuilder`3.cs @@ -80,7 +80,7 @@ public TBuilderEntity WithImage(IImage image) return Clone(new ContainerConfiguration(image: image)); } - return Clone(new ContainerConfiguration(image: new DockerImage(image.Repository, image.Name, image.Tag, TestcontainersSettings.HubImageNamePrefix))); + return Clone(new ContainerConfiguration(image: new DockerImage(image.Repository, image.Registry, image.Tag, image.Digest, TestcontainersSettings.HubImageNamePrefix))); } /// diff --git a/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs b/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs index f0fed9f40..73bbfcede 100644 --- a/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs +++ b/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs @@ -2,7 +2,6 @@ namespace DotNet.Testcontainers.Builders { using System; using System.Collections.Concurrent; - using System.IO; using System.Linq; using System.Text.Json; using DotNet.Testcontainers.Configurations; diff --git a/src/Testcontainers/Builders/RootlessUnixEndpointAuthenticationProvider.cs b/src/Testcontainers/Builders/RootlessUnixEndpointAuthenticationProvider.cs index ac758ae53..e8e1d6f6d 100644 --- a/src/Testcontainers/Builders/RootlessUnixEndpointAuthenticationProvider.cs +++ b/src/Testcontainers/Builders/RootlessUnixEndpointAuthenticationProvider.cs @@ -2,7 +2,6 @@ namespace DotNet.Testcontainers.Builders { using System; using System.IO; - using System.Linq; using System.Runtime.InteropServices; using DotNet.Testcontainers.Configurations; using JetBrains.Annotations; @@ -27,7 +26,7 @@ public RootlessUnixEndpointAuthenticationProvider() /// A list of socket paths. public RootlessUnixEndpointAuthenticationProvider(params string[] socketPaths) { - var socketPath = socketPaths.FirstOrDefault(File.Exists); + var socketPath = Array.Find(socketPaths, File.Exists); DockerEngine = socketPath == null ? null : new Uri("unix://" + socketPath); } diff --git a/src/Testcontainers/Images/DockerImage.cs b/src/Testcontainers/Images/DockerImage.cs index 8347b3fbe..f5f2ba00c 100644 --- a/src/Testcontainers/Images/DockerImage.cs +++ b/src/Testcontainers/Images/DockerImage.cs @@ -1,8 +1,7 @@ -namespace DotNet.Testcontainers.Images +namespace DotNet.Testcontainers.Images { using System; using System.Globalization; - using System.Linq; using System.Text.RegularExpressions; using JetBrains.Annotations; @@ -14,24 +13,31 @@ public sealed class DockerImage : IImage private const string NightlyTag = "nightly"; + private static readonly char[] TrimChars = [' ', ':', '/']; + private static readonly Func GetDockerImage = MatchImage.Match; - private static readonly char[] TrimChars = { ' ', ':', '/' }; + [NotNull] + private readonly string _repository; - private static readonly char[] HostnameIdentifierChars = { '.', ':' }; + [CanBeNull] + private readonly string _registry; - private readonly string _hubImageNamePrefix; + [CanBeNull] + private readonly string _tag; - private readonly Lazy _lazyFullName; + [CanBeNull] + private readonly string _digit; - private readonly Lazy _lazyHostname; + [CanBeNull] + private readonly string _hubImageNamePrefix; /// /// Initializes a new instance of the class. /// /// The image. public DockerImage(IImage image) - : this(image.Repository, image.Name, image.Tag) + : this(image.Repository, image.Registry, image.Tag, image.Digest) { } @@ -39,8 +45,7 @@ public DockerImage(IImage image) /// Initializes a new instance of the class. /// /// The image. - /// Thrown when any argument is null. - /// "fedora/httpd:version1.0" where "fedora" is the repository, "httpd" the name and "version1.0" the tag. + /// fedora/httpd:version1.0 where fedora/httpd is the repository and version1.0 the tag. public DockerImage(string image) : this(GetDockerImage(image)) { @@ -51,65 +56,78 @@ public DockerImage(string image) /// /// The repository. /// The name. + [Obsolete("We will remove this construct and replace it with a more efficient implementation. Please use 'DockerImage(string, string = null, string = null, string = null, string = null)' instead. All arguments except for 'repository' (the first) are optional.")] + public DockerImage(string repository, string name) + : this(string.Join("/", repository, name).Trim('/')) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The repository. + /// The name. + /// The tag. + [Obsolete("We will remove this construct and replace it with a more efficient implementation. Please use 'DockerImage(string, string = null, string = null, string = null, string = null)' instead. All arguments except for 'repository' (the first) are optional.")] + public DockerImage(string repository, string name, string tag) + : this(string.Join("/", repository, name).Trim('/') + (":" + tag).TrimEnd(':')) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The repository. + /// The registry. /// The tag. + /// The digest. /// The Docker Hub image name prefix. - /// Thrown when any argument is null. - /// "fedora/httpd:version1.0" where "fedora" is the repository, "httpd" the name and "version1.0" the tag. + /// fedora/httpd:version1.0 where fedora/httpd is the repository and version1.0 the tag. public DockerImage( string repository, - string name, + string registry = null, string tag = null, + string digest = null, string hubImageNamePrefix = null) { _ = Guard.Argument(repository, nameof(repository)) - .NotNull() - .NotUppercase(); - - _ = Guard.Argument(name, nameof(name)) .NotNull() .NotEmpty() .NotUppercase(); - _hubImageNamePrefix = TrimOrDefault(hubImageNamePrefix); + var defaultTag = tag == null && digest == null ? LatestTag : null; - Repository = TrimOrDefault(repository, repository); - Name = TrimOrDefault(name, name); - Tag = TrimOrDefault(tag, LatestTag); - - _lazyFullName = new Lazy(() => - { - var imageComponents = new[] { _hubImageNamePrefix, Repository, Name } - .Where(imageComponent => !string.IsNullOrEmpty(imageComponent)); + _repository = TrimOrDefault(repository); + _registry = TrimOrDefault(registry); + _tag = TrimOrDefault(tag, defaultTag); + _digit = TrimOrDefault(digest); + _hubImageNamePrefix = TrimOrDefault(hubImageNamePrefix); + } - return string.Join("/", imageComponents) + ":" + Tag; - }); + /// + public string Repository => _repository; - _lazyHostname = new Lazy(() => - { - var firstSegmentOfRepository = new[] { _hubImageNamePrefix, Repository } - .Where(imageComponent => !string.IsNullOrEmpty(imageComponent)) - .DefaultIfEmpty(string.Empty) - .First() - .Split('/')[0]; - - return firstSegmentOfRepository.IndexOfAny(HostnameIdentifierChars) >= 0 ? firstSegmentOfRepository : null; - }); - } + /// + public string Registry => string.IsNullOrEmpty(_hubImageNamePrefix) ? _registry : _hubImageNamePrefix; /// - public string Repository { get; } + public string Tag => _tag; /// - public string Name { get; } + public string Digest => _digit; /// - public string Tag { get; } + public string FullName => $"{Registry}/{Repository}:{Tag}".Trim(TrimChars); /// - public string FullName => _lazyFullName.Value; + [Obsolete("We will remove this property, it does not follow the DSL. Use the 'Repository' property instead.")] + public string Name => GetBackwardsCompatibleName(); /// - public string GetHostname() => _lazyHostname.Value; + public string GetHostname() + { + return Registry; + } /// public bool MatchLatestOrNightly() @@ -126,6 +144,11 @@ public bool MatchVersion(Predicate predicate) /// public bool MatchVersion(Predicate predicate) { + if (Tag == null) + { + return false; + } + var versionMatch = Regex.Match(Tag, "^(\\d+)(\\.\\d+)?(\\.\\d+)?", RegexOptions.None, TimeSpan.FromSeconds(1)); if (!versionMatch.Success) @@ -142,7 +165,14 @@ public bool MatchVersion(Predicate predicate) return predicate(new Version(int.Parse(versionMatch.Groups[1].Value, NumberStyles.None), 0)); } - private static string TrimOrDefault(string value, string defaultValue = default) + private string GetBackwardsCompatibleName() + { + // The last index will never be a `/`, we trim it in the constructor. + var lastIndex = _repository.LastIndexOf('/'); + return lastIndex == -1 ? _repository : _repository.Substring(lastIndex + 1); + } + + private static string TrimOrDefault(string value, string defaultValue = null) { return string.IsNullOrEmpty(value) ? defaultValue : value.Trim(TrimChars); } diff --git a/src/Testcontainers/Images/FutureDockerImage.cs b/src/Testcontainers/Images/FutureDockerImage.cs index 23817169d..9b4f0cd77 100644 --- a/src/Testcontainers/Images/FutureDockerImage.cs +++ b/src/Testcontainers/Images/FutureDockerImage.cs @@ -39,12 +39,12 @@ public string Repository } /// - public string Name + public string Registry { get { ThrowIfResourceNotFound(); - return _configuration.Image.Name; + return _configuration.Image.Registry; } } @@ -58,6 +58,16 @@ public string Tag } } + /// + public string Digest + { + get + { + ThrowIfResourceNotFound(); + return _configuration.Image.Digest; + } + } + /// public string FullName { @@ -68,6 +78,16 @@ public string FullName } } + /// + public string Name + { + get + { + ThrowIfResourceNotFound(); + return _configuration.Image.Name; + } + } + /// public string GetHostname() { diff --git a/src/Testcontainers/Images/IImage.cs b/src/Testcontainers/Images/IImage.cs index 307879d23..41cec17ae 100644 --- a/src/Testcontainers/Images/IImage.cs +++ b/src/Testcontainers/Images/IImage.cs @@ -16,17 +16,23 @@ public interface IImage string Repository { get; } /// - /// Gets the name. + /// Gets the registry. /// - [NotNull] - string Name { get; } + [CanBeNull] + string Registry { get; } /// /// Gets the tag. /// - [NotNull] + [CanBeNull] string Tag { get; } + /// + /// Gets the digest. + /// + [CanBeNull] + string Digest { get; } + /// /// Gets the full image name. /// @@ -36,6 +42,13 @@ public interface IImage [NotNull] string FullName { get; } + /// + /// Gets the name. + /// + [NotNull] + [Obsolete("We will remove this property, it does not follow the DSL. Use the 'Repository' property instead.")] + string Name { get; } + /// /// Gets the registry hostname. /// diff --git a/src/Testcontainers/Images/MatchImage.cs b/src/Testcontainers/Images/MatchImage.cs index 58c5d227f..3be5b114b 100644 --- a/src/Testcontainers/Images/MatchImage.cs +++ b/src/Testcontainers/Images/MatchImage.cs @@ -1,37 +1,48 @@ namespace DotNet.Testcontainers.Images { using System; - using System.Linq; + using System.Text.RegularExpressions; internal static class MatchImage { - private static readonly char[] SlashSeparator = { '/' }; - - private static readonly char[] ColonSeparator = { ':' }; - public static IImage Match(string image) { _ = Guard.Argument(image, nameof(image)) .NotNull() .NotEmpty(); - var imageComponents = image.Split(SlashSeparator, StringSplitOptions.RemoveEmptyEntries); + var referenceMatch = ReferenceRegex.Instance.Match(image); + var remoteName = referenceMatch.Groups["remote_name"].Value; + var tag = referenceMatch.Groups["tag"].Value; + var digest = referenceMatch.Groups["digest"].Value; - var registry = string.Join("/", imageComponents.Take(imageComponents.Length - 1)); + // https://github.com/distribution/reference/blob/8c942b0459dfdcc5b6685581dd0a5a470f615bff/normalize.go#L146-L191 + var slices = remoteName.Split(['/'], 2); - imageComponents = imageComponents[imageComponents.Length - 1].Split(ColonSeparator, StringSplitOptions.RemoveEmptyEntries); + // The following part does not implement the entire Go implementation. It does + // not resolve or set the default domain and repository prefix. This is not + // necessary at the moment, and it is something we can address later. + var (registry, repository) = slices.Length == 2 && slices[0].LastIndexOfAny(['.', ':']) > -1 ? (slices[0], slices[1]) : (null, remoteName); + return new DockerImage(repository, registry, tag, digest); + } + + // The regular expression used here is taken from the Go implementation: + // https://github.com/distribution/reference/blob/8c942b0459dfdcc5b6685581dd0a5a470f615bff/reference.go. + private sealed class ReferenceRegex : Regex + { + private const string Pattern = "^(?(?:(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*|\\[(?:[a-fA-F0-9:]+)\\])(?::[0-9]+)?\\/)?[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*(?:\\/[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*)*)(?::(?[\\w][\\w.-]{0,127}))?(?:@(?[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][0-9A-Fa-f]{32,}))?$"; - if (2.Equals(imageComponents.Length)) + static ReferenceRegex() { - return new DockerImage(registry, imageComponents[0], imageComponents[1]); } - if (1.Equals(imageComponents.Length)) + private ReferenceRegex() + : base(Pattern, RegexOptions.Compiled, TimeSpan.FromSeconds(1)) { - return new DockerImage(registry, imageComponents[0], string.Empty); } - throw new ArgumentException("Cannot parse image: " + image, nameof(image)); + public static Regex Instance { get; } + = new ReferenceRegex(); } } } diff --git a/tests/Testcontainers.Tests/Fixtures/Images/HealthCheckFixture.cs b/tests/Testcontainers.Tests/Fixtures/Images/HealthCheckFixture.cs index 932c95526..4d0b31837 100644 --- a/tests/Testcontainers.Tests/Fixtures/Images/HealthCheckFixture.cs +++ b/tests/Testcontainers.Tests/Fixtures/Images/HealthCheckFixture.cs @@ -17,12 +17,16 @@ public sealed class HealthCheckFixture : IImage, IAsyncLifetime public string Repository => _image.Repository; - public string Name => _image.Name; + public string Registry => _image.Registry; public string Tag => _image.Tag; + public string Digest => _image.Digest; + public string FullName => _image.FullName; + public string Name => _image.Name; + public string GetHostname() { return _image.GetHostname(); diff --git a/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs b/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs index 2a954c030..ab140d8f2 100644 --- a/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs +++ b/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs @@ -45,13 +45,13 @@ public void GetHostnameFromDockerImage(string dockerImageName, string hostname) } [Theory] - [InlineData("", "docker", "stable")] - [InlineData("fedora", "httpd", "1.0")] - [InlineData("foo/bar", "baz", "1.0.0")] - public void GetHostnameFromHubImageNamePrefix(string repository, string name, string tag) + [InlineData("baz/foo/bar", null)] + [InlineData("baz/foo/bar", "")] + [InlineData("baz/foo/bar", "1.0.0")] + public void GetHostnameFromHubImageNamePrefix(string repository, string tag) { const string hubImageNamePrefix = "myregistry.azurecr.io"; - IImage image = new DockerImage(repository, name, tag, hubImageNamePrefix); + IImage image = new DockerImage(repository, null, tag, null, hubImageNamePrefix); Assert.Equal(hubImageNamePrefix, image.GetHostname()); } diff --git a/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs b/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs index af5ff7a43..940e06744 100644 --- a/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs +++ b/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs @@ -12,7 +12,6 @@ public void ShouldThrowArgumentNullExceptionWhenInstantiateDockerImage() { Assert.Throws(() => new DockerImage((string)null)); Assert.Throws(() => new DockerImage(null, null)); - Assert.Throws(() => new DockerImage("fedora", null)); } [Fact] @@ -55,9 +54,11 @@ public void WhenImageNameGetsAssigned(DockerImageFixtureSerializable serializabl // Then Assert.Equal(expected.Repository, dockerImage.Repository); - Assert.Equal(expected.Name, dockerImage.Name); + Assert.Equal(expected.Registry, dockerImage.Registry); Assert.Equal(expected.Tag, dockerImage.Tag); + Assert.Equal(expected.Digest, dockerImage.Digest); Assert.Equal(expected.FullName, dockerImage.FullName); + Assert.Equal(expected.Name, dockerImage.Name); } [Fact] From 15b495fc45929793824ca0177dbe6acee72b27f5 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Mon, 9 Sep 2024 16:37:54 +0200 Subject: [PATCH 2/4] chore: Throw exception for invalid reference format --- Directory.Build.props | 2 +- src/Testcontainers.Pulsar/PulsarContainer.cs | 2 +- src/Testcontainers/Images/MatchImage.cs | 7 +++++++ .../DockerRegistryAuthenticationProviderTest.cs | 7 +++---- tests/Testcontainers.Tests/Unit/GuardTest.cs | 4 ++-- .../Unit/Images/TestcontainersImageTest.cs | 8 ++++++++ 6 files changed, 22 insertions(+), 8 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 93cab1326..f0c488fef 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ $(AssemblyName) - 3.11.0 + 4.0.0 $(Version) $(Version) Testcontainers diff --git a/src/Testcontainers.Pulsar/PulsarContainer.cs b/src/Testcontainers.Pulsar/PulsarContainer.cs index 927e60e5d..3989fe7f4 100644 --- a/src/Testcontainers.Pulsar/PulsarContainer.cs +++ b/src/Testcontainers.Pulsar/PulsarContainer.cs @@ -56,7 +56,7 @@ public async Task CreateAuthenticationTokenAsync(TimeSpan expiryTime, Ca "--secret-key", PulsarBuilder.SecretKeyFilePath, "--subject", - PulsarBuilder.Username + PulsarBuilder.Username, }; if (!Timeout.InfiniteTimeSpan.Equals(expiryTime)) diff --git a/src/Testcontainers/Images/MatchImage.cs b/src/Testcontainers/Images/MatchImage.cs index 3be5b114b..681fe4330 100644 --- a/src/Testcontainers/Images/MatchImage.cs +++ b/src/Testcontainers/Images/MatchImage.cs @@ -1,6 +1,7 @@ namespace DotNet.Testcontainers.Images { using System; + using System.Globalization; using System.Text.RegularExpressions; internal static class MatchImage @@ -11,7 +12,13 @@ public static IImage Match(string image) .NotNull() .NotEmpty(); + const string invalidReferenceFormat = "Error parsing reference: '{0}' is not a valid repository/tag."; + var referenceMatch = ReferenceRegex.Instance.Match(image); + + _ = Guard.Argument(referenceMatch, nameof(image)) + .ThrowIf(argument => !argument.Value.Success, argument => new ArgumentException(string.Format(CultureInfo.InvariantCulture, invalidReferenceFormat, image), argument.Name)); + var remoteName = referenceMatch.Groups["remote_name"].Value; var tag = referenceMatch.Groups["tag"].Value; var digest = referenceMatch.Groups["digest"].Value; diff --git a/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs b/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs index ab140d8f2..8be2f65b3 100644 --- a/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs +++ b/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs @@ -34,10 +34,7 @@ public sealed class DockerRegistryAuthenticationProviderTest [InlineData("fedora/httpd:version1.0", null)] [InlineData("myregistryhost:5000/fedora/httpd:version1.0", "myregistryhost:5000")] [InlineData("myregistryhost:5000/httpd:version1.0", "myregistryhost:5000")] - [InlineData("baz/.foo/bar:1.0.0", null)] - [InlineData("baz/:foo/bar:1.0.0", null)] [InlineData("myregistry.azurecr.io/baz.foo/bar:1.0.0", "myregistry.azurecr.io")] - [InlineData("myregistry.azurecr.io/baz:foo/bar:1.0.0", "myregistry.azurecr.io")] public void GetHostnameFromDockerImage(string dockerImageName, string hostname) { IImage image = new DockerImage(dockerImageName); @@ -58,7 +55,9 @@ public void GetHostnameFromHubImageNamePrefix(string repository, string tag) [Fact] public void ShouldGetDefaultDockerRegistryAuthenticationConfiguration() { - var authenticationProvider = new DockerRegistryAuthenticationProvider(DockerConfig.Instance, NullLogger.Instance); + // Make sure the auth provider does not accidentally read the user's `config.json` file. + ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.config=" + Path.Combine("C:", "CON") }); + var authenticationProvider = new DockerRegistryAuthenticationProvider(new DockerConfig(customConfiguration), NullLogger.Instance); Assert.Equal(default(DockerRegistryAuthenticationConfiguration), authenticationProvider.GetAuthConfig("index.docker.io")); } diff --git a/tests/Testcontainers.Tests/Unit/GuardTest.cs b/tests/Testcontainers.Tests/Unit/GuardTest.cs index 76b95f10f..e13ef47b3 100644 --- a/tests/Testcontainers.Tests/Unit/GuardTest.cs +++ b/tests/Testcontainers.Tests/Unit/GuardTest.cs @@ -12,7 +12,7 @@ public sealed class DoNotThrowException [Fact] public void IfNull() { - var exception = Record.Exception(() => Guard.Argument((object)null, nameof(IfNull)).Null()); + var exception = Record.Exception(() => Guard.Argument(null, nameof(IfNull)).Null()); Assert.Null(exception); } @@ -36,7 +36,7 @@ public sealed class ThrowArgumentException [Fact] public void IfNull() { - Assert.Throws(() => Guard.Argument((object)null, nameof(IfNull)).NotNull()); + Assert.Throws(() => Guard.Argument(null, nameof(IfNull)).NotNull()); } [Fact] diff --git a/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs b/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs index 940e06744..621a2ba4a 100644 --- a/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs +++ b/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs @@ -33,6 +33,14 @@ public void ShouldThrowArgumentExceptionIfImageNameHasUppercaseCharacters(string Assert.Throws(() => new DockerImage(image)); } + [Theory] + [InlineData("baz/.foo/bar:1.0.0")] + [InlineData("baz/:foo/bar:1.0.0")] + public void ShouldThrowArgumentExceptionIfImageIsInvalidReferenceFormat(string image) + { + Assert.Throws(() => new DockerImage(image)); + } + [Theory] [InlineData("bar:LATEST")] [InlineData("foo/bar:LATEST")] From 4918417876ea8847334c2247e06b76b8183ee8d2 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Mon, 9 Sep 2024 19:57:35 +0200 Subject: [PATCH 3/4] chore: Add digest image test data --- .../Fixtures/Images/DockerImageFixture.cs | 44 ++++++++++++------- .../Images/DockerImageFixtureSerializable.cs | 8 ++-- .../Configurations/ResourcePropertiesTest.cs | 4 +- .../Unit/Images/ImageFromDockerfileTest.cs | 2 +- .../Unit/Images/TestcontainersImageTest.cs | 18 ++++---- 5 files changed, 48 insertions(+), 28 deletions(-) diff --git a/tests/Testcontainers.Tests/Fixtures/Images/DockerImageFixture.cs b/tests/Testcontainers.Tests/Fixtures/Images/DockerImageFixture.cs index 1b8404ad9..80ce04da5 100644 --- a/tests/Testcontainers.Tests/Fixtures/Images/DockerImageFixture.cs +++ b/tests/Testcontainers.Tests/Fixtures/Images/DockerImageFixture.cs @@ -3,24 +3,38 @@ namespace DotNet.Testcontainers.Tests.Fixtures using DotNet.Testcontainers.Images; using Xunit; - public sealed class DockerImageFixture : TheoryData + public sealed class DockerImageFixture : TheoryData { + private const string FooBarBaz = "foo/bar/baz"; + private const string BarBaz = "bar/baz"; + private const string Baz = "baz"; + private const string FedoraHttpd = "fedora/httpd"; + private const string LatestTag = "latest"; + private const string SemVerTag = "1.0.0"; + private const string CustomTag1 = "version1.0"; + private const string CustomTag2 = "version1.0.test"; + private const string DotSeparatorRegistry = "myregistry.azurecr.io"; + private const string PortSeparatorRegistry = "myregistry:5000"; + private const string Digest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + public DockerImageFixture() { - Add(new DockerImageFixtureSerializable(new DockerImage("baz/foo", "bar", "1.0.0")), "baz/foo/bar:1.0.0"); - Add(new DockerImageFixtureSerializable(new DockerImage("baz/foo", "bar", string.Empty)), "baz/foo/bar"); - Add(new DockerImageFixtureSerializable(new DockerImage("baz/foo", "bar", string.Empty)), "baz/foo/bar:latest"); - Add(new DockerImageFixtureSerializable(new DockerImage("foo", "bar", "1.0.0")), "foo/bar:1.0.0"); - Add(new DockerImageFixtureSerializable(new DockerImage("foo", "bar", string.Empty)), "foo/bar"); - Add(new DockerImageFixtureSerializable(new DockerImage("foo", "bar", string.Empty)), "foo/bar:latest"); - Add(new DockerImageFixtureSerializable(new DockerImage(string.Empty, "bar", "1.0.0")), "bar:1.0.0"); - Add(new DockerImageFixtureSerializable(new DockerImage(string.Empty, "bar", string.Empty)), "bar:latest"); - Add(new DockerImageFixtureSerializable(new DockerImage("myregistry.azurecr.io/baz/foo", "bar", "1.0.0")), "myregistry.azurecr.io/baz/foo/bar:1.0.0"); - Add(new DockerImageFixtureSerializable(new DockerImage("myregistry.azurecr.io/baz/foo", "bar", string.Empty)), "myregistry.azurecr.io/baz/foo/bar"); - Add(new DockerImageFixtureSerializable(new DockerImage("myregistry.azurecr.io/baz/foo", "bar", string.Empty)), "myregistry.azurecr.io/baz/foo/bar:latest"); - Add(new DockerImageFixtureSerializable(new DockerImage("fedora", "httpd", "version1.0.test")), "fedora/httpd:version1.0.test"); - Add(new DockerImageFixtureSerializable(new DockerImage("fedora", "httpd", "version1.0")), "fedora/httpd:version1.0"); - Add(new DockerImageFixtureSerializable(new DockerImage("myregistryhost:5000/fedora", "httpd", "version1.0")), "myregistryhost:5000/fedora/httpd:version1.0"); + Add(new DockerImageFixtureSerializable(new DockerImage(FooBarBaz, null, SemVerTag, null)), $"{FooBarBaz}:{SemVerTag}", $"{FooBarBaz}:{SemVerTag}"); + Add(new DockerImageFixtureSerializable(new DockerImage(FooBarBaz, null, null, null)), FooBarBaz, $"{FooBarBaz}:{LatestTag}"); + Add(new DockerImageFixtureSerializable(new DockerImage(FooBarBaz, null, null, null)), $"{FooBarBaz}:{LatestTag}", $"{FooBarBaz}:{LatestTag}"); + Add(new DockerImageFixtureSerializable(new DockerImage(BarBaz, null, SemVerTag, null)), $"{BarBaz}:{SemVerTag}", $"{BarBaz}:{SemVerTag}"); + Add(new DockerImageFixtureSerializable(new DockerImage(BarBaz, null, null, null)), BarBaz, $"{BarBaz}:{LatestTag}"); + Add(new DockerImageFixtureSerializable(new DockerImage(BarBaz, null, null, null)), $"{BarBaz}:{LatestTag}", $"{BarBaz}:{LatestTag}"); + Add(new DockerImageFixtureSerializable(new DockerImage(Baz, null, SemVerTag, null)), $"{Baz}:{SemVerTag}", $"{Baz}:{SemVerTag}"); + Add(new DockerImageFixtureSerializable(new DockerImage(Baz, null, null, null)), $"{Baz}:{LatestTag}", $"{Baz}:{LatestTag}"); + Add(new DockerImageFixtureSerializable(new DockerImage(FooBarBaz, DotSeparatorRegistry, SemVerTag, null)), $"{DotSeparatorRegistry}/{FooBarBaz}:{SemVerTag}", $"{DotSeparatorRegistry}/{FooBarBaz}:{SemVerTag}"); + Add(new DockerImageFixtureSerializable(new DockerImage(FooBarBaz, DotSeparatorRegistry, null, null)), $"{DotSeparatorRegistry}/{FooBarBaz}", $"{DotSeparatorRegistry}/{FooBarBaz}:{LatestTag}"); + Add(new DockerImageFixtureSerializable(new DockerImage(FooBarBaz, DotSeparatorRegistry, null, null)), $"{DotSeparatorRegistry}/{FooBarBaz}:{LatestTag}", $"{DotSeparatorRegistry}/{FooBarBaz}:{LatestTag}"); + Add(new DockerImageFixtureSerializable(new DockerImage(FedoraHttpd, null, CustomTag1, null)), $"{FedoraHttpd}:{CustomTag1}", $"{FedoraHttpd}:{CustomTag1}"); + Add(new DockerImageFixtureSerializable(new DockerImage(FedoraHttpd, null, CustomTag2, null)), $"{FedoraHttpd}:{CustomTag2}", $"{FedoraHttpd}:{CustomTag2}"); + Add(new DockerImageFixtureSerializable(new DockerImage(FedoraHttpd, PortSeparatorRegistry, CustomTag1, null)), $"{PortSeparatorRegistry}/{FedoraHttpd}:{CustomTag1}", $"{PortSeparatorRegistry}/{FedoraHttpd}:{CustomTag1}"); + Add(new DockerImageFixtureSerializable(new DockerImage(FooBarBaz, DotSeparatorRegistry, SemVerTag, Digest)), $"{DotSeparatorRegistry}/{FooBarBaz}:{SemVerTag}@{Digest}", $"{DotSeparatorRegistry}/{FooBarBaz}:{SemVerTag}@{Digest}"); + Add(new DockerImageFixtureSerializable(new DockerImage(FooBarBaz, DotSeparatorRegistry, null, Digest)), $"{DotSeparatorRegistry}/{FooBarBaz}@{Digest}", $"{DotSeparatorRegistry}/{FooBarBaz}@{Digest}"); } } } diff --git a/tests/Testcontainers.Tests/Fixtures/Images/DockerImageFixtureSerializable.cs b/tests/Testcontainers.Tests/Fixtures/Images/DockerImageFixtureSerializable.cs index fba2f1fef..d23f9b484 100644 --- a/tests/Testcontainers.Tests/Fixtures/Images/DockerImageFixtureSerializable.cs +++ b/tests/Testcontainers.Tests/Fixtures/Images/DockerImageFixtureSerializable.cs @@ -19,16 +19,18 @@ public DockerImageFixtureSerializable(IImage image) public void Deserialize(IXunitSerializationInfo info) { var repository = info.GetValue("Repository"); - var name = info.GetValue("Name"); + var registry = info.GetValue("Registry"); var tag = info.GetValue("Tag"); - Image = new DockerImage(repository, name, tag); + var digest = info.GetValue("Digest"); + Image = new DockerImage(repository, registry, tag, digest); } public void Serialize(IXunitSerializationInfo info) { info.AddValue("Repository", Image.Repository); - info.AddValue("Name", Image.Name); + info.AddValue("Registry", Image.Registry); info.AddValue("Tag", Image.Tag); + info.AddValue("Digest", Image.Digest); } } } diff --git a/tests/Testcontainers.Tests/Unit/Configurations/ResourcePropertiesTest.cs b/tests/Testcontainers.Tests/Unit/Configurations/ResourcePropertiesTest.cs index aefa927ac..cc8270668 100644 --- a/tests/Testcontainers.Tests/Unit/Configurations/ResourcePropertiesTest.cs +++ b/tests/Testcontainers.Tests/Unit/Configurations/ResourcePropertiesTest.cs @@ -99,9 +99,11 @@ await Task.CompletedTask // Then Assert.Throws(() => image.Repository); - Assert.Throws(() => image.Name); + Assert.Throws(() => image.Registry); Assert.Throws(() => image.Tag); + Assert.Throws(() => image.Digest); Assert.Throws(() => image.FullName); + Assert.Throws(() => image.Name); Assert.Throws(() => image.GetHostname()); } diff --git a/tests/Testcontainers.Tests/Unit/Images/ImageFromDockerfileTest.cs b/tests/Testcontainers.Tests/Unit/Images/ImageFromDockerfileTest.cs index a590d83f6..b8f7c608c 100644 --- a/tests/Testcontainers.Tests/Unit/Images/ImageFromDockerfileTest.cs +++ b/tests/Testcontainers.Tests/Unit/Images/ImageFromDockerfileTest.cs @@ -130,9 +130,9 @@ await imageFromDockerfileBuilder.CreateAsync() Assert.True(DockerCli.ResourceExists(DockerCli.DockerResource.Image, tag1.FullName)); Assert.True(DockerCli.ResourceExists(DockerCli.DockerResource.Image, tag2.FullName)); Assert.NotNull(imageFromDockerfileBuilder.Repository); - Assert.NotNull(imageFromDockerfileBuilder.Name); Assert.NotNull(imageFromDockerfileBuilder.Tag); Assert.NotNull(imageFromDockerfileBuilder.FullName); + Assert.NotNull(imageFromDockerfileBuilder.Name); Assert.Null(imageFromDockerfileBuilder.GetHostname()); } } diff --git a/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs b/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs index 621a2ba4a..c069f1bac 100644 --- a/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs +++ b/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs @@ -52,21 +52,23 @@ public void ShouldNotThrowArgumentExceptionIfImageTagHasUppercaseCharacters(stri [Theory] [ClassData(typeof(DockerImageFixture))] - public void WhenImageNameGetsAssigned(DockerImageFixtureSerializable serializable, string fullName) + public void WhenImageNameGetsAssigned(DockerImageFixtureSerializable serializable, string image, string fullName) { // Given var expected = serializable.Image; // When - IImage dockerImage = new DockerImage(fullName); + IImage actual = new DockerImage(image); // Then - Assert.Equal(expected.Repository, dockerImage.Repository); - Assert.Equal(expected.Registry, dockerImage.Registry); - Assert.Equal(expected.Tag, dockerImage.Tag); - Assert.Equal(expected.Digest, dockerImage.Digest); - Assert.Equal(expected.FullName, dockerImage.FullName); - Assert.Equal(expected.Name, dockerImage.Name); + Assert.Equal(expected.Repository, actual.Repository); + Assert.Equal(expected.Registry, actual.Registry); + Assert.Equal(expected.Tag, actual.Tag); + Assert.Equal(expected.Digest, actual.Digest); + Assert.Equal(expected.FullName, actual.FullName); + Assert.Equal(expected.Name, actual.Name); + // Assert.Equal(fullName, expected.FullName); + // Assert.Equal(fullName, actual.FullName); } [Fact] From 0e656aded7ea86dc2e53c7c9d0767793dc94de37 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Mon, 9 Sep 2024 20:33:09 +0200 Subject: [PATCH 4/4] chore: Use discard arg for unused arg --- .../Unit/Images/TestcontainersImageTest.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs b/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs index c069f1bac..7e25ed0d6 100644 --- a/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs +++ b/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs @@ -52,7 +52,7 @@ public void ShouldNotThrowArgumentExceptionIfImageTagHasUppercaseCharacters(stri [Theory] [ClassData(typeof(DockerImageFixture))] - public void WhenImageNameGetsAssigned(DockerImageFixtureSerializable serializable, string image, string fullName) + public void WhenImageNameGetsAssigned(DockerImageFixtureSerializable serializable, string image, string _) { // Given var expected = serializable.Image; @@ -67,7 +67,6 @@ public void WhenImageNameGetsAssigned(DockerImageFixtureSerializable serializabl Assert.Equal(expected.Digest, actual.Digest); Assert.Equal(expected.FullName, actual.FullName); Assert.Equal(expected.Name, actual.Name); - // Assert.Equal(fullName, expected.FullName); // Assert.Equal(fullName, actual.FullName); }