import datetime from decimal import Decimal from django.core.exceptions import ValidationError from django.db import models from django.test import TestCase from django.utils import translation from apps.common.fields.month_year import MonthYearModelField from apps.common.functions.dates import remaining_days_in_month from apps.common.functions.decimals import truncate_decimal from apps.common.templatetags.decimal import drop_trailing_zeros, localize_number from apps.common.templatetags.month_name import month_name class DateFunctionsTests(TestCase): def test_remaining_days_in_month(self): # Test with a date in the middle of the month current_date_mid = datetime.date(2023, 10, 15) self.assertEqual(remaining_days_in_month(2023, 10, current_date_mid), 17) # 31 - 15 + 1 # Test with the first day of the month current_date_first = datetime.date(2023, 10, 1) self.assertEqual(remaining_days_in_month(2023, 10, current_date_first), 31) # Test with the last day of the month current_date_last = datetime.date(2023, 10, 31) self.assertEqual(remaining_days_in_month(2023, 10, current_date_last), 1) # Test with a different month (should return total days in that month) self.assertEqual(remaining_days_in_month(2023, 11, current_date_mid), 30) # Test leap year (February 2024) current_date_feb_leap = datetime.date(2024, 2, 10) self.assertEqual(remaining_days_in_month(2024, 2, current_date_feb_leap), 20) # 29 - 10 + 1 current_date_feb_leap_other = datetime.date(2023, 1, 1) self.assertEqual(remaining_days_in_month(2024, 2, current_date_feb_leap_other), 29) # Test non-leap year (February 2023) current_date_feb_non_leap = datetime.date(2023, 2, 10) self.assertEqual(remaining_days_in_month(2023, 2, current_date_feb_non_leap), 19) # 28 - 10 + 1 class DecimalFunctionsTests(TestCase): def test_truncate_decimal(self): self.assertEqual(truncate_decimal(Decimal("123.456789"), 0), Decimal("123")) self.assertEqual(truncate_decimal(Decimal("123.456789"), 2), Decimal("123.45")) self.assertEqual(truncate_decimal(Decimal("123.45"), 4), Decimal("123.45")) # No change if fewer places self.assertEqual(truncate_decimal(Decimal("123"), 2), Decimal("123")) self.assertEqual(truncate_decimal(Decimal("0.12345"), 3), Decimal("0.123")) self.assertEqual(truncate_decimal(Decimal("-123.456"), 2), Decimal("-123.45")) # Dummy model for testing MonthYearModelField class Event(models.Model): name = models.CharField(max_length=100) event_month = MonthYearModelField() class Meta: app_label = 'common' # Required for temporary models in tests class MonthYearModelFieldTests(TestCase): def test_to_python_valid_formats(self): field = MonthYearModelField() # YYYY-MM format self.assertEqual(field.to_python("2023-10"), datetime.date(2023, 10, 1)) # YYYY-MM-DD format (should still set day to 1) self.assertEqual(field.to_python("2023-10-15"), datetime.date(2023, 10, 1)) # Already a date object date_obj = datetime.date(2023, 11, 1) self.assertEqual(field.to_python(date_obj), date_obj) # None value self.assertIsNone(field.to_python(None)) def test_to_python_invalid_formats(self): field = MonthYearModelField() with self.assertRaises(ValidationError): field.to_python("2023/10") with self.assertRaises(ValidationError): field.to_python("10-2023") with self.assertRaises(ValidationError): field.to_python("invalid-date") with self.assertRaises(ValidationError): # Invalid month field.to_python("2023-13") # More involved test requiring database interaction (migrations for dummy model) # This part might fail in the current sandbox if migrations can't be run for 'common.Event' # For now, focusing on to_python. A full test would involve creating an Event instance. # def test_db_storage_and_retrieval(self): # Event.objects.create(name="Test Event", event_month=datetime.date(2023, 9, 15)) # event = Event.objects.get(name="Test Event") # self.assertEqual(event.event_month, datetime.date(2023, 9, 1)) # # Test with string input that to_python handles # event_str_input = Event.objects.create(name="Event String", event_month="2024-07") # retrieved_event_str = Event.objects.get(name="Event String") # self.assertEqual(retrieved_event_str.event_month, datetime.date(2024, 7, 1)) class CommonTemplateTagTests(TestCase): def test_drop_trailing_zeros(self): self.assertEqual(drop_trailing_zeros(Decimal("10.500")), Decimal("10.5")) self.assertEqual(drop_trailing_zeros(Decimal("10.00")), Decimal("10")) self.assertEqual(drop_trailing_zeros(Decimal("10")), Decimal("10")) self.assertEqual(drop_trailing_zeros("12.340"), Decimal("12.34")) self.assertEqual(drop_trailing_zeros(12.0), Decimal("12")) # float input self.assertEqual(drop_trailing_zeros("not_a_decimal"), "not_a_decimal") self.assertIsNone(drop_trailing_zeros(None)) def test_localize_number(self): # Basic test, full localization testing is complex self.assertEqual(localize_number(Decimal("12345.678"), decimal_places=2), "12,345.68") # Assuming EN locale default self.assertEqual(localize_number(Decimal("12345"), decimal_places=0), "12,345") self.assertEqual(localize_number(12345.67, decimal_places=1), "12,345.7") self.assertEqual(localize_number("not_a_number"), "not_a_number") # Test with a different language if possible, though environment might be fixed # with translation.override('fr'): # self.assertEqual(localize_number(Decimal("12345.67"), decimal_places=2), "12 345,67") # Non-breaking space for FR def test_month_name_tag(self): self.assertEqual(month_name(1), "January") self.assertEqual(month_name(12), "December") # Assuming English as default, Django's translation might affect this # For more robust test, you might need to activate a specific language with translation.override('es'): self.assertEqual(month_name(1), "enero") with translation.override('en'): # Switch back self.assertEqual(month_name(1), "January") def test_month_name_invalid_input(self): # Test behavior for invalid month numbers, though calendar.month_name would raise IndexError # The filter should ideally handle this gracefully or be documented with self.assertRaises(IndexError): # calendar.month_name[0] is empty string, 13 is out of bounds month_name(0) with self.assertRaises(IndexError): month_name(13) # Depending on desired behavior, might expect empty string or specific error # For now, expecting it to follow calendar.month_name behavior from django.contrib.auth.models import AnonymousUser, User # Using Django's User for tests from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect from django.urls import reverse from django.test import RequestFactory from apps.common.decorators.htmx import only_htmx from apps.common.decorators.user import htmx_login_required, is_superuser # Assuming login_url can be resolved, e.g., from settings.LOGIN_URL or a known named URL # For testing, we might need to ensure LOGIN_URL is set or mock it. # Let's assume 'login' is a valid URL name for redirection. # Dummy views for testing decorators @only_htmx def dummy_view_only_htmx(request): return HttpResponse("HTMX Success") @htmx_login_required def dummy_view_htmx_login_required(request): return HttpResponse("User Authenticated HTMX") @is_superuser def dummy_view_is_superuser(request): return HttpResponse("Superuser Access Granted") class DecoratorTests(TestCase): def setUp(self): self.factory = RequestFactory() self.user = User.objects.create_user(username='testuser', email='test@example.com', password='password') self.superuser = User.objects.create_superuser(username='super', email='super@example.com', password='password') # Ensure LOGIN_URL is set for tests that redirect to login # This can be done via settings override if not already set globally self.settings_override = self.settings(LOGIN_URL='/fake-login/') # Use a dummy login URL self.settings_override.enable() def tearDown(self): self.settings_override.disable() # @only_htmx tests def test_only_htmx_allows_htmx_request(self): request = self.factory.get('/dummy-path', HTTP_HX_REQUEST='true') response = dummy_view_only_htmx(request) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b"HTMX Success") def test_only_htmx_forbids_non_htmx_request(self): request = self.factory.get('/dummy-path') response = dummy_view_only_htmx(request) self.assertEqual(response.status_code, 403) # Or whatever HttpResponseForbidden returns by default # @htmx_login_required tests def test_htmx_login_required_allows_authenticated_user(self): request = self.factory.get('/dummy-path', HTTP_HX_REQUEST='true') request.user = self.user response = dummy_view_htmx_login_required(request) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b"User Authenticated HTMX") def test_htmx_login_required_redirects_anonymous_user_for_htmx(self): request = self.factory.get('/dummy-path', HTTP_HX_REQUEST='true') request.user = AnonymousUser() response = dummy_view_htmx_login_required(request) self.assertEqual(response.status_code, 302) # Redirect # Check for HX-Redirect header for HTMX redirects to login self.assertIn('HX-Redirect', response.headers) self.assertEqual(response.headers['HX-Redirect'], '/fake-login/?next=/dummy-path') def test_htmx_login_required_redirects_anonymous_user_for_non_htmx(self): # This decorator specifically checks for HX-Request and returns 403 if not present *before* auth check. # However, if it were a general login_required for htmx, it might redirect non-htmx too. # The current name `htmx_login_required` implies it's for HTMX, let's test its behavior for non-HTMX. # Based on its typical implementation (like in `apps.users.views.UserLoginView` which is `only_htmx`), # it might return 403 if not an HTMX request, or redirect if it's a general login_required adapted for htmx. # Let's assume it's strictly for HTMX and would deny non-HTMX, or that the login_required part # would kick in. # Given the decorator might be composed or simple, let's test the redirect path. request = self.factory.get('/dummy-path') # Non-HTMX request.user = AnonymousUser() response = dummy_view_htmx_login_required(request) # If it's a standard @login_required behavior for non-HTMX part: self.assertTrue(response.status_code == 302 or response.status_code == 403) if response.status_code == 302: self.assertTrue(response.url.startswith('/fake-login/')) # @is_superuser tests def test_is_superuser_allows_superuser(self): request = self.factory.get('/dummy-path') request.user = self.superuser response = dummy_view_is_superuser(request) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b"Superuser Access Granted") def test_is_superuser_forbids_regular_user(self): request = self.factory.get('/dummy-path') request.user = self.user response = dummy_view_is_superuser(request) self.assertEqual(response.status_code, 403) # Or redirects to login if @login_required is also part of it def test_is_superuser_forbids_anonymous_user(self): request = self.factory.get('/dummy-path') request.user = AnonymousUser() response = dummy_view_is_superuser(request) # This typically redirects to login if @login_required is implicitly part of such checks, # or returns 403 if it's purely a superuser check after authentication. self.assertTrue(response.status_code == 302 or response.status_code == 403) if response.status_code == 302: # Standard redirect to login self.assertTrue(response.url.startswith('/fake-login/')) from io import StringIO from django.core.management import call_command from django.contrib.auth import get_user_model # Ensure User is available for management command test User = get_user_model() class ManagementCommandTests(TestCase): def test_setup_users_command(self): # Capture output out = StringIO() # Call the command. Provide dummy passwords or expect prompts to be handled if interactive. # For non-interactive, environment variables or default passwords in command might be used. # Let's assume it creates users with default/predictable passwords if run non-interactively # or we can mock input if needed. # For this test, we'll just check if it runs without error and creates some expected users. # This command might need specific environment variables like ADMIN_EMAIL, ADMIN_PASSWORD. # We'll set them for the test. test_admin_email = "admin@command.com" test_admin_pass = "CommandPass123" with self.settings(ADMIN_EMAIL=test_admin_email, ADMIN_PASSWORD=test_admin_pass): call_command('setup_users', stdout=out) # Check if the admin user was created (if the command is supposed to create one) self.assertTrue(User.objects.filter(email=test_admin_email).exists()) admin_user = User.objects.get(email=test_admin_email) self.assertTrue(admin_user.is_superuser) self.assertTrue(admin_user.check_password(test_admin_pass)) # The command also creates a 'user@example.com' self.assertTrue(User.objects.filter(email='user@example.com').exists()) # Check output for success messages (optional, depends on command's verbosity) # self.assertIn("Superuser admin@command.com created.", out.getvalue()) # self.assertIn("User user@example.com created.", out.getvalue()) # Note: The actual success messages might differ. This is a basic check. # The command might also try to create groups, assign permissions etc. # A more thorough test would check all side effects of the command.