blob: 87478727d3e20528fea165f0634df7d299d5a6ea [file] [log] [blame]
try:
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
except ImportError:
# Python 3 case
from http.server import BaseHTTPRequestHandler, HTTPServer
import contextlib
import errno
import json
import os
import shutil
import stat
import subprocess
import sys
import tempfile
import threading
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
import pr_preview
TEST_HOST = 'localhost'
def same_members(a, b):
if len(a) != len(b):
return False
a_copy = list(a)
for elem in b:
try:
a_copy.remove(elem)
except ValueError:
return False
return len(a_copy) == 0
# When these tests are executed in Windows, files in the temporary git
# repositories may be marked as "read only" at the moment they are intended to
# be deleted. The following handler for `shutil.rmtree` accounts for this by
# making the files writable and attempting to delete them a second time.
#
# Source:
# https://stackoverflow.com/questions/1213706/what-user-do-python-scripts-run-as-in-windows
def handle_remove_readonly(func, path, exc):
excvalue = exc[1]
candidates = (os.rmdir, os.remove, os.unlink)
if func in candidates and excvalue.errno == errno.EACCES:
os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # 0777
func(path)
else:
raise
class MockHandler(BaseHTTPRequestHandler, object):
def do_all(self):
path = self.path.split('?')[0]
request_body = None
if 'Content-Length' in self.headers:
request_body = self.rfile.read(
int(self.headers['Content-Length'])
).decode('utf-8')
if self.headers.get('Content-Type') == 'application/json':
request_body = json.loads(request_body)
for request, response in self.server.expected_traffic:
if request[0] != self.command:
continue
if request[1] != path:
continue
body_matches = True
for key in request[2]:
body_matches &= request[2][key] == request_body.get(key)
if not body_matches:
continue
break
else:
request = (self.command, path, request_body)
response = (400, {})
self.server.actual_traffic.append((request, response))
self.send_response(response[0])
self.end_headers()
if self.server.reponse_body_is_json:
self.wfile.write(json.dumps(response[1]).encode('utf-8'))
else:
self.wfile.write(response[1].encode('utf-8'))
def do_DELETE(self):
return self.do_all()
def do_GET(self):
return self.do_all()
def do_PATCH(self):
return self.do_all()
def do_POST(self):
return self.do_all()
class MockServer(HTTPServer, object):
'''HTTP server that responds to all requests with status code 200 and body
'{}' unless an alternative status code and body are specified for the given
method and path in the `responses` parameter.'''
def __init__(self, address, expected_traffic, reponse_body_is_json=True):
super(MockServer, self).__init__(address, MockHandler)
self.expected_traffic = expected_traffic
self.actual_traffic = []
self.reponse_body_is_json = reponse_body_is_json
def __enter__(self):
threading.Thread(target=lambda: self.serve_forever()).start()
return self
def __exit__(self, *args):
self.shutdown()
class Requests(object):
get_rate = ('GET', '/rate_limit', {})
ref_create_open = (
'POST', '/repos/test-org/test-repo/git/refs', {'ref':'refs/prs-open/45'}
)
ref_create_trusted = (
'POST',
'/repos/test-org/test-repo/git/refs',
{'ref':'refs/prs-trusted-for-preview/45'}
)
ref_update_open = (
'PATCH', '/repos/test-org/test-repo/git/refs/prs-open/45', {}
)
ref_update_trusted = (
'PATCH', '/repos/test-org/test-repo/git/refs/prs-trusted-for-preview/45', {}
)
deployment_get = ('GET', '/repos/test-org/test-repo/deployments', {})
deployment_create = ('POST', '/repos/test-org/test-repo/deployments', {})
deployment_status_create_pending = (
'POST',
'/repos/test-org/test-repo/deployments/24601/statuses',
{'state':'pending'}
)
deployment_status_create_error = (
'POST',
'/repos/test-org/test-repo/deployments/24601/statuses',
{'state':'error'}
)
deployment_status_create_success = (
'POST',
'/repos/test-org/test-repo/deployments/24601/statuses',
{'state':'success'}
)
preview = ('GET', '/.git/worktrees/45/HEAD', {})
class Responses(object):
no_limit = (200, {
'resources': {
'search': {
'remaining': 100,
'limit': 100
},
'core': {
'remaining': 100,
'limit': 100
}
}
})
@contextlib.contextmanager
def temp_repo(change_dir=False):
original_dir = os.getcwd()
directory = tempfile.mkdtemp()
if change_dir:
os.chdir(directory)
try:
subprocess.check_call(['git', 'init'], cwd=directory)
# Explicitly create the default branch.
subprocess.check_call(
['git', 'checkout', '-b', 'master'],
cwd=directory
)
subprocess.check_call(
['git', 'config', 'user.name', 'example'],
cwd=directory
)
subprocess.check_call(
['git', 'config', 'user.email', 'example@example.com'],
cwd=directory
)
subprocess.check_call(
['git', 'commit', '--allow-empty', '-m', 'first'],
cwd=directory
)
yield directory
finally:
if change_dir:
os.chdir(original_dir)
shutil.rmtree(
directory, ignore_errors=False, onerror=handle_remove_readonly
)
def update_mirror_refs(pull_request, expected_traffic, refs={}):
os.environ['GITHUB_TOKEN'] = 'c0ffee'
github_server = MockServer((TEST_HOST, 0), expected_traffic)
github_port = github_server.server_address[1]
remote_refs = {}
method_threw = False
with temp_repo(change_dir=True), temp_repo() as remote_repo, github_server:
subprocess.check_call(
['git', 'commit', '--allow-empty', '-m', 'first'],
cwd=remote_repo
)
subprocess.check_call(
['git', 'commit', '--allow-empty', '-m', 'second'],
cwd=remote_repo
)
subprocess.check_call(['git', 'remote', 'add', 'origin', remote_repo])
for name, value in refs.items():
subprocess.check_call(
['git', 'update-ref', name, value],
cwd=remote_repo
)
subprocess.check_call(['git', 'remote', '-v'])
project = pr_preview.Project(
'http://{}:{}'.format(TEST_HOST, github_port),
'test-org/test-repo',
)
remote = pr_preview.Remote('test-org/test-repo')
try:
pr_preview.update_mirror_refs(project, remote, pull_request)
except pr_preview.GitHubRateLimitException:
method_threw = True
lines = subprocess.check_output(['git', 'ls-remote', 'origin'])
for line in lines.decode('utf-8').strip().split('\n'):
revision, ref = line.split()
if not ref or ref in ('HEAD', 'refs/heads/master'):
continue
remote_refs[ref] = revision
return (
method_threw,
github_server.actual_traffic,
remote_refs,
)
def deploy(pr_num, revision, expected_github_traffic, expected_preview_traffic):
os.environ['GITHUB_TOKEN'] = 'c0ffee'
github_server = MockServer((TEST_HOST, 0), expected_github_traffic)
github_port = github_server.server_address[1]
preview_server = MockServer((TEST_HOST, 0), expected_preview_traffic, reponse_body_is_json=False)
preview_port = preview_server.server_address[1]
method_threw = False
with github_server, preview_server:
project = pr_preview.Project(
'http://{}:{}'.format(TEST_HOST, github_port),
'test-org/test-repo',
)
target = 'http://{}:{}'.format(TEST_HOST, preview_port)
pull_request = {'number': pr_num}
timeout = 1
try:
pr_preview.deploy(project, target, pull_request, revision, timeout)
except (pr_preview.GitHubRateLimitException, pr_preview.DeploymentFailedException):
method_threw = True
return (
method_threw,
github_server.actual_traffic,
preview_server.actual_traffic
)
def test_update_mirror_refs_fail_rate_limited():
pull_request = {
'number': 45,
'labels': [],
'user': {'login': 'stephenmcgruer'},
'author_association': 'COLLABORATOR',
'closed_at': None,
}
expected_traffic = [
(Requests.get_rate, (
200,
{
'resources': {
'core': {
'remaining': 1,
'limit': 10
}
}
}
))
]
method_threw, actual_traffic, remote_refs = update_mirror_refs(
pull_request, expected_traffic
)
assert method_threw
assert same_members(expected_traffic, actual_traffic)
def test_synchronize_ignore_closed():
# No existing refs, but a closed PR event comes in. Nothing should happen.
pull_request = {
'number': 45,
'labels': [],
'user': {'login': 'stephenmcgruer'},
'author_association': 'COLLABORATOR',
'closed_at': '2019-10-28',
}
expected_traffic = []
method_threw, actual_traffic, remote_refs = update_mirror_refs(
pull_request, expected_traffic
)
assert not method_threw
assert same_members(expected_traffic, actual_traffic)
def test_update_mirror_refs_collaborator():
pull_request = {
'number': 45,
'labels': [],
'user': {'login': 'stephenmcgruer'},
'author_association': 'COLLABORATOR',
'closed_at': None,
}
expected_traffic = [
(Requests.get_rate, Responses.no_limit),
(Requests.get_rate, Responses.no_limit),
(Requests.ref_create_open, (200, {})),
(Requests.ref_create_trusted, (200, {})),
]
method_threw, actual_traffic, remote_refs = update_mirror_refs(
pull_request, expected_traffic
)
assert not method_threw
assert same_members(expected_traffic, actual_traffic)
def test_update_mirror_refs_ignore_collaborator_bot():
pull_request = {
'number': 45,
'labels': [],
'user': {'login': 'chromium-wpt-export-bot'},
'author_association': 'COLLABORATOR',
'closed_at': None,
}
expected_traffic = []
method_threw, actual_traffic, remote_refs = update_mirror_refs(
pull_request, expected_traffic
)
assert not method_threw
assert same_members(expected_traffic, actual_traffic)
def test_update_mirror_refs_ignore_untrusted_contributor():
pull_request = {
'number': 45,
'labels': [],
'user': {'login': 'stephenmcgruer'},
'author_association': 'CONTRIBUTOR',
'closed_at': None,
}
expected_traffic = []
method_threw, actual_traffic, remote_refs = update_mirror_refs(
pull_request, expected_traffic
)
assert not method_threw
assert same_members(expected_traffic, actual_traffic)
def test_update_mirror_refs_trusted_contributor():
pull_request = {
'number': 45,
# user here is a contributor (untrusted), but the issue
# has been labelled as safe.
'labels': [{'name': 'safe for preview'}],
'user': {'login': 'Hexcles'},
'author_association': 'CONTRIBUTOR',
'closed_at': None,
}
expected_traffic = [
(Requests.get_rate, Responses.no_limit),
(Requests.get_rate, Responses.no_limit),
(Requests.ref_create_open, (200, {})),
(Requests.ref_create_trusted, (200, {})),
]
method_threw, actual_traffic, remote_refs = update_mirror_refs(
pull_request, expected_traffic
)
assert not method_threw
assert same_members(expected_traffic, actual_traffic)
def test_synchronize_sync_bot_with_label():
pull_request = {
'number': 45,
# user here is a bot which is normally not mirrored,
# but the issue has been labelled as safe.
'labels': [{'name': 'safe for preview'}],
'user': {'login': 'chromium-wpt-export-bot'},
'author_association': 'COLLABORATOR',
'closed_at': None,
}
expected_traffic = [
(Requests.get_rate, Responses.no_limit),
(Requests.get_rate, Responses.no_limit),
(Requests.ref_create_open, (200, {})),
(Requests.ref_create_trusted, (200, {})),
]
method_threw, actual_traffic, remote_refs = update_mirror_refs(
pull_request, expected_traffic
)
assert not method_threw
assert same_members(expected_traffic, actual_traffic)
def test_update_mirror_refs_update_collaborator():
pull_request = {
'number': 45,
'labels': [],
'user': {'login': 'stephenmcgruer'},
'author_association': 'COLLABORATOR',
'closed_at': None,
}
expected_traffic = [
(Requests.get_rate, Responses.no_limit),
(Requests.get_rate, Responses.no_limit),
(Requests.ref_update_open, (200, {})),
(Requests.ref_update_trusted, (200, {})),
]
refs = {
'refs/pull/45/head': 'HEAD',
'refs/prs-open/45': 'HEAD~',
'refs/prs-trusted-for-preview/45': 'HEAD~'
}
method_threw, actual_traffic, remote_refs = update_mirror_refs(
pull_request, expected_traffic, refs
)
assert not method_threw
assert same_members(expected_traffic, actual_traffic)
def test_synchronize_update_member():
pull_request = {
'number': 45,
'labels': [],
'user': {'login': 'jgraham'},
'author_association': 'MEMBER',
'closed_at': None,
}
expected_traffic = [
(Requests.get_rate, Responses.no_limit),
(Requests.get_rate, Responses.no_limit),
(Requests.ref_update_open, (200, {})),
(Requests.ref_update_trusted, (200, {}))
]
refs = {
'refs/pull/45/head': 'HEAD',
'refs/prs-open/45': 'HEAD~',
'refs/prs-trusted-for-preview/45': 'HEAD~'
}
method_threw, actual_traffic, remote_refs = update_mirror_refs(
pull_request, expected_traffic, refs
)
assert not method_threw
assert same_members(expected_traffic, actual_traffic)
def test_update_mirror_refs_delete_collaborator():
pull_request = {
'number': 45,
'labels': [],
'user': {'login': 'stephenmcgruer'},
'author_association': 'COLLABORATOR',
'closed_at': 2019-10-30,
}
expected_traffic = []
refs = {
'refs/pull/45/head': 'HEAD',
'refs/prs-open/45': 'HEAD~',
'refs/prs-trusted-for-preview/45': 'HEAD~'
}
method_threw, actual_traffic, remote_refs = update_mirror_refs(
pull_request, expected_traffic, refs
)
assert not method_threw
assert same_members(expected_traffic, actual_traffic)
assert list(remote_refs) == ['refs/pull/45/head']
def test_deploy_fail_rate_limited():
expected_github_traffic = [
(Requests.get_rate, (
200,
{
'resources': {
'core': {
'remaining': 1,
'limit': 10
}
}
}
))
]
expected_preview_traffic = []
pr_num = 45
revision = "abcdef123"
method_threw, actual_github_traffic, actual_preview_traffic = deploy(
pr_num, revision, expected_github_traffic, expected_preview_traffic
)
assert method_threw
assert actual_github_traffic == expected_github_traffic
assert actual_preview_traffic == expected_preview_traffic
def test_deploy_success():
pr_num = 45
revision = 'abcdef123'
expected_github_traffic = [
(Requests.get_rate, Responses.no_limit),
(Requests.deployment_get, (200, [])),
(Requests.get_rate, Responses.no_limit),
(Requests.deployment_create, (200, {
'id': 24601,
'sha': revision,
'environment': 'wpt-preview-45',
})),
(Requests.get_rate, Responses.no_limit),
(Requests.deployment_status_create_pending, (200, {})),
(Requests.get_rate, Responses.no_limit),
(Requests.deployment_status_create_success, (200, {}))
]
expected_preview_traffic = [
(Requests.preview, (200, revision))
]
method_threw, actual_github_traffic, actual_preview_traffic = deploy(
pr_num, revision, expected_github_traffic, expected_preview_traffic
)
assert not method_threw
assert actual_github_traffic == expected_github_traffic
assert actual_preview_traffic == expected_preview_traffic
def test_deploy_timeout_missing():
pr_num = 45
revision = 'abcdef123'
expected_github_traffic = [
(Requests.get_rate, Responses.no_limit),
(Requests.deployment_get, (200, [])),
(Requests.get_rate, Responses.no_limit),
(Requests.deployment_create, (200, {
'id': 24601,
'sha': revision,
'environment': 'wpt-preview-45',
})),
(Requests.get_rate, Responses.no_limit),
(Requests.deployment_status_create_pending, (200, {})),
(Requests.get_rate, Responses.no_limit),
(Requests.deployment_status_create_error, (200, {}))
]
expected_preview_traffic = [
(Requests.preview, (404, ""))
]
method_threw, actual_github_traffic, actual_preview_traffic = deploy(
pr_num, revision, expected_github_traffic, expected_preview_traffic
)
assert method_threw
assert expected_github_traffic == actual_github_traffic
ping_count = len(actual_preview_traffic)
assert ping_count > 0
assert actual_preview_traffic == expected_preview_traffic * ping_count
def test_deploy_timeout_wrong_revision():
pr_num = 45
revision = 'abcdef123'
expected_github_traffic = [
(Requests.get_rate, Responses.no_limit),
(Requests.deployment_get, (200, [])),
(Requests.get_rate, Responses.no_limit),
(Requests.deployment_create, (200, {
'id': 24601,
'sha': revision,
'environment': 'wpt-preview-45',
})),
(Requests.get_rate, Responses.no_limit),
(Requests.deployment_status_create_pending, (200, {})),
(Requests.get_rate, Responses.no_limit),
(Requests.deployment_status_create_error, (200, {}))
]
expected_preview_traffic = [
# wptpr.live has the wrong revision deployed
(Requests.preview, (200, 'ghijkl456'))
]
method_threw, actual_github_traffic, actual_preview_traffic = deploy(
pr_num, revision, expected_github_traffic, expected_preview_traffic
)
assert method_threw
assert expected_github_traffic == actual_github_traffic
ping_count = len(actual_preview_traffic)
assert ping_count > 0
assert actual_preview_traffic == expected_preview_traffic * ping_count