181 lines
5.0 KiB
Go
181 lines
5.0 KiB
Go
// Package mail provides runtime mail-delivery adapters for the auth/session
|
|
// service.
|
|
package mail
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"galaxy/authsession/internal/ports"
|
|
)
|
|
|
|
var errForcedFailure = errors.New("stub mail sender: forced failure")
|
|
|
|
// StubMode identifies the deterministic outcome used by StubSender for one
|
|
// delivery attempt.
|
|
type StubMode string
|
|
|
|
const (
|
|
// StubModeSent reports that the adapter accepts delivery and returns the
|
|
// stable sent outcome expected by the auth flow.
|
|
StubModeSent StubMode = "sent"
|
|
|
|
// StubModeSuppressed reports that the adapter intentionally suppresses
|
|
// outward delivery while still returning a successful suppressed outcome.
|
|
StubModeSuppressed StubMode = "suppressed"
|
|
|
|
// StubModeFailed reports that the adapter returns an explicit delivery
|
|
// failure instead of a successful outcome.
|
|
StubModeFailed StubMode = "failed"
|
|
)
|
|
|
|
// IsKnown reports whether mode is one of the supported stub delivery modes.
|
|
func (mode StubMode) IsKnown() bool {
|
|
switch mode {
|
|
case StubModeSent, StubModeSuppressed, StubModeFailed:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// StubStep overrides the default stub behavior for one queued delivery
|
|
// attempt.
|
|
type StubStep struct {
|
|
// Mode selects the delivery behavior for this queued step.
|
|
Mode StubMode
|
|
|
|
// Err optionally overrides the failure returned when Mode is StubModeFailed.
|
|
Err error
|
|
}
|
|
|
|
// Validate reports whether step contains one supported queued behavior.
|
|
func (step StubStep) Validate() error {
|
|
if !step.Mode.IsKnown() {
|
|
return fmt.Errorf("stub mail step mode %q is unsupported", step.Mode)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Attempt records one validated delivery request handled by StubSender,
|
|
// including the auth challenge-derived idempotency key.
|
|
type Attempt struct {
|
|
// Input stores the validated cleartext mail-delivery request exactly as it
|
|
// was passed into SendLoginCode.
|
|
Input ports.SendLoginCodeInput
|
|
|
|
// Mode stores the resolved stub mode after queued overrides were applied.
|
|
Mode StubMode
|
|
}
|
|
|
|
// StubSender is a deterministic runtime MailSender implementation intended
|
|
// for development, local integration, and explicit stub-based tests.
|
|
//
|
|
// The zero value is ready to use and defaults to StubModeSent.
|
|
type StubSender struct {
|
|
// DefaultMode controls the fallback behavior when Script is empty. The zero
|
|
// value is treated as StubModeSent so the zero-value sender is usable
|
|
// without extra configuration.
|
|
DefaultMode StubMode
|
|
|
|
// DefaultError optionally overrides the failure returned when DefaultMode
|
|
// resolves to StubModeFailed.
|
|
DefaultError error
|
|
|
|
// Script stores queued one-shot overrides consumed in FIFO order before the
|
|
// default behavior is used.
|
|
Script []StubStep
|
|
|
|
mu sync.Mutex
|
|
attempts []Attempt
|
|
}
|
|
|
|
// SendLoginCode records one validated delivery request and returns the
|
|
// deterministic stub outcome selected by the queued script or the default
|
|
// mode.
|
|
func (s *StubSender) SendLoginCode(ctx context.Context, input ports.SendLoginCodeInput) (ports.SendLoginCodeResult, error) {
|
|
if ctx == nil {
|
|
return ports.SendLoginCodeResult{}, errors.New("stub mail sender: nil context")
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return ports.SendLoginCodeResult{}, err
|
|
}
|
|
if err := input.Validate(); err != nil {
|
|
return ports.SendLoginCodeResult{}, err
|
|
}
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
mode, errOverride, err := s.resolveNextStepLocked()
|
|
if err != nil {
|
|
return ports.SendLoginCodeResult{}, err
|
|
}
|
|
|
|
s.attempts = append(s.attempts, Attempt{
|
|
Input: input,
|
|
Mode: mode,
|
|
})
|
|
|
|
switch mode {
|
|
case StubModeSent:
|
|
return ports.SendLoginCodeResult{Outcome: ports.SendLoginCodeOutcomeSent}, nil
|
|
case StubModeSuppressed:
|
|
return ports.SendLoginCodeResult{Outcome: ports.SendLoginCodeOutcomeSuppressed}, nil
|
|
case StubModeFailed:
|
|
if errOverride != nil {
|
|
return ports.SendLoginCodeResult{}, errOverride
|
|
}
|
|
return ports.SendLoginCodeResult{}, errForcedFailure
|
|
default:
|
|
return ports.SendLoginCodeResult{}, fmt.Errorf("stub mail sender: unsupported resolved mode %q", mode)
|
|
}
|
|
}
|
|
|
|
// RecordedAttempts returns a stable defensive copy of every validated delivery
|
|
// attempt handled by the stub.
|
|
func (s *StubSender) RecordedAttempts() []Attempt {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
return append([]Attempt(nil), s.attempts...)
|
|
}
|
|
|
|
func (s *StubSender) resolveNextStepLocked() (StubMode, error, error) {
|
|
if len(s.Script) > 0 {
|
|
step := s.Script[0]
|
|
s.Script = append([]StubStep(nil), s.Script[1:]...)
|
|
if err := step.Validate(); err != nil {
|
|
return "", nil, fmt.Errorf("stub mail sender: %w", err)
|
|
}
|
|
if step.Mode == StubModeFailed {
|
|
if step.Err != nil {
|
|
return step.Mode, step.Err, nil
|
|
}
|
|
return step.Mode, errForcedFailure, nil
|
|
}
|
|
return step.Mode, nil, nil
|
|
}
|
|
|
|
mode := s.DefaultMode
|
|
if mode == "" {
|
|
mode = StubModeSent
|
|
}
|
|
if !mode.IsKnown() {
|
|
return "", nil, fmt.Errorf("stub mail sender: default mode %q is unsupported", mode)
|
|
}
|
|
if mode == StubModeFailed {
|
|
if s.DefaultError != nil {
|
|
return mode, s.DefaultError, nil
|
|
}
|
|
return mode, errForcedFailure, nil
|
|
}
|
|
|
|
return mode, nil, nil
|
|
}
|
|
|
|
var _ ports.MailSender = (*StubSender)(nil)
|