package integration import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/treytartt/casera-api/internal/apperrors" "github.com/treytartt/casera-api/internal/config" "github.com/treytartt/casera-api/internal/handlers" "github.com/treytartt/casera-api/internal/middleware" "github.com/treytartt/casera-api/internal/models" "github.com/treytartt/casera-api/internal/repositories" "github.com/treytartt/casera-api/internal/services" "github.com/treytartt/casera-api/internal/testutil" "github.com/treytartt/casera-api/internal/validator" "gorm.io/gorm" ) // SubscriptionTestApp holds components for subscription integration testing type SubscriptionTestApp struct { DB *gorm.DB Router *echo.Echo SubscriptionService *services.SubscriptionService SubscriptionRepo *repositories.SubscriptionRepository } func setupSubscriptionTest(t *testing.T) *SubscriptionTestApp { // Echo does not need test mode db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) // Create repositories userRepo := repositories.NewUserRepository(db) residenceRepo := repositories.NewResidenceRepository(db) taskRepo := repositories.NewTaskRepository(db) contractorRepo := repositories.NewContractorRepository(db) documentRepo := repositories.NewDocumentRepository(db) subscriptionRepo := repositories.NewSubscriptionRepository(db) // Create config cfg := &config.Config{ Security: config.SecurityConfig{ SecretKey: "test-secret-key-for-subscription-tests", PasswordResetExpiry: 15 * time.Minute, ConfirmationExpiry: 24 * time.Hour, MaxPasswordResetRate: 3, }, } // Create services authService := services.NewAuthService(userRepo, cfg) residenceService := services.NewResidenceService(residenceRepo, userRepo, cfg) subscriptionService := services.NewSubscriptionService(subscriptionRepo, residenceRepo, taskRepo, contractorRepo, documentRepo) // Create handlers authHandler := handlers.NewAuthHandler(authService, nil, nil) residenceHandler := handlers.NewResidenceHandler(residenceService, nil, nil) subscriptionHandler := handlers.NewSubscriptionHandler(subscriptionService) // Create router e := echo.New() e.Validator = validator.NewCustomValidator() e.HTTPErrorHandler = apperrors.HTTPErrorHandler // Public routes auth := e.Group("/api/auth") { auth.POST("/register", authHandler.Register) auth.POST("/login", authHandler.Login) } // Protected routes authMiddleware := middleware.NewAuthMiddleware(db, nil) api := e.Group("/api") api.Use(authMiddleware.TokenAuth()) { api.GET("/auth/me", authHandler.CurrentUser) residences := api.Group("/residences") { residences.POST("", residenceHandler.CreateResidence) } subscription := api.Group("/subscription") { subscription.GET("/", subscriptionHandler.GetSubscription) subscription.GET("/status/", subscriptionHandler.GetSubscriptionStatus) } } return &SubscriptionTestApp{ DB: db, Router: e, SubscriptionService: subscriptionService, SubscriptionRepo: subscriptionRepo, } } // Helper to make authenticated requests func (app *SubscriptionTestApp) makeAuthenticatedRequest(t *testing.T, method, path string, body interface{}, token string) *httptest.ResponseRecorder { var reqBody []byte var err error if body != nil { reqBody, err = json.Marshal(body) require.NoError(t, err) } req := httptest.NewRequest(method, path, nil) if body != nil { req = httptest.NewRequest(method, path, bytes.NewBuffer(reqBody)) } req.Header.Set("Content-Type", "application/json") if token != "" { req.Header.Set("Authorization", "Token "+token) } w := httptest.NewRecorder() app.Router.ServeHTTP(w, req) return w } // Helper to register and login a user, returns token and user ID func (app *SubscriptionTestApp) registerAndLogin(t *testing.T, username, email, password string) (string, uint) { // Register registerBody := map[string]string{ "username": username, "email": email, "password": password, } w := app.makeAuthenticatedRequest(t, "POST", "/api/auth/register", registerBody, "") require.Equal(t, http.StatusCreated, w.Code) // Login loginBody := map[string]string{ "username": username, "password": password, } w = app.makeAuthenticatedRequest(t, "POST", "/api/auth/login", loginBody, "") require.Equal(t, http.StatusOK, w.Code) var loginResp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &loginResp) require.NoError(t, err) token := loginResp["token"].(string) userMap := loginResp["user"].(map[string]interface{}) userID := uint(userMap["id"].(float64)) return token, userID } // TestIntegration_IsFreeBypassesLimitations tests that users with IsFree=true // see limitations_enabled=false regardless of global settings func TestIntegration_IsFreeBypassesLimitations(t *testing.T) { app := setupSubscriptionTest(t) // Register and login a user token, userID := app.registerAndLogin(t, "freeuser", "free@test.com", "password123") // Enable global limitations - first delete any existing, then create with enabled app.DB.Where("1=1").Delete(&models.SubscriptionSettings{}) settings := &models.SubscriptionSettings{EnableLimitations: true} err := app.DB.Create(settings).Error require.NoError(t, err) // Verify limitations are enabled globally var verifySettings models.SubscriptionSettings app.DB.First(&verifySettings) require.True(t, verifySettings.EnableLimitations, "Global limitations should be enabled") // ========== Test 1: Normal user sees limitations_enabled=true ========== w := app.makeAuthenticatedRequest(t, "GET", "/api/subscription/status/", nil, token) require.Equal(t, http.StatusOK, w.Code) var statusResp map[string]interface{} err = json.Unmarshal(w.Body.Bytes(), &statusResp) require.NoError(t, err) assert.True(t, statusResp["limitations_enabled"].(bool), "Normal user should see limitations_enabled=true when global setting is enabled") // ========== Test 2: Set IsFree=true for user ========== // Get user's subscription sub, err := app.SubscriptionRepo.GetOrCreate(userID) require.NoError(t, err) // Set IsFree=true sub.IsFree = true err = app.SubscriptionRepo.Update(sub) require.NoError(t, err) // ========== Test 3: User with IsFree=true sees limitations_enabled=false ========== w = app.makeAuthenticatedRequest(t, "GET", "/api/subscription/status/", nil, token) require.Equal(t, http.StatusOK, w.Code) err = json.Unmarshal(w.Body.Bytes(), &statusResp) require.NoError(t, err) assert.False(t, statusResp["limitations_enabled"].(bool), "User with IsFree=true should see limitations_enabled=false regardless of global setting") } // TestIntegration_IsFreeBypassesCheckLimit tests that IsFree users can create // resources beyond their tier limits func TestIntegration_IsFreeBypassesCheckLimit(t *testing.T) { app := setupSubscriptionTest(t) // Register and login a user _, userID := app.registerAndLogin(t, "limituser", "limit@test.com", "password123") // Enable global limitations settings := &models.SubscriptionSettings{EnableLimitations: true} app.DB.Where("1=1").Delete(&models.SubscriptionSettings{}) app.DB.Create(settings) // Set free tier limit to 1 property one := 1 freeLimits := &models.TierLimits{ Tier: models.TierFree, PropertiesLimit: &one, } app.DB.Where("tier = ?", models.TierFree).Delete(&models.TierLimits{}) app.DB.Create(freeLimits) // Get user's subscription (should be free tier) sub, err := app.SubscriptionRepo.GetOrCreate(userID) require.NoError(t, err) require.Equal(t, models.TierFree, sub.Tier) // ========== Test 1: Normal free user hits limit ========== // First property should succeed err = app.SubscriptionService.CheckLimit(userID, "properties") assert.NoError(t, err, "First property should be allowed") // Create a property to use up the limit residence := &models.Residence{ Name: "Test Property", OwnerID: userID, } app.DB.Create(residence) // Second property should fail err = app.SubscriptionService.CheckLimit(userID, "properties") assert.Error(t, err, "Second property should be blocked for normal free user") var appErr *apperrors.AppError require.ErrorAs(t, err, &appErr) assert.Equal(t, http.StatusForbidden, appErr.Code) assert.Equal(t, "error.properties_limit_exceeded", appErr.MessageKey) // ========== Test 2: Set IsFree=true ========== sub.IsFree = true err = app.SubscriptionRepo.Update(sub) require.NoError(t, err) // ========== Test 3: IsFree user bypasses limit ========== err = app.SubscriptionService.CheckLimit(userID, "properties") assert.NoError(t, err, "IsFree user should bypass property limits") // Should also bypass other limits err = app.SubscriptionService.CheckLimit(userID, "tasks") assert.NoError(t, err, "IsFree user should bypass task limits") err = app.SubscriptionService.CheckLimit(userID, "contractors") assert.NoError(t, err, "IsFree user should bypass contractor limits") err = app.SubscriptionService.CheckLimit(userID, "documents") assert.NoError(t, err, "IsFree user should bypass document limits") } // TestIntegration_IsFreeIndependentOfTier tests that IsFree works regardless of // the user's subscription tier func TestIntegration_IsFreeIndependentOfTier(t *testing.T) { app := setupSubscriptionTest(t) // Register and login a user token, userID := app.registerAndLogin(t, "tieruser", "tier@test.com", "password123") // Enable global limitations settings := &models.SubscriptionSettings{EnableLimitations: true} app.DB.Where("1=1").Delete(&models.SubscriptionSettings{}) app.DB.Create(settings) // Get user's subscription sub, err := app.SubscriptionRepo.GetOrCreate(userID) require.NoError(t, err) // ========== Test with Free tier + IsFree ========== sub.Tier = models.TierFree sub.IsFree = true err = app.SubscriptionRepo.Update(sub) require.NoError(t, err) w := app.makeAuthenticatedRequest(t, "GET", "/api/subscription/status/", nil, token) require.Equal(t, http.StatusOK, w.Code) var statusResp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &statusResp) assert.False(t, statusResp["limitations_enabled"].(bool), "Free tier user with IsFree should see limitations_enabled=false") // ========== Test with Pro tier + IsFree ========== sub.Tier = models.TierPro sub.IsFree = true err = app.SubscriptionRepo.Update(sub) require.NoError(t, err) w = app.makeAuthenticatedRequest(t, "GET", "/api/subscription/status/", nil, token) require.Equal(t, http.StatusOK, w.Code) json.Unmarshal(w.Body.Bytes(), &statusResp) assert.False(t, statusResp["limitations_enabled"].(bool), "Pro tier user with IsFree should see limitations_enabled=false") // ========== Test disabling IsFree restores normal behavior ========== sub.Tier = models.TierFree sub.IsFree = false err = app.SubscriptionRepo.Update(sub) require.NoError(t, err) w = app.makeAuthenticatedRequest(t, "GET", "/api/subscription/status/", nil, token) require.Equal(t, http.StatusOK, w.Code) json.Unmarshal(w.Body.Bytes(), &statusResp) assert.True(t, statusResp["limitations_enabled"].(bool), "Free tier user without IsFree should see limitations_enabled=true") } // TestIntegration_IsFreeWhenGlobalLimitationsDisabled tests that IsFree has no // effect when global limitations are already disabled func TestIntegration_IsFreeWhenGlobalLimitationsDisabled(t *testing.T) { app := setupSubscriptionTest(t) // Register and login a user token, userID := app.registerAndLogin(t, "globaluser", "global@test.com", "password123") // Disable global limitations settings := &models.SubscriptionSettings{EnableLimitations: false} app.DB.Where("1=1").Delete(&models.SubscriptionSettings{}) app.DB.Create(settings) // Get user's subscription sub, err := app.SubscriptionRepo.GetOrCreate(userID) require.NoError(t, err) // ========== Test 1: Without IsFree, limitations are disabled ========== sub.IsFree = false app.SubscriptionRepo.Update(sub) w := app.makeAuthenticatedRequest(t, "GET", "/api/subscription/status/", nil, token) require.Equal(t, http.StatusOK, w.Code) var statusResp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &statusResp) assert.False(t, statusResp["limitations_enabled"].(bool), "When global limitations are disabled, limitations_enabled should be false") // ========== Test 2: With IsFree, limitations are still disabled ========== sub.IsFree = true app.SubscriptionRepo.Update(sub) w = app.makeAuthenticatedRequest(t, "GET", "/api/subscription/status/", nil, token) require.Equal(t, http.StatusOK, w.Code) json.Unmarshal(w.Body.Bytes(), &statusResp) assert.False(t, statusResp["limitations_enabled"].(bool), "With IsFree and global limitations disabled, limitations_enabled should be false") // Both cases result in the same outcome - no limitations err = app.SubscriptionService.CheckLimit(userID, "properties") assert.NoError(t, err, "Should bypass limits when global limitations are disabled") }