Ejemplo n.º 1
0
def test_config_default() -> None:
    file_path = load_config_fixture("v1-default.toml")
    loaded = toml.load(file_path)
    actual = V1.parse_obj(cast(Dict[Any, Any], loaded))
    expected = V1(version=1)

    assert actual == expected
Ejemplo n.º 2
0
def test_config_parsing_opposite() -> None:
    """
    parse config with all opposite settings so we can ensure the config is
    correctly formatted.
    """
    file_path = load_config_fixture("v1-opposite.toml")
    loaded = toml.load(file_path)
    actual = V1.parse_obj(cast(Dict[Any, Any], loaded))

    expected = V1(
        version=1,
        app_id="12345",
        merge=Merge(
            automerge_label="mergeit!",
            require_automerge_label=False,
            blacklist_title_regex="",
            blacklist_labels=["wip", "block-merge"],
            method=MergeMethod.squash,
            delete_branch_on_merge=True,
            block_on_reviews_requested=True,
            notify_on_conflict=False,
            optimistic_updates=False,
            message=MergeMessage(
                title=MergeTitleStyle.pull_request_title,
                body=MergeBodyStyle.pull_request_body,
                include_pr_number=False,
                body_type=BodyText.plain_text,
                strip_html_comments=True,
            ),
        ),
    )

    assert actual == expected
Ejemplo n.º 3
0
def test_app_id(
    pull_request: PullRequest, config: V1, branch_protection: BranchProtectionRule
) -> None:
    config.app_id = "123"
    with pytest.raises(MissingAppID):
        mergeable(
            app_id="1234",
            config=config,
            pull_request=pull_request,
            branch_protection=branch_protection,
            review_requests_count=0,
            reviews=[],
            contexts=[],
            check_runs=[],
            valid_signature=False,
            valid_merge_methods=[],
        )
    # try without passing an app_id
    with pytest.raises(MissingAppID):
        mergeable(
            config=config,
            pull_request=pull_request,
            branch_protection=branch_protection,
            review_requests_count=0,
            reviews=[],
            contexts=[],
            check_runs=[],
            valid_signature=False,
            valid_merge_methods=[],
        )
Ejemplo n.º 4
0
def test_get_merge_body_include_pull_request_author_mannequin() -> None:
    """
    Test case where actor is not a User and Bot to see how we handle weird cases.
    """
    pull_request = create_pull_request()
    pull_request.body = "hello world"
    pull_request.author.name = None
    pull_request.author.type = "Mannequin"

    actual = get_merge_body(
        config=V1(
            version=1,
            merge=Merge(message=MergeMessage(
                body=MergeBodyStyle.pull_request_body,
                include_pull_request_author=True,
            )),
        ),
        pull_request=pull_request,
        merge_method=MergeMethod.squash,
        commits=[],
    )
    expected = MergeBody(
        merge_method="squash",
        commit_message=
        "hello world\n\nCo-authored-by: barry <*****@*****.**>",
    )
    assert actual == expected
Ejemplo n.º 5
0
def test_get_merge_body_includes_pull_request_url() -> None:
    """
    Ensure that when the appropriate config option is set, we include the
    pull request url in the commit message.
    """
    pull_request = create_pull_request()
    actual = get_merge_body(
        config=V1(
            version=1,
            merge=Merge(
                message=MergeMessage(body=MergeBodyStyle.pull_request_body,
                                     include_pull_request_url=True)),
        ),
        pull_request=pull_request,
        merge_method=MergeMethod.squash,
        commits=[],
    )
    expected = MergeBody(
        merge_method="squash",
        commit_message="""\
# some description

PR-URL: https://github.com/example_org/example_repo/pull/65""",
    )
    assert actual == expected
Ejemplo n.º 6
0
def test_get_merge_body_includes_pull_request_url_with_coauthor() -> None:
    """
    Coauthor should appear after the pull request url
    """
    pull_request = create_pull_request()
    actual = get_merge_body(
        config=V1(
            version=1,
            merge=Merge(message=MergeMessage(
                body=MergeBodyStyle.pull_request_body,
                include_pull_request_url=True,
                include_pull_request_author=True,
            )),
        ),
        pull_request=pull_request,
        merge_method=MergeMethod.squash,
        commits=[],
    )
    expected = MergeBody(
        merge_method="squash",
        commit_message="""\
# some description

PR-URL: https://github.com/example_org/example_repo/pull/65
Co-authored-by: Barry Berkman <*****@*****.**>""",
    )
    assert actual == expected
