Skip to content

Commit e63500d

Browse files
committed
B2 - uh
1 parent 116eb89 commit e63500d

3 files changed

Lines changed: 289 additions & 0 deletions

File tree

tests/test_core.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from typing import Any, Dict
2+
import httplib as app
3+
import pathlib
4+
import pytest
5+
6+
7+
@pytest.mark.asyncio
8+
async def test_url_encode_decode() -> None:
9+
s = 'Hello World!'
10+
encoded = await app.url_encode(s)
11+
assert '%20' in encoded
12+
decoded = await app.url_decode(encoded)
13+
assert decoded == s
14+
15+
@pytest.mark.asyncio
16+
async def test_get_content_type() -> None:
17+
assert await app.get_content_type('index.html') == 'text/html'
18+
assert await app.get_content_type('style.css') == 'text/css'
19+
assert await app.get_content_type('data.json') == 'application/json'
20+
assert await app.get_content_type('image.png') == 'image/png'
21+
22+
def test_cookie_set_delete_headers():
23+
resp = app.Response()
24+
resp.set_cookie('x', 'y', path='/', max_age=3600, httponly=True, secure=True, samesite='Lax')
25+
headers = resp.get_cookie_headers()
26+
assert any('x=y' in h for h in headers)
27+
resp.delete_cookie('x')
28+
headers = resp.get_cookie_headers()
29+
assert any('x=' in h and 'Expires' in h or 'expires' in h for h in headers)
30+
31+
def test_user_create_login_session(tmp_path: pathlib.Path) -> None:
32+
# Clear state
33+
app.users.clear()
34+
app.profiles.clear()
35+
app.sessions.clear()
36+
37+
assert app.create_user('alice', 'pass123') is True
38+
assert app.user_exists('alice') is True
39+
assert app.login('alice', 'pass123') is True
40+
assert app.login('alice', 'wrong') is False
41+
42+
token = app.start_session('alice')
43+
assert app.verify_session(token) is True
44+
assert app.get_username(token) == 'alice'
45+
app.end_session(token)
46+
assert app.verify_session(token) is False
47+
48+
def test_save_load_users(tmp_path: pathlib.Path) -> None:
49+
# Prepare users
50+
app.users.clear()
51+
app.profiles.clear()
52+
app.create_user('bob', 'secret')
53+
app.update_user_profile('bob', 'hi')
54+
55+
p = tmp_path / 'users.json'
56+
assert app.save_users(str(p)) is True
57+
# Clear and load
58+
app.users.clear()
59+
app.profiles.clear()
60+
assert app.load_users(str(p)) is True
61+
assert app.user_exists('bob')
62+
profile: Dict[str, Any] | None = app.get_user_profile('bob')
63+
assert profile is not None
64+
assert profile['bio'] == 'hi'
65+
66+
@pytest.mark.asyncio
67+
async def test_template_and_redirect(tmp_path: pathlib.Path, monkeypatch: 'pytest.MonkeyPatch') -> None:
68+
# create a static file
69+
f = tmp_path / 'index.html'
70+
f.write_text('<h1>{{ title }}</h1>')
71+
# monkeypatch os.path to use tmp_path
72+
monkeypatch.setenv('PYHTTPLIB_TEST_STATIC', str(tmp_path))
73+
# patch _normalize_path to return our file name
74+
async def fake_normalize(path: str) -> str:
75+
return str(f)
76+
monkeypatch.setattr(app, '_normalize_path', fake_normalize)
77+
out = await app.template('index.html', title='Hi')
78+
assert '<h1>Hi</h1>' in out
79+
code, html = await app.redirect('https://example.com')
80+
assert code == 302
81+
assert 'refresh' in html.lower()

