Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
All services: admin-v2, studio-v2, website, ai-compliance-sdk, consent-service, klausur-service, voice-service, and infrastructure. Large PDFs and compiled binaries excluded via .gitignore.
327 lines
8.3 KiB
Go
327 lines
8.3 KiB
Go
package services
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/breakpilot/billing-service/internal/models"
|
|
)
|
|
|
|
func TestSubscriptionStatus_Transitions(t *testing.T) {
|
|
// Test valid subscription status values
|
|
validStatuses := []models.SubscriptionStatus{
|
|
models.StatusTrialing,
|
|
models.StatusActive,
|
|
models.StatusPastDue,
|
|
models.StatusCanceled,
|
|
models.StatusExpired,
|
|
}
|
|
|
|
for _, status := range validStatuses {
|
|
if status == "" {
|
|
t.Errorf("Status should not be empty")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPlanID_ValidValues(t *testing.T) {
|
|
validPlanIDs := []models.PlanID{
|
|
models.PlanBasic,
|
|
models.PlanStandard,
|
|
models.PlanPremium,
|
|
}
|
|
|
|
expected := []string{"basic", "standard", "premium"}
|
|
|
|
for i, planID := range validPlanIDs {
|
|
if string(planID) != expected[i] {
|
|
t.Errorf("PlanID should be '%s', got '%s'", expected[i], planID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPlanFeatures_JSONSerialization(t *testing.T) {
|
|
features := models.PlanFeatures{
|
|
MonthlyTaskAllowance: 100,
|
|
MaxTaskBalance: 500,
|
|
FeatureFlags: []string{"basic_ai", "templates"},
|
|
MaxTeamMembers: 3,
|
|
PrioritySupport: false,
|
|
CustomBranding: false,
|
|
BatchProcessing: true,
|
|
CustomTemplates: true,
|
|
FairUseMode: false,
|
|
}
|
|
|
|
// Test JSON serialization
|
|
data, err := json.Marshal(features)
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal PlanFeatures: %v", err)
|
|
}
|
|
|
|
// Test JSON deserialization
|
|
var decoded models.PlanFeatures
|
|
err = json.Unmarshal(data, &decoded)
|
|
if err != nil {
|
|
t.Fatalf("Failed to unmarshal PlanFeatures: %v", err)
|
|
}
|
|
|
|
// Verify fields
|
|
if decoded.MonthlyTaskAllowance != features.MonthlyTaskAllowance {
|
|
t.Errorf("MonthlyTaskAllowance mismatch: got %d, expected %d",
|
|
decoded.MonthlyTaskAllowance, features.MonthlyTaskAllowance)
|
|
}
|
|
if decoded.MaxTaskBalance != features.MaxTaskBalance {
|
|
t.Errorf("MaxTaskBalance mismatch: got %d, expected %d",
|
|
decoded.MaxTaskBalance, features.MaxTaskBalance)
|
|
}
|
|
if decoded.BatchProcessing != features.BatchProcessing {
|
|
t.Errorf("BatchProcessing mismatch: got %v, expected %v",
|
|
decoded.BatchProcessing, features.BatchProcessing)
|
|
}
|
|
}
|
|
|
|
func TestBillingPlan_DefaultPlansAreValid(t *testing.T) {
|
|
plans := models.GetDefaultPlans()
|
|
|
|
if len(plans) != 3 {
|
|
t.Fatalf("Expected 3 default plans, got %d", len(plans))
|
|
}
|
|
|
|
// Verify all plans have required fields
|
|
for _, plan := range plans {
|
|
if plan.ID == "" {
|
|
t.Errorf("Plan ID should not be empty")
|
|
}
|
|
if plan.Name == "" {
|
|
t.Errorf("Plan '%s' should have a name", plan.ID)
|
|
}
|
|
if plan.Description == "" {
|
|
t.Errorf("Plan '%s' should have a description", plan.ID)
|
|
}
|
|
if plan.PriceCents <= 0 {
|
|
t.Errorf("Plan '%s' should have a positive price, got %d", plan.ID, plan.PriceCents)
|
|
}
|
|
if plan.Currency != "eur" {
|
|
t.Errorf("Plan '%s' currency should be 'eur', got '%s'", plan.ID, plan.Currency)
|
|
}
|
|
if plan.Interval != "month" {
|
|
t.Errorf("Plan '%s' interval should be 'month', got '%s'", plan.ID, plan.Interval)
|
|
}
|
|
if !plan.IsActive {
|
|
t.Errorf("Plan '%s' should be active", plan.ID)
|
|
}
|
|
if plan.SortOrder <= 0 {
|
|
t.Errorf("Plan '%s' should have a positive sort order, got %d", plan.ID, plan.SortOrder)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBillingPlan_TaskAllowanceProgression(t *testing.T) {
|
|
plans := models.GetDefaultPlans()
|
|
|
|
// Basic should have lowest allowance
|
|
basic := plans[0]
|
|
standard := plans[1]
|
|
premium := plans[2]
|
|
|
|
if basic.Features.MonthlyTaskAllowance >= standard.Features.MonthlyTaskAllowance {
|
|
t.Error("Standard plan should have more tasks than Basic")
|
|
}
|
|
|
|
if standard.Features.MonthlyTaskAllowance >= premium.Features.MonthlyTaskAllowance {
|
|
t.Error("Premium plan should have more tasks than Standard")
|
|
}
|
|
}
|
|
|
|
func TestBillingPlan_PriceProgression(t *testing.T) {
|
|
plans := models.GetDefaultPlans()
|
|
|
|
// Prices should increase with each tier
|
|
if plans[0].PriceCents >= plans[1].PriceCents {
|
|
t.Error("Standard should cost more than Basic")
|
|
}
|
|
if plans[1].PriceCents >= plans[2].PriceCents {
|
|
t.Error("Premium should cost more than Standard")
|
|
}
|
|
}
|
|
|
|
func TestBillingPlan_FairUseModeOnlyForPremium(t *testing.T) {
|
|
plans := models.GetDefaultPlans()
|
|
|
|
for _, plan := range plans {
|
|
if plan.ID == models.PlanPremium {
|
|
if !plan.Features.FairUseMode {
|
|
t.Error("Premium plan should have FairUseMode enabled")
|
|
}
|
|
} else {
|
|
if plan.Features.FairUseMode {
|
|
t.Errorf("Plan '%s' should not have FairUseMode enabled", plan.ID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBillingPlan_MaxTaskBalanceCalculation(t *testing.T) {
|
|
plans := models.GetDefaultPlans()
|
|
|
|
for _, plan := range plans {
|
|
expected := plan.Features.MonthlyTaskAllowance * models.CarryoverMonthsCap
|
|
if plan.Features.MaxTaskBalance != expected {
|
|
t.Errorf("Plan '%s' MaxTaskBalance should be %d (allowance * 5), got %d",
|
|
plan.ID, expected, plan.Features.MaxTaskBalance)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAuditLogJSON_Marshaling(t *testing.T) {
|
|
// Test that audit log values can be properly serialized
|
|
oldValue := map[string]interface{}{
|
|
"plan_id": "basic",
|
|
"status": "active",
|
|
}
|
|
|
|
newValue := map[string]interface{}{
|
|
"plan_id": "standard",
|
|
"status": "active",
|
|
}
|
|
|
|
metadata := map[string]interface{}{
|
|
"reason": "upgrade",
|
|
}
|
|
|
|
// Marshal all values
|
|
oldJSON, err := json.Marshal(oldValue)
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal oldValue: %v", err)
|
|
}
|
|
|
|
newJSON, err := json.Marshal(newValue)
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal newValue: %v", err)
|
|
}
|
|
|
|
metaJSON, err := json.Marshal(metadata)
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal metadata: %v", err)
|
|
}
|
|
|
|
// Verify non-empty
|
|
if len(oldJSON) == 0 || len(newJSON) == 0 || len(metaJSON) == 0 {
|
|
t.Error("JSON outputs should not be empty")
|
|
}
|
|
}
|
|
|
|
func TestSubscriptionTrialCalculation(t *testing.T) {
|
|
// Test trial days calculation logic
|
|
trialDays := 7
|
|
|
|
if trialDays <= 0 {
|
|
t.Error("Trial days should be positive")
|
|
}
|
|
|
|
if trialDays > 30 {
|
|
t.Error("Trial days should not exceed 30")
|
|
}
|
|
}
|
|
|
|
func TestSubscriptionInfo_TrialingStatus(t *testing.T) {
|
|
info := models.SubscriptionInfo{
|
|
PlanID: models.PlanBasic,
|
|
PlanName: "Basic",
|
|
Status: models.StatusTrialing,
|
|
IsTrialing: true,
|
|
TrialDaysLeft: 5,
|
|
CancelAtPeriodEnd: false,
|
|
PriceCents: 990,
|
|
Currency: "eur",
|
|
}
|
|
|
|
if !info.IsTrialing {
|
|
t.Error("Should be trialing")
|
|
}
|
|
if info.Status != models.StatusTrialing {
|
|
t.Errorf("Status should be 'trialing', got '%s'", info.Status)
|
|
}
|
|
if info.TrialDaysLeft <= 0 {
|
|
t.Error("TrialDaysLeft should be positive during trial")
|
|
}
|
|
}
|
|
|
|
func TestSubscriptionInfo_ActiveStatus(t *testing.T) {
|
|
info := models.SubscriptionInfo{
|
|
PlanID: models.PlanStandard,
|
|
PlanName: "Standard",
|
|
Status: models.StatusActive,
|
|
IsTrialing: false,
|
|
TrialDaysLeft: 0,
|
|
CancelAtPeriodEnd: false,
|
|
PriceCents: 1990,
|
|
Currency: "eur",
|
|
}
|
|
|
|
if info.IsTrialing {
|
|
t.Error("Should not be trialing")
|
|
}
|
|
if info.Status != models.StatusActive {
|
|
t.Errorf("Status should be 'active', got '%s'", info.Status)
|
|
}
|
|
}
|
|
|
|
func TestSubscriptionInfo_CanceledStatus(t *testing.T) {
|
|
info := models.SubscriptionInfo{
|
|
PlanID: models.PlanStandard,
|
|
PlanName: "Standard",
|
|
Status: models.StatusActive,
|
|
IsTrialing: false,
|
|
CancelAtPeriodEnd: true, // Scheduled for cancellation
|
|
PriceCents: 1990,
|
|
Currency: "eur",
|
|
}
|
|
|
|
if !info.CancelAtPeriodEnd {
|
|
t.Error("CancelAtPeriodEnd should be true")
|
|
}
|
|
// Status remains active until period end
|
|
if info.Status != models.StatusActive {
|
|
t.Errorf("Status should still be 'active', got '%s'", info.Status)
|
|
}
|
|
}
|
|
|
|
func TestWebhookEventTypes(t *testing.T) {
|
|
// Test common Stripe webhook event types we handle
|
|
eventTypes := []string{
|
|
"checkout.session.completed",
|
|
"customer.subscription.created",
|
|
"customer.subscription.updated",
|
|
"customer.subscription.deleted",
|
|
"invoice.paid",
|
|
"invoice.payment_failed",
|
|
}
|
|
|
|
for _, eventType := range eventTypes {
|
|
if eventType == "" {
|
|
t.Error("Event type should not be empty")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIdempotencyKey_Format(t *testing.T) {
|
|
// Test that we can handle Stripe event IDs
|
|
sampleEventIDs := []string{
|
|
"evt_1234567890abcdef",
|
|
"evt_test_abc123xyz789",
|
|
"evt_live_real_event_id",
|
|
}
|
|
|
|
for _, eventID := range sampleEventIDs {
|
|
if len(eventID) < 10 {
|
|
t.Errorf("Event ID '%s' seems too short", eventID)
|
|
}
|
|
// Stripe event IDs typically start with "evt_"
|
|
if eventID[:4] != "evt_" {
|
|
t.Errorf("Event ID '%s' should start with 'evt_'", eventID)
|
|
}
|
|
}
|
|
}
|