Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions tests/fixtures/cockroachdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import pytest

from tests.fixtures.utils import cleanup_connection, is_port_open, run_cli
from tests.fixtures.utils import cleanup_connection, is_binary_port_open, run_cli

# CockroachDB connection settings for Docker
COCKROACHDB_HOST = os.environ.get("COCKROACHDB_HOST", "localhost")
Expand All @@ -19,7 +19,7 @@

def cockroachdb_available() -> bool:
"""Check if CockroachDB is available."""
return is_port_open(COCKROACHDB_HOST, COCKROACHDB_PORT)
return is_binary_port_open(COCKROACHDB_HOST, COCKROACHDB_PORT)


@pytest.fixture(scope="session")
Expand Down
4 changes: 2 additions & 2 deletions tests/fixtures/db2.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import pytest

from tests.fixtures.utils import cleanup_connection, is_port_open, run_cli
from tests.fixtures.utils import cleanup_connection, is_binary_port_open, run_cli

DB2_HOST = os.environ.get("DB2_HOST", "localhost")
DB2_PORT = int(os.environ.get("DB2_PORT", "50000"))
Expand All @@ -18,7 +18,7 @@

def db2_available() -> bool:
"""Check if Db2 is available."""
return is_port_open(DB2_HOST, DB2_PORT)
return is_binary_port_open(DB2_HOST, DB2_PORT)


@pytest.fixture(scope="session")
Expand Down
4 changes: 2 additions & 2 deletions tests/fixtures/firebird.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import pytest

from tests.fixtures.utils import cleanup_connection, is_port_open, run_cli
from tests.fixtures.utils import cleanup_connection, is_binary_port_open, run_cli

FIREBIRD_HOST = os.environ.get("FIREBIRD_HOST", "localhost")
FIREBIRD_PORT = int(os.environ.get("FIREBIRD_PORT", "3050"))
Expand All @@ -18,7 +18,7 @@

def firebird_available() -> bool:
"""Check if Firebird is available."""
return is_port_open(FIREBIRD_HOST, FIREBIRD_PORT)
return is_binary_port_open(FIREBIRD_HOST, FIREBIRD_PORT)


@pytest.fixture(scope="session")
Expand Down
4 changes: 2 additions & 2 deletions tests/fixtures/impala.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import pytest

from tests.fixtures.utils import cleanup_connection, is_port_open, run_cli
from tests.fixtures.utils import cleanup_connection, is_binary_port_open, run_cli

IMPALA_HOST = os.environ.get("IMPALA_HOST", "localhost")
IMPALA_PORT = int(os.environ.get("IMPALA_PORT", "21050"))
Expand All @@ -18,7 +18,7 @@

def impala_available() -> bool:
"""Check if Impala is available."""
return is_port_open(IMPALA_HOST, IMPALA_PORT)
return is_binary_port_open(IMPALA_HOST, IMPALA_PORT)


@pytest.fixture(scope="session")
Expand Down
4 changes: 2 additions & 2 deletions tests/fixtures/mariadb.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import pytest

from tests.fixtures.utils import cleanup_connection, is_port_open, run_cli
from tests.fixtures.utils import cleanup_connection, is_binary_port_open, run_cli

# Note: Using 127.0.0.1 instead of localhost to force TCP connection (localhost uses Unix socket)
MARIADB_HOST = os.environ.get("MARIADB_HOST", "127.0.0.1")
Expand All @@ -19,7 +19,7 @@

def mariadb_available() -> bool:
"""Check if MariaDB is available."""
return is_port_open(MARIADB_HOST, MARIADB_PORT)
return is_binary_port_open(MARIADB_HOST, MARIADB_PORT)


@pytest.fixture(scope="session")
Expand Down
6 changes: 3 additions & 3 deletions tests/fixtures/mariadb_charset.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import pytest

from tests.fixtures.utils import cleanup_connection, is_port_open, run_cli
from tests.fixtures.utils import cleanup_connection, is_binary_port_open, run_cli

# TIS-620 (Thai) MariaDB
MARIADB_TIS620_HOST = os.environ.get("MARIADB_TIS620_HOST", "127.0.0.1")
Expand All @@ -25,12 +25,12 @@

def mariadb_tis620_available() -> bool:
"""Check if MariaDB TIS-620 is available."""
return is_port_open(MARIADB_TIS620_HOST, MARIADB_TIS620_PORT)
return is_binary_port_open(MARIADB_TIS620_HOST, MARIADB_TIS620_PORT)


def mariadb_latin1_available() -> bool:
"""Check if MariaDB Latin1 is available."""
return is_port_open(MARIADB_LATIN1_HOST, MARIADB_LATIN1_PORT)
return is_binary_port_open(MARIADB_LATIN1_HOST, MARIADB_LATIN1_PORT)