tests/test_more_core.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import pytest
2+
import httplib as app
3+
import os
4+
import socket
5+
import types
6+
7+
@pytest.mark.asyncio
8+
async def test_normalize_path_variants():
9+
assert await app._normalize_path('/') == 'static/index.html'
10+
assert await app._normalize_path('/about') == 'static/about.html'
11+
assert await app._normalize_path('/static/page.html') == 'static/page.html'
12+
# traversal should default to index
13+
assert await app._normalize_path('../etc/passwd') == 'static/index.html'
14+
15+
@pytest.mark.asyncio
16+
async def test_parsers():
17+
headers = 'Host: example.com\nX-Test: value\n'
18+
parsed = await app._parse_headers(headers)
19+
assert parsed['Host'] == 'example.com'
20+
assert parsed['X-Test'] == 'value'
21+
22+
params = await app._parse_params('a=1&b=two')
23+
assert params['a'] == '1' and params['b'] == 'two'
24+
25+
cookies = await app._parse_cookies('a=1; b=2')
26+
assert cookies['a'] == '1' and cookies['b'] == '2'
27+
28+
@pytest.mark.asyncio
29+
async def test_error_shorthand_and_content_type():
30+
assert await app.get_error_shorthand(200) == 'OK'
31+
assert await app.get_error_shorthand(418) == "I'm a teapot"
32+
assert await app.get_error_shorthand(999) == 'Unknown Error'
33+
34+
assert await app.get_content_type('index.html') == 'text/html'
35+
assert await app.get_content_type('file.unknown') == 'text/plain'
36+
37+
def test_client_ip_and_request_context(monkeypatch):
38+
# prepare request context
39+
req = app._get_request()
40+
req.client_ip = '1.2.3.4'
41+
assert app.get_client_ip() == '1.2.3.4'
42+
req.client_ip = ''
43+
req.headers['X-Forwarded-For'] = '5.6.7.8, 9.9.9.9'
44+
assert app.get_client_ip() == '5.6.7.8'
45+
req.headers.clear()
46+
47+
def test_dos_and_blocking():
48+
ip = '10.0.0.1'
49+
# ensure clean
50+
if ip in app.dos_ip_requests: del app.dos_ip_requests[ip]
51+
if ip in app.dos_blocked_ips: del app.dos_blocked_ips[ip]
52+
53+
# simulate many requests quickly to trigger block
54+
for _ in range(app.DOS_MAX_REQUESTS + 1):
55+
blocked = app.check_dos_protection(ip)
56+
assert app.is_ip_blocked(ip) is True
57+
blocked_ips = app.get_blocked_ips()
58+
assert ip in blocked_ips
59+
# unblock
60+
assert app.unblock_ip(ip) is True
61+
assert app.is_ip_blocked(ip) is False
62+
63+
def test_dos_stats_empty():
64+
stats = app.get_dos_stats()
65+
assert 'tracked_ips' in stats and 'blocked_ips' in stats
66+
67+
@pytest.mark.asyncio
68+
async def test_match_route_and_build_response():
69+
vars = await app._match_route('/user/123', '/user/<id>')
70+
assert vars == {'id': '123'}
71+
assert await app._match_route('/a/b', '/a/b') == {}
72+
assert await app._match_route('/a/b', '/a/c') is None
73+
74+
# test response building with cookies
75+
resp = app._get_response()
76+
resp.set_cookie('s', 'v', max_age=10, httponly=True)
77+
header = await app._build_response(200, 'text/plain')
78+
assert b'Set-Cookie' in header
79+
80+
def test_user_operations_and_sessions(tmp_path):
81+
app.users.clear(); app.profiles.clear(); app.sessions.clear()
82+
assert app.create_user('x', 'p') is True
83+
assert app.user_exists('x')
84+
assert app.login('x', 'p') is True
85+
assert app.edit_user('x', 'new') is True
86+
assert app.delete_user('x') is True
87+
88+
app.create_user('y', 'p')
89+
token = app.start_session('y')
90+
assert app.verify_session(token)
91+
assert app.get_username(token) == 'y'
92+
app.end_session(token)
93+
assert not app.verify_session(token)
94+
95+
@pytest.mark.asyncio
96+
async def test_ratelimit_decorator_and_socket_mode(monkeypatch):
97+
# Decorator mode: ensure it returns a callable
98+
dec = app.ratelimit(0.1)
99+
@dec
100+
async def f():
101+
return 'ok'
102+
assert callable(f)
103+
104+
# Socket mode: create a fake socket with getpeername
105+
class FakeSock:
106+
def getpeername(self):
107+
return ('127.0.0.1', 12345)
108+
sock = FakeSock()
109+
# delay-based: first call allowed, immediate second call should be rate-limited
110+
assert app.ratelimit(sock, 0.5, 'test') is False
111+
assert app.ratelimit(sock, 0.5, 'test') is True
112+
113+
*** End Patch

