Skip to content

Commit 6476f5b

Browse files
authored
test: test_storage test_support pytest refactor (#2239)
* core integration test conftest * added test storage functional tests * remove extraneous conf * rmv dead wright TestStorage from test_storage * fmt * rmv test_support
1 parent cd7847d commit 6476f5b

3 files changed

Lines changed: 2404 additions & 2635 deletions

File tree

tools/integration_tests/conftest.py

Lines changed: 198 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11
"""Pytest configuration and fixtures for integration tests."""
22

3-
import os
3+
import contextlib
44
import 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
720
SYNC_SERVER_STARTUP_MAX_ATTEMPTS = 35
@@ -10,8 +23,6 @@
1023
logger = logging.getLogger("tools.integration-tests")
1124

1225
if 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

Comments
 (0)