Skip to content

Commit 26a69a3

Browse files
committed
feat: testcontainers
1 parent f315a02 commit 26a69a3

6 files changed

Lines changed: 326 additions & 0 deletions

File tree

Directory.Packages.props

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
<PackageVersion Include="SonarAnalyzer.CSharp" Version="10.15.0.120848" />
2727
<PackageVersion Include="System.Net.Http.Json" Version="9.0.9" />
2828
<PackageVersion Include="System.Text.Json" Version="9.0.9" />
29+
<PackageVersion Include="Testcontainers" Version="4.7.0" />
30+
<PackageVersion Include="Testcontainers.Elasticsearch" Version="4.7.0" />
31+
<PackageVersion Include="Testcontainers.PostgreSql" Version="4.7.0" />
32+
<PackageVersion Include="Testcontainers.Redis" Version="4.7.0" />
2933
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
3034
<PackageVersion Include="xunit.v3" Version="3.1.0" />
3135
</ItemGroup>
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
using System.Threading.Tasks;
5+
using DotNet.Testcontainers.Builders;
6+
using DotNet.Testcontainers.Configurations;
7+
using Testcontainers.Elasticsearch;
8+
using Testcontainers.PostgreSql;
9+
using Testcontainers.Redis;
10+
using Xunit;
11+
using Zammad.Client.IntegrationTests.Setup;
12+
13+
[assembly: AssemblyFixture(typeof(ZammadStackFixture))]
14+
15+
namespace Zammad.Client.IntegrationTests.Setup;
16+
17+
public class ZammadStackFixture : IAsyncLifetime
18+
{
19+
private const string ZammadImage = "ghcr.io/zammad/zammad:6.5.2";
20+
21+
private readonly List<IAsyncDisposable> _resources = [];
22+
23+
private async Task<string> GetAutowizardJson()
24+
{
25+
var json = await TestFile.ReadStringAsync("autowizard.json");
26+
var utf8 = Encoding.UTF8.GetBytes(json);
27+
var base64 = Convert.ToBase64String(utf8);
28+
return base64;
29+
}
30+
31+
private TaskCompletionSource _ready = new();
32+
33+
public async Task WaitUntilReadyAsync() => await _ready.Task;
34+
35+
private void SetReady() => _ready.TrySetResult();
36+
37+
private int? _publicPort = null;
38+
39+
public async Task<Uri> GetPublicUriAsync()
40+
{
41+
await WaitUntilReadyAsync();
42+
return new Uri($"http://127.0.0.1:{_publicPort.Value}");
43+
}
44+
45+
public async ValueTask InitializeAsync()
46+
{
47+
using IOutputConsumer outputConsumer = Consume.RedirectStdoutAndStderrToConsole();
48+
49+
var environment = new Dictionary<string, string>
50+
{
51+
["MEMCACHE_SERVERS"] = "zammad-memcached:11211",
52+
["POSTGRESQL_DB"] = "zammad_production",
53+
["POSTGRESQL_HOST"] = "zammad-postgres",
54+
["POSTGRESQL_PASSWORD"] = "zammad",
55+
["POSTGRESQL_USER"] = "zammad",
56+
["POSTGRESQL_PORT"] = "5432",
57+
["POSTGRESQL_OPTIONS"] = "?pool=10",
58+
["REDIS_URL"] = "redis://zammad-redis:6379",
59+
["AUTOWIZARD_JSON"] = await GetAutowizardJson(),
60+
};
61+
62+
var network = new NetworkBuilder().WithName(Guid.NewGuid().ToString("D")).WithCleanUp(true).Build();
63+
_resources.Add(network);
64+
65+
var storage = new VolumeBuilder().WithName(Guid.NewGuid().ToString("D")).WithCleanUp(true).Build();
66+
_resources.Add(storage);
67+
68+
var zammadElasticsearch = new ElasticsearchBuilder()
69+
.WithImage("elasticsearch:8.19.4")
70+
.WithEnvironment("discovery.type", "single-node")
71+
.WithEnvironment("xpack.security.enabled", "false")
72+
.WithEnvironment("ES_JAVA_OPTS", "-Xms512m -Xmx512m")
73+
.WithNetwork(network)
74+
.WithName("zammad-elasticsearch")
75+
.WithCleanUp(true)
76+
.Build();
77+
_resources.Add(zammadElasticsearch);
78+
79+
var zammadPostgres = new PostgreSqlBuilder()
80+
.WithDatabase("zammad_production")
81+
.WithUsername("zammad")
82+
.WithPassword("zammad")
83+
.WithImage("postgres:17.6-alpine")
84+
.WithNetwork(network)
85+
.WithName("zammad-postgres")
86+
.WithCleanUp(true)
87+
.Build();
88+
_resources.Add(zammadPostgres);
89+
90+
var zammadRedis = new RedisBuilder()
91+
.WithImage("redis:7.4.5-alpine")
92+
.WithNetwork(network)
93+
.WithName("zammad-redis")
94+
.WithCleanUp(true)
95+
.Build();
96+
_resources.Add(zammadRedis);
97+
98+
var zammadMemcached = new ContainerBuilder()
99+
.WithImage("memcached:1.6.39-alpine")
100+
.WithName("zammad-memcached")
101+
.WithCommand("--memory-limit=64")
102+
.WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(11211))
103+
.WithNetwork(network)
104+
.WithCleanUp(true)
105+
.WithOutputConsumer(outputConsumer)
106+
.Build();
107+
_resources.Add(zammadMemcached);
108+
109+
var zammadInit = new ContainerBuilder()
110+
.WithImage(ZammadImage)
111+
.WithCommand("zammad-init")
112+
.WithName("zammad-init")
113+
.DependsOn(zammadPostgres)
114+
.WithEnvironment(environment)
115+
.WithCreateParameterModifier(x => x.User = "0:0")
116+
.WithNetwork(network)
117+
.WithVolumeMount(storage, "/opt/zammad/storage")
118+
.WithCleanUp(true)
119+
.Build();
120+
_resources.Add(zammadInit);
121+
122+
var zammadRailsserver = new ContainerBuilder()
123+
.WithImage(ZammadImage)
124+
.WithCommand("zammad-railsserver")
125+
.WithName("zammad-railsserver")
126+
.DependsOn(zammadMemcached)
127+
.DependsOn(zammadPostgres)
128+
.DependsOn(zammadRedis)
129+
.WithEnvironment(environment)
130+
.WithVolumeMount(storage, "/opt/zammad/storage")
131+
.WithNetwork(network)
132+
.WithCleanUp(true)
133+
.Build();
134+
_resources.Add(zammadRailsserver);
135+
136+
var zammadNginx = new ContainerBuilder()
137+
.WithImage(ZammadImage)
138+
.WithCommand("zammad-nginx")
139+
.WithName("zammad-nginx")
140+
.DependsOn(zammadRailsserver)
141+
.WithEnvironment(environment)
142+
.WithNetwork(network)
143+
.WithExposedPort(8080)
144+
.WithPortBinding(8080, true)
145+
.WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(8080))
146+
.WithVolumeMount(storage, "/opt/zammad/storage")
147+
.WithCleanUp(true)
148+
.Build();
149+
_resources.Add(zammadNginx);
150+
151+
var zammadScheduler = new ContainerBuilder()
152+
.WithImage(ZammadImage)
153+
.WithCommand("zammad-scheduler")
154+
.WithName("zammad-scheduler")
155+
.DependsOn(zammadMemcached)
156+
.DependsOn(zammadPostgres)
157+
.DependsOn(zammadRedis)
158+
.WithEnvironment(environment)
159+
.WithVolumeMount(storage, "/opt/zammad/storage")
160+
.WithNetwork(network)
161+
.WithCleanUp(true)
162+
.Build();
163+
_resources.Add(zammadScheduler);
164+
165+
var zammadWebsocket = new ContainerBuilder()
166+
.WithImage(ZammadImage)
167+
.WithCommand("zammad-websocket")
168+
.WithName("zammad-websocket")
169+
.DependsOn(zammadMemcached)
170+
.DependsOn(zammadPostgres)
171+
.DependsOn(zammadRedis)
172+
.WithEnvironment(environment)
173+
.WithVolumeMount(storage, "/opt/zammad/storage")
174+
.WithNetwork(network)
175+
.WithCleanUp(true)
176+
.Build();
177+
_resources.Add(zammadWebsocket);
178+
179+
await Task.WhenAll([network.CreateAsync(), storage.CreateAsync()]);
180+
181+
await Task.WhenAll(
182+
[
183+
zammadElasticsearch.StartAsync(),
184+
zammadPostgres.StartAsync(),
185+
zammadRedis.StartAsync(),
186+
zammadMemcached.StartAsync(),
187+
]
188+
);
189+
190+
await Task.WhenAll(
191+
[
192+
zammadInit.StartAsync(),
193+
zammadRailsserver.StartAsync(),
194+
zammadNginx.StartAsync(),
195+
zammadScheduler.StartAsync(),
196+
zammadWebsocket.StartAsync(),
197+
]
198+
);
199+
200+
_publicPort = zammadNginx.GetMappedPublicPort(8080);
201+
SetReady();
202+
}
203+
204+
public async ValueTask DisposeAsync()
205+
{
206+
_resources.Reverse();
207+
foreach (IAsyncDisposable asyncDisposable in _resources)
208+
{
209+
await asyncDisposable.DisposeAsync();
210+
}
211+
}
212+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
{
2+
"Users": [
3+
{
4+
"login": "admin@example.com",
5+
"firstname": "Test Admin",
6+
"lastname": "Agent",
7+
"email": "admin@example.com",
8+
"password": "test"
9+
},
10+
{
11+
"login": "agent1@example.com",
12+
"firstname": "Agent 1",
13+
"lastname": "Test",
14+
"email": "agent1@example.com",
15+
"password": "test",
16+
"roles": ["Agent"]
17+
}
18+
],
19+
"Groups": [
20+
{
21+
"name": "some group1",
22+
"users": ["admin@example.com", "agent1@example.com"]
23+
},
24+
{
25+
"name": "Users",
26+
"users": ["admin@example.com", "agent1@example.com"],
27+
"signature": "default",
28+
"email_address_id": 1
29+
}
30+
],
31+
"Channels": [
32+
{
33+
"id": 3,
34+
"area": "Email::Account",
35+
"group": "Users",
36+
"active": false,
37+
"options": {
38+
"inbound": {
39+
"adapter": "imap",
40+
"options": {
41+
"host": "mx1.example.com",
42+
"user": "not_existing",
43+
"password": "not_existing",
44+
"ssl": "ssl"
45+
}
46+
},
47+
"outbound": {
48+
"adapter": "sendmail"
49+
}
50+
}
51+
}
52+
],
53+
"EmailAddresses": [
54+
{
55+
"id": 1,
56+
"channel_id": 3,
57+
"name": "Zammad Helpdesk",
58+
"email": "zammad@localhost"
59+
}
60+
],
61+
"Settings": [
62+
{
63+
"name": "product_name",
64+
"value": "Zammad Test System"
65+
},
66+
{
67+
"name": "developer_mode",
68+
"value": true
69+
}
70+
],
71+
"TextModuleLocale": {
72+
"Locale": "de-de"
73+
}
74+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.IO;
2+
using System.Runtime.CompilerServices;
3+
using System.Threading.Tasks;
4+
5+
namespace Zammad.Client.IntegrationTests;
6+
7+
public static class TestFile
8+
{
9+
public static async Task<string> ReadStringAsync(string file, [CallerFilePath] string filePath = "")
10+
{
11+
var directoryPath = Path.GetDirectoryName(filePath)!;
12+
var fullPath = Path.Combine(directoryPath, file);
13+
return await File.ReadAllTextAsync(fullPath);
14+
}
15+
}

test/Zammad.Client.IntegrationTests/Zammad.Client.IntegrationTests.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1515
</PackageReference>
1616
<PackageReference Include="Microsoft.NET.Test.Sdk" />
17+
<PackageReference Include="Testcontainers" />
18+
<PackageReference Include="Testcontainers.Elasticsearch" />
19+
<PackageReference Include="Testcontainers.PostgreSql" />
20+
<PackageReference Include="Testcontainers.Redis" />
1721
<PackageReference Include="xunit.runner.visualstudio">
1822
<PrivateAssets>all</PrivateAssets>
1923
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Xunit;
4+
using Zammad.Client.IntegrationTests.Setup;
5+
6+
namespace Zammad.Client.IntegrationTests;
7+
8+
public class ZammadFixtureTests(ZammadStackFixture zammadStack)
9+
{
10+
[Fact]
11+
public async Task Stack()
12+
{
13+
var url = await zammadStack.GetPublicUriAsync();
14+
TestContext.Current.TestOutputHelper!.WriteLine("Zammad URL: {0}", url);
15+
await Task.Delay(TimeSpan.FromMinutes(15), TestContext.Current.CancellationToken);
16+
}
17+
}

0 commit comments

Comments
 (0)