tests/test_ws_and_misc.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import os
2+
import time
3+
import pytest
4+
import httplib as app
5+
import socket
6+
from typing import Any
7+
from pathlib import Path
8+
9+
10+
class FakeSock(socket.socket):
11+
def __init__(self, ip: str = '127.0.0.1'):
12+
super().__init__(socket.AF_INET, socket.SOCK_STREAM)
13+
self._ip = ip
14+
15+
def getpeername(self):
16+
return (self._ip, 12345)
17+
18+
19+
def setup_function(func: Any) -> None:
20+
# Reset DOS and rate-limit related global state before each test
21+
app.ws_dos_violations.clear()
22+
app.ws_last_message.clear()
23+
app.ws_messages_per_second.clear()
24+
app.dos_ip_requests.clear()
25+
app.dos_blocked_ips.clear()
26+
app.ws_rate_limit_delays.clear()
27+
app.ws_rate_limit_counts.clear()
28+
app.rate_limit_delays.clear()
29+
app.rate_limit_counts.clear()
30+
31+
32+
def test_check_websocket_dos_disconnect():
33+
sock = FakeSock('9.9.9.9')
34+
35+
# Ensure fresh state
36+
if sock in app.ws_dos_violations: del app.ws_dos_violations[sock]
37+
if sock in app.ws_messages_per_second: del app.ws_messages_per_second[sock]
38+
if sock in app.ws_last_message: del app.ws_last_message[sock]
39+
40+
now = time.time()
41+
# Simulate message count already at the limit
42+
app.ws_messages_per_second[sock] = (now, app.DOS_WS_MAX_MESSAGES_PER_SECOND)
43+
app.ws_dos_violations[sock] = app.DOS_WS_MAX_VIOLATIONS - 1
44+
45+
# This should increment violations and cause a disconnect (True)
46+
res = app._check_websocket_dos_protection(sock)
47+
assert res is True
48+
assert app.ws_dos_violations.get(sock, 0) >= app.DOS_WS_MAX_VIOLATIONS
49+
50+
51+
def test_ratelimit_count_based_socket_mode():
52+
sock = FakeSock('127.0.0.1')
53+
key = (sock, 'count_test')
54+
# Use count-based: allow 2 requests per 1 second
55+
# First two should be allowed, third should be rate-limited
56+
assert app.ratelimit(sock, (2, 1), 'count_test') is False
57+
assert app.ratelimit(sock, (2, 1), 'count_test') is False
58+
assert app.ratelimit(sock, (2, 1), 'count_test') is True
59+
60+
61+
@pytest.mark.asyncio
62+
async def test_template_redirect_and_save_load_users(tmp_path: Path):
63+
# Create static dir and a simple template file
64+
os.makedirs('static', exist_ok=True)
65+
tpl_path = os.path.join('static', 'test_template.html')
66+
with open(tpl_path, 'w', encoding='utf-8') as f:
67+
f.write('<h1>{{ name }}</h1>')
68+
69+
# Render template
70+
out = await app.template('test_template.html', name='Alice')
71+
assert '<h1>Alice</h1>' in out
72+
73+
# Redirect
74+
code, html = await app.redirect('https://example.com')
75+
assert code == 302
76+
assert 'https://example.com' in html
77+
78+
# Save and load users
79+
app.users.clear(); app.profiles.clear()
80+
assert app.create_user('u1', 'pass') is True
81+
tmpfile = tmp_path / 'users.json'
82+
assert app.save_users(str(tmpfile)) is True
83+
84+
# Clear and reload
85+
app.users.clear(); app.profiles.clear()
86+
assert app.load_users(str(tmpfile)) is True
87+
assert 'u1' in app.users
88+
89+
# Cleanup files created
90+
try:
91+
os.remove(tpl_path)
92+
if not os.listdir('static'):
93+
os.rmdir('static')
94+
except Exception:
95+
pass

0 commit comments

Comments
 (0)