diff --git a/.coverage b/.coverage
index c004478..401fa9c 100644
Binary files a/.coverage and b/.coverage differ
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 0000000..e98db99
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,310 @@
+"""Test module for json2xml.utils functionality."""
+import json
+import tempfile
+from typing import TYPE_CHECKING
+from unittest.mock import Mock, patch
+
+import pytest
+
+from json2xml.utils import (
+ InvalidDataError,
+ JSONReadError,
+ StringReadError,
+ URLReadError,
+ readfromjson,
+ readfromstring,
+ readfromurl,
+)
+
+if TYPE_CHECKING:
+ from _pytest.capture import CaptureFixture
+ from _pytest.fixtures import FixtureRequest
+ from _pytest.logging import LogCaptureFixture
+ from _pytest.monkeypatch import MonkeyPatch
+ from pytest_mock.plugin import MockerFixture
+
+
+class TestExceptions:
+ """Test custom exception classes."""
+
+ def test_json_read_error(self) -> None:
+ """Test JSONReadError exception."""
+ with pytest.raises(JSONReadError) as exc_info:
+ raise JSONReadError("Test error message")
+ assert str(exc_info.value) == "Test error message"
+
+ def test_invalid_data_error(self) -> None:
+ """Test InvalidDataError exception."""
+ with pytest.raises(InvalidDataError) as exc_info:
+ raise InvalidDataError("Invalid data")
+ assert str(exc_info.value) == "Invalid data"
+
+ def test_url_read_error(self) -> None:
+ """Test URLReadError exception."""
+ with pytest.raises(URLReadError) as exc_info:
+ raise URLReadError("URL error")
+ assert str(exc_info.value) == "URL error"
+
+ def test_string_read_error(self) -> None:
+ """Test StringReadError exception."""
+ with pytest.raises(StringReadError) as exc_info:
+ raise StringReadError("String error")
+ assert str(exc_info.value) == "String error"
+
+
+class TestReadFromJson:
+ """Test readfromjson function."""
+
+ def test_readfromjson_valid_file(self) -> None:
+ """Test reading a valid JSON file."""
+ test_data = {"key": "value", "number": 42}
+
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+ json.dump(test_data, f)
+ temp_filename = f.name
+
+ try:
+ result = readfromjson(temp_filename)
+ assert result == test_data
+ finally:
+ import os
+ os.unlink(temp_filename)
+
+ def test_readfromjson_invalid_json_content(self) -> None:
+ """Test reading a file with invalid JSON content."""
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+ f.write('{"invalid": json content}') # Invalid JSON
+ temp_filename = f.name
+
+ try:
+ with pytest.raises(JSONReadError, match="Invalid JSON File"):
+ readfromjson(temp_filename)
+ finally:
+ import os
+ os.unlink(temp_filename)
+
+ def test_readfromjson_file_not_found(self) -> None:
+ """Test reading a non-existent file."""
+ with pytest.raises(JSONReadError, match="Invalid JSON File"):
+ readfromjson("non_existent_file.json")
+
+ @patch('builtins.open')
+ def test_readfromjson_permission_error(self, mock_open: Mock) -> None:
+ """Test reading a file with permission issues."""
+ # Mock open to raise PermissionError
+ mock_open.side_effect = PermissionError("Permission denied")
+
+ with pytest.raises(JSONReadError, match="Invalid JSON File"):
+ readfromjson("some_file.json")
+
+ @patch('builtins.open')
+ def test_readfromjson_os_error(self, mock_open: Mock) -> None:
+ """Test reading a file with OS error."""
+ # Mock open to raise OSError (covers line 34-35 in utils.py)
+ mock_open.side_effect = OSError("Device not ready")
+
+ with pytest.raises(JSONReadError, match="Invalid JSON File"):
+ readfromjson("some_file.json")
+
+
+class TestReadFromUrl:
+ """Test readfromurl function."""
+
+ @patch('json2xml.utils.urllib3.PoolManager')
+ def test_readfromurl_success(self, mock_pool_manager: Mock) -> None:
+ """Test successful URL reading."""
+ # Mock response
+ mock_response = Mock()
+ mock_response.status = 200
+ mock_response.data = b'{"key": "value", "number": 42}'
+
+ # Mock PoolManager
+ mock_http = Mock()
+ mock_http.request.return_value = mock_response
+ mock_pool_manager.return_value = mock_http
+
+ result = readfromurl("http://example.com/data.json")
+
+ assert result == {"key": "value", "number": 42}
+ mock_pool_manager.assert_called_once()
+ mock_http.request.assert_called_once_with("GET", "http://example.com/data.json", fields=None)
+
+ @patch('json2xml.utils.urllib3.PoolManager')
+ def test_readfromurl_success_with_params(self, mock_pool_manager: Mock) -> None:
+ """Test successful URL reading with parameters."""
+ # Mock response
+ mock_response = Mock()
+ mock_response.status = 200
+ mock_response.data = b'{"result": "success"}'
+
+ # Mock PoolManager
+ mock_http = Mock()
+ mock_http.request.return_value = mock_response
+ mock_pool_manager.return_value = mock_http
+
+ params = {"param1": "value1", "param2": "value2"}
+ result = readfromurl("http://example.com/api", params=params)
+
+ assert result == {"result": "success"}
+ mock_http.request.assert_called_once_with("GET", "http://example.com/api", fields=params)
+
+ @patch('json2xml.utils.urllib3.PoolManager')
+ def test_readfromurl_http_error(self, mock_pool_manager: Mock) -> None:
+ """Test URL reading with HTTP error status."""
+ # Mock response with error status
+ mock_response = Mock()
+ mock_response.status = 404
+
+ # Mock PoolManager
+ mock_http = Mock()
+ mock_http.request.return_value = mock_response
+ mock_pool_manager.return_value = mock_http
+
+ with pytest.raises(URLReadError, match="URL is not returning correct response"):
+ readfromurl("http://example.com/nonexistent.json")
+
+ @patch('json2xml.utils.urllib3.PoolManager')
+ def test_readfromurl_server_error(self, mock_pool_manager: Mock) -> None:
+ """Test URL reading with server error status."""
+ # Mock response with server error status
+ mock_response = Mock()
+ mock_response.status = 500
+
+ # Mock PoolManager
+ mock_http = Mock()
+ mock_http.request.return_value = mock_response
+ mock_pool_manager.return_value = mock_http
+
+ with pytest.raises(URLReadError, match="URL is not returning correct response"):
+ readfromurl("http://example.com/error.json")
+
+ @patch('json2xml.utils.urllib3.PoolManager')
+ def test_readfromurl_invalid_json_response(self, mock_pool_manager: Mock) -> None:
+ """Test URL reading with invalid JSON response."""
+ # Mock response with invalid JSON
+ mock_response = Mock()
+ mock_response.status = 200
+ mock_response.data = b'invalid json content'
+
+ # Mock PoolManager
+ mock_http = Mock()
+ mock_http.request.return_value = mock_response
+ mock_pool_manager.return_value = mock_http
+
+ with pytest.raises(json.JSONDecodeError):
+ readfromurl("http://example.com/invalid.json")
+
+
+class TestReadFromString:
+ """Test readfromstring function."""
+
+ def test_readfromstring_valid_json(self) -> None:
+ """Test reading valid JSON string."""
+ json_string = '{"key": "value", "number": 42, "boolean": true}'
+ result = readfromstring(json_string)
+ assert result == {"key": "value", "number": 42, "boolean": True}
+
+ def test_readfromstring_empty_object(self) -> None:
+ """Test reading empty JSON object."""
+ json_string = '{}'
+ result = readfromstring(json_string)
+ assert result == {}
+
+ def test_readfromstring_complex_object(self) -> None:
+ """Test reading complex JSON object."""
+ json_string = '{"users": [{"name": "John", "age": 30}, {"name": "Jane", "age": 25}], "total": 2}'
+ result = readfromstring(json_string)
+ expected = {
+ "users": [
+ {"name": "John", "age": 30},
+ {"name": "Jane", "age": 25}
+ ],
+ "total": 2
+ }
+ assert result == expected
+
+ def test_readfromstring_invalid_type_int(self) -> None:
+ """Test reading with integer input."""
+ with pytest.raises(StringReadError, match="Input is not a proper JSON string"):
+ readfromstring(123) # type: ignore[arg-type]
+
+ def test_readfromstring_invalid_type_list(self) -> None:
+ """Test reading with list input."""
+ with pytest.raises(StringReadError, match="Input is not a proper JSON string"):
+ readfromstring(["not", "a", "string"]) # type: ignore[arg-type]
+
+ def test_readfromstring_invalid_type_dict(self) -> None:
+ """Test reading with dict input."""
+ with pytest.raises(StringReadError, match="Input is not a proper JSON string"):
+ readfromstring({"not": "a string"}) # type: ignore[arg-type]
+
+ def test_readfromstring_invalid_type_none(self) -> None:
+ """Test reading with None input."""
+ with pytest.raises(StringReadError, match="Input is not a proper JSON string"):
+ readfromstring(None) # type: ignore[arg-type]
+
+ def test_readfromstring_invalid_json_syntax(self) -> None:
+ """Test reading string with invalid JSON syntax."""
+ with pytest.raises(StringReadError, match="Input is not a proper JSON string"):
+ readfromstring('{"invalid": json, syntax}')
+
+ def test_readfromstring_invalid_json_incomplete(self) -> None:
+ """Test reading incomplete JSON string."""
+ with pytest.raises(StringReadError, match="Input is not a proper JSON string"):
+ readfromstring('{"incomplete":')
+
+ def test_readfromstring_invalid_json_extra_comma(self) -> None:
+ """Test reading JSON string with trailing comma."""
+ with pytest.raises(StringReadError, match="Input is not a proper JSON string"):
+ readfromstring('{"key": "value",}')
+
+ def test_readfromstring_invalid_json_single_quotes(self) -> None:
+ """Test reading JSON string with single quotes."""
+ with pytest.raises(StringReadError, match="Input is not a proper JSON string"):
+ readfromstring("{'key': 'value'}")
+
+ def test_readfromstring_empty_string(self) -> None:
+ """Test reading empty string."""
+ with pytest.raises(StringReadError, match="Input is not a proper JSON string"):
+ readfromstring("")
+
+ def test_readfromstring_plain_text(self) -> None:
+ """Test reading plain text."""
+ with pytest.raises(StringReadError, match="Input is not a proper JSON string"):
+ readfromstring("this is just plain text")
+
+
+class TestIntegration:
+ """Integration tests combining multiple utilities."""
+
+ def test_readfromstring_then_convert_to_xml(self) -> None:
+ """Test reading JSON string and converting to XML."""
+ from json2xml import dicttoxml
+
+ json_string = '{"name": "test", "value": 123}'
+ data = readfromstring(json_string)
+ xml_result = dicttoxml.dicttoxml(data, attr_type=False, root=False)
+
+ assert b"test" in xml_result
+ assert b"123" in xml_result
+
+ @patch('json2xml.utils.urllib3.PoolManager')
+ def test_readfromurl_then_convert_to_xml(self, mock_pool_manager: Mock) -> None:
+ """Test reading from URL and converting to XML."""
+ from json2xml import dicttoxml
+
+ # Mock response
+ mock_response = Mock()
+ mock_response.status = 200
+ mock_response.data = b'{"api": "response", "status": "ok"}'
+
+ # Mock PoolManager
+ mock_http = Mock()
+ mock_http.request.return_value = mock_response
+ mock_pool_manager.return_value = mock_http
+
+ data = readfromurl("http://example.com/api.json")
+ xml_result = dicttoxml.dicttoxml(data, attr_type=False, root=False)
+
+ assert b"response" in xml_result
+ assert b"ok" in xml_result