veza/veza-backend-api/internal/core/subscription/service_test.go
senke f6ca52c3dc feat(v0.12.1): subscription plans service, handler, and routes
- Add subscription module (models, service, tests)
- Plans: Free, Creator ($9.99/mo), Premium ($19.99/mo)
- Features: subscribe, cancel, reactivate, change billing cycle
- 14-day trial for Premium plan
- Upgrade immediate, downgrade at period end
- Invoice tracking and history
- Handler tests for auth and validation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 19:36:57 +01:00

226 lines
5.6 KiB
Go

package subscription
import (
"testing"
"time"
"github.com/google/uuid"
)
func TestPlanTableName(t *testing.T) {
p := Plan{}
if p.TableName() != "subscription_plans" {
t.Errorf("expected subscription_plans, got %s", p.TableName())
}
}
func TestUserSubscriptionTableName(t *testing.T) {
s := UserSubscription{}
if s.TableName() != "user_subscriptions" {
t.Errorf("expected user_subscriptions, got %s", s.TableName())
}
}
func TestInvoiceTableName(t *testing.T) {
i := Invoice{}
if i.TableName() != "subscription_invoices" {
t.Errorf("expected subscription_invoices, got %s", i.TableName())
}
}
func TestPlanConstants(t *testing.T) {
tests := []struct {
name string
plan PlanName
expected string
}{
{"free plan", PlanFree, "free"},
{"creator plan", PlanCreator, "creator"},
{"premium plan", PlanPremium, "premium"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if string(tt.plan) != tt.expected {
t.Errorf("expected %s, got %s", tt.expected, string(tt.plan))
}
})
}
}
func TestSubscriptionStatusConstants(t *testing.T) {
tests := []struct {
name string
status SubscriptionStatus
expected string
}{
{"active", StatusActive, "active"},
{"trialing", StatusTrialing, "trialing"},
{"canceled", StatusCanceled, "canceled"},
{"past_due", StatusPastDue, "past_due"},
{"expired", StatusExpired, "expired"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if string(tt.status) != tt.expected {
t.Errorf("expected %s, got %s", tt.expected, string(tt.status))
}
})
}
}
func TestBillingCycleConstants(t *testing.T) {
if string(BillingMonthly) != "monthly" {
t.Errorf("expected monthly, got %s", string(BillingMonthly))
}
if string(BillingYearly) != "yearly" {
t.Errorf("expected yearly, got %s", string(BillingYearly))
}
}
func TestInvoiceStatusConstants(t *testing.T) {
tests := []struct {
name string
status InvoiceStatus
expected string
}{
{"pending", InvoicePending, "pending"},
{"paid", InvoicePaid, "paid"},
{"failed", InvoiceFailed, "failed"},
{"refunded", InvoiceRefunded, "refunded"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if string(tt.status) != tt.expected {
t.Errorf("expected %s, got %s", tt.expected, string(tt.status))
}
})
}
}
func TestUserSubscription_IsTrialing(t *testing.T) {
tests := []struct {
name string
sub UserSubscription
expected bool
}{
{
name: "trialing with future trial end",
sub: UserSubscription{
Status: StatusTrialing,
TrialEnd: timePtr(time.Now().Add(24 * time.Hour)),
},
expected: true,
},
{
name: "trialing with past trial end",
sub: UserSubscription{
Status: StatusTrialing,
TrialEnd: timePtr(time.Now().Add(-24 * time.Hour)),
},
expected: false,
},
{
name: "active status",
sub: UserSubscription{
Status: StatusActive,
},
expected: false,
},
{
name: "trialing without trial end",
sub: UserSubscription{
Status: StatusTrialing,
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.sub.IsTrialing(); got != tt.expected {
t.Errorf("IsTrialing() = %v, want %v", got, tt.expected)
}
})
}
}
func TestUserSubscription_IsActiveOrTrialing(t *testing.T) {
tests := []struct {
name string
status SubscriptionStatus
expected bool
}{
{"active", StatusActive, true},
{"trialing", StatusTrialing, true},
{"canceled", StatusCanceled, false},
{"expired", StatusExpired, false},
{"past_due", StatusPastDue, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sub := UserSubscription{Status: tt.status}
if got := sub.IsActiveOrTrialing(); got != tt.expected {
t.Errorf("IsActiveOrTrialing() = %v, want %v", got, tt.expected)
}
})
}
}
func TestPlanFields(t *testing.T) {
plan := Plan{
ID: uuid.New(),
Name: PlanPremium,
DisplayName: "Premium",
PriceMonthly: 1999,
PriceYearly: 19999,
Currency: "USD",
StorageLimitBytes: 214748364800,
MarketplaceCommission: 0.10,
CanSellOnMarketplace: true,
HasPrioritySupport: true,
HasCollaborationTools: true,
HasDistribution: true,
TrialDays: 14,
IsActive: true,
SortOrder: 2,
}
if plan.PriceMonthly != 1999 {
t.Errorf("expected price_monthly_cents 1999, got %d", plan.PriceMonthly)
}
if plan.PriceYearly != 19999 {
t.Errorf("expected price_yearly_cents 19999, got %d", plan.PriceYearly)
}
if plan.TrialDays != 14 {
t.Errorf("expected trial_days 14, got %d", plan.TrialDays)
}
if plan.MarketplaceCommission != 0.10 {
t.Errorf("expected commission 0.10, got %f", plan.MarketplaceCommission)
}
}
func TestServiceErrors(t *testing.T) {
if ErrPlanNotFound.Error() != "subscription plan not found" {
t.Errorf("unexpected error message: %s", ErrPlanNotFound.Error())
}
if ErrAlreadySubscribed.Error() != "user already has an active subscription to this plan" {
t.Errorf("unexpected error message: %s", ErrAlreadySubscribed.Error())
}
if ErrNoActiveSubscription.Error() != "no active subscription found" {
t.Errorf("unexpected error message: %s", ErrNoActiveSubscription.Error())
}
if ErrInvalidBillingCycle.Error() != "invalid billing cycle: must be 'monthly' or 'yearly'" {
t.Errorf("unexpected error message: %s", ErrInvalidBillingCycle.Error())
}
if ErrFreePlanNoBilling.Error() != "free plan does not require billing" {
t.Errorf("unexpected error message: %s", ErrFreePlanNoBilling.Error())
}
}
func timePtr(t time.Time) *time.Time {
return &t
}