Skip to content

Commit f05743a

Browse files
committedJun 19, 2017
Use a data factory for Phabricator mock responses
Using carefully-crafted test data fixtures can lead to brittle tests and difficult-to-construct test scenarios in our integration tests. Switch the test suite to use a data factory instead of static fixtures to mock Phabricator responses.
1 parent 3a0c822 commit f05743a

File tree

8 files changed

+291
-132
lines changed

8 files changed

+291
-132
lines changed
 

‎landoapi/utils.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this
3+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
6+
def extract_rawdiff_id_from_uri(uri):
7+
"""Extract a raw diff ID from a Diff uri."""
8+
# The raw diff is part of a URI, such as
9+
# "https://secure.phabricator.com/differential/diff/43480/".
10+
parts = uri.rsplit('/', 4)
11+
12+
# Check that the URI Path is something we understand. Fail if the
13+
# URI path changed (signalling that the raw diff part of the URI may
14+
# be in a different segment of the URI string).
15+
if parts[1:-2] != ['differential', 'diff']:
16+
raise RuntimeError(
17+
"Phabricator Raw Diff URI parsing error: The "
18+
"URI {} is not in a format we "
19+
"understand!".format(uri)
20+
)
21+
22+
# Take the second-last member because of the trailing slash on the URL.
23+
return int(parts[-2])

‎tests/canned_responses/phabricator/revisions.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
"error_info": None,
7979
}
8080

