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())
|
writeError(w, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||||
return
|
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
|
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 {
|
if !ok {
|
||||||
writeError(w, http.StatusBadRequest, "invalid cartId")
|
writeError(w, http.StatusBadRequest, "invalid cartId")
|
||||||
return
|
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 {
|
type CreateCheckoutRequest struct {
|
||||||
CartId string `json:"cartId"`
|
CartId string `json:"cartId"`
|
||||||
Items []CartItemInput `json:"items,omitempty"`
|
Items []CartItemInput `json:"items,omitempty"`
|
||||||
|
LineItems []CartItemInput `json:"line_items,omitempty"`
|
||||||
Country string `json:"country,omitempty"`
|
Country string `json:"country,omitempty"`
|
||||||
Currency string `json:"currency,omitempty"`
|
Currency string `json:"currency,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user