package challenge import ( "crypto/ed25519" "github.com/stretchr/testify/require" "testing" "time" "galaxy/authsession/internal/domain/common" ) func TestPolicyConstants(t *testing.T) { t.Parallel() if InitialTTL != 5*time.Minute { require.Failf(t, "test failed", "InitialTTL = %s, want %s", InitialTTL, 5*time.Minute) } if ResendThrottleCooldown != time.Minute { require.Failf(t, "test failed", "ResendThrottleCooldown = %s, want %s", ResendThrottleCooldown, time.Minute) } if ConfirmedRetention != 5*time.Minute { require.Failf(t, "test failed", "ConfirmedRetention = %s, want %s", ConfirmedRetention, 5*time.Minute) } if MaxInvalidConfirmAttempts != 5 { require.Failf(t, "test failed", "MaxInvalidConfirmAttempts = %d, want %d", MaxInvalidConfirmAttempts, 5) } } func TestStatusIsKnown(t *testing.T) { t.Parallel() tests := []struct { name string value Status want bool }{ {name: "pending send", value: StatusPendingSend, want: true}, {name: "sent", value: StatusSent, want: true}, {name: "suppressed", value: StatusDeliverySuppressed, want: true}, {name: "throttled", value: StatusDeliveryThrottled, want: true}, {name: "confirmed", value: StatusConfirmedPendingExpire, want: true}, {name: "expired", value: StatusExpired, want: true}, {name: "failed", value: StatusFailed, want: true}, {name: "cancelled", value: StatusCancelled, want: true}, {name: "unknown", value: Status("unknown"), want: false}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() if got := tt.value.IsKnown(); got != tt.want { require.Failf(t, "test failed", "IsKnown() = %v, want %v", got, tt.want) } }) } } func TestStatusIsTerminal(t *testing.T) { t.Parallel() tests := []struct { name string value Status want bool }{ {name: "pending send", value: StatusPendingSend, want: false}, {name: "sent", value: StatusSent, want: false}, {name: "delivery suppressed", value: StatusDeliverySuppressed, want: false}, {name: "delivery throttled", value: StatusDeliveryThrottled, want: false}, {name: "confirmed pending expire", value: StatusConfirmedPendingExpire, want: false}, {name: "expired", value: StatusExpired, want: true}, {name: "failed", value: StatusFailed, want: true}, {name: "cancelled", value: StatusCancelled, want: true}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() if got := tt.value.IsTerminal(); got != tt.want { require.Failf(t, "test failed", "IsTerminal() = %v, want %v", got, tt.want) } }) } } func TestStatusAcceptsFreshConfirm(t *testing.T) { t.Parallel() tests := []struct { name string value Status want bool }{ {name: "pending send", value: StatusPendingSend, want: false}, {name: "sent", value: StatusSent, want: true}, {name: "delivery suppressed", value: StatusDeliverySuppressed, want: true}, {name: "delivery throttled", value: StatusDeliveryThrottled, want: false}, {name: "confirmed", value: StatusConfirmedPendingExpire, want: false}, {name: "expired", value: StatusExpired, want: false}, {name: "failed", value: StatusFailed, want: false}, {name: "cancelled", value: StatusCancelled, want: false}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() if got := tt.value.AcceptsFreshConfirm(); got != tt.want { require.Failf(t, "test failed", "AcceptsFreshConfirm() = %v, want %v", got, tt.want) } }) } } func TestStatusIsConfirmedRetryState(t *testing.T) { t.Parallel() tests := []struct { name string value Status want bool }{ {name: "sent", value: StatusSent, want: false}, {name: "delivery suppressed", value: StatusDeliverySuppressed, want: false}, {name: "delivery throttled", value: StatusDeliveryThrottled, want: false}, {name: "confirmed", value: StatusConfirmedPendingExpire, want: true}, {name: "expired", value: StatusExpired, want: false}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() if got := tt.value.IsConfirmedRetryState(); got != tt.want { require.Failf(t, "test failed", "IsConfirmedRetryState() = %v, want %v", got, tt.want) } }) } } func TestStatusCanTransitionTo(t *testing.T) { t.Parallel() tests := []struct { name string from Status to Status want bool }{ {name: "pending to sent", from: StatusPendingSend, to: StatusSent, want: true}, {name: "pending to suppressed", from: StatusPendingSend, to: StatusDeliverySuppressed, want: true}, {name: "pending to throttled", from: StatusPendingSend, to: StatusDeliveryThrottled, want: true}, {name: "pending to failed", from: StatusPendingSend, to: StatusFailed, want: true}, {name: "pending to cancelled", from: StatusPendingSend, to: StatusCancelled, want: true}, {name: "pending to expired", from: StatusPendingSend, to: StatusExpired, want: true}, {name: "pending to confirmed", from: StatusPendingSend, to: StatusConfirmedPendingExpire, want: false}, {name: "sent to confirmed", from: StatusSent, to: StatusConfirmedPendingExpire, want: true}, {name: "sent to failed", from: StatusSent, to: StatusFailed, want: true}, {name: "suppressed to confirmed", from: StatusDeliverySuppressed, to: StatusConfirmedPendingExpire, want: true}, {name: "throttled to confirmed", from: StatusDeliveryThrottled, to: StatusConfirmedPendingExpire, want: false}, {name: "confirmed to expired", from: StatusConfirmedPendingExpire, to: StatusExpired, want: true}, {name: "confirmed to failed", from: StatusConfirmedPendingExpire, to: StatusFailed, want: false}, {name: "expired terminal", from: StatusExpired, to: StatusCancelled, want: false}, {name: "failed terminal", from: StatusFailed, to: StatusExpired, want: false}, {name: "cancelled terminal", from: StatusCancelled, to: StatusExpired, want: false}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() if got := tt.from.CanTransitionTo(tt.to); got != tt.want { require.Failf(t, "test failed", "CanTransitionTo() = %v, want %v", got, tt.want) } }) } } func TestDeliveryStateIsKnown(t *testing.T) { t.Parallel() tests := []struct { name string value DeliveryState want bool }{ {name: "pending", value: DeliveryPending, want: true}, {name: "sent", value: DeliverySent, want: true}, {name: "suppressed", value: DeliverySuppressed, want: true}, {name: "throttled", value: DeliveryThrottled, want: true}, {name: "failed", value: DeliveryFailed, want: true}, {name: "unknown", value: DeliveryState("unknown"), want: false}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() if got := tt.value.IsKnown(); got != tt.want { require.Failf(t, "test failed", "IsKnown() = %v, want %v", got, tt.want) } }) } } func TestDeliveryStateCanTransitionTo(t *testing.T) { t.Parallel() tests := []struct { name string from DeliveryState to DeliveryState want bool }{ {name: "pending to sent", from: DeliveryPending, to: DeliverySent, want: true}, {name: "pending to suppressed", from: DeliveryPending, to: DeliverySuppressed, want: true}, {name: "pending to throttled", from: DeliveryPending, to: DeliveryThrottled, want: true}, {name: "pending to failed", from: DeliveryPending, to: DeliveryFailed, want: true}, {name: "sent terminal", from: DeliverySent, to: DeliveryFailed, want: false}, {name: "suppressed terminal", from: DeliverySuppressed, to: DeliverySent, want: false}, {name: "failed terminal", from: DeliveryFailed, to: DeliverySent, want: false}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() if got := tt.from.CanTransitionTo(tt.to); got != tt.want { require.Failf(t, "test failed", "CanTransitionTo() = %v, want %v", got, tt.want) } }) } } func TestChallengeIsExpiredAt(t *testing.T) { t.Parallel() now := time.Unix(1_775_121_700, 0).UTC() tests := []struct { name string mutate func(*Challenge) want bool }{ {name: "active before expiration", want: false}, { name: "expired status", mutate: func(c *Challenge) { c.Status = StatusExpired }, want: true, }, { name: "expiration timestamp passed", mutate: func(c *Challenge) { c.ExpiresAt = now }, want: true, }, { name: "confirmed retained before expiration", mutate: func(c *Challenge) { c.Status = StatusConfirmedPendingExpire c.DeliveryState = DeliverySent c.Confirmation = validConfirmation(t) c.ExpiresAt = now.Add(time.Second) }, want: false, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() challenge := validChallenge(t) challenge.CreatedAt = now.Add(-time.Minute) challenge.ExpiresAt = now.Add(time.Minute) if tt.mutate != nil { tt.mutate(&challenge) } if err := challenge.Validate(); err != nil { require.Failf(t, "test failed", "Validate() returned error: %v", err) } if got := challenge.IsExpiredAt(now); got != tt.want { require.Failf(t, "test failed", "IsExpiredAt() = %v, want %v", got, tt.want) } }) } } func TestChallengeValidate(t *testing.T) { t.Parallel() tests := []struct { name string mutate func(*Challenge) wantErr bool }{ {name: "valid pending"}, { name: "valid confirmed", mutate: func(c *Challenge) { c.Status = StatusConfirmedPendingExpire c.DeliveryState = DeliverySent c.Confirmation = validConfirmation(t) }, }, { name: "confirmed requires metadata", mutate: func(c *Challenge) { c.Status = StatusConfirmedPendingExpire c.DeliveryState = DeliverySent }, wantErr: true, }, { name: "unconfirmed rejects metadata", mutate: func(c *Challenge) { c.Confirmation = validConfirmation(t) }, wantErr: true, }, { name: "pending requires pending delivery", mutate: func(c *Challenge) { c.DeliveryState = DeliverySent }, wantErr: true, }, { name: "sent requires sent delivery", mutate: func(c *Challenge) { c.Status = StatusSent c.DeliveryState = DeliverySuppressed }, wantErr: true, }, { name: "throttled requires throttled delivery", mutate: func(c *Challenge) { c.Status = StatusDeliveryThrottled c.DeliveryState = DeliverySent }, wantErr: true, }, { name: "expiration before creation", mutate: func(c *Challenge) { c.ExpiresAt = c.CreatedAt.Add(-time.Second) }, wantErr: true, }, { name: "negative confirm attempts", mutate: func(c *Challenge) { c.Attempts.Confirm = -1 }, wantErr: true, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() challenge := validChallenge(t) if tt.mutate != nil { tt.mutate(&challenge) } err := challenge.Validate() if tt.wantErr && err == nil { require.FailNow(t, "Validate() returned nil error") } if !tt.wantErr && err != nil { require.Failf(t, "test failed", "Validate() returned error: %v", err) } }) } } func validChallenge(t *testing.T) Challenge { t.Helper() return Challenge{ ID: common.ChallengeID("challenge-123"), Email: common.Email("pilot@example.com"), CodeHash: []byte("hash-123"), Status: StatusPendingSend, DeliveryState: DeliveryPending, CreatedAt: time.Unix(1_775_121_600, 0).UTC(), ExpiresAt: time.Unix(1_775_121_900, 0).UTC(), Attempts: AttemptCounters{ Send: 0, Confirm: 0, }, } } func validConfirmation(t *testing.T) *Confirmation { t.Helper() raw := make(ed25519.PublicKey, ed25519.PublicKeySize) for index := range raw { raw[index] = byte(index + 1) } key, err := common.NewClientPublicKey(raw) if err != nil { require.Failf(t, "test failed", "NewClientPublicKey() returned error: %v", err) } return &Confirmation{ SessionID: common.DeviceSessionID("device-session-123"), ClientPublicKey: key, ConfirmedAt: time.Unix(1_775_121_700, 0).UTC(), } }