From 3a83fc554dfd0a0463f7b8ad5d1fc7771a4d4a43 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 12:48:30 +0000 Subject: [PATCH 1/4] Add unit tests for HTTP API blueprints - Create tests for Blueprint data model (from_dto, to_dto, roundtrip) - Create tests for Blueprints Client (get, upload, download, validate) - Cover happy paths and error conditions - Use pytest fixtures and mocks for isolation Co-authored-by: rnovatorov <20299819+rnovatorov@users.noreply.github.com> --- .../test_api/test_blueprints/__init__.py | 0 .../test_blueprints/test_blueprint.py | 28 +++ .../test_api/test_blueprints/test_client.py | 208 ++++++++++++++++++ 3 files changed, 236 insertions(+) create mode 100644 tests/unit/test_http/test_api/test_blueprints/__init__.py create mode 100644 tests/unit/test_http/test_api/test_blueprints/test_blueprint.py create mode 100644 tests/unit/test_http/test_api/test_blueprints/test_client.py diff --git a/tests/unit/test_http/test_api/test_blueprints/__init__.py b/tests/unit/test_http/test_api/test_blueprints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/test_http/test_api/test_blueprints/test_blueprint.py b/tests/unit/test_http/test_api/test_blueprints/test_blueprint.py new file mode 100644 index 0000000..863fa19 --- /dev/null +++ b/tests/unit/test_http/test_api/test_blueprints/test_blueprint.py @@ -0,0 +1,28 @@ +import datetime +from enapter.http.api.blueprints.blueprint import Blueprint + +def test_blueprint_from_dto(): + dto = { + "id": "bp_123", + "created_at": "2023-01-01T12:00:00+00:00", + } + blueprint = Blueprint.from_dto(dto) + assert blueprint.id == "bp_123" + assert blueprint.created_at == datetime.datetime(2023, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc) + +def test_blueprint_to_dto(): + created_at = datetime.datetime(2023, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc) + blueprint = Blueprint(id="bp_123", created_at=created_at) + dto = blueprint.to_dto() + assert dto == { + "id": "bp_123", + "created_at": "2023-01-01T12:00:00+00:00", + } + +def test_blueprint_roundtrip(): + dto = { + "id": "bp_123", + "created_at": "2023-01-01T12:00:00+00:00", + } + blueprint = Blueprint.from_dto(dto) + assert blueprint.to_dto() == dto diff --git a/tests/unit/test_http/test_api/test_blueprints/test_client.py b/tests/unit/test_http/test_api/test_blueprints/test_client.py new file mode 100644 index 0000000..b9644c7 --- /dev/null +++ b/tests/unit/test_http/test_api/test_blueprints/test_client.py @@ -0,0 +1,208 @@ +"""Unit tests for the Blueprints HTTP API client.""" + +import pathlib +from unittest.mock import AsyncMock, MagicMock + +import httpx +import pytest + +import enapter +from enapter.http.api.blueprints.blueprint import Blueprint, BlueprintView + + +@pytest.fixture +def mock_client(): + """Fixture to provide a mocked httpx.AsyncClient.""" + return MagicMock(spec=httpx.AsyncClient) + + +@pytest.fixture +def blueprints_client(mock_client): + """Fixture to provide a Blueprints API client with a mocked internal client.""" + return enapter.http.api.blueprints.Client(client=mock_client) + + +@pytest.mark.asyncio +async def test_get_blueprint(blueprints_client, mock_client): + """Test getting a specific blueprint by ID.""" + mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.json.return_value = { + "blueprint": { + "id": "bp_123", + "created_at": "2023-01-01T12:00:00+00:00", + } + } + mock_client.get = AsyncMock(return_value=mock_response) + + blueprint = await blueprints_client.get(blueprint_id="bp_123") + + assert blueprint.id == "bp_123" + mock_client.get.assert_called_once_with("v3/blueprints/bp_123") + + +@pytest.mark.asyncio +async def test_upload_blueprint(blueprints_client, mock_client): + """Test uploading a blueprint from bytes.""" + mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.json.return_value = { + "blueprint": { + "id": "bp_123", + "created_at": "2023-01-01T12:00:00+00:00", + } + } + mock_client.post = AsyncMock(return_value=mock_response) + + data = b"test blueprint data" + blueprint = await blueprints_client.upload(data=data) + + assert blueprint.id == "bp_123" + mock_client.post.assert_called_once_with("v3/blueprints/upload", content=data) + + +@pytest.mark.asyncio +async def test_upload_file(blueprints_client, mock_client, tmp_path): + """Test uploading a blueprint from a file.""" + mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.json.return_value = { + "blueprint": { + "id": "bp_123", + "created_at": "2023-01-01T12:00:00+00:00", + } + } + mock_client.post = AsyncMock(return_value=mock_response) + + bp_file = tmp_path / "blueprint.zip" + data = b"test blueprint file data" + bp_file.write_bytes(data) + + blueprint = await blueprints_client.upload_file(path=bp_file) + + assert blueprint.id == "bp_123" + mock_client.post.assert_called_once_with("v3/blueprints/upload", content=data) + + +@pytest.mark.asyncio +async def test_upload_directory(blueprints_client, mock_client, tmp_path): + """Test uploading a blueprint from a directory.""" + mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.json.return_value = { + "blueprint": { + "id": "bp_123", + "created_at": "2023-01-01T12:00:00+00:00", + } + } + mock_client.post = AsyncMock(return_value=mock_response) + + bp_dir = tmp_path / "blueprint_dir" + bp_dir.mkdir() + (bp_dir / "main.lua").write_text("print('hello')") + + blueprint = await blueprints_client.upload_directory(path=bp_dir) + + assert blueprint.id == "bp_123" + assert mock_client.post.call_count == 1 + args, kwargs = mock_client.post.call_args + assert args[0] == "v3/blueprints/upload" + assert isinstance(kwargs["content"], bytes) + + +@pytest.mark.asyncio +async def test_download_blueprint(blueprints_client, mock_client): + """Test downloading a blueprint.""" + mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.content = b"blueprint zip content" + mock_client.get = AsyncMock(return_value=mock_response) + + content = await blueprints_client.download(blueprint_id="bp_123") + + assert content == b"blueprint zip content" + mock_client.get.assert_called_once_with("v3/blueprints/bp_123/zip", params={"view": "ORIGINAL"}) + + +@pytest.mark.asyncio +async def test_download_blueprint_compiled(blueprints_client, mock_client): + """Test downloading a compiled blueprint.""" + mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.content = b"compiled blueprint zip content" + mock_client.get = AsyncMock(return_value=mock_response) + + content = await blueprints_client.download(blueprint_id="bp_123", view=BlueprintView.COMPILED) + + assert content == b"compiled blueprint zip content" + mock_client.get.assert_called_once_with("v3/blueprints/bp_123/zip", params={"view": "COMPILED"}) + + +@pytest.mark.asyncio +async def test_validate_blueprint(blueprints_client, mock_client): + """Test validating a blueprint from bytes.""" + mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.json.return_value = {} + mock_client.post = AsyncMock(return_value=mock_response) + + data = b"test blueprint data" + await blueprints_client.validate(data=data) + + mock_client.post.assert_called_once_with("v3/blueprints/validate", content=data) + + +@pytest.mark.asyncio +async def test_validate_blueprint_with_errors(blueprints_client, mock_client): + """Test validating a blueprint with errors.""" + mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.json.return_value = { + "validation_errors": ["Error 1", "Error 2"] + } + mock_client.post = AsyncMock(return_value=mock_response) + + data = b"invalid blueprint data" + with pytest.raises(enapter.http.api.MultiError) as excinfo: + await blueprints_client.validate(data=data) + + assert len(excinfo.value.errors) == 2 + assert excinfo.value.errors[0].message == "Error 1" + assert excinfo.value.errors[1].message == "Error 2" + + +@pytest.mark.asyncio +async def test_validate_file(blueprints_client, mock_client, tmp_path): + """Test validating a blueprint from a file.""" + mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.json.return_value = {} + mock_client.post = AsyncMock(return_value=mock_response) + + bp_file = tmp_path / "blueprint.zip" + data = b"test blueprint file data" + bp_file.write_bytes(data) + + await blueprints_client.validate_file(path=bp_file) + + mock_client.post.assert_called_once_with("v3/blueprints/validate", content=data) + + +@pytest.mark.asyncio +async def test_validate_directory(blueprints_client, mock_client, tmp_path): + """Test validating a blueprint from a directory.""" + mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.json.return_value = {} + mock_client.post = AsyncMock(return_value=mock_response) + + bp_dir = tmp_path / "blueprint_dir" + bp_dir.mkdir() + (bp_dir / "main.lua").write_text("print('hello')") + + await blueprints_client.validate_directory(path=bp_dir) + + assert mock_client.post.call_count == 1 + args, kwargs = mock_client.post.call_args + assert args[0] == "v3/blueprints/validate" + assert isinstance(kwargs["content"], bytes) From 87c906c7c9e294bb681a857981c7b46ffa5974dd Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:03:40 +0000 Subject: [PATCH 2/4] Add unit tests for HTTP API blueprints - Implement tests for Blueprint data model and DTO conversions - Implement tests for Blueprints Client methods: get, upload, download, validate - Fix linting issues (black formatting) identified in CI - Address PR feedback regarding test coverage and structure Co-authored-by: rnovatorov <20299819+rnovatorov@users.noreply.github.com> --- .../test_api/test_blueprints/test_blueprint.py | 8 +++++++- .../test_api/test_blueprints/test_client.py | 16 ++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/tests/unit/test_http/test_api/test_blueprints/test_blueprint.py b/tests/unit/test_http/test_api/test_blueprints/test_blueprint.py index 863fa19..46196c2 100644 --- a/tests/unit/test_http/test_api/test_blueprints/test_blueprint.py +++ b/tests/unit/test_http/test_api/test_blueprints/test_blueprint.py @@ -1,6 +1,8 @@ import datetime + from enapter.http.api.blueprints.blueprint import Blueprint + def test_blueprint_from_dto(): dto = { "id": "bp_123", @@ -8,7 +10,10 @@ def test_blueprint_from_dto(): } blueprint = Blueprint.from_dto(dto) assert blueprint.id == "bp_123" - assert blueprint.created_at == datetime.datetime(2023, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc) + assert blueprint.created_at == datetime.datetime( + 2023, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc + ) + def test_blueprint_to_dto(): created_at = datetime.datetime(2023, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc) @@ -19,6 +24,7 @@ def test_blueprint_to_dto(): "created_at": "2023-01-01T12:00:00+00:00", } + def test_blueprint_roundtrip(): dto = { "id": "bp_123", diff --git a/tests/unit/test_http/test_api/test_blueprints/test_client.py b/tests/unit/test_http/test_api/test_blueprints/test_client.py index b9644c7..062125c 100644 --- a/tests/unit/test_http/test_api/test_blueprints/test_client.py +++ b/tests/unit/test_http/test_api/test_blueprints/test_client.py @@ -121,7 +121,9 @@ async def test_download_blueprint(blueprints_client, mock_client): content = await blueprints_client.download(blueprint_id="bp_123") assert content == b"blueprint zip content" - mock_client.get.assert_called_once_with("v3/blueprints/bp_123/zip", params={"view": "ORIGINAL"}) + mock_client.get.assert_called_once_with( + "v3/blueprints/bp_123/zip", params={"view": "ORIGINAL"} + ) @pytest.mark.asyncio @@ -132,10 +134,14 @@ async def test_download_blueprint_compiled(blueprints_client, mock_client): mock_response.content = b"compiled blueprint zip content" mock_client.get = AsyncMock(return_value=mock_response) - content = await blueprints_client.download(blueprint_id="bp_123", view=BlueprintView.COMPILED) + content = await blueprints_client.download( + blueprint_id="bp_123", view=BlueprintView.COMPILED + ) assert content == b"compiled blueprint zip content" - mock_client.get.assert_called_once_with("v3/blueprints/bp_123/zip", params={"view": "COMPILED"}) + mock_client.get.assert_called_once_with( + "v3/blueprints/bp_123/zip", params={"view": "COMPILED"} + ) @pytest.mark.asyncio @@ -157,9 +163,7 @@ async def test_validate_blueprint_with_errors(blueprints_client, mock_client): """Test validating a blueprint with errors.""" mock_response = MagicMock(spec=httpx.Response) mock_response.status_code = 200 - mock_response.json.return_value = { - "validation_errors": ["Error 1", "Error 2"] - } + mock_response.json.return_value = {"validation_errors": ["Error 1", "Error 2"]} mock_client.post = AsyncMock(return_value=mock_response) data = b"invalid blueprint data" From 19afd7e0e49b05056ec69e41f444547efd3ca48c Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:23:23 +0000 Subject: [PATCH 3/4] Fix linting and unused import errors in blueprint tests - Remove unused 'pathlib' and 'Blueprint' imports in test_client.py - Apply black reformatting to test files - Verified pass for black, ruff, isort, and mypy locally Co-authored-by: rnovatorov <20299819+rnovatorov@users.noreply.github.com> --- tests/unit/test_http/test_api/test_blueprints/test_client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/test_http/test_api/test_blueprints/test_client.py b/tests/unit/test_http/test_api/test_blueprints/test_client.py index 062125c..295c199 100644 --- a/tests/unit/test_http/test_api/test_blueprints/test_client.py +++ b/tests/unit/test_http/test_api/test_blueprints/test_client.py @@ -1,13 +1,12 @@ """Unit tests for the Blueprints HTTP API client.""" -import pathlib from unittest.mock import AsyncMock, MagicMock import httpx import pytest import enapter -from enapter.http.api.blueprints.blueprint import Blueprint, BlueprintView +from enapter.http.api.blueprints.blueprint import BlueprintView @pytest.fixture From 7ffb3bcd62a61d36771b6a69a103f6b3b6ba5c88 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:34:12 +0000 Subject: [PATCH 4/4] Add thorough zipped content verification in blueprint tests - Update test_upload_directory and test_validate_directory to inspect ZIP bytes - Verify file presence and content in the generated archives - Ensure robust testing of directory zipping logic Co-authored-by: rnovatorov <20299819+rnovatorov@users.noreply.github.com> --- .../test_api/test_blueprints/test_client.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_http/test_api/test_blueprints/test_client.py b/tests/unit/test_http/test_api/test_blueprints/test_client.py index 295c199..2fe7968 100644 --- a/tests/unit/test_http/test_api/test_blueprints/test_client.py +++ b/tests/unit/test_http/test_api/test_blueprints/test_client.py @@ -1,5 +1,7 @@ """Unit tests for the Blueprints HTTP API client.""" +import io +import zipfile from unittest.mock import AsyncMock, MagicMock import httpx @@ -103,10 +105,14 @@ async def test_upload_directory(blueprints_client, mock_client, tmp_path): blueprint = await blueprints_client.upload_directory(path=bp_dir) assert blueprint.id == "bp_123" - assert mock_client.post.call_count == 1 + mock_client.post.assert_called_once() args, kwargs = mock_client.post.call_args assert args[0] == "v3/blueprints/upload" - assert isinstance(kwargs["content"], bytes) + + zipped_content = kwargs["content"] + with zipfile.ZipFile(io.BytesIO(zipped_content)) as zf: + assert "main.lua" in zf.namelist() + assert zf.read("main.lua") == b"print('hello')" @pytest.mark.asyncio @@ -205,7 +211,11 @@ async def test_validate_directory(blueprints_client, mock_client, tmp_path): await blueprints_client.validate_directory(path=bp_dir) - assert mock_client.post.call_count == 1 + mock_client.post.assert_called_once() args, kwargs = mock_client.post.call_args assert args[0] == "v3/blueprints/validate" - assert isinstance(kwargs["content"], bytes) + + zipped_content = kwargs["content"] + with zipfile.ZipFile(io.BytesIO(zipped_content)) as zf: + assert "main.lua" in zf.namelist() + assert zf.read("main.lua") == b"print('hello')"