package lifecycleevents import ( "context" "strconv" "testing" "time" "galaxy/user/internal/domain/common" "galaxy/user/internal/ports" "github.com/alicebob/miniredis/v2" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/require" ) func TestPublisherPublishesPermanentBlockedEnvelope(t *testing.T) { t.Parallel() server := miniredis.RunT(t) publisher, err := New(redis.NewClient(&redis.Options{Addr: server.Addr()}), Config{ Stream: "user:lifecycle_events", StreamMaxLen: 10, OperationTimeout: time.Second, }) require.NoError(t, err) occurredAt := time.Unix(1_775_240_000, 0).UTC() require.NoError(t, publisher.PublishUserLifecycleEvent(context.Background(), ports.UserLifecycleEvent{ EventType: ports.UserLifecyclePermanentBlockedEventType, UserID: common.UserID("user-123"), OccurredAt: occurredAt, Source: common.Source("admin_internal_api"), Actor: common.ActorRef{Type: common.ActorType("admin"), ID: common.ActorID("admin-1")}, ReasonCode: common.ReasonCode("terminal_policy_violation"), TraceID: "4bf92f3577b34da6a3ce929d0e0e4736", })) entries, err := publisher.client.XRange(context.Background(), publisher.stream, "-", "+").Result() require.NoError(t, err) require.Len(t, entries, 1) values := entries[0].Values require.Equal(t, string(ports.UserLifecyclePermanentBlockedEventType), values["event_type"]) require.Equal(t, "user-123", values["user_id"]) require.Equal(t, strconv.FormatInt(occurredAt.UnixMilli(), 10), values["occurred_at_ms"]) require.Equal(t, "admin_internal_api", values["source"]) require.Equal(t, "admin", values["actor_type"]) require.Equal(t, "admin-1", values["actor_id"]) require.Equal(t, "terminal_policy_violation", values["reason_code"]) require.Equal(t, "4bf92f3577b34da6a3ce929d0e0e4736", values["trace_id"]) } func TestPublisherOmitsOptionalActorIDAndTraceID(t *testing.T) { t.Parallel() server := miniredis.RunT(t) publisher, err := New(redis.NewClient(&redis.Options{Addr: server.Addr()}), Config{ Stream: "user:lifecycle_events", StreamMaxLen: 10, OperationTimeout: time.Second, }) require.NoError(t, err) require.NoError(t, publisher.PublishUserLifecycleEvent(context.Background(), ports.UserLifecycleEvent{ EventType: ports.UserLifecycleDeletedEventType, UserID: common.UserID("user-123"), OccurredAt: time.Unix(1_775_240_000, 0).UTC(), Source: common.Source("admin_internal_api"), Actor: common.ActorRef{Type: common.ActorType("admin")}, ReasonCode: common.ReasonCode("user_right_to_be_forgotten"), })) entries, err := publisher.client.XRange(context.Background(), publisher.stream, "-", "+").Result() require.NoError(t, err) require.Len(t, entries, 1) values := entries[0].Values _, hasActorID := values["actor_id"] require.False(t, hasActorID) _, hasTraceID := values["trace_id"] require.False(t, hasTraceID) require.Equal(t, string(ports.UserLifecycleDeletedEventType), values["event_type"]) } func TestPublisherRejectsInvalidEventBeforeXAdd(t *testing.T) { t.Parallel() server := miniredis.RunT(t) publisher, err := New(redis.NewClient(&redis.Options{Addr: server.Addr()}), Config{ Stream: "user:lifecycle_events", StreamMaxLen: 10, OperationTimeout: time.Second, }) require.NoError(t, err) err = publisher.PublishUserLifecycleEvent(context.Background(), ports.UserLifecycleEvent{ EventType: "user.lifecycle.unknown", UserID: common.UserID("user-123"), OccurredAt: time.Unix(1_775_240_000, 0).UTC(), Source: common.Source("admin_internal_api"), Actor: common.ActorRef{Type: common.ActorType("admin")}, ReasonCode: common.ReasonCode("manual_block"), }) require.Error(t, err) length, xLenErr := publisher.client.XLen(context.Background(), publisher.stream).Result() require.NoError(t, xLenErr) require.Zero(t, length) } func TestPublisherTrimsBeyondMaxLen(t *testing.T) { t.Parallel() server := miniredis.RunT(t) publisher, err := New(redis.NewClient(&redis.Options{Addr: server.Addr()}), Config{ Stream: "user:lifecycle_events", StreamMaxLen: 5, OperationTimeout: time.Second, }) require.NoError(t, err) occurredAt := time.Unix(1_775_240_000, 0).UTC() for index := 0; index < 20; index++ { require.NoError(t, publisher.PublishUserLifecycleEvent(context.Background(), ports.UserLifecycleEvent{ EventType: ports.UserLifecyclePermanentBlockedEventType, UserID: common.UserID("user-123"), OccurredAt: occurredAt.Add(time.Duration(index+1) * time.Second), Source: common.Source("admin_internal_api"), Actor: common.ActorRef{Type: common.ActorType("admin")}, ReasonCode: common.ReasonCode("terminal_policy_violation"), })) } length, err := publisher.client.XLen(context.Background(), publisher.stream).Result() require.NoError(t, err) require.LessOrEqual(t, length, int64(20)) } func TestPublisherPingReportsReachability(t *testing.T) { t.Parallel() server := miniredis.RunT(t) publisher, err := New(redis.NewClient(&redis.Options{Addr: server.Addr()}), Config{ Stream: "user:lifecycle_events", StreamMaxLen: 10, OperationTimeout: time.Second, }) require.NoError(t, err) require.NoError(t, publisher.Ping(context.Background())) }