diff --git a/Dockerfile b/Dockerfile index 9f398ab1b..5ea5de0b3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ RUN /node_modules/.bin/jest FROM public.ecr.aws/docker/library/python:3.11-alpine3.18 AS base -ARG HELM_VERSION=3.5.4 +ARG HELM_VERSION=3.14.1 ARG HELM_TARBALL=helm-v${HELM_VERSION}-linux-amd64.tar.gz ARG HELM_BASEURL=https://get.helm.sh diff --git a/controlpanel/api/cluster.py b/controlpanel/api/cluster.py index bcc068801..1a1df2459 100644 --- a/controlpanel/api/cluster.py +++ b/controlpanel/api/cluster.py @@ -389,7 +389,6 @@ class App(EntityResource): AUTHENTICATION_REQUIRED = "AUTHENTICATION_REQUIRED" AUTH0_PASSWORDLESS = "AUTH0_PASSWORDLESS" APP_ROLE_ARN = "APP_ROLE_ARN" - DATA_ACCOUNT_ID = 'DATA_ACCOUNT_ID' def __init__(self, app, github_api_token=None, auth0_instance=None): super(App, self).__init__() @@ -415,7 +414,6 @@ def _create_secrets(self, env_name, client=None): secret_data: dict = { App.IP_RANGES: self.app.env_allowed_ip_ranges(env_name=env_name), App.APP_ROLE_ARN: self.app.iam_role_arn, - App.DATA_ACCOUNT_ID: settings.AWS_DATA_ACCOUNT_ID } if client: secret_data[App.AUTH0_CLIENT_ID] = client["client_id"] @@ -501,7 +499,7 @@ def oidc_provider_statement(self): "identity_provider_arn": iam_arn( f"oidc-provider/{settings.OIDC_APP_EKS_PROVIDER}" ), - "app_name": self.app.slug, + "app_namespace": self.app.namespace, } ) return json.loads(statement) @@ -510,6 +508,8 @@ def create_iam_role(self): assume_role_policy = deepcopy(BASE_ASSUME_ROLE_POLICY) assume_role_policy["Statement"].append(self.oidc_provider_statement) self.aws_role_service.create_role(self.iam_role_name, assume_role_policy) + for env in self.get_deployment_envs(): + self._create_secrets(env_name=env) def grant_bucket_access(self, bucket_arn, access_level, path_arns): self.aws_role_service.grant_bucket_access( @@ -534,24 +534,27 @@ def format_github_key_name(key_name): Format the self-defined secret/variable by adding prefix if create/update value back to github and there is no prefix in the name """ - if key_name not in settings.AUTH_SETTINGS_ENVS \ - and key_name not in settings.AUTH_SETTINGS_SECRETS: - if not key_name.startswith(settings.APP_SELF_DEFINE_SETTING_PREFIX): - return f"{settings.APP_SELF_DEFINE_SETTING_PREFIX}{key_name}" - return key_name + if key_name in settings.AUTH_SETTINGS_ENVS: + return key_name + + if key_name in settings.AUTH_SETTINGS_SECRETS: + return key_name + + if key_name.startswith(settings.APP_SELF_DEFINE_SETTING_PREFIX): + return key_name + + return f"{settings.APP_SELF_DEFINE_SETTING_PREFIX}{key_name}" @staticmethod - def get_github_key_display_name(key_name): + def get_github_key_display_name(key_name: str) -> str: """ Format the self-defined secret/variable by removing the prefix if reading it from github and there is prefix in the name """ - if key_name and key_name not in settings.AUTH_SETTINGS_ENVS \ - and key_name not in settings.AUTH_SETTINGS_SECRETS: - if settings.APP_SELF_DEFINE_SETTING_PREFIX in key_name: - return key_name.replace( - settings.APP_SELF_DEFINE_SETTING_PREFIX, "") - return key_name + if not key_name.startswith(settings.APP_SELF_DEFINE_SETTING_PREFIX): + return key_name + + return key_name.replace(settings.APP_SELF_DEFINE_SETTING_PREFIX, "", 1) def create_or_update_secret(self, env_name, secret_key, secret_value): org_name, repo_name = extract_repo_info_from_url(self.app.repo_url) diff --git a/controlpanel/api/migrations/0032_app_namespace.py b/controlpanel/api/migrations/0032_app_namespace.py new file mode 100644 index 000000000..c0bb1d482 --- /dev/null +++ b/controlpanel/api/migrations/0032_app_namespace.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.7 on 2024-02-20 16:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0031_add_soft_delete_fields"), + ] + + operations = [ + migrations.AddField( + model_name="app", + name="namespace", + field=models.CharField(blank=True, max_length=63, null=True, unique=True), + ), + ] diff --git a/controlpanel/api/migrations/0033_add_namespaces_values_to_apps.py b/controlpanel/api/migrations/0033_add_namespaces_values_to_apps.py new file mode 100644 index 000000000..64dd9c9b6 --- /dev/null +++ b/controlpanel/api/migrations/0033_add_namespaces_values_to_apps.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.7 on 2024-02-20 16:01 + +from django.db import migrations + + +def add_namespaces(apps, schema_editor): + App = apps.get_model("api", "App") + for app in App.objects.all(): + app.namespace = f"data-platform-app-{app.slug}" + app.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0032_app_namespace"), + ] + + operations = [ + migrations.RunPython(code=add_namespaces, reverse_code=migrations.RunPython.noop) + ] diff --git a/controlpanel/api/migrations/0034_remove_null_blank_from_namespace.py b/controlpanel/api/migrations/0034_remove_null_blank_from_namespace.py new file mode 100644 index 000000000..c406a65fb --- /dev/null +++ b/controlpanel/api/migrations/0034_remove_null_blank_from_namespace.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.7 on 2024-02-20 16:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0033_add_namespaces_values_to_apps"), + ] + + operations = [ + migrations.AlterField( + model_name="app", + name="namespace", + field=models.CharField(max_length=63, unique=True), + ), + ] diff --git a/controlpanel/api/models/app.py b/controlpanel/api/models/app.py index 288d53811..b5365c80e 100644 --- a/controlpanel/api/models/app.py +++ b/controlpanel/api/models/app.py @@ -10,10 +10,9 @@ from django_extensions.db.models import TimeStampedModel # First-party/Local -from controlpanel.api import auth0, cluster +from controlpanel.api import auth0, cluster, tasks from controlpanel.api.models import IPAllowlist from controlpanel.utils import github_repository_name, s3_slugify, webapp_release_name -from controlpanel.api import tasks class App(TimeStampedModel): @@ -35,6 +34,9 @@ class App(TimeStampedModel): # are not within the fields which will be searched frequently app_conf = models.JSONField(null=True) + # Stores the Cloud Platform namespace name + namespace = models.CharField(unique=True, max_length=63) + # Non database field just for passing extra parameters disable_authentication = False connections = {} @@ -252,12 +254,13 @@ def auth0_client_name(self, env_name=None): def app_url_name(self, env_name): format_pattern = settings.APP_URL_NAME_PATTERN.get(env_name.upper()) + namespace = self.namespace.removeprefix("data-platform-app-") if not format_pattern: format_pattern = settings.APP_URL_NAME_PATTERN.get(self.DEFAULT_SETTING_KEY_WORD) if format_pattern: - return format_pattern.format(app_name=self.slug, env=env_name) + return format_pattern.format(app_name=namespace, env=env_name) else: - return self.slug + return namespace def get_auth_client(self, env_name): env_name = env_name or self.DEFAULT_AUTH_CATEGORY @@ -314,7 +317,8 @@ class DeleteCustomerError(Exception): App.DeleteCustomerError = DeleteCustomerError -from django.db.models.signals import post_save, post_delete +# Third-party +from django.db.models.signals import post_delete, post_save from django.dispatch import receiver @@ -336,6 +340,7 @@ def trigger_app_create_related_messages(sender, instance, created, **kwargs): @receiver(post_delete, sender=App) def remove_app_related_tasks(sender, instance, **kwargs): + # First-party/Local from controlpanel.api.models import Task related_app_tasks = Task.objects.filter(entity_class="App", entity_id=instance.id) for task in related_app_tasks: diff --git a/controlpanel/api/tasks/handlers/app.py b/controlpanel/api/tasks/handlers/app.py index accc6d68b..0b9d05f8d 100644 --- a/controlpanel/api/tasks/handlers/app.py +++ b/controlpanel/api/tasks/handlers/app.py @@ -28,5 +28,6 @@ class CreateAppAWSRole(BaseModelTaskHandler): name = "create_app_aws_role" def handle(self): - cluster.App(self.object).create_iam_role() + task_user = User.objects.filter(pk=self.task_user_pk).first() + cluster.App(self.object, task_user.github_api_token).create_iam_role() self.complete() diff --git a/controlpanel/api/templates/assume_roles/cloud_platform_oidc.json b/controlpanel/api/templates/assume_roles/cloud_platform_oidc.json index 9c13e16ba..a5e5b8b42 100644 --- a/controlpanel/api/templates/assume_roles/cloud_platform_oidc.json +++ b/controlpanel/api/templates/assume_roles/cloud_platform_oidc.json @@ -9,8 +9,8 @@ "StringEquals": { "{{ identity_provider }}:aud": "sts.amazonaws.com", "{{ identity_provider }}:sub": [ - "system:serviceaccount:data-platform-app-{{ app_name }}-dev:data-platform-app-{{ app_name }}-dev-sa", - "system:serviceaccount:data-platform-app-{{ app_name }}-prod:data-platform-app-{{ app_name }}-prod-sa" + "system:serviceaccount:{{ app_namespace }}-dev:{{ app_namespace }}-dev-sa", + "system:serviceaccount:{{ app_namespace }}-prod:{{ app_namespace }}-prod-sa" ] } } diff --git a/controlpanel/frontend/forms.py b/controlpanel/frontend/forms.py index 982c37d87..be8a27dd4 100644 --- a/controlpanel/frontend/forms.py +++ b/controlpanel/frontend/forms.py @@ -116,7 +116,7 @@ def _check_inputs_for_custom_connection(self, cleaned_data): return auth0_conn_data -class CreateAppForm(AppAuth0Form): +class CreateAppForm(forms.Form): repo_url = forms.CharField( max_length=512, @@ -148,8 +148,10 @@ class CreateAppForm(AppAuth0Form): empty_label="Select", required=False, ) + namespace = forms.CharField(required=True, max_length=63) def __init__(self, *args, **kwargs): + self.request = kwargs.pop("request", None) super().__init__(*args, **kwargs) self.fields["existing_datasource_id"].queryset = self.get_datasource_queryset() @@ -204,6 +206,18 @@ def clean_repo_url(self): return repo_url + def clean_namespace(self): + """ + Removes the env suffix if the user included it + """ + namespace = self.cleaned_data["namespace"] + for suffix in ["-dev", "-prod"]: + if suffix in namespace: + namespace = namespace.removesuffix(suffix) + break + + return namespace + class UpdateAppAuth0ConnectionsForm(AppAuth0Form): env_name = forms.CharField(widget=forms.HiddenInput) diff --git a/controlpanel/frontend/jinja2/customers-list.html b/controlpanel/frontend/jinja2/customers-list.html index 9c3f042a4..f693c9a2f 100644 --- a/controlpanel/frontend/jinja2/customers-list.html +++ b/controlpanel/frontend/jinja2/customers-list.html @@ -20,7 +20,7 @@

