Coverage for tests/test_robots.py: 95%
80 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-10 22:08 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-10 22:08 +0000
1import os
2import re
3import unittest
5from protego import Protego
7from webapp.app import create_app
9ROBOTS_PATH = os.path.join(os.path.dirname(__file__), "..", "robots.txt")
10BASE_URL = "https://snapcraft.io"
13def parse_groups(text):
14 groups = {}
15 pending = []
16 in_rules = False
18 for raw_line in text.splitlines():
19 line = raw_line.split("#", 1)[0].strip()
21 if not line or ":" not in line:
22 continue
24 key, value = (part.strip() for part in line.split(":", 1))
25 key = key.lower()
27 if key == "user-agent":
28 if in_rules:
29 pending = []
30 in_rules = False
31 pending.append(value.lower())
32 groups.setdefault(value.lower(), [])
33 elif key in ("allow", "disallow"):
34 in_rules = True
35 for agent in pending:
36 groups[agent].append((key, value))
38 return groups
41def is_login_gated(view):
42 seen = set()
44 while view is not None and id(view) not in seen:
45 seen.add(id(view))
46 code = getattr(view, "__code__", None)
47 if code is not None and code.co_name == "is_user_logged_in":
48 return True
49 view = getattr(view, "__wrapped__", None)
51 return False
54def sample_path(rule):
55 path = re.sub(r"<any\([^)]*\):[^>]+>", "snaps", rule)
56 path = re.sub(r"<regex\([^)]*\):[^>]+>", "firefox", path)
57 path = re.sub(r"<path:[^>]+>", "x/y", path)
58 path = re.sub(r"<[^>]*snap_name>", "firefox", path)
60 return re.sub(r"<[^>]+>", "x", path)
63class TestRobots(unittest.TestCase):
64 def setUp(self):
65 with open(ROBOTS_PATH) as robots_file:
66 robots = robots_file.read()
68 self.robots = Protego.parse(robots)
69 self.groups = parse_groups(robots)
70 self.app = create_app(testing=True)
72 def can_fetch(self, agent, path):
73 return self.robots.can_fetch(BASE_URL + path, agent)
75 def find_crawlable(self, paths):
76 return [
77 (agent, path)
78 for agent in self.groups
79 for path in paths
80 if self.can_fetch(agent, path)
81 ]
83 def find_blocked(self, paths):
84 return [
85 (agent, path)
86 for agent in self.groups
87 for path in paths
88 if not self.can_fetch(agent, path)
89 ]
91 def test_robots_txt_is_served_and_not_empty(self):
92 response = self.app.test_client().get("/robots.txt")
94 self.assertEqual(response.status_code, 200)
95 self.assertIn(b"User-Agent", response.data)
97 def test_login_gated_routes_are_disallowed_for_every_agent(self):
98 gated = sorted(
99 {
100 sample_path(rule.rule)
101 for rule in self.app.url_map.iter_rules()
102 if is_login_gated(self.app.view_functions.get(rule.endpoint))
103 }
104 )
106 self.assertTrue(gated, "no login-gated routes found")
108 leaks = self.find_crawlable(gated)
110 self.assertEqual(leaks, [], f"login-gated routes crawlable: {leaks}")
112 def test_public_content_is_crawlable_for_every_agent(self):
113 public_paths = [
114 "/",
115 "/about",
116 "/about/publish",
117 "/about/listing",
118 "/about/release",
119 "/about/publicise",
120 "/store",
121 "/store/categories/games",
122 "/blog/",
123 "/docs/",
124 "/build",
125 "/iot",
126 "/tutorials",
127 "/firefox",
128 "/publisher/canonical",
129 ]
131 blocked = self.find_blocked(public_paths)
133 self.assertEqual(
134 blocked, [], f"public content is not crawlable: {blocked}"
135 )
137 def test_snaps_named_after_reserved_paths_are_crawlable(self):
138 snap_paths = [
139 "/accountable2you",
140 "/administrative-assistant",
141 "/searchsploit",
142 "/search-helper-tool",
143 "/build-and-measure",
144 "/iot-manager",
145 "/iotconnect",
146 "/publisher-subscriber",
147 "/store-admin",
148 "/api-mocker-gateway",
149 "/apilume",
150 ]
152 blocked = self.find_blocked(snap_paths)
154 self.assertEqual(
155 blocked, [], f"snap pages blocked by an unanchored rule: {blocked}"
156 )
158 def test_wildcard_rules_apply_to_every_named_agent(self):
159 wildcard = set(self.groups["*"])
161 for agent, rules in self.groups.items():
162 if agent == "*":
163 continue
165 missing = sorted(wildcard - set(rules))
167 self.assertEqual(
168 missing, [], f"{agent} is missing rules from '*': {missing}"
169 )