From ea247e26007094953306b221f97e3b0f97529337 Mon Sep 17 00:00:00 2001 From: matst80 Date: Tue, 18 Nov 2025 16:40:25 +0100 Subject: [PATCH] update tests and discovery --- cmd/cart/checkout_builder.go | 9 +-- cmd/cart/pool-server.go | 25 ++++---- pkg/cart/price_test.go | 103 ++++++++++++++++++++++++++++++++ pkg/discovery/discovery.go | 27 ++++++--- pkg/discovery/discovery_mock.go | 13 ++-- pkg/discovery/types.go | 5 ++ 6 files changed, 146 insertions(+), 36 deletions(-) diff --git a/cmd/cart/checkout_builder.go b/cmd/cart/checkout_builder.go index aca2ec9..2d19ab0 100644 --- a/cmd/cart/checkout_builder.go +++ b/cmd/cart/checkout_builder.go @@ -14,9 +14,6 @@ type CheckoutMeta struct { Terms string Checkout string Confirmation string - Notification string - Validation string - Push string Country string Currency string // optional override (defaults to "SEK" if empty) Locale string // optional override (defaults to "sv-se" if empty) @@ -108,9 +105,9 @@ func BuildCheckoutOrderPayload(grain *cart.CartGrain, meta *CheckoutMeta) ([]byt Terms: meta.Terms, Checkout: meta.Checkout, Confirmation: meta.Confirmation, - Notification: meta.Notification, - Validation: meta.Validation, - Push: meta.Push, + Notification: "https://cart.tornberg.me/notification", + Validation: "https://cart.tornberg.me/validate", + Push: "https://cart.tornberg.me/push?order_id={checkout.order.id}", }, } diff --git a/cmd/cart/pool-server.go b/cmd/cart/pool-server.go index 397caf1..02e5ed2 100644 --- a/cmd/cart/pool-server.go +++ b/cmd/cart/pool-server.go @@ -335,30 +335,27 @@ func getInventoryRequests(items []*cart.CartItem) []inventory.ReserveRequest { return requests } -func (s *PoolServer) CreateOrUpdateCheckout(ctx context.Context, host string, id cart.CartId) (*CheckoutOrder, error) { - country := getCountryFromHost(host) +func (s *PoolServer) CreateOrUpdateCheckout(r *http.Request, id cart.CartId) (*CheckoutOrder, error) { + country := getCountryFromHost(r.Host) meta := &CheckoutMeta{ - Terms: fmt.Sprintf("https://%s/terms", host), - Checkout: fmt.Sprintf("https://%s/checkout?order_id={checkout.order.id}", host), - Confirmation: fmt.Sprintf("https://%s/confirmation/{checkout.order.id}", host), - Notification: "https://cart.tornberg.me/notification", - Validation: "https://cart.tornberg.me/validate", - Push: "https://cart.tornberg.me/push?order_id={checkout.order.id}", + Terms: fmt.Sprintf("https://%s/terms", r.Host), + Checkout: fmt.Sprintf("https://%s/checkout?order_id={checkout.order.id}", r.Host), + Confirmation: fmt.Sprintf("https://%s/confirmation/{checkout.order.id}", r.Host), Country: country, Currency: getCurrency(country), Locale: getLocale(country), } // Get current grain state (may be local or remote) - grain, err := s.Get(ctx, uint64(id)) + grain, err := s.Get(r.Context(), uint64(id)) if err != nil { return nil, err } if s.inventoryService != nil { inventoryRequests := getInventoryRequests(grain.Items) - failingRequest, err := s.inventoryService.ReservationCheck(ctx, inventoryRequests...) + failingRequest, err := s.inventoryService.ReservationCheck(r.Context(), inventoryRequests...) if err != nil { - logger.WarnContext(ctx, "inventory check failed", string(failingRequest.SKU), string(failingRequest.LocationID)) + logger.WarnContext(r.Context(), "inventory check failed", string(failingRequest.SKU), string(failingRequest.LocationID)) return nil, err } } @@ -370,9 +367,9 @@ func (s *PoolServer) CreateOrUpdateCheckout(ctx context.Context, host string, id } if grain.OrderReference != "" { - return s.klarnaClient.UpdateOrder(ctx, grain.OrderReference, bytes.NewReader(payload)) + return s.klarnaClient.UpdateOrder(r.Context(), grain.OrderReference, bytes.NewReader(payload)) } else { - return s.klarnaClient.CreateOrder(ctx, bytes.NewReader(payload)) + return s.klarnaClient.CreateOrder(r.Context(), bytes.NewReader(payload)) } } @@ -584,7 +581,7 @@ func (s *PoolServer) CheckoutHandler(fn func(order *CheckoutOrder, w http.Respon return CookieCartIdHandler(s.ProxyHandler(func(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error { orderId := r.URL.Query().Get("order_id") if orderId == "" { - order, err := s.CreateOrUpdateCheckout(r.Context(), r.Host, cartId) + order, err := s.CreateOrUpdateCheckout(r, cartId) if err != nil { logger.Error("unable to create klarna session", "error", err) return err diff --git a/pkg/cart/price_test.go b/pkg/cart/price_test.go index 285424b..37f39cf 100644 --- a/pkg/cart/price_test.go +++ b/pkg/cart/price_test.go @@ -133,3 +133,106 @@ func TestPriceMultiplyMethod(t *testing.T) { t.Fatalf("expected exVat %d got %d", exBefore*2, p.ValueExVat()) } } + +func TestGetTaxAmount(t *testing.T) { + tests := []struct { + total int64 + tax int + expected int64 + desc string + }{ + {1250, 2500, 250, "25% VAT"}, // 1250 / (1 + 100/25) = 1250 / 5 = 250 + {1000, 2000, 166, "20% VAT"}, // 1000 / (1 + 100/20) = 1000 / 6 ≈ 166 + {1200, 2500, 240, "25% VAT on 1200"}, + {0, 2500, 0, "zero total"}, + {100, 1000, 9, "10% VAT"}, // tax=1000 for 10%, 100 / (1 + 100/10) = 100 / 11 ≈ 9 + {100, 10000, 50, "100% VAT"}, // tax=10000 for 100%, 100 / (1 + 100/100) = 100 / 2 = 50 + } + + for _, tt := range tests { + result := GetTaxAmount(tt.total, tt.tax) + if result != tt.expected { + t.Errorf("GetTaxAmount(%d, %d) [%s] = %d; expected %d", tt.total, tt.tax, tt.desc, result, tt.expected) + } + } +} + +func TestNewPriceFromIncVatEdgeCases(t *testing.T) { + // Zero VAT rate + p := NewPriceFromIncVat(1000, 0) + if p.IncVat != 1000 { + t.Errorf("expected IncVat 1000, got %d", p.IncVat) + } + if len(p.VatRates) != 1 || p.VatRates[0] != 0 { + t.Errorf("expected VAT 0 for rate 0, got %v", p.VatRates) + } + if p.ValueExVat() != 1000 { + t.Errorf("expected exVat 1000, got %d", p.ValueExVat()) + } + + // High VAT rate, e.g., 50% + p = NewPriceFromIncVat(1500, 50) + expectedVat := int64(1500 / (1 + 100/50)) // 1500 / 3 = 500 + if p.VatRates[50] != expectedVat { + t.Errorf("expected VAT %d for 50%%, got %d", expectedVat, p.VatRates[50]) + } + if p.ValueExVat() != 1500-expectedVat { + t.Errorf("expected exVat %d, got %d", 1500-expectedVat, p.ValueExVat()) + } +} + +func TestPriceValueExVatAndTotalVat(t *testing.T) { + p := Price{IncVat: 13700, VatRates: map[float32]int64{25: 2500, 12: 1200}} + exVat := p.ValueExVat() + totalVat := p.TotalVat() + if exVat != 10000 { + t.Errorf("expected exVat 10000, got %d", exVat) + } + if totalVat != 3700 { + t.Errorf("expected totalVat 3700, got %d", totalVat) + } + if exVat+totalVat != p.IncVat { + t.Errorf("exVat + totalVat should equal IncVat: %d + %d != %d", exVat, totalVat, p.IncVat) + } + + // Empty VAT rates + p2 := Price{IncVat: 500, VatRates: nil} + if p2.ValueExVat() != 500 { + t.Errorf("expected exVat 500 for no VAT, got %d", p2.ValueExVat()) + } + if p2.TotalVat() != 0 { + t.Errorf("expected totalVat 0, got %d", p2.TotalVat()) + } +} + +func TestMultiplyPriceWithZeroQty(t *testing.T) { + base := Price{IncVat: 1250, VatRates: map[float32]int64{25: 250}} + multiplied := MultiplyPrice(base, 0) + if multiplied.IncVat != 0 { + t.Errorf("expected IncVat 0, got %d", multiplied.IncVat) + } + if len(multiplied.VatRates) != 1 || multiplied.VatRates[25] != 0 { + t.Errorf("expected VAT 0, got %v", multiplied.VatRates) + } +} + +func TestPriceAddSubtractEdgeCases(t *testing.T) { + a := Price{IncVat: 1000, VatRates: map[float32]int64{25: 200}} + b := Price{IncVat: 500, VatRates: map[float32]int64{12: 54}} // Different rate + + acc := NewPrice() + acc.Add(a) + acc.Add(b) + + if acc.VatRates[25] != 200 || acc.VatRates[12] != 54 { + t.Errorf("expected VAT 25:200, 12:54, got %v", acc.VatRates) + } + + // Subtract more than added (negative VAT) + acc.Subtract(a) + acc.Subtract(b) + acc.Subtract(a) // Subtract extra a + if acc.VatRates[25] != -200 || acc.VatRates[12] != 0 { + t.Errorf("expected negative VAT for 25 after over-subtract, got %v", acc.VatRates) + } +} diff --git a/pkg/discovery/discovery.go b/pkg/discovery/discovery.go index df19716..d7122e9 100644 --- a/pkg/discovery/discovery.go +++ b/pkg/discovery/discovery.go @@ -2,6 +2,8 @@ package discovery import ( "context" + "slices" + "sync" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -33,13 +35,10 @@ func (k *K8sDiscovery) DiscoverInNamespace(namespace string) ([]string, error) { return hosts, nil } -type HostChange struct { - Host string - Type watch.EventType -} - func (k *K8sDiscovery) Watch() (<-chan HostChange, error) { timeout := int64(30) + ipsThatAreReady := make(map[string]bool) + m := sync.Mutex{} watcherFn := func(options metav1.ListOptions) (watch.Interface, error) { return k.client.CoreV1().Pods("").Watch(k.ctx, metav1.ListOptions{ LabelSelector: "actor-pool=cart", @@ -55,10 +54,22 @@ func (k *K8sDiscovery) Watch() (<-chan HostChange, error) { for event := range watcher.ResultChan() { pod := event.Object.(*v1.Pod) - // log.Printf("pod change %+v", pod.Status.Phase == v1.PodRunning) + isReady := slices.ContainsFunc(pod.Status.Conditions, func(condition v1.PodCondition) bool { + return condition.Type == v1.PodReady && condition.Status == v1.ConditionTrue + }) + m.Lock() + oldState := ipsThatAreReady[pod.Status.PodIP] + ipsThatAreReady[pod.Status.PodIP] = isReady + m.Unlock() + if oldState != isReady { + ch <- HostChange{ + Host: pod.Status.PodIP, + IsReady: isReady, + } + } ch <- HostChange{ - Host: pod.Status.PodIP, - Type: event.Type, + Host: pod.Status.PodIP, + IsReady: isReady, } } }() diff --git a/pkg/discovery/discovery_mock.go b/pkg/discovery/discovery_mock.go index 85f791a..1e2996a 100644 --- a/pkg/discovery/discovery_mock.go +++ b/pkg/discovery/discovery_mock.go @@ -2,9 +2,8 @@ package discovery import ( "context" + "slices" "sync" - - "k8s.io/apimachinery/pkg/watch" ) // MockDiscovery is an in-memory Discovery implementation for tests. @@ -56,14 +55,12 @@ func (m *MockDiscovery) AddHost(host string) { if m.closed { return } - for _, h := range m.hosts { - if h == host { - return - } + if slices.Contains(m.hosts, host) { + return } m.hosts = append(m.hosts, host) if m.started { - m.events <- HostChange{Host: host, Type: watch.Added} + m.events <- HostChange{Host: host, IsReady: true} } } @@ -86,7 +83,7 @@ func (m *MockDiscovery) RemoveHost(host string) { } m.hosts = append(m.hosts[:idx], m.hosts[idx+1:]...) if m.started { - m.events <- HostChange{Host: host, Type: watch.Deleted} + m.events <- HostChange{Host: host, IsReady: false} } } diff --git a/pkg/discovery/types.go b/pkg/discovery/types.go index 6613553..9eaeb48 100644 --- a/pkg/discovery/types.go +++ b/pkg/discovery/types.go @@ -1,5 +1,10 @@ package discovery +type HostChange struct { + Host string + IsReady bool +} + type Discovery interface { Discover() ([]string, error) Watch() (<-chan HostChange, error)