Files
Academic-Affairs-System/tests/Jiaowu.Api.Tests/OperationsControllerTests.cs
T
biss 551f1143b7 超级管理员现在可在“组织与权限 → 运维与审计 → 系统性能”中直接查看:
请求量、5xx 比例、HTTP/数据库 P95
请求速率与延迟趋势
最慢接口排行
慢查询与数据库查询排行
Grafana 原始调用链入口
2026-07-31 09:34:31 +08:00

264 lines
9.6 KiB
C#

using System.Reflection;
using Jiaowu.Api.Contracts;
using Jiaowu.Api.Controllers;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Domain.System;
using Jiaowu.Api.Infrastructure.BackgroundJobs;
using Jiaowu.Api.Infrastructure.Caching;
using Jiaowu.Api.Infrastructure.Operations;
using Jiaowu.Api.Infrastructure.Observability;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Jiaowu.Api.Tests;
public sealed class OperationsControllerTests
{
[Fact]
public void Controller_is_restricted_to_super_admin()
{
var authorize = typeof(OperationsController)
.GetCustomAttribute<AuthorizeAttribute>();
Assert.NotNull(authorize);
Assert.Equal(SystemRoles.SuperAdmin, authorize.Roles);
}
[Fact]
public async Task Audit_and_failed_job_queries_return_operational_records()
{
var root = CreateTemporaryRoot();
try
{
await using var fixture = await OperationsFixture.CreateAsync(root);
var term = new AcademicTerm
{
Code = "OPS-TERM",
Name = "运维测试学期",
AcademicYear = "2026-2027",
Season = TermSeason.Autumn,
StartDate = new DateOnly(2026, 9, 1),
EndDate = new DateOnly(2027, 1, 15)
};
var plan = new SchedulePlan
{
AcademicTerm = term,
Name = "恢复演练排课方案",
Version = "V1"
};
var job = new AutomaticScheduleJob
{
SchedulePlan = plan,
Status = AutomaticScheduleJobStatus.Failed,
ErrorMessage = "排课求解器未找到可行解。",
CompletedAt = DateTime.UtcNow
};
var outbox = BackgroundJobOutboxMessage.Create(
BackgroundJobKind.AutomaticSchedule,
job.Id);
outbox.State = BackgroundJobOutboxState.Completed;
outbox.ProcessingAttempts = 3;
fixture.Db.AuditLogs.Add(new AuditLog
{
UserName = "root",
Method = "POST",
Path = "/api/schedules",
StatusCode = 500,
IpAddress = "127.0.0.1"
});
fixture.Db.AddRange(term, plan, job, outbox);
await fixture.Db.SaveChangesAsync();
var auditAction = await fixture.Controller.GetAuditLogs(
cancellationToken: CancellationToken.None);
var auditResult = Assert.IsType<OkObjectResult>(auditAction.Result);
var auditPage = Assert.IsType<PagedResult<AuditLogItem>>(auditResult.Value);
Assert.Single(auditPage.Items);
Assert.Equal(500, auditPage.Items.Single().StatusCode);
var jobsAction = await fixture.Controller.GetFailedJobs(
cancellationToken: CancellationToken.None);
var jobsResult = Assert.IsType<OkObjectResult>(jobsAction.Result);
var jobsPage =
Assert.IsType<PagedResult<FailedBackgroundJobItem>>(jobsResult.Value);
var failed = Assert.Single(jobsPage.Items);
Assert.Equal("AutomaticSchedule", failed.Kind);
Assert.Equal(3, failed.ProcessingAttempts);
Assert.Contains("可行解", failed.ErrorMessage);
}
finally
{
DeleteTemporaryRoot(root);
}
}
[Fact]
public async Task Sqlite_backup_can_be_verified_in_an_isolated_restore_drill()
{
var root = CreateTemporaryRoot();
try
{
await using var fixture = await OperationsFixture.CreateAsync(root);
fixture.Db.AuditLogs.Add(new AuditLog
{
UserName = "backup-test",
Method = "POST",
Path = "/api/test",
StatusCode = 204
});
await fixture.Db.SaveChangesAsync();
var artifact = await fixture.Backups.CreateAsync(
"自动化测试",
CancellationToken.None);
var result = await fixture.Backups.RunRestoreDrillAsync(
artifact.Id,
CancellationToken.None);
var listed = await fixture.Backups.ListAsync(CancellationToken.None);
Assert.True(File.Exists(Path.Combine(
root,
"backups",
artifact.FileName)));
Assert.Equal(64, artifact.Sha256.Length);
Assert.True(result.Succeeded, result.Detail);
Assert.True(result.TableCount > 0);
Assert.True(Assert.Single(listed).LastDrillSucceeded);
}
finally
{
DeleteTemporaryRoot(root);
}
}
private static string CreateTemporaryRoot()
{
var root = Path.Combine(
Path.GetTempPath(),
$"jiaowu-operations-{Guid.NewGuid():N}");
Directory.CreateDirectory(root);
return root;
}
private static void DeleteTemporaryRoot(string root)
{
var resolved = Path.GetFullPath(root);
var temporary = Path.GetFullPath(Path.GetTempPath());
Assert.StartsWith(temporary, resolved, StringComparison.OrdinalIgnoreCase);
if (Directory.Exists(resolved))
Directory.Delete(resolved, recursive: true);
}
private sealed class OperationsFixture : IAsyncDisposable
{
private readonly ServiceProvider provider;
private OperationsFixture(
ServiceProvider provider,
AppDbContext db,
OperationsController controller,
DatabaseBackupService backups)
{
this.provider = provider;
Db = db;
Controller = controller;
Backups = backups;
}
public AppDbContext Db { get; }
public OperationsController Controller { get; }
public DatabaseBackupService Backups { get; }
public static async Task<OperationsFixture> CreateAsync(string root)
{
var databasePath = Path.Combine(root, "source.sqlite");
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["ConnectionStrings:SQLite"] =
$"Data Source={databasePath};Pooling=False"
})
.Build();
var databaseOptions = new DatabaseOptions { Provider = "SQLite" };
var cacheOptions = new AppCacheOptions();
var operationsOptions = new OperationsOptions
{
BackupDirectory = "backups"
};
var dbContextOptions = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlite(configuration.GetConnectionString("SQLite"))
.Options;
var db = new AppDbContext(dbContextOptions);
await db.Database.EnsureCreatedAsync();
var services = new ServiceCollection()
.AddLogging()
.AddMemoryCache()
.BuildServiceProvider();
var logger = services.GetRequiredService<
ILogger<DatabaseBackupService>>();
var environment = new TestHostEnvironment
{
ContentRootPath = root
};
var transport = new InMemoryBackgroundJobTransport();
var monitoring = new BackgroundJobMonitoringService(db);
var health = new OperationalHealthService(
db,
services,
transport,
monitoring,
databaseOptions,
cacheOptions,
configuration);
var backups = new DatabaseBackupService(
databaseOptions,
operationsOptions,
configuration,
environment,
logger);
var performance = new PerformanceReportService(
new HttpClient(),
services.GetRequiredService<
Microsoft.Extensions.Caching.Memory.IMemoryCache>(),
new PerformanceReportingOptions(),
new ObservabilityOptions(),
services.GetRequiredService<
ILogger<PerformanceReportService>>());
var controller = new OperationsController(
db,
health,
backups,
performance,
operationsOptions);
return new OperationsFixture(services, db, controller, backups);
}
public async ValueTask DisposeAsync()
{
await Db.DisposeAsync();
await provider.DisposeAsync();
}
}
private sealed class TestHostEnvironment : IWebHostEnvironment
{
public string ApplicationName { get; set; } = "Jiaowu.Api.Tests";
public IFileProvider WebRootFileProvider { get; set; } = new NullFileProvider();
public string WebRootPath { get; set; } = "";
public string EnvironmentName { get; set; } = Environments.Development;
public string ContentRootPath { get; set; } = "";
public IFileProvider ContentRootFileProvider { get; set; } =
new NullFileProvider();
}
}