Files
2026-06-29 12:35:32 +02:00

307 lines
7.5 KiB
Go

package shipping
import (
"context"
"errors"
"sync"
"testing"
)
// InMemoryProvider is a thread-safe in-memory implementation of Provider for
// tests — no external API calls needed. Backed by a map from
// "country|postcode" to available delivery option groups.
type InMemoryProvider struct {
mu sync.Mutex
opts map[string][]DeliveryOptionGroup // key: "country|postcode"
err error // injected error for testing failure paths
}
// NewInMemoryProvider creates a provider seeded with delivery option groups.
// seed is a sequence of (country, postcode, []DeliveryOptionGroup) triples.
func NewInMemoryProvider(seed ...any) *InMemoryProvider {
p := &InMemoryProvider{
opts: make(map[string][]DeliveryOptionGroup),
}
for i := 0; i+2 < len(seed); i += 3 {
country, ok := seed[i].(string)
if !ok {
continue
}
postcode, ok := seed[i+1].(string)
if !ok {
continue
}
groups, ok := seed[i+2].([]DeliveryOptionGroup)
if !ok {
continue
}
p.opts[providerKey(country, postcode)] = groups
}
return p
}
func providerKey(country, postcode string) string {
return country + "|" + postcode
}
// SetError injects an error that GetShippingOptions returns on every call
// (for testing failure paths). Nil clears the error.
func (p *InMemoryProvider) SetError(err error) {
p.mu.Lock()
defer p.mu.Unlock()
p.err = err
}
func (p *InMemoryProvider) GetShippingOptions(_ context.Context, req GetOptionsRequest) ([]DeliveryOptionGroup, error) {
p.mu.Lock()
defer p.mu.Unlock()
if p.err != nil {
return nil, p.err
}
groups, ok := p.opts[providerKey(req.Country, req.PostCode)]
if !ok {
return []DeliveryOptionGroup{}, nil
}
return groups, nil
}
// ── Tests ─────────────────────────────────────────────────────────────────
func TestProviderReturnsOptions(t *testing.T) {
ctx := context.Background()
p := NewInMemoryProvider(
"SE", "17173", []DeliveryOptionGroup{
{
Type: "home",
Name: "Home delivery",
DefaultOption: DeliveryOption{
ID: "home-1",
Name: "Home delivery",
Description: "Delivered to your door",
DeliveryDays: "2-3 working days",
},
},
{
Type: "pickup",
Name: "Pickup point",
DefaultOption: DeliveryOption{
ID: "pickup-1",
Name: "PostNord Storgatan",
Description: "Collect at PostNord Storgatan",
DeliveryDays: "2-3 working days",
PickupLocation: &PickupLocation{
ID: "loc-1",
Name: "PostNord Storgatan",
Address: Address{
Street: "Storgatan 9",
City: "Solna",
PostCode: "17173",
Country: "SE",
},
},
},
},
},
)
groups, err := p.GetShippingOptions(ctx, GetOptionsRequest{
Country: "SE",
PostCode: "17173",
Locale: "sv",
})
if err != nil {
t.Fatalf("GetShippingOptions: %v", err)
}
if len(groups) != 2 {
t.Fatalf("expected 2 option groups, got %d", len(groups))
}
// Verify home delivery option
if groups[0].Type != "home" {
t.Fatalf("first group: expected type=home, got %q", groups[0].Type)
}
if groups[0].DefaultOption.ID != "home-1" {
t.Fatalf("home default option: expected ID=home-1, got %q", groups[0].DefaultOption.ID)
}
// Verify pickup option with location
if groups[1].Type != "pickup" {
t.Fatalf("second group: expected type=pickup, got %q", groups[1].Type)
}
loc := groups[1].DefaultOption.PickupLocation
if loc == nil {
t.Fatal("pickup option: expected a PickupLocation, got nil")
}
if loc.Address.City != "Solna" {
t.Fatalf("pickup address city: expected Solna, got %q", loc.Address.City)
}
}
func TestProviderUnsupportedAddress(t *testing.T) {
ctx := context.Background()
p := NewInMemoryProvider() // no seed data
groups, err := p.GetShippingOptions(ctx, GetOptionsRequest{
Country: "NO",
PostCode: "0150",
})
if err != nil {
t.Fatalf("expected nil error for unsupported address, got %v", err)
}
if groups == nil {
t.Fatal("expected non-nil empty slice for unsupported address, got nil")
}
if len(groups) != 0 {
t.Fatalf("expected 0 option groups for unsupported address, got %d", len(groups))
}
}
func TestProviderError(t *testing.T) {
ctx := context.Background()
p := NewInMemoryProvider(
"SE", "17173", []DeliveryOptionGroup{
{
Type: "home",
Name: "Home delivery",
DefaultOption: DeliveryOption{
ID: "home-1",
Name: "Home delivery",
},
},
},
)
injected := errors.New("api timeout")
p.SetError(injected)
_, err := p.GetShippingOptions(ctx, GetOptionsRequest{
Country: "SE",
PostCode: "17173",
})
if err == nil {
t.Fatal("expected injected error, got nil")
}
if !errors.Is(err, injected) {
t.Fatalf("expected injected error %v, got %v", injected, err)
}
}
func TestProviderAdditionalOptions(t *testing.T) {
ctx := context.Background()
additionalPoints := make([]DeliveryOption, 2)
additionalPoints[0] = DeliveryOption{
ID: "pickup-2",
Name: "ICA Maxi Solna",
PickupLocation: &PickupLocation{
ID: "loc-2",
Name: "ICA Maxi Solna",
Address: Address{
Street: "Solna Centrum 5",
City: "Solna",
PostCode: "17173",
Country: "SE",
},
},
}
additionalPoints[1] = DeliveryOption{
ID: "pickup-3",
Name: "Coop Solna",
PickupLocation: &PickupLocation{
ID: "loc-3",
Name: "Coop Solna",
Address: Address{
Street: "Solna Torg 2",
City: "Solna",
PostCode: "17173",
Country: "SE",
},
},
}
p := NewInMemoryProvider(
"SE", "17173", []DeliveryOptionGroup{
{
Type: "pickup",
Name: "Pickup point",
DefaultOption: DeliveryOption{
ID: "pickup-1",
Name: "PostNord Storgatan",
},
AdditionalOptions: additionalPoints,
},
},
)
groups, err := p.GetShippingOptions(ctx, GetOptionsRequest{
Country: "SE",
PostCode: "17173",
})
if err != nil {
t.Fatalf("GetShippingOptions: %v", err)
}
if len(groups) != 1 {
t.Fatalf("expected 1 option group, got %d", len(groups))
}
addl := groups[0].AdditionalOptions
if len(addl) != 2 {
t.Fatalf("expected 2 additional options, got %d", len(addl))
}
if addl[0].ID != "pickup-2" {
t.Fatalf("additional[0]: expected ID=pickup-2, got %q", addl[0].ID)
}
}
func TestRoundTripEncodeDecode(t *testing.T) {
original := []DeliveryOptionGroup{
{
Type: "home",
Name: "Home delivery",
DefaultOption: DeliveryOption{
ID: "home-1",
Name: "Home delivery",
DeliveryDays: "2-3 working days",
},
},
}
encoded, err := Encode(original)
if err != nil {
t.Fatalf("Encode: %v", err)
}
decoded, err := Decode(encoded)
if err != nil {
t.Fatalf("Decode: %v", err)
}
if len(decoded) != 1 {
t.Fatalf("round-trip: expected 1 group, got %d", len(decoded))
}
if decoded[0].DefaultOption.ID != "home-1" {
t.Fatalf("round-trip: expected ID=home-1, got %q", decoded[0].DefaultOption.ID)
}
}
func TestNoOptionsSentinelNotReturnedBySingleProvider(t *testing.T) {
// ErrNoOptions is for multi-provider orchestrators. A single provider
// returns an empty slice for unsupported addresses. Verify that the
// in-memory provider follows this contract.
ctx := context.Background()
p := NewInMemoryProvider()
groups, err := p.GetShippingOptions(ctx, GetOptionsRequest{
Country: "FI",
PostCode: "00100",
})
if err != nil {
t.Fatalf("expected nil for unsupported address, got %v", err)
}
if groups == nil {
t.Fatal("expected non-nil empty slice, got nil")
}
}