{% if not groups_dict %}

- No need to manage the customers of the app on Control panel as it does not require authentication + Customer management is disabled. To manage customers, create an Auth0 client and enable authentication on the manage app page.

{% else %}
diff --git a/controlpanel/frontend/jinja2/includes/app-list.html b/controlpanel/frontend/jinja2/includes/app-list.html index f5338ec20..c210b1bb6 100644 --- a/controlpanel/frontend/jinja2/includes/app-list.html +++ b/controlpanel/frontend/jinja2/includes/app-list.html @@ -37,8 +37,7 @@ {{ yes_no(user.is_app_admin(app.id)) }} - Manage customers + Manage customers {% endset %} @@ -68,13 +73,28 @@

{{ page_title }}

}) }} {% endif %} - + -
-
+ +
+ + + + {% set error_repo_msg = form.errors.get("namespace") %} + {% if error_repo_msg %} + {% set errorId = 'namespace-error' %} + {{ govukErrorMessage({ + "id": errorId, + "html": error_repo_msg|join(". "), + }) }} + {% endif %} + Enter namespace with the -env suffix removed +
+ {{ govukRadios({ "name": "connect_bucket", "fieldset": { @@ -84,7 +104,7 @@

{{ page_title }}

}, }, "hint": { - "text": "Connect an existing app data source to your app, or create a new one.", + "text": "Connect an existing app data source to your app, or create a new one. If you don't need to connect to an S3 bucket, select 'Do this later'", }, "items": [ { @@ -95,6 +115,9 @@

{{ page_title }}

{ "value": "existing", "html": existing_datasource_html|safe, + "hint": { + "text": "Only buckets that you have admin access to are displayed", + }, "checked": form.connect_bucket.value() == "existing" }, { diff --git a/controlpanel/frontend/views/app.py b/controlpanel/frontend/views/app.py index 34324bbcc..1fa990525 100644 --- a/controlpanel/frontend/views/app.py +++ b/controlpanel/frontend/views/app.py @@ -141,8 +141,6 @@ def get_form_kwargs(self): kwargs = FormMixin.get_form_kwargs(self) kwargs.update( request=self.request, - all_connections_names=auth0.ExtendedAuth0().connections.get_all_connection_names(), # noqa: E501 - custom_connections=auth0.ExtendedConnections.custom_connections(), ) return kwargs @@ -151,7 +149,7 @@ def get_success_url(self): self.request, f"Successfully registered {self.object.name} webapp", ) - return reverse_lazy("list-apps") + return reverse_lazy("manage-app", kwargs={"pk": self.object.pk}) def form_valid(self, form): try: diff --git a/controlpanel/frontend/views/app_variables.py b/controlpanel/frontend/views/app_variables.py index bc373fda9..b946c9ae6 100644 --- a/controlpanel/frontend/views/app_variables.py +++ b/controlpanel/frontend/views/app_variables.py @@ -37,7 +37,7 @@ def get_form_kwargs(self): kwargs["initial"]["env_name"] = data.get("env_name") kwargs["initial"]["key"] = self.kwargs.get("var_name") kwargs["initial"]["display_key"] = cluster.App.get_github_key_display_name( - self.kwargs.get("var_name")) + self.kwargs.get("var_name", "")) if kwargs["initial"]["key"]: try: var_info = cluster.App( diff --git a/controlpanel/frontend/views/apps_mng.py b/controlpanel/frontend/views/apps_mng.py index c930b52f1..58331f48c 100644 --- a/controlpanel/frontend/views/apps_mng.py +++ b/controlpanel/frontend/views/apps_mng.py @@ -31,6 +31,7 @@ def register_app(self, user, app_data): name=name, repo_url=repo_url, current_user=user, + namespace=app_data["namespace"], ) self._add_app_to_users(new_app, user) self._create_or_link_datasource(new_app, user, app_data) diff --git a/controlpanel/frontend/views/secrets.py b/controlpanel/frontend/views/secrets.py index b2967e6e7..b9c01df68 100644 --- a/controlpanel/frontend/views/secrets.py +++ b/controlpanel/frontend/views/secrets.py @@ -40,7 +40,7 @@ def get_form_kwargs(self): kwargs["initial"]["env_name"] = data.get("env_name") kwargs["initial"]["key"] = self.kwargs.get("secret_name") kwargs["initial"]["display_key"] = cluster.App.get_github_key_display_name( - self.kwargs.get("secret_name")) + self.kwargs.get("secret_name", "")) return kwargs def get_success_url(self, app_id): diff --git a/settings.yaml b/settings.yaml index aaa9713fa..e72b5f680 100644 --- a/settings.yaml +++ b/settings.yaml @@ -95,12 +95,14 @@ AUTH_SETTINGS_SECRETS: - AUTH0_CLIENT_ID - AUTH0_CLIENT_SECRET - IP_RANGES + - APP_ROLE_ARN AUTH_SETTINGS_NO_EDIT: - AUTH0_CLIENT_ID - AUTH0_CLIENT_SECRET - AUTH0_DOMAIN - AUTH0_PASSWORDLESS + - APP_ROLE_ARN AUTH_SETTINGS_ENVS: - AUTH0_DOMAIN diff --git a/tests/api/cluster/test_app.py b/tests/api/cluster/test_app.py index 7530b755d..c9b4c6e48 100644 --- a/tests/api/cluster/test_app.py +++ b/tests/api/cluster/test_app.py @@ -1,10 +1,9 @@ # Standard library from copy import deepcopy -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, patch, call # Third-party import pytest -from django.conf import settings # First-party/Local from controlpanel.api import cluster, models @@ -13,7 +12,11 @@ @pytest.fixture def app(): - return models.App(slug="test-app", repo_url="https://gitpub.example.com/test-repo") + return models.App( + slug="test-app", + repo_url="https://gitpub.example.com/test-repo", + namespace="test-namespace", + ) @pytest.fixture @@ -65,8 +68,8 @@ def oidc_provider_statement(app, settings): "StringEquals": { f"{settings.OIDC_APP_EKS_PROVIDER}:aud": "sts.amazonaws.com", f"{settings.OIDC_APP_EKS_PROVIDER}:sub": [ - f"system:serviceaccount:data-platform-app-{app.slug}-dev:data-platform-app-{app.slug}-dev-sa", # noqa - f"system:serviceaccount:data-platform-app-{app.slug}-prod:data-platform-app-{app.slug}-prod-sa" # noqa + f"system:serviceaccount:{app.namespace}-dev:{app.namespace}-dev-sa", # noqa + f"system:serviceaccount:{app.namespace}-prod:{app.namespace}-prod-sa" # noqa ] } } @@ -77,13 +80,22 @@ def test_oidc_provider_statement(app, oidc_provider_statement): assert cluster.App(app).oidc_provider_statement == oidc_provider_statement -def test_app_create_iam_role(aws_create_role, app, oidc_provider_statement): +@patch("controlpanel.api.cluster.App.get_deployment_envs") +@patch("controlpanel.api.cluster.App._create_secrets") +def test_app_create_iam_role( + _create_secrets, get_deployment_envs, aws_create_role, app, oidc_provider_statement +): expected_assume_role = deepcopy(BASE_ASSUME_ROLE_POLICY) expected_assume_role["Statement"].append(oidc_provider_statement) + get_deployment_envs.return_value = ["dev", "prod"] cluster.App(app).create_iam_role() aws_create_role.assert_called_with(app.iam_role_name, expected_assume_role) + _create_secrets.assert_has_calls([ + call(env_name="dev"), + call(env_name="prod"), + ]) @pytest.fixture # noqa: F405 @@ -163,7 +175,6 @@ def test_create_secrets(app): secrets = { app_cluster.IP_RANGES: "1.2.3", app_cluster.APP_ROLE_ARN: app.iam_role_arn, - app_cluster.DATA_ACCOUNT_ID: settings.AWS_DATA_ACCOUNT_ID } with patch.object(app_cluster, "create_or_update_secrets"): app_cluster._create_secrets(env_name="dev", client=None) @@ -173,6 +184,31 @@ def test_create_secrets(app): ) +@pytest.mark.parametrize("key, expected", [ + ("AUTH0_CLIENT_ID", "AUTH0_CLIENT_ID"), + ("AUTH0_CLIENT_SECRET", "AUTH0_CLIENT_SECRET"), + ("AUTH0_DOMAIN", "AUTH0_DOMAIN"), + ("AUTH0_PASSWORDLESS", "AUTH0_PASSWORDLESS"), + ("APP_ROLE_ARN", "APP_ROLE_ARN"), + ("CUSTOM_SETTING", "XXX_CUSTOM_SETTING"), +]) +def test_format_github_key_name(key, expected): + assert cluster.App(None).format_github_key_name(key_name=key) == expected + + +@pytest.mark.parametrize("key, expected", [ + ("AUTH0_CLIENT_ID", "AUTH0_CLIENT_ID"), + ("AUTH0_CLIENT_SECRET", "AUTH0_CLIENT_SECRET"), + ("AUTH0_DOMAIN", "AUTH0_DOMAIN"), + ("AUTH0_PASSWORDLESS", "AUTH0_PASSWORDLESS"), + ("APP_ROLE_ARN", "APP_ROLE_ARN"), + ("XXX_CUSTOM_SETTING", "CUSTOM_SETTING"), + ("XXX_XXX_CUSTOM_SETTING", "XXX_CUSTOM_SETTING"), +]) +def test_get_github_key_display_name(key, expected): + assert cluster.App(None).get_github_key_display_name(key) == expected + + # TODO can this be removed? mock_ingress = MagicMock(name="Ingress") mock_ingress.spec.rules = [MagicMock(name="Rule", host="test-app.example.com")] diff --git a/tests/api/models/test_app.py b/tests/api/models/test_app.py index c9132c5b0..58d5e8d8c 100644 --- a/tests/api/models/test_app.py +++ b/tests/api/models/test_app.py @@ -61,10 +61,10 @@ def test_slug_characters_replaced(): @pytest.mark.django_db def test_slug_collisions_increments(): - app = App.objects.create(repo_url="git@github.com:org/foo-bar.git") + app = App.objects.create(repo_url="git@github.com:org/foo-bar.git", namespace="foo-bar") assert "foo-bar" == app.slug - app2 = App.objects.create(repo_url="https://www.example.com/org/foo-bar") + app2 = App.objects.create(repo_url="https://www.example.com/org/foo-bar", namespace="foo-bar-2") assert "foo-bar-2" == app2.slug @@ -209,3 +209,16 @@ def test_app_allowed_ip_ranges(): def test_iam_role_arn(): app = App(slug="example-app") assert app.iam_role_arn == f"arn:aws:iam::{settings.AWS_DATA_ACCOUNT_ID}:role/test_app_example-app" + + +@pytest.mark.parametrize("namespace, env, expected", [ + ("data-platform-app-example", "dev", "example-dev"), + ("example", "dev", "example-dev"), + ("data-platform-example", "dev", "data-platform-example-dev"), + ("data-platform-app-example", "prod", "example"), + ("example", "prod", "example"), + ("data-platform-example", "prod", "data-platform-example"), +]) +def test_app_url_name(namespace, env, expected): + app = App(namespace=namespace) + assert app.app_url_name(env_name=env) == expected diff --git a/tests/api/tasks/test_create_app_aws_role.py b/tests/api/tasks/test_create_app_aws_role.py index a882a9845..b377323fa 100644 --- a/tests/api/tasks/test_create_app_aws_role.py +++ b/tests/api/tasks/test_create_app_aws_role.py @@ -1,5 +1,5 @@ # Standard library -from unittest.mock import patch +from unittest.mock import patch, MagicMock # Third-party import pytest @@ -25,6 +25,7 @@ def test_cluster_not_called_without_valid_app(cluster, complete, users): @pytest.mark.django_db +@patch("controlpanel.api.auth0.ExtendedAuth0", new=MagicMock()) @patch("controlpanel.api.tasks.handlers.base.BaseModelTaskHandler.complete") @patch("controlpanel.api.tasks.handlers.app.cluster") def test_valid_app_and_user(cluster, complete, users): @@ -32,6 +33,6 @@ def test_valid_app_and_user(cluster, complete, users): create_app_aws_role(app.pk, users["superuser"].pk) - cluster.App.assert_called_once_with(app) + cluster.App.assert_called_once_with(app, users["superuser"].github_api_token) cluster.App.return_value.create_iam_role.assert_called_once() complete.assert_called_once() diff --git a/tests/api/views/test_customer.py b/tests/api/views/test_customer.py index 18450af9f..044c025c1 100644 --- a/tests/api/views/test_customer.py +++ b/tests/api/views/test_customer.py @@ -3,10 +3,10 @@ from unittest.mock import patch # Third-party +import pytest from auth0.rest import Auth0Error from bs4 import BeautifulSoup from model_mommy import mommy -import pytest from rest_framework import status from rest_framework.reverse import reverse @@ -221,7 +221,7 @@ def test_no_exist_auth0_clients_on_customers_page(client, app, users, ExtendedAu def test_no_auth0_customers_page(client, app, users, ExtendedAuth0): app.app_conf = None app.save() - message = "No need to manage the customers of the app on Control pane" + message = "Customer management is disabled." client.force_login(users["superuser"]) response = client.get(reverse("appcustomers-page", args=(app.id, 1))) diff --git a/tests/frontend/test_forms.py b/tests/frontend/test_forms.py index 3c5b76138..071994469 100644 --- a/tests/frontend/test_forms.py +++ b/tests/frontend/test_forms.py @@ -5,6 +5,7 @@ import pytest from django.core.exceptions import ValidationError from django.urls import reverse +from mock import MagicMock # First-party/Local from controlpanel.api import aws @@ -136,6 +137,7 @@ def test_create_app_form_clean_new_datasource(create_app_request_superuser): "repo_url": "https://github.com/ministryofjustice/my_repo", "connect_bucket": "new", "new_datasource_name": "test-bucketname", + "namespace": "my-repo" }, request=create_app_request_superuser, ) @@ -151,6 +153,7 @@ def test_create_app_form_clean_new_datasource(create_app_request_superuser): "deployment_envs": ["test"], "repo_url": "https://github.com/ministryofjustice/my_repo", "connect_bucket": "new", + "namespace": "my-repo" }, request=create_app_request_superuser, ) @@ -268,6 +271,7 @@ def test_create_app_form_clean_repo_url(create_app_request_superuser): "repo_url": "https://github.com/ministryofjustice/my_repo", "connect_bucket": "new", "new_datasource_name": "test-bucketname", + "namespace": "my-repo" }, request=create_app_request_superuser, ) @@ -445,3 +449,15 @@ def test_ip_allowlist_form_missing_name(): } f = forms.IPAllowlistForm(data) assert f.errors["name"] == ["This field is required."] + + +@pytest.mark.parametrize("env", ["dev", "prod"]) +@mock.patch( + "controlpanel.frontend.forms.CreateAppForm.get_datasource_queryset", + new=MagicMock, +) +def test_clean_namespace(env): + form = forms.CreateAppForm() + form.cleaned_data = {"namespace": f"my-namespace-{env}"} + + assert form.clean_namespace() == "my-namespace" diff --git a/tests/frontend/views/test_app.py b/tests/frontend/views/test_app.py index d3697c02d..d25ed94ff 100644 --- a/tests/frontend/views/test_app.py +++ b/tests/frontend/views/test_app.py @@ -710,6 +710,7 @@ def test_register_app_with_creating_datasource(client, users): repo_url=f"https://github.com/ministryofjustice/{test_app_name}", connect_bucket="new", new_datasource_name=test_bucket_name, + namespace="test-app-namespace", ) response = client.post(reverse("create-app"), data) @@ -721,6 +722,9 @@ def test_register_app_with_creating_datasource(client, users): related_bucket_ids = [a.s3bucket_id for a in created_app.apps3buckets.all()] assert len(related_bucket_ids) == 1 assert bucket.id in related_bucket_ids + assert response.url == reverse( + "manage-app", kwargs={"pk": created_app.pk} + ) def test_register_app_invalid_organisation(client, users): @@ -731,7 +735,8 @@ def test_register_app_invalid_organisation(client, users): connect_bucket="later", ) - response = client.post(reverse("create-app"), data) + url = reverse("create-app") + response = client.post(url, data) # 200 due to errors assert response.status_code == 200