From f308c22d24470cbe5a98fcaea88e4891d33053cb Mon Sep 17 00:00:00 2001 From: MariusBaldovin Date: Fri, 19 Jul 2024 21:01:55 +0100 Subject: [PATCH 01/17] added tests for profile endpoints --- testing/api/test_api.py | 125 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/testing/api/test_api.py b/testing/api/test_api.py index 75811e3bb..decdef7df 100644 --- a/testing/api/test_api.py +++ b/testing/api/test_api.py @@ -1135,3 +1135,128 @@ def test_create_invalid_chars(empty_devices_dir, testrun): # pylint: disable=W06 timeout=5) print(r.text) print(r.status_code) + + +# --------------------tests for profile endpoints--------------------------------- + +def load_json(file_name): + file_path = os.path.join(os.path.dirname(__file__), 'profiles', file_name) + with open(file_path, 'r') as file: + return json.load(file) + +#test for profiles format +def test_get_profiles_format(testrun): # pylint: disable=W0613 + r = requests.get(f"{API}/profiles/format", timeout=5) + assert r.status_code == 200 + response = json.loads(r.text) + assert isinstance(response, list) # Ensure the response is a list + + # Check that each item in the response has keys "questions" and "type" + for item in response: + assert "question" in item + assert "type" in item + +#test for get profiles +def test_get_profiles(testrun): # pylint: disable=W0613 + r = requests.get(f"{API}/profiles", timeout=5) + assert r.status_code == 200 + response = json.loads(r.text) + assert isinstance(response, list) # check if response is a list + + # Check that each profile has the expected fields + for profile in response: + assert "name" in profile + assert "status" in profile + assert "created" in profile + assert "version" in profile + assert "questions" in profile + assert isinstance(profile["questions"], list) #check if questions values is list + + #check that "questions" has the expected fields + for element in profile["questions"]: + assert isinstance(element, dict) #check if each element is dict + assert "question" in element + assert "type" in element + assert "answer" in element + +#check if profile exists (helper function) +def profile_exists(profile_name): + r = requests.get(f"{API}/profiles", timeout=5) + assert r.status_code == 200 + profiles = r.json() + return any(p["name"] == profile_name for p in profiles) + +#test for create profile if not exists +def test_profile_create_profile(testrun): + new_profile = load_json('new_profile.json') + profile_name = new_profile["name"] + + assert not profile_exists(profile_name), f"Profile '{profile_name}' exists" + + r = requests.post(f"{API}/profiles", data=json.dumps(new_profile), timeout=5) + assert r.status_code == 201, f"Expected status code 201, got {r.status_code}" + response = r.json() + assert "success" in response, f"Expected 'success' in response, got {response}" + + # Verify profile creation + r = requests.get(f"{API}/profiles", timeout=5) + assert r.status_code == 200, f"Expected status code 200, got {r.status_code}" + profiles = r.json() + created_profile = next((p for p in profiles if p["name"] == profile_name), None) + assert created_profile is not None, f"Profile '{profile_name}' not found" + assert created_profile["status"] == "New Draft" + +#test for update profile when exists +def test_profile_update_profile(testrun): + updated_profile = load_json('updated_profile.json') + profile_name = updated_profile["name"] + + assert profile_exists(profile_name), f"Profile '{profile_name}' does not exist. Update test cannot proceed." + + r = requests.post(f"{API}/profiles", data=json.dumps(updated_profile), timeout=5) + assert r.status_code == 200, f"Expected status code 200, got {r.status_code}" + response = r.json() + assert "success" in response, f"Expected 'success' in response, got {response}" + + # Verify profile update + r = requests.get(f"{API}/profiles", timeout=5) + assert r.status_code == 200, f"Expected status code 200, got {r.status_code}" + profiles = r.json() + updated_profile_check = next((p for p in profiles if p["name"] == profile_name), None) + assert updated_profile_check is not None, f"Profile '{profile_name}' not found" + assert updated_profile_check["status"] == "Updated Draft" + +#test for delete profile +def test_profile_delete_profile(testrun): + profile = load_json('updated_profile.json') + profile_name = profile["name"] + + #create profile if not exists + if not profile_exists(profile_name): + r = requests.post(f"{API}/profiles", data=json.dumps(new_profile), timeout=5) + assert r.status_code == 201, f"Expected status code 201, got {r.status_code}" + + # Verify the profile exists + r = requests.get(f"{API}/profiles", timeout=5) + assert r.status_code == 200 + profiles = r.json() + + #display all profiles names + for profile in profiles: + print(profile["name"]) + + profile_to_delete = next(p for p in profiles if p["name"] == profile_name) + assert profile_to_delete is not None + + #delete profile + r = requests.delete(f"{API}/profiles", data=json.dumps(profile_to_delete), timeout=5) + assert r.status_code == 200 + response = json.loads(r.text) + assert "success" in response #check if the response contains "success" key + + #check if the profile has been deleted + r = requests.get(f"{API}/profiles", timeout=5) + assert r.status_code == 200 + profiles = json.loads(r.text) + deleted_profile = next((p for p in profiles if p["name"] == profile_name), None) + assert deleted_profile is None From 2160792a591d950a216043406448cf9ec145457f Mon Sep 17 00:00:00 2001 From: MariusBaldovin Date: Sat, 20 Jul 2024 23:28:44 +0100 Subject: [PATCH 02/17] Modified test_start_testrun_started_successfully payload to match the expected json format, updated the profile endpoints tests --- framework/python/src/common/session.py | 96 ++++++------ testing/api/profiles/new_profile.json | 145 +++++++++++++++++ testing/api/profiles/new_profile_2.json | 145 +++++++++++++++++ testing/api/profiles/updated_profile.json | 146 +++++++++++++++++ testing/api/test_api.py | 183 ++++++++++++++++------ 5 files changed, 618 insertions(+), 97 deletions(-) create mode 100644 testing/api/profiles/new_profile.json create mode 100644 testing/api/profiles/new_profile_2.json create mode 100644 testing/api/profiles/updated_profile.json diff --git a/framework/python/src/common/session.py b/framework/python/src/common/session.py index f555a9732..f1c934a13 100644 --- a/framework/python/src/common/session.py +++ b/framework/python/src/common/session.py @@ -456,74 +456,72 @@ def _get_profile_question(self, profile_json, question): return None def update_profile(self, profile_json): - profile_name = profile_json['name'] + LOGGER.debug(f"Profile name: {profile_name}") # Add version, timestamp and status profile_json['version'] = self.get_version() profile_json['created'] = datetime.datetime.now().strftime('%Y-%m-%d') if 'status' in profile_json and profile_json.get('status') == 'Valid': - # Attempting to submit a risk profile, we need to check it - - # Check all questions have been answered - all_questions_answered = True - - for question in self.get_profiles_format(): - - # Check question is present - profile_question = self._get_profile_question(profile_json, - question.get('question')) - - if profile_question is not None: - - # Check answer is present - if 'answer' not in profile_question: - LOGGER.error('Missing answer for question: ' + - question.get('question')) - all_questions_answered = False - - else: - LOGGER.error('Missing question: ' + question.get('question')) - all_questions_answered = False - - if not all_questions_answered: - LOGGER.error('Not all questions answered') - return None + LOGGER.debug("Attempting to set profile status to 'Valid'. Checking all questions are answered.") + # Check all questions have been answered + all_questions_answered = True + + for question in self.get_profiles_format(): + profile_question = self._get_profile_question(profile_json, question.get('question')) + if profile_question is not None: + if 'answer' not in profile_question: + LOGGER.error(f"Missing answer for question: {question.get('question')}") + all_questions_answered = False + else: + LOGGER.error(f"Missing question: {question.get('question')}") + all_questions_answered = False + if not all_questions_answered: + LOGGER.error('Not all questions answered') + return None else: - profile_json['status'] = 'Draft' + profile_json['status'] = 'Draft' risk_profile = self.get_profile(profile_name) + LOGGER.debug(f"Retrieved risk profile: {risk_profile}") if risk_profile is None: - - # Create a new risk profile - risk_profile = RiskProfile( - profile_json=profile_json, - profile_format=self._profile_format) - self._profiles.append(risk_profile) - + LOGGER.debug("Creating a new risk profile.") + risk_profile = RiskProfile( + profile_json=profile_json, + profile_format=self._profile_format) + self._profiles.append(risk_profile) else: - - # Update the profile - risk_profile.update(profile_json, profile_format=self._profile_format) - - # Check if name has changed - if 'rename' in profile_json: - old_name = profile_json.get('name') - - # Delete the original file - os.remove(os.path.join(PROFILES_DIR, old_name + '.json')) + LOGGER.debug("Updating the existing profile.") + risk_profile.update(profile_json, profile_format=self._profile_format) + + # Check if name has changed + if 'rename' in profile_json and profile_json['rename'] != risk_profile.name: + old_name = risk_profile.name + new_name = profile_json['rename'] + LOGGER.debug(f"Renaming profile from {old_name} to {new_name}") + risk_profile.name = new_name + try: + os.remove(os.path.join(PROFILES_DIR, old_name + '.json')) + except OSError as e: + LOGGER.error(f"Error deleting old profile file: {e}") + return None # Write file to disk - with open(os.path.join(PROFILES_DIR, risk_profile.name + '.json'), - 'w', - encoding='utf-8') as f: - f.write(risk_profile.to_json(pretty=True)) + try: + with open(os.path.join(PROFILES_DIR, risk_profile.name + '.json'), + 'w', + encoding='utf-8') as f: + f.write(risk_profile.to_json(pretty=True)) + except Exception as e: + LOGGER.error(f"Error writing profile to disk: {e}") + return None return risk_profile + def check_profile_status(self, profile): if profile.status == 'Valid': diff --git a/testing/api/profiles/new_profile.json b/testing/api/profiles/new_profile.json new file mode 100644 index 000000000..7043f6cfa --- /dev/null +++ b/testing/api/profiles/new_profile.json @@ -0,0 +1,145 @@ +{ + "name": "New Profile", + "status": "Draft", + "created": "2024-05-23 12:38:26", + "version": "v1.3", + "questions": [ + { + "question": "What type of device is this?", + "type": "select", + "options": [ + "IoT Sensor", + "IoT Controller", + "Smart Device", + "Something else" + ], + "answer": "IoT Sensor", + "validation": { + "required": true + } + }, + { + "question": "How will this device be used at Google?", + "type": "text-long", + "answer": "Installed in a building", + "validation": { + "max": "128", + "required": true + } + }, + { + "question": "What is the email of the device owner(s)?", + "type": "email-multiple", + "answer": "boddey@google.com, cmeredith@google.com", + "validation": { + "required": true, + "max": "128" + } + }, + { + "question": "Is this device going to be managed by Google or a third party?", + "type": "select", + "options": [ + "Google", + "Third Party" + ], + "answer": "Google", + "validation": { + "required": true + } + }, + { + "question": "Will the third-party device administrator be able to grant access to authorized Google personnel upon request?", + "type": "select", + "options": [ + "Yes", + "No", + "N/A" + ], + "default": "N/A", + "answer": "Yes", + "validation": { + "required": true + } + }, + { + "question": "Are any of the following statements true about your device?", + "description": "This tells us about the data your device will collect", + "type": "select-multiple", + "answer": [ + 0, + 2 + ], + "options": [ + "The device collects any Personal Identifiable Information (PII) or Personal Health Information (PHI)", + "The device collects intellectual property and trade secrets, sensitive business data, critical infrastructure data, identity assets", + "The device stream confidential business data in real-time (seconds)?" + ] + }, + { + "question": "Which of the following statements are true about this device?", + "description": "This tells us about the types of data that are transmitted from this device and how the transmission is performed from a technical standpoint.", + "type": "select-multiple", + "answer": [ + 0, + 1, + 5 + ], + "options": [ + "PII/PHI, confidential business data, or crown jewel data is transmitted to a destination outside Alphabet's ownership", + "Data transmission occurs across less-trusted networks (e.g. the internet).", + "A failure in data transmission would likely have a substantial negative impact (https://www.rra.rocks/docs/standard_levels#levels-definitions)", + "A confidentiality breach during transmission would have a substantial negative impact", + "The device encrypts data during transmission", + "The device network protocol is well-established and currently used by Google" + ] + }, + { + "question": "Does the network protocol assure server-to-client identity verification?", + "type": "select", + "answer": "Yes", + "options": [ + "Yes", + "No", + "I don't know" + ], + "validation": { + "required": true + } + }, + { + "question": "Click the statements that best describe the characteristics of this device.", + "description": "This tells us about how this device is managed remotely.", + "type": "select-multiple", + "answer": [ + 0, + 1, + 2 + ], + "options": [ + "PII/PHI, or confidential business data is accessible from the device without authentication", + "Unrecoverable actions (e.g. disk wipe) can be performed remotely", + "Authentication is required for remote access", + "The management interface is accessible from the public internet", + "Static credentials are used for administration" + ] + }, + { + "question": "Are any of the following statements true about this device?", + "description": "This informs us about what other systems and processes this device is a part of.", + "type": "select-multiple", + "answer": [ + 2, + 3 + ], + "options": [ + "The device monitors an environment for active risks to human life.", + "The device is used to convey people, or critical property.", + "The device controls robotics in human-accessible spaces.", + "The device controls physical access systems.", + "The device is involved in processes required by regulations, or compliance. (ex. privacy, security, safety regulations)", + "The device's failure would cause faults in other high-criticality processes." + ] + } + ] +} \ No newline at end of file diff --git a/testing/api/profiles/new_profile_2.json b/testing/api/profiles/new_profile_2.json new file mode 100644 index 000000000..51782e187 --- /dev/null +++ b/testing/api/profiles/new_profile_2.json @@ -0,0 +1,145 @@ +{ + "name": "New Profile 2", + "status": "Draft", + "created": "2024-05-23 12:38:26", + "version": "v1.3", + "questions": [ + { + "question": "What type of device is this?", + "type": "select", + "options": [ + "IoT Sensor", + "IoT Controller", + "Smart Device", + "Something else" + ], + "answer": "IoT Sensor", + "validation": { + "required": true + } + }, + { + "question": "How will this device be used at Google?", + "type": "text-long", + "answer": "Installed in a building", + "validation": { + "max": "128", + "required": true + } + }, + { + "question": "What is the email of the device owner(s)?", + "type": "email-multiple", + "answer": "boddey@google.com, cmeredith@google.com", + "validation": { + "required": true, + "max": "128" + } + }, + { + "question": "Is this device going to be managed by Google or a third party?", + "type": "select", + "options": [ + "Google", + "Third Party" + ], + "answer": "Google", + "validation": { + "required": true + } + }, + { + "question": "Will the third-party device administrator be able to grant access to authorized Google personnel upon request?", + "type": "select", + "options": [ + "Yes", + "No", + "N/A" + ], + "default": "N/A", + "answer": "Yes", + "validation": { + "required": true + } + }, + { + "question": "Are any of the following statements true about your device?", + "description": "This tells us about the data your device will collect", + "type": "select-multiple", + "answer": [ + 0, + 2 + ], + "options": [ + "The device collects any Personal Identifiable Information (PII) or Personal Health Information (PHI)", + "The device collects intellectual property and trade secrets, sensitive business data, critical infrastructure data, identity assets", + "The device stream confidential business data in real-time (seconds)?" + ] + }, + { + "question": "Which of the following statements are true about this device?", + "description": "This tells us about the types of data that are transmitted from this device and how the transmission is performed from a technical standpoint.", + "type": "select-multiple", + "answer": [ + 0, + 1, + 5 + ], + "options": [ + "PII/PHI, confidential business data, or crown jewel data is transmitted to a destination outside Alphabet's ownership", + "Data transmission occurs across less-trusted networks (e.g. the internet).", + "A failure in data transmission would likely have a substantial negative impact (https://www.rra.rocks/docs/standard_levels#levels-definitions)", + "A confidentiality breach during transmission would have a substantial negative impact", + "The device encrypts data during transmission", + "The device network protocol is well-established and currently used by Google" + ] + }, + { + "question": "Does the network protocol assure server-to-client identity verification?", + "type": "select", + "answer": "Yes", + "options": [ + "Yes", + "No", + "I don't know" + ], + "validation": { + "required": true + } + }, + { + "question": "Click the statements that best describe the characteristics of this device.", + "description": "This tells us about how this device is managed remotely.", + "type": "select-multiple", + "answer": [ + 0, + 1, + 2 + ], + "options": [ + "PII/PHI, or confidential business data is accessible from the device without authentication", + "Unrecoverable actions (e.g. disk wipe) can be performed remotely", + "Authentication is required for remote access", + "The management interface is accessible from the public internet", + "Static credentials are used for administration" + ] + }, + { + "question": "Are any of the following statements true about this device?", + "description": "This informs us about what other systems and processes this device is a part of.", + "type": "select-multiple", + "answer": [ + 2, + 3 + ], + "options": [ + "The device monitors an environment for active risks to human life.", + "The device is used to convey people, or critical property.", + "The device controls robotics in human-accessible spaces.", + "The device controls physical access systems.", + "The device is involved in processes required by regulations, or compliance. (ex. privacy, security, safety regulations)", + "The device's failure would cause faults in other high-criticality processes." + ] + } + ] +} \ No newline at end of file diff --git a/testing/api/profiles/updated_profile.json b/testing/api/profiles/updated_profile.json new file mode 100644 index 000000000..a659cd937 --- /dev/null +++ b/testing/api/profiles/updated_profile.json @@ -0,0 +1,146 @@ +{ + "name": "New Profile", + "rename": "Updated Profile", + "status": "Draft", + "created": "2024-05-23 12:38:26", + "version": "v1.3", + "questions": [ + { + "question": "What type of device is this?", + "type": "select", + "options": [ + "IoT Sensor", + "IoT Controller", + "Smart Device", + "Something else" + ], + "answer": "IoT Sensor", + "validation": { + "required": true + } + }, + { + "question": "How will this device be used at Google?", + "type": "text-long", + "answer": "Installed in a building", + "validation": { + "max": "128", + "required": true + } + }, + { + "question": "What is the email of the device owner(s)?", + "type": "email-multiple", + "answer": "boddey@google.com, cmeredith@google.com", + "validation": { + "required": true, + "max": "128" + } + }, + { + "question": "Is this device going to be managed by Google or a third party?", + "type": "select", + "options": [ + "Google", + "Third Party" + ], + "answer": "Google", + "validation": { + "required": true + } + }, + { + "question": "Will the third-party device administrator be able to grant access to authorized Google personnel upon request?", + "type": "select", + "options": [ + "Yes", + "No", + "N/A" + ], + "default": "N/A", + "answer": "Yes", + "validation": { + "required": true + } + }, + { + "question": "Are any of the following statements true about your device?", + "description": "This tells us about the data your device will collect", + "type": "select-multiple", + "answer": [ + 0, + 2 + ], + "options": [ + "The device collects any Personal Identifiable Information (PII) or Personal Health Information (PHI)", + "The device collects intellectual property and trade secrets, sensitive business data, critical infrastructure data, identity assets", + "The device stream confidential business data in real-time (seconds)?" + ] + }, + { + "question": "Which of the following statements are true about this device?", + "description": "This tells us about the types of data that are transmitted from this device and how the transmission is performed from a technical standpoint.", + "type": "select-multiple", + "answer": [ + 0, + 1, + 5 + ], + "options": [ + "PII/PHI, confidential business data, or crown jewel data is transmitted to a destination outside Alphabet's ownership", + "Data transmission occurs across less-trusted networks (e.g. the internet).", + "A failure in data transmission would likely have a substantial negative impact (https://www.rra.rocks/docs/standard_levels#levels-definitions)", + "A confidentiality breach during transmission would have a substantial negative impact", + "The device encrypts data during transmission", + "The device network protocol is well-established and currently used by Google" + ] + }, + { + "question": "Does the network protocol assure server-to-client identity verification?", + "type": "select", + "answer": "Yes", + "options": [ + "Yes", + "No", + "I don't know" + ], + "validation": { + "required": true + } + }, + { + "question": "Click the statements that best describe the characteristics of this device.", + "description": "This tells us about how this device is managed remotely.", + "type": "select-multiple", + "answer": [ + 0, + 1, + 2 + ], + "options": [ + "PII/PHI, or confidential business data is accessible from the device without authentication", + "Unrecoverable actions (e.g. disk wipe) can be performed remotely", + "Authentication is required for remote access", + "The management interface is accessible from the public internet", + "Static credentials are used for administration" + ] + }, + { + "question": "Are any of the following statements true about this device?", + "description": "This informs us about what other systems and processes this device is a part of.", + "type": "select-multiple", + "answer": [ + 2, + 3 + ], + "options": [ + "The device monitors an environment for active risks to human life.", + "The device is used to convey people, or critical property.", + "The device controls robotics in human-accessible spaces.", + "The device controls physical access systems.", + "The device is involved in processes required by regulations, or compliance. (ex. privacy, security, safety regulations)", + "The device's failure would cause faults in other high-criticality processes." + ] + } + ] +} \ No newline at end of file diff --git a/testing/api/test_api.py b/testing/api/test_api.py index decdef7df..063905700 100644 --- a/testing/api/test_api.py +++ b/testing/api/test_api.py @@ -34,6 +34,7 @@ LOG_PATH = "/tmp/testrun.log" TEST_SITE_DIR = ".." + DEVICES_DIRECTORY = "local/devices" TESTING_DEVICES = "../device_configs" SYSTEM_CONFIG_PATH = "local/system.json" @@ -88,7 +89,6 @@ def stop_test_device(device_name): ) print(cmd.stdout) - def docker_logs(device_name): """ Print docker logs from given docker container name """ cmd = subprocess.run( @@ -223,7 +223,7 @@ def local_get_devices(): ) ) - + def test_get_system_interfaces(testrun): # pylint: disable=W0613 """Tests API system interfaces against actual local interfaces""" r = requests.get(f"{API}/system/interfaces", timeout=5) @@ -234,7 +234,7 @@ def test_get_system_interfaces(testrun): # pylint: disable=W0613 # schema expects a flat list assert all(isinstance(x, str) for x in response) - + def test_status_idle(testrun): # pylint: disable=W0613 until_true( lambda: query_system_status().lower() == "idle", @@ -297,6 +297,7 @@ def test_status_non_compliant(testing_devices, testrun): # pylint: disable=W0613 stop_test_device("x123") + def test_create_get_devices(empty_devices_dir, testrun): # pylint: disable=W0613 device_1 = { "manufacturer": "Google", @@ -359,7 +360,7 @@ def test_create_get_devices(empty_devices_dir, testrun): # pylint: disable=W0613 [device_1[key], device_2[key]] ) - + def test_delete_device_success(empty_devices_dir, testrun): # pylint: disable=W0613 device_1 = { "manufacturer": "Google", @@ -437,7 +438,7 @@ def test_delete_device_success(empty_devices_dir, testrun): # pylint: disable=W0 [device_2[key]] ) - + def test_delete_device_not_found(empty_devices_dir, testrun): # pylint: disable=W0613 device_1 = { "manufacturer": "Google", @@ -476,7 +477,7 @@ def test_delete_device_not_found(empty_devices_dir, testrun): # pylint: disable= assert r.status_code == 404 assert len(local_get_devices()) == 0 - + def test_delete_device_no_mac(empty_devices_dir, testrun): # pylint: disable=W0613 device_1 = { "manufacturer": "Google", @@ -550,11 +551,17 @@ def test_delete_device_testrun_running(testing_devices, testrun): # pylint: disa timeout=5) assert r.status_code == 403 - + def test_start_testrun_started_successfully( testing_devices, # pylint: disable=W0613 testrun): # pylint: disable=W0613 - payload = {"device": {"mac_addr": BASELINE_MAC_ADDR, "firmware": "asd"}} + payload = {"device": {"mac_addr": BASELINE_MAC_ADDR, "firmware": "asd", "test_modules": { + "dns": {"enabled": False}, + "connection": {"enabled": True}, + "ntp": {"enabled": False}, + "baseline": {"enabled": False}, + "nmap": {"enabled": False} + }}} r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) assert r.status_code == 200 @@ -583,6 +590,7 @@ def test_start_testrun_already_in_progress( r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) assert r.status_code == 409 + def test_start_system_not_configured_correctly( empty_devices_dir, # pylint: disable=W0613 testrun): # pylint: disable=W0613 @@ -611,7 +619,7 @@ def test_start_system_not_configured_correctly( timeout=10) assert r.status_code == 500 - + def test_start_device_not_found(empty_devices_dir, # pylint: disable=W0613 testrun): # pylint: disable=W0613 device_1 = { @@ -644,7 +652,7 @@ def test_start_device_not_found(empty_devices_dir, # pylint: disable=W0613 timeout=10) assert r.status_code == 404 - + def test_start_missing_device_information( empty_devices_dir, # pylint: disable=W0613 testrun): # pylint: disable=W0613 @@ -673,7 +681,7 @@ def test_start_missing_device_information( timeout=10) assert r.status_code == 400 - + def test_create_device_already_exists( empty_devices_dir, # pylint: disable=W0613 testrun): # pylint: disable=W0613 @@ -703,7 +711,7 @@ def test_create_device_already_exists( print(r.text) assert r.status_code == 409 - + def test_create_device_invalid_json( empty_devices_dir, # pylint: disable=W0613 testrun): # pylint: disable=W0613 @@ -716,7 +724,7 @@ def test_create_device_invalid_json( print(r.text) assert r.status_code == 400 - + def test_create_device_invalid_request( empty_devices_dir, # pylint: disable=W0613 testrun): # pylint: disable=W0613 @@ -727,7 +735,7 @@ def test_create_device_invalid_request( print(r.text) assert r.status_code == 400 - + def test_device_edit_device( testing_devices, # pylint: disable=W0613 testrun): # pylint: disable=W0613 @@ -776,7 +784,7 @@ def test_device_edit_device( assert updated_device_api["model"] == new_model assert updated_device_api["test_modules"] == new_test_modules - + def test_device_edit_device_not_found( empty_devices_dir, # pylint: disable=W0613 testrun): # pylint: disable=W0613 @@ -814,7 +822,7 @@ def test_device_edit_device_not_found( assert r.status_code == 404 - + def test_device_edit_device_incorrect_json_format( empty_devices_dir, # pylint: disable=W0613 testrun): # pylint: disable=W0613 @@ -847,7 +855,7 @@ def test_device_edit_device_incorrect_json_format( assert r.status_code == 400 - + def test_device_edit_device_with_mac_already_exists( empty_devices_dir, # pylint: disable=W0613 testrun): # pylint: disable=W0613 @@ -904,13 +912,14 @@ def test_device_edit_device_with_mac_already_exists( assert r.status_code == 409 - + def test_system_latest_version(testrun): # pylint: disable=W0613 r = requests.get(f"{API}/system/version", timeout=5) assert r.status_code == 200 updated_system_version = json.loads(r.text)["update_available"] assert updated_system_version is False + def test_get_system_config(testrun): # pylint: disable=W0613 r = requests.get(f"{API}/system/config", timeout=5) @@ -936,7 +945,7 @@ def test_get_system_config(testrun): # pylint: disable=W0613 == api_config["network"]["internet_intf"] ) - + def test_invalid_path_get(testrun): # pylint: disable=W0613 r = requests.get(f"{API}/blah/blah", timeout=5) response = json.loads(r.text) @@ -1041,7 +1050,7 @@ def test_stop_running_test(testing_devices, testrun): # pylint: disable=W0613 assert response["status"] == "Cancelled" - + def test_stop_running_not_running(testrun): # pylint: disable=W0613 # Validate response r = requests.post(f"{API}/system/stop", @@ -1110,7 +1119,7 @@ def test_multiple_runs(testing_devices, testrun): # pylint: disable=W0613 stop_test_device("x123") - + def test_create_invalid_chars(empty_devices_dir, testrun): # pylint: disable=W0613 # local_delete_devices(ALL_DEVICES) # We must start test run with no devices in local/devices for this test @@ -1137,12 +1146,36 @@ def test_create_invalid_chars(empty_devices_dir, testrun): # pylint: disable=W06 print(r.status_code) -# --------------------tests for profile endpoints--------------------------------- +#Tests for profile endpoints + +PROFILES_DIRECTORY = "local/risk_profiles" + +#delete all profiles form risk_profile folder +def delete_all_profiles(): + profiles_path = Path(PROFILES_DIRECTORY) + if profiles_path.exists() and profiles_path.is_dir(): + shutil.rmtree(profiles_path) #remove risk_profiles + profiles_path.mkdir(parents=True, exist_ok=True) #create risk_profiles + +#clean the profiles before and after each test +@pytest.fixture(scope="function") +def clean_profiles_dir(): + delete_all_profiles() + yield + delete_all_profiles() +#load the json files def load_json(file_name): file_path = os.path.join(os.path.dirname(__file__), 'profiles', file_name) with open(file_path, 'r') as file: return json.load(file) + +#check if profile exists +def profile_exists(profile_name): + r = requests.get(f"{API}/profiles", timeout=5) + assert r.status_code == 200 + profiles = r.json() + return any(p["name"] == profile_name for p in profiles) #test for profiles format def test_get_profiles_format(testrun): # pylint: disable=W0613 @@ -1155,14 +1188,32 @@ def test_get_profiles_format(testrun): # pylint: disable=W0613 for item in response: assert "question" in item assert "type" in item - -#test for get profiles -def test_get_profiles(testrun): # pylint: disable=W0613 + +#test for get profiles (no profile, one profile, two profiles) +def test_get_profiles(testrun, clean_profiles_dir): # pylint: disable=W0613 + #test no profiles r = requests.get(f"{API}/profiles", timeout=5) assert r.status_code == 200 response = json.loads(r.text) assert isinstance(response, list) # check if response is a list + assert len(response) == 0 #check if list is empty + #create the first profile + new_profile = load_json('new_profile.json') + profile_name = new_profile["name"] + assert not profile_exists(profile_name), f"Profile '{profile_name}' exists" + + r = requests.post(f"{API}/profiles", data=json.dumps(new_profile), timeout=5) + assert r.status_code == 201, f"Expected status code 201, got {r.status_code}" + response = r.json() + assert "success" in response, f"Expected 'success' in response, got {response}" + + #get the profile + r = requests.get(f"{API}/profiles", timeout=5) + assert r.status_code == 200 + response = json.loads(r.text) + assert isinstance(response, list) # check if response is a list + assert len(response) == 1 #check if there is one profile # Check that each profile has the expected fields for profile in response: assert "name" in profile @@ -1178,16 +1229,42 @@ def test_get_profiles(testrun): # pylint: disable=W0613 assert "question" in element assert "type" in element assert "answer" in element - -#check if profile exists (helper function) -def profile_exists(profile_name): + + + #create the second profile + new_profile_2 = load_json('new_profile_2.json') + profile_name_2 = new_profile_2["name"] + assert not profile_exists(profile_name_2), f"Profile '{profile_name_2}' exists" + + r = requests.post(f"{API}/profiles", data=json.dumps(new_profile_2), timeout=5) + assert r.status_code == 201, f"Expected status code 200, got {r.status_code}" + response = r.json() + assert "success" in response, f"Expected 'success' in response, got {response}" + + #get the two profiles r = requests.get(f"{API}/profiles", timeout=5) assert r.status_code == 200 - profiles = r.json() - return any(p["name"] == profile_name for p in profiles) + response = json.loads(r.text) + assert isinstance(response, list) # check if response is a list + assert len(response) == 2 #check if there are 2 profiles + # Check that each profile has the expected fields + for profile in response: + assert "name" in profile + assert "status" in profile + assert "created" in profile + assert "version" in profile + assert "questions" in profile + assert isinstance(profile["questions"], list) #check if questions values is list + + #check that "questions" has the expected fields + for element in profile["questions"]: + assert isinstance(element, dict) #check if each element is dict + assert "question" in element + assert "type" in element + assert "answer" in element #test for create profile if not exists -def test_profile_create_profile(testrun): +def test_create_profile(testrun, clean_profiles_dir): new_profile = load_json('new_profile.json') profile_name = new_profile["name"] @@ -1204,51 +1281,61 @@ def test_profile_create_profile(testrun): profiles = r.json() created_profile = next((p for p in profiles if p["name"] == profile_name), None) assert created_profile is not None, f"Profile '{profile_name}' not found" - assert created_profile["status"] == "New Draft" + #test for update profile when exists -def test_profile_update_profile(testrun): +def test_update_profile(testrun, clean_profiles_dir): + new_profile = load_json('new_profile.json') updated_profile = load_json('updated_profile.json') - profile_name = updated_profile["name"] + profile_name = new_profile["name"] + updated_profile_name = updated_profile["rename"] + + #create the profile + r = requests.post(f"{API}/profiles", data=json.dumps(new_profile), timeout=5) + assert r.status_code == 201, f"Expected status code 201, got {r.status_code}" + response = r.json() + assert "success" in response, f"Expected 'success' in response, got {response}" assert profile_exists(profile_name), f"Profile '{profile_name}' does not exist. Update test cannot proceed." + #update the profile r = requests.post(f"{API}/profiles", data=json.dumps(updated_profile), timeout=5) assert r.status_code == 200, f"Expected status code 200, got {r.status_code}" response = r.json() assert "success" in response, f"Expected 'success' in response, got {response}" - # Verify profile update + # verify profile update r = requests.get(f"{API}/profiles", timeout=5) assert r.status_code == 200, f"Expected status code 200, got {r.status_code}" profiles = r.json() - updated_profile_check = next((p for p in profiles if p["name"] == profile_name), None) + updated_profile_check = next((p for p in profiles if p["name"] == updated_profile_name), None) assert updated_profile_check is not None, f"Profile '{profile_name}' not found" - assert updated_profile_check["status"] == "Updated Draft" + #assert updated_profile_check["status"] == "Draft" #test for delete profile -def test_profile_delete_profile(testrun): - profile = load_json('updated_profile.json') - profile_name = profile["name"] - - #create profile if not exists - if not profile_exists(profile_name): - r = requests.post(f"{API}/profiles", data=json.dumps(new_profile), timeout=5) - assert r.status_code == 201, f"Expected status code 201, got {r.status_code}" +def test_delete_profile(testrun, clean_profiles_dir): +#create the profile + new_profile = load_json('new_profile.json') + profile_name = new_profile["name"] + assert not profile_exists(profile_name), f"Profile '{profile_name}' exists" + + r = requests.post(f"{API}/profiles", data=json.dumps(new_profile), timeout=5) + assert r.status_code == 201, f"Expected status code 201, got {r.status_code}" + response = r.json() + assert "success" in response, f"Expected 'success' in response, got {response}" # Verify the profile exists r = requests.get(f"{API}/profiles", timeout=5) assert r.status_code == 200 profiles = r.json() - #display all profiles names + #display all profiles names in case of failure for profile in profiles: print(profile["name"]) + #delete the profile profile_to_delete = next(p for p in profiles if p["name"] == profile_name) assert profile_to_delete is not None - - #delete profile r = requests.delete(f"{API}/profiles", data=json.dumps(profile_to_delete), timeout=5) assert r.status_code == 200 response = json.loads(r.text) From acd63b93c9185a396aee9257b4a0a3cae6c9c368 Mon Sep 17 00:00:00 2001 From: MariusBaldovin Date: Sun, 21 Jul 2024 09:30:12 +0100 Subject: [PATCH 03/17] fixed the pylint errors from test_api.py --- testing/api/test_api.py | 356 +++++++++++++++++++++------------------- 1 file changed, 187 insertions(+), 169 deletions(-) diff --git a/testing/api/test_api.py b/testing/api/test_api.py index 063905700..06bd0da23 100644 --- a/testing/api/test_api.py +++ b/testing/api/test_api.py @@ -124,9 +124,9 @@ def testrun(request): # pylint: disable=W0613 stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding="utf-8", - preexec_fn=os.setsid + preexec_fn=os.setsid #start_new_session=True ) as proc: - + while True: try: outs = proc.communicate(timeout=1)[0] @@ -223,7 +223,7 @@ def local_get_devices(): ) ) - + def test_get_system_interfaces(testrun): # pylint: disable=W0613 """Tests API system interfaces against actual local interfaces""" r = requests.get(f"{API}/system/interfaces", timeout=5) @@ -234,7 +234,7 @@ def test_get_system_interfaces(testrun): # pylint: disable=W0613 # schema expects a flat list assert all(isinstance(x, str) for x in response) - + def test_status_idle(testrun): # pylint: disable=W0613 until_true( lambda: query_system_status().lower() == "idle", @@ -297,7 +297,7 @@ def test_status_non_compliant(testing_devices, testrun): # pylint: disable=W0613 stop_test_device("x123") - + def test_create_get_devices(empty_devices_dir, testrun): # pylint: disable=W0613 device_1 = { "manufacturer": "Google", @@ -360,7 +360,7 @@ def test_create_get_devices(empty_devices_dir, testrun): # pylint: disable=W0613 [device_1[key], device_2[key]] ) - + def test_delete_device_success(empty_devices_dir, testrun): # pylint: disable=W0613 device_1 = { "manufacturer": "Google", @@ -438,7 +438,7 @@ def test_delete_device_success(empty_devices_dir, testrun): # pylint: disable=W0 [device_2[key]] ) - + def test_delete_device_not_found(empty_devices_dir, testrun): # pylint: disable=W0613 device_1 = { "manufacturer": "Google", @@ -477,7 +477,7 @@ def test_delete_device_not_found(empty_devices_dir, testrun): # pylint: disable= assert r.status_code == 404 assert len(local_get_devices()) == 0 - + def test_delete_device_no_mac(empty_devices_dir, testrun): # pylint: disable=W0613 device_1 = { "manufacturer": "Google", @@ -551,11 +551,13 @@ def test_delete_device_testrun_running(testing_devices, testrun): # pylint: disa timeout=5) assert r.status_code == 403 - + def test_start_testrun_started_successfully( testing_devices, # pylint: disable=W0613 testrun): # pylint: disable=W0613 - payload = {"device": {"mac_addr": BASELINE_MAC_ADDR, "firmware": "asd", "test_modules": { + payload = {"device": {"mac_addr": BASELINE_MAC_ADDR, + "firmware": "asd", + "test_modules": { "dns": {"enabled": False}, "connection": {"enabled": True}, "ntp": {"enabled": False}, @@ -590,7 +592,7 @@ def test_start_testrun_already_in_progress( r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) assert r.status_code == 409 - + def test_start_system_not_configured_correctly( empty_devices_dir, # pylint: disable=W0613 testrun): # pylint: disable=W0613 @@ -619,7 +621,7 @@ def test_start_system_not_configured_correctly( timeout=10) assert r.status_code == 500 - + def test_start_device_not_found(empty_devices_dir, # pylint: disable=W0613 testrun): # pylint: disable=W0613 device_1 = { @@ -652,7 +654,7 @@ def test_start_device_not_found(empty_devices_dir, # pylint: disable=W0613 timeout=10) assert r.status_code == 404 - + def test_start_missing_device_information( empty_devices_dir, # pylint: disable=W0613 testrun): # pylint: disable=W0613 @@ -681,7 +683,7 @@ def test_start_missing_device_information( timeout=10) assert r.status_code == 400 - + def test_create_device_already_exists( empty_devices_dir, # pylint: disable=W0613 testrun): # pylint: disable=W0613 @@ -711,7 +713,7 @@ def test_create_device_already_exists( print(r.text) assert r.status_code == 409 - + def test_create_device_invalid_json( empty_devices_dir, # pylint: disable=W0613 testrun): # pylint: disable=W0613 @@ -724,7 +726,7 @@ def test_create_device_invalid_json( print(r.text) assert r.status_code == 400 - + def test_create_device_invalid_request( empty_devices_dir, # pylint: disable=W0613 testrun): # pylint: disable=W0613 @@ -735,7 +737,7 @@ def test_create_device_invalid_request( print(r.text) assert r.status_code == 400 - + def test_device_edit_device( testing_devices, # pylint: disable=W0613 testrun): # pylint: disable=W0613 @@ -784,7 +786,7 @@ def test_device_edit_device( assert updated_device_api["model"] == new_model assert updated_device_api["test_modules"] == new_test_modules - + def test_device_edit_device_not_found( empty_devices_dir, # pylint: disable=W0613 testrun): # pylint: disable=W0613 @@ -822,7 +824,7 @@ def test_device_edit_device_not_found( assert r.status_code == 404 - + def test_device_edit_device_incorrect_json_format( empty_devices_dir, # pylint: disable=W0613 testrun): # pylint: disable=W0613 @@ -855,7 +857,7 @@ def test_device_edit_device_incorrect_json_format( assert r.status_code == 400 - + def test_device_edit_device_with_mac_already_exists( empty_devices_dir, # pylint: disable=W0613 testrun): # pylint: disable=W0613 @@ -912,14 +914,14 @@ def test_device_edit_device_with_mac_already_exists( assert r.status_code == 409 - + def test_system_latest_version(testrun): # pylint: disable=W0613 r = requests.get(f"{API}/system/version", timeout=5) assert r.status_code == 200 updated_system_version = json.loads(r.text)["update_available"] assert updated_system_version is False - + def test_get_system_config(testrun): # pylint: disable=W0613 r = requests.get(f"{API}/system/config", timeout=5) @@ -945,7 +947,7 @@ def test_get_system_config(testrun): # pylint: disable=W0613 == api_config["network"]["internet_intf"] ) - + def test_invalid_path_get(testrun): # pylint: disable=W0613 r = requests.get(f"{API}/blah/blah", timeout=5) response = json.loads(r.text) @@ -1050,7 +1052,7 @@ def test_stop_running_test(testing_devices, testrun): # pylint: disable=W0613 assert response["status"] == "Cancelled" - + def test_stop_running_not_running(testrun): # pylint: disable=W0613 # Validate response r = requests.post(f"{API}/system/stop", @@ -1119,7 +1121,7 @@ def test_multiple_runs(testing_devices, testrun): # pylint: disable=W0613 stop_test_device("x123") - + def test_create_invalid_chars(empty_devices_dir, testrun): # pylint: disable=W0613 # local_delete_devices(ALL_DEVICES) # We must start test run with no devices in local/devices for this test @@ -1152,177 +1154,187 @@ def test_create_invalid_chars(empty_devices_dir, testrun): # pylint: disable=W06 #delete all profiles form risk_profile folder def delete_all_profiles(): - profiles_path = Path(PROFILES_DIRECTORY) - if profiles_path.exists() and profiles_path.is_dir(): - shutil.rmtree(profiles_path) #remove risk_profiles - profiles_path.mkdir(parents=True, exist_ok=True) #create risk_profiles - + profiles_path = Path(PROFILES_DIRECTORY) + if profiles_path.exists() and profiles_path.is_dir(): + shutil.rmtree(profiles_path) #remove risk_profiles + profiles_path.mkdir(parents=True, exist_ok=True) #create risk_profiles + #clean the profiles before and after each test @pytest.fixture(scope="function") def clean_profiles_dir(): - delete_all_profiles() - yield - delete_all_profiles() + delete_all_profiles() + yield + delete_all_profiles() #load the json files def load_json(file_name): - file_path = os.path.join(os.path.dirname(__file__), 'profiles', file_name) - with open(file_path, 'r') as file: - return json.load(file) - + file_path = os.path.join(os.path.dirname(__file__), "profiles", file_name) + with open(file_path, "r", encoding="utf-8") as file: + return json.load(file) + #check if profile exists def profile_exists(profile_name): - r = requests.get(f"{API}/profiles", timeout=5) - assert r.status_code == 200 - profiles = r.json() - return any(p["name"] == profile_name for p in profiles) + r = requests.get(f"{API}/profiles", timeout=5) + assert r.status_code == 200 + profiles = r.json() + return any(p["name"] == profile_name for p in profiles) #test for profiles format def test_get_profiles_format(testrun): # pylint: disable=W0613 - r = requests.get(f"{API}/profiles/format", timeout=5) - assert r.status_code == 200 - response = json.loads(r.text) - assert isinstance(response, list) # Ensure the response is a list - - # Check that each item in the response has keys "questions" and "type" - for item in response: - assert "question" in item - assert "type" in item - + r = requests.get(f"{API}/profiles/format", timeout=5) + assert r.status_code == 200 + response = json.loads(r.text) + assert isinstance(response, list) # Ensure the response is a list + + # Check that each item in the response has keys "questions" and "type" + for item in response: + assert "question" in item + assert "type" in item + #test for get profiles (no profile, one profile, two profiles) def test_get_profiles(testrun, clean_profiles_dir): # pylint: disable=W0613 - #test no profiles - r = requests.get(f"{API}/profiles", timeout=5) - assert r.status_code == 200 - response = json.loads(r.text) - assert isinstance(response, list) # check if response is a list - assert len(response) == 0 #check if list is empty - - #create the first profile - new_profile = load_json('new_profile.json') - profile_name = new_profile["name"] - assert not profile_exists(profile_name), f"Profile '{profile_name}' exists" - - r = requests.post(f"{API}/profiles", data=json.dumps(new_profile), timeout=5) - assert r.status_code == 201, f"Expected status code 201, got {r.status_code}" - response = r.json() - assert "success" in response, f"Expected 'success' in response, got {response}" - - #get the profile - r = requests.get(f"{API}/profiles", timeout=5) - assert r.status_code == 200 - response = json.loads(r.text) - assert isinstance(response, list) # check if response is a list - assert len(response) == 1 #check if there is one profile - # Check that each profile has the expected fields - for profile in response: - assert "name" in profile - assert "status" in profile - assert "created" in profile - assert "version" in profile - assert "questions" in profile - assert isinstance(profile["questions"], list) #check if questions values is list - - #check that "questions" has the expected fields - for element in profile["questions"]: - assert isinstance(element, dict) #check if each element is dict - assert "question" in element - assert "type" in element - assert "answer" in element - - - #create the second profile - new_profile_2 = load_json('new_profile_2.json') - profile_name_2 = new_profile_2["name"] - assert not profile_exists(profile_name_2), f"Profile '{profile_name_2}' exists" - - r = requests.post(f"{API}/profiles", data=json.dumps(new_profile_2), timeout=5) - assert r.status_code == 201, f"Expected status code 200, got {r.status_code}" - response = r.json() - assert "success" in response, f"Expected 'success' in response, got {response}" - - #get the two profiles - r = requests.get(f"{API}/profiles", timeout=5) - assert r.status_code == 200 - response = json.loads(r.text) - assert isinstance(response, list) # check if response is a list - assert len(response) == 2 #check if there are 2 profiles - # Check that each profile has the expected fields - for profile in response: - assert "name" in profile - assert "status" in profile - assert "created" in profile - assert "version" in profile - assert "questions" in profile - assert isinstance(profile["questions"], list) #check if questions values is list - - #check that "questions" has the expected fields - for element in profile["questions"]: - assert isinstance(element, dict) #check if each element is dict - assert "question" in element - assert "type" in element - assert "answer" in element + #test no profiles + r = requests.get(f"{API}/profiles", timeout=5) + assert r.status_code == 200 + response = json.loads(r.text) + assert isinstance(response, list) # check if response is a list + assert len(response) == 0 #check if list is empty + + #create the first profile + new_profile = load_json("new_profile.json") + profile_name = new_profile["name"] + assert not profile_exists(profile_name), f"Profile:{profile_name} exists" + + r = requests.post(f"{API}/profiles", data=json.dumps(new_profile), timeout=5) + assert r.status_code == 201, f"Expected status code 201, got {r.status_code}" + response = r.json() + assert "success" in response, f"Expected 'success' got {response}" + + #get the profile + r = requests.get(f"{API}/profiles", timeout=5) + assert r.status_code == 200 + response = json.loads(r.text) + assert isinstance(response, list) # check if response is a list + assert len(response) == 1 #check if there is one profile + # Check that each profile has the expected fields + for profile in response: + assert "name" in profile + assert "status" in profile + assert "created" in profile + assert "version" in profile + assert "questions" in profile + assert isinstance(profile["questions"], list) + + #check that "questions" has the expected fields + for element in profile["questions"]: + assert isinstance(element, dict) #check if each element is dict + assert "question" in element + assert "type" in element + assert "answer" in element + + + #create the second profile + new_profile_2 = load_json("new_profile_2.json") + profile_name_2 = new_profile_2["name"] + assert not profile_exists(profile_name_2), f"Profile:{profile_name_2} exists" + + r = requests.post( + f"{API}/profiles", + data=json.dumps(new_profile_2), + timeout=5) + assert r.status_code == 201, f"Expected status code 200, got {r.status_code}" + response = r.json() + assert "success" in response, f"Expected 'success' got {response}" + + #get the two profiles + r = requests.get(f"{API}/profiles", timeout=5) + assert r.status_code == 200 + response = json.loads(r.text) + assert isinstance(response, list) # check if response is a list + assert len(response) == 2 #check if there are 2 profiles + # Check that each profile has the expected fields + for profile in response: + assert "name" in profile + assert "status" in profile + assert "created" in profile + assert "version" in profile + assert "questions" in profile + assert isinstance(profile["questions"], list) #questions values is a list + + #check that "questions" has the expected fields + for element in profile["questions"]: + assert isinstance(element, dict) #check if each element is dict + assert "question" in element + assert "type" in element + assert "answer" in element #test for create profile if not exists -def test_create_profile(testrun, clean_profiles_dir): - new_profile = load_json('new_profile.json') - profile_name = new_profile["name"] +def test_create_profile(testrun, clean_profiles_dir): # pylint: disable=W0613 + new_profile = load_json("new_profile.json") + profile_name = new_profile["name"] - assert not profile_exists(profile_name), f"Profile '{profile_name}' exists" + assert not profile_exists(profile_name), f"Profile '{profile_name}' exists" - r = requests.post(f"{API}/profiles", data=json.dumps(new_profile), timeout=5) - assert r.status_code == 201, f"Expected status code 201, got {r.status_code}" - response = r.json() - assert "success" in response, f"Expected 'success' in response, got {response}" + r = requests.post(f"{API}/profiles", data=json.dumps(new_profile), timeout=5) + assert r.status_code == 201, f"Expected status code 201, got {r.status_code}" + response = r.json() + assert "success" in response, f"Expected 'success' got {response}" - # Verify profile creation - r = requests.get(f"{API}/profiles", timeout=5) - assert r.status_code == 200, f"Expected status code 200, got {r.status_code}" - profiles = r.json() - created_profile = next((p for p in profiles if p["name"] == profile_name), None) - assert created_profile is not None, f"Profile '{profile_name}' not found" - + # Verify profile creation + r = requests.get(f"{API}/profiles", timeout=5) + assert r.status_code == 200, f"Expected status code 200, got {r.status_code}" + profiles = r.json() + created_profile = next( + (p for p in profiles if p["name"] == profile_name), None + ) + assert created_profile is not None, f"Profile '{profile_name}' not found" #test for update profile when exists -def test_update_profile(testrun, clean_profiles_dir): - new_profile = load_json('new_profile.json') - updated_profile = load_json('updated_profile.json') - profile_name = new_profile["name"] - updated_profile_name = updated_profile["rename"] - - #create the profile - r = requests.post(f"{API}/profiles", data=json.dumps(new_profile), timeout=5) - assert r.status_code == 201, f"Expected status code 201, got {r.status_code}" - response = r.json() - assert "success" in response, f"Expected 'success' in response, got {response}" - - assert profile_exists(profile_name), f"Profile '{profile_name}' does not exist. Update test cannot proceed." - - #update the profile - r = requests.post(f"{API}/profiles", data=json.dumps(updated_profile), timeout=5) - assert r.status_code == 200, f"Expected status code 200, got {r.status_code}" - response = r.json() - assert "success" in response, f"Expected 'success' in response, got {response}" - - # verify profile update - r = requests.get(f"{API}/profiles", timeout=5) - assert r.status_code == 200, f"Expected status code 200, got {r.status_code}" - profiles = r.json() - updated_profile_check = next((p for p in profiles if p["name"] == updated_profile_name), None) - assert updated_profile_check is not None, f"Profile '{profile_name}' not found" - #assert updated_profile_check["status"] == "Draft" +def test_update_profile(testrun, clean_profiles_dir): # pylint: disable=W0613 + new_profile = load_json("new_profile.json") + updated_profile = load_json("updated_profile.json") + profile_name = new_profile["name"] + updated_profile_name = updated_profile["rename"] + + #create the profile + r = requests.post(f"{API}/profiles", data=json.dumps(new_profile), timeout=5) + assert r.status_code == 201, f"Expected status code 201, got {r.status_code}" + response = r.json() + assert "success" in response, f"Expected 'success' got {response}" + + assert profile_exists(profile_name), f"{profile_name} doesn't exist" + + #update the profile + r = requests.post( + f"{API}/profiles", + data=json.dumps(updated_profile), + timeout=5) + assert r.status_code == 200, f"Expected status code 200, got {r.status_code}" + response = r.json() + assert "success" in response, f"Expected 'success' got {response}" + + # verify profile update + r = requests.get(f"{API}/profiles", timeout=5) + assert r.status_code == 200, f"Expected status code 200, got {r.status_code}" + profiles = r.json() + updated_profile_check = next( + (p for p in profiles if p["name"] == updated_profile_name), + None + ) + assert updated_profile_check is not None, f"Profile:{profile_name} not found" + #test for delete profile -def test_delete_profile(testrun, clean_profiles_dir): +def test_delete_profile(testrun, clean_profiles_dir): # pylint: disable=W0613 #create the profile - new_profile = load_json('new_profile.json') + new_profile = load_json("new_profile.json") profile_name = new_profile["name"] assert not profile_exists(profile_name), f"Profile '{profile_name}' exists" r = requests.post(f"{API}/profiles", data=json.dumps(new_profile), timeout=5) assert r.status_code == 201, f"Expected status code 201, got {r.status_code}" response = r.json() - assert "success" in response, f"Expected 'success' in response, got {response}" + assert "success" in response, f"Expected 'success' got {response}" # Verify the profile exists r = requests.get(f"{API}/profiles", timeout=5) @@ -1336,7 +1348,10 @@ def test_delete_profile(testrun, clean_profiles_dir): #delete the profile profile_to_delete = next(p for p in profiles if p["name"] == profile_name) assert profile_to_delete is not None - r = requests.delete(f"{API}/profiles", data=json.dumps(profile_to_delete), timeout=5) + r = requests.delete( + f"{API}/profiles", + data=json.dumps(profile_to_delete), + timeout=5) assert r.status_code == 200 response = json.loads(r.text) assert "success" in response #check if the response contains "success" key @@ -1345,5 +1360,8 @@ def test_delete_profile(testrun, clean_profiles_dir): r = requests.get(f"{API}/profiles", timeout=5) assert r.status_code == 200 profiles = json.loads(r.text) - deleted_profile = next((p for p in profiles if p["name"] == profile_name), None) + deleted_profile = next( + (p for p in profiles if p["name"] == profile_name), + None + ) assert deleted_profile is None From 7fad9a37d32ea8aef471d904d1d897b9fcb7065a Mon Sep 17 00:00:00 2001 From: MariusBaldovin Date: Sun, 21 Jul 2024 23:09:44 +0100 Subject: [PATCH 04/17] fixed few more pylint errors --- testing/api/test_api.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/testing/api/test_api.py b/testing/api/test_api.py index 06bd0da23..7c4c42952 100644 --- a/testing/api/test_api.py +++ b/testing/api/test_api.py @@ -124,9 +124,9 @@ def testrun(request): # pylint: disable=W0613 stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding="utf-8", - preexec_fn=os.setsid #start_new_session=True + preexec_fn=os.setsid #start_new_session=True ) as proc: - + while True: try: outs = proc.communicate(timeout=1)[0] @@ -1246,7 +1246,7 @@ def test_get_profiles(testrun, clean_profiles_dir): # pylint: disable=W0613 response = r.json() assert "success" in response, f"Expected 'success' got {response}" - #get the two profiles + #get the two profiles r = requests.get(f"{API}/profiles", timeout=5) assert r.status_code == 200 response = json.loads(r.text) @@ -1307,7 +1307,7 @@ def test_update_profile(testrun, clean_profiles_dir): # pylint: disable=W0613 #update the profile r = requests.post( f"{API}/profiles", - data=json.dumps(updated_profile), + data=json.dumps(updated_profile), timeout=5) assert r.status_code == 200, f"Expected status code 200, got {r.status_code}" response = r.json() @@ -1352,7 +1352,7 @@ def test_delete_profile(testrun, clean_profiles_dir): # pylint: disable=W0613 f"{API}/profiles", data=json.dumps(profile_to_delete), timeout=5) - assert r.status_code == 200 + assert r.status_code == 200 response = json.loads(r.text) assert "success" in response #check if the response contains "success" key From 08d0300a19f3df039136f4d990e74b548b45010a Mon Sep 17 00:00:00 2001 From: Sofia Kurilova Date: Mon, 22 Jul 2024 11:25:20 +0200 Subject: [PATCH 05/17] Expired profile profile (#619) --- modules/ui/src/app/mocks/profile.mock.ts | 66 +- modules/ui/src/app/model/profile.ts | 13 +- .../profile-form/profile-form.component.html | 2 +- .../profile-form/profile-form.component.scss | 4 + .../profile-form.component.spec.ts | 568 +++++++++--------- .../profile-form/profile-form.component.ts | 45 +- .../profile-item/profile-item.component.html | 18 +- .../profile-item/profile-item.component.scss | 10 + .../risk-assessment.component.spec.ts | 4 +- .../risk-assessment/risk-assessment.store.ts | 16 +- .../ui/src/app/services/test-run.service.ts | 6 +- 11 files changed, 431 insertions(+), 321 deletions(-) diff --git a/modules/ui/src/app/mocks/profile.mock.ts b/modules/ui/src/app/mocks/profile.mock.ts index f2dbe82c8..614b6e752 100644 --- a/modules/ui/src/app/mocks/profile.mock.ts +++ b/modules/ui/src/app/mocks/profile.mock.ts @@ -17,7 +17,7 @@ import { FormControlType, Profile, - ProfileFormat, + Question, ProfileStatus, } from '../model/profile'; @@ -29,22 +29,51 @@ export const PROFILE_MOCK: Profile = { { question: 'What is the email of the device owner(s)?', answer: 'boddey@google.com, cmeredith@google.com', + type: FormControlType.EMAIL_MULTIPLE, + validation: { + required: true, + max: '30', + }, }, { question: 'What type of device do you need reviewed?', - answer: 'IoT Sensor', + answer: 'Type', + type: FormControlType.TEXTAREA, + validation: { + required: true, + max: '28', + }, + description: 'This tells us about the device', }, { question: 'Are any of the following statements true about your device?', answer: 'First', + type: FormControlType.SELECT, + options: ['First', 'Second'], + validation: { + required: true, + }, }, { question: 'What features does the device have?', + description: + 'This tells us about the data your device will collectThis tells us about the data your device will collect', + type: FormControlType.SELECT_MULTIPLE, answer: [0, 1, 2], + options: ['Wi-fi', 'Bluetooth', 'ZigBee / Z-Wave / Thread / Matter'], + validation: { + required: true, + }, }, { question: 'Comments', answer: 'Yes', + type: FormControlType.TEXT, + description: 'Please enter any comments here', + validation: { + max: '28', + required: true, + }, }, ], }; @@ -61,7 +90,7 @@ export const PROFILE_MOCK_3: Profile = { questions: [], }; -export const PROFILE_FORM: ProfileFormat[] = [ +export const PROFILE_FORM: Question[] = [ { question: 'Email', type: FormControlType.EMAIL_MULTIPLE, @@ -163,22 +192,51 @@ export const COPY_PROFILE_MOCK: Profile = { { question: 'What is the email of the device owner(s)?', answer: 'boddey@google.com, cmeredith@google.com', + type: FormControlType.EMAIL_MULTIPLE, + validation: { + required: true, + max: '30', + }, }, { question: 'What type of device do you need reviewed?', - answer: 'IoT Sensor', + answer: 'Type', + type: FormControlType.TEXTAREA, + validation: { + required: true, + max: '28', + }, + description: 'This tells us about the device', }, { question: 'Are any of the following statements true about your device?', answer: 'First', + type: FormControlType.SELECT, + options: ['First', 'Second'], + validation: { + required: true, + }, }, { question: 'What features does the device have?', + description: + 'This tells us about the data your device will collectThis tells us about the data your device will collect', + type: FormControlType.SELECT_MULTIPLE, answer: [0, 1, 2], + options: ['Wi-fi', 'Bluetooth', 'ZigBee / Z-Wave / Thread / Matter'], + validation: { + required: true, + }, }, { question: 'Comments', answer: 'Yes', + type: FormControlType.TEXT, + description: 'Please enter any comments here', + validation: { + max: '28', + required: true, + }, }, ], }; diff --git a/modules/ui/src/app/model/profile.ts b/modules/ui/src/app/model/profile.ts index 5d9d88fd9..fef15f101 100644 --- a/modules/ui/src/app/model/profile.ts +++ b/modules/ui/src/app/model/profile.ts @@ -22,11 +22,6 @@ export interface Profile { created?: string; } -export interface Question { - question?: string; - answer?: string | number[]; -} - export enum FormControlType { SELECT = 'select', TEXTAREA = 'text-long', @@ -40,17 +35,13 @@ export interface Validation { max?: string; } -export interface ProfileFormat { +export interface Question { question: string; - type: FormControlType; + type?: FormControlType; description?: string; options?: string[]; default?: string; validation?: Validation; -} - -export interface Question { - question?: string; answer?: string | number[]; } diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html index 12821764d..c2f12a510 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html @@ -43,7 +43,7 @@ - + { - [ - 'very long value very long value very long value very long value very long value very long value very long value', - 'as&@3$', - ].forEach(value => { + it('should have "invalid_format" error when field does not satisfy validation rules', () => { + [ + 'very long value very long value very long value very long value very long value very long value very long value', + 'as&@3$', + ].forEach(value => { + const name: HTMLInputElement = compiled.querySelector( + '.form-name' + ) as HTMLInputElement; + name.value = value; + name.dispatchEvent(new Event('input')); + component.nameControl.markAsTouched(); + fixture.detectChanges(); + + const nameError = compiled.querySelector('mat-error')?.innerHTML; + const error = component.nameControl.hasError('invalid_format'); + + expect(error).toBeTruthy(); + expect(nameError).toContain( + 'Please, check. The Profile name must be a maximum of 28 characters. Only letters, numbers, and accented letters are permitted.' + ); + }); + }); + + it('should have "required" error when field is not filled', () => { const name: HTMLInputElement = compiled.querySelector( '.form-name' ) as HTMLInputElement; - name.value = value; - name.dispatchEvent(new Event('input')); - component.nameControl.markAsTouched(); - fixture.detectChanges(); + ['', ' '].forEach(value => { + name.value = value; + name.dispatchEvent(new Event('input')); + component.nameControl.markAsTouched(); + fixture.detectChanges(); - const nameError = compiled.querySelector('mat-error')?.innerHTML; - const error = component.nameControl.hasError('invalid_format'); + const nameError = compiled.querySelector('mat-error')?.innerHTML; + const error = component.nameControl.hasError('required'); - expect(error).toBeTruthy(); - expect(nameError).toContain( - 'Please, check. The Profile name must be a maximum of 28 characters. Only letters, numbers, and accented letters are permitted.' - ); + expect(error).toBeTruthy(); + expect(nameError).toContain('The Profile name is required'); + }); }); - }); - it('should have "required" error when field is not filled', () => { - const name: HTMLInputElement = compiled.querySelector( - '.form-name' - ) as HTMLInputElement; - ['', ' '].forEach(value => { - name.value = value; + it('should have different profile name error when profile with name is exist', () => { + const name: HTMLInputElement = compiled.querySelector( + '.form-name' + ) as HTMLInputElement; + name.value = 'Primary profile'; name.dispatchEvent(new Event('input')); component.nameControl.markAsTouched(); + fixture.detectChanges(); const nameError = compiled.querySelector('mat-error')?.innerHTML; - const error = component.nameControl.hasError('required'); + const error = component.nameControl.hasError('has_same_profile_name'); expect(error).toBeTruthy(); - expect(nameError).toContain('The Profile name is required'); + expect(nameError).toContain( + 'This Profile name is already used for another Risk Assessment profile' + ); }); }); - it('should have different profile name error when profile with name is exist', () => { - const name: HTMLInputElement = compiled.querySelector( - '.form-name' - ) as HTMLInputElement; - name.value = 'Primary profile'; - name.dispatchEvent(new Event('input')); - component.nameControl.markAsTouched(); + PROFILE_FORM.forEach((item, index) => { + const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i - fixture.detectChanges(); + it(`should have form field with specific type"`, () => { + const fields = compiled.querySelectorAll('.profile-form-field'); - const nameError = compiled.querySelector('mat-error')?.innerHTML; - const error = component.nameControl.hasError('has_same_profile_name'); + if (item.type === FormControlType.SELECT) { + const select = fields[uiIndex].querySelector('mat-select'); + expect(select).toBeTruthy(); + } else if (item.type === FormControlType.SELECT_MULTIPLE) { + const select = fields[uiIndex].querySelector('mat-checkbox'); + expect(select).toBeTruthy(); + } else if (item.type === FormControlType.TEXTAREA) { + const input = fields[uiIndex]?.querySelector('textarea'); + expect(input).toBeTruthy(); + } else { + const input = fields[uiIndex]?.querySelector('input'); + expect(input).toBeTruthy(); + } + }); - expect(error).toBeTruthy(); - expect(nameError).toContain( - 'This Profile name is already used for another Risk Assessment profile' - ); - }); - }); + it('should have label', () => { + const labels = compiled.querySelectorAll('.field-label'); + const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i - PROFILE_FORM.forEach((item, index) => { - const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i + const label = item?.validation?.required + ? item.question + ' *' + : item.question; + expect(labels[uiIndex].textContent?.trim()).toEqual(label); + }); - it(`should have form field with specific type"`, () => { - const fields = compiled.querySelectorAll('.profile-form-field'); + it('should have hint', () => { + const fields = compiled.querySelectorAll('.profile-form-field'); + const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i + const hint = fields[uiIndex].querySelector('mat-hint'); + + if (item.description) { + expect(hint?.textContent?.trim()).toEqual(item.description); + } else { + expect(hint).toBeNull(); + } + }); if (item.type === FormControlType.SELECT) { - const select = fields[uiIndex].querySelector('mat-select'); - expect(select).toBeTruthy(); - } else if (item.type === FormControlType.SELECT_MULTIPLE) { - const select = fields[uiIndex].querySelector('mat-checkbox'); - expect(select).toBeTruthy(); - } else if (item.type === FormControlType.TEXTAREA) { - const input = fields[uiIndex]?.querySelector('textarea'); - expect(input).toBeTruthy(); - } else { - const input = fields[uiIndex]?.querySelector('input'); - expect(input).toBeTruthy(); - } - }); + describe('select', () => { + it(`should have default value if provided`, () => { + const fields = compiled.querySelectorAll('.profile-form-field'); + const select = fields[uiIndex].querySelector('mat-select'); + expect(select?.textContent?.trim()).toEqual(item.default || ''); + }); - it('should have label', () => { - const labels = compiled.querySelectorAll('.field-label'); - const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i + it('should have "required" error when field is not filled', () => { + const fields = compiled.querySelectorAll('.profile-form-field'); - const label = item?.validation?.required - ? item.question + ' *' - : item.question; - expect(labels[uiIndex].textContent?.trim()).toEqual(label); - }); + component.getControl(index).setValue(''); + component.getControl(index).markAsTouched(); - it('should have hint', () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i - const hint = fields[uiIndex].querySelector('mat-hint'); + fixture.detectChanges(); - if (item.description) { - expect(hint?.textContent?.trim()).toEqual(item.description); - } else { - expect(hint).toBeNull(); - } - }); + const error = + fields[uiIndex].querySelector('mat-error')?.innerHTML; - if (item.type === FormControlType.SELECT) { - describe('select', () => { - it(`should have default value if provided`, () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - const select = fields[uiIndex].querySelector('mat-select'); - expect(select?.textContent?.trim()).toEqual(item.default || ''); + expect(error).toContain('The field is required'); + }); }); + } - it('should have "required" error when field is not filled', () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - - component.getControl(index).setValue(''); - component.getControl(index).markAsTouched(); - - fixture.detectChanges(); - - const error = fields[uiIndex].querySelector('mat-error')?.innerHTML; + if (item.type === FormControlType.SELECT_MULTIPLE) { + describe('select multiple', () => { + it(`should mark form group as dirty while tab navigation`, () => { + const fields = compiled.querySelectorAll('.profile-form-field'); + const checkbox = fields[uiIndex].querySelector( + '.field-select-checkbox:last-of-type mat-checkbox' + ); + checkbox?.dispatchEvent( + new KeyboardEvent('keydown', { key: 'Tab' }) + ); + fixture.detectChanges(); - expect(error).toContain('The field is required'); + expect(component.getControl(index).dirty).toBeTrue(); + }); }); - }); - } - - if (item.type === FormControlType.SELECT_MULTIPLE) { - describe('select multiple', () => { - it(`should mark form group as dirty while tab navigation`, () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - const checkbox = fields[uiIndex].querySelector( - '.field-select-checkbox:last-of-type mat-checkbox' - ); - checkbox?.dispatchEvent( - new KeyboardEvent('keydown', { key: 'Tab' }) - ); - fixture.detectChanges(); + } - expect(component.getControl(index).dirty).toBeTrue(); - }); - }); - } - - if ( - item.type === FormControlType.TEXT || - item.type === FormControlType.TEXTAREA || - item.type === FormControlType.EMAIL_MULTIPLE - ) { - describe('text or text-long or email-multiple', () => { - if (item.validation?.required) { - it('should have "required" error when field is not filled', () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i - const input = fields[uiIndex].querySelector( - '.mat-mdc-input-element' - ) as HTMLInputElement; - ['', ' '].forEach(value => { - input.value = value; - input.dispatchEvent(new Event('input')); - component.getControl(index).markAsTouched(); - fixture.detectChanges(); - const errors = fields[uiIndex].querySelectorAll('mat-error'); - let hasError = false; - errors.forEach(error => { - if (error.textContent === 'The field is required') { - hasError = true; - } + if ( + item.type === FormControlType.TEXT || + item.type === FormControlType.TEXTAREA || + item.type === FormControlType.EMAIL_MULTIPLE + ) { + describe('text or text-long or email-multiple', () => { + if (item.validation?.required) { + it('should have "required" error when field is not filled', () => { + const fields = compiled.querySelectorAll('.profile-form-field'); + const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i + const input = fields[uiIndex].querySelector( + '.mat-mdc-input-element' + ) as HTMLInputElement; + ['', ' '].forEach(value => { + input.value = value; + input.dispatchEvent(new Event('input')); + component.getControl(index).markAsTouched(); + fixture.detectChanges(); + const errors = fields[uiIndex].querySelectorAll('mat-error'); + let hasError = false; + errors.forEach(error => { + if (error.textContent === 'The field is required') { + hasError = true; + } + }); + + expect(hasError).toBeTrue(); }); - - expect(hasError).toBeTrue(); }); - }); - } - - it('should have "invalid_format" error when field does not satisfy validation rules', () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i - const input: HTMLInputElement = fields[uiIndex].querySelector( - '.mat-mdc-input-element' - ) as HTMLInputElement; - input.value = 'as\\\\\\\\\\""""""""'; - input.dispatchEvent(new Event('input')); - component.getControl(index).markAsTouched(); - fixture.detectChanges(); - const result = - item.type === FormControlType.EMAIL_MULTIPLE - ? 'Please, check the email address. Valid e-mail can contain only latin letters, numbers, @ and . (dot).' - : 'Please, check. “ and \\ are not allowed.'; - const errors = fields[uiIndex].querySelectorAll('mat-error'); - let hasError = false; - errors.forEach(error => { - if (error.textContent === result) { - hasError = true; - } - }); - - expect(hasError).toBeTrue(); - }); + } - if (item.validation?.max) { - it('should have "maxlength" error when field is exceeding max length', () => { + it('should have "invalid_format" error when field does not satisfy validation rules', () => { const fields = compiled.querySelectorAll('.profile-form-field'); const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i const input: HTMLInputElement = fields[uiIndex].querySelector( '.mat-mdc-input-element' ) as HTMLInputElement; - input.value = - 'very long value very long value very long value very long value very long value very long value very long value very long value very long value very long value'; + input.value = 'as\\\\\\\\\\""""""""'; input.dispatchEvent(new Event('input')); component.getControl(index).markAsTouched(); fixture.detectChanges(); - + const result = + item.type === FormControlType.EMAIL_MULTIPLE + ? 'Please, check the email address. Valid e-mail can contain only latin letters, numbers, @ and . (dot).' + : 'Please, check. “ and \\ are not allowed.'; const errors = fields[uiIndex].querySelectorAll('mat-error'); let hasError = false; errors.forEach(error => { - if ( - error.textContent === - `The field must be a maximum of ${item.validation?.max} characters.` - ) { + if (error.textContent === result) { hasError = true; } }); + expect(hasError).toBeTrue(); }); - } - }); - } - }); - describe('Draft button', () => { - it('should be disabled when profile name is empty', () => { - component.nameControl.setValue(''); - fixture.detectChanges(); - const draftButton = compiled.querySelector( - '.save-draft-button' - ) as HTMLButtonElement; + if (item.validation?.max) { + it('should have "maxlength" error when field is exceeding max length', () => { + const fields = compiled.querySelectorAll('.profile-form-field'); + const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i + const input: HTMLInputElement = fields[uiIndex].querySelector( + '.mat-mdc-input-element' + ) as HTMLInputElement; + input.value = + 'very long value very long value very long value very long value very long value very long value very long value very long value very long value very long value'; + input.dispatchEvent(new Event('input')); + component.getControl(index).markAsTouched(); + fixture.detectChanges(); - expect(draftButton.disabled).toBeTrue(); + const errors = fields[uiIndex].querySelectorAll('mat-error'); + let hasError = false; + errors.forEach(error => { + if ( + error.textContent === + `The field must be a maximum of ${item.validation?.max} characters.` + ) { + hasError = true; + } + }); + expect(hasError).toBeTrue(); + }); + } + }); + } }); - it('should be disabled when profile name is not empty but other fields in wrong format', () => { - component.nameControl.setValue('New profile'); - component.getControl('0').setValue('test'); - fixture.detectChanges(); - const draftButton = compiled.querySelector( - '.save-draft-button' - ) as HTMLButtonElement; + describe('Draft button', () => { + it('should be disabled when profile name is empty', () => { + component.nameControl.setValue(''); + fixture.detectChanges(); + const draftButton = compiled.querySelector( + '.save-draft-button' + ) as HTMLButtonElement; - expect(draftButton.disabled).toBeTrue(); - }); + expect(draftButton.disabled).toBeTrue(); + }); - it('should be enabled when profile name is not empty; other fields are empty or in correct format', () => { - component.nameControl.setValue('New profile'); - component.getControl('0').setValue('a@test.te;b@test.te, c@test.te'); - fixture.detectChanges(); - const draftButton = compiled.querySelector( - '.save-draft-button' - ) as HTMLButtonElement; + it('should be disabled when profile name is not empty but other fields in wrong format', () => { + component.nameControl.setValue('New profile'); + component.getControl('0').setValue('test'); + fixture.detectChanges(); + const draftButton = compiled.querySelector( + '.save-draft-button' + ) as HTMLButtonElement; - expect(draftButton.disabled).toBeFalse(); - }); + expect(draftButton.disabled).toBeTrue(); + }); - it('should emit new profile in draft status', () => { - component.nameControl.setValue('New profile'); - fixture.detectChanges(); - const emitSpy = spyOn(component.saveProfile, 'emit'); - const draftButton = compiled.querySelector( - '.save-draft-button' - ) as HTMLButtonElement; - draftButton.click(); + it('should be enabled when profile name is not empty; other fields are empty or in correct format', () => { + component.nameControl.setValue('New profile'); + component.getControl('0').setValue('a@test.te;b@test.te, c@test.te'); + fixture.detectChanges(); + const draftButton = compiled.querySelector( + '.save-draft-button' + ) as HTMLButtonElement; - expect(emitSpy).toHaveBeenCalledWith({ - ...NEW_PROFILE_MOCK_DRAFT, + expect(draftButton.disabled).toBeFalse(); }); - }); - }); - describe('Save button', () => { - beforeEach(() => { - fillForm(component); - fixture.detectChanges(); + it('should emit new profile in draft status', () => { + component.nameControl.setValue('New profile'); + fixture.detectChanges(); + const emitSpy = spyOn(component.saveProfile, 'emit'); + const draftButton = compiled.querySelector( + '.save-draft-button' + ) as HTMLButtonElement; + draftButton.click(); + + expect(emitSpy).toHaveBeenCalledWith({ + ...NEW_PROFILE_MOCK_DRAFT, + }); + }); }); - it('should be enabled when required fields are present', () => { - const saveButton = compiled.querySelector( - '.save-profile-button' - ) as HTMLButtonElement; + describe('Save button', () => { + beforeEach(() => { + fillForm(component); + fixture.detectChanges(); + }); + + it('should be enabled when required fields are present', () => { + const saveButton = compiled.querySelector( + '.save-profile-button' + ) as HTMLButtonElement; - expect(saveButton.disabled).toBeFalse(); + expect(saveButton.disabled).toBeFalse(); + }); + + it('should emit new profile', () => { + const emitSpy = spyOn(component.saveProfile, 'emit'); + const saveButton = compiled.querySelector( + '.save-profile-button' + ) as HTMLButtonElement; + saveButton.click(); + + expect(emitSpy).toHaveBeenCalledWith({ + ...NEW_PROFILE_MOCK, + }); + }); }); - it('should emit new profile', () => { - const emitSpy = spyOn(component.saveProfile, 'emit'); - const saveButton = compiled.querySelector( - '.save-profile-button' - ) as HTMLButtonElement; - saveButton.click(); + describe('Discard button', () => { + beforeEach(() => { + fillForm(component); + fixture.detectChanges(); + }); + + it('should be enabled when form is filled', () => { + const discardButton = compiled.querySelector( + '.discard-button' + ) as HTMLButtonElement; + + expect(discardButton.disabled).toBeFalse(); + }); + + it('should emit discard', () => { + const emitSpy = spyOn(component.discard, 'emit'); + const discardButton = compiled.querySelector( + '.discard-button' + ) as HTMLButtonElement; + discardButton.click(); - expect(emitSpy).toHaveBeenCalledWith({ - ...NEW_PROFILE_MOCK, + expect(emitSpy).toHaveBeenCalled(); }); }); }); - describe('Discard button', () => { + describe('with expire profile', () => { beforeEach(() => { - fillForm(component); + component.selectedProfile = Object.assign({}, PROFILE_MOCK, { + status: ProfileStatus.EXPIRED, + }); fixture.detectChanges(); }); - it('should be enabled when form is filled', () => { - const discardButton = compiled.querySelector( - '.discard-button' - ) as HTMLButtonElement; - - expect(discardButton.disabled).toBeFalse(); - }); - - it('should emit discard', () => { - const emitSpy = spyOn(component.discard, 'emit'); - const discardButton = compiled.querySelector( - '.discard-button' - ) as HTMLButtonElement; - discardButton.click(); - - expect(emitSpy).toHaveBeenCalled(); + it('should have disabled fields', () => { + const fields = compiled.querySelectorAll('mat-form-field'); + fields.forEach(field => { + expect( + field.classList.contains('mat-form-field-disabled') + ).toBeTrue(); + }); }); }); }); @@ -425,6 +446,7 @@ describe('ProfileFormComponent', () => { it('save profile should have rename field', () => { const emitSpy = spyOn(component.saveProfile, 'emit'); fillForm(component); + fixture.detectChanges(); component.onSaveClick(ProfileStatus.VALID); expect(emitSpy).toHaveBeenCalledWith(RENAME_PROFILE_MOCK); diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts index 684bc67da..7ac96fd18 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts @@ -46,7 +46,6 @@ import { DeviceValidators } from '../../devices/components/device-form/device.va import { FormControlType, Profile, - ProfileFormat, ProfileStatus, Question, Validation, @@ -81,7 +80,8 @@ export class ProfileFormComponent implements OnInit { profileForm: FormGroup = this.fb.group({}); @ViewChildren(CdkTextareaAutosize) autosize!: QueryList; - @Input() profileFormat!: ProfileFormat[]; + questionnaire!: Question[]; + @Input() profileFormat!: Question[]; @Input() set profiles(profiles: Profile[]) { this.profileList = profiles; @@ -95,9 +95,8 @@ export class ProfileFormComponent implements OnInit { @Input() set selectedProfile(profile: Profile | null) { this.profile = profile; - if (profile && this.nameControl) { - this.updateNameValidator(); - this.fillProfileForm(this.profileFormat, profile); + if (profile && this.questionnaire) { + this.updateForm(profile); } } get selectedProfile() { @@ -112,9 +111,22 @@ export class ProfileFormComponent implements OnInit { private fb: FormBuilder ) {} ngOnInit() { - this.profileForm = this.createProfileForm(this.profileFormat); if (this.selectedProfile) { - this.fillProfileForm(this.profileFormat, this.selectedProfile); + this.updateForm(this.selectedProfile); + } else { + this.questionnaire = this.profileFormat; + this.profileForm = this.createProfileForm(this.questionnaire); + } + } + + updateForm(profile: Profile) { + this.questionnaire = profile.questions; + this.profileForm = this.createProfileForm(this.questionnaire); + this.fillProfileForm(profile); + if (profile.status === ProfileStatus.EXPIRED) { + this.profileForm.disable(); + } else { + this.profileForm.enable(); } } @@ -123,7 +135,7 @@ export class ProfileFormComponent implements OnInit { } private get fieldsHasError(): boolean { - return this.profileFormat.some((field, index) => { + return this.questionnaire.some((field, index) => { return ( this.getControl(index).hasError('invalid_format') || this.getControl(index).hasError('maxlength') @@ -139,7 +151,7 @@ export class ProfileFormComponent implements OnInit { return this.profileForm.get(name.toString()) as AbstractControl; } - createProfileForm(questions: ProfileFormat[]): FormGroup { + createProfileForm(questions: Question[]): FormGroup { // eslint-disable-next-line @typescript-eslint/no-explicit-any const group: any = {}; @@ -159,7 +171,7 @@ export class ProfileFormComponent implements OnInit { group[index] = this.getMultiSelectGroup(question); } else { const validators = this.getValidators( - question.type, + question.type!, question.validation ); group[index] = new FormControl(question.default || '', validators); @@ -187,7 +199,7 @@ export class ProfileFormComponent implements OnInit { return validators; } - getMultiSelectGroup(question: ProfileFormat): FormGroup { + getMultiSelectGroup(question: Question): FormGroup { // eslint-disable-next-line @typescript-eslint/no-explicit-any const group: any = {}; question.options?.forEach((option, index) => { @@ -204,9 +216,9 @@ export class ProfileFormComponent implements OnInit { return this.profileForm?.controls[name] as FormGroup; } - fillProfileForm(profileFormat: ProfileFormat[], profile: Profile): void { + fillProfileForm(profile: Profile): void { this.nameControl.setValue(profile.name); - profileFormat.forEach((question, index) => { + profile.questions.forEach((question, index) => { if (question.type === FormControlType.SELECT_MULTIPLE) { question.options?.forEach((item, idx) => { if ((profile.questions[index].answer as number[])?.includes(idx)) { @@ -248,7 +260,7 @@ export class ProfileFormComponent implements OnInit { } private buildResponseFromForm( - initialQuestions: ProfileFormat[], + initialQuestions: Question[], profileForm: FormGroup, status: ProfileStatus, profile: Profile | null @@ -266,8 +278,9 @@ export class ProfileFormComponent implements OnInit { const questions: Question[] = []; initialQuestions.forEach((initialQuestion, index) => { - const question: Question = {}; - question.question = initialQuestion.question; + const question: Question = { + question: initialQuestion.question, + }; if (initialQuestion.type === FormControlType.SELECT_MULTIPLE) { const answer: number[] = []; diff --git a/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.html b/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.html index 3f5978476..5d1b86514 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.html +++ b/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.html @@ -13,7 +13,11 @@ See the License for the specific language governing permissions and limitations under the License. --> -
+
+ +

- {{ profile.created | date: 'dd MMM yyyy' }} + + Outdated ({{ profile.created | date: 'dd MMM yyyy' }}) + + + {{ profile.created | date: 'dd MMM yyyy' }} +