# -*- coding: utf-8 -*- """Tests for misc.py utility functions.""" from __future__ import print_function import re import sys import os import pytest sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import misc class TestTimestamp: """Tests for timestamp() function.""" def test_timestamp_format(self): """timestamp() returns HH:MM:SS format.""" ts = misc.timestamp() assert re.match(r'^\d{2}:\d{2}:\d{2}$', ts) def test_timestamp_valid_hours(self): """timestamp() hours are 00-23.""" ts = misc.timestamp() hours = int(ts.split(':')[0]) assert 0 <= hours <= 23 def test_timestamp_valid_minutes(self): """timestamp() minutes are 00-59.""" ts = misc.timestamp() minutes = int(ts.split(':')[1]) assert 0 <= minutes <= 59 def test_timestamp_valid_seconds(self): """timestamp() seconds are 00-59.""" ts = misc.timestamp() seconds = int(ts.split(':')[2]) assert 0 <= seconds <= 59 class TestTorProxyUrl: """Tests for tor_proxy_url() function.""" def test_basic_format(self): """tor_proxy_url() returns socks5:// prefix.""" result = misc.tor_proxy_url('127.0.0.1:9050') assert result == 'socks5://127.0.0.1:9050' def test_custom_host_port(self): """tor_proxy_url() works with custom host:port.""" result = misc.tor_proxy_url('10.200.1.1:9150') assert result == 'socks5://10.200.1.1:9150' def test_ipv6_host(self): """tor_proxy_url() handles IPv6 addresses.""" result = misc.tor_proxy_url('[::1]:9050') assert result == 'socks5://[::1]:9050' class TestLogLevel: """Tests for set_log_level() and get_log_level() functions.""" def setup_method(self): """Reset log level before each test.""" misc.set_log_level(1) # Default info level def test_get_default_level(self): """get_log_level() returns default level.""" assert misc.get_log_level() == 1 def test_set_integer_level(self): """set_log_level() accepts integer.""" misc.set_log_level(0) assert misc.get_log_level() == 0 misc.set_log_level(2) assert misc.get_log_level() == 2 def test_set_string_debug(self): """set_log_level('debug') sets level 0.""" misc.set_log_level('debug') assert misc.get_log_level() == 0 def test_set_string_info(self): """set_log_level('info') sets level 1.""" misc.set_log_level('info') assert misc.get_log_level() == 1 def test_set_string_warn(self): """set_log_level('warn') sets level 2.""" misc.set_log_level('warn') assert misc.get_log_level() == 2 def test_set_string_error(self): """set_log_level('error') sets level 3.""" misc.set_log_level('error') assert misc.get_log_level() == 3 def test_set_string_none(self): """set_log_level('none') suppresses all.""" misc.set_log_level('none') assert misc.get_log_level() == 99 def test_invalid_string_defaults_to_info(self): """set_log_level() with invalid string defaults to 1.""" misc.set_log_level('invalid') assert misc.get_log_level() == 1 class TestIsSSLProtocolError: """Tests for is_ssl_protocol_error() function.""" def test_none_returns_false(self): """is_ssl_protocol_error(None) returns False.""" assert misc.is_ssl_protocol_error(None) is False def test_empty_returns_false(self): """is_ssl_protocol_error('') returns False.""" assert misc.is_ssl_protocol_error('') is False def test_wrong_version_number(self): """Detects 'wrong version number' as protocol error.""" assert misc.is_ssl_protocol_error('wrong version number') is True def test_unsupported_protocol(self): """Detects 'unsupported protocol' as protocol error.""" assert misc.is_ssl_protocol_error('unsupported protocol') is True def test_unexpected_eof(self): """Detects 'unexpected eof' as protocol error.""" assert misc.is_ssl_protocol_error('unexpected eof') is True def test_eof_occurred(self): """Detects 'eof occurred' as protocol error.""" assert misc.is_ssl_protocol_error('eof occurred') is True def test_alert_handshake_failure(self): """Detects 'alert handshake failure' as protocol error.""" assert misc.is_ssl_protocol_error('alert handshake failure') is True def test_http_request(self): """Detects 'http request' as protocol error (sent HTTP to HTTPS).""" assert misc.is_ssl_protocol_error('http request') is True def test_no_ciphers_available(self): """Detects 'no ciphers available' as protocol error.""" assert misc.is_ssl_protocol_error('no ciphers available') is True def test_case_insensitive(self): """is_ssl_protocol_error() is case-insensitive.""" assert misc.is_ssl_protocol_error('WRONG VERSION NUMBER') is True assert misc.is_ssl_protocol_error('Wrong Version Number') is True def test_certificate_error_not_protocol(self): """Certificate errors are not protocol errors.""" assert misc.is_ssl_protocol_error('certificate verify failed') is False def test_hostname_mismatch_not_protocol(self): """Hostname mismatch is not protocol error.""" assert misc.is_ssl_protocol_error('hostname mismatch') is False def test_expired_cert_not_protocol(self): """Expired certificate is not protocol error.""" assert misc.is_ssl_protocol_error('certificate has expired') is False def test_embedded_in_message(self): """Detects pattern embedded in longer message.""" assert misc.is_ssl_protocol_error( 'SSL error: wrong version number in record') is True class TestFailureConstants: """Tests for failure category constants.""" def test_constants_are_strings(self): """Failure constants are strings.""" assert isinstance(misc.FAIL_TIMEOUT, str) assert isinstance(misc.FAIL_REFUSED, str) assert isinstance(misc.FAIL_AUTH, str) assert isinstance(misc.FAIL_UNREACHABLE, str) assert isinstance(misc.FAIL_DNS, str) assert isinstance(misc.FAIL_SSL, str) assert isinstance(misc.FAIL_CLOSED, str) assert isinstance(misc.FAIL_PROXY, str) assert isinstance(misc.FAIL_OTHER, str) def test_constants_unique(self): """Failure constants have unique values.""" constants = [ misc.FAIL_TIMEOUT, misc.FAIL_REFUSED, misc.FAIL_AUTH, misc.FAIL_UNREACHABLE, misc.FAIL_DNS, misc.FAIL_SSL, misc.FAIL_CLOSED, misc.FAIL_PROXY, misc.FAIL_OTHER, ] assert len(constants) == len(set(constants)) def test_ssl_errors_contains_ssl(self): """SSL_ERRORS contains FAIL_SSL.""" assert misc.FAIL_SSL in misc.SSL_ERRORS def test_conn_errors_contents(self): """CONN_ERRORS contains connection-related failures.""" assert misc.FAIL_TIMEOUT in misc.CONN_ERRORS assert misc.FAIL_REFUSED in misc.CONN_ERRORS assert misc.FAIL_UNREACHABLE in misc.CONN_ERRORS assert misc.FAIL_CLOSED in misc.CONN_ERRORS assert misc.FAIL_DNS in misc.CONN_ERRORS # Auth and proxy errors are not connection errors assert misc.FAIL_AUTH not in misc.CONN_ERRORS assert misc.FAIL_PROXY not in misc.CONN_ERRORS class TestLogLevels: """Tests for LOG_LEVELS dictionary.""" def test_debug_is_lowest(self): """debug is the lowest (most verbose) level.""" assert misc.LOG_LEVELS['debug'] == 0 def test_info_is_one(self): """info level is 1.""" assert misc.LOG_LEVELS['info'] == 1 def test_warn_is_two(self): """warn level is 2.""" assert misc.LOG_LEVELS['warn'] == 2 def test_error_is_three(self): """error level is 3.""" assert misc.LOG_LEVELS['error'] == 3 def test_none_suppresses_all(self): """none level (99) suppresses all output.""" assert misc.LOG_LEVELS['none'] == 99 def test_aliases_equal_info(self): """rate, scraper, stats, diag are aliases for info.""" assert misc.LOG_LEVELS['rate'] == misc.LOG_LEVELS['info'] assert misc.LOG_LEVELS['scraper'] == misc.LOG_LEVELS['info'] assert misc.LOG_LEVELS['stats'] == misc.LOG_LEVELS['info'] assert misc.LOG_LEVELS['diag'] == misc.LOG_LEVELS['info']