Coverage for tests/endpoints/publisher/tests_builds.py: 100%
126 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-31 22:23 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-31 22:23 +0000
1from unittest.mock import patch
2from requests.exceptions import HTTPError
3from tests.endpoints.endpoint_testing import TestEndpoints
6class TestGetSnapBuildPage(TestEndpoints):
7 def setUp(self):
8 super().setUp()
9 self.snap_name = "test-snap"
10 self.build_id = "12345"
11 self.endpoint_url = f"/{self.snap_name}/builds/{self.build_id}"
13 @patch("webapp.endpoints.publisher.builds.dashboard")
14 def test_get_snap_build_page_success(self, mock_dashboard):
15 """Test successful rendering of snap build page"""
16 # Mock snap info response
17 mock_snap_info = {
18 "snap_name": self.snap_name,
19 "title": "Test Snap",
20 "snap_id": "test-snap-id-123",
21 }
22 mock_dashboard.get_snap_info.return_value = mock_snap_info
24 response = self.client.get(self.endpoint_url)
26 # Assert response
27 self.assertEqual(response.status_code, 200)
28 self.assertIn(b"text/html", response.content_type.encode())
30 # Verify dashboard method was called with correct session and snap name
31 mock_dashboard.get_snap_info.assert_called_once()
32 call_args = mock_dashboard.get_snap_info.call_args
33 self.assertEqual(call_args[0][1], self.snap_name)
35 def test_get_snap_build_page_requires_login(self):
36 """Test that the endpoint requires login"""
37 # Create a new client without logging in
38 app = self.app
39 client = app.test_client()
41 response = client.get(self.endpoint_url)
43 # Should redirect to login or return unauthorized
44 # The exact behavior depends on the login_required decorator
45 self.assertIn(response.status_code, [302, 401, 403])
48class TestPostBuild(TestEndpoints):
49 def setUp(self):
50 super().setUp()
51 self.snap_name = "test-snap"
52 self.endpoint_url = f"/api/{self.snap_name}/builds/trigger-build"
54 @patch("webapp.endpoints.publisher.builds.launchpad")
55 @patch("webapp.endpoints.publisher.builds.dashboard")
56 def test_post_build_success(self, mock_dashboard, mock_launchpad):
57 """Test successful build trigger"""
58 # Mock account snaps to include our test snap
59 mock_dashboard.get_account_snaps.return_value = {
60 self.snap_name: {"snap_name": self.snap_name}
61 }
63 # Mock launchpad methods
64 mock_launchpad.is_snap_building.return_value = False
65 mock_launchpad.build_snap.return_value = "build-12345"
67 response = self.client.post(self.endpoint_url)
69 # Assert response
70 self.assertEqual(response.status_code, 200)
71 response_data = response.get_json()
72 self.assertTrue(response_data["success"])
73 self.assertEqual(response_data["build_id"], "build-12345")
75 # Verify method calls
76 mock_dashboard.get_account_snaps.assert_called_once()
77 mock_launchpad.is_snap_building.assert_called_once_with(self.snap_name)
78 mock_launchpad.build_snap.assert_called_once_with(self.snap_name)
80 @patch("webapp.endpoints.publisher.builds.launchpad")
81 @patch("webapp.endpoints.publisher.builds.dashboard")
82 def test_post_build_does_not_reauthorize_before_triggering(
83 self, mock_dashboard, mock_launchpad
84 ):
85 """Triggering a build should use existing Launchpad auth."""
86 mock_dashboard.get_account_snaps.return_value = {
87 self.snap_name: {"snap_name": self.snap_name}
88 }
89 mock_launchpad.is_snap_building.return_value = False
90 mock_launchpad.build_snap.return_value = "build-12345"
92 response = self.client.post(self.endpoint_url)
94 self.assertEqual(response.status_code, 200)
95 self.assertTrue(response.get_json()["success"])
97 mock_dashboard.get_package_upload_macaroon.assert_not_called()
98 mock_launchpad.get_snap_by_store_name.assert_not_called()
99 mock_launchpad.complete_snap_authorization.assert_not_called()
100 mock_launchpad.is_snap_building.assert_called_once_with(self.snap_name)
101 mock_launchpad.build_snap.assert_called_once_with(self.snap_name)
103 @patch("webapp.endpoints.publisher.builds.launchpad")
104 @patch("webapp.endpoints.publisher.builds.dashboard")
105 def test_post_build_skips_reauthorization_when_snap_not_in_launchpad(
106 self, mock_dashboard, mock_launchpad
107 ):
108 """If the snap isn't linked in Launchpad yet, there's nothing
109 to reauthorize, so the reauthorization calls are skipped."""
110 mock_dashboard.get_account_snaps.return_value = {
111 self.snap_name: {"snap_name": self.snap_name}
112 }
113 mock_launchpad.get_snap_by_store_name.return_value = None
114 mock_launchpad.is_snap_building.return_value = False
115 mock_launchpad.build_snap.return_value = "build-12345"
117 response = self.client.post(self.endpoint_url)
119 self.assertEqual(response.status_code, 200)
120 self.assertTrue(response.get_json()["success"])
122 mock_dashboard.get_package_upload_macaroon.assert_not_called()
123 mock_launchpad.complete_snap_authorization.assert_not_called()
125 @patch("webapp.endpoints.publisher.builds.launchpad")
126 @patch("webapp.endpoints.publisher.builds.dashboard")
127 def test_post_build_cancels_existing_build(
128 self, mock_dashboard, mock_launchpad
129 ):
130 """Test that existing builds are cancelled before starting new one"""
131 # Mock account snaps to include our test snap
132 mock_dashboard.get_account_snaps.return_value = {
133 self.snap_name: {"snap_name": self.snap_name}
134 }
136 # Mock launchpad methods - existing build is running
137 mock_launchpad.is_snap_building.return_value = True
138 mock_launchpad.build_snap.return_value = "build-12345"
140 response = self.client.post(self.endpoint_url)
142 # Assert response
143 self.assertEqual(response.status_code, 200)
144 response_data = response.get_json()
145 self.assertTrue(response_data["success"])
146 self.assertEqual(response_data["build_id"], "build-12345")
148 # Verify existing build was cancelled
149 mock_launchpad.is_snap_building.assert_called_once_with(self.snap_name)
150 mock_launchpad.cancel_snap_builds.assert_called_once_with(
151 self.snap_name
152 )
153 mock_launchpad.build_snap.assert_called_once_with(self.snap_name)
155 @patch("webapp.endpoints.publisher.builds.dashboard")
156 def test_post_build_forbidden_non_contributor(self, mock_dashboard):
157 """Test that non-contributors cannot trigger builds"""
158 # Mock account snaps to NOT include our test snap
159 mock_dashboard.get_account_snaps.return_value = {}
161 response = self.client.post(self.endpoint_url)
163 # Assert response
164 self.assertEqual(response.status_code, 200)
165 response_data = response.get_json()
166 self.assertFalse(response_data["success"])
167 self.assertEqual(response_data["error"]["type"], "FORBIDDEN")
168 self.assertIn(
169 "not allowed to request builds", response_data["error"]["message"]
170 )
172 @patch("webapp.endpoints.publisher.builds.launchpad")
173 @patch("webapp.endpoints.publisher.builds.dashboard")
174 def test_post_build_http_error(self, mock_dashboard, mock_launchpad):
175 """Test handling of HTTP errors from Launchpad"""
176 from unittest.mock import Mock
178 # Mock account snaps to include our test snap
179 mock_dashboard.get_account_snaps.return_value = {
180 self.snap_name: {"snap_name": self.snap_name}
181 }
183 # Mock launchpad methods
184 mock_launchpad.is_snap_building.return_value = False
186 # Create mock HTTP error
187 mock_response = Mock()
188 mock_response.text = "Launchpad error message"
189 mock_response.status_code = 500
190 http_error = HTTPError()
191 http_error.response = mock_response
192 mock_launchpad.build_snap.side_effect = http_error
194 response = self.client.post(self.endpoint_url)
196 # Assert response
197 self.assertEqual(response.status_code, 200)
198 response_data = response.get_json()
199 self.assertFalse(response_data["success"])
200 self.assertIn(
201 "error happened building", response_data["error"]["message"]
202 )
203 self.assertEqual(response_data["details"], "Launchpad error message")
204 self.assertEqual(response_data["status_code"], 500)
206 def test_post_build_requires_login(self):
207 """Test that the endpoint requires login"""
208 # Create a new client without logging in
209 app = self.app
210 client = app.test_client()
212 response = client.post(self.endpoint_url)
214 # Should redirect to login or return unauthorized
215 # The exact behavior depends on the login_required decorator
216 self.assertIn(response.status_code, [302, 401, 403])
219class TestPostDisconnectRepo(TestEndpoints):
220 def setUp(self):
221 super().setUp()
222 self.snap_name = "test-snap"
223 self.endpoint_url = f"/api/{self.snap_name}/builds/disconnect/"
225 def test_post_disconnect_repo_requires_login(self):
226 """Test that the endpoint requires login"""
227 # Create a new client without logging in
228 app = self.app
229 client = app.test_client()
231 response = client.post(self.endpoint_url)
233 # Should redirect to login or return unauthorized
234 # The exact behavior depends on the login_required decorator
235 self.assertIn(response.status_code, [302, 401, 403])