mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-07-12 15:52:54 +02:00
feat: replace action row with a FAB
This commit is contained in:
+171
-65
@@ -9,22 +9,28 @@ from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.currencies.models import Currency, ExchangeRate, ExchangeRateService
|
||||
from apps.accounts.models import Account # For ExchangeRateService target_accounts
|
||||
from apps.accounts.models import Account # For ExchangeRateService target_accounts
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class BaseCurrencyAppTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(email="curtestuser@example.com", password="password")
|
||||
self.user = User.objects.create_user(
|
||||
email="curtestuser@example.com", password="password"
|
||||
)
|
||||
self.client = Client()
|
||||
self.client.login(email="curtestuser@example.com", password="password")
|
||||
|
||||
self.usd = Currency.objects.create(code="USD", name="US Dollar", decimal_places=2, prefix="$")
|
||||
self.eur = Currency.objects.create(code="EUR", name="Euro", decimal_places=2, prefix="€")
|
||||
self.usd = Currency.objects.create(
|
||||
code="USD", name="US Dollar", decimal_places=2, prefix="$"
|
||||
)
|
||||
self.eur = Currency.objects.create(
|
||||
code="EUR", name="Euro", decimal_places=2, prefix="€"
|
||||
)
|
||||
|
||||
|
||||
class CurrencyModelTests(BaseCurrencyAppTest): # Changed from CurrencyTests
|
||||
class CurrencyModelTests(BaseCurrencyAppTest): # Changed from CurrencyTests
|
||||
def test_currency_creation(self):
|
||||
"""Test basic currency creation"""
|
||||
# self.usd is already created in BaseCurrencyAppTest
|
||||
@@ -33,10 +39,11 @@ class CurrencyModelTests(BaseCurrencyAppTest): # Changed from CurrencyTests
|
||||
self.assertEqual(self.usd.decimal_places, 2)
|
||||
self.assertEqual(self.usd.prefix, "$")
|
||||
# Test creation with suffix
|
||||
jpy = Currency.objects.create(code="JPY", name="Japanese Yen", decimal_places=0, suffix="円")
|
||||
jpy = Currency.objects.create(
|
||||
code="JPY", name="Japanese Yen", decimal_places=0, suffix="円"
|
||||
)
|
||||
self.assertEqual(jpy.suffix, "円")
|
||||
|
||||
|
||||
def test_currency_decimal_places_validation(self):
|
||||
"""Test decimal places validation for maximum value"""
|
||||
currency = Currency(code="TESTMAX", name="Test Currency Max", decimal_places=31)
|
||||
@@ -58,11 +65,14 @@ class CurrencyModelTests(BaseCurrencyAppTest): # Changed from CurrencyTests
|
||||
self.usd.exchange_currency = self.usd
|
||||
with self.assertRaises(ValidationError) as context:
|
||||
self.usd.full_clean()
|
||||
self.assertIn('exchange_currency', context.exception.message_dict)
|
||||
self.assertIn("Currency cannot have itself as exchange currency.", context.exception.message_dict['exchange_currency'])
|
||||
self.assertIn("exchange_currency", context.exception.message_dict)
|
||||
self.assertIn(
|
||||
"Currency cannot have itself as exchange currency.",
|
||||
context.exception.message_dict["exchange_currency"],
|
||||
)
|
||||
|
||||
|
||||
class ExchangeRateModelTests(BaseCurrencyAppTest): # Changed from ExchangeRateTests
|
||||
class ExchangeRateModelTests(BaseCurrencyAppTest): # Changed from ExchangeRateTests
|
||||
def test_exchange_rate_creation(self):
|
||||
"""Test basic exchange rate creation"""
|
||||
rate = ExchangeRate.objects.create(
|
||||
@@ -83,11 +93,11 @@ class ExchangeRateModelTests(BaseCurrencyAppTest): # Changed from ExchangeRateTe
|
||||
rate=Decimal("0.85"),
|
||||
date=date,
|
||||
)
|
||||
with self.assertRaises(IntegrityError): # Specifically expect IntegrityError
|
||||
with self.assertRaises(IntegrityError): # Specifically expect IntegrityError
|
||||
ExchangeRate.objects.create(
|
||||
from_currency=self.usd,
|
||||
to_currency=self.eur,
|
||||
rate=Decimal("0.86"), # Different rate, same pair and date
|
||||
rate=Decimal("0.86"), # Different rate, same pair and date
|
||||
date=date,
|
||||
)
|
||||
|
||||
@@ -95,14 +105,17 @@ class ExchangeRateModelTests(BaseCurrencyAppTest): # Changed from ExchangeRateTe
|
||||
"""Test that from_currency and to_currency cannot be the same."""
|
||||
rate = ExchangeRate(
|
||||
from_currency=self.usd,
|
||||
to_currency=self.usd, # Same currency
|
||||
to_currency=self.usd, # Same currency
|
||||
rate=Decimal("1.00"),
|
||||
date=timezone.now()
|
||||
date=timezone.now(),
|
||||
)
|
||||
with self.assertRaises(ValidationError) as context:
|
||||
rate.full_clean()
|
||||
self.assertIn('to_currency', context.exception.message_dict)
|
||||
self.assertIn("From and To currencies cannot be the same.", context.exception.message_dict['to_currency'])
|
||||
self.assertIn("to_currency", context.exception.message_dict)
|
||||
self.assertIn(
|
||||
"From and To currencies cannot be the same.",
|
||||
context.exception.message_dict["to_currency"],
|
||||
)
|
||||
|
||||
|
||||
class ExchangeRateServiceModelTests(BaseCurrencyAppTest):
|
||||
@@ -111,7 +124,7 @@ class ExchangeRateServiceModelTests(BaseCurrencyAppTest):
|
||||
name="Test Coingecko Free",
|
||||
service_type=ExchangeRateService.ServiceType.COINGECKO_FREE,
|
||||
interval_type=ExchangeRateService.IntervalType.EVERY,
|
||||
fetch_interval="12" # Every 12 hours
|
||||
fetch_interval="12", # Every 12 hours
|
||||
)
|
||||
self.assertEqual(str(service), "Test Coingecko Free")
|
||||
self.assertTrue(service.is_active)
|
||||
@@ -119,17 +132,22 @@ class ExchangeRateServiceModelTests(BaseCurrencyAppTest):
|
||||
def test_fetch_interval_validation_every_x_hours(self):
|
||||
# Valid
|
||||
service = ExchangeRateService(
|
||||
name="Valid Every", service_type=ExchangeRateService.ServiceType.SYNTH_FINANCE,
|
||||
interval_type=ExchangeRateService.IntervalType.EVERY, fetch_interval="6"
|
||||
name="Valid Every",
|
||||
service_type=ExchangeRateService.ServiceType.SYNTH_FINANCE,
|
||||
interval_type=ExchangeRateService.IntervalType.EVERY,
|
||||
fetch_interval="6",
|
||||
)
|
||||
service.full_clean() # Should not raise
|
||||
service.full_clean() # Should not raise
|
||||
|
||||
# Invalid - not a digit
|
||||
service.fetch_interval = "abc"
|
||||
with self.assertRaises(ValidationError) as context:
|
||||
service.full_clean()
|
||||
self.assertIn("fetch_interval", context.exception.message_dict)
|
||||
self.assertIn("'Every X hours' interval type requires a positive integer.", context.exception.message_dict['fetch_interval'][0])
|
||||
self.assertIn(
|
||||
"'Every X hours' interval type requires a positive integer.",
|
||||
context.exception.message_dict["fetch_interval"][0],
|
||||
)
|
||||
|
||||
# Invalid - out of range
|
||||
service.fetch_interval = "0"
|
||||
@@ -144,49 +162,66 @@ class ExchangeRateServiceModelTests(BaseCurrencyAppTest):
|
||||
valid_intervals = ["1", "0,12", "1-5", "1-5,8,10-12", "0,1,2,3,22,23"]
|
||||
for interval in valid_intervals:
|
||||
service = ExchangeRateService(
|
||||
name=f"Test On {interval}", service_type=ExchangeRateService.ServiceType.SYNTH_FINANCE,
|
||||
interval_type=ExchangeRateService.IntervalType.ON, fetch_interval=interval
|
||||
name=f"Test On {interval}",
|
||||
service_type=ExchangeRateService.ServiceType.SYNTH_FINANCE,
|
||||
interval_type=ExchangeRateService.IntervalType.ON,
|
||||
fetch_interval=interval,
|
||||
)
|
||||
service.full_clean() # Should not raise
|
||||
service.full_clean() # Should not raise
|
||||
# Check normalized form (optional, but good if model does it)
|
||||
# self.assertEqual(service.fetch_interval, ",".join(str(h) for h in sorted(service._parse_hour_ranges(interval))))
|
||||
|
||||
|
||||
invalid_intervals = [
|
||||
"abc", "1-", "-5", "24", "-1", "1-24", "1,2,25", "5-1", # Invalid hour, range, or format
|
||||
"1.5", "1, 2, 3," # decimal, trailing comma
|
||||
"abc",
|
||||
"1-",
|
||||
"-5",
|
||||
"24",
|
||||
"-1",
|
||||
"1-24",
|
||||
"1,2,25",
|
||||
"5-1", # Invalid hour, range, or format
|
||||
"1.5",
|
||||
"1, 2, 3,", # decimal, trailing comma
|
||||
]
|
||||
for interval in invalid_intervals:
|
||||
service = ExchangeRateService(
|
||||
name=f"Test On Invalid {interval}", service_type=ExchangeRateService.ServiceType.SYNTH_FINANCE,
|
||||
interval_type=ExchangeRateService.IntervalType.NOT_ON, fetch_interval=interval
|
||||
name=f"Test On Invalid {interval}",
|
||||
service_type=ExchangeRateService.ServiceType.SYNTH_FINANCE,
|
||||
interval_type=ExchangeRateService.IntervalType.NOT_ON,
|
||||
fetch_interval=interval,
|
||||
)
|
||||
with self.assertRaises(ValidationError) as context:
|
||||
service.full_clean()
|
||||
self.assertIn("fetch_interval", context.exception.message_dict)
|
||||
self.assertTrue("Invalid hour format" in context.exception.message_dict['fetch_interval'][0] or \
|
||||
"Hours must be between 0 and 23" in context.exception.message_dict['fetch_interval'][0] or \
|
||||
"Invalid range" in context.exception.message_dict['fetch_interval'][0]
|
||||
self.assertTrue(
|
||||
"Invalid hour format"
|
||||
in context.exception.message_dict["fetch_interval"][0]
|
||||
or "Hours must be between 0 and 23"
|
||||
in context.exception.message_dict["fetch_interval"][0]
|
||||
or "Invalid range"
|
||||
in context.exception.message_dict["fetch_interval"][0]
|
||||
)
|
||||
|
||||
|
||||
@patch('apps.currencies.exchange_rates.fetcher.PROVIDER_MAPPING')
|
||||
@patch("apps.currencies.exchange_rates.fetcher.PROVIDER_MAPPING")
|
||||
def test_get_provider(self, mock_provider_mapping):
|
||||
# Mock a provider class
|
||||
class MockProvider:
|
||||
def __init__(self, api_key=None):
|
||||
self.api_key = api_key
|
||||
|
||||
mock_provider_mapping.__getitem__.return_value = MockProvider
|
||||
|
||||
service = ExchangeRateService(
|
||||
name="Test Get Provider",
|
||||
service_type=ExchangeRateService.ServiceType.COINGECKO_FREE, # Any valid choice
|
||||
api_key="testkey"
|
||||
service_type=ExchangeRateService.ServiceType.COINGECKO_FREE, # Any valid choice
|
||||
api_key="testkey",
|
||||
)
|
||||
provider_instance = service.get_provider()
|
||||
self.assertIsInstance(provider_instance, MockProvider)
|
||||
self.assertEqual(provider_instance.api_key, "testkey")
|
||||
mock_provider_mapping.__getitem__.assert_called_with(ExchangeRateService.ServiceType.COINGECKO_FREE)
|
||||
mock_provider_mapping.__getitem__.assert_called_with(
|
||||
ExchangeRateService.ServiceType.COINGECKO_FREE
|
||||
)
|
||||
|
||||
|
||||
class CurrencyViewTests(BaseCurrencyAppTest):
|
||||
@@ -197,21 +232,35 @@ class CurrencyViewTests(BaseCurrencyAppTest):
|
||||
self.assertContains(response, self.eur.name)
|
||||
|
||||
def test_currency_add_view(self):
|
||||
data = {"code": "GBP", "name": "British Pound", "decimal_places": 2, "prefix": "£"}
|
||||
data = {
|
||||
"code": "GBP",
|
||||
"name": "British Pound",
|
||||
"decimal_places": 2,
|
||||
"prefix": "£",
|
||||
}
|
||||
response = self.client.post(reverse("currency_add"), data)
|
||||
self.assertEqual(response.status_code, 204) # HTMX success
|
||||
self.assertEqual(response.status_code, 204) # HTMX success
|
||||
self.assertTrue(Currency.objects.filter(code="GBP").exists())
|
||||
|
||||
def test_currency_edit_view(self):
|
||||
gbp = Currency.objects.create(code="GBP", name="Pound Sterling", decimal_places=2)
|
||||
data = {"code": "GBP", "name": "British Pound Sterling", "decimal_places": 2, "prefix": "£"}
|
||||
gbp = Currency.objects.create(
|
||||
code="GBP", name="Pound Sterling", decimal_places=2
|
||||
)
|
||||
data = {
|
||||
"code": "GBP",
|
||||
"name": "British Pound Sterling",
|
||||
"decimal_places": 2,
|
||||
"prefix": "£",
|
||||
}
|
||||
response = self.client.post(reverse("currency_edit", args=[gbp.id]), data)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
gbp.refresh_from_db()
|
||||
self.assertEqual(gbp.name, "British Pound Sterling")
|
||||
|
||||
def test_currency_delete_view(self):
|
||||
cad = Currency.objects.create(code="CAD", name="Canadian Dollar", decimal_places=2)
|
||||
cad = Currency.objects.create(
|
||||
code="CAD", name="Canadian Dollar", decimal_places=2
|
||||
)
|
||||
response = self.client.delete(reverse("currency_delete", args=[cad.id]))
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.assertFalse(Currency.objects.filter(code="CAD").exists())
|
||||
@@ -220,38 +269,72 @@ class CurrencyViewTests(BaseCurrencyAppTest):
|
||||
class ExchangeRateViewTests(BaseCurrencyAppTest):
|
||||
def test_exchange_rate_list_view_main(self):
|
||||
# This view lists pairs, not individual rates directly in the main list
|
||||
ExchangeRate.objects.create(from_currency=self.usd, to_currency=self.eur, rate=Decimal("0.9"), date=timezone.now())
|
||||
ExchangeRate.objects.create(
|
||||
from_currency=self.usd,
|
||||
to_currency=self.eur,
|
||||
rate=Decimal("0.9"),
|
||||
date=timezone.now(),
|
||||
)
|
||||
response = self.client.get(reverse("exchange_rates_list"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, self.usd.name) # Check if pair components are mentioned
|
||||
self.assertContains(
|
||||
response, self.usd.name
|
||||
) # Check if pair components are mentioned
|
||||
self.assertContains(response, self.eur.name)
|
||||
|
||||
def test_exchange_rate_list_pair_view(self):
|
||||
rate_date = timezone.now()
|
||||
ExchangeRate.objects.create(from_currency=self.usd, to_currency=self.eur, rate=Decimal("0.9"), date=rate_date)
|
||||
url = reverse("exchange_rates_list_pair") + f"?from={self.usd.name}&to={self.eur.name}"
|
||||
ExchangeRate.objects.create(
|
||||
from_currency=self.usd,
|
||||
to_currency=self.eur,
|
||||
rate=Decimal("0.9"),
|
||||
date=rate_date,
|
||||
)
|
||||
url = (
|
||||
reverse("exchange_rates_list_pair")
|
||||
+ f"?from={self.usd.name}&to={self.eur.name}"
|
||||
)
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "0.9") # Check if the rate is displayed
|
||||
self.assertContains(response, "0.9") # Check if the rate is displayed
|
||||
|
||||
def test_exchange_rate_add_view(self):
|
||||
data = {
|
||||
"from_currency": self.usd.id,
|
||||
"to_currency": self.eur.id,
|
||||
"rate": "0.88",
|
||||
"date": timezone.now().strftime('%Y-%m-%d %H:%M:%S') # Match form field format
|
||||
"date": timezone.now().strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
), # Match form field format
|
||||
}
|
||||
response = self.client.post(reverse("exchange_rate_add"), data)
|
||||
self.assertEqual(response.status_code, 204, response.content.decode() if response.content and response.status_code != 204 else "No content on 204")
|
||||
self.assertTrue(ExchangeRate.objects.filter(from_currency=self.usd, to_currency=self.eur, rate=Decimal("0.88")).exists())
|
||||
self.assertEqual(
|
||||
response.status_code,
|
||||
204,
|
||||
(
|
||||
response.content.decode()
|
||||
if response.content and response.status_code != 204
|
||||
else "No content on 204"
|
||||
),
|
||||
)
|
||||
self.assertTrue(
|
||||
ExchangeRate.objects.filter(
|
||||
from_currency=self.usd, to_currency=self.eur, rate=Decimal("0.88")
|
||||
).exists()
|
||||
)
|
||||
|
||||
def test_exchange_rate_edit_view(self):
|
||||
rate = ExchangeRate.objects.create(from_currency=self.usd, to_currency=self.eur, rate=Decimal("0.91"), date=timezone.now())
|
||||
rate = ExchangeRate.objects.create(
|
||||
from_currency=self.usd,
|
||||
to_currency=self.eur,
|
||||
rate=Decimal("0.91"),
|
||||
date=timezone.now(),
|
||||
)
|
||||
data = {
|
||||
"from_currency": self.usd.id,
|
||||
"to_currency": self.eur.id,
|
||||
"rate": "0.92",
|
||||
"date": rate.date.strftime('%Y-%m-%d %H:%M:%S')
|
||||
"date": rate.date.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
}
|
||||
response = self.client.post(reverse("exchange_rate_edit", args=[rate.id]), data)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
@@ -259,7 +342,12 @@ class ExchangeRateViewTests(BaseCurrencyAppTest):
|
||||
self.assertEqual(rate.rate, Decimal("0.92"))
|
||||
|
||||
def test_exchange_rate_delete_view(self):
|
||||
rate = ExchangeRate.objects.create(from_currency=self.usd, to_currency=self.eur, rate=Decimal("0.93"), date=timezone.now())
|
||||
rate = ExchangeRate.objects.create(
|
||||
from_currency=self.usd,
|
||||
to_currency=self.eur,
|
||||
rate=Decimal("0.93"),
|
||||
date=timezone.now(),
|
||||
)
|
||||
response = self.client.delete(reverse("exchange_rate_delete", args=[rate.id]))
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.assertFalse(ExchangeRate.objects.filter(id=rate.id).exists())
|
||||
@@ -267,8 +355,12 @@ class ExchangeRateViewTests(BaseCurrencyAppTest):
|
||||
|
||||
class ExchangeRateServiceViewTests(BaseCurrencyAppTest):
|
||||
def test_exchange_rate_service_list_view(self):
|
||||
service = ExchangeRateService.objects.create(name="My Test Service", service_type=ExchangeRateService.ServiceType.SYNTH_FINANCE, fetch_interval="1")
|
||||
response = self.client.get(reverse("exchange_rates_services_list"))
|
||||
service = ExchangeRateService.objects.create(
|
||||
name="My Test Service",
|
||||
service_type=ExchangeRateService.ServiceType.SYNTH_FINANCE,
|
||||
fetch_interval="1",
|
||||
)
|
||||
response = self.client.get(reverse("automatic_exchange_rates_list"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, service.name)
|
||||
|
||||
@@ -281,33 +373,47 @@ class ExchangeRateServiceViewTests(BaseCurrencyAppTest):
|
||||
"fetch_interval": "24",
|
||||
# target_currencies and target_accounts are M2M, handled differently or optional
|
||||
}
|
||||
response = self.client.post(reverse("exchange_rate_service_add"), data)
|
||||
response = self.client.post(reverse("automatic_exchange_rate_add"), data)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.assertTrue(ExchangeRateService.objects.filter(name="New Fetcher Service").exists())
|
||||
self.assertTrue(
|
||||
ExchangeRateService.objects.filter(name="New Fetcher Service").exists()
|
||||
)
|
||||
|
||||
def test_exchange_rate_service_edit_view(self):
|
||||
service = ExchangeRateService.objects.create(name="Editable Service", service_type=ExchangeRateService.ServiceType.SYNTH_FINANCE, fetch_interval="1")
|
||||
service = ExchangeRateService.objects.create(
|
||||
name="Editable Service",
|
||||
service_type=ExchangeRateService.ServiceType.SYNTH_FINANCE,
|
||||
fetch_interval="1",
|
||||
)
|
||||
data = {
|
||||
"name": "Edited Fetcher Service",
|
||||
"service_type": service.service_type,
|
||||
"is_active": "on",
|
||||
"interval_type": service.interval_type,
|
||||
"fetch_interval": "6", # Changed interval
|
||||
"fetch_interval": "6", # Changed interval
|
||||
}
|
||||
response = self.client.post(reverse("exchange_rate_service_edit", args=[service.id]), data)
|
||||
response = self.client.post(
|
||||
reverse("automatic_exchange_rate_edit", args=[service.id]), data
|
||||
)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
service.refresh_from_db()
|
||||
self.assertEqual(service.name, "Edited Fetcher Service")
|
||||
self.assertEqual(service.fetch_interval, "6")
|
||||
|
||||
def test_exchange_rate_service_delete_view(self):
|
||||
service = ExchangeRateService.objects.create(name="Deletable Service", service_type=ExchangeRateService.ServiceType.SYNTH_FINANCE, fetch_interval="1")
|
||||
response = self.client.delete(reverse("exchange_rate_service_delete", args=[service.id]))
|
||||
service = ExchangeRateService.objects.create(
|
||||
name="Deletable Service",
|
||||
service_type=ExchangeRateService.ServiceType.SYNTH_FINANCE,
|
||||
fetch_interval="1",
|
||||
)
|
||||
response = self.client.delete(
|
||||
reverse("automatic_exchange_rate_delete", args=[service.id])
|
||||
)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.assertFalse(ExchangeRateService.objects.filter(id=service.id).exists())
|
||||
|
||||
@patch('apps.currencies.tasks.manual_fetch_exchange_rates.defer')
|
||||
@patch("apps.currencies.tasks.manual_fetch_exchange_rates.defer")
|
||||
def test_exchange_rate_service_force_fetch_view(self, mock_defer):
|
||||
response = self.client.get(reverse("exchange_rate_service_force_fetch"))
|
||||
self.assertEqual(response.status_code, 204) # Triggers toast
|
||||
response = self.client.get(reverse("automatic_exchange_rate_force_fetch"))
|
||||
self.assertEqual(response.status_code, 204) # Triggers toast
|
||||
mock_defer.assert_called_once()
|
||||
|
||||
Reference in New Issue
Block a user