test: add unit tests for services and utilities

New test files:
- test_onboarding_service.py: 30 tests for vendor onboarding flow
- test_team_service.py: 11 tests for team management
- test_capacity_forecast_service.py: 14 tests for capacity forecasting
- test_i18n.py: 50+ tests for internationalization
- test_money.py: 37 tests for money handling utilities

Coverage improved from 67.09% to 69.06%

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-28 16:48:45 +01:00
parent 559be59412
commit 622321600d
5 changed files with 1795 additions and 0 deletions

View File

@@ -0,0 +1,349 @@
# tests/unit/utils/test_i18n.py
"""
Unit tests for i18n utilities.
Tests cover:
- Language configuration
- Translation loading
- Translation lookup
- Accept-Language parsing
- Language resolution
- Jinja2 integration
"""
import pytest
from app.utils.i18n import (
DEFAULT_LANGUAGE,
LANGUAGE_FLAGS,
LANGUAGE_NAMES,
LANGUAGE_NAMES_EN,
SUPPORTED_LANGUAGES,
TranslationContext,
clear_translation_cache,
create_translation_context,
get_jinja2_globals,
get_language_choices,
get_language_info,
get_locales_path,
get_nested_value,
is_rtl_language,
load_translations,
parse_accept_language,
resolve_storefront_language,
resolve_vendor_dashboard_language,
t,
translate,
)
@pytest.mark.unit
@pytest.mark.utils
class TestLanguageConfiguration:
"""Test language configuration constants"""
def test_supported_languages(self):
"""Test SUPPORTED_LANGUAGES contains expected languages"""
assert "en" in SUPPORTED_LANGUAGES
assert "fr" in SUPPORTED_LANGUAGES
assert "de" in SUPPORTED_LANGUAGES
assert "lb" in SUPPORTED_LANGUAGES
def test_default_language_is_french(self):
"""Test default language is French (Luxembourg context)"""
assert DEFAULT_LANGUAGE == "fr"
def test_language_names_defined(self):
"""Test all languages have names defined"""
for lang in SUPPORTED_LANGUAGES:
assert lang in LANGUAGE_NAMES
assert lang in LANGUAGE_NAMES_EN
assert lang in LANGUAGE_FLAGS
def test_get_locales_path_exists(self):
"""Test locales path is accessible"""
path = get_locales_path()
assert path is not None
@pytest.mark.unit
@pytest.mark.utils
class TestTranslationLoading:
"""Test translation loading functionality"""
def test_load_translations_english(self):
"""Test loading English translations"""
clear_translation_cache()
translations = load_translations("en")
assert isinstance(translations, dict)
def test_load_translations_french(self):
"""Test loading French translations"""
clear_translation_cache()
translations = load_translations("fr")
assert isinstance(translations, dict)
def test_load_translations_unsupported(self):
"""Test loading unsupported language falls back to default"""
clear_translation_cache()
translations = load_translations("xx") # Non-existent
# Should fall back to French (default)
assert isinstance(translations, dict)
def test_clear_translation_cache(self):
"""Test clearing translation cache"""
load_translations("en")
clear_translation_cache()
# Should not raise
load_translations("en")
@pytest.mark.unit
@pytest.mark.utils
class TestNestedValue:
"""Test get_nested_value function"""
def test_get_nested_value_simple(self):
"""Test getting simple key"""
data = {"key": "value"}
result = get_nested_value(data, "key")
assert result == "value"
def test_get_nested_value_nested(self):
"""Test getting nested key"""
data = {"level1": {"level2": {"level3": "value"}}}
result = get_nested_value(data, "level1.level2.level3")
assert result == "value"
def test_get_nested_value_missing(self):
"""Test getting missing key returns key path"""
data = {"key": "value"}
result = get_nested_value(data, "missing.key")
assert result == "missing.key"
def test_get_nested_value_with_default(self):
"""Test getting missing key with default"""
data = {"key": "value"}
result = get_nested_value(data, "missing.key", "default")
assert result == "default"
def test_get_nested_value_non_string(self):
"""Test getting non-string value returns default"""
data = {"key": {"nested": "obj"}}
result = get_nested_value(data, "key", "default")
assert result == "default"
@pytest.mark.unit
@pytest.mark.utils
class TestTranslate:
"""Test translate function"""
def test_translate_with_language(self):
"""Test translate with specified language"""
result = translate("common.save", language="en")
# Should return something (either translation or key)
assert result is not None
def test_translate_default_language(self):
"""Test translate uses default language when not specified"""
result = translate("common.save")
assert result is not None
def test_translate_missing_key(self):
"""Test translate returns key when not found"""
result = translate("nonexistent.key.path")
assert result == "nonexistent.key.path"
def test_translate_with_interpolation(self):
"""Test translate with variable interpolation"""
# Create a translation that uses variables
result = translate("test.key", language="en", name="John")
# Should return something (translation with vars or key)
assert result is not None
def test_t_alias(self):
"""Test t() is alias for translate()"""
result1 = translate("common.save", language="en")
result2 = t("common.save", language="en")
assert result1 == result2
@pytest.mark.unit
@pytest.mark.utils
class TestTranslationContext:
"""Test TranslationContext class"""
def test_translation_context_init(self):
"""Test TranslationContext initialization"""
ctx = TranslationContext("en")
assert ctx.language == "en"
def test_translation_context_default_language(self):
"""Test TranslationContext uses default when not specified"""
ctx = TranslationContext()
assert ctx.language == DEFAULT_LANGUAGE
def test_translation_context_callable(self):
"""Test TranslationContext is callable"""
ctx = TranslationContext("en")
result = ctx("common.save")
assert result is not None
def test_translation_context_set_language(self):
"""Test set_language changes language"""
ctx = TranslationContext("en")
ctx.set_language("fr")
assert ctx.language == "fr"
def test_translation_context_set_unsupported(self):
"""Test set_language rejects unsupported language"""
ctx = TranslationContext("en")
ctx.set_language("xx")
assert ctx.language == "en" # Should not change
@pytest.mark.unit
@pytest.mark.utils
class TestJinja2Integration:
"""Test Jinja2 integration functions"""
def test_create_translation_context(self):
"""Test create_translation_context factory"""
ctx = create_translation_context("de")
assert isinstance(ctx, TranslationContext)
assert ctx.language == "de"
def test_get_jinja2_globals(self):
"""Test get_jinja2_globals returns required globals"""
globals = get_jinja2_globals("en")
assert "_" in globals
assert "t" in globals
assert "SUPPORTED_LANGUAGES" in globals
assert "DEFAULT_LANGUAGE" in globals
assert "LANGUAGE_NAMES" in globals
assert "LANGUAGE_FLAGS" in globals
assert "current_language" in globals
assert globals["current_language"] == "en"
def test_get_jinja2_globals_default_language(self):
"""Test get_jinja2_globals uses default when not specified"""
globals = get_jinja2_globals()
assert globals["current_language"] == DEFAULT_LANGUAGE
@pytest.mark.unit
@pytest.mark.utils
class TestLanguageResolution:
"""Test language resolution functions"""
def test_resolve_vendor_dashboard_user_preferred(self):
"""Test vendor dashboard prefers user's language"""
result = resolve_vendor_dashboard_language("en", "fr")
assert result == "en"
def test_resolve_vendor_dashboard_vendor_fallback(self):
"""Test vendor dashboard falls back to vendor setting"""
result = resolve_vendor_dashboard_language(None, "de")
assert result == "de"
def test_resolve_vendor_dashboard_default(self):
"""Test vendor dashboard uses default when nothing set"""
result = resolve_vendor_dashboard_language(None, None)
assert result == DEFAULT_LANGUAGE
def test_resolve_storefront_customer_preferred(self):
"""Test storefront prefers customer's language"""
result = resolve_storefront_language("de", "fr", "en", None)
assert result == "de"
def test_resolve_storefront_session(self):
"""Test storefront uses session language"""
result = resolve_storefront_language(None, "de", "fr", None)
assert result == "de"
def test_resolve_storefront_vendor(self):
"""Test storefront uses vendor default"""
result = resolve_storefront_language(None, None, "en", None)
assert result == "en"
def test_resolve_storefront_browser(self):
"""Test storefront uses browser language"""
result = resolve_storefront_language(None, None, None, "de")
assert result == "de"
def test_resolve_storefront_enabled_filter(self):
"""Test storefront respects enabled languages"""
result = resolve_storefront_language("de", None, None, None, ["en", "fr"])
# de is not in enabled list, should fallback
assert result in ["en", "fr", DEFAULT_LANGUAGE]
@pytest.mark.unit
@pytest.mark.utils
class TestAcceptLanguageParsing:
"""Test Accept-Language header parsing"""
def test_parse_accept_language_simple(self):
"""Test parsing simple Accept-Language"""
result = parse_accept_language("fr")
assert result == "fr"
def test_parse_accept_language_with_region(self):
"""Test parsing Accept-Language with region"""
result = parse_accept_language("fr-FR")
assert result == "fr"
def test_parse_accept_language_multiple(self):
"""Test parsing multiple languages with quality"""
result = parse_accept_language("de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7")
assert result == "de" # Highest quality supported
def test_parse_accept_language_none(self):
"""Test parsing None returns None"""
result = parse_accept_language(None)
assert result is None
def test_parse_accept_language_unsupported(self):
"""Test parsing unsupported language returns None"""
result = parse_accept_language("zh-CN")
assert result is None
@pytest.mark.unit
@pytest.mark.utils
class TestUtilityFunctions:
"""Test utility functions"""
def test_get_language_choices(self):
"""Test get_language_choices returns tuples"""
choices = get_language_choices()
assert len(choices) == len(SUPPORTED_LANGUAGES)
for code, name in choices:
assert code in SUPPORTED_LANGUAGES
assert name == LANGUAGE_NAMES[code]
def test_get_language_info_supported(self):
"""Test get_language_info for supported language"""
info = get_language_info("en")
assert info["code"] == "en"
assert info["name"] == "English"
assert info["name_en"] == "English"
assert "flag" in info
def test_get_language_info_unsupported(self):
"""Test get_language_info for unsupported language"""
info = get_language_info("xx")
# Should fallback to default
assert info["code"] == DEFAULT_LANGUAGE
def test_is_rtl_language_false(self):
"""Test is_rtl_language returns False for LTR languages"""
for lang in SUPPORTED_LANGUAGES:
assert is_rtl_language(lang) is False
def test_is_rtl_language_arabic(self):
"""Test is_rtl_language returns True for RTL languages"""
assert is_rtl_language("ar") is True
assert is_rtl_language("he") is True

