//go:build unit package service import ( "errors" "testing" "github.com/stretchr/testify/assert" ) // --------------------------------------------------------------------------- // resolveRedeemAction — pure idempotency decision logic // --------------------------------------------------------------------------- func TestResolveRedeemAction_CodeNotFound(t *testing.T) { t.Parallel() action := resolveRedeemAction(nil, nil) assert.Equal(t, redeemActionCreate, action, "nil code with nil error should create") } func TestResolveRedeemAction_LookupError(t *testing.T) { t.Parallel() action := resolveRedeemAction(nil, errors.New("db connection lost")) assert.Equal(t, redeemActionCreate, action, "lookup error should fall back to create") } func TestResolveRedeemAction_LookupErrorWithNonNilCode(t *testing.T) { t.Parallel() // Edge case: both code and error are non-nil (shouldn't happen in practice, // but the function should still treat error as authoritative) code := &RedeemCode{Status: StatusUnused} action := resolveRedeemAction(code, errors.New("partial error")) assert.Equal(t, redeemActionCreate, action, "non-nil error should always result in create regardless of code") } func TestResolveRedeemAction_CodeExistsAndUsed(t *testing.T) { t.Parallel() code := &RedeemCode{ Code: "test-code-123", Status: StatusUsed, Type: RedeemTypeBalance, Value: 10.0, } action := resolveRedeemAction(code, nil) assert.Equal(t, redeemActionSkipCompleted, action, "used code should skip to completed") } func TestResolveRedeemAction_CodeExistsAndUnused(t *testing.T) { t.Parallel() code := &RedeemCode{ Code: "test-code-456", Status: StatusUnused, Type: RedeemTypeBalance, Value: 25.0, } action := resolveRedeemAction(code, nil) assert.Equal(t, redeemActionRedeem, action, "unused code should skip creation and proceed to redeem") } func TestResolveRedeemAction_CodeExistsWithExpiredStatus(t *testing.T) { t.Parallel() // A code with a non-standard status (neither "unused" nor "used") // should NOT be treated as used, so it falls through to redeemActionRedeem. code := &RedeemCode{ Code: "expired-code", Status: StatusExpired, } action := resolveRedeemAction(code, nil) assert.Equal(t, redeemActionRedeem, action, "expired-status code is not IsUsed(), should redeem") } // --------------------------------------------------------------------------- // Table-driven comprehensive test // --------------------------------------------------------------------------- func TestResolveRedeemAction_Table(t *testing.T) { t.Parallel() tests := []struct { name string code *RedeemCode err error expected redeemAction }{ { name: "nil code, nil error — first run", code: nil, err: nil, expected: redeemActionCreate, }, { name: "nil code, lookup error — treat as not found", code: nil, err: ErrRedeemCodeNotFound, expected: redeemActionCreate, }, { name: "nil code, generic DB error — treat as not found", code: nil, err: errors.New("connection refused"), expected: redeemActionCreate, }, { name: "code exists, used — previous run completed redeem", code: &RedeemCode{Status: StatusUsed}, err: nil, expected: redeemActionSkipCompleted, }, { name: "code exists, unused — previous run created code but crashed before redeem", code: &RedeemCode{Status: StatusUnused}, err: nil, expected: redeemActionRedeem, }, { name: "code exists but error also set — error takes precedence", code: &RedeemCode{Status: StatusUsed}, err: errors.New("unexpected"), expected: redeemActionCreate, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() got := resolveRedeemAction(tt.code, tt.err) assert.Equal(t, tt.expected, got) }) } } // --------------------------------------------------------------------------- // redeemAction enum value sanity // --------------------------------------------------------------------------- func TestRedeemAction_DistinctValues(t *testing.T) { t.Parallel() // Ensure the three actions have distinct values (iota correctness) assert.NotEqual(t, redeemActionCreate, redeemActionRedeem) assert.NotEqual(t, redeemActionCreate, redeemActionSkipCompleted) assert.NotEqual(t, redeemActionRedeem, redeemActionSkipCompleted) } // --------------------------------------------------------------------------- // RedeemCode.IsUsed / CanUse interaction with resolveRedeemAction // --------------------------------------------------------------------------- func TestResolveRedeemAction_IsUsedCanUseConsistency(t *testing.T) { t.Parallel() usedCode := &RedeemCode{Status: StatusUsed} unusedCode := &RedeemCode{Status: StatusUnused} // Verify our decision function is consistent with the domain model methods assert.True(t, usedCode.IsUsed()) assert.False(t, usedCode.CanUse()) assert.Equal(t, redeemActionSkipCompleted, resolveRedeemAction(usedCode, nil)) assert.False(t, unusedCode.IsUsed()) assert.True(t, unusedCode.CanUse()) assert.Equal(t, redeemActionRedeem, resolveRedeemAction(unusedCode, nil)) }