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

1from unittest.mock import patch 

2from requests.exceptions import HTTPError 

3from tests.endpoints.endpoint_testing import TestEndpoints 

4 

5 

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}" 

12 

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 

23 

24 response = self.client.get(self.endpoint_url) 

25 

26 # Assert response 

27 self.assertEqual(response.status_code, 200) 

28 self.assertIn(b"text/html", response.content_type.encode()) 

29 

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) 

34 

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() 

40 

41 response = client.get(self.endpoint_url) 

42 

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]) 

46 

47 

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" 

53 

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 } 

62 

63 # Mock launchpad methods 

64 mock_launchpad.is_snap_building.return_value = False 

65 mock_launchpad.build_snap.return_value = "build-12345" 

66 

67 response = self.client.post(self.endpoint_url) 

68 

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") 

74 

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) 

79 

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" 

91 

92 response = self.client.post(self.endpoint_url) 

93 

94 self.assertEqual(response.status_code, 200) 

95 self.assertTrue(response.get_json()["success"]) 

96 

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) 

102 

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" 

116 

117 response = self.client.post(self.endpoint_url) 

118 

119 self.assertEqual(response.status_code, 200) 

120 self.assertTrue(response.get_json()["success"]) 

121 

122 mock_dashboard.get_package_upload_macaroon.assert_not_called() 

123 mock_launchpad.complete_snap_authorization.assert_not_called() 

124 

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 } 

135 

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" 

139 

140 response = self.client.post(self.endpoint_url) 

141 

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") 

147 

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) 

154 

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 = {} 

160 

161 response = self.client.post(self.endpoint_url) 

162 

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 ) 

171 

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 

177 

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 } 

182 

183 # Mock launchpad methods 

184 mock_launchpad.is_snap_building.return_value = False 

185 

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 

193 

194 response = self.client.post(self.endpoint_url) 

195 

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) 

205 

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() 

211 

212 response = client.post(self.endpoint_url) 

213 

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]) 

217 

218 

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/" 

224 

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() 

230 

231 response = client.post(self.endpoint_url) 

232 

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])