diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cc7765..90ac3f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Updated versions of external actions and MongoDB in GitHub workflows (#374) - Allow updating contact's email and href separately using the command line (#381) - Conftest file to reflect a patient's contact with email, roles and additionalContacts (#383) +- Refactored endpoints tests to avoid duplicated code (#388) ### Fixed - Fixed typo on conda installation docs (#373) - Parsing and saving `additionalContacts` field when a patient is saved using the endpoint (#386) diff --git a/tests/server/test_server_responses.py b/tests/server/test_server_responses.py index 77f0063..332f57d 100644 --- a/tests/server/test_server_responses.py +++ b/tests/server/test_server_responses.py @@ -16,6 +16,56 @@ CONTENT_TYPE = "application/vnd.ga4gh.matchmaker.v1.0+json" +def _setup(mock_app, test_client, test_node, database): + # common setup used in all tests + ok_token = test_client["auth_token"] + + add_node(mongo_db=mock_app.db, obj=test_client, is_client=True) + add_node(mongo_db=mock_app.db, obj=test_node, is_client=False) + + assert database["matches"].find_one() is None + assert database["patients"].find_one() is None + + return ok_token + + +def _setup_patients(database, gpx4_patients): + """Insert demo patients into DB""" + assert len(gpx4_patients) == 2 + inserted_ids = [] + for pat in gpx4_patients: + mme_pat = mme_patient(pat, True) + inserted_ids.append(backend_add_patient(database, mme_pat)) + assert len(inserted_ids) == 2 + return inserted_ids + + +def _run_match_request(mock_app, endpoint, query_patient, ok_token): + response = mock_app.test_client().post( + endpoint, + data=json.dumps(query_patient), + headers=auth_headers(ok_token), + ) + + assert response.status_code == 200 + data = json.loads(response.data) + + assert isinstance(data["results"], list) + assert len(data["results"]) == 2 + assert "patient" in data["results"][0] + assert "score" in data["results"][0] + assert "contact" in data["results"][0]["patient"] + + return data + + +def _assert_valid_match_structure(match): + for res in match["results"]: + for pat in res["patients"]: + assert pat["patient"]["contact"] + assert pat["score"]["patient"] > 0 + + def test_heartbeat(mock_app, database, test_client): """Test sending a GET request to see if app has a heartbeat""" @@ -53,56 +103,36 @@ def test_add_patient_no_auth(mock_app, gpx4_patients): assert response.status_code == 401 -def test_add_patient_malformed_patient(mock_app, test_client, gpx4_patients, test_node): +def test_add_patient_malformed_patient(mock_app, test_client, gpx4_patients, test_node, database): """Test sending a POST request to server to add a patient with malformed patient json""" - # Given a node with authorized token - ok_token = test_client["auth_token"] - - add_node(mongo_db=mock_app.db, obj=test_client, is_client=True) - add_node(mongo_db=mock_app.db, obj=test_node, is_client=False) # + ok_token = _setup(mock_app, test_client, test_node, database) - # send a malformed json object using a valid auth token malformed_json = "{'_id': 'patient_id' }" + response = mock_app.test_client().post( - ADD_PATIENT_ENDPOINT, data=malformed_json, headers=auth_headers(ok_token) + ADD_PATIENT_ENDPOINT, + data=malformed_json, + headers=auth_headers(ok_token), ) - # and check that you get the correct error code from server(400) + assert response.status_code == 400 -def test_add_patient_malformed_data(mock_app, test_client, gpx4_patients, test_node): +def test_add_patient_malformed_data(mock_app, test_client, gpx4_patients, test_node, database): """Test sending a POST request to server to add a patient with malformed data""" patient_data = gpx4_patients[1] - # Given a node with authorized token - ok_token = test_client["auth_token"] - - add_node(mongo_db=mock_app.db, obj=test_client, is_client=True) - add_node( - mongo_db=mock_app.db, obj=test_node, is_client=False - ) # add a test node, to perform external matching + ok_token = _setup(mock_app, test_client, test_node, database) - # add a patient not conforming to MME API using a valid auth token response = mock_app.test_client().post( - ADD_PATIENT_ENDPOINT, data=json.dumps(patient_data), headers=auth_headers(ok_token) + ADD_PATIENT_ENDPOINT, + data=json.dumps(patient_data), + headers=auth_headers(ok_token), ) - # and check that the server returns an error 422 (unprocessable entity) - assert response.status_code == 422 - -def _setup(mock_app, test_client, test_node, database): - # common setup used in all tests - ok_token = test_client["auth_token"] - - add_node(mongo_db=mock_app.db, obj=test_client, is_client=True) - add_node(mongo_db=mock_app.db, obj=test_node, is_client=False) - - assert database["matches"].find_one() is None - assert database["patients"].find_one() is None - - return ok_token + assert response.status_code == 422 @responses.activate @@ -214,329 +244,273 @@ def test_metrics(mock_app, database, test_client, demo_data_path, match_objs): def test_nodes_view(mock_app, database, test_node, test_client): """testing viewing the list of connected nodes as an authenticated client""" - # insert a test client in database - add_node(mongo_db=database, obj=test_client, is_client=True) - ok_token = test_client["auth_token"] + ok_token = _setup(mock_app, test_client, test_node, database) - # If you try to see nodes without being authorized + # Clear nodes added by _setup to preserve original test logic + database["nodes"].delete_many({}) + + # Unauthorized request response = mock_app.test_client().get("nodes") - # You should get a not authorized code from server assert response.status_code == 401 - # since there are no connected nodes in database + # No nodes in database assert database["nodes"].find_one() is None - # When you send an authorized request + + # Add only client (as in original test) + add_node(mongo_db=database, obj=test_client, is_client=True) + + # Authorized request → empty list response = mock_app.test_client().get("nodes", headers=auth_headers(ok_token)) data = json.loads(response.data) - # you shoud get an empty list assert data == [] - # insert a test node in database + # Add a test node add_node(mongo_db=database, obj=test_node, is_client=False) - # When you send an authorized request + + # Authorized request → one node response = mock_app.test_client().get("nodes", headers=auth_headers(ok_token)) data = json.loads(response.data) - # this time you should get a list with one element assert len(data) == 1 - # and the id of the element is the id of the node assert data[0]["id"] == test_node["_id"] -def test_delete_patient(mock_app, database, gpx4_patients, test_client, match_objs): +def test_delete_patient(mock_app, database, gpx4_patients, test_client, test_node, match_objs): """Test deleting a patient from database by sending a DELETE request""" - # load 2 patients from demo data in mock database + # GIVEN 2 patients already exist in DB assert len(gpx4_patients) == 2 - inserted_ids = [] + for pat in gpx4_patients: - # convert patient in mme patient type (convert also gene to ensembl) mme_pat = mme_patient(pat, True) - inserted_ids.append(backend_add_patient(database, mme_pat)) + backend_add_patient(database, mme_pat) - assert len(inserted_ids) == 2 - - # 50 cases present on patients collection delete_id = "P0001058" - # try to delete patient without auth token: - response = mock_app.test_client().delete("".join([DELETE_PATIENT_ENDPOINT, delete_id])) + # WHEN deleting without auth + response = mock_app.test_client().delete(DELETE_PATIENT_ENDPOINT + delete_id) assert response.status_code == 401 - # Add a valid client node + # GIVEN an authorized client node ok_token = test_client["auth_token"] add_node(mongo_db=mock_app.db, obj=test_client, is_client=True) - # Send delete request providing a valid token but a non valid id + # WHEN deleting with invalid patient ID response = mock_app.test_client().delete( - "".join([DELETE_PATIENT_ENDPOINT, "not_a_valid_ID"]), headers=auth_headers(ok_token) + DELETE_PATIENT_ENDPOINT + "not_a_valid_ID", + headers=auth_headers(ok_token), ) assert response.status_code == 200 + data = json.loads(response.data) - # but server returns error assert ( data["message"] == "ERROR. Could not delete a patient with ID not_a_valid_ID from database" ) - assert database["matches"].find_one() is None # no matches in database - # insert into database some mock matching objects + # ensure no matches initially + assert database["matches"].find_one() is None + + # insert mock matches database["matches"].insert_many(match_objs) - # patient "delete_id" should have two associated matches in database + # ensure correct match count before deletion results = database["matches"].find({"data.patient.id": delete_id}) assert len(list(results)) == 2 - # Send valid patient ID and valid token + # WHEN deleting valid patient response = mock_app.test_client().delete( - "".join([DELETE_PATIENT_ENDPOINT, delete_id]), headers=auth_headers(ok_token) + DELETE_PATIENT_ENDPOINT + delete_id, + headers=auth_headers(ok_token), ) assert response.status_code == 200 - # make sure that the patient was removed from database - results = database["patients"].find() - assert len(list(results)) == 1 + # THEN patient should be removed + assert len(list(database["patients"].find({}))) == 1 - # make sure that patient matches are also gone - results = database["matches"].find() - assert len(list(results)) == 1 + # AND related matches should be cleaned up + assert len(list(database["matches"].find({}))) == 1 -def test_patient_matches(mock_app, database, match_objs, test_client): +def test_patient_matches(mock_app, database, match_objs, test_client, test_node): """testing the endpoint that retrieves the matchings by patient ID""" - # Add a valid client node - ok_token = test_client["auth_token"] - add_node(mongo_db=mock_app.db, obj=test_client, is_client=True) + # Setup authorized client and node + ok_token = _setup(mock_app, test_client, test_node, database) # start from a database with no matches assert database["matches"].find_one() is None - # import mock matches into datababase + + # import mock matches into database database["matches"].insert_many(match_objs) - # database now should have two matching objects - # test endpoint to get matches by ID - # test by sending a non-authorized request + # unauthorized request response = mock_app.test_client().get("matches/P0001058") - # response gives a 401 code (not authorized) assert response.status_code == 401 - # try with an authorized request with a used ID that is not in database - response = mock_app.test_client().get("matches/unknown_patient", headers=auth_headers(ok_token)) - # response gives success + # authorized request with unknown patient + response = mock_app.test_client().get( + "matches/unknown_patient", + headers=auth_headers(ok_token), + ) assert response.status_code == 200 + data = json.loads(response.data) - # but the patient is not found by server assert ( data["message"] == "Could not find any matches in database for patient ID unknown_patient" ) - # Try with authenticates request and valid patient - response = mock_app.test_client().get("matches/P0001058", headers=auth_headers(ok_token)) - # response gives success + # authorized request with valid patient + response = mock_app.test_client().get( + "matches/P0001058", + headers=auth_headers(ok_token), + ) assert response.status_code == 200 + data = json.loads(response.data) - # and there are matches in it - assert ( - len(data["matches"]) == 2 - ) # 2 matches returned because endpoint returns only matches with results + assert len(data["matches"]) == 2 # only matches with results returned - # Test that there are actually 3 matches by calling directly the function returning matches + # direct function: all matches matches = patient_matches( - database=database, patient_id="P0001058", type=None, with_results=False + database=database, + patient_id="P0001058", + type=None, + with_results=False, ) assert len(matches) == 3 + for match in matches: for result in match["results"]: for patient in result["patients"]: assert patient["patient"]["id"] - # Call the same function to get only external matches + # external matches only matches = patient_matches( - database=database, patient_id="P0001058", type="external", with_results=False + database=database, + patient_id="P0001058", + type="external", + with_results=False, ) assert len(matches) == 1 - # Call the same function to get only external matches + # internal matches only matches = patient_matches( - database=database, patient_id="P0001058", type="internal", with_results=False + database=database, + patient_id="P0001058", + type="internal", + with_results=False, ) assert len(matches) == 2 @responses.activate def test_match_hgnc_symbol_patient( - mock_app, gpx4_patients, test_client, database, mocked_ensemble_responses + mock_app, gpx4_patients, test_client, test_node, database, mocked_ensemble_responses ): - """Testing matching patient with gene symbol against patientMatcher database (internal matching)""" - - # add an authorized client to database - ok_token = test_client["auth_token"] - add_node(mongo_db=mock_app.db, obj=test_client, is_client=True) + ok_token = _setup(mock_app, test_client, test_node, database) query_patient = {"patient": gpx4_patients[0]} assert query_patient["patient"]["genomicFeatures"][0]["gene"]["id"] == "GPX4" - # load 2 test patient in mock database - assert len(gpx4_patients) == 2 - inserted_ids = [] - for pat in gpx4_patients: - # convert patient in mme patient type (convert also gene to ensembl) - mme_pat = mme_patient(pat, True) - inserted_ids.append(backend_add_patient(database, mme_pat)) + _setup_patients(database, gpx4_patients) - assert len(inserted_ids) == 2 - - # test the API response validator with non valid patient data: - malformed_match_results = {"results": "fakey_results"} - assert validate_response(malformed_match_results) == 422 - - # make sure that there are no patient matches in the 'matches collection' + assert validate_response({"results": "fakey_results"}) == 422 assert database["matches"].find_one() is None - # send a POST request to match patient with patients in database - response = mock_app.test_client().post( - MATCH_ENDPOINT, data=json.dumps(query_patient), headers=auth_headers(ok_token) - ) - assert response.status_code == 200 # POST request should be successful - data = json.loads(response.data) - # data should contain results and the max number of results is as defined in the config file - assert len(data["results"]) == 2 - assert type(data["results"]) == list # which is a list - assert "patient" in data["results"][0] # of patients - assert "score" in data["results"][0] # with matching scores - assert "contact" in data["results"][0]["patient"] # contact info should be available as well + _run_match_request(mock_app, MATCH_ENDPOINT, query_patient, ok_token) - # make sure that there are match object is created in db for this internal matching match = database["matches"].find_one() - for res in match["results"]: - for pat in res["patients"]: - assert pat["patient"]["contact"] # each result should have a contact person - assert pat["score"]["patient"] > 0 + _assert_valid_match_structure(match) @responses.activate def test_match_ensembl_patient( - mock_app, test_client, gpx4_patients, database, mocked_ensemble_responses + mock_app, test_client, gpx4_patients, test_node, database, mocked_ensemble_responses ): - """Test matching patient with ensembl gene against patientMatcher database (internal matching)""" - - # add an authorized client to database - ok_token = test_client["auth_token"] - add_node(mongo_db=mock_app.db, obj=test_client, is_client=True) + ok_token = _setup(mock_app, test_client, test_node, database) query_patient = {"patient": mme_patient(gpx4_patients[0], True)} assert query_patient["patient"]["genomicFeatures"][0]["gene"]["id"].startswith("ENSG") - # load 2 test patient in mock database - assert len(gpx4_patients) == 2 - inserted_ids = [] - for pat in gpx4_patients: - # convert patient in mme patient type (convert also gene to ensembl) - mme_pat = mme_patient(pat, True) - inserted_ids.append(backend_add_patient(database, mme_pat)) - - assert len(inserted_ids) == 2 + _setup_patients(database, gpx4_patients) - # make sure that there are no patient matches in the 'matches collection' assert database["matches"].find_one() is None - # send a POST request to match patient with patients in database - response = mock_app.test_client().post( - MATCH_ENDPOINT, data=json.dumps(query_patient), headers=auth_headers(ok_token) - ) - assert response.status_code == 200 # POST request should be successful - data = json.loads(response.data) - # data should contain results and the max number of results is as defined in the config file - assert len(data["results"]) == 2 - assert type(data["results"]) == list # which is a list - assert "patient" in data["results"][0] # of patients - assert "score" in data["results"][0] # with matching scores - assert "contact" in data["results"][0]["patient"] # contact info should be available as well + _run_match_request(mock_app, MATCH_ENDPOINT, query_patient, ok_token) - # make sure that a match object is created in db for this internal matching match = database["matches"].find_one() - for res in match["results"]: - for pat in res["patients"]: - assert pat["patient"]["contact"] # each result should have a contact person - assert pat["score"]["patient"] > 0 - # and query patient should have hgnc gene symbol saved as non-standard _geneName field + _assert_valid_match_structure(match) + assert match["data"]["patient"]["genomicFeatures"][0]["gene"]["_geneName"] == "GPX4" @responses.activate -def test_match_entrez_patient(mock_app, test_client, gpx4_patients, database): - """Test matching patient with ensembl gene against patientMatcher database (internal matching)""" +def test_match_entrez_patient(mock_app, test_client, gpx4_patients, test_node, database): + """Test matching patient with Ensembl gene against patientMatcher database (internal matching)""" - # GIVEN a mocked Ensembl REST API converting gene symbol to Ensembl ID + # Mock Ensembl services responses.add( responses.GET, - f"https://grch37.rest.ensembl.org/xrefs/symbol/homo_sapiens/GPX4?external_db=HGNC", + "https://grch37.rest.ensembl.org/xrefs/symbol/homo_sapiens/GPX4?external_db=HGNC", json=[{"id": "ENSG00000167468", "type": "gene"}], status=200, ) - # GIVEN a mocked Ensembl gene lookup service: responses.add( responses.GET, - f"https://grch37.rest.ensembl.org/lookup/id/ENSG00000167468", + "https://grch37.rest.ensembl.org/lookup/id/ENSG00000167468", json=[{"display_name": "GPX4"}], status=200, ) - # GIVEN a mocked liftover service: responses.add( responses.GET, - f"https://grch37.rest.ensembl.org/map/human/GRCh37/19:1105813..1105814/GRCh38?content-type=application/json", + "https://grch37.rest.ensembl.org/map/human/GRCh37/19:1105813..1105814/GRCh38?content-type=application/json", json=[], status=200, ) + responses.add( responses.GET, - f"https://grch37.rest.ensembl.org/map/human/GRCh37/19:1106232..1106238/GRCh38?content-type=application/json", + "https://grch37.rest.ensembl.org/map/human/GRCh37/19:1106232..1106238/GRCh38?content-type=application/json", json=[], status=200, ) - # add an authorized client to database - ok_token = test_client["auth_token"] - add_node(mongo_db=mock_app.db, obj=test_client, is_client=True) + # Setup authorized client + node + ok_token = _setup(mock_app, test_client, test_node, database) query_patient = {"patient": gpx4_patients[0]} for feat in query_patient["patient"]["genomicFeatures"]: assert feat["gene"]["id"] == "GPX4" - # load 2 test patient in mock database assert len(gpx4_patients) == 2 inserted_ids = [] for pat in gpx4_patients: - # convert patient in mme patient type (convert also gene to ensembl) mme_pat = mme_patient(pat, True) inserted_ids.append(backend_add_patient(database, mme_pat)) - assert len(inserted_ids) == 2 - # make sure that there are no patient matches in the 'matches collection' assert database["matches"].find_one() is None - # send a POST request to match patient with patients in database response = mock_app.test_client().post( - MATCH_ENDPOINT, data=json.dumps(query_patient), headers=auth_headers(ok_token) + MATCH_ENDPOINT, + data=json.dumps(query_patient), + headers=auth_headers(ok_token), ) - assert response.status_code == 200 # POST request should be successful + assert response.status_code == 200 + data = json.loads(response.data) - # data should contain results and the max number of results is as defined in the config file + assert isinstance(data["results"], list) assert len(data["results"]) == 2 - assert type(data["results"]) == list # which is a list - assert "patient" in data["results"][0] # of patients - assert "score" in data["results"][0] # with matching scores - assert "contact" in data["results"][0]["patient"] # contact info should be available as well + assert "patient" in data["results"][0] + assert "score" in data["results"][0] + assert "contact" in data["results"][0]["patient"] - # make sure that a match object is created in db for this internal matching match = database["matches"].find_one() for res in match["results"]: for pat in res["patients"]: - assert pat["patient"]["contact"] # each result should have a contact person + assert pat["patient"]["contact"] assert pat["score"]["patient"] > 0 - # and query patient should have hgnc gene symbol saved as non-standard _geneName field + assert match["data"]["patient"]["genomicFeatures"][0]["gene"]["_geneName"] == "GPX4"