Ejemplo n.º 7
0
def test_get_markdown_for_config_toml_error() -> None:
    config = "[[[ version = 12"
    error = V1.parse_toml(config)
    assert not isinstance(error, V1)
    markdown = get_markdown_for_config(error,
                                       config_str=config,
                                       git_path="master:.kodiak.toml")
    assert markdown == load_config_fixture("toml-error.md").read_text()
Ejemplo n.º 8
0
def validate_config(config_path: str) -> None:
    """
    parse and output the json representation of a Kodiak config
    """
    cfg_text = Path(config_path).read_text()
    cfg_file = V1.parse_toml(cfg_text)
    assert isinstance(cfg_file, V1)
    click.echo(cfg_file.json(indent=2))
Ejemplo n.º 9
0
def test_bad_file() -> None:
    res = V1.parse_toml("something[invalid[")
    assert isinstance(res, toml.TomlDecodeError)

    res = V1.parse_toml("version = 20")
    assert isinstance(res, pydantic.ValidationError)

    # we should return an error when we try to parse a different version
    res = V1.parse_toml("version = 20")
    assert isinstance(res, pydantic.ValidationError)

    # we should always require that the version is specified, even if we provide defaults for everything else
    res = V1.parse_toml("")
    assert isinstance(res, pydantic.ValidationError)

    res = V1.parse_toml("merge.automerge_label = 123")
    assert isinstance(res, pydantic.ValidationError)
Ejemplo n.º 10
0
    async def get_config_for_ref(
            self, *, ref: str,
            org_repo_default_branch: str | None) -> CfgInfo | None:
        repo_root_config_expression = create_root_config_file_expression(
            branch=ref)
        repo_github_config_expression = create_github_config_file_expression(
            branch=ref)
        org_root_config_expression: str | None = None
        org_github_config_file_expression: str | None = None

        if org_repo_default_branch is not None:
            org_root_config_expression = create_root_config_file_expression(
                branch=org_repo_default_branch)
            org_github_config_file_expression = create_github_config_file_expression(
                branch=org_repo_default_branch)
        res = await self.send_query(
            query=GET_CONFIG_QUERY,
            variables=dict(
                owner=self.owner,
                repo=self.repo,
                rootConfigFileExpression=repo_root_config_expression,
                githubConfigFileExpression=repo_github_config_expression,
                orgRootConfigFileExpression=org_root_config_expression,
                orgGithubConfigFileExpression=org_github_config_file_expression,
            ),
            installation_id=self.installation_id,
        )
        if res is None:
            return None
        data = res.get("data")
        if data is None:
            self.log.error("could not fetch default branch name", res=res)
            return None

        parsed_config = parse_config(data)
        if parsed_config is None:
            return None

        def get_file_expression() -> str:
            assert parsed_config is not None
            if parsed_config.kind == "repo_root":
                return repo_root_config_expression
            if parsed_config.kind == "repo_github":
                return repo_github_config_expression
            if parsed_config.kind == "org_root":
                assert org_root_config_expression is not None
                return org_root_config_expression
            if parsed_config.kind == "org_github":
                assert org_github_config_file_expression is not None
                return org_github_config_file_expression
            raise Exception(f"unknown config kind {parsed_config.kind!r}")

        return CfgInfo(
            parsed=V1.parse_toml(parsed_config.text),
            text=parsed_config.text,
            file_expression=get_file_expression(),
        )
Ejemplo n.º 11
0
def test_pr_get_merge_body_empty() -> None:
    pull_request = create_pull_request()
    actual = get_merge_body(
        config=V1(version=1),
        pull_request=pull_request,
        merge_method=MergeMethod.squash,
        commits=[],
    )
    expected = MergeBody(merge_method="squash")
    assert actual == expected
