diff --git a/.gitignore b/.gitignore index 8f23f904..fb843f96 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ appsettings-*.*.json generated/ *.tmp DataDir/ + +*.tests.trx +*.tests.html diff --git a/doc/dev-tracking/client-editor-overhaul.md b/doc/dev-tracking/client-editor-overhaul.md index 0cff2343..0231e6aa 100644 --- a/doc/dev-tracking/client-editor-overhaul.md +++ b/doc/dev-tracking/client-editor-overhaul.md @@ -207,3 +207,72 @@ Once the 4 compile errors are fixed and the pages render: today; consider adding a `Yavsc.Org.Tests` project that drives the controller via `WebApplicationFactory`. + +## Test bootstrap notes (session of 2026-06-21 17:00+) + +When adding new integration tests against `WebServerFixture`: + +1. **Skip `/Account/Login` roundtrip.** The fixture ships without + `MapRazorPages()` (commented out in `HostingExtensions.ConfigurePipeline`), + so `/Identity/Account/Login` is 404, and the custom + `/Account/Login` route requires a complex antiforgery dance. + Instead, build a `ClaimsPrincipal` for the test user via + `UserManager` + `IUserClaimsPrincipalFactory`, + then call `IAuthenticationService.SignInAsync` on a synthetic + `DefaultHttpContext` and replay the resulting `Set-Cookie` header + into the test `HttpClient`. See + `ClientControllerCollectionTests.IssueIdentityCookie`. + +2. **Create the `Administrator` role before assigning it.** ASP.NET + Identity stores roles in `AspNetRoles`; there is no automatic seed. + The constant name is `YavscConstants.AdminGroupName` = `"Administrator"`. + Use `RoleManager.CreateAsync(new IdentityRole("Administrator"))` + before `AddToRoleAsync`. + +3. **Use `InMemory` connection string to bypass the prod signing-cert + requirement.** `HostingExtensions.AddIdentityServer` requires a + PEM cert unless `builder.Environment.IsDevelopment()` OR + `UsesInMemoryProvider(connectionString)`. The fixture already + uses `InMemory`, so `AddDeveloperSigningCredential()` is called + automatically — but only after we wired this check in (see + commit history). + +4. **Field-name gotchas** (from disassembling HigginsSoft + IdentityServer8.EntityFramework.Entities.Client 8.0.5-preview-net9): + - `PairWiseSubjectSalt` (capital W on "Wise"), not `PairwiseSubjectSalt`. + - `CibaLifetime` and `PollingInterval` do NOT exist on `Client` in + this version. + - `ConsentLifetime` and `UserSsoLifetime` are `int?`. + +5. **`MapStaticAssets()` fails on test projects.** Calling + `MapStaticAssets()` resolves a manifest file + (`.staticwebassets.endpoints.json`) that test projects + don't produce. Skip when `WebRootPath` points at the test + assembly directory. + +6. **Routing 404 on /Client/Edit/{id} via WebServerFixture.** As of + this session, the GET endpoint returns 404 even with admin + header. The route mapping is intact + (`MapDefaultControllerRoute()`), so this is likely an MVC + convention routing issue with the + `Controllers/Administration/` subdirectory. To investigate + next session: log middleware pipeline or hit `/Client` index + first to see if any Client route resolves. + +7. **`MapStaticAssets()` is unconditional in prod, but blocks tests.** + `WebApplication.CreateBuilder` defaults `ContentRootPath` to + `AppContext.BaseDirectory`. In test runs that resolves to + `src/Yavsc.Org.Tests/bin/Debug/net10.0/`, where + `Yavsc.Org.Tests.staticwebassets.endpoints.json` doesn't exist + (it's generated only by projects with the Web SDK). The + `app.MapStaticAssets()` call inside `ConfigurePipeline` then + throws and the fixture fails to start — taking every test in + the `[Collection("Yavsc Server")]` down with it. + + This is a pre-existing fragility of the WebServerFixture that + the new test work surfaced. Fixing it cleanly requires either: + (a) moving the test project to the Web SDK so it produces its + own manifest, (b) copying the manifest at build time via an + MSBuild target, or (c) routing `MapStaticAssets` through an + assembly-resolution fallback. None attempted in this session — + recorded for next session. diff --git a/src/Yavsc.Org.Tests/Controllers/ClientControllerCollectionTests.cs b/src/Yavsc.Org.Tests/Controllers/ClientControllerCollectionTests.cs new file mode 100644 index 00000000..352f25f9 --- /dev/null +++ b/src/Yavsc.Org.Tests/Controllers/ClientControllerCollectionTests.cs @@ -0,0 +1,214 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using IdentityServer8.EntityFramework.Entities; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Yavsc.Models; + +namespace Yavsc.Org.Tests.Controllers; + +/// +/// Integration tests for the per-collection edit pages of the OAuth2 +/// client editor. Hits the real ASP.NET pipeline against +/// : the WebApplication is built once per +/// collection and shared, so these tests must be defensive about row +/// IDs (they pick their own dedicated client to mutate). +/// +/// Admin auth is satisfied by sending an X-Test-Role: Administrator +/// header. The short-circuits the +/// production policy to honour that header, sidestepping the login flow +/// (which is itself not exercised by these tests). +/// +[Collection("Yavsc Server")] +public class ClientControllerCollectionTests : IClassFixture +{ + private const string TargetClientId = "collection-tests-target"; + + private readonly TestWebApplicationFactory _factory; + + public ClientControllerCollectionTests(TestWebApplicationFactory factory) + { + _factory = factory; + EnsureTargetClient(); + } + + private void EnsureTargetClient() + { + using var scope = _factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + if (db.Clients.Any(c => c.ClientId == TargetClientId)) return; + + db.Clients.Add(new Client + { + ClientId = TargetClientId, + ClientName = "Collection-edit tests target", + Enabled = true, + RequireClientSecret = false, + RequirePkce = true, + ProtocolType = "oidc", + AllowedGrantTypes = new List + { + new() { GrantType = "authorization_code" }, + }, + AllowedScopes = new List + { + new() { Scope = "openid" }, + }, + RedirectUris = new List + { + new() { RedirectUri = "https://app.example.com/cb" }, + }, + }); + db.SaveChanges(); + } + + private int TargetClientDbId() + { + using var scope = _factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + return db.Clients.Single(c => c.ClientId == TargetClientId).Id; + } + + private HttpClient CreateAdminClient() + { + // WebApplicationFactory.CreateClient() returns an HttpClient wired + // directly to the in-memory test server — no Kestrel socket, no + // self-signed cert, no IServerAddressesFeature lookup. + var http = _factory.CreateClient(new WebApplicationFactoryClientOptions + { + HandleCookies = false, + }); + http.DefaultRequestHeaders.Add(TestAuthPolicyProvider.HeaderName, TestAuthPolicyProvider.AdminRole); + return http; + } + + [Fact] + public async Task Edit_GET_returns_200_for_admin() + { + var http = CreateAdminClient(); + var id = TargetClientDbId(); + var resp = await http.GetAsync($"/Client/Edit/{id}", TestContext.Current.CancellationToken); + Assert.Equal(HttpStatusCode.OK, resp.StatusCode); + var body = await resp.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + Assert.Contains(TargetClientId, body); + } + + [Fact] + public async Task EditRedirectUris_GET_returns_200_and_lists_seeded_uri() + { + var http = CreateAdminClient(); + var id = TargetClientDbId(); + var resp = await http.GetAsync($"/Client/EditRedirectUris/{id}", TestContext.Current.CancellationToken); + Assert.Equal(HttpStatusCode.OK, resp.StatusCode); + var body = await resp.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + Assert.Contains("https://app.example.com/cb", body); + } + + [Fact] + public async Task AddRedirectUri_POST_appends_to_database() + { + var http = CreateAdminClient(); + var id = TargetClientDbId(); + const string newUri = "https://app.example.com/cb2"; + + // Read the GET page first to grab the antiforgery token attached + // to the Add form on EditRedirectUris. + var pageResp = await http.GetAsync($"/Client/EditRedirectUris/{id}", TestContext.Current.CancellationToken); + pageResp.EnsureSuccessStatusCode(); + var pageHtml = await pageResp.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + var token = ExtractAntiforgeryToken(pageHtml); + Assert.False(string.IsNullOrEmpty(token)); + + var form = new MultipartFormDataContent + { + { new StringContent(newUri), "redirectUri" }, + { new StringContent(token!), "__RequestVerificationToken" }, + }; + var postResp = await http.PostAsync($"/Client/AddRedirectUri", form, TestContext.Current.CancellationToken); + Assert.Equal(HttpStatusCode.Redirect, postResp.StatusCode); + + // Verify in DB. + using var scope = _factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var client = db.Clients + .Include(c => c.RedirectUris) + .Single(c => c.ClientId == TargetClientId); + Assert.Contains(client.RedirectUris, r => r.RedirectUri == newUri); + + // Cleanup so the test is idempotent across runs. + var toRemove = client.RedirectUris.First(r => r.RedirectUri == newUri); + db.ClientRedirectUris.Remove(toRemove); + db.SaveChanges(); + } + + [Fact] + public async Task RemoveRedirectUri_POST_with_foreign_rowId_returns_NotFound() + { + // SECURITY: a Remove call for a row that belongs to a different + // client must be rejected. The test creates a second client, + // gets a real rowId from it, then calls Remove on the target + // client with that foreign rowId. Expected: 404, target + // client unchanged. + using (var scope = _factory.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + if (!db.Clients.Any(c => c.ClientId == "collection-tests-other")) + { + db.Clients.Add(new Client + { + ClientId = "collection-tests-other", + ClientName = "Other target", + Enabled = true, + ProtocolType = "oidc", + RequireClientSecret = false, + RequirePkce = true, + }); + db.SaveChanges(); + } + } + + int otherRowId; + using (var scope = _factory.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + var other = db.Clients.Include(c => c.RedirectUris) + .Single(c => c.ClientId == "collection-tests-other"); + if (!other.RedirectUris.Any()) + { + other.RedirectUris.Add(new ClientRedirectUri { RedirectUri = "https://other.example/cb" }); + db.SaveChanges(); + } + otherRowId = other.RedirectUris.First().Id; + } + + var http = CreateAdminClient(); + var targetId = TargetClientDbId(); + var pageResp = await http.GetAsync($"/Client/EditRedirectUris/{targetId}", TestContext.Current.CancellationToken); + var token = ExtractAntiforgeryToken(await pageResp.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)); + + var form = new MultipartFormDataContent + { + { new StringContent(targetId.ToString()), "id" }, + { new StringContent(otherRowId.ToString()), "rowId" }, + { new StringContent(token!), "__RequestVerificationToken" }, + }; + var resp = await http.PostAsync($"/Client/RemoveRedirectUri", form, + TestContext.Current.CancellationToken); + Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode); + } + + private static string? ExtractAntiforgeryToken(string html) + { + const string marker = "name=\"__RequestVerificationToken\""; + var idx = html.IndexOf(marker, StringComparison.Ordinal); + if (idx < 0) return null; + var valueStart = html.IndexOf("value=\"", idx, StringComparison.Ordinal); + if (valueStart < 0) return null; + valueStart += "value=\"".Length; + var valueEnd = html.IndexOf('"', valueStart); + return valueEnd < 0 ? null : html[valueStart..valueEnd]; + } +} diff --git a/src/Yavsc.Org.Tests/Directory.Packages.props b/src/Yavsc.Org.Tests/Directory.Packages.props index 1e349ac9..56d90fd7 100644 --- a/src/Yavsc.Org.Tests/Directory.Packages.props +++ b/src/Yavsc.Org.Tests/Directory.Packages.props @@ -5,6 +5,7 @@ + diff --git a/src/Yavsc.Org.Tests/StaticAssetsPathsTests.cs b/src/Yavsc.Org.Tests/StaticAssetsPathsTests.cs new file mode 100644 index 00000000..01962268 --- /dev/null +++ b/src/Yavsc.Org.Tests/StaticAssetsPathsTests.cs @@ -0,0 +1,62 @@ +using System.IO; +using Xunit; +using Xunit.v3; + +namespace Yavsc.Org.Tests; + +/// +/// Diagnostic-only test that confirms the static-assets manifests +/// the WebServerFixture relies on are present in the test bin. +/// +/// The MSBuild target CopyYavscOrgStaticAssets in +/// Yavsc.Org.Tests.csproj mirrors the Yavsc.Org static-assets +/// manifests (Yavsc.Org.staticwebassets.endpoints.json and +/// Yavsc.Org.staticwebassets.runtime.json) from +/// ../Yavsc.Org/bin/$(Configuration)/$(TargetFramework)/ into +/// the test output directory. The fixture then calls +/// _app.MapStaticAssets(testRuntimeManifest) with the +/// runtime.json path so the test host resolves the manifest +/// explicitly by file location, bypassing the default +/// {AssemblyName}.staticwebassets.* lookup (which would look for +/// Yavsc.Org.Tests.staticwebassets.*, files we don't produce). +/// +/// This test is a guard for that build-time copy: if it ever fails +/// or is removed, the WebServerFixture will throw +/// "The static resources manifest file ... was not found" at boot. +/// Once the WebServerFixture is stable in CI, this test can be +/// deleted. +/// +public class StaticAssetsPathsTests +{ + private readonly ITestOutputHelper _output; + + public StaticAssetsPathsTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void YavscOrg_staticwebassets_manifests_are_mirrored_into_test_bin() + { + var testBin = AppContext.BaseDirectory; + _output.WriteLine($"Test bin (AppContext.BaseDirectory) = {testBin}"); + + var runtimeManifest = Path.Combine(testBin, + "Yavsc.Org.staticwebassets.runtime.json"); + var endpointsManifest = Path.Combine(testBin, + "Yavsc.Org.staticwebassets.endpoints.json"); + + _output.WriteLine($"runtime manifest: exists = {File.Exists(runtimeManifest)}, path = {runtimeManifest}"); + _output.WriteLine($"endpoints manifest: exists = {File.Exists(endpointsManifest)}, path = {endpointsManifest}"); + + Assert.True(File.Exists(runtimeManifest), + $"Expected {runtimeManifest} to be copied by CopyYavscOrgStaticAssets, but it is missing. " + + "Check that the Yavsc.Org project builds before Yavsc.Org.Tests so the source manifest exists " + + "and that the in Yavsc.Org.Tests.csproj is wired up correctly."); + + Assert.True(File.Exists(endpointsManifest), + $"Expected {endpointsManifest} to be copied by CopyYavscOrgStaticAssets, but it is missing. " + + "The runtime manifest alone is not enough — MapStaticAssets() may also read the endpoints " + + "manifest depending on the SDK version."); + } +} diff --git a/src/Yavsc.Org.Tests/TestAuthPolicyProvider.cs b/src/Yavsc.Org.Tests/TestAuthPolicyProvider.cs new file mode 100644 index 00000000..1391a3a2 --- /dev/null +++ b/src/Yavsc.Org.Tests/TestAuthPolicyProvider.cs @@ -0,0 +1,70 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Options; + +namespace Yavsc.Org.Tests; + +/// +/// Authorization policy provider used by integration tests. Replaces the +/// production provider in the WebApplicationFactory so that any +/// policy-protected controller can be exercised by sending a +/// X-Test-Role: Administrator header — no login roundtrip, no +/// cookie, no database user. +/// +/// The role names accepted in the header are the same as the +/// production . Any +/// policy that requires one of those roles short-circuits to success +/// when the matching header is present; otherwise the production +/// policy is preserved. +/// +public sealed class TestAuthPolicyProvider : IAuthorizationPolicyProvider +{ + public const string HeaderName = "X-Test-Role"; + public const string AdminRole = "Administrator"; + + private readonly DefaultAuthorizationPolicyProvider _fallback; + + public TestAuthPolicyProvider(IOptions options) + { + _fallback = new DefaultAuthorizationPolicyProvider(options); + } + + public Task GetDefaultPolicyAsync() => _fallback.GetDefaultPolicyAsync(); + + public Task GetFallbackPolicyAsync() => _fallback.GetFallbackPolicyAsync(); + + public async Task GetPolicyAsync(string policyName) + { + var policy = await _fallback.GetPolicyAsync(policyName); + if (policy is null) return null; + return new AuthorizationPolicyBuilder() + .RequireAssertion(ctx => + { + // ASP.NET Core sets ctx.Resource to the HttpContext when + // the authorization middleware invokes the policy. Use + // the request headers directly to honour X-Test-Role. + var http = ctx.Resource as Microsoft.AspNetCore.Http.HttpContext; + if (http is null) return false; + var role = http.Request.Headers[HeaderName].ToString(); + if (string.IsNullOrEmpty(role)) return false; + // The test does not perform a real login, so the + // authenticated user has no claims. Attach an + // in-memory identity carrying the role claim to the + // HttpContext (ctx.User is read-only) so the + // production policy's claim requirement is satisfied. + if (http.User.Identity is null || !http.User.Identity.IsAuthenticated) + { + var identity = new System.Security.Claims.ClaimsIdentity( + new[] + { + new System.Security.Claims.Claim( + "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", + role), + }, + authenticationType: "TestAuth"); + http.User = new System.Security.Claims.ClaimsPrincipal(identity); + } + return true; + }) + .Build(); + } +} diff --git a/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs b/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs new file mode 100644 index 00000000..a061e0d2 --- /dev/null +++ b/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; + +namespace Yavsc.Org.Tests; + +/// +/// WebApplicationFactory-based fixture for integration tests that need +/// to override services registered by the production Program. +/// Uses the in-memory so tests can hit real +/// HTTP endpoints without sockets or self-signed certificates. +/// +/// Currently overrides so that +/// [Authorize("AdministratorOnly")] (and any other policy +/// requiring a role) is satisfied by sending an +/// X-Test-Role: Administrator header, without a real login. +/// +public class TestWebApplicationFactory : WebApplicationFactory +{ + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + // UseDevelopmentEnvironment triggers the dev signing credential + // path in the production startup, so we don't need a real cert + // to satisfy IdentityServer at boot. + builder.UseEnvironment("Development"); + + builder.ConfigureTestServices(services => + { + // Replace the production IAuthorizationPolicyProvider with + // the test one. The default registered by AddAuthorization + // becomes irrelevant: any GetPolicyAsync call is routed here. + services.AddSingleton(); + }); + } +} diff --git a/src/Yavsc.Org.Tests/WebServerFixture.cs b/src/Yavsc.Org.Tests/WebServerFixture.cs index 099d1e0a..fd2af225 100644 --- a/src/Yavsc.Org.Tests/WebServerFixture.cs +++ b/src/Yavsc.Org.Tests/WebServerFixture.cs @@ -1,6 +1,7 @@ using IdentityServer8.EntityFramework.Entities; using IdentityServer8.Models; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; @@ -70,7 +71,7 @@ namespace Yavsc.Org.Tests _instanceCount++; if (!_isInitialized) { - + SetupHost().Wait(); _isInitialized = true; } @@ -116,11 +117,18 @@ namespace Yavsc.Org.Tests TestingUserPassword = _sharedTestingUserPassword; TestingUserEmail = _sharedTestingUserEmail; } - + public async Task SetupHost() { var builder = WebApplication.CreateBuilder(); - + + // WebApplication.CreateBuilder defaults WebRootPath to + // {ContentRoot}/wwwroot. The test assembly runs from + // src/Yavsc.Org.Tests/bin/.../, which has no wwwroot of + // its own — so point the host at the Yavsc.Org project's + // wwwroot so that ConfigurePipeline's + + builder.AddConfiguration(null).AddInMemoryCollection(new Dictionary { [$"ConnectionStrings:{YavscConstants.YavscConnectionStringName}"] = "InMemory" @@ -137,7 +145,30 @@ namespace Yavsc.Org.Tests Configuration = builder.Configuration; + // Swap the production authorization policy provider for + // TestAuthPolicyProvider BEFORE ConfigureWebAppServices + // runs. ConfigureWebAppServices calls builder.Build() at + // the end, which freezes the service collection. Tests + // can satisfy [Authorize("AdministratorOnly")] (and any + // other policy that requires a role) by sending an + // X-Test-Role header; the production policy is replaced + // by the test one via the last-write-wins semantics of + // IServiceCollection.AddSingleton. + builder.Services.AddSingleton(); + _app = builder.ConfigureWebAppServices(); + + // The MSBuild target CopyYavscOrgStaticAssets in + // Yavsc.Org.Tests.csproj mirrors the Yavsc.Org static + // assets manifest into the test bin directory. Call + // MapStaticAssets() with the explicit path so the test + // host resolves the manifest by file location rather + // than by {AssemblyName}.staticwebassets.* convention + // (which would look for Yavsc.Org.Tests.staticwebassets.*, + // a file we don't produce). + var testRuntimeManifest = Path.Combine(AppContext.BaseDirectory, + "Yavsc.Org.staticwebassets.runtime.json"); + Services = _app.Services; SiteSettings = _app.Services.GetRequiredService>().Value; @@ -160,8 +191,8 @@ namespace Yavsc.Org.Tests var testScope = db.ApiScopes.FirstOrDefault(s => s.Name == "test"); if (testScope == null) { - db.ApiScopes.Add(new IdentityServer8.EntityFramework.Entities.ApiScope - { + db.ApiScopes.Add(new IdentityServer8.EntityFramework.Entities.ApiScope + { Name = "test", Enabled = true, DisplayName = "Test API Scope", @@ -172,7 +203,7 @@ namespace Yavsc.Org.Tests new IdentityServer8.EntityFramework.Entities.ApiScopeClaim { Type = "email" } } }); - + // Add a basic API resource for the test scope var apiResource = new IdentityServer8.EntityFramework.Entities.ApiResource { @@ -193,8 +224,8 @@ namespace Yavsc.Org.Tests } - - _app = await _app.ConfigurePipeline(); + + _app = await _app.ConfigurePipeline(testRuntimeManifest); await _app.StartAsync(); diff --git a/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj b/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj index f2e137fe..f2106a88 100644 --- a/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj +++ b/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj @@ -8,6 +8,7 @@ 78a4efec-68dc-4745-ba06-d8545ef9ee91 true exe + $(MSBuildProjectDirectory)\test.runsettings @@ -18,13 +19,14 @@ + - + @@ -48,4 +50,25 @@ + + + + <_YavscOrgStaticAssetsDir>$(MSBuildProjectDirectory)\..\Yavsc.Org\bin\$(Configuration)\$(TargetFramework) + + + <_YavscOrgStaticAssetsFiles Include="$(_YavscOrgStaticAssetsDir)\Yavsc.Org.staticwebassets.*.json" /> + + + diff --git a/src/Yavsc.Org.Tests/test.runsettings b/src/Yavsc.Org.Tests/test.runsettings new file mode 100644 index 00000000..72467039 --- /dev/null +++ b/src/Yavsc.Org.Tests/test.runsettings @@ -0,0 +1,42 @@ + + + + + + + + + + quiet + + + + + Yavsc.Org.tests.trx + + + + + Yavsc.Org.tests.html + + + + + + + + + + + + + + + + + + + diff --git a/src/Yavsc.Org/Controllers/Administration/ClientController.Collections.cs b/src/Yavsc.Org/Controllers/Administration/ClientController.Collections.cs index 0c5d2558..b31ec0c3 100644 --- a/src/Yavsc.Org/Controllers/Administration/ClientController.Collections.cs +++ b/src/Yavsc.Org/Controllers/Administration/ClientController.Collections.cs @@ -27,12 +27,8 @@ public partial class ClientController readonly IHtmlLocalizer _localizer; - public ClientController( - IHtmlLocalizer localizer - ) - { - _localizer = localizer; - } + // ClientController has a single constructor declared in + // ClientController.cs; this partial shares its fields. [HttpGet] diff --git a/src/Yavsc.Org/Controllers/Administration/ClientController.cs b/src/Yavsc.Org/Controllers/Administration/ClientController.cs index 2e4a4b73..ac3dc326 100644 --- a/src/Yavsc.Org/Controllers/Administration/ClientController.cs +++ b/src/Yavsc.Org/Controllers/Administration/ClientController.cs @@ -3,6 +3,7 @@ using IdentityServer8.EntityFramework.Entities; using IdentityServer8.EntityFramework.Stores; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Localization; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; @@ -21,7 +22,8 @@ namespace Yavsc.Controllers public ClientController( ApplicationDbContext dbContext, - ClientStore clientStore, IOptions siteSettingsOptions + ClientStore clientStore, IOptions siteSettingsOptions, + IHtmlLocalizer localizer ) @@ -29,6 +31,7 @@ namespace Yavsc.Controllers this.dbContext = dbContext; this.clientStore = clientStore; this.siteSettings = siteSettingsOptions.Value; + _localizer = localizer; } // GET: Client diff --git a/src/Yavsc.Org/Extensions/HostingExtensions.cs b/src/Yavsc.Org/Extensions/HostingExtensions.cs index a439b7f1..9ba36563 100644 --- a/src/Yavsc.Org/Extensions/HostingExtensions.cs +++ b/src/Yavsc.Org/Extensions/HostingExtensions.cs @@ -101,7 +101,7 @@ public static class HostingExtensions services.AddTransient() .AddTransient(); - + services.AddTransient() .AddTransient() @@ -143,7 +143,7 @@ public static class HostingExtensions { IServiceCollection services = builder.Services; var connectionString = builder.Configuration.GetConnectionString(YavscConstants.YavscConnectionStringName); - + services.AddDbContext(options => { if (UsesInMemoryProvider(connectionString)) @@ -284,7 +284,7 @@ public static class HostingExtensions }); var migrationsAssembly = typeof(Program).GetTypeInfo().Assembly.GetName().Name; var connectionString = builder.Configuration.GetConnectionString(YavscConstants.YavscConnectionStringName); - + string sqliteConnectionString = $"Data Source={Path.Combine(Path.GetTempPath(), "yavsc_test.db")}"; var identityServerBuilder = builder.Services.AddIdentityServer(options => @@ -334,10 +334,18 @@ public static class HostingExtensions sql => sql.MigrationsAssembly(migrationsAssembly)); } }; - + }); - if (builder.Environment.IsDevelopment()) + // Skip the production signing-cert requirement when running with + // an in-memory database (test fixtures) or in the Development + // environment. In those cases IdentityServer8 falls back to + // AddDeveloperSigningCredential which mints an ephemeral key + // at startup; signing real tokens against it would fail, but + // the test fixtures only use the discovery/JWKS endpoints. + var useDevSigning = builder.Environment.IsDevelopment() + || UsesInMemoryProvider(connectionString); + if (useDevSigning) { identityServerBuilder.AddDeveloperSigningCredential(); } @@ -774,7 +782,7 @@ public static class HostingExtensions } - public async static Task ConfigurePipeline(this WebApplication app) + public async static Task ConfigurePipeline(this WebApplication app, string staticAssetsManifestPath=null) { ILoggerFactory loggerFactory = app.Services.GetRequiredService(); var logger = loggerFactory.CreateLogger(); @@ -815,8 +823,8 @@ public static class HostingExtensions app.UseIdentityServer(); app.UseAuthorization(); app.UseCors("default"); - app.MapStaticAssets(); app.MapDefaultControllerRoute(); + app.MapStaticAssets(staticAssetsManifestPath); //app.MapRazorPages(); app.MapHub("/chatHub");