from unittest.mock import patch from dateutil.relativedelta import relativedelta from django.test import TestCase from django.utils import timezone from bookmarks.utils import ( humanize_absolute_date, humanize_relative_date, parse_timestamp, normalize_url, ) class UtilsTestCase(TestCase): def test_humanize_absolute_date(self): test_cases = [ ( timezone.datetime(2021, 1, 1), timezone.datetime(2023, 1, 1), "01/01/2021", ), ( timezone.datetime(2021, 1, 1), timezone.datetime(2021, 2, 1), "01/01/2021", ), ( timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 8), "01/01/2021", ), (timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 7), "Friday"), ( timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 7, 23, 59), "Friday", ), (timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 3), "Friday"), (timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 2), "Yesterday"), ( timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 2, 23, 59), "Yesterday", ), (timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 1), "Today"), ] for test_case in test_cases: result = humanize_absolute_date(test_case[0], test_case[1]) self.assertEqual(test_case[2], result) def test_humanize_absolute_date_should_use_current_date_as_default(self): with patch.object(timezone, "now", return_value=timezone.datetime(2021, 1, 1)): self.assertEqual( humanize_absolute_date(timezone.datetime(2021, 1, 1)), "Today" ) # Regression: Test that subsequent calls use current date instead of cached date (#107) with patch.object(timezone, "now", return_value=timezone.datetime(2021, 1, 13)): self.assertEqual( humanize_absolute_date(timezone.datetime(2021, 1, 13)), "Today" ) def test_humanize_relative_date(self): test_cases = [ ( timezone.datetime(2021, 1, 1), timezone.datetime(2022, 1, 1), "1 year ago", ), ( timezone.datetime(2021, 1, 1), timezone.datetime(2022, 12, 31), "1 year ago", ), ( timezone.datetime(2021, 1, 1), timezone.datetime(2023, 1, 1), "2 years ago", ), ( timezone.datetime(2021, 1, 1), timezone.datetime(2023, 12, 31), "2 years ago", ), ( timezone.datetime(2021, 1, 1), timezone.datetime(2021, 12, 31), "11 months ago", ), ( timezone.datetime(2021, 1, 1), timezone.datetime(2021, 2, 1), "1 month ago", ), ( timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 31), "4 weeks ago", ), ( timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 14), "1 week ago", ), ( timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 8), "1 week ago", ), (timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 7), "Friday"), ( timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 7, 23, 59), "Friday", ), (timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 3), "Friday"), (timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 2), "Yesterday"), ( timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 2, 23, 59), "Yesterday", ), (timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 1), "Today"), ] for test_case in test_cases: result = humanize_relative_date(test_case[0], test_case[1]) self.assertEqual(test_case[2], result) def test_humanize_relative_date_should_use_current_date_as_default(self): with patch.object(timezone, "now", return_value=timezone.datetime(2021, 1, 1)): self.assertEqual( humanize_relative_date(timezone.datetime(2021, 1, 1)), "Today" ) # Regression: Test that subsequent calls use current date instead of cached date (#107) with patch.object(timezone, "now", return_value=timezone.datetime(2021, 1, 13)): self.assertEqual( humanize_relative_date(timezone.datetime(2021, 1, 13)), "Today" ) def verify_timestamp(self, date, factor=1): timestamp_string = str(int(date.timestamp() * factor)) parsed_date = parse_timestamp(timestamp_string) self.assertEqual(date, parsed_date) def test_parse_timestamp_fails_for_invalid_timestamps(self): with self.assertRaises(ValueError): parse_timestamp("invalid") def test_parse_timestamp_parses_millisecond_timestamps(self): now = timezone.now().replace(microsecond=0) fifty_years_ago = now - relativedelta(year=50) fifty_years_from_now = now + relativedelta(year=50) self.verify_timestamp(now) self.verify_timestamp(fifty_years_ago) self.verify_timestamp(fifty_years_from_now) def test_parse_timestamp_parses_microsecond_timestamps(self): now = timezone.now().replace(microsecond=0) fifty_years_ago = now - relativedelta(year=50) fifty_years_from_now = now + relativedelta(year=50) self.verify_timestamp(now, 1000) self.verify_timestamp(fifty_years_ago, 1000) self.verify_timestamp(fifty_years_from_now, 1000) def test_parse_timestamp_parses_nanosecond_timestamps(self): now = timezone.now().replace(microsecond=0) fifty_years_ago = now - relativedelta(year=50) fifty_years_from_now = now + relativedelta(year=50) self.verify_timestamp(now, 1000000) self.verify_timestamp(fifty_years_ago, 1000000) self.verify_timestamp(fifty_years_from_now, 1000000) def test_parse_timestamp_fails_for_out_of_range_timestamp(self): now = timezone.now().replace(microsecond=0) with self.assertRaises(ValueError): self.verify_timestamp(now, 1000000000) def test_normalize_url_trailing_slash_handling(self): test_cases = [ ("https://example.com/", "https://example.com"), ( "https://example.com/path/", "https://example.com/path", ), ("https://example.com/path/to/page/", "https://example.com/path/to/page"), ( "https://example.com/path", "https://example.com/path", ), ] for original, expected in test_cases: with self.subTest(url=original): result = normalize_url(original) self.assertEqual(expected, result) def test_normalize_url_query_parameters(self): test_cases = [ ("https://example.com?z=1&a=2", "https://example.com?a=2&z=1"), ("https://example.com?c=3&b=2&a=1", "https://example.com?a=1&b=2&c=3"), ("https://example.com?param=value", "https://example.com?param=value"), ("https://example.com?", "https://example.com"), ( "https://example.com?empty=&filled=value", "https://example.com?empty=&filled=value", ), ] for original, expected in test_cases: with self.subTest(url=original): result = normalize_url(original) self.assertEqual(expected, result) def test_normalize_url_case_sensitivity(self): test_cases = [ ( "https://EXAMPLE.com/Path/To/Page", "https://example.com/Path/To/Page", ), ("https://EXAMPLE.COM/API/v1/Users", "https://example.com/API/v1/Users"), ( "HTTPS://EXAMPLE.COM/path", "https://example.com/path", ), ] for original, expected in test_cases: with self.subTest(url=original): result = normalize_url(original) self.assertEqual(expected, result) def test_normalize_url_special_characters_and_encoding(self): test_cases = [ ( "https://example.com/path%20with%20spaces", "https://example.com/path%20with%20spaces", ), ("https://example.com/caf%C3%A9", "https://example.com/caf%C3%A9"), ( "https://example.com/path?q=hello%20world", "https://example.com/path?q=hello%20world", ), ("https://example.com/pàth", "https://example.com/pàth"), ] for original, expected in test_cases: with self.subTest(url=original): result = normalize_url(original) self.assertEqual(expected, result) def test_normalize_url_various_protocols(self): test_cases = [ ("FTP://example.com", "ftp://example.com"), ("HTTP://EXAMPLE.COM", "http://example.com"), ("https://example.com", "https://example.com"), ("file:///path/to/file", "file:///path/to/file"), ] for original, expected in test_cases: with self.subTest(url=original): result = normalize_url(original) self.assertEqual(expected, result) def test_normalize_url_port_handling(self): test_cases = [ ("https://example.com:8080", "https://example.com:8080"), ("https://EXAMPLE.COM:8080", "https://example.com:8080"), ("http://example.com:80", "http://example.com:80"), ("https://example.com:443", "https://example.com:443"), ] for original, expected in test_cases: with self.subTest(url=original): result = normalize_url(original) self.assertEqual(expected, result) def test_normalize_url_authentication_handling(self): test_cases = [ ("https://user:pass@EXAMPLE.COM", "https://user:pass@example.com"), ("https://user@EXAMPLE.COM", "https://user@example.com"), ("ftp://admin:secret@EXAMPLE.COM", "ftp://admin:secret@example.com"), ] for original, expected in test_cases: with self.subTest(url=original): result = normalize_url(original) self.assertEqual(expected, result) def test_normalize_url_fragment_handling(self): test_cases = [ ("https://example.com#", "https://example.com"), ("https://example.com#section", "https://example.com#section"), ("https://EXAMPLE.COM/path#Section", "https://example.com/path#Section"), ("https://EXAMPLE.COM/path/#Section", "https://example.com/path#Section"), ("https://example.com?a=1#fragment", "https://example.com?a=1#fragment"), ( "https://example.com?z=2&a=1#fragment", "https://example.com?a=1&z=2#fragment", ), ] for original, expected in test_cases: with self.subTest(url=original): result = normalize_url(original) self.assertEqual(expected, result) def test_normalize_url_edge_cases(self): test_cases = [ ("", ""), (" ", ""), (" https://example.com ", "https://example.com"), ("not-a-url", "not-a-url"), ("://invalid", "://invalid"), ] for original, expected in test_cases: with self.subTest(url=original): result = normalize_url(original) self.assertEqual(expected, result) def test_normalize_url_internationalized_domain_names(self): test_cases = [ ( "https://xn--fsq.xn--0zwm56d", "https://xn--fsq.xn--0zwm56d", ), ("https://测试.中国", "https://测试.中国"), ] for original, expected in test_cases: with self.subTest(url=original): result = normalize_url(original) self.assertEqual(expected.lower() if expected else expected, result) def test_normalize_url_complex_query_parameters(self): test_cases = [ ( "https://example.com?z=1&a=2&z=3&b=4", "https://example.com?a=2&b=4&z=1&z=3", # Multiple values for same key ), ( "https://example.com?param=value1¶m=value2", "https://example.com?param=value1¶m=value2", ), ( "https://example.com?special=%21%40%23%24%25", "https://example.com?special=%21%40%23%24%25", ), ] for original, expected in test_cases: with self.subTest(url=original): result = normalize_url(original) self.assertEqual(expected, result)