From 6fbf1903ac55dc174a3da8ac257e2e43c2ec096b Mon Sep 17 00:00:00 2001 From: mmeest Date: Fri, 31 Oct 2025 12:39:35 +0200 Subject: [PATCH 1/6] initial --- .../whois_server_integration_test.rb | 225 +++++++++++++++++ test/logging_test.rb | 237 +++++++++++++++++- test/models/contact_test.rb | 26 ++ test/test_helper.rb | 5 +- test/unit/whois_server_core_test.rb | 40 +++ test/unit/whois_server_methods_test.rb | 219 ++++++++++++++++ test/validators/unicode_validator_test.rb | 47 +++- test/whois_server_test.rb | 12 - 8 files changed, 792 insertions(+), 19 deletions(-) create mode 100644 test/integration/whois_server_integration_test.rb create mode 100644 test/unit/whois_server_core_test.rb create mode 100644 test/unit/whois_server_methods_test.rb delete mode 100644 test/whois_server_test.rb diff --git a/test/integration/whois_server_integration_test.rb b/test/integration/whois_server_integration_test.rb new file mode 100644 index 0000000..331be75 --- /dev/null +++ b/test/integration/whois_server_integration_test.rb @@ -0,0 +1,225 @@ +require 'test_helper' +require 'simpleidn' +require_relative '../../lib/whois_server' +require_relative '../../lib/whois_server_core' +require_relative '../../app/models/whois_record' +require_relative '../../app/validators/unicode_validator' + +class WhoisServerIntegrationTest < Minitest::Test + include WhoisServer + + def setup + super + @sent_data = [] + @connection_closed = false + @connection_closed_after_writing = false + @logger_output = StringIO.new + @logger = Logger.new(@logger_output) + ENV['WHOIS_ENV'] = 'test' + end + + def test_receive_data_with_invalid_data + stubs(:invalid_data?).returns(true) + stubs(:invalid_encoding_msg).returns('Invalid encoding') + stubs(:connection).returns(nil) + + receive_data('invalid\xFF') + + assert_equal 1, @sent_data.length + assert_includes @sent_data.first, 'Invalid encoding' + assert @connection_closed_after_writing + end + + def test_receive_data_with_no_ip + stubs(:extract_ip).returns(nil) + stubs(:connection).returns(nil) + + receive_data('example.ee') + + assert_equal 0, @sent_data.length + end + + def test_receive_data_full_flow + data = 'example.ee' + ip = ['12345', '127.0.0.1'] + mock_record = Minitest::Mock.new + mock_record.expect(:unix_body, 'Whois body') + mock_record.expect(:id, 1) + + stubs(:connection).returns(nil) + stubs(:extract_ip).returns(ip) + stubs(:invalid_data?).returns(false) + WhoisRecord.stubs(:find_by).returns(mock_record) + + receive_data(data) + + assert_equal 1, @sent_data.length + assert @connection_closed_after_writing + mock_record.verify + end + + def test_receive_data_integration_full_flow + data = 'integration-test.ee' + ip = ['12345', '127.0.0.1'] + + stubs(:connection).returns(nil) + stubs(:extract_ip).returns(ip) + stubs(:invalid_data?).returns(false) + + mock_record = Minitest::Mock.new + mock_record.expect(:unix_body, 'Integration test body') + mock_record.expect(:id, 1) + + WhoisRecord.stubs(:find_by).returns(mock_record) + + receive_data(data) + + assert_equal 1, @sent_data.length + assert @connection_closed_after_writing + mock_record.verify + end + + def test_process_whois_request_with_invalid_encoding + data = "\xFF\xFE".force_encoding('ASCII-8BIT') + ip = ['12345', '127.0.0.1'] + + send(:process_whois_request, data, ip) + + assert_equal 1, @sent_data.length + assert_includes @sent_data.first, 'invalid encoding' + end + + def test_process_whois_request_with_invalid_domain_format + data = 'invalid..domain.ee' + ip = ['12345', '127.0.0.1'] + + send(:process_whois_request, data, ip) + + assert_equal 1, @sent_data.length + assert_includes @sent_data.first, 'Policy error' + end + + def test_process_whois_request_with_valid_domain_found + data = 'example.ee' + ip = ['12345', '127.0.0.1'] + mock_record = Minitest::Mock.new + mock_record.expect(:unix_body, 'Whois body') + mock_record.expect(:id, 1) + + WhoisRecord.stubs(:find_by).returns(mock_record) + + send(:process_whois_request, data, ip) + + assert_equal 1, @sent_data.length + assert_equal 'Whois body', @sent_data.first + mock_record.verify + end + + def test_process_whois_request_with_valid_domain_not_found + data = 'nonexistent.ee' + ip = ['12345', '127.0.0.1'] + + WhoisRecord.stubs(:find_by).returns(nil) + + send(:process_whois_request, data, ip) + + assert_equal 1, @sent_data.length + assert_includes @sent_data.first, 'Domain not found' + end + + def test_process_whois_request_strips_whitespace + data = ' example.ee ' + ip = ['12345', '127.0.0.1'] + mock_record = Minitest::Mock.new + mock_record.expect(:unix_body, 'Whois body') + mock_record.expect(:id, 1) + + WhoisRecord.stubs(:find_by).returns(mock_record) + + send(:process_whois_request, data, ip) + + assert_equal 1, @sent_data.length + assert_equal 'Whois body', @sent_data.first + mock_record.verify + end + + def test_process_whois_request_with_whitespace_only + data = ' ' + ip = ['12345', '127.0.0.1'] + + send(:process_whois_request, data, ip) + + assert_equal 1, @sent_data.length + assert_includes @sent_data.first, 'Policy error' + end + + def test_process_whois_request_calls_logging_with_punycode + data = 'example.ee' + ip = ['12345', '127.0.0.1'] + + mock_record = Minitest::Mock.new + mock_record.expect(:unix_body, 'Whois body') + mock_record.expect(:id, 1) + + WhoisRecord.stubs(:find_by).returns(mock_record) + + send(:process_whois_request, data, ip) + + assert_equal 1, @sent_data.length + assert_equal 'Whois body', @sent_data.first + mock_record.verify + end + + def test_handle_whois_record_found + name = 'example.ee' + ip = ['12345', '127.0.0.1'] + cleaned_data = 'example.ee' + + mock_record = Minitest::Mock.new + mock_record.expect(:unix_body, 'Whois record body') + mock_record.expect(:id, 1) + + WhoisRecord.stubs(:find_by).with(name: name).returns(mock_record) + + send(:handle_whois_record, name, ip, cleaned_data) + + assert_equal 1, @sent_data.length + assert_equal 'Whois record body', @sent_data.first + mock_record.verify + end + + def test_handle_whois_record_not_found + name = 'nonexistent.ee' + ip = ['12345', '127.0.0.1'] + cleaned_data = 'nonexistent.ee' + + WhoisRecord.stubs(:find_by).with(name: name).returns(nil) + stubs(:no_entries_msg).returns('Domain not found') + + send(:handle_whois_record, name, ip, cleaned_data) + + assert_equal 1, @sent_data.length + assert_includes @sent_data.first, 'Domain not found' + end + + def send_data(data) + @sent_data << data + end + + def close_connection_after_writing + @connection_closed_after_writing = true + end + + def close_connection + @connection_closed = true + end + + def get_peername + Socket.pack_sockaddr_in(12345, '127.0.0.1') + end + + def logger + @logger + end +end + diff --git a/test/logging_test.rb b/test/logging_test.rb index 089dafc..3820f8c 100644 --- a/test/logging_test.rb +++ b/test/logging_test.rb @@ -9,9 +9,17 @@ class LoggingTest < Minitest::Test def setup @output = StringIO.new - @logger = Logger.new(@output) - @logger.formatter = proc do |severity, datetime, progname, msg| - "[#{progname}] #{msg}\n" + @logger = nil + end + + def logger + @logger ||= begin + original_logger = super + io_logger = Logger.new(@output, progname: 'whois') + io_logger.formatter = proc do |severity, datetime, progname, msg| + "[#{progname}] #{msg}\n" + end + @logger = io_logger end end @@ -74,4 +82,227 @@ def test_log_record_not_found assert_includes log_output, '"status":"not_found"' assert_includes log_output, '"record_found":false' end + + def test_log_json_with_invalid_encoding + payload = { + domain: "test\xFF\xFE".force_encoding('BINARY'), + ip: '127.0.0.1' + } + + log_json(payload) + @output.rewind + log_output = @output.read + + assert log_output.length > 0 + end + + def test_log_json_with_json_generator_error + payload = { + domain: 'test' + } + + logger.stubs(:info).raises(JSON::GeneratorError.new('test')) + logger.expects(:error).at_least_once + + log_json(payload) + end + + def test_sanitize_for_json_with_array + array_with_invalid = ["test\xFF".force_encoding('BINARY'), 'valid'] + result = send(:sanitize_for_json, array_with_invalid) + + assert_equal Array, result.class + assert_equal 2, result.length + end + + def test_sanitize_for_json_with_hash + hash_with_invalid = { + key1: "test\xFF".force_encoding('BINARY'), + key2: 'valid' + } + result = send(:sanitize_for_json, hash_with_invalid) + + assert_equal Hash, result.class + assert_equal 2, result.length + end + + def test_sanitize_for_json_with_other_types + assert_equal 42, send(:sanitize_for_json, 42) + assert_nil send(:sanitize_for_json, nil) + assert_equal true, send(:sanitize_for_json, true) + assert_equal false, send(:sanitize_for_json, false) + end + + def test_build_base_log_includes_timestamp + payload = { domain: 'test.ee', ip: '127.0.0.1' } + result = send(:build_base_log, payload) + + assert result.key?(:timestamp) + assert result[:timestamp].is_a?(String) + assert_match /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/, result[:timestamp] + end + + def test_build_base_log_includes_service_info + payload = { domain: 'test.ee' } + result = send(:build_base_log, payload) + + assert result.key?(:service) + assert_equal 'whois', result[:service][:name] + assert_equal 'whois_server', result[:service][:type] + end + + def test_event_info_with_record_found + payload = { record_found: true, status: 'custom_status' } + result = send(:event_info, payload) + + assert_equal 'custom_status', result[:status] + end + + def test_event_info_without_record_found + payload = { record_found: false } + result = send(:event_info, payload) + + assert_equal 'not_found', result[:status] + end + + def test_event_info_defaults_to_not_found + payload = {} + result = send(:event_info, payload) + + assert_equal 'not_found', result[:status] + end + + def test_source_info_returns_host_and_ip + result = send(:source_info) + + assert result.key?(:host) + assert result.key?(:ip) + assert result[:host].is_a?(String) + assert [NilClass, String].include?(result[:ip].class) if result[:ip] + end + + def test_current_user_with_env + ENV['WHOIS_USER'] = 'test_user' + result = send(:current_user) + + assert_equal 'test_user', result + ENV.delete('WHOIS_USER') + end + + def test_current_user_without_env + original_whois_user = ENV.delete('WHOIS_USER') + + result = send(:current_user) + assert result.is_a?(String) + assert result.length > 0 + + ENV['WHOIS_USER'] = original_whois_user if original_whois_user + end + + def test_logger_info_with_env_version + ENV['APP_VERSION'] = '2.0.0' + result = send(:logger_info) + + assert_equal 'whois', result[:name] + assert_equal '2.0.0', result[:version] + ENV.delete('APP_VERSION') + end + + def test_logger_info_without_env_version + original_version = ENV.delete('APP_VERSION') + + result = send(:logger_info) + assert_equal 'whois', result[:name] + assert_equal '1.0', result[:version] + + ENV['APP_VERSION'] = original_version if original_version + end + + def test_client_info_extracts_ip_and_port + payload = { ip: '192.168.1.1', session_id: '9999' } + result = send(:client_info, payload) + + assert_equal '192.168.1.1', result[:ip] + assert_equal '9999', result[:port] + end + + def test_data_info_extracts_all_fields + payload = { + domain: 'test.ee', + searched_by: 'test.ee', + record_found: true, + record_id: 42 + } + result = send(:data_info, payload) + + assert_equal 'test.ee', result[:query] + assert_equal 'test.ee', result[:normalized_query] + assert_equal true, result[:record_found] + assert_equal 42, result[:record_id] + end + + def test_data_info_with_nil_values + payload = {} + result = send(:data_info, payload) + + assert_nil result[:query] + assert_nil result[:normalized_query] + assert_nil result[:record_found] + assert_nil result[:record_id] + end + + def test_metadata_info + result = send(:metadata_info) + + assert_equal 'whois', result[:protocol] + assert_equal '1.0', result[:version] + end + + def test_build_base_log_with_compact_removes_nil_error + payload = { domain: 'test.ee', ip: '127.0.0.1' } + result = send(:build_base_log, payload) + + refute result.key?(:error) unless payload[:message] + end + + def test_build_base_log_includes_all_sections + payload = { + domain: 'test.ee', + ip: '127.0.0.1', + session_id: '12345', + message: 'test message' + } + result = send(:build_base_log, payload) + + assert result.key?(:timestamp) + assert result.key?(:level) + assert result.key?(:logger) + assert result.key?(:service) + assert result.key?(:event) + assert result.key?(:source) + assert result.key?(:client) + assert result.key?(:user) + assert result.key?(:data) + assert result.key?(:metadata) + assert_equal 'test message', result[:error] + end + + def test_log_json_with_encoding_error + payload = { domain: 'test' } + + stubs(:sanitize_for_json).raises(Encoding::UndefinedConversionError.new('test')) + logger.expects(:error).at_least_once + + log_json(payload) + end + + def test_service_info_with_env + ENV['WHOIS_ENV'] = 'production' + result = send(:service_info) + + assert_equal 'whois', result[:name] + assert_equal 'whois_server', result[:type] + assert_equal 'production', result[:env] + ENV['WHOIS_ENV'] = 'test' + end end diff --git a/test/models/contact_test.rb b/test/models/contact_test.rb index 948c846..50c8f85 100644 --- a/test/models/contact_test.rb +++ b/test/models/contact_test.rb @@ -11,4 +11,30 @@ def test_attribute_concealed contact = Contact.new(disclosed_attributes: %w[other]) refute contact.attribute_disclosed?(:name) end + + def test_attribute_disclosed_with_string_attribute + contact = Contact.new(disclosed_attributes: %w[name email]) + assert contact.attribute_disclosed?('name') + assert contact.attribute_disclosed?(:email) + end + + def test_attribute_disclosed_with_nil_disclosed_attributes + contact = Contact.new(disclosed_attributes: nil) + refute contact.attribute_disclosed?(:name) + end + + def test_publishable_returns_true + contact = Contact.new(registrant_publishable: true) + assert contact.publishable? + end + + def test_publishable_returns_false + contact = Contact.new(registrant_publishable: false) + refute contact.publishable? + end + + def test_publishable_with_nil + contact = Contact.new(registrant_publishable: nil) + refute contact.publishable? + end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 0ecfa6a..ed10f9b 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,3 +1,6 @@ +require 'simplecov' +SimpleCov.start 'rails' + require 'minitest/autorun' require 'mocha/minitest' require 'active_record' @@ -5,8 +8,6 @@ ENV['WHOIS_ENV'] ||= 'test' -require 'simplecov' -SimpleCov.start 'rails' class Minitest::Test def dbconfig diff --git a/test/unit/whois_server_core_test.rb b/test/unit/whois_server_core_test.rb new file mode 100644 index 0000000..8ee058d --- /dev/null +++ b/test/unit/whois_server_core_test.rb @@ -0,0 +1,40 @@ +require 'test_helper' +require_relative '../../lib/whois_server' +require_relative '../../lib/whois_server_core' + +class WhoisServerCoreTest < Minitest::Test + include WhoisServer + + def test_allows_one_letter_domain + assert_match WhoisServerCore::DOMAIN_NAME_REGEXP, 'a.ee' + assert_match WhoisServerCore::DOMAIN_NAME_REGEXP, 'õ.ee' + assert_match WhoisServerCore::DOMAIN_NAME_REGEXP, '1.ee' + refute_match WhoisServerCore::DOMAIN_NAME_REGEXP, 'a..ee' + assert_match WhoisServerCore::DOMAIN_NAME_REGEXP, 'ab.ee' + end + + def test_domain_valid_format_valid_domains + assert send(:domain_valid_format?, 'example.ee') + assert send(:domain_valid_format?, 'test-domain.ee') + assert send(:domain_valid_format?, 'õ.ee') + assert send(:domain_valid_format?, 'example.ee'.downcase) + end + + def test_domain_valid_format_invalid_domains + refute send(:domain_valid_format?, 'invalid..ee') + refute send(:domain_valid_format?, '') + refute send(:domain_valid_format?, 'no-tld') + refute send(:domain_valid_format?, '.ee') + refute send(:domain_valid_format?, 'example.') + end + + def test_domain_valid_format_with_whitespace + assert send(:domain_valid_format?, ' example.ee ') + assert send(:domain_valid_format?, "\texample.ee\n") + end + + def test_domain_valid_format_with_mixed_case + assert send(:domain_valid_format?, 'EXAMPLE.EE') + assert send(:domain_valid_format?, 'ExAmPlE.Ee') + end +end diff --git a/test/unit/whois_server_methods_test.rb b/test/unit/whois_server_methods_test.rb new file mode 100644 index 0000000..51de74c --- /dev/null +++ b/test/unit/whois_server_methods_test.rb @@ -0,0 +1,219 @@ +require 'test_helper' +require 'tempfile' +require_relative '../../lib/whois_server' +require_relative '../../lib/whois_server_core' +require_relative '../../app/validators/unicode_validator' + +class WhoisServerMethodsTest < Minitest::Test + include WhoisServer + + def setup + super + @logger_output = StringIO.new + @logger = Logger.new(@logger_output) + ENV['WHOIS_ENV'] = 'test' + end + + def test_policy_error_msg + msg = send(:policy_error_msg) + assert_includes msg, 'Policy error' + assert_includes msg, 'internet.ee' + assert_includes msg, send(:footer_msg) + end + + def test_no_entries_msg + msg = send(:no_entries_msg) + assert_includes msg, 'Domain not found' + assert_includes msg, send(:footer_msg) + end + + def test_invalid_encoding_msg + msg = send(:invalid_encoding_msg) + assert_includes msg, 'invalid encoding' + assert_includes msg, 'utf-8' + assert_includes msg, send(:footer_msg) + end + + def test_footer_msg + msg = send(:footer_msg) + assert_includes msg, 'Estonia .ee Top Level Domain WHOIS server' + assert_includes msg, 'internet.ee' + end + + def test_no_body_msg + msg = send(:no_body_msg) + assert_includes msg, 'technical issue' + assert_includes msg, send(:footer_msg) + end + + def test_invalid_data_with_nil + ip = ['12345', '127.0.0.1'] + assert send(:invalid_data?, nil, ip) + end + + def test_invalid_data_with_empty_string + ip = ['12345', '127.0.0.1'] + result = send(:invalid_data?, '', ip) + assert [true, false].include?(result) + end + + def test_invalid_data_with_invalid_encoding + ip = ['12345', '127.0.0.1'] + invalid_data = 'example.ee' + UnicodeValidator.any_instance.stubs(:valid?).returns(false) + assert send(:invalid_data?, invalid_data, ip) + end + + def test_invalid_data_with_valid_data + ip = ['12345', '127.0.0.1'] + valid_data = 'example.ee' + UnicodeValidator.any_instance.stubs(:valid?).returns(true) + refute send(:invalid_data?, valid_data, ip) + end + + def test_extract_ip_success + ip = send(:extract_ip) + assert_equal 12345, ip[0] + assert_equal '127.0.0.1', ip[1] + end + + def test_extract_ip_with_error + stubs(:get_peername).raises(StandardError.new('Connection failed')) + ip = send(:extract_ip) + assert_nil ip + assert @connection_closed + end + + def test_extract_ip_logs_error_on_exception + logger.expects(:error).at_least_once + stubs(:get_peername).raises(StandardError.new('connection failed')) + + ip = send(:extract_ip) + assert_nil ip + assert @connection_closed + end + + def test_yaml_properly_load_file_with_aliases + test_file = Tempfile.new(['test', '.yml']) + test_file.write(<<~YAML) + default: &default + key: value + test: + <<: *default + YAML + test_file.close + + result = YAML.properly_load_file(test_file.path) + assert_equal 'value', result['test']['key'] + test_file.unlink + rescue ArgumentError + skip 'YAML aliases not supported in this Ruby version' + end + + def test_yaml_properly_load_file_fallback + test_file = Tempfile.new(['test', '.yml']) + test_file.write(<<~YAML) + test: + key: value + YAML + test_file.close + + result = YAML.properly_load_file(test_file.path) + assert_equal 'value', result['test']['key'] + test_file.unlink + end + + def test_dbconfig_returns_config + ENV['WHOIS_ENV'] = 'test' + config = dbconfig + refute_nil config + assert config.key?('database') + end + + def test_dbconfig_with_error + original_dbconfig = @dbconfig + @dbconfig = nil + + YAML.stubs(:properly_load_file).raises(NoMethodError.new('test error')) + + config = dbconfig + @dbconfig = original_dbconfig + end + + def test_dbconfig_logs_fatal_error + original_dbconfig = @dbconfig + @dbconfig = nil + + logger.expects(:fatal).at_least_once + YAML.stubs(:properly_load_file).raises(NoMethodError.new('test')) + + dbconfig + + @dbconfig = original_dbconfig + end + + def test_connection_establishes_connection + stubs(:dbconfig).returns({ 'database' => 'test_db', 'adapter' => 'postgresql' }) + + conn1 = connection + refute_nil conn1 + + conn2 = connection + assert_equal conn1.object_id, conn2.object_id + end + + def test_logging_methods_are_called_on_invalid_encoding + ip = ['12345', '127.0.0.1'] + data = 'invalid\xFF' + + logger.expects(:info).at_least_once + + send(:log_invalid_encoding, ip, data) + end + + def test_logging_methods_are_called_on_policy_error + ip = ['12345', '127.0.0.1'] + cleaned_data = 'invalid..domain.ee' + name = 'invalid..domain.ee' + + logger.expects(:info).at_least_once + + send(:log_policy_error, ip, cleaned_data, name) + end + + def test_logging_methods_are_called_on_record_found + ip = ['12345', '127.0.0.1'] + cleaned_data = 'example.ee' + name = 'example.ee' + mock_record = Minitest::Mock.new + mock_record.expect(:id, 42) + + logger.expects(:info).at_least_once + + send(:log_record_found, ip, cleaned_data, name, mock_record) + mock_record.verify + end + + def test_logging_methods_are_called_on_record_not_found + ip = ['12345', '127.0.0.1'] + cleaned_data = 'nonexistent.ee' + name = 'nonexistent.ee' + + logger.expects(:info).at_least_once + + send(:log_record_not_found, ip, cleaned_data, name) + end + + def get_peername + Socket.pack_sockaddr_in(12345, '127.0.0.1') + end + + def close_connection + @connection_closed = true + end + + def logger + @logger + end +end + diff --git a/test/validators/unicode_validator_test.rb b/test/validators/unicode_validator_test.rb index 15272f4..b18d694 100644 --- a/test/validators/unicode_validator_test.rb +++ b/test/validators/unicode_validator_test.rb @@ -23,14 +23,12 @@ def test_domain_name_not_in_utf8_is_not_valid end def test_invalid_utf8_sequences - # Test with escaped backslash as received from whois command refute @validator.new("\\xFF.ee\r\n").valid? refute @validator.new("\\xFF\\xFE\\xFD.ee\r\n").valid? refute @validator.new("test\\xC3\\x28.ee\r\n").valid? end def test_invalid_utf8_bytes - # Test with actual invalid bytes for direct programmatic access refute @validator.new("\xFF.ee").valid? refute @validator.new("\xFF\xFE\xFD.ee").valid? refute @validator.new("test\xC3\x28.ee").valid? @@ -50,4 +48,49 @@ def test_mixed_chinese_and_ascii_domain_name value = 'test中国123.ee'.encode('utf-8') assert @validator.new(value).valid? end + + def test_nil_value + refute @validator.new(nil).valid? + end + + def test_empty_string + assert @validator.new('').valid? + end + + def test_unicode_escaped_sequences_valid + assert @validator.new("\\x61.ee").valid? + assert @validator.new("test\\x61test.ee").valid? + end + + def test_unicode_escaped_sequences_invalid + refute @validator.new("\\xFF.ee").valid? + end + + def test_string_with_trailing_carriage_return + assert @validator.new("example.ee\r\n").valid? + refute @validator.new("\\xFF.ee\r\n").valid? + end + + def test_valid_encoding_but_rescues_argument_error + value = "\xFF\xFE".force_encoding('BINARY') + validator = @validator.new(value) + result = validator.valid? + assert [true, false].include?(result) + end + + def test_hex_escaped_multibyte_checkmark_is_valid + assert @validator.new('\\xE2\\x9C\\x94').valid? + end + + def test_returns_false_when_valid_utf8_encoding_raises_argument_error + validator = @validator.new('test') + validator.stub(:valid_utf8_encoding?, proc { raise ArgumentError }) do + refute validator.valid? + end + end + + def test_unescape_hex_sequences_returns_original_when_no_hex_patterns + validator = @validator.new('PlainText') + assert_equal 'PlainText', validator.send(:unescape_hex_sequences, 'PlainText') + end end diff --git a/test/whois_server_test.rb b/test/whois_server_test.rb deleted file mode 100644 index 9ebf54d..0000000 --- a/test/whois_server_test.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'minitest/autorun' -require_relative '../lib/whois_server_core' - -class WhoisServerTest < Minitest::Test - def test_allows_one_letter_domain - assert_match WhoisServerCore::DOMAIN_NAME_REGEXP, 'a.ee' - assert_match WhoisServerCore::DOMAIN_NAME_REGEXP, 'õ.ee' - assert_match WhoisServerCore::DOMAIN_NAME_REGEXP, '1.ee' - refute_match WhoisServerCore::DOMAIN_NAME_REGEXP, 'a..ee' - assert_match WhoisServerCore::DOMAIN_NAME_REGEXP, 'ab.ee' - end -end From 653fdb8926e7e3cbe5e2fca97b1b1770edca0d41 Mon Sep 17 00:00:00 2001 From: mmeest Date: Fri, 31 Oct 2025 15:51:40 +0200 Subject: [PATCH 2/6] test: prevent EventMachine from starting in tests; improve coverage --- test/test_helper.rb | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/test/test_helper.rb b/test/test_helper.rb index ed10f9b..9df012a 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -8,16 +8,35 @@ ENV['WHOIS_ENV'] ||= 'test' +# --- Prevent EventMachine from starting in tests --- +# This monkey-patch must run before whois server files are required. +require 'eventmachine' +module EventMachine + class << self + def run(*) + yield if block_given? + end + + def start_server(*) + # no-op in tests to avoid binding real ports + end + + def set_effective_user(*) + # no-op in tests + end + end +end +# --------------------------------------------------- class Minitest::Test def dbconfig return @dbconfig unless @dbconfig.nil? begin - dbconf = YAML.load_file(File.open(File.expand_path('../config/database.yml', __dir__)), aliases: true) - @dbconfig = dbconf[(ENV['WHOIS_ENV'])] + dbconf = YAML.load_file(File.expand_path('../config/database.yml', __dir__), aliases: true) + @dbconfig = dbconf[ENV['WHOIS_ENV']] rescue NoMethodError => e - logger.fatal "\n----> Please inspect config/database.yml for issues! Error: #{e}\n\n" + warn "\n----> Please inspect config/database.yml for issues! Error: #{e}\n\n" end end @@ -27,7 +46,6 @@ def connection def setup super - connection end end From 1d90c8ff7f5413fb7df58a872b1537110c640adc Mon Sep 17 00:00:00 2001 From: mmeest Date: Wed, 12 Nov 2025 12:21:14 +0200 Subject: [PATCH 3/6] update tests --- .../whois_server_integration_test.rb | 197 ++++----------- test/logging_test.rb | 225 +----------------- 2 files changed, 44 insertions(+), 378 deletions(-) diff --git a/test/integration/whois_server_integration_test.rb b/test/integration/whois_server_integration_test.rb index 331be75..24087ff 100644 --- a/test/integration/whois_server_integration_test.rb +++ b/test/integration/whois_server_integration_test.rb @@ -18,188 +18,77 @@ def setup ENV['WHOIS_ENV'] = 'test' end - def test_receive_data_with_invalid_data - stubs(:invalid_data?).returns(true) - stubs(:invalid_encoding_msg).returns('Invalid encoding') - stubs(:connection).returns(nil) - - receive_data('invalid\xFF') - - assert_equal 1, @sent_data.length - assert_includes @sent_data.first, 'Invalid encoding' - assert @connection_closed_after_writing - end - - def test_receive_data_with_no_ip - stubs(:extract_ip).returns(nil) - stubs(:connection).returns(nil) - - receive_data('example.ee') - - assert_equal 0, @sent_data.length - end - - def test_receive_data_full_flow - data = 'example.ee' - ip = ['12345', '127.0.0.1'] - mock_record = Minitest::Mock.new - mock_record.expect(:unix_body, 'Whois body') - mock_record.expect(:id, 1) - + def stub_connection_and_ip(ip = ['12345', '127.0.0.1']) stubs(:connection).returns(nil) stubs(:extract_ip).returns(ip) - stubs(:invalid_data?).returns(false) - WhoisRecord.stubs(:find_by).returns(mock_record) - - receive_data(data) - - assert_equal 1, @sent_data.length - assert @connection_closed_after_writing - mock_record.verify end - def test_receive_data_integration_full_flow - data = 'integration-test.ee' - ip = ['12345', '127.0.0.1'] - - stubs(:connection).returns(nil) - stubs(:extract_ip).returns(ip) - stubs(:invalid_data?).returns(false) - + def mock_whois_record(body: 'Whois body', id: 1) mock_record = Minitest::Mock.new - mock_record.expect(:unix_body, 'Integration test body') - mock_record.expect(:id, 1) - + mock_record.expect(:unix_body, body) + mock_record.expect(:id, id) WhoisRecord.stubs(:find_by).returns(mock_record) - - receive_data(data) - - assert_equal 1, @sent_data.length - assert @connection_closed_after_writing - mock_record.verify + mock_record end - def test_process_whois_request_with_invalid_encoding - data = "\xFF\xFE".force_encoding('ASCII-8BIT') - ip = ['12345', '127.0.0.1'] - - send(:process_whois_request, data, ip) + def test_receive_data_rejects_invalid_encoding + invalid_payload = "\xFF\xFE".dup.force_encoding('ASCII-8BIT') + receive_data(invalid_payload) assert_equal 1, @sent_data.length - assert_includes @sent_data.first, 'invalid encoding' - end - - def test_process_whois_request_with_invalid_domain_format - data = 'invalid..domain.ee' - ip = ['12345', '127.0.0.1'] - - send(:process_whois_request, data, ip) - - assert_equal 1, @sent_data.length - assert_includes @sent_data.first, 'Policy error' + assert_includes @sent_data.first.downcase, 'invalid encoding' + assert @connection_closed_after_writing end - def test_process_whois_request_with_valid_domain_found - data = 'example.ee' - ip = ['12345', '127.0.0.1'] - mock_record = Minitest::Mock.new - mock_record.expect(:unix_body, 'Whois body') - mock_record.expect(:id, 1) - - WhoisRecord.stubs(:find_by).returns(mock_record) - - send(:process_whois_request, data, ip) - + def test_receive_data_returns_valid_whois_body + stub_connection_and_ip + mock_record = mock_whois_record(body: 'This is a valid WHOIS record') + + receive_data('example.ee') + assert_equal 1, @sent_data.length - assert_equal 'Whois body', @sent_data.first + assert_includes @sent_data.first, 'This is a valid WHOIS record' + assert @connection_closed_after_writing + mock_record.verify end - def test_process_whois_request_with_valid_domain_not_found - data = 'nonexistent.ee' - ip = ['12345', '127.0.0.1'] - - WhoisRecord.stubs(:find_by).returns(nil) - - send(:process_whois_request, data, ip) + def test_receive_data_accepts_valid_data + receive_data('example.ee') assert_equal 1, @sent_data.length - assert_includes @sent_data.first, 'Domain not found' + assert_includes @sent_data.first.downcase, 'domain not found' + assert @connection_closed_after_writing end - def test_process_whois_request_strips_whitespace - data = ' example.ee ' - ip = ['12345', '127.0.0.1'] - mock_record = Minitest::Mock.new - mock_record.expect(:unix_body, 'Whois body') - mock_record.expect(:id, 1) - - WhoisRecord.stubs(:find_by).returns(mock_record) - - send(:process_whois_request, data, ip) - - assert_equal 1, @sent_data.length - assert_equal 'Whois body', @sent_data.first - mock_record.verify - end + def test_receive_data_with_no_ip + stubs(:extract_ip).returns(nil) + stubs(:connection).returns(nil) - def test_process_whois_request_with_whitespace_only - data = ' ' - ip = ['12345', '127.0.0.1'] - - send(:process_whois_request, data, ip) - - assert_equal 1, @sent_data.length - assert_includes @sent_data.first, 'Policy error' - end + receive_data('example.ee') - def test_process_whois_request_calls_logging_with_punycode - data = 'example.ee' - ip = ['12345', '127.0.0.1'] - - mock_record = Minitest::Mock.new - mock_record.expect(:unix_body, 'Whois body') - mock_record.expect(:id, 1) - - WhoisRecord.stubs(:find_by).returns(mock_record) - - send(:process_whois_request, data, ip) - - assert_equal 1, @sent_data.length - assert_equal 'Whois body', @sent_data.first - mock_record.verify + assert_equal 0, @sent_data.length end - def test_handle_whois_record_found - name = 'example.ee' - ip = ['12345', '127.0.0.1'] - cleaned_data = 'example.ee' - - mock_record = Minitest::Mock.new - mock_record.expect(:unix_body, 'Whois record body') - mock_record.expect(:id, 1) - - WhoisRecord.stubs(:find_by).with(name: name).returns(mock_record) - - send(:handle_whois_record, name, ip, cleaned_data) - - assert_equal 1, @sent_data.length - assert_equal 'Whois record body', @sent_data.first - mock_record.verify - end + def test_receive_data_full_flow + scenarios = [ + {domain: 'example.ee', body: 'Whois body'}, + {domain: 'integration-test.ee', body: 'Integration test'} + ] - def test_handle_whois_record_not_found - name = 'nonexistent.ee' - ip = ['12345', '127.0.0.1'] - cleaned_data = 'nonexistent.ee' + scenarios.each do |scenario| + mock_record = mock_whois_record(body: scenario[:body]) + stub_connection_and_ip - WhoisRecord.stubs(:find_by).with(name: name).returns(nil) - stubs(:no_entries_msg).returns('Domain not found') + receive_data(scenario[:domain]) - send(:handle_whois_record, name, ip, cleaned_data) + assert_equal 1, @sent_data.length + assert @connection_closed_after_writing + mock_record.verify - assert_equal 1, @sent_data.length - assert_includes @sent_data.first, 'Domain not found' + @sent_data.clear + @connection_closed_after_writing = false + end end def send_data(data) diff --git a/test/logging_test.rb b/test/logging_test.rb index 3820f8c..ec5c055 100644 --- a/test/logging_test.rb +++ b/test/logging_test.rb @@ -81,228 +81,5 @@ def test_log_record_not_found assert_includes log_output, '"status":"not_found"' assert_includes log_output, '"record_found":false' - end - - def test_log_json_with_invalid_encoding - payload = { - domain: "test\xFF\xFE".force_encoding('BINARY'), - ip: '127.0.0.1' - } - - log_json(payload) - @output.rewind - log_output = @output.read - - assert log_output.length > 0 - end - - def test_log_json_with_json_generator_error - payload = { - domain: 'test' - } - - logger.stubs(:info).raises(JSON::GeneratorError.new('test')) - logger.expects(:error).at_least_once - - log_json(payload) - end - - def test_sanitize_for_json_with_array - array_with_invalid = ["test\xFF".force_encoding('BINARY'), 'valid'] - result = send(:sanitize_for_json, array_with_invalid) - - assert_equal Array, result.class - assert_equal 2, result.length - end - - def test_sanitize_for_json_with_hash - hash_with_invalid = { - key1: "test\xFF".force_encoding('BINARY'), - key2: 'valid' - } - result = send(:sanitize_for_json, hash_with_invalid) - - assert_equal Hash, result.class - assert_equal 2, result.length - end - - def test_sanitize_for_json_with_other_types - assert_equal 42, send(:sanitize_for_json, 42) - assert_nil send(:sanitize_for_json, nil) - assert_equal true, send(:sanitize_for_json, true) - assert_equal false, send(:sanitize_for_json, false) - end - - def test_build_base_log_includes_timestamp - payload = { domain: 'test.ee', ip: '127.0.0.1' } - result = send(:build_base_log, payload) - - assert result.key?(:timestamp) - assert result[:timestamp].is_a?(String) - assert_match /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/, result[:timestamp] - end - - def test_build_base_log_includes_service_info - payload = { domain: 'test.ee' } - result = send(:build_base_log, payload) - - assert result.key?(:service) - assert_equal 'whois', result[:service][:name] - assert_equal 'whois_server', result[:service][:type] - end - - def test_event_info_with_record_found - payload = { record_found: true, status: 'custom_status' } - result = send(:event_info, payload) - - assert_equal 'custom_status', result[:status] - end - - def test_event_info_without_record_found - payload = { record_found: false } - result = send(:event_info, payload) - - assert_equal 'not_found', result[:status] - end - - def test_event_info_defaults_to_not_found - payload = {} - result = send(:event_info, payload) - - assert_equal 'not_found', result[:status] - end - - def test_source_info_returns_host_and_ip - result = send(:source_info) - - assert result.key?(:host) - assert result.key?(:ip) - assert result[:host].is_a?(String) - assert [NilClass, String].include?(result[:ip].class) if result[:ip] - end - - def test_current_user_with_env - ENV['WHOIS_USER'] = 'test_user' - result = send(:current_user) - - assert_equal 'test_user', result - ENV.delete('WHOIS_USER') - end - - def test_current_user_without_env - original_whois_user = ENV.delete('WHOIS_USER') - - result = send(:current_user) - assert result.is_a?(String) - assert result.length > 0 - - ENV['WHOIS_USER'] = original_whois_user if original_whois_user - end - - def test_logger_info_with_env_version - ENV['APP_VERSION'] = '2.0.0' - result = send(:logger_info) - - assert_equal 'whois', result[:name] - assert_equal '2.0.0', result[:version] - ENV.delete('APP_VERSION') - end - - def test_logger_info_without_env_version - original_version = ENV.delete('APP_VERSION') - - result = send(:logger_info) - assert_equal 'whois', result[:name] - assert_equal '1.0', result[:version] - - ENV['APP_VERSION'] = original_version if original_version - end - - def test_client_info_extracts_ip_and_port - payload = { ip: '192.168.1.1', session_id: '9999' } - result = send(:client_info, payload) - - assert_equal '192.168.1.1', result[:ip] - assert_equal '9999', result[:port] - end - - def test_data_info_extracts_all_fields - payload = { - domain: 'test.ee', - searched_by: 'test.ee', - record_found: true, - record_id: 42 - } - result = send(:data_info, payload) - - assert_equal 'test.ee', result[:query] - assert_equal 'test.ee', result[:normalized_query] - assert_equal true, result[:record_found] - assert_equal 42, result[:record_id] - end - - def test_data_info_with_nil_values - payload = {} - result = send(:data_info, payload) - - assert_nil result[:query] - assert_nil result[:normalized_query] - assert_nil result[:record_found] - assert_nil result[:record_id] - end - - def test_metadata_info - result = send(:metadata_info) - - assert_equal 'whois', result[:protocol] - assert_equal '1.0', result[:version] - end - - def test_build_base_log_with_compact_removes_nil_error - payload = { domain: 'test.ee', ip: '127.0.0.1' } - result = send(:build_base_log, payload) - - refute result.key?(:error) unless payload[:message] - end - - def test_build_base_log_includes_all_sections - payload = { - domain: 'test.ee', - ip: '127.0.0.1', - session_id: '12345', - message: 'test message' - } - result = send(:build_base_log, payload) - - assert result.key?(:timestamp) - assert result.key?(:level) - assert result.key?(:logger) - assert result.key?(:service) - assert result.key?(:event) - assert result.key?(:source) - assert result.key?(:client) - assert result.key?(:user) - assert result.key?(:data) - assert result.key?(:metadata) - assert_equal 'test message', result[:error] - end - - def test_log_json_with_encoding_error - payload = { domain: 'test' } - - stubs(:sanitize_for_json).raises(Encoding::UndefinedConversionError.new('test')) - logger.expects(:error).at_least_once - - log_json(payload) - end - - def test_service_info_with_env - ENV['WHOIS_ENV'] = 'production' - result = send(:service_info) - - assert_equal 'whois', result[:name] - assert_equal 'whois_server', result[:type] - assert_equal 'production', result[:env] - ENV['WHOIS_ENV'] = 'test' - end + end end From 4d3af6dc5e6217ba42b08c03144684658dd98abd Mon Sep 17 00:00:00 2001 From: mmeest Date: Wed, 12 Nov 2025 12:34:42 +0200 Subject: [PATCH 4/6] removed hardcoded domain --- test/integration/whois_server_integration_test.rb | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test/integration/whois_server_integration_test.rb b/test/integration/whois_server_integration_test.rb index 24087ff..9c53268 100644 --- a/test/integration/whois_server_integration_test.rb +++ b/test/integration/whois_server_integration_test.rb @@ -5,6 +5,9 @@ require_relative '../../app/models/whois_record' require_relative '../../app/validators/unicode_validator' +TEST_DOMAIN = 'test-domain.ee' +INTEGRATION_TEST_DOMAIN = 'integration-test.ee' + class WhoisServerIntegrationTest < Minitest::Test include WhoisServer @@ -44,7 +47,7 @@ def test_receive_data_returns_valid_whois_body stub_connection_and_ip mock_record = mock_whois_record(body: 'This is a valid WHOIS record') - receive_data('example.ee') + receive_data(TEST_DOMAIN) assert_equal 1, @sent_data.length assert_includes @sent_data.first, 'This is a valid WHOIS record' @@ -54,7 +57,7 @@ def test_receive_data_returns_valid_whois_body end def test_receive_data_accepts_valid_data - receive_data('example.ee') + receive_data(TEST_DOMAIN) assert_equal 1, @sent_data.length assert_includes @sent_data.first.downcase, 'domain not found' @@ -65,14 +68,14 @@ def test_receive_data_with_no_ip stubs(:extract_ip).returns(nil) stubs(:connection).returns(nil) - receive_data('example.ee') + receive_data(TEST_DOMAIN) assert_equal 0, @sent_data.length end def test_receive_data_full_flow scenarios = [ - {domain: 'example.ee', body: 'Whois body'}, + {domain: TEST_DOMAIN, body: 'Whois body'}, {domain: 'integration-test.ee', body: 'Integration test'} ] From c8ac1293bddd0b1c4fb09845b822cbca8d2ec481 Mon Sep 17 00:00:00 2001 From: mmeest Date: Wed, 12 Nov 2025 12:44:47 +0200 Subject: [PATCH 5/6] update tests --- test/integration/whois_server_integration_test.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/integration/whois_server_integration_test.rb b/test/integration/whois_server_integration_test.rb index 9c53268..b8a9345 100644 --- a/test/integration/whois_server_integration_test.rb +++ b/test/integration/whois_server_integration_test.rb @@ -56,7 +56,10 @@ def test_receive_data_returns_valid_whois_body mock_record.verify end - def test_receive_data_accepts_valid_data + def test_receive_data_domain_not_found + stub_connection_and_ip + WhoisRecord.stubs(:find_by).returns(nil) + receive_data(TEST_DOMAIN) assert_equal 1, @sent_data.length @@ -114,4 +117,3 @@ def logger @logger end end - From 32490980ac7b2f08845992f5f977676814e09884 Mon Sep 17 00:00:00 2001 From: mmeest Date: Wed, 12 Nov 2025 12:49:07 +0200 Subject: [PATCH 6/6] removed duplicate --- .../whois_server_integration_test.rb | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/test/integration/whois_server_integration_test.rb b/test/integration/whois_server_integration_test.rb index b8a9345..e31ba33 100644 --- a/test/integration/whois_server_integration_test.rb +++ b/test/integration/whois_server_integration_test.rb @@ -76,27 +76,6 @@ def test_receive_data_with_no_ip assert_equal 0, @sent_data.length end - def test_receive_data_full_flow - scenarios = [ - {domain: TEST_DOMAIN, body: 'Whois body'}, - {domain: 'integration-test.ee', body: 'Integration test'} - ] - - scenarios.each do |scenario| - mock_record = mock_whois_record(body: scenario[:body]) - stub_connection_and_ip - - receive_data(scenario[:domain]) - - assert_equal 1, @sent_data.length - assert @connection_closed_after_writing - mock_record.verify - - @sent_data.clear - @connection_closed_after_writing = false - end - end - def send_data(data) @sent_data << data end