better ucp conformance
This commit is contained in:
@@ -45,12 +45,70 @@ func (s *CheckoutServer) handleCreateCheckout(w http.ResponseWriter, r *http.Req
|
||||
writeError(w, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
if req.CartId == "" {
|
||||
writeError(w, http.StatusBadRequest, "cartId is required")
|
||||
|
||||
items := req.Items
|
||||
if len(items) == 0 && len(req.LineItems) > 0 {
|
||||
items = req.LineItems
|
||||
}
|
||||
|
||||
var cartIdStr string
|
||||
if req.CartId != "" {
|
||||
cartIdStr = req.CartId
|
||||
} else if len(items) > 0 {
|
||||
// Fetch cart base URL
|
||||
cartBaseURL := os.Getenv("CART_INTERNAL_URL")
|
||||
if cartBaseURL == "" {
|
||||
cartBaseURL = "http://cart:8080"
|
||||
}
|
||||
|
||||
// Prepare POST request to /ucp/v1/carts/ to create and seed the cart
|
||||
createReqBody := CreateCartRequest{
|
||||
Items: items,
|
||||
}
|
||||
bodyBytes, err := json.Marshal(createReqBody)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to marshal create cart request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
cartId, ok := cart.ParseCartId(req.CartId)
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
cartReq, err := http.NewRequestWithContext(r.Context(), "POST", fmt.Sprintf("%s/ucp/v1/carts/", cartBaseURL), bytes.NewReader(bodyBytes))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to create post cart request: "+err.Error())
|
||||
return
|
||||
}
|
||||
cartReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
profile := os.Getenv("UCP_AGENT_PROFILE")
|
||||
if profile == "" {
|
||||
profile = DefaultUCPAgentProfile
|
||||
}
|
||||
cartReq.Header.Set("UCP-Agent", `profile="`+profile+`"`)
|
||||
|
||||
cartResp, err := client.Do(cartReq)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to create implicit cart: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer cartResp.Body.Close()
|
||||
|
||||
if cartResp.StatusCode != http.StatusCreated && cartResp.StatusCode != http.StatusOK {
|
||||
writeError(w, http.StatusBadRequest, fmt.Sprintf("cart service returned status: %d when creating implicit cart", cartResp.StatusCode))
|
||||
return
|
||||
}
|
||||
|
||||
var cartRespBody CartResponse
|
||||
if err := json.NewDecoder(cartResp.Body).Decode(&cartRespBody); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to decode implicit cart response: "+err.Error())
|
||||
return
|
||||
}
|
||||
cartIdStr = cartRespBody.Id
|
||||
} else {
|
||||
writeError(w, http.StatusBadRequest, "cartId or items/line_items is required")
|
||||
return
|
||||
}
|
||||
|
||||
cartId, ok := cart.ParseCartId(cartIdStr)
|
||||
if !ok {
|
||||
writeError(w, http.StatusBadRequest, "invalid cartId")
|
||||
return
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
package ucp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/actor"
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/cart"
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/checkout"
|
||||
checkoutMessages "git.k6n.net/mats/go-cart-actor/proto/checkout"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type testCheckoutApplier struct {
|
||||
grains map[uint64]*checkout.CheckoutGrain
|
||||
}
|
||||
|
||||
func newTestCheckoutApplier() *testCheckoutApplier {
|
||||
return &testCheckoutApplier{
|
||||
grains: make(map[uint64]*checkout.CheckoutGrain),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *testCheckoutApplier) Get(_ context.Context, id uint64) (*checkout.CheckoutGrain, error) {
|
||||
g, ok := a.grains[id]
|
||||
if !ok {
|
||||
g = checkout.NewCheckoutGrain(id, 0, 0, time.UnixMilli(1000000), nil)
|
||||
a.grains[id] = g
|
||||
}
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (a *testCheckoutApplier) Apply(ctx context.Context, id uint64, msgs ...proto.Message) (*actor.MutationResult[checkout.CheckoutGrain], error) {
|
||||
g, err := a.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, msg := range msgs {
|
||||
switch m := msg.(type) {
|
||||
case *checkoutMessages.InitializeCheckout:
|
||||
g.CartId = cart.CartId(m.CartId)
|
||||
}
|
||||
}
|
||||
return &actor.MutationResult[checkout.CheckoutGrain]{
|
||||
Result: *g,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestCheckoutHandler_CreateWithCartId(t *testing.T) {
|
||||
// Start a mock Cart server.
|
||||
mockCartServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.Contains(r.URL.Path, "/cart/byid/") {
|
||||
t.Errorf("expected path to contain /cart/byid/, got %q", r.URL.Path)
|
||||
}
|
||||
// Return dummy cart state bytes.
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{}`))
|
||||
}))
|
||||
defer mockCartServer.Close()
|
||||
|
||||
os.Setenv("CART_INTERNAL_URL", mockCartServer.URL)
|
||||
defer os.Unsetenv("CART_INTERNAL_URL")
|
||||
|
||||
applier := newTestCheckoutApplier()
|
||||
handler := CheckoutHandler(applier)
|
||||
|
||||
// Create a dummy cart ID.
|
||||
dummyCartId, _ := cart.NewCartId()
|
||||
|
||||
reqBody := fmt.Sprintf(`{"cartId": %q}`, dummyCartId.String())
|
||||
req := httptest.NewRequest("POST", "/", strings.NewReader(reqBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusCreated {
|
||||
t.Fatalf("expected 201 Created, got %d. Body: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var resp CheckoutResponse
|
||||
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 checkout session id")
|
||||
}
|
||||
if resp.CartId != dummyCartId.String() {
|
||||
t.Fatalf("expected cartId %q, got %q", dummyCartId.String(), resp.CartId)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckoutHandler_CreateWithLineItemsFallback(t *testing.T) {
|
||||
// Start a mock Cart server that supports both creating a cart and getting it.
|
||||
var createdCartId cart.CartId
|
||||
mockCartServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" && strings.Contains(r.URL.Path, "/ucp/v1/carts/") {
|
||||
var err error
|
||||
createdCartId, err = cart.NewCartId()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write([]byte(fmt.Sprintf(`{"id": %q, "status": "active"}`, createdCartId.String())))
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == "GET" && strings.Contains(r.URL.Path, "/cart/byid/") {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{}`))
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer mockCartServer.Close()
|
||||
|
||||
os.Setenv("CART_INTERNAL_URL", mockCartServer.URL)
|
||||
defer os.Unsetenv("CART_INTERNAL_URL")
|
||||
|
||||
applier := newTestCheckoutApplier()
|
||||
handler := CheckoutHandler(applier)
|
||||
|
||||
// POST with line_items and no cartId.
|
||||
reqBody := `{"line_items": [{"sku": "SKU123", "quantity": 2}]}`
|
||||
req := httptest.NewRequest("POST", "/", strings.NewReader(reqBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusCreated {
|
||||
t.Fatalf("expected 201 Created, got %d. Body: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var resp CheckoutResponse
|
||||
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 checkout session id")
|
||||
}
|
||||
if resp.CartId != createdCartId.String() {
|
||||
t.Fatalf("expected cartId %q to match created implicit cart %q", resp.CartId, createdCartId.String())
|
||||
}
|
||||
}
|
||||
@@ -145,6 +145,7 @@ type Totals struct {
|
||||
type CreateCheckoutRequest struct {
|
||||
CartId string `json:"cartId"`
|
||||
Items []CartItemInput `json:"items,omitempty"`
|
||||
LineItems []CartItemInput `json:"line_items,omitempty"`
|
||||
Country string `json:"country,omitempty"`
|
||||
Currency string `json:"currency,omitempty"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user