diff --git a/engine/apps/alerts/models/alert_receive_channel.py b/engine/apps/alerts/models/alert_receive_channel.py index 5435fddc..e48d69a8 100644 --- a/engine/apps/alerts/models/alert_receive_channel.py +++ b/engine/apps/alerts/models/alert_receive_channel.py @@ -32,6 +32,7 @@ from apps.slack.constants import SLACK_RATE_LIMIT_DELAY, SLACK_RATE_LIMIT_TIMEOU from apps.slack.tasks import post_slack_rate_limit_message from apps.slack.utils import post_message_to_channel from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log +from common.api_helpers.utils import create_engine_url from common.exceptions import TeamCanNotBeChangedError, UnableToSendDemoAlert from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length @@ -495,10 +496,7 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject): AlertReceiveChannel.INTEGRATION_MAINTENANCE, ]: return None - return urljoin( - settings.BASE_URL, - f"integrations/v1/{self.config.slug}/{self.token}/", - ) + return create_engine_url(f"integrations/v1/{self.config.slug}/{self.token}/") @property def inbound_email(self): diff --git a/engine/apps/api/views/schedule.py b/engine/apps/api/views/schedule.py index d27676d4..cf4040d1 100644 --- a/engine/apps/api/views/schedule.py +++ b/engine/apps/api/views/schedule.py @@ -1,8 +1,6 @@ import datetime -from urllib.parse import urljoin import pytz -from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.db.models import OuterRef, Subquery from django.db.utils import IntegrityError @@ -38,6 +36,7 @@ from common.api_helpers.mixins import ( ShortSerializerMixin, UpdateSerializerMixin, ) +from common.api_helpers.utils import create_engine_url class ScheduleView( @@ -287,10 +286,9 @@ class ScheduleView( except IntegrityError: raise Conflict("Schedule export token for user already exists") - export_url = urljoin( - settings.BASE_URL, + export_url = create_engine_url( reverse("api-public:schedules-export", kwargs={"pk": schedule.public_primary_key}) - + f"?{SCHEDULE_EXPORT_TOKEN_NAME}={token}", + + f"?{SCHEDULE_EXPORT_TOKEN_NAME}={token}" ) data = {"token": token, "created_at": instance.created_at, "export_url": export_url} diff --git a/engine/apps/api/views/user.py b/engine/apps/api/views/user.py index e7d20a32..ffdac928 100644 --- a/engine/apps/api/views/user.py +++ b/engine/apps/api/views/user.py @@ -1,5 +1,4 @@ import logging -from urllib.parse import urljoin from django.apps import apps from django.conf import settings @@ -44,6 +43,7 @@ from apps.user_management.organization_log_creator import OrganizationLogType, c from common.api_helpers.exceptions import Conflict from common.api_helpers.mixins import FilterSerializerMixin, PublicPrimaryKeyMixin from common.api_helpers.paginators import HundredPageSizePaginator +from common.api_helpers.utils import create_engine_url from common.constants.role import Role logger = logging.getLogger(__name__) @@ -406,10 +406,9 @@ class UserView( except IntegrityError: raise Conflict("Schedule export token for user already exists") - export_url = urljoin( - settings.BASE_URL, + export_url = create_engine_url( reverse("api-public:users-schedule-export", kwargs={"pk": user.public_primary_key}) - + f"?{SCHEDULE_EXPORT_TOKEN_NAME}={token}", + + f"?{SCHEDULE_EXPORT_TOKEN_NAME}={token}" ) data = {"token": token, "created_at": instance.created_at, "export_url": export_url} diff --git a/engine/apps/slack/scenarios/resolution_note.py b/engine/apps/slack/scenarios/resolution_note.py index a82ccb69..739a28ad 100644 --- a/engine/apps/slack/scenarios/resolution_note.py +++ b/engine/apps/slack/scenarios/resolution_note.py @@ -1,14 +1,13 @@ import json import logging -from urllib.parse import urljoin from django.apps import apps -from django.conf import settings from django.db.models import Q from django.utils import timezone from apps.slack.scenarios import scenario_step from apps.slack.slack_client.exceptions import SlackAPIException +from common.api_helpers.utils import create_engine_url from .step_mixins import CheckAlertIsUnarchivedMixin @@ -448,7 +447,7 @@ class ResolutionNoteModalStep(CheckAlertIsUnarchivedMixin, scenario_step.Scenari if not blocks: # there aren't any resolution notes yet, display a hint instead - link_to_instruction = urljoin(settings.BASE_URL, "static/images/postmortem.gif") + link_to_instruction = create_engine_url("static/images/postmortem.gif") blocks = [ { "type": "divider", @@ -474,7 +473,7 @@ class ResolutionNoteModalStep(CheckAlertIsUnarchivedMixin, scenario_step.Scenari return blocks def get_invite_bot_tip_blocks(self, channel): - link_to_instruction = urljoin(settings.BASE_URL, "static/images/postmortem.gif") + link_to_instruction = create_engine_url("static/images/postmortem.gif") blocks = [ { "type": "divider", diff --git a/engine/apps/slack/tests/test_scenario_steps/test_resolution_note.py b/engine/apps/slack/tests/test_scenario_steps/test_resolution_note.py index 080f5d40..f950effb 100644 --- a/engine/apps/slack/tests/test_scenario_steps/test_resolution_note.py +++ b/engine/apps/slack/tests/test_scenario_steps/test_resolution_note.py @@ -1,10 +1,9 @@ import json -from urllib.parse import urljoin import pytest -from django.conf import settings from apps.slack.scenarios.scenario_step import ScenarioStep +from common.api_helpers.utils import create_engine_url @pytest.mark.django_db @@ -22,7 +21,7 @@ def test_get_resolution_notes_blocks_default_if_empty( blocks = step.get_resolution_notes_blocks(alert_group, "", False) - link_to_instruction = urljoin(settings.BASE_URL, "static/images/postmortem.gif") + link_to_instruction = create_engine_url("static/images/postmortem.gif") expected_blocks = [ { "type": "divider", diff --git a/engine/apps/twilioapp/twilio_client.py b/engine/apps/twilioapp/twilio_client.py index 9c2d6cc3..30804549 100644 --- a/engine/apps/twilioapp/twilio_client.py +++ b/engine/apps/twilioapp/twilio_client.py @@ -2,7 +2,6 @@ import logging import urllib.parse from django.apps import apps -from django.conf import settings from django.urls import reverse from twilio.base.exceptions import TwilioRestException from twilio.rest import Client @@ -10,6 +9,7 @@ from twilio.rest import Client from apps.base.utils import live_settings from apps.twilioapp.constants import TEST_CALL_TEXT, TwilioLogRecordStatus, TwilioLogRecordType from apps.twilioapp.utils import get_calling_code, get_gather_message, get_gather_url, parse_phone_number +from common.api_helpers.utils import create_engine_url logger = logging.getLogger(__name__) @@ -24,7 +24,7 @@ class TwilioClient: return live_settings.TWILIO_NUMBER def send_message(self, body, to): - status_callback = settings.BASE_URL + reverse("twilioapp:sms_status_events") + status_callback = create_engine_url(reverse("twilioapp:sms_status_events")) return self.twilio_api_client.messages.create( body=body, to=to, from_=self.twilio_number, status_callback=status_callback ) @@ -135,7 +135,7 @@ class TwilioClient: ) url = "http://twimlets.com/echo?Twiml=" + twiml_query - status_callback = settings.BASE_URL + reverse("twilioapp:call_status_events") + status_callback = create_engine_url(reverse("twilioapp:call_status_events")) status_callback_events = ["initiated", "ringing", "answered", "completed"] diff --git a/engine/apps/twilioapp/utils.py b/engine/apps/twilioapp/utils.py index 10986dda..7b14b9bd 100644 --- a/engine/apps/twilioapp/utils.py +++ b/engine/apps/twilioapp/utils.py @@ -3,11 +3,12 @@ import re from string import digits from django.apps import apps -from django.conf import settings from django.urls import reverse from phonenumbers import COUNTRY_CODE_TO_REGION_CODE from twilio.twiml.voice_response import Gather, VoiceResponse +from common.api_helpers.utils import create_engine_url + logger = logging.getLogger(__name__) @@ -19,7 +20,7 @@ def get_calling_code(iso): def get_gather_url(): - gather_url = settings.BASE_URL + reverse("twilioapp:gather") + gather_url = create_engine_url(reverse("twilioapp:gather")) return gather_url diff --git a/engine/common/api_helpers/utils.py b/engine/common/api_helpers/utils.py index 1f8ecc54..06b17f0f 100644 --- a/engine/common/api_helpers/utils.py +++ b/engine/common/api_helpers/utils.py @@ -1,3 +1,5 @@ +from urllib.parse import urljoin + import requests from django.conf import settings from icalendar import Calendar @@ -50,3 +52,11 @@ def validate_ical_url(url): raise serializers.ValidationError("Ical parse failed") return url return None + + +def create_engine_url(path): + base = settings.BASE_URL + if not base.endswith("/"): + base += "/" + trimmed_path = path.lstrip("/") + return urljoin(base, trimmed_path) diff --git a/engine/common/tests/test_create_engine_url.py b/engine/common/tests/test_create_engine_url.py new file mode 100644 index 00000000..7097709f --- /dev/null +++ b/engine/common/tests/test_create_engine_url.py @@ -0,0 +1,35 @@ +from django.test.utils import override_settings + +from common.api_helpers.utils import create_engine_url + + +@override_settings(BASE_URL="http://localhost:8000") +def test_create_engine_url_no_slash(): + assert create_engine_url("destination") == "http://localhost:8000/destination" + assert create_engine_url("/destination") == "http://localhost:8000/destination" + assert create_engine_url("destination/") == "http://localhost:8000/destination/" + assert create_engine_url("/destination/") == "http://localhost:8000/destination/" + + +@override_settings(BASE_URL="http://localhost:8000/") +def test_create_engine_url_slash(): + assert create_engine_url("destination") == "http://localhost:8000/destination" + assert create_engine_url("/destination") == "http://localhost:8000/destination" + assert create_engine_url("destination/") == "http://localhost:8000/destination/" + assert create_engine_url("/destination/") == "http://localhost:8000/destination/" + + +@override_settings(BASE_URL="http://localhost:8000/test123") +def test_create_engine_url_prefix_no_slash(): + assert create_engine_url("destination") == "http://localhost:8000/test123/destination" + assert create_engine_url("/destination") == "http://localhost:8000/test123/destination" + assert create_engine_url("destination/") == "http://localhost:8000/test123/destination/" + assert create_engine_url("/destination/") == "http://localhost:8000/test123/destination/" + + +@override_settings(BASE_URL="http://localhost:8000/test123/") +def test_create_engine_url_prefix_slash(): + assert create_engine_url("destination") == "http://localhost:8000/test123/destination" + assert create_engine_url("/destination") == "http://localhost:8000/test123/destination" + assert create_engine_url("destination/") == "http://localhost:8000/test123/destination/" + assert create_engine_url("/destination/") == "http://localhost:8000/test123/destination/"