// // SubscriptionGatingTests.swift // honeyDueTests // // Unit tests for SubscriptionCacheWrapper feature gating logic: // currentTier, shouldShowUpgradePrompt, canShareResidence, canShareContractor. // // Uses the shared singleton with serialized tests to avoid race conditions. // import Testing import Foundation @testable import honeyDue import ComposeApp // MARK: - Helpers /// Build a SubscriptionStatus with sensible defaults for testing. private func makeSubscription( expiresAt: String? = nil, limitationsEnabled: Bool = false, limits: [String: TierLimits] = [:] ) -> SubscriptionStatus { SubscriptionStatus( tier: "free", isActive: false, subscribedAt: nil, expiresAt: expiresAt, autoRenew: true, usage: UsageStats(propertiesCount: 0, tasksCount: 0, contractorsCount: 0, documentsCount: 0), limits: limits, limitationsEnabled: limitationsEnabled, trialStart: nil, trialEnd: nil, trialActive: false, subscriptionSource: nil ) } private let freeLimits = TierLimits( properties: KotlinInt(int: 1), tasks: KotlinInt(int: 10), contractors: KotlinInt(int: 5), documents: KotlinInt(int: 5) ) private let proLimits = TierLimits( properties: nil, tasks: nil, contractors: nil, documents: nil ) // MARK: - Serialized Suite (SubscriptionCacheWrapper is a @MainActor shared singleton) @MainActor @Suite(.serialized) struct SubscriptionGatingTests { private let cache = SubscriptionCacheWrapper.shared // MARK: - currentTier Tests @Test func noSubscriptionTierIsFree() { cache.currentSubscription = nil #expect(cache.currentTier == "free") } @Test func subscriptionWithExpiresAtTierIsPro() { cache.currentSubscription = makeSubscription(expiresAt: "2030-01-01T00:00:00Z") #expect(cache.currentTier == "pro") } @Test func subscriptionWithEmptyExpiresAtTierIsFree() { cache.currentSubscription = makeSubscription(expiresAt: "") #expect(cache.currentTier == "free") } @Test func subscriptionWithNilExpiresAtTierIsFree() { cache.currentSubscription = makeSubscription(expiresAt: nil) #expect(cache.currentTier == "free") } // MARK: - shouldShowUpgradePrompt Tests @Test func nilSubscriptionNeverBlocks() { cache.currentSubscription = nil #expect(cache.shouldShowUpgradePrompt(currentCount: 100, limitKey: "properties") == false) } @Test func limitationsDisabledNeverBlocks() { cache.currentSubscription = makeSubscription( limitationsEnabled: false, limits: ["free": freeLimits] ) #expect(cache.shouldShowUpgradePrompt(currentCount: 100, limitKey: "properties") == false) } @Test func proTierNeverBlocks() { cache.currentSubscription = makeSubscription( expiresAt: "2030-01-01T00:00:00Z", limitationsEnabled: true, limits: ["pro": proLimits] ) #expect(cache.shouldShowUpgradePrompt(currentCount: 100, limitKey: "properties") == false) } @Test func freeTierUnderLimitAllowed() { cache.currentSubscription = makeSubscription( limitationsEnabled: true, limits: ["free": freeLimits] ) #expect(cache.shouldShowUpgradePrompt(currentCount: 0, limitKey: "properties") == false) } @Test func freeTierAtLimitBlocked() { cache.currentSubscription = makeSubscription( limitationsEnabled: true, limits: ["free": freeLimits] ) // freeLimits.properties = 1, so count of 1 should block #expect(cache.shouldShowUpgradePrompt(currentCount: 1, limitKey: "properties") == true) } @Test func freeTierOverLimitBlocked() { cache.currentSubscription = makeSubscription( limitationsEnabled: true, limits: ["free": freeLimits] ) #expect(cache.shouldShowUpgradePrompt(currentCount: 5, limitKey: "properties") == true) } @Test func tasksLimitEnforced() { cache.currentSubscription = makeSubscription( limitationsEnabled: true, limits: ["free": freeLimits] ) // freeLimits.tasks = 10 #expect(cache.shouldShowUpgradePrompt(currentCount: 9, limitKey: "tasks") == false) #expect(cache.shouldShowUpgradePrompt(currentCount: 10, limitKey: "tasks") == true) } @Test func contractorsLimitEnforced() { cache.currentSubscription = makeSubscription( limitationsEnabled: true, limits: ["free": freeLimits] ) // freeLimits.contractors = 5 #expect(cache.shouldShowUpgradePrompt(currentCount: 4, limitKey: "contractors") == false) #expect(cache.shouldShowUpgradePrompt(currentCount: 5, limitKey: "contractors") == true) } @Test func documentsLimitEnforced() { cache.currentSubscription = makeSubscription( limitationsEnabled: true, limits: ["free": freeLimits] ) // freeLimits.documents = 5 #expect(cache.shouldShowUpgradePrompt(currentCount: 4, limitKey: "documents") == false) #expect(cache.shouldShowUpgradePrompt(currentCount: 5, limitKey: "documents") == true) } @Test func nilLimitMeansUnlimited() { let unlimitedTasks = TierLimits( properties: KotlinInt(int: 1), tasks: nil, contractors: nil, documents: nil ) cache.currentSubscription = makeSubscription( limitationsEnabled: true, limits: ["free": unlimitedTasks] ) #expect(cache.shouldShowUpgradePrompt(currentCount: 10000, limitKey: "tasks") == false) } @Test func unknownLimitKeyReturnsFalse() { cache.currentSubscription = makeSubscription( limitationsEnabled: true, limits: ["free": freeLimits] ) #expect(cache.shouldShowUpgradePrompt(currentCount: 100, limitKey: "unknown") == false) } @Test func noLimitsForTierReturnsFalse() { cache.currentSubscription = makeSubscription( limitationsEnabled: true, limits: [:] // no "free" key ) #expect(cache.shouldShowUpgradePrompt(currentCount: 100, limitKey: "properties") == false) } // MARK: - Deprecated shouldShowUpgradePrompt (computed property) @Test func deprecatedPromptFreeWithLimitations() { cache.currentSubscription = makeSubscription(limitationsEnabled: true) #expect(cache.shouldShowUpgradePrompt == true) } @Test func deprecatedPromptFreeWithoutLimitations() { cache.currentSubscription = makeSubscription(limitationsEnabled: false) #expect(cache.shouldShowUpgradePrompt == false) } @Test func deprecatedPromptNilSubscription() { cache.currentSubscription = nil #expect(cache.shouldShowUpgradePrompt == false) } // MARK: - canShareResidence Tests @Test func canShareResidenceWhenNoSubscription() { cache.currentSubscription = nil #expect(cache.canShareResidence() == true) } @Test func canShareResidenceWhenLimitationsDisabled() { cache.currentSubscription = makeSubscription(limitationsEnabled: false) #expect(cache.canShareResidence() == true) } @Test func cannotShareResidenceWhenFreeWithLimitations() { cache.currentSubscription = makeSubscription(limitationsEnabled: true) #expect(cache.canShareResidence() == false) } @Test func canShareResidenceWhenProWithLimitations() { cache.currentSubscription = makeSubscription( expiresAt: "2030-01-01T00:00:00Z", limitationsEnabled: true ) #expect(cache.canShareResidence() == true) } // MARK: - canShareContractor Tests @Test func canShareContractorWhenNoSubscription() { cache.currentSubscription = nil #expect(cache.canShareContractor() == true) } @Test func canShareContractorWhenLimitationsDisabled() { cache.currentSubscription = makeSubscription(limitationsEnabled: false) #expect(cache.canShareContractor() == true) } @Test func cannotShareContractorWhenFreeWithLimitations() { cache.currentSubscription = makeSubscription(limitationsEnabled: true) #expect(cache.canShareContractor() == false) } @Test func canShareContractorWhenProWithLimitations() { cache.currentSubscription = makeSubscription( expiresAt: "2030-01-01T00:00:00Z", limitationsEnabled: true ) #expect(cache.canShareContractor() == true) } }