diff --git a/doc/source/library/simulator/calls_request.rst b/doc/source/library/simulator/calls_request.rst new file mode 100644 index 000000000..1e8e274e2 --- /dev/null +++ b/doc/source/library/simulator/calls_request.rst @@ -0,0 +1,14 @@ +.. code-block:: json + + { + "submit": "Simulate" + "response_clear_after": 0, + "response_cr": "", + "response_cr_pct": 0, + "response_split": "", + "split_delay": 1 + "response_delay": 0, + "response_error": 0, + "response_junk_datalen": 0, + "response_type": 0, + } \ No newline at end of file diff --git a/doc/source/library/simulator/calls_response.rst b/doc/source/library/simulator/calls_response.rst new file mode 100644 index 000000000..f3cb303fd --- /dev/null +++ b/doc/source/library/simulator/calls_response.rst @@ -0,0 +1,167 @@ +.. code-block:: json + + { + "simulation_action": "ACTIVE", + "range_start": null, + "range_stop": null, + "function_codes": [ + { + "value": 3, + "text": "read_holding_registers", + "selected": false + }, + { + "value": 2, + "text": "read_discrete_input", + "selected": false + }, + { + "value": 4, + "text": "read_input_registers", + "selected": false + }, + { + "value": 1, + "text": "read_coils", + "selected": false + }, + { + "value": 15, + "text": "write_coils", + "selected": false + }, + { + "value": 16, + "text": "write_registers", + "selected": false + }, + { + "value": 6, + "text": "write_register", + "selected": false + }, + { + "value": 5, + "text": "write_coil", + "selected": false + }, + { + "value": 23, + "text": "read_write_multiple_registers", + "selected": false + }, + { + "value": 8, + "text": "diagnostic_status", + "selected": false + }, + { + "value": 7, + "text": "read_exception_status", + "selected": false + }, + { + "value": 11, + "text": "get_event_counter", + "selected": false + }, + { + "value": 12, + "text": "get_event_log", + "selected": false + }, + { + "value": 17, + "text": "report_slave_id", + "selected": false + }, + { + "value": 20, + "text": "read_file_record", + "selected": false + }, + { + "value": 21, + "text": "write_file_record", + "selected": false + }, + { + "value": 22, + "text": "mask_write_register", + "selected": false + }, + { + "value": 24, + "text": "read_fifo_queue", + "selected": false + }, + { + "value": 43, + "text": "read_device_information", + "selected": false + } + ], + "function_show_hex_checked": false, + "function_show_decoded_checked": false, + "function_response_normal_checked": true, + "function_response_error_checked": false, + "function_response_empty_checked": false, + "function_response_junk_checked": false, + "function_response_split_checked": true, + "function_response_split_delay": 1, + "function_response_cr_checked": false, + "function_response_cr_pct": 0, + "function_response_delay": 0, + "function_response_junk": 0, + "function_error": [ + { + "value": 1, + "text": "IllegalFunction", + "selected": false + }, + { + "value": 2, + "text": "IllegalAddress", + "selected": false + }, + { + "value": 3, + "text": "IllegalValue", + "selected": false + }, + { + "value": 4, + "text": "SlaveFailure", + "selected": false + }, + { + "value": 5, + "text": "Acknowledge", + "selected": false + }, + { + "value": 6, + "text": "SlaveBusy", + "selected": false + }, + { + "value": 7, + "text": "MemoryParityError", + "selected": false + }, + { + "value": 10, + "text": "GatewayPathUnavailable", + "selected": false + }, + { + "value": 11, + "text": "GatewayNoResponse", + "selected": false + } + ], + "function_response_clear_after": 1, + "call_rows": [], + "foot": "not active", + "result": "ok" + } \ No newline at end of file diff --git a/doc/source/library/simulator/registers_request.rst b/doc/source/library/simulator/registers_request.rst new file mode 100644 index 000000000..24e5ece1a --- /dev/null +++ b/doc/source/library/simulator/registers_request.rst @@ -0,0 +1,7 @@ +.. code-block:: json + + { + "range_start": 16, + "range_end": 16, + "submit": "Register" + } \ No newline at end of file diff --git a/doc/source/library/simulator/registers_response.rst b/doc/source/library/simulator/registers_response.rst new file mode 100644 index 000000000..1bdfa70c8 --- /dev/null +++ b/doc/source/library/simulator/registers_response.rst @@ -0,0 +1,34 @@ +.. code-block:: json + + { + "result": "ok", + "footer": "Operation completed successfully", + "register_types": { + "bits": 1, + "uint16": 2, + "uint32": 3, + "float32": 4, + "string": 5, + "next": 6, + "invalid": 0 + }, + "register_actions": { + "null": 0, + "increment": 1, + "random": 2, + "reset": 3, + "timestamp": 4, + "uptime": 5 + }, + "register_rows": [ + { + "index": "16", + "type": "uint16", + "access": "True", + "action": "none", + "value": "3124", + "count_read": "0", + "count_write": "0" + } + ] + } \ No newline at end of file diff --git a/doc/source/library/simulator/restapi.rst b/doc/source/library/simulator/restapi.rst index a976a9e1e..74d9bedfa 100644 --- a/doc/source/library/simulator/restapi.rst +++ b/doc/source/library/simulator/restapi.rst @@ -1,4 +1,120 @@ Pymodbus simulator ReST API =========================== -TO BE DOCUMENTED. +This is still a Work In Progress. There may be large changes to the API in the +future. + +The API is a simple copy of +having most of the same features as in the Web UI. + +The API provides the following endpoints: + +- /restapi/registers +- /restapi/calls +- /restapi/server +- /restapi/log + +Registers Endpoint +------------------ + +/restapi/registers +^^^^^^^^^^^^^^^^^^ + + The registers endpoint is used to read and write registers. + + **Request Parameters** + + - `submit` (string, required): + The action to perform. Must be one of `Register`, `Set`. + - `range_start` (integer, optional): + The starting register to read from. Defaults to 0. + - `range_end` (integer, optional): + The ending register to read from. Defaults to `range_start`. + + **Response Parameters** + + Returns a json object with the following keys: + + - `result` (string): + The result of the action. Either `ok` or `error`. + - `error` (string, conditional): + The error message if the result is `error`. + - `register_rows` (list): + A list of objects containing the data of the registers. + - `footer` (string): + A cleartext status of the action. HTML leftover. + - `register_types` (list): + A static list of register types. HTML leftover. + - `register_actions` (list): + A static list of register actions. HTML leftover. + + **Example Request and Response** + + Request Example: + + .. include:: registers_request.rst + + Response Example: + + .. include:: registers_response.rst + +Calls Endpoint +-------------- + +The calls endpoint is used to handle ModBus response manipulation. + +/restapi/calls +^^^^^^^^^^^^^^ + + The calls endpoint is used to simulate different conditions for ModBus + responses. + + **Request Parameters** + + - `submit` (string, required): + The action to perform. Must be one of `Simulate`, `Reset`. + + The following must be present if `submit` is `Simulate`: + + - `response_clear_after` (integer, required): + The number of packet to clear simulation after. + - `response_cr` (string, required): + Must be present but can be any value. Turns on change rate simulation (WIP). + - `response_cr_pct` (integer, required): + The percentage of change rate, how many percent of packets should be + changed. + - `response_split` (string, required): + Must be present but can be any value. Turns on split response simulation (WIP). + - `split_delay` (integer, required): + The delay in seconds to wait before sending the second part of the split response. + - `response_delay` (integer, required): + The delay in seconds to wait before sending the response. + - `response_error` (integer, required): + The error code to send in the response. The valid values can be one from + the response `function_error` list. + + When `submit` is `Reset`, no other parameters are required. It resets all + simulation options to their defaults (off). + + **Example Request and Response** + + Request: + + .. include:: calls_request.rst + + Response: + + Unfortunately, the endpoint response contains extra clutter due to + not being finalized. + + .. include:: calls_response.rst + +Server Endpoint +--------------- + +The server endpoint has not yet been implemented. + +Log Endpoint +------------ + +The log endpoint has not yet been implemented. \ No newline at end of file diff --git a/pymodbus/server/simulator/http_server.py b/pymodbus/server/simulator/http_server.py index 9e4afb570..6bf170716 100644 --- a/pymodbus/server/simulator/http_server.py +++ b/pymodbus/server/simulator/http_server.py @@ -180,7 +180,7 @@ def __init__( self.web_app.add_routes( [ web.get("/api/{tail:[a-z]*}", self.handle_html), - web.post("/api/{tail:[a-z]*}", self.handle_json), + web.post("/restapi/{tail:[a-z]*}", self.handle_json), web.get("/{tail:[a-z0-9.]*}", self.handle_html_static), web.get("/", self.handle_html_static), ] @@ -193,13 +193,13 @@ def __init__( "calls": ["", self.build_html_calls], "server": ["", self.build_html_server], } - self.generator_json: dict[str, list] = { - "log_json": [None, self.build_json_log], - "registers_json": [None, self.build_json_registers], - "calls_json": [None, self.build_json_calls], - "server_json": [None, self.build_json_server], + self.generator_json = { + "log": self.build_json_log, + "registers": self.build_json_registers, + "calls": self.build_json_calls, + "server": self.build_json_server, } - self.submit = { + self.submit_html = { "Clear": self.action_clear, "Stop": self.action_stop, "Reset": self.action_reset, @@ -301,15 +301,18 @@ async def handle_html(self, request): async def handle_json(self, request): """Handle api registers.""" - page_type = request.path.split("/")[-1] - params = await request.post() - json_dict = self.generator_html[page_type][0].copy() - result = self.generator_json[page_type][1](params, json_dict) - return web.Response(text=f"json build: {page_type} - {params} - {result}") + command = request.path.split("/")[-1] + params = await request.json() + try: + result = self.generator_json[command](params) + except (KeyError, ValueError, TypeError, IndexError) as exc: + Log.error("Unhandled error during json request: {}", exc) + return web.json_response({"result": "error", "error": f"Unhandled error Error: {exc}"}) + return web.json_response(result) def build_html_registers(self, params, html): """Build html registers page.""" - result_txt, foot = self.helper_build_html_submit(params) + result_txt, foot = self.helper_handle_submit(params, self.submit_html) if not result_txt: result_txt = "ok" if not foot: @@ -354,7 +357,7 @@ def build_html_registers(self, params, html): def build_html_calls(self, params: dict, html: str) -> str: """Build html calls page.""" - result_txt, foot = self.helper_build_html_submit(params) + result_txt, foot = self.helper_handle_submit(params, self.submit_html) if not foot: foot = "Montitoring active" if self.call_monitor.active else "not active" if not result_txt: @@ -461,23 +464,154 @@ def build_html_server(self, _params, html): """Build html server page.""" return html - def build_json_registers(self, params, json_dict): - """Build html registers page.""" - return f"json build registers: {params} - {json_dict}" + def build_json_registers(self, params): + """Build json registers response.""" + # Process params using the helper function + result_txt, foot = self.helper_handle_submit(params, { + "Set": self.action_set, + }) - def build_json_calls(self, params, json_dict): - """Build html calls page.""" - return f"json build calls: {params} - {json_dict}" + if not result_txt: + result_txt = "ok" + if not foot: + foot = "Operation completed successfully" + + # Extract necessary parameters + try: + range_start = int(params.get("range_start", 0)) + range_stop = int(params.get("range_stop", range_start)) + except ValueError: + return {"result": "error", "error": "Invalid range parameters"} + + # Retrieve register details + register_rows = [] + for i in range(range_start, range_stop + 1): + inx, reg = self.datastore_context.get_text_register(i) + row = { + "index": inx, + "type": reg.type, + "access": reg.access, + "action": reg.action, + "value": reg.value, + "count_read": reg.count_read, + "count_write": reg.count_write + } + register_rows.append(row) + + # Generate register types and actions (assume these are predefined mappings) + register_types = dict(self.datastore_context.registerType_name_to_id) + register_actions = dict(self.datastore_context.action_name_to_id) + + # Build the JSON response + json_response = { + "result": result_txt, + "footer": foot, + "register_types": register_types, + "register_actions": register_actions, + "register_rows": register_rows, + } + + return json_response + + def build_json_calls(self, params: dict) -> dict: + """Build json calls response.""" + result_txt, foot = self.helper_handle_submit(params, { + "Reset": self.action_reset, + "Add": self.action_add, + "Simulate": self.action_simulate, + }) + if not foot: + foot = "Monitoring active" if self.call_monitor.active else "not active" + if not result_txt: + result_txt = "ok" + + function_error = [] + for i, txt in ( + (1, "IllegalFunction"), + (2, "IllegalAddress"), + (3, "IllegalValue"), + (4, "SlaveFailure"), + (5, "Acknowledge"), + (6, "SlaveBusy"), + (7, "MemoryParityError"), + (10, "GatewayPathUnavailable"), + (11, "GatewayNoResponse"), + ): + function_error.append({ + "value": i, + "text": txt, + "selected": i == self.call_response.error_response + }) + + range_start = ( + self.call_monitor.range_start + if self.call_monitor.range_start != -1 + else None + ) + range_stop = ( + self.call_monitor.range_stop + if self.call_monitor.range_stop != -1 + else None + ) + + function_codes = [] + for function in self.request_lookup.values(): + function_codes.append({ + "value": function.function_code, # type: ignore[attr-defined] + "text": function.function_code_name, # type: ignore[attr-defined] + "selected": function.function_code == self.call_monitor.function # type: ignore[attr-defined] + }) + + simulation_action = "ACTIVE" if self.call_response.active != RESPONSE_INACTIVE else "" + + max_len = MAX_FILTER if self.call_monitor.active else 0 + while len(self.call_list) > max_len: + del self.call_list[0] + call_rows = [] + for entry in reversed(self.call_list): + call_rows.append({ + "call": entry.call, + "fc": entry.fc, + "address": entry.address, + "count": entry.count, + "data": entry.data.decode() + }) + + json_response = { + "simulation_action": simulation_action, + "range_start": range_start, + "range_stop": range_stop, + "function_codes": function_codes, + "function_show_hex_checked": self.call_monitor.hex, + "function_show_decoded_checked": self.call_monitor.decode, + "function_response_normal_checked": self.call_response.active == RESPONSE_NORMAL, + "function_response_error_checked": self.call_response.active == RESPONSE_ERROR, + "function_response_empty_checked": self.call_response.active == RESPONSE_EMPTY, + "function_response_junk_checked": self.call_response.active == RESPONSE_JUNK, + "function_response_split_checked": self.call_response.split > 0, + "function_response_split_delay": self.call_response.split, + "function_response_cr_checked": self.call_response.change_rate > 0, + "function_response_cr_pct": self.call_response.change_rate, + "function_response_delay": self.call_response.delay, + "function_response_junk": self.call_response.junk_len, + "function_error": function_error, + "function_response_clear_after": self.call_response.clear_after, + "call_rows": call_rows, + "foot": foot, + "result": result_txt + } + + return json_response - def build_json_log(self, params, json_dict): + def build_json_log(self, params): """Build json log page.""" - return f"json build log: {params} - {json_dict}" + return {"result": "error", "error": "log endpoint not implemented", "params": params} - def build_json_server(self, params, json_dict): + def build_json_server(self, params): """Build html server page.""" - return f"json build server: {params} - {json_dict}" + return {"result": "error", "error": "server endpoint not implemented", "params": params} - def helper_build_html_submit(self, params): + def helper_handle_submit(self, params, submit_actions): """Build html register submit.""" try: range_start = int(params.get("range_start", -1)) @@ -487,9 +621,9 @@ def helper_build_html_submit(self, params): range_stop = int(params.get("range_stop", range_start)) except ValueError: range_stop = -1 - if (submit := params["submit"]) not in self.submit: + if (submit := params["submit"]) not in submit_actions: return None, None - return self.submit[submit](params, range_start, range_stop) + return submit_actions[submit](params, range_start, range_stop) def action_clear(self, _params, _range_start, _range_stop): """Clear register filter.""" diff --git a/pyproject.toml b/pyproject.toml index 75f146ed4..4c3bbba50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,7 @@ development = [ "pytest-profiling>=1.7.0", "pytest-timeout>=2.3.1", "pytest-xdist>=3.6.1", + "pytest-aiohttp>=1.0.5", "ruff>=0.5.3", "twine>=5.1.1", "types-Pygments", @@ -218,6 +219,7 @@ all_files = "1" testpaths = ["test"] addopts = "--cov-report html --durations=10 --dist loadscope --numprocesses auto" asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" timeout = 120 [tool.coverage.run] diff --git a/test/sub_server/test_simulator_api.py b/test/sub_server/test_simulator_api.py new file mode 100644 index 000000000..094134522 --- /dev/null +++ b/test/sub_server/test_simulator_api.py @@ -0,0 +1,466 @@ +"""Test simulator API.""" +import asyncio +import json + +import pytest +from aiohttp import ClientSession + +from pymodbus.server import ModbusSimulatorServer +from pymodbus.server.simulator import http_server + + +class TestSimulatorApi: + """Integration tests for the pymodbus.SimutorServer module.""" + + default_config = { + "server_list": { + "test-device-server": { + # The test does not care about the server configuration, but + # they must be present for the simulator to start. + "comm": "tcp", + "host": "0.0.0.0", + "port": 25020, + "framer": "socket", + } + }, + "device_list": { + "test-device": { + "setup": { + "co size": 100, + "di size": 150, + "hr size": 200, + "ir size": 250, + "shared blocks": True, + "type exception": False, + "defaults": { + "value": { + "bits": 0x0708, + "uint16": 1, + "uint32": 45000, + "float32": 127.4, + "string": "X", + }, + "action": { + "bits": None, + "uint16": None, + "uint32": None, + "float32": None, + "string": None, + }, + }, + }, + "invalid": [ + 1, + [3, 4], + ], + "write": [ + 5, + [7, 8], + [16, 18], + [21, 26], + [33, 38], + ], + "bits": [ + 5, + [7, 8], + {"addr": 10, "value": 0x81}, + {"addr": [11, 12], "value": 0x04342}, + {"addr": 13, "action": "random"}, + {"addr": 14, "value": 15, "action": "reset"}, + ], + "uint16": [ + {"addr": 16, "value": 3124}, + {"addr": [17, 18], "value": 5678}, + { + "addr": [19, 20], + "value": 14661, + "action": "increment", + "args": {"minval": 1, "maxval": 100}, + }, + ], + "uint32": [ + {"addr": [21, 22], "value": 3124}, + {"addr": [23, 26], "value": 5678}, + {"addr": [27, 30], "value": 345000, "action": "increment"}, + { + "addr": [31, 32], + "value": 50, + "action": "random", + "parameters": {"minval": 10, "maxval": 80}, + }, + ], + "float32": [ + {"addr": [33, 34], "value": 3124.5}, + {"addr": [35, 38], "value": 5678.19}, + {"addr": [39, 42], "value": 345000.18, "action": "increment"}, + ], + "string": [ + {"addr": [43, 44], "value": "Str"}, + {"addr": [45, 48], "value": "Strxyz12"}, + ], + "repeat": [{"addr": [0, 48], "to": [49, 147]}], + } + } + } + + # Fixture to set up the aiohttp app + @pytest.fixture + async def client(self, aiohttp_client, tmp_path): + """Fixture to provide usable aiohttp client for testing.""" + async with ClientSession() as session: + yield session + + @pytest.fixture + async def simulator(self, tmp_path): + """Fixture to provide a standard simulator for testing.""" + config_path = tmp_path / "config.json" + # Dump the config to a json file for the simulator + with open(config_path, "w") as file: + json.dump(self.default_config, file) + + simulator = ModbusSimulatorServer( + modbus_server = "test-device-server", + modbus_device = "test-device", + http_host = "localhost", + http_port = 18080, + log_file = "simulator.log", + json_file = config_path + ) + + # Run the simulator in the current event loop. Store the task so they live + # until the test is done. + loop = asyncio.get_running_loop() + task = loop.create_task(simulator.run_forever(only_start=True)) + + # TODO: Make a better way to wait for the simulator to start + await asyncio.sleep(1) + + yield simulator + + # Stop the simulator after the test is done + task.cancel() + await task + await simulator.stop() + + @pytest.mark.asyncio + async def test_registers_json_valid(self, client, simulator): + """Test the /restapi/registers endpoint with valid parameters.""" + url = f"http://{simulator.http_host}:{simulator.http_port}/restapi/registers" + data = { + "submit": "Registers", + "range_start": 16, + "range_stop": 16, + } + + async with client.post(url, json=data) as resp: + assert resp.status == 200 + + json_response = await resp.json() + + assert "result" in json_response + assert "footer" in json_response + assert "register_types" in json_response + assert "register_actions" in json_response + assert "register_rows" in json_response + + assert json_response["result"] == "ok" + + # Check that we got the correct register and correct fields. Ignore + # the contents of the ones that haven't been explicitly set in + # config, just make sure they are present. + assert json_response["register_rows"][0]["index"] == "16" + assert json_response["register_rows"][0]["type"] == "uint16" + assert json_response["register_rows"][0]["value"] == "3124" + assert "action" in json_response["register_rows"][0] + assert "access" in json_response["register_rows"][0] + assert "count_read" in json_response["register_rows"][0] + assert "count_write" in json_response["register_rows"][0] + + @pytest.mark.asyncio + async def test_registers_json_invalid_params(self, client, simulator): + """Test the /restapi/registers endpoint with invalid parameters.""" + url = f"http://{simulator.http_host}:{simulator.http_port}/restapi/registers" + data = { + "submit": "Registers", + "range_start": "invalid", + "range_stop": 5, + } + + async with client.post(url, json=data) as resp: + # At the moment, errors are stored in the data. Only malformed + # requests or bad endpoints will return a non-200 status code. + assert resp.status == 200 + + json_response = await resp.json() + + assert "error" in json_response + assert json_response["error"] == "Invalid range parameters" + + @pytest.mark.asyncio + async def test_registers_json_non_existent_range(self, client, simulator): + """Test the /restapi/registers endpoint with a non-existent range.""" + url = f"http://{simulator.http_host}:{simulator.http_port}/restapi/registers" + data = { + "submit": "Registers", + "range_start": 5, + "range_stop": 7, + } + + async with client.post(url, json=data) as resp: + assert resp.status == 200 + + json_response = await resp.json() + assert json_response["result"] == "ok" + + # The simulator should respond with all of the ranges, but the ones that + # do not exist are marked as "invalid" + assert len(json_response["register_rows"]) == 3 + + assert json_response["register_rows"][0]["index"] == "5" + assert json_response["register_rows"][0]["type"] == "bits" + assert json_response["register_rows"][1]["index"] == "6" + assert json_response["register_rows"][1]["type"] == "invalid" + assert json_response["register_rows"][2]["index"] == "7" + assert json_response["register_rows"][2]["type"] == "bits" + + @pytest.mark.asyncio + async def test_registers_json_set_value(self, client, simulator): + """Test the /restapi/registers endpoint with a set value request.""" + url = f"http://{simulator.http_host}:{simulator.http_port}/restapi/registers" + data = { + "submit": "Set", + "register": 16, + "value": 1234, + # The range parameters are her edue to the API not being properly + # formed (yet). They are equivalent of form fields that should not + # be present in a smark json request. + "range_start": 16, + "range_stop": 16, + } + + async with client.post(url, json=data) as resp: + assert resp.status == 200 + + json_response = await resp.json() + + assert "result" in json_response + assert json_response["result"] == "ok" + + # Check that the value was set correctly + assert json_response["register_rows"][0]["index"] == "16" + assert json_response["register_rows"][0]["value"] == "1234" + + # Double check that the value was set correctly, not just the response + data2 = { + "submit": "Registers", + "range_start": 16, + "range_stop": 16, + } + async with client.post(url, json=data2) as resp: + assert resp.status == 200 + + json_response = await resp.json() + assert json_response["result"] == "ok" + + assert json_response["register_rows"][0]["index"] == "16" + assert json_response["register_rows"][0]["value"] == "1234" + + @pytest.mark.asyncio + async def test_registers_json_set_invalid_value(self, client, simulator): + """Test the /restapi/registers endpoint with an invalid set value request.""" + url = f"http://{simulator.http_host}:{simulator.http_port}/restapi/registers" + data = { + "submit": "Set", + "register": 16, + "value": "invalid", + } + + async with client.post(url, json=data) as resp: + assert resp.status == 200 + + json_response = await resp.json() + + assert "error" in json_response + # Do not check for error content. It is currently + # unhandled, so it is not guaranteed to be consistent. + + @pytest.mark.parametrize("response_type", [ + http_server.RESPONSE_NORMAL, + http_server.RESPONSE_ERROR, + http_server.RESPONSE_EMPTY, + http_server.RESPONSE_JUNK + ]) + @pytest.mark.parametrize("call", [ + ("split_delay", 1), + ("response_cr_pct", 1), + ("response_delay", 1), + ("response_error", 1), + ("response_junk_datalen", 1), + ("response_clear_after", 1), + ]) + @pytest.mark.asyncio + async def test_calls_json_simulate(self, client, simulator, response_type, call): + """ + Test the /restapi/calls endpoint to make sure simulations are set without errors. + + Some have extra parameters, others don't + """ + url = f"http://{simulator.http_host}:{simulator.http_port}/restapi/calls" + + # The default arguments which must be present in the request + data = { + "submit": "Simulate", + "response_type": response_type, + "response_split": "nomatter", + "split_delay": 0, + "response_cr": "nomatter", + "response_cr_pct": 0, + "response_delay": 0, + "response_error": 0, + "response_junk_datalen": 0, + "response_clear_after": 0, + } + + # Change the value of one call based on the parameter + data[call[0]] = call[1] + + async with client.post(url, json=data) as resp: + assert resp.status == 200 + + json_response = await resp.json() + assert json_response["result"] == "ok" + + + @pytest.mark.asyncio + async def test_calls_json_simulate_reset_no_simulation(self, client, simulator): + """ + Test the /restapi/calls endpoint with a reset request. + + Just make sure that there will be no error when resetting without triggering + a simulation. + """ + url = f"http://{simulator.http_host}:{simulator.http_port}/restapi/calls" + + data = { + "submit": "Reset", + } + + async with client.post(url, json=data) as resp: + assert resp.status == 200 + + json_response = await resp.json() + assert json_response["result"] == "ok" + + @pytest.mark.asyncio + async def test_calls_json_simulate_reset_with_simulation(self, client, simulator): + """Test the /restapi/calls endpoint with a reset request after a simulation.""" + url = f"http://{simulator.http_host}:{simulator.http_port}/restapi/calls" + + data = { + "submit": "Simulate", + "response_type": http_server.RESPONSE_EMPTY, + "response_split": "nomatter", + "split_delay": 0, + "response_cr": "nomatter", + "response_cr_pct": 0, + "response_delay": 100, + "response_error": 0, + "response_junk_datalen": 0, + "response_clear_after": 0, + } + + async with client.post(url, json=data) as resp: + assert resp.status == 200 + + json_response = await resp.json() + assert json_response["result"] == "ok" + + data = { + "submit": "Reset", + } + + async with client.post(url, json=data) as resp: + assert resp.status == 200 + + json_response = await resp.json() + assert json_response["result"] == "ok" + + @pytest.mark.asyncio + async def test_log_json_download(self, client, simulator): + """ + Test the /restapi/log endpoint with a download request. + + This test is just a placeholder at the moment to make sure the endpoint + is reachable. The actual functionality is not implemented. + """ + url = f"http://{simulator.http_host}:{simulator.http_port}/restapi/log" + data = { + "submit": "Download", + } + + # The call is undefined. Just make sure it returns 200 + async with client.post(url, json=data) as resp: + assert resp.status == 200 + + json_response = await resp.json() + assert json_response["error"] == "log endpoint not implemented" + + @pytest.mark.asyncio + async def test_log_json_monitor(self, client, simulator): + """ + Test the /restapi/log endpoint with a monitor request. + + This test is just a placeholder at the moment to make sure the endpoint + is reachable. The actual functionality is not implemented. + """ + url = f"http://{simulator.http_host}:{simulator.http_port}/restapi/log" + data = { + "submit": "Monitor", + } + + # The call is undefined. Just make sure it returns 200 + async with client.post(url, json=data) as resp: + assert resp.status == 200 + + json_response = await resp.json() + assert json_response["error"] == "log endpoint not implemented" + + @pytest.mark.asyncio + async def test_log_json_clear(self, client, simulator): + """ + Test the /restapi/log endpoint with a clear request. + + This test is just a placeholder at the moment to make sure the endpoint + is reachable. The actual functionality is not implemented. + """ + url = f"http://{simulator.http_host}:{simulator.http_port}/restapi/log" + data = { + "submit": "Clear", + } + + # The call is undefined. Just make sure it returns 200 + async with client.post(url, json=data) as resp: + assert resp.status == 200 + + json_response = await resp.json() + assert json_response["error"] == "log endpoint not implemented" + + @pytest.mark.asyncio + async def test_server_json_restart(self, client, simulator): + """ + Test the /restapi/server endpoint with a restart request. + + This test is just a placeholder at the moment to make sure the endpoint + is reachable. The actual functionality is not implemented. + """ + url = f"http://{simulator.http_host}:{simulator.http_port}/restapi/server" + data = { + "submit": "Restart", + } + + # The call is undefined. Just make sure it returns 200 + async with client.post(url, json=data) as resp: + assert resp.status == 200 + + json_response = await resp.json() + assert json_response["error"] == "server endpoint not implemented"