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