Coverage for tests/test_snippets.py: 100%
44 statements
« prev ^ index » next coverage.py v7.10.3, created at 2026-09-13 18:40 +0000
« prev ^ index » next coverage.py v7.10.3, created at 2026-09-13 18:40 +0000
1import typing
3import hypothesis
4import hypothesis.strategies as st
5import pytest
7from meta_tags_parser import parse_snippets_from_source, structs
10SNIPPET_PAGE_FIXTURE: typing.Final = """
11<html><head>
12<meta property="og:title" content="Meta Tags — Preview, Edit and Generate">
13<meta property="og:description" content="Edit and experiment with your content.">
14<meta property="og:url" content="https://metatags.io/">
15<meta property="og:image" content="https://metatags.io/assets/preview.png">
16<meta property="og:image:width" content="1200">
17<meta property="og:image:height" content="630">
18<meta name="twitter:card" content="summary_large_image">
19<meta name="twitter:title" content="Privet, kak dela to vcelom?">
20<meta name="twitter:description" content="Twitter flavoured description.">
21<meta name="twitter:url" content="https://metatags.io/">
22<meta name="twitter:image" content="https://metatags.io/assets/hm-fail.png">
23<meta name="twitter:image:width" content="800">
24<meta name="twitter:image:height" content="418">
25</head></html>
26"""
27EXPECTED_SNIPPET_GROUP: typing.Final = structs.SnippetGroup(
28 open_graph=structs.SocialMediaSnippet(
29 title="Meta Tags — Preview, Edit and Generate",
30 description="Edit and experiment with your content.",
31 image="https://metatags.io/assets/preview.png",
32 image_width=1200,
33 image_height=630,
34 url="https://metatags.io/",
35 ),
36 twitter=structs.SocialMediaSnippet(
37 title="Privet, kak dela to vcelom?",
38 description="Twitter flavoured description.",
39 image="https://metatags.io/assets/hm-fail.png",
40 image_width=800,
41 image_height=418,
42 url="https://metatags.io/",
43 ),
44)
45REPEATED_PARSE_COUNT: typing.Final = 3
46MAX_GENERATED_DIMENSION: typing.Final = 100_000
47UNICODE_DIGITS: typing.Final = st.characters(whitelist_categories=["Nd"])
48SAFE_ATTRIBUTE_ALPHABET: typing.Final = st.characters(blacklist_categories=("Cc", "Cs"), blacklist_characters='<>"&')
51@pytest.mark.parametrize("_repeat_number", range(REPEATED_PARSE_COUNT))
52def test_snippets_are_stable_across_repeated_parsing(_repeat_number: int) -> None:
53 assert parse_snippets_from_source(SNIPPET_PAGE_FIXTURE) == EXPECTED_SNIPPET_GROUP
56def test_snippets_are_empty_without_social_tags() -> None:
57 snippet_result: typing.Final[structs.SnippetGroup] = parse_snippets_from_source(
58 '<meta name="twitter:title" content="Hi, whatsup kekeke">'
59 )
61 assert snippet_result.twitter.title == "Hi, whatsup kekeke"
62 assert snippet_result.twitter.image_width == 0
63 assert snippet_result.open_graph == structs.SocialMediaSnippet()
66def test_first_duplicated_tag_wins() -> None:
67 """Social networks treat repeated tags as a list where the first entry is the primary one."""
68 snippet_result: typing.Final[structs.SnippetGroup] = parse_snippets_from_source(
69 '<meta property="og:title" content="First title">'
70 '<meta property="og:title" content="Second title">'
71 '<meta property="og:image" content="https://example.com/first.png">'
72 '<meta property="og:image:width" content="640">'
73 '<meta property="og:image" content="https://example.com/second.png">'
74 '<meta property="og:image:width" content="1280">'
75 )
77 assert snippet_result.open_graph.title == "First title"
78 assert snippet_result.open_graph.image == "https://example.com/first.png"
79 assert snippet_result.open_graph.image_width == 640
82def test_unknown_social_tags_do_not_reach_the_snippet() -> None:
83 snippet_result: typing.Final[structs.SnippetGroup] = parse_snippets_from_source(
84 '<meta property="og:video:duration" content="754"><meta property="og:audio" content="x.mp3">'
85 )
87 assert snippet_result.open_graph == structs.SocialMediaSnippet()
90def test_snippets_accept_bytes() -> None:
91 snippet_result: typing.Final[structs.SnippetGroup] = parse_snippets_from_source(
92 '<html><head><meta charset="windows-1251"><meta property="og:title" content="Заголовок"></head></html>'.encode(
93 "windows-1251"
94 )
95 )
97 assert snippet_result.open_graph.title == "Заголовок"
100@pytest.mark.parametrize(
101 ("dimension_text", "expected_width"),
102 [
103 ("", 0),
104 (" ", 0),
105 ("123", 123),
106 (" 55 ", 55),
107 ("0", 0),
108 ("abc", 0),
109 ("12.5", 0),
110 ("-12", 0),
111 ("1200px", 0),
112 ("١٢٣", 0),
113 ("٥", 0),
114 ("²", 0),
115 ],
116)
117def test_image_width_examples(dimension_text: str, expected_width: int) -> None:
118 snippet_result: typing.Final[structs.SnippetGroup] = parse_snippets_from_source(
119 f'<meta property="twitter:image:width" content="{dimension_text}">'
120 )
122 assert snippet_result.twitter.image_width == expected_width
125@hypothesis.given(
126 dimension_text=st.one_of(
127 st.integers(min_value=0, max_value=MAX_GENERATED_DIMENSION).map(str),
128 st.text(alphabet=UNICODE_DIGITS, min_size=1, max_size=8),
129 st.text(alphabet=SAFE_ATTRIBUTE_ALPHABET, max_size=8),
130 )
131)
132def test_image_dimensions_follow_the_ascii_digit_rule(dimension_text: str) -> None:
133 """Anything that is not a plain ascii number (unicode digits, units, decimals) becomes a zero."""
134 cleaned_text: typing.Final[str] = dimension_text.strip()
136 snippet_result: typing.Final[structs.SnippetGroup] = parse_snippets_from_source(
137 f'<meta property="og:image:width" content="{dimension_text}">'
138 )
140 assert snippet_result.open_graph.image_width == (
141 int(cleaned_text) if cleaned_text.isascii() and cleaned_text.isdigit() else 0
142 )
145@hypothesis.settings(max_examples=50)
146@hypothesis.given(snippet_title=st.text(alphabet=SAFE_ATTRIBUTE_ALPHABET, max_size=80))
147def test_any_title_survives_the_round_trip(snippet_title: str) -> None:
148 snippet_result: typing.Final[structs.SnippetGroup] = parse_snippets_from_source(
149 f'<meta property="og:title" content="{snippet_title}">'
150 )
152 assert snippet_result.open_graph.title == snippet_title