1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
|
// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
namespace Progmasoft.Progmaweb.Api.Tests;
/// <summary>Starts the account API in memory for a test.</summary>
/// <param name="configureGoogle">
/// Whether the Google client is configured. Without it the service has to run and refuse only Google sign-in.
/// </param>
/// <param name="accountDatabase">
/// Connection string of the PostgreSQL database that keeps the accounts; <see langword="null"/> keeps them in memory.
/// </param>
/// <param name="environment">Name of the hosting environment.</param>
/// <param name="accountStore">Value of <c>Accounts:Store</c>; <see langword="null"/> leaves it unset.</param>
/// <param name="oauthKeys">
/// Whether to give Progmasoft OAuth key files. Production refuses to start without them; other environments make
/// their own keys.
/// </param>
internal sealed class AccountApiFactory(
bool configureGoogle = true,
string? accountDatabase = null,
string environment = "Testing",
string? accountStore = null,
bool oauthKeys = true) : WebApplicationFactory<Program>
{
private readonly string keyDirectory = Path.Combine(Path.GetTempPath(), "progmaweb-oauth-keys-" + Guid.NewGuid().ToString("N"));
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment(environment);
// The program chooses the store while it registers services, so these are host settings as well. An empty
// value overrides a database the environment of the test run names.
builder.UseSetting("ConnectionStrings:Accounts", accountDatabase ?? string.Empty);
builder.UseSetting("Accounts:Store", accountStore ?? string.Empty);
// The program decides while it registers services whether the Google scheme exists, so these two values
// have to be host settings: configuration sources added below are applied only when the host is built,
// after that decision. An empty value also overrides credentials the environment of the test run carries.
builder.UseSetting(
"Authentication:Google:ClientId",
configureGoogle ? "test-client.apps.googleusercontent.com" : string.Empty);
builder.UseSetting("Authentication:Google:ClientSecret", configureGoogle ? "test-secret" : string.Empty);
// Progmasoft OAuth reads its host while services are registered, like the Google client above. Its own host
// keeps the host filter honest: the account API answers on localhost, OAuth on oauth.localhost.
builder.UseSetting("OAuth:Host", OAuthHost);
builder.UseSetting("OAuth:AuthorizeHost", AuthorizeHost);
builder.UseSetting("OAuth:ConsoleHost", ConsoleHost);
builder.UseSetting("AllowedHosts",
$"localhost;127.0.0.1;progmasoft.com;account.progmasoft.com;www.progmasoft.com;{OAuthHost};{AuthorizeHost};{ConsoleHost}");
if (oauthKeys && environment == "Production")
{
Directory.CreateDirectory(keyDirectory);
using System.Security.Cryptography.RSA signing = System.Security.Cryptography.RSA.Create(2048);
string signingPath = Path.Combine(keyDirectory, "signing.pem");
string encryptionPath = Path.Combine(keyDirectory, "encryption.key");
File.WriteAllText(signingPath, signing.ExportPkcs8PrivateKeyPem());
File.WriteAllText(encryptionPath,
Convert.ToBase64String(System.Security.Cryptography.RandomNumberGenerator.GetBytes(32)));
builder.UseSetting("OAuth:SigningKeyPath", signingPath);
builder.UseSetting("OAuth:EncryptionKeyPath", encryptionPath);
}
else
{
builder.UseSetting("OAuth:SigningKeyPath", string.Empty);
builder.UseSetting("OAuth:EncryptionKeyPath", string.Empty);
}
builder.ConfigureAppConfiguration((_, configuration) =>
{
configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
["AccountSessions:LifetimeHours"] = "2",
["AccountSessions:CookieName"] = "__Host-ProgmasoftSession",
// A host-only cookie: the console tests send the account session themselves.
["AccountSessions:CookieDomain"] = string.Empty
});
});
}
/// <summary>The host Progmasoft OAuth answers on in the tests.</summary>
public const string OAuthHost = "oauth.localhost";
public const string AuthorizeHost = "authorize.localhost";
public const string ConsoleHost = "console.localhost";
/// <summary>A client of the OAuth host that keeps its cookies and does not follow redirections.</summary>
public HttpClient CreateOAuthClient() => CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
BaseAddress = new Uri($"https://{OAuthHost}"),
HandleCookies = true
});
/// <summary>A browser on the authorize host: sign-in, consent and the allowed applications.</summary>
public HttpClient CreateAuthorizeClient() => CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
BaseAddress = new Uri($"https://{AuthorizeHost}"),
HandleCookies = true
});
/// <summary>
/// A browser on the Progmasoft Console. Production shares the account session cookie with progmasoft.com; the
/// test hosts are not under one domain, so a test sends the session cookie of an account itself.
/// </summary>
public HttpClient CreateConsoleClient(string? sessionCookie = null)
{
HttpClient client = CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
BaseAddress = new Uri($"https://{ConsoleHost}"),
HandleCookies = false
});
if (sessionCookie is not null)
{
client.DefaultRequestHeaders.Add("Cookie", sessionCookie);
}
return client;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (Directory.Exists(keyDirectory))
{
Directory.Delete(keyDirectory, recursive: true);
}
}
public HttpClient CreateAccountClient(bool handleCookies = true) => CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
BaseAddress = new Uri("https://localhost"),
HandleCookies = handleCookies
});
}
|