package actor import ( "context" "reflect" "slices" "testing" "time" cart_messages "git.k6n.net/go-cart-actor/proto/cart" ) type cartState struct { calls int lastAdded *cart_messages.AddItem } func TestRegisteredMutationBasics(t *testing.T) { reg := NewMutationRegistry().(*ProtoMutationRegistry) addItemMutation := NewMutation( func(state *cartState, msg *cart_messages.AddItem) error { state.calls++ // copy to avoid external mutation side-effects (not strictly necessary for the test) cp := msg state.lastAdded = cp return nil }, ) // Sanity check on mutation metadata if addItemMutation.Name() != "AddItem" { t.Fatalf("expected mutation Name() == AddItem, got %s", addItemMutation.Name()) } if got, want := addItemMutation.Type(), reflect.TypeOf(cart_messages.AddItem{}); got != want { t.Fatalf("expected Type() == %v, got %v", want, got) } reg.RegisterMutations(addItemMutation) // RegisteredMutations: membership (order not guaranteed) names := reg.RegisteredMutations() if !slices.Contains(names, "AddItem") { t.Fatalf("RegisteredMutations missing AddItem, got %v", names) } // RegisteredMutationTypes: membership (order not guaranteed) types := reg.RegisteredMutationTypes() if !slices.Contains(types, reflect.TypeOf(cart_messages.AddItem{})) { t.Fatalf("RegisteredMutationTypes missing AddItem type, got %v", types) } // GetTypeName should resolve for a pointer instance name, ok := reg.GetTypeName(&cart_messages.AddItem{}) if !ok || name != "AddItem" { t.Fatalf("GetTypeName returned (%q,%v), expected (AddItem,true)", name, ok) } // GetTypeName should fail for unregistered type if name, ok := reg.GetTypeName(&cart_messages.RemoveItem{}); ok || name != "" { t.Fatalf("expected GetTypeName to fail for unregistered message, got (%q,%v)", name, ok) } // Create by name msg, ok := reg.Create("AddItem") if !ok { t.Fatalf("Create failed for registered mutation") } if _, isAddItem := msg.(*cart_messages.AddItem); !isAddItem { t.Fatalf("Create returned wrong concrete type: %T", msg) } // Create unknown if m2, ok := reg.Create("Unknown"); ok || m2 != nil { t.Fatalf("Create should fail for unknown mutation, got (%T,%v)", m2, ok) } // Apply happy path state := &cartState{} add := &cart_messages.AddItem{ItemId: 42, Quantity: 3, Sku: "ABC"} if _, err := reg.Apply(context.Background(), state, add); err != nil { t.Fatalf("Apply returned error: %v", err) } if state.calls != 1 { t.Fatalf("handler not invoked expected calls=1 got=%d", state.calls) } if state.lastAdded == nil || state.lastAdded.ItemId != 42 || state.lastAdded.Quantity != 3 { t.Fatalf("state not updated correctly: %+v", state.lastAdded) } // Apply nil grain if _, err := reg.Apply(context.Background(), nil, add); err == nil { t.Fatalf("expected error for nil grain") } // Apply nil message if _, err := reg.Apply(context.Background(), state, nil); err == nil { t.Fatalf("expected error for nil mutation message") } // Apply unregistered message _, err := reg.Apply(context.Background(), state, &cart_messages.RemoveItem{}) if err != ErrMutationNotRegistered { t.Fatalf("expected ErrMutationNotRegistered, got %v", err) } } func TestEventChannel(t *testing.T) { reg := NewMutationRegistry().(*ProtoMutationRegistry) addItemMutation := NewMutation( func(state *cartState, msg *cart_messages.AddItem) error { state.calls++ return nil }, ) reg.RegisterMutations(addItemMutation) eventCh := make(chan ApplyResult, 10) reg.SetEventChannel(eventCh) state := &cartState{} add := &cart_messages.AddItem{ItemId: 42, Quantity: 3, Sku: "ABC"} results, err := reg.Apply(context.Background(), state, add) if err != nil { t.Fatalf("Apply returned error: %v", err) } if len(results) != 1 { t.Fatalf("expected 1 result, got %d", len(results)) } // Receive from channel with timeout select { case res := <-eventCh: if res.Type != "AddItem" { t.Fatalf("expected type AddItem, got %s", res.Type) } if res.Error != nil { t.Fatalf("expected no error, got %v", res.Error) } case <-time.After(time.Second): t.Fatalf("expected to receive event on channel within timeout") } } func TestEventChannelClosed(t *testing.T) { reg := NewMutationRegistry().(*ProtoMutationRegistry) addItemMutation := NewMutation( func(state *cartState, msg *cart_messages.AddItem) error { state.calls++ return nil }, ) reg.RegisterMutations(addItemMutation) eventCh := make(chan ApplyResult, 10) reg.SetEventChannel(eventCh) close(eventCh) // Close the channel to simulate external close state := &cartState{} add := &cart_messages.AddItem{ItemId: 42, Quantity: 3, Sku: "ABC"} // This should not panic due to recover in goroutine results, err := reg.Apply(context.Background(), state, add) if err != nil { t.Fatalf("Apply returned error: %v", err) } if len(results) != 1 { t.Fatalf("expected 1 result, got %d", len(results)) } // Test passes if no panic occurs } func TestEventChannelUnbufferedNoListener(t *testing.T) { reg := NewMutationRegistry().(*ProtoMutationRegistry) addItemMutation := NewMutation( func(state *cartState, msg *cart_messages.AddItem) error { state.calls++ return nil }, ) reg.RegisterMutations(addItemMutation) eventCh := make(chan ApplyResult) // unbuffered reg.SetEventChannel(eventCh) // No goroutine reading from eventCh state := &cartState{} add := &cart_messages.AddItem{ItemId: 42, Quantity: 3, Sku: "ABC"} results, err := reg.Apply(context.Background(), state, add) if err != nil { t.Fatalf("Apply returned error: %v", err) } if len(results) != 1 { t.Fatalf("expected 1 result, got %d", len(results)) } // Since no listener, the send should go to default and not block // Test passes if Apply completes without hanging } // Helpers