# tests/integration/api/v1/platform/test_signup.py """Integration tests for platform signup API endpoints. Tests the /api/v1/platform/signup/* endpoints. """ from unittest.mock import MagicMock, patch import pytest from models.database.company import Company from models.database.subscription import TierCode from models.database.user import User from models.database.vendor import Vendor @pytest.fixture def mock_stripe_service(): """Mock the Stripe service for tests.""" with patch("app.services.platform_signup_service.stripe_service") as mock: mock.create_customer.return_value = "cus_test_123" mock.create_setup_intent.return_value = MagicMock( id="seti_test_123", client_secret="seti_test_123_secret_abc", status="requires_payment_method", ) mock.get_setup_intent.return_value = MagicMock( id="seti_test_123", status="succeeded", payment_method="pm_test_123", ) mock.attach_payment_method_to_customer.return_value = None yield mock @pytest.fixture def signup_session(client): """Create a signup session for testing.""" response = client.post( "/api/v1/platform/signup/start", json={"tier_code": TierCode.PROFESSIONAL.value, "is_annual": False}, ) return response.json()["session_id"] @pytest.fixture def existing_user(db, auth_manager): """Create an existing user for testing duplicate email.""" user = User( email="existing@example.com", username="existing_user", hashed_password=auth_manager.hash_password("password123"), first_name="Existing", last_name="User", role="vendor", is_active=True, ) db.add(user) db.commit() return user @pytest.fixture def claimed_owner_user(db, auth_manager): """Create an owner user for the claimed vendor.""" user = User( email="claimed_owner@test.com", username="claimed_owner", hashed_password=auth_manager.hash_password("testpass123"), role="vendor", is_active=True, ) db.add(user) db.commit() return user @pytest.fixture def claimed_letzshop_vendor(db, claimed_owner_user): """Create a vendor that has already claimed a Letzshop shop.""" company = Company( name="Claimed Company", owner_user_id=claimed_owner_user.id, contact_email="claimed@test.com", ) db.add(company) db.flush() vendor = Vendor( company_id=company.id, vendor_code="CLAIMED", subdomain="claimed", name="Claimed Vendor", contact_email="claimed@test.com", is_active=True, letzshop_vendor_slug="already-claimed-shop", ) db.add(vendor) db.commit() return vendor @pytest.mark.integration @pytest.mark.api @pytest.mark.platform class TestSignupStartAPI: """Test signup start endpoint at /api/v1/platform/signup/start.""" def test_start_signup_success(self, client): """Test starting a signup session.""" response = client.post( "/api/v1/platform/signup/start", json={"tier_code": TierCode.ESSENTIAL.value, "is_annual": False}, ) assert response.status_code == 200 data = response.json() assert "session_id" in data assert data["tier_code"] == TierCode.ESSENTIAL.value assert data["is_annual"] is False def test_start_signup_with_annual_billing(self, client): """Test starting signup with annual billing.""" response = client.post( "/api/v1/platform/signup/start", json={"tier_code": TierCode.PROFESSIONAL.value, "is_annual": True}, ) assert response.status_code == 200 data = response.json() assert data["is_annual"] is True def test_start_signup_all_tiers(self, client): """Test starting signup for all valid tiers.""" for tier in [TierCode.ESSENTIAL, TierCode.PROFESSIONAL, TierCode.BUSINESS, TierCode.ENTERPRISE]: response = client.post( "/api/v1/platform/signup/start", json={"tier_code": tier.value, "is_annual": False}, ) assert response.status_code == 200 assert response.json()["tier_code"] == tier.value def test_start_signup_invalid_tier(self, client): """Test starting signup with invalid tier code.""" response = client.post( "/api/v1/platform/signup/start", json={"tier_code": "invalid_tier", "is_annual": False}, ) assert response.status_code == 422 # ValidationException data = response.json() assert "invalid tier" in data["message"].lower() def test_start_signup_session_id_is_unique(self, client): """Test that each signup session gets a unique ID.""" response1 = client.post( "/api/v1/platform/signup/start", json={"tier_code": TierCode.ESSENTIAL.value, "is_annual": False}, ) response2 = client.post( "/api/v1/platform/signup/start", json={"tier_code": TierCode.ESSENTIAL.value, "is_annual": False}, ) assert response1.json()["session_id"] != response2.json()["session_id"] @pytest.mark.integration @pytest.mark.api @pytest.mark.platform class TestClaimVendorAPI: """Test claim vendor endpoint at /api/v1/platform/signup/claim-vendor.""" def test_claim_vendor_success(self, client, signup_session): """Test claiming a Letzshop vendor.""" response = client.post( "/api/v1/platform/signup/claim-vendor", json={ "session_id": signup_session, "letzshop_slug": "my-new-shop", }, ) assert response.status_code == 200 data = response.json() assert data["session_id"] == signup_session assert data["letzshop_slug"] == "my-new-shop" assert data["vendor_name"] is not None def test_claim_vendor_with_vendor_id(self, client, signup_session): """Test claiming vendor with Letzshop vendor ID.""" response = client.post( "/api/v1/platform/signup/claim-vendor", json={ "session_id": signup_session, "letzshop_slug": "my-shop", "letzshop_vendor_id": "letz_vendor_123", }, ) assert response.status_code == 200 data = response.json() assert data["letzshop_slug"] == "my-shop" def test_claim_vendor_invalid_session(self, client): """Test claiming vendor with invalid session.""" response = client.post( "/api/v1/platform/signup/claim-vendor", json={ "session_id": "invalid_session_id", "letzshop_slug": "my-shop", }, ) assert response.status_code == 404 data = response.json() assert "not found" in data["message"].lower() def test_claim_vendor_already_claimed(self, client, signup_session, claimed_letzshop_vendor): """Test claiming a vendor that's already claimed.""" response = client.post( "/api/v1/platform/signup/claim-vendor", json={ "session_id": signup_session, "letzshop_slug": "already-claimed-shop", }, ) assert response.status_code == 409 # ConflictException data = response.json() assert "already claimed" in data["message"].lower() @pytest.mark.integration @pytest.mark.api @pytest.mark.platform class TestCreateAccountAPI: """Test create account endpoint at /api/v1/platform/signup/create-account.""" def test_create_account_success(self, client, signup_session, mock_stripe_service): """Test creating an account.""" response = client.post( "/api/v1/platform/signup/create-account", json={ "session_id": signup_session, "email": "newuser@example.com", "password": "SecurePass123!", "first_name": "John", "last_name": "Doe", "company_name": "Test Company", }, ) assert response.status_code == 200 data = response.json() assert data["session_id"] == signup_session assert data["user_id"] > 0 assert data["vendor_id"] > 0 assert data["stripe_customer_id"] == "cus_test_123" def test_create_account_with_phone(self, client, signup_session, mock_stripe_service): """Test creating an account with phone number.""" response = client.post( "/api/v1/platform/signup/create-account", json={ "session_id": signup_session, "email": "user2@example.com", "password": "SecurePass123!", "first_name": "Jane", "last_name": "Smith", "company_name": "Another Company", "phone": "+352 123 456 789", }, ) assert response.status_code == 200 data = response.json() assert data["user_id"] > 0 def test_create_account_invalid_session(self, client, mock_stripe_service): """Test creating account with invalid session.""" response = client.post( "/api/v1/platform/signup/create-account", json={ "session_id": "invalid_session", "email": "test@example.com", "password": "SecurePass123!", "first_name": "Test", "last_name": "User", "company_name": "Test Co", }, ) assert response.status_code == 404 def test_create_account_duplicate_email( self, client, signup_session, existing_user, mock_stripe_service ): """Test creating account with existing email.""" response = client.post( "/api/v1/platform/signup/create-account", json={ "session_id": signup_session, "email": "existing@example.com", "password": "SecurePass123!", "first_name": "Another", "last_name": "User", "company_name": "Duplicate Co", }, ) assert response.status_code == 409 # ConflictException data = response.json() assert "already exists" in data["message"].lower() def test_create_account_invalid_email(self, client, signup_session): """Test creating account with invalid email format.""" response = client.post( "/api/v1/platform/signup/create-account", json={ "session_id": signup_session, "email": "not-an-email", "password": "SecurePass123!", "first_name": "Test", "last_name": "User", "company_name": "Test Co", }, ) assert response.status_code == 422 # Validation error def test_create_account_with_letzshop_claim(self, client, mock_stripe_service): """Test creating account after claiming Letzshop vendor.""" # Start signup start_response = client.post( "/api/v1/platform/signup/start", json={"tier_code": TierCode.PROFESSIONAL.value, "is_annual": False}, ) session_id = start_response.json()["session_id"] # Claim vendor client.post( "/api/v1/platform/signup/claim-vendor", json={ "session_id": session_id, "letzshop_slug": "my-shop-claim", }, ) # Create account response = client.post( "/api/v1/platform/signup/create-account", json={ "session_id": session_id, "email": "shop@example.com", "password": "SecurePass123!", "first_name": "Shop", "last_name": "Owner", "company_name": "My Shop", }, ) assert response.status_code == 200 data = response.json() assert data["vendor_id"] > 0 @pytest.mark.integration @pytest.mark.api @pytest.mark.platform class TestSetupPaymentAPI: """Test setup payment endpoint at /api/v1/platform/signup/setup-payment.""" def test_setup_payment_success(self, client, signup_session, mock_stripe_service): """Test setting up payment after account creation.""" # Create account first client.post( "/api/v1/platform/signup/create-account", json={ "session_id": signup_session, "email": "payment@example.com", "password": "SecurePass123!", "first_name": "Payment", "last_name": "Test", "company_name": "Payment Co", }, ) # Setup payment response = client.post( "/api/v1/platform/signup/setup-payment", json={"session_id": signup_session}, ) assert response.status_code == 200 data = response.json() assert data["session_id"] == signup_session assert "client_secret" in data assert data["stripe_customer_id"] == "cus_test_123" def test_setup_payment_invalid_session(self, client, mock_stripe_service): """Test setup payment with invalid session.""" response = client.post( "/api/v1/platform/signup/setup-payment", json={"session_id": "invalid_session"}, ) assert response.status_code == 404 def test_setup_payment_without_account(self, client, signup_session, mock_stripe_service): """Test setup payment without creating account first.""" response = client.post( "/api/v1/platform/signup/setup-payment", json={"session_id": signup_session}, ) assert response.status_code == 422 # ValidationException data = response.json() assert "account not created" in data["message"].lower() @pytest.mark.integration @pytest.mark.api @pytest.mark.platform class TestCompleteSignupAPI: """Test complete signup endpoint at /api/v1/platform/signup/complete.""" def test_complete_signup_success(self, client, signup_session, mock_stripe_service, db): """Test completing signup after payment setup.""" # Create account client.post( "/api/v1/platform/signup/create-account", json={ "session_id": signup_session, "email": "complete@example.com", "password": "SecurePass123!", "first_name": "Complete", "last_name": "User", "company_name": "Complete Co", }, ) # Setup payment client.post( "/api/v1/platform/signup/setup-payment", json={"session_id": signup_session}, ) # Complete signup response = client.post( "/api/v1/platform/signup/complete", json={ "session_id": signup_session, "setup_intent_id": "seti_test_123", }, ) assert response.status_code == 200 data = response.json() assert data["success"] is True assert "vendor_code" in data assert "vendor_id" in data assert "redirect_url" in data assert "trial_ends_at" in data def test_complete_signup_returns_access_token( self, client, signup_session, mock_stripe_service, db ): """Test that completing signup returns a valid JWT access token for auto-login.""" # Create account client.post( "/api/v1/platform/signup/create-account", json={ "session_id": signup_session, "email": "token_test@example.com", "password": "SecurePass123!", "first_name": "Token", "last_name": "Test", "company_name": "Token Test Co", }, ) # Setup payment client.post( "/api/v1/platform/signup/setup-payment", json={"session_id": signup_session}, ) # Complete signup response = client.post( "/api/v1/platform/signup/complete", json={ "session_id": signup_session, "setup_intent_id": "seti_test_123", }, ) assert response.status_code == 200 data = response.json() # Verify access_token is returned assert "access_token" in data assert data["access_token"] is not None assert len(data["access_token"]) > 50 # JWT tokens are long def test_complete_signup_token_can_authenticate( self, client, signup_session, mock_stripe_service, db ): """Test that the returned access token can be used to authenticate API calls.""" # Create account client.post( "/api/v1/platform/signup/create-account", json={ "session_id": signup_session, "email": "auth_test@example.com", "password": "SecurePass123!", "first_name": "Auth", "last_name": "Test", "company_name": "Auth Test Co", }, ) # Setup payment client.post( "/api/v1/platform/signup/setup-payment", json={"session_id": signup_session}, ) # Complete signup complete_response = client.post( "/api/v1/platform/signup/complete", json={ "session_id": signup_session, "setup_intent_id": "seti_test_123", }, ) assert complete_response.status_code == 200 access_token = complete_response.json()["access_token"] # Use the token to access a protected vendor endpoint auth_response = client.get( "/api/v1/vendor/onboarding/status", headers={"Authorization": f"Bearer {access_token}"}, ) # Should be able to access the onboarding endpoint assert auth_response.status_code == 200 def test_complete_signup_sets_vendor_token_cookie( self, client, signup_session, mock_stripe_service, db ): """Test that completing signup sets the vendor_token HTTP-only cookie.""" # Create account client.post( "/api/v1/platform/signup/create-account", json={ "session_id": signup_session, "email": "cookie_test@example.com", "password": "SecurePass123!", "first_name": "Cookie", "last_name": "Test", "company_name": "Cookie Test Co", }, ) # Setup payment client.post( "/api/v1/platform/signup/setup-payment", json={"session_id": signup_session}, ) # Complete signup response = client.post( "/api/v1/platform/signup/complete", json={ "session_id": signup_session, "setup_intent_id": "seti_test_123", }, ) assert response.status_code == 200 # Check that vendor_token cookie is set cookies = response.cookies assert "vendor_token" in cookies def test_complete_signup_invalid_session(self, client, mock_stripe_service): """Test completing signup with invalid session.""" response = client.post( "/api/v1/platform/signup/complete", json={ "session_id": "invalid_session", "setup_intent_id": "seti_test_123", }, ) assert response.status_code == 404 def test_complete_signup_payment_not_succeeded( self, client, signup_session, mock_stripe_service ): """Test completing signup when payment setup failed.""" # Create account client.post( "/api/v1/platform/signup/create-account", json={ "session_id": signup_session, "email": "fail@example.com", "password": "SecurePass123!", "first_name": "Fail", "last_name": "User", "company_name": "Fail Co", }, ) # Setup payment client.post( "/api/v1/platform/signup/setup-payment", json={"session_id": signup_session}, ) # Mock failed setup intent mock_stripe_service.get_setup_intent.return_value = MagicMock( id="seti_failed", status="requires_payment_method", payment_method=None, ) # Complete signup response = client.post( "/api/v1/platform/signup/complete", json={ "session_id": signup_session, "setup_intent_id": "seti_failed", }, ) assert response.status_code == 422 # ValidationException data = response.json() assert "not completed" in data["message"].lower() @pytest.mark.integration @pytest.mark.api @pytest.mark.platform class TestGetSignupSessionAPI: """Test get signup session endpoint at /api/v1/platform/signup/session/{session_id}.""" def test_get_session_after_start(self, client, signup_session): """Test getting session after starting signup.""" response = client.get(f"/api/v1/platform/signup/session/{signup_session}") assert response.status_code == 200 data = response.json() assert data["session_id"] == signup_session assert data["step"] == "tier_selected" assert data["tier_code"] == TierCode.PROFESSIONAL.value assert "created_at" in data def test_get_session_after_claim(self, client, signup_session): """Test getting session after claiming vendor.""" client.post( "/api/v1/platform/signup/claim-vendor", json={ "session_id": signup_session, "letzshop_slug": "my-session-shop", }, ) response = client.get(f"/api/v1/platform/signup/session/{signup_session}") assert response.status_code == 200 data = response.json() assert data["step"] == "vendor_claimed" assert data["letzshop_slug"] == "my-session-shop" def test_get_session_invalid_id(self, client): """Test getting non-existent session.""" response = client.get("/api/v1/platform/signup/session/invalid_id") assert response.status_code == 404 @pytest.mark.integration @pytest.mark.api @pytest.mark.platform class TestSignupFullFlow: """Test complete signup flow end-to-end.""" def test_full_signup_flow(self, client, mock_stripe_service, db): """Test the complete signup flow from start to finish.""" # Step 1: Start signup start_response = client.post( "/api/v1/platform/signup/start", json={"tier_code": TierCode.BUSINESS.value, "is_annual": True}, ) assert start_response.status_code == 200 session_id = start_response.json()["session_id"] # Step 2: Claim Letzshop vendor (optional) claim_response = client.post( "/api/v1/platform/signup/claim-vendor", json={ "session_id": session_id, "letzshop_slug": "full-flow-shop", }, ) assert claim_response.status_code == 200 # Step 3: Create account account_response = client.post( "/api/v1/platform/signup/create-account", json={ "session_id": session_id, "email": "fullflow@example.com", "password": "SecurePass123!", "first_name": "Full", "last_name": "Flow", "company_name": "Full Flow Company", "phone": "+352 123 456", }, ) assert account_response.status_code == 200 vendor_id = account_response.json()["vendor_id"] # Step 4: Setup payment payment_response = client.post( "/api/v1/platform/signup/setup-payment", json={"session_id": session_id}, ) assert payment_response.status_code == 200 assert "client_secret" in payment_response.json() # Step 5: Complete signup complete_response = client.post( "/api/v1/platform/signup/complete", json={ "session_id": session_id, "setup_intent_id": "seti_test_123", }, ) assert complete_response.status_code == 200 assert complete_response.json()["success"] is True # Verify vendor was created with Letzshop link vendor = db.query(Vendor).filter(Vendor.id == vendor_id).first() assert vendor is not None assert vendor.letzshop_vendor_slug == "full-flow-shop" assert vendor.is_active is True def test_signup_flow_without_letzshop_claim(self, client, mock_stripe_service, db): """Test signup flow skipping Letzshop claim step.""" # Step 1: Start signup start_response = client.post( "/api/v1/platform/signup/start", json={"tier_code": TierCode.ESSENTIAL.value, "is_annual": False}, ) session_id = start_response.json()["session_id"] # Skip Step 2, go directly to Step 3 account_response = client.post( "/api/v1/platform/signup/create-account", json={ "session_id": session_id, "email": "noletzshop@example.com", "password": "SecurePass123!", "first_name": "No", "last_name": "Letzshop", "company_name": "Independent Shop", }, ) assert account_response.status_code == 200 vendor_id = account_response.json()["vendor_id"] # Step 4 & 5: Payment and complete client.post( "/api/v1/platform/signup/setup-payment", json={"session_id": session_id}, ) complete_response = client.post( "/api/v1/platform/signup/complete", json={ "session_id": session_id, "setup_intent_id": "seti_test_123", }, ) assert complete_response.status_code == 200 # Verify vendor was created without Letzshop link vendor = db.query(Vendor).filter(Vendor.id == vendor_id).first() assert vendor is not None assert vendor.letzshop_vendor_slug is None