using System.Reflection; using Jiaowu.Api.Controllers; using Jiaowu.Api.Domain.Identity; using Jiaowu.Api.Infrastructure.Persistence; using Jiaowu.Api.Infrastructure.Plugins; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Jiaowu.Api.Infrastructure.Middleware; using Microsoft.Extensions.DependencyInjection; namespace Jiaowu.Api.Tests; public sealed class PluginRuntimeTests { [Fact] public void Management_endpoints_require_super_admin() { var method = typeof(PluginsController).GetMethod( nameof(PluginsController.SetState)); var authorize = method?.GetCustomAttribute(); Assert.NotNull(authorize); Assert.Equal(SystemRoles.SuperAdmin, authorize.Roles); } [Fact] public void Catalog_maps_only_complete_api_path_segments() { var catalog = new BuiltInPluginCatalog(); Assert.Equal( "mingxu.experiments", catalog.FindByApiPath("/api/experiments/123")?.Id); Assert.Null(catalog.FindByApiPath("/api/experiments-other")); Assert.Null(catalog.FindByApiPath("/api/plugins/runtime")); } [Fact] public async Task Plugins_are_enabled_by_default_and_state_is_persisted() { var options = new DbContextOptionsBuilder() .UseSqlite("Data Source=:memory:") .Options; await using var db = new AppDbContext(options); await db.Database.OpenConnectionAsync(); await db.Database.EnsureCreatedAsync(); var runtime = new PluginRuntimeService( db, new BuiltInPluginCatalog()); Assert.True(await runtime.IsEnabledAsync("mingxu.experiments")); var updated = await runtime.SetEnabledAsync( "mingxu.experiments", false, "admin-user"); Assert.NotNull(updated); Assert.False(updated.IsEnabled); Assert.False(await runtime.IsEnabledAsync("mingxu.experiments")); var persisted = await db.PluginInstallations.SingleAsync(); Assert.Equal("admin-user", persisted.ChangedByUserId); Assert.Equal("1.0.0", persisted.InstalledVersion); } [Fact] public async Task Unknown_plugin_cannot_be_enabled() { var options = new DbContextOptionsBuilder() .UseSqlite("Data Source=:memory:") .Options; await using var db = new AppDbContext(options); await db.Database.OpenConnectionAsync(); await db.Database.EnsureCreatedAsync(); var runtime = new PluginRuntimeService( db, new BuiltInPluginCatalog()); Assert.Null(await runtime.SetEnabledAsync("unknown.plugin", true, null)); Assert.False(await runtime.IsEnabledAsync("unknown.plugin")); } [Fact] public async Task Disabled_plugin_blocks_its_api_without_invoking_the_endpoint() { var options = new DbContextOptionsBuilder() .UseSqlite("Data Source=:memory:") .Options; await using var db = new AppDbContext(options); await db.Database.OpenConnectionAsync(); await db.Database.EnsureCreatedAsync(); var catalog = new BuiltInPluginCatalog(); var runtime = new PluginRuntimeService(db, catalog); await runtime.SetEnabledAsync("mingxu.experiments", false, "admin-user"); var endpointInvoked = false; var middleware = new PluginAvailabilityMiddleware(_ => { endpointInvoked = true; return Task.CompletedTask; }); var context = new DefaultHttpContext(); await using var requestServices = new ServiceCollection() .AddLogging() .AddProblemDetails() .BuildServiceProvider(); context.RequestServices = requestServices; context.Request.Path = "/api/experiments"; context.Response.Body = new MemoryStream(); await middleware.InvokeAsync(context, catalog, runtime); Assert.False(endpointInvoked); Assert.Equal(StatusCodes.Status404NotFound, context.Response.StatusCode); } }