package redisstate import ( "context" "testing" "time" "galaxy/mail/internal/domain/attempt" deliverydomain "galaxy/mail/internal/domain/delivery" "github.com/alicebob/miniredis/v2" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/require" ) func TestIndexCleanerRemovesStaleMembersAfterDeliveryExpiry(t *testing.T) { t.Parallel() server := miniredis.RunT(t) client := redis.NewClient(&redis.Options{Addr: server.Addr()}) t.Cleanup(func() { require.NoError(t, client.Close()) }) writer, err := NewAtomicWriter(client) require.NoError(t, err) cleaner, err := NewIndexCleaner(client) require.NoError(t, err) record := validDelivery(t) record.Source = deliverydomain.SourceNotification record.ResendParentDeliveryID = "" record.Status = deliverydomain.StatusQueued record.SentAt = nil record.LocaleFallbackUsed = false record.UpdatedAt = record.CreatedAt.Add(time.Minute) require.NoError(t, record.Validate()) input := CreateAcceptanceInput{ Delivery: record, FirstAttempt: ptr(validScheduledAttempt(t, record.DeliveryID)), Idempotency: ptr(validIdempotencyRecord(t, record.Source, record.DeliveryID, record.IdempotencyKey)), } require.NoError(t, writer.CreateAcceptance(context.Background(), input)) deadLetterEntry := validDeadLetterEntry(t, record.DeliveryID) deadLetterPayload, err := MarshalDeadLetter(deadLetterEntry) require.NoError(t, err) require.NoError(t, client.Set(context.Background(), Keyspace{}.DeadLetter(record.DeliveryID), deadLetterPayload, DeadLetterTTL).Err()) server.FastForward(DeliveryTTL + time.Second) require.False(t, server.Exists(Keyspace{}.Delivery(record.DeliveryID))) require.True(t, server.Exists(Keyspace{}.Attempt(record.DeliveryID, input.FirstAttempt.AttemptNo))) require.True(t, server.Exists(Keyspace{}.DeadLetter(record.DeliveryID))) report, err := cleaner.CleanDeliveryIndexes(context.Background()) require.NoError(t, err) require.Positive(t, report.ScannedIndexes) require.Positive(t, report.ScannedMembers) require.Positive(t, report.RemovedMembers) assertZCard := func(key string, want int64) { t.Helper() got, err := client.ZCard(context.Background(), key).Result() require.NoError(t, err) require.Equal(t, want, got) } assertZCard(Keyspace{}.CreatedAtIndex(), 0) assertZCard(Keyspace{}.SourceIndex(record.Source), 0) assertZCard(Keyspace{}.StatusIndex(record.Status), 0) assertZCard(Keyspace{}.RecipientIndex(record.Envelope.To[0]), 0) assertZCard(Keyspace{}.RecipientIndex(record.Envelope.Cc[0]), 0) assertZCard(Keyspace{}.RecipientIndex(record.Envelope.Bcc[0]), 0) assertZCard(Keyspace{}.TemplateIndex(record.TemplateID), 0) assertZCard(Keyspace{}.IdempotencyIndex(record.Source, record.IdempotencyKey), 0) require.True(t, server.Exists(Keyspace{}.Attempt(record.DeliveryID, input.FirstAttempt.AttemptNo))) require.True(t, server.Exists(Keyspace{}.DeadLetter(record.DeliveryID))) scheduleCard, err := client.ZCard(context.Background(), Keyspace{}.AttemptSchedule()).Result() require.NoError(t, err) require.EqualValues(t, 1, scheduleCard) } func TestIndexCleanerSkipsMalformedCommandIndex(t *testing.T) { t.Parallel() server := miniredis.RunT(t) client := redis.NewClient(&redis.Options{Addr: server.Addr()}) t.Cleanup(func() { require.NoError(t, client.Close()) }) cleaner, err := NewIndexCleaner(client) require.NoError(t, err) entry := validMalformedCommandEntry(t) require.NoError(t, client.ZAdd(context.Background(), Keyspace{}.MalformedCommandCreatedAtIndex(), redis.Z{ Score: float64(entry.RecordedAt.UTC().UnixMilli()), Member: entry.StreamEntryID, }).Err()) report, err := cleaner.CleanDeliveryIndexes(context.Background()) require.NoError(t, err) require.Zero(t, report.ScannedIndexes) require.Zero(t, report.ScannedMembers) require.Zero(t, report.RemovedMembers) indexMembers, err := client.ZRange(context.Background(), Keyspace{}.MalformedCommandCreatedAtIndex(), 0, -1).Result() require.NoError(t, err) require.Equal(t, []string{entry.StreamEntryID}, indexMembers) } var _ = attempt.Attempt{}