From edb4a7591bfb11cedb01a63a3fe23d2d4f2c63c8 Mon Sep 17 00:00:00 2001 From: Novak Zaballa <41410593+novakzaballa@users.noreply.github.com> Date: Fri, 24 May 2024 07:52:32 -0400 Subject: [PATCH] fix: Add support for versioning v2 on GitHub resource linking (#4015) --- api/conftest.py | 2 +- .../feature_external_resources/models.py | 20 +++- api/integrations/github/client.py | 2 +- ...t_unit_feature_external_resources_views.py | 95 +++++++++++++++++-- 4 files changed, 109 insertions(+), 10 deletions(-) diff --git a/api/conftest.py b/api/conftest.py index 31bda86c3a84..4ab7d3f4adbc 100644 --- a/api/conftest.py +++ b/api/conftest.py @@ -563,7 +563,7 @@ def feature_with_value_segment( @pytest.fixture() -def segment_featurestate_and_feature_with_value( +def segment_override_for_feature_with_value( feature_with_value_segment: FeatureSegment, feature_with_value: Feature, environment: Environment, diff --git a/api/features/feature_external_resources/models.py b/api/features/feature_external_resources/models.py index c41c81022bfe..2151cb71b3d5 100644 --- a/api/features/feature_external_resources/models.py +++ b/api/features/feature_external_resources/models.py @@ -1,6 +1,7 @@ import logging from django.db import models +from django.db.models import Q from django_lifecycle import ( AFTER_SAVE, BEFORE_DELETE, @@ -8,6 +9,7 @@ hook, ) +from environments.models import Environment from features.models import Feature, FeatureState from integrations.github.github import call_github_task from organisations.models import Organisation @@ -52,9 +54,23 @@ def execute_after_save_actions(self): .get(id=self.feature.project.organisation_id) .github_config.first() ): - feature_states = FeatureState.objects.filter( - feature_id=self.feature_id, identity_id__isnull=True + feature_states: list[FeatureState] = [] + + environments = Environment.objects.filter( + project_id=self.feature.project_id ) + + for environment in environments: + q = Q( + feature_id=self.feature_id, + identity__isnull=True, + ) + feature_states.extend( + FeatureState.objects.get_live_feature_states( + environment=environment, additional_filters=q + ) + ) + call_github_task( organisation_id=self.feature.project.organisation_id, type=WebhookEventType.FEATURE_EXTERNAL_RESOURCE_ADDED.value, diff --git a/api/integrations/github/client.py b/api/integrations/github/client.py index cb4d0565200e..97a0480ee831 100644 --- a/api/integrations/github/client.py +++ b/api/integrations/github/client.py @@ -73,7 +73,7 @@ def post_comment_to_github( url = f"{GITHUB_API_URL}repos/{owner}/{repo}/issues/{issue}/comments" headers = build_request_headers(installation_id) payload = {"body": body} - response = response = requests.post( + response = requests.post( url, json=payload, headers=headers, timeout=GITHUB_API_CALLS_TIMEOUT ) response.raise_for_status() diff --git a/api/tests/unit/features/test_unit_feature_external_resources_views.py b/api/tests/unit/features/test_unit_feature_external_resources_views.py index f014ca26c6f6..8fba7210dc05 100644 --- a/api/tests/unit/features/test_unit_feature_external_resources_views.py +++ b/api/tests/unit/features/test_unit_feature_external_resources_views.py @@ -82,7 +82,7 @@ def json(self): def test_create_feature_external_resource( admin_client_new: APIClient, feature_with_value: Feature, - segment_featurestate_and_feature_with_value: FeatureState, + segment_override_for_feature_with_value: FeatureState, environment: Environment, project: Project, github_configuration: GithubConfiguration, @@ -123,7 +123,7 @@ def test_create_feature_external_resource( .updated_at.strftime(get_format("DATETIME_INPUT_FORMATS")[0]) ) segment_override_updated_at = FeatureState.objects.get( - id=segment_featurestate_and_feature_with_value.id + id=segment_override_for_feature_with_value.id ).updated_at.strftime(get_format("DATETIME_INPUT_FORMATS")[0]) expected_comment_body = ( @@ -511,7 +511,7 @@ def test_create_github_comment_on_feature_was_deleted( def test_create_github_comment_on_segment_override_updated( feature_with_value: Feature, - segment_featurestate_and_feature_with_value: FeatureState, + segment_override_for_feature_with_value: FeatureState, feature_with_value_external_resource: FeatureExternalResource, project: Project, github_configuration: GithubConfiguration, @@ -521,7 +521,7 @@ def test_create_github_comment_on_segment_override_updated( admin_client: APIClient, ) -> None: # Given - feature_state = segment_featurestate_and_feature_with_value + feature_state = segment_override_for_feature_with_value mock_generate_token = mocker.patch( "integrations.github.client.generate_token", ) @@ -545,7 +545,7 @@ def test_create_github_comment_on_segment_override_updated( # Then segment_override_updated_at = FeatureState.objects.get( - id=segment_featurestate_and_feature_with_value.id + id=segment_override_for_feature_with_value.id ).updated_at.strftime(get_format("DATETIME_INPUT_FORMATS")[0]) expected_comment_body = ( @@ -576,7 +576,7 @@ def test_create_github_comment_on_segment_override_updated( def test_create_github_comment_on_segment_override_deleted( - segment_featurestate_and_feature_with_value: FeatureState, + segment_override_for_feature_with_value: FeatureState, feature_with_value_segment: FeatureSegment, feature_with_value_external_resource: FeatureExternalResource, github_configuration: GithubConfiguration, @@ -753,3 +753,86 @@ def test_create_github_comment_using_v2_fails_on_wrong_params( # Then assert response.status_code == status.HTTP_400_BAD_REQUEST github_request_mock.assert_not_called() + + +@responses.activate +def test_create_feature_external_resource_on_environment_with_v2( + admin_client_new: APIClient, + project: Project, + github_configuration: GithubConfiguration, + github_repository: GithubRepository, + segment_override_for_feature_with_value: FeatureState, + environment_v2_versioning: Environment, + mocker: MockerFixture, +) -> None: + # Given + feature_id = segment_override_for_feature_with_value.feature_id + + mock_generate_token = mocker.patch( + "integrations.github.client.generate_token", + return_value="mocked_token", + ) + + mock_generate_token.return_value = "mocked_token" + github_request_mock = mocker.patch( + "requests.post", side_effect=mocked_requests_post + ) + + feature_external_resource_data = { + "type": "GITHUB_ISSUE", + "url": "https://github.com/repoowner/repo-name/issues/35", + "feature": feature_id, + "metadata": {"state": "open"}, + } + + url = reverse( + "api-v1:projects:feature-external-resources-list", + kwargs={"project_pk": project.id, "feature_pk": feature_id}, + ) + + # When + response = admin_client_new.post( + url, data=feature_external_resource_data, format="json" + ) + + # Then + feature_state_update_at = FeatureState.objects.get( + id=segment_override_for_feature_with_value.id + ).updated_at.strftime(get_format("DATETIME_INPUT_FORMATS")[0]) + + segment_override_updated_at = FeatureState.objects.get( + id=segment_override_for_feature_with_value.id + ).updated_at.strftime(get_format("DATETIME_INPUT_FORMATS")[0]) + + expected_comment_body = ( + "**Flagsmith feature linked:** `feature_with_value`\n" + + "Default Values:\n" + + expected_default_body( + project.id, + environment_v2_versioning.api_key, + feature_id, + feature_state_update_at, + ) + + "\n" + + expected_segment_comment_body( + project.id, + environment_v2_versioning.api_key, + feature_id, + segment_override_updated_at, + "❌ Disabled", + "`value`", + ) + ) + + assert response.status_code == status.HTTP_201_CREATED + + github_request_mock.assert_called_with( + "https://api.github.com/repos/repoowner/repo-name/issues/35/comments", + json={"body": f"{expected_comment_body}"}, + headers={ + "Accept": "application/vnd.github.v3+json", + "X-GitHub-Api-Version": GITHUB_API_VERSION, + "Authorization": "Bearer mocked_token", + }, + timeout=10, + )