# Copyright (c) 2023-2025 Arista Networks, Inc. # Use of this source code is governed by the Apache License 2.0 # that can be found in the LICENSE file. """Unit tests for the asynceapi._models module.""" import logging from typing import TYPE_CHECKING from uuid import UUID import pytest from asynceapi._constants import EapiCommandFormat from asynceapi._errors import EapiReponseError from asynceapi._models import EapiCommandResult, EapiRequest, EapiResponse if TYPE_CHECKING: from asynceapi._types import EapiComplexCommand, EapiSimpleCommand class TestEapiRequest: """Test cases for the EapiRequest class.""" def test_init_with_defaults(self) -> None: """Test initialization with just required parameters.""" commands: list[EapiSimpleCommand | EapiComplexCommand] = ["show version", "show interfaces"] req = EapiRequest(commands=commands) # Check required attributes assert req.commands == commands # Check default values assert req.version == "latest" assert req.format == EapiCommandFormat.JSON assert req.timestamps is False assert req.auto_complete is False assert req.expand_aliases is False assert req.stop_on_error is True # Check that ID is generated as a UUID hex string try: UUID(str(req.id)) is_valid_uuid = True except ValueError: is_valid_uuid = False assert is_valid_uuid def test_init_with_custom_values(self) -> None: """Test initialization with custom parameter values.""" commands: list[EapiSimpleCommand | EapiComplexCommand] = [{"cmd": "enable", "input": "password"}, "show version"] req = EapiRequest( commands=commands, version=1, format=EapiCommandFormat.TEXT, timestamps=True, auto_complete=True, expand_aliases=True, stop_on_error=False, id="custom-id-123", ) # Check all attributes match expected values assert req.commands == commands assert req.version == 1 assert req.format == EapiCommandFormat.TEXT assert req.timestamps is True assert req.auto_complete is True assert req.expand_aliases is True assert req.stop_on_error is False assert req.id == "custom-id-123" def test_to_jsonrpc(self) -> None: """Test conversion to JSON-RPC dictionary.""" commands: list[EapiSimpleCommand | EapiComplexCommand] = ["show version", "show interfaces"] req = EapiRequest(commands=commands, version=1, format=EapiCommandFormat.TEXT, id="test-id-456") jsonrpc = req.to_jsonrpc() # Check that structure matches expected JSON-RPC format assert jsonrpc["jsonrpc"] == "2.0" assert jsonrpc["method"] == "runCmds" assert jsonrpc["id"] == "test-id-456" # Check params matches our configuration params = jsonrpc["params"] assert params["version"] == 1 assert params["cmds"] == commands assert params["format"] == EapiCommandFormat.TEXT assert params["timestamps"] is False assert params["autoComplete"] is False assert params["expandAliases"] is False assert params["stopOnError"] is True def test_to_jsonrpc_with_complex_commands(self) -> None: """Test JSON-RPC conversion with complex commands.""" commands: list[EapiSimpleCommand | EapiComplexCommand] = [ {"cmd": "enable", "input": "password"}, {"cmd": "configure", "input": ""}, {"cmd": "hostname test-device"}, ] req = EapiRequest(commands=commands) jsonrpc = req.to_jsonrpc() # Verify commands are passed correctly assert jsonrpc["params"]["cmds"] == commands def test_immutability(self) -> None: """Test that the dataclass is truly immutable (frozen).""" commands: list[EapiSimpleCommand | EapiComplexCommand] = ["show version"] req = EapiRequest(commands=commands) # Attempting to modify any attribute should raise an error with pytest.raises(AttributeError): req.commands = ["new command"] # type: ignore[misc] with pytest.raises(AttributeError): req.id = "new-id" # type: ignore[misc] class TestEapiResponse: """Test cases for the EapiResponse class.""" def test_init_and_properties(self) -> None: """Test basic initialization and properties.""" # Create mock command results result1 = EapiCommandResult(command="show version", output={"version": "4.33.2F-40713977.4332F (engineering build)"}) result2 = EapiCommandResult(command="show hostname", output={"hostname": "DC1-LEAF1A"}) # Create response with results results = {0: result1, 1: result2} response = EapiResponse(request_id="test-123", _results=results) # Check attributes assert response.request_id == "test-123" assert response.error_code is None assert response.error_message is None # Check properties assert response.success is True assert len(response.results) == 2 assert response.results[0] == result1 assert response.results[1] == result2 def test_error_response(self) -> None: """Test initialization with error information.""" result = EapiCommandResult(command="show bad command", output=None, errors=["Invalid input (at token 1: 'bad')"], success=False) results = {0: result} response = EapiResponse( request_id="test-456", _results=results, error_code=1002, error_message="CLI command 1 of 1 'show bad command' failed: invalid command" ) assert response.request_id == "test-456" assert response.error_code == 1002 assert response.error_message == "CLI command 1 of 1 'show bad command' failed: invalid command" assert response.success is False assert len(response.results) == 1 assert response.results[0].success is False assert "Invalid input (at token 1: 'bad')" in response.results[0].errors def test_len_and_iteration(self) -> None: """Test __len__ and __iter__ methods.""" # Create 3 command results results = { 0: EapiCommandResult(command="cmd1", output="out1"), 1: EapiCommandResult(command="cmd2", output="out2"), 2: EapiCommandResult(command="cmd3", output="out3"), } response = EapiResponse(request_id="test-789", _results=results) # Test __len__ assert len(response) == 3 # Test __iter__ iterated_results = list(response) assert len(iterated_results) == 3 assert [r.command for r in iterated_results] == ["cmd1", "cmd2", "cmd3"] def test_from_jsonrpc_success(self) -> None: """Test from_jsonrpc with successful response.""" # Mock request request = EapiRequest(commands=["show version", "show hostname"], format=EapiCommandFormat.JSON) # Mock response data jsonrpc_response = { "jsonrpc": "2.0", "id": "test-id-123", "result": [{"modelName": "cEOSLab", "version": "4.33.2F-40713977.4332F (engineering build)"}, {"hostname": "DC1-LEAF1A"}], } response = EapiResponse.from_jsonrpc(jsonrpc_response, request) # Verify response object assert response.request_id == "test-id-123" assert response.success is True assert response.error_code is None assert response.error_message is None # Verify results assert len(response) == 2 assert response.results[0].command == "show version" assert response.results[0].output == {"modelName": "cEOSLab", "version": "4.33.2F-40713977.4332F (engineering build)"} assert response.results[0].success is True assert response.results[1].command == "show hostname" assert response.results[1].output == {"hostname": "DC1-LEAF1A"} assert response.results[1].success is True def test_from_jsonrpc_text_format(self) -> None: """Test from_jsonrpc with TEXT format responses.""" # Mock request with TEXT format request = EapiRequest(commands=["show version", "show hostname"], format=EapiCommandFormat.TEXT) # Mock response data jsonrpc_response = { "jsonrpc": "2.0", "id": "text-format-id", "result": [{"output": "Arista cEOSLab\n\nSoftware image version: 4.33.2F-40713977.4332F"}, {"output": "Hostname: DC1-LEAF1A\nFQDN: DC1-LEAF1A\n"}], } response = EapiResponse.from_jsonrpc(jsonrpc_response, request) # Verify results contain the text output assert len(response) == 2 assert response.results[0].output is not None assert "Arista cEOSLab" in response.results[0].output assert response.results[1].output is not None assert "Hostname: DC1-LEAF1A" in response.results[1].output def test_from_jsonrpc_with_timestamps(self) -> None: """Test from_jsonrpc with timestamps enabled.""" # Mock request with timestamps request = EapiRequest(commands=["show version"], format=EapiCommandFormat.JSON, timestamps=True) # Mock response data with timestamps jsonrpc_response = { "jsonrpc": "2.0", "id": "timestamp-id", "result": [ { "modelName": "cEOSLab", "version": "4.33.2F-40713977.4332F (engineering build)", "_meta": {"execStartTime": 1741014072.2534037, "execDuration": 0.0024061203002929688}, } ], } response = EapiResponse.from_jsonrpc(jsonrpc_response, request) # Verify timestamp data is processed assert len(response) == 1 assert response.results[0].start_time == 1741014072.2534037 assert response.results[0].duration == 0.0024061203002929688 # Verify _meta is removed from output assert response.results[0].output is not None assert "_meta" not in response.results[0].output def test_from_jsonrpc_error_stop_on_error_true(self) -> None: """Test from_jsonrpc with error and stop_on_error=True.""" # Mock request with stop_on_error=True request = EapiRequest(commands=["show bad command", "show version", "show hostname"], stop_on_error=True) # Mock error response jsonrpc_response = { "jsonrpc": "2.0", "id": "error-id", "error": { "code": 1002, "message": "CLI command 1 of 3 'show bad command' failed: invalid command", "data": [{"errors": ["Invalid input (at token 1: 'bad')"]}], }, } response = EapiResponse.from_jsonrpc(jsonrpc_response, request) # Verify error info assert response.request_id == "error-id" assert response.error_code == 1002 assert response.error_message == "CLI command 1 of 3 'show bad command' failed: invalid command" assert response.success is False # Verify results - should have entries for all commands assert len(response) == 3 # First command failed assert response.results[0].command == "show bad command" assert response.results[0].output is None assert response.results[0].success is False assert response.results[0].errors == ["Invalid input (at token 1: 'bad')"] # Remaining commands weren't executed due to stop_on_error=True assert response.results[1].command == "show version" assert response.results[1].output is None assert response.results[1].success is False assert "Command not executed due to previous error" in response.results[1].errors assert response.results[1].executed is False assert response.results[2].command == "show hostname" assert response.results[2].output is None assert response.results[2].success is False assert "Command not executed due to previous error" in response.results[2].errors assert response.results[2].executed is False def test_from_jsonrpc_error_stop_on_error_false(self) -> None: """Test from_jsonrpc with error and stop_on_error=False.""" # Mock request with stop_on_error=False request = EapiRequest(commands=["show bad command", "show version", "show hostname"], stop_on_error=False) # Mock response with error for first command but others succeed jsonrpc_response = { "jsonrpc": "2.0", "id": "error-continue-id", "error": { "code": 1002, "message": "CLI command 1 of 3 'show bad command' failed: invalid command", "data": [ {"errors": ["Invalid input (at token 1: 'bad')"]}, {"modelName": "cEOSLab", "version": "4.33.2F-40713977.4332F (engineering build)"}, {"hostname": "DC1-LEAF1A"}, ], }, } response = EapiResponse.from_jsonrpc(jsonrpc_response, request) # Verify error info assert response.request_id == "error-continue-id" assert response.error_code == 1002 assert response.error_message == "CLI command 1 of 3 'show bad command' failed: invalid command" assert response.success is False # Verify individual command results assert len(response) == 3 # First command failed assert response.results[0].command == "show bad command" assert response.results[0].output is None assert response.results[0].success is False assert response.results[0].errors == ["Invalid input (at token 1: 'bad')"] # Remaining commands succeeded assert response.results[1].command == "show version" assert response.results[1].output == {"modelName": "cEOSLab", "version": "4.33.2F-40713977.4332F (engineering build)"} assert response.results[1].success is True assert response.results[2].command == "show hostname" assert response.results[2].output == {"hostname": "DC1-LEAF1A"} assert response.results[2].success is True def test_from_jsonrpc_raise_on_error(self) -> None: """Test from_jsonrpc with raise_on_error=True.""" # Mock request request = EapiRequest(commands=["show bad command"]) # Mock error response jsonrpc_response = { "jsonrpc": "2.0", "id": "raise-error-id", "error": { "code": 1002, "message": "CLI command 1 of 1 'show bad command' failed: invalid command", "data": [{"errors": ["Invalid input (at token 1: 'bad')"]}], }, } # Should raise EapiReponseError with pytest.raises(EapiReponseError) as excinfo: EapiResponse.from_jsonrpc(jsonrpc_response, request, raise_on_error=True) # Verify the exception contains the response assert excinfo.value.response.request_id == "raise-error-id" assert excinfo.value.response.error_code == 1002 assert excinfo.value.response.error_message == "CLI command 1 of 1 'show bad command' failed: invalid command" def test_from_jsonrpc_string_data(self, caplog: pytest.LogCaptureFixture) -> None: """Test from_jsonrpc with string data response.""" caplog.set_level(logging.WARNING) # Mock request request = EapiRequest(commands=["show bgp ipv4 unicast summary", "show bad command"]) # Mock response with JSON string jsonrpc_response = { "jsonrpc": "2.0", "id": "EapiExplorer-1", "error": { "code": 1002, "message": "CLI command 2 of 2 'show bad command' failed: invalid command", "data": [ '{"vrfs":{"default":{"vrf":"default","routerId":"10.1.0.11","asn":"65101","peers":{}}}}\n', {"errors": ["Invalid input (at token 1: 'bad')"]}, ], }, } response = EapiResponse.from_jsonrpc(jsonrpc_response, request) # Verify string was parsed as JSON assert response.results[0].output == {"vrfs": {"default": {"vrf": "default", "routerId": "10.1.0.11", "asn": "65101", "peers": {}}}} # Now test with a non-JSON string jsonrpc_response = { "jsonrpc": "2.0", "id": "EapiExplorer-1", "error": { "code": 1002, "message": "CLI command 2 of 2 'show bad command' failed: invalid command", "data": ["This is not JSON", {"errors": ["Invalid input (at token 1: 'bad')"]}], }, } response = EapiResponse.from_jsonrpc(jsonrpc_response, request) # Verify WARNING log message assert "Invalid JSON response for command: show bgp ipv4 unicast summary. Storing as text: This is not JSON" in caplog.text # Verify string is kept as is assert response.results[0].output == "This is not JSON" def test_from_jsonrpc_complex_commands(self) -> None: """Test from_jsonrpc with complex command structures.""" # Mock request with complex commands request = EapiRequest(commands=[{"cmd": "enable", "input": "password"}, "show version"]) # Mock response jsonrpc_response = { "jsonrpc": "2.0", "id": "complex-cmd-id", "result": [{}, {"modelName": "cEOSLab", "version": "4.33.2F-40713977.4332F (engineering build)"}], } response = EapiResponse.from_jsonrpc(jsonrpc_response, request) # Verify command strings are extracted correctly assert response.results[0].command == "enable" assert response.results[1].command == "show version"