mcp and ucp
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
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},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user