Skip to content

Commit 5ac5085

Browse files
committed
[IntegrationTest] Add TestContainers
1 parent 85bca83 commit 5ac5085

9 files changed

+756
-1
lines changed

redmine-net-api.sln

+9
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Others", "Others", "{4ADECA
5252
global.json = global.json
5353
EndProjectSection
5454
EndProject
55+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "redmine-net-api.Integration.Tests", "tests\redmine-net-api.Integration.Tests\redmine-net-api.Integration.Tests.csproj", "{254DABFE-7C92-4C16-84A5-630330D56D4D}"
56+
EndProject
5557
Global
5658
GlobalSection(SolutionConfigurationPlatforms) = preSolution
5759
Debug|Any CPU = Debug|Any CPU
@@ -70,6 +72,12 @@ Global
7072
{900EF0B3-0233-45DA-811F-4C59483E8452}.DebugJson|Any CPU.ActiveCfg = DebugJson|Any CPU
7173
{900EF0B3-0233-45DA-811F-4C59483E8452}.DebugJson|Any CPU.Build.0 = DebugJson|Any CPU
7274
{900EF0B3-0233-45DA-811F-4C59483E8452}.Release|Any CPU.ActiveCfg = Debug|Any CPU
75+
{254DABFE-7C92-4C16-84A5-630330D56D4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
76+
{254DABFE-7C92-4C16-84A5-630330D56D4D}.Debug|Any CPU.Build.0 = Debug|Any CPU
77+
{254DABFE-7C92-4C16-84A5-630330D56D4D}.DebugJson|Any CPU.ActiveCfg = Debug|Any CPU
78+
{254DABFE-7C92-4C16-84A5-630330D56D4D}.DebugJson|Any CPU.Build.0 = Debug|Any CPU
79+
{254DABFE-7C92-4C16-84A5-630330D56D4D}.Release|Any CPU.ActiveCfg = Release|Any CPU
80+
{254DABFE-7C92-4C16-84A5-630330D56D4D}.Release|Any CPU.Build.0 = Release|Any CPU
7381
EndGlobalSection
7482
GlobalSection(SolutionProperties) = preSolution
7583
HideSolutionNode = FALSE
@@ -82,6 +90,7 @@ Global
8290
{707B6A3F-1A2C-4EFE-851F-1DB0E68CFFFB} = {E8C35EC2-DD90-46E8-9B63-84EFD5F2FDE3}
8391
{1D340EEB-C535-45D4-80D7-ADD4434D7B77} = {E8C35EC2-DD90-46E8-9B63-84EFD5F2FDE3}
8492
{4ADECA2A-4D7B-4F05-85A2-0C0963A83689} = {E8C35EC2-DD90-46E8-9B63-84EFD5F2FDE3}
93+
{254DABFE-7C92-4C16-84A5-630330D56D4D} = {F3F4278D-6271-4F77-BA88-41555D53CBD1}
8594
EndGlobalSection
8695
GlobalSection(ExtensibilityGlobals) = postSolution
8796
SolutionGuid = {4AA87D90-ABD0-4793-BE47-955B35FAE2BB}

src/redmine-net-api/redmine-net-api.csproj

+4-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,10 @@
100100

101101
<ItemGroup>
102102
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
103-
<_Parameter1>Padi.DotNet.RedmineAPI.Tests</_Parameter1>
103+
<_Parameter1>Padi.DotNet.RedmineAPI.Tests</_Parameter1>
104+
</AssemblyAttribute>
105+
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
106+
<_Parameter1>Padi.DotNet.RedmineAPI.Integration.Tests</_Parameter1>
104107
</AssemblyAttribute>
105108
</ItemGroup>
106109

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Padi.DotNet.RedmineAPI.Integration.Tests;
2+
3+
public static class Constants
4+
{
5+
public const string RedmineTestContainerCollection = nameof(RedmineTestContainerCollection);
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
namespace Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures;
2+
3+
[CollectionDefinition(Constants.RedmineTestContainerCollection)]
4+
public sealed class RedmineTestContainerCollection : ICollectionFixture<RedmineTestContainerFixture> { }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
using DotNet.Testcontainers.Builders;
2+
using DotNet.Testcontainers.Configurations;
3+
using DotNet.Testcontainers.Containers;
4+
using DotNet.Testcontainers.Networks;
5+
using Npgsql;
6+
using Redmine.Net.Api;
7+
using Testcontainers.PostgreSql;
8+
9+
namespace Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures;
10+
11+
public class RedmineTestContainerFixture : IAsyncLifetime
12+
{
13+
private const int RedminePort = 3000;
14+
private const int PostgresPort = 5432;
15+
private const string PostgresImage = "postgres:17.4-alpine";
16+
private const string RedmineImage = "redmine:6.0.5-alpine";
17+
private const string PostgresDb = "postgres";
18+
private const string PostgresUser = "postgres";
19+
private const string PostgresPassword = "postgres";
20+
private const string RedmineSqlFilePath = "TestData/init-redmine.sql";
21+
22+
public const string RedmineApiKey = "029a9d38-17e8-41ae-bc8c-fbf71e193c57";
23+
24+
private readonly string RedmineNetworkAlias = Guid.NewGuid().ToString();
25+
private INetwork Network { get; set; }
26+
private PostgreSqlContainer PostgresContainer { get; set; }
27+
private IContainer RedmineContainer { get; set; }
28+
public RedmineManager RedmineManager { get; private set; }
29+
public string RedmineHost { get; private set; }
30+
31+
public RedmineTestContainerFixture()
32+
{
33+
BuildContainers();
34+
}
35+
36+
private void BuildContainers()
37+
{
38+
Network = new NetworkBuilder()
39+
.WithDriver(NetworkDriver.Bridge)
40+
.Build();
41+
42+
PostgresContainer = new PostgreSqlBuilder()
43+
.WithImage(PostgresImage)
44+
.WithNetwork(Network)
45+
.WithNetworkAliases(RedmineNetworkAlias)
46+
.WithPortBinding(PostgresPort, assignRandomHostPort: true)
47+
.WithEnvironment(new Dictionary<string, string>
48+
{
49+
{ "POSTGRES_DB", PostgresDb },
50+
{ "POSTGRES_USER", PostgresUser },
51+
{ "POSTGRES_PASSWORD", PostgresPassword },
52+
})
53+
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(PostgresPort))
54+
.Build();
55+
56+
RedmineContainer = new ContainerBuilder()
57+
.WithImage(RedmineImage)
58+
.WithNetwork(Network)
59+
.WithPortBinding(RedminePort, assignRandomHostPort: true)
60+
.WithEnvironment(new Dictionary<string, string>
61+
{
62+
{ "REDMINE_DB_POSTGRES", RedmineNetworkAlias },
63+
{ "REDMINE_DB_PORT", PostgresPort.ToString() },
64+
{ "REDMINE_DB_DATABASE", PostgresDb },
65+
{ "REDMINE_DB_USERNAME", PostgresUser },
66+
{ "REDMINE_DB_PASSWORD", PostgresPassword },
67+
})
68+
.DependsOn(PostgresContainer)
69+
.WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request => request.ForPort(RedminePort).ForPath("/")))
70+
.Build();
71+
}
72+
73+
public async Task InitializeAsync()
74+
{
75+
await Network.CreateAsync();
76+
77+
await PostgresContainer.StartAsync();
78+
79+
await RedmineContainer.StartAsync();
80+
81+
await SeedTestDataAsync(PostgresContainer, CancellationToken.None);
82+
83+
RedmineHost = $"http://{RedmineContainer.Hostname}:{RedmineContainer.GetMappedPublicPort(RedminePort)}";
84+
85+
var rmgBuilder = new RedmineManagerOptionsBuilder()
86+
.WithHost(RedmineHost)
87+
.WithBasicAuthentication("adminuser", "1qaz2wsx");
88+
89+
RedmineManager = new RedmineManager(rmgBuilder);
90+
}
91+
92+
public async Task DisposeAsync()
93+
{
94+
var exceptions = new List<Exception>();
95+
96+
await SafeDisposeAsync(() => RedmineContainer.StopAsync());
97+
await SafeDisposeAsync(() => PostgresContainer.StopAsync());
98+
await SafeDisposeAsync(() => Network.DisposeAsync().AsTask());
99+
100+
if (exceptions.Count > 0)
101+
{
102+
throw new AggregateException(exceptions);
103+
}
104+
105+
return;
106+
107+
async Task SafeDisposeAsync(Func<Task> disposeFunc)
108+
{
109+
try
110+
{
111+
await disposeFunc();
112+
}
113+
catch (Exception ex)
114+
{
115+
exceptions.Add(ex);
116+
}
117+
}
118+
}
119+
120+
private static async Task SeedTestDataAsync(PostgreSqlContainer container, CancellationToken ct)
121+
{
122+
const int maxDbAttempts = 10;
123+
var dbRetryDelay = TimeSpan.FromSeconds(2);
124+
var connectionString = container.GetConnectionString();
125+
for (var attempt = 1; attempt <= maxDbAttempts; attempt++)
126+
{
127+
try
128+
{
129+
await using var conn = new NpgsqlConnection(connectionString);
130+
await conn.OpenAsync(ct);
131+
break;
132+
}
133+
catch
134+
{
135+
if (attempt == maxDbAttempts)
136+
{
137+
throw;
138+
}
139+
await Task.Delay(dbRetryDelay, ct);
140+
}
141+
}
142+
var sql = await System.IO.File.ReadAllTextAsync(RedmineSqlFilePath, ct);
143+
var res = await container.ExecScriptAsync(sql, ct);
144+
if (!string.IsNullOrWhiteSpace(res.Stderr))
145+
{
146+
// Optionally log stderr
147+
}
148+
}
149+
}

0 commit comments

Comments
 (0)