From d71145610b06563cc0694990619f5c379d4d61b5 Mon Sep 17 00:00:00 2001 From: MariusBaldovin Date: Wed, 8 Jan 2025 13:51:19 +0000 Subject: [PATCH 1/8] added a new endpoint for profile export --- framework/python/src/api/api.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/framework/python/src/api/api.py b/framework/python/src/api/api.py index 2bba5e62f..90565b4e8 100644 --- a/framework/python/src/api/api.py +++ b/framework/python/src/api/api.py @@ -43,6 +43,7 @@ DEVICE_ADDITIONAL_INFO_KEY = "additional_info" DEVICES_PATH = "local/devices" +PROFILES_PATH = "local/risk_profiles" RESOURCES_PATH = "resources" DEVICE_FOLDER_PATH = "devices" @@ -133,6 +134,7 @@ def __init__(self, testrun): self._router.add_api_route("/profiles", self.delete_profile, methods=["DELETE"]) + self._router.add_api_route("/profile/{profile_name}", self.get_profile) # Allow all origins to access the API origins = ["*"] @@ -926,6 +928,29 @@ async def delete_profile(self, request: Request, response: Response): return self._generate_msg(True, "Successfully deleted that profile") + async def get_profile(self, response: Response, profile_name): + + profile = self._session.get_profile(profile_name) + print(profile) + + # If the profile not found return 404 + if profile is None: + LOGGER.info("Profile not found, returning 404") + response.status_code = 404 + return self._generate_msg(False, "Profile could not be found") + + # Profile file path + profile_path = os.path.join(PROFILES_PATH, f"{profile_name}.json") + + LOGGER.debug(f"Received get profile request for {profile_name}") + + if os.path.isfile(profile_path): + return FileResponse(profile_path) + else: + LOGGER.info("Profile could not be found, returning 404") + response.status_code = 404 + return self._generate_msg(False, "Profile could not be found") + # Certificates def get_certs(self): LOGGER.debug("Received certs list request") From 875dd819c6ba9dd7aced618f87958d3f485666e3 Mon Sep 17 00:00:00 2001 From: MariusBaldovin Date: Wed, 8 Jan 2025 13:55:33 +0000 Subject: [PATCH 2/8] remove print --- framework/python/src/api/api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/framework/python/src/api/api.py b/framework/python/src/api/api.py index 90565b4e8..b769993cf 100644 --- a/framework/python/src/api/api.py +++ b/framework/python/src/api/api.py @@ -931,7 +931,6 @@ async def delete_profile(self, request: Request, response: Response): async def get_profile(self, response: Response, profile_name): profile = self._session.get_profile(profile_name) - print(profile) # If the profile not found return 404 if profile is None: From 48883bb26023184c608537ddcc8cce1c99b13ffc Mon Sep 17 00:00:00 2001 From: MariusBaldovin Date: Thu, 9 Jan 2025 11:31:45 +0000 Subject: [PATCH 3/8] added the pdf logic with optional device information to be added into the risk assessment --- framework/python/src/api/api.py | 83 ++++++++++++++++++--- framework/python/src/common/risk_profile.py | 39 +++++++++- 2 files changed, 109 insertions(+), 13 deletions(-) diff --git a/framework/python/src/api/api.py b/framework/python/src/api/api.py index b769993cf..2c60c53b8 100644 --- a/framework/python/src/api/api.py +++ b/framework/python/src/api/api.py @@ -928,8 +928,33 @@ async def delete_profile(self, request: Request, response: Response): return self._generate_msg(True, "Successfully deleted that profile") - async def get_profile(self, response: Response, profile_name): + async def get_profile(self, request: Request, response: Response, + profile_name): + LOGGER.debug(f"Received get profile request for {profile_name}") + + device = None + + try: + req_raw = (await request.body()).decode("UTF-8") + req_json = json.loads(req_raw) + + # Check if device mac_addr has been specified + if "mac_addr" in req_json and len(req_json.get("mac_addr")) > 0: + device_mac_addr = req_json.get("mac_addr") + device = self.get_session().get_device(device_mac_addr) + + # If device is not found return 404 + if device is None: + response.status_code = status.HTTP_404_NOT_FOUND + return self._generate_msg( + False, "A device with that mac address could not be found") + + except JSONDecodeError: + # Device not specified + pass + + # Retrieve the profile profile = self._session.get_profile(profile_name) # If the profile not found return 404 @@ -938,17 +963,57 @@ async def get_profile(self, response: Response, profile_name): response.status_code = 404 return self._generate_msg(False, "Profile could not be found") - # Profile file path - profile_path = os.path.join(PROFILES_PATH, f"{profile_name}.json") + # If device has been added into the body + if device: - LOGGER.debug(f"Received get profile request for {profile_name}") + try: + + # Path where the PDF will be saved + profile_pdf_path = os.path.join(PROFILES_PATH, f"{profile_name}.pdf") + + # Write the PDF content + with open(profile_pdf_path, "wb") as f: + f.write(profile.to_pdf(device).getvalue()) - if os.path.isfile(profile_path): - return FileResponse(profile_path) + # Return the pdf file + if os.path.isfile(profile_pdf_path): + return FileResponse(profile_pdf_path) + else: + LOGGER.info("Profile could not be found, returning 404") + response.status_code = 404 + return self._generate_msg(False, "Profile could not be found") + + # Exceptions if the PDF creation fails + except Exception as e: + LOGGER.error(f"Error creating the profile PDF: {e}") + response.status_code = 500 + return self._generate_msg(False, "Error retrieving the profile PDF") + + # If device not added into the body else: - LOGGER.info("Profile could not be found, returning 404") - response.status_code = 404 - return self._generate_msg(False, "Profile could not be found") + + try: + + # Path where the PDF will be saved + profile_pdf_path = os.path.join(PROFILES_PATH, f"{profile_name}.pdf") + + # Write the PDF content + with open(profile_pdf_path, "wb") as f: + f.write(profile.to_pdf_no_device().getvalue()) + + # Return the pdf file + if os.path.isfile(profile_pdf_path): + return FileResponse(profile_pdf_path) + else: + LOGGER.info("Profile could not be found, returning 404") + response.status_code = 404 + return self._generate_msg(False, "Profile could not be found") + + # Exceptions if the PDF creation fails + except Exception as e: + LOGGER.error(f"Error creating the profile PDF: {e}") + response.status_code = 500 + return self._generate_msg(False, "Error retrieving the profile PDF") # Certificates def get_certs(self): diff --git a/framework/python/src/common/risk_profile.py b/framework/python/src/common/risk_profile.py index 559117aec..c14f28e6c 100644 --- a/framework/python/src/common/risk_profile.py +++ b/framework/python/src/common/risk_profile.py @@ -351,7 +351,7 @@ def to_html(self, device): logo_img_b64 = base64.b64encode(f.read()).decode('utf-8') self._device = self._format_device_profile(device) - pages = self._generate_report_pages() + pages = self._generate_report_pages(device) return self._template.render( styles=self._template_styles, manufacturer=self._device.manufacturer, @@ -366,7 +366,35 @@ def to_html(self, device): created_at=self.created.strftime('%d.%m.%Y') ) - def _generate_report_pages(self): + def to_html_no_device(self): + """Returns the risk profile in HTML format without device info""" + + with open(test_run_img_file, 'rb') as f: + logo_img_b64 = base64.b64encode(f.read()).decode('utf-8') + + pages = self._generate_report_pages() + return self._template.render( + styles=self._template_styles, + logo=logo_img_b64, + risk=self.risk, + pages=pages, + total_pages=len(pages), + version=self.version, + created_at=self.created.strftime('%d.%m.%Y') + ) + + def to_pdf_no_device(self): + """Returns the risk profile in PDF format without device info""" + + # Resolve the data as html first + html = self.to_html_no_device() + + # Convert HTML to PDF in memory using weasyprint + pdf_bytes = BytesIO() + HTML(string=html).write_pdf(pdf_bytes) + return pdf_bytes + + def _generate_report_pages(self, device=None): # Text block heght block_height = 18 @@ -391,8 +419,11 @@ def _generate_report_pages(self): current_page = [] index = 1 - questions = deepcopy(self._device.additional_info) - questions.extend(self.questions) + questions = deepcopy(self.questions) + + if device: + questions = deepcopy(self._device.additional_info) + questions.extend(self.questions) for question in questions: From ca6213d7a917d52a18702030693da8a2fac24636 Mon Sep 17 00:00:00 2001 From: MariusBaldovin Date: Thu, 9 Jan 2025 12:18:12 +0000 Subject: [PATCH 4/8] fixed _load_profiles from session to ignore pdf files, added the limited or high risk message into pdf report with no device selected --- framework/python/src/common/risk_profile.py | 31 +++++++++++++-------- framework/python/src/core/session.py | 3 ++ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/framework/python/src/common/risk_profile.py b/framework/python/src/common/risk_profile.py index c14f28e6c..fe613f69a 100644 --- a/framework/python/src/common/risk_profile.py +++ b/framework/python/src/common/risk_profile.py @@ -369,6 +369,13 @@ def to_html(self, device): def to_html_no_device(self): """Returns the risk profile in HTML format without device info""" + high_risk_message = '''The device has been assessed to be high + risk due to the nature of the answers provided + about the device functionality.''' + limited_risk_message = '''The device has been assessed to be limited risk + due to the nature of the answers provided about + the device functionality.''' + with open(test_run_img_file, 'rb') as f: logo_img_b64 = base64.b64encode(f.read()).decode('utf-8') @@ -377,23 +384,14 @@ def to_html_no_device(self): styles=self._template_styles, logo=logo_img_b64, risk=self.risk, + high_risk_message=high_risk_message, + limited_risk_message=limited_risk_message, pages=pages, total_pages=len(pages), version=self.version, created_at=self.created.strftime('%d.%m.%Y') ) - def to_pdf_no_device(self): - """Returns the risk profile in PDF format without device info""" - - # Resolve the data as html first - html = self.to_html_no_device() - - # Convert HTML to PDF in memory using weasyprint - pdf_bytes = BytesIO() - HTML(string=html).write_pdf(pdf_bytes) - return pdf_bytes - def _generate_report_pages(self, device=None): # Text block heght @@ -487,6 +485,17 @@ def to_pdf(self, device): HTML(string=html).write_pdf(pdf_bytes) return pdf_bytes + def to_pdf_no_device(self): + """Returns the risk profile in PDF format without device info""" + + # Resolve the data as html first + html = self.to_html_no_device() + + # Convert HTML to PDF in memory using weasyprint + pdf_bytes = BytesIO() + HTML(string=html).write_pdf(pdf_bytes) + return pdf_bytes + # Adding risks to device profile questions def _format_device_profile(self, device): device_copy = deepcopy(device) diff --git a/framework/python/src/core/session.py b/framework/python/src/core/session.py index 3c2662ae3..3e8802aca 100644 --- a/framework/python/src/core/session.py +++ b/framework/python/src/core/session.py @@ -539,6 +539,9 @@ def _load_profiles(self): for risk_profile_file in os.listdir( os.path.join(self._root_dir, PROFILES_DIR)): + if not risk_profile_file.endswith('.json'): + continue + LOGGER.debug(f'Discovered profile {risk_profile_file}') # Open the risk profile file From 1486c2556da2a2705aa62cf132b7723903890eb3 Mon Sep 17 00:00:00 2001 From: MariusBaldovin Date: Thu, 9 Jan 2025 12:55:25 +0000 Subject: [PATCH 5/8] update mockoon and postman --- docs/dev/mockoon.json | 109 ++++++++++++++++++++ docs/dev/postman.json | 233 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 324 insertions(+), 18 deletions(-) diff --git a/docs/dev/mockoon.json b/docs/dev/mockoon.json index a73eb5beb..608bb899d 100644 --- a/docs/dev/mockoon.json +++ b/docs/dev/mockoon.json @@ -1602,6 +1602,111 @@ } ], "responseMode": null + }, + { + "uuid": "af7fdcb0-721d-4198-a8ef-c6d8c4eba8c8", + "type": "http", + "documentation": "Get a Testrun PDF profile", + "method": "get", + "endpoint": "report/{profile_name}", + "responses": [ + { + "uuid": "9a759f46-4bc4-433a-be86-e456f069c217", + "body": "", + "latency": 0, + "statusCode": 200, + "label": "Profile found - no device selected", + "headers": [], + "bodyType": "FILE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": true, + "crudKey": "id", + "callbacks": [] + }, + { + "uuid": "c9a09ae7-3158-4956-93ac-4c8a90dfced8", + "body": "", + "latency": 0, + "statusCode": 200, + "label": "Profile found - device selected ", + "headers": [], + "bodyType": "FILE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": false, + "crudKey": "id", + "callbacks": [] + }, + { + "uuid": "5f98471e-15b6-47a4-a68d-e98c3a538b40", + "body": "{\n \"error\": \"Profile could not be found\"\n}", + "latency": 0, + "statusCode": 404, + "label": "Profile not found", + "headers": [], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": false, + "crudKey": "id", + "callbacks": [] + }, + { + "uuid": "767d9e78-386e-4bf7-bec8-71a005efdce9", + "body": "{\n \"error\": \"A device with that mac address could not be found\"\n}", + "latency": 0, + "statusCode": 404, + "label": "Device not found", + "headers": [], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": false, + "crudKey": "id", + "callbacks": [] + }, + { + "uuid": "5d76bea0-39c1-45f2-80f1-de6f770cb999", + "body": "{\n \"error\": \"Error retrieving the profile PDF\"\n}", + "latency": 0, + "statusCode": 500, + "label": "Error occured", + "headers": [], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": false, + "crudKey": "id", + "callbacks": [] + } + ], + "responseMode": null } ], "rootChildren": [ @@ -1700,6 +1805,10 @@ { "type": "route", "uuid": "26f0f76f-e787-4ebe-a3f8-ea3a6004bc15" + }, + { + "type": "route", + "uuid": "af7fdcb0-721d-4198-a8ef-c6d8c4eba8c8" } ], "proxyMode": false, diff --git a/docs/dev/postman.json b/docs/dev/postman.json index 642090dd4..f15373fac 100644 --- a/docs/dev/postman.json +++ b/docs/dev/postman.json @@ -1348,7 +1348,7 @@ ] }, { - "name": "Export", + "name": "Export Report", "request": { "method": "POST", "header": [], @@ -2305,7 +2305,6 @@ { "key": "Content-Type", "value": "application/json", - "name": "Content-Type", "description": "", "type": "text" } @@ -2501,7 +2500,6 @@ { "key": "Content-Type", "value": "application/json", - "name": "Content-Type", "description": "", "type": "text" } @@ -2578,7 +2576,6 @@ { "key": "Content-Type", "value": "application/json", - "name": "Content-Type", "description": "", "type": "text" } @@ -2622,7 +2619,6 @@ { "key": "Content-Type", "value": "application/json", - "name": "Content-Type", "description": "", "type": "text" } @@ -2666,7 +2662,6 @@ { "key": "Content-Type", "value": "application/json", - "name": "Content-Type", "description": "", "type": "text" } @@ -2741,7 +2736,6 @@ { "key": "Content-Type", "value": "application/json", - "name": "Content-Type", "description": "", "type": "text" } @@ -2784,7 +2778,6 @@ { "key": "Content-Type", "value": "application/json", - "name": "Content-Type", "description": "", "type": "text" } @@ -2827,7 +2820,6 @@ { "key": "Content-Type", "value": "application/json", - "name": "Content-Type", "description": "", "type": "text" } @@ -2971,6 +2963,218 @@ } ] }, + { + "name": "Get Profile", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/profile/{profile_name}", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profile", + "{profile_name}" + ] + }, + "description": "Get the PDF report for a specific device and timestamp" + }, + "response": [ + { + "name": "Get Profile No Device (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/profile/Test", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profile", + "Test" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "text", + "header": [ + { + "key": "Content-Type", + "value": "application/pdf", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{profile_name}.pdf" + }, + { + "name": "Get Profile with Device (200))", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\"mac_addr\": \"00:1e:42:35:73:c4\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profile/Test", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profile", + "Test" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "text", + "header": [ + { + "key": "Content-Type", + "value": "application/pdf", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "report.pdf" + }, + { + "name": "Profile Not Found (404)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/profile/NonExistingProfile", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profile", + "NonExistingProfile" + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Profile could not be found\"\n}" + }, + { + "name": "Device Not Found (404)", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\"mac_addr\": \"non_existing_mac_addr\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profile/Test", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profile", + "Test" + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"A device with that mac address could not be found\"\n}" + }, + { + "name": "Internal Server Error (500) Copy", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/profile/Test", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profile", + "Test" + ] + } + }, + "status": "Internal Server Error", + "code": 500, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Error retrieving the profile PDF\"\n}" + } + ] + }, { "name": "Update Profile", "request": { @@ -3006,7 +3210,7 @@ "header": [], "body": { "mode": "raw", - "raw": "[\n {\n \"name\": \"New Profile\",\n \"version\": \"2.0\",\n \"created\": \"2024-10-08\",\n \"status\": \"Valid\",\n \"risk\": \"High\",\n \"questions\": [\n {\n \"question\": \"How will this device be used at Google?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"answer\": \"Google\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"answer\": \"No\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Which of the following statements are true about this device?\",\n \"answer\": [\n 0,\n 1,\n 2\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"answer\": \"Yes\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Are any of the following statements true about this device?\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Comments\",\n \"answer\": \"\"\n }\n ]\n }\n]", + "raw": "\n{\n \"name\": \"New Profile\",\n \"rename\": \"Updated New Profile\",\n \"version\": \"2.0.1\",\n \"created\": \"2024-10-08\",\n \"status\": \"Valid\",\n \"risk\": \"High\",\n \"questions\": [\n {\n \"question\": \"How will this device be used at Google?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"answer\": \"Google\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"answer\": \"No\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Which of the following statements are true about this device?\",\n \"answer\": [\n 0,\n 1,\n 2\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"answer\": \"Yes\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Are any of the following statements true about this device?\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Comments\",\n \"answer\": \"\"\n }\n ]\n}", "options": { "raw": { "language": "json" @@ -3032,7 +3236,6 @@ { "key": "Content-Type", "value": "application/json", - "name": "Content-Type", "description": "", "type": "text" } @@ -3047,7 +3250,7 @@ "header": [], "body": { "mode": "raw", - "raw": "[\n {\n \"name\": \"New Profile\",\n \"version\": \"2.0\",\n \"created\": \"2024-10-08\",\n \"status\": \"Valid\",\n \"risk\": \"High\",\n \"questions\": [\n {\n \"question\": \"How will this device be used at Google?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"answer\": \"Google\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"answer\": \"No\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Which of the following statements are true about this device?\",\n \"answer\": [\n 0,\n 1,\n 2\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"answer\": \"Yes\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Are any of the following statements true about this device?\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Comments\",\n \"answer\": \"\"\n }\n ]\n }\n]", + "raw": "\n{\n \"name\": \"New Profile\",\n \"version\": \"2.0\",\n \"created\": \"2024-10-08\",\n \"status\": \"Valid\",\n \"risk\": \"High\",\n \"questions\": [\n {\n \"question\": \"How will this device be used at Google?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"answer\": \"Google\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"answer\": \"No\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Which of the following statements are true about this device?\",\n \"answer\": [\n 0,\n 1,\n 2\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"answer\": \"Yes\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Are any of the following statements true about this device?\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Comments\",\n \"answer\": \"\"\n }\n ]\n}\n", "options": { "raw": { "language": "json" @@ -3073,7 +3276,6 @@ { "key": "Content-Type", "value": "application/json", - "name": "Content-Type", "description": "", "type": "text" } @@ -3114,7 +3316,6 @@ { "key": "Content-Type", "value": "application/json", - "name": "Content-Type", "description": "", "type": "text" } @@ -3225,7 +3426,6 @@ { "key": "Content-Type", "value": "application/json", - "name": "Content-Type", "description": "", "type": "text" } @@ -3266,7 +3466,6 @@ { "key": "Content-Type", "value": "application/json", - "name": "Content-Type", "description": "", "type": "text" } @@ -3307,7 +3506,6 @@ { "key": "Content-Type", "value": "application/json", - "name": "Content-Type", "description": "", "type": "text" } @@ -3348,7 +3546,6 @@ { "key": "Content-Type", "value": "application/json", - "name": "Content-Type", "description": "", "type": "text" } From 1ed5c673541e8ce904a116a9a7de72aa71e3bf44 Mon Sep 17 00:00:00 2001 From: MariusBaldovin Date: Fri, 17 Jan 2025 16:53:02 +0000 Subject: [PATCH 6/8] added tests for export profile endpoint, changed export profile to post method --- framework/python/src/api/api.py | 6 +- testing/api/test_api.py | 114 +++++++++++++++++++++++++++++++- 2 files changed, 117 insertions(+), 3 deletions(-) diff --git a/framework/python/src/api/api.py b/framework/python/src/api/api.py index 644c740ae..8e36120d7 100644 --- a/framework/python/src/api/api.py +++ b/framework/python/src/api/api.py @@ -134,7 +134,9 @@ def __init__(self, testrun): self._router.add_api_route("/profiles", self.delete_profile, methods=["DELETE"]) - self._router.add_api_route("/profile/{profile_name}", self.get_profile) + self._router.add_api_route("/profile/{profile_name}", + self.export_profile, + methods=["POST"]) # Allow all origins to access the API origins = ["*"] @@ -930,7 +932,7 @@ async def delete_profile(self, request: Request, response: Response): return self._generate_msg(True, "Successfully deleted that profile") - async def get_profile(self, request: Request, response: Response, + async def export_profile(self, request: Request, response: Response, profile_name): LOGGER.debug(f"Received get profile request for {profile_name}") diff --git a/testing/api/test_api.py b/testing/api/test_api.py index f328d7885..76a39605b 100644 --- a/testing/api/test_api.py +++ b/testing/api/test_api.py @@ -1285,7 +1285,7 @@ def test_export_report_not_found(empty_devices_dir, add_devices, testrun): # pyl # 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) + # Check if status code is 404 (Not Found) assert r.status_code == 404 # Parse the json response @@ -3123,6 +3123,118 @@ def test_delete_profile_server_error(empty_profiles_dir, add_profiles, # pylint: # Check if error in response assert "error" in response +@pytest.mark.parametrize("add_profiles", [ + ["valid_profile.json"] +],indirect=True) +def test_export_profile_success(empty_profiles_dir, add_profiles, testrun): # pylint: disable=W0613 + """Test for successfully export profile as PDF (200)""" + + # Assign the profile from the fixture + profile = load_json("valid_profile.json", directory=PROFILES_PATH) + + # Assign the profile name to profile_name + profile_name = profile["name"] + + # Send the get request + r = requests.post(f"{API}/profile/{profile_name}", 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_export_profile_not_found(testrun): # pylint: disable=W0613 + """Test export profile as PDF when profile doesn't exist (404)""" + + # Assign the profile name + profile_name = "non_existing" + + # Send the get request + r = requests.post(f"{API}/report/{profile_name}", 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 "Profile could not be found" in response["error"] + +@pytest.mark.parametrize("add_devices, add_profiles", [ + (["device_1"], ["valid_profile.json"]) +], indirect=True) +def test_export_profile_with_device(empty_devices_dir, add_devices, # pylint: disable=W0613 + empty_profiles_dir, add_profiles, # pylint: disable=W0613 + testrun): # pylint: disable=W0613 + """ Test export profile as PDF with existing device (200)""" + + # Load the profile using load_json utility method + profile = load_json("valid_profile.json", directory=PROFILES_PATH) + + # Assign the profile name to profile_name + profile_name = profile["name"] + + # Load the device using load_json utility method + device = load_json("device_config.json", directory=DEVICE_1_PATH) + + # Assign the device mac address + mac_addr = device["mac_addr"] + + # Payload with device mac address + payload = {"mac_addr": mac_addr} + + print(payload) + + # Send the post request + r = requests.post(f"{API}/profile/{profile_name}", + json=payload, + timeout=5) + + # Check if status code is 200 (OK) + assert r.status_code == 200 + + # Check if the response is a pdf file + assert r.headers["Content-Type"] == "application/pdf" + +@pytest.mark.parametrize("add_profiles", [ + ["valid_profile.json"] +],indirect=True) +def test_export_profile_device_not_found(empty_profiles_dir, add_profiles, # pylint: disable=W0613 + testrun): # pylint: disable=W0613 + """ Test export profile as PDF when device not found (404)""" + + # Load the profile using load_json utility method + profile = load_json("valid_profile.json", directory=PROFILES_PATH) + + # Assign the profile name to profile_name + profile_name = profile["name"] + + # Assign the device mac address + device_mac = {"mac_addr": "non_existing"} + + # Send the post request + r = requests.post(f"{API}/profile/{profile_name}", + json=device_mac, + 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 + error_message = "A device with that mac address could not be found" + assert error_message in response["error"] + # Skipped tests currently not working due to blocking during monitoring period @pytest.mark.skip() From afdf63d0d245aea875bf8f791c96a2feacc265e9 Mon Sep 17 00:00:00 2001 From: MariusBaldovin Date: Mon, 20 Jan 2025 09:56:40 +0000 Subject: [PATCH 7/8] fix failed test --- testing/api/test_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/api/test_api.py b/testing/api/test_api.py index 76a39605b..c9ef17f86 100644 --- a/testing/api/test_api.py +++ b/testing/api/test_api.py @@ -3150,8 +3150,8 @@ def test_export_profile_not_found(testrun): # pylint: disable=W0613 # Assign the profile name profile_name = "non_existing" - # Send the get request - r = requests.post(f"{API}/report/{profile_name}", timeout=5) + # Send the post request + r = requests.post(f"{API}/profile/{profile_name}", timeout=5) # Check if status code is 404 (not found) assert r.status_code == 404 From 87ddf4ad8845dd2f6a2600dffb0b5b65047e2b3c Mon Sep 17 00:00:00 2001 From: MariusBaldovin Date: Tue, 21 Jan 2025 09:18:27 +0000 Subject: [PATCH 8/8] update postman and mockoon --- docs/dev/mockoon.json | 2 +- docs/dev/postman.json | 51 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/docs/dev/mockoon.json b/docs/dev/mockoon.json index 608bb899d..443343a89 100644 --- a/docs/dev/mockoon.json +++ b/docs/dev/mockoon.json @@ -1607,7 +1607,7 @@ "uuid": "af7fdcb0-721d-4198-a8ef-c6d8c4eba8c8", "type": "http", "documentation": "Get a Testrun PDF profile", - "method": "get", + "method": "post", "endpoint": "report/{profile_name}", "responses": [ { diff --git a/docs/dev/postman.json b/docs/dev/postman.json index f15373fac..73617d2f9 100644 --- a/docs/dev/postman.json +++ b/docs/dev/postman.json @@ -2964,9 +2964,9 @@ ] }, { - "name": "Get Profile", + "name": "Export Profile", "request": { - "method": "GET", + "method": "POST", "header": [], "url": { "raw": "http://localhost:8000/profile/{profile_name}", @@ -2986,7 +2986,7 @@ { "name": "Get Profile No Device (200)", "originalRequest": { - "method": "GET", + "method": "POST", "header": [], "url": { "raw": "http://localhost:8000/profile/Test", @@ -3013,12 +3013,12 @@ } ], "cookie": [], - "body": "{profile_name}.pdf" + "body": "profile.pdf" }, { "name": "Get Profile with Device (200))", "originalRequest": { - "method": "GET", + "method": "POST", "header": [ { "key": "Content-Type", @@ -3060,12 +3060,12 @@ } ], "cookie": [], - "body": "report.pdf" + "body": "profile.pdf" }, { "name": "Profile Not Found (404)", "originalRequest": { - "method": "GET", + "method": "POST", "header": [], "url": { "raw": "http://localhost:8000/profile/NonExistingProfile", @@ -3144,7 +3144,7 @@ { "name": "Internal Server Error (500) Copy", "originalRequest": { - "method": "GET", + "method": "POST", "header": [], "url": { "raw": "http://localhost:8000/profile/Test", @@ -3554,6 +3554,41 @@ "body": "{\n \"error\": \"Invalid request received\"\n}" } ] + }, + { + "name": "http://localhost:8000/profile/Test", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\"mac_addr\": \"non_existing_mac_addr\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profile/Test", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profile", + "Test" + ] + } + }, + "response": [] } ] } \ No newline at end of file