Coverage for tests/login/tests_login_handler.py: 100%
137 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-08 13:26 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-08 13:26 +0000
1import requests
3import responses
4from flask_testing import TestCase
5from pymacaroons import Macaroon
6from webapp.app import create_app
8from unittest.mock import patch, MagicMock
11class LoginHandlerTest(TestCase):
12 def setUp(self):
13 self.api_url = "https://dashboard.snapcraft.io/dev/api/acl/"
14 self.endpoint_url = "/login"
16 def create_app(self):
17 app = create_app(testing=True)
18 app.secret_key = "secret_key"
19 app.config["WTF_CSRF_METHODS"] = []
21 return app
23 def test_redirect_user_logged_in(self):
24 with self.client.session_transaction() as s:
25 s["publisher"] = "openid"
26 s["macaroon_exchanged"] = "macaroon_exchanged"
28 response = self.client.get(self.endpoint_url)
29 assert response.status_code == 302
30 self.assertEqual("http://localhost/", response.location)
32 def test_redirect_user_logged_in_next_url(self):
33 with self.client.session_transaction() as s:
34 s["publisher"] = "openid"
35 s["macaroon_exchanged"] = "macaroon_exchanged"
37 response = self.client.get(self.endpoint_url + "?next=/test")
38 assert response.status_code == 302
39 self.assertEqual("/test", response.location)
41 @responses.activate
42 def test_login_handler_redirect(self):
43 m = Macaroon()
44 m.add_third_party_caveat("login.ubuntu.com", "key", "id")
46 serialized_macaroon = m.serialize()
48 responses.add(
49 responses.Response(
50 method="POST",
51 url=self.api_url,
52 json={"macaroon": serialized_macaroon},
53 status=200,
54 )
55 )
57 response = self.client.get(self.endpoint_url)
59 assert len(responses.calls) == 1
60 assert response.status_code == 302
62 @responses.activate
63 def test_login_api_500(self):
64 responses.add(
65 responses.Response(method="POST", url=self.api_url, status=500)
66 )
68 response = self.client.get(self.endpoint_url)
70 assert len(responses.calls) == 1
71 assert response.status_code == 502
73 @responses.activate
74 def test_login_api_401(self):
75 responses.add(
76 responses.Response(method="POST", url=self.api_url, status=401)
77 )
79 response = self.client.get(self.endpoint_url)
81 assert len(responses.calls) == 1
82 assert response.status_code == 302
83 self.assertEqual("/logout", response.location)
85 @responses.activate
86 def test_login_connection_error(self):
87 responses.add(
88 responses.Response(
89 method="POST",
90 url=self.api_url,
91 body=requests.exceptions.ConnectionError(),
92 status=500,
93 )
94 )
96 response = self.client.get(self.endpoint_url)
98 assert response.status_code == 502
101class AfterLoginHandlerTest(TestCase):
102 def create_app(self):
103 app = create_app(testing=True)
105 # set up a fake route for testing the after_login function
106 # since it is decorated with @open_id.after_login
107 @app.route("/_test_after_login")
108 def _test_after_login():
109 from webapp.login.views import after_login
111 return after_login(self.mock_resp)
113 return app
115 # creates a mocked responses for the get_account function
116 # and the login response passed to after_login
117 def prepare_mock_response(
118 self, mock_get_account, email="test@test.com", groups=[]
119 ):
120 root = Macaroon(location="store", identifier="root", key="root-key")
121 root.add_third_party_caveat(
122 "login.ubuntu.com", "caveat-key", "caveat-id"
123 )
124 self.root_macaroon = root.serialize()
125 discharge = Macaroon(
126 location="login.ubuntu.com",
127 identifier="caveat-id",
128 key="caveat-key",
129 ).serialize()
131 self.mock_resp = MagicMock()
132 self.mock_resp.nickname = "test"
133 self.mock_resp.identity_url = "https://login.ubuntu.com/test"
134 self.mock_resp.fullname = "Test"
135 self.mock_resp.image = "test.png"
136 self.mock_resp.email = email
137 self.mock_resp.extensions = {
138 "macaroon": MagicMock(discharge=discharge),
139 "lp": MagicMock(is_member=groups),
140 }
142 mock_get_account.return_value = {
143 "username": self.mock_resp.nickname,
144 "displayname": self.mock_resp.fullname,
145 "email": email,
146 "stores": [],
147 }
149 @patch("webapp.login.views.ENVIRONMENT", "staging")
150 @patch("webapp.login.views.dashboard.get_stores", return_value=[])
151 @patch("webapp.login.views.logic.get_stores", return_value=[])
152 @patch(
153 "webapp.login.views.dashboard.get_validation_sets", return_value=None
154 )
155 @patch(
156 "webapp.login.views.publisher_gateway.exchange_dashboard_macaroons",
157 return_value="exchanged-macaroon",
158 )
159 @patch("webapp.login.views.dashboard.get_account")
160 def test_is_canonical_true_if_email_ends_with_canonical_on_staging(
161 self,
162 mock_get_account,
163 *_,
164 ):
165 # on test environments, we treat publisher's account as "canonical"
166 # if their email is (at)canonical email
167 self.prepare_mock_response(
168 mock_get_account, email="test@canonical.com", groups=[]
169 )
171 with self.client.session_transaction() as s:
172 s["macaroon_root"] = self.root_macaroon
173 self.client.get("/_test_after_login")
175 with self.client.session_transaction() as s:
176 publisher = s.get("publisher")
177 assert publisher is not None
178 assert publisher["is_canonical"] is True
179 assert s["macaroon_exchanged"] == "exchanged-macaroon"
180 assert "macaroon_root" not in s
181 assert "macaroon_discharge" not in s
183 @patch("webapp.login.views.ENVIRONMENT", "production")
184 @patch("webapp.login.views.dashboard.get_stores", return_value=[])
185 @patch("webapp.login.views.logic.get_stores", return_value=[])
186 @patch(
187 "webapp.login.views.dashboard.get_validation_sets", return_value=None
188 )
189 @patch(
190 "webapp.login.views.publisher_gateway.exchange_dashboard_macaroons",
191 return_value="exchanged-macaroon",
192 )
193 @patch("webapp.login.views.dashboard.get_account")
194 def test_is_canonical_true_if_member_of_team_on_production(
195 self,
196 mock_get_account,
197 *_,
198 ):
199 # on production, we treat publisher's account as "canonical"
200 # if they are a member of the canonical team
201 self.prepare_mock_response(mock_get_account, groups=["canonical"])
203 with self.client.session_transaction() as s:
204 s["macaroon_root"] = self.root_macaroon
205 self.client.get("/_test_after_login")
207 with self.client.session_transaction() as s:
208 publisher = s.get("publisher")
209 assert publisher is not None
210 assert publisher["is_canonical"] is True
212 @patch("webapp.login.views.ENVIRONMENT", "production")
213 @patch("webapp.login.views.dashboard.get_stores", return_value=[])
214 @patch("webapp.login.views.logic.get_stores", return_value=[])
215 @patch(
216 "webapp.login.views.dashboard.get_validation_sets", return_value=None
217 )
218 @patch(
219 "webapp.login.views.publisher_gateway.exchange_dashboard_macaroons",
220 return_value="exchanged-macaroon",
221 )
222 @patch("webapp.login.views.dashboard.get_account")
223 def test_is_canonical_false_if_not_member_of_team_on_production(
224 self,
225 mock_get_account,
226 *_,
227 ):
228 # on production, we treat publisher's account as "canonical"
229 # only if they are a member of the canonical team, not based on email
230 self.prepare_mock_response(
231 mock_get_account, email="test@canonical.com", groups=[]
232 )
234 with self.client.session_transaction() as s:
235 s["macaroon_root"] = self.root_macaroon
236 self.client.get("/_test_after_login")
238 with self.client.session_transaction() as s:
239 publisher = s.get("publisher")
240 assert publisher is not None
241 assert publisher["is_canonical"] is False
243 def test_after_login_exchanges_macaroons_and_clears_root_and_discharge(
244 self,
245 ):
246 self.prepare_mock_response(MagicMock(), groups=["canonical"])
248 with patch(
249 "webapp.login.views.dashboard.get_account",
250 return_value={
251 "username": "test",
252 "displayname": "Test",
253 "email": "test@test.com",
254 "stores": [],
255 },
256 ), patch(
257 "webapp.login.views.dashboard.get_validation_sets",
258 return_value=None,
259 ), patch(
260 "webapp.login.views.dashboard.get_stores", return_value=[]
261 ), patch(
262 "webapp.login.views.logic.get_stores", return_value=[]
263 ), patch(
264 "webapp.login.views.publisher_gateway"
265 ".exchange_dashboard_macaroons",
266 return_value="exchanged-macaroon",
267 ) as mock_exchange:
268 with self.client.session_transaction() as s:
269 s["macaroon_root"] = self.root_macaroon
271 response = self.client.get("/_test_after_login")
273 assert response.status_code == 302
274 with self.client.session_transaction() as s:
275 assert s["macaroon_exchanged"] == "exchanged-macaroon"
276 assert "macaroon_root" not in s
277 assert "macaroon_discharge" not in s
279 mock_exchange.assert_called_once()