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
13 changes: 12 additions & 1 deletion sqlit/domains/connections/app/tunnel.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,20 @@


def ensure_ssh_tunnel_available() -> None:
"""Ensure SSH tunnel dependencies are installed."""
"""Ensure SSH tunnel dependencies are installed and compatible."""
try:
import paramiko
import sshtunnel # noqa: F401

# paramiko 4 removed DSSKey, but sshtunnel still references it at
# runtime. Surface this as a MissingDriverError so the installer
# prompt suggests reinstalling with the [ssh] extra (pins paramiko<4).
if not hasattr(paramiko, "DSSKey"):
raise RuntimeError(
f"paramiko {getattr(paramiko, '__version__', '?')} is incompatible "
"with sshtunnel (DSSKey was removed in paramiko 4); reinstall with "
"the [ssh] extra to pin paramiko<4."
)
except Exception as e:
from sqlit.domains.connections.providers.exceptions import MissingDriverError

Expand Down
43 changes: 43 additions & 0 deletions tests/unit/test_tunnel_paramiko_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Regression test for issue #186: paramiko 4 removed DSSKey, breaking sshtunnel."""

from __future__ import annotations

import sys
import types

import pytest

from sqlit.domains.connections.app.tunnel import ensure_ssh_tunnel_available
from sqlit.domains.connections.providers.exceptions import MissingDriverError


def test_ensure_ssh_tunnel_raises_missing_driver_when_paramiko_lacks_dsskey(monkeypatch):
"""A paramiko without DSSKey (i.e. 4.x) must surface as MissingDriverError, not AttributeError."""
fake_paramiko = types.ModuleType("paramiko")
fake_paramiko.__version__ = "4.0.0" # type: ignore[attr-defined]
# Intentionally no DSSKey attribute — simulating paramiko 4.

fake_sshtunnel = types.ModuleType("sshtunnel")

monkeypatch.setitem(sys.modules, "paramiko", fake_paramiko)
monkeypatch.setitem(sys.modules, "sshtunnel", fake_sshtunnel)

with pytest.raises(MissingDriverError) as excinfo:
ensure_ssh_tunnel_available()

assert excinfo.value.extra_name == "ssh"
assert "paramiko" in str(excinfo.value.import_error or "").lower()


def test_ensure_ssh_tunnel_passes_when_paramiko_has_dsskey(monkeypatch):
"""A paramiko 3.x with DSSKey must not raise."""
fake_paramiko = types.ModuleType("paramiko")
fake_paramiko.__version__ = "3.5.0" # type: ignore[attr-defined]
fake_paramiko.DSSKey = object # type: ignore[attr-defined]

fake_sshtunnel = types.ModuleType("sshtunnel")

monkeypatch.setitem(sys.modules, "paramiko", fake_paramiko)
monkeypatch.setitem(sys.modules, "sshtunnel", fake_sshtunnel)

ensure_ssh_tunnel_available() # must not raise
Loading