441 lines
12 KiB
Go
441 lines
12 KiB
Go
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"),
|
|
PreferredLanguage: "en",
|
|
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(),
|
|
}
|
|
}
|