import pytest from unittest.mock import patch, MagicMock from fastapi import HTTPException class TestCouponRoutes: """Test cases for coupon management routes""" def test_generate_single_code_unauthorized(self, client): """Test generate single code without authentication""" response = client.post("/generate", data={"mode": "single", "count": 1}) assert response.status_code == 401 data = response.json() assert data["detail"] == "Unauthorized" def test_generate_single_code_success(self, client, auth_headers): """Test successful single code generation""" with patch('routes.auth.generate_coupon') as mock_generate: mock_generate.return_value = "ABC123DEF4" response = client.post("/generate", data={"mode": "single", "count": 1}, headers=auth_headers) assert response.status_code == 200 data = response.json() assert data["code"] == "ABC123DEF4" mock_generate.assert_called_once() def test_generate_bulk_codes_success(self, client, auth_headers): """Test successful bulk code generation""" with patch('routes.auth.generate_coupon') as mock_generate: mock_generate.side_effect = ["CODE1", "CODE2", "CODE3"] response = client.post("/generate", data={"mode": "bulk", "count": 3}, headers=auth_headers) assert response.status_code == 200 data = response.json() assert data["codes"] == ["CODE1", "CODE2", "CODE3"] assert mock_generate.call_count == 3 def test_generate_invalid_mode(self, client, auth_headers): """Test code generation with invalid mode""" response = client.post("/generate", data={"mode": "invalid", "count": 1}, headers=auth_headers) assert response.status_code == 400 data = response.json() assert data["detail"] == "Invalid mode" def test_generate_bulk_zero_count(self, client, auth_headers): """Test bulk generation with zero count""" response = client.post("/generate", data={"mode": "bulk", "count": 0}, headers=auth_headers) assert response.status_code == 200 data = response.json() assert data["codes"] == [] def test_list_codes_pagination(self, client, sample_coupons): """Test coupon listing with pagination""" response = client.get("/list?page=1&limit=2") assert response.status_code == 200 data = response.json() assert "codes" in data assert "total" in data assert "page" in data assert "limit" in data assert "total_pages" in data assert data["page"] == 1 assert data["limit"] == 2 assert data["total"] == 3 assert len(data["codes"]) == 2 def test_list_codes_default_pagination(self, client, sample_coupons): """Test coupon listing with default pagination""" response = client.get("/list") assert response.status_code == 200 data = response.json() assert data["page"] == 1 assert data["limit"] == 20 assert len(data["codes"]) == 3 def test_list_codes_empty_database(self, client): """Test coupon listing with empty database""" response = client.get("/list") assert response.status_code == 200 data = response.json() assert data["codes"] == [] assert data["total"] == 0 assert data["page"] == 1 assert data["limit"] == 20 assert data["total_pages"] == 0 def test_list_codes_second_page(self, client, sample_coupons): """Test coupon listing second page""" response = client.get("/list?page=2&limit=2") assert response.status_code == 200 data = response.json() assert data["page"] == 2 assert data["limit"] == 2 assert len(data["codes"]) == 1 # Only 1 code left on page 2 def test_search_codes_success(self, client, sample_coupons): """Test successful code search""" response = client.get("/search-codes?query=TEST") assert response.status_code == 200 data = response.json() assert len(data) == 1 assert data[0]["code"] == "TEST123" assert "used" in data[0] assert "usage_count" in data[0] assert "used_at" in data[0] def test_search_codes_case_insensitive(self, client, sample_coupons): """Test case-insensitive code search""" response = client.get("/search-codes?query=test") assert response.status_code == 200 data = response.json() assert len(data) == 1 assert data[0]["code"] == "TEST123" def test_search_codes_partial_match(self, client, sample_coupons): """Test partial code search""" response = client.get("/search-codes?query=123") assert response.status_code == 200 data = response.json() assert len(data) == 1 assert data[0]["code"] == "TEST123" def test_search_codes_no_results(self, client, sample_coupons): """Test code search with no results""" response = client.get("/search-codes?query=NONEXISTENT") assert response.status_code == 200 data = response.json() assert data == [] def test_search_codes_empty_query(self, client, sample_coupons): """Test code search with empty query""" response = client.get("/search-codes?query=") assert response.status_code == 200 data = response.json() # Should return all codes when query is empty assert len(data) == 3 def test_use_code_success(self, client, sample_coupons): """Test successful code usage""" response = client.post("/use-code", json={"code": "TEST123"}) assert response.status_code == 200 data = response.json() assert data["code"] == "TEST123" assert "used_at" in data def test_use_code_case_insensitive(self, client, sample_coupons): """Test case-insensitive code usage""" response = client.post("/use-code", json={"code": "test123"}) assert response.status_code == 200 data = response.json() assert data["code"] == "TEST123" def test_use_code_not_found(self, client): """Test using non-existent code""" response = client.post("/use-code", json={"code": "NONEXISTENT"}) assert response.status_code == 404 data = response.json() assert data["detail"] == "Invalid code" def test_use_code_already_used(self, client, used_coupon): """Test using already used code""" response = client.post("/use-code", json={"code": "USED123"}) assert response.status_code == 400 data = response.json() assert data["detail"] == "Coupon already used" def test_use_code_whitespace_handling(self, client, sample_coupons): """Test code usage with whitespace""" response = client.post("/use-code", json={"code": " TEST123 "}) assert response.status_code == 200 data = response.json() assert data["code"] == "TEST123" def test_check_code_success(self, client, sample_coupons): """Test successful code check""" response = client.get("/check-code/TEST123") assert response.status_code == 200 data = response.json() assert data["code"] == "TEST123" assert data["used"] == 0 def test_check_code_case_insensitive(self, client, sample_coupons): """Test case-insensitive code check""" response = client.get("/check-code/test123") assert response.status_code == 200 data = response.json() assert data["code"] == "TEST123" def test_check_code_not_found(self, client): """Test checking non-existent code""" response = client.get("/check-code/NONEXISTENT") assert response.status_code == 404 data = response.json() assert data["detail"] == "Code not found" def test_check_code_whitespace_handling(self, client, sample_coupons): """Test code check with whitespace""" response = client.get("/check-code/ TEST123 ") assert response.status_code == 200 data = response.json() assert data["code"] == "TEST123" def test_verify_coupon_success(self, client, sample_coupons): """Test successful coupon verification""" response = client.post("/verify", json={"code": "TEST123"}) assert response.status_code == 200 data = response.json() assert data["message"] == "Coupon verified" assert "used_at" in data def test_verify_coupon_case_insensitive(self, client, sample_coupons): """Test case-insensitive coupon verification""" response = client.post("/verify", json={"code": "test123"}) assert response.status_code == 200 data = response.json() assert data["message"] == "Coupon verified" def test_verify_coupon_not_found(self, client): """Test verifying non-existent coupon""" response = client.post("/verify", json={"code": "NONEXISTENT"}) assert response.status_code == 404 data = response.json() assert data["detail"] == "Invalid coupon code" def test_verify_coupon_already_used(self, client, used_coupon): """Test verifying already used coupon""" response = client.post("/verify", json={"code": "USED123"}) assert response.status_code == 400 data = response.json() assert data["detail"] == "Coupon already used" def test_verify_coupon_whitespace_handling(self, client, sample_coupons): """Test coupon verification with whitespace""" response = client.post("/verify", json={"code": " TEST123 "}) assert response.status_code == 200 data = response.json() assert data["message"] == "Coupon verified" def test_add_code_unauthorized(self, client): """Test adding code without authentication""" code_data = {"code": "NEW123", "usage": 0} response = client.post("/add-code", json=code_data) assert response.status_code == 401 data = response.json() assert data["detail"] == "Unauthorized" def test_add_code_success(self, client, auth_headers): """Test successful code addition""" code_data = {"code": "NEW123", "usage": 0} response = client.post("/add-code", json=code_data, headers=auth_headers) assert response.status_code == 200 data = response.json() assert data["message"] == "Code added successfully" def test_add_code_already_exists(self, client, sample_coupons, auth_headers): """Test adding code that already exists""" code_data = {"code": "TEST123", "usage": 0} response = client.post("/add-code", json=code_data, headers=auth_headers) assert response.status_code == 400 data = response.json() assert data["detail"] == "Code already exists" def test_add_code_case_normalization(self, client, auth_headers): """Test code case normalization during addition""" code_data = {"code": "new123", "usage": 0} response = client.post("/add-code", json=code_data, headers=auth_headers) assert response.status_code == 200 # Verify the code was stored in uppercase response = client.get("/check-code/NEW123") assert response.status_code == 200 def test_add_code_negative_usage(self, client, auth_headers): """Test adding code with negative usage count""" code_data = {"code": "NEW123", "usage": -5} response = client.post("/add-code", json=code_data, headers=auth_headers) assert response.status_code == 200 # Verify usage count was normalized to 0 response = client.get("/check-code/NEW123") assert response.status_code == 200 data = response.json() assert data["used"] == 0 def test_delete_code_unauthorized(self, client): """Test deleting code without authentication""" response = client.delete("/delete-code/TEST123") assert response.status_code == 401 data = response.json() assert data["detail"] == "Unauthorized" def test_delete_code_success(self, client, sample_coupons, auth_headers): """Test successful code deletion""" response = client.delete("/delete-code/TEST123", headers=auth_headers) assert response.status_code == 200 data = response.json() assert data["message"] == "Code deleted successfully" # Verify code is deleted response = client.get("/check-code/TEST123") assert response.status_code == 404 def test_delete_code_case_insensitive(self, client, sample_coupons, auth_headers): """Test case-insensitive code deletion""" response = client.delete("/delete-code/test123", headers=auth_headers) assert response.status_code == 200 data = response.json() assert data["message"] == "Code deleted successfully" def test_delete_code_not_found(self, client, auth_headers): """Test deleting non-existent code""" response = client.delete("/delete-code/NONEXISTENT", headers=auth_headers) assert response.status_code == 404 data = response.json() assert data["detail"] == "Code not found" def test_delete_code_whitespace_handling(self, client, sample_coupons, auth_headers): """Test code deletion with whitespace""" response = client.delete("/delete-code/ TEST123 ", headers=auth_headers) assert response.status_code == 200 data = response.json() assert data["message"] == "Code deleted successfully" def test_upload_codes_unauthorized(self, client): """Test uploading codes without authentication""" upload_data = { "codes": [ {"code": "UPLOAD1", "usage": 0}, {"code": "UPLOAD2", "usage": 0} ] } response = client.post("/upload-codes", json=upload_data) assert response.status_code == 401 data = response.json() assert data["detail"] == "Unauthorized" def test_upload_codes_success(self, client, auth_headers): """Test successful code upload""" upload_data = { "codes": [ {"code": "UPLOAD1", "usage": 0}, {"code": "UPLOAD2", "usage": 1} ] } response = client.post("/upload-codes", json=upload_data, headers=auth_headers) assert response.status_code == 200 data = response.json() assert data["uploaded"] == 2 assert data["skipped"] == 0 assert data["total"] == 2 def test_upload_codes_with_duplicates(self, client, sample_coupons, auth_headers): """Test code upload with duplicate codes""" upload_data = { "codes": [ {"code": "TEST123", "usage": 0}, # Already exists {"code": "NEW123", "usage": 0} # New code ] } response = client.post("/upload-codes", json=upload_data, headers=auth_headers) assert response.status_code == 200 data = response.json() assert data["uploaded"] == 1 assert data["skipped"] == 1 assert data["total"] == 2 def test_upload_codes_case_normalization(self, client, auth_headers): """Test code case normalization during upload""" upload_data = { "codes": [ {"code": "lowercase", "usage": 0}, {"code": "MIXEDCase", "usage": 0} ] } response = client.post("/upload-codes", json=upload_data, headers=auth_headers) assert response.status_code == 200 data = response.json() assert data["uploaded"] == 2 # Verify codes were stored in uppercase response = client.get("/check-code/LOWERCASE") assert response.status_code == 200 response = client.get("/check-code/MIXEDCASE") assert response.status_code == 200