feat: mail service
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
// Package idempotency defines the deduplication record used by Mail Service
|
||||
// acceptance flows.
|
||||
package idempotency
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"galaxy/mail/internal/domain/common"
|
||||
"galaxy/mail/internal/domain/delivery"
|
||||
)
|
||||
|
||||
// Record stores the first accepted fingerprint bound to one `(source,
|
||||
// idempotency_key)` scope.
|
||||
type Record struct {
|
||||
// Source stores the frozen delivery source vocabulary value.
|
||||
Source delivery.Source
|
||||
|
||||
// IdempotencyKey stores the caller-owned deduplication key.
|
||||
IdempotencyKey common.IdempotencyKey
|
||||
|
||||
// DeliveryID stores the accepted delivery linked to the scope.
|
||||
DeliveryID common.DeliveryID
|
||||
|
||||
// RequestFingerprint stores the stable fingerprint of the first accepted
|
||||
// request.
|
||||
RequestFingerprint string
|
||||
|
||||
// CreatedAt stores when the deduplication record was created.
|
||||
CreatedAt time.Time
|
||||
|
||||
// ExpiresAt stores when the deduplication record becomes invalid.
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// Validate reports whether Record satisfies the frozen Stage 2 structural
|
||||
// invariants.
|
||||
func (record Record) Validate() error {
|
||||
if !record.Source.IsKnown() {
|
||||
return fmt.Errorf("idempotency source %q is unsupported", record.Source)
|
||||
}
|
||||
if err := record.IdempotencyKey.Validate(); err != nil {
|
||||
return fmt.Errorf("idempotency key: %w", err)
|
||||
}
|
||||
if err := record.DeliveryID.Validate(); err != nil {
|
||||
return fmt.Errorf("idempotency delivery id: %w", err)
|
||||
}
|
||||
if err := validateToken("idempotency request fingerprint", record.RequestFingerprint); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := common.ValidateTimestamp("idempotency created at", record.CreatedAt); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := common.ValidateTimestamp("idempotency expires at", record.ExpiresAt); err != nil {
|
||||
return err
|
||||
}
|
||||
if !record.ExpiresAt.After(record.CreatedAt) {
|
||||
return fmt.Errorf("idempotency expires at must be after created at")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateToken(name string, value string) error {
|
||||
switch {
|
||||
case strings.TrimSpace(value) == "":
|
||||
return fmt.Errorf("%s must not be empty", name)
|
||||
case strings.TrimSpace(value) != value:
|
||||
return fmt.Errorf("%s must not contain surrounding whitespace", name)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package idempotency
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"galaxy/mail/internal/domain/common"
|
||||
"galaxy/mail/internal/domain/delivery"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRecordValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
createdAt := time.Unix(1_775_121_700, 0).UTC()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
record Record
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
record: Record{
|
||||
Source: delivery.SourceNotification,
|
||||
IdempotencyKey: common.IdempotencyKey("notification:delivery-123"),
|
||||
DeliveryID: common.DeliveryID("delivery-123"),
|
||||
RequestFingerprint: "sha256:abcdef",
|
||||
CreatedAt: createdAt,
|
||||
ExpiresAt: createdAt.Add(7 * 24 * time.Hour),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "expires at must be after created at",
|
||||
record: Record{
|
||||
Source: delivery.SourceNotification,
|
||||
IdempotencyKey: common.IdempotencyKey("notification:delivery-123"),
|
||||
DeliveryID: common.DeliveryID("delivery-123"),
|
||||
RequestFingerprint: "sha256:abcdef",
|
||||
CreatedAt: createdAt,
|
||||
ExpiresAt: createdAt,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "fingerprint required",
|
||||
record: Record{
|
||||
Source: delivery.SourceNotification,
|
||||
IdempotencyKey: common.IdempotencyKey("notification:delivery-123"),
|
||||
DeliveryID: common.DeliveryID("delivery-123"),
|
||||
CreatedAt: createdAt,
|
||||
ExpiresAt: createdAt.Add(time.Hour),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := tt.record.Validate()
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user