diff --git a/Testcontainers.sln b/Testcontainers.sln index ac471c14e..176958bfa 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -63,6 +63,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Neo4j", "src EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Oracle", "src\Testcontainers.Oracle\Testcontainers.Oracle.csproj", "{596EAFC1-0496-495C-B382-D57415FA456A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Papercut", "src\Testcontainers.Papercut\Testcontainers.Papercut.csproj", "{464F1120-A0DA-462B-B9E8-45176D883625}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.PostgreSql", "src\Testcontainers.PostgreSql\Testcontainers.PostgreSql.csproj", "{8AB91636-9055-4900-A72A-7CFFACDFDBF0}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.PubSub", "src\Testcontainers.PubSub\Testcontainers.PubSub.csproj", "{E6642255-667D-476B-B584-089AA5E6C0B1}" @@ -135,6 +137,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Neo4j.Tests" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Oracle.Tests", "tests\Testcontainers.Oracle.Tests\Testcontainers.Oracle.Tests.csproj", "{4AC1088B-9965-4497-AC8E-570F1AD5631F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Papercut.Tests", "tests\Testcontainers.Papercut.Tests\Testcontainers.Papercut.Tests.csproj", "{904C8476-FCEF-41F0-8948-9EFA7C08712E}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Platform.Linux.Tests", "tests\Testcontainers.Platform.Linux.Tests\Testcontainers.Platform.Linux.Tests.csproj", "{DA1D7ADE-452C-4369-83CC-56289176EACD}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Platform.Windows.Tests", "tests\Testcontainers.Platform.Windows.Tests\Testcontainers.Platform.Windows.Tests.csproj", "{3E55CBE8-AFE8-426D-9470-49D63CD1051C}" @@ -268,6 +272,10 @@ Global {596EAFC1-0496-495C-B382-D57415FA456A}.Debug|Any CPU.Build.0 = Debug|Any CPU {596EAFC1-0496-495C-B382-D57415FA456A}.Release|Any CPU.ActiveCfg = Release|Any CPU {596EAFC1-0496-495C-B382-D57415FA456A}.Release|Any CPU.Build.0 = Release|Any CPU + {464F1120-A0DA-462B-B9E8-45176D883625}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {464F1120-A0DA-462B-B9E8-45176D883625}.Debug|Any CPU.Build.0 = Debug|Any CPU + {464F1120-A0DA-462B-B9E8-45176D883625}.Release|Any CPU.ActiveCfg = Release|Any CPU + {464F1120-A0DA-462B-B9E8-45176D883625}.Release|Any CPU.Build.0 = Release|Any CPU {8AB91636-9055-4900-A72A-7CFFACDFDBF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8AB91636-9055-4900-A72A-7CFFACDFDBF0}.Debug|Any CPU.Build.0 = Debug|Any CPU {8AB91636-9055-4900-A72A-7CFFACDFDBF0}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -412,6 +420,10 @@ Global {4AC1088B-9965-4497-AC8E-570F1AD5631F}.Debug|Any CPU.Build.0 = Debug|Any CPU {4AC1088B-9965-4497-AC8E-570F1AD5631F}.Release|Any CPU.ActiveCfg = Release|Any CPU {4AC1088B-9965-4497-AC8E-570F1AD5631F}.Release|Any CPU.Build.0 = Release|Any CPU + {904C8476-FCEF-41F0-8948-9EFA7C08712E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {904C8476-FCEF-41F0-8948-9EFA7C08712E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {904C8476-FCEF-41F0-8948-9EFA7C08712E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {904C8476-FCEF-41F0-8948-9EFA7C08712E}.Release|Any CPU.Build.0 = Release|Any CPU {DA1D7ADE-452C-4369-83CC-56289176EACD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DA1D7ADE-452C-4369-83CC-56289176EACD}.Debug|Any CPU.Build.0 = Debug|Any CPU {DA1D7ADE-452C-4369-83CC-56289176EACD}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -487,6 +499,7 @@ Global {BF37BEA1-0816-4326-B1E0-E82290F8FCE0} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {ADC2372B-6FE0-421D-8277-BB628E8EFC22} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {596EAFC1-0496-495C-B382-D57415FA456A} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} + {464F1120-A0DA-462B-B9E8-45176D883625} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {8AB91636-9055-4900-A72A-7CFFACDFDBF0} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {E6642255-667D-476B-B584-089AA5E6C0B1} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {A6D480BC-FDE8-4B92-A2A6-FF16BEE486AE} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -523,6 +536,7 @@ Global {87A3F137-6DC3-4CE5-91E6-01797D076086} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {D3F63405-C0FA-4F83-8B79-E30BFF5FF5BF} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {4AC1088B-9965-4497-AC8E-570F1AD5631F} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} + {904C8476-FCEF-41F0-8948-9EFA7C08712E} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {DA1D7ADE-452C-4369-83CC-56289176EACD} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {3E55CBE8-AFE8-426D-9470-49D63CD1051C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {56D0DCA5-567F-4B3B-8B79-CB108F8EB8A6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} diff --git a/src/Testcontainers.Consul/Usings.cs b/src/Testcontainers.Consul/Usings.cs index 49507dd3e..bf2829a65 100644 --- a/src/Testcontainers.Consul/Usings.cs +++ b/src/Testcontainers.Consul/Usings.cs @@ -1,5 +1,4 @@ global using System; -global using System.Net; global using Docker.DotNet.Models; global using DotNet.Testcontainers.Builders; global using DotNet.Testcontainers.Configurations; diff --git a/src/Testcontainers.Papercut/.editorconfig b/src/Testcontainers.Papercut/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/src/Testcontainers.Papercut/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/src/Testcontainers.Papercut/PapercutBuilder.cs b/src/Testcontainers.Papercut/PapercutBuilder.cs new file mode 100644 index 000000000..4a16972c2 --- /dev/null +++ b/src/Testcontainers.Papercut/PapercutBuilder.cs @@ -0,0 +1,69 @@ +namespace Testcontainers.Papercut; + +/// +[PublicAPI] +public sealed class PapercutBuilder : ContainerBuilder +{ + public const string PapercutImage = "jijiechen/papercut:latest"; + + public const ushort HttpPort = 37408; + + public const ushort SmtpPort = 25; + + /// + /// Initializes a new instance of the class. + /// + public PapercutBuilder() + : this(new PapercutConfiguration()) + { + DockerResourceConfiguration = Init().DockerResourceConfiguration; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + private PapercutBuilder(PapercutConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + DockerResourceConfiguration = resourceConfiguration; + } + + /// + protected override PapercutConfiguration DockerResourceConfiguration { get; } + + /// + public override PapercutContainer Build() + { + Validate(); + return new PapercutContainer(DockerResourceConfiguration, TestcontainersSettings.Logger); + } + + /// + protected override PapercutBuilder Init() + { + return base.Init() + .WithImage(PapercutImage) + .WithPortBinding(HttpPort, true) + .WithPortBinding(SmtpPort, true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request => request.ForPort(HttpPort))); + } + + /// + protected override PapercutBuilder Clone(IResourceConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new PapercutConfiguration(resourceConfiguration)); + } + + /// + protected override PapercutBuilder Clone(IContainerConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new PapercutConfiguration(resourceConfiguration)); + } + + /// + protected override PapercutBuilder Merge(PapercutConfiguration oldValue, PapercutConfiguration newValue) + { + return new PapercutBuilder(new PapercutConfiguration(oldValue, newValue)); + } +} \ No newline at end of file diff --git a/src/Testcontainers.Papercut/PapercutConfiguration.cs b/src/Testcontainers.Papercut/PapercutConfiguration.cs new file mode 100644 index 000000000..6582a0276 --- /dev/null +++ b/src/Testcontainers.Papercut/PapercutConfiguration.cs @@ -0,0 +1,53 @@ +namespace Testcontainers.Papercut; + +/// +[PublicAPI] +public sealed class PapercutConfiguration : ContainerConfiguration +{ + /// + /// Initializes a new instance of the class. + /// + public PapercutConfiguration() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public PapercutConfiguration(IResourceConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + // Passes the configuration upwards to the base implementations to create an updated immutable copy. + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public PapercutConfiguration(IContainerConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + // Passes the configuration upwards to the base implementations to create an updated immutable copy. + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public PapercutConfiguration(PapercutConfiguration resourceConfiguration) + : this(new PapercutConfiguration(), resourceConfiguration) + { + // Passes the configuration upwards to the base implementations to create an updated immutable copy. + } + + /// + /// Initializes a new instance of the class. + /// + /// The old Docker resource configuration. + /// The new Docker resource configuration. + public PapercutConfiguration(PapercutConfiguration oldValue, PapercutConfiguration newValue) + : base(oldValue, newValue) + { + } +} \ No newline at end of file diff --git a/src/Testcontainers.Papercut/PapercutContainer.cs b/src/Testcontainers.Papercut/PapercutContainer.cs new file mode 100644 index 000000000..e73e762e6 --- /dev/null +++ b/src/Testcontainers.Papercut/PapercutContainer.cs @@ -0,0 +1,30 @@ +namespace Testcontainers.Papercut; + +/// +[PublicAPI] +public sealed class PapercutContainer : DockerContainer +{ + /// + /// Initializes a new instance of the class. + /// + /// The container configuration. + /// The logger. + public PapercutContainer(PapercutConfiguration configuration, ILogger logger) + : base(configuration, logger) + { + } + + /// + /// Gets the SMTP port. + /// + public ushort SmtpPort => GetMappedPublicPort(PapercutBuilder.SmtpPort); + + /// + /// Gets the Papercut base address. + /// + /// The Papercut base address. + public string GetBaseAddress() + { + return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(PapercutBuilder.HttpPort)).ToString(); + } +} \ No newline at end of file diff --git a/src/Testcontainers.Papercut/Testcontainers.Papercut.csproj b/src/Testcontainers.Papercut/Testcontainers.Papercut.csproj new file mode 100644 index 000000000..054b86339 --- /dev/null +++ b/src/Testcontainers.Papercut/Testcontainers.Papercut.csproj @@ -0,0 +1,16 @@ + + + netstandard2.0;netstandard2.1 + latest + Copyright (c) 2019 - 2023 Liam Wilson, Andre Hofmeister and other authors + Liam Wilson, Andre Hofmeister and contributors + A Testcontainers Papercut module for testing SMTP clients and sending emails. + + + + + + + + + \ No newline at end of file diff --git a/src/Testcontainers.Papercut/Usings.cs b/src/Testcontainers.Papercut/Usings.cs new file mode 100644 index 000000000..bf2829a65 --- /dev/null +++ b/src/Testcontainers.Papercut/Usings.cs @@ -0,0 +1,7 @@ +global using System; +global using Docker.DotNet.Models; +global using DotNet.Testcontainers.Builders; +global using DotNet.Testcontainers.Configurations; +global using DotNet.Testcontainers.Containers; +global using JetBrains.Annotations; +global using Microsoft.Extensions.Logging; \ No newline at end of file diff --git a/tests/Testcontainers.Papercut.Tests/.editorconfig b/tests/Testcontainers.Papercut.Tests/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/tests/Testcontainers.Papercut.Tests/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/tests/Testcontainers.Papercut.Tests/PapercutContainerTest.cs b/tests/Testcontainers.Papercut.Tests/PapercutContainerTest.cs new file mode 100644 index 000000000..6c51f803c --- /dev/null +++ b/tests/Testcontainers.Papercut.Tests/PapercutContainerTest.cs @@ -0,0 +1,72 @@ +namespace Testcontainers.Papercut; + +public sealed class PapercutContainerTest : IAsyncLifetime +{ + private readonly PapercutContainer _papercutContainer = new PapercutBuilder().Build(); + + public Task InitializeAsync() + { + return _papercutContainer.StartAsync(); + } + + public Task DisposeAsync() + { + return _papercutContainer.DisposeAsync().AsTask(); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task ReceivesSentMessage() + { + // Given + const string subject = "Test"; + + Message[] messages; + + using var httpClient = new HttpClient(); + httpClient.BaseAddress = new Uri(_papercutContainer.GetBaseAddress()); + + using var smtpClient = new SmtpClient(_papercutContainer.Hostname, _papercutContainer.SmtpPort); + + // When + smtpClient.Send("from@example.com", "to@example.com", subject, "A test message"); + + do + { + var messagesJson = await httpClient.GetStringAsync("/api/messages") + .ConfigureAwait(false); + + var jsonDocument = JsonDocument.Parse(messagesJson); + messages = jsonDocument.RootElement.GetProperty("messages").Deserialize(); + } + while (messages.Length == 0); + + // Then + Assert.NotEmpty(messages); + Assert.Equal(subject, messages[0].Subject); + } + + private readonly struct Message + { + [JsonConstructor] + public Message(string id, string subject, string size, DateTime createdAt) + { + Id = id; + Subject = subject; + Size = size; + CreatedAt = createdAt; + } + + [JsonPropertyName("id")] + public string Id { get; } + + [JsonPropertyName("subject")] + public string Subject { get; } + + [JsonPropertyName("size")] + public string Size { get; } + + [JsonPropertyName("createdAt")] + public DateTime CreatedAt { get; } + } +} \ No newline at end of file diff --git a/tests/Testcontainers.Papercut.Tests/Testcontainers.Papercut.Tests.csproj b/tests/Testcontainers.Papercut.Tests/Testcontainers.Papercut.Tests.csproj new file mode 100644 index 000000000..d42adb2ec --- /dev/null +++ b/tests/Testcontainers.Papercut.Tests/Testcontainers.Papercut.Tests.csproj @@ -0,0 +1,17 @@ + + + net6.0 + false + false + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Testcontainers.Papercut.Tests/Usings.cs b/tests/Testcontainers.Papercut.Tests/Usings.cs new file mode 100644 index 000000000..763582fff --- /dev/null +++ b/tests/Testcontainers.Papercut.Tests/Usings.cs @@ -0,0 +1,8 @@ +global using System; +global using System.Net.Http; +global using System.Net.Mail; +global using System.Text.Json; +global using System.Text.Json.Serialization; +global using System.Threading.Tasks; +global using DotNet.Testcontainers.Commons; +global using Xunit; \ No newline at end of file