async def test_deleting_branch_after_merge( labels: List[str], expected: bool, event_response: queries.EventInfoResponse, mocker: MockFixture, ) -> None: """ ensure client.delete_branch is called when a PR that is already merged is evaluated. """ event_response.pull_request.state = queries.PullRequestState.MERGED event_response.pull_request.labels = labels assert isinstance(event_response.config, V1) event_response.config.merge.delete_branch_on_merge = True mocker.patch.object(PR, "get_event", return_value=wrap_future(event_response)) mocker.patch.object(PR, "set_status", return_value=wrap_future(None)) delete_branch = mocker.patch.object( queries.Client, "delete_branch", return_value=wrap_future(True) ) pr = PR( number=123, owner="tester", repo="repo", installation_id="abc", client=queries.Client(owner="tester", repo="repo", installation_id="abc"), ) await pr.mergeability() assert delete_branch.called == expected
async def test_cross_repo_missing_head( event_response: queries.EventInfoResponse, mocker: MockFixture ) -> None: """ if a repository is from a fork (isCrossRepository), we will not be able to see head information due to a problem with the v4 api failing to return head information for forks, unlike the v3 api. """ event_response.head_exists = False event_response.pull_request.isCrossRepository = True assert event_response.pull_request.mergeStateStatus == MergeStateStatus.BEHIND event_response.pull_request.labels = ["automerge"] assert event_response.branch_protection is not None event_response.branch_protection.requiresApprovingReviews = False event_response.branch_protection.requiresStrictStatusChecks = True mocker.patch.object(PR, "get_event", return_value=wrap_future(event_response)) set_status = mocker.patch.object(PR, "set_status", return_value=wrap_future(None)) pr = PR( number=123, owner="tester", repo="repo", installation_id="abc", client=queries.Client(owner="tester", repo="repo", installation_id="abc"), ) await pr.mergeability() assert set_status.call_count == 1 set_status.assert_called_with( summary=mocker.ANY, markdown_content=messages.FORKS_CANNOT_BE_UPDATED )
async def test_attempting_to_notify_pr_author_with_no_automerge_label( api_client: queries.Client, mocker: MockFixture, event_response: queries.EventInfoResponse, ) -> None: """ ensure that when Kodiak encounters a merge conflict it doesn't notify the user if an automerge label isn't required. """ pr = PR( number=123, owner="ghost", repo="ghost", installation_id="abc123", client=api_client, ) assert isinstance(event_response.config, V1) event_response.config.merge.require_automerge_label = False pr.event = event_response create_comment = mocker.patch.object( PR, "create_comment", return_value=wrap_future(None) ) # mock to ensure we have a chance of hitting the create_comment call mocker.patch.object(PR, "delete_label", return_value=wrap_future(True)) assert await pr.notify_pr_creator() is False assert not create_comment.called
async def test_update_pr_with_retry_failure(pr: PR, mocker: MockFixture) -> None: asyncio_sleep = mocker.patch( "kodiak.queue.asyncio.sleep", return_value=wrap_future(None) ) mocker.patch.object(pr, "update", return_value=wrap_future(False)) res = await update_pr_with_retry(pr) assert not res assert asyncio_sleep.call_count == 5
async def test_mergeability_missing_skippable_checks( mocker: MockFixture, event_response: queries.EventInfoResponse, pr: PR ) -> None: mocker.patch.object(PR, "get_event", return_value=wrap_future(event_response)) mergeable = mocker.patch("kodiak.pull_request.mergeable") mergeable.side_effect = MissingSkippableChecks([]) mocker.patch.object(PR, "set_status", return_value=wrap_future(None)) res, event = await pr.mergeability() assert res == MergeabilityResponse.SKIPPABLE_CHECKS
async def test_pr_update_ok( mocker: MockFixture, event_response: queries.EventInfoResponse, pr: PR ) -> None: """ Update should return true on success """ mocker.patch.object(PR, "get_event", return_value=wrap_future(event_response)) res = Response() res.status_code = 200 mocker.patch( "kodiak.pull_request.queries.Client.merge_branch", return_value=wrap_future(res) ) res = await pr.update() assert res, "should be true when we have a successful call"
async def test_get_event_info_blocked( api_client: Client, blocked_response: dict, block_event: EventInfoResponse, mocker: MockFixture, setup_redis: object, ) -> None: mocker.patch.object( api_client, "send_query", return_value=wrap_future( GraphQLResponse(data=blocked_response.get("data"), errors=blocked_response.get("errors"))), ) async def get_permissions_for_username_patch(username: str) -> Permission: if username in ("walrus", "ghost"): return Permission.WRITE if username in ("kodiak", ): return Permission.ADMIN raise Exception mocker.patch.object(api_client, "get_permissions_for_username", get_permissions_for_username_patch) res = await api_client.get_event_info(branch_name="master", pr_number=100) assert res is not None assert res == block_event
async def test_pr_update_bad_merge( mocker: MockFixture, event_response: queries.EventInfoResponse, pr: PR ) -> None: """ Update should return false on an error """ mocker.patch.object(PR, "get_event", return_value=wrap_future(event_response)) res = Response() res.status_code = 409 res._content = b"{}" mocker.patch( "kodiak.pull_request.queries.Client.merge_branch", return_value=wrap_future(res) ) res = await pr.update() assert not res
def test_webhook_event(client: TestClient, event_name: str, mocker: MockFixture) -> None: """Test all of the events we have""" fake_redis = FakeRedis() mocker.patch("kodiak.entrypoints.ingest.get_redis", return_value=wrap_future(fake_redis)) for index, fixture_path in enumerate( (Path(__file__).parent / "test" / "fixtures" / "events" / event_name).rglob("*.json")): data = json.loads(fixture_path.read_bytes()) body, sha = get_body_and_hash(data) assert fake_redis.called_rpush_cnt == index res = client.post( "/api/github/hook", data=body, headers={ "X-Github-Event": event_name, "X-Hub-Signature": sha }, ) assert res.status_code == status.HTTP_200_OK assert fake_redis.called_rpush_cnt == index + 1 assert fake_redis.called_rpush_cnt == fake_redis.called_ltrim_cnt
async def test_get_config_for_ref_dot_github( api_client: Client, mocker: MockFixture ) -> None: """ We should be able to parse from .github/.kodiak.toml """ mocker.patch.object( api_client, "send_query", return_value=wrap_future( dict( data=dict( repository=dict( rootConfigFile=None, githubConfigFile=dict( text="# .github/.kodiak.toml\nversion = 1\nmerge.method = 'rebase'" ), ) ) ) ), ) res = await api_client.get_config_for_ref(ref="main") assert res is not None assert isinstance(res.parsed, V1) and res.parsed.merge.method == MergeMethod.rebase
async def test_pr_update_missing_event(mocker: MockFixture, pr: PR) -> None: """ Return False if get_event res is missing """ mocker.patch.object(PR, "get_event", return_value=wrap_future(None)) res = await pr.update() assert not res
async def test_get_permissions_for_username_missing( api_client: Client, mocker: MockFixture, mock_get_token_for_install: None ) -> None: not_found = Response() not_found.status_code = 404 mocker.patch("kodiak.queries.http.Session.get", return_value=wrap_future(not_found)) async with api_client as api_client: res = await api_client.get_permissions_for_username("_invalid_username") assert res == Permission.NONE
async def test_get_permissions_for_username_read( api_client: Client, mocker: MockFixture, mock_get_token_for_install: None ) -> None: response = Response() response.status_code = 200 response._content = PERMISSION_OK_READ_USER_RESPONSE mocker.patch("kodiak.queries.http.Session.get", return_value=wrap_future(response)) async with api_client as api_client: res = await api_client.get_permissions_for_username("ghost") assert res == Permission.READ
async def test_get_default_branch_name_error( api_client: Client, mocker: MockFixture ) -> None: mocker.patch.object( api_client, "send_query", return_value=wrap_future(dict(data=None, errors=[{"test": 123}])), ) res = await api_client.get_default_branch_name() assert res is None
async def test_get_subscription_missing_blocker_fully( api_client: Client, mocker: MockFixture, mock_get_token_for_install: None) -> None: """ If a user is new to Kodiak we will not have set subscription information in Redis. We should handle this case by returning an empty subscription. """ fake_redis = create_fake_redis_reply({}) mocker.patch("kodiak.event_handlers.get_redis", return_value=wrap_future(fake_redis)) async with api_client as api_client: res = await api_client.get_subscription() assert res is None
async def test_get_open_pull_requests( mocker: MockFixture, api_client: Client, mock_get_token_for_install: None ) -> None: """ We should stop calling the API after reaching an empty page. """ patched_session_get = mocker.patch( "kodiak.queries.http.Session.get", side_effect=[ wrap_future(generate_page_of_prs(range(1, 101))), wrap_future(generate_page_of_prs(range(101, 201))), wrap_future(generate_page_of_prs(range(201, 251))), wrap_future(generate_page_of_prs([])), ], ) async with api_client as api_client: res = await api_client.get_open_pull_requests() assert res is not None assert len(res) == 250 assert patched_session_get.call_count == 4
def test_webhook_event_missing_github_event(client: TestClient, mocker: MockFixture) -> None: handle_webhook_event = mocker.patch("kodiak.main.handle_webhook_event", return_value=wrap_future(None)) data = {"hello": 123} body, sha = get_body_and_hash(data) assert handle_webhook_event.called is False res = client.post("/api/github/hook", data=body, headers={"X-Hub-Signature": sha}) assert res.status_code == status.HTTP_400_BAD_REQUEST assert handle_webhook_event.called is False
async def test_get_config_for_ref_error( api_client: Client, mocker: MockFixture ) -> None: """ We should return None when there is an error. """ mocker.patch.object( api_client, "send_query", return_value=wrap_future(dict(data=None, errors=[{"test": 123}])), ) res = await api_client.get_config_for_ref(ref="main") assert res is None
def test_webhook_event_missing_github_event(client: TestClient, mocker: MockFixture) -> None: fake_redis = FakeRedis() mocker.patch("kodiak.entrypoints.ingest.get_redis", return_value=wrap_future(fake_redis)) data = {"hello": 123} body, sha = get_body_and_hash(data) assert fake_redis.called_rpush_cnt == 0 res = client.post("/api/github/hook", data=body, headers={"X-Hub-Signature": sha}) assert res.status_code == status.HTTP_400_BAD_REQUEST assert fake_redis.called_rpush_cnt == 0
async def test_get_subscription_missing_blocker_and_data( api_client: Client, mocker: MockFixture, mock_get_token_for_install: None) -> None: """ Check with empty string for data """ fake_redis = create_fake_redis_reply({ b"account_id": b"DF5C23EB-585B-4031-B082-7FF951B4DE15", b"subscription_blocker": b"", b"data": b"", }) mocker.patch("kodiak.event_handlers.get_redis", return_value=wrap_future(fake_redis)) async with api_client as api_client: res = await api_client.get_subscription() assert res == Subscription( account_id="DF5C23EB-585B-4031-B082-7FF951B4DE15", subscription_blocker=None)
async def test_get_open_pull_requests_page_limit( mocker: MockFixture, api_client: Client, mock_get_token_for_install: None ) -> None: """ We should fetch at most 20 pages. """ pages = [range(n, n + 100) for n in range(1, 3001, 100)] assert len(pages) == 30 patched_session_get = mocker.patch( "kodiak.queries.http.Session.get", side_effect=[wrap_future(generate_page_of_prs(p)) for p in pages], ) async with api_client as api_client: res = await api_client.get_open_pull_requests() assert res is not None assert len(res) == 2000 assert patched_session_get.call_count == 20, "stop calling after 20 pages"
async def test_get_subscription_missing_blocker( api_client: Client, mocker: MockFixture, mock_get_token_for_install: None) -> None: """ We set subscription_blocker to empty string from the web_api. This should be consider equivalent to a missing subscription blocker. """ fake_redis = create_fake_redis_reply({ b"account_id": b"DF5C23EB-585B-4031-B082-7FF951B4DE15", b"subscription_blocker": b"", }) mocker.patch("kodiak.event_handlers.get_redis", return_value=wrap_future(fake_redis)) async with api_client as api_client: res = await api_client.get_subscription() assert res == Subscription( account_id="DF5C23EB-585B-4031-B082-7FF951B4DE15", subscription_blocker=None)
async def test_get_event_info_blocked( api_client: Client, blocked_response: Dict[str, Any], block_event: EventInfoResponse, mocker: MockFixture, setup_redis: object, ) -> None: mocker.patch.object( api_client, "send_query", return_value=wrap_future( GraphQLResponse(data=blocked_response.get("data"), errors=blocked_response.get("errors"))), ) res = await api_client.get_event_info(pr_number=100) assert res is not None assert res == block_event
def test_webhook_event_invalid_signature(client: TestClient, mocker: MockFixture) -> None: handle_webhook_event = mocker.patch("kodiak.main.handle_webhook_event", return_value=wrap_future(None)) data = {"hello": 123} # use a different dict for the signature so we get an signature mismatch _, sha = get_body_and_hash({}) assert handle_webhook_event.called is False res = client.post( "/api/github/hook", json=data, headers={ "X-Github-Event": "content_reference", "X-Hub-Signature": sha }, ) assert res.status_code == status.HTTP_400_BAD_REQUEST assert handle_webhook_event.called is False
async def test_get_subscription_unknown_blocker( api_client: Client, mocker: MockFixture, mock_get_token_for_install: None) -> None: """ Handle unknown blocker by allowing user access. """ fake_redis = create_fake_redis_reply({ b"account_id": b"DF5C23EB-585B-4031-B082-7FF951B4DE15", b"subscription_blocker": b"invalid_subscription_blocker", }) mocker.patch("kodiak.event_handlers.get_redis", return_value=wrap_future(fake_redis)) async with api_client as api_client: res = await api_client.get_subscription() assert res == Subscription( account_id="DF5C23EB-585B-4031-B082-7FF951B4DE15", subscription_blocker=None)
async def test_get_subscription_seats_exceeded_invalid_data( api_client: Client, mocker: MockFixture, mock_get_token_for_install: None) -> None: """ Handle invalid data gracefully. """ fake_redis = create_fake_redis_reply({ b"account_id": b"DF5C23EB-585B-4031-B082-7FF951B4DE15", b"subscription_blocker": b"seats_exceeded", b"data": b"*(invalid-data4#", }) mocker.patch("kodiak.event_handlers.get_redis", return_value=wrap_future(fake_redis)) async with api_client as api_client: res = await api_client.get_subscription() assert res == Subscription( account_id="DF5C23EB-585B-4031-B082-7FF951B4DE15", subscription_blocker=SeatsExceeded(allowed_user_ids=[]), )
async def test_get_event_info_blocked( api_client: Client, blocked_response: dict, block_event: EventInfoResponse, mocker: MockFixture, ) -> None: # TODO(sbdchd): we should use monkeypatching # mypy doesn't handle this circular type mocker.patch.object( api_client, "send_query", return_value=wrap_future( GraphQLResponse(data=blocked_response.get("data"), errors=blocked_response.get("errors"))), ) res = await api_client.get_event_info( config_file_expression="master:.kodiak.toml", pr_number=100) assert res is not None assert res == block_event
def test_webhook_event_invalid_signature(client: TestClient, mocker: MockFixture) -> None: fake_redis = FakeRedis() mocker.patch("kodiak.entrypoints.ingest.get_redis", return_value=wrap_future(fake_redis)) data = {"hello": 123} # use a different dict for the signature so we get an signature mismatch _, sha = get_body_and_hash({}) assert fake_redis.called_rpush_cnt == 0 res = client.post( "/api/github/hook", json=data, headers={ "X-Github-Event": "content_reference", "X-Hub-Signature": sha }, ) assert res.status_code == status.HTTP_400_BAD_REQUEST assert fake_redis.called_rpush_cnt == 0
async def test_get_subscription_seats_exceeded_missing_data( api_client: Client, mocker: MockFixture, mock_get_token_for_install: None) -> None: """ For backwards compatibility we cannot guarantee that "seats_exceeded" will have the data parameter. """ fake_redis = create_fake_redis_reply({ b"account_id": b"DF5C23EB-585B-4031-B082-7FF951B4DE15", b"subscription_blocker": b"seats_exceeded", }) mocker.patch("kodiak.event_handlers.get_redis", return_value=wrap_future(fake_redis)) async with api_client as api_client: res = await api_client.get_subscription() assert res == Subscription( account_id="DF5C23EB-585B-4031-B082-7FF951B4DE15", subscription_blocker=SeatsExceeded(allowed_user_ids=[]), )
async def test_get_subscription_seats_exceeded_no_seats( api_client: Client, mocker: MockFixture, mock_get_token_for_install: None) -> None: """ When an account has 0 seats we will not have any allowed_user_ids. """ fake_redis = create_fake_redis_reply({ b"account_id": b"DF5C23EB-585B-4031-B082-7FF951B4DE15", b"subscription_blocker": b"seats_exceeded", b"data": b'{"kind":"seats_exceeded", "allowed_user_ids": []}', }) mocker.patch("kodiak.event_handlers.get_redis", return_value=wrap_future(fake_redis)) async with api_client as api_client: res = await api_client.get_subscription() assert res == Subscription( account_id="DF5C23EB-585B-4031-B082-7FF951B4DE15", subscription_blocker=SeatsExceeded(allowed_user_ids=[]), )