diff --git a/.github/workflows/check-pr.yaml b/.github/workflows/check-pr.yaml index 919a6ca..a2f2c33 100644 --- a/.github/workflows/check-pr.yaml +++ b/.github/workflows/check-pr.yaml @@ -46,6 +46,6 @@ jobs: run: | task test-container env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GH_WATCHER_INTEGRATION_TEST_TOKEN }} TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }} TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} diff --git a/backend/lib/github/clients/gql.py b/backend/lib/github/clients/gql.py index 5d5e816..80949de 100644 --- a/backend/lib/github/clients/gql.py +++ b/backend/lib/github/clients/gql.py @@ -38,7 +38,7 @@ def to_dataclass(self) -> typing.Any: @dataclasses.dataclass(frozen=True) class GetRepositoriesRequest(BaseRequest): - owner: str + owner: github_models.OwnerName limit: int = 100 after: str | None = None @@ -78,9 +78,9 @@ class GetRepositoriesResponse(BaseResponse): class Search(BaseModel): class Repository(BaseModel): class Owner(BaseModel): - login: str + login: github_models.OwnerName - name: str + name: github_models.RepositoryName owner: Owner class PageInfo(BaseModel): @@ -104,8 +104,8 @@ def to_dataclass(self) -> list[github_models.Repository]: @dataclasses.dataclass(frozen=True) class GetRepositoryIssuesRequest(BaseRequest): - owner: str - repository: str + owner: github_models.OwnerName + repository: github_models.RepositoryName created_after: datetime.datetime limit: int = 100 @@ -150,7 +150,7 @@ class GetRepositoryIssuesResponse(BaseResponse): class Search(BaseModel): class Issue(BaseModel): class Author(BaseModel): - login: str + login: github_models.UserLogin id: str url: str @@ -179,8 +179,8 @@ def to_dataclass(self) -> list[github_models.Issue]: @dataclasses.dataclass(frozen=True) class GetRepositoryPRsRequest(BaseRequest): - owner: str - repository: str + owner: github_models.OwnerName + repository: github_models.RepositoryName created_after: datetime.datetime limit: int = 100 @@ -225,7 +225,7 @@ class GetRepositoryPRsResponse(BaseResponse): class Search(BaseModel): class PR(BaseModel): class Author(BaseModel): - login: str + login: github_models.UserLogin id: str url: str diff --git a/backend/lib/github/clients/rest.py b/backend/lib/github/clients/rest.py index 45fa0c9..93d05dd 100644 --- a/backend/lib/github/clients/rest.py +++ b/backend/lib/github/clients/rest.py @@ -5,6 +5,7 @@ import typing import aiohttp +import pydantic import lib.github.models as github_models import lib.utils.pydantic as pydantic_utils @@ -31,6 +32,7 @@ def to_dataclass(self) -> typing.Any: raise NotImplementedError +# https://docs.github.com/en/rest/actions/workflow-runs#list-workflow-runs-for-a-repository @dataclasses.dataclass(frozen=True) class GetRepositoryWorkflowRunsRequest(BaseRequest): owner: str @@ -84,11 +86,55 @@ def to_dataclass(self) -> list[github_models.WorkflowRun]: ] +# https://docs.github.com/en/rest/teams/members#list-team-members +@dataclasses.dataclass(frozen=True) +class GetOrganizationTeamMembersRequest(BaseRequest): + owner: github_models.OwnerName + team_slug: github_models.TeamSlug + + role: typing.Literal["member", "maintainer", "all"] = "all" + per_page: int = 100 + page: int = 1 + + @property + def method(self) -> str: + return "GET" + + @property + def url(self) -> str: + return f"https://api.github.com/orgs/{self.owner}/teams/{self.team_slug}/members" + + @property + def params(self) -> dict[str, typing.Any]: + return { + "role": self.role, + "per_page": self.per_page, + "page": self.page, + } + + +class _TeamMember(pydantic_utils.BaseModel): + login: github_models.UserLogin + + +class GetOrganizationTeamMembersResponse(BaseResponse, pydantic.RootModel[list[_TeamMember]]): + root: list[_TeamMember] + + def to_dataclass(self) -> list[github_models.UserLogin]: + return [member.login for member in self.root] + + @dataclasses.dataclass(frozen=True) class RestGithubClient: aiohttp_client: aiohttp.ClientSession token: str + class BaseError(Exception): ... + + class NotFoundError(BaseError): ... + + class UnknownResponseError(BaseError): ... + @classmethod def from_token(cls, token: str) -> typing.Self: aiohttp_client = aiohttp.ClientSession() @@ -97,11 +143,10 @@ def from_token(cls, token: str) -> typing.Self: async def dispose(self) -> None: await self.aiohttp_client.close() - async def _request[ResponseT: BaseResponse]( + async def _request( self, request: BaseRequest, - response_model: type[ResponseT], - ) -> ResponseT: + ) -> typing.Any: headers = { "Authorization": f"Bearer {self.token}", "Accept": "application/vnd.github.v3+json", @@ -114,14 +159,19 @@ async def _request[ResponseT: BaseResponse]( headers=headers, ) as response: response.raise_for_status() - data = await response.json() - return response_model.model_validate(data) + return await response.json() async def _get_repository_workflow_runs( self, request: GetRepositoryWorkflowRunsRequest, ) -> GetRepositoryWorkflowRunsResponse: - return await self._request(request, GetRepositoryWorkflowRunsResponse) + try: + raw_data = await self._request(request) + except aiohttp.ClientResponseError as e: # pragma: no cover + logger.exception("Unknown response error") + raise self.UnknownResponseError from e + + return GetRepositoryWorkflowRunsResponse.model_validate(raw_data) async def get_repository_workflow_runs( self, @@ -147,8 +197,46 @@ async def get_repository_workflow_runs( page += 1 + async def _get_organization_team_members( + self, + request: GetOrganizationTeamMembersRequest, + ) -> GetOrganizationTeamMembersResponse: + try: + raw_data = await self._request(request) + return GetOrganizationTeamMembersResponse.model_validate(raw_data) + except aiohttp.ClientResponseError as e: + if e.status == 404: + logger.info(e.message) + raise self.NotFoundError from e + + logger.exception("Unknown response error") # pragma: no cover + raise self.UnknownResponseError from e # pragma: no cover + + async def get_organization_team_members( + self, + request: GetOrganizationTeamMembersRequest, + ) -> typing.AsyncGenerator[github_models.UserLogin, None]: + page = request.page + while True: + response = await self._get_organization_team_members( + request=GetOrganizationTeamMembersRequest( + owner=request.owner, + team_slug=request.team_slug, + page=page, + per_page=request.per_page, + ), + ) + if not response.root: + return + + for member in response.to_dataclass(): + yield member + + page += 1 + __all__ = [ + "GetOrganizationTeamMembersRequest", "GetRepositoryWorkflowRunsRequest", "RestGithubClient", ] diff --git a/backend/lib/github/models.py b/backend/lib/github/models.py index 5c913c8..53aa6d7 100644 --- a/backend/lib/github/models.py +++ b/backend/lib/github/models.py @@ -1,17 +1,26 @@ import dataclasses import datetime +type UserLogin = str +type OrganizationName = str +type OwnerName = UserLogin | OrganizationName +type RepositoryName = str +type TeamSlug = str + +type WorkflowRunStatus = str +type WorkflowRunConclusion = str + @dataclasses.dataclass(frozen=True) class Repository: - owner: str - name: str + owner: OwnerName + name: RepositoryName @dataclasses.dataclass(frozen=True) class Issue: id: str - author: str | None + author: UserLogin | None url: str title: str body: str @@ -21,7 +30,7 @@ class Issue: @dataclasses.dataclass(frozen=True) class PullRequest: id: str - author: str | None + author: UserLogin | None url: str title: str body: str @@ -33,14 +42,21 @@ class WorkflowRun: id: int name: str url: str - status: str - conclusion: str | None + status: WorkflowRunStatus + conclusion: WorkflowRunConclusion | None created_at: datetime.datetime __all__ = [ "Issue", + "OrganizationName", + "OwnerName", "PullRequest", "Repository", + "RepositoryName", + "TeamSlug", + "UserLogin", "WorkflowRun", + "WorkflowRunConclusion", + "WorkflowRunStatus", ] diff --git a/backend/lib/github/triggers.py b/backend/lib/github/triggers.py index d54d81a..dbcc687 100644 --- a/backend/lib/github/triggers.py +++ b/backend/lib/github/triggers.py @@ -18,7 +18,7 @@ logger = logging.getLogger(__name__) -class SubtriggerConfig(pydantic_utils.TypedBaseModel, pydantic_utils.IDMixinModel): +class BaseSubtriggerConfig(pydantic_utils.TypedBaseModel, pydantic_utils.IDMixinModel): @pydantic.model_validator(mode="before") @classmethod def set_default_id(cls, data: dict[str, typing.Any]) -> dict[str, typing.Any]: @@ -27,18 +27,18 @@ def set_default_id(cls, data: dict[str, typing.Any]) -> dict[str, typing.Any]: return data -def _check_match(string: str, items: list[str] | None = None) -> bool: +def _check_match(string: str, items: set[str] | None = None) -> bool: return items is not None and len(items) != 0 and string in items -def _check_regex_match(string: str, regex_items: list[str] | None = None) -> bool: +def _check_regex_match(string: str, regex_items: set[str] | None = None) -> bool: return regex_items is not None and len(regex_items) != 0 and any(re.match(regex, string) for regex in regex_items) def _check_included( string: str, - include: list[str] | None = None, - include_regex: list[str] | None = None, + include: set[str] | None = None, + include_regex: set[str] | None = None, ) -> bool: return ( (not include and not include_regex) @@ -49,35 +49,43 @@ def _check_included( def _check_excluded( string: str, - exclude: list[str] | None = None, - exclude_regex: list[str] | None = None, + exclude: set[str] | None = None, + exclude_regex: set[str] | None = None, ) -> bool: return _check_match(string, exclude) or _check_regex_match(string, exclude_regex) def _check_applicable( string: str, - include: list[str] | None = None, - exclude: list[str] | None = None, - include_regex: list[str] | None = None, - exclude_regex: list[str] | None = None, + include: set[str] | None = None, + exclude: set[str] | None = None, + include_regex: set[str] | None = None, + exclude_regex: set[str] | None = None, ) -> bool: return _check_included(string, include=include, include_regex=include_regex) and not _check_excluded( string, exclude=exclude, exclude_regex=exclude_regex ) -class RepositoryIssueCreatedSubtriggerConfig(SubtriggerConfig): +class RepositoryIssueCreatedSubtriggerConfig(BaseSubtriggerConfig): type_name: str = "repository_issue_created" - include_author: list[str] = pydantic.Field(default_factory=list) - exclude_author: list[str] = pydantic.Field(default_factory=list) - include_title: list[str] = pydantic.Field(default_factory=list) - exclude_title: list[str] = pydantic.Field(default_factory=list) - include_title_regex: list[str] = pydantic.Field(default_factory=list) - exclude_title_regex: list[str] = pydantic.Field(default_factory=list) + include_author: set[github_models.UserLogin] = pydantic.Field(default_factory=set) + exclude_author: set[github_models.UserLogin] = pydantic.Field(default_factory=set) + include_author_group: set[github_models.TeamSlug] = pydantic.Field(default_factory=set) + exclude_author_group: set[github_models.TeamSlug] = pydantic.Field(default_factory=set) + + include_title: set[str] = pydantic.Field(default_factory=set) + exclude_title: set[str] = pydantic.Field(default_factory=set) + include_title_regex: set[str] = pydantic.Field(default_factory=set) + exclude_title_regex: set[str] = pydantic.Field(default_factory=set) def is_applicable(self, issue: github_models.Issue) -> bool: + if self.include_author_group: + raise NotImplementedError("include_author_group must be resolved before is_applicable") + if self.exclude_author_group: + raise NotImplementedError("exclude_author_group must be resolved before is_applicable") + if issue.author is not None and not _check_applicable( issue.author, include=self.include_author, @@ -96,13 +104,23 @@ def is_applicable(self, issue: github_models.Issue) -> bool: return True -class RepositoryPRCreatedSubtriggerConfig(SubtriggerConfig): +class RepositoryPRCreatedSubtriggerConfig(BaseSubtriggerConfig): type_name: str = "repository_pr_created" - include_author: list[str] = pydantic.Field(default_factory=list) - exclude_author: list[str] = pydantic.Field(default_factory=list) + include_author: set[github_models.UserLogin] = pydantic.Field(default_factory=set) + exclude_author: set[github_models.UserLogin] = pydantic.Field(default_factory=set) + include_author_group: set[github_models.TeamSlug] = pydantic.Field(default_factory=set) + exclude_author_group: set[github_models.TeamSlug] = pydantic.Field(default_factory=set) + + def is_applicable( + self, + pr: github_models.PullRequest, + ) -> bool: + if self.include_author_group: + raise NotImplementedError("include_author_group must be resolved before is_applicable") + if self.exclude_author_group: + raise NotImplementedError("exclude_author_group must be resolved before is_applicable") - def is_applicable(self, pr: github_models.PullRequest) -> bool: if pr.author is not None and not _check_applicable( pr.author, include=self.include_author, @@ -113,11 +131,11 @@ def is_applicable(self, pr: github_models.PullRequest) -> bool: return True -class RepositoryFailedWorkflowRunSubtriggerConfig(SubtriggerConfig): +class RepositoryFailedWorkflowRunSubtriggerConfig(BaseSubtriggerConfig): type_name: str = "repository_failed_workflow_run" - include: list[str] = pydantic.Field(default_factory=list) - exclude: list[str] = pydantic.Field(default_factory=list) + include: set[str] = pydantic.Field(default_factory=set) + exclude: set[str] = pydantic.Field(default_factory=set) def is_applicable(self, workflow_run: github_models.WorkflowRun) -> bool: if workflow_run.status != "completed": @@ -137,13 +155,13 @@ def is_applicable(self, workflow_run: github_models.WorkflowRun) -> bool: class GithubTriggerConfig(task_base.BaseTriggerConfig): token_secret: pydantic_utils.TypedAnnotation[task_base.BaseSecretConfig] owner: str - repos: list[str] = pydantic.Field(default_factory=list) # TODO 1.0.0: remove - include_repos: list[str] = pydantic.Field(default_factory=list) - exclude_repos: list[str] = pydantic.Field(default_factory=list) + repos: list[github_models.RepositoryName] = pydantic.Field(default_factory=list) # TODO 1.0.0: remove + include_repos: list[github_models.RepositoryName] = pydantic.Field(default_factory=list) + exclude_repos: list[github_models.RepositoryName] = pydantic.Field(default_factory=list) default_timedelta_seconds: int = 60 * 60 * 24 # 1 day subtriggers: typing.Annotated[ - list[pydantic.SerializeAsAny[SubtriggerConfig]], - pydantic.BeforeValidator(SubtriggerConfig.list_factory), + list[pydantic.SerializeAsAny[BaseSubtriggerConfig]], + pydantic.BeforeValidator(BaseSubtriggerConfig.list_factory), pydantic.AfterValidator(pydantic_utils.check_unique_ids), ] @@ -256,7 +274,8 @@ async def _acquire_state(self) -> typing.AsyncIterator[GithubTriggerState]: try: yield state finally: - await self.raw_state.set(state.model_dump(mode="json")) + raw_state = state.model_dump(mode="json") + await self.raw_state.set(raw_state) async def _get_repositories(self) -> list[github_models.Repository]: request = github_clients.GetRepositoriesRequest( @@ -271,17 +290,30 @@ async def _get_repositories(self) -> list[github_models.Repository]: return result + async def _resolve_author_groups(self, groups: set[github_models.TeamSlug]) -> set[github_models.UserLogin]: + result: set[github_models.UserLogin] = set() + for group in groups: + async for member in self.rest_github_client.get_organization_team_members( + request=github_clients.GetOrganizationTeamMembersRequest( + owner=self.config.owner, + team_slug=group, + ), + ): + result.add(member) + + return result + async def produce_events(self) -> typing.AsyncGenerator[task_base.Event, None]: repositories = await self._get_repositories() async with self._acquire_state() as state: iterators = ( self._process_subtrigger_factory( - subtrigger=subtrigger, + config=subtrigger_config, state=state, repositories=repositories, ) - for subtrigger in self.config.subtriggers + for subtrigger_config in self.config.subtriggers ) async for event in asyncio_utils.GatherIterators(iterators): @@ -289,41 +321,46 @@ async def produce_events(self) -> typing.AsyncGenerator[task_base.Event, None]: def _process_subtrigger_factory( self, - subtrigger: SubtriggerConfig, + config: BaseSubtriggerConfig, state: GithubTriggerState, repositories: list[github_models.Repository], ) -> typing.AsyncGenerator[task_base.Event, None]: - if isinstance(subtrigger, RepositoryIssueCreatedSubtriggerConfig): + if isinstance(config, RepositoryIssueCreatedSubtriggerConfig): return self._process_all_repository_issue_created( state=state, - subtrigger=subtrigger, + config=config, repositories=repositories, ) - if isinstance(subtrigger, RepositoryPRCreatedSubtriggerConfig): + if isinstance(config, RepositoryPRCreatedSubtriggerConfig): return self._process_all_repository_pr_created( state=state, - subtrigger=subtrigger, + config=config, repositories=repositories, ) - if isinstance(subtrigger, RepositoryFailedWorkflowRunSubtriggerConfig): + if isinstance(config, RepositoryFailedWorkflowRunSubtriggerConfig): return self._process_all_repository_failed_workflow_run( state=state, - subtrigger=subtrigger, + config=config, repositories=repositories, ) - raise ValueError(f"Unknown subtrigger: {subtrigger}") + raise ValueError(f"Unknown subtrigger: {config}") async def _process_all_repository_issue_created( self, state: GithubTriggerState, - subtrigger: RepositoryIssueCreatedSubtriggerConfig, + config: RepositoryIssueCreatedSubtriggerConfig, repositories: list[github_models.Repository], ) -> typing.AsyncGenerator[task_base.Event, None]: + config.include_author |= await self._resolve_author_groups(config.include_author_group) + config.include_author_group = set() + config.exclude_author |= await self._resolve_author_groups(config.exclude_author_group) + config.exclude_author_group = set() + async for event in asyncio_utils.GatherIterators( self._process_repository_issue_created( state=state, - subtrigger=subtrigger, + config=config, repository=repository.name, ) for repository in repositories @@ -333,7 +370,7 @@ async def _process_all_repository_issue_created( async def _process_repository_issue_created( self, state: GithubTriggerState, - subtrigger: RepositoryIssueCreatedSubtriggerConfig, + config: RepositoryIssueCreatedSubtriggerConfig, repository: str, ) -> typing.AsyncGenerator[task_base.Event, None]: if repository not in state.repository_issue_created: @@ -355,7 +392,7 @@ async def _process_repository_issue_created( break for issue in issues: - if subtrigger.is_applicable(issue): + if config.is_applicable(issue): yield task_base.Event( id=f"issue_created__{issue.id}", title=f"📋New issue in {self.config.owner}/{repository}", @@ -368,13 +405,18 @@ async def _process_repository_issue_created( async def _process_all_repository_pr_created( self, state: GithubTriggerState, - subtrigger: RepositoryPRCreatedSubtriggerConfig, + config: RepositoryPRCreatedSubtriggerConfig, repositories: list[github_models.Repository], ) -> typing.AsyncGenerator[task_base.Event, None]: + config.include_author |= await self._resolve_author_groups(config.include_author_group) + config.include_author_group = set() + config.exclude_author |= await self._resolve_author_groups(config.exclude_author_group) + config.exclude_author_group = set() + async for event in asyncio_utils.GatherIterators( self._process_repository_pr_created( state=state, - subtrigger=subtrigger, + config=config, repository=repository.name, ) for repository in repositories @@ -384,7 +426,7 @@ async def _process_all_repository_pr_created( async def _process_repository_pr_created( self, state: GithubTriggerState, - subtrigger: RepositoryPRCreatedSubtriggerConfig, + config: RepositoryPRCreatedSubtriggerConfig, repository: str, ) -> typing.AsyncGenerator[task_base.Event, None]: if repository not in state.repository_pr_created: @@ -406,7 +448,7 @@ async def _process_repository_pr_created( break for pr in prs: - if subtrigger.is_applicable(pr): + if config.is_applicable(pr): yield task_base.Event( id=f"pr_created__{pr.id}", title=f"🛠New PR in {self.config.owner}/{repository}", @@ -419,13 +461,13 @@ async def _process_repository_pr_created( async def _process_all_repository_failed_workflow_run( self, state: GithubTriggerState, - subtrigger: RepositoryFailedWorkflowRunSubtriggerConfig, + config: RepositoryFailedWorkflowRunSubtriggerConfig, repositories: list[github_models.Repository], ) -> typing.AsyncGenerator[task_base.Event, None]: async for event in asyncio_utils.GatherIterators( self._process_repository_failed_workflow_run( state=state, - subtrigger=subtrigger, + config=config, repository=repository.name, ) for repository in repositories @@ -435,7 +477,7 @@ async def _process_all_repository_failed_workflow_run( async def _process_repository_failed_workflow_run( self, state: GithubTriggerState, - subtrigger: RepositoryFailedWorkflowRunSubtriggerConfig, + config: RepositoryFailedWorkflowRunSubtriggerConfig, repository: str, ) -> typing.AsyncGenerator[task_base.Event, None]: if repository not in state.repository_failed_workflow_run: @@ -454,10 +496,7 @@ async def _process_repository_failed_workflow_run( created_after=repository_state.oldest_incomplete_created, ), ): - if ( - subtrigger.is_applicable(workflow_run) - and workflow_run not in repository_state.already_reported_failed_runs - ): + if config.is_applicable(workflow_run) and workflow_run not in repository_state.already_reported_failed_runs: yield task_base.Event( id=f"failed_workflow_run__{self.config.owner}__{repository}__{workflow_run.id}", title=f"🔥Failed workflow run in {self.config.owner}/{repository}", @@ -494,9 +533,9 @@ def register_default_plugins() -> None: processor_class=GithubTriggerProcessor, ) - SubtriggerConfig.register("repository_issue_created", RepositoryIssueCreatedSubtriggerConfig) - SubtriggerConfig.register("repository_pr_created", RepositoryPRCreatedSubtriggerConfig) - SubtriggerConfig.register("repository_failed_workflow_run", RepositoryFailedWorkflowRunSubtriggerConfig) + BaseSubtriggerConfig.register("repository_issue_created", RepositoryIssueCreatedSubtriggerConfig) + BaseSubtriggerConfig.register("repository_pr_created", RepositoryPRCreatedSubtriggerConfig) + BaseSubtriggerConfig.register("repository_failed_workflow_run", RepositoryFailedWorkflowRunSubtriggerConfig) __all__ = [ diff --git a/backend/tests/integration/github/clients/rest/test_get_organization_team_members.py b/backend/tests/integration/github/clients/rest/test_get_organization_team_members.py new file mode 100644 index 0000000..71bde24 --- /dev/null +++ b/backend/tests/integration/github/clients/rest/test_get_organization_team_members.py @@ -0,0 +1,54 @@ +import pytest + +import lib.github.clients as github_clients + + +@pytest.mark.asyncio +async def test_default(github_rest_client: github_clients.RestGithubClient): + iterator = github_rest_client.get_organization_team_members( + request=github_clients.GetOrganizationTeamMembersRequest( + owner="ovsds-example-organization", + team_slug="github-watcher-repository-test", + ), + ) + + members = [member async for member in iterator] + assert members == ["ovsds", "ovsds-personal-robot"] + + +@pytest.mark.asyncio +async def test_per_page(github_rest_client: github_clients.RestGithubClient): + iterator = github_rest_client.get_organization_team_members( + request=github_clients.GetOrganizationTeamMembersRequest( + owner="ovsds-example-organization", + team_slug="github-watcher-repository-test", + per_page=1, + ), + ) + + members = [member async for member in iterator] + assert members == ["ovsds", "ovsds-personal-robot"] + + +@pytest.mark.asyncio +async def test_no_team(github_rest_client: github_clients.RestGithubClient): + iterator = github_rest_client.get_organization_team_members( + request=github_clients.GetOrganizationTeamMembersRequest( + owner="ovsds-example-organization", + team_slug="github-watcher-repository-test-not-exists", + ), + ) + with pytest.raises(github_clients.RestGithubClient.NotFoundError): + await anext(iterator) + + +@pytest.mark.asyncio +async def test_no_organization(github_rest_client: github_clients.RestGithubClient): + iterator = github_rest_client.get_organization_team_members( + request=github_clients.GetOrganizationTeamMembersRequest( + owner="ovsds-example-organization-not-exists", + team_slug="github-watcher-repository-test", + ), + ) + with pytest.raises(github_clients.RestGithubClient.NotFoundError): + await anext(iterator) diff --git a/backend/tests/unit/github/triggers/test_repository_issue_created_subtrigger_config.py b/backend/tests/unit/github/triggers/test_repository_issue_created_subtrigger_config.py index 1074ddf..8bf343f 100644 --- a/backend/tests/unit/github/triggers/test_repository_issue_created_subtrigger_config.py +++ b/backend/tests/unit/github/triggers/test_repository_issue_created_subtrigger_config.py @@ -19,7 +19,7 @@ def issue_fixture() -> github_models.Issue: def test_is_applicable_default(issue: github_models.Issue): - config = github_triggers.SubtriggerConfig.factory( + config = github_triggers.BaseSubtriggerConfig.factory( data={ "type": "repository_issue_created", "id": "test_id", @@ -30,7 +30,7 @@ def test_is_applicable_default(issue: github_models.Issue): def test_is_applicable_include_author(issue: github_models.Issue): - config = github_triggers.SubtriggerConfig.factory( + config = github_triggers.BaseSubtriggerConfig.factory( data={ "type": "repository_issue_created", "id": "test_id", @@ -42,7 +42,7 @@ def test_is_applicable_include_author(issue: github_models.Issue): def test_is_applicable_include_author_not_matching(issue: github_models.Issue): - config = github_triggers.SubtriggerConfig.factory( + config = github_triggers.BaseSubtriggerConfig.factory( data={ "type": "repository_issue_created", "id": "test_id", @@ -54,7 +54,7 @@ def test_is_applicable_include_author_not_matching(issue: github_models.Issue): def test_is_applicable_exclude_author(issue: github_models.Issue): - config = github_triggers.SubtriggerConfig.factory( + config = github_triggers.BaseSubtriggerConfig.factory( data={ "type": "repository_issue_created", "id": "test_id", @@ -66,7 +66,7 @@ def test_is_applicable_exclude_author(issue: github_models.Issue): def test_is_applicable_exclude_author_matching(issue: github_models.Issue): - config = github_triggers.SubtriggerConfig.factory( + config = github_triggers.BaseSubtriggerConfig.factory( data={ "type": "repository_issue_created", "id": "test_id", @@ -78,7 +78,7 @@ def test_is_applicable_exclude_author_matching(issue: github_models.Issue): def test_is_applicable_include_and_exclude_author(issue: github_models.Issue): - config = github_triggers.SubtriggerConfig.factory( + config = github_triggers.BaseSubtriggerConfig.factory( data={ "type": "repository_issue_created", "id": "test_id", @@ -91,7 +91,7 @@ def test_is_applicable_include_and_exclude_author(issue: github_models.Issue): def test_is_applicable_include_title(issue: github_models.Issue): - config = github_triggers.SubtriggerConfig.factory( + config = github_triggers.BaseSubtriggerConfig.factory( data={ "type": "repository_issue_created", "id": "test_id", @@ -103,7 +103,7 @@ def test_is_applicable_include_title(issue: github_models.Issue): def test_is_applicable_include_title_not_matching(issue: github_models.Issue): - config = github_triggers.SubtriggerConfig.factory( + config = github_triggers.BaseSubtriggerConfig.factory( data={ "type": "repository_issue_created", "id": "test_id", @@ -115,7 +115,7 @@ def test_is_applicable_include_title_not_matching(issue: github_models.Issue): def test_is_applicable_exclude_title(issue: github_models.Issue): - config = github_triggers.SubtriggerConfig.factory( + config = github_triggers.BaseSubtriggerConfig.factory( data={ "type": "repository_issue_created", "id": "test_id", @@ -127,7 +127,7 @@ def test_is_applicable_exclude_title(issue: github_models.Issue): def test_is_applicable_exclude_title_matching(issue: github_models.Issue): - config = github_triggers.SubtriggerConfig.factory( + config = github_triggers.BaseSubtriggerConfig.factory( data={ "type": "repository_issue_created", "id": "test_id", @@ -139,7 +139,7 @@ def test_is_applicable_exclude_title_matching(issue: github_models.Issue): def test_is_applicable_include_and_exclude_title(issue: github_models.Issue): - config = github_triggers.SubtriggerConfig.factory( + config = github_triggers.BaseSubtriggerConfig.factory( data={ "type": "repository_issue_created", "id": "test_id", @@ -152,7 +152,7 @@ def test_is_applicable_include_and_exclude_title(issue: github_models.Issue): def test_is_applicable_include_regex_title(issue: github_models.Issue): - config = github_triggers.SubtriggerConfig.factory( + config = github_triggers.BaseSubtriggerConfig.factory( data={ "type": "repository_issue_created", "id": "test_id", @@ -164,7 +164,7 @@ def test_is_applicable_include_regex_title(issue: github_models.Issue): def test_is_applicable_include_regex_title_not_matching(issue: github_models.Issue): - config = github_triggers.SubtriggerConfig.factory( + config = github_triggers.BaseSubtriggerConfig.factory( data={ "type": "repository_issue_created", "id": "test_id", @@ -176,7 +176,7 @@ def test_is_applicable_include_regex_title_not_matching(issue: github_models.Iss def test_is_applicable_exclude_regex_title(issue: github_models.Issue): - config = github_triggers.SubtriggerConfig.factory( + config = github_triggers.BaseSubtriggerConfig.factory( data={ "type": "repository_issue_created", "id": "test_id", @@ -188,7 +188,7 @@ def test_is_applicable_exclude_regex_title(issue: github_models.Issue): def test_is_applicable_exclude_regex_title_matching(issue: github_models.Issue): - config = github_triggers.SubtriggerConfig.factory( + config = github_triggers.BaseSubtriggerConfig.factory( data={ "type": "repository_issue_created", "id": "test_id", @@ -200,7 +200,7 @@ def test_is_applicable_exclude_regex_title_matching(issue: github_models.Issue): def test_is_applicable_include_and_exclude_regex_title(issue: github_models.Issue): - config = github_triggers.SubtriggerConfig.factory( + config = github_triggers.BaseSubtriggerConfig.factory( data={ "type": "repository_issue_created", "id": "test_id", @@ -213,7 +213,7 @@ def test_is_applicable_include_and_exclude_regex_title(issue: github_models.Issu def test_is_applicable_include_and_include_regex_title(issue: github_models.Issue): - config = github_triggers.SubtriggerConfig.factory( + config = github_triggers.BaseSubtriggerConfig.factory( data={ "type": "repository_issue_created", "id": "test_id", diff --git a/backend/tests/unit/github/triggers/test_repository_pr_created_subtrigger_config.py b/backend/tests/unit/github/triggers/test_repository_pr_created_subtrigger_config.py index 0341260..b6c8dac 100644 --- a/backend/tests/unit/github/triggers/test_repository_pr_created_subtrigger_config.py +++ b/backend/tests/unit/github/triggers/test_repository_pr_created_subtrigger_config.py @@ -27,7 +27,7 @@ def test_is_applicable_default(pr: github_models.PullRequest): def test_is_applicable_include_author(pr: github_models.PullRequest): - config = github_triggers.SubtriggerConfig.factory( + config = github_triggers.BaseSubtriggerConfig.factory( data={ "type": "repository_pr_created", "id": "test_id", @@ -39,7 +39,7 @@ def test_is_applicable_include_author(pr: github_models.PullRequest): def test_is_applicable_include_author_not_matching(pr: github_models.PullRequest): - config = github_triggers.SubtriggerConfig.factory( + config = github_triggers.BaseSubtriggerConfig.factory( data={ "type": "repository_pr_created", "id": "test_id", @@ -51,7 +51,7 @@ def test_is_applicable_include_author_not_matching(pr: github_models.PullRequest def test_is_applicable_exclude_author(pr: github_models.PullRequest): - config = github_triggers.SubtriggerConfig.factory( + config = github_triggers.BaseSubtriggerConfig.factory( data={ "type": "repository_pr_created", "id": "test_id", @@ -63,7 +63,7 @@ def test_is_applicable_exclude_author(pr: github_models.PullRequest): def test_is_applicable_exclude_author_matching(pr: github_models.PullRequest): - config = github_triggers.SubtriggerConfig.factory( + config = github_triggers.BaseSubtriggerConfig.factory( data={ "type": "repository_pr_created", "id": "test_id",