From 40f14ad3c5ae1ac7339007b641bbc03ead946d76 Mon Sep 17 00:00:00 2001
From: Jacob Marks <jacobjmarks@outlook.com>
Date: Sat, 29 Jul 2023 16:59:22 +1000
Subject: [PATCH] Refactor image properties to match DSL

Add `IImage.Registry`; Remove `IImage.Name`
---
 .../Builders/ContainerBuilder`3.cs            |  2 +-
 .../Builders/ImageFromDockerfileBuilder.cs    |  2 +-
 src/Testcontainers/Images/DockerImage.cs      | 24 ++++++++--------
 .../Images/FutureDockerImage.cs               |  8 +++---
 src/Testcontainers/Images/IImage.cs           |  8 +++---
 src/Testcontainers/Images/MatchImage.cs       | 28 ++++++-------------
 .../Fixtures/Images/DockerImageFixture.cs     | 26 +++++++++--------
 .../Images/DockerImageFixtureSerializable.cs  | 14 +++++-----
 .../Fixtures/Images/HealthCheckFixture.cs     |  4 +--
 ...ockerRegistryAuthenticationProviderTest.cs |  8 +++---
 .../Configurations/ResourcePropertiesTest.cs  |  2 +-
 .../Unit/Images/ImageFromDockerfileTest.cs    |  8 +++---
 .../Unit/Images/TestcontainersImageTest.cs    |  4 +--
 13 files changed, 64 insertions(+), 74 deletions(-)

diff --git a/src/Testcontainers/Builders/ContainerBuilder`3.cs b/src/Testcontainers/Builders/ContainerBuilder`3.cs
index 9f3c6cf81..599274644 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.Registry, image.Repository, image.Tag, TestcontainersSettings.HubImageNamePrefix)));
     }
 
     /// <inheritdoc />
diff --git a/src/Testcontainers/Builders/ImageFromDockerfileBuilder.cs b/src/Testcontainers/Builders/ImageFromDockerfileBuilder.cs
index c48a0f4f3..0221e2312 100644
--- a/src/Testcontainers/Builders/ImageFromDockerfileBuilder.cs
+++ b/src/Testcontainers/Builders/ImageFromDockerfileBuilder.cs
@@ -109,7 +109,7 @@ public override IFutureDockerImage Build()
     /// <inheritdoc />
     protected sealed override ImageFromDockerfileBuilder Init()
     {
-      return base.Init().WithImageBuildPolicy(PullPolicy.Always).WithDockerfile("Dockerfile").WithDockerfileDirectory(Directory.GetCurrentDirectory()).WithName(new DockerImage("localhost/testcontainers", Guid.NewGuid().ToString("D"), string.Empty));
+      return base.Init().WithImageBuildPolicy(PullPolicy.Always).WithDockerfile("Dockerfile").WithDockerfileDirectory(Directory.GetCurrentDirectory()).WithName(new DockerImage("localhost", $"testcontainers/{Guid.NewGuid():D}", string.Empty));
     }
 
     /// <inheritdoc />
diff --git a/src/Testcontainers/Images/DockerImage.cs b/src/Testcontainers/Images/DockerImage.cs
index 3ce296fdf..6d3493917 100644
--- a/src/Testcontainers/Images/DockerImage.cs
+++ b/src/Testcontainers/Images/DockerImage.cs
@@ -17,7 +17,7 @@ public sealed class DockerImage : IImage
     /// </summary>
     /// <param name="image">The image.</param>
     public DockerImage(IImage image)
-      : this(image.Repository, image.Name, image.Tag)
+      : this(image.Registry, image.Repository, image.Tag)
     {
     }
 
@@ -35,39 +35,39 @@ public DockerImage(string image)
     /// <summary>
     /// Initializes a new instance of the <see cref="DockerImage" /> class.
     /// </summary>
+    /// <param name="registry">The registry.</param>
     /// <param name="repository">The repository.</param>
-    /// <param name="name">The name.</param>
     /// <param name="tag">The tag.</param>
     /// <param name="hubImageNamePrefix">The Docker Hub image name prefix.</param>
     /// <exception cref="ArgumentNullException">Thrown when any argument is null.</exception>
-    /// <example>"fedora/httpd:version1.0" where "fedora" is the repository, "httpd" the name and "version1.0" the tag.</example>
+    /// <example>"docker.io/fedora/httpd:version1.0" where "docker.io" is the registry, "fedora/httpd" is the repository and "version1.0" the tag.</example>
     public DockerImage(
+      string registry,
       string repository,
-      string name,
       string tag,
       string hubImageNamePrefix = null)
     {
-      _ = Guard.Argument(repository, nameof(repository))
+      _ = Guard.Argument(registry, nameof(registry))
         .NotNull()
         .NotUppercase();
 
-      _ = Guard.Argument(name, nameof(name))
+      _ = Guard.Argument(repository, nameof(repository))
         .NotNull()
         .NotEmpty()
         .NotUppercase();
 
       _hubImageNamePrefix = hubImageNamePrefix;
 
+      Registry = registry;
       Repository = repository;
-      Name = name;
       Tag = string.IsNullOrEmpty(tag) ? "latest" : tag;
     }
 
     /// <inheritdoc />
-    public string Repository { get; }
+    public string Registry { get; }
 
     /// <inheritdoc />
-    public string Name { get; }
+    public string Repository { get; }
 
     /// <inheritdoc />
     public string Tag { get; }
@@ -77,7 +77,7 @@ public string FullName
     {
       get
       {
-        var imageComponents = new[] { _hubImageNamePrefix, Repository, Name }
+        var imageComponents = new[] { _hubImageNamePrefix, Registry, Repository }
           .Where(imageComponent => !string.IsNullOrEmpty(imageComponent))
           .Select(imageComponent => imageComponent.Trim('/', ':'))
           .Where(imageComponent => !string.IsNullOrEmpty(imageComponent));
@@ -88,8 +88,8 @@ public string FullName
     /// <inheritdoc />
     public string GetHostname()
     {
-      var firstSegmentOfRepository = (string.IsNullOrEmpty(_hubImageNamePrefix) ? Repository : _hubImageNamePrefix).Split('/')[0];
-      return firstSegmentOfRepository.IndexOfAny(new[] { '.', ':' }) >= 0 ? firstSegmentOfRepository : null;
+      var firstSegmentOfRegistry = (string.IsNullOrEmpty(_hubImageNamePrefix) ? Registry : _hubImageNamePrefix).Split('/')[0];
+      return firstSegmentOfRegistry.IndexOfAny(new[] { '.', ':' }) >= 0 ? firstSegmentOfRegistry : null;
     }
   }
 }
diff --git a/src/Testcontainers/Images/FutureDockerImage.cs b/src/Testcontainers/Images/FutureDockerImage.cs
index 4352f6f15..703f032d0 100644
--- a/src/Testcontainers/Images/FutureDockerImage.cs
+++ b/src/Testcontainers/Images/FutureDockerImage.cs
@@ -30,22 +30,22 @@ public FutureDockerImage(IImageFromDockerfileConfiguration configuration, ILogge
     }
 
     /// <inheritdoc />
-    public string Repository
+    public string Registry
     {
       get
       {
         ThrowIfResourceNotFound();
-        return _configuration.Image.Repository;
+        return _configuration.Image.Registry;
       }
     }
 
     /// <inheritdoc />
-    public string Name
+    public string Repository
     {
       get
       {
         ThrowIfResourceNotFound();
-        return _configuration.Image.Name;
+        return _configuration.Image.Repository;
       }
     }
 
diff --git a/src/Testcontainers/Images/IImage.cs b/src/Testcontainers/Images/IImage.cs
index b9badc5d3..c1b5d17a3 100644
--- a/src/Testcontainers/Images/IImage.cs
+++ b/src/Testcontainers/Images/IImage.cs
@@ -9,16 +9,16 @@ namespace DotNet.Testcontainers.Images
   public interface IImage
   {
     /// <summary>
-    /// Gets the repository.
+    /// Gets the registry.
     /// </summary>
     [NotNull]
-    string Repository { get; }
+    string Registry { get; }
 
     /// <summary>
-    /// Gets the name.
+    /// Gets the repository.
     /// </summary>
     [NotNull]
-    string Name { get; }
+    string Repository { get; }
 
     /// <summary>
     /// Gets the tag.
diff --git a/src/Testcontainers/Images/MatchImage.cs b/src/Testcontainers/Images/MatchImage.cs
index 8819cdcc7..507780c31 100644
--- a/src/Testcontainers/Images/MatchImage.cs
+++ b/src/Testcontainers/Images/MatchImage.cs
@@ -1,36 +1,24 @@
 namespace DotNet.Testcontainers.Images
 {
-  using System;
-  using System.Linq;
+  using System.Text.RegularExpressions;
 
   internal static class MatchImage
   {
+    private static readonly Regex _imagePattern = new Regex(@"^((?<registry>[^\.\/\:]+(\.[^\.\/\:]*)+(\:[^\/]+)?|[^\:\/]+(\:[^\/]+)|localhost)\/)?(?<repository>[^\:\n]*)(\:(?<tag>.+)?)?$", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+
     public static IImage Match(string image)
     {
       _ = Guard.Argument(image, nameof(image))
         .NotNull()
         .NotEmpty();
 
-      var imageComponents = image
-        .Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
-
-      var repository = string.Join("/", imageComponents
-        .Take(imageComponents.Length - 1));
-
-      var name = imageComponents
-        .Last()
-        .Split(':')
-        .DefaultIfEmpty(string.Empty)
-        .First();
+      var match = _imagePattern.Match(image);
 
-      var tag = imageComponents
-        .Last()
-        .Split(':')
-        .Skip(1)
-        .DefaultIfEmpty(string.Empty)
-        .First();
+      var registry = match.Groups[1].Value;
+      var repository = match.Groups[2].Value;
+      var tag = match.Groups[3].Value;
 
-      return new DockerImage(repository, name, tag);
+      return new DockerImage(registry, repository, tag);
     }
   }
 }
diff --git a/tests/Testcontainers.Tests/Fixtures/Images/DockerImageFixture.cs b/tests/Testcontainers.Tests/Fixtures/Images/DockerImageFixture.cs
index 1b8404ad9..36c77f381 100644
--- a/tests/Testcontainers.Tests/Fixtures/Images/DockerImageFixture.cs
+++ b/tests/Testcontainers.Tests/Fixtures/Images/DockerImageFixture.cs
@@ -7,20 +7,22 @@ public sealed class DockerImageFixture : TheoryData<DockerImageFixtureSerializab
   {
     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, "baz/foo/bar", "1.0.0")), "baz/foo/bar:1.0.0");
+      Add(new DockerImageFixtureSerializable(new DockerImage(string.Empty, "baz/foo/bar", string.Empty)), "baz/foo/bar");
+      Add(new DockerImageFixtureSerializable(new DockerImage(string.Empty, "baz/foo/bar", string.Empty)), "baz/foo/bar:latest");
+      Add(new DockerImageFixtureSerializable(new DockerImage(string.Empty, "foo/bar", "1.0.0")), "foo/bar:1.0.0");
+      Add(new DockerImageFixtureSerializable(new DockerImage(string.Empty, "foo/bar", string.Empty)), "foo/bar");
+      Add(new DockerImageFixtureSerializable(new DockerImage(string.Empty, "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("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(string.Empty, "fedora/httpd", "version1.0.test")), "fedora/httpd:version1.0.test");
+      Add(new DockerImageFixtureSerializable(new DockerImage(string.Empty, "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("localhost", "foo/bar", string.Empty)), "localhost/foo/bar");
+      Add(new DockerImageFixtureSerializable(new DockerImage("localhost:5000", "foo/bar", "baz")), "localhost:5000/foo/bar:baz");
     }
   }
 }
diff --git a/tests/Testcontainers.Tests/Fixtures/Images/DockerImageFixtureSerializable.cs b/tests/Testcontainers.Tests/Fixtures/Images/DockerImageFixtureSerializable.cs
index fba2f1fef..e403a43cf 100644
--- a/tests/Testcontainers.Tests/Fixtures/Images/DockerImageFixtureSerializable.cs
+++ b/tests/Testcontainers.Tests/Fixtures/Images/DockerImageFixtureSerializable.cs
@@ -18,17 +18,17 @@ public DockerImageFixtureSerializable(IImage image)
 
     public void Deserialize(IXunitSerializationInfo info)
     {
-      var repository = info.GetValue<string>("Repository");
-      var name = info.GetValue<string>("Name");
-      var tag = info.GetValue<string>("Tag");
-      Image = new DockerImage(repository, name, tag);
+      var registry = info.GetValue<string>(nameof(IImage.Registry));
+      var repository = info.GetValue<string>(nameof(IImage.Repository));
+      var tag = info.GetValue<string>(nameof(IImage.Tag));
+      Image = new DockerImage(registry, repository, tag);
     }
 
     public void Serialize(IXunitSerializationInfo info)
     {
-      info.AddValue("Repository", Image.Repository);
-      info.AddValue("Name", Image.Name);
-      info.AddValue("Tag", Image.Tag);
+      info.AddValue(nameof(IImage.Registry), Image.Registry);
+      info.AddValue(nameof(IImage.Repository), Image.Repository);
+      info.AddValue(nameof(IImage.Tag), Image.Tag);
     }
   }
 }
diff --git a/tests/Testcontainers.Tests/Fixtures/Images/HealthCheckFixture.cs b/tests/Testcontainers.Tests/Fixtures/Images/HealthCheckFixture.cs
index db4034d3e..2685d0f86 100644
--- a/tests/Testcontainers.Tests/Fixtures/Images/HealthCheckFixture.cs
+++ b/tests/Testcontainers.Tests/Fixtures/Images/HealthCheckFixture.cs
@@ -14,9 +14,9 @@ public sealed class HealthCheckFixture : IImage, IAsyncLifetime
       .WithDockerfileDirectory(Path.Combine(Directory.GetCurrentDirectory(), "Assets", "healthWaitStrategy"))
       .Build();
 
-    public string Repository => _image.Repository;
+    public string Registry => _image.Registry;
 
-    public string Name => _image.Name;
+    public string Repository => _image.Repository;
 
     public string Tag => _image.Tag;
 
diff --git a/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs b/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs
index 11a080559..977946a58 100644
--- a/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs
+++ b/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs
@@ -44,12 +44,12 @@ 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("", "fedora/httpd", "1.0")]
+    [InlineData("", "foo/bar/baz", "1.0.0")]
+    public void GetHostnameFromHubImageNamePrefix(string registry, string repository, string tag)
     {
       const string hubImageNamePrefix = "myregistry.azurecr.io";
-      IImage image = new DockerImage(repository, name, tag, hubImageNamePrefix);
+      IImage image = new DockerImage(registry, repository, tag, hubImageNamePrefix);
       Assert.Equal(hubImageNamePrefix, image.GetHostname());
     }
 
diff --git a/tests/Testcontainers.Tests/Unit/Configurations/ResourcePropertiesTest.cs b/tests/Testcontainers.Tests/Unit/Configurations/ResourcePropertiesTest.cs
index 2e39f06ef..06b62f26d 100644
--- a/tests/Testcontainers.Tests/Unit/Configurations/ResourcePropertiesTest.cs
+++ b/tests/Testcontainers.Tests/Unit/Configurations/ResourcePropertiesTest.cs
@@ -126,8 +126,8 @@ await Task.CompletedTask
         .ConfigureAwait(false);
 
       // Then
+      Assert.Throws<InvalidOperationException>(() => image.Registry);
       Assert.Throws<InvalidOperationException>(() => image.Repository);
-      Assert.Throws<InvalidOperationException>(() => image.Name);
       Assert.Throws<InvalidOperationException>(() => image.Tag);
       Assert.Throws<InvalidOperationException>(() => image.FullName);
       Assert.Throws<InvalidOperationException>(() => image.GetHostname());
diff --git a/tests/Testcontainers.Tests/Unit/Images/ImageFromDockerfileTest.cs b/tests/Testcontainers.Tests/Unit/Images/ImageFromDockerfileTest.cs
index eba21b75e..c036d51d6 100644
--- a/tests/Testcontainers.Tests/Unit/Images/ImageFromDockerfileTest.cs
+++ b/tests/Testcontainers.Tests/Unit/Images/ImageFromDockerfileTest.cs
@@ -18,7 +18,7 @@ public sealed class ImageFromDockerfileTest
     public async Task DockerfileArchiveTar()
     {
       // Given
-      var image = new DockerImage("testcontainers", "test", "0.1.0");
+      var image = new DockerImage(string.Empty, "testcontainers/test", "0.1.0");
 
       var expected = new SortedSet<string> { ".dockerignore", "Dockerfile", "setup/setup.sh" };
 
@@ -84,9 +84,9 @@ public async Task ThrowsDockerfileDirectoryDoesNotExist()
     public async Task BuildsDockerImage()
     {
       // Given
-      IImage tag1 = new DockerImage("localhost/testcontainers", Guid.NewGuid().ToString("D"), string.Empty);
+      IImage tag1 = new DockerImage("localhost", $"testcontainers/{Guid.NewGuid():D}", string.Empty);
 
-      IImage tag2 = new DockerImage("localhost/testcontainers", Guid.NewGuid().ToString("D"), string.Empty);
+      IImage tag2 = new DockerImage("localhost", $"testcontainers/{Guid.NewGuid():D}", string.Empty);
 
       var imageFromDockerfileBuilder = new ImageFromDockerfileBuilder()
         .WithName(tag1)
@@ -106,8 +106,8 @@ await imageFromDockerfileBuilder.CreateAsync()
       // Then
       Assert.True(DockerCli.ResourceExists(DockerCli.DockerResource.Image, tag1.FullName));
       Assert.True(DockerCli.ResourceExists(DockerCli.DockerResource.Image, tag2.FullName));
+      Assert.NotNull(imageFromDockerfileBuilder.Registry);
       Assert.NotNull(imageFromDockerfileBuilder.Repository);
-      Assert.NotNull(imageFromDockerfileBuilder.Name);
       Assert.NotNull(imageFromDockerfileBuilder.Tag);
       Assert.NotNull(imageFromDockerfileBuilder.FullName);
       Assert.Null(imageFromDockerfileBuilder.GetHostname());
diff --git a/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs b/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs
index 40a47743e..d8b81c914 100644
--- a/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs
+++ b/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs
@@ -12,7 +12,7 @@ public void ShouldThrowArgumentNullExceptionWhenInstantiateDockerImage()
     {
       Assert.Throws<ArgumentException>(() => new DockerImage((string)null));
       Assert.Throws<ArgumentException>(() => new DockerImage(null, null, null));
-      Assert.Throws<ArgumentException>(() => new DockerImage("fedora", null, null));
+      Assert.Throws<ArgumentException>(() => new DockerImage(null, "fedora", null));
     }
 
     [Fact]
@@ -54,8 +54,8 @@ public void WhenImageNameGetsAssigned(DockerImageFixtureSerializable serializabl
       IImage dockerImage = new DockerImage(fullName);
 
       // Then
+      Assert.Equal(expected.Registry, dockerImage.Registry);
       Assert.Equal(expected.Repository, dockerImage.Repository);
-      Assert.Equal(expected.Name, dockerImage.Name);
       Assert.Equal(expected.Tag, dockerImage.Tag);
       Assert.Equal(expected.FullName, dockerImage.FullName);
     }