diff --git a/Directory.Build.props b/Directory.Build.props index e04826178..70c21f5e4 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,7 +9,7 @@ $(Version) $(Version) Testcontainers - Copyright (c) 2019 - 2023 Andre Hofmeister and other authors + Copyright (c) 2019 - 2024 Andre Hofmeister and other authors Andre Hofmeister and contributors Andre Hofmeister Testcontainers for .NET is a library to support tests with throwaway instances of Docker containers for all compatible .NET Standard versions. diff --git a/LICENSE b/LICENSE index 83918c3a7..4ebf42b2d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2019 - 2023 Andre Hofmeister and other authors +Copyright (c) 2019 - 2024 Andre Hofmeister and other authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.md b/README.md index 6c59acf34..b27fd6184 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ See [LICENSE](https://github.com/testcontainers/testcontainers-dotnet/blob/main/ ## Copyright -Copyright (c) 2019 - 2023 Andre Hofmeister and other authors. +Copyright (c) 2019 - 2024 Andre Hofmeister and other authors. See [contributors][testcontainers-dotnet-contributors] for all contributors. diff --git a/docs/index.md b/docs/index.md index 593b526f6..135e8c274 100644 --- a/docs/index.md +++ b/docs/index.md @@ -88,7 +88,7 @@ See [LICENSE](https://raw.githubusercontent.com/testcontainers/testcontainers-do ## Copyright -Copyright (c) 2019 - 2023 Andre Hofmeister and other authors. +Copyright (c) 2019 - 2024 Andre Hofmeister and other authors. See [contributors][testcontainers-dotnet-contributors] for all contributors. diff --git a/src/Testcontainers.Papercut/Testcontainers.Papercut.csproj b/src/Testcontainers.Papercut/Testcontainers.Papercut.csproj index 4ef9bff5f..e16b4437c 100644 --- a/src/Testcontainers.Papercut/Testcontainers.Papercut.csproj +++ b/src/Testcontainers.Papercut/Testcontainers.Papercut.csproj @@ -2,7 +2,7 @@ netstandard2.0;netstandard2.1 latest - Copyright (c) 2019 - 2023 Liam Wilson, Andre Hofmeister and other authors + Copyright (c) 2019 - 2024 Liam Wilson, Andre Hofmeister and other authors Liam Wilson, Andre Hofmeister and contributors A Testcontainers Papercut module for testing SMTP clients and sending emails. diff --git a/src/Testcontainers/Builders/Base64Provider.cs b/src/Testcontainers/Builders/Base64Provider.cs index fe71dddba..7998aa5e8 100644 --- a/src/Testcontainers/Builders/Base64Provider.cs +++ b/src/Testcontainers/Builders/Base64Provider.cs @@ -72,16 +72,37 @@ public IDockerRegistryAuthenticationConfiguration GetAuthConfig(string hostname) return null; } - if (string.IsNullOrEmpty(auth.GetString())) + if (!JsonValueKind.String.Equals(auth.ValueKind) && !JsonValueKind.Null.Equals(auth.ValueKind)) { + _logger.DockerRegistryAuthPropertyValueKindInvalid(hostname, auth.ValueKind); return null; } - var credentialInBytes = Convert.FromBase64String(auth.GetString()); - var credential = Encoding.UTF8.GetString(credentialInBytes).Split(new[] { ':' }, 2); + var authValue = auth.GetString(); + + if (string.IsNullOrEmpty(authValue)) + { + _logger.DockerRegistryAuthPropertyValueNotFound(hostname); + return null; + } + + byte[] credentialInBytes; + + try + { + credentialInBytes = Convert.FromBase64String(authValue); + } + catch (FormatException e) + { + _logger.DockerRegistryAuthPropertyValueInvalidBase64(hostname, e); + return null; + } + + var credential = Encoding.Default.GetString(credentialInBytes).Split(new[] { ':' }, 2); if (credential.Length != 2) { + _logger.DockerRegistryAuthPropertyValueInvalidBasicAuthenticationFormat(hostname); return null; } diff --git a/src/Testcontainers/Logging.cs b/src/Testcontainers/Logging.cs index c9bfddf90..18f5d2ae0 100644 --- a/src/Testcontainers/Logging.cs +++ b/src/Testcontainers/Logging.cs @@ -2,6 +2,7 @@ namespace DotNet.Testcontainers { using System; using System.Collections.Generic; + using System.Text.Json; using System.Text.RegularExpressions; using DotNet.Testcontainers.Images; using Microsoft.Extensions.Logging; @@ -82,6 +83,18 @@ private static readonly Action _DockerConfigFileNotF private static readonly Action _SearchingDockerRegistryCredential = LoggerMessage.Define(LogLevel.Information, default, "Searching Docker registry credential in {CredentialStore}"); + private static readonly Action _DockerRegistryAuthPropertyValueKindInvalid + = LoggerMessage.Define(LogLevel.Warning, default, "The \"auth\" property value kind for {DockerRegistry} is invalid: {ValueKind}"); + + private static readonly Action _DockerRegistryAuthPropertyValueNotFound + = LoggerMessage.Define(LogLevel.Warning, default, "The \"auth\" property value for {DockerRegistry} not found"); + + private static readonly Action _DockerRegistryAuthPropertyValueInvalidBase64 + = LoggerMessage.Define(LogLevel.Warning, default, "The \"auth\" property value for {DockerRegistry} is not a valid Base64 string"); + + private static readonly Action _DockerRegistryAuthPropertyValueInvalidBasicAuthenticationFormat + = LoggerMessage.Define(LogLevel.Warning, default, "The \"auth\" property value for {DockerRegistry} should contain one colon separating the username and the password (basic authentication)"); + private static readonly Action _DockerRegistryCredentialNotFound = LoggerMessage.Define(LogLevel.Information, default, "Docker registry credential {DockerRegistry} not found"); @@ -212,6 +225,26 @@ public static void SearchingDockerRegistryCredential(this ILogger logger, string _SearchingDockerRegistryCredential(logger, credentialStore, null); } + public static void DockerRegistryAuthPropertyValueKindInvalid(this ILogger logger, string dockerRegistry, JsonValueKind valueKind) + { + _DockerRegistryAuthPropertyValueKindInvalid(logger, dockerRegistry, valueKind, null); + } + + public static void DockerRegistryAuthPropertyValueNotFound(this ILogger logger, string dockerRegistry) + { + _DockerRegistryAuthPropertyValueNotFound(logger, dockerRegistry, null); + } + + public static void DockerRegistryAuthPropertyValueInvalidBase64(this ILogger logger, string dockerRegistry, Exception e) + { + _DockerRegistryAuthPropertyValueInvalidBase64(logger, dockerRegistry, e); + } + + public static void DockerRegistryAuthPropertyValueInvalidBasicAuthenticationFormat(this ILogger logger, string dockerRegistry) + { + _DockerRegistryAuthPropertyValueInvalidBasicAuthenticationFormat(logger, dockerRegistry, null); + } + public static void DockerRegistryCredentialNotFound(this ILogger logger, string dockerRegistry) { _DockerRegistryCredentialNotFound(logger, dockerRegistry, null); diff --git a/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs b/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs index 11a080559..27beda932 100644 --- a/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs +++ b/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs @@ -1,6 +1,7 @@ namespace DotNet.Testcontainers.Tests.Unit { using System; + using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; @@ -8,6 +9,7 @@ namespace DotNet.Testcontainers.Tests.Unit using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Configurations; using DotNet.Testcontainers.Images; + using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Xunit; @@ -62,6 +64,8 @@ public void ShouldGetDefaultDockerRegistryAuthenticationConfiguration() public sealed class Base64ProviderTest { + private readonly WarnLogger _warnLogger = new WarnLogger(); + [Theory] [InlineData("{\"auths\":{\"ghcr.io\":{}}}")] [InlineData("{\"auths\":{\"://ghcr.io\":{}}}")] @@ -79,26 +83,37 @@ public void ResolvePartialDockerRegistry(string jsonDocument) } [Theory] - [InlineData("{}", false)] - [InlineData("{\"auths\":null}", false)] - [InlineData("{\"auths\":{}}", false)] - [InlineData("{\"auths\":{\"ghcr.io\":{}}}", false)] - [InlineData("{\"auths\":{\"" + DockerRegistry + "\":{}}}", true)] - [InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":null}}}", true)] - [InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":\"\"}}}", true)] - [InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":\"dXNlcm5hbWU=\"}}}", true)] - public void ShouldGetNull(string jsonDocument, bool isApplicable) + [InlineData("{}", false, null)] + [InlineData("{\"auths\":null}", false, null)] + [InlineData("{\"auths\":{}}", false, null)] + [InlineData("{\"auths\":{\"ghcr.io\":{}}}", false, null)] + [InlineData("{\"auths\":{\"" + DockerRegistry + "\":{}}}", true, null)] + [InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":null}}}", true, "The \"auth\" property value for https://index.docker.io/v1/ not found")] + [InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":\"\"}}}", true, "The \"auth\" property value for https://index.docker.io/v1/ not found")] + [InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":{}}}}", true, "The \"auth\" property value kind for https://index.docker.io/v1/ is invalid: Object")] + [InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":\"Not_Base64_encoded\"}}}", true, "The \"auth\" property value for https://index.docker.io/v1/ is not a valid Base64 string")] + [InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":\"dXNlcm5hbWU=\"}}}", true, "The \"auth\" property value for https://index.docker.io/v1/ should contain one colon separating the username and the password (basic authentication)")] + public void ShouldGetNull(string jsonDocument, bool isApplicable, string logMessage) { // Given var jsonElement = JsonDocument.Parse(jsonDocument).RootElement; // When - var authenticationProvider = new Base64Provider(jsonElement, NullLogger.Instance); + var authenticationProvider = new Base64Provider(jsonElement, _warnLogger); var authConfig = authenticationProvider.GetAuthConfig(DockerRegistry); // Then Assert.Equal(isApplicable, authenticationProvider.IsApplicable(DockerRegistry)); Assert.Null(authConfig); + + if (string.IsNullOrEmpty(logMessage)) + { + Assert.Empty(_warnLogger.LogMessages); + } + else + { + Assert.Single(_warnLogger.LogMessages, item => logMessage.Equals(item.Item2)); + } } [Fact] @@ -223,5 +238,48 @@ static SetEnvVarPath() .Distinct())); } } + + private sealed class Disposable : IDisposable + { + static Disposable() + { + } + + private Disposable() + { + } + + public static IDisposable Empty { get; } + = new Disposable(); + + public void Dispose() + { + } + } + + private sealed class WarnLogger : ILogger + { + private readonly List> _logMessages = new List>(); + + public IEnumerable> LogMessages => _logMessages; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (IsEnabled(logLevel)) + { + _logMessages.Add(Tuple.Create(logLevel, formatter.Invoke(state, exception))); + } + } + + public bool IsEnabled(LogLevel logLevel) + { + return LogLevel.Warning.Equals(logLevel); + } + + public IDisposable BeginScope(TState state) + { + return Disposable.Empty; + } + } } }