diff --git a/.env.example b/.env.example
new file mode 100644
index 00000000..21644aad
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,18 @@
+# Copy to .env to override defaults before running `docker compose up`.
+# .env is gitignored — never commit real credentials.
+
+# Passwords for the seeded accounts. Both are OPTIONAL: when unset they fall back
+# to the compiled-in defaults (Admin123! / User123!), which are what the login
+# page's quick-login buttons use.
+#
+# Those defaults are published in this repository — set these before exposing the
+# deployment to anyone else, and turn off the "Show Test Accounts" setting.
+SEED_ADMIN_PASSWORD=
+SEED_USER_PASSWORD=
+
+# PostgreSQL password for the bundled database container.
+# Defaults to "simplemodule" when unset — override for anything exposed.
+POSTGRES_PASSWORD=simplemodule
+
+# Public base URL, used by OpenIddict to register redirect URIs.
+APP_BASE_URL=http://localhost:8080
diff --git a/.gitignore b/.gitignore
index f3c169dd..6f1bff6d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -451,3 +451,9 @@ baseline/
# Verification artifacts
.verify/
.qa/
+
+# Local environment files — docker-compose reads SEED_ADMIN_PASSWORD and
+# POSTGRES_PASSWORD from here, so these must never be committed.
+.env
+.env.*
+!.env.example
diff --git a/Dockerfile.worker b/Dockerfile.worker
index 875649fd..1ec6f487 100644
--- a/Dockerfile.worker
+++ b/Dockerfile.worker
@@ -94,7 +94,12 @@ RUN dotnet publish template/SimpleModule.Worker/SimpleModule.Worker.csproj \
--no-restore \
-p:ErrorOnDuplicatePublishOutputFiles=false
-FROM mcr.microsoft.com/dotnet/runtime:10.0 AS runtime
+# The worker hosts the same modules as the api, and every module carries a
+# . The plain runtime
+# image does not ship that shared framework, so the worker started and then died
+# with "No frameworks were found" on every boot. This must stay in sync with the
+# api image in ./Dockerfile.
+FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
RUN apt-get update \
diff --git a/README.md b/README.md
index 6ce88149..70af1485 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,44 @@ dotnet run --project template/SimpleModule.Host # https://localhost:5001
docker compose up # http://localhost:8080 with PostgreSQL 16
```
+#### Seed credentials
+
+Two accounts are seeded on first start, and the login page's quick-login buttons use these exact
+credentials:
+
+| Account | Default password | Override with |
+|---------|------------------|---------------|
+| `admin@simplemodule.dev` | `Admin123!` | `Seed__AdminPassword` |
+| `user@simplemodule.dev` | `User123!` | `Seed__UserPassword` |
+
+The defaults apply in every environment so the app boots and is usable out of the box. Outside
+`Development` and `Testing` the app logs a warning when it falls back to them.
+
+> **These passwords are published in this repository.** Before exposing a deployment to anyone else,
+> set `Seed__AdminPassword` (or change the password after first login) and turn off the
+> **Show Test Accounts** setting, which renders the quick-login buttons and defaults to on.
+
+```bash
+docker build -t simplemodule .
+docker run -e Seed__AdminPassword='' -p 8080:8080 simplemodule
+```
+
+`Seed__AdminPassword` is the environment-variable spelling of the `Seed:AdminPassword` configuration
+key — use the latter in `appsettings.*.json`. Accounts are seeded only when they don't already
+exist, so changing these values later will **not** rotate an existing password; change it from the
+account page instead.
+
+> **Upgrading an existing deployment.** Previously the demo `user@simplemodule.dev` account was
+> skipped outside `Development` and `Testing` when `Seed__UserPassword` was unset, so deployments that set only
+> `Seed__AdminPassword` have no such account today. They will gain one, seeded with `User123!`, on
+> the next restart. Set `Seed__UserPassword`, or delete the account after it appears, if you don't
+> want it. The admin account is unaffected — it already exists, and existing accounts are never
+> re-seeded or rotated.
+
+Note that `Production` additionally requires OpenIddict signing and encryption certificates
+(`OpenIddict__SigningCertificatePath` / `OpenIddict__EncryptionCertificatePath`, PKCS#12), and
+refuses to start without them.
+
### Development
```bash
diff --git a/docker-compose.yml b/docker-compose.yml
index 84d4af01..e4906fc8 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -22,11 +22,12 @@ services:
# combined with seeded credentials it would hand out fully-privileged
# tokens with a single POST /connect/token.
OpenIddict__AllowPasswordGrant: "false"
- # Required: password for the seeded admin account. There is deliberately
- # no default — put SEED_ADMIN_PASSWORD in your .env file.
- Seed__AdminPassword: ${SEED_ADMIN_PASSWORD:?Set SEED_ADMIN_PASSWORD in .env}
- # Optional: password for the seeded demo user (outside Development the
- # demo user is skipped entirely when this is unset).
+ # Optional: passwords for the seeded accounts. When unset, both fall back to
+ # the compiled-in defaults (Admin123! / User123!) so the stack comes up
+ # usable — those are the same credentials the login page's quick-login
+ # buttons use. They are published in this repository, so set these (or
+ # change the passwords after first login) before exposing the deployment.
+ Seed__AdminPassword: ${SEED_ADMIN_PASSWORD:-}
Seed__UserPassword: ${SEED_USER_PASSWORD:-}
# api stays in Producer mode — it enqueues jobs but never runs them.
# All IModuleJob execution lives in the worker service below.
@@ -56,11 +57,10 @@ services:
Database__Provider: PostgreSQL
BackgroundJobs__WorkerMode: Consumer
# The worker runs the same module set as the api, including the user
- # seeder. It must see the SAME seed passwords — otherwise whichever
- # process wins the startup race seeds the admin account, and a worker
- # without these would silently seed the compiled-in default password,
- # defeating the api's required SEED_ADMIN_PASSWORD.
- Seed__AdminPassword: ${SEED_ADMIN_PASSWORD:?Set SEED_ADMIN_PASSWORD in .env}
+ # seeder. It must see the SAME seed passwords as the api — whichever
+ # process wins the startup race is the one that creates the accounts, so a
+ # mismatch here makes the seeded password depend on that race.
+ Seed__AdminPassword: ${SEED_ADMIN_PASSWORD:-}
Seed__UserPassword: ${SEED_USER_PASSWORD:-}
volumes:
- storage_data:/app/storage
diff --git a/modules/AuditLogs/src/SimpleModule.AuditLogs/Pipeline/AuditWriterService.cs b/modules/AuditLogs/src/SimpleModule.AuditLogs/Pipeline/AuditWriterService.cs
index 301f1fba..e10ab378 100644
--- a/modules/AuditLogs/src/SimpleModule.AuditLogs/Pipeline/AuditWriterService.cs
+++ b/modules/AuditLogs/src/SimpleModule.AuditLogs/Pipeline/AuditWriterService.cs
@@ -81,7 +81,18 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
#pragma warning restore CA1031
{
LogFlushError(logger, ex);
- await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
+
+ // This backoff runs inside a catch block, so the enclosing try
+ // cannot observe its cancellation — guard it here or a shutdown
+ // during the backoff escapes ExecuteAsync as a spurious crash.
+ try
+ {
+ await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
}
}
@@ -93,7 +104,21 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
}
if (batch.Count > 0)
{
- await FlushBatchAsync(batch);
+ // The service provider is frequently already disposed by the time a
+ // failed startup unwinds to here, so this final flush can throw. It
+ // runs outside the loop's try, so letting it escape would surface a
+ // normal shutdown as a crashed BackgroundService — the exact noise
+ // this class of fix exists to remove — and skip LogStopped too.
+ try
+ {
+ await FlushBatchAsync(batch);
+ }
+#pragma warning disable CA1031
+ catch (Exception ex)
+#pragma warning restore CA1031
+ {
+ LogFlushError(logger, ex);
+ }
}
LogStopped(logger);
diff --git a/modules/AuditLogs/src/SimpleModule.AuditLogs/Retention/AuditRetentionService.cs b/modules/AuditLogs/src/SimpleModule.AuditLogs/Retention/AuditRetentionService.cs
index a6d5c4e0..05c34496 100644
--- a/modules/AuditLogs/src/SimpleModule.AuditLogs/Retention/AuditRetentionService.cs
+++ b/modules/AuditLogs/src/SimpleModule.AuditLogs/Retention/AuditRetentionService.cs
@@ -16,29 +16,41 @@ ILogger logger
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
- await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
+ try
+ {
+ await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
- var checkInterval = moduleOptions.Value.RetentionCheckInterval;
+ var checkInterval = moduleOptions.Value.RetentionCheckInterval;
- while (!stoppingToken.IsCancellationRequested)
- {
- try
- {
- await RunCleanupAsync(stoppingToken);
- }
- catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
+ while (!stoppingToken.IsCancellationRequested)
{
- break;
- }
+ try
+ {
+ await RunCleanupAsync(stoppingToken);
+ }
+ catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
+ {
+ break;
+ }
#pragma warning disable CA1031
- catch (Exception ex)
+ catch (Exception ex)
#pragma warning restore CA1031
- {
- LogError(logger, ex);
- }
+ {
+ LogError(logger, ex);
+ }
- await Task.Delay(checkInterval, stoppingToken);
+ await Task.Delay(checkInterval, stoppingToken);
+ }
+ }
+ catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
+ {
+ // Normal shutdown while waiting on the startup delay or the interval
+ // delay. Returning quietly keeps this off the BackgroundService failure
+ // path, which would otherwise log a critical "BackgroundService failed"
+ // on top of — and obscuring — whatever actually stopped the host.
}
+
+ LogStopped(logger);
}
private async Task RunCleanupAsync(CancellationToken ct)
@@ -87,4 +99,7 @@ private async Task RunCleanupAsync(CancellationToken ct)
[LoggerMessage(Level = LogLevel.Error, Message = "Retention cleanup failed")]
private static partial void LogError(ILogger logger, Exception ex);
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "AuditRetentionService stopped")]
+ private static partial void LogStopped(ILogger logger);
}
diff --git a/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/Services/ProgressFlushService.cs b/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/Services/ProgressFlushService.cs
index 7adee6fe..50f05911 100644
--- a/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/Services/ProgressFlushService.cs
+++ b/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/Services/ProgressFlushService.cs
@@ -57,7 +57,18 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
#pragma warning restore CA1031
{
LogFlushError(logger, ex);
- await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
+
+ // This backoff runs inside a catch block, so the enclosing try
+ // cannot observe its cancellation — guard it here or a shutdown
+ // during the backoff escapes ExecuteAsync as a spurious crash.
+ try
+ {
+ await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
}
}
@@ -69,7 +80,25 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
}
if (batch.Count > 0)
{
- await FlushBatchAsync(batch, moduleOptions.Value.MaxLogEntries, CancellationToken.None);
+ // The service provider is frequently already disposed by the time a
+ // failed startup unwinds to here, so this final flush can throw. It
+ // runs outside the loop's try, so letting it escape would surface a
+ // normal shutdown as a crashed BackgroundService — the exact noise
+ // this class of fix exists to remove — and skip LogStopped too.
+ try
+ {
+ await FlushBatchAsync(
+ batch,
+ moduleOptions.Value.MaxLogEntries,
+ CancellationToken.None
+ );
+ }
+#pragma warning disable CA1031
+ catch (Exception ex)
+#pragma warning restore CA1031
+ {
+ LogFlushError(logger, ex);
+ }
}
LogStopped(logger);
diff --git a/modules/Users/src/SimpleModule.Users/Services/SeedConfigurationException.cs b/modules/Users/src/SimpleModule.Users/Services/SeedConfigurationException.cs
deleted file mode 100644
index 48665495..00000000
--- a/modules/Users/src/SimpleModule.Users/Services/SeedConfigurationException.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-namespace SimpleModule.Users.Services;
-
-///
-/// Thrown when seeding cannot proceed safely — e.g. a required seed password is
-/// missing outside Development. Unlike transient database errors (which the seed
-/// service logs and tolerates), this exception is allowed to escape
-/// so host startup fails loudly instead
-/// of seeding a publicly known default credential.
-///
-public sealed class SeedConfigurationException : InvalidOperationException
-{
- public SeedConfigurationException() { }
-
- public SeedConfigurationException(string message)
- : base(message) { }
-
- public SeedConfigurationException(string message, Exception innerException)
- : base(message, innerException) { }
-}
diff --git a/modules/Users/src/SimpleModule.Users/Services/SeedPasswordOutcome.cs b/modules/Users/src/SimpleModule.Users/Services/SeedPasswordOutcome.cs
deleted file mode 100644
index 1f1fdec2..00000000
--- a/modules/Users/src/SimpleModule.Users/Services/SeedPasswordOutcome.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-namespace SimpleModule.Users.Services;
-
-///
-/// The decision made by for a
-/// single seed account.
-///
-internal enum SeedPasswordOutcome
-{
- /// Seed the account with the resolved password.
- Seed,
-
- /// Skip the (optional) account — unconfigured in a real deployment.
- Skip,
-
- ///
- /// Fail host startup — a required account is unconfigured in a real
- /// deployment and must not fall back to the compiled-in default.
- ///
- Fail,
-}
diff --git a/modules/Users/src/SimpleModule.Users/Services/UserSeedService.cs b/modules/Users/src/SimpleModule.Users/Services/UserSeedService.cs
index 3b201271..6a8c8e04 100644
--- a/modules/Users/src/SimpleModule.Users/Services/UserSeedService.cs
+++ b/modules/Users/src/SimpleModule.Users/Services/UserSeedService.cs
@@ -44,8 +44,7 @@ await SeedUserAsync(
SeedConstants.AdminDisplayName,
ConfigKeys.SeedAdminPassword,
SeedConstants.DefaultAdminPassword,
- SeedConstants.AdminRole,
- requiredOutsideDevelopment: true
+ SeedConstants.AdminRole
);
await SeedUserAsync(
userManager,
@@ -53,12 +52,11 @@ await SeedUserAsync(
SeedConstants.UserDisplayName,
ConfigKeys.SeedUserPassword,
SeedConstants.DefaultUserPassword,
- SeedConstants.UserRole,
- requiredOutsideDevelopment: false
+ SeedConstants.UserRole
);
}
#pragma warning disable CA1031 // Seed service must not crash the host on database errors
- catch (Exception ex) when (ex is not SeedConfigurationException)
+ catch (Exception ex)
#pragma warning restore CA1031
{
LogSeedError(logger, ex.Message);
@@ -102,32 +100,18 @@ private async Task SeedUserAsync(
string displayName,
string passwordConfigKey,
string defaultPassword,
- string role,
- bool requiredOutsideDevelopment
+ string role
)
{
if (await userManager.FindByEmailAsync(email) is not null)
return;
- var outcome = ResolveSeedPassword(
- configuration[passwordConfigKey],
- defaultPassword,
- environment.IsLocalOrTest(),
- requiredOutsideDevelopment,
- out var password
- );
+ var configuredPassword = configuration[passwordConfigKey];
+ var password = ResolveSeedPassword(configuredPassword, defaultPassword);
- switch (outcome)
+ if (string.IsNullOrEmpty(configuredPassword) && !environment.IsLocalOrTest())
{
- case SeedPasswordOutcome.Fail:
- throw new SeedConfigurationException(
- $"'{passwordConfigKey}' must be configured in the '{environment.EnvironmentName}' "
- + $"environment. Refusing to create '{email}' with the compiled-in default "
- + "password."
- );
- case SeedPasswordOutcome.Skip:
- LogSkippingSeedUser(logger, email, passwordConfigKey);
- return;
+ LogDefaultPasswordWarning(logger, email, passwordConfigKey);
}
LogSeedingUser(logger, email);
@@ -141,9 +125,7 @@ out var password
CreatedAt = DateTime.UtcNow,
};
- // Only SeedPasswordOutcome.Seed falls through the switch above, and it
- // always yields a non-null password.
- var result = await userManager.CreateAsync(user, password!);
+ var result = await userManager.CreateAsync(user, password);
if (result.Succeeded)
{
await userManager.AddToRoleAsync(user, role);
@@ -158,45 +140,28 @@ out var password
}
///
- /// Decides how to seed a user's password. Extracted as a pure function so the
- /// security-critical branching — never seed a real deployment with the
- /// compiled-in default — is unit-testable without an Identity stack.
+ /// Resolves the password to seed an account with: the configured value when one
+ /// is present, otherwise the compiled-in default. Extracted as a pure function
+ /// so the fallback is unit-testable without an Identity stack.
///
+ ///
+ /// Both seeded accounts always fall back to their compiled-in defaults so the
+ /// app boots and is usable out of the box in every environment — the login page
+ /// advertises these same credentials via its quick-login buttons, so seeding
+ /// them is what makes those buttons work. The defaults are published in this
+ /// repository: any deployment that is reachable by anyone else must set the
+ /// Seed:AdminPassword configuration key (or rotate the password after
+ /// first login), and turn off the "Show Test Accounts" system setting
+ /// (auth.show_test_accounts) from the Settings UI — that one is a
+ /// SettingDefinition read through ISettingsContracts, not an
+ /// IConfiguration key, so it cannot be set via an environment variable.
+ ///
/// The password from configuration, if any.
- /// The compiled-in fallback (local/CI only).
- /// True for Development/Testing environments.
- ///
- /// True for the admin account (must fail closed); false for the optional demo
- /// user (skipped when unconfigured in a real deployment).
- ///
- /// The password to use when the outcome is Seed.
- internal static SeedPasswordOutcome ResolveSeedPassword(
+ /// The compiled-in fallback.
+ internal static string ResolveSeedPassword(
string? configuredPassword,
- string defaultPassword,
- bool isLocalOrTest,
- bool requiredOutsideLocal,
- out string? password
- )
- {
- if (!string.IsNullOrEmpty(configuredPassword))
- {
- password = configuredPassword;
- return SeedPasswordOutcome.Seed;
- }
-
- // The compiled-in default passwords are a local/CI convenience only. In a
- // real deployment (anything but Development/Testing) the configured
- // password is mandatory: seeding the admin with a published default would
- // leave it one POST /connect/token away from a fully-privileged token.
- if (isLocalOrTest)
- {
- password = defaultPassword;
- return SeedPasswordOutcome.Seed;
- }
-
- password = null;
- return requiredOutsideLocal ? SeedPasswordOutcome.Fail : SeedPasswordOutcome.Skip;
- }
+ string defaultPassword
+ ) => string.IsNullOrEmpty(configuredPassword) ? defaultPassword : configuredPassword;
[LoggerMessage(Level = LogLevel.Information, Message = "Seeding role: {RoleName}")]
private static partial void LogSeedingRole(ILogger logger, string roleName);
@@ -205,10 +170,14 @@ out string? password
private static partial void LogSeedingUser(ILogger logger, string email);
[LoggerMessage(
- Level = LogLevel.Information,
- Message = "Skipping seed user {Email}: '{ConfigKey}' is not configured and default passwords are disabled outside Development."
+ Level = LogLevel.Warning,
+ Message = "Seeded {Email} with the compiled-in default password because '{ConfigKey}' is not configured. This password is published in the SimpleModule repository — set '{ConfigKey}' or change the password after first login before exposing this deployment."
)]
- private static partial void LogSkippingSeedUser(ILogger logger, string email, string configKey);
+ private static partial void LogDefaultPasswordWarning(
+ ILogger logger,
+ string email,
+ string configKey
+ );
[LoggerMessage(Level = LogLevel.Error, Message = "Seed error: {ErrorDescription}")]
private static partial void LogSeedError(ILogger logger, string errorDescription);
diff --git a/modules/Users/tests/SimpleModule.Users.Tests/Unit/UserSeedServiceTests.cs b/modules/Users/tests/SimpleModule.Users.Tests/Unit/UserSeedServiceTests.cs
index e349e9e7..8d920363 100644
--- a/modules/Users/tests/SimpleModule.Users.Tests/Unit/UserSeedServiceTests.cs
+++ b/modules/Users/tests/SimpleModule.Users.Tests/Unit/UserSeedServiceTests.cs
@@ -8,88 +8,26 @@ public class UserSeedServiceTests
private const string Default = "Default123!";
private const string Configured = "Configured123!";
- [Theory]
- [InlineData(true, true)] // local, required (admin)
- [InlineData(true, false)] // local, optional (demo)
- [InlineData(false, true)] // real deployment, required
- [InlineData(false, false)] // real deployment, optional
- public void ResolveSeedPassword_ConfiguredPassword_AlwaysUsed(
- bool isLocalOrTest,
- bool requiredOutsideLocal
- )
- {
- var outcome = UserSeedService.ResolveSeedPassword(
- Configured,
- Default,
- isLocalOrTest,
- requiredOutsideLocal,
- out var password
- );
-
- outcome.Should().Be(SeedPasswordOutcome.Seed);
- password.Should().Be(Configured);
- }
-
- [Theory]
- [InlineData(true)] // required (admin)
- [InlineData(false)] // optional (demo)
- public void ResolveSeedPassword_LocalOrTest_NoConfig_UsesDefault(bool requiredOutsideLocal)
- {
- var outcome = UserSeedService.ResolveSeedPassword(
- configuredPassword: null,
- Default,
- isLocalOrTest: true,
- requiredOutsideLocal,
- out var password
- );
-
- outcome.Should().Be(SeedPasswordOutcome.Seed);
- password.Should().Be(Default);
- }
-
[Fact]
- public void ResolveSeedPassword_RealDeployment_RequiredAndUnconfigured_Fails()
+ public void ResolveSeedPassword_ConfiguredPassword_IsUsed()
{
- // The security-critical path: the admin account must never fall back to
- // the compiled-in default outside a local/test environment.
- var outcome = UserSeedService.ResolveSeedPassword(
- configuredPassword: null,
- Default,
- isLocalOrTest: false,
- requiredOutsideLocal: true,
- out var password
- );
-
- outcome.Should().Be(SeedPasswordOutcome.Fail);
- password.Should().BeNull();
+ UserSeedService.ResolveSeedPassword(Configured, Default).Should().Be(Configured);
}
+ ///
+ /// Both seeded accounts fall back to their compiled-in defaults in every
+ /// environment so the app is usable out of the box — the login page's
+ /// quick-login buttons advertise these exact credentials.
+ ///
[Fact]
- public void ResolveSeedPassword_RealDeployment_OptionalAndUnconfigured_Skips()
+ public void ResolveSeedPassword_NoConfiguredPassword_FallsBackToDefault()
{
- var outcome = UserSeedService.ResolveSeedPassword(
- configuredPassword: null,
- Default,
- isLocalOrTest: false,
- requiredOutsideLocal: false,
- out var password
- );
-
- outcome.Should().Be(SeedPasswordOutcome.Skip);
- password.Should().BeNull();
+ UserSeedService.ResolveSeedPassword(null, Default).Should().Be(Default);
}
[Fact]
public void ResolveSeedPassword_EmptyConfiguredPassword_TreatedAsUnconfigured()
{
- var outcome = UserSeedService.ResolveSeedPassword(
- configuredPassword: "",
- Default,
- isLocalOrTest: false,
- requiredOutsideLocal: true,
- out _
- );
-
- outcome.Should().Be(SeedPasswordOutcome.Fail);
+ UserSeedService.ResolveSeedPassword("", Default).Should().Be(Default);
}
}