Repository: airtai/faststream
Pull request #1979 (2024-12-10 10:11:47)

fix: when / present in virtual host name and passing as uri
e.g name of vh is  "/test"

there is no problenm if passing vh separately, result url will "amqp://root:123@localhost:5672//test"

but if pass vh in uri, result uri will 
"amqp://root:123@localhost:5672/test"
cause lstrip("/") calling and slash missing in result url,  so replace with count 1 is ok

# Description

Please include a summary of the change and specify which issue is being addressed. Additionally, provide relevant motivation and context.

Fixes # (issue number)

## Type of change

Please delete options that are not relevant.

- [ ] Documentation (typos, code examples, or any documentation updates)
- [x] Bug fix (a non-breaking change that resolves an issue)
- [ ] New feature (a non-breaking change that adds functionality)
- [ ] Breaking change (a fix or feature that would disrupt existing functionality)
- [ ] This change requires a documentation update

## Checklist

- [ ] My code adheres to the style guidelines of this project (`scripts/lint.sh` shows no errors)
- [ ] I have conducted a self-review of my own code
- [ ] I have made the necessary changes to the documentation
- [ ] My changes do not generate any new warnings
- [ ] I have added tests to validate the effectiveness of my fix or the functionality of my new feature
- [ ] Both new and existing unit tests pass successfully on my local environment by running `scripts/test-cov.sh`
- [ ] I have ensured that static analysis tests are passing by running `scripts/static-analysis.sh`
- [ ] I have included code examples to illustrate the modifications



Patch:
diff --git a/docs/docs/SUMMARY.md b/docs/docs/SUMMARY.md
index c2f9915d..95f4245e 100644
--- a/docs/docs/SUMMARY.md
+++ b/docs/docs/SUMMARY.md
@@ -960,6 +960,7 @@ search:
                     - [build_message](api/faststream/rabbit/testing/build_message.md)
                 - utils
                     - [build_url](api/faststream/rabbit/utils/build_url.md)
+                    - [build_virtual_host](api/faststream/rabbit/utils/build_virtual_host.md)
                     - [is_routing_exchange](api/faststream/rabbit/utils/is_routing_exchange.md)
             - redis
                 - [ListSub](api/faststream/redis/ListSub.md)
diff --git a/docs/docs/en/api/faststream/rabbit/utils/build_virtual_host.md b/docs/docs/en/api/faststream/rabbit/utils/build_virtual_host.md
new file mode 100644
index 00000000..bab95629
--- /dev/null
+++ b/docs/docs/en/api/faststream/rabbit/utils/build_virtual_host.md
@@ -0,0 +1,11 @@
+---
+# 0.5 - API
+# 2 - Release
+# 3 - Contributing
+# 5 - Template Page
+# 10 - Default
+search:
+  boost: 0.5
+---
+
+::: faststream.rabbit.utils.build_virtual_host
diff --git a/faststream/rabbit/utils.py b/faststream/rabbit/utils.py
index 1af8a30f..a5d48a05 100644
--- a/faststream/rabbit/utils.py
+++ b/faststream/rabbit/utils.py
@@ -12,6 +12,17 @@ if TYPE_CHECKING:
     from faststream.rabbit.schemas import RabbitExchange
 
 
+def build_virtual_host(
+    url: Union[str, "URL", None], virtualhost: Optional[str], path: str
+) -> str:
+    if (not url and not virtualhost) or virtualhost == "/":
+        return ""
+    elif virtualhost:
+        return virtualhost.replace("/", "", 1)
+    else:
+        return path.replace("/", "", 1)
+
+
 def build_url(
     url: Union[str, "URL", None] = None,
     *,
@@ -36,7 +47,7 @@ def build_url(
         port=port or original_url.port or default_port,
         login=login or original_url.user or "guest",
         password=password or original_url.password or "guest",
-        virtualhost=virtualhost or original_url.path.lstrip("/"),
+        virtualhost=build_virtual_host(url, virtualhost, original_url.path),
         ssl=use_ssl,
         ssl_options=ssl_options,
         client_properties=client_properties,


Test patch:
diff --git a/tests/brokers/rabbit/test_connect.py b/tests/brokers/rabbit/test_connect.py
index b5f14dc8..61934ec0 100644
--- a/tests/brokers/rabbit/test_connect.py
+++ b/tests/brokers/rabbit/test_connect.py
@@ -15,7 +15,7 @@ class TestConnection(BrokerConnectionTestcase):
         return {"url": settings.url}
 
     @pytest.mark.asyncio
-    async def test_init_connect_by_raw_data(self, settings):
+    async def test_connect_handover_config_to_init(self, settings):
         broker = self.broker(
             host=settings.host,
             port=settings.port,
@@ -28,7 +28,7 @@ class TestConnection(BrokerConnectionTestcase):
         await broker.close()
 
     @pytest.mark.asyncio
-    async def test_connection_by_params(self, settings):
+    async def test_connect_handover_config_to_connect(self, settings):
         broker = self.broker()
         assert await broker.connect(
             host=settings.host,
@@ -41,20 +41,7 @@ class TestConnection(BrokerConnectionTestcase):
         await broker.close()
 
     @pytest.mark.asyncio
-    async def test_connect_merge_kwargs_with_priority(self, settings):
-        broker = self.broker(host="fake-host", port=5677)  # kwargs will be ignored
-        assert await broker.connect(
-            host=settings.host,
-            port=settings.port,
-            security=SASLPlaintext(
-                username=settings.login,
-                password=settings.password,
-            ),
-        )
-        await broker.close()
-
-    @pytest.mark.asyncio
-    async def test_connect_merge_args_and_kwargs_native(self, settings):
+    async def test_connect_handover_config_to_connect_override_init(self, settings):
         broker = self.broker("fake-url")  # will be ignored
         assert await broker.connect(url=settings.url)
         await broker.close()
diff --git a/tests/brokers/rabbit/test_url_builder.py b/tests/brokers/rabbit/test_url_builder.py
new file mode 100644
index 00000000..01852071
--- /dev/null
+++ b/tests/brokers/rabbit/test_url_builder.py
@@ -0,0 +1,38 @@
+from typing import Any, Dict
+
+import pytest
+from yarl import URL
+
+from faststream.rabbit.utils import build_url
+
+
+@pytest.mark.parametrize(
+    ("url_kwargs", "expected_url"),
+    [
+        pytest.param(
+            {},
+            URL("amqp://guest:guest@localhost:5672/"),  # pragma: allowlist secret
+            id="blank params use defaults",
+        ),
+        pytest.param(
+            {"ssl": True},
+            URL("amqps://guest:guest@localhost:5672/"),  # pragma: allowlist secret
+            id="ssl affects protocol",
+        ),
+        pytest.param(
+            {"url": "fake", "virtualhost": "/", "host": "host"},
+            URL("amqp://guest:guest@host:5672/"),  # pragma: allowlist secret
+            id="kwargs overrides url",
+        ),
+        pytest.param(
+            {"virtualhost": "//test"},  # pragma: allowlist secret
+            URL(
+                "amqp://guest:guest@localhost:5672//test"  # pragma: allowlist secret
+            ),
+            id="exotic virtualhost",
+        ),
+    ],
+)
+def test_unpack_args(url_kwargs: Dict[str, Any], expected_url: URL) -> None:
+    url = build_url(**url_kwargs)
+    assert url == expected_url