- Stripe integration: add StripeService with checkout sessions, customer portal, and webhook handling for subscription lifecycle events. - Free trials: auto-start configurable trial on first subscription check, with admin-controllable duration and enable/disable toggle. - Cross-platform guard: prevent duplicate subscriptions across iOS, Android, and Stripe by checking existing platform before allowing purchase. - Subscription model: add Stripe fields (customer_id, subscription_id, price_id), trial fields (trial_start, trial_end, trial_used), and SubscriptionSource/IsTrialActive helpers. - API: add trial and source fields to status response, update OpenAPI spec. - Clean up stale migration and audit docs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
188 lines
4.2 KiB
Go
188 lines
4.2 KiB
Go
package models
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestIsTrialActive(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
future := now.Add(24 * time.Hour)
|
|
past := now.Add(-24 * time.Hour)
|
|
|
|
tests := []struct {
|
|
name string
|
|
sub *UserSubscription
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "trial_end in future returns true",
|
|
sub: &UserSubscription{TrialEnd: &future},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "trial_end in past returns false",
|
|
sub: &UserSubscription{TrialEnd: &past},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "trial_end nil returns false",
|
|
sub: &UserSubscription{TrialEnd: nil},
|
|
expected: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := tt.sub.IsTrialActive()
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsPro(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
future := now.Add(24 * time.Hour)
|
|
past := now.Add(-24 * time.Hour)
|
|
|
|
tests := []struct {
|
|
name string
|
|
sub *UserSubscription
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "tier=pro, expires_at in future returns true",
|
|
sub: &UserSubscription{
|
|
Tier: TierPro,
|
|
ExpiresAt: &future,
|
|
},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "tier=pro, expires_at in past returns false",
|
|
sub: &UserSubscription{
|
|
Tier: TierPro,
|
|
ExpiresAt: &past,
|
|
},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "tier=free, trial active returns true",
|
|
sub: &UserSubscription{
|
|
Tier: TierFree,
|
|
TrialEnd: &future,
|
|
},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "tier=free, trial expired returns false",
|
|
sub: &UserSubscription{
|
|
Tier: TierFree,
|
|
TrialEnd: &past,
|
|
},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "tier=free, no trial returns false",
|
|
sub: &UserSubscription{
|
|
Tier: TierFree,
|
|
TrialEnd: nil,
|
|
},
|
|
expected: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := tt.sub.IsPro()
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHasSubscriptionHelpers(t *testing.T) {
|
|
empty := ""
|
|
validStripeID := "sub_1234567890"
|
|
validReceipt := "MIIT..."
|
|
validToken := "google-purchase-token-123"
|
|
|
|
tests := []struct {
|
|
name string
|
|
sub *UserSubscription
|
|
method string
|
|
expected bool
|
|
}{
|
|
// HasStripeSubscription
|
|
{
|
|
name: "HasStripeSubscription with nil returns false",
|
|
sub: &UserSubscription{StripeSubscriptionID: nil},
|
|
method: "stripe",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "HasStripeSubscription with empty string returns false",
|
|
sub: &UserSubscription{StripeSubscriptionID: &empty},
|
|
method: "stripe",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "HasStripeSubscription with valid ID returns true",
|
|
sub: &UserSubscription{StripeSubscriptionID: &validStripeID},
|
|
method: "stripe",
|
|
expected: true,
|
|
},
|
|
// HasAppleSubscription
|
|
{
|
|
name: "HasAppleSubscription with nil returns false",
|
|
sub: &UserSubscription{AppleReceiptData: nil},
|
|
method: "apple",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "HasAppleSubscription with empty string returns false",
|
|
sub: &UserSubscription{AppleReceiptData: &empty},
|
|
method: "apple",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "HasAppleSubscription with valid receipt returns true",
|
|
sub: &UserSubscription{AppleReceiptData: &validReceipt},
|
|
method: "apple",
|
|
expected: true,
|
|
},
|
|
// HasGoogleSubscription
|
|
{
|
|
name: "HasGoogleSubscription with nil returns false",
|
|
sub: &UserSubscription{GooglePurchaseToken: nil},
|
|
method: "google",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "HasGoogleSubscription with empty string returns false",
|
|
sub: &UserSubscription{GooglePurchaseToken: &empty},
|
|
method: "google",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "HasGoogleSubscription with valid token returns true",
|
|
sub: &UserSubscription{GooglePurchaseToken: &validToken},
|
|
method: "google",
|
|
expected: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var result bool
|
|
switch tt.method {
|
|
case "stripe":
|
|
result = tt.sub.HasStripeSubscription()
|
|
case "apple":
|
|
result = tt.sub.HasAppleSubscription()
|
|
case "google":
|
|
result = tt.sub.HasGoogleSubscription()
|
|
}
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|