81-
CANNED_REVISION_EMPTY = {
81+
CANNED_EMPTY_RESULT = {
8282
"result": [],
8383
"error_code": None,
8484
"error_info": None

‎tests/conftest.py

+36
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,48 @@
11
# This Source Code Form is subject to the terms of the Mozilla Public
22
# License, v. 2.0. If a copy of the MPL was not distributed with this
33
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
import json
45

56
import pytest
7+
import requests_mock
8+
9+
from landoapi.app import create_app
10+
from tests.factories import PhabResponseFactory
611

712

813
@pytest.fixture
914
def docker_env_vars(monkeypatch):
1015
"""Monkeypatch environment variables that we'd get running under docker."""
1116
monkeypatch.setenv('PHABRICATOR_URL', 'http://phabricator.test')
1217
monkeypatch.setenv('TRANSPLANT_URL', 'http://autoland.test')
18+
19+
20+
@pytest.fixture
21+
def phabfactory():
22+
"""Mock the Phabricator service and build fake response objects."""
23+
with requests_mock.mock() as m:
24+
yield PhabResponseFactory(m)
25+
26+
27+
@pytest.fixture
28+
def versionfile(tmpdir):
29+
"""Provide a temporary version.json on disk."""
30+
v = tmpdir.mkdir('app').join('version.json')
31+
v.write(
32+
json.dumps(
33+
{
34+
'source': 'https://github.com/mozilla-conduit/lando-api',
35+
'version': '0.0.0',
36+
'commit': '',
37+
'build': 'test',
38+
}
39+
)
40+
)
41+
return v
42+
43+
44+
@pytest.fixture
45+
def app(versionfile):
46+
"""Needed for pytest-flask."""
47+
app = create_app(versionfile.strpath)
48+
return app.app

‎tests/factories.py

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this
3+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
"""
5+
Data factories for writing integration tests.
6+
"""
7+
from copy import deepcopy
8+
9+
from landoapi.utils import extract_rawdiff_id_from_uri
10+
from tests.canned_responses.phabricator.repos import CANNED_REPO_MOZCENTRAL
11+
from tests.canned_responses.phabricator.revisions import CANNED_EMPTY_RESULT, \
12+
CANNED_REVISION_1, CANNED_REVISION_1_DIFF, CANNED_REVISION_1_RAW_DIFF
13+
from tests.canned_responses.phabricator.users import CANNED_USER_1
14+
from tests.utils import phab_url, first_result_in_response, phid_for_response, \
15+
form_matcher
16+
17+
18+
class PhabResponseFactory:
19+
"""Mock Phabricator service responses with generated data."""
20+
21+
def __init__(self, requestmocker):
22+
"""
23+
Args:
24+
requestmocker: A requests_mock.mock() object.
25+
"""
26+
self.mock = requestmocker
27+
self.mock_responses = {}
28+
self.install_404_responses()
29+
30+
def install_404_responses(self):
31+
"""Install catch-all 404 response handlers for API queries."""
32+
query_urls = ['differential.query', 'phid.query', 'user.query']
33+
for query_url in query_urls:
34+
self.mock.get(
35+
phab_url(query_url), status_code=404, json=CANNED_EMPTY_RESULT
36+
)
37+
38+
def user(self):
39+
"""Return a Phabricator User."""
40+
user = deepcopy(CANNED_USER_1)
41+
self.mock.get(phab_url('user.query'), status_code=200, json=user)
42+
return user
43+
44+
def revision(self, **kwargs):
45+
"""Return a Phabricator Revision."""
46+
result_json = deepcopy(CANNED_REVISION_1)
47+
revision = first_result_in_response(result_json)
48+
49+
if 'id' in kwargs:
50+
# Convert 'D000' form to just '000'.
51+
str_id = kwargs['id']
52+
num_id = str_id[1:]
53+
revision['id'] = num_id
54+
revision['phid'] = "PHID-DREV-%s" % num_id
55+
56+
if 'depends_on' in kwargs:
57+
parent_revision_response_data = kwargs['depends_on']
58+
if parent_revision_response_data:
59+
# This Revisions depends on another Revision.
60+
new_value = [phid_for_response(parent_revision_response_data)]
61+
else:
62+
# The user passed in None or an empty list, saying "this
63+
# revision has no parent revisions."
64+
new_value = []
65+
revision['auxiliary']['phabricator:depends-on'] = new_value
66+
67+
# Revisions have at least one Diff.
68+
diff = self.diff()
69+
diffID = extract_rawdiff_id_from_uri(
70+
first_result_in_response(diff)['uri']
71+
)
72+
rawdiff = self.rawdiff(diffID=str(diffID))
73+
revision['activeDiffPHID'] = phid_for_response(diff)
74+
75+
# Revisions may have a Repo.
76+
repo = self.repo()
77+
revision['repositoryPHID'] = phid_for_response(repo)
78+
79+
def match_revision(request):
80+
# Revisions can be looked up by PHID or ID.
81+
found_phid = form_matcher('phids[]', revision['phid'])(request)
82+
found_id = form_matcher('ids[]', revision['id'])(request)
83+
return found_phid or found_id
84+
85+
self.mock.get(
86+
phab_url('differential.query'),
87+
status_code=200,
88+
json=result_json,
89+
additional_matcher=match_revision
90+
)
91+
92+
# Revisions can also be looked up by phid.query.
93+
self.phid(result_json)
94+
95+
return result_json
96+
97+
def diff(self):
98+
"""Return a Revision Diff."""
99+
diff = deepcopy(CANNED_REVISION_1_DIFF)
100+
self.phid(diff)
101+
return diff
102+
103+
def rawdiff(self, diffID='12345'):
104+
"""Return raw diff text for a Revision Diff."""
105+
rawdiff = deepcopy(CANNED_REVISION_1_RAW_DIFF)
106+
self.mock.get(
107+
phab_url('differential.getrawdiff'),
108+
status_code=200,
109+
json=rawdiff,
110+
additional_matcher=form_matcher('diffID', diffID)
111+
)
112+
return rawdiff
113+
114+
def repo(self):
115+
"""Return a Phabricator Repo."""
116+
repo = deepcopy(CANNED_REPO_MOZCENTRAL)
117+
self.phid(repo)
118+
return repo
119+
120+
def phid(self, response_data):
121+
"""Add a phid.query matcher for the given Phabricator response object.
122+
"""
123+
phid = phid_for_response(response_data)
124+
self.mock.get(
125+
phab_url('phid.query'),
126+
status_code=200,
127+
additional_matcher=form_matcher('phids[]', phid),
128+
json=response_data
129+
)

‎tests/test_dockerflow.py

-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44

55
import json
66

7-
from tests.utils import versionfile, app
8-
97

108
def test_dockerflow_lb_endpoint_returns_200(client):
119
assert client.get('/__lbheartbeat__').status_code == 200

‎tests/test_phabricator_client.py

+14
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from landoapi.phabricator_client import PhabricatorClient, \
1111
PhabricatorAPIException
12+
from landoapi.utils import extract_rawdiff_id_from_uri
1213

1314
from tests.utils import *
1415
from tests.canned_responses.phabricator.revisions import *
@@ -80,3 +81,16 @@ def test_phabricator_exception():
8081
phab.get_revision(id=CANNED_REVISION_1['result'][0]['id'])
8182
assert e_info.value.error_code == CANNED_ERROR_1['error_code']
8283
assert e_info.value.error_info == CANNED_ERROR_1['error_info']
84+
85+
86+
def test_extracting_rawdiff_id_from_properly_formatted_uri():
87+
# Raw diff ID is '43480'
88+
uri = "https://secure.phabricator.com/differential/diff/43480/"
89+
rawdiff_id = extract_rawdiff_id_from_uri(uri)
90+
assert rawdiff_id == 43480
91+
92+
93+
def test_raises_error_if_rawdiff_uri_segments_change():
94+
uri = "https://secure.phabricator.com/differential/SOMETHINGNEW/43480/"
95+
with pytest.raises(RuntimeError):
96+
extract_rawdiff_id_from_uri(uri)

0 commit comments

Comments
 (0)
Failed to load comments.