From 8fdd56d33a6cb3814c918bfedd6f68a2d731aef4 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Thu, 25 Jun 2026 23:27:47 +0100 Subject: [PATCH] Test the seed --- .../NonRegression/OidcSeedTests.cs | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 src/Yavsc.Org.Tests/NonRegression/OidcSeedTests.cs diff --git a/src/Yavsc.Org.Tests/NonRegression/OidcSeedTests.cs b/src/Yavsc.Org.Tests/NonRegression/OidcSeedTests.cs new file mode 100644 index 00000000..084559f2 --- /dev/null +++ b/src/Yavsc.Org.Tests/NonRegression/OidcSeedTests.cs @@ -0,0 +1,101 @@ +using Xunit; + +namespace Yavsc +{ + /// + /// Non-regression tests for the IdentityServer8 configuration seed. + /// Today (2026-06-25) we hit a runtime crash on prod because the + /// seed attempted to insert 'openid' / 'profile' both as + /// IdentityResources and as ApiScopes: + /// + /// Found identity scopes and API scopes that use the same names. + /// This is an invalid configuration. Use different names for + /// identity scopes and API scopes. Scopes found: openid, profile + /// + /// These tests pin the invariants that prevent that mistake from + /// regressing: the application-defined scope list must never + /// contain names that collide with identity scopes, every entry + /// must reference an ApiResource, and the seed's translation to + /// SQL must stay valid. + /// + [Trait("regression", "OIDC seed")] + public class OidcSeedTests + { + // Identity scopes that IdentityServer8 manages on its own. They + // must never appear in Constants.BuildInApiScopes nor in + // Constants.ApiResourcesScopes — an ApiScope that shares its + // Name with an IdentityResource prevents the host from starting. + private static readonly string[] ReservedIdentityScopeNames = + { + "openid", + "profile", + "offline_access" + }; + + [Fact] + public void BuildInApiScopes_DoesNotContainIdentityScopes() + { + foreach (var reserved in ReservedIdentityScopeNames) + { + Assert.DoesNotContain(reserved, Constants.BuildInApiScopes); + } + } + + [Fact] + public void ApiResourcesScopes_DoesNotContainIdentityScopes() + { + foreach (var reserved in ReservedIdentityScopeNames) + { + foreach (var spec in Constants.ApiResourcesScopes) + { + Assert.NotEqual(reserved, spec.ScopeName); + } + } + } + + [Fact] + public void ApiResourcesScopes_EveryEntryReferencesAUniqueResource() + { + // Same ScopeName appearing twice would mean two ApiScopes + // (and two ApiResourceScopes rows) trying to claim the same + // name — IdentityServer8 would reject that on startup. + var scopeNames = Constants.ApiResourcesScopes + .Select(s => s.ScopeName) + .ToList(); + Assert.Equal(scopeNames.Count, scopeNames.Distinct().Count()); + + // Same ResourceName appearing twice would mean two + // ApiResources rows with the same Name — the EF seed would + // crash on the unique index. + var resourceNames = Constants.ApiResourcesScopes + .Select(s => s.ResourceName) + .ToList(); + Assert.Equal(resourceNames.Count, resourceNames.Distinct().Count()); + } + + [Fact] + public void ApiResourcesScopes_EveryEntryHasNonEmptyStrings() + { + // An empty ScopeName or ResourceName silently produces an + // ApiScope / ApiResource row with no key, which breaks the + // scope validator's string-based lookup later on. + foreach (var spec in Constants.ApiResourcesScopes) + { + Assert.False(string.IsNullOrWhiteSpace(spec.ScopeName), + $"ScopeName must be set (got '{spec.ScopeName}')"); + Assert.False(string.IsNullOrWhiteSpace(spec.ResourceName), + $"ResourceName must be set (got '{spec.ResourceName}')"); + Assert.False(string.IsNullOrWhiteSpace(spec.ResourceDisplayName), + $"ResourceDisplayName must be set (got '{spec.ResourceDisplayName}')"); + } + } + + [Fact] + public void ApiResourcesScopes_IsNotEmpty() + { + // Belt and braces: if someone ever wipes the list thinking + // it's stale, this test forces them to be explicit about it. + Assert.NotEmpty(Constants.ApiResourcesScopes); + } + } +} \ No newline at end of file