View File

@@ -0,0 +1,307 @@
# tests/unit/utils/test_money.py
"""
Unit tests for money handling utilities.
Tests cover:
- Euro to cents conversion
- Cents to euros conversion
- Price string parsing
- Money class formatting
- Arithmetic operations
"""
from decimal import Decimal
import pytest
from app.utils.money import (
CURRENCY_DECIMALS,
DEFAULT_CURRENCY,
Money,
cents_to_euros,
euros_to_cents,
parse_price_to_cents,
)
@pytest.mark.unit
@pytest.mark.utils
class TestEurosToCents:
"""Test euros_to_cents conversion"""
def test_float_conversion(self):
"""Test converting float to cents"""
assert euros_to_cents(105.91) == 10591
assert euros_to_cents(19.99) == 1999
assert euros_to_cents(0.01) == 1
assert euros_to_cents(0.0) == 0
def test_string_conversion(self):
"""Test converting string to cents"""
assert euros_to_cents("105.91") == 10591
assert euros_to_cents("19.99") == 1999
assert euros_to_cents("0.01") == 1
def test_string_with_currency(self):
"""Test converting string with currency symbols"""
assert euros_to_cents("€105.91") == 10591
assert euros_to_cents("$ 19.99") == 1999
assert euros_to_cents("£0.01") == 1
def test_string_with_comma_decimal(self):
"""Test converting European-style decimal comma"""
assert euros_to_cents("105,91") == 10591
assert euros_to_cents("19,99") == 1999
def test_decimal_conversion(self):
"""Test converting Decimal to cents"""
assert euros_to_cents(Decimal("105.91")) == 10591
assert euros_to_cents(Decimal("19.99")) == 1999
def test_integer_conversion(self):
"""Test converting integer euros to cents"""
assert euros_to_cents(100) == 10000
assert euros_to_cents(1) == 100
def test_none_returns_zero(self):
"""Test None returns 0"""
assert euros_to_cents(None) == 0
def test_rounding(self):
"""Test rounding behavior"""
# ROUND_HALF_UP: 0.5 rounds up
assert euros_to_cents("0.995") == 100 # Rounds up
assert euros_to_cents("0.994") == 99 # Rounds down
@pytest.mark.unit
@pytest.mark.utils
class TestCentsToEuros:
"""Test cents_to_euros conversion"""
def test_basic_conversion(self):
"""Test basic cents to euros conversion"""
assert cents_to_euros(10591) == 105.91
assert cents_to_euros(1999) == 19.99
assert cents_to_euros(1) == 0.01
assert cents_to_euros(0) == 0.0
def test_none_returns_zero(self):
"""Test None returns 0.0"""
assert cents_to_euros(None) == 0.0
def test_large_amounts(self):
"""Test large amounts"""
assert cents_to_euros(10000000) == 100000.0 # 100k euros
assert cents_to_euros(999999999) == 9999999.99
@pytest.mark.unit
@pytest.mark.utils
class TestParsePriceToCents:
"""Test parse_price_to_cents function"""
def test_parse_simple_euro(self):
"""Test parsing simple EUR prices"""
cents, currency = parse_price_to_cents("19.99 EUR")
assert cents == 1999
assert currency == "EUR"
def test_parse_euro_symbol(self):
"""Test parsing with € symbol"""
cents, currency = parse_price_to_cents("19,99 €")
assert cents == 1999
assert currency == "EUR"
def test_parse_usd(self):
"""Test parsing USD prices"""
cents, currency = parse_price_to_cents("19.99 USD")
assert cents == 1999
assert currency == "USD"
def test_parse_dollar_symbol(self):
"""Test parsing with $ symbol"""
cents, currency = parse_price_to_cents("$19.99")
assert cents == 1999
assert currency == "USD"
def test_parse_gbp(self):
"""Test parsing GBP prices"""
cents, currency = parse_price_to_cents("£19.99")
assert cents == 1999
assert currency == "GBP"
def test_parse_chf(self):
"""Test parsing CHF prices"""
cents, currency = parse_price_to_cents("19.99 CHF")
assert cents == 1999
assert currency == "CHF"
def test_parse_numeric_float(self):
"""Test parsing numeric float"""
cents, currency = parse_price_to_cents(19.99)
assert cents == 1999
assert currency == DEFAULT_CURRENCY
def test_parse_numeric_int(self):
"""Test parsing numeric int"""
cents, currency = parse_price_to_cents(20)
assert cents == 2000
assert currency == DEFAULT_CURRENCY
def test_parse_none(self):
"""Test parsing None"""
cents, currency = parse_price_to_cents(None)
assert cents == 0
assert currency == DEFAULT_CURRENCY
def test_parse_thousand_separator(self):
"""Test parsing with thousand separators"""
cents, currency = parse_price_to_cents("1.000,00 EUR")
assert cents == 100000
assert currency == "EUR"
@pytest.mark.unit
@pytest.mark.utils
class TestMoneyClass:
"""Test Money class methods"""
def test_from_euros(self):
"""Test Money.from_euros"""
assert Money.from_euros(105.91) == 10591
assert Money.from_euros("19.99") == 1999
assert Money.from_euros(None) == 0
def test_from_cents(self):
"""Test Money.from_cents"""
assert Money.from_cents(10591) == 10591
assert Money.from_cents(None) == 0
def test_to_euros(self):
"""Test Money.to_euros"""
assert Money.to_euros(10591) == 105.91
assert Money.to_euros(None) == 0.0
@pytest.mark.unit
@pytest.mark.utils
class TestMoneyFormat:
"""Test Money.format method"""
def test_format_basic(self):
"""Test basic formatting"""
assert Money.format(10591) == "105.91"
assert Money.format(1999) == "19.99"
assert Money.format(1) == "0.01"
assert Money.format(0) == "0.00"
def test_format_with_currency(self):
"""Test formatting with currency"""
assert Money.format(10591, "EUR") == "105.91 EUR"
assert Money.format(1999, "USD") == "19.99 USD"
def test_format_german_locale(self):
"""Test German locale formatting"""
assert Money.format(10591, "", "de") == "105,91"
assert Money.format(1000000, "", "de") == "10.000,00"
def test_format_french_locale(self):
"""Test French locale formatting"""
assert Money.format(10591, "", "fr") == "105,91"
assert Money.format(1000000, "", "fr") == "10.000,00"
def test_format_english_locale(self):
"""Test English locale formatting"""
assert Money.format(10591, "", "en") == "105.91"
assert Money.format(1000000, "", "en") == "10,000.00"
def test_format_none(self):
"""Test formatting None"""
assert Money.format(None) == "0.00"
def test_format_large_amount(self):
"""Test formatting large amounts"""
assert Money.format(10000000) == "100,000.00"
@pytest.mark.unit
@pytest.mark.utils
class TestMoneyParse:
"""Test Money.parse method"""
def test_parse_string(self):
"""Test parsing string"""
assert Money.parse("19.99 EUR") == 1999
assert Money.parse("€105,91") == 10591
def test_parse_float(self):
"""Test parsing float"""
assert Money.parse(19.99) == 1999
def test_parse_none(self):
"""Test parsing None"""
assert Money.parse(None) == 0
@pytest.mark.unit
@pytest.mark.utils
class TestMoneyArithmetic:
"""Test Money arithmetic operations"""
def test_add(self):
"""Test Money.add"""
assert Money.add(1000, 500, 250) == 1750
assert Money.add(1000, None, 500) == 1500
assert Money.add() == 0
def test_subtract(self):
"""Test Money.subtract"""
assert Money.subtract(1000, 500) == 500
assert Money.subtract(1000, 500, 250) == 250
assert Money.subtract(1000, None) == 1000
def test_multiply(self):
"""Test Money.multiply"""
assert Money.multiply(1999, 3) == 5997
assert Money.multiply(1000, 0) == 0
def test_calculate_line_total(self):
"""Test Money.calculate_line_total"""
assert Money.calculate_line_total(1999, 5) == 9995
assert Money.calculate_line_total(500, 10) == 5000
def test_calculate_order_total(self):
"""Test Money.calculate_order_total"""
# Subtotal 100€, Tax 17€, Shipping 5€, Discount 10€
total = Money.calculate_order_total(10000, 1700, 500, 1000)
assert total == 11200 # 100 + 17 + 5 - 10 = 112€
def test_calculate_order_total_no_extras(self):
"""Test order total without extras"""
total = Money.calculate_order_total(10000)
assert total == 10000
@pytest.mark.unit
@pytest.mark.utils
class TestCurrencyConfiguration:
"""Test currency configuration"""
def test_default_currency(self):
"""Test default currency is EUR"""
assert DEFAULT_CURRENCY == "EUR"
def test_currency_decimals(self):
"""Test currency decimal places"""
assert CURRENCY_DECIMALS["EUR"] == 2
assert CURRENCY_DECIMALS["USD"] == 2
assert CURRENCY_DECIMALS["GBP"] == 2
assert CURRENCY_DECIMALS["JPY"] == 0 # No decimals
def test_supported_currencies(self):
"""Test supported currencies are defined"""
assert "EUR" in CURRENCY_DECIMALS
assert "USD" in CURRENCY_DECIMALS
assert "GBP" in CURRENCY_DECIMALS
assert "CHF" in CURRENCY_DECIMALS