diff --git a/src/Host/FSH.Starter.Api/Dockerfile b/src/Host/FSH.Starter.Api/Dockerfile
index e4f65cd076..3a2a354d5d 100644
--- a/src/Host/FSH.Starter.Api/Dockerfile
+++ b/src/Host/FSH.Starter.Api/Dockerfile
@@ -20,7 +20,11 @@ RUN dotnet publish src/Host/FSH.Starter.Api/FSH.Starter.Api.csproj \
# ── Runtime stage ──────────────────────────────────────────────────
# Chiseled = no shell, no apt, no debugger. Smaller pull + smaller CVE
# surface. APP_UID (1654) is the non-root user baked into the image.
-FROM mcr.microsoft.com/dotnet/nightly/aspnet:10.0-noble-chiseled AS runtime
+# This Dockerfile (docker compose) stays chiseled on purpose. The csproj
+# noble (SDK container publish, used by
+# the AWS deploy) is full noble because chiseled strips libgssapi_krb5 and
+# Npgsql's GSSAPI probe then logs a noisy (benign) load error in CloudWatch.
+FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled AS runtime
WORKDIR /app
COPY --from=build /publish .
diff --git a/src/Host/FSH.Starter.Api/appsettings.Production.json b/src/Host/FSH.Starter.Api/appsettings.Production.json
index 1f26741901..6c40734bbd 100644
--- a/src/Host/FSH.Starter.Api/appsettings.Production.json
+++ b/src/Host/FSH.Starter.Api/appsettings.Production.json
@@ -82,11 +82,13 @@
},
"MailOptions": {
"From": "",
- "Host": "",
- "Port": 0,
- "UserName": "",
- "Password": "",
- "DisplayName": ""
+ "DisplayName": "",
+ "Smtp": {
+ "Host": "",
+ "Port": 587,
+ "UserName": "",
+ "Password": ""
+ }
},
"RateLimitingOptions": {
"Enabled": true,
diff --git a/src/Host/FSH.Starter.DbMigrator/Dockerfile b/src/Host/FSH.Starter.DbMigrator/Dockerfile
index 7919c4abf6..d0d963929e 100644
--- a/src/Host/FSH.Starter.DbMigrator/Dockerfile
+++ b/src/Host/FSH.Starter.DbMigrator/Dockerfile
@@ -15,7 +15,10 @@ RUN dotnet publish src/Host/FSH.Starter.DbMigrator/FSH.Starter.DbMigrator.csproj
--no-restore \
-o /publish
-FROM mcr.microsoft.com/dotnet/nightly/aspnet:10.0-noble-chiseled AS runtime
+# Chiseled on purpose (compose). The csproj ContainerFamily (SDK publish, AWS)
+# is full noble: chiseled strips libgssapi_krb5, and Npgsql's GSSAPI probe
+# then logs a noisy (benign) load error. See the API Dockerfile.
+FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled AS runtime
WORKDIR /app
COPY --from=build /publish .
diff --git a/src/Tests/Integration.Tests/Infrastructure/TenantProvisioningWait.cs b/src/Tests/Integration.Tests/Infrastructure/TenantProvisioningWait.cs
new file mode 100644
index 0000000000..a5525a669e
--- /dev/null
+++ b/src/Tests/Integration.Tests/Infrastructure/TenantProvisioningWait.cs
@@ -0,0 +1,46 @@
+using FSH.Modules.Multitenancy.Contracts.Dtos;
+
+namespace Integration.Tests.Infrastructure;
+
+///
+/// Polls a tenant's provisioning status until the whole run finishes.
+///
+/// The status body also lists each step, and a finished step reads "Completed" while later steps
+/// (seeding the tenant admin) are still running. Matching "Completed" anywhere in the body returns
+/// after the first step, so only the overall Status field is trusted.
+///
+internal static class TenantProvisioningWait
+{
+ public static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
+ {
+ ArgumentNullException.ThrowIfNull(client);
+
+ TenantProvisioningStatusDto? status = null;
+ for (int i = 0; i < maxRetries; i++)
+ {
+ var statusResponse = await client.GetAsync(
+ $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
+
+ if (statusResponse.IsSuccessStatusCode)
+ {
+ status = await statusResponse.Content.ReadFromJsonAsync();
+ if (string.Equals(status?.Status, "Completed", StringComparison.Ordinal))
+ {
+ return;
+ }
+
+ if (string.Equals(status?.Status, "Failed", StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException(
+ $"Tenant {tenantId} provisioning failed at {status?.CurrentStep}: {status?.Error}");
+ }
+ }
+
+ await Task.Delay(1000);
+ }
+
+ throw new TimeoutException(
+ $"Tenant {tenantId} provisioning did not complete within {maxRetries} seconds. " +
+ $"Last status: {status?.Status ?? ""} (step {status?.CurrentStep ?? ""}).");
+ }
+}
diff --git a/src/Tests/Integration.Tests/Tests/Auditing/AuditQueryTenantIsolationTests.cs b/src/Tests/Integration.Tests/Tests/Auditing/AuditQueryTenantIsolationTests.cs
index e6e8f2c091..6c4cdeb2b9 100644
--- a/src/Tests/Integration.Tests/Tests/Auditing/AuditQueryTenantIsolationTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Auditing/AuditQueryTenantIsolationTests.cs
@@ -29,7 +29,7 @@ public async Task ByIdCorrelationTrace_Should_NotLeakAcrossTenants()
var otherAdminEmail = $"audit-key-admin-{uniqueId}@tenant.com";
await CreateTenantAsync(rootClient, otherTenantId, otherAdminEmail);
- await WaitForProvisioningAsync(rootClient, otherTenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, otherTenantId);
using var otherClient = await CreateTenantAdminClientWithRetryAsync(
otherAdminEmail, TestConstants.DefaultPassword, otherTenantId);
@@ -105,33 +105,4 @@ private static async Task CreateTenantAsync(HttpClient rootClient, string tenant
var body = await response.Content.ReadAsStringAsync();
response.StatusCode.ShouldBe(HttpStatusCode.Created, $"Create tenant failed: {body}");
}
-
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
- {
- for (int i = 0; i < maxRetries; i++)
- {
- var statusResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
-
- if (statusResponse.IsSuccessStatusCode)
- {
- var content = await statusResponse.Content.ReadAsStringAsync();
- if (content.Contains("Completed", StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
-
- if (content.Contains("Failed", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException(
- $"Tenant {tenantId} provisioning failed: {content}");
- }
- }
-
- await Task.Delay(1000);
- }
-
- throw new TimeoutException(
- $"Tenant {tenantId} provisioning did not complete within {maxRetries} seconds.");
- }
}
diff --git a/src/Tests/Integration.Tests/Tests/Auditing/AuditTenantIsolationTests.cs b/src/Tests/Integration.Tests/Tests/Auditing/AuditTenantIsolationTests.cs
index 52751e8cc0..1cd80b1130 100644
--- a/src/Tests/Integration.Tests/Tests/Auditing/AuditTenantIsolationTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Auditing/AuditTenantIsolationTests.cs
@@ -21,7 +21,7 @@ public async Task GetAudits_Should_OnlyReturnCurrentTenantsRecords()
var otherAdminEmail = $"audit-admin-{uniqueId}@tenant.com";
await CreateTenantAsync(rootClient, otherTenantId, otherAdminEmail);
- await WaitForProvisioningAsync(rootClient, otherTenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, otherTenantId);
// Authenticating as both tenants triggers audit events on both sides.
_ = await _auth.GetRootAdminTokenAsync();
@@ -81,39 +81,4 @@ private static async Task CreateTenantAsync(HttpClient rootClient, string tenant
var body = await response.Content.ReadAsStringAsync();
response.StatusCode.ShouldBe(HttpStatusCode.Created, $"Create tenant failed: {body}");
}
-
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
- {
- for (int i = 0; i < maxRetries; i++)
- {
- var statusResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
-
- if (statusResponse.IsSuccessStatusCode)
- {
- var content = await statusResponse.Content.ReadAsStringAsync();
- if (content.Contains("Completed", StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
-
- if (content.Contains("Failed", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException(
- $"Tenant {tenantId} provisioning failed: {content}");
- }
- }
-
- await Task.Delay(1000);
- }
-
- var finalResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
- var finalContent = finalResponse.IsSuccessStatusCode
- ? await finalResponse.Content.ReadAsStringAsync()
- : $"HTTP {finalResponse.StatusCode}";
-
- throw new TimeoutException(
- $"Tenant {tenantId} provisioning did not complete within {maxRetries} seconds. Last status: {finalContent}");
- }
}
diff --git a/src/Tests/Integration.Tests/Tests/Billing/BillingTenantIsolationTests.cs b/src/Tests/Integration.Tests/Tests/Billing/BillingTenantIsolationTests.cs
index 614e852eb3..dbeb63b424 100644
--- a/src/Tests/Integration.Tests/Tests/Billing/BillingTenantIsolationTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Billing/BillingTenantIsolationTests.cs
@@ -65,7 +65,7 @@ public async Task GetInvoiceById_Should_Return404_When_OwnedByDifferentTenant()
var otherAdminEmail = $"billing-inv-admin-{uniqueId}@tenant.com";
await CreateTenantAsync(rootClient, otherTenantId, otherAdminEmail);
- await WaitForProvisioningAsync(rootClient, otherTenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, otherTenantId);
using var otherClient = await CreateTenantAdminClientWithRetryAsync(
otherAdminEmail, TestConstants.DefaultPassword, otherTenantId);
@@ -108,7 +108,7 @@ public async Task GetSubscription_Should_NotLeak_When_OtherTenantPassesOwnersTen
var otherAdminEmail = $"billing-sub-admin-{uniqueId}@tenant.com";
await CreateTenantAsync(rootClient, otherTenantId, otherAdminEmail);
- await WaitForProvisioningAsync(rootClient, otherTenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, otherTenantId);
using var otherClient = await CreateTenantAdminClientWithRetryAsync(
otherAdminEmail, TestConstants.DefaultPassword, otherTenantId);
@@ -427,7 +427,7 @@ private async Task CreateTenantAdminClientWithRetryAsync(
var tenantId = $"billing-{label}-iso-{uniqueId}";
var adminEmail = $"billing-{label}-admin-{uniqueId}@tenant.com";
await CreateTenantAsync(rootClient, tenantId, adminEmail);
- await WaitForProvisioningAsync(rootClient, tenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, tenantId);
var client = await CreateTenantAdminClientWithRetryAsync(adminEmail, TestConstants.DefaultPassword, tenantId);
return (client, tenantId);
}
@@ -458,40 +458,5 @@ private static async Task CreateTenantAsync(HttpClient rootClient, string tenant
response.StatusCode.ShouldBe(HttpStatusCode.Created, $"Create tenant failed: {body}");
}
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
- {
- for (int i = 0; i < maxRetries; i++)
- {
- var statusResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
-
- if (statusResponse.IsSuccessStatusCode)
- {
- var content = await statusResponse.Content.ReadAsStringAsync();
- if (content.Contains("Completed", StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
-
- if (content.Contains("Failed", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException(
- $"Tenant {tenantId} provisioning failed: {content}");
- }
- }
-
- await Task.Delay(1000);
- }
-
- var finalResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
- var finalContent = finalResponse.IsSuccessStatusCode
- ? await finalResponse.Content.ReadAsStringAsync()
- : $"HTTP {finalResponse.StatusCode}";
-
- throw new TimeoutException(
- $"Tenant {tenantId} provisioning did not complete within {maxRetries} seconds. Last status: {finalContent}");
- }
-
#endregion
}
diff --git a/src/Tests/Integration.Tests/Tests/Billing/InvoicePdfTests.cs b/src/Tests/Integration.Tests/Tests/Billing/InvoicePdfTests.cs
index ede5332ffd..119cdb5774 100644
--- a/src/Tests/Integration.Tests/Tests/Billing/InvoicePdfTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Billing/InvoicePdfTests.cs
@@ -33,7 +33,7 @@ public async Task InvoicePdf_Should_Be_Downloadable_By_Owner_And_RootOperator_Bu
var adminEmail = $"pdf-{unique}@tenant.com";
var planKey = await CreatePlanAsync(rootClient, $"pdf-m-{unique}", 29m);
await CreateTenantAsync(rootClient, tenantId, adminEmail, planKey);
- await WaitForProvisioningAsync(rootClient, tenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, tenantId);
// TenantSubscribed goes through the outbox now, so the invoice is issued on the next
// dispatch cycle rather than inside the create request.
@@ -62,7 +62,7 @@ public async Task InvoicePdf_Should_Be_Downloadable_By_Owner_And_RootOperator_Bu
var otherTenantId = $"pdf-other-{otherUnique}";
var otherEmail = $"pdf-other-{otherUnique}@tenant.com";
await CreateTenantAsync(rootClient, otherTenantId, otherEmail, planKey);
- await WaitForProvisioningAsync(rootClient, otherTenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, otherTenantId);
using var otherClient = await CreateTenantAdminClientWithRetryAsync(
otherEmail, TestConstants.DefaultPassword, otherTenantId);
@@ -121,26 +121,4 @@ private static async Task CreateTenantAsync(HttpClient rootClient, string tenant
var body = await response.Content.ReadAsStringAsync();
response.StatusCode.ShouldBe(HttpStatusCode.Created, $"Create tenant failed: {body}");
}
-
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
- {
- for (var i = 0; i < maxRetries; i++)
- {
- var statusResponse = await client.GetAsync($"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
- if (statusResponse.IsSuccessStatusCode)
- {
- var content = await statusResponse.Content.ReadAsStringAsync();
- if (content.Contains("Completed", StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
- if (content.Contains("Failed", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException($"Tenant {tenantId} provisioning failed: {content}");
- }
- }
- await Task.Delay(1000);
- }
- throw new TimeoutException($"Tenant {tenantId} did not finish provisioning.");
- }
}
diff --git a/src/Tests/Integration.Tests/Tests/Billing/TopupApprovalTests.cs b/src/Tests/Integration.Tests/Tests/Billing/TopupApprovalTests.cs
index 9f72fccc3c..1ae6cebe06 100644
--- a/src/Tests/Integration.Tests/Tests/Billing/TopupApprovalTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Billing/TopupApprovalTests.cs
@@ -147,7 +147,7 @@ public async Task NonRoot_cannot_see_other_tenants_requests()
var tenantBId = $"topup-iso-{uniqueId}";
var tenantBAdminEmail = $"topup-admin-{uniqueId}@tenant.com";
await CreateTenantAsync(rootClient, tenantBId, tenantBAdminEmail);
- await WaitForProvisioningAsync(rootClient, tenantBId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, tenantBId);
using var tenantBClient = await CreateTenantAdminClientWithRetryAsync(
tenantBAdminEmail, TestConstants.DefaultPassword, tenantBId);
@@ -256,28 +256,6 @@ private static async Task CreateTenantAsync(HttpClient rootClient, string tenant
response.StatusCode.ShouldBe(HttpStatusCode.Created, $"Create tenant failed: {body}");
}
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
- {
- for (int i = 0; i < maxRetries; i++)
- {
- var statusResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
-
- if (statusResponse.IsSuccessStatusCode)
- {
- var content = await statusResponse.Content.ReadAsStringAsync();
- if (content.Contains("Completed", StringComparison.OrdinalIgnoreCase))
- return;
- if (content.Contains("Failed", StringComparison.OrdinalIgnoreCase))
- throw new InvalidOperationException($"Tenant {tenantId} provisioning failed: {content}");
- }
-
- await Task.Delay(1000);
- }
-
- throw new TimeoutException($"Tenant {tenantId} provisioning did not complete within {maxRetries}s.");
- }
-
private async Task CreateTenantAdminClientWithRetryAsync(
string email, string password, string tenant, int maxRetries = 30)
{
diff --git a/src/Tests/Integration.Tests/Tests/Billing/WalletEndpointsTests.cs b/src/Tests/Integration.Tests/Tests/Billing/WalletEndpointsTests.cs
index 87ba8a29b3..54bc8de1f2 100644
--- a/src/Tests/Integration.Tests/Tests/Billing/WalletEndpointsTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Billing/WalletEndpointsTests.cs
@@ -159,7 +159,7 @@ public async Task GetMyTopupRequests_Must_NotLeak_Requests_From_Other_Tenant()
var otherAdminEmail = $"wallet-admin-{uniqueId}@tenant.com";
await CreateTenantAsync(rootClient, otherTenantId, otherAdminEmail);
- await WaitForProvisioningAsync(rootClient, otherTenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, otherTenantId);
using var otherClient = await CreateTenantAdminClientWithRetryAsync(
otherAdminEmail, TestConstants.DefaultPassword, otherTenantId);
@@ -212,41 +212,6 @@ private static async Task CreateTenantAsync(HttpClient rootClient, string tenant
response.StatusCode.ShouldBe(HttpStatusCode.Created, $"Create tenant failed: {body}");
}
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
- {
- for (int i = 0; i < maxRetries; i++)
- {
- var statusResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
-
- if (statusResponse.IsSuccessStatusCode)
- {
- var content = await statusResponse.Content.ReadAsStringAsync();
- if (content.Contains("Completed", StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
-
- if (content.Contains("Failed", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException(
- $"Tenant {tenantId} provisioning failed: {content}");
- }
- }
-
- await Task.Delay(1000);
- }
-
- var finalResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
- var finalContent = finalResponse.IsSuccessStatusCode
- ? await finalResponse.Content.ReadAsStringAsync()
- : $"HTTP {finalResponse.StatusCode}";
-
- throw new TimeoutException(
- $"Tenant {tenantId} provisioning did not complete within {maxRetries} seconds. Last status: {finalContent}");
- }
-
private async Task CreateTenantAdminClientWithRetryAsync(
string email, string password, string tenant, int maxRetries = 30)
{
diff --git a/src/Tests/Integration.Tests/Tests/Catalog/CatalogTenantIsolationTests.cs b/src/Tests/Integration.Tests/Tests/Catalog/CatalogTenantIsolationTests.cs
index 1dc0475564..c678c2fa01 100644
--- a/src/Tests/Integration.Tests/Tests/Catalog/CatalogTenantIsolationTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Catalog/CatalogTenantIsolationTests.cs
@@ -145,7 +145,7 @@ private async Task ProvisionTenantClientAsync(HttpClient rootClient,
{
var adminEmail = $"{tenantId}-admin@tenant.com";
await CreateTenantAsync(rootClient, tenantId, adminEmail);
- await WaitForProvisioningAsync(rootClient, tenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, tenantId);
return await CreateTenantAdminClientWithRetryAsync(
adminEmail, TestConstants.DefaultPassword, tenantId);
}
@@ -182,39 +182,4 @@ private static async Task CreateTenantAsync(HttpClient rootClient, string tenant
var body = await response.Content.ReadAsStringAsync();
response.StatusCode.ShouldBe(HttpStatusCode.Created, $"Create tenant failed: {body}");
}
-
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
- {
- for (int i = 0; i < maxRetries; i++)
- {
- var statusResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
-
- if (statusResponse.IsSuccessStatusCode)
- {
- var content = await statusResponse.Content.ReadAsStringAsync();
- if (content.Contains("Completed", StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
-
- if (content.Contains("Failed", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException(
- $"Tenant {tenantId} provisioning failed: {content}");
- }
- }
-
- await Task.Delay(1000);
- }
-
- var finalResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
- var finalContent = finalResponse.IsSuccessStatusCode
- ? await finalResponse.Content.ReadAsStringAsync()
- : $"HTTP {finalResponse.StatusCode}";
-
- throw new TimeoutException(
- $"Tenant {tenantId} provisioning did not complete within {maxRetries} seconds. Last status: {finalContent}");
- }
}
diff --git a/src/Tests/Integration.Tests/Tests/Chat/ChatTenantIsolationTests.cs b/src/Tests/Integration.Tests/Tests/Chat/ChatTenantIsolationTests.cs
index e06715062f..934bee1cdb 100644
--- a/src/Tests/Integration.Tests/Tests/Chat/ChatTenantIsolationTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Chat/ChatTenantIsolationTests.cs
@@ -231,7 +231,7 @@ private async Task CreateProvisionedTenantAdminClientAsync()
using var rootClient = await _auth.CreateRootAdminClientAsync();
await CreateTenantAsync(rootClient, tenantId, adminEmail);
- await WaitForProvisioningAsync(rootClient, tenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, tenantId);
return await CreateTenantAdminClientWithRetryAsync(adminEmail, TestConstants.DefaultPassword, tenantId);
}
@@ -267,39 +267,4 @@ private static async Task CreateTenantAsync(HttpClient rootClient, string tenant
var body = await response.Content.ReadAsStringAsync();
response.StatusCode.ShouldBe(HttpStatusCode.Created, $"Create tenant failed: {body}");
}
-
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
- {
- for (int i = 0; i < maxRetries; i++)
- {
- var statusResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
-
- if (statusResponse.IsSuccessStatusCode)
- {
- var content = await statusResponse.Content.ReadAsStringAsync();
- if (content.Contains("Completed", StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
-
- if (content.Contains("Failed", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException(
- $"Tenant {tenantId} provisioning failed: {content}");
- }
- }
-
- await Task.Delay(1000);
- }
-
- var finalResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
- var finalContent = finalResponse.IsSuccessStatusCode
- ? await finalResponse.Content.ReadAsStringAsync()
- : $"HTTP {finalResponse.StatusCode}";
-
- throw new TimeoutException(
- $"Tenant {tenantId} provisioning did not complete within {maxRetries} seconds. Last status: {finalContent}");
- }
}
diff --git a/src/Tests/Integration.Tests/Tests/Files/FileTenantIsolationTests.cs b/src/Tests/Integration.Tests/Tests/Files/FileTenantIsolationTests.cs
index 45838b56f1..87ee4e59a6 100644
--- a/src/Tests/Integration.Tests/Tests/Files/FileTenantIsolationTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Files/FileTenantIsolationTests.cs
@@ -92,7 +92,7 @@ private async Task CreateOtherTenantAdminClientAsync()
var otherAdminEmail = $"file-admin-{uniqueId}@tenant.com";
await CreateTenantAsync(rootClient, otherTenantId, otherAdminEmail);
- await WaitForProvisioningAsync(rootClient, otherTenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, otherTenantId);
return await CreateTenantAdminClientWithRetryAsync(
otherAdminEmail, TestConstants.DefaultPassword, otherTenantId);
}
@@ -130,41 +130,6 @@ private static async Task CreateTenantAsync(HttpClient rootClient, string tenant
response.StatusCode.ShouldBe(HttpStatusCode.Created, $"Create tenant failed: {body}");
}
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
- {
- for (int i = 0; i < maxRetries; i++)
- {
- var statusResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
-
- if (statusResponse.IsSuccessStatusCode)
- {
- var content = await statusResponse.Content.ReadAsStringAsync();
- if (content.Contains("Completed", StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
-
- if (content.Contains("Failed", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException(
- $"Tenant {tenantId} provisioning failed: {content}");
- }
- }
-
- await Task.Delay(1000);
- }
-
- var finalResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
- var finalContent = finalResponse.IsSuccessStatusCode
- ? await finalResponse.Content.ReadAsStringAsync()
- : $"HTTP {finalResponse.StatusCode}";
-
- throw new TimeoutException(
- $"Tenant {tenantId} provisioning did not complete within {maxRetries} seconds. Last status: {finalContent}");
- }
-
#endregion
#region Upload helpers
diff --git a/src/Tests/Integration.Tests/Tests/Groups/GroupTenantIsolationTests.cs b/src/Tests/Integration.Tests/Tests/Groups/GroupTenantIsolationTests.cs
index 09a9980188..b78019c1ee 100644
--- a/src/Tests/Integration.Tests/Tests/Groups/GroupTenantIsolationTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Groups/GroupTenantIsolationTests.cs
@@ -147,7 +147,7 @@ private async Task ProvisionTenantClientAsync(HttpClient rootClient,
{
var adminEmail = $"{tenantId}-admin@tenant.com";
await CreateTenantAsync(rootClient, tenantId, adminEmail);
- await WaitForProvisioningAsync(rootClient, tenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, tenantId);
return await CreateTenantAdminClientWithRetryAsync(
adminEmail, TestConstants.DefaultPassword, tenantId);
}
@@ -184,39 +184,4 @@ private static async Task CreateTenantAsync(HttpClient rootClient, string tenant
var body = await response.Content.ReadAsStringAsync();
response.StatusCode.ShouldBe(HttpStatusCode.Created, $"Create tenant failed: {body}");
}
-
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
- {
- for (int i = 0; i < maxRetries; i++)
- {
- var statusResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
-
- if (statusResponse.IsSuccessStatusCode)
- {
- var content = await statusResponse.Content.ReadAsStringAsync();
- if (content.Contains("Completed", StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
-
- if (content.Contains("Failed", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException(
- $"Tenant {tenantId} provisioning failed: {content}");
- }
- }
-
- await Task.Delay(1000);
- }
-
- var finalResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
- var finalContent = finalResponse.IsSuccessStatusCode
- ? await finalResponse.Content.ReadAsStringAsync()
- : $"HTTP {finalResponse.StatusCode}";
-
- throw new TimeoutException(
- $"Tenant {tenantId} provisioning did not complete within {maxRetries} seconds. Last status: {finalContent}");
- }
}
diff --git a/src/Tests/Integration.Tests/Tests/Impersonation/ImpersonationTests.cs b/src/Tests/Integration.Tests/Tests/Impersonation/ImpersonationTests.cs
index 923abc5cfb..275e3c9f3b 100644
--- a/src/Tests/Integration.Tests/Tests/Impersonation/ImpersonationTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Impersonation/ImpersonationTests.cs
@@ -58,7 +58,7 @@ public async Task InitializeAsync()
using var rootClient = await _auth.CreateRootAdminClientAsync();
await CreateTenantAsync(rootClient, _tenantId, _tenantAdminEmail);
- await WaitForProvisioningAsync(rootClient, _tenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, _tenantId);
// Sign in as the seeded tenant admin to capture their userId from the JWT — the search
// endpoint would couple these tests to the (currently buggy) cross-tenant search override.
@@ -552,7 +552,7 @@ public async Task Revoke_Should_Return404_When_TenantAdmin_TargetsGrant_OutsideT
using var rootClient = await _auth.CreateRootAdminClientAsync();
await CreateTenantAsync(rootClient, otherTenantId, otherAdminEmail);
- await WaitForProvisioningAsync(rootClient, otherTenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, otherTenantId);
var otherToken = await GetTokenWithRetryAsync(otherAdminEmail, TestConstants.DefaultPassword, otherTenantId);
var otherAdminUserId = ReadSubject(otherToken.AccessToken);
@@ -905,28 +905,6 @@ private static async Task CreateTenantAsync(HttpClient rootClient, string tenant
response.StatusCode.ShouldBe(HttpStatusCode.Created);
}
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
- {
- for (var i = 0; i < maxRetries; i++)
- {
- var statusResponse = await client.GetAsync($"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
- if (statusResponse.IsSuccessStatusCode)
- {
- var content = await statusResponse.Content.ReadAsStringAsync();
- if (content.Contains("Completed", StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
- if (content.Contains("Failed", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException($"Tenant {tenantId} provisioning failed: {content}");
- }
- }
- await Task.Delay(1000);
- }
- throw new TimeoutException($"Tenant {tenantId} did not finish provisioning.");
- }
-
// ─── shape mirrors ─────────────────────────────────────────────────
private sealed class ImpersonationResponse
diff --git a/src/Tests/Integration.Tests/Tests/Multitenancy/AdjustTenantValidityTests.cs b/src/Tests/Integration.Tests/Tests/Multitenancy/AdjustTenantValidityTests.cs
index e16a33f422..47ad1fd43b 100644
--- a/src/Tests/Integration.Tests/Tests/Multitenancy/AdjustTenantValidityTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Multitenancy/AdjustTenantValidityTests.cs
@@ -135,7 +135,7 @@ public async Task AdjustValidity_Should_Forbid_When_CallerIsTenantAdmin()
var adminEmail = $"adj-authz-{unique}@tenant.com";
var planKey = await CreatePlanAsync(rootClient, $"adj-az-{unique}", monthlyBasePrice: 5m);
await CreateTenantAsync(rootClient, tenantId, adminEmail, planKey);
- await WaitForProvisioningAsync(rootClient, tenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, tenantId);
using var tenantClient = await CreateTenantAdminClientWithRetryAsync(
adminEmail, TestConstants.DefaultPassword, tenantId);
@@ -207,28 +207,6 @@ private static async Task GetStatusAsync(HttpClient client, string
return status;
}
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
- {
- for (var i = 0; i < maxRetries; i++)
- {
- var statusResponse = await client.GetAsync($"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
- if (statusResponse.IsSuccessStatusCode)
- {
- var content = await statusResponse.Content.ReadAsStringAsync();
- if (content.Contains("Completed", StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
- if (content.Contains("Failed", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException($"Tenant {tenantId} provisioning failed: {content}");
- }
- }
- await Task.Delay(1000);
- }
- throw new TimeoutException($"Tenant {tenantId} did not finish provisioning.");
- }
-
private sealed record AdjustResult(string TenantId, DateTime ValidUpto);
private sealed record TenantStatus
diff --git a/src/Tests/Integration.Tests/Tests/Multitenancy/ChangeTenantActivationValidationTests.cs b/src/Tests/Integration.Tests/Tests/Multitenancy/ChangeTenantActivationValidationTests.cs
index 11439e46c0..285afc217f 100644
--- a/src/Tests/Integration.Tests/Tests/Multitenancy/ChangeTenantActivationValidationTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Multitenancy/ChangeTenantActivationValidationTests.cs
@@ -121,7 +121,7 @@ public async Task ChangeActivation_Should_Reactivate_When_ProvisioningCompleted(
var unique = Guid.NewGuid().ToString("N")[..8];
var tenantId = $"act-reactivate-{unique}";
await CreateTenantAsync(rootClient, tenantId, $"act-reactivate-{unique}@tenant.com");
- await WaitForProvisioningAsync(rootClient, tenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, tenantId);
await WaitForStableCompletedAsync(rootClient, tenantId);
var deactivate = await rootClient.PostAsJsonAsync(
@@ -196,28 +196,6 @@ private static async Task CreateTenantAsync(HttpClient rootClient, string tenant
response.StatusCode.ShouldBe(HttpStatusCode.Created, $"Create tenant failed: {body}");
}
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
- {
- for (var i = 0; i < maxRetries; i++)
- {
- var statusResponse = await client.GetAsync($"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
- if (statusResponse.IsSuccessStatusCode)
- {
- var content = await statusResponse.Content.ReadAsStringAsync();
- if (content.Contains("Completed", StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
- if (content.Contains("Failed", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException($"Tenant {tenantId} provisioning failed: {content}");
- }
- }
- await Task.Delay(1000);
- }
- throw new TimeoutException($"Tenant {tenantId} did not finish provisioning.");
- }
-
///
/// Waits until the provisioning status reads "Completed" on several consecutive
/// polls. Guards against the async-provisioning window where the latest record
diff --git a/src/Tests/Integration.Tests/Tests/Multitenancy/MyTenantStatusTests.cs b/src/Tests/Integration.Tests/Tests/Multitenancy/MyTenantStatusTests.cs
index 8bdca2661e..097a95702e 100644
--- a/src/Tests/Integration.Tests/Tests/Multitenancy/MyTenantStatusTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Multitenancy/MyTenantStatusTests.cs
@@ -36,7 +36,7 @@ public async Task GetMyStatus_Should_Return_CallersOwn_Plan_And_ExpiryState()
var adminEmail = $"mystatus-{unique}@tenant.com";
var planKey = await CreatePlanAsync(rootClient, $"mystatus-m-{unique}", 10m);
await CreateTenantAsync(rootClient, tenantId, adminEmail, planKey);
- await WaitForProvisioningAsync(rootClient, tenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, tenantId);
using var tenantClient = await CreateTenantAdminClientWithRetryAsync(adminEmail, TestConstants.DefaultPassword, tenantId);
@@ -102,28 +102,6 @@ private static async Task CreateTenantAsync(HttpClient rootClient, string tenant
response.StatusCode.ShouldBe(HttpStatusCode.Created, $"Create tenant failed: {body}");
}
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
- {
- for (var i = 0; i < maxRetries; i++)
- {
- var statusResponse = await client.GetAsync($"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
- if (statusResponse.IsSuccessStatusCode)
- {
- var content = await statusResponse.Content.ReadAsStringAsync();
- if (content.Contains("Completed", StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
- if (content.Contains("Failed", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException($"Tenant {tenantId} provisioning failed: {content}");
- }
- }
- await Task.Delay(1000);
- }
- throw new TimeoutException($"Tenant {tenantId} did not finish provisioning.");
- }
-
private sealed record MyStatus
{
public string Id { get; init; } = string.Empty;
diff --git a/src/Tests/Integration.Tests/Tests/Multitenancy/RenewTenantTests.cs b/src/Tests/Integration.Tests/Tests/Multitenancy/RenewTenantTests.cs
index cb89d328dd..10628b19bc 100644
--- a/src/Tests/Integration.Tests/Tests/Multitenancy/RenewTenantTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Multitenancy/RenewTenantTests.cs
@@ -140,7 +140,7 @@ public async Task RenewTenant_Should_Advance_SubscriptionEndUtc_On_SamePlanRenew
var tenantId = $"renew-drift-{unique}";
var planKey = await CreatePlanAsync(rootClient, $"drift-m-{unique}", monthlyBasePrice: 10m);
await CreateTenantAsync(rootClient, tenantId, $"renew-drift-{unique}@tenant.com", planKey);
- await WaitForProvisioningAsync(rootClient, tenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, tenantId);
// TenantSubscribed/TenantRenewed go through the outbox now, so Billing reacts on the next
// dispatch cycle rather than inside the request.
@@ -224,7 +224,7 @@ public async Task RenewTenant_Should_Forbid_When_CallerIsTenantAdmin()
var adminEmail = $"renew-authz-{unique}@tenant.com";
var planKey = await CreatePlanAsync(rootClient, $"az-{unique}", monthlyBasePrice: 5m);
await CreateTenantAsync(rootClient, tenantId, adminEmail, planKey);
- await WaitForProvisioningAsync(rootClient, tenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, tenantId);
using var tenantClient = await CreateTenantAdminClientWithRetryAsync(
adminEmail, TestConstants.DefaultPassword, tenantId);
@@ -309,28 +309,6 @@ private static async Task GetStatusAsync(HttpClient client, string
return status;
}
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
- {
- for (var i = 0; i < maxRetries; i++)
- {
- var statusResponse = await client.GetAsync($"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
- if (statusResponse.IsSuccessStatusCode)
- {
- var content = await statusResponse.Content.ReadAsStringAsync();
- if (content.Contains("Completed", StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
- if (content.Contains("Failed", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException($"Tenant {tenantId} provisioning failed: {content}");
- }
- }
- await Task.Delay(1000);
- }
- throw new TimeoutException($"Tenant {tenantId} did not finish provisioning.");
- }
-
private static async Task GetSubscriptionEndUtcAsync(HttpClient client, string tenantId)
{
var resp = await client.GetAsync($"{BillingBasePath}/subscriptions?tenantId={tenantId}");
diff --git a/src/Tests/Integration.Tests/Tests/Multitenancy/TenantHeaderOverrideTests.cs b/src/Tests/Integration.Tests/Tests/Multitenancy/TenantHeaderOverrideTests.cs
index 1e5859d396..e0afd8069d 100644
--- a/src/Tests/Integration.Tests/Tests/Multitenancy/TenantHeaderOverrideTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Multitenancy/TenantHeaderOverrideTests.cs
@@ -55,8 +55,8 @@ public async Task InitializeAsync()
using var rootClient = await _auth.CreateRootAdminClientAsync();
await CreateTenantAsync(rootClient, _tenantA, _tenantAAdminEmail);
await CreateTenantAsync(rootClient, _tenantB, _tenantBAdminEmail);
- await WaitForProvisioningAsync(rootClient, _tenantA);
- await WaitForProvisioningAsync(rootClient, _tenantB);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, _tenantA);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, _tenantB);
// Provisioning can report "Completed" a tick before the seeded admin user is queryable; a
// successful token issuance cross-checks the user exists, avoiding a first-test race on empty lists.
@@ -209,28 +209,6 @@ private static async Task CreateTenantAsync(HttpClient rootClient, string tenant
response.StatusCode.ShouldBe(HttpStatusCode.Created);
}
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
- {
- for (var i = 0; i < maxRetries; i++)
- {
- var statusResponse = await client.GetAsync($"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
- if (statusResponse.IsSuccessStatusCode)
- {
- var content = await statusResponse.Content.ReadAsStringAsync();
- if (content.Contains("Completed", StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
- if (content.Contains("Failed", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException($"Tenant {tenantId} provisioning failed: {content}");
- }
- }
- await Task.Delay(1000);
- }
- throw new TimeoutException($"Tenant {tenantId} did not finish provisioning.");
- }
-
private sealed class SearchUserDto
{
public string? Id { get; set; }
diff --git a/src/Tests/Integration.Tests/Tests/Multitenancy/TenantIsolationTests.cs b/src/Tests/Integration.Tests/Tests/Multitenancy/TenantIsolationTests.cs
index 791d55cc5c..13c32a23c8 100644
--- a/src/Tests/Integration.Tests/Tests/Multitenancy/TenantIsolationTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Multitenancy/TenantIsolationTests.cs
@@ -22,7 +22,7 @@ public async Task TenantIsolation_Should_NotLeakUsers_When_QueryingAcrossTenants
var tenantId = $"iso-{uniqueId}";
var tenantAdminEmail = $"isoadmin-{uniqueId}@tenant.com";
await CreateTenantAsync(rootClient, tenantId, tenantAdminEmail);
- await WaitForProvisioningAsync(rootClient, tenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, tenantId);
// Login as new tenant admin — retry because provisioning may still be finalizing
using var tenantClient = await CreateTenantAdminClientWithRetryAsync(
@@ -64,8 +64,8 @@ public async Task TenantIsolation_Should_AllowSameEmailInDifferentTenants()
await CreateTenantAsync(rootClient, tenant1Id, t1AdminEmail);
await CreateTenantAsync(rootClient, tenant2Id, t2AdminEmail);
- await WaitForProvisioningAsync(rootClient, tenant1Id);
- await WaitForProvisioningAsync(rootClient, tenant2Id);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, tenant1Id);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, tenant2Id);
// Register same email in tenant 1
using var client1 = await CreateTenantAdminClientWithRetryAsync(
@@ -121,40 +121,4 @@ private static async Task CreateTenantAsync(HttpClient rootClient, string tenant
var body = await response.Content.ReadAsStringAsync();
response.StatusCode.ShouldBe(HttpStatusCode.Created, $"Create tenant failed: {body}");
}
-
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
- {
- for (int i = 0; i < maxRetries; i++)
- {
- var statusResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
-
- if (statusResponse.IsSuccessStatusCode)
- {
- var content = await statusResponse.Content.ReadAsStringAsync();
- if (content.Contains("Completed", StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
-
- if (content.Contains("Failed", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException(
- $"Tenant {tenantId} provisioning failed: {content}");
- }
- }
-
- await Task.Delay(1000);
- }
-
- // Get final status for the error message
- var finalResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
- var finalContent = finalResponse.IsSuccessStatusCode
- ? await finalResponse.Content.ReadAsStringAsync()
- : $"HTTP {finalResponse.StatusCode}";
-
- throw new TimeoutException(
- $"Tenant {tenantId} provisioning did not complete within {maxRetries} seconds. Last status: {finalContent}");
- }
}
diff --git a/src/Tests/Integration.Tests/Tests/Multitenancy/TenantMigrationsTests.cs b/src/Tests/Integration.Tests/Tests/Multitenancy/TenantMigrationsTests.cs
index cac01e5cb2..5a693b1a1c 100644
--- a/src/Tests/Integration.Tests/Tests/Multitenancy/TenantMigrationsTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Multitenancy/TenantMigrationsTests.cs
@@ -67,7 +67,7 @@ public async Task GetMigrations_Should_IncludeNewlyProvisionedTenant_When_Provis
var unique = Guid.NewGuid().ToString("N")[..8];
var tenantId = $"mig-{unique}";
await CreateTenantAsync(rootClient, tenantId, $"mig-{unique}@tenant.com");
- await WaitForProvisioningAsync(rootClient, tenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, tenantId);
// Act
var response = await rootClient.GetAsync(MigrationsPath);
@@ -112,7 +112,7 @@ public async Task GetMigrations_Should_Forbid_When_CallerIsTenantAdmin()
var tenantId = $"mig-authz-{unique}";
var adminEmail = $"mig-authz-{unique}@tenant.com";
await CreateTenantAsync(rootClient, tenantId, adminEmail);
- await WaitForProvisioningAsync(rootClient, tenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, tenantId);
using var tenantClient = await CreateTenantAdminClientWithRetryAsync(
adminEmail, TestConstants.DefaultPassword, tenantId);
@@ -160,28 +160,6 @@ private static async Task CreateTenantAsync(HttpClient rootClient, string tenant
response.StatusCode.ShouldBe(HttpStatusCode.Created, $"Create tenant failed: {body}");
}
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
- {
- for (var i = 0; i < maxRetries; i++)
- {
- var statusResponse = await client.GetAsync($"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
- if (statusResponse.IsSuccessStatusCode)
- {
- var content = await statusResponse.Content.ReadAsStringAsync();
- if (content.Contains("Completed", StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
- if (content.Contains("Failed", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException($"Tenant {tenantId} provisioning failed: {content}");
- }
- }
- await Task.Delay(1000);
- }
- throw new TimeoutException($"Tenant {tenantId} did not finish provisioning.");
- }
-
private sealed record MigrationStatus
{
public string TenantId { get; init; } = string.Empty;
diff --git a/src/Tests/Integration.Tests/Tests/Multitenancy/TenantSeedDataTests.cs b/src/Tests/Integration.Tests/Tests/Multitenancy/TenantSeedDataTests.cs
index 59c77f23a2..0fc3ff8697 100644
--- a/src/Tests/Integration.Tests/Tests/Multitenancy/TenantSeedDataTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Multitenancy/TenantSeedDataTests.cs
@@ -21,7 +21,7 @@ public async Task NewTenant_Should_BeSeededWith_AdminUser_And_Permissions()
var adminEmail = $"seed-admin-{uniqueId}@tenant.com";
await CreateTenantAsync(rootClient, tenantId, adminEmail);
- await WaitForProvisioningAsync(rootClient, tenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, tenantId);
using var tenantAdminClient = await CreateTenantAdminClientWithRetryAsync(
adminEmail, TestConstants.DefaultPassword, tenantId);
@@ -74,32 +74,4 @@ private static async Task CreateTenantAsync(HttpClient rootClient, string tenant
var body = await response.Content.ReadAsStringAsync();
response.StatusCode.ShouldBe(HttpStatusCode.Created, $"Create tenant failed: {body}");
}
-
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
- {
- for (int i = 0; i < maxRetries; i++)
- {
- var statusResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
-
- if (statusResponse.IsSuccessStatusCode)
- {
- var content = await statusResponse.Content.ReadAsStringAsync();
- if (content.Contains("Completed", StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
- if (content.Contains("Failed", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException(
- $"Tenant {tenantId} provisioning failed: {content}");
- }
- }
-
- await Task.Delay(1000);
- }
-
- throw new TimeoutException(
- $"Tenant {tenantId} provisioning did not complete within {maxRetries} seconds.");
- }
}
diff --git a/src/Tests/Integration.Tests/Tests/Multitenancy/TenantThemeTests.cs b/src/Tests/Integration.Tests/Tests/Multitenancy/TenantThemeTests.cs
index 1f305f7b5e..792dc0a01d 100644
--- a/src/Tests/Integration.Tests/Tests/Multitenancy/TenantThemeTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Multitenancy/TenantThemeTests.cs
@@ -51,8 +51,8 @@ public async Task InitializeAsync()
using var rootClient = await _auth.CreateRootAdminClientAsync();
await CreateTenantAsync(rootClient, _tenantA, _tenantAAdminEmail);
await CreateTenantAsync(rootClient, _tenantB, _tenantBAdminEmail);
- await WaitForProvisioningAsync(rootClient, _tenantA);
- await WaitForProvisioningAsync(rootClient, _tenantB);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, _tenantA);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, _tenantB);
// Ensure both tenant admins are queryable (token issuance is the strongest
// cross-check that identity seeding finished — see TenantHeaderOverrideTests).
@@ -425,28 +425,6 @@ private static async Task CreateTenantAsync(HttpClient rootClient, string tenant
response.StatusCode.ShouldBe(HttpStatusCode.Created, $"Create tenant failed: {body}");
}
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
- {
- for (var i = 0; i < maxRetries; i++)
- {
- var statusResponse = await client.GetAsync($"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
- if (statusResponse.IsSuccessStatusCode)
- {
- var content = await statusResponse.Content.ReadAsStringAsync();
- if (content.Contains("Completed", StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
- if (content.Contains("Failed", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException($"Tenant {tenantId} provisioning failed: {content}");
- }
- }
- await Task.Delay(1000);
- }
- throw new TimeoutException($"Tenant {tenantId} did not finish provisioning.");
- }
-
// Local copy of the theme response shape — only the fields these tests assert on.
private sealed record TenantThemeDto
{
diff --git a/src/Tests/Integration.Tests/Tests/Roles/RolePrivilegeEscalationTests.cs b/src/Tests/Integration.Tests/Tests/Roles/RolePrivilegeEscalationTests.cs
index 6095b5812f..6a13dea5c7 100644
--- a/src/Tests/Integration.Tests/Tests/Roles/RolePrivilegeEscalationTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Roles/RolePrivilegeEscalationTests.cs
@@ -34,7 +34,7 @@ public async Task UpdateRolePermissions_Should_StripRootPermissions_When_CallerI
var tenantId = $"esc-{unique}";
var adminEmail = $"esc-admin-{unique}@tenant.com";
await CreateTenantAsync(rootClient, tenantId, adminEmail);
- await WaitForProvisioningAsync(rootClient, tenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, tenantId);
using var tenantAdmin = await CreateTenantAdminClientWithRetryAsync(
adminEmail, TestConstants.DefaultPassword, tenantId);
@@ -123,28 +123,6 @@ private static async Task CreateTenantAsync(HttpClient rootClient, string tenant
resp.StatusCode.ShouldBe(HttpStatusCode.Created, await resp.Content.ReadAsStringAsync());
}
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
- {
- for (var i = 0; i < maxRetries; i++)
- {
- var resp = await client.GetAsync($"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
- if (resp.IsSuccessStatusCode)
- {
- var content = await resp.Content.ReadAsStringAsync();
- if (content.Contains("Completed", StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
- if (content.Contains("Failed", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException($"Tenant {tenantId} provisioning failed: {content}");
- }
- }
- await Task.Delay(1000);
- }
- throw new TimeoutException($"Tenant {tenantId} did not finish provisioning.");
- }
-
private sealed record RoleRow
{
public string Id { get; init; } = string.Empty;
diff --git a/src/Tests/Integration.Tests/Tests/Roles/RoleTenantIsolationTests.cs b/src/Tests/Integration.Tests/Tests/Roles/RoleTenantIsolationTests.cs
index f52fd19501..d1997693b3 100644
--- a/src/Tests/Integration.Tests/Tests/Roles/RoleTenantIsolationTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Roles/RoleTenantIsolationTests.cs
@@ -33,7 +33,7 @@ public async Task GetRoles_Should_NotReturnRoleCreatedInAnotherTenant()
var rootRole = await CreateRoleAsync(rootClient, rootRoleName);
await CreateTenantAsync(rootClient, otherTenantId, otherAdminEmail);
- await WaitForProvisioningAsync(rootClient, otherTenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, otherTenantId);
using var otherClient = await CreateTenantAdminClientWithRetryAsync(
otherAdminEmail, TestConstants.DefaultPassword, otherTenantId);
@@ -69,7 +69,7 @@ public async Task GetRoleById_Should_Return404_When_RoleBelongsToAnotherTenant()
var rootRole = await CreateRoleAsync(rootClient, $"RootByIdRole-{uniqueId}");
await CreateTenantAsync(rootClient, otherTenantId, otherAdminEmail);
- await WaitForProvisioningAsync(rootClient, otherTenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, otherTenantId);
using var otherClient = await CreateTenantAdminClientWithRetryAsync(
otherAdminEmail, TestConstants.DefaultPassword, otherTenantId);
@@ -134,39 +134,4 @@ private static async Task CreateTenantAsync(HttpClient rootClient, string tenant
var body = await response.Content.ReadAsStringAsync();
response.StatusCode.ShouldBe(HttpStatusCode.Created, $"Create tenant failed: {body}");
}
-
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
- {
- for (int i = 0; i < maxRetries; i++)
- {
- var statusResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
-
- if (statusResponse.IsSuccessStatusCode)
- {
- var content = await statusResponse.Content.ReadAsStringAsync();
- if (content.Contains("Completed", StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
-
- if (content.Contains("Failed", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException(
- $"Tenant {tenantId} provisioning failed: {content}");
- }
- }
-
- await Task.Delay(1000);
- }
-
- var finalResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
- var finalContent = finalResponse.IsSuccessStatusCode
- ? await finalResponse.Content.ReadAsStringAsync()
- : $"HTTP {finalResponse.StatusCode}";
-
- throw new TimeoutException(
- $"Tenant {tenantId} provisioning did not complete within {maxRetries} seconds. Last status: {finalContent}");
- }
}
diff --git a/src/Tests/Integration.Tests/Tests/Sessions/SessionCleanupTests.cs b/src/Tests/Integration.Tests/Tests/Sessions/SessionCleanupTests.cs
index b891572518..f59f3d068f 100644
--- a/src/Tests/Integration.Tests/Tests/Sessions/SessionCleanupTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Sessions/SessionCleanupTests.cs
@@ -43,7 +43,7 @@ public async Task Cleanup_Should_DeleteSessionsPastRetention_InEveryTenant_And_K
var otherAdminEmail = $"sess-clean-admin-{uniqueId}@tenant.com";
await CreateTenantAsync(rootClient, otherTenantId, otherAdminEmail);
- await WaitForProvisioningAsync(rootClient, otherTenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, otherTenantId);
var now = DateTime.UtcNow;
var rootExpired = await SeedSessionAsync(
@@ -130,36 +130,4 @@ private static async Task CreateTenantAsync(HttpClient rootClient, string tenant
var body = await response.Content.ReadAsStringAsync();
response.StatusCode.ShouldBe(HttpStatusCode.Created, $"Create tenant failed: {body}");
}
-
- // The status body also lists each step, and a finished step reads "Completed" while later steps
- // (seeding the tenant admin) are still running, so only the overall Status field is trusted.
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId)
- {
- const int maxRetries = 60;
- for (int i = 0; i < maxRetries; i++)
- {
- var statusResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
-
- if (statusResponse.IsSuccessStatusCode)
- {
- var status = await statusResponse.Content.ReadFromJsonAsync();
- if (string.Equals(status?.Status, "Completed", StringComparison.Ordinal))
- {
- return;
- }
-
- if (string.Equals(status?.Status, "Failed", StringComparison.Ordinal))
- {
- throw new InvalidOperationException(
- $"Tenant {tenantId} provisioning failed at {status?.CurrentStep}: {status?.Error}");
- }
- }
-
- await Task.Delay(1000);
- }
-
- throw new TimeoutException(
- $"Tenant {tenantId} provisioning did not complete within {maxRetries} seconds.");
- }
}
diff --git a/src/Tests/Integration.Tests/Tests/Sessions/SessionTenantIsolationTests.cs b/src/Tests/Integration.Tests/Tests/Sessions/SessionTenantIsolationTests.cs
index 53596337fc..5b3aca892e 100644
--- a/src/Tests/Integration.Tests/Tests/Sessions/SessionTenantIsolationTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Sessions/SessionTenantIsolationTests.cs
@@ -32,7 +32,7 @@ public async Task GetTenantSessions_Should_NotExposeOtherTenantsSessions()
var otherAdminEmail = $"sess-iso-admin-{uniqueId}@tenant.com";
await CreateTenantAsync(rootClient, otherTenantId, otherAdminEmail);
- await WaitForProvisioningAsync(rootClient, otherTenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, otherTenantId);
var rootUser = await IdentityUserSeeder.CreateLoginableUserAsync(_factory, rootClient, "sess-iso-rootuser");
await _auth.GetTokenAsync(rootUser.Email, rootUser.Password);
@@ -65,7 +65,7 @@ public async Task GetUserSessions_Should_ReturnEmpty_When_UserBelongsToAnotherTe
var otherAdminEmail = $"sess-iso2-admin-{uniqueId}@tenant.com";
await CreateTenantAsync(rootClient, otherTenantId, otherAdminEmail);
- await WaitForProvisioningAsync(rootClient, otherTenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, otherTenantId);
var rootUser = await IdentityUserSeeder.CreateLoginableUserAsync(_factory, rootClient, "sess-iso2-rootuser");
await _auth.GetTokenAsync(rootUser.Email, rootUser.Password);
@@ -115,33 +115,4 @@ private static async Task CreateTenantAsync(HttpClient rootClient, string tenant
var body = await response.Content.ReadAsStringAsync();
response.StatusCode.ShouldBe(HttpStatusCode.Created, $"Create tenant failed: {body}");
}
-
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
- {
- for (int i = 0; i < maxRetries; i++)
- {
- var statusResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
-
- if (statusResponse.IsSuccessStatusCode)
- {
- var content = await statusResponse.Content.ReadAsStringAsync();
- if (content.Contains("Completed", StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
-
- if (content.Contains("Failed", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException(
- $"Tenant {tenantId} provisioning failed: {content}");
- }
- }
-
- await Task.Delay(1000);
- }
-
- throw new TimeoutException(
- $"Tenant {tenantId} provisioning did not complete within {maxRetries} seconds.");
- }
}
diff --git a/src/Tests/Integration.Tests/Tests/Tickets/TicketTenantIsolationTests.cs b/src/Tests/Integration.Tests/Tests/Tickets/TicketTenantIsolationTests.cs
index f475a9ef71..be920b43b3 100644
--- a/src/Tests/Integration.Tests/Tests/Tickets/TicketTenantIsolationTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Tickets/TicketTenantIsolationTests.cs
@@ -116,7 +116,7 @@ private async Task ProvisionTenantClientAsync(HttpClient rootClient,
{
var adminEmail = $"{tenantId}-admin@tenant.com";
await CreateTenantAsync(rootClient, tenantId, adminEmail);
- await WaitForProvisioningAsync(rootClient, tenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, tenantId);
return await CreateTenantAdminClientWithRetryAsync(
adminEmail, TestConstants.DefaultPassword, tenantId);
}
@@ -153,39 +153,4 @@ private static async Task CreateTenantAsync(HttpClient rootClient, string tenant
var body = await response.Content.ReadAsStringAsync();
response.StatusCode.ShouldBe(HttpStatusCode.Created, $"Create tenant failed: {body}");
}
-
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
- {
- for (int i = 0; i < maxRetries; i++)
- {
- var statusResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
-
- if (statusResponse.IsSuccessStatusCode)
- {
- var content = await statusResponse.Content.ReadAsStringAsync();
- if (content.Contains("Completed", StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
-
- if (content.Contains("Failed", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException(
- $"Tenant {tenantId} provisioning failed: {content}");
- }
- }
-
- await Task.Delay(1000);
- }
-
- var finalResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
- var finalContent = finalResponse.IsSuccessStatusCode
- ? await finalResponse.Content.ReadAsStringAsync()
- : $"HTTP {finalResponse.StatusCode}";
-
- throw new TimeoutException(
- $"Tenant {tenantId} provisioning did not complete within {maxRetries} seconds. Last status: {finalContent}");
- }
}
diff --git a/src/Tests/Integration.Tests/Tests/Webhooks/WebhookDeliveryTests.cs b/src/Tests/Integration.Tests/Tests/Webhooks/WebhookDeliveryTests.cs
index 90abbcb60b..c36246c242 100644
--- a/src/Tests/Integration.Tests/Tests/Webhooks/WebhookDeliveryTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Webhooks/WebhookDeliveryTests.cs
@@ -220,7 +220,7 @@ public async Task GetDeliveries_Should_NotLeakOtherTenantsDeliveries_When_Querie
var otherTenantId = $"wh-deliv-iso-{uniqueId}";
var otherAdminEmail = $"wh-deliv-iso-{uniqueId}@tenant.com";
await CreateTenantAsync(rootClient, otherTenantId, otherAdminEmail);
- await WaitForProvisioningAsync(rootClient, otherTenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, otherTenantId);
using var otherClient = await CreateTenantAdminClientWithRetryAsync(
capturingFactory, otherAdminEmail, TestConstants.DefaultPassword, otherTenantId);
@@ -372,33 +372,6 @@ private static async Task CreateTenantAsync(HttpClient rootClient, string tenant
response.StatusCode.ShouldBe(HttpStatusCode.Created, $"Create tenant failed: {body}");
}
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
- {
- for (int i = 0; i < maxRetries; i++)
- {
- var statusResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
-
- if (statusResponse.IsSuccessStatusCode)
- {
- var content = await statusResponse.Content.ReadAsStringAsync();
- if (content.Contains("Completed", StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
-
- if (content.Contains("Failed", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException($"Tenant {tenantId} provisioning failed: {content}");
- }
- }
-
- await Task.Delay(1000);
- }
-
- throw new TimeoutException($"Tenant {tenantId} provisioning did not complete within {maxRetries} seconds.");
- }
-
///
/// Test transport for the named "Webhooks" HttpClient. Returns a steerable status code and records
/// the outbound request's content headers (signature/event/delivery-id are set on the CONTENT).
diff --git a/src/Tests/Integration.Tests/Tests/Webhooks/WebhookTenantIsolationTests.cs b/src/Tests/Integration.Tests/Tests/Webhooks/WebhookTenantIsolationTests.cs
index 485775c50c..a133298fcc 100644
--- a/src/Tests/Integration.Tests/Tests/Webhooks/WebhookTenantIsolationTests.cs
+++ b/src/Tests/Integration.Tests/Tests/Webhooks/WebhookTenantIsolationTests.cs
@@ -22,7 +22,7 @@ public async Task GetSubscriptions_Should_OnlyReturnCurrentTenantsSubscriptions(
var otherAdminEmail = $"webhook-admin-{uniqueId}@tenant.com";
await CreateTenantAsync(rootClient, otherTenantId, otherAdminEmail);
- await WaitForProvisioningAsync(rootClient, otherTenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, otherTenantId);
using var otherClient = await CreateTenantAdminClientWithRetryAsync(
otherAdminEmail, TestConstants.DefaultPassword, otherTenantId);
@@ -63,7 +63,7 @@ public async Task DeleteSubscription_Should_Return404_When_OwnedByDifferentTenan
var otherAdminEmail = $"webhook-deladmin-{uniqueId}@tenant.com";
await CreateTenantAsync(rootClient, otherTenantId, otherAdminEmail);
- await WaitForProvisioningAsync(rootClient, otherTenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, otherTenantId);
using var otherClient = await CreateTenantAdminClientWithRetryAsync(
otherAdminEmail, TestConstants.DefaultPassword, otherTenantId);
@@ -95,7 +95,7 @@ public async Task TestSubscription_Should_Return404_When_OwnedByDifferentTenant(
var otherAdminEmail = $"webhook-testadmin-{uniqueId}@tenant.com";
await CreateTenantAsync(rootClient, otherTenantId, otherAdminEmail);
- await WaitForProvisioningAsync(rootClient, otherTenantId);
+ await TenantProvisioningWait.WaitForProvisioningAsync(rootClient, otherTenantId);
using var otherClient = await CreateTenantAdminClientWithRetryAsync(
otherAdminEmail, TestConstants.DefaultPassword, otherTenantId);
@@ -147,39 +147,4 @@ private static async Task CreateTenantAsync(HttpClient rootClient, string tenant
var body = await response.Content.ReadAsStringAsync();
response.StatusCode.ShouldBe(HttpStatusCode.Created, $"Create tenant failed: {body}");
}
-
- private static async Task WaitForProvisioningAsync(HttpClient client, string tenantId, int maxRetries = 60)
- {
- for (int i = 0; i < maxRetries; i++)
- {
- var statusResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
-
- if (statusResponse.IsSuccessStatusCode)
- {
- var content = await statusResponse.Content.ReadAsStringAsync();
- if (content.Contains("Completed", StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
-
- if (content.Contains("Failed", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException(
- $"Tenant {tenantId} provisioning failed: {content}");
- }
- }
-
- await Task.Delay(1000);
- }
-
- var finalResponse = await client.GetAsync(
- $"{TestConstants.TenantsBasePath}/{tenantId}/provisioning");
- var finalContent = finalResponse.IsSuccessStatusCode
- ? await finalResponse.Content.ReadAsStringAsync()
- : $"HTTP {finalResponse.StatusCode}";
-
- throw new TimeoutException(
- $"Tenant {tenantId} provisioning did not complete within {maxRetries} seconds. Last status: {finalContent}");
- }
}