package main import ( "bytes" "context" "encoding/json" "fmt" "testing" "time" messages "git.tornberg.me/go-cart-actor/proto" "google.golang.org/grpc" ) // TestCartActorMutationAndState validates end-to-end gRPC mutation + state retrieval // against a locally started gRPC server (single-node scenario). func TestCartActorMutationAndState(t *testing.T) { // Setup local grain pool + synced pool (no discovery, single host) pool := NewGrainLocalPool(1024, time.Minute, spawn) synced, err := NewSyncedPool(pool, "127.0.0.1", nil) if err != nil { t.Fatalf("NewSyncedPool error: %v", err) } // Start gRPC server (CartActor + ControlPlane) on :1337 grpcSrv, err := StartGRPCServer(":1337", pool, synced) if err != nil { t.Fatalf("StartGRPCServer error: %v", err) } defer grpcSrv.GracefulStop() // Dial the local server ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() conn, err := grpc.DialContext(ctx, "127.0.0.1:1337", grpc.WithInsecure(), grpc.WithBlock(), ) if err != nil { t.Fatalf("grpc.Dial error: %v", err) } defer conn.Close() cartClient := messages.NewCartActorClient(conn) // Create a short cart id (<=16 chars so it fits into the fixed CartId 16-byte array cleanly) cartID := fmt.Sprintf("cart-%d", time.Now().UnixNano()) // Build an AddRequest payload (quantity=1, sku=test-sku) addReq := &messages.AddRequest{ Quantity: 1, Sku: "test-sku", Country: "se", } // Marshal underlying mutation payload using the existing handler code path // We can directly marshal with proto since envelope expects raw bytes handler, ok := Handlers[AddRequestType] if !ok { t.Fatalf("Handler for AddRequestType missing") } payloadData, err := getSerializedPayload(handler, addReq) if err != nil { t.Fatalf("serialize add request: %v", err) } // Issue Mutate RPC mutResp, err := cartClient.Mutate(context.Background(), &messages.MutationRequest{ CartId: cartID, Type: messages.MutationType(AddRequestType), Payload: payloadData, ClientTimestamp: time.Now().Unix(), }) if err != nil { t.Fatalf("Mutate RPC error: %v", err) } if mutResp.StatusCode != 200 { t.Fatalf("Mutate returned non-200 status: %d payload=%s", mutResp.StatusCode, string(mutResp.Payload)) } // Decode cart state JSON and validate state := &CartGrain{} if err := json.Unmarshal(mutResp.Payload, state); err != nil { t.Fatalf("Unmarshal mutate cart state: %v\nPayload: %s", err, string(mutResp.Payload)) } if len(state.Items) != 1 { t.Fatalf("Expected 1 item after mutation, got %d", len(state.Items)) } if state.Items[0].Sku != "test-sku" { t.Fatalf("Unexpected item SKU: %s", state.Items[0].Sku) } // Issue GetState RPC getResp, err := cartClient.GetState(context.Background(), &messages.StateRequest{ CartId: cartID, }) if err != nil { t.Fatalf("GetState RPC error: %v", err) } if getResp.StatusCode != 200 { t.Fatalf("GetState returned non-200 status: %d payload=%s", getResp.StatusCode, string(getResp.Payload)) } state2 := &CartGrain{} if err := json.Unmarshal(getResp.Payload, state2); err != nil { t.Fatalf("Unmarshal get state: %v", err) } if len(state2.Items) != 1 { t.Fatalf("Expected 1 item in GetState, got %d", len(state2.Items)) } if state2.Items[0].Sku != "test-sku" { t.Fatalf("Unexpected SKU in GetState: %s", state2.Items[0].Sku) } } // getSerializedPayload serializes a mutation proto using the registered handler. func getSerializedPayload(handler MessageHandler, content interface{}) ([]byte, error) { msg := &Message{ Type: AddRequestType, Content: content, } var buf bytes.Buffer if err := handler.Write(msg, &buf); err != nil { return nil, err } return buf.Bytes(), nil }