package redisstate import ( "context" "testing" "time" "galaxy/mail/internal/domain/attempt" "galaxy/mail/internal/domain/common" deliverydomain "galaxy/mail/internal/domain/delivery" "galaxy/mail/internal/service/listdeliveries" "galaxy/mail/internal/service/resenddelivery" "github.com/alicebob/miniredis/v2" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/require" ) func TestOperatorStoreListFilters(t *testing.T) { t.Parallel() type testCase struct { name string filters listdeliveries.Filters wantIDs []common.DeliveryID } cases := []testCase{ { name: "recipient", filters: listdeliveries.Filters{Recipient: common.Email("recipient-filter@example.com")}, wantIDs: []common.DeliveryID{"delivery-recipient"}, }, { name: "status", filters: listdeliveries.Filters{Status: deliverydomain.StatusSuppressed}, wantIDs: []common.DeliveryID{"delivery-status"}, }, { name: "source", filters: listdeliveries.Filters{Source: deliverydomain.SourceOperatorResend}, wantIDs: []common.DeliveryID{"delivery-source"}, }, { name: "template", filters: listdeliveries.Filters{TemplateID: common.TemplateID("template.filter")}, wantIDs: []common.DeliveryID{"delivery-template"}, }, { name: "idempotency", filters: listdeliveries.Filters{IdempotencyKey: common.IdempotencyKey("idempotency-filter")}, wantIDs: []common.DeliveryID{"delivery-idempotency"}, }, } for _, tt := range cases { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() store, client := newOperatorStoreForTest(t) seedOperatorFilterDataset(t, client) result, err := store.List(context.Background(), listdeliveries.Input{ Limit: 10, Filters: tt.filters, }) require.NoError(t, err) require.Equal(t, tt.wantIDs, deliveryIDs(result.Items)) require.Nil(t, result.NextCursor) }) } } func TestOperatorStoreListCursorPaginationUsesCreatedAtDescDeliveryIDDesc(t *testing.T) { t.Parallel() store, client := newOperatorStoreForTest(t) createdAt := time.Unix(1_775_122_500, 0).UTC() seedDeliveryRecord(t, client, buildStoredDelivery("delivery-a", createdAt, deliverydomain.SourceNotification, common.IdempotencyKey("notification:delivery-a"), deliverydomain.StatusSent)) seedDeliveryRecord(t, client, buildStoredDelivery("delivery-c", createdAt, deliverydomain.SourceNotification, common.IdempotencyKey("notification:delivery-c"), deliverydomain.StatusSent)) seedDeliveryRecord(t, client, buildStoredDelivery("delivery-b", createdAt, deliverydomain.SourceNotification, common.IdempotencyKey("notification:delivery-b"), deliverydomain.StatusSent)) firstPage, err := store.List(context.Background(), listdeliveries.Input{Limit: 2}) require.NoError(t, err) require.Equal(t, []common.DeliveryID{"delivery-c", "delivery-b"}, deliveryIDs(firstPage.Items)) require.NotNil(t, firstPage.NextCursor) secondPage, err := store.List(context.Background(), listdeliveries.Input{ Limit: 2, Cursor: firstPage.NextCursor, }) require.NoError(t, err) require.Equal(t, []common.DeliveryID{"delivery-a"}, deliveryIDs(secondPage.Items)) require.Nil(t, secondPage.NextCursor) } func TestOperatorStoreListMergesIdempotencyAcrossSources(t *testing.T) { t.Parallel() store, client := newOperatorStoreForTest(t) sharedKey := common.IdempotencyKey("shared-idempotency") seedDeliveryRecord(t, client, buildStoredDelivery("delivery-auth", time.Unix(1_775_122_100, 0).UTC(), deliverydomain.SourceAuthSession, sharedKey, deliverydomain.StatusSuppressed)) seedDeliveryRecord(t, client, buildStoredDelivery("delivery-notification", time.Unix(1_775_122_200, 0).UTC(), deliverydomain.SourceNotification, sharedKey, deliverydomain.StatusSent)) seedDeliveryRecord(t, client, buildStoredDelivery("delivery-resend", time.Unix(1_775_122_300, 0).UTC(), deliverydomain.SourceOperatorResend, sharedKey, deliverydomain.StatusSent)) result, err := store.List(context.Background(), listdeliveries.Input{ Limit: 10, Filters: listdeliveries.Filters{ IdempotencyKey: sharedKey, }, }) require.NoError(t, err) require.Equal(t, []common.DeliveryID{"delivery-resend", "delivery-notification", "delivery-auth"}, deliveryIDs(result.Items)) } func TestOperatorStoreGetDeadLetter(t *testing.T) { t.Parallel() store, client := newOperatorStoreForTest(t) record := buildStoredDelivery("delivery-dead-letter", time.Unix(1_775_122_400, 0).UTC(), deliverydomain.SourceNotification, common.IdempotencyKey("notification:delivery-dead-letter"), deliverydomain.StatusDeadLetter) seedDeliveryRecord(t, client, record) entry := validDeadLetterEntry(t, record.DeliveryID) payload, err := MarshalDeadLetter(entry) require.NoError(t, err) require.NoError(t, client.Set(context.Background(), Keyspace{}.DeadLetter(record.DeliveryID), payload, DeadLetterTTL).Err()) got, found, err := store.GetDeadLetter(context.Background(), record.DeliveryID) require.NoError(t, err) require.True(t, found) require.Equal(t, entry, got) } func TestOperatorStoreListAttempts(t *testing.T) { t.Parallel() store, client := newOperatorStoreForTest(t) record := buildStoredDelivery("delivery-attempts", time.Unix(1_775_122_410, 0).UTC(), deliverydomain.SourceNotification, common.IdempotencyKey("notification:delivery-attempts"), deliverydomain.StatusFailed) record.AttemptCount = 2 failedAt := record.UpdatedAt record.FailedAt = &failedAt require.NoError(t, record.Validate()) seedDeliveryRecord(t, client, record) firstAttempt := validTerminalAttempt(t, record.DeliveryID) firstAttempt.AttemptNo = 1 secondAttempt := validTerminalAttempt(t, record.DeliveryID) secondAttempt.AttemptNo = 2 secondAttempt.Status = attempt.StatusProviderRejected payload, err := MarshalAttempt(firstAttempt) require.NoError(t, err) require.NoError(t, client.Set(context.Background(), Keyspace{}.Attempt(record.DeliveryID, 1), payload, AttemptTTL).Err()) payload, err = MarshalAttempt(secondAttempt) require.NoError(t, err) require.NoError(t, client.Set(context.Background(), Keyspace{}.Attempt(record.DeliveryID, 2), payload, AttemptTTL).Err()) got, err := store.ListAttempts(context.Background(), record.DeliveryID, 2) require.NoError(t, err) require.Equal(t, []attempt.Attempt{firstAttempt, secondAttempt}, got) } func TestOperatorStoreCreateResendAtomicallyCreatesCloneState(t *testing.T) { t.Parallel() store, client := newOperatorStoreForTest(t) createdAt := time.Unix(1_775_122_600, 0).UTC() clone := buildStoredDelivery("delivery-clone", createdAt, deliverydomain.SourceOperatorResend, common.IdempotencyKey("operator:resend:delivery-parent"), deliverydomain.StatusQueued) clone.ResendParentDeliveryID = common.DeliveryID("delivery-parent") clone.AttemptCount = 1 require.NoError(t, clone.Validate()) firstAttempt := validScheduledAttempt(t, clone.DeliveryID) firstAttempt.AttemptNo = 1 firstAttempt.ScheduledFor = createdAt require.NoError(t, firstAttempt.Validate()) deliveryPayload := validDeliveryPayload(t, clone.DeliveryID) input := resenddelivery.CreateResendInput{ Delivery: clone, FirstAttempt: firstAttempt, DeliveryPayload: &deliveryPayload, } require.NoError(t, store.CreateResend(context.Background(), input)) storedDelivery, found, err := store.GetDelivery(context.Background(), clone.DeliveryID) require.NoError(t, err) require.True(t, found) require.Equal(t, clone, storedDelivery) storedPayload, found, err := store.GetDeliveryPayload(context.Background(), clone.DeliveryID) require.NoError(t, err) require.True(t, found) require.Equal(t, deliveryPayload, storedPayload) attemptPayload, err := client.Get(context.Background(), Keyspace{}.Attempt(clone.DeliveryID, 1)).Bytes() require.NoError(t, err) decodedAttempt, err := UnmarshalAttempt(attemptPayload) require.NoError(t, err) require.Equal(t, firstAttempt, decodedAttempt) scheduledMembers, err := client.ZRange(context.Background(), Keyspace{}.AttemptSchedule(), 0, -1).Result() require.NoError(t, err) require.Equal(t, []string{clone.DeliveryID.String()}, scheduledMembers) indexMembers, err := client.ZRange(context.Background(), Keyspace{}.IdempotencyIndex(clone.Source, clone.IdempotencyKey), 0, -1).Result() require.NoError(t, err) require.Equal(t, []string{clone.DeliveryID.String()}, indexMembers) _, err = client.Get(context.Background(), Keyspace{}.Idempotency(clone.Source, clone.IdempotencyKey)).Bytes() require.ErrorIs(t, err, redis.Nil) } func newOperatorStoreForTest(t *testing.T) (*OperatorStore, *redis.Client) { t.Helper() server := miniredis.RunT(t) client := redis.NewClient(&redis.Options{Addr: server.Addr()}) t.Cleanup(func() { require.NoError(t, client.Close()) }) store, err := NewOperatorStore(client) require.NoError(t, err) return store, client } func seedOperatorFilterDataset(t *testing.T, client *redis.Client) { t.Helper() seedDeliveryRecord(t, client, func() deliverydomain.Delivery { record := buildStoredDelivery("delivery-recipient", time.Unix(1_775_122_001, 0).UTC(), deliverydomain.SourceNotification, common.IdempotencyKey("notification:delivery-recipient"), deliverydomain.StatusSent) record.Envelope.To = []common.Email{common.Email("recipient-filter@example.com")} require.NoError(t, record.Validate()) return record }()) seedDeliveryRecord(t, client, func() deliverydomain.Delivery { record := buildStoredDelivery("delivery-status", time.Unix(1_775_122_002, 0).UTC(), deliverydomain.SourceAuthSession, common.IdempotencyKey("authsession:delivery-status"), deliverydomain.StatusSuppressed) record.SentAt = nil suppressedAt := record.UpdatedAt record.SuppressedAt = &suppressedAt require.NoError(t, record.Validate()) return record }()) seedDeliveryRecord(t, client, buildStoredDelivery("delivery-source", time.Unix(1_775_122_003, 0).UTC(), deliverydomain.SourceOperatorResend, common.IdempotencyKey("operator:resend:delivery-source"), deliverydomain.StatusSent)) seedDeliveryRecord(t, client, func() deliverydomain.Delivery { record := buildStoredDelivery("delivery-template", time.Unix(1_775_122_004, 0).UTC(), deliverydomain.SourceNotification, common.IdempotencyKey("notification:delivery-template"), deliverydomain.StatusSent) record.TemplateID = common.TemplateID("template.filter") record.PayloadMode = deliverydomain.PayloadModeTemplate record.Locale = common.Locale("en") record.TemplateVariables = map[string]any{"name": "Pilot"} require.NoError(t, record.Validate()) return record }()) seedDeliveryRecord(t, client, buildStoredDelivery("delivery-idempotency", time.Unix(1_775_122_005, 0).UTC(), deliverydomain.SourceNotification, common.IdempotencyKey("idempotency-filter"), deliverydomain.StatusSent)) } func seedDeliveryRecord(t *testing.T, client *redis.Client, record deliverydomain.Delivery) { t.Helper() keyspace := Keyspace{} payload, err := MarshalDelivery(record) require.NoError(t, err) require.NoError(t, client.Set(context.Background(), keyspace.Delivery(record.DeliveryID), payload, DeliveryTTL).Err()) score := CreatedAtScore(record.CreatedAt) for _, indexKey := range keyspace.DeliveryIndexKeys(record) { require.NoError(t, client.ZAdd(context.Background(), indexKey, redis.Z{ Score: score, Member: record.DeliveryID.String(), }).Err()) } } func buildStoredDelivery( deliveryID string, createdAt time.Time, source deliverydomain.Source, idempotencyKey common.IdempotencyKey, status deliverydomain.Status, ) deliverydomain.Delivery { updatedAt := createdAt.Add(time.Minute) record := deliverydomain.Delivery{ DeliveryID: common.DeliveryID(deliveryID), Source: source, PayloadMode: deliverydomain.PayloadModeRendered, Envelope: deliverydomain.Envelope{ To: []common.Email{common.Email("pilot@example.com")}, }, Content: deliverydomain.Content{ Subject: "Test subject", TextBody: "Test body", }, IdempotencyKey: idempotencyKey, Status: status, CreatedAt: createdAt, UpdatedAt: updatedAt, } switch status { case deliverydomain.StatusSent: record.AttemptCount = 1 record.LastAttemptStatus = attempt.StatusProviderAccepted sentAt := updatedAt record.SentAt = &sentAt case deliverydomain.StatusSuppressed: suppressedAt := updatedAt record.SuppressedAt = &suppressedAt case deliverydomain.StatusFailed: record.AttemptCount = 1 record.LastAttemptStatus = attempt.StatusProviderRejected failedAt := updatedAt record.FailedAt = &failedAt case deliverydomain.StatusDeadLetter: record.AttemptCount = 1 record.LastAttemptStatus = attempt.StatusTimedOut deadLetteredAt := updatedAt record.DeadLetteredAt = &deadLetteredAt default: record.AttemptCount = 1 } if source == deliverydomain.SourceOperatorResend { record.ResendParentDeliveryID = common.DeliveryID("parent-" + deliveryID) } if err := record.Validate(); err != nil { panic(err) } return record } func deliveryIDs(records []deliverydomain.Delivery) []common.DeliveryID { result := make([]common.DeliveryID, len(records)) for index, record := range records { result[index] = record.DeliveryID } return result }