1
+ using System . Net ;
2
+ using System . Net . Http . Json ;
3
+ using System . Text ;
4
+ using System . Text . Json ;
5
+ using DotNet . Testcontainers . Builders ;
6
+ using DotNet . Testcontainers . Containers ;
7
+ using DotNet . Testcontainers . Networks ;
8
+ using Microsoft . Extensions . DependencyInjection ;
9
+ using Testcontainers . MsSql ;
10
+ using Xunit ;
11
+
12
+ namespace Passwordless . Tests . Fixtures ;
13
+
14
+ public class TestApiFixture : IAsyncLifetime
15
+ {
16
+ private readonly HttpClient _http = new ( ) ;
17
+
18
+ private readonly INetwork _network ;
19
+ private readonly MsSqlContainer _databaseContainer ;
20
+ private readonly IContainer _apiContainer ;
21
+
22
+ private readonly MemoryStream _databaseContainerStdOut = new ( ) ;
23
+ private readonly MemoryStream _databaseContainerStdErr = new ( ) ;
24
+ private readonly MemoryStream _apiContainerStdOut = new ( ) ;
25
+ private readonly MemoryStream _apiContainerStdErr = new ( ) ;
26
+
27
+ private string PublicApiUrl => $ "http://localhost:{ _apiContainer . GetMappedPublicPort ( 80 ) } ";
28
+
29
+ public TestApiFixture ( )
30
+ {
31
+ const string managementKey = "yourStrong(!)ManagementKey" ;
32
+ const string databaseHost = "database" ;
33
+
34
+ _network = new NetworkBuilder ( )
35
+ . Build ( ) ;
36
+
37
+ _databaseContainer = new MsSqlBuilder ( )
38
+ . WithImage ( "mcr.microsoft.com/mssql/server:2022-latest" )
39
+ . WithNetwork ( _network )
40
+ . WithNetworkAliases ( databaseHost )
41
+ . WithOutputConsumer (
42
+ Consume . RedirectStdoutAndStderrToStream ( _databaseContainerStdOut , _databaseContainerStdErr )
43
+ )
44
+ . Build ( ) ;
45
+
46
+ _apiContainer = new ContainerBuilder ( )
47
+ // https://github.com/passwordless/passwordless-server/pkgs/container/passwordless-test-api
48
+ // TODO: replace with ':stable' after the next release of the server.
49
+ . WithImage ( "ghcr.io/passwordless/passwordless-test-api:latest" )
50
+ . WithNetwork ( _network )
51
+ // Run in development environment to execute migrations
52
+ . WithEnvironment ( "ASPNETCORE_ENVIRONMENT" , "Development" )
53
+ . WithEnvironment ( "ConnectionStrings__sqlite:api" , "" )
54
+ . WithEnvironment ( "ConnectionStrings__mssql:api" ,
55
+ $ "Server={ databaseHost } ,{ MsSqlBuilder . MsSqlPort } ;" +
56
+ "Database=Passwordless;" +
57
+ $ "User Id={ MsSqlBuilder . DefaultUsername } ;" +
58
+ $ "Password={ MsSqlBuilder . DefaultPassword } ;" +
59
+ "Trust Server Certificate=true;" +
60
+ "Trusted_Connection=false;"
61
+ )
62
+ . WithEnvironment ( "PasswordlessManagement__ManagementKey" , managementKey )
63
+ . WithPortBinding ( 80 , true )
64
+ // Wait until the API is launched, has performed migrations, and is ready to accept requests
65
+ . WithWaitStrategy ( Wait
66
+ . ForUnixContainer ( )
67
+ . UntilHttpRequestIsSucceeded ( r => r
68
+ . ForPath ( "/" )
69
+ . ForStatusCode ( HttpStatusCode . OK )
70
+ )
71
+ )
72
+ . WithOutputConsumer (
73
+ Consume . RedirectStdoutAndStderrToStream ( _apiContainerStdOut , _apiContainerStdErr )
74
+ )
75
+ . Build ( ) ;
76
+
77
+ _http . DefaultRequestHeaders . Add ( "ManagementKey" , managementKey ) ;
78
+ }
79
+
80
+ public async Task InitializeAsync ( )
81
+ {
82
+ await _network . CreateAsync ( ) ;
83
+ await _databaseContainer . StartAsync ( ) ;
84
+ await _apiContainer . StartAsync ( ) ;
85
+ }
86
+
87
+ public async Task < IPasswordlessClient > CreateClientAsync ( )
88
+ {
89
+ using var response = await _http . PostAsJsonAsync (
90
+ $ "{ PublicApiUrl } /admin/apps/app{ Guid . NewGuid ( ) : N} /create",
91
+ new { AdminEmail = "[email protected] " , EventLoggingIsEnabled = true }
92
+ ) ;
93
+
94
+ if ( ! response . IsSuccessStatusCode )
95
+ {
96
+ throw new InvalidOperationException (
97
+ $ "Failed to create an app. " +
98
+ $ "Status code: { ( int ) response . StatusCode } . " +
99
+ $ "Response body: { await response . Content . ReadAsStringAsync ( ) } ."
100
+ ) ;
101
+ }
102
+
103
+ var responseContent = await response . Content . ReadFromJsonAsync < JsonElement > ( ) ;
104
+ var apiKey = responseContent . GetProperty ( "apiKey1" ) . GetString ( ) ;
105
+ var apiSecret = responseContent . GetProperty ( "apiSecret1" ) . GetString ( ) ;
106
+
107
+ var services = new ServiceCollection ( ) ;
108
+
109
+ services . AddPasswordlessSdk ( options =>
110
+ {
111
+ options . ApiUrl = PublicApiUrl ;
112
+ options . ApiKey = apiKey ;
113
+ options . ApiSecret = apiSecret ??
114
+ throw new InvalidOperationException ( "Cannot extract API Secret from the response." ) ;
115
+ } ) ;
116
+
117
+ return services . BuildServiceProvider ( ) . GetRequiredService < IPasswordlessClient > ( ) ;
118
+ }
119
+
120
+ public string GetLogs ( )
121
+ {
122
+ var databaseContainerStdOutText = Encoding . UTF8 . GetString (
123
+ _databaseContainerStdOut . ToArray ( )
124
+ ) ;
125
+
126
+ var databaseContainerStdErrText = Encoding . UTF8 . GetString (
127
+ _databaseContainerStdErr . ToArray ( )
128
+ ) ;
129
+
130
+ var apiContainerStdOutText = Encoding . UTF8 . GetString (
131
+ _apiContainerStdOut . ToArray ( )
132
+ ) ;
133
+
134
+ var apiContainerStdErrText = Encoding . UTF8 . GetString (
135
+ _apiContainerStdErr . ToArray ( )
136
+ ) ;
137
+
138
+ // API logs are typically more relevant, so put them first
139
+ return
140
+ $ """
141
+ # API container STDOUT:
142
+
143
+ { apiContainerStdOutText }
144
+
145
+ # API container STDERR:
146
+
147
+ { apiContainerStdErrText }
148
+
149
+ # Database container STDOUT:
150
+
151
+ { databaseContainerStdOutText }
152
+
153
+ # Database container STDERR:
154
+
155
+ { databaseContainerStdErrText }
156
+ """ ;
157
+ }
158
+
159
+ public async Task DisposeAsync ( )
160
+ {
161
+ await _apiContainer . DisposeAsync ( ) ;
162
+ await _databaseContainer . DisposeAsync ( ) ;
163
+ await _network . DisposeAsync ( ) ;
164
+
165
+ _databaseContainerStdOut . Dispose ( ) ;
166
+ _databaseContainerStdErr . Dispose ( ) ;
167
+ _apiContainerStdOut . Dispose ( ) ;
168
+ _apiContainerStdErr . Dispose ( ) ;
169
+
170
+ _http . Dispose ( ) ;
171
+ }
172
+ }
0 commit comments