def test_raise_error_exception_on_error_response(api_url, status, body): api = LandoAPI(api_url) with requests_mock.mock() as m: m.get(api_url + "/stacks/D1", status_code=status, text=body) with pytest.raises(LandoAPIError): api.request("GET", "stacks/D1") assert m.called
def test_raise_communication_exception_on_invalid_json(api_url): api = LandoAPI(api_url) with requests_mock.mock() as m: m.get(api_url + "/stacks/D1", text="invalid } json {[[") with pytest.raises(LandoAPICommunicationException): api.request("GET", "stacks/D1") assert m.called
def test_raise_communication_exception_on_request_exceptions(api_url, exc): api = LandoAPI(api_url) with requests_mock.mock() as m: m.get(api_url + "/stacks/D1", exc=exc) with pytest.raises(LandoAPICommunicationException): api.request("GET", "stacks/D1") assert m.called
def sec_approval_request_handler(): if not current_app.config.get("ENABLE_SEC_APPROVAL"): abort(404) if not is_user_authenticated(): errors = make_form_error( 'You must be logged in to request sec-approval' ) return jsonify(errors=errors), 401 token = get_phabricator_api_token() if not token: # The user has not set their API Token. Lando API will return an # error if it is missing. errors = make_form_error( 'You must set your Phabricator API token in the Lando User ' 'Settings to request sec-approval' ) return jsonify(errors=errors), 400 api = LandoAPI( current_app.config['LANDO_API_URL'], auth0_access_token=session.get('access_token'), phabricator_api_token=token, ) form = SecApprovalRequestForm() if not form.validate(): return jsonify(errors=form.errors), 400 else: logger.info( "sec-approval requested", extra={"revision_id": form.revision_id.data} ) # NOTE: We let errors in the upstream service get turned into # exceptions and bubble up to the application's default exception # handler. It will generate a HTTP 500 for the UI to handle. api.request( "POST", "requestSecApproval", require_auth0=True, json={ "revision_id": form.revision_id.data, "sanitized_message": form.new_message.data, } ) return jsonify({})
def test_phabricator_api_token_in_request_header(api_url): api = LandoAPI(api_url, phabricator_api_token="api_token") with requests_mock.mock() as m: m.get( api_url + "/stacks/D1", request_headers={"X-Phabricator-API-Key": "api_token"}, status_code=404, text="{}", ) # NoMockAddress would be raised for the wrong header with pytest.raises(LandoAPIError): api.request("GET", "stacks/D1") assert m.called
def test_raise_error_with_details_on_error_response(api_url): api = LandoAPI(api_url) error = { "detail": "Couldn't find it", "status": 404, "title": "Not Found", "type": "about:blank", } with requests_mock.mock() as m: m.get(api_url + "/stacks/D1", status_code=error["status"], json=error) with pytest.raises(LandoAPIError) as exc_info: api.request("GET", "stacks/D1") assert m.called assert exc_info.value.detail == error["detail"] assert exc_info.value.title == error["title"] assert exc_info.value.type == error["type"] assert exc_info.value.status_code == error["status"] assert exc_info.value.response == error assert exc_info.value.instance is None
def update_landing_job(landing_job_id): if not is_user_authenticated(): errors = make_form_error( "You must be logged in to update a landing job.") return jsonify(errors=errors), 401 token = get_phabricator_api_token() api = LandoAPI( current_app.config["LANDO_API_URL"], auth0_access_token=session.get("access_token"), phabricator_api_token=token, ) try: data = api.request( "PUT", f"landing_jobs/{landing_job_id}", require_auth0=True, json=request.get_json(), ) except LandoAPIError as e: return e.response, e.response["status"] return data
def revision(revision_id): api = LandoAPI(current_app.config['LANDO_API_URL'], auth0_access_token=session.get('access_token'), phabricator_api_token=get_phabricator_api_token()) form = TransplantRequestForm() errors = [] if form.is_submitted(): if not is_user_authenticated(): errors.append('You must be logged in to request a landing') elif not form.validate(): for _, field_errors in form.errors.items(): errors.extend(field_errors) else: try: api.request('POST', 'transplants', require_auth0=True, json={ 'landing_path': json.loads(form.landing_path.data), 'confirmation_token': form.confirmation_token.data, }) # We don't actually need any of the data from the # the submission. As long as an exception wasn't # raised we're successful. return redirect( url_for('revisions.revision', revision_id=revision_id)) except LandoAPIError as e: if not e.detail: raise errors.append(e.detail) # Request the entire stack. try: stack = api.request('GET', 'stacks/D{}'.format(revision_id)) except LandoAPIError as e: if e.status_code == 404: raise RevisionNotFound(revision_id) else: raise # Build a mapping from phid to revision and identify # the data for the revision used to load this page. revision = None revisions = {} for r in stack['revisions']: revisions[r['phid']] = r if r['id'] == 'D{}'.format(revision_id): revision = r['phid'] # Request all previous transplants for the stack. transplants = api.request( 'GET', 'transplants', params={'stack_revision_id': 'D{}'.format(revision_id)}) # TODO: support displaying the full DAG, and landing *past* the # current revision. # # The revision may appear in many `landable_paths`` if it has # multiple children, or any of its landable descendents have # multiple children. That being said, there should only be a # single unique path up to this revision, so find the first # it appears in. The revisions up to the target one in this # path form the landable series. series = None for p in stack['landable_paths']: try: series = p[:p.index(revision) + 1] break except ValueError: pass dryrun = None if series and is_user_authenticated(): landing_path = [{ 'revision_id': revisions[phid]['id'], 'diff_id': revisions[phid]['diff']['id'], } for phid in series] form.landing_path.data = json.dumps(landing_path) dryrun = api.request('POST', 'transplants/dryrun', require_auth0=True, json={'landing_path': landing_path}) form.confirmation_token.data = dryrun.get('confirmation_token') series = list(reversed(series)) return render_template( 'stack/stack.html', revision_id='D{}'.format(revision_id), series=series, dryrun=dryrun, stack=stack, transplants=transplants, revisions=revisions, revision_phid=revision, errors=errors, form=form, )
def revision(revision_id): api = LandoAPI( current_app.config["LANDO_API_URL"], auth0_access_token=session.get("access_token"), phabricator_api_token=get_phabricator_api_token(), ) form = TransplantRequestForm() sec_approval_form = SecApprovalRequestForm() errors = [] if form.is_submitted(): if not is_user_authenticated(): errors.append("You must be logged in to request a landing") elif not form.validate(): for _, field_errors in form.errors.items(): errors.extend(field_errors) else: try: api.request( "POST", "transplants", require_auth0=True, json={ "landing_path": json.loads(form.landing_path.data), "confirmation_token": form.confirmation_token.data, }, ) # We don't actually need any of the data from the # the submission. As long as an exception wasn't # raised we're successful. return redirect(url_for("revisions.revision", revision_id=revision_id)) except LandoAPIError as e: if not e.detail: raise errors.append(e.detail) # Request the entire stack. try: stack = api.request("GET", "stacks/D{}".format(revision_id)) except LandoAPIError as e: if e.status_code == 404: raise RevisionNotFound(revision_id) else: raise # Build a mapping from phid to revision and identify # the data for the revision used to load this page. revision = None revisions = {} for r in stack["revisions"]: revisions[r["phid"]] = r if r["id"] == "D{}".format(revision_id): revision = r["phid"] # Build a mapping from phid to repository. repositories = {} for r in stack["repositories"]: repositories[r["phid"]] = r # Request all previous transplants for the stack. transplants = api.request( "GET", "transplants", params={"stack_revision_id": "D{}".format(revision_id)} ) # The revision may appear in many `landable_paths`` if it has # multiple children, or any of its landable descendents have # multiple children. That being said, there should only be a # single unique path up to this revision, so find the first # it appears in. The revisions up to the target one in this # path form the landable series. # # We also form a set of all the revisions that are landable # so we can present selection for what to land. series = None landable = set() for p in stack["landable_paths"]: for phid in p: landable.add(phid) try: series = p[: p.index(revision) + 1] except ValueError: pass dryrun = None target_repo = None if series and is_user_authenticated(): landing_path = [ { "revision_id": revisions[phid]["id"], "diff_id": revisions[phid]["diff"]["id"], } for phid in series ] form.landing_path.data = json.dumps(landing_path) dryrun = api.request( "POST", "transplants/dryrun", require_auth0=True, json={"landing_path": landing_path}, ) form.confirmation_token.data = dryrun.get("confirmation_token") series = list(reversed(series)) target_repo = repositories.get(revisions[series[0]]["repo_phid"]) phids = set(revisions.keys()) edges = set(Edge(child=e[0], parent=e[1]) for e in stack["edges"]) order = sort_stack_topological( phids, edges, key=lambda x: int(revisions[x]["id"][1:]) ) drawing_width, drawing_rows = draw_stack_graph(phids, edges, order) annotate_sec_approval_workflow_info(revisions) # Are we showing the "sec-approval request submitted" dialog? # If we are then fill in its values. submitted_revision = request.args.get("show_approval_success") submitted_rev_url = None if submitted_revision: for rev in revisions.values(): if rev["id"] == submitted_revision: submitted_rev_url = rev["url"] break return render_template( "stack/stack.html", revision_id="D{}".format(revision_id), series=series, landable=landable, dryrun=dryrun, stack=stack, rows=list(zip(reversed(order), reversed(drawing_rows))), drawing_width=drawing_width, transplants=transplants, revisions=revisions, revision_phid=revision, sec_approval_form=sec_approval_form, submitted_rev_url=submitted_rev_url, target_repo=target_repo, errors=errors, form=form, )
def revision(revision_id): api = LandoAPI(current_app.config['LANDO_API_URL'], auth0_access_token=session.get('access_token'), phabricator_api_token=get_phabricator_api_token()) form = TransplantRequestForm() errors = [] if form.is_submitted(): if not is_user_authenticated(): errors.append('You must be logged in to request a landing') elif not form.validate(): for _, field_errors in form.errors.items(): errors.extend(field_errors) else: try: api.request('POST', 'transplants', require_auth0=True, json={ 'landing_path': json.loads(form.landing_path.data), 'confirmation_token': form.confirmation_token.data, }) # We don't actually need any of the data from the # the submission. As long as an exception wasn't # raised we're successful. return redirect( url_for('revisions.revision', revision_id=revision_id)) except LandoAPIError as e: if not e.detail: raise errors.append(e.detail) # Request the entire stack. try: stack = api.request('GET', 'stacks/D{}'.format(revision_id)) except LandoAPIError as e: if e.status_code == 404: raise RevisionNotFound(revision_id) else: raise # Build a mapping from phid to revision and identify # the data for the revision used to load this page. revision = None revisions = {} for r in stack['revisions']: revisions[r['phid']] = r if r['id'] == 'D{}'.format(revision_id): revision = r['phid'] # Build a mapping from phid to repository. repositories = {} for r in stack['repositories']: repositories[r['phid']] = r # Request all previous transplants for the stack. transplants = api.request( 'GET', 'transplants', params={'stack_revision_id': 'D{}'.format(revision_id)}) # The revision may appear in many `landable_paths`` if it has # multiple children, or any of its landable descendents have # multiple children. That being said, there should only be a # single unique path up to this revision, so find the first # it appears in. The revisions up to the target one in this # path form the landable series. # # We also form a set of all the revisions that are landable # so we can present selection for what to land. series = None landable = set() for p in stack['landable_paths']: for phid in p: landable.add(phid) try: series = p[:p.index(revision) + 1] except ValueError: pass dryrun = None target_repo = None if series and is_user_authenticated(): landing_path = [{ 'revision_id': revisions[phid]['id'], 'diff_id': revisions[phid]['diff']['id'], } for phid in series] form.landing_path.data = json.dumps(landing_path) dryrun = api.request('POST', 'transplants/dryrun', require_auth0=True, json={'landing_path': landing_path}) form.confirmation_token.data = dryrun.get('confirmation_token') series = list(reversed(series)) target_repo = repositories.get(revisions[series[0]]['repo_phid']) phids = set(revisions.keys()) edges = set(Edge(child=e[0], parent=e[1]) for e in stack['edges']) order = sort_stack_topological(phids, edges, key=lambda x: int(revisions[x]['id'][1:])) drawing_width, drawing_rows = draw_stack_graph(phids, edges, order) return render_template( 'stack/stack.html', revision_id='D{}'.format(revision_id), series=series, landable=landable, dryrun=dryrun, stack=stack, rows=list(zip(reversed(order), reversed(drawing_rows))), drawing_width=drawing_width, transplants=transplants, revisions=revisions, revision_phid=revision, target_repo=target_repo, errors=errors, form=form, )