# tests/unit/services/test_letzshop_service.py """ Unit tests for Letzshop integration services. Tests cover: - Encryption utility - Credentials service - GraphQL client (mocked) """ from unittest.mock import MagicMock, patch import pytest from app.services.letzshop import ( CredentialsNotFoundError, LetzshopAPIError, LetzshopClient, LetzshopCredentialsService, ) from app.utils.encryption import ( EncryptionError, EncryptionService, mask_api_key, ) # ============================================================================ # Encryption Tests # ============================================================================ @pytest.mark.unit @pytest.mark.letzshop class TestEncryptionService: """Test suite for encryption utility.""" def test_encrypt_and_decrypt(self): """Test basic encryption and decryption.""" service = EncryptionService(secret_key="test-secret-key-12345") original = "my-secret-api-key" encrypted = service.encrypt(original) decrypted = service.decrypt(encrypted) assert encrypted != original assert decrypted == original def test_encrypt_empty_string_fails(self): """Test that encrypting empty string raises error.""" service = EncryptionService(secret_key="test-secret-key-12345") with pytest.raises(EncryptionError): service.encrypt("") def test_decrypt_empty_string_fails(self): """Test that decrypting empty string raises error.""" service = EncryptionService(secret_key="test-secret-key-12345") with pytest.raises(EncryptionError): service.decrypt("") def test_decrypt_invalid_ciphertext_fails(self): """Test that decrypting invalid ciphertext raises error.""" service = EncryptionService(secret_key="test-secret-key-12345") with pytest.raises(EncryptionError): service.decrypt("invalid-ciphertext") def test_is_valid_ciphertext(self): """Test ciphertext validation.""" service = EncryptionService(secret_key="test-secret-key-12345") encrypted = service.encrypt("test-value") assert service.is_valid_ciphertext(encrypted) is True assert service.is_valid_ciphertext("invalid") is False def test_different_keys_produce_different_results(self): """Test that different keys produce different encryptions.""" service1 = EncryptionService(secret_key="key-one-12345") service2 = EncryptionService(secret_key="key-two-12345") original = "test-value" encrypted1 = service1.encrypt(original) encrypted2 = service2.encrypt(original) assert encrypted1 != encrypted2 @pytest.mark.unit @pytest.mark.letzshop class TestMaskApiKey: """Test suite for API key masking.""" def test_mask_api_key_default(self): """Test default masking (4 visible chars).""" masked = mask_api_key("letzshop-api-key-12345") assert masked == "letz******************" def test_mask_api_key_custom_visible(self): """Test masking with custom visible chars.""" masked = mask_api_key("abcdefghij", visible_chars=6) assert masked == "abcdef****" def test_mask_api_key_short(self): """Test masking short key.""" masked = mask_api_key("abc", visible_chars=4) assert masked == "***" def test_mask_api_key_empty(self): """Test masking empty string.""" masked = mask_api_key("") assert masked == "" # ============================================================================ # Credentials Service Tests # ============================================================================ @pytest.mark.unit @pytest.mark.letzshop class TestLetzshopCredentialsService: """Test suite for Letzshop credentials service.""" def test_create_credentials(self, db, test_vendor): """Test creating credentials for a vendor.""" service = LetzshopCredentialsService(db) credentials = service.create_credentials( vendor_id=test_vendor.id, api_key="test-api-key-12345", auto_sync_enabled=False, sync_interval_minutes=30, ) assert credentials.vendor_id == test_vendor.id assert credentials.api_key_encrypted != "test-api-key-12345" assert credentials.auto_sync_enabled is False assert credentials.sync_interval_minutes == 30 def test_get_credentials(self, db, test_vendor): """Test getting credentials for a vendor.""" service = LetzshopCredentialsService(db) # Create first service.create_credentials( vendor_id=test_vendor.id, api_key="test-api-key", ) # Get credentials = service.get_credentials(test_vendor.id) assert credentials is not None assert credentials.vendor_id == test_vendor.id def test_get_credentials_not_found(self, db, test_vendor): """Test getting non-existent credentials returns None.""" service = LetzshopCredentialsService(db) credentials = service.get_credentials(test_vendor.id) assert credentials is None def test_get_credentials_or_raise(self, db, test_vendor): """Test get_credentials_or_raise raises for non-existent.""" service = LetzshopCredentialsService(db) with pytest.raises(CredentialsNotFoundError): service.get_credentials_or_raise(test_vendor.id) def test_update_credentials(self, db, test_vendor): """Test updating credentials.""" service = LetzshopCredentialsService(db) # Create first service.create_credentials( vendor_id=test_vendor.id, api_key="original-key", auto_sync_enabled=False, ) # Update updated = service.update_credentials( vendor_id=test_vendor.id, auto_sync_enabled=True, sync_interval_minutes=60, ) assert updated.auto_sync_enabled is True assert updated.sync_interval_minutes == 60 def test_delete_credentials(self, db, test_vendor): """Test deleting credentials.""" service = LetzshopCredentialsService(db) # Create first service.create_credentials( vendor_id=test_vendor.id, api_key="test-key", ) # Delete result = service.delete_credentials(test_vendor.id) assert result is True # Verify deleted assert service.get_credentials(test_vendor.id) is None def test_delete_credentials_not_found(self, db, test_vendor): """Test deleting non-existent credentials returns False.""" service = LetzshopCredentialsService(db) result = service.delete_credentials(test_vendor.id) assert result is False def test_upsert_credentials_create(self, db, test_vendor): """Test upsert creates when not exists.""" service = LetzshopCredentialsService(db) credentials = service.upsert_credentials( vendor_id=test_vendor.id, api_key="new-key", ) assert credentials.vendor_id == test_vendor.id def test_upsert_credentials_update(self, db, test_vendor): """Test upsert updates when exists.""" service = LetzshopCredentialsService(db) # Create first service.create_credentials( vendor_id=test_vendor.id, api_key="original-key", auto_sync_enabled=False, ) # Upsert with new values credentials = service.upsert_credentials( vendor_id=test_vendor.id, api_key="updated-key", auto_sync_enabled=True, ) assert credentials.auto_sync_enabled is True def test_get_decrypted_api_key(self, db, test_vendor): """Test getting decrypted API key.""" service = LetzshopCredentialsService(db) original_key = "my-secret-api-key" service.create_credentials( vendor_id=test_vendor.id, api_key=original_key, ) decrypted = service.get_decrypted_api_key(test_vendor.id) assert decrypted == original_key def test_get_masked_api_key(self, db, test_vendor): """Test getting masked API key.""" service = LetzshopCredentialsService(db) service.create_credentials( vendor_id=test_vendor.id, api_key="letzshop-api-key-12345", ) masked = service.get_masked_api_key(test_vendor.id) assert masked.startswith("letz") assert "*" in masked def test_is_configured(self, db, test_vendor): """Test is_configured check.""" service = LetzshopCredentialsService(db) assert service.is_configured(test_vendor.id) is False service.create_credentials( vendor_id=test_vendor.id, api_key="test-key", ) assert service.is_configured(test_vendor.id) is True def test_get_status(self, db, test_vendor): """Test getting integration status.""" service = LetzshopCredentialsService(db) # Not configured status = service.get_status(test_vendor.id) assert status["is_configured"] is False assert status["auto_sync_enabled"] is False # Configured service.create_credentials( vendor_id=test_vendor.id, api_key="test-key", auto_sync_enabled=True, ) status = service.get_status(test_vendor.id) assert status["is_configured"] is True assert status["auto_sync_enabled"] is True # ============================================================================ # GraphQL Client Tests (Mocked) # ============================================================================ @pytest.mark.unit @pytest.mark.letzshop class TestLetzshopClient: """Test suite for Letzshop GraphQL client (mocked).""" def test_client_initialization(self): """Test client initialization.""" client = LetzshopClient( api_key="test-key", endpoint="https://test.example.com/graphql", timeout=60, ) assert client.api_key == "test-key" assert client.endpoint == "https://test.example.com/graphql" assert client.timeout == 60 def test_client_context_manager(self): """Test client can be used as context manager.""" with LetzshopClient(api_key="test-key") as client: assert client is not None @patch("requests.Session.post") def test_test_connection_success(self, mock_post): """Test successful connection test.""" mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = {"data": {"__typename": "Query"}} mock_post.return_value = mock_response client = LetzshopClient(api_key="test-key") success, response_time, error = client.test_connection() assert success is True assert response_time > 0 assert error is None @patch("requests.Session.post") def test_test_connection_auth_failure(self, mock_post): """Test connection test with auth failure.""" mock_response = MagicMock() mock_response.status_code = 401 mock_post.return_value = mock_response client = LetzshopClient(api_key="invalid-key") success, response_time, error = client.test_connection() assert success is False assert "Authentication" in error @patch("requests.Session.post") def test_get_shipments(self, mock_post): """Test getting shipments.""" mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { "data": { "shipments": { "nodes": [ {"id": "ship_1", "state": "unconfirmed"}, {"id": "ship_2", "state": "unconfirmed"}, ] } } } mock_post.return_value = mock_response client = LetzshopClient(api_key="test-key") shipments = client.get_shipments(state="unconfirmed") assert len(shipments) == 2 assert shipments[0]["id"] == "ship_1" @patch("requests.Session.post") def test_confirm_inventory_units(self, mock_post): """Test confirming inventory units.""" mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { "data": { "confirmInventoryUnits": { "inventoryUnits": [ {"id": "unit_1", "state": "confirmed"}, ], "errors": [], } } } mock_post.return_value = mock_response client = LetzshopClient(api_key="test-key") result = client.confirm_inventory_units(["unit_1"]) assert result["inventoryUnits"][0]["state"] == "confirmed" assert len(result["errors"]) == 0 @patch("requests.Session.post") def test_set_shipment_tracking(self, mock_post): """Test setting shipment tracking.""" mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { "data": { "setShipmentTracking": { "shipment": { "id": "ship_1", "tracking": {"code": "1Z999AA1", "provider": "ups"}, }, "errors": [], } } } mock_post.return_value = mock_response client = LetzshopClient(api_key="test-key") result = client.set_shipment_tracking( shipment_id="ship_1", tracking_code="1Z999AA1", tracking_provider="ups", ) assert result["shipment"]["tracking"]["code"] == "1Z999AA1" @patch("requests.Session.post") def test_graphql_error_handling(self, mock_post): """Test GraphQL error response handling.""" mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { "errors": [{"message": "Invalid shipment ID"}] } mock_post.return_value = mock_response client = LetzshopClient(api_key="test-key") with pytest.raises(LetzshopAPIError) as exc_info: client.get_shipments() assert "Invalid shipment ID" in str(exc_info.value)