@pytest.fixture(scope="session")
Expand Down
4 changes: 2 additions & 2 deletions tests/fixtures/mssql.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import pytest

from tests.fixtures.utils import cleanup_connection, is_port_open, run_cli
from tests.fixtures.utils import cleanup_connection, is_binary_port_open, run_cli

MSSQL_HOST = os.environ.get("MSSQL_HOST", "localhost")
MSSQL_PORT = int(os.environ.get("MSSQL_PORT", "1434"))
Expand All @@ -18,7 +18,7 @@

def mssql_available() -> bool:
"""Check if SQL Server is available."""
return is_port_open(MSSQL_HOST, MSSQL_PORT)
return is_binary_port_open(MSSQL_HOST, MSSQL_PORT)


@pytest.fixture(scope="session")
Expand Down
4 changes: 2 additions & 2 deletions tests/fixtures/mysql.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import pytest

from tests.fixtures.utils import cleanup_connection, is_port_open, run_cli
from tests.fixtures.utils import cleanup_connection, is_binary_port_open, run_cli

# Note: We use root user because MySQL's testuser only has localhost access inside the container
MYSQL_HOST = os.environ.get("MYSQL_HOST", "localhost")
Expand All @@ -19,7 +19,7 @@

def mysql_available() -> bool:
"""Check if MySQL is available."""
return is_port_open(MYSQL_HOST, MYSQL_PORT)
return is_binary_port_open(MYSQL_HOST, MYSQL_PORT)


@pytest.fixture(scope="session")
Expand Down
6 changes: 3 additions & 3 deletions tests/fixtures/mysql_charset.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import pytest

from tests.fixtures.utils import cleanup_connection, is_port_open, run_cli
from tests.fixtures.utils import cleanup_connection, is_binary_port_open, run_cli

# TIS-620 (Thai) MySQL
MYSQL_TIS620_HOST = os.environ.get("MYSQL_TIS620_HOST", "localhost")
Expand All @@ -25,12 +25,12 @@

def mysql_tis620_available() -> bool:
"""Check if MySQL TIS-620 is available."""
return is_port_open(MYSQL_TIS620_HOST, MYSQL_TIS620_PORT)
return is_binary_port_open(MYSQL_TIS620_HOST, MYSQL_TIS620_PORT)


def mysql_latin1_available() -> bool:
"""Check if MySQL Latin1 is available."""
return is_port_open(MYSQL_LATIN1_HOST, MYSQL_LATIN1_PORT)
return is_binary_port_open(MYSQL_LATIN1_HOST, MYSQL_LATIN1_PORT)


@pytest.fixture(scope="session")
Expand Down
4 changes: 2 additions & 2 deletions tests/fixtures/oracle.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import pytest

from tests.fixtures.utils import cleanup_connection, is_port_open, run_cli
from tests.fixtures.utils import cleanup_connection, is_binary_port_open, run_cli

# Oracle connection settings for Docker
ORACLE_HOST = os.environ.get("ORACLE_HOST", "localhost")
Expand All @@ -19,7 +19,7 @@

def oracle_available() -> bool:
"""Check if Oracle is available."""
return is_port_open(ORACLE_HOST, ORACLE_PORT)
return is_binary_port_open(ORACLE_HOST, ORACLE_PORT)


@pytest.fixture(scope="session")
Expand Down
4 changes: 2 additions & 2 deletions tests/fixtures/oracle_legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import pytest

from tests.fixtures.utils import cleanup_connection, is_port_open, run_cli
from tests.fixtures.utils import cleanup_connection, is_binary_port_open, run_cli

ORACLE11G_HOST = os.environ.get("ORACLE11G_HOST", "localhost")
ORACLE11G_PORT = int(os.environ.get("ORACLE11G_PORT", "1522"))
Expand All @@ -23,7 +23,7 @@ def oracle11g_available() -> bool:
"""Check if Oracle 11g is available."""
if not ORACLE11G_RUN_TESTS:
return False
return is_port_open(ORACLE11G_HOST, ORACLE11G_PORT)
return is_binary_port_open(ORACLE11G_HOST, ORACLE11G_PORT)


def _init_oracle_client(oracledb) -> None:
Expand Down
4 changes: 2 additions & 2 deletions tests/fixtures/postgres.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import pytest

from tests.fixtures.utils import cleanup_connection, is_port_open, run_cli
from tests.fixtures.utils import cleanup_connection, is_binary_port_open, run_cli

