307 lines
7.5 KiB
Go
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")
|
|
}
|
|
}
|