Coverage for tests/test_gate_unreleased_snap_pages.py: 100%
45 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-25 22:09 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-25 22:09 +0000
1from unittest.mock import patch
3from tests.endpoints.endpoint_testing import TestEndpoints
6class TestGateUnreleasedSnapPages(TestEndpoints):
7 """
8 The blueprint-level gate blocks state-changing requests to per-<snap_name>
9 routes when the snap has no published revisions. Read requests, skip-listed
10 endpoints, and dashboard errors must pass through.
11 """
13 snap_name = "test-snap"
15 def _expect_release_history(self, mock_dashboard, revisions):
16 mock_dashboard.snap_release_history.return_value = {
17 "revisions": revisions,
18 }
20 @patch("webapp.decorators._dashboard")
21 def test_post_blocked_for_snap_with_no_revisions(self, mock_dashboard):
22 self._expect_release_history(mock_dashboard, revisions=[])
24 response = self.client.post(f"/api/{self.snap_name}/listing")
26 self.assertEqual(403, response.status_code)
27 body = response.get_json()
28 self.assertFalse(body["success"])
29 self.assertEqual("no-releases", body["errors"][0]["code"])
31 @patch("webapp.decorators._dashboard")
32 def test_post_passes_through_for_snap_with_revisions(self, mock_dashboard):
33 self._expect_release_history(
34 mock_dashboard, revisions=[{"revision": 1}]
35 )
37 response = self.client.post(f"/api/{self.snap_name}/listing")
39 # Gate is out of the way; the downstream handler runs. We don't assert
40 # the exact downstream response, only that it isn't the gate's 403.
41 self.assertNotEqual(403, response.status_code)
43 @patch("webapp.decorators._dashboard")
44 def test_get_is_not_gated(self, mock_dashboard):
45 # snap_release_history should not even be consulted for a GET request.
46 mock_dashboard.snap_release_history.side_effect = AssertionError(
47 "GET should bypass the gate"
48 )
50 # We don't care about the eventual status — only that the gate did not
51 # short-circuit with 403 and did not call snap_release_history.
52 self.client.get(f"/api/{self.snap_name}/listing")
53 mock_dashboard.snap_release_history.assert_not_called()
55 @patch("webapp.decorators._dashboard")
56 def test_delete_package_endpoint_is_skip_listed(self, mock_dashboard):
57 # delete_package must remain reachable so users can unregister names
58 # that have never had a release.
59 self._expect_release_history(mock_dashboard, revisions=[])
60 with self.client.session_transaction() as session:
61 session["publisher"][
62 "identity_url"
63 ] = "https://login.ubuntu.com/test-user"
64 session.modified = True
66 response = self.client.delete(f"/packages/{self.snap_name}")
68 self.assertEqual(202, response.status_code)
69 mock_dashboard.snap_release_history.assert_not_called()
71 @patch("webapp.decorators._dashboard")
72 def test_dashboard_error_does_not_block_request(self, mock_dashboard):
73 mock_dashboard.snap_release_history.side_effect = Exception("boom")
75 response = self.client.post(f"/api/{self.snap_name}/listing")
77 # On dashboard failure the gate passes through; do not return 403.
78 self.assertNotEqual(403, response.status_code)
80 @patch("webapp.decorators._dashboard")
81 def test_unauthenticated_request_is_not_gated(self, mock_dashboard):
82 unauth_client = self.app.test_client()
83 mock_dashboard.snap_release_history.side_effect = AssertionError(
84 "Unauthenticated requests should bypass the gate"
85 )
87 response = unauth_client.post(f"/api/{self.snap_name}/listing")
89 # login_required redirects to /login; the gate should not be the one
90 # short-circuiting the request.
91 self.assertNotEqual(403, response.status_code)
92 mock_dashboard.snap_release_history.assert_not_called()