POSTGRES_HOST = os.environ.get("POSTGRES_HOST", "localhost")
POSTGRES_PORT = int(os.environ.get("POSTGRES_PORT", "5432"))
Expand All @@ -18,7 +18,7 @@

def postgres_available() -> bool:
"""Check if PostgreSQL is available."""
return is_port_open(POSTGRES_HOST, POSTGRES_PORT)
return is_binary_port_open(POSTGRES_HOST, POSTGRES_PORT)


@pytest.fixture(scope="session")
Expand Down
39 changes: 39 additions & 0 deletions tests/fixtures/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,45 @@ def is_port_open(host: str, port: int, timeout: float = 1.0) -> bool:
return False


def probe_port(host: str, port: int, timeout: float = 0.5) -> str:
"""Classify a TCP port as 'refused', 'http', 'binary', or 'timeout'.

A bare TCP connect (`is_port_open`) returns True for any service that
accepts the connection — including HTTP security agents that bind common
ports. When a fixture then opens a binary-protocol driver (Thrift,
MySQL/Postgres wire, TNS, …) to that port, the driver misreads the
HTTP response bytes as protocol framing and hangs indefinitely.

This probe sends a one-line HTTP request and inspects the response:
if it starts with `HTTP/` the port is owned by an HTTP daemon, not the
binary server the fixture expects. See issue #200.
"""
try:
with socket.create_connection((host, port), timeout=timeout) as sock:
sock.settimeout(timeout)
try:
sock.sendall(b"GET / HTTP/1.0\r\nHost: localhost\r\n\r\n")
data = sock.recv(8)
return "http" if data.startswith(b"HTTP/") else "binary"
except socket.timeout:
# Binary protocol server that ignored our HTTP gibberish.
return "binary"
except ConnectionRefusedError:
return "refused"
except (TimeoutError, OSError):
return "timeout"


def is_binary_port_open(host: str, port: int, timeout: float = 0.5) -> bool:
"""Like `is_port_open` but rejects HTTP interceptors.

Use in fixture availability checks for binary-protocol servers so the
test suite doesn't hang on a security agent that happens to bind the
same port. See issue #200.
"""
return probe_port(host, port, timeout) == "binary"


def wait_for_port(host: str, port: int, timeout: float = 60.0) -> bool:
"""Wait for a TCP port to become available."""
start = time.time()
Expand Down
61 changes: 61 additions & 0 deletions tests/unit/test_probe_port.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Tests for probe_port — distinguishes HTTP interceptors from binary servers.

See issue #200: a bare TCP connect classifies any accepting daemon as
"open", so test fixtures based on `is_port_open` proceed against HTTP
security agents and the binary driver hangs reading framing data.
"""

from __future__ import annotations

import socket
from unittest.mock import MagicMock, patch

from tests.fixtures.utils import is_binary_port_open, probe_port


def _fake_socket_returning(initial_bytes: bytes) -> MagicMock:
"""Build a mock sock that returns initial_bytes on the first recv()."""
sock = MagicMock()
sock.recv.return_value = initial_bytes
sock.__enter__.return_value = sock
sock.__exit__.return_value = False
return sock


def test_http_interceptor_is_detected_as_http():
sock = _fake_socket_returning(b"HTTP/1.1")
with patch("socket.create_connection", return_value=sock):
assert probe_port("localhost", 10001) == "http"
assert is_binary_port_open("localhost", 10001) is False


def test_binary_server_replies_with_non_http_bytes():
# Postgres greeting starts with a length-prefixed message — not "HTTP/".
sock = _fake_socket_returning(b"\x00\x00\x00\x08")
with patch("socket.create_connection", return_value=sock):
assert probe_port("localhost", 5432) == "binary"
assert is_binary_port_open("localhost", 5432) is True


def test_binary_server_that_doesnt_reply_to_http_is_still_binary():
"""A strict binary protocol may simply ignore our HTTP gibberish; the
subsequent recv() then times out, which we still classify as binary."""
sock = MagicMock()
sock.recv.side_effect = socket.timeout
sock.__enter__.return_value = sock
sock.__exit__.return_value = False
with patch("socket.create_connection", return_value=sock):
assert probe_port("localhost", 3306) == "binary"
assert is_binary_port_open("localhost", 3306) is True


def test_connection_refused_classified_separately_from_timeout():
with patch("socket.create_connection", side_effect=ConnectionRefusedError):
assert probe_port("localhost", 1) == "refused"
assert is_binary_port_open("localhost", 1) is False


def test_connect_timeout_classified_as_timeout():
with patch("socket.create_connection", side_effect=TimeoutError):
assert probe_port("localhost", 1) == "timeout"
assert is_binary_port_open("localhost", 1) is False
Loading