From 11658137c860c325cc1a0eaeb65ca057e06ed8de Mon Sep 17 00:00:00 2001 From: CodeBeaverAI Date: Thu, 20 Feb 2025 20:58:59 +0100 Subject: [PATCH 1/5] test: Add coverage improvement test for tests/test_notify.py --- tests/test_notify.py | 284 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 tests/test_notify.py diff --git a/tests/test_notify.py b/tests/test_notify.py new file mode 100644 index 000000000..6f3912e5d --- /dev/null +++ b/tests/test_notify.py @@ -0,0 +1,284 @@ +import pytest +import re +import sherlock_project.notify as notify_mod +import webbrowser +from sherlock_project.notify import ( + QueryNotify, + QueryNotifyPrint, + globvar, +) +from sherlock_project.result import ( + QueryStatus, +) + + +class DummyQueryResult: + """Dummy query result class used for testing. + This object simulates the QueryResult() expected by the notify module.""" + + def __init__( + self, + status, + site_name="dummy_site", + query_time=None, + site_url_user="http://dummy.com", + context="dummy context", + ): + self.status = status + self.site_name = site_name + self.query_time = query_time + self.site_url_user = site_url_user + self.context = context + + def __str__(self): + return f"DummyQueryResult({self.status}, {self.site_name})" + + +def test_update_with_invalid_status_raises_value_error(): + """ + Test that the update method in QueryNotifyPrint raises ValueError when the DummyQueryResult + object contains an invalid query status. + """ + invalid_status = "NON_EXISTENT_STATUS" + dummy_result = DummyQueryResult(status=invalid_status) + notifier = QueryNotifyPrint(verbose=True, print_all=True, browse=False) + with pytest.raises(ValueError) as exc_info: + notifier.update(dummy_result) + assert "Unknown Query Status" in str(exc_info.value) + + +def strip_ansi(text): + """ + Strip ANSI escape sequences from the text. + """ + ansi_escape = re.compile("\\x1B(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])") + return ansi_escape.sub("", text) + + +def test_update_claimed_calls_webbrowser_and_prints(monkeypatch, capsys): + """ + Test that the update method for a CLAIMED query result with browse enabled: + - Calls webbrowser.open with the correct URL and parameter. + - Prints expected output, ignoring ANSI escape sequences. + """ + webbrowser_calls = [] + + def fake_open(url, new): + webbrowser_calls.append((url, new)) + + monkeypatch.setattr(webbrowser, "open", fake_open) + dummy_result = DummyQueryResult( + status=QueryStatus.CLAIMED, + site_name="dummy_site", + query_time=0.005, + site_url_user="http://example.com", + ) + notifier = QueryNotifyPrint(verbose=True, print_all=True, browse=True) + notifier.update(dummy_result) + captured = capsys.readouterr().out + plain_output = strip_ansi(captured) + assert "[+" in plain_output, "Expected marker '[+' in output" + assert "dummy_site:" in plain_output, "Expected site name in output" + assert "5ms" in plain_output, "Expected formatted query time in output" + assert len(webbrowser_calls) == 1, "Expected one call to webbrowser.open" + assert webbrowser_calls[0] == ( + "http://example.com", + 2, + ), "webbrowser.open called with wrong arguments" + + +def test_finish_prints_correct_output(capsys): + """ + Test that the finish method prints the correct output message with "Search completed" + and indicates 0 results after resetting the global counter. + This test strips ANSI escape sequences from the output before checking. + """ + notify_mod.globvar = 0 + notifier = QueryNotifyPrint(verbose=False, print_all=True, browse=False) + notifier.finish() + captured = capsys.readouterr().out + captured_plain = strip_ansi(captured) + assert ( + "Search completed" in captured_plain + ), "Finish message should contain 'Search completed'" + assert "0 results" in captured_plain, "Finish message should indicate 0 results" + + +def test_update_unknown_prints_output(capsys): + """ + Test that the update method for an UNKNOWN query result prints the expected output. + It verifies that the output contains the site name and the context message. + """ + dummy_result = DummyQueryResult( + status=QueryStatus.UNKNOWN, + site_name="dummy_unknown", + query_time=0.01, + site_url_user="http://dummy.com", + context="Test context unknown", + ) + notifier = QueryNotifyPrint(verbose=False, print_all=True, browse=False) + notifier.update(dummy_result) + captured = capsys.readouterr().out + plain_output = re.sub("\\x1B(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])", "", captured) + assert "dummy_unknown:" in plain_output, "Site name not printed as expected." + assert "Test context unknown" in plain_output, "Context not printed as expected." + + +def test_update_available_prints_output(capsys): + """ + Test that the update method for an AVAILABLE query result prints the expected output. + It verifies that the output contains the site name, the 'Not Found!' message and the + correctly formatted response time when verbose output is enabled. + """ + dummy_result = DummyQueryResult( + status=QueryStatus.AVAILABLE, + site_name="dummy_available", + query_time=0.002, + site_url_user="http://dummyavailable.com", + context="irrelevant", + ) + notifier = QueryNotifyPrint(verbose=True, print_all=True, browse=False) + notifier.update(dummy_result) + captured = capsys.readouterr().out + plain_output = re.sub("\\x1B(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])", "", captured) + assert ( + "dummy_available:" in plain_output + ), "Site name should be printed for AVAILABLE result" + assert ( + "Not Found!" in plain_output + ), "Expected output to indicate 'Not Found!' for AVAILABLE result" + assert "2ms" in plain_output, "Expected response time to be printed correctly" + + +def test_update_illegal_prints_output(capsys): + """ + Test that the update method for an ILLEGAL query result prints the expected output message. + It verifies that the printed output contains the site name and the illegal username format message. + """ + dummy_result = DummyQueryResult( + status=QueryStatus.ILLEGAL, + site_name="dummy_illegal", + query_time=0.0, + site_url_user="http://dummyillegal.com", + ) + notifier = QueryNotifyPrint(verbose=False, print_all=True, browse=False) + notifier.update(dummy_result) + captured = capsys.readouterr().out + plain_output = re.sub("\\x1B(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])", "", captured) + assert ( + "dummy_illegal:" in plain_output + ), "Site name not printed as expected for ILLEGAL status." + assert ( + "Illegal Username Format For This Site!" in plain_output + ), "Expected illegal username format message in output." + + +def test_update_waf_prints_output(capsys): + """ + Test that the update method for a WAF query result prints the expected output. + It verifies that when the dummy query result's status is set to WAF, + the output indicates that the request was blocked by bot detection and suggests that a proxy may help. + """ + dummy_result = DummyQueryResult( + status=QueryStatus.WAF, + site_name="dummy_waf", + query_time=0.003, + site_url_user="http://dummywaf.com", + context="irrelevant", + ) + notifier = QueryNotifyPrint(verbose=False, print_all=True, browse=False) + notifier.update(dummy_result) + captured = capsys.readouterr().out + plain_output = re.sub("\\x1B(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])", "", captured) + assert ( + "dummy_waf:" in plain_output + ), "Site name not printed as expected for WAF status." + assert ( + "Blocked by bot detection" in plain_output + ), "Expected WAF block message in output." + assert ( + "proxy may help" in plain_output + ), "Expected suggestion that proxy may help in output." + + +def test_start_prints_correct_output(capsys): + """ + Test that the start() method of QueryNotifyPrint prints the correct start message. + It verifies the output contains the expected title, the given username and ends with 'on:'. + """ + notifier = QueryNotifyPrint(verbose=False, print_all=True, browse=False) + test_username = "testuser" + notifier.start(test_username) + output = capsys.readouterr().out + ansi_escape = re.compile("\\x1B(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])") + plain_output = ansi_escape.sub("", output) + assert ( + "Checking username" in plain_output + ), "The start message should contain 'Checking username'" + assert ( + test_username in plain_output + ), "The username should be included in the start message" + assert "on:" in plain_output, "The start message should end with 'on:'" + + +def test_update_available_no_prints_nothing(capsys): + """ + Test that the update method for an AVAILABLE query result prints no output + when print_all is set to False. + """ + dummy_result = DummyQueryResult( + status=QueryStatus.AVAILABLE, + site_name="dummy_no_print", + query_time=0.001, + site_url_user="http://dummy.no.print", + context="irrelevant", + ) + notifier = QueryNotifyPrint(verbose=True, print_all=False, browse=False) + notifier.update(dummy_result) + captured_output = capsys.readouterr().out + assert ( + captured_output == "" + ), "No output should be printed when print_all is False for AVAILABLE status." + + +def test_finish_with_multiple_claimed_results(capsys): + """ + Test that finish() correctly reports the number of CLAIMED results when multiple updates + have been performed. The global counter (globvar) is reset before the test. Two update() + calls with a CLAIMED status increment the counter, and finish() calls countResults() once + more. The expected output is that finish() reports 2 results. + """ + notify_mod.globvar = 0 + dummy_result = DummyQueryResult( + status=QueryStatus.CLAIMED, + site_name="dummy_multiple", + query_time=0.004, + site_url_user="http://dummymultiple.com", + ) + notifier = QueryNotifyPrint(verbose=True, print_all=True, browse=False) + notifier.update(dummy_result) + notifier.update(dummy_result) + notifier.finish() + captured = capsys.readouterr().out + plain_output = re.sub("\\x1B(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])", "", captured) + assert "2 results" in plain_output, "Finish output should indicate 2 results." + + +def test_str_method_returns_query_result(): + """ + Test that the __str__ method of QueryNotifyPrint returns the string representation of the query result. + The test sets a dummy query result on the notifier and checks if str(notifier) equals str(dummy_result). + """ + dummy_result = DummyQueryResult( + status=QueryStatus.CLAIMED, + site_name="test_site", + query_time=0.005, + site_url_user="http://test.com", + ) + notifier = QueryNotifyPrint(verbose=True, print_all=True, browse=False) + notifier.update(dummy_result) + result_str = str(notifier) + expected_str = str(dummy_result) + assert ( + result_str == expected_str + ), f"Expected __str__ to return '{expected_str}' but got '{result_str}'" From cd94d3aad24efc3746df3ba1288ec536d06a61c3 Mon Sep 17 00:00:00 2001 From: CodeBeaverAI Date: Thu, 20 Feb 2025 20:59:01 +0100 Subject: [PATCH 2/5] test: Add coverage improvement test for tests/test_result.py --- tests/test_result.py | 72 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 tests/test_result.py diff --git a/tests/test_result.py b/tests/test_result.py new file mode 100644 index 000000000..fd7fe9a15 --- /dev/null +++ b/tests/test_result.py @@ -0,0 +1,72 @@ +import pytest +from sherlock_project.result import ( + QueryResult, + QueryStatus, +) + + +def test_queryresult_with_context(): + """ + Test the QueryResult __str__ method when additional context is provided. + The test creates a QueryResult with a specific context and verifies + that the returned string includes the context in the expected format. + """ + username = "testuser" + site_name = "example_site" + site_url_user = "http://example.com/testuser" + status = QueryStatus.CLAIMED + query_time = 0.456 + context = "Timeout occurred" + result_obj = QueryResult( + username, site_name, site_url_user, status, query_time, context + ) + expected_str = "Claimed (Timeout occurred)" + assert str(result_obj) == expected_str + + +def test_queryresult_without_context(): + """ + Test the QueryResult __str__ method when no additional context is provided. + This verifies that the returned string is just the status string without + any appended context information. + """ + username = "anotheruser" + site_name = "another_site" + site_url_user = "http://anothersite.com/anotheruser" + status = QueryStatus.AVAILABLE + result_obj = QueryResult(username, site_name, site_url_user, status) + expected_str = "Available" + assert str(result_obj) == expected_str + + +def test_queryresult_with_empty_context(): + """ + Test the QueryResult __str__ method when an empty string is provided as context. + This verifies that even an empty context string is appended in parentheses. + """ + username = "emptycontextuser" + site_name = "empty_site" + site_url_user = "http://emptysite.com/emptycontextuser" + status = QueryStatus.CLAIMED + context = "" + result_obj = QueryResult( + username, site_name, site_url_user, status, context=context + ) + expected_str = "Claimed ()" + assert str(result_obj) == expected_str + + +def test_querystatus_str(): + """ + Test the __str__ method of QueryStatus to ensure it returns the correct + string representation for each enumeration member. + """ + statuses = { + QueryStatus.CLAIMED: "Claimed", + QueryStatus.AVAILABLE: "Available", + QueryStatus.UNKNOWN: "Unknown", + QueryStatus.ILLEGAL: "Illegal", + QueryStatus.WAF: "WAF", + } + for status, expected_str in statuses.items(): + assert str(status) == expected_str From 8f18cf61d9e98ece9c78007ec9ef00e2df96ffc1 Mon Sep 17 00:00:00 2001 From: CodeBeaverAI Date: Thu, 20 Feb 2025 20:59:03 +0100 Subject: [PATCH 3/5] test: Add coverage improvement test for tests/test_sherlock.py --- tests/test_sherlock.py | 341 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 341 insertions(+) create mode 100644 tests/test_sherlock.py diff --git a/tests/test_sherlock.py b/tests/test_sherlock.py new file mode 100644 index 000000000..e13dec1dc --- /dev/null +++ b/tests/test_sherlock.py @@ -0,0 +1,341 @@ +import csv +import os +import pandas as pd +import pytest +import re +import requests +import signal +import sys +from argparse import ( + ArgumentTypeError, +) +from sherlock_project.result import ( + QueryResult, + QueryStatus, +) +from sherlock_project.sherlock import ( + SherlockFuturesSession, + check_for_parameter, + get_response, + interpolate_string, + multiple_usernames, + sherlock, + timeout_check, +) +from time import ( + monotonic, +) + +try: + from sherlock_project.sherlock import timeout_check +except ImportError: + pytest.skip( + "Unable to import timeout_check from sherlock_project.sherlock", + allow_module_level=True, + ) + + +def test_timeout_check_validation(): + """ + Test the timeout_check function with valid and invalid inputs. + + This test verifies that: + - A valid timeout string is converted correctly to a float. + - A timeout of 0 or negative value raises an ArgumentTypeError. + """ + valid_timeout = "30" + assert timeout_check(valid_timeout) == 30.0 + with pytest.raises(ArgumentTypeError): + timeout_check("0") + with pytest.raises(ArgumentTypeError): + timeout_check("-10") + + +def test_interpolate_string_correct_substitution(): + """ + Test the interpolate_string function for various data types. + + This test verifies that: + - When given a string with '{}' placeholder, it correctly replaces it. + - When given a dictionary containing strings with '{}' placeholders (including nested dictionaries), + it correctly performs the substitution. + - When given a list of strings with '{}' placeholders, it correctly performs the substitution. + - When given a value that is not a string, list, or dict, the value is returned unchanged. + """ + input_str = "User: {}" + expected_str = "User: testuser" + assert interpolate_string(input_str, "testuser") == expected_str + input_dict = {"greeting": "Hi, {}!", "nested": {"farewell": "Bye, {}!"}} + expected_dict = { + "greeting": "Hi, testuser!", + "nested": {"farewell": "Bye, testuser!"}, + } + assert interpolate_string(input_dict, "testuser") == expected_dict + input_list = ["Welcome, {}!", "{} just logged in."] + expected_list = ["Welcome, testuser!", "testuser just logged in."] + assert interpolate_string(input_list, "testuser") == expected_list + input_int = 42 + assert interpolate_string(input_int, "testuser") == 42 + input_none = None + assert interpolate_string(input_none, "testuser") is None + + +def test_get_response_connection_error(): + """ + Test the get_response function to handle a ConnectionError. + This test creates a fake Future object that raises a ConnectionError when its result() method is called, + and then asserts that get_response returns None for the response, sets the error_context to "Error Connecting", + and includes the exception message in the exception_text. + """ + + class FakeFuture: + + def result(self): + raise requests.exceptions.ConnectionError("Test connection error") + + fake_future = FakeFuture() + response, error_context, exception_text = get_response( + fake_future, error_type="dummy", social_network="dummy" + ) + assert response is None + assert error_context == "Error Connecting" + assert "Test connection error" in exception_text + + +class FakeResponse: + """Fake response object for testing a successful response scenario.""" + + def __init__(self, status_code=200, text="", encoding="utf-8", elapsed=0.1): + self.status_code = status_code + self.text = text + self.encoding = encoding + self.elapsed = elapsed + + +class FakeFutureSuccess: + """Fake future that returns a successful FakeResponse.""" + + def result(self): + return FakeResponse() + + +def test_get_response_successful(): + """ + Test the get_response function for a successful response scenario. + + This test creates a FakeFutureSuccess object that returns a FakeResponse + with a valid HTTP status code. It asserts that get_response returns the fake + response with error_context set to None and no exception text. + """ + fake_future = FakeFutureSuccess() + response, error_context, exception_text = get_response( + request_future=fake_future, error_type="dummy", social_network="dummy" + ) + assert response is not None + assert response.status_code == 200 + assert isinstance(response.elapsed, float) + assert error_context is None + assert exception_text is None + + +def test_multiple_usernames_replacement(): + """ + Test that the multiple_usernames function correctly replaces the "{?}" placeholder + with all supported symbols ("_", "-", and "."). + """ + input_username = "test{?}" + expected = ["test_", "test-", "test."] + result = multiple_usernames(input_username) + assert result == expected + + +def test_check_for_parameter(): + """ + Test the check_for_parameter function to ensure that it correctly identifies + whether a username contains the placeholder "{?}". + + If "{?}" is present in the username, the function should return True, + otherwise False. + """ + username_with_placeholder = "test{?}_user" + assert check_for_parameter(username_with_placeholder) is True + username_without_placeholder = "test_user" + assert check_for_parameter(username_without_placeholder) is False + + +def test_get_response_timeout_error(): + """ + Test the get_response function to handle a Timeout error. + This test creates a FakeFutureTimeout object that raises a Timeout exception + when its result() method is called, and then asserts that get_response returns + None for the response, sets the error_context to "Timeout Error", and includes + the exception message in the exception_text. + """ + + class FakeFutureTimeout: + + def result(self): + raise requests.exceptions.Timeout("Test timeout exceeded") + + fake_future = FakeFutureTimeout() + response, error_context, exception_text = get_response( + request_future=fake_future, error_type="dummy", social_network="dummy" + ) + assert response is None + assert error_context == "Timeout Error" + assert "Test timeout exceeded" in exception_text + + +def test_get_response_http_error(): + """ + Test get_response function to handle an HTTPError. + + This test creates a fake Future object that raises an HTTPError when its result() method is called. + It asserts that get_response returns None for the response, sets the error_context to "HTTP Error", + and that the exception_text contains the raised error message. + """ + + class FakeFutureHTTPError: + + def result(self): + raise requests.exceptions.HTTPError("Test HTTP error occurred") + + fake_future = FakeFutureHTTPError() + response, error_context, exception_text = get_response( + request_future=fake_future, error_type="dummy", social_network="dummy" + ) + assert response is None + assert error_context == "HTTP Error" + assert "Test HTTP error occurred" in exception_text + + +class DummyQueryNotify: + + def __init__(self): + self.updates = [] + + def start(self, username): + pass + + def update(self, result): + self.updates.append(result) + + def finish(self): + return 0 + + +def test_sherlock_illegal_username(): + """ + Test that the sherlock function correctly marks a username as illegal when + the provided username does not match the site's regexCheck. + """ + dummy_site_data = { + "dummy": { + "url": "http://example.com/{}", + "urlMain": "http://example.com", + "regexCheck": "^[a-z]+$", + "errorType": "status_code", + "errorMsg": "Not Found", + "errorCode": [404], + } + } + illegal_username = "TestUser" + dummy_notify = DummyQueryNotify() + results = sherlock( + username=illegal_username, + site_data=dummy_site_data, + query_notify=dummy_notify, + tor=False, + unique_tor=False, + dump_response=False, + proxy=None, + timeout=60, + ) + site_result = results["dummy"] + assert site_result["status"].status == QueryStatus.ILLEGAL + assert site_result["url_user"] == "" + assert site_result["http_status"] == "" + + +class FakeFutureClaimed: + + def result(self): + return FakeResponse(status_code=200, text="User profile exists", elapsed=0.2) + + +class FakeFutureAvailable: + + def result(self): + return FakeResponse( + status_code=200, + text="Error: Not Found - Profile does not exist", + elapsed=0.3, + ) + + +def fake_get(self, url, headers=None, allow_redirects=True, timeout=60, json=None): + """ + A fake GET method to replace SherlockFuturesSession.get. + It inspects a custom header "X-Fake-Response" to decide which fake future + to return. + """ + if headers is None: + headers = {} + fake_response_type = headers.get("X-Fake-Response") + if fake_response_type == "claimed": + return FakeFutureClaimed() + elif fake_response_type == "available": + return FakeFutureAvailable() + else: + raise ValueError("Unknown fake response type in headers.") + + +def test_sherlock_message_detection(monkeypatch): + """ + Test that the sherlock function correctly processes a site with errorType 'message' + by using a monkey-patched GET method. Two scenarios are tested: + - For the dummy site where the fake response text does NOT include the error message, + the account is assumed to exist (CLAIMED). + - For the dummy site where the fake response text DOES include the error message, + the account is considered available (AVAILABLE). + """ + dummy_site_data = { + "dummy_claimed": { + "url": "http://example.com/{}", + "urlMain": "http://example.com", + "regexCheck": "^[a-z]+$", + "errorType": "message", + "errorMsg": "Not Found", + "errorCode": [404], + "headers": {"X-Fake-Response": "claimed"}, + }, + "dummy_available": { + "url": "http://example.com/{}", + "urlMain": "http://example.com", + "regexCheck": "^[a-z]+$", + "errorType": "message", + "errorMsg": "Not Found", + "errorCode": [404], + "headers": {"X-Fake-Response": "available"}, + }, + } + username = "testuser" + dummy_notify = DummyQueryNotify() + monkeypatch.setattr(SherlockFuturesSession, "get", fake_get) + results = sherlock( + username=username, + site_data=dummy_site_data, + query_notify=dummy_notify, + tor=False, + unique_tor=False, + dump_response=False, + proxy=None, + timeout=60, + ) + expected_url = "http://example.com/testuser" + result_claimed = results["dummy_claimed"]["status"] + assert result_claimed.status == QueryStatus.CLAIMED + assert results["dummy_claimed"]["url_user"] == expected_url + result_available = results["dummy_available"]["status"] + assert result_available.status == QueryStatus.AVAILABLE + assert results["dummy_available"]["url_user"] == expected_url From a5880ed2c33585ce503d6c77d92dc9b9e10ac813 Mon Sep 17 00:00:00 2001 From: CodeBeaverAI Date: Thu, 20 Feb 2025 20:59:04 +0100 Subject: [PATCH 4/5] test: Add coverage improvement test for tests/test_sites.py --- tests/test_sites.py | 262 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 tests/test_sites.py diff --git a/tests/test_sites.py b/tests/test_sites.py new file mode 100644 index 000000000..4d9d58f9e --- /dev/null +++ b/tests/test_sites.py @@ -0,0 +1,262 @@ +import json +import pytest +import secrets +from pathlib import ( + Path, +) +from sherlock_project import ( + sites, +) + + +def test_invalid_file_extension(tmp_path): + """ + Test that SitesInformation raises FileNotFoundError when provided a file + with an invalid extension (i.e., not ending with '.json'). + """ + invalid_file = tmp_path / "data.txt" + file_content = { + "testsite": { + "urlMain": "http://example.com", + "url": "http://example.com/{}", + "username_claimed": "exists", + "isNSFW": False, + } + } + invalid_file.write_text(json.dumps(file_content)) + with pytest.raises(FileNotFoundError, match="Incorrect JSON file extension"): + sites.SitesInformation(str(invalid_file)) + + +def test_remove_nsfw_and_site_name_list(tmp_path): + """ + Test SitesInformation's remove_nsfw_sites method with and without + exceptions, and validate that site_name_list returns a sorted list. + """ + file_path = tmp_path / "data.json" + site_data = { + "safe_site": { + "urlMain": "http://safesite.com", + "url": "http://safesite.com/{}", + "username_claimed": "claimed", + "isNSFW": False, + }, + "unsafe_site": { + "urlMain": "http://unsafesite.com", + "url": "http://unsafesite.com/{}", + "username_claimed": "claimed", + "isNSFW": True, + }, + } + file_path.write_text(json.dumps(site_data)) + sites_info = sites.SitesInformation(str(file_path)) + assert len(sites_info) == 2 + sites_info.remove_nsfw_sites() + assert len(sites_info) == 1 + remaining_site_names = [site.name for site in sites_info] + assert "safe_site" in remaining_site_names + assert "unsafe_site" not in remaining_site_names + sites_info_exception = sites.SitesInformation(str(file_path)) + sites_info_exception.remove_nsfw_sites(do_not_remove=["unsafe_site"]) + assert len(sites_info_exception) == 2 + all_site_names = [site.name for site in sites_info_exception] + assert "safe_site" in all_site_names + assert "unsafe_site" in all_site_names + name_list = sites_info_exception.site_name_list() + assert name_list == sorted(name_list, key=str.lower) + + +def test_remote_url_bad_status(monkeypatch): + """ + Test that when SitesInformation loads data from a remote URL that returns a non-200 + status code, it raises a FileNotFoundError indicating a bad response. + """ + + class FakeResponse: + + def __init__(self, status_code): + self.status_code = status_code + + def json(self): + return {} + + def fake_get(*args, **kwargs): + return FakeResponse(404) + + monkeypatch.setattr(sites.requests, "get", fake_get) + with pytest.raises( + FileNotFoundError, match="Bad response while accessing data file URL" + ): + sites.SitesInformation("http://example.com/data.json") + + +def test_invalid_site_structure(tmp_path, capsys): + """ + Test that SitesInformation skips a site with an invalid structure (non-dictionary) + and prints an error message when encountering a TypeError. + """ + file_path = tmp_path / "data.json" + site_data = { + "invalid_site": "not_a_dict", + "valid_site": { + "urlMain": "http://valid.com", + "url": "http://valid.com/{}", + "username_claimed": "claimed", + "isNSFW": False, + }, + } + file_path.write_text(json.dumps(site_data)) + sites_info = sites.SitesInformation(str(file_path)) + site_names = [site.name for site in sites_info] + assert "valid_site" in site_names + assert "invalid_site" not in site_names + captured = capsys.readouterr().out + assert "Encountered TypeError parsing json contents" in captured + + +def test_remote_valid_data(monkeypatch): + """ + Test that SitesInformation correctly loads valid remote JSON data and that + the SiteInformation __str__ method returns the expected string. + This simulates a valid remote URL by monkeypatching requests.get. + """ + + class FakeResponse: + + def __init__(self, json_data, status_code=200): + self._json_data = json_data + self.status_code = status_code + + def json(self): + return self._json_data + + def fake_get(*args, **kwargs): + fake_data = { + "example_site": { + "urlMain": "http://example.com", + "url": "http://example.com/{}", + "username_claimed": "claimed", + "isNSFW": False, + } + } + return FakeResponse(fake_data, status_code=200) + + monkeypatch.setattr(sites.requests, "get", fake_get) + sites_info = sites.SitesInformation("http://fakeurl.com/data.json") + assert len(sites_info) == 1 + site = next(iter(sites_info)) + expected_str = "example_site (http://example.com)" + assert str(site) == expected_str + + +def test_missing_required_attribute(tmp_path): + """ + Test that SitesInformation raises ValueError when a required attribute + (e.g., "urlMain") is missing from a site's data in the JSON file. + This ensures that the code properly detects incomplete site data. + """ + file_path = tmp_path / "data.json" + site_data = { + "incomplete_site": { + "url": "http://incomplete.com/{}", + "username_claimed": "claimed", + "isNSFW": False, + } + } + file_path.write_text(json.dumps(site_data)) + with pytest.raises(ValueError, match="Missing attribute"): + sites.SitesInformation(str(file_path)) + + +def test_local_file_not_found(tmp_path): + """ + Test that SitesInformation raises a FileNotFoundError when provided a path to a non-existent local JSON file. + """ + non_existent_file = tmp_path / "non_existent.json" + with pytest.raises( + FileNotFoundError, match="Problem while attempting to access data file" + ): + sites.SitesInformation(str(non_existent_file)) + + +def test_invalid_json_parsing_local(tmp_path): + """ + Test that SitesInformation raises ValueError when the local JSON file contains invalid JSON. + This ensures that the JSON parse errors are correctly handled. + """ + file_path = tmp_path / "invalid.json" + file_path.write_text("This is not valid JSON") + with pytest.raises(ValueError, match="Problem parsing json contents"): + sites.SitesInformation(str(file_path)) + + +def test_schema_key_removed(tmp_path): + """ + Test that SitesInformation correctly removes the "$schema" key from the JSON file, + ensuring that it does not attempt to create a site from that entry. + """ + file_path = tmp_path / "data.json" + site_data = { + "$schema": "https://example.com/schema.json", + "test_site": { + "urlMain": "http://testsite.com", + "url": "http://testsite.com/{}", + "username_claimed": "claimed", + "isNSFW": False, + }, + } + file_path.write_text(json.dumps(site_data)) + sites_info = sites.SitesInformation(str(file_path)) + assert len(sites_info) == 1 + site_names = [site.name for site in sites_info] + assert "test_site" in site_names + + +def test_site_information_username_unclaimed_override(): + """ + Test that SiteInformation always generates a new secret token for username_unclaimed, + even if a value is provided through the constructor. + """ + provided_token = "provided_value" + site_dict = { + "urlMain": "http://override.com", + "url": "http://override.com/{}", + "username_claimed": "claimed", + "isNSFW": False, + } + site_info = sites.SiteInformation( + name="override_test", + url_home=site_dict["urlMain"], + url_username_format=site_dict["url"], + username_claimed=site_dict["username_claimed"], + information=site_dict, + is_nsfw=site_dict["isNSFW"], + username_unclaimed=provided_token, + ) + assert ( + site_info.username_unclaimed != provided_token + ), "username_unclaimed should be overridden by a new secret token." + assert isinstance(site_info.username_unclaimed, str) + assert len(site_info.username_unclaimed) > len(provided_token) + + +def test_remote_invalid_json(monkeypatch): + """ + Test that SitesInformation raises a ValueError when a remote URL returns invalid JSON. + This simulates the scenario where response.json() raises an exception. + """ + + class FakeResponse: + + def __init__(self, status_code=200): + self.status_code = status_code + + def json(self): + raise ValueError("Invalid JSON") + + def fake_get(*args, **kwargs): + return FakeResponse() + + monkeypatch.setattr(sites.requests, "get", fake_get) + with pytest.raises(ValueError, match="Problem parsing json contents at"): + sites.SitesInformation("http://fakeurl.com/data_invalid.json") From a4dc2d8c1550e7bcd7ea834d40574caf295aba37 Mon Sep 17 00:00:00 2001 From: CodeBeaverAI Date: Thu, 20 Feb 2025 20:59:06 +0100 Subject: [PATCH 5/5] --- codebeaver.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 codebeaver.yml diff --git a/codebeaver.yml b/codebeaver.yml new file mode 100644 index 000000000..68c1df67f --- /dev/null +++ b/codebeaver.yml @@ -0,0 +1,2 @@ +from:python-pytest-poetry +# This file was generated automatically by CodeBeaver based on your repository. Learn how to customize it here: https://docs.codebeaver.ai/configuration \ No newline at end of file