package actor import ( "context" "reflect" "slices" "testing" "git.k6n.net/go-cart-actor/pkg/messages" ) type cartState struct { calls int lastAdded *messages.AddItem } func TestRegisteredMutationBasics(t *testing.T) { reg := NewMutationRegistry().(*ProtoMutationRegistry) addItemMutation := NewMutation( func(state *cartState, msg *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 }, func() *messages.AddItem { return &messages.AddItem{} }, ) // 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(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(messages.AddItem{})) { t.Fatalf("RegisteredMutationTypes missing AddItem type, got %v", types) } // GetTypeName should resolve for a pointer instance name, ok := reg.GetTypeName(&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(&messages.Noop{}); 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.(*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 := &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, &messages.Noop{}) if err != ErrMutationNotRegistered { t.Fatalf("expected ErrMutationNotRegistered, got %v", err) } } // func TestConcurrentSafeRegistrationLookup(t *testing.T) { // // This test is light-weight; it ensures locks don't deadlock under simple concurrent access. // reg := NewMutationRegistry().(*ProtoMutationRegistry) // mut := NewMutation[cartState, *messages.Noop]( // func(state *cartState, msg *messages.Noop) error { state.calls++; return nil }, // func() *messages.Noop { return &messages.Noop{} }, // ) // reg.RegisterMutations(mut) // done := make(chan struct{}) // const workers = 25 // for i := 0; i < workers; i++ { // go func() { // for j := 0; j < 100; j++ { // _, _ = reg.Create("Noop") // _, _ = reg.GetTypeName(&messages.Noop{}) // _ = reg.Apply(&cartState{}, &messages.Noop{}) // } // done <- struct{}{} // }() // } // for i := 0; i < workers; i++ { // <-done // } // } // Helpers