Files
galaxy-game/mail/internal/adapters/stubprovider/provider.go
T
2026-04-17 18:39:16 +02:00

212 lines
5.7 KiB
Go

// Package stubprovider provides the deterministic local provider used by Mail
// Service tests and local bootstrap flows.
package stubprovider
import (
"context"
"errors"
"fmt"
"sync"
"galaxy/mail/internal/domain/common"
deliverydomain "galaxy/mail/internal/domain/delivery"
"galaxy/mail/internal/ports"
)
const providerName = "stub"
// ScriptedOutcome stores one queued stub-provider result consumed by the next
// Send call.
type ScriptedOutcome struct {
// Classification stores the stable provider result classification.
Classification ports.Classification
// Script stores the optional stable script label included in the redacted
// provider summary.
Script string
// Details stores optional in-memory-only diagnostic fields associated with
// the scripted result.
Details map[string]string
}
// Validate reports whether outcome contains one supported queued stub result.
func (outcome ScriptedOutcome) Validate() error {
if !outcome.Classification.IsKnown() {
return fmt.Errorf("stub scripted classification %q is unsupported", outcome.Classification)
}
if outcome.Script != "" {
if _, err := ports.BuildSafeSummary(ports.SummaryFields{
Provider: providerName,
Result: string(outcome.Classification),
Script: outcome.Script,
}); err != nil {
return fmt.Errorf("stub scripted outcome: %w", err)
}
}
for key, value := range outcome.Details {
result := ports.Result{
Classification: outcome.Classification,
Summary: "provider=stub result=accepted",
Details: map[string]string{
key: value,
},
}
if err := result.Validate(); err != nil {
return fmt.Errorf("stub scripted details: %w", err)
}
}
return nil
}
// Provider stores one deterministic in-memory provider implementation.
type Provider struct {
mu sync.Mutex
queue []ScriptedOutcome
inputs []ports.Message
closed bool
}
// New constructs the deterministic stub provider.
func New(initial ...ScriptedOutcome) (*Provider, error) {
provider := &Provider{}
if err := provider.Enqueue(initial...); err != nil {
return nil, fmt.Errorf("new stub provider: %w", err)
}
return provider, nil
}
// Send records message and returns the next scripted outcome, or a stable
// accepted outcome when no script remains.
func (provider *Provider) Send(ctx context.Context, message ports.Message) (ports.Result, error) {
switch {
case ctx == nil:
return ports.Result{}, errors.New("send with stub provider: nil context")
case provider == nil:
return ports.Result{}, errors.New("send with stub provider: nil provider")
}
if err := message.Validate(); err != nil {
return ports.Result{}, fmt.Errorf("send with stub provider: %w", err)
}
provider.mu.Lock()
defer provider.mu.Unlock()
if provider.closed {
return ports.Result{}, errors.New("send with stub provider: provider is closed")
}
provider.inputs = append(provider.inputs, cloneMessage(message))
if len(provider.queue) == 0 {
return scriptedResult(ScriptedOutcome{
Classification: ports.ClassificationAccepted,
})
}
next := provider.queue[0]
provider.queue = provider.queue[1:]
return scriptedResult(next)
}
// Close marks the provider as closed. Future Send calls fail fast.
func (provider *Provider) Close() error {
if provider == nil {
return nil
}
provider.mu.Lock()
defer provider.mu.Unlock()
provider.closed = true
return nil
}
// Enqueue appends scripted outcomes to the stub queue.
func (provider *Provider) Enqueue(outcomes ...ScriptedOutcome) error {
if provider == nil {
return errors.New("enqueue stub provider outcomes: nil provider")
}
provider.mu.Lock()
defer provider.mu.Unlock()
for index, outcome := range outcomes {
if err := outcome.Validate(); err != nil {
return fmt.Errorf("enqueue stub provider outcomes[%d]: %w", index, err)
}
provider.queue = append(provider.queue, ScriptedOutcome{
Classification: outcome.Classification,
Script: outcome.Script,
Details: ports.CloneDetails(outcome.Details),
})
}
return nil
}
// Inputs returns a detached snapshot of the accepted Send inputs.
func (provider *Provider) Inputs() []ports.Message {
if provider == nil {
return nil
}
provider.mu.Lock()
defer provider.mu.Unlock()
inputs := make([]ports.Message, len(provider.inputs))
for index, input := range provider.inputs {
inputs[index] = cloneMessage(input)
}
return inputs
}
func scriptedResult(outcome ScriptedOutcome) (ports.Result, error) {
summary, err := ports.BuildSafeSummary(ports.SummaryFields{
Provider: providerName,
Result: string(outcome.Classification),
Script: outcome.Script,
})
if err != nil {
return ports.Result{}, fmt.Errorf("build stub provider summary: %w", err)
}
result := ports.Result{
Classification: outcome.Classification,
Summary: summary,
Details: ports.CloneDetails(outcome.Details),
}
if err := result.Validate(); err != nil {
return ports.Result{}, fmt.Errorf("build stub provider result: %w", err)
}
return result, nil
}
func cloneMessage(message ports.Message) ports.Message {
cloned := ports.Message{
Envelope: deliverydomain.Envelope{
To: append([]common.Email(nil), message.Envelope.To...),
Cc: append([]common.Email(nil), message.Envelope.Cc...),
Bcc: append([]common.Email(nil), message.Envelope.Bcc...),
ReplyTo: append([]common.Email(nil), message.Envelope.ReplyTo...),
},
Content: message.Content,
}
if len(message.Attachments) > 0 {
cloned.Attachments = make([]ports.Attachment, len(message.Attachments))
for index, attachment := range message.Attachments {
content := make([]byte, len(attachment.Content))
copy(content, attachment.Content)
cloned.Attachments[index] = ports.Attachment{
Metadata: attachment.Metadata,
Content: content,
}
}
}
return cloned
}