package mq import ( "context" "errors" "log/slog" "testing" "time" "kra/internal/integration/runtimeconfig" platformmq "kra/pkg/mq" ) type fakeClient struct { subscribed []string unsubscribed []string handlers map[string]platformmq.Handler subscribeErrors map[string]error unsubscribeErrors map[string]error } func (*fakeClient) Publish(context.Context, string, []byte, byte, bool) error { return nil } func (f *fakeClient) Subscribe(_ context.Context, topic string, _ byte, handler platformmq.Handler) error { f.subscribed = append(f.subscribed, topic) if err := f.subscribeErrors[topic]; err != nil { return err } if f.handlers == nil { f.handlers = make(map[string]platformmq.Handler) } f.handlers[topic] = handler return nil } func (f *fakeClient) Unsubscribe(_ context.Context, topics ...string) error { f.unsubscribed = append(f.unsubscribed, topics...) for _, topic := range topics { if err := f.unsubscribeErrors[topic]; err != nil { return err } } return nil } func (*fakeClient) Connected() bool { return true } func (*fakeClient) Close() error { return nil } func TestReloadableTracksSubscriptions(t *testing.T) { client := &fakeClient{} r := &Reloadable{ clients: map[string]platformmq.Client{ProviderEMQX: client}, configs: map[string]runtimeconfig.Config{ProviderEMQX: {Kind: "mq", Provider: ProviderEMQX, Enabled: true}}, subscriptions: make(map[string]map[string]map[string]subscription), bindings: make(map[string]map[string]byte), pending: make(map[string]bool), nextRetry: make(map[string]time.Time), } handler := func(context.Context, platformmq.Message) {} if err := r.Subscribe(context.Background(), "orders/+/paid", platformmq.AtLeastOnce, handler); err != nil { t.Fatal(err) } if _, ok := r.subscriptions[ProviderEMQX]["orders/+/paid"]; !ok { t.Fatal("subscription was not retained for configuration reload") } if err := r.Unsubscribe(context.Background(), "orders/+/paid"); err != nil { t.Fatal(err) } if _, ok := r.subscriptions[ProviderEMQX]["orders/+/paid"]; ok { t.Fatal("unsubscribed topic remained in the reload registry") } } func TestReloadableRestoresSubscriptions(t *testing.T) { client := &fakeClient{} r := &Reloadable{subscriptions: map[string]map[string]map[string]subscription{ ProviderEMQX: { "orders/+/paid": { "orders": {qos: platformmq.AtLeastOnce, handler: func(context.Context, platformmq.Message) {}}, }, }, }} bindings, err := r.restoreSubscriptionsLocked(ProviderEMQX, client) if err != nil { t.Fatal(err) } if len(client.subscribed) != 1 || client.subscribed[0] != "orders/+/paid" { t.Fatalf("restored subscriptions = %v", client.subscribed) } if bindings["orders/+/paid"] != platformmq.AtLeastOnce { t.Fatalf("restored binding qos = %d", bindings["orders/+/paid"]) } } func TestReloadableRegistersWhileOfflineAndRestoresLater(t *testing.T) { client := &fakeClient{} r := &Reloadable{ clients: make(map[string]platformmq.Client), configs: map[string]runtimeconfig.Config{ProviderEMQX: {Kind: "mq", Provider: ProviderEMQX, Enabled: true}}, subscriptions: make(map[string]map[string]map[string]subscription), bindings: make(map[string]map[string]byte), pending: make(map[string]bool), nextRetry: make(map[string]time.Time), logger: slog.Default(), } called := make(chan struct{}, 1) err := r.Register(platformmq.SubscriptionSet{ Owner: "orders", Provider: ProviderEMQX, Topics: []platformmq.TopicSubscription{{Topic: "orders.created", QoS: platformmq.AtLeastOnce, Handler: func(context.Context, platformmq.Message) { called <- struct{}{} }}}, }) if err != nil { t.Fatal(err) } if !r.pending[ProviderEMQX] { t.Fatal("offline subscription was not marked pending") } r.clients[ProviderEMQX] = client if err = r.reconcileProviderLocked(ProviderEMQX); err != nil { t.Fatal(err) } if len(client.subscribed) != 1 || client.subscribed[0] != "orders.created" { t.Fatalf("restored subscriptions = %v", client.subscribed) } } func TestReloadableDispatchesSameTopicToMultipleOwners(t *testing.T) { client := &fakeClient{} r := &Reloadable{ clients: map[string]platformmq.Client{ProviderEMQX: client}, configs: map[string]runtimeconfig.Config{ProviderEMQX: {Kind: "mq", Provider: ProviderEMQX, Enabled: true}}, subscriptions: make(map[string]map[string]map[string]subscription), bindings: make(map[string]map[string]byte), pending: make(map[string]bool), nextRetry: make(map[string]time.Time), logger: slog.Default(), } first, second := make(chan struct{}, 1), make(chan struct{}, 1) for _, set := range []platformmq.SubscriptionSet{ {Owner: "orders", Provider: ProviderEMQX, Topics: []platformmq.TopicSubscription{{Topic: "events.created", Handler: func(context.Context, platformmq.Message) { first <- struct{}{} }}}}, {Owner: "audit", Provider: ProviderEMQX, Topics: []platformmq.TopicSubscription{{Topic: "events.created", Handler: func(context.Context, platformmq.Message) { second <- struct{}{} }}}}, } { if err := r.Register(set); err != nil { t.Fatal(err) } } if err := r.reconcileProviderLocked(ProviderEMQX); err != nil { t.Fatalf("explicit reconcile failed: %v", err) } if len(client.subscribed) != 1 { t.Fatalf("broker subscriptions = %v, want one shared topic; desired=%#v bindings=%#v clients=%#v", client.subscribed, r.subscriptions, r.bindings, r.clients) } client.handlers["events.created"](context.Background(), platformmq.Message{Topic: "events.created"}) select { case <-first: default: t.Fatal("orders handler was not called") } select { case <-second: default: t.Fatal("audit handler was not called") } } func TestReloadablePersistsPartialBindingChangesWhenReconcileFails(t *testing.T) { client := &fakeClient{subscribeErrors: map[string]error{"events.new": errors.New("subscribe failed")}} r := &Reloadable{ clients: map[string]platformmq.Client{ProviderEMQX: client}, subscriptions: map[string]map[string]map[string]subscription{ ProviderEMQX: { "events.keep": {"owner": {qos: platformmq.AtMostOnce, handler: func(context.Context, platformmq.Message) {}}}, "events.new": {"owner": {qos: platformmq.AtMostOnce, handler: func(context.Context, platformmq.Message) {}}}, }, }, bindings: map[string]map[string]byte{ProviderEMQX: { "events.old": platformmq.AtMostOnce, "events.keep": platformmq.AtMostOnce, }}, pending: make(map[string]bool), nextRetry: make(map[string]time.Time), } if err := r.reconcileProviderLocked(ProviderEMQX); err == nil { t.Fatal("reconcileProviderLocked() error = nil, want subscribe failure") } if _, exists := r.bindings[ProviderEMQX]["events.old"]; exists { t.Fatal("successfully removed binding remained after failed reconcile") } if _, exists := r.bindings[ProviderEMQX]["events.keep"]; !exists { t.Fatal("unchanged binding was lost after failed reconcile") } if _, exists := r.bindings[ProviderEMQX]["events.new"]; exists { t.Fatal("failed subscription was recorded as bound") } } func TestReloadableLegacySubscribeDoesNotLeaveOfflineDeclaration(t *testing.T) { r := &Reloadable{ clients: make(map[string]platformmq.Client), configs: map[string]runtimeconfig.Config{ProviderEMQX: {Kind: "mq", Provider: ProviderEMQX, Enabled: true}}, subscriptions: make(map[string]map[string]map[string]subscription), bindings: make(map[string]map[string]byte), pending: make(map[string]bool), nextRetry: make(map[string]time.Time), } err := r.Subscribe(context.Background(), "events.offline", platformmq.AtLeastOnce, func(context.Context, platformmq.Message) {}) if !errors.Is(err, platformmq.ErrUnavailable) { t.Fatalf("Subscribe() error = %v, want ErrUnavailable", err) } if len(r.subscriptions[ProviderEMQX]) != 0 { t.Fatalf("offline legacy declaration remained: %#v", r.subscriptions[ProviderEMQX]) } }