From 0724cb1fbea9ce17212dac2d333d58d6e44910c2 Mon Sep 17 00:00:00 2001 From: MariusBalovin Date: Wed, 14 Aug 2024 15:20:32 +0100 Subject: [PATCH 01/13] added tests for report endpoints --- framework/python/src/api/api.py | 46 ++- testing/api/reports/report.json | 267 +++++++++++++++++ testing/api/reports/report.pdf | Bin 0 -> 29659 bytes testing/api/test_api.py | 501 +++++++++++++++++++++++++++++++- 4 files changed, 804 insertions(+), 10 deletions(-) create mode 100644 testing/api/reports/report.json create mode 100644 testing/api/reports/report.pdf diff --git a/framework/python/src/api/api.py b/framework/python/src/api/api.py index e8e87465d..e52b18c77 100644 --- a/framework/python/src/api/api.py +++ b/framework/python/src/api/api.py @@ -383,7 +383,7 @@ async def delete_report(self, request: Request, response: Response): if len(body_raw) == 0: response.status_code = 400 - return self._generate_msg(False, "Invalid request received") + return self._generate_msg(False, "Invalid request received, missing body") try: body_json = json.loads(body_raw) @@ -395,12 +395,18 @@ async def delete_report(self, request: Request, response: Response): if "mac_addr" not in body_json or "timestamp" not in body_json: response.status_code = 400 - return self._generate_msg(False, "Invalid request received") + return self._generate_msg(False, "Missing mac address or timestamp") mac_addr = body_json.get("mac_addr").lower() timestamp = body_json.get("timestamp") - parsed_timestamp = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S") - timestamp_formatted = parsed_timestamp.strftime("%Y-%m-%dT%H:%M:%S") + + try: + parsed_timestamp = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S") + timestamp_formatted = parsed_timestamp.strftime("%Y-%m-%dT%H:%M:%S") + + except ValueError: + response.status_code = 400 + return self._generate_msg(False, "Incorrect timestamp format") # Get device from MAC address device = self._session.get_device(mac_addr) @@ -591,6 +597,12 @@ async def edit_device(self, request: Request, response: Response): async def get_report(self, response: Response, device_name, timestamp): device = self._session.get_device_by_name(device_name) + # If the device not found + if device is None: + LOGGER.info("Device not found, returning 404") + response.status_code = 404 + return self._generate_msg(False, "Device not found") + # 1.3 file path file_path = os.path.join( DEVICES_PATH, @@ -644,16 +656,34 @@ async def get_results(self, request: Request, response: Response, device_name, return self._generate_msg(False, "A device with that name could not be found") - file_path = self._get_test_run().get_test_orc().zip_results( + # Check if report exists (1.3 file path) + report_file_path = os.path.join( + DEVICES_PATH, + device_name, + "reports", + timestamp,"test", + device.mac_addr.replace(":","")) + + if not os.path.isdir(report_file_path): + # pre 1.3 file path + report_file_path = os.path.join(DEVICES_PATH, device_name, "reports", + timestamp) + + if not os.path.isdir(report_file_path): + LOGGER.info("Report could not be found, returning 404") + response.status_code = 404 + return self._generate_msg(False, "Report could not be found") + + zip_file_path = self._get_test_run().get_test_orc().zip_results( device, timestamp, profile) - if file_path is None: + if zip_file_path is None: response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR return self._generate_msg( False, "An error occurred whilst archiving test results") - if os.path.isfile(file_path): - return FileResponse(file_path) + if os.path.isfile(zip_file_path): + return FileResponse(zip_file_path) else: LOGGER.info("Test results could not be found, returning 404") response.status_code = 404 diff --git a/testing/api/reports/report.json b/testing/api/reports/report.json new file mode 100644 index 000000000..60e356e4c --- /dev/null +++ b/testing/api/reports/report.json @@ -0,0 +1,267 @@ +{ + + "testrun": { + + "version": "1.3.1" + + }, + + "mac_addr": null, + + "device": { + + "mac_addr": "00:1e:42:35:73:c4", + + "manufacturer": "Teltonika", + + "model": "TRB140", + + "firmware": "1.2.3", + + "test_modules": { + + "protocol": { + + "enabled": true + + }, + + "services": { + + "enabled": false + + }, + + "connection": { + + "enabled": false + + }, + + "tls": { + + "enabled": true + + }, + + "ntp": { + + "enabled": true + + }, + + "dns": { + + "enabled": true + + } + + } + + }, + + "status": "Non-Compliant", + + "started": "2024-08-05 13:37:53", + + "finished": "2024-08-05 13:39:35", + + "tests": { + + "total": 12, + + "results": [ + + { + + "name": "protocol.valid_bacnet", + + "description": "BACnet device could not be discovered", + + "expected_behavior": "BACnet traffic can be seen on the network and packets are valid and not malformed", + + "required_result": "Recommended", + + "result": "Feature Not Detected" + + }, + + { + + "name": "protocol.bacnet.version", + + "description": "Device did not respond to BACnet discovery", + + "expected_behavior": "The BACnet client implements an up to date version of BACnet", + + "required_result": "Recommended", + + "result": "Feature Not Detected" + + }, + + { + + "name": "protocol.valid_modbus", + + "description": "Device did not respond to Modbus connection", + + "expected_behavior": "Any Modbus functionality works as expected and valid Modbus traffic can be observed", + + "required_result": "Recommended", + + "result": "Feature Not Detected" + + }, + + { + + "name": "security.tls.v1_2_server", + + "description": "TLS 1.2 certificate is invalid", + + "expected_behavior": "TLS 1.2 certificate is issued to the web browser client when accessed", + + "required_result": "Required if Applicable", + + "result": "Non-Compliant", + + "recommendations": [ + + "Enable TLS 1.2 support in the web server configuration", + + "Disable TLS 1.0 and 1.1", + + "Sign the certificate used by the web server" + + ] + + }, + + { + + "name": "security.tls.v1_2_client", + + "description": "TLS 1.2 client connections valid", + + "expected_behavior": "The packet indicates a TLS connection with at least TLS 1.2 and support for ECDH and ECDSA ciphers", + + "required_result": "Required if Applicable", + + "result": "Compliant" + + }, + + { + + "name": "security.tls.v1_3_server", + + "description": "TLS 1.3 certificate is invalid", + + "expected_behavior": "TLS 1.3 certificate is issued to the web browser client when accessed", + + "required_result": "Informational", + + "result": "Informational" + + }, + + { + + "name": "security.tls.v1_3_client", + + "description": "TLS 1.3 client connections valid", + + "expected_behavior": "The packet indicates a TLS connection with at least TLS 1.3", + + "required_result": "Informational", + + "result": "Informational" + + }, + + { + + "name": "ntp.network.ntp_support", + + "description": "Device sent NTPv3 packets. NTPv3 is not allowed", + + "expected_behavior": "The device sends an NTPv4 request to the configured NTP server.", + + "required_result": "Required", + + "result": "Non-Compliant", + + "recommendations": [ + + "Set the NTP version to v4 in the NTP client", + + "Install an NTP client that supports NTPv4" + + ] + + }, + + { + + "name": "ntp.network.ntp_dhcp", + + "description": "Device sent NTP request to non-DHCP provided server", + + "expected_behavior": "Device can accept NTP server address, provided by the DHCP server (DHCP OFFER PACKET)", + + "required_result": "Roadmap", + + "result": "Feature Not Detected" + + }, + + { + + "name": "dns.network.hostname_resolution", + + "description": "DNS traffic detected from device", + + "expected_behavior": "The device sends DNS requests.", + + "required_result": "Required", + + "result": "Compliant" + + }, + + { + + "name": "dns.network.from_dhcp", + + "description": "DNS traffic detected only to DHCP provided server", + + "expected_behavior": "The device sends DNS requests to the DNS server provided by the DHCP server", + + "required_result": "Informational", + + "result": "Informational" + + }, + + { + + "name": "dns.mdns", + + "description": "No MDNS traffic detected from the device", + + "expected_behavior": "Device may send MDNS requests", + + "required_result": "Informational", + + "result": "Informational" + + } + + ] + + }, + + "report": "http://localhost:8000/report/Teltonika TRB140/2024-08-05T13:37:53" + + } \ No newline at end of file diff --git a/testing/api/reports/report.pdf b/testing/api/reports/report.pdf new file mode 100644 index 0000000000000000000000000000000000000000..0e449f19679ca495eadec409fd554cd8a40a631a GIT binary patch literal 29659 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 Ang9R* literal 0 HcmV?d00001 diff --git a/testing/api/test_api.py b/testing/api/test_api.py index 70c1a617f..9a9c33198 100644 --- a/testing/api/test_api.py +++ b/testing/api/test_api.py @@ -41,6 +41,7 @@ SYSTEM_CONFIG_PATH = "local/system.json" SYSTEM_CONFIG_RESTORE_PATH = "testing/api/system.json" PROFILES_PATH = "testing/api/profiles" +REPORTS_PATH = "testing/api/reports" BASELINE_MAC_ADDR = "02:42:aa:00:01:01" ALL_MAC_ADDR = "02:42:aa:00:00:01" @@ -110,7 +111,13 @@ def load_json(file_name, directory): @pytest.fixture def empty_devices_dir(): - """ Use e,pty devices directory """ + """ Fixture to empty devices directory """ + # Empty the directory before the test + local_delete_devices(ALL_DEVICES) + + yield + + # Empty the directory after the test local_delete_devices(ALL_DEVICES) @pytest.fixture @@ -726,8 +733,72 @@ def test_system_latest_version(testrun): # pylint: disable=W0613 # Tests for reports endpoints +@pytest.fixture +def create_report_folder(): # pylint: disable=W0613 + """Fixture to create the report.json and report.pdf in the expected path""" + def _create_report_folder(device_name, mac_address, timestamp): + + # Create the device folder path + main_folder = Path(DEVICES_DIRECTORY) / device_name + + # Remove the ":" from mac address for the folder structure + mac_address = mac_address.replace(":", "") + + # Change the timestamp format for the folder structure + timestamp = timestamp.replace(" ", "T") + + # Create the report folder path + report_folder = (main_folder / "reports" / timestamp / + "test" / mac_address) + + # Ensure the report folder exists + report_folder.mkdir(parents=True, exist_ok=True) + + # Define the source and target paths for the report.json + json_source_report_path = Path(REPORTS_PATH) / "report.json" + json_target_report_path = report_folder / "report.json" + + # Define the source and target paths for the report.pdf + pdf_source_report_path = Path(REPORTS_PATH) / "report.pdf" + pdf_target_report_path = report_folder / "report.pdf" + + # Copy the report.json file from the source to the target location + shutil.copy(json_source_report_path, json_target_report_path) + + # Copy the report.json file from the source to the target location + shutil.copy(pdf_source_report_path, pdf_target_report_path) + + return report_folder + + return _create_report_folder + +def create_and_get_device(): + """Utility method to create a device""" + + # Payload with device details + device = { + "mac_addr": "00:1e:42:28:9e:4a", + "manufacturer": "Teltonika", + "model": "TRB140" + } + + # Send a POST request to create a new device + requests.post(f"{API}/device", data=json.dumps(device), timeout=5) + + # Send the GET request to retrieve the created device's folder name + r = requests.get(f"{API}/devices", timeout=5) + + # Return the parsed the json response + return r.json() + +@pytest.fixture() +def add_device(): + """Fixture to add device during tests""" + # Returning the reference to create_device + return create_and_get_device + def test_get_reports_no_reports(testrun): # pylint: disable=W0613 - """Test get reports when no reports exist.""" + """Test get reports when no reports exist""" # Send a GET request to the /reports endpoint r = requests.get(f"{API}/reports", timeout=5) @@ -744,6 +815,432 @@ def test_get_reports_no_reports(testrun): # pylint: disable=W0613 # Check if the response is an empty list assert response == [] +def test_delete_report_success(testrun, empty_devices_dir, # pylint: disable=W0613 + add_device, create_report_folder): + """Test for succesfully delete a report (200)""" + + # Load a device using the add_device fixture + device = add_device() + + # Assign the device name + device_name = device[0]["device_folder"] + + # Assign the device name + mac_address = device[0]["mac_addr"] + + # Assign the timestamp format for the folder structure + timestamp = "2024-01-01 00:00:00" + + # Create the report directory + report_folder = create_report_folder(device_name, mac_address, timestamp) + + # Payload + delete_data = { + "mac_addr": mac_address, + "timestamp": timestamp + } + + # Send a DELETE request to remove the report + r = requests.delete(f"{API}/report", data=json.dumps(delete_data), timeout=5) + + # Check if status code is 200 (OK) + assert r.status_code == 200 + + # Parse the json response + response = r.json() + + # Check if "success" in response + assert "success" in response + + # Check if report folder has been deleted + assert not report_folder.exists() + +def test_delete_report_no_payload(testrun, empty_devices_dir, # pylint: disable=W0613 + add_device, create_report_folder): # pylint: disable=W0613 + """Test delete report bad request when the payload is missing (400)""" + + # Send a DELETE request to remove the report without the payload + r = requests.delete(f"{API}/report", timeout=5) + + # Check if status code is 400 (bad request) + assert r.status_code == 400 + + # Parse the json response + response = r.json() + + # Check if "error" in response + assert "error" in response + + # Check if the correct error message returned + assert "Invalid request received, missing body" in response["error"] + +def test_delete_report_invalid_payload(testrun, empty_devices_dir, # pylint: disable=W0613 + add_device, create_report_folder): + """Test delete report bad request, mac addr and timestamp are missing (400)""" + + # Load a device using the add_device fixture + device = add_device() + + # Assign the device name + device_name = device[0]["device_folder"] + + # Assign the device name + mac_address = device[0]["mac_addr"] + + # Assign the timestamp format for the folder structure + timestamp = "2024-01-01 00:00:00" + + # Create the report directory + create_report_folder(device_name, mac_address, timestamp) + + # Empty payload + delete_data = {} + + # Send a DELETE request to remove the report + r = requests.delete(f"{API}/report", data=json.dumps(delete_data), timeout=5) + + # Check if status code is 400 (bad request) + assert r.status_code == 400 + + # Parse the json response + response = r.json() + + # Check if "error" in response + assert "error" in response + + # Check if the correct error message returned + assert "Missing mac address or timestamp" in response["error"] + +def test_delete_report_incorrect_timestamp_format(testrun, empty_devices_dir, # pylint: disable=W0613 + add_device, create_report_folder): + """Test delete report bad request when timestamp format is incorrect (400)""" + + # Load a device using the add_device fixture + device = add_device() + + # Assign the device name + device_name = device[0]["device_folder"] + + # Assign the device name + mac_address = device[0]["mac_addr"] + + # Assign the incorrect timestamp format + timestamp = "2024-01-01 incorrect" + + # Create the report.json + create_report_folder(device_name, mac_address, timestamp) + + # Payload + delete_data = { + "mac_addr": mac_address, + "timestamp": timestamp + } + + # Send a DELETE request to remove the report + r = requests.delete(f"{API}/report", data=json.dumps(delete_data), timeout=5) + + # Check if status code is 400 (bad request) + assert r.status_code == 400 + + # Parse the json response + response = r.json() + + # Check if "error" in response + assert "error" in response + + # Check if the correct error message returned + assert "Incorrect timestamp format" in response["error"] + +def test_delete_report_no_report(testrun, empty_devices_dir): # pylint: disable=W0613 + """Test delete report when report does not exist (404)""" + + # Payload to be deleted for a non existing device + delete_data = { + "mac_addr": "00:1e:42:35:73:c4", + "timestamp": "2024-01-01 00:00:00" + } + + # Send the delete request to the endpoint + r = requests.delete(f"{API}/report", data=json.dumps(delete_data), timeout=5) + + # Check if status is 404 (not found) + assert r.status_code == 404 + + # Parse the response json + response = r.json() + + # Check if "error" in response + assert "error" in response + +def test_delete_report_server_error(testrun, empty_devices_dir, # pylint: disable=W0613 + add_device, create_report_folder): + """Test for delete report causing internal server error (500)""" + + # Load a device using the add_device fixture + device = add_device() + + # Assign the device name + device_name = device[0]["device_folder"] + + # Assign the device name + mac_address = device[0]["mac_addr"] + + # Assign the timestamp format for the folder structure + timestamp = "2024-01-01 00:00:00" + + # Create the report folder and JSON file + create_report_folder(device_name, mac_address, timestamp) + + # Construct the device report path + device_directory = os.path.join(DEVICES_DIRECTORY, device_name) + + # Remove the folder and all its content before delete request + shutil.rmtree(device_directory) + + # Prepare the payload for the DELETE request + delete_data = { + "mac_addr": mac_address, + "timestamp": timestamp + } + + # Send the delete request to delete the report + r = requests.delete(f"{API}/report", + data=json.dumps(delete_data), + timeout=5) + + # Check if status code is 500 (Internal Server Error) + assert r.status_code == 500 + + # Parse the JSON response + response = r.json() + + # Check if error is present in the response + assert "error" in response + + # Check if the correct error message is returned + assert "Error occured whilst deleting report" in response["error"] + +def test_get_report_success(testrun, empty_devices_dir, # pylint: disable=W0613 + add_device, create_report_folder): + """Test get report when report exists (200)""" + + # Load a device using the add_device fixture + device = add_device() + + # Assign the device name + device_name = device[0]["device_folder"] + + # Assign the device name + mac_address = device[0]["mac_addr"] + + # Assign the timestamp format + timestamp = "2024-01-01T00:00:00" + + # Create the report for the device + create_report_folder(device_name, mac_address, timestamp) + + # Send the get request + r = requests.get(f"{API}/report/{device_name}/{timestamp}", timeout=5) + + # Check if status code is 200 (ok) + assert r.status_code == 200 + + # Check if the response is a PDF + assert r.headers["Content-Type"] == "application/pdf" + +def test_get_report_not_found(testrun, empty_devices_dir, add_device): # pylint: disable=W0613 + """Test get report when report doesn't exist (404)""" + + # Assign timestamp + timestamp = "2024-01-01T00:00:00" + + # Load a device using the add_device fixture + device = add_device() + + # Assign the device name + device_name = device[0]["device_folder"] + + # Send the get request + r = requests.get(f"{API}/report/{device_name}/{timestamp}", timeout=5) + + # Check if status code is 404 (not found) + assert r.status_code == 404 + + # Parse the response json + response = r.json() + + # Check if "error" in response + assert "error" in response + + # Check if the correct error message returned + assert "Report could not be found" in response["error"] + +def test_get_report_device_not_found(empty_devices_dir, testrun): # pylint: disable=W0613 + """Test getting a report when the device is not found (404)""" + + # Assign device name and timestamp + device_name = "nonexistent_device" + timestamp = "2024-01-01T00:00:00" + + # Send the get request + r = requests.get(f"{API}/report/{device_name}/{timestamp}", timeout=5) + + # Check if is 404 (not found) + assert r.status_code == 404 + + # Parse the json response + response = r.json() + + # Check if "error" in response + assert "error" in response + + # Check if the correct error message is returned + assert "Device not found" in response["error"] + +def test_export_report_device_not_found(testrun, empty_devices_dir, # pylint: disable=W0613 + create_report_folder): + """Test for export the report result when the device could not be found""" + + # Assign the non-existing device name, mac_address and timestamp + device_name = "non existing device" + mac_address = "00:1e:42:35:73:c4" + timestamp = "2024-01-01T00:00:00" + + # Create the report for the non-existing device + create_report_folder(device_name, mac_address, timestamp) + + # Send the post request + r = requests.post(f"{API}/export/{device_name}/{timestamp}", timeout=5) + + # Check if is 404 (not found) + assert r.status_code == 404 + + # Parse the json response + response = r.json() + + # Check if "error" in response + assert "error" in response + + # Check if the correct error message returned + assert "A device with that name could not be found" in response["error"] + +def test_export_report_profile_not_found(testrun, empty_devices_dir, # pylint: disable=W0613 + add_device, create_report_folder): + """Test for export report result when the profile is not found""" + + # Load a device using the add_device fixture + device = add_device() + + # Assign the device name and mac address + device_name = device[0]["device_folder"] + mac_address = device[0]["mac_addr"] + + # Set the timestamp + timestamp = "2024-01-01T00:00:00" + + # Create the report for the device + create_report_folder(device_name, mac_address, timestamp) + + # Add a non existing profile into the payload + payload = {"profile": "non_existent_profile"} + + # Send the post request + r = requests.post(f"{API}/export/{device_name}/{timestamp}", + json=payload, + timeout=5) + + # Check if is 404 (not found) + assert r.status_code == 404 + + # Parse the json response + response = r.json() + + # Check if "error" in response + assert "error" in response + + # Check if the correct error message returned + assert "A profile with that name could not be found" in response["error"] + +def test_export_report_not_found(testrun, empty_devices_dir, add_device): # pylint: disable=W0613 + """Test for export the report result when the report could not be found""" + + # Load a device using the add_device fixture + device = add_device() + + # Assign the device name and mac address + device_name = device[0]["device_folder"] + + # Set the timestamp + timestamp = "2024-01-01T00:00:00" + + # Send the post request to trigger the zipping process + r = requests.post(f"{API}/export/{device_name}/{timestamp}", timeout=10) + + # Check if status code is 500 (Internal Server Error) + assert r.status_code == 404 + + # Parse the json response + response = r.json() + + # Check if "error" in response + assert "error" in response + + # Check if the correct error message is returned + assert "Report could not be found" in response["error"] + +def test_export_report_with_profile(testrun, empty_devices_dir, add_device, # pylint: disable=W0613 + create_report_folder, reset_profiles, add_profile): # pylint: disable=W0613 + """Test export results with existing profile when report exists (200)""" + + # Create a profile using the add_profile fixture + profile = add_profile("new_profile.json") + + # Load a device using the add_device fixture + device = add_device() + + # Assign the device name, mac address and timestamp + device_name = device[0]["device_folder"] + mac_address = device[0]["mac_addr"] + timestamp = "2024-01-01T00:00:00" + + # Create the report for the device + create_report_folder(device_name, mac_address, timestamp) + + # Send the post request + r = requests.post(f"{API}/export/{device_name}/{timestamp}", + json=profile, + timeout=5) + + # Check if status code is 200 (OK) + assert r.status_code == 200 + + # Check if the response is a zip file + assert r.headers["Content-Type"] == "application/zip" + +def test_export_results_with_no_profile(testrun, empty_devices_dir, # pylint: disable=W0613 + add_device, create_report_folder): + """Test export results with no profile when report exists (200)""" + + # Load a device using the add_device fixture + device = add_device() + + # Assign the device name, mac address and timestamp + device_name = device[0]["device_folder"] + mac_address = device[0]["mac_addr"] + timestamp = "2024-01-01T00:00:00" + + # Create the report for the device + create_report_folder(device_name, mac_address, timestamp) + + # Send the post request + r = requests.post(f"{API}/export/{device_name}/{timestamp}", timeout=5) + + # Check if status code is 200 (OK) + assert r.status_code == 200 + + # Check if the response is a zip file + assert r.headers["Content-Type"] == "application/zip" + # Tests for device endpoints @pytest.mark.skip() From 17983922879840ecc797566f184a80cb2ed84826 Mon Sep 17 00:00:00 2001 From: MariusBalovin Date: Wed, 14 Aug 2024 15:36:21 +0100 Subject: [PATCH 02/13] added tests for certificate endpoints --- testing/api/certificates/WR2.pem | 29 ++ testing/api/certificates/crt.pem | 31 ++ testing/api/certificates/invalid.pem | 1 + .../certificates/invalidname1234567891234.pem | 31 ++ testing/api/test_api.py | 371 ++++++++++++++++++ 5 files changed, 463 insertions(+) create mode 100644 testing/api/certificates/WR2.pem create mode 100644 testing/api/certificates/crt.pem create mode 100644 testing/api/certificates/invalid.pem create mode 100644 testing/api/certificates/invalidname1234567891234.pem diff --git a/testing/api/certificates/WR2.pem b/testing/api/certificates/WR2.pem new file mode 100644 index 000000000..f82f4d12d --- /dev/null +++ b/testing/api/certificates/WR2.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFCzCCAvOgAwIBAgIQf/AFoHxM3tEArZ1mpRB7mDANBgkqhkiG9w0BAQsFADBH +MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM +QzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMjMxMjEzMDkwMDAwWhcNMjkwMjIw +MTQwMDAwWjA7MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNl +cnZpY2VzMQwwCgYDVQQDEwNXUjIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCp/5x/RR5wqFOfytnlDd5GV1d9vI+aWqxG8YSau5HbyfsvAfuSCQAWXqAc ++MGr+XgvSszYhaLYWTwO0xj7sfUkDSbutltkdnwUxy96zqhMt/TZCPzfhyM1IKji +aeKMTj+xWfpgoh6zySBTGYLKNlNtYE3pAJH8do1cCA8Kwtzxc2vFE24KT3rC8gIc +LrRjg9ox9i11MLL7q8Ju26nADrn5Z9TDJVd06wW06Y613ijNzHoU5HEDy01hLmFX +xRmpC5iEGuh5KdmyjS//V2pm4M6rlagplmNwEmceOuHbsCFx13ye/aoXbv4r+zgX +FNFmp6+atXDMyGOBOozAKql2N87jAgMBAAGjgf4wgfswDgYDVR0PAQH/BAQDAgGG +MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/ +AgEAMB0GA1UdDgQWBBTeGx7teRXUPjckwyG77DQ5bUKyMDAfBgNVHSMEGDAWgBTk +rysmcRorSCeFL1JmLO/wiRNxPjA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAKG +GGh0dHA6Ly9pLnBraS5nb29nL3IxLmNydDArBgNVHR8EJDAiMCCgHqAchhpodHRw +Oi8vYy5wa2kuZ29vZy9yL3IxLmNybDATBgNVHSAEDDAKMAgGBmeBDAECATANBgkq +hkiG9w0BAQsFAAOCAgEARXWL5R87RBOWGqtY8TXJbz3S0DNKhjO6V1FP7sQ02hYS +TL8Tnw3UVOlIecAwPJQl8hr0ujKUtjNyC4XuCRElNJThb0Lbgpt7fyqaqf9/qdLe +SiDLs/sDA7j4BwXaWZIvGEaYzq9yviQmsR4ATb0IrZNBRAq7x9UBhb+TV+PfdBJT +DhEl05vc3ssnbrPCuTNiOcLgNeFbpwkuGcuRKnZc8d/KI4RApW//mkHgte8y0YWu +ryUJ8GLFbsLIbjL9uNrizkqRSvOFVU6xddZIMy9vhNkSXJ/UcZhjJY1pXAprffJB +vei7j+Qi151lRehMCofa6WBmiA4fx+FOVsV2/7R6V2nyAiIJJkEd2nSi5SnzxJrl +Xdaqev3htytmOPvoKWa676ATL/hzfvDaQBEcXd2Ppvy+275W+DKcH0FBbX62xevG +iza3F4ydzxl6NJ8hk8R+dDXSqv1MbRT1ybB5W0k8878XSOjvmiYTDIfyc9acxVJr +Y/cykHipa+te1pOhv7wYPYtZ9orGBV5SGOJm4NrB3K1aJar0RfzxC3ikr7Dyc6Qw +qDTBU39CluVIQeuQRgwG3MuSxl7zRERDRilGoKb8uY45JzmxWuKxrfwT/478JuHU +/oTxUFqOl2stKnn7QGTq8z29W+GgBLCXSBxC9epaHM0myFH/FJlniXJfHeytWt0= +-----END CERTIFICATE----- diff --git a/testing/api/certificates/crt.pem b/testing/api/certificates/crt.pem new file mode 100644 index 000000000..410b1f104 --- /dev/null +++ b/testing/api/certificates/crt.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo +27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w +Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw +TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl +qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH +szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 +Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk +MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p +aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN +VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb +C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe +QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy +h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 +7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J +ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef +MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ +Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT +6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ +0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm +2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb +bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c +-----END CERTIFICATE----- diff --git a/testing/api/certificates/invalid.pem b/testing/api/certificates/invalid.pem new file mode 100644 index 000000000..d3f5a12fa --- /dev/null +++ b/testing/api/certificates/invalid.pem @@ -0,0 +1 @@ + diff --git a/testing/api/certificates/invalidname1234567891234.pem b/testing/api/certificates/invalidname1234567891234.pem new file mode 100644 index 000000000..410b1f104 --- /dev/null +++ b/testing/api/certificates/invalidname1234567891234.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo +27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w +Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw +TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl +qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH +szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 +Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk +MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p +aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN +VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb +C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe +QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy +h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 +7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J +ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef +MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ +Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT +6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ +0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm +2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb +bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c +-----END CERTIFICATE----- diff --git a/testing/api/test_api.py b/testing/api/test_api.py index 9a9c33198..e78c992e9 100644 --- a/testing/api/test_api.py +++ b/testing/api/test_api.py @@ -39,7 +39,10 @@ TESTING_DEVICES = "../device_configs" PROFILES_DIRECTORY = "local/risk_profiles" SYSTEM_CONFIG_PATH = "local/system.json" +CERTS_DIRECTORY = "local/root_certs" + SYSTEM_CONFIG_RESTORE_PATH = "testing/api/system.json" +CERTS_PATH = "testing/api/certificates" PROFILES_PATH = "testing/api/profiles" REPORTS_PATH = "testing/api/reports" @@ -1889,7 +1892,375 @@ def test_get_test_modules(testrun): # pylint: disable=W0613 # Check if the response is a list assert isinstance(response, list) +# Tests for certificates endpoints + +def delete_all_certs(): + """Utility method to delete all certificates from root_certs folder""" + + # Assign the certificates directory + certs_path = Path(CERTS_DIRECTORY) + + try: + # Check if the profile_path (local/root_certs) exists and is a folder + if certs_path.exists() and certs_path.is_dir(): + + # Iterate over all certificates from root_certs folder + for item in certs_path.iterdir(): + + # Check if item is a file + if item.is_file(): + + #If True remove file + item.unlink() + + else: + # If item is a folder remove it + shutil.rmtree(item) + + except PermissionError: + # Permission related issues + print(f"Permission Denied: {item}") + + except OSError as err: + # System related issues + print(f"Error removing {item}: {err}") + +def load_certificate_file(cert_filename): + """Utility method to load a certificate file in binary read mode.""" + + # Construct the full file path + cert_path = os.path.join(CERTS_PATH, cert_filename) + + # Open the certificate file in binary read mode + with open(cert_path, "rb") as cert_file: + + # Return the certificate file + return cert_file.read() + +def upload_cert(filename): + """Utility method to upload a certificate""" + + # Load the certificate using the utility method + cert_file = load_certificate_file(filename) + + # Send a POST request to the API endpoint to upload the certificate + response = requests.post( + f"{API}/system/config/certs", + files={"file": (filename, cert_file, "application/x-x509-ca-cert")}, + timeout=5) + + # Return the response + return response + +@pytest.fixture() +def reset_certs(): + """Delete the certificates before and after each test""" + + # Delete before the test + delete_all_certs() + + yield + + # Delete after the test + delete_all_certs() + +@pytest.fixture() +def add_cert(): + """Fixture to upload certificates during tests.""" + + # Returning the reference to upload_certificate + return upload_cert + +def test_get_certificates_no_certificates(testrun, reset_certs): # pylint: disable=W0613 + """Test for get certificates when no certificates have been uploaded""" + + # Send the get request to "/system/config/certs" endpoint + r = requests.get(f"{API}/system/config/certs", timeout=5) + + # Check if status code is 200 (OK) + assert r.status_code == 200 + + # Parse the response (certificates) + response = r.json() + + # Check if response is a list + assert isinstance(response, list) + + # Check if the list is empty + assert len(response) == 0 + +def test_get_certificates(testrun, reset_certs, add_cert): # pylint: disable=W0613 + """Test for get certificates when two certificates have been uploaded""" + + # Use add_cert fixture to upload the first certificate + add_cert("crt.pem") + + # Send the get request to "/system/config/certs" endpoint + r = requests.get(f"{API}/system/config/certs", timeout=5) + + # Check if status code is 200 (OK) + assert r.status_code == 200 + + # Parse the response (certificates) + response = r.json() + + # Check if response is a list + assert isinstance(response, list) + + # Check if response contains one certificate + assert len(response) == 1 + + # Use add_cert fixture to upload the second certificate + add_cert("WR2.pem") + + # Send the get request to "/system/config/certs" endpoint + r = requests.get(f"{API}/system/config/certs", timeout=5) + + # Check if status code is 200 (OK) + assert r.status_code == 200 + + # Parse the response (certificates) + response = r.json() + + # Check if response is a list + assert isinstance(response, list) + + # Check if response contains two certificates + assert len(response) == 2 + +def test_upload_certificate(testrun, reset_certs): # pylint: disable=W0613 + """Test for upload certificate successfully""" + + # Load the first certificate file content using the utility method + cert_file = load_certificate_file("crt.pem") + + # Send a POST request to the API endpoint to upload the certificate + r = requests.post( + f"{API}/system/config/certs", + files={"file": ("crt.pem", cert_file, "application/x-x509-ca-cert")}, + timeout=5 + ) + + # Check if status code is 201 (Created) + assert r.status_code == 201 + + # Parse the response + response = r.json() + + # Check if 'filename' field is in the response + assert "filename" in response + + # Check if the certificate filename is 'crt.pem' + assert response["filename"] == "crt.pem" + + # Load the second certificate file using the utility method + cert_file = load_certificate_file("WR2.pem") + + # Send a POST request to the API endpoint to upload the second certificate + r = requests.post( + f"{API}/system/config/certs", + files={"file": ("WR2.pem", cert_file, "application/x-x509-ca-cert")}, + timeout=5 + ) + + # Check if status code is 201 (Created) + assert r.status_code == 201 + + # Parse the response + response = r.json() + + # Check if 'filename' field is in the response + assert "filename" in response + + # Check if the certificate filename is 'WR2.pem' + assert response["filename"] == "WR2.pem" + + # Send get request to check that the certificates are listed + r = requests.get(f"{API}/system/config/certs", timeout=5) + + # Parse the response + response = r.json() + + # Check if "crt.pem" exists + assert any(cert["filename"] == "crt.pem" for cert in response) + + # Check if "WR2.pem" exists + assert any(cert["filename"] == "WR2.pem" for cert in response) + +def test_upload_invalid_certificate_format(testrun, reset_certs): # pylint: disable=W0613 + """Test for upload an invalid certificate format """ + + # Load the first certificate file content using the utility method + cert_file = load_certificate_file("invalid.pem") + + # Send a POST request to the API endpoint to upload the certificate + r = requests.post( + f"{API}/system/config/certs", + files={"file": ("invalid.pem", cert_file, "application/x-x509-ca-cert")}, + timeout=5 + ) + + # Check if status code is 400 (bad request) + assert r.status_code == 400 + + # Parse the response + response = r.json() + + # Check if "error" key is in response + assert "error" in response + +def test_upload_invalid_certificate_name(testrun, reset_certs): # pylint: disable=W0613 + """Test for upload a valid certificate with invalid filename""" + + # Assign the certificate name to a variable + cert_name = "invalidname1234567891234.pem" + + # Load the first certificate file content using the utility method + cert_file = load_certificate_file(cert_name) + + # Send a POST request to the API endpoint to upload the certificate + r = requests.post( + f"{API}/system/config/certs", + files={"file": (cert_name, cert_file, "application/x-x509-ca-cert")}, + timeout=5 + ) + + # Check if status code is 400 (bad request) + assert r.status_code == 400 + + # Parse the response + response = r.json() + + # Check if "error" key is in response + assert "error" in response + +def test_upload_existing_certificate(testrun, reset_certs): # pylint: disable=W0613 + """Test for upload an existing certificate""" + + # Load the first certificate file content using the utility method + cert_file = load_certificate_file("crt.pem") + + # Send a POST request to the API endpoint to upload the certificate + r = requests.post( + f"{API}/system/config/certs", + files={"file": ("crt.pem", cert_file, "application/x-x509-ca-cert")}, + timeout=5 + ) + + # Check if status code is 201 (Created) + assert r.status_code == 201 + + # Parse the response + response = r.json() + + # Check if 'filename' field is in the response + assert "filename" in response + + # Check if the certificate name is 'crt.pem' + assert response["filename"] == "crt.pem" + + # Load the same certificate file content using the utility method + cert_file = load_certificate_file("crt.pem") + + # Send a POST request to the API endpoint to upload the second certificate + r = requests.post( + f"{API}/system/config/certs", + files={"file": ("crt.pem", cert_file, "application/x-x509-ca-cert")}, + timeout=5 + ) + + # Check if status code is 409 (conflict) + assert r.status_code == 409 + + # Parse the json response + response = r.json() + + # Check if "error" key is in response + assert "error" in response + +def test_delete_certificate_success(testrun, reset_certs, add_cert): # pylint: disable=W0613 + """Test for successfully deleting an existing certificate""" + + # Use the add_cert fixture to upload the first certificate + add_cert("crt.pem") + + # Retrieve the uploaded certificate's details + r = requests.get(f"{API}/system/config/certs", timeout=5) + + # Parse the json response + response = r.json() + + # Extract the name of the uploaded certificate + uploaded_cert = next( + (cert for cert in response if cert["filename"] == "crt.pem") + ) + + # Assign the certificate name + cert_name = uploaded_cert["name"] + + # Send delete certificate request + delete_payload = {"name": cert_name} + r = requests.delete(f"{API}/system/config/certs", + data=json.dumps(delete_payload), + timeout=5) + + # Check if status code is 200 (OK) + assert r.status_code == 200 + + # Send the get request to display all certificates + r = requests.get(f"{API}/system/config/certs", timeout=5) + + # Parse the json response + response = r.json() + + # Check that the certificate is no longer listed + assert not any(cert["filename"] == "crt.pem" for cert in response) + +def test_delete_certificate_bad_request(testrun, reset_certs, add_cert): # pylint: disable=W0613 + """Test for delete a certificate without providing the name""" + + # Use the add_cert fixture to upload the first certificate + add_cert("crt.pem") + + # Empty payload + delete_payload = {} + + # Send the delete request + r = requests.delete(f"{API}/system/config/certs", + data=json.dumps(delete_payload), + timeout=5) + + # Check if status code is 400 (Bad Request) + assert r.status_code == 400 + + # parse the json response + response = r.json() + + # Check if error in response + assert "error" in response + +def test_delete_certificate_not_found(testrun, reset_certs): # pylint: disable=W0613 + """Test for delete certificate when does not exist""" + + # Attempt to delete a certificate with a name that doesn't exist + delete_payload = {"name": "non_existing"} + + # Send the delete request + r = requests.delete(f"{API}/system/config/certs", + data=json.dumps(delete_payload), + timeout=5) + + # Check if status code is 404 (Not Found) + assert r.status_code == 404 + + # parse the json response + response = r.json() + + # Check if error in response + assert "error" in response + # Tests for profile endpoints + def delete_all_profiles(): """Utility method to delete all profiles from risk_profiles folder""" From 70aef9690ca115038d6c0254fdbdefdbd4ba8df3 Mon Sep 17 00:00:00 2001 From: MariusBalovin Date: Wed, 14 Aug 2024 16:13:48 +0100 Subject: [PATCH 03/13] fix report.json --- testing/api/reports/report.json | 397 +++++++++++--------------------- 1 file changed, 132 insertions(+), 265 deletions(-) diff --git a/testing/api/reports/report.json b/testing/api/reports/report.json index 60e356e4c..bd697654d 100644 --- a/testing/api/reports/report.json +++ b/testing/api/reports/report.json @@ -1,267 +1,134 @@ { - - "testrun": { - - "version": "1.3.1" - - }, - - "mac_addr": null, - - "device": { - - "mac_addr": "00:1e:42:35:73:c4", - - "manufacturer": "Teltonika", - - "model": "TRB140", - - "firmware": "1.2.3", - - "test_modules": { - - "protocol": { - - "enabled": true - - }, - - "services": { - - "enabled": false - - }, - - "connection": { - - "enabled": false - - }, - - "tls": { - - "enabled": true - - }, - - "ntp": { - - "enabled": true - - }, - - "dns": { - - "enabled": true - - } - + "testrun": { + "version": "1.3.1" + }, + "mac_addr": null, + "device": { + "mac_addr": "00:1e:42:35:73:c4", + "manufacturer": "Teltonika", + "model": "TRB140", + "firmware": "1.2.3", + "test_modules": { + "protocol": { + "enabled": true + }, + "services": { + "enabled": false + }, + "connection": { + "enabled": false + }, + "tls": { + "enabled": true + }, + "ntp": { + "enabled": true + }, + "dns": { + "enabled": true } - - }, - - "status": "Non-Compliant", - - "started": "2024-08-05 13:37:53", - - "finished": "2024-08-05 13:39:35", - - "tests": { - - "total": 12, - - "results": [ - - { - - "name": "protocol.valid_bacnet", - - "description": "BACnet device could not be discovered", - - "expected_behavior": "BACnet traffic can be seen on the network and packets are valid and not malformed", - - "required_result": "Recommended", - - "result": "Feature Not Detected" - - }, - - { - - "name": "protocol.bacnet.version", - - "description": "Device did not respond to BACnet discovery", - - "expected_behavior": "The BACnet client implements an up to date version of BACnet", - - "required_result": "Recommended", - - "result": "Feature Not Detected" - - }, - - { - - "name": "protocol.valid_modbus", - - "description": "Device did not respond to Modbus connection", - - "expected_behavior": "Any Modbus functionality works as expected and valid Modbus traffic can be observed", - - "required_result": "Recommended", - - "result": "Feature Not Detected" - - }, - - { - - "name": "security.tls.v1_2_server", - - "description": "TLS 1.2 certificate is invalid", - - "expected_behavior": "TLS 1.2 certificate is issued to the web browser client when accessed", - - "required_result": "Required if Applicable", - - "result": "Non-Compliant", - - "recommendations": [ - - "Enable TLS 1.2 support in the web server configuration", - - "Disable TLS 1.0 and 1.1", - - "Sign the certificate used by the web server" - - ] - - }, - - { - - "name": "security.tls.v1_2_client", - - "description": "TLS 1.2 client connections valid", - - "expected_behavior": "The packet indicates a TLS connection with at least TLS 1.2 and support for ECDH and ECDSA ciphers", - - "required_result": "Required if Applicable", - - "result": "Compliant" - - }, - - { - - "name": "security.tls.v1_3_server", - - "description": "TLS 1.3 certificate is invalid", - - "expected_behavior": "TLS 1.3 certificate is issued to the web browser client when accessed", - - "required_result": "Informational", - - "result": "Informational" - - }, - - { - - "name": "security.tls.v1_3_client", - - "description": "TLS 1.3 client connections valid", - - "expected_behavior": "The packet indicates a TLS connection with at least TLS 1.3", - - "required_result": "Informational", - - "result": "Informational" - - }, - - { - - "name": "ntp.network.ntp_support", - - "description": "Device sent NTPv3 packets. NTPv3 is not allowed", - - "expected_behavior": "The device sends an NTPv4 request to the configured NTP server.", - - "required_result": "Required", - - "result": "Non-Compliant", - - "recommendations": [ - - "Set the NTP version to v4 in the NTP client", - - "Install an NTP client that supports NTPv4" - - ] - - }, - - { - - "name": "ntp.network.ntp_dhcp", - - "description": "Device sent NTP request to non-DHCP provided server", - - "expected_behavior": "Device can accept NTP server address, provided by the DHCP server (DHCP OFFER PACKET)", - - "required_result": "Roadmap", - - "result": "Feature Not Detected" - - }, - - { - - "name": "dns.network.hostname_resolution", - - "description": "DNS traffic detected from device", - - "expected_behavior": "The device sends DNS requests.", - - "required_result": "Required", - - "result": "Compliant" - - }, - - { - - "name": "dns.network.from_dhcp", - - "description": "DNS traffic detected only to DHCP provided server", - - "expected_behavior": "The device sends DNS requests to the DNS server provided by the DHCP server", - - "required_result": "Informational", - - "result": "Informational" - - }, - - { - - "name": "dns.mdns", - - "description": "No MDNS traffic detected from the device", - - "expected_behavior": "Device may send MDNS requests", - - "required_result": "Informational", - - "result": "Informational" - - } - - ] - - }, - - "report": "http://localhost:8000/report/Teltonika TRB140/2024-08-05T13:37:53" - - } \ No newline at end of file + } + }, + "status": "Non-Compliant", + "started": "2024-08-05 13:37:53", + "finished": "2024-08-05 13:39:35", + "tests": { + "total": 12, + "results": [ + { + "name": "protocol.valid_bacnet", + "description": "BACnet device could not be discovered", + "expected_behavior": "BACnet traffic can be seen on the network and packets are valid and not malformed", + "required_result": "Recommended", + "result": "Feature Not Detected" + }, + { + "name": "protocol.bacnet.version", + "description": "Device did not respond to BACnet discovery", + "expected_behavior": "The BACnet client implements an up to date version of BACnet", + "required_result": "Recommended", + "result": "Feature Not Detected" + }, + { + "name": "protocol.valid_modbus", + "description": "Device did not respond to Modbus connection", + "expected_behavior": "Any Modbus functionality works as expected and valid Modbus traffic can be observed", + "required_result": "Recommended", + "result": "Feature Not Detected" + }, + { + "name": "security.tls.v1_2_server", + "description": "TLS 1.2 certificate is invalid", + "expected_behavior": "TLS 1.2 certificate is issued to the web browser client when accessed", + "required_result": "Required if Applicable", + "result": "Non-Compliant", + "recommendations": [ + "Enable TLS 1.2 support in the web server configuration", + "Disable TLS 1.0 and 1.1", + "Sign the certificate used by the web server" + ] + }, + { + "name": "security.tls.v1_2_client", + "description": "TLS 1.2 client connections valid", + "expected_behavior": "The packet indicates a TLS connection with at least TLS 1.2 and support for ECDH and ECDSA ciphers", + "required_result": "Required if Applicable", + "result": "Compliant" + }, + { + "name": "security.tls.v1_3_server", + "description": "TLS 1.3 certificate is invalid", + "expected_behavior": "TLS 1.3 certificate is issued to the web browser client when accessed", + "required_result": "Informational", + "result": "Informational" + }, + { + "name": "security.tls.v1_3_client", + "description": "TLS 1.3 client connections valid", + "expected_behavior": "The packet indicates a TLS connection with at least TLS 1.3", + "required_result": "Informational", + "result": "Informational" + }, + { + "name": "ntp.network.ntp_support", + "description": "Device sent NTPv3 packets. NTPv3 is not allowed", + "expected_behavior": "The device sends an NTPv4 request to the configured NTP server.", + "required_result": "Required", + "result": "Non-Compliant", + "recommendations": [ + "Set the NTP version to v4 in the NTP client", + "Install an NTP client that supports NTPv4" + ] + }, + { + "name": "ntp.network.ntp_dhcp", + "description": "Device sent NTP request to non-DHCP provided server", + "expected_behavior": "Device can accept NTP server address, provided by the DHCP server (DHCP OFFER PACKET)", + "required_result": "Roadmap", + "result": "Feature Not Detected" + }, + { + "name": "dns.network.hostname_resolution", + "description": "DNS traffic detected from device", + "expected_behavior": "The device sends DNS requests.", + "required_result": "Required", + "result": "Compliant" + }, + { + "name": "dns.network.from_dhcp", + "description": "DNS traffic detected only to DHCP provided server", + "expected_behavior": "The device sends DNS requests to the DNS server provided by the DHCP server", + "required_result": "Informational", + "result": "Informational" + }, + { + "name": "dns.mdns", + "description": "No MDNS traffic detected from the device", + "expected_behavior": "Device may send MDNS requests", + "required_result": "Informational", + "result": "Informational" + } + ] + }, + "report": "http://localhost:8000/report/Teltonika TRB140/2024-08-05T13:37:53" +} \ No newline at end of file From f925fb8068a8a3f1d2250771057c1eb67b32e0af Mon Sep 17 00:00:00 2001 From: MariusBalovin Date: Thu, 15 Aug 2024 10:48:09 +0100 Subject: [PATCH 04/13] removed responses library, not used in testing --- framework/requirements.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/framework/requirements.txt b/framework/requirements.txt index 0484905ee..402009ef9 100644 --- a/framework/requirements.txt +++ b/framework/requirements.txt @@ -21,8 +21,6 @@ pydantic==2.7.1 # Requirements for testing pytest==7.4.4 pytest-timeout==2.2.0 -responses==0.25.3 - # Requirements for the report markdown==3.5.2 From b94bb51d71fd9aa5712aaa22a9552728af8d552b Mon Sep 17 00:00:00 2001 From: MariusBalovin Date: Thu, 15 Aug 2024 10:50:10 +0100 Subject: [PATCH 05/13] updated the test for /system/version endpoint --- testing/api/test_api.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/testing/api/test_api.py b/testing/api/test_api.py index e78c992e9..90edb2ba6 100644 --- a/testing/api/test_api.py +++ b/testing/api/test_api.py @@ -29,7 +29,6 @@ import pytest import requests - ALL_DEVICES = "*" API = "http://127.0.0.1:8000" LOG_PATH = "/tmp/testrun.log" @@ -720,19 +719,30 @@ def test_system_shutdown_in_progress(testrun): # pylint: disable=W0613 # Check if the response status code is 400 (test in progress) assert r.status_code == 400 -def test_system_latest_version(testrun): # pylint: disable=W0613 - """Test for testrun version when the latest version is installed""" +def test_system_version(testrun): # pylint: disable=W0613 + """Test for testrun version endpoint""" # Send the get request to the API r = requests.get(f"{API}/system/version", timeout=5) + # Check if status code is 200 (ok) + assert r.status_code == 200 + # Parse the response response = r.json() - # Check if status code is 200 (update available) - assert r.status_code == 200 - # Check if an update is available - assert response["update_available"] is False + # Assign the json keys to a list + version_json = ["installed_version", + "update_available", + "latest_version", + "latest_version_url"] + + # Check if all the keys are in json response + for key in version_json: + assert key in response + + # Check if the update_available is a boolean + assert isinstance(response["update_available"], bool) # Tests for reports endpoints @@ -1582,7 +1592,7 @@ def test_start_device_not_found(empty_devices_dir, # pylint: disable=W0613 assert r.status_code == 200 payload = {"device": {"mac_addr": device_1["mac_addr"], "firmware": "asd"}} - r = requests.post(f"{API}/system/start", + r = requests.post(f"{API}/`system/start`", data=json.dumps(payload), timeout=10) assert r.status_code == 404 From 3eb3fe63c1e4b952217ee825f08317b9b1c32a4d Mon Sep 17 00:00:00 2001 From: MariusBalovin Date: Thu, 15 Aug 2024 15:26:48 +0100 Subject: [PATCH 06/13] fix pylint error --- framework/python/src/api/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/python/src/api/api.py b/framework/python/src/api/api.py index 472432ab5..7aec3288b 100644 --- a/framework/python/src/api/api.py +++ b/framework/python/src/api/api.py @@ -383,7 +383,7 @@ async def delete_report(self, request: Request, response: Response): if len(body_raw) == 0: response.status_code = 400 - return self._generate_msg(False, "Invalid request received, missing body") + return self._generate_msg(False, "Invalid request received, missing body") try: body_json = json.loads(body_raw) From 2f7a91235b23b5ce289cd8653140f79fe25bf29d Mon Sep 17 00:00:00 2001 From: MariusBalovin Date: Thu, 15 Aug 2024 16:32:16 +0100 Subject: [PATCH 07/13] improved the code structure --- testing/api/test_api.py | 200 ++++++++++++++++++++++------------------ 1 file changed, 112 insertions(+), 88 deletions(-) diff --git a/testing/api/test_api.py b/testing/api/test_api.py index 90edb2ba6..9e49d41e0 100644 --- a/testing/api/test_api.py +++ b/testing/api/test_api.py @@ -48,6 +48,8 @@ BASELINE_MAC_ADDR = "02:42:aa:00:01:01" ALL_MAC_ADDR = "02:42:aa:00:00:01" +TIMESTAMP = "2024-01-01 00:00:00" + def pretty_print(dictionary: dict): """ Pretty print dictionary """ print(json.dumps(dictionary, indent=4)) @@ -102,9 +104,9 @@ def docker_logs(device_name): def load_json(file_name, directory): """Utility method to load json files' """ # Construct the base path relative to the main folder - base_path = Path(__file__).resolve().parent.parent.parent + base_path = os.path.abspath(os.path.join(__file__, "../../..")) # Construct the full file path - file_path = base_path / directory / file_name + file_path = os.path.join(base_path, directory, file_name) # Open the file in read mode with open(file_path, "r", encoding="utf-8") as file: @@ -359,7 +361,7 @@ def test_get_system_config(testrun): # pylint: disable=W0613 == api_config["network"]["internet_intf"] ) -def test_start_testrun_started_successfully(testing_devices, testrun): # pylint: disable=W0613 +def test_start_testrun_success(testing_devices, testrun): # pylint: disable=W0613 """Test for testrun started successfully """ # Payload with device details @@ -472,7 +474,7 @@ def test_start_testrun_device_not_found(testing_devices, testrun): # pylint: dis # Currently not working due to blocking during monitoring period @pytest.mark.skip() -def test_status_in_progress(testing_devices, testrun): # pylint: disable=W0613 +def test_status_in_progress(testing_devices, testrun): # pylint: disable=W0613 payload = {"device": {"mac_addr": BASELINE_MAC_ADDR, "firmware": "asd"}} r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) @@ -689,7 +691,7 @@ def test_system_shutdown(testrun): # pylint: disable=W0613 # Check if the response status code is 200 (OK) assert r.status_code == 200, f"Expected status code 200, got {r.status_code}" -def test_system_shutdown_in_progress(testrun): # pylint: disable=W0613 +def test_system_shutdown_in_progress(testrun): # pylint: disable=W0613 """Test system shutdown during an in-progress test""" # Payload with device details to start a test payload = { @@ -752,7 +754,7 @@ def create_report_folder(): # pylint: disable=W0613 def _create_report_folder(device_name, mac_address, timestamp): # Create the device folder path - main_folder = Path(DEVICES_DIRECTORY) / device_name + main_folder = os.path.join(DEVICES_DIRECTORY, device_name) # Remove the ":" from mac address for the folder structure mac_address = mac_address.replace(":", "") @@ -761,19 +763,19 @@ def _create_report_folder(device_name, mac_address, timestamp): timestamp = timestamp.replace(" ", "T") # Create the report folder path - report_folder = (main_folder / "reports" / timestamp / - "test" / mac_address) + report_folder = os.path.join(main_folder, "reports", timestamp, + "test", mac_address) # Ensure the report folder exists - report_folder.mkdir(parents=True, exist_ok=True) + os.makedirs(report_folder, exist_ok=True) # Define the source and target paths for the report.json - json_source_report_path = Path(REPORTS_PATH) / "report.json" - json_target_report_path = report_folder / "report.json" + json_source_report_path = os.path.join(REPORTS_PATH, "report.json") + json_target_report_path = os.path.join(report_folder, "report.json") # Define the source and target paths for the report.pdf - pdf_source_report_path = Path(REPORTS_PATH) / "report.pdf" - pdf_target_report_path = report_folder / "report.pdf" + pdf_source_report_path = os.path.join(REPORTS_PATH, "report.pdf") + pdf_target_report_path = os.path.join(report_folder, "report.pdf") # Copy the report.json file from the source to the target location shutil.copy(json_source_report_path, json_target_report_path) @@ -841,21 +843,20 @@ def test_delete_report_success(testrun, empty_devices_dir, # pylint: disable=W06 # Assign the device name mac_address = device[0]["mac_addr"] - # Assign the timestamp format for the folder structure - timestamp = "2024-01-01 00:00:00" - # Create the report directory - report_folder = create_report_folder(device_name, mac_address, timestamp) + report_folder = create_report_folder(device_name, mac_address, TIMESTAMP) # Payload delete_data = { "mac_addr": mac_address, - "timestamp": timestamp + "timestamp": TIMESTAMP } # Send a DELETE request to remove the report r = requests.delete(f"{API}/report", data=json.dumps(delete_data), timeout=5) + print(f"delete error is: {r.json()}") + # Check if status code is 200 (OK) assert r.status_code == 200 @@ -866,7 +867,7 @@ def test_delete_report_success(testrun, empty_devices_dir, # pylint: disable=W06 assert "success" in response # Check if report folder has been deleted - assert not report_folder.exists() + assert not os.path.exists(report_folder) def test_delete_report_no_payload(testrun, empty_devices_dir, # pylint: disable=W0613 add_device, create_report_folder): # pylint: disable=W0613 @@ -900,11 +901,8 @@ def test_delete_report_invalid_payload(testrun, empty_devices_dir, # pylint: dis # Assign the device name mac_address = device[0]["mac_addr"] - # Assign the timestamp format for the folder structure - timestamp = "2024-01-01 00:00:00" - # Create the report directory - create_report_folder(device_name, mac_address, timestamp) + create_report_folder(device_name, mac_address, TIMESTAMP) # Empty payload delete_data = {} @@ -924,9 +922,9 @@ def test_delete_report_invalid_payload(testrun, empty_devices_dir, # pylint: dis # Check if the correct error message returned assert "Missing mac address or timestamp" in response["error"] -def test_delete_report_incorrect_timestamp_format(testrun, empty_devices_dir, # pylint: disable=W0613 +def test_delete_report_invalid_timestamp(testrun, empty_devices_dir, # pylint: disable=W0613 add_device, create_report_folder): - """Test delete report bad request when timestamp format is incorrect (400)""" + """Test delete report bad request when timestamp format is not valid (400)""" # Load a device using the add_device fixture device = add_device() @@ -938,7 +936,7 @@ def test_delete_report_incorrect_timestamp_format(testrun, empty_devices_dir, # mac_address = device[0]["mac_addr"] # Assign the incorrect timestamp format - timestamp = "2024-01-01 incorrect" + timestamp = "2024-01-01 invalid" # Create the report.json create_report_folder(device_name, mac_address, timestamp) @@ -964,7 +962,7 @@ def test_delete_report_incorrect_timestamp_format(testrun, empty_devices_dir, # # Check if the correct error message returned assert "Incorrect timestamp format" in response["error"] -def test_delete_report_no_report(testrun, empty_devices_dir): # pylint: disable=W0613 +def test_delete_report_no_report(testrun, empty_devices_dir): # pylint: disable=W0613 """Test delete report when report does not exist (404)""" # Payload to be deleted for a non existing device @@ -985,7 +983,7 @@ def test_delete_report_no_report(testrun, empty_devices_dir): # pylint: disable # Check if "error" in response assert "error" in response -def test_delete_report_server_error(testrun, empty_devices_dir, # pylint: disable=W0613 +def test_delete_report_server_error(testrun, empty_devices_dir, # pylint: disable=W0613 add_device, create_report_folder): """Test for delete report causing internal server error (500)""" @@ -999,7 +997,7 @@ def test_delete_report_server_error(testrun, empty_devices_dir, # pylint: disab mac_address = device[0]["mac_addr"] # Assign the timestamp format for the folder structure - timestamp = "2024-01-01 00:00:00" + timestamp = TIMESTAMP # Create the report folder and JSON file create_report_folder(device_name, mac_address, timestamp) @@ -1033,7 +1031,7 @@ def test_delete_report_server_error(testrun, empty_devices_dir, # pylint: disab # Check if the correct error message is returned assert "Error occured whilst deleting report" in response["error"] -def test_get_report_success(testrun, empty_devices_dir, # pylint: disable=W0613 +def test_get_report_success(testrun, empty_devices_dir, # pylint: disable=W0613 add_device, create_report_folder): """Test get report when report exists (200)""" @@ -1046,8 +1044,8 @@ def test_get_report_success(testrun, empty_devices_dir, # pylint: disable=W0613 # Assign the device name mac_address = device[0]["mac_addr"] - # Assign the timestamp format - timestamp = "2024-01-01T00:00:00" + # Assign the timestamp and change the format + timestamp = TIMESTAMP.replace(" ", "T") # Create the report for the device create_report_folder(device_name, mac_address, timestamp) @@ -1061,11 +1059,11 @@ def test_get_report_success(testrun, empty_devices_dir, # pylint: disable=W0613 # Check if the response is a PDF assert r.headers["Content-Type"] == "application/pdf" -def test_get_report_not_found(testrun, empty_devices_dir, add_device): # pylint: disable=W0613 +def test_get_report_not_found(testrun, empty_devices_dir, add_device): # pylint: disable=W0613 """Test get report when report doesn't exist (404)""" # Assign timestamp - timestamp = "2024-01-01T00:00:00" + timestamp = TIMESTAMP # Load a device using the add_device fixture device = add_device() @@ -1088,12 +1086,12 @@ def test_get_report_not_found(testrun, empty_devices_dir, add_device): # pylint # Check if the correct error message returned assert "Report could not be found" in response["error"] -def test_get_report_device_not_found(empty_devices_dir, testrun): # pylint: disable=W0613 +def test_get_report_device_not_found(empty_devices_dir, testrun): # pylint: disable=W0613 """Test getting a report when the device is not found (404)""" # Assign device name and timestamp device_name = "nonexistent_device" - timestamp = "2024-01-01T00:00:00" + timestamp = TIMESTAMP # Send the get request r = requests.get(f"{API}/report/{device_name}/{timestamp}", timeout=5) @@ -1117,7 +1115,7 @@ def test_export_report_device_not_found(testrun, empty_devices_dir, # pylint: di # Assign the non-existing device name, mac_address and timestamp device_name = "non existing device" mac_address = "00:1e:42:35:73:c4" - timestamp = "2024-01-01T00:00:00" + timestamp = TIMESTAMP # Create the report for the non-existing device create_report_folder(device_name, mac_address, timestamp) @@ -1148,17 +1146,14 @@ def test_export_report_profile_not_found(testrun, empty_devices_dir, # pylint: d device_name = device[0]["device_folder"] mac_address = device[0]["mac_addr"] - # Set the timestamp - timestamp = "2024-01-01T00:00:00" - # Create the report for the device - create_report_folder(device_name, mac_address, timestamp) + create_report_folder(device_name, mac_address, TIMESTAMP) # Add a non existing profile into the payload payload = {"profile": "non_existent_profile"} # Send the post request - r = requests.post(f"{API}/export/{device_name}/{timestamp}", + r = requests.post(f"{API}/export/{device_name}/{TIMESTAMP}", json=payload, timeout=5) @@ -1174,7 +1169,7 @@ def test_export_report_profile_not_found(testrun, empty_devices_dir, # pylint: d # Check if the correct error message returned assert "A profile with that name could not be found" in response["error"] -def test_export_report_not_found(testrun, empty_devices_dir, add_device): # pylint: disable=W0613 +def test_export_report_not_found(testrun, empty_devices_dir, add_device): # pylint: disable=W0613 """Test for export the report result when the report could not be found""" # Load a device using the add_device fixture @@ -1183,11 +1178,8 @@ def test_export_report_not_found(testrun, empty_devices_dir, add_device): # pyl # Assign the device name and mac address device_name = device[0]["device_folder"] - # Set the timestamp - timestamp = "2024-01-01T00:00:00" - # Send the post request to trigger the zipping process - r = requests.post(f"{API}/export/{device_name}/{timestamp}", timeout=10) + r = requests.post(f"{API}/export/{device_name}/{TIMESTAMP}", timeout=10) # Check if status code is 500 (Internal Server Error) assert r.status_code == 404 @@ -1211,10 +1203,12 @@ def test_export_report_with_profile(testrun, empty_devices_dir, add_device, # py # Load a device using the add_device fixture device = add_device() - # Assign the device name, mac address and timestamp + # Assign the device name, mac address device_name = device[0]["device_folder"] mac_address = device[0]["mac_addr"] - timestamp = "2024-01-01T00:00:00" + + # Assign the timestamp and change the format + timestamp = TIMESTAMP.replace(" ", "T") # Create the report for the device create_report_folder(device_name, mac_address, timestamp) @@ -1237,10 +1231,12 @@ def test_export_results_with_no_profile(testrun, empty_devices_dir, # pylint: di # Load a device using the add_device fixture device = add_device() - # Assign the device name, mac address and timestamp + # Assign the device name, mac address device_name = device[0]["device_folder"] mac_address = device[0]["mac_addr"] - timestamp = "2024-01-01T00:00:00" + + # Assign the timestamp and change the format + timestamp = TIMESTAMP.replace(" ", "T") # Create the report for the device create_report_folder(device_name, mac_address, timestamp) @@ -1592,7 +1588,7 @@ def test_start_device_not_found(empty_devices_dir, # pylint: disable=W0613 assert r.status_code == 200 payload = {"device": {"mac_addr": device_1["mac_addr"], "firmware": "asd"}} - r = requests.post(f"{API}/`system/start`", + r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) assert r.status_code == 404 @@ -1907,31 +1903,35 @@ def test_get_test_modules(testrun): # pylint: disable=W0613 def delete_all_certs(): """Utility method to delete all certificates from root_certs folder""" - # Assign the certificates directory - certs_path = Path(CERTS_DIRECTORY) - try: + # Check if the profile_path (local/root_certs) exists and is a folder - if certs_path.exists() and certs_path.is_dir(): + if os.path.exists(CERTS_DIRECTORY) and os.path.isdir(CERTS_DIRECTORY): # Iterate over all certificates from root_certs folder - for item in certs_path.iterdir(): + for item in os.listdir(CERTS_DIRECTORY): + + # Combine the directory path with the item name to create the full path + item_path = os.path.join(CERTS_DIRECTORY, item) # Check if item is a file - if item.is_file(): + if os.path.isfile(item_path): #If True remove file - item.unlink() + os.unlink(item_path) else: + # If item is a folder remove it - shutil.rmtree(item) + shutil.rmtree(item_path) except PermissionError: + # Permission related issues print(f"Permission Denied: {item}") except OSError as err: + # System related issues print(f"Error removing {item}: {err}") @@ -2038,7 +2038,7 @@ def test_get_certificates(testrun, reset_certs, add_cert): # pylint: disable=W06 # Check if response contains two certificates assert len(response) == 2 -def test_upload_certificate(testrun, reset_certs): # pylint: disable=W0613 +def test_upload_certificate(testrun, reset_certs): # pylint: disable=W0613 """Test for upload certificate successfully""" # Load the first certificate file content using the utility method @@ -2097,7 +2097,7 @@ def test_upload_certificate(testrun, reset_certs): # pylint: disable=W0613 # Check if "WR2.pem" exists assert any(cert["filename"] == "WR2.pem" for cert in response) -def test_upload_invalid_certificate_format(testrun, reset_certs): # pylint: disable=W0613 +def test_upload_invalid_certificate_format(testrun, reset_certs): # pylint: disable=W0613 """Test for upload an invalid certificate format """ # Load the first certificate file content using the utility method @@ -2119,10 +2119,10 @@ def test_upload_invalid_certificate_format(testrun, reset_certs): # pylint: dis # Check if "error" key is in response assert "error" in response -def test_upload_invalid_certificate_name(testrun, reset_certs): # pylint: disable=W0613 +def test_upload_invalid_certificate_name(testrun, reset_certs): # pylint: disable=W0613 """Test for upload a valid certificate with invalid filename""" - # Assign the certificate name to a variable + # Assign the invalid certificate name to a variable cert_name = "invalidname1234567891234.pem" # Load the first certificate file content using the utility method @@ -2144,7 +2144,7 @@ def test_upload_invalid_certificate_name(testrun, reset_certs): # pylint: disab # Check if "error" key is in response assert "error" in response -def test_upload_existing_certificate(testrun, reset_certs): # pylint: disable=W0613 +def test_upload_existing_certificate(testrun, reset_certs): # pylint: disable=W0613 """Test for upload an existing certificate""" # Load the first certificate file content using the utility method @@ -2188,7 +2188,7 @@ def test_upload_existing_certificate(testrun, reset_certs): # pylint: disable=W # Check if "error" key is in response assert "error" in response -def test_delete_certificate_success(testrun, reset_certs, add_cert): # pylint: disable=W0613 +def test_delete_certificate_success(testrun, reset_certs, add_cert): # pylint: disable=W0613 """Test for successfully deleting an existing certificate""" # Use the add_cert fixture to upload the first certificate @@ -2226,7 +2226,7 @@ def test_delete_certificate_success(testrun, reset_certs, add_cert): # pylint: # Check that the certificate is no longer listed assert not any(cert["filename"] == "crt.pem" for cert in response) -def test_delete_certificate_bad_request(testrun, reset_certs, add_cert): # pylint: disable=W0613 +def test_delete_certificate_bad_request(testrun, reset_certs, add_cert): # pylint: disable=W0613 """Test for delete a certificate without providing the name""" # Use the add_cert fixture to upload the first certificate @@ -2249,7 +2249,7 @@ def test_delete_certificate_bad_request(testrun, reset_certs, add_cert): # pyli # Check if error in response assert "error" in response -def test_delete_certificate_not_found(testrun, reset_certs): # pylint: disable=W0613 +def test_delete_certificate_not_found(testrun, reset_certs): # pylint: disable=W0613 """Test for delete certificate when does not exist""" # Attempt to delete a certificate with a name that doesn't exist @@ -2275,25 +2275,37 @@ def delete_all_profiles(): """Utility method to delete all profiles from risk_profiles folder""" # Assign the profiles directory - profiles_path = Path(PROFILES_DIRECTORY) + profiles_path = os.path.join(PROFILES_DIRECTORY) try: + # Check if the profile_path (local/risk_profiles) exists and is a folder - if profiles_path.exists() and profiles_path.is_dir(): + if os.path.exists(profiles_path) and os.path.isdir(profiles_path): + # Iterate over all profiles from risk_profiles folder - for item in profiles_path.iterdir(): + for item in os.listdir(profiles_path): + + # Create the full path + item_path = os.path.join(profiles_path, item) + # Check if item is a file - if item.is_file(): + if os.path.isfile(item_path): + #If True remove file - item.unlink() + os.unlink(item_path) + else: + # If item is a folder remove it - shutil.rmtree(item) + shutil.rmtree(item_path) except PermissionError: + # Permission related issues print(f"Permission Denied: {item}") + except OSError as err: + # System related issues print(f"Error removing {item}: {err}") @@ -2334,23 +2346,28 @@ def reset_profiles(): @pytest.fixture() def add_profile(): - """Fixture to create profiles during tests.""" + """Fixture to create profiles during tests""" + # Returning the reference to create_profile return create_profile def profile_exists(profile_name): """Utility method to check if profile exists""" + # Send the get request r = requests.get(f"{API}/profiles", timeout=5) + # Check if status code is not 200 (OK) if r.status_code != 200: raise ValueError(f"Api request failed with code: {r.status_code}") + # Parse the JSON response to get the list of profiles profiles = r.json() + # Return if name is in the list of profiles return any(p["name"] == profile_name for p in profiles) -def test_get_profiles_format(testrun): # pylint: disable=W0613 +def test_get_profiles_format(testrun): # pylint: disable=W0613 """Test profiles format""" # Send the get request @@ -2370,10 +2387,10 @@ def test_get_profiles_format(testrun): # pylint: disable=W0613 assert "question" in item assert "type" in item -def test_get_profiles(testrun, reset_profiles, add_profile): # pylint: disable=W0613 +def test_get_profiles(testrun, reset_profiles, add_profile): # pylint: disable=W0613 """Test for get profiles (no profile, one profile, two profiles)""" - # Test for no profiles + # Test for no profile # Send the get request to "/profiles" endpoint r = requests.get(f"{API}/profiles", timeout=5) @@ -2415,11 +2432,15 @@ def test_get_profiles(testrun, reset_profiles, add_profile): # pylint: disable= for field in ["name", "status", "created", "version", "questions", "risk"]: assert field in profile + # Assign profile["questions"] + profile_questions = profile["questions"] + # Check if "questions" value is a list - assert isinstance(profile["questions"], list) + assert isinstance(profile_questions, list) # Check that "questions" value has the expected fields - for element in profile["questions"]: + for element in profile_questions: + # Check if each element is dict assert isinstance(element, dict) @@ -2450,7 +2471,7 @@ def test_get_profiles(testrun, reset_profiles, add_profile): # pylint: disable= assert len(response) == 2 def test_create_profile(testrun, reset_profiles): # pylint: disable=W0613 - """Test for create profile if not exists""" + """Test for create profile when profile does not exist""" # Load the profile new_profile = load_json("new_profile.json", directory=PROFILES_PATH) @@ -2492,7 +2513,7 @@ def test_create_profile(testrun, reset_profiles): # pylint: disable=W0613 assert created_profile is not None def test_update_profile(testrun, reset_profiles, add_profile): # pylint: disable=W0613 - """Test for update profile when exists""" + """Test for update profile when profile already exists""" # Load the new profile using add_profile fixture new_profile = add_profile("new_profile.json") @@ -2549,7 +2570,7 @@ def test_update_profile_invalid_json(testrun, reset_profiles, add_profile): # py # Load the new profile using add_profile fixture add_profile("new_profile.json") - # invalid JSON + # Invalid JSON updated_profile = {} # Send the post request to update the profile @@ -2570,7 +2591,7 @@ def test_update_profile_invalid_json(testrun, reset_profiles, add_profile): # py def test_create_profile_invalid_json(testrun, reset_profiles): # pylint: disable=W0613 """Test for create profile invalid JSON payload """ - # invalid JSON + # Invalid JSON new_profile = {} # Send the post request to update the profile @@ -2597,7 +2618,7 @@ def test_delete_profile(testrun, reset_profiles, add_profile): # pylint: disable # Assign the profile name profile_name = profile_to_delete["name"] - # Delete the profile + # Send the delete request r = requests.delete( f"{API}/profiles", data=json.dumps(profile_to_delete), @@ -2626,6 +2647,7 @@ def test_delete_profile(testrun, reset_profiles, add_profile): # pylint: disable (p for p in profiles if p["name"] == profile_name), None ) + # Check if profile was deleted assert deleted_profile is None @@ -2647,6 +2669,7 @@ def test_delete_profile_no_profile(testrun, reset_profiles): # pylint: disable=W def test_delete_profile_invalid_json(testrun, reset_profiles): # pylint: disable=W0613 """Test for delete profile wrong JSON payload""" + # Invalid payload profile_to_delete = {} # Delete the profile @@ -2664,7 +2687,9 @@ def test_delete_profile_invalid_json(testrun, reset_profiles): # pylint: disable # Check if "error" key in response assert "error" in response + # Invalid payload profile_to_delete_2 = {"status": "Draft"} + # Delete the profile r = requests.delete( f"{API}/profiles", @@ -2680,9 +2705,8 @@ def test_delete_profile_invalid_json(testrun, reset_profiles): # pylint: disable # Check if "error" key in response assert "error" in response -def test_delete_profile_internal_server_error(testrun, # pylint: disable=W0613 - reset_profiles, # pylint: disable=W0613 - add_profile ): +def test_delete_profile_server_error(testrun, reset_profiles, # pylint: disable=W0613 + add_profile): """Test for delete profile causing internal server error""" # Assign the profile from the fixture From c5361c9ffe3b8e0f083d312e83da0ce145933f66 Mon Sep 17 00:00:00 2001 From: MariusBalovin Date: Thu, 15 Aug 2024 16:40:40 +0100 Subject: [PATCH 08/13] removed timestamp variable in few tests --- testing/api/test_api.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/testing/api/test_api.py b/testing/api/test_api.py index 9e49d41e0..c173502f7 100644 --- a/testing/api/test_api.py +++ b/testing/api/test_api.py @@ -996,11 +996,8 @@ def test_delete_report_server_error(testrun, empty_devices_dir, # pylint: disabl # Assign the device name mac_address = device[0]["mac_addr"] - # Assign the timestamp format for the folder structure - timestamp = TIMESTAMP - # Create the report folder and JSON file - create_report_folder(device_name, mac_address, timestamp) + create_report_folder(device_name, mac_address, TIMESTAMP) # Construct the device report path device_directory = os.path.join(DEVICES_DIRECTORY, device_name) @@ -1011,7 +1008,7 @@ def test_delete_report_server_error(testrun, empty_devices_dir, # pylint: disabl # Prepare the payload for the DELETE request delete_data = { "mac_addr": mac_address, - "timestamp": timestamp + "timestamp": TIMESTAMP } # Send the delete request to delete the report @@ -1062,9 +1059,6 @@ def test_get_report_success(testrun, empty_devices_dir, # pylint: disable=W0613 def test_get_report_not_found(testrun, empty_devices_dir, add_device): # pylint: disable=W0613 """Test get report when report doesn't exist (404)""" - # Assign timestamp - timestamp = TIMESTAMP - # Load a device using the add_device fixture device = add_device() @@ -1072,7 +1066,7 @@ def test_get_report_not_found(testrun, empty_devices_dir, add_device): # pylint: device_name = device[0]["device_folder"] # Send the get request - r = requests.get(f"{API}/report/{device_name}/{timestamp}", timeout=5) + r = requests.get(f"{API}/report/{device_name}/{TIMESTAMP}", timeout=5) # Check if status code is 404 (not found) assert r.status_code == 404 @@ -1091,10 +1085,9 @@ def test_get_report_device_not_found(empty_devices_dir, testrun): # pylint: disa # Assign device name and timestamp device_name = "nonexistent_device" - timestamp = TIMESTAMP # Send the get request - r = requests.get(f"{API}/report/{device_name}/{timestamp}", timeout=5) + r = requests.get(f"{API}/report/{device_name}/{TIMESTAMP}", timeout=5) # Check if is 404 (not found) assert r.status_code == 404 @@ -1112,16 +1105,15 @@ def test_export_report_device_not_found(testrun, empty_devices_dir, # pylint: di create_report_folder): """Test for export the report result when the device could not be found""" - # Assign the non-existing device name, mac_address and timestamp + # Assign the non-existing device name, mac_address device_name = "non existing device" mac_address = "00:1e:42:35:73:c4" - timestamp = TIMESTAMP # Create the report for the non-existing device - create_report_folder(device_name, mac_address, timestamp) + create_report_folder(device_name, mac_address, TIMESTAMP) # Send the post request - r = requests.post(f"{API}/export/{device_name}/{timestamp}", timeout=5) + r = requests.post(f"{API}/export/{device_name}/{TIMESTAMP}", timeout=5) # Check if is 404 (not found) assert r.status_code == 404 From 053fcd56cff8d3ebf8e0172172ad89d29b589b42 Mon Sep 17 00:00:00 2001 From: MariusBalovin Date: Thu, 22 Aug 2024 13:36:14 +0100 Subject: [PATCH 09/13] updated create_report_folder and test_system_version --- testing/api/test_api.py | 46 +++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/testing/api/test_api.py b/testing/api/test_api.py index c173502f7..c492042d9 100644 --- a/testing/api/test_api.py +++ b/testing/api/test_api.py @@ -733,24 +733,28 @@ def test_system_version(testrun): # pylint: disable=W0613 # Parse the response response = r.json() - # Assign the json keys to a list - version_json = ["installed_version", - "update_available", - "latest_version", - "latest_version_url"] - - # Check if all the keys are in json response - for key in version_json: + # Assign the json response keys and expected types + expected_keys = { + "installed_version":str, + "update_available":bool, + "latest_version":str, + "latest_version_url":str + } + + # Iterate over the dict keys and values + for key, key_type in expected_keys.items(): + + # Check if the key is in the JSON response assert key in response - # Check if the update_available is a boolean - assert isinstance(response["update_available"], bool) + # Check if the key has the expected data type + assert isinstance(response[key], key_type) # Tests for reports endpoints @pytest.fixture def create_report_folder(): # pylint: disable=W0613 - """Fixture to create the report.json and report.pdf in the expected path""" + """Fixture to create the reports folder in local/devices""" def _create_report_folder(device_name, mac_address, timestamp): # Create the device folder path @@ -769,19 +773,17 @@ def _create_report_folder(device_name, mac_address, timestamp): # Ensure the report folder exists os.makedirs(report_folder, exist_ok=True) - # Define the source and target paths for the report.json - json_source_report_path = os.path.join(REPORTS_PATH, "report.json") - json_target_report_path = os.path.join(report_folder, "report.json") + # Iterate over the files from 'testing/api/reports' folder + for file in os.listdir(REPORTS_PATH): - # Define the source and target paths for the report.pdf - pdf_source_report_path = os.path.join(REPORTS_PATH, "report.pdf") - pdf_target_report_path = os.path.join(report_folder, "report.pdf") + # Construct full path of the file from 'testing/api/reports' folder + source_path = os.path.join(REPORTS_PATH, file) - # Copy the report.json file from the source to the target location - shutil.copy(json_source_report_path, json_target_report_path) + # Construct full path where the file will be copied + target_path = os.path.join(report_folder, file) - # Copy the report.json file from the source to the target location - shutil.copy(pdf_source_report_path, pdf_target_report_path) + # Copy the file + shutil.copy(source_path, target_path) return report_folder @@ -968,7 +970,7 @@ def test_delete_report_no_report(testrun, empty_devices_dir): # pylint: disable= # Payload to be deleted for a non existing device delete_data = { "mac_addr": "00:1e:42:35:73:c4", - "timestamp": "2024-01-01 00:00:00" + "timestamp": TIMESTAMP } # Send the delete request to the endpoint From b91a2dd57066326eda9285451e88239a702941ae Mon Sep 17 00:00:00 2001 From: MariusBalovin Date: Fri, 23 Aug 2024 13:18:24 +0100 Subject: [PATCH 10/13] fixed test_api.py failing tests --- testing/api/profiles/new_profile.json | 14 +- testing/api/profiles/new_profile_2.json | 80 +++++------- testing/api/profiles/updated_profile.json | 84 +++++------- testing/api/test_api.py | 151 ++++++++-------------- 4 files changed, 120 insertions(+), 209 deletions(-) diff --git a/testing/api/profiles/new_profile.json b/testing/api/profiles/new_profile.json index d63ecd17c..e2e46f3ab 100644 --- a/testing/api/profiles/new_profile.json +++ b/testing/api/profiles/new_profile.json @@ -2,10 +2,6 @@ "name": "New Profile", "status": "Valid", "questions": [ - { - "question": "What type of device is this?", - "answer": "IoT Gateway" - }, { "question": "How will this device be used at Google?", "answer": "Monitoring" @@ -18,17 +14,11 @@ "question": "Will the third-party device administrator be able to grant access to authorized Google personnel upon request?", "answer": "N/A" }, - { - "question": "Are any of the following statements true about your device?", - "answer": [ - 0 - ] - }, { "question": "Which of the following statements are true about this device?", "answer": [ 0 - ] + ] }, { "question": "Does the network protocol assure server-to-client identity verification?", @@ -38,7 +28,7 @@ "question": "Click the statements that best describe the characteristics of this device.", "answer": [ 0 - ] + ] }, { "question": "Are any of the following statements true about this device?", diff --git a/testing/api/profiles/new_profile_2.json b/testing/api/profiles/new_profile_2.json index 2ac93dc17..963265fe0 100644 --- a/testing/api/profiles/new_profile_2.json +++ b/testing/api/profiles/new_profile_2.json @@ -2,55 +2,35 @@ "name": "New Profile 2", "status": "Draft", "questions": [ - { - "question": "What type of device is this?", - "answer": "IoT Gateway" - }, - { - "question": "How will this device be used at Google?", - "answer": "Installed in a building" - }, - { - "question": "Is this device going to be managed by Google or a third party?", - "answer": "Google" - }, - { - "question": "Will the third-party device administrator be able to grant access to authorized Google personnel upon request?", - "answer": "Yes" - }, - { - "question": "Are any of the following statements true about your device?", - "answer": [ - 0, - 2 - ] - }, - { - "question": "Which of the following statements are true about this device?", - "answer": [ - 0, - 1, - 5 - ] - }, - { - "question": "Does the network protocol assure server-to-client identity verification?", - "answer": "Yes" - }, - { - "question": "Click the statements that best describe the characteristics of this device.", - "answer": [ - 0, - 1, - 2 - ] - }, - { - "question": "Are any of the following statements true about this device?", - "answer": [ - 2, - 3 - ] - } + { + "question": "How will this device be used at Google?", + "answer": "Monitoring" + }, + { + "question": "Is this device going to be managed by Google or a third party?", + "answer": "Google" + }, + { + "question": "Will the third-party device administrator be able to grant access to authorized Google personnel upon request?", + "answer": "N/A" + }, + { + "question": "Which of the following statements are true about this device?", + "answer": [] + }, + { + "question": "Does the network protocol assure server-to-client identity verification?", + "answer": "Yes" + }, + { + "question": "Click the statements that best describe the characteristics of this device.", + "answer": [] + }, + { + "question": "Are any of the following statements true about this device?", + "answer": [ + 0 + ] + } ] } \ No newline at end of file diff --git a/testing/api/profiles/updated_profile.json b/testing/api/profiles/updated_profile.json index 91714bcfa..9184cd07d 100644 --- a/testing/api/profiles/updated_profile.json +++ b/testing/api/profiles/updated_profile.json @@ -3,55 +3,39 @@ "rename": "Updated Profile", "status": "Draft", "questions": [ - { - "question": "What type of device is this?", - "answer": "IoT Gateway" - }, - { - "question": "How will this device be used at Google?", - "answer": "Installed in a building" - }, - { - "question": "Is this device going to be managed by Google or a third party?", - "answer": "Google" - }, - { - "question": "Will the third-party device administrator be able to grant access to authorized Google personnel upon request?", - "answer": "Yes" - }, - { - "question": "Are any of the following statements true about your device?", - "answer": [ - 0, - 2 - ] - }, - { - "question": "Which of the following statements are true about this device?", - "answer": [ - 0, - 1, - 5 - ] - }, - { - "question": "Does the network protocol assure server-to-client identity verification?", - "answer": "Yes" - }, - { - "question": "Click the statements that best describe the characteristics of this device.", - "answer": [ - 0, - 1, - 2 - ] - }, - { - "question": "Are any of the following statements true about this device?", - "answer": [ - 2, - 3 - ] - } + { + "question": "How will this device be used at Google?", + "answer": "Monitoring" + }, + { + "question": "Is this device going to be managed by Google or a third party?", + "answer": "Google" + }, + { + "question": "Will the third-party device administrator be able to grant access to authorized Google personnel upon request?", + "answer": "N/A" + }, + { + "question": "Which of the following statements are true about this device?", + "answer": [] + }, + { + "question": "Does the network protocol assure server-to-client identity verification?", + "answer": "Yes" + }, + { + "question": "Click the statements that best describe the characteristics of this device.", + "answer": [] + }, + { + "question": "Are any of the following statements true about this device?", + "answer": [ + 0 + ] + }, + { + "question": "Comments", + "answer": "" + } ] } \ No newline at end of file diff --git a/testing/api/test_api.py b/testing/api/test_api.py index c492042d9..145651527 100644 --- a/testing/api/test_api.py +++ b/testing/api/test_api.py @@ -805,8 +805,17 @@ def create_and_get_device(): # Send the GET request to retrieve the created device's folder name r = requests.get(f"{API}/devices", timeout=5) - # Return the parsed the json response - return r.json() + # Parse the json response + response = r.json() + + # Extract the device name and MAC address from response + device_name = f"{response[0]["manufacturer"]} {response[0]["model"]}" + mac_address = response[0]["mac_addr"] + + # Return only the device name and MAC address + return device_name, mac_address + + @pytest.fixture() def add_device(): @@ -832,22 +841,24 @@ def test_get_reports_no_reports(testrun): # pylint: disable=W0613 # Check if the response is an empty list assert response == [] -def test_delete_report_success(testrun, empty_devices_dir, # pylint: disable=W0613 +def test_delete_report_success(empty_devices_dir, testrun, # pylint: disable=W0613 add_device, create_report_folder): """Test for succesfully delete a report (200)""" - # Load a device using the add_device fixture - device = add_device() + r = requests.get(f"{API}/devices", timeout=5) - # Assign the device name - device_name = device[0]["device_folder"] + print(f"devices before: {r.json()}") - # Assign the device name - mac_address = device[0]["mac_addr"] + # Load the device_name and mac_address from add_device fixture + device_name, mac_address = add_device() # Create the report directory report_folder = create_report_folder(device_name, mac_address, TIMESTAMP) + r = requests.get(f"{API}/devices", timeout=5) + + print(f"devices after: {r.json()}") + # Payload delete_data = { "mac_addr": mac_address, @@ -857,8 +868,6 @@ def test_delete_report_success(testrun, empty_devices_dir, # pylint: disable=W06 # Send a DELETE request to remove the report r = requests.delete(f"{API}/report", data=json.dumps(delete_data), timeout=5) - print(f"delete error is: {r.json()}") - # Check if status code is 200 (OK) assert r.status_code == 200 @@ -871,8 +880,8 @@ def test_delete_report_success(testrun, empty_devices_dir, # pylint: disable=W06 # Check if report folder has been deleted assert not os.path.exists(report_folder) -def test_delete_report_no_payload(testrun, empty_devices_dir, # pylint: disable=W0613 - add_device, create_report_folder): # pylint: disable=W0613 +def test_delete_report_no_payload(empty_devices_dir, testrun, # pylint: disable=W0613 + add_device, create_report_folder): # pylint: disable=W0613 """Test delete report bad request when the payload is missing (400)""" # Send a DELETE request to remove the report without the payload @@ -890,18 +899,12 @@ def test_delete_report_no_payload(testrun, empty_devices_dir, # pylint: disable= # Check if the correct error message returned assert "Invalid request received, missing body" in response["error"] -def test_delete_report_invalid_payload(testrun, empty_devices_dir, # pylint: disable=W0613 +def test_delete_report_invalid_payload(empty_devices_dir, testrun, # pylint: disable=W0613 add_device, create_report_folder): """Test delete report bad request, mac addr and timestamp are missing (400)""" - # Load a device using the add_device fixture - device = add_device() - - # Assign the device name - device_name = device[0]["device_folder"] - - # Assign the device name - mac_address = device[0]["mac_addr"] + # Load the device_name and mac_address from add_device fixture + device_name, mac_address = add_device() # Create the report directory create_report_folder(device_name, mac_address, TIMESTAMP) @@ -924,18 +927,12 @@ def test_delete_report_invalid_payload(testrun, empty_devices_dir, # pylint: dis # Check if the correct error message returned assert "Missing mac address or timestamp" in response["error"] -def test_delete_report_invalid_timestamp(testrun, empty_devices_dir, # pylint: disable=W0613 - add_device, create_report_folder): +def test_delete_report_invalid_timestamp(empty_devices_dir, testrun, # pylint: disable=W0613 + add_device, create_report_folder): """Test delete report bad request when timestamp format is not valid (400)""" - # Load a device using the add_device fixture - device = add_device() - - # Assign the device name - device_name = device[0]["device_folder"] - - # Assign the device name - mac_address = device[0]["mac_addr"] + # Load the device_name and mac_address from add_device fixture + device_name, mac_address = add_device() # Assign the incorrect timestamp format timestamp = "2024-01-01 invalid" @@ -964,7 +961,7 @@ def test_delete_report_invalid_timestamp(testrun, empty_devices_dir, # pylint: d # Check if the correct error message returned assert "Incorrect timestamp format" in response["error"] -def test_delete_report_no_report(testrun, empty_devices_dir): # pylint: disable=W0613 +def test_delete_report_no_report(empty_devices_dir, testrun): # pylint: disable=W0613 """Test delete report when report does not exist (404)""" # Payload to be deleted for a non existing device @@ -985,18 +982,12 @@ def test_delete_report_no_report(testrun, empty_devices_dir): # pylint: disable= # Check if "error" in response assert "error" in response -def test_delete_report_server_error(testrun, empty_devices_dir, # pylint: disable=W0613 - add_device, create_report_folder): +def test_delete_report_server_error(empty_devices_dir, testrun, # pylint: disable=W0613 + add_device, create_report_folder): """Test for delete report causing internal server error (500)""" - # Load a device using the add_device fixture - device = add_device() - - # Assign the device name - device_name = device[0]["device_folder"] - - # Assign the device name - mac_address = device[0]["mac_addr"] + # Load the device_name and mac_address from add_device fixture + device_name, mac_address = add_device() # Create the report folder and JSON file create_report_folder(device_name, mac_address, TIMESTAMP) @@ -1030,18 +1021,12 @@ def test_delete_report_server_error(testrun, empty_devices_dir, # pylint: disabl # Check if the correct error message is returned assert "Error occured whilst deleting report" in response["error"] -def test_get_report_success(testrun, empty_devices_dir, # pylint: disable=W0613 - add_device, create_report_folder): +def test_get_report_success(empty_devices_dir, testrun, # pylint: disable=W0613 + add_device, create_report_folder): """Test get report when report exists (200)""" - # Load a device using the add_device fixture - device = add_device() - - # Assign the device name - device_name = device[0]["device_folder"] - - # Assign the device name - mac_address = device[0]["mac_addr"] + # Load the device_name and mac_address from add_device fixture + device_name, mac_address = add_device() # Assign the timestamp and change the format timestamp = TIMESTAMP.replace(" ", "T") @@ -1058,14 +1043,11 @@ def test_get_report_success(testrun, empty_devices_dir, # pylint: disable=W0613 # Check if the response is a PDF assert r.headers["Content-Type"] == "application/pdf" -def test_get_report_not_found(testrun, empty_devices_dir, add_device): # pylint: disable=W0613 +def test_get_report_not_found(empty_devices_dir, testrun, add_device): # pylint: disable=W0613 """Test get report when report doesn't exist (404)""" - # Load a device using the add_device fixture - device = add_device() - - # Assign the device name - device_name = device[0]["device_folder"] + # Load the device_name and ignore mac_address from add_device fixture + device_name, _ = add_device() # Send the get request r = requests.get(f"{API}/report/{device_name}/{TIMESTAMP}", timeout=5) @@ -1103,7 +1085,7 @@ def test_get_report_device_not_found(empty_devices_dir, testrun): # pylint: disa # Check if the correct error message is returned assert "Device not found" in response["error"] -def test_export_report_device_not_found(testrun, empty_devices_dir, # pylint: disable=W0613 +def test_export_report_device_not_found(empty_devices_dir, testrun, # pylint: disable=W0613 create_report_folder): """Test for export the report result when the device could not be found""" @@ -1129,16 +1111,12 @@ def test_export_report_device_not_found(testrun, empty_devices_dir, # pylint: di # Check if the correct error message returned assert "A device with that name could not be found" in response["error"] -def test_export_report_profile_not_found(testrun, empty_devices_dir, # pylint: disable=W0613 +def test_export_report_profile_not_found(empty_devices_dir, testrun, # pylint: disable=W0613 add_device, create_report_folder): """Test for export report result when the profile is not found""" - # Load a device using the add_device fixture - device = add_device() - - # Assign the device name and mac address - device_name = device[0]["device_folder"] - mac_address = device[0]["mac_addr"] + # Load the device_name and mac_address from add_device fixture + device_name, mac_address = add_device() # Create the report for the device create_report_folder(device_name, mac_address, TIMESTAMP) @@ -1163,14 +1141,11 @@ def test_export_report_profile_not_found(testrun, empty_devices_dir, # pylint: d # Check if the correct error message returned assert "A profile with that name could not be found" in response["error"] -def test_export_report_not_found(testrun, empty_devices_dir, add_device): # pylint: disable=W0613 +def test_export_report_not_found(empty_devices_dir, testrun, add_device): # pylint: disable=W0613 """Test for export the report result when the report could not be found""" - # Load a device using the add_device fixture - device = add_device() - - # Assign the device name and mac address - device_name = device[0]["device_folder"] + # Load the device_name and ignore mac_address from add_device fixture + device_name, _ = add_device() # Send the post request to trigger the zipping process r = requests.post(f"{API}/export/{device_name}/{TIMESTAMP}", timeout=10) @@ -1187,19 +1162,15 @@ def test_export_report_not_found(testrun, empty_devices_dir, add_device): # pyli # Check if the correct error message is returned assert "Report could not be found" in response["error"] -def test_export_report_with_profile(testrun, empty_devices_dir, add_device, # pylint: disable=W0613 +def test_export_report_with_profile(empty_devices_dir, testrun, add_device, # pylint: disable=W0613 create_report_folder, reset_profiles, add_profile): # pylint: disable=W0613 """Test export results with existing profile when report exists (200)""" # Create a profile using the add_profile fixture profile = add_profile("new_profile.json") - # Load a device using the add_device fixture - device = add_device() - - # Assign the device name, mac address - device_name = device[0]["device_folder"] - mac_address = device[0]["mac_addr"] + # Load the device_name and mac_address from add_device fixture + device_name, mac_address = add_device() # Assign the timestamp and change the format timestamp = TIMESTAMP.replace(" ", "T") @@ -1218,16 +1189,12 @@ def test_export_report_with_profile(testrun, empty_devices_dir, add_device, # py # Check if the response is a zip file assert r.headers["Content-Type"] == "application/zip" -def test_export_results_with_no_profile(testrun, empty_devices_dir, # pylint: disable=W0613 +def test_export_results_with_no_profile(empty_devices_dir, testrun, # pylint: disable=W0613 add_device, create_report_folder): """Test export results with no profile when report exists (200)""" - # Load a device using the add_device fixture - device = add_device() - - # Assign the device name, mac address - device_name = device[0]["device_folder"] - mac_address = device[0]["mac_addr"] + # Load the device_name and mac_address from add_device fixture + device_name, mac_address = add_device() # Assign the timestamp and change the format timestamp = TIMESTAMP.replace(" ", "T") @@ -1547,7 +1514,6 @@ def test_start_system_not_configured_correctly( r = requests.post(f"{API}/device", data=json.dumps(device_1), timeout=5) - print(r.text) payload = {"device": {"mac_addr": None, "firmware": "asd"}} r = requests.post(f"{API}/system/start", @@ -1574,7 +1540,6 @@ def test_start_device_not_found(empty_devices_dir, # pylint: disable=W0613 r = requests.post(f"{API}/device", data=json.dumps(device_1), timeout=5) - print(r.text) r = requests.delete(f"{API}/device/", data=json.dumps(device_1), @@ -1607,7 +1572,6 @@ def test_start_missing_device_information( r = requests.post(f"{API}/device", data=json.dumps(device_1), timeout=5) - print(r.text) payload = {} r = requests.post(f"{API}/system/start", @@ -1634,14 +1598,12 @@ def test_create_device_already_exists( r = requests.post(f"{API}/device", data=json.dumps(device_1), timeout=5) - print(r.text) assert r.status_code == 201 assert len(local_get_devices()) == 1 r = requests.post(f"{API}/device", data=json.dumps(device_1), timeout=5) - print(r.text) assert r.status_code == 409 def test_create_device_invalid_json( @@ -1653,7 +1615,6 @@ def test_create_device_invalid_json( r = requests.post(f"{API}/device", data=json.dumps(device_1), timeout=5) - print(r.text) assert r.status_code == 400 def test_create_device_invalid_request( @@ -1663,7 +1624,6 @@ def test_create_device_invalid_request( r = requests.post(f"{API}/device", data=None, timeout=5) - print(r.text) assert r.status_code == 400 def test_device_edit_device( @@ -1733,7 +1693,6 @@ def test_device_edit_device_not_found( r = requests.post(f"{API}/device", data=json.dumps(device_1), timeout=5) - print(r.text) assert r.status_code == 201 assert len(local_get_devices()) == 1 @@ -1770,7 +1729,6 @@ def test_device_edit_device_incorrect_json_format( r = requests.post(f"{API}/device", data=json.dumps(device_1), timeout=5) - print(r.text) assert r.status_code == 201 assert len(local_get_devices()) == 1 @@ -1802,7 +1760,6 @@ def test_device_edit_device_with_mac_already_exists( r = requests.post(f"{API}/device", data=json.dumps(device_1), timeout=5) - print(r.text) assert r.status_code == 201 assert len(local_get_devices()) == 1 From 4fdd30a21a35cd2911e6113faae9390ea63fb623 Mon Sep 17 00:00:00 2001 From: MariusBalovin Date: Fri, 23 Aug 2024 13:24:35 +0100 Subject: [PATCH 11/13] removed 2 unnecessary lines --- testing/api/test_api.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/testing/api/test_api.py b/testing/api/test_api.py index 145651527..673341fdd 100644 --- a/testing/api/test_api.py +++ b/testing/api/test_api.py @@ -855,10 +855,6 @@ def test_delete_report_success(empty_devices_dir, testrun, # pylint: disable=W06 # Create the report directory report_folder = create_report_folder(device_name, mac_address, TIMESTAMP) - r = requests.get(f"{API}/devices", timeout=5) - - print(f"devices after: {r.json()}") - # Payload delete_data = { "mac_addr": mac_address, From d1f6dc4b94e5f384ac7c920cc1965e4f264a60f5 Mon Sep 17 00:00:00 2001 From: MariusBalovin Date: Fri, 23 Aug 2024 15:13:47 +0100 Subject: [PATCH 12/13] fixed f string error --- testing/api/test_api.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/testing/api/test_api.py b/testing/api/test_api.py index 673341fdd..ab0da6b8a 100644 --- a/testing/api/test_api.py +++ b/testing/api/test_api.py @@ -809,7 +809,7 @@ def create_and_get_device(): response = r.json() # Extract the device name and MAC address from response - device_name = f"{response[0]["manufacturer"]} {response[0]["model"]}" + device_name = f'{response[0]["manufacturer"]} {response[0]["model"]}' mac_address = response[0]["mac_addr"] # Return only the device name and MAC address @@ -847,8 +847,6 @@ def test_delete_report_success(empty_devices_dir, testrun, # pylint: disable=W06 r = requests.get(f"{API}/devices", timeout=5) - print(f"devices before: {r.json()}") - # Load the device_name and mac_address from add_device fixture device_name, mac_address = add_device() From 3c2c1af23311e045b6bbd0cca3bea24f3aa4de70 Mon Sep 17 00:00:00 2001 From: MariusBaldovin Date: Wed, 28 Aug 2024 17:10:58 +0100 Subject: [PATCH 13/13] corrected the testrun session variable name --- framework/python/src/api/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/framework/python/src/api/api.py b/framework/python/src/api/api.py index a833987a2..45904bbbe 100644 --- a/framework/python/src/api/api.py +++ b/framework/python/src/api/api.py @@ -324,7 +324,7 @@ async def stop_testrun(self, response: Response): LOGGER.debug("Received stop command") # Check if Testrun is running - if (self._test_run.get_session().get_status() + if (self._testrun.get_session().get_status() not in [TestrunStatus.IN_PROGRESS, TestrunStatus.WAITING_FOR_DEVICE, TestrunStatus.MONITORING]): @@ -547,7 +547,7 @@ async def save_device(self, request: Request, response: Response): ) # Check if device folder exists - device_folder = os.path.join(self._test_run.get_root_dir(), + device_folder = os.path.join(self._testrun.get_root_dir(), DEVICES_PATH, device_json.get(DEVICE_MANUFACTURER_KEY) + " " + @@ -730,7 +730,7 @@ async def get_results(self, request: Request, response: Response, device_name, response.status_code = 404 return self._generate_msg(False, "Report could not be found") - zip_file_path = self._get_test_run().get_test_orc().zip_results( + zip_file_path = self._get_testrun().get_test_orc().zip_results( device, timestamp, profile) if zip_file_path is None: