예제 #1
0
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
예제 #2
0
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
예제 #3
0
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
예제 #4
0
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({})
예제 #5
0
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
예제 #6
0
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
예제 #7
0
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
예제 #8
0
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,
    )
예제 #9
0
파일: revisions.py 프로젝트: hwine/lando-ui
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,
    )