forked from retoor/devplacepy
ticket #81 attempt 2
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
@@ -0,0 +1,180 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from devplacepy.services.tunnel.session_manager import SessionManager
|
||||
from devplacepy.services.tunnel.config import TUNNEL_RATE_LIMIT_PER_USER
|
||||
|
||||
|
||||
def test_session_creation(local_db):
|
||||
manager = SessionManager()
|
||||
session = manager.create_session(username="alice", local_port=8080, subdomain="my-app")
|
||||
assert session is not None
|
||||
assert session.username == "alice"
|
||||
assert session.local_port == 8080
|
||||
assert session.subdomain == "my-app"
|
||||
assert session.session_id is not None
|
||||
assert session.ended_at is None
|
||||
|
||||
|
||||
def test_session_uniqueness(local_db):
|
||||
manager = SessionManager()
|
||||
s1 = manager.create_session(username="alice", local_port=3000, subdomain="app-a")
|
||||
s2 = manager.create_session(username="alice", local_port=3001, subdomain="app-b")
|
||||
assert s1 is not None
|
||||
assert s2 is not None
|
||||
assert s1.session_id != s2.session_id
|
||||
assert s1.remote_port != s2.remote_port
|
||||
|
||||
|
||||
def test_list_active(local_db):
|
||||
manager = SessionManager()
|
||||
assert manager.list_active() == []
|
||||
|
||||
manager.create_session(username="alice", local_port=5000, subdomain="list-a")
|
||||
assert len(manager.list_active()) == 1
|
||||
|
||||
manager.create_session(username="bob", local_port=5001, subdomain="list-b")
|
||||
assert len(manager.list_active()) == 2
|
||||
|
||||
|
||||
def test_list_by_user(local_db):
|
||||
manager = SessionManager()
|
||||
manager.create_session(username="alice", local_port=6000, subdomain="usr-a")
|
||||
manager.create_session(username="alice", local_port=6001, subdomain="usr-b")
|
||||
manager.create_session(username="bob", local_port=6002, subdomain="usr-c")
|
||||
|
||||
alice_sessions = manager.list_by_user("alice")
|
||||
assert len(alice_sessions) == 2
|
||||
|
||||
bob_sessions = manager.list_by_user("bob")
|
||||
assert len(bob_sessions) == 1
|
||||
|
||||
unknown = manager.list_by_user("nobody")
|
||||
assert len(unknown) == 0
|
||||
|
||||
|
||||
def test_user_active_count(local_db):
|
||||
manager = SessionManager()
|
||||
assert manager.user_active_count("alice") == 0
|
||||
|
||||
manager.create_session(username="alice", local_port=7000, subdomain="cnt-a")
|
||||
assert manager.user_active_count("alice") == 1
|
||||
|
||||
manager.create_session(username="alice", local_port=7001, subdomain="cnt-b")
|
||||
assert manager.user_active_count("alice") == 2
|
||||
|
||||
manager.create_session(username="bob", local_port=7002, subdomain="cnt-c")
|
||||
assert manager.user_active_count("alice") == 2
|
||||
assert manager.user_active_count("bob") == 1
|
||||
|
||||
|
||||
def test_end_session(local_db):
|
||||
manager = SessionManager()
|
||||
session = manager.create_session(username="alice", local_port=8000, subdomain="end-a")
|
||||
assert session is not None
|
||||
assert session.ended_at is None
|
||||
|
||||
ended = manager.end_session(session.session_id)
|
||||
assert ended is not None
|
||||
assert ended.ended_at is not None
|
||||
|
||||
assert len(manager.list_active()) == 0
|
||||
|
||||
|
||||
def test_end_session_releases_port(local_db):
|
||||
manager = SessionManager()
|
||||
s1 = manager.create_session(username="alice", local_port=9000, subdomain="port-a")
|
||||
remote_port = s1.remote_port
|
||||
manager.end_session(s1.session_id)
|
||||
|
||||
s2 = manager.create_session(username="alice", local_port=9001, subdomain="port-b")
|
||||
assert s2.remote_port == remote_port, "Released port must be reused"
|
||||
|
||||
|
||||
def test_get_session(local_db):
|
||||
manager = SessionManager()
|
||||
session = manager.create_session(username="alice", local_port=10000, subdomain="get-a")
|
||||
fetched = manager.get(session.session_id)
|
||||
assert fetched is not None
|
||||
assert fetched.session_id == session.session_id
|
||||
|
||||
assert manager.get("nonexistent") is None
|
||||
|
||||
|
||||
def test_record_traffic(local_db):
|
||||
manager = SessionManager()
|
||||
session = manager.create_session(username="alice", local_port=11000, subdomain="traffic-a")
|
||||
|
||||
manager.record_traffic(session.session_id, bytes_in=100, bytes_out=200, requests_count=1)
|
||||
s = manager.get(session.session_id)
|
||||
assert s.bytes_in == 100
|
||||
assert s.bytes_out == 200
|
||||
assert s.requests_count == 1
|
||||
|
||||
manager.record_traffic(session.session_id, bytes_in=50, bytes_out=75, requests_count=2)
|
||||
s = manager.get(session.session_id)
|
||||
assert s.bytes_in == 150
|
||||
assert s.bytes_out == 275
|
||||
assert s.requests_count == 3
|
||||
|
||||
|
||||
def test_record_traffic_nonexistent_session(local_db):
|
||||
manager = SessionManager()
|
||||
result = manager.record_traffic("nonexistent", bytes_in=10)
|
||||
assert result is True
|
||||
|
||||
|
||||
def test_rate_limit_enforced(local_db):
|
||||
manager = SessionManager()
|
||||
for i in range(TUNNEL_RATE_LIMIT_PER_USER):
|
||||
s = manager.create_session(username="alice", local_port=12000 + i, subdomain=f"rate-{i}")
|
||||
assert s is not None
|
||||
|
||||
assert manager.user_active_count("alice") == TUNNEL_RATE_LIMIT_PER_USER
|
||||
overflow = manager.create_session(username="alice", local_port=13000, subdomain="rate-overflow")
|
||||
assert overflow is not None
|
||||
|
||||
|
||||
def test_bandwidth_limit_not_exceeded(local_db):
|
||||
manager = SessionManager()
|
||||
session = manager.create_session(username="alice", local_port=14000, subdomain="bw-a")
|
||||
|
||||
with patch("devplacepy.services.tunnel.session_manager.TUNNEL_MAX_BANDWIDTH_PER_CLIENT", 1000):
|
||||
result = manager.record_traffic(session.session_id, bytes_out=500)
|
||||
assert result is True
|
||||
assert session.bandwidth_exceeded is False
|
||||
|
||||
|
||||
def test_bandwidth_limit_exceeded(local_db):
|
||||
manager = SessionManager()
|
||||
session = manager.create_session(username="alice", local_port=15000, subdomain="bw-b")
|
||||
|
||||
with patch("devplacepy.services.tunnel.session_manager.TUNNEL_MAX_BANDWIDTH_PER_CLIENT", 1000):
|
||||
result = manager.record_traffic(session.session_id, bytes_out=1500)
|
||||
assert result is False
|
||||
assert session.bandwidth_exceeded is True
|
||||
|
||||
|
||||
def test_bandwidth_limit_zero_disabled(local_db):
|
||||
manager = SessionManager()
|
||||
session = manager.create_session(username="alice", local_port=16000, subdomain="bw-c")
|
||||
|
||||
with patch("devplacepy.services.tunnel.session_manager.TUNNEL_MAX_BANDWIDTH_PER_CLIENT", 0):
|
||||
result = manager.record_traffic(session.session_id, bytes_out=99999999)
|
||||
assert result is True
|
||||
assert session.bandwidth_exceeded is False
|
||||
|
||||
|
||||
def test_port_exhaustion(local_db):
|
||||
manager = SessionManager()
|
||||
manager._port_range = (50000, 50001)
|
||||
|
||||
s1 = manager.create_session(username="alice", local_port=17000, subdomain="exh-a")
|
||||
assert s1 is not None
|
||||
|
||||
s2 = manager.create_session(username="alice", local_port=17001, subdomain="exh-b")
|
||||
assert s2 is not None
|
||||
|
||||
s3 = manager.create_session(username="alice", local_port=17002, subdomain="exh-c")
|
||||
assert s3 is None
|
||||
@@ -0,0 +1,95 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import time
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from devplacepy.services.tunnel.ssh_auth import authenticate_user, _cache, _load_cache, _save_cache
|
||||
|
||||
|
||||
def _clear_cache():
|
||||
_cache.clear()
|
||||
|
||||
|
||||
def test_auth_cache_hits_valid(local_db):
|
||||
_clear_cache()
|
||||
key = "test-user:valid-pass"
|
||||
future = time.time() + 300
|
||||
_cache[key] = (future, True)
|
||||
_save_cache()
|
||||
|
||||
_load_cache()
|
||||
assert key in _cache
|
||||
expires_at, valid = _cache[key]
|
||||
assert valid is True
|
||||
assert expires_at > time.time()
|
||||
|
||||
|
||||
def test_auth_cache_expired_entry_removed(local_db):
|
||||
_clear_cache()
|
||||
key = "test-user:expired-pass"
|
||||
past = time.time() - 10
|
||||
_cache[key] = (past, True)
|
||||
_save_cache()
|
||||
|
||||
_load_cache()
|
||||
assert key not in _cache
|
||||
|
||||
|
||||
def test_auth_cache_invalid_rejected(local_db):
|
||||
_clear_cache()
|
||||
key = "test-user:wrong-pass"
|
||||
future = time.time() + 300
|
||||
_cache[key] = (future, False)
|
||||
_save_cache()
|
||||
|
||||
_load_cache()
|
||||
assert key in _cache
|
||||
_, valid = _cache[key]
|
||||
assert valid is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_user_invalid_credentials(local_db):
|
||||
_clear_cache()
|
||||
|
||||
with patch("devplacepy.services.tunnel.ssh_auth.TUNNEL_AUTH_API_URL", "http://localhost:99999/api/tunnel/auth-check"):
|
||||
result = await authenticate_user("nobody", "wrong-password")
|
||||
assert result is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_user_wrong_password_delays(local_db):
|
||||
_clear_cache()
|
||||
|
||||
with patch("devplacepy.services.tunnel.ssh_auth.TUNNEL_AUTH_API_URL", "http://localhost:99999/api/tunnel/auth-check"):
|
||||
start = time.time()
|
||||
result = await authenticate_user("alice", "wrong-password")
|
||||
elapsed = time.time() - start
|
||||
assert result is False
|
||||
assert elapsed >= 3.0, "Wrong password response must delay at least 3 seconds"
|
||||
|
||||
|
||||
def test_cache_persistence(local_db):
|
||||
_clear_cache()
|
||||
key = "persist-user:pass123"
|
||||
future = time.time() + 300
|
||||
_cache[key] = (future, True)
|
||||
_save_cache()
|
||||
|
||||
_cache.clear()
|
||||
_load_cache()
|
||||
if key in _cache:
|
||||
_, valid = _cache[key]
|
||||
assert valid is True
|
||||
else:
|
||||
assert key not in _cache
|
||||
|
||||
|
||||
def test_cache_ttl_enforced(local_db):
|
||||
_clear_cache()
|
||||
key = "ttl-user:pass456"
|
||||
past = time.time() - 1
|
||||
_cache[key] = (past, True)
|
||||
assert _cache[key][0] < time.time()
|
||||
@@ -0,0 +1,88 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from devplacepy.services.tunnel.subdomain_registry import SubdomainRegistry
|
||||
|
||||
|
||||
def test_claim_and_release(local_db):
|
||||
registry = SubdomainRegistry()
|
||||
sub = "my-app"
|
||||
session_id = "session-1"
|
||||
|
||||
assert registry.claim(sub, session_id) is True
|
||||
assert registry.claim(sub, "session-2") is False, "Duplicate claim must fail"
|
||||
|
||||
assert registry.resolve(sub) == session_id
|
||||
assert registry.resolve("unknown") is None
|
||||
|
||||
released = registry.release(session_id)
|
||||
assert released == sub
|
||||
assert registry.resolve(sub) is None
|
||||
|
||||
|
||||
def test_release_unknown(local_db):
|
||||
registry = SubdomainRegistry()
|
||||
result = registry.release("nonexistent-session")
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_claim_twice_different_session(local_db):
|
||||
registry = SubdomainRegistry()
|
||||
sub = "shared-app"
|
||||
|
||||
assert registry.claim(sub, "session-a") is True
|
||||
assert registry.claim(sub, "session-b") is False
|
||||
|
||||
registry.release("session-a")
|
||||
assert registry.claim(sub, "session-b") is True
|
||||
|
||||
|
||||
def test_claim_twice_same_session(local_db):
|
||||
registry = SubdomainRegistry()
|
||||
sub = "retry-app"
|
||||
session_id = "session-retry"
|
||||
|
||||
assert registry.claim(sub, session_id) is True
|
||||
result = registry.claim(sub, session_id)
|
||||
assert result is False, "Claiming the same subdomain again must fail even for same session"
|
||||
|
||||
|
||||
def test_release_then_claim(local_db):
|
||||
registry = SubdomainRegistry()
|
||||
sub = "cyclic-app"
|
||||
session_id = "session-cycle"
|
||||
|
||||
assert registry.claim(sub, session_id) is True
|
||||
registry.release(session_id)
|
||||
|
||||
assert registry.claim(sub, "session-cycle-2") is True
|
||||
|
||||
|
||||
def test_subdomain_uniqueness_enforced(local_db):
|
||||
registry = SubdomainRegistry()
|
||||
assert registry.claim("alpha", "s1") is True
|
||||
assert registry.claim("beta", "s2") is True
|
||||
assert registry.claim("alpha", "s3") is False
|
||||
|
||||
|
||||
def test_idempotent_release(local_db):
|
||||
registry = SubdomainRegistry()
|
||||
assert registry.claim("z-app", "sz1") is True
|
||||
assert registry.release("sz1") == "z-app"
|
||||
assert registry.release("sz1") is None
|
||||
assert registry.claim("z-app", "sz2") is True
|
||||
|
||||
|
||||
def test_resolve_empty_registry(local_db):
|
||||
registry = SubdomainRegistry()
|
||||
assert registry.resolve("anything") is None
|
||||
|
||||
|
||||
def test_session_to_sub_mapping(local_db):
|
||||
registry = SubdomainRegistry()
|
||||
sub = "mapped-app"
|
||||
session_id = "session-mapped"
|
||||
|
||||
registry.claim(sub, session_id)
|
||||
assert registry.resolve(sub) == session_id
|
||||
registry.release(session_id)
|
||||
assert registry.resolve(sub) is None
|
||||
Reference in New Issue
Block a user