""" Tests for utility functions Organized by function category: - Slug generation tests - Helper function tests - Edge cases and security tests """ import pytest from datetime import datetime from pathlib import Path from starpunk.utils import ( # Helper functions extract_first_words, normalize_slug_text, generate_random_suffix, # Slug functions generate_slug, make_slug_unique, validate_slug, # Content hashing calculate_content_hash, # Path operations generate_note_path, ensure_note_directory, validate_note_path, # File operations write_note_file, read_note_file, delete_note_file, # Date/time utilities format_rfc822, format_iso8601, parse_iso8601, # Constants RANDOM_SUFFIX_LENGTH, MAX_SLUG_LENGTH, TRASH_DIR_NAME, ) class TestHelperFunctions: """Test helper functions used by slug generation""" def test_extract_first_words_default(self): """Test extracting first 5 words (default).""" text = "Hello world this is a test note content" result = extract_first_words(text) assert result == "Hello world this is a" def test_extract_first_words_custom_count(self): """Test extracting custom number of words.""" text = "One two three four five six seven" result = extract_first_words(text, max_words=3) assert result == "One two three" def test_extract_first_words_fewer_than_max(self): """Test extracting when text has fewer words than max.""" text = "Only three words" result = extract_first_words(text, max_words=5) assert result == "Only three words" def test_extract_first_words_multiple_spaces(self): """Test extracting handles multiple spaces correctly.""" text = " Multiple spaces between words " result = extract_first_words(text, max_words=3) assert result == "Multiple spaces between" def test_extract_first_words_empty(self): """Test extracting from empty string.""" text = "" result = extract_first_words(text) assert result == "" def test_normalize_slug_text_basic(self): """Test basic text normalization.""" assert normalize_slug_text("Hello World") == "hello-world" def test_normalize_slug_text_special_characters(self): """Test normalization removes special characters.""" assert normalize_slug_text("Testing!@#$%^&*()") == "testing" assert normalize_slug_text("Hello... World!!!") == "hello-world" def test_normalize_slug_text_multiple_hyphens(self): """Test normalization collapses multiple hyphens.""" assert normalize_slug_text("Hello -- World") == "hello-world" assert normalize_slug_text("Test---note") == "test-note" def test_normalize_slug_text_leading_trailing_hyphens(self): """Test normalization strips leading/trailing hyphens.""" assert normalize_slug_text("-Hello-") == "hello" assert normalize_slug_text("---Test---") == "test" def test_normalize_slug_text_numbers(self): """Test normalization preserves numbers.""" assert normalize_slug_text("Test 123 Note") == "test-123-note" assert normalize_slug_text("2024-11-18") == "2024-11-18" def test_normalize_slug_text_unicode(self): """Test normalization handles unicode (strips it).""" assert normalize_slug_text("Hello 世界") == "hello" assert normalize_slug_text("Café") == "caf" def test_generate_random_suffix_length(self): """Test random suffix has correct length.""" suffix = generate_random_suffix() assert len(suffix) == RANDOM_SUFFIX_LENGTH def test_generate_random_suffix_custom_length(self): """Test random suffix with custom length.""" suffix = generate_random_suffix(8) assert len(suffix) == 8 def test_generate_random_suffix_alphanumeric(self): """Test random suffix contains only lowercase alphanumeric.""" suffix = generate_random_suffix() assert suffix.isalnum() assert suffix.islower() def test_generate_random_suffix_uniqueness(self): """Test random suffixes are different (high probability).""" suffixes = [generate_random_suffix() for _ in range(100)] # Should have high uniqueness (not all the same) assert len(set(suffixes)) > 90 # At least 90 unique out of 100 class TestSlugGeneration: """Test slug generation functions""" def test_generate_slug_basic(self): """Test basic slug generation from content.""" slug = generate_slug("Hello World This Is My Note") assert slug == "hello-world-this-is-my" def test_generate_slug_special_characters(self): """Test slug generation removes special characters.""" slug = generate_slug("Testing... with!@# special chars") assert slug == "testing-with-special-chars" def test_generate_slug_punctuation(self): """Test slug generation handles punctuation.""" slug = generate_slug("What's up? How are you doing today?") assert slug == "whats-up-how-are-you" def test_generate_slug_numbers(self): """Test slug generation preserves numbers.""" slug = generate_slug("The 2024 Guide to Python Programming") assert slug == "the-2024-guide-to-python" def test_generate_slug_first_five_words(self): """Test slug uses first 5 words (default).""" slug = generate_slug("One two three four five six seven eight") assert slug == "one-two-three-four-five" def test_generate_slug_fewer_than_five_words(self): """Test slug with fewer than 5 words.""" slug = generate_slug("Three word note") assert slug == "three-word-note" def test_generate_slug_empty_content_raises_error(self): """Test slug generation raises error on empty content.""" with pytest.raises(ValueError, match="Content cannot be empty"): generate_slug("") def test_generate_slug_whitespace_only_raises_error(self): """Test slug generation raises error on whitespace-only content.""" with pytest.raises(ValueError, match="Content cannot be empty"): generate_slug(" \n\t ") def test_generate_slug_special_chars_only_uses_timestamp(self): """Test slug falls back to timestamp when only special chars.""" dt = datetime(2024, 11, 18, 14, 30, 45) slug = generate_slug("!@#$%^&*()", created_at=dt) assert slug == "20241118-143045" def test_generate_slug_very_short_content_uses_timestamp(self): """Test slug falls back to timestamp for very short content.""" # Single character is valid after normalization slug = generate_slug("A") assert slug == "a" # But if normalized result is empty, uses timestamp dt = datetime(2024, 11, 18, 14, 30, 0) slug = generate_slug("!", created_at=dt) assert slug == "20241118-143000" def test_generate_slug_timestamp_fallback_uses_utcnow(self): """Test slug timestamp fallback uses current time if not provided.""" slug = generate_slug("!@#$") # Should be in timestamp format assert len(slug) == 15 # YYYYMMDD-HHMMSS assert slug[8] == "-" assert slug[:8].isdigit() assert slug[9:].isdigit() def test_generate_slug_truncation(self): """Test slug is truncated to max length.""" # Create very long content long_content = " ".join(["verylongword" for _ in range(20)]) slug = generate_slug(long_content) assert len(slug) <= MAX_SLUG_LENGTH def test_generate_slug_unicode_content(self): """Test slug generation handles unicode gracefully.""" slug = generate_slug("Hello 世界 World Python Programming") # Unicode is stripped, should keep ASCII words assert "hello" in slug assert "world" in slug def test_generate_slug_multiple_spaces(self): """Test slug handles multiple spaces between words.""" slug = generate_slug("Hello World This Is Test") assert slug == "hello-world-this-is-test" class TestSlugUniqueness: """Test slug uniqueness enforcement""" def test_make_slug_unique_no_collision(self): """Test make_slug_unique returns original if no collision.""" slug = make_slug_unique("test-note", set()) assert slug == "test-note" def test_make_slug_unique_with_collision(self): """Test make_slug_unique adds suffix on collision.""" slug = make_slug_unique("test-note", {"test-note"}) assert slug.startswith("test-note-") assert len(slug) == len("test-note-") + RANDOM_SUFFIX_LENGTH def test_make_slug_unique_suffix_is_alphanumeric(self): """Test unique slug suffix is alphanumeric.""" slug = make_slug_unique("test-note", {"test-note"}) suffix = slug.split("-")[-1] assert suffix.isalnum() assert suffix.islower() def test_make_slug_unique_multiple_collisions(self): """Test make_slug_unique handles multiple collisions.""" existing = {"test-note", "test-note-abcd", "test-note-xyz1"} slug = make_slug_unique("test-note", existing) assert slug.startswith("test-note-") assert slug not in existing def test_make_slug_unique_generates_different_suffixes(self): """Test make_slug_unique generates different suffixes each time.""" existing = {"test-note"} slugs = [] for _ in range(10): slug = make_slug_unique("test-note", existing) slugs.append(slug) existing.add(slug) # All should be unique assert len(set(slugs)) == 10 def test_make_slug_unique_empty_existing_set(self): """Test make_slug_unique with empty set.""" slug = make_slug_unique("hello-world", set()) assert slug == "hello-world" class TestSlugValidation: """Test slug validation""" def test_validate_slug_valid_basic(self): """Test validation accepts valid basic slugs.""" assert validate_slug("hello-world") is True assert validate_slug("test-note") is True assert validate_slug("my-first-post") is True def test_validate_slug_valid_with_numbers(self): """Test validation accepts slugs with numbers.""" assert validate_slug("post-123") is True assert validate_slug("2024-11-18-note") is True assert validate_slug("test1-note2") is True def test_validate_slug_valid_single_character(self): """Test validation accepts single character slug.""" assert validate_slug("a") is True assert validate_slug("1") is True def test_validate_slug_valid_max_length(self): """Test validation accepts slug at max length.""" slug = "a" * MAX_SLUG_LENGTH assert validate_slug(slug) is True def test_validate_slug_invalid_empty(self): """Test validation rejects empty slug.""" assert validate_slug("") is False def test_validate_slug_invalid_uppercase(self): """Test validation rejects uppercase characters.""" assert validate_slug("Hello-World") is False assert validate_slug("TEST") is False def test_validate_slug_invalid_leading_hyphen(self): """Test validation rejects leading hyphen.""" assert validate_slug("-hello") is False assert validate_slug("-test-note") is False def test_validate_slug_invalid_trailing_hyphen(self): """Test validation rejects trailing hyphen.""" assert validate_slug("hello-") is False assert validate_slug("test-note-") is False def test_validate_slug_invalid_double_hyphen(self): """Test validation rejects consecutive hyphens.""" assert validate_slug("hello--world") is False assert validate_slug("test---note") is False def test_validate_slug_invalid_underscore(self): """Test validation rejects underscores.""" assert validate_slug("hello_world") is False assert validate_slug("test_note") is False def test_validate_slug_invalid_special_characters(self): """Test validation rejects special characters.""" assert validate_slug("hello@world") is False assert validate_slug("test!note") is False assert validate_slug("note#123") is False assert validate_slug("hello.world") is False def test_validate_slug_invalid_spaces(self): """Test validation rejects spaces.""" assert validate_slug("hello world") is False assert validate_slug("test note") is False def test_validate_slug_invalid_too_long(self): """Test validation rejects slug exceeding max length.""" slug = "a" * (MAX_SLUG_LENGTH + 1) assert validate_slug(slug) is False def test_validate_slug_invalid_reserved_admin(self): """Test validation rejects reserved slug 'admin'.""" assert validate_slug("admin") is False def test_validate_slug_invalid_reserved_api(self): """Test validation rejects reserved slug 'api'.""" assert validate_slug("api") is False def test_validate_slug_invalid_reserved_static(self): """Test validation rejects reserved slug 'static'.""" assert validate_slug("static") is False def test_validate_slug_invalid_reserved_auth(self): """Test validation rejects reserved slug 'auth'.""" assert validate_slug("auth") is False def test_validate_slug_invalid_reserved_feed(self): """Test validation rejects reserved slug 'feed'.""" assert validate_slug("feed") is False def test_validate_slug_invalid_reserved_login(self): """Test validation rejects reserved slug 'login'.""" assert validate_slug("login") is False def test_validate_slug_invalid_reserved_logout(self): """Test validation rejects reserved slug 'logout'.""" assert validate_slug("logout") is False class TestEdgeCases: """Test edge cases and boundary conditions""" def test_slug_with_only_hyphens_after_normalization(self): """Test content that becomes only hyphens after normalization.""" dt = datetime(2024, 11, 18, 15, 0, 0) slug = generate_slug("--- --- ---", created_at=dt) # Should fall back to timestamp assert slug == "20241118-150000" def test_slug_with_mixed_case(self): """Test slug generation normalizes mixed case.""" slug = generate_slug("HeLLo WoRLd ThIs Is TeSt") assert slug == "hello-world-this-is-test" def test_slug_with_newlines(self): """Test slug generation handles newlines in content.""" slug = generate_slug( "First line\nSecond line\nThird line\nFourth line\nFifth line" ) assert slug == "first-line-second-line-third" def test_slug_with_tabs(self): """Test slug generation handles tabs.""" slug = generate_slug("First\tSecond\tThird\tFourth\tFifth") assert slug == "first-second-third-fourth-fifth" def test_slug_timestamp_format(self): """Test timestamp format is correct.""" dt = datetime(2024, 11, 18, 14, 30, 45) slug = generate_slug("@@@", created_at=dt) assert slug == "20241118-143045" # Verify format assert len(slug) == 15 assert slug[8] == "-" def test_very_long_single_word(self): """Test slug with very long single word gets truncated.""" long_word = "a" * 200 slug = generate_slug(long_word) assert len(slug) <= MAX_SLUG_LENGTH assert slug == "a" * MAX_SLUG_LENGTH def test_slug_with_emoji(self): """Test slug generation strips emoji.""" slug = generate_slug("Hello 🌍 World 🎉 This Is Test") # Emoji should be stripped, leaving only the first 5 words assert slug == "hello-world-this" def test_slug_uniqueness_with_reserved_base(self): """Test making reserved slug unique (though shouldn't happen).""" # If somehow a reserved slug needs uniqueness existing = {"admin"} slug = make_slug_unique("admin", existing) assert slug.startswith("admin-") assert validate_slug(slug) is True # Should be valid after suffix def test_normalize_all_special_chars(self): """Test normalization with all special characters.""" text = "!@#$%^&*()_+={}[]|\\:;\"'<>,.?/~`" result = normalize_slug_text(text) assert result == "" def test_generate_slug_with_markdown_heading(self): """Test slug generation from markdown heading.""" slug = generate_slug("# My First Blog Post About Python") # The # should be treated as special char and removed, first 5 words assert slug == "my-first-blog-post" class TestSecurityCases: """Test security-related edge cases""" def test_slug_no_path_traversal_characters(self): """Test slug doesn't contain path traversal patterns.""" slug = generate_slug("../../etc/passwd is the test note") assert ".." not in slug assert "/" not in slug # Dots/slashes removed, "../../etc/passwd" becomes "etcpasswd" # Then "is the test note" = 4 more words, total 5 words assert slug == "etcpasswd-is-the-test-note" def test_slug_no_null_bytes(self): """Test slug handles null bytes safely.""" # Python strings can contain null bytes content = "Test\x00note\x00content" slug = generate_slug(content) assert "\x00" not in slug def test_slug_no_sql_injection_patterns(self): """Test slug doesn't preserve SQL injection patterns.""" slug = generate_slug("'; DROP TABLE notes; -- is my note") # Should be normalized, no special SQL chars assert ";" not in slug assert "'" not in slug assert "--" not in slug def test_slug_no_script_tags(self): """Test slug doesn't preserve script tags.""" slug = generate_slug(" My Note Title") assert "<" not in slug assert ">" not in slug assert "script" in slug # The word itself is fine # Special chars removed, becomes one word, then first 5 words total assert slug == "scriptalertxssscript-my-note-title" def test_random_suffix_uses_secrets_module(self): """Test random suffix is cryptographically secure (not predictable).""" # Generate many suffixes and ensure high entropy suffixes = [generate_random_suffix() for _ in range(1000)] unique_count = len(set(suffixes)) # Should have very high uniqueness (>99%) assert unique_count > 990 class TestContentHashing: """Test content hashing functions""" def test_calculate_content_hash_consistency(self): """Test hash is consistent for same content.""" hash1 = calculate_content_hash("Test content") hash2 = calculate_content_hash("Test content") assert hash1 == hash2 def test_calculate_content_hash_different(self): """Test different content produces different hash.""" hash1 = calculate_content_hash("Test content 1") hash2 = calculate_content_hash("Test content 2") assert hash1 != hash2 def test_calculate_content_hash_empty(self): """Test hash of empty string.""" hash_empty = calculate_content_hash("") assert len(hash_empty) == 64 # SHA-256 produces 64 hex chars assert hash_empty.isalnum() def test_calculate_content_hash_unicode(self): """Test hash handles unicode correctly.""" hash_val = calculate_content_hash("Hello 世界") assert len(hash_val) == 64 assert hash_val.isalnum() def test_calculate_content_hash_known_value(self): """Test hash matches known SHA-256 value.""" # Known SHA-256 hash for "Hello World" expected = "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e" actual = calculate_content_hash("Hello World") assert actual == expected def test_calculate_content_hash_multiline(self): """Test hash of multiline content.""" content = "Line 1\nLine 2\nLine 3" hash_val = calculate_content_hash(content) assert len(hash_val) == 64 def test_calculate_content_hash_special_characters(self): """Test hash handles special characters.""" content = "Special chars: !@#$%^&*()_+-=[]{}|;:',.<>?/~`" hash_val = calculate_content_hash(content) assert len(hash_val) == 64 class TestFilePathOperations: """Test file path generation and validation""" def test_generate_note_path_basic(self): """Test basic note path generation.""" dt = datetime(2024, 11, 18, 14, 30) path = generate_note_path("test-note", dt, Path("data")) assert path == Path("data/notes/2024/11/test-note.md") def test_generate_note_path_different_months(self): """Test path generation for different months.""" dt_jan = datetime(2024, 1, 5, 10, 0) dt_dec = datetime(2024, 12, 25, 15, 30) path_jan = generate_note_path("jan-note", dt_jan, Path("data")) path_dec = generate_note_path("dec-note", dt_dec, Path("data")) assert path_jan == Path("data/notes/2024/01/jan-note.md") assert path_dec == Path("data/notes/2024/12/dec-note.md") def test_generate_note_path_different_years(self): """Test path generation for different years.""" dt_2024 = datetime(2024, 6, 15) dt_2025 = datetime(2025, 6, 15) path_2024 = generate_note_path("note-2024", dt_2024, Path("data")) path_2025 = generate_note_path("note-2025", dt_2025, Path("data")) assert path_2024 == Path("data/notes/2024/06/note-2024.md") assert path_2025 == Path("data/notes/2025/06/note-2025.md") def test_generate_note_path_invalid_slug(self): """Test note path generation rejects invalid slug.""" dt = datetime(2024, 11, 18) with pytest.raises(ValueError, match="Invalid slug"): generate_note_path("Invalid Slug!", dt, Path("data")) def test_generate_note_path_with_numbers(self): """Test path generation with slug containing numbers.""" dt = datetime(2024, 11, 18) path = generate_note_path("note-123-test", dt, Path("data")) assert path == Path("data/notes/2024/11/note-123-test.md") def test_ensure_note_directory_creates_dirs(self, tmp_path): """Test ensure_note_directory creates directories.""" note_path = tmp_path / "notes" / "2024" / "11" / "test.md" assert not note_path.parent.exists() result = ensure_note_directory(note_path) assert note_path.parent.exists() assert result == note_path.parent def test_ensure_note_directory_existing_dirs(self, tmp_path): """Test ensure_note_directory with existing directories.""" note_path = tmp_path / "notes" / "2024" / "11" / "test.md" note_path.parent.mkdir(parents=True) # Should not raise error result = ensure_note_directory(note_path) assert result == note_path.parent def test_ensure_note_directory_deep_structure(self, tmp_path): """Test ensure_note_directory with deep directory structure.""" note_path = tmp_path / "a" / "b" / "c" / "d" / "e" / "test.md" result = ensure_note_directory(note_path) assert note_path.parent.exists() assert result == note_path.parent def test_validate_note_path_safe(self, tmp_path): """Test path validation accepts safe paths.""" note_path = tmp_path / "notes" / "2024" / "11" / "note.md" assert validate_note_path(note_path, tmp_path) is True def test_validate_note_path_traversal_dotdot(self, tmp_path): """Test path validation rejects .. traversal.""" note_path = tmp_path / "notes" / ".." / ".." / "etc" / "passwd" assert validate_note_path(note_path, tmp_path) is False def test_validate_note_path_absolute_outside(self, tmp_path): """Test path validation rejects absolute paths outside data dir.""" assert validate_note_path(Path("/etc/passwd"), tmp_path) is False def test_validate_note_path_within_subdirectory(self, tmp_path): """Test path validation accepts paths in subdirectories.""" note_path = tmp_path / "notes" / "2024" / "11" / "subfolder" / "note.md" assert validate_note_path(note_path, tmp_path) is True def test_validate_note_path_symlink_outside(self, tmp_path): """Test path validation handles symlinks pointing outside.""" # Create a symlink pointing outside data_dir outside_dir = tmp_path.parent / "outside" outside_dir.mkdir(exist_ok=True) link_path = tmp_path / "link" link_path.symlink_to(outside_dir) target_path = link_path / "file.md" assert validate_note_path(target_path, tmp_path) is False def test_validate_note_path_same_directory(self, tmp_path): """Test path validation for file in data_dir root.""" note_path = tmp_path / "note.md" assert validate_note_path(note_path, tmp_path) is True class TestAtomicFileOperations: """Test atomic file write/read/delete operations""" def test_write_and_read_note_file(self, tmp_path): """Test writing and reading note file.""" file_path = tmp_path / "test.md" content = "# Test Note\n\nThis is a test." write_note_file(file_path, content) assert file_path.exists() read_content = read_note_file(file_path) assert read_content == content def test_write_note_file_atomic(self, tmp_path): """Test write is atomic (temp file cleaned up).""" file_path = tmp_path / "test.md" temp_path = file_path.with_suffix(".md.tmp") write_note_file(file_path, "Test") # Temp file should not exist after write assert not temp_path.exists() assert file_path.exists() def test_write_note_file_overwrites(self, tmp_path): """Test writing overwrites existing file.""" file_path = tmp_path / "test.md" write_note_file(file_path, "Original content") write_note_file(file_path, "New content") content = read_note_file(file_path) assert content == "New content" def test_write_note_file_unicode(self, tmp_path): """Test writing unicode content.""" file_path = tmp_path / "test.md" content = "Unicode: 你好世界 🌍" write_note_file(file_path, content) read_content = read_note_file(file_path) assert read_content == content def test_write_note_file_empty(self, tmp_path): """Test writing empty file.""" file_path = tmp_path / "test.md" write_note_file(file_path, "") content = read_note_file(file_path) assert content == "" def test_write_note_file_multiline(self, tmp_path): """Test writing multiline content.""" file_path = tmp_path / "test.md" content = "Line 1\nLine 2\nLine 3\n" write_note_file(file_path, content) read_content = read_note_file(file_path) assert read_content == content def test_read_note_file_not_found(self, tmp_path): """Test reading non-existent file raises error.""" file_path = tmp_path / "nonexistent.md" with pytest.raises(FileNotFoundError): read_note_file(file_path) def test_delete_note_file_hard(self, tmp_path): """Test hard delete removes file.""" file_path = tmp_path / "test.md" file_path.write_text("Test") delete_note_file(file_path, soft=False) assert not file_path.exists() def test_delete_note_file_soft(self, tmp_path): """Test soft delete moves file to trash.""" # Create note file notes_dir = tmp_path / "notes" / "2024" / "11" notes_dir.mkdir(parents=True) file_path = notes_dir / "test.md" file_path.write_text("Test") # Soft delete delete_note_file(file_path, soft=True, data_dir=tmp_path) # Original should be gone assert not file_path.exists() # Should be in trash trash_path = tmp_path / TRASH_DIR_NAME / "2024" / "11" / "test.md" assert trash_path.exists() assert trash_path.read_text() == "Test" def test_delete_note_file_soft_without_data_dir(self, tmp_path): """Test soft delete requires data_dir.""" file_path = tmp_path / "test.md" file_path.write_text("Test") with pytest.raises(ValueError, match="data_dir is required"): delete_note_file(file_path, soft=True, data_dir=None) def test_delete_note_file_soft_different_months(self, tmp_path): """Test soft delete preserves year/month structure.""" # Create note in January jan_dir = tmp_path / "notes" / "2024" / "01" jan_dir.mkdir(parents=True) jan_file = jan_dir / "jan-note.md" jan_file.write_text("January note") # Create note in December dec_dir = tmp_path / "notes" / "2024" / "12" dec_dir.mkdir(parents=True) dec_file = dec_dir / "dec-note.md" dec_file.write_text("December note") # Soft delete both delete_note_file(jan_file, soft=True, data_dir=tmp_path) delete_note_file(dec_file, soft=True, data_dir=tmp_path) # Check trash structure jan_trash = tmp_path / TRASH_DIR_NAME / "2024" / "01" / "jan-note.md" dec_trash = tmp_path / TRASH_DIR_NAME / "2024" / "12" / "dec-note.md" assert jan_trash.exists() assert dec_trash.exists() def test_delete_note_file_hard_not_found(self, tmp_path): """Test hard delete of non-existent file raises error.""" file_path = tmp_path / "nonexistent.md" with pytest.raises(FileNotFoundError): delete_note_file(file_path, soft=False) class TestDateTimeFormatting: """Test date/time formatting functions""" def test_format_rfc822_basic(self): """Test RFC-822 date formatting.""" dt = datetime(2024, 11, 18, 14, 30, 45) formatted = format_rfc822(dt) assert formatted == "Mon, 18 Nov 2024 14:30:45 +0000" def test_format_rfc822_different_dates(self): """Test RFC-822 formatting for different dates.""" dt1 = datetime(2024, 1, 1, 0, 0, 0) dt2 = datetime(2024, 12, 31, 23, 59, 59) assert format_rfc822(dt1) == "Mon, 01 Jan 2024 00:00:00 +0000" assert format_rfc822(dt2) == "Tue, 31 Dec 2024 23:59:59 +0000" def test_format_rfc822_weekdays(self): """Test RFC-822 format includes correct weekday.""" # Known dates and weekdays monday = datetime(2024, 11, 18, 12, 0, 0) friday = datetime(2024, 11, 22, 12, 0, 0) sunday = datetime(2024, 11, 24, 12, 0, 0) assert format_rfc822(monday).startswith("Mon,") assert format_rfc822(friday).startswith("Fri,") assert format_rfc822(sunday).startswith("Sun,") def test_format_iso8601_basic(self): """Test ISO 8601 date formatting.""" dt = datetime(2024, 11, 18, 14, 30, 45) formatted = format_iso8601(dt) assert formatted == "2024-11-18T14:30:45Z" def test_format_iso8601_different_dates(self): """Test ISO 8601 formatting for different dates.""" dt1 = datetime(2024, 1, 1, 0, 0, 0) dt2 = datetime(2024, 12, 31, 23, 59, 59) assert format_iso8601(dt1) == "2024-01-01T00:00:00Z" assert format_iso8601(dt2) == "2024-12-31T23:59:59Z" def test_format_iso8601_single_digits(self): """Test ISO 8601 format pads single digits.""" dt = datetime(2024, 1, 5, 9, 8, 7) formatted = format_iso8601(dt) assert formatted == "2024-01-05T09:08:07Z" def test_parse_iso8601_basic(self): """Test ISO 8601 date parsing.""" dt = parse_iso8601("2024-11-18T14:30:45Z") assert dt.year == 2024 assert dt.month == 11 assert dt.day == 18 assert dt.hour == 14 assert dt.minute == 30 assert dt.second == 45 def test_parse_iso8601_without_z(self): """Test ISO 8601 parsing without Z suffix.""" dt = parse_iso8601("2024-11-18T14:30:45") assert dt.year == 2024 assert dt.month == 11 assert dt.day == 18 def test_parse_iso8601_roundtrip(self): """Test ISO 8601 format and parse roundtrip.""" original = datetime(2024, 11, 18, 14, 30, 45) formatted = format_iso8601(original) parsed = parse_iso8601(formatted) assert parsed == original def test_parse_iso8601_invalid_format(self): """Test ISO 8601 parsing rejects invalid format.""" with pytest.raises(ValueError): parse_iso8601("not-a-date") def test_parse_iso8601_invalid_date(self): """Test ISO 8601 parsing rejects invalid date values.""" with pytest.raises(ValueError): parse_iso8601("2024-13-01T00:00:00Z") # Invalid month def test_format_and_parse_consistency(self): """Test RFC-822 and ISO 8601 are both consistent.""" dt = datetime(2024, 11, 18, 14, 30, 45) # ISO 8601 roundtrip iso_formatted = format_iso8601(dt) iso_parsed = parse_iso8601(iso_formatted) assert iso_parsed == dt # RFC-822 format is consistent rfc_formatted = format_rfc822(dt) assert "2024" in rfc_formatted assert "14:30:45" in rfc_formatted