Files
orion/app/modules/loyalty/tests/unit/test_wallet_service.py
Samir Boulahtit 56afb9192b
Some checks failed
CI / ruff (push) Successful in 10s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled
fix(loyalty): fix wallet service test fixtures and mock paths
- Add customer_id to card fixtures (NOT NULL constraint)
- Use test_customer shared fixture instead of inline Customer creation
- Fix mock path to target source module for lazy imports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 22:50:16 +01:00

392 lines
14 KiB
Python

# app/modules/loyalty/tests/unit/test_wallet_service.py
"""
Unit tests for WalletService — wallet object creation during enrollment.
Tests that:
- Google Wallet objects are created and DB fields populated on enrollment
- Apple Wallet serial numbers are set when apple_pass_type_id is configured
- Wallet creation failures don't crash enrollment
- Wallet creation is skipped when not configured
"""
import uuid
from unittest.mock import MagicMock, patch
import pytest
from app.modules.loyalty.models import LoyaltyCard, LoyaltyProgram
from app.modules.loyalty.models.loyalty_program import LoyaltyType
from app.modules.loyalty.services.wallet_service import WalletService
# ============================================================================
# Fixtures
# Uses test_merchant and test_customer from shared fixtures (store_fixtures,
# customer_fixtures) which handle owner_user_id and other required fields.
# ============================================================================
@pytest.fixture
def wt_program(db, test_merchant):
"""Create a loyalty program for wallet tests."""
program = LoyaltyProgram(
merchant_id=test_merchant.id,
loyalty_type=LoyaltyType.POINTS.value,
points_per_euro=10,
welcome_bonus_points=0,
minimum_redemption_points=100,
cooldown_minutes=0,
max_daily_stamps=5,
require_staff_pin=False,
card_name="Wallet Test Rewards",
card_color="#4F46E5",
is_active=True,
)
db.add(program)
db.commit()
db.refresh(program)
return program
@pytest.fixture
def wt_program_with_apple(db, test_merchant):
"""Create a loyalty program with Apple Wallet configured."""
program = LoyaltyProgram(
merchant_id=test_merchant.id,
loyalty_type=LoyaltyType.POINTS.value,
points_per_euro=10,
welcome_bonus_points=0,
minimum_redemption_points=100,
cooldown_minutes=0,
max_daily_stamps=5,
require_staff_pin=False,
card_name="Apple Wallet Test",
card_color="#000000",
apple_pass_type_id="pass.com.test.loyalty",
is_active=True,
)
db.add(program)
db.commit()
db.refresh(program)
return program
@pytest.fixture
def wt_card(db, wt_program, test_customer):
"""Create a loyalty card for wallet tests."""
card = LoyaltyCard(
merchant_id=wt_program.merchant_id,
program_id=wt_program.id,
customer_id=test_customer.id,
card_number=f"WT-{uuid.uuid4().hex[:8].upper()}",
is_active=True,
)
db.add(card)
db.commit()
db.refresh(card)
return card
@pytest.fixture
def wt_card_apple(db, wt_program_with_apple, test_customer):
"""Create a loyalty card for Apple Wallet tests."""
card = LoyaltyCard(
merchant_id=wt_program_with_apple.merchant_id,
program_id=wt_program_with_apple.id,
customer_id=test_customer.id,
card_number=f"WTA-{uuid.uuid4().hex[:8].upper()}",
is_active=True,
)
db.add(card)
db.commit()
db.refresh(card)
return card
# ============================================================================
# WalletService instantiation
# ============================================================================
@pytest.mark.unit
@pytest.mark.loyalty
class TestWalletService:
"""Test suite for WalletService."""
def setup_method(self):
self.service = WalletService()
def test_service_instantiation(self):
"""Service can be instantiated."""
assert self.service is not None
# ============================================================================
# Google Wallet object creation
# ============================================================================
@pytest.mark.unit
@pytest.mark.loyalty
class TestCreateWalletObjectsGoogle:
"""Tests for Google Wallet object creation during enrollment."""
@patch("app.modules.loyalty.services.google_wallet_service.google_wallet_service")
def test_google_wallet_object_created_when_configured(
self, mock_gw, db, wt_card
):
"""Google Wallet object is created when service is configured."""
mock_gw.is_configured = True
mock_gw.create_object.return_value = "test_object_id"
service = WalletService()
results = service.create_wallet_objects(db, wt_card)
assert results["google_wallet"] is True
mock_gw.create_object.assert_called_once_with(db, wt_card)
@patch("app.modules.loyalty.services.google_wallet_service.google_wallet_service")
def test_google_wallet_skipped_when_not_configured(
self, mock_gw, db, wt_card
):
"""Google Wallet is skipped when not configured."""
mock_gw.is_configured = False
service = WalletService()
results = service.create_wallet_objects(db, wt_card)
assert results["google_wallet"] is False
mock_gw.create_object.assert_not_called()
@patch("app.modules.loyalty.services.google_wallet_service.google_wallet_service")
def test_google_wallet_failure_does_not_crash(
self, mock_gw, db, wt_card
):
"""Enrollment continues even if Google Wallet creation fails."""
mock_gw.is_configured = True
mock_gw.create_object.side_effect = Exception("Google API error")
service = WalletService()
results = service.create_wallet_objects(db, wt_card)
assert results["google_wallet"] is False
# Card should still be valid
db.refresh(wt_card)
assert wt_card.is_active is True
# ============================================================================
# Apple Wallet serial number setup
# ============================================================================
@pytest.mark.unit
@pytest.mark.loyalty
class TestCreateWalletObjectsApple:
"""Tests for Apple Wallet serial number assignment during enrollment."""
def test_apple_serial_number_set_when_configured(self, db, wt_card_apple):
"""Apple serial number is set when program has apple_pass_type_id."""
assert wt_card_apple.apple_serial_number is None
service = WalletService()
with patch(
"app.modules.loyalty.services.google_wallet_service.google_wallet_service"
) as mock_gw:
mock_gw.is_configured = False
results = service.create_wallet_objects(db, wt_card_apple)
assert results["apple_wallet"] is True
db.refresh(wt_card_apple)
assert wt_card_apple.apple_serial_number is not None
assert wt_card_apple.apple_serial_number.startswith(f"card_{wt_card_apple.id}_")
def test_apple_serial_number_not_set_when_unconfigured(self, db, wt_card):
"""Apple serial number not set when program lacks apple_pass_type_id."""
service = WalletService()
with patch(
"app.modules.loyalty.services.google_wallet_service.google_wallet_service"
) as mock_gw:
mock_gw.is_configured = False
results = service.create_wallet_objects(db, wt_card)
assert results["apple_wallet"] is False
db.refresh(wt_card)
assert wt_card.apple_serial_number is None
# ============================================================================
# GoogleWalletService.create_object — DB field population
# ============================================================================
@pytest.mark.unit
@pytest.mark.loyalty
class TestGoogleWalletCreateObject:
"""Tests that GoogleWalletService.create_object populates DB fields correctly."""
@patch("app.modules.loyalty.services.google_wallet_service.settings")
def test_create_object_sets_google_object_id(self, mock_settings, db, wt_card, wt_program):
"""create_object sets card.google_object_id in the database."""
from app.modules.loyalty.services.google_wallet_service import (
GoogleWalletService,
)
mock_settings.loyalty_google_issuer_id = "1234567890"
mock_settings.loyalty_google_service_account_json = "/fake/path.json"
# Pre-set class_id on program so create_class isn't called
wt_program.google_class_id = "1234567890.loyalty_program_1"
db.commit()
service = GoogleWalletService()
service._credentials = MagicMock() # Skip credential loading
mock_http = MagicMock()
mock_response = MagicMock()
mock_response.status_code = 201
mock_http.post.return_value = mock_response
service._http_client = mock_http
result = service.create_object(db, wt_card)
expected_object_id = f"1234567890.loyalty_card_{wt_card.id}"
assert result == expected_object_id
db.refresh(wt_card)
assert wt_card.google_object_id == expected_object_id
@patch("app.modules.loyalty.services.google_wallet_service.settings")
def test_create_object_handles_409_conflict(self, mock_settings, db, wt_card, wt_program):
"""create_object handles 409 (already exists) by still setting the object_id."""
from app.modules.loyalty.services.google_wallet_service import (
GoogleWalletService,
)
mock_settings.loyalty_google_issuer_id = "1234567890"
mock_settings.loyalty_google_service_account_json = "/fake/path.json"
wt_program.google_class_id = "1234567890.loyalty_program_1"
db.commit()
service = GoogleWalletService()
service._credentials = MagicMock()
mock_http = MagicMock()
mock_response = MagicMock()
mock_response.status_code = 409 # Already exists
mock_http.post.return_value = mock_response
service._http_client = mock_http
service.create_object(db, wt_card)
db.refresh(wt_card)
assert wt_card.google_object_id == f"1234567890.loyalty_card_{wt_card.id}"
@patch("app.modules.loyalty.services.google_wallet_service.settings")
def test_create_class_sets_google_class_id(self, mock_settings, db, wt_program):
"""create_class sets program.google_class_id in the database."""
from app.modules.loyalty.services.google_wallet_service import (
GoogleWalletService,
)
mock_settings.loyalty_google_issuer_id = "1234567890"
mock_settings.loyalty_google_service_account_json = "/fake/path.json"
assert wt_program.google_class_id is None
service = GoogleWalletService()
service._credentials = MagicMock()
mock_http = MagicMock()
mock_response = MagicMock()
mock_response.status_code = 201
mock_http.post.return_value = mock_response
service._http_client = mock_http
result = service.create_class(db, wt_program)
expected_class_id = f"1234567890.loyalty_program_{wt_program.id}"
assert result == expected_class_id
db.refresh(wt_program)
assert wt_program.google_class_id == expected_class_id
# ============================================================================
# End-to-end: enrollment → wallet creation
# ============================================================================
@pytest.mark.unit
@pytest.mark.loyalty
class TestEnrollmentWalletCreation:
"""Tests that enrollment triggers wallet object creation and populates DB fields."""
@patch("app.modules.loyalty.services.google_wallet_service.settings")
def test_enrollment_creates_google_wallet_object(
self, mock_settings, db, wt_program, test_merchant, test_customer
):
"""Full enrollment flow creates Google Wallet class + object in DB."""
from app.modules.loyalty.services.google_wallet_service import (
google_wallet_service,
)
mock_settings.loyalty_google_issuer_id = "1234567890"
mock_settings.loyalty_google_service_account_json = "/fake/path.json"
# Mock the HTTP client to simulate Google API success
mock_http = MagicMock()
mock_response = MagicMock()
mock_response.status_code = 201
mock_http.post.return_value = mock_response
google_wallet_service._http_client = mock_http
google_wallet_service._credentials = MagicMock()
# Enroll via card_service
from app.modules.loyalty.services.card_service import card_service
card = card_service.enroll_customer(db, test_customer.id, test_merchant.id)
# Verify wallet DB fields are populated
db.refresh(card)
assert card.google_object_id is not None
assert card.google_object_id == f"1234567890.loyalty_card_{card.id}"
# Verify program class_id is set
db.refresh(wt_program)
assert wt_program.google_class_id is not None
assert wt_program.google_class_id == f"1234567890.loyalty_program_{wt_program.id}"
# Clean up singleton state
google_wallet_service._http_client = None
google_wallet_service._credentials = None
def test_enrollment_succeeds_without_wallet_config(
self, db, wt_program, test_merchant, test_customer
):
"""Enrollment works even when Google Wallet is not configured."""
from app.modules.loyalty.services.card_service import card_service
card = card_service.enroll_customer(db, test_customer.id, test_merchant.id)
db.refresh(card)
assert card.id is not None
assert card.is_active is True
# Wallet fields should be None since not configured
assert card.google_object_id is None
@patch("app.modules.loyalty.services.google_wallet_service.settings")
def test_enrollment_with_apple_wallet_sets_serial(
self, mock_settings, db, wt_program_with_apple, test_customer
):
"""Enrollment sets apple_serial_number when program has apple_pass_type_id."""
mock_settings.loyalty_google_issuer_id = None
mock_settings.loyalty_google_service_account_json = None
from app.modules.loyalty.services.card_service import card_service
card = card_service.enroll_customer(
db, test_customer.id, wt_program_with_apple.merchant_id
)
db.refresh(card)
assert card.apple_serial_number is not None
assert card.apple_serial_number.startswith(f"card_{card.id}_")