From 771b3b4e3b3fc6b741ffe57248ecfd896eb3707c Mon Sep 17 00:00:00 2001 From: Lyndon Fan Date: Thu, 18 Apr 2024 19:36:54 +0100 Subject: [PATCH 1/7] feat: quote password in url --- core/testcontainers/core/generic.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/testcontainers/core/generic.py b/core/testcontainers/core/generic.py index a3bff96e2..515c2831b 100644 --- a/core/testcontainers/core/generic.py +++ b/core/testcontainers/core/generic.py @@ -11,6 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. from typing import Optional +from urllib.parse import quote from testcontainers.core.container import DockerContainer from testcontainers.core.exceptions import ContainerStartException @@ -60,7 +61,8 @@ def _create_connection_url( raise ContainerStartException("container has not been started") host = host or self.get_container_host_ip() port = self.get_exposed_port(port) - url = f"{dialect}://{username}:{password}@{host}:{port}" + quoted_password = quote(password, safe=" +") + url = f"{dialect}://{username}:{quoted_password}@{host}:{port}" if dbname: url = f"{url}/{dbname}" return url From bae3ba47ce9176447488d9e71fa4f70303e3e3b4 Mon Sep 17 00:00:00 2001 From: Lyndon Fan Date: Thu, 18 Apr 2024 20:13:03 +0100 Subject: [PATCH 2/7] test: test quoted password for mysql --- modules/mysql/tests/test_mysql.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/modules/mysql/tests/test_mysql.py b/modules/mysql/tests/test_mysql.py index 40eb536b0..eabee6602 100644 --- a/modules/mysql/tests/test_mysql.py +++ b/modules/mysql/tests/test_mysql.py @@ -47,3 +47,22 @@ def test_docker_env_variables(): url = container.get_connection_url() pattern = r"mysql\+pymysql:\/\/demo:test@[\w,.]+:(3306|32785)\/custom_db" assert re.match(pattern, url) + + +# This is a feature in the generic DbContainer class +# but it can't be tested on its own +# so is tested in various database modules: +# - mysql / mariadb +# - postgresql +# - sqlserver +# - oracle +def test_quoted_password(): + user = "root" + password = "p@$%25+0&%rd :/!=?" + quoted_password = "p%40%24%2525+0%26%25rd %3A%2F%21%3D%3F" + driver = "pymysql" + port = 3306 + expected_url = f"mysql+{driver}://{user}:{quoted_password}@localhost:{port}/test" + with MySqlContainer("mariadb:10.6.5", username=user, password=password).with_bind_ports(port, port) as container: + url = container.get_connection_url() + assert url == expected_url From b5abf834d59f4d9ea0557546c2de7c3e15624bdc Mon Sep 17 00:00:00 2001 From: Lyndon Fan Date: Thu, 18 Apr 2024 20:19:56 +0100 Subject: [PATCH 3/7] test: test quoted password for psql --- modules/postgres/tests/test_postgres.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/modules/postgres/tests/test_postgres.py b/modules/postgres/tests/test_postgres.py index d0f61e64a..7f880758a 100644 --- a/modules/postgres/tests/test_postgres.py +++ b/modules/postgres/tests/test_postgres.py @@ -42,3 +42,27 @@ def test_docker_run_postgres_with_driver_pg8000(): engine = sqlalchemy.create_engine(postgres.get_connection_url()) with engine.begin() as connection: connection.execute(sqlalchemy.text("select 1=1")) + + +# This is a feature in the generic DbContainer class +# but it can't be tested on its own +# so is tested in various database modules: +# - mysql / mariadb +# - postgresql +# - sqlserver +# - oracle +def test_quoted_password(): + user = "root" + password = "p@$%25+0&%rd :/!=?" + quoted_password = "p%40%24%2525+0%26%25rd %3A%2F%21%3D%3F" + driver = "psycopg2" + port = 5432 + expected_url = f"postgresql+{driver}://{user}:{quoted_password}@localhost:{port}/test" + kwargs = { + "driver": driver, + "username": user, + "password": password, + } + with PostgresContainer("postgres:16", **kwargs).with_bind_ports(port, port) as container: + url = container.get_connection_url() + assert url == expected_url From 045d29b7bafc58defa387c9d85acfa85f1830ccb Mon Sep 17 00:00:00 2001 From: Lyndon Fan Date: Fri, 19 Apr 2024 19:59:49 +0100 Subject: [PATCH 4/7] test: test quoted password for mssql --- modules/mssql/tests/test_mssql.py | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/modules/mssql/tests/test_mssql.py b/modules/mssql/tests/test_mssql.py index e7273042f..9d0b44f32 100644 --- a/modules/mssql/tests/test_mssql.py +++ b/modules/mssql/tests/test_mssql.py @@ -23,3 +23,35 @@ def test_docker_run_azure_sql_edge(): result = connection.execute(sqlalchemy.text("select @@servicename")) for row in result: assert row[0] == "MSSQLSERVER" + + +# This is a feature in the generic DbContainer class +# but it can't be tested on its own +# so is tested in various database modules: +# - mysql / mariadb +# - postgresql +# - sqlserver +# - oracle +def test_quoted_password(): + user = "SA" + # spaces seem to cause issues? + password = "p@$%25+0&%rd:/!=?" + quoted_password = "p%40%24%2525+0%26%25rd%3A%2F%21%3D%3F" + driver = "pymssql" + port = 1433 + expected_url = f"mssql+{driver}://{user}:{quoted_password}@localhost:{port}/tempdb" + kwargs = { + "username": user, + "password": password, + } + with ( + SqlServerContainer("mcr.microsoft.com/azure-sql-edge:1.0.7", **kwargs) + .with_env("ACCEPT_EULA", "Y") + .with_env( + "MSSQL_SA_PASSWORD", "{" + password + "}" + ) # special characters have to be quoted in braces in env vars + ) as container: + exposed_port = container.get_exposed_port(container.port) + expected_url = expected_url.replace(f":{port}", f":{exposed_port}") + url = container.get_connection_url() + assert url == expected_url From f73a54d55313b4c149b5b9071dc986c8161e533b Mon Sep 17 00:00:00 2001 From: Lyndon Fan Date: Sat, 20 Apr 2024 18:39:09 +0100 Subject: [PATCH 5/7] feat: test quoted_password with mongodb --- modules/mongodb/tests/test_mongodb.py | 23 +++++++++++++++++++++++ modules/mssql/tests/test_mssql.py | 2 +- modules/mysql/tests/test_mysql.py | 2 +- modules/postgres/tests/test_postgres.py | 2 +- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/modules/mongodb/tests/test_mongodb.py b/modules/mongodb/tests/test_mongodb.py index 34642103e..b65af6bb5 100644 --- a/modules/mongodb/tests/test_mongodb.py +++ b/modules/mongodb/tests/test_mongodb.py @@ -26,3 +26,26 @@ def test_docker_run_mongodb(version: str): cursor = db.restaurants.find({"borough": "Manhattan"}) assert cursor.next()["restaurant_id"] == doc["restaurant_id"] + + +# This is a feature in the generic DbContainer class +# but it can't be tested on its own +# so is tested in various database modules: +# - mysql / mariadb +# - postgresql +# - sqlserver +# - mongodb +def test_quoted_password(): + user = "root" + password = "p@$%25+0&%rd :/!=?" + quoted_password = "p%40%24%2525+0%26%25rd %3A%2F%21%3D%3F" + driver = "pymongo" + port = 27017 + expected_url = f"mongodb://{user}:{quoted_password}@localhost:{port}" + kwargs = { + "username": user, + "password": password, + } + with MongoDbContainer("mongo:7.0.7", **kwargs).with_bind_ports(port, port) as container: + url = container.get_connection_url() + assert url == expected_url diff --git a/modules/mssql/tests/test_mssql.py b/modules/mssql/tests/test_mssql.py index 9d0b44f32..f7aabd3af 100644 --- a/modules/mssql/tests/test_mssql.py +++ b/modules/mssql/tests/test_mssql.py @@ -31,7 +31,7 @@ def test_docker_run_azure_sql_edge(): # - mysql / mariadb # - postgresql # - sqlserver -# - oracle +# - mongodb def test_quoted_password(): user = "SA" # spaces seem to cause issues? diff --git a/modules/mysql/tests/test_mysql.py b/modules/mysql/tests/test_mysql.py index eabee6602..62deac592 100644 --- a/modules/mysql/tests/test_mysql.py +++ b/modules/mysql/tests/test_mysql.py @@ -55,7 +55,7 @@ def test_docker_env_variables(): # - mysql / mariadb # - postgresql # - sqlserver -# - oracle +# - mongodb def test_quoted_password(): user = "root" password = "p@$%25+0&%rd :/!=?" diff --git a/modules/postgres/tests/test_postgres.py b/modules/postgres/tests/test_postgres.py index 7f880758a..9d06db6d9 100644 --- a/modules/postgres/tests/test_postgres.py +++ b/modules/postgres/tests/test_postgres.py @@ -50,7 +50,7 @@ def test_docker_run_postgres_with_driver_pg8000(): # - mysql / mariadb # - postgresql # - sqlserver -# - oracle +# - mongodb def test_quoted_password(): user = "root" password = "p@$%25+0&%rd :/!=?" From 51100d2841b5173db92fc5bacb6cc6acf674b154 Mon Sep 17 00:00:00 2001 From: David Ankin Date: Sat, 20 Apr 2024 14:58:41 -0400 Subject: [PATCH 6/7] use random port and test with sqlalchemy --- modules/mysql/tests/test_mysql.py | 15 ++++++++++++--- modules/postgres/tests/test_postgres.py | 17 ++++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/modules/mysql/tests/test_mysql.py b/modules/mysql/tests/test_mysql.py index 62deac592..ee1e2b45e 100644 --- a/modules/mysql/tests/test_mysql.py +++ b/modules/mysql/tests/test_mysql.py @@ -61,8 +61,17 @@ def test_quoted_password(): password = "p@$%25+0&%rd :/!=?" quoted_password = "p%40%24%2525+0%26%25rd %3A%2F%21%3D%3F" driver = "pymysql" - port = 3306 - expected_url = f"mysql+{driver}://{user}:{quoted_password}@localhost:{port}/test" - with MySqlContainer("mariadb:10.6.5", username=user, password=password).with_bind_ports(port, port) as container: + with MySqlContainer("mariadb:10.6.5", username=user, password=password) as container: + host = container.get_container_host_ip() + port = container.get_exposed_port(3306) + expected_url = f"mysql+{driver}://{user}:{quoted_password}@{host}:{port}/test" url = container.get_connection_url() assert url == expected_url + + with sqlalchemy.create_engine(expected_url).begin() as connection: + connection.execute(sqlalchemy.text("select version()")) + + raw_pass_url = f"mysql+{driver}://{user}:{password}@{host}:{port}/test" + with pytest.raises(Exception): + with sqlalchemy.create_engine(raw_pass_url).begin() as connection: + connection.execute(sqlalchemy.text("select version()")) diff --git a/modules/postgres/tests/test_postgres.py b/modules/postgres/tests/test_postgres.py index 9d06db6d9..fbba6932d 100644 --- a/modules/postgres/tests/test_postgres.py +++ b/modules/postgres/tests/test_postgres.py @@ -56,13 +56,24 @@ def test_quoted_password(): password = "p@$%25+0&%rd :/!=?" quoted_password = "p%40%24%2525+0%26%25rd %3A%2F%21%3D%3F" driver = "psycopg2" - port = 5432 - expected_url = f"postgresql+{driver}://{user}:{quoted_password}@localhost:{port}/test" kwargs = { "driver": driver, "username": user, "password": password, } - with PostgresContainer("postgres:16", **kwargs).with_bind_ports(port, port) as container: + with PostgresContainer("postgres:16-alpine", **kwargs) as container: + port = container.get_exposed_port(5432) + host = container.get_container_host_ip() + expected_url = f"postgresql+{driver}://{user}:{quoted_password}@{host}:{port}/test" + url = container.get_connection_url() assert url == expected_url + + with sqlalchemy.create_engine(expected_url).begin() as connection: + connection.execute(sqlalchemy.text("select 1=1")) + + raw_pass_url = f"postgresql+{driver}://{user}:{password}@{host}:{port}/test" + with pytest.raises(Exception): + # it raises ValueError, but auth (OperationalError) = more interesting + with sqlalchemy.create_engine(raw_pass_url).begin() as connection: + connection.execute(sqlalchemy.text("select 1=1")) From 5185d294bf44add512a2f69dc6a497f512d983ec Mon Sep 17 00:00:00 2001 From: David Ankin Date: Sat, 20 Apr 2024 15:02:58 -0400 Subject: [PATCH 7/7] also use exposed port with mongodb --- modules/mongodb/tests/test_mongodb.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/modules/mongodb/tests/test_mongodb.py b/modules/mongodb/tests/test_mongodb.py index b65af6bb5..da3465dbb 100644 --- a/modules/mongodb/tests/test_mongodb.py +++ b/modules/mongodb/tests/test_mongodb.py @@ -39,13 +39,14 @@ def test_quoted_password(): user = "root" password = "p@$%25+0&%rd :/!=?" quoted_password = "p%40%24%2525+0%26%25rd %3A%2F%21%3D%3F" - driver = "pymongo" - port = 27017 - expected_url = f"mongodb://{user}:{quoted_password}@localhost:{port}" + # driver = "pymongo" kwargs = { "username": user, "password": password, } - with MongoDbContainer("mongo:7.0.7", **kwargs).with_bind_ports(port, port) as container: + with MongoDbContainer("mongo:7.0.7", **kwargs) as container: + host = container.get_container_host_ip() + port = container.get_exposed_port(27017) + expected_url = f"mongodb://{user}:{quoted_password}@{host}:{port}" url = container.get_connection_url() assert url == expected_url