package services import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/treytartt/casera-api/internal/models" "github.com/treytartt/casera-api/internal/repositories" "github.com/treytartt/casera-api/internal/testutil" ) // setupSubscriptionService creates a SubscriptionService with the given // IAP clients (nil means "not configured"). It bypasses NewSubscriptionService // which tries to load config from environment. func setupSubscriptionService(t *testing.T, appleClient *AppleIAPClient, googleClient *GoogleIAPClient) (*SubscriptionService, *repositories.SubscriptionRepository) { db := testutil.SetupTestDB(t) subscriptionRepo := repositories.NewSubscriptionRepository(db) residenceRepo := repositories.NewResidenceRepository(db) taskRepo := repositories.NewTaskRepository(db) contractorRepo := repositories.NewContractorRepository(db) documentRepo := repositories.NewDocumentRepository(db) // Create a test user and subscription record for the test user := testutil.CreateTestUser(t, db, "subuser", "subuser@test.com", "password") // Create subscription record so GetOrCreate will find it sub := &models.UserSubscription{ UserID: user.ID, Tier: models.TierFree, } err := db.Create(sub).Error require.NoError(t, err) svc := &SubscriptionService{ subscriptionRepo: subscriptionRepo, residenceRepo: residenceRepo, taskRepo: taskRepo, contractorRepo: contractorRepo, documentRepo: documentRepo, appleClient: appleClient, googleClient: googleClient, } return svc, subscriptionRepo } func TestProcessApplePurchase_ClientNil_ReturnsError(t *testing.T) { db := testutil.SetupTestDB(t) subscriptionRepo := repositories.NewSubscriptionRepository(db) residenceRepo := repositories.NewResidenceRepository(db) taskRepo := repositories.NewTaskRepository(db) contractorRepo := repositories.NewContractorRepository(db) documentRepo := repositories.NewDocumentRepository(db) user := testutil.CreateTestUser(t, db, "subuser", "subuser@test.com", "password") sub := &models.UserSubscription{UserID: user.ID, Tier: models.TierFree} require.NoError(t, db.Create(sub).Error) svc := &SubscriptionService{ subscriptionRepo: subscriptionRepo, residenceRepo: residenceRepo, taskRepo: taskRepo, contractorRepo: contractorRepo, documentRepo: documentRepo, appleClient: nil, // Not configured googleClient: nil, } _, err := svc.ProcessApplePurchase(user.ID, "fake-receipt", "") assert.Error(t, err, "ProcessApplePurchase should return error when Apple IAP client is nil") // Verify user was NOT upgraded to Pro updatedSub, err := subscriptionRepo.GetOrCreate(user.ID) require.NoError(t, err) assert.Equal(t, models.TierFree, updatedSub.Tier, "User should remain on free tier when IAP client is nil") } func TestProcessApplePurchase_ValidationFails_DoesNotUpgrade(t *testing.T) { // We cannot easily create a real AppleIAPClient that will fail validation // in a unit test (it requires real keys and network access). // Instead, we test the code path logic: // When appleClient is nil, the service must NOT upgrade the user. // This is the same as TestProcessApplePurchase_ClientNil_ReturnsError // but validates no fallback occurs for the specific case. db := testutil.SetupTestDB(t) subscriptionRepo := repositories.NewSubscriptionRepository(db) residenceRepo := repositories.NewResidenceRepository(db) taskRepo := repositories.NewTaskRepository(db) contractorRepo := repositories.NewContractorRepository(db) documentRepo := repositories.NewDocumentRepository(db) user := testutil.CreateTestUser(t, db, "subuser2", "subuser2@test.com", "password") sub := &models.UserSubscription{UserID: user.ID, Tier: models.TierFree} require.NoError(t, db.Create(sub).Error) svc := &SubscriptionService{ subscriptionRepo: subscriptionRepo, residenceRepo: residenceRepo, taskRepo: taskRepo, contractorRepo: contractorRepo, documentRepo: documentRepo, appleClient: nil, googleClient: nil, } // Neither receipt data nor transaction ID - should still not grant Pro _, err := svc.ProcessApplePurchase(user.ID, "", "") assert.Error(t, err, "ProcessApplePurchase should return error when client is nil, even with empty data") // Verify no upgrade happened updatedSub, err := subscriptionRepo.GetOrCreate(user.ID) require.NoError(t, err) assert.Equal(t, models.TierFree, updatedSub.Tier, "User should remain on free tier") } func TestProcessGooglePurchase_ClientNil_ReturnsError(t *testing.T) { db := testutil.SetupTestDB(t) subscriptionRepo := repositories.NewSubscriptionRepository(db) residenceRepo := repositories.NewResidenceRepository(db) taskRepo := repositories.NewTaskRepository(db) contractorRepo := repositories.NewContractorRepository(db) documentRepo := repositories.NewDocumentRepository(db) user := testutil.CreateTestUser(t, db, "subuser3", "subuser3@test.com", "password") sub := &models.UserSubscription{UserID: user.ID, Tier: models.TierFree} require.NoError(t, db.Create(sub).Error) svc := &SubscriptionService{ subscriptionRepo: subscriptionRepo, residenceRepo: residenceRepo, taskRepo: taskRepo, contractorRepo: contractorRepo, documentRepo: documentRepo, appleClient: nil, googleClient: nil, // Not configured } _, err := svc.ProcessGooglePurchase(user.ID, "fake-token", "com.tt.casera.pro.monthly") assert.Error(t, err, "ProcessGooglePurchase should return error when Google IAP client is nil") // Verify user was NOT upgraded to Pro updatedSub, err := subscriptionRepo.GetOrCreate(user.ID) require.NoError(t, err) assert.Equal(t, models.TierFree, updatedSub.Tier, "User should remain on free tier when IAP client is nil") } func TestProcessGooglePurchase_ValidationFails_DoesNotUpgrade(t *testing.T) { db := testutil.SetupTestDB(t) subscriptionRepo := repositories.NewSubscriptionRepository(db) residenceRepo := repositories.NewResidenceRepository(db) taskRepo := repositories.NewTaskRepository(db) contractorRepo := repositories.NewContractorRepository(db) documentRepo := repositories.NewDocumentRepository(db) user := testutil.CreateTestUser(t, db, "subuser4", "subuser4@test.com", "password") sub := &models.UserSubscription{UserID: user.ID, Tier: models.TierFree} require.NoError(t, db.Create(sub).Error) svc := &SubscriptionService{ subscriptionRepo: subscriptionRepo, residenceRepo: residenceRepo, taskRepo: taskRepo, contractorRepo: contractorRepo, documentRepo: documentRepo, appleClient: nil, googleClient: nil, // Not configured } // With empty token _, err := svc.ProcessGooglePurchase(user.ID, "", "") assert.Error(t, err, "ProcessGooglePurchase should return error when client is nil") // Verify no upgrade happened updatedSub, err := subscriptionRepo.GetOrCreate(user.ID) require.NoError(t, err) assert.Equal(t, models.TierFree, updatedSub.Tier, "User should remain on free tier") }