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()