Identity testing must prove more than a successful password. Delivery evidence comes from failure branches, tenant isolation, ticket replay, client-platform token isolation, adapter replacement, and production alerts.
1. Test layers
Higher layers are closer to delivery and slower. Lower layers enumerate state; upper layers prove that composition and infrastructure preserve that state.
2. Aggregate and pure-policy tests
| Test class | Focus |
|---|---|
UserAggregateTests | Security-stamp rotation, lockout, enable/disable, activation, Host semantics |
OrganizationUnitAggregateTests | Path, move, descendant, enable, and ordering |
PlatformTenantLifecycleTests | Provisioning steps, completion, suspension, terminal state |
TenantTransitionGuardTests | Every permitted edge, same-state idempotency, invalid code |
PasswordHasherTests | BCrypt verification and rehash detection |
PasswordValidatorTests | Independent failure for every complexity rule |
| FIDO2 / MFA tests | Credential clone detection, cache, and risk policy |
Use a fixed DateTimeOffset, not wall-clock time. Assert error code and error type as the stable contract; localized message text is not stable.
3. Deep login-module tests
LoginFlowTests drives ILoginFlow directly and currently covers:
- Normal login and tokens.
- User 2FA and risk MFA.
- Fail closed when MFA challenge issuance fails.
- No success when refresh-token storage fails.
- Refresh-token revocation when session storage fails.
- Captcha first challenge, correct answer, missing answer, generation and verification failure.
- Risk Block before password.
- Wrong password, absent user, locked, disabled, and timed unlock.
- Current fail-open behavior when the risk engine fails.
- Forced password change, cross-tenant email lookup, and cancellation.
Run the group:
# FullyQualifiedName keeps this run on the Identity deep module.dotnet test tests/BitzOrcas.Application.Tests/BitzOrcas.Application.Tests.csproj \ --configuration Release \ --no-restore \ --filter 'FullyQualifiedName~Identity.LoginFlowTests|FullyQualifiedName~Identity.LoginRiskIntegrationTests|FullyQualifiedName~Identity.VerifyMfaLoginCompletionTests'When issuance order changes, add a test where one step succeeds and the next fails. Happy-path tests cannot prove that a valid refresh token is not leaked.
4. Account-admission tests
AccountAdmissionFlowTests is a continuous business case, not unrelated mocked handlers. It covers:
- Directed invitation accepts only its target email.
- Open link enforces email domain, usage, and expiry.
- Approval creates PendingActivation; activation enables login.
- Default roles apply, and invalid roles reject approval.
- A rejected admission cannot activate.
- Missing and replayed tokens return generic security errors.
After adding a transaction adapter, prove against a real database that a failed accept cannot split Admission from UsedCount, and that activation-token deletion failure cannot leave both an Active user and a replayable token.
5. Client-platform token isolation
PlatformTokenIsolationTests uses real JwtTokenService with in-memory stores and pins three contracts:
- Refreshing Web does not change the App slot.
- Refreshing App does not change the Web slot.
- The new
RefreshTokenRecordinherits the original platform.
# Use the real JwtTokenService to pin claims and Web/App isolation.dotnet test tests/BitzOrcas.Unit.Tests/BitzOrcas.Unit.Tests.csproj \ --configuration Release \ --no-restore \ --filter 'FullyQualifiedName~Identity.TokenServiceClaimsTests|FullyQualifiedName~Identity.PlatformTokenIsolationTests'Run the same behavior against production stores. Dictionary isolation does not prove ORM unique keys and filters.
6. Persistence and both ORMs
Identity combines unified aggregate repositories with specialized query stores. Persistence tests need at least:
| Contract | SqlSugar | EF Core |
|---|---|---|
| User TenantId filter | Required | Required |
| Global Host user lookup | Required | Required |
| Invitation TokenHash lookup | Required | Required |
| Refresh-token platform and hash index | Required | Required |
| OU tree and pagination | Required | Required |
| Admission, User, and Token transaction | Required | Required |
Existing entries include ProductionIdentityStoreTests, RepositoryIdentityStoreTests, IdentityQueryHandlerTests, and read-model adapter parity. A new store must test cross-tenant denial and hidden soft-deleted data, not only save and load.
7. API Shell boundary
Without production database, API Shell uses fail-closed default stores but routes still start. API tests prove:
- Invitation acceptance and activation are not 401; unavailable store or invalid ticket fails for the correct reason.
- Invitation management is 401 without authentication.
- Login/Refresh are anonymous; Me/Logout require authentication.
- SmartEnum JSON writes names and reads names or values.
- Result/Error maps consistently to Problem Details.
# Shell needs no production database but must preserve public and management auth boundaries.dotnet test tests/BitzOrcas.Integration.Tests/BitzOrcas.Integration.Tests.csproj \ --configuration Release \ --no-restore \ --filter 'FullyQualifiedName~ApiShellTests'A public endpoint being “not 401” does not mean it may succeed. Missing stores, invalid tickets, and untrusted tenants still fail closed.
8. Coverage of the production adapter gate
ProductionAdapterReadinessGuard currently blocks NullUnitOfWork, InMemoryTenantStore, InMemoryApiClientStore, and other defaults in Production/Staging. It also checks Redis, file storage, notification publisher, Feature, and critical Webhook ports.
It does not yet cover every Identity email/SMS delivery port, IRefreshTokenStore, federation provider, MFA configuration store, or distributed challenge cache. Identity GA therefore needs supplemental probes; a Host that passes the common guard is not complete evidence.
9. Observability signals
| Signal | Dimensions | Suggested alert |
|---|---|---|
| Login failure rate | Tenant, provider, reason; no plaintext user name | Baseline spike and brute-force pattern |
| Risk degradation | Risk error and instance | Alert on sustained growth |
| Captcha generation/verification | Provider and stage | Separate provider failure from attack traffic |
| MFA challenge failure | Method and cache implementation | Cross-instance failure |
| Refresh-token reuse | Tenant, hashed UserId, platform | High-priority security alert |
| Session compensation | Store error code | Risk after token issuance |
| Activation notification | Channel, provider, error class | Onboarding blocked |
| Invalid tenant transition | Current and target state | Bad operation script or concurrency |
Logs may use UserId or token hash as diagnostic identifiers, but never password, raw token, full phone, JWT, refresh token, or connection string.
10. Login troubleshooting
InvalidCredentials
Check tenant resolution, Host/email/tenant-local lookup, account state, and password verification in order. Keep the public error generic and correlate internal sanitized login logs by CorrelationId.
Captcha repeats forever
Confirm stage two returns the original captchaChallengeId and answer; check whether another attempt consumed the ticket; confirm proxy behavior did not change IP, tenant, or device context.
Correct password but no token
Inspect requiresMfa, requiresCaptcha, TwoFactorEnabled, and risk challenge. Then inspect role lookup, refresh-token store, and session-store failures.
Refreshing one client signs out another
Inspect names RefreshToken:Web and RefreshToken:App, confirm the store key includes Name, and verify rotated records preserve Platform.
11. Invitation and activation troubleshooting
| Symptom | First checks |
|---|---|
| Every invitation is Invalid | Effective tenant, token hash, clock, store query |
| Directed email mismatch | Normalization and submitted address |
| No mail after approval | Final IEmailDeliveryPort, frontend URL, provider receipt |
| Activation always Invalid | 72h expiry, token encoding, UserToken Provider/Name |
| Approval leaves partial user | UoW, role write, notification failure, recovery command |
| Replay succeeds | Token deletion, Admission consume, concurrency uniqueness |
Preserve audit evidence before repairing data. Do not manually set a user Active and skip Admission or token state. Recovery should run through an audited use case or one-time operations command.
12. Security incident response
Suspected refresh-token theft
- Locate
RefreshTokenRecordand family by token hash. - Revoke the family and related sessions.
- Require reauthentication or password reset based on risk.
- Check client platforms separately before widening impact.
- Preserve platform, IP, DeviceId, time, and CorrelationId in sanitized audit.
Leaked invitation link
- Revoke Invitation and preserve actor and time.
- Find admissions created from it and review their state.
- Reject suspicious admissions; treat already active users as account incidents.
- Create a new token; the old raw token cannot be recovered.
Leaked JWT signing key
- Activate a new Kid and stop old-key issuance.
- Shorten or remove old-key validation grace according to severity.
- Revoke refresh tokens and high-risk sessions.
- Verify every instance loaded the same key ring.
- Record rotation evidence and investigate the secret source.
13. Full pre-release commands
# Restore and build from one Release input to pin dependencies and output.dotnet restore BitzOrcas.Modern.slnx --locked-modedotnet build BitzOrcas.Modern.slnx --configuration Release --no-restore
# Run Identity tests by layer so a failure identifies its responsible boundary.dotnet test tests/BitzOrcas.Unit.Tests/BitzOrcas.Unit.Tests.csproj \ --configuration Release --no-build --no-restore \ --filter 'FullyQualifiedName~Identity|FullyQualifiedName~Platform.Tenancy'
dotnet test tests/BitzOrcas.Application.Tests/BitzOrcas.Application.Tests.csproj \ --configuration Release --no-build --no-restore \ --filter 'FullyQualifiedName~Identity'
dotnet test tests/BitzOrcas.Architecture.Tests/BitzOrcas.Architecture.Tests.csproj \ --configuration Release --no-build --no-restore \ --filter 'FullyQualifiedName~Identity'Whether Integration Tests can use --no-build depends on the solution build output. CI should use the repository’s established test entry.
14. Global sweeps and expectations
# Find copied login orchestration or direct password processing in a Host.rg -n "VerifyPasswordFailed|DummyPasswordHash|IssueMfaChallenge" \ src/Hosts src/Platform/Identity -g '*.cs'
# Matches outside LoginFlow require explanation and must not form a second password-login state machine.
# Unified aggregates should have no one-to-one entity/mapper copies.find src/Platform/Identity -type f \ \( -name 'UserEntity.cs' -o -name 'AccountInvitationEntity.cs' -o -name 'PlatformTenantEntity.cs' \)
# Expected: no output unless an asymmetric exception is recorded under docs/architecture.15. GA sign-off
- Every layer passes with real ORM, messaging, cache, and notification adapters in integration tests.
- Risk fail open is an explicit product and security decision.
- Password-change session revocation has a clear product decision, matching tests and UI.
- Notification, federation, MFA, and refresh-token stores are observable concrete implementations.
- Cross-tenant, Host, client-platform, ticket replay, and terminal-state negatives are covered.
- Key leak, token theft, invitation leak, and partial approval have rehearsed procedures.
Identity overview · Configuration · Production security checklist