237 lines
6.3 KiB
Go
237 lines
6.3 KiB
Go
package ucp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.k6n.net/mats/go-cart-actor/pkg/cart"
|
|
"git.k6n.net/mats/go-cart-actor/pkg/actor"
|
|
messages "git.k6n.net/mats/go-cart-actor/proto/cart"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
// testApplier implements CartApplier with an in-memory map of grains.
|
|
type testApplier struct {
|
|
grains map[uint64]*cart.CartGrain
|
|
}
|
|
|
|
func newTestApplier() *testApplier {
|
|
return &testApplier{
|
|
grains: make(map[uint64]*cart.CartGrain),
|
|
}
|
|
}
|
|
|
|
func (a *testApplier) Get(_ context.Context, id uint64) (*cart.CartGrain, error) {
|
|
g, ok := a.grains[id]
|
|
if !ok {
|
|
g = cart.NewCartGrain(id, time.UnixMilli(1000000))
|
|
a.grains[id] = g
|
|
}
|
|
return g, nil
|
|
}
|
|
|
|
func (a *testApplier) Apply(_ context.Context, id uint64, msgs ...proto.Message) (*actor.MutationResult[cart.CartGrain], error) {
|
|
g, err := a.Get(nil, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
results := make([]actor.ApplyResult, len(msgs))
|
|
for i, msg := range msgs {
|
|
results[i] = actor.ApplyResult{Type: string(msg.ProtoReflect().Descriptor().FullName())}
|
|
switch m := msg.(type) {
|
|
case *messages.ClearCartRequest:
|
|
g.Items = nil
|
|
g.Vouchers = nil
|
|
g.TotalDiscount = cart.NewPrice()
|
|
g.TotalPrice = cart.NewPrice()
|
|
case *messages.SetUserId:
|
|
// userId is unexported; rely on serialization to verify.
|
|
_ = m
|
|
}
|
|
}
|
|
return &actor.MutationResult[cart.CartGrain]{
|
|
Result: *g,
|
|
Mutations: results,
|
|
}, nil
|
|
}
|
|
|
|
// mustParseID returns the uint64 representation of a base62 cart id string.
|
|
func mustParseID(s string) uint64 {
|
|
id, _ := cart.ParseCartId(s)
|
|
return uint64(id)
|
|
}
|
|
|
|
func TestCartHandler_CreateCart(t *testing.T) {
|
|
applier := newTestApplier()
|
|
handler := CartHandler(applier)
|
|
|
|
req := httptest.NewRequest("POST", "/", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("expected 201 Created, got %d", rec.Code)
|
|
}
|
|
|
|
var resp CartResponse
|
|
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
if resp.Id == "" {
|
|
t.Fatal("expected non-empty cart id")
|
|
}
|
|
if resp.Status != "active" {
|
|
t.Fatalf("expected status 'active', got %q", resp.Status)
|
|
}
|
|
}
|
|
|
|
func TestCartHandler_GetCartNotFound(t *testing.T) {
|
|
applier := newTestApplier()
|
|
handler := CartHandler(applier)
|
|
|
|
req := httptest.NewRequest("GET", "/nonexistent", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
// /nonexistent is a valid base62 string, so it parses, but the cart
|
|
// auto-creates on first Get, so we get a 200 with an empty cart.
|
|
// This is expected behavior (UCP auto-creates carts on read).
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 (auto-create), got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestCartHandler_CreateAndGet(t *testing.T) {
|
|
applier := newTestApplier()
|
|
handler := CartHandler(applier)
|
|
|
|
// Create a cart.
|
|
req := httptest.NewRequest("POST", "/", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("create failed: %d", rec.Code)
|
|
}
|
|
|
|
var created CartResponse
|
|
json.NewDecoder(rec.Body).Decode(&created)
|
|
|
|
// GET it back.
|
|
req = httptest.NewRequest("GET", "/"+created.Id, nil)
|
|
rec = httptest.NewRecorder()
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", rec.Code)
|
|
}
|
|
|
|
var got CartResponse
|
|
json.NewDecoder(rec.Body).Decode(&got)
|
|
if got.Id != created.Id {
|
|
t.Fatalf("expected id %q, got %q", created.Id, got.Id)
|
|
}
|
|
}
|
|
|
|
func TestCartHandler_CancelCart(t *testing.T) {
|
|
applier := newTestApplier()
|
|
handler := CartHandler(applier)
|
|
|
|
// Create a cart.
|
|
req := httptest.NewRequest("POST", "/", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
var created CartResponse
|
|
json.NewDecoder(rec.Body).Decode(&created)
|
|
|
|
// Cancel it.
|
|
req = httptest.NewRequest("POST", "/"+created.Id+"/cancel", nil)
|
|
rec = httptest.NewRecorder()
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", rec.Code)
|
|
}
|
|
|
|
var cancelled CartResponse
|
|
json.NewDecoder(rec.Body).Decode(&cancelled)
|
|
if cancelled.Id != created.Id {
|
|
t.Fatalf("expected same id %q, got %q", created.Id, cancelled.Id)
|
|
}
|
|
}
|
|
|
|
func TestCartHandler_UpdateCart(t *testing.T) {
|
|
applier := newTestApplier()
|
|
handler := CartHandler(applier)
|
|
|
|
// Create a cart.
|
|
req := httptest.NewRequest("POST", "/", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
var created CartResponse
|
|
json.NewDecoder(rec.Body).Decode(&created)
|
|
|
|
// Update with items (test applier doesn't process AddItem, but the
|
|
// handler still returns 200 — items are populated by the real grain pool).
|
|
updateBody := `{"items": [{"sku": "test-sku", "quantity": 2, "name": "Test Item"}]}`
|
|
req = httptest.NewRequest("PUT", "/"+created.Id, strings.NewReader(updateBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec = httptest.NewRecorder()
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var updated CartResponse
|
|
json.NewDecoder(rec.Body).Decode(&updated)
|
|
if updated.Id != created.Id {
|
|
t.Fatalf("expected same id %q, got %q", created.Id, updated.Id)
|
|
}
|
|
}
|
|
|
|
func TestCartResponse_Conversion(t *testing.T) {
|
|
id := mustParseID("ABCD")
|
|
g := cart.NewCartGrain(id, time.UnixMilli(1000000))
|
|
g.Currency = "SEK"
|
|
g.Items = append(g.Items, &cart.CartItem{
|
|
Sku: "test-1",
|
|
Quantity: 2,
|
|
Price: *mustPrice(10000, 2500),
|
|
TotalPrice: *mustPrice(20000, 5000),
|
|
Tax: 2500,
|
|
Meta: &cart.ItemMeta{Name: "Test Item"},
|
|
})
|
|
g.TotalPrice = mustPrice(20000, 5000)
|
|
|
|
resp := cartGrainToResponse(g.Id.String(), g)
|
|
if len(resp.Items) != 1 {
|
|
t.Fatalf("expected 1 item, got %d", len(resp.Items))
|
|
}
|
|
if resp.Items[0].Sku != "test-1" {
|
|
t.Fatalf("expected sku 'test-1', got %q", resp.Items[0].Sku)
|
|
}
|
|
if resp.Items[0].Name != "Test Item" {
|
|
t.Fatalf("expected name 'Test Item', got %q", resp.Items[0].Name)
|
|
}
|
|
if resp.Totals.TotalIncVat != 20000 {
|
|
t.Fatalf("expected total 20000, got %d", resp.Totals.TotalIncVat)
|
|
}
|
|
if resp.Totals.Currency != "SEK" {
|
|
t.Fatalf("expected currency SEK, got %q", resp.Totals.Currency)
|
|
}
|
|
}
|
|
|
|
func mustPrice(incVat int64, totalVat int64) *cart.Price {
|
|
return &cart.Price{
|
|
IncVat: incVat,
|
|
VatRates: map[float32]int64{25: totalVat},
|
|
}
|
|
}
|