@@ -25,3 +25,205 @@ def _logged_do_request(self, req, *args, **kwargs):
2525 return resp
2626
2727 webtest .TestApp .do_request = _logged_do_request
28+
29+
30+ import contextlib
31+ import random
32+ import time
33+ import uuid
34+
35+ import hawkauthlib
36+ import pytest
37+ import webtest
38+ from pyramid .authorization import ACLAuthorizationPolicy
39+ from pyramid .interfaces import IAuthenticationPolicy
40+ from pyramid .request import Request
41+ from webtest import TestApp
42+
43+ from tools .integration_tests .test_support import get_test_configurator
44+
45+
46+ def _retry_send (func , * args , ** kwargs ):
47+ """Call a webtest method, retrying once on 409/503."""
48+ try :
49+ return func (* args , ** kwargs )
50+ except webtest .AppError as ex :
51+ if "409 " not in ex .args [0 ] and "503 " not in ex .args [0 ]:
52+ raise
53+ time .sleep (0.01 )
54+ return func (* args , ** kwargs )
55+
56+
57+ def retry_post_json (app , * args , ** kwargs ):
58+ """POST JSON with retry on transient errors."""
59+ return _retry_send (app .post_json , * args , ** kwargs )
60+
61+
62+ def retry_put_json (app , * args , ** kwargs ):
63+ """PUT JSON with retry on transient errors."""
64+ return _retry_send (app .put_json , * args , ** kwargs )
65+
66+
67+ def retry_delete (app , * args , ** kwargs ):
68+ """DELETE with retry on transient errors."""
69+ return _retry_send (app .delete , * args , ** kwargs )
70+
71+
72+ def _make_auth_state (config , host_url ):
73+ """Generate hawk credentials for a new random user."""
74+ global_secret = os .environ .get ("SYNC_MASTER_SECRET" )
75+ policy = config .registry .getUtility (IAuthenticationPolicy )
76+ if global_secret is not None :
77+ policy .secrets ._secrets = [global_secret ]
78+ user_id = random .randint (1 , 100000 )
79+ fxa_uid = "DECAFBAD" + str (uuid .uuid4 ().hex )[8 :]
80+ hashed_fxa_uid = str (uuid .uuid4 ().hex )
81+ fxa_kid = "0000000000000-DECAFBAD" + str (uuid .uuid4 ().hex )[8 :]
82+ req = Request .blank (host_url )
83+ creds = policy .encode_hawk_id (
84+ req ,
85+ user_id ,
86+ extra = {
87+ "hashed_fxa_uid" : hashed_fxa_uid ,
88+ "fxa_uid" : fxa_uid ,
89+ "fxa_kid" : fxa_kid ,
90+ },
91+ )
92+ auth_token , auth_secret = creds
93+ return {
94+ "user_id" : user_id ,
95+ "fxa_uid" : fxa_uid ,
96+ "hashed_fxa_uid" : hashed_fxa_uid ,
97+ "fxa_kid" : fxa_kid ,
98+ "auth_token" : auth_token ,
99+ "auth_secret" : auth_secret ,
100+ }
101+
102+
103+ @pytest .fixture (scope = "function" )
104+ def st_ctx ():
105+ """Functional test context for storage API tests.
106+
107+ Sets up a Pyramid configurator, creates a TestApp with hawk signing,
108+ authenticates a random user, clears that user's data, and yields a
109+ context dict. Tears down configurator on exit.
110+ """
111+ ini_file = os .environ .get ("MOZSVC_TEST_INI_FILE" , "tests.ini" )
112+ os .environ ["MOZSVC_UUID" ] = str (uuid .uuid4 ())
113+ if "MOZSVC_SQLURI" not in os .environ :
114+ os .environ ["MOZSVC_SQLURI" ] = "sqlite:///:memory:"
115+ if "MOZSVC_ONDISK_SQLURI" not in os .environ :
116+ ondisk = os .environ ["MOZSVC_SQLURI" ]
117+ if ":memory:" in ondisk :
118+ ondisk = "sqlite:////tmp/tests-sync-%s.db" % os .environ ["MOZSVC_UUID" ]
119+ os .environ ["MOZSVC_ONDISK_SQLURI" ] = ondisk
120+
121+ # Locate tests.ini relative to test_storage.py
122+ this_dir = os .path .dirname (os .path .abspath (__file__ ))
123+ config = get_test_configurator (this_dir , ini_file )
124+ authz_policy = ACLAuthorizationPolicy ()
125+ config .set_authorization_policy (authz_policy )
126+ config .commit ()
127+ config .make_wsgi_app ()
128+
129+ host_url = os .environ .get ("SYNC_SERVER_URL" , "http://localhost:8000" )
130+ import urllib .parse as urlparse
131+
132+ host_parts = urlparse .urlparse (host_url )
133+ app = TestApp (
134+ host_url ,
135+ extra_environ = {
136+ "HTTP_HOST" : host_parts .netloc ,
137+ "wsgi.url_scheme" : host_parts .scheme or "http" ,
138+ "SERVER_NAME" : host_parts .hostname ,
139+ "REMOTE_ADDR" : "127.0.0.1" ,
140+ "SCRIPT_NAME" : host_parts .path ,
141+ },
142+ )
143+
144+ # Mutable auth state — shared with the do_request closure so that
145+ # switch_user() and the expired-token test can swap credentials at runtime.
146+ auth = _make_auth_state (config , host_url )
147+ auth_state = {
148+ "auth_token" : auth ["auth_token" ],
149+ "auth_secret" : auth ["auth_secret" ],
150+ }
151+
152+ orig_do_request = app .do_request
153+
154+ def new_do_request (req , * args , ** kwds ):
155+ hawkauthlib .sign_request (
156+ req , auth_state ["auth_token" ], auth_state ["auth_secret" ]
157+ )
158+ return orig_do_request (req , * args , ** kwds )
159+
160+ app .do_request = new_do_request
161+
162+ root = "/1.5/%d" % auth ["user_id" ]
163+ retry_delete (app , root )
164+
165+ ctx = {
166+ "app" : app ,
167+ "root" : root ,
168+ "user_id" : auth ["user_id" ],
169+ "fxa_uid" : auth ["fxa_uid" ],
170+ "hashed_fxa_uid" : auth ["hashed_fxa_uid" ],
171+ "fxa_kid" : auth ["fxa_kid" ],
172+ "auth_state" : auth_state ,
173+ "config" : config ,
174+ "host_url" : host_url ,
175+ }
176+
177+ yield ctx
178+
179+ config .end ()
180+ del os .environ ["MOZSVC_UUID" ]
181+
182+
183+ @contextlib .contextmanager
184+ def switch_user (st_ctx ):
185+ """Context manager: temporarily switch to a fresh random user.
186+
187+ Updates both st_ctx and the auth_state dict (shared with the
188+ do_request closure) for the duration of the block, then restores
189+ the original user on exit.
190+ """
191+ orig_root = st_ctx ["root" ]
192+ orig_user_id = st_ctx ["user_id" ]
193+ orig_fxa_uid = st_ctx ["fxa_uid" ]
194+ orig_hashed_fxa_uid = st_ctx ["hashed_fxa_uid" ]
195+ orig_fxa_kid = st_ctx ["fxa_kid" ]
196+ orig_auth_token = st_ctx ["auth_state" ]["auth_token" ]
197+ orig_auth_secret = st_ctx ["auth_state" ]["auth_secret" ]
198+
199+ config = st_ctx ["config" ]
200+ host_url = st_ctx ["host_url" ]
201+ app = st_ctx ["app" ]
202+
203+ for _ in range (10 ):
204+ new_auth = _make_auth_state (config , host_url )
205+ if new_auth ["user_id" ] != orig_user_id :
206+ break
207+ else :
208+ raise RuntimeError ("Failed to switch to new user id" )
209+
210+ st_ctx ["auth_state" ]["auth_token" ] = new_auth ["auth_token" ]
211+ st_ctx ["auth_state" ]["auth_secret" ] = new_auth ["auth_secret" ]
212+ st_ctx ["user_id" ] = new_auth ["user_id" ]
213+ st_ctx ["fxa_uid" ] = new_auth ["fxa_uid" ]
214+ st_ctx ["hashed_fxa_uid" ] = new_auth ["hashed_fxa_uid" ]
215+ st_ctx ["fxa_kid" ] = new_auth ["fxa_kid" ]
216+ new_root = "/1.5/%d" % new_auth ["user_id" ]
217+ st_ctx ["root" ] = new_root
218+ retry_delete (app , new_root )
219+
220+ try :
221+ yield
222+ finally :
223+ st_ctx ["auth_state" ]["auth_token" ] = orig_auth_token
224+ st_ctx ["auth_state" ]["auth_secret" ] = orig_auth_secret
225+ st_ctx ["user_id" ] = orig_user_id
226+ st_ctx ["fxa_uid" ] = orig_fxa_uid
227+ st_ctx ["hashed_fxa_uid" ] = orig_hashed_fxa_uid
228+ st_ctx ["fxa_kid" ] = orig_fxa_kid
229+ st_ctx ["root" ] = orig_root
0 commit comments