< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.WebApi.EmailRegistrationExtensions
Assembly: ArturRios.Heimdall.WebApi
File(s): /home/ci/agent/workspace/heimdall-api-docs_main/app/src/Presentation/ArturRios.Heimdall.WebApi/Registration/EmailRegistrationExtensions.cs
Line coverage
70%
Covered lines: 46
Uncovered lines: 19
Coverable lines: 65
Total lines: 139
Line coverage: 70.7%
Branch coverage
50%
Covered branches: 3
Total branches: 6
Branch coverage: 50%
Method coverage
100%
Covered methods: 2
Fully covered methods: 0
Total methods: 2
Method coverage: 100%
Full method coverage: 0%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
AddEmailSenders(...)50%6453.12%
AddAccountEmailFlows(...)50%2287.87%

File(s)

/home/ci/agent/workspace/heimdall-api-docs_main/app/src/Presentation/ArturRios.Heimdall.WebApi/Registration/EmailRegistrationExtensions.cs

#LineLine coverage
 1using ArturRios.Data.Relational.Core.DependencyInjection;
 2using ArturRios.Heimdall.Command.Auditing;
 3using ArturRios.Heimdall.Command.Handlers;
 4using ArturRios.Heimdall.Command.Input;
 5using ArturRios.Heimdall.Command.Output;
 6using ArturRios.Heimdall.Command.Services;
 7using ArturRios.Heimdall.WebApi.Email;
 8using ArturRios.Messaging.Email;
 9using ArturRios.Util.WebApi.Configuration;
 10using ArturRios.Util.WebApi.Security.Extensions;
 11using Serilog;
 12
 13namespace ArturRios.Heimdall.WebApi;
 14
 15public static class EmailRegistrationExtensions
 16{
 17    /// <summary>
 18    ///     Chooses how the two transactional emails — UC-06's verification token and UC-12's
 19    ///     password reset token — are delivered. With Mailgun credentials present, both go out for
 20    ///     real through <c>ArturRios.Messaging</c>; without them, both are logged.
 21    /// </summary>
 22    /// <remarks>
 23    ///     The fallback is deliberate rather than a failure: a developer running the API locally,
 24    ///     and the functional suite, both need person creation and password recovery to work without
 25    ///     credentials and without reaching the network. Failing startup instead would make Mailgun
 26    ///     a prerequisite for running the tests. In Production, though, the fallback logs a
 27    ///     verification token, a password reset token, or a 2FA code in plaintext on every send — an
 28    ///     account-takeover primitive for anyone who can read the logs — so an unconfigured Production
 29    ///     deployment fails startup outright instead of silently degrading into that.
 30    /// </remarks>
 31    public static IServiceCollection AddEmailSenders(this IServiceCollection services, IHostEnvironment environment)
 32    {
 97333        var options = EmailDeliveryOptions.FromEnvironment();
 34
 97335        services.AddSingleton(options);
 36
 97337        if (!options.MailgunConfigured)
 38        {
 97339            if (environment.IsProduction())
 40            {
 041                throw new InvalidOperationException(
 042                    $"Mailgun is not configured ({MailgunEmailService.ApiKeyVariable} / " +
 043                    $"{MailgunEmailService.DomainVariable}) in Production. Refusing to start: the " +
 044                    "fallback sender logs verification tokens, password reset tokens, and 2FA codes " +
 045                    "in plaintext, which must never happen outside local development and tests.");
 46            }
 47
 97348            Log.Warning(
 97349                "Mailgun is not configured ({ApiKeyVariable} / {DomainVariable}); verification and " +
 97350                "password reset tokens will be logged instead of emailed",
 97351                MailgunEmailService.ApiKeyVariable, MailgunEmailService.DomainVariable);
 52
 97353            services.AddScoped<IEmailVerificationSender, LoggingEmailVerificationSender>();
 97354            services.AddScoped<IPasswordResetSender, LoggingPasswordResetSender>();
 97355            services.AddScoped<ITwoFactorEmailSender, LoggingTwoFactorEmailSender>();
 97356            services.AddScoped<IRestrictionLiftNotifier, LoggingRestrictionLiftNotifier>();
 97357            services.AddScoped<IRegistrationAttemptSender, LoggingRegistrationAttemptSender>();
 97358            services.AddScoped<IRegistrationLinkSender, LoggingRegistrationLinkSender>();
 97359            services.AddScoped<IEmailChangeNoticeSender, LoggingEmailChangeNoticeSender>();
 97360            services.AddScoped<IScopeInvitationNoticeSender, LoggingScopeInvitationNoticeSender>();
 61
 97362            return services;
 63        }
 64
 65        // A typed client, so the Mailgun service reuses pooled connections instead of creating an
 66        // HttpClient per send.
 067        services.AddHttpClient<IEmailService, MailgunEmailService>();
 068        services.AddScoped<IEmailVerificationSender, MailgunEmailVerificationSender>();
 069        services.AddScoped<IPasswordResetSender, MailgunPasswordResetSender>();
 070        services.AddScoped<ITwoFactorEmailSender, MailgunTwoFactorEmailSender>();
 071        services.AddScoped<IRestrictionLiftNotifier, MailgunRestrictionLiftNotifier>();
 072        services.AddScoped<IRegistrationAttemptSender, MailgunRegistrationAttemptSender>();
 073        services.AddScoped<IRegistrationLinkSender, MailgunRegistrationLinkSender>();
 074        services.AddScoped<IEmailChangeNoticeSender, MailgunEmailChangeNoticeSender>();
 075        services.AddScoped<IScopeInvitationNoticeSender, MailgunScopeInvitationNoticeSender>();
 76
 077        return services;
 78    }
 79
 80    /// <summary>
 81    ///     Registers the e-mail verification and password reset services, the cooldown and invitation
 82    ///     options, the password recovery and registration queues with their dispatchers, and the audited
 83    ///     handlers of the administrator-initiated e-mails (UC-12, UC-47, UC-51, UC-52).
 84    /// </summary>
 85    public static IServiceCollection AddAccountEmailFlows(this IServiceCollection services)
 86    {
 97387        services.AddSingleton(EmailVerificationOptions.FromEnvironment());
 97388        services.AddScoped<IEmailVerificationService, EmailVerificationService>();
 97389        services.AddSingleton(PasswordResetOptions.FromEnvironment());
 97390        services.AddScoped<IPasswordResetService, PasswordResetService>();
 91        // UC-51/UC-52 (FR-EV-07, FR-PR-06): the per-person cooldown on administrator-initiated email,
 92        // read once at start-up like the token lifetimes it is compared against.
 97393        var adminEmail = AdminEmailOptions.FromEnvironment();
 97394        ConfigurationWarnings.WarnOnFallback(AdminEmailOptions.CooldownVariable, adminEmail.RejectedCooldown,
 97395            AdminEmailOptions.DefaultCooldownSeconds);
 97396        services.AddSingleton(adminEmail);
 97
 98        // UC-21 (FR-SC-18): how long a scope owner invitation stays acceptable, read once at start-up.
 97399        var scopeInvitation = ScopeInvitationOptions.FromEnvironment();
 973100        ConfigurationWarnings.WarnOnFallback(ScopeInvitationOptions.LifetimeVariable, scopeInvitation.RejectedLifetime,
 973101            ScopeInvitationOptions.DefaultLifetimeSeconds);
 973102        services.AddSingleton(scopeInvitation);
 103        // UC-51: no validator — the only input is the route's person id, already constrained to a GUID.
 973104        services.AddAuditedCommandHandler<SendPersonVerificationEmailCommand, SendPersonVerificationEmailCommandOutput, 
 105        // UC-52: no validator either — the route's person id is the whole of the input.
 973106        services.AddAuditedCommandHandler<SendPersonPasswordResetCommand, SendPersonPasswordResetCommandOutput, SendPers
 107        // UC-12 runs in two halves (AF-12a): the handler queues, the dispatcher does the work after
 108        // the caller has been answered, so no address takes longer to answer than another. See
 109        // PasswordRecoveryQueue for why this is an in-memory queue rather than a floor or an outbox.
 973110        services.AddSingleton<PasswordRecoveryQueue>();
 973111        services.AddSingleton<IPasswordRecoveryQueue>(
 989112            provider => provider.GetRequiredService<PasswordRecoveryQueue>());
 973113        services.AddScoped<PasswordRecoveryProcessor>();
 973114        services.AddHostedService<PasswordRecoveryDispatcher>();
 115        // UC-47 runs in the same two halves as UC-12 (AF-47b): the handler queues, the dispatcher
 116        // stores the pending registration or tells the holder of the attempt after the caller has been
 117        // answered. Following the mailed link (UC-14) then reaches the confirmer, which creates the User.
 973118        services.AddSingleton<RegistrationQueue>();
 973119        services.AddSingleton<IRegistrationQueue>(
 985120            provider => provider.GetRequiredService<RegistrationQueue>());
 973121        services.AddScoped<RegistrationProcessor>();
 122        // #206: the cap on a scope's live pending registrations, read once at start-up.
 973123        var registration = RegistrationOptions.FromEnvironment();
 124
 973125        if (registration.RejectedMaxPendingPerScope is not null)
 126        {
 0127            Log.Warning(
 0128                "Ignoring unusable {Variable}={Value}; using the default of {Default}",
 0129                RegistrationOptions.MaxPendingPerScopeVariable, registration.RejectedMaxPendingPerScope,
 0130                RegistrationOptions.DefaultMaxPendingPerScope);
 131        }
 132
 973133        services.AddSingleton(registration);
 973134        services.AddScoped<IPendingRegistrationConfirmer, PendingRegistrationConfirmer>();
 973135        services.AddHostedService<RegistrationDispatcher>();
 136
 973137        return services;
 138    }
 139}