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