forked from gongxuegit/tiku-backend.net
feat(security): complete capability messaging workflows
This commit is contained in:
@@ -16,6 +16,8 @@ using Tiku.Domain.Identity;
|
||||
using Tiku.Domain.Operations;
|
||||
using Tiku.Domain.QuestionBanks;
|
||||
using Tiku.Domain.Content;
|
||||
using Tiku.Domain.Commerce;
|
||||
using Tiku.Domain.Platform;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using Tiku.IntegrationTests.Infrastructure;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
@@ -134,6 +136,54 @@ public sealed class ApiTestFactory(
|
||||
scope.ServiceProvider.GetRequiredService<ITenantContextInitializer>()
|
||||
.InitializeSystem(null, "Integration test fixture seeding");
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
|
||||
var tenants = entities.OfType<Tenant>().Where(tenant => tenant.Mode == TenantMode.Saas).ToArray();
|
||||
var hasExplicitCapabilitySetup = entities.Any(entity =>
|
||||
entity is PlatformSaasPlan or ProductModule or PlanModuleEntitlement or TenantSubscription);
|
||||
if (tenants.Length > 0 && !hasExplicitCapabilitySetup)
|
||||
{
|
||||
const string integrationPlanCode = "integration-full-access";
|
||||
if (!await dbContext.PlatformSaasPlans.AnyAsync(plan => plan.Code == integrationPlanCode))
|
||||
{
|
||||
dbContext.PlatformSaasPlans.Add(new PlatformSaasPlan
|
||||
{
|
||||
Code = integrationPlanCode,
|
||||
Name = "Integration Full Access"
|
||||
});
|
||||
}
|
||||
|
||||
var existingModules = await dbContext.ProductModules
|
||||
.Select(module => module.Code)
|
||||
.ToArrayAsync();
|
||||
foreach (var module in ProductModuleCatalog.All.Where(module => !existingModules.Contains(module.Key)))
|
||||
{
|
||||
dbContext.ProductModules.Add(new ProductModule { Code = module.Key, Name = module.Value });
|
||||
}
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
var entitledModules = await dbContext.PlanModuleEntitlements
|
||||
.Where(entitlement => entitlement.PlanCode == integrationPlanCode)
|
||||
.Select(entitlement => entitlement.ModuleCode)
|
||||
.ToArrayAsync();
|
||||
foreach (var moduleCode in ProductModuleCatalog.All.Keys.Except(entitledModules, StringComparer.Ordinal))
|
||||
{
|
||||
dbContext.PlanModuleEntitlements.Add(new PlanModuleEntitlement
|
||||
{
|
||||
PlanCode = integrationPlanCode,
|
||||
ModuleCode = moduleCode
|
||||
});
|
||||
}
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
entities = entities.Concat(tenants.Select(tenant => new TenantSubscription
|
||||
{
|
||||
TenantId = tenant.Id,
|
||||
PlanCode = integrationPlanCode,
|
||||
Status = TenantSubscriptionStatus.Active,
|
||||
StartsAt = now.AddDays(-1),
|
||||
ExpiresAt = now.AddYears(1)
|
||||
})).ToArray();
|
||||
}
|
||||
dbContext.AddRange(entities);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
|
||||
@@ -7,14 +7,17 @@ using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MassTransit;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Infrastructure.Messaging;
|
||||
|
||||
namespace Tiku.IntegrationTests.Api;
|
||||
|
||||
public sealed class AuthorizationManifestTests
|
||||
{
|
||||
private const int ExpectedActionCount = 330;
|
||||
private const string ExpectedSha256 = "ad09167662cb9dc25111f40902c5f16a6633465f0ca7e7da0e50cdc10cfb8bb5";
|
||||
private const int ExpectedActionCount = 332;
|
||||
private const string ExpectedSha256 = "a80fe477ba3021625e17c9fc639e5109bab678178f8024a51c3c732bf5a46d3f";
|
||||
|
||||
[Fact]
|
||||
public void Controller_authorization_surface_matches_reviewed_manifest()
|
||||
@@ -61,6 +64,30 @@ public sealed class AuthorizationManifestTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Message_consumers_have_reviewed_authorization_and_audit_metadata()
|
||||
{
|
||||
var consumers = typeof(MessagingOptions).Assembly.GetTypes()
|
||||
.Where(type => !type.IsAbstract && type.GetInterfaces().Any(candidate =>
|
||||
candidate.IsGenericType && candidate.GetGenericTypeDefinition() == typeof(IConsumer<>)))
|
||||
.ToArray();
|
||||
|
||||
Assert.Equal(2, consumers.Length);
|
||||
foreach (var consumer in consumers)
|
||||
{
|
||||
var metadata = consumer.GetCustomAttribute<ConsumerAuthorizationMetadataAttribute>();
|
||||
Assert.NotNull(metadata);
|
||||
Assert.Contains(metadata.Realm, new[] { "tenant", "platform", "system" });
|
||||
Assert.False(string.IsNullOrWhiteSpace(metadata.Module));
|
||||
Assert.False(string.IsNullOrWhiteSpace(metadata.AuditAction));
|
||||
if (consumer.Name == "BackgroundJobRequestedConsumer")
|
||||
{
|
||||
Assert.Equal(CapabilityOperation.Write, metadata.Operation);
|
||||
Assert.True(metadata.RequiresSystemScope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string Describe(Type controller, MethodInfo action)
|
||||
{
|
||||
var controllerRoute = controller.GetCustomAttribute<RouteAttribute>()?.Template ?? string.Empty;
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Text.Json;
|
||||
using Tiku.Application.Jobs;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Commerce;
|
||||
using Tiku.Domain.Operations;
|
||||
using Tiku.Domain.Platform;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
@@ -9,6 +13,48 @@ namespace Tiku.IntegrationTests.Api;
|
||||
|
||||
public sealed class CapabilityAuthorizationTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Background_job_rechecks_capability_after_enqueue_before_execution()
|
||||
{
|
||||
await using var factory = new ApiTestFactory();
|
||||
var tenantId = Guid.NewGuid();
|
||||
await factory.SeedAsync(
|
||||
new Tenant { Id = tenantId, Slug = tenantId.ToString("N"), Name = "Job Capability Tenant" },
|
||||
new PlatformSaasPlan { Code = "job-capability-test", Name = "Job Capability Test" },
|
||||
new PlanModuleEntitlement { PlanCode = "job-capability-test", ModuleCode = "content" },
|
||||
new TenantSubscription
|
||||
{
|
||||
TenantId = tenantId,
|
||||
PlanCode = "job-capability-test",
|
||||
Status = TenantSubscriptionStatus.Active,
|
||||
StartsAt = DateTimeOffset.UtcNow.AddDays(-1),
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(30)
|
||||
});
|
||||
|
||||
using var scope = factory.CreateSystemScope("Verify job capability at consumption");
|
||||
var jobs = scope.ServiceProvider.GetRequiredService<IBackgroundJobService>();
|
||||
var job = await jobs.EnqueueAsync(new CreateBackgroundJobCommand(
|
||||
tenantId,
|
||||
"content_export",
|
||||
JsonSerializer.SerializeToElement(new { exportType = "capability-test" })));
|
||||
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
|
||||
dbContext.TenantModuleOverrides.Add(new TenantModuleOverride
|
||||
{
|
||||
TenantId = tenantId,
|
||||
ModuleCode = "content",
|
||||
Mode = TenantModuleOverrideMode.Disabled,
|
||||
Reason = "Integration test revocation"
|
||||
});
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
Assert.True(await jobs.ProcessRequestedAsync(
|
||||
job.Id, tenantId, job.JobType, "capability-test-worker"));
|
||||
var stored = await dbContext.BackgroundJobs.AsNoTracking().SingleAsync(item => item.Id == job.Id);
|
||||
Assert.Equal(BackgroundJobStatus.Failed, stored.Status);
|
||||
Assert.Equal("Tenant capability was revoked before job execution.", stored.LastError);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Entitlement_is_database_backed_and_past_due_is_read_only()
|
||||
{
|
||||
@@ -17,7 +63,6 @@ public sealed class CapabilityAuthorizationTests
|
||||
await factory.SeedAsync(
|
||||
new Tenant { Id = tenantId, Slug = tenantId.ToString("N"), Name = "Capability Tenant" },
|
||||
new PlatformSaasPlan { Code = "capability-test", Name = "Capability Test" },
|
||||
new ProductModule { Code = "content", Name = "Content" },
|
||||
new TenantSubscription
|
||||
{
|
||||
TenantId = tenantId,
|
||||
|
||||
@@ -62,6 +62,9 @@ public sealed class PlatformAdminEndpointTests
|
||||
|
||||
var overview = await client.GetAsync("/api/platform-admin/overview");
|
||||
var tenants = await client.GetAsync("/api/platform-admin/tenants?search=six-a");
|
||||
var planModules = await client.PutAsJsonAsync(
|
||||
"/api/platform-admin/plans/standard/modules",
|
||||
new ReplacePlatformPlanModulesDto { ModuleCodes = ["content", "job", "settings"] });
|
||||
var subscription = await client.PostAsJsonAsync(
|
||||
"/api/platform-admin/subscriptions",
|
||||
new UpsertPlatformSubscriptionDto
|
||||
@@ -73,6 +76,13 @@ public sealed class PlatformAdminEndpointTests
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(30),
|
||||
AmountCents = 99900
|
||||
});
|
||||
var moduleOverride = await client.PutAsJsonAsync(
|
||||
$"/api/platform-admin/tenants/{tenantId}/module-overrides/content",
|
||||
new UpsertPlatformTenantModuleOverrideDto
|
||||
{
|
||||
Mode = TenantModuleOverrideMode.Disabled,
|
||||
Reason = "integration test capability revocation"
|
||||
});
|
||||
var recheck = await client.PostAsync($"/api/platform-admin/domains/{domainId}/recheck", null);
|
||||
var suspended = await client.PatchAsJsonAsync(
|
||||
"/api/platform-admin/tenants/status",
|
||||
@@ -90,7 +100,9 @@ public sealed class PlatformAdminEndpointTests
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, overview.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, tenants.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, planModules.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, subscription.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, moduleOverride.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, recheck.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, suspended.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.NotFound, runtimeAfterSuspend.StatusCode);
|
||||
@@ -103,6 +115,12 @@ public sealed class PlatformAdminEndpointTests
|
||||
Assert.True(await dbContext.AuditLogs.AnyAsync(log =>
|
||||
log.ActorUserId == platform.UserId &&
|
||||
log.Action == "platform.tenant.status_changed"));
|
||||
Assert.True(await dbContext.AuditLogs.AnyAsync(log =>
|
||||
log.ActorUserId == platform.UserId &&
|
||||
log.Action == "platform.plan.modules.replaced"));
|
||||
Assert.True(await dbContext.AuditLogs.AnyAsync(log =>
|
||||
log.ActorUserId == platform.UserId &&
|
||||
log.Action == "platform.tenant.module_override.updated"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -17,6 +17,7 @@ public sealed class QuestionBankEndpointTests
|
||||
var otherTenantId = Guid.NewGuid();
|
||||
await using var factory = new ApiTestFactory();
|
||||
await factory.SeedAsync(
|
||||
PlatformTenant(),
|
||||
Tenant(tenantId, "master"),
|
||||
Tenant(otherTenantId, "other"),
|
||||
new QuestionBank
|
||||
@@ -62,6 +63,7 @@ public sealed class QuestionBankEndpointTests
|
||||
var versionId = Guid.NewGuid();
|
||||
await using var factory = new ApiTestFactory();
|
||||
await factory.SeedAsync(
|
||||
PlatformTenant(),
|
||||
Tenant(tenantId, "master"),
|
||||
new Subject { Id = subjectId, TenantId = tenantId, Name = "测试科目" },
|
||||
new Category { Id = categoryId, TenantId = tenantId, SubjectId = subjectId, Name = "测试分类" },
|
||||
@@ -130,6 +132,7 @@ public sealed class QuestionBankEndpointTests
|
||||
var questionId = Guid.NewGuid();
|
||||
await using var factory = new ApiTestFactory();
|
||||
await factory.SeedAsync(
|
||||
PlatformTenant(),
|
||||
Tenant(tenantId, "master"),
|
||||
new Question
|
||||
{
|
||||
@@ -154,6 +157,7 @@ public sealed class QuestionBankEndpointTests
|
||||
var questionId = Guid.NewGuid();
|
||||
await using var factory = new ApiTestFactory();
|
||||
await factory.SeedAsync(
|
||||
PlatformTenant(),
|
||||
Tenant(tenantId, "master"),
|
||||
new Question
|
||||
{
|
||||
@@ -199,6 +203,16 @@ public sealed class QuestionBankEndpointTests
|
||||
};
|
||||
}
|
||||
|
||||
private static Tenant PlatformTenant() => new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Slug = $"platform-{Guid.NewGuid():N}",
|
||||
Name = "Platform Question Bank",
|
||||
Status = TenantStatus.Active,
|
||||
Mode = TenantMode.PlatformOwned,
|
||||
Metadata = JsonDefaults.Object()
|
||||
};
|
||||
|
||||
private static async Task<JsonElement[]> ReadItemsAsync(HttpResponseMessage response)
|
||||
{
|
||||
var body = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
|
||||
|
||||
@@ -4,11 +4,16 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using StackExchange.Redis;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Sockets;
|
||||
using System.Text.Json;
|
||||
using Tiku.Application;
|
||||
using Tiku.Application.Jobs;
|
||||
using Tiku.Contracts;
|
||||
using Tiku.Domain.Operations;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using Tiku.Infrastructure;
|
||||
using Tiku.Infrastructure.Messaging;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
@@ -17,6 +22,199 @@ namespace Tiku.IntegrationTests;
|
||||
|
||||
public sealed class MassTransitOutboxTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Bus_outbox_drains_after_real_broker_restart()
|
||||
{
|
||||
var rabbitMqHost = Environment.GetEnvironmentVariable("TIKU_TEST_RABBITMQ");
|
||||
var containerName = Environment.GetEnvironmentVariable("TIKU_TEST_RABBITMQ_CONTAINER");
|
||||
if (string.IsNullOrWhiteSpace(rabbitMqHost) ||
|
||||
Environment.GetEnvironmentVariable("TIKU_TEST_RABBITMQ_RESTART") != "1" ||
|
||||
string.IsNullOrWhiteSpace(containerName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await using var factory = CreateRabbitFactory(rabbitMqHost);
|
||||
using var client = factory.CreateClient();
|
||||
Assert.True(await WaitForReadyAsync(client), "API dependencies did not become ready before restart drill.");
|
||||
|
||||
await RunDockerAsync("stop", containerName);
|
||||
try
|
||||
{
|
||||
Assert.True(await WaitForBrokerPortClosedAsync(new Uri(rabbitMqHost)),
|
||||
"RabbitMQ AMQP port remained reachable after stopping the test container.");
|
||||
|
||||
using (var scope = factory.CreateSystemScope("Commit outbox while RabbitMQ is stopped"))
|
||||
{
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
|
||||
var publisher = scope.ServiceProvider.GetRequiredService<ISecurityEventPublisher>();
|
||||
await using var transaction = await dbContext.Database.BeginTransactionAsync();
|
||||
await publisher.AuthorizationChangedAsync(
|
||||
null, null, "broker_restart_test", 3, Guid.NewGuid().ToString("N"));
|
||||
await dbContext.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
}
|
||||
|
||||
using var verification = factory.CreateSystemScope("Verify restart outbox backlog");
|
||||
var verificationDbContext = verification.ServiceProvider.GetRequiredService<TikuDbContext>();
|
||||
Assert.NotEmpty(await verificationDbContext.Set<OutboxMessage>().ToArrayAsync());
|
||||
}
|
||||
finally
|
||||
{
|
||||
await RunDockerAsync("start", containerName);
|
||||
}
|
||||
|
||||
Assert.True(await WaitForReadyAsync(client, 240),
|
||||
"RabbitMQ did not become ready within 60 seconds after restart.");
|
||||
var drained = false;
|
||||
for (var attempt = 0; attempt < 240; attempt++)
|
||||
{
|
||||
using var verification = factory.CreateSystemScope("Wait for post-restart outbox drain");
|
||||
var dbContext = verification.ServiceProvider.GetRequiredService<TikuDbContext>();
|
||||
if (!await dbContext.Set<OutboxMessage>().AnyAsync())
|
||||
{
|
||||
drained = true;
|
||||
break;
|
||||
}
|
||||
await Task.Delay(250);
|
||||
}
|
||||
Assert.True(drained, "Bus outbox did not drain within 60 seconds after RabbitMQ restart.");
|
||||
}
|
||||
|
||||
private static async Task RunDockerAsync(string operation, string containerName)
|
||||
{
|
||||
using var process = Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "docker",
|
||||
ArgumentList = { operation, containerName },
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false
|
||||
}) ?? throw new InvalidOperationException("Failed to start Docker CLI for RabbitMQ restart drill.");
|
||||
var standardOutput = await process.StandardOutput.ReadToEndAsync();
|
||||
var standardError = await process.StandardError.ReadToEndAsync();
|
||||
await process.WaitForExitAsync();
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"docker {operation} failed for the RabbitMQ test container: {standardError}{standardOutput}");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<bool> WaitForBrokerPortClosedAsync(Uri broker)
|
||||
{
|
||||
var port = broker.IsDefaultPort ? 5672 : broker.Port;
|
||||
for (var attempt = 0; attempt < 40; attempt++)
|
||||
{
|
||||
using var tcpClient = new TcpClient();
|
||||
try
|
||||
{
|
||||
await tcpClient.ConnectAsync(broker.Host, port).WaitAsync(TimeSpan.FromMilliseconds(250));
|
||||
}
|
||||
catch (Exception exception) when (exception is SocketException or TimeoutException)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
await Task.Delay(250);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Background_job_request_is_transactional_consumed_once_and_keeps_database_status_view()
|
||||
{
|
||||
var rabbitMqHost = Environment.GetEnvironmentVariable("TIKU_TEST_RABBITMQ");
|
||||
if (string.IsNullOrWhiteSpace(rabbitMqHost))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await using var factory = CreateRabbitFactory(rabbitMqHost);
|
||||
using var client = factory.CreateClient();
|
||||
Assert.True(await WaitForReadyAsync(client), "API dependencies did not become ready within 10 seconds.");
|
||||
|
||||
var tenantId = Guid.NewGuid();
|
||||
await factory.SeedAsync(new Tenant
|
||||
{
|
||||
Id = tenantId,
|
||||
Slug = tenantId.ToString("N"),
|
||||
Name = "RabbitMQ Background Job Tenant"
|
||||
});
|
||||
|
||||
var workerBuilder = Host.CreateApplicationBuilder();
|
||||
workerBuilder.Services.AddApplication();
|
||||
workerBuilder.Services.AddInfrastructure(factory.DatabaseConnectionString);
|
||||
workerBuilder.Services.AddReliableMessaging(CreateRabbitOptions(rabbitMqHost, configureConsumers: true));
|
||||
using var worker = workerBuilder.Build();
|
||||
await worker.StartAsync();
|
||||
|
||||
Guid rolledBackJobId;
|
||||
using (var scope = factory.CreateSystemScope("Roll back background job request"))
|
||||
{
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
|
||||
var jobs = scope.ServiceProvider.GetRequiredService<IBackgroundJobService>();
|
||||
await using var transaction = await dbContext.Database.BeginTransactionAsync();
|
||||
var job = await jobs.EnqueueAsync(new CreateBackgroundJobCommand(
|
||||
tenantId, "tenant_domain_recheck", JsonSerializer.SerializeToElement(new { })));
|
||||
rolledBackJobId = job.Id;
|
||||
await transaction.RollbackAsync();
|
||||
}
|
||||
|
||||
using (var verification = factory.CreateSystemScope("Verify rolled back background job request"))
|
||||
{
|
||||
var dbContext = verification.ServiceProvider.GetRequiredService<TikuDbContext>();
|
||||
Assert.False(await dbContext.BackgroundJobs.AnyAsync(item => item.Id == rolledBackJobId));
|
||||
Assert.False(await dbContext.Set<OutboxMessage>().AnyAsync(
|
||||
item => item.MessageId == rolledBackJobId));
|
||||
}
|
||||
|
||||
BackgroundJobItem committedJob;
|
||||
using (var scope = factory.CreateSystemScope("Commit background job request"))
|
||||
{
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
|
||||
var jobs = scope.ServiceProvider.GetRequiredService<IBackgroundJobService>();
|
||||
await using var transaction = await dbContext.Database.BeginTransactionAsync();
|
||||
committedJob = await jobs.EnqueueAsync(new CreateBackgroundJobCommand(
|
||||
tenantId, "tenant_domain_recheck", JsonSerializer.SerializeToElement(new { })));
|
||||
await transaction.CommitAsync();
|
||||
}
|
||||
|
||||
BackgroundJobStatus? status = null;
|
||||
for (var attempt = 0; attempt < 60; attempt++)
|
||||
{
|
||||
using var verification = factory.CreateSystemScope("Wait for RabbitMQ background job consumer");
|
||||
var dbContext = verification.ServiceProvider.GetRequiredService<TikuDbContext>();
|
||||
status = await dbContext.BackgroundJobs
|
||||
.Where(item => item.Id == committedJob.Id)
|
||||
.Select(item => (BackgroundJobStatus?)item.Status)
|
||||
.SingleAsync();
|
||||
if (status == BackgroundJobStatus.Succeeded) break;
|
||||
await Task.Delay(250);
|
||||
}
|
||||
Assert.Equal(BackgroundJobStatus.Succeeded, status);
|
||||
|
||||
using (var verification = factory.CreateSystemScope("Verify background job inbox and status view"))
|
||||
{
|
||||
var dbContext = verification.ServiceProvider.GetRequiredService<TikuDbContext>();
|
||||
Assert.Single(await dbContext.Set<InboxState>()
|
||||
.Where(item => item.MessageId == committedJob.Id)
|
||||
.ToArrayAsync());
|
||||
var jobs = verification.ServiceProvider.GetRequiredService<IBackgroundJobService>();
|
||||
var statusView = await jobs.ListAsync(tenantId, "tenant_domain_recheck");
|
||||
Assert.Contains(statusView, item =>
|
||||
item.Id == committedJob.Id && item.Status == BackgroundJobStatus.Succeeded);
|
||||
}
|
||||
|
||||
var managementEndpoint = ResolveRabbitManagementEndpoint(rabbitMqHost);
|
||||
if (managementEndpoint is not null)
|
||||
{
|
||||
Assert.Equal(0, await GetQueueMessageCountAsync(
|
||||
managementEndpoint, "background-job-requested_error"));
|
||||
}
|
||||
|
||||
await worker.StopAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Worker_consumer_uses_inbox_and_updates_non_authoritative_redis_version()
|
||||
{
|
||||
@@ -177,9 +375,9 @@ public sealed class MassTransitOutboxTests
|
||||
ConfigureConsumers = configureConsumers
|
||||
};
|
||||
|
||||
private static async Task<bool> WaitForReadyAsync(HttpClient client)
|
||||
private static async Task<bool> WaitForReadyAsync(HttpClient client, int attempts = 40)
|
||||
{
|
||||
for (var attempt = 0; attempt < 40; attempt++)
|
||||
for (var attempt = 0; attempt < attempts; attempt++)
|
||||
{
|
||||
if ((await client.GetAsync("/api/health/ready")).StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
@@ -210,6 +408,10 @@ public sealed class MassTransitOutboxTests
|
||||
var credentials = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes($"{username}:{password}"));
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials);
|
||||
using var response = await client.GetAsync($"/api/queues/%2F/{Uri.EscapeDataString(queueName)}");
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
response.EnsureSuccessStatusCode();
|
||||
using var document = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
|
||||
return document.RootElement.GetProperty("messages").GetInt32();
|
||||
|
||||
@@ -16,10 +16,20 @@ public sealed class SystemScopeAuditTests
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => executionScope.ExecuteAsync(
|
||||
new SystemScopeRequest(null, SystemScopeCallerType.Worker, "worker", "", "job-1"),
|
||||
(_, _) => Task.CompletedTask));
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => executionScope.ExecuteAsync(
|
||||
new SystemScopeRequest(null, SystemScopeCallerType.Worker, "worker", "missing target", "job-2"),
|
||||
(_, _) => Task.CompletedTask));
|
||||
|
||||
var correlationId = Guid.NewGuid().ToString("N");
|
||||
var tenantId = Guid.NewGuid();
|
||||
await factory.SeedAsync(new Tenant
|
||||
{
|
||||
Id = tenantId,
|
||||
Slug = tenantId.ToString("N"),
|
||||
Name = "System Scope Tenant"
|
||||
});
|
||||
await executionScope.ExecuteAsync(
|
||||
new SystemScopeRequest(null, SystemScopeCallerType.Worker, "background-worker", "test audit", correlationId),
|
||||
new SystemScopeRequest(tenantId, SystemScopeCallerType.Worker, "background-worker", "test audit", correlationId),
|
||||
(_, _) => Task.CompletedTask);
|
||||
|
||||
using var verification = factory.CreateSystemScope("Verify execution scope audit");
|
||||
|
||||
Reference in New Issue
Block a user