Repository: safe-global/safe-config-service

Issue:
Config service does not accept urls without TLD
- Docker maps hostnames to the ip addresses, so a valid url could be http://redis
- Only exception is `localhost`
- https://stackoverflow.com/questions/49102410/django-urlfield-doesnt-accept-hostname-only-urls


Discussion:

Resolving patch:
diff --git a/src/chains/migrations/0036_alter_chain_transaction_service_uri_and_more.py b/src/chains/migrations/0036_alter_chain_transaction_service_uri_and_more.py
new file mode 100644
--- /dev/null
+++ b/src/chains/migrations/0036_alter_chain_transaction_service_uri_and_more.py
@@ -0,0 +1,29 @@
+# Generated by Django 4.0.3 on 2022-03-23 09:39
+
+from django.db import migrations, models
+
+import chains.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("chains", "0035_alter_chain_currency_logo_uri"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="chain",
+            name="transaction_service_uri",
+            field=models.CharField(
+                max_length=255, validators=[chains.models.validate_tx_service_url]
+            ),
+        ),
+        migrations.AlterField(
+            model_name="chain",
+            name="vpc_transaction_service_uri",
+            field=models.CharField(
+                max_length=255, validators=[chains.models.validate_tx_service_url]
+            ),
+        ),
+    ]
diff --git a/src/chains/models.py b/src/chains/models.py
--- a/src/chains/models.py
+++ b/src/chains/models.py
@@ -1,6 +1,7 @@
 import os
 import re
 from typing import IO, Union
+from urllib.parse import urlparse
 
 from django.core.exceptions import ValidationError
 from django.core.files.images import get_image_dimensions
@@ -32,6 +33,21 @@ def validate_native_currency_size(image: Union[str, IO[bytes]]) -> None:
         raise ValidationError("Image width and height need to be at most 512 pixels")
 
 
+def validate_tx_service_url(url: str) -> None:
+    result = urlparse(url)
+    if not all(
+        (
+            result.scheme
+            in (
+                "http",
+                "https",
+            ),
+            result.netloc,
+        )
+    ):
+        raise ValidationError(f"{url} is not a valid url")
+
+
 class Chain(models.Model):
     class RpcAuthentication(models.TextChoices):
         API_KEY_PATH = "API_KEY_PATH"
@@ -74,8 +90,12 @@ class RpcAuthentication(models.TextChoices):
         upload_to=native_currency_path,
         max_length=255,
     )
-    transaction_service_uri = models.URLField()
-    vpc_transaction_service_uri = models.URLField()
+    transaction_service_uri = models.CharField(
+        max_length=255, validators=[validate_tx_service_url]
+    )
+    vpc_transaction_service_uri = models.CharField(
+        max_length=255, validators=[validate_tx_service_url]
+    )
     theme_text_color = models.CharField(
         validators=[color_validator],
         max_length=9,


Test patch:
diff --git a/src/chains/tests/test_models.py b/src/chains/tests/test_models.py
--- a/src/chains/tests/test_models.py
+++ b/src/chains/tests/test_models.py
@@ -159,6 +159,42 @@ def test_valid_addresses(self) -> None:
                 chain.full_clean()
 
 
+class ChainTransactionServiceUrlValidationTestCase(TestCase):
+    def test_invalid_urls(self) -> None:
+        param_list = [
+            "foo://bar",
+            "foo",
+            "://",
+        ]
+        for invalid_url in param_list:
+            with self.subTest(msg=f"{invalid_url} is not a valid url"):
+                with self.assertRaises(ValidationError):
+                    chain = ChainFactory.create(transaction_service_uri=invalid_url)
+                    chain.full_clean()
+
+            with self.subTest(msg=f"{invalid_url} is not a valid url"):
+                with self.assertRaises(ValidationError):
+                    chain = ChainFactory.create(vpc_transaction_service_uri=invalid_url)
+                    chain.full_clean()
+
+    def test_valid_urls(self) -> None:
+        param_list = [
+            "http://tx-service",
+            "https://tx-service",
+            "https://tx-service:8000",
+            "https://safe-transaction.mainnet.gnosis.io",
+            "http://mainnet-safe-transaction-web.safe.svc.cluster.local",
+        ]
+        for valid_url in param_list:
+            with self.subTest(msg=f"Valid url {valid_url} should not throw"):
+                chain = ChainFactory.create(transaction_service_uri=valid_url)
+                chain.full_clean()
+
+            with self.subTest(msg=f"Valid url {valid_url} should not throw"):
+                chain = ChainFactory.create(vpc_transaction_service_uri=valid_url)
+                chain.full_clean()
+
+
 class ChainGweiFactorTestCase(TestCase):
     @staticmethod
     def test_ether_to_gwei_conversion_rate_valid() -> None: