From ace699b872385719384f034d63d973a463eb3ad4 Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Tue, 21 Jul 2026 17:51:20 -0400 Subject: [PATCH] fix(hashview): accept native-JSON customer arrays from /v1/customers The server now returns `users` as a native JSON array (issue #229, no double-decode), but list_customers unconditionally ran json.loads() on it, raising TypeError and breaking the entire customer->hashfile enumeration flow. Accept both the native array and the legacy double-encoded string, mirroring list_wordlists. Co-Authored-By: Claude Opus 4.8 --- hate_crack/api.py | 6 +++++- tests/test_hashview.py | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/hate_crack/api.py b/hate_crack/api.py index 7d7e027..eecffc0 100644 --- a/hate_crack/api.py +++ b/hate_crack/api.py @@ -1169,7 +1169,11 @@ class HashviewAPI: resp.raise_for_status() data = resp.json() if "users" in data: - customers = json.loads(data["users"]) + customers = data["users"] + # Newer servers return a native JSON array (issue #229); older ones + # double-encode it as a JSON string. Support both. + if isinstance(customers, str): + customers = json.loads(customers) return {"customers": customers} return data diff --git a/tests/test_hashview.py b/tests/test_hashview.py index cd009c8..86e2a63 100644 --- a/tests/test_hashview.py +++ b/tests/test_hashview.py @@ -176,6 +176,26 @@ class TestHashviewAPI: captured = capsys.readouterr() assert "No customers found" in captured.out + def test_list_customers_native_json_array(self, api): + """Server returns `users` as a native JSON array (issue #229, no double-decode).""" + mock_resp = Mock() + mock_resp.json.return_value = {"users": [{"id": 1, "name": "Acme"}]} + mock_resp.raise_for_status = Mock() + api.session.get.return_value = mock_resp + + result = api.list_customers() + assert result["customers"] == [{"id": 1, "name": "Acme"}] + + def test_list_customers_legacy_json_string(self, api): + """Older servers double-encode `users` as a JSON string; still supported.""" + mock_resp = Mock() + mock_resp.json.return_value = {"users": json.dumps([{"id": 2, "name": "Beta"}])} + mock_resp.raise_for_status = Mock() + api.session.get.return_value = mock_resp + + result = api.list_customers() + assert result["customers"] == [{"id": 2, "name": "Beta"}] + def test_upload_cracked_hashes_success(self, api, tmp_path): """Test uploading cracked hashes with valid lines (real API if possible).""" hashview_url, hashview_api_key = self._get_hashview_config()