Ejemplo n.º 12
0
def test_config_parsing_opposite(config_fixture_name: str, expected_config: V1) -> None:
    """
    parse config with all opposite settings so we can ensure the config is
    correctly formatted.
    """
    file_path = load_config_fixture(config_fixture_name)
    loaded = toml.load(file_path)
    actual = V1.parse_obj(cast(Dict[Any, Any], loaded))

    assert actual == expected_config
Ejemplo n.º 13
0
def test_config_schema() -> None:
    """
    Although we don't use the config schema anywhere, it provides a form of
    documentation for the bot config.
    """
    schema_path = load_config_fixture("config-schema.json")
    assert json.loads(V1.schema_json()) == json.loads(schema_path.read_text()), (
        "schema shouldn't change unexpectedly.\n"
        "Did you remember to run `poetry run kodiak gen-conf-json-schema > kodiak/test/fixtures/config/config-schema.json` in the `bot` directory?"
    )
Ejemplo n.º 14
0
def test_bad_file() -> None:
    with pytest.raises(toml.TomlDecodeError):
        V1.parse_toml("something[invalid[")

    with pytest.raises(ValueError):
        # we should raise an error when we try to parse a different version
        V1.parse_toml("version = 20")

    with pytest.raises(ValueError):
        # we should always require that the version is specified, even if we provide defaults for everything else
        V1.parse_toml("")

    with pytest.raises(ValueError):
        V1.parse_toml("merge.automerge_label = 123")
Ejemplo n.º 15
0
def test_get_merge_body_empty() -> None:
    pull_request = create_pull_request()
    pull_request.body = "hello world"
    actual = get_merge_body(
        config=V1(
            version=1,
            merge=Merge(message=MergeMessage(body=MergeBodyStyle.empty))),
        pull_request=pull_request,
        merge_method=MergeMethod.squash,
        commits=[],
    )
    expected = MergeBody(merge_method="squash", commit_message="")
    assert actual == expected
Ejemplo n.º 16
0
def test_get_merge_body_empty(pull_request: queries.PullRequest) -> None:
    pull_request.body = "hello world"
    actual = get_merge_body(
        V1(
            version=1,
            merge=Merge(
                method=MergeMethod.squash,
                message=MergeMessage(body=MergeBodyStyle.empty),
            ),
        ),
        pull_request,
    )
    expected = dict(merge_method="squash", commit_message="")
    assert actual == expected
Ejemplo n.º 17
0
def test_get_merge_body_strip_html_comments() -> None:
    pull_request = create_pull_request()
    pull_request.body = "hello <!-- testing -->world"
    actual = get_merge_body(
        config=V1(
            version=1,
            merge=Merge(
                message=MergeMessage(body=MergeBodyStyle.pull_request_body,
                                     strip_html_comments=True)),
        ),
        pull_request=pull_request,
        merge_method=MergeMethod.squash,
        commits=[],
    )
    expected = MergeBody(merge_method="squash", commit_message="hello world")
    assert actual == expected
Ejemplo n.º 18
0
def test_get_merge_body_strip_html_comments(pull_request: queries.PullRequest,
                                            original: str,
                                            stripped: str) -> None:
    pull_request.body = "hello <!-- testing -->world"
    actual = get_merge_body(
        V1(
            version=1,
            merge=Merge(
                method=MergeMethod.squash,
                message=MergeMessage(body=MergeBodyStyle.pull_request_body,
                                     strip_html_comments=True),
            ),
        ),
        pull_request,
    )
    expected = dict(merge_method="squash", commit_message="hello world")
    assert actual == expected
Ejemplo n.º 19
0
    async def get_config_for_ref(self, *, ref: str) -> CfgInfo | None:
        root_config_file_expression = create_root_config_file_expression(
            branch=ref)
        github_config_file_expression = create_github_config_file_expression(
            branch=ref)
        res = await self.send_query(
            query=GET_CONFIG_QUERY,
            variables=dict(
                owner=self.owner,
                repo=self.repo,
                rootConfigFileExpression=root_config_file_expression,
                githubConfigFileExpression=github_config_file_expression,
            ),
            installation_id=self.installation_id,
        )
        if res is None:
            return None
        data = res.get("data")
        errors = res.get("errors")
        if errors is not None or data is None:
            self.log.error("could not fetch default branch name", res=res)
            return None
        try:
            parsed = ConfigQueryResponse.parse_obj(data)
        except pydantic.ValidationError:
            self.log.exception("problem parsing api response for config")
            return None

        if not parsed.repository:
            return None
        if (parsed.repository.rootConfigFile is not None
                and parsed.repository.rootConfigFile.text is not None):
            config_file_expression = root_config_file_expression
            config_text = parsed.repository.rootConfigFile.text
        elif (parsed.repository.githubConfigFile is not None
              and parsed.repository.githubConfigFile.text is not None):
            config_file_expression = github_config_file_expression
            config_text = parsed.repository.githubConfigFile.text
        else:
            return None

        return CfgInfo(
            parsed=V1.parse_toml(config_text),
            text=config_text,
            file_expression=config_file_expression,
        )
Ejemplo n.º 20
0
def test_get_merge_body_includes_pull_request_url_github_default() -> None:
    """
    We should not set a commit message when merge.body = "github_default".
    """
    pull_request = create_pull_request()
    actual = get_merge_body(
        config=V1(
            version=1,
            merge=Merge(
                message=MergeMessage(body=MergeBodyStyle.github_default,
                                     include_pull_request_url=True)),
        ),
        pull_request=pull_request,
        merge_method=MergeMethod.squash,
        commits=[],
    )
    expected = MergeBody(merge_method="squash", commit_message=None)
    assert actual == expected
Ejemplo n.º 21
0
def test_pr_get_merge_body_full() -> None:
    pull_request = create_pull_request()
    actual = get_merge_body(
        config=V1(
            version=1,
            merge=Merge(message=MergeMessage(
                title=MergeTitleStyle.pull_request_title,
                body=MergeBodyStyle.pull_request_body,
                include_pr_number=True,
            )),
        ),
        pull_request=pull_request,
        merge_method=MergeMethod.squash,
        commits=[],
    )
    expected = MergeBody(
        merge_method="squash",
        commit_title=pull_request.title + f" (#{pull_request.number})",
        commit_message=pull_request.body,
    )
    assert expected == actual
Ejemplo n.º 22
0
def test_pr_get_merge_body_full(pull_request: queries.PullRequest) -> None:
    actual = get_merge_body(
        V1(
            version=1,
            merge=Merge(
                method=MergeMethod.squash,
                message=MergeMessage(
                    title=MergeTitleStyle.pull_request_title,
                    body=MergeBodyStyle.pull_request_body,
                    include_pr_number=True,
                ),
            ),
        ),
        pull_request,
    )
    expected = dict(
        merge_method="squash",
        commit_title=pull_request.title + f" (#{pull_request.number})",
        commit_message=pull_request.body,
    )
    assert actual == expected
Ejemplo n.º 23
0
def test_get_merge_body_include_pull_request_author_user() -> None:
    pull_request = create_pull_request()
    pull_request.body = "hello world"

    actual = get_merge_body(
        config=V1(
            version=1,
            merge=Merge(message=MergeMessage(
                body=MergeBodyStyle.pull_request_body,
                include_pull_request_author=True,
            )),
        ),
        pull_request=pull_request,
        merge_method=MergeMethod.squash,
        commits=[],
    )
    expected = MergeBody(
        merge_method="squash",
        commit_message=
        "hello world\n\nCo-authored-by: Barry Berkman <*****@*****.**>",
    )
    assert actual == expected
Ejemplo n.º 24
0
def test_get_merge_body_cut_body_after() -> None:
    """
    Basic check of cut_body_after removing content.
    """
    pull_request = create_pull_request()
    pull_request.body = "hello <!-- testing -->world"
    actual = get_merge_body(
        config=V1(
            version=1,
            merge=Merge(
                message=MergeMessage(
                    body=MergeBodyStyle.pull_request_body,
                    cut_body_after="<!-- testing -->",
                )
            ),
        ),
        pull_request=pull_request,
        merge_method=MergeMethod.squash,
        commits=[],
    )
    expected = MergeBody(merge_method="squash", commit_message="hello <!-- testing -->")
    assert actual == expected
Ejemplo n.º 25
0
def test_get_merge_body_cut_body_and_text_before() -> None:
    """
    Verify that the separator is also gone after removing content.
    """
    pull_request = create_pull_request()
    pull_request.body = "hello <!-- testing -->world"
    actual = get_merge_body(
        config=V1(
            version=1,
            merge=Merge(
                message=MergeMessage(
                    body=MergeBodyStyle.pull_request_body,
                    cut_body_before="<!-- testing -->",
                    cut_body_and_text=True,
                )
            ),
        ),
        pull_request=pull_request,
        merge_method=MergeMethod.squash,
        commits=[],
    )
    expected = MergeBody(merge_method="squash", commit_message="world")
    assert actual == expected
Ejemplo n.º 26
0
def test_get_merge_body_cut_body_after_strip_html() -> None:
    """
    We should be able to use strip_html_comments with cut_body_after.
    """
    pull_request = create_pull_request()
    pull_request.body = "hello <!-- testing -->world"
    actual = get_merge_body(
        config=V1(
            version=1,
            merge=Merge(
                message=MergeMessage(
                    body=MergeBodyStyle.pull_request_body,
                    cut_body_after="<!-- testing -->",
                    strip_html_comments=True,
                )
            ),
        ),
        pull_request=pull_request,
        merge_method=MergeMethod.squash,
        commits=[],
    )
    expected = MergeBody(merge_method="squash", commit_message="hello ")
    assert actual == expected
Ejemplo n.º 27
0
def test_get_merge_body_cut_body_before_multiple_markers() -> None:
    """
    We should choose the first substring matching cut_body_before.
    """
    pull_request = create_pull_request()
    pull_request.body = "hello <!-- testing -->world<!-- testing --> 123"
    actual = get_merge_body(
        config=V1(
            version=1,
            merge=Merge(
                message=MergeMessage(
                    body=MergeBodyStyle.pull_request_body,
                    cut_body_before="<!-- testing -->",
                    strip_html_comments=True,
                )
            ),
        ),
        pull_request=pull_request,
        merge_method=MergeMethod.squash,
        commits=[],
    )
    expected = MergeBody(merge_method="squash", commit_message="world 123")
    assert actual == expected
Ejemplo n.º 28
0
def test_get_merge_body_cut_body_before_no_match_found() -> None:
    """
    Ensure we don't edit the message if there isn't any match found with
    cut_body_before.
    """
    pull_request = create_pull_request()
    pr_body = "hello <!-- foo -->world<!-- bar --> 123"
    pull_request.body = pr_body
    actual = get_merge_body(
        config=V1(
            version=1,
            merge=Merge(
                message=MergeMessage(
                    body=MergeBodyStyle.pull_request_body,
                    cut_body_before="<!-- buzz -->",
                )
            ),
        ),
        pull_request=pull_request,
        merge_method=MergeMethod.squash,
        commits=[],
    )
    expected = MergeBody(merge_method="squash", commit_message=pr_body)
    assert actual == expected
Ejemplo n.º 29
0
    async def get_event_info(
        self, branch_name: str, pr_number: int
    ) -> Optional[EventInfoResponse]:
        """
        Retrieve all the information we need to evaluate a pull request

        This is basically the "do-all-the-things" query
        """

        log = self.log.bind(pr=pr_number)

        root_config_file_expression = create_root_config_file_expression(
            branch=branch_name
        )
        github_config_file_expression = create_github_config_file_expression(
            branch=branch_name
        )

        res = await self.send_query(
            query=GET_EVENT_INFO_QUERY,
            variables=dict(
                owner=self.owner,
                repo=self.repo,
                rootConfigFileExpression=root_config_file_expression,
                githubConfigFileExpression=github_config_file_expression,
                PRNumber=pr_number,
            ),
            installation_id=self.installation_id,
        )
        if res is None:
            return None

        data = res.get("data")
        errors = res.get("errors")
        if errors is not None or data is None:
            log.error("could not fetch event info", res=res)
            return None

        repository = get_repo(data=data)
        if not repository:
            log.warning("could not find repository")
            return None

        subscription = await self.get_subscription()

        root_config_str = get_root_config_str(repo=repository)
        github_config_str = get_github_config_str(repo=repository)
        if root_config_str is not None:
            config_str = root_config_str
            config_file_expression = root_config_file_expression
        elif github_config_str is not None:
            config_str = github_config_str
            config_file_expression = github_config_file_expression
        else:
            # NOTE(chdsbd): we don't want to show a message for this as the lack
            # of a config allows kodiak to be selectively installed
            log.info("could not find configuration file")
            return None

        pull_request = get_pull_request(repo=repository)
        if not pull_request:
            log.warning("Could not find PR")
            return None

        config = V1.parse_toml(config_str)

        # update the dictionary to match what we need for parsing
        pull_request["labels"] = get_labels(pr=pull_request)
        pull_request["latest_sha"] = get_sha(pr=pull_request)
        pull_request["number"] = pr_number
        try:
            pr = PullRequest.parse_obj(pull_request)
        except ValueError:
            log.warning("Could not parse pull request")
            return None

        branch_protection = get_branch_protection(
            repo=repository, ref_name=pr.baseRefName
        )

        partial_reviews = get_reviews(pr=pull_request)
        reviews_with_permissions = await self.get_reviewers_and_permissions(
            reviews=partial_reviews
        )
        return EventInfoResponse(
            config=config,
            config_str=config_str,
            config_file_expression=config_file_expression,
            pull_request=pr,
            repository=RepoInfo(
                merge_commit_allowed=repository.get("mergeCommitAllowed", False),
                rebase_merge_allowed=repository.get("rebaseMergeAllowed", False),
                squash_merge_allowed=repository.get("squashMergeAllowed", False),
                delete_branch_on_merge=repository.get("deleteBranchOnMerge") is True,
                is_private=repository.get("isPrivate") is True,
            ),
            subscription=subscription,
            branch_protection=branch_protection,
            review_requests=get_requested_reviews(pr=pull_request),
            reviews=reviews_with_permissions,
            status_contexts=get_status_contexts(pr=pull_request),
            check_runs=get_check_runs(pr=pull_request),
            head_exists=get_head_exists(pr=pull_request),
            valid_signature=get_valid_signature(pr=pull_request),
            valid_merge_methods=get_valid_merge_methods(repo=repository),
        )
Ejemplo n.º 30
0
def create_event() -> EventInfoResponse:
    config = V1(version=1,
                merge=Merge(automerge_label="automerge",
                            method=MergeMethod.squash))
    pr = PullRequest(
        id="e14ff7599399478fb9dbc2dacb87da72",
        number=100,
        author=PullRequestAuthor(login="******", databaseId=49118, type="Bot"),
        mergeStateStatus=MergeStateStatus.BEHIND,
        state=PullRequestState.OPEN,
        isDraft=False,
        mergeable=MergeableState.MERGEABLE,
        isCrossRepository=False,
        labels=["automerge"],
        latest_sha="8d728d017cac4f5ba37533debe65730abe65730a",
        baseRefName="master",
        headRefName="df825f90-9825-424c-a97e-733522027e4c",
        title="Update README.md",
        body="",
        bodyText="",
        bodyHTML="",
        url="https://github.com/delos-corp/hive-mind/pull/324",
    )
    rep_info = RepoInfo(
        merge_commit_allowed=False,
        rebase_merge_allowed=False,
        squash_merge_allowed=True,
        is_private=True,
        delete_branch_on_merge=False,
    )
    branch_protection = BranchProtectionRule(
        requiresApprovingReviews=True,
        requiredApprovingReviewCount=2,
        requiresStatusChecks=True,
        requiredStatusCheckContexts=[
            "ci/circleci: frontend_lint",
            "ci/circleci: frontend_test",
        ],
        requiresStrictStatusChecks=True,
        requiresCodeOwnerReviews=False,
        requiresCommitSignatures=False,
        restrictsPushes=False,
        pushAllowances=NodeListPushAllowance(nodes=[]),
    )

    return EventInfoResponse(
        config=config,
        config_str="""\
version = 1
[merge]
method = "squash"
""",
        config_file_expression="master:.kodiak.toml",
        head_exists=True,
        pull_request=pr,
        repository=rep_info,
        branch_protection=branch_protection,
        review_requests=[],
        reviews=[],
        status_contexts=[],
        check_runs=[],
        valid_signature=True,
        valid_merge_methods=[MergeMethod.squash],
        subscription=None,
    )