diff --git a/pyproject.toml b/pyproject.toml index 227c591d9..b97f81c40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,6 +99,7 @@ mypy_path = "mypy_stubs" addopts = "-v -rP --import-mode=importlib --disable-socket --allow-hosts localhost,::1,127.0.0.1" asyncio_mode = 'strict' testpaths = "tests/" +pythonpath = "." norecursedirs = "tests/helpers/" # Uncomment the following lines to include application log output in Pytest logs. # log_cli = true diff --git a/src/mavedb/routers/score_sets.py b/src/mavedb/routers/score_sets.py index 202174d6c..d294d9c0b 100644 --- a/src/mavedb/routers/score_sets.py +++ b/src/mavedb/routers/score_sets.py @@ -651,7 +651,9 @@ async def upload_score_set_variant_data( return item -@router.put("/score-sets/{urn}", response_model=score_set.ScoreSet, responses={422: {}}) +@router.put( + "/score-sets/{urn}", response_model=score_set.ScoreSet, responses={422: {}}, response_model_exclude_none=True +) async def update_score_set( *, urn: str, @@ -673,68 +675,65 @@ async def update_score_set( assert_permission(user_data, item, Action.UPDATE) - # Editing unpublished score set - if item.private is True: - license_ = None + for var, value in vars(item_update).items(): + if var not in [ + "contributors", + "score_ranges", + "doi_identifiers", + "experiment_urn", + "license_id", + "secondary_publication_identifiers", + "primary_publication_identifiers", + "target_genes", + ]: + setattr(item, var, value) if value else None + + if item_update.license_id is not None: + save_to_logging_context({"license": item_update.license_id}) + license_ = db.query(License).filter(License.id == item_update.license_id).one_or_none() + + if not license_: + logger.info( + msg="Failed to update score set; The requested license does not exist.", extra=logging_context() + ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Unknown license") - if item_update.license_id is not None: - save_to_logging_context({"license": item_update.license_id}) - license_ = db.query(License).filter(License.id == item_update.license_id).one_or_none() + item.license = license_ - if not license_: - logger.info( - msg="Failed to update score set; The requested license does not exist.", extra=logging_context() - ) - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Unknown license") + item.doi_identifiers = [ + await find_or_create_doi_identifier(db, identifier.identifier) + for identifier in item_update.doi_identifiers or [] + ] + primary_publication_identifiers = [ + await find_or_create_publication_identifier(db, identifier.identifier, identifier.db_name) + for identifier in item_update.primary_publication_identifiers or [] + ] + publication_identifiers = [ + await find_or_create_publication_identifier(db, identifier.identifier, identifier.db_name) + for identifier in item_update.secondary_publication_identifiers or [] + ] + primary_publication_identifiers - item.license = license_ + # create a temporary `primary` attribute on each of our publications that indicates + # to our association proxy whether it is a primary publication or not + primary_identifiers = [p.identifier for p in primary_publication_identifiers] + for publication in publication_identifiers: + setattr(publication, "primary", publication.identifier in primary_identifiers) - for var, value in vars(item_update).items(): - if var not in [ - "contributors", - "score_ranges", - "doi_identifiers", - "experiment_urn", - "license_id", - "secondary_publication_identifiers", - "primary_publication_identifiers", - "target_genes", - ]: - setattr(item, var, value) if value else None - - try: - item.contributors = [ - await find_or_create_contributor(db, contributor.orcid_id) - for contributor in item_update.contributors or [] - ] - except NonexistentOrcidUserError as e: - logger.error(msg="Could not find ORCID user with the provided user ID.", extra=logging_context()) - raise pydantic.ValidationError( - [pydantic.error_wrappers.ErrorWrapper(ValidationError(str(e)), loc="contributors")], - model=score_set.ScoreSetUpdate, - ) + item.publication_identifiers = publication_identifiers - item.doi_identifiers = [ - await find_or_create_doi_identifier(db, identifier.identifier) - for identifier in item_update.doi_identifiers or [] - ] - primary_publication_identifiers = [ - await find_or_create_publication_identifier(db, identifier.identifier, identifier.db_name) - for identifier in item_update.primary_publication_identifiers or [] + try: + item.contributors = [ + await find_or_create_contributor(db, contributor.orcid_id) for contributor in item_update.contributors or [] ] - publication_identifiers = [ - await find_or_create_publication_identifier(db, identifier.identifier, identifier.db_name) - for identifier in item_update.secondary_publication_identifiers or [] - ] + primary_publication_identifiers - - # create a temporary `primary` attribute on each of our publications that indicates - # to our association proxy whether it is a primary publication or not - primary_identifiers = [pub.identifier for pub in primary_publication_identifiers] - for publication in publication_identifiers: - setattr(publication, "primary", publication.identifier in primary_identifiers) - - item.publication_identifiers = publication_identifiers + except NonexistentOrcidUserError as e: + logger.error(msg="Could not find ORCID user with the provided user ID.", extra=logging_context()) + raise pydantic.ValidationError( + [pydantic.error_wrappers.ErrorWrapper(ValidationError(str(e)), loc="contributors")], + model=score_set.ScoreSetUpdate, + ) + # Score set has not been published and attributes affecting scores may still be edited. + if item.private: if item_update.score_ranges: item.score_ranges = item_update.score_ranges.dict() else: @@ -889,35 +888,8 @@ async def update_score_set( if job is not None: save_to_logging_context({"worker_job_id": job.job_id}) logger.info(msg="Enqueud variant creation job.", extra=logging_context()) - - for var, value in vars(item_update).items(): - if var not in [ - "score_ranges", - "contributors", - "doi_identifiers", - "experiment_urn", - "primary_publication_identifiers", - "secondary_publication_identifiers", - "target_genes", - ]: - setattr(item, var, value) if value else None - - # Editing published score set else: - for var, value in vars(item_update).items(): - if var in ["title", "method_text", "abstract_text", "short_description"]: - setattr(item, var, value) if value else None - try: - item.contributors = [ - await find_or_create_contributor(db, contributor.orcid_id) - for contributor in item_update.contributors or [] - ] - except NonexistentOrcidUserError as e: - logger.error(msg="Could not find ORCID user with the provided user ID.", extra=logging_context()) - raise pydantic.ValidationError( - [pydantic.error_wrappers.ErrorWrapper(ValidationError(str(e)), loc="contributors")], - model=score_set.ScoreSetUpdate, - ) + logger.debug(msg="Skipped score range and target gene update. Score set is published.", extra=logging_context()) db.add(item) db.commit() diff --git a/src/mavedb/view_models/score_set.py b/src/mavedb/view_models/score_set.py index 0b2011134..a24a16172 100644 --- a/src/mavedb/view_models/score_set.py +++ b/src/mavedb/view_models/score_set.py @@ -254,7 +254,6 @@ def wild_type_score_in_normal_range(cls, field_value: Optional[ScoreRanges]): range_model.range for range_model in field_value.ranges if range_model.classification == "normal" ] for range in normal_ranges: - print(range) if field_value.wt_score >= inf_or_float(range[0], lower=True) and field_value.wt_score < inf_or_float( range[1], lower=False ): diff --git a/tests/helpers/constants.py b/tests/helpers/constants.py index 75052d437..e74f0a0d3 100644 --- a/tests/helpers/constants.py +++ b/tests/helpers/constants.py @@ -13,6 +13,26 @@ VALID_ACCESSION = "NM_001637.3" VALID_GENE = "BRCA1" +SAVED_PUBMED_PUBLICATION = { + "identifier": "20711194", + "dbName": "PubMed", + "title": "None", + "authors": [], + "abstract": "test", + "doi": "test", + "publicationYear": 1999, + "publicationJournal": "test", + "url": "http://www.ncbi.nlm.nih.gov/pubmed/20711194", + "referenceHtml": ". None. test. 1999; (Unknown volume):(Unknown pages). test", + "id": 1, +} + +SAVED_DOI_IDENTIFIER = { + "identifier": TEST_CROSSREF_IDENTIFIER, + "url": f"https://doi.org/{TEST_CROSSREF_IDENTIFIER}", + "id": 1, +} + TEST_USER = { "username": "0000-1111-2222-3333", "first_name": "First", @@ -24,6 +44,18 @@ "is_first_login": True, } +CONTRIBUTOR = { + "orcid_id": TEST_USER["username"], + "given_name": TEST_USER["first_name"], + "family_name": TEST_USER["last_name"], +} + +SAVED_CONTRIBUTOR = { + "orcidId": TEST_USER["username"], + "givenName": TEST_USER["first_name"], + "familyName": TEST_USER["last_name"], +} + TEST_USER_DECODED_JWT = { "sub": TEST_USER["username"], "given_name": TEST_USER["first_name"], @@ -41,6 +73,18 @@ "is_first_login": True, } +EXTRA_CONTRIBUTOR = { + "orcid_id": EXTRA_USER["username"], + "given_name": EXTRA_USER["first_name"], + "family_name": EXTRA_USER["last_name"], +} + +SAVED_EXTRA_CONTRIBUTOR = { + "orcidId": EXTRA_USER["username"], + "givenName": EXTRA_USER["first_name"], + "familyName": EXTRA_USER["last_name"], +} + EXTRA_USER_DECODED_JWT = { "sub": EXTRA_USER["username"], "given_name": EXTRA_USER["first_name"], @@ -293,6 +337,31 @@ "version": "1.0", } +SAVED_SHORT_TEST_LICENSE = { + "id": TEST_LICENSE["id"], + "shortName": TEST_LICENSE["short_name"], + "longName": TEST_LICENSE["long_name"], + "link": TEST_LICENSE["link"], + "version": TEST_LICENSE["version"], +} + +EXTRA_LICENSE = { + "id": 2, + "short_name": "Extra", + "long_name": "License", + "text": "Don't be tooooo evil.", + "link": "localhost", + "version": "1.0", +} + +SAVED_SHORT_EXTRA_LICENSE = { + "id": EXTRA_LICENSE["id"], + "shortName": EXTRA_LICENSE["short_name"], + "longName": EXTRA_LICENSE["long_name"], + "link": EXTRA_LICENSE["link"], + "version": EXTRA_LICENSE["version"], +} + TEST_SEQ_SCORESET = { "title": "Test Score Set Title", "short_description": "Test score set", @@ -457,6 +526,7 @@ "targetGenes": [ { "name": "TEST2", + "id": 2, "category": "protein_coding", "externalIdentifiers": [], "targetAccession": {"accession": VALID_ACCESSION, "assembly": "GRCh37", "gene": VALID_GENE}, @@ -515,3 +585,20 @@ "dcd_mapping_version": "pytest.0.0", "mapped_date_utc": datetime.isoformat(datetime.now()), } + + +TEST_SCORESET_RANGE = { + "wt_score": 1.0, + "ranges": [ + {"label": "test1", "classification": "normal", "range": (0, 2.0)}, + {"label": "test2", "classification": "abnormal", "range": (-2.0, 0)}, + ], +} + +TEST_SAVED_SCORESET_RANGE = { + "wtScore": 1.0, + "ranges": [ + {"label": "test1", "classification": "normal", "range": [0.0, 2.0]}, + {"label": "test2", "classification": "abnormal", "range": [-2.0, 0.0]}, + ], +} diff --git a/tests/helpers/util.py b/tests/helpers/util.py index cda39c998..ee872004e 100644 --- a/tests/helpers/util.py +++ b/tests/helpers/util.py @@ -5,6 +5,7 @@ import jsonschema from arq import ArqRedis from sqlalchemy import select +from sqlalchemy.exc import NoResultFound from mavedb.lib.score_sets import columns_for_dataset, create_variants, create_variants_data, csv_data_to_df from mavedb.lib.validation.dataframe import validate_and_standardize_dataframe_pair @@ -27,8 +28,13 @@ def add_contributor(db, urn, model, orcid_id: str, given_name: str, family_name: """Without making an API call, add a new contributor to the record (experiment or score set) with given urn and model.""" item = db.query(model).filter(model.urn == urn).one_or_none() assert item is not None - contributor = Contributor(orcid_id=orcid_id, given_name=given_name, family_name=family_name) - db.add(contributor) + + try: + contributor = db.execute(select(Contributor).where(Contributor.orcid_id == orcid_id)).one() + except NoResultFound: + contributor = Contributor(orcid_id=orcid_id, given_name=given_name, family_name=family_name) + db.add(contributor) + item.contributors = [contributor] db.add(item) db.commit() @@ -214,3 +220,16 @@ def mark_user_inactive(session, username): async def awaitable_exception(): return Exception() + + +def update_expected_response_for_created_resources(expected_response, created_experiment, created_score_set): + expected_response.update({"urn": created_score_set["urn"]}) + expected_response["experiment"].update( + { + "urn": created_experiment["urn"], + "experimentSetUrn": created_experiment["experimentSetUrn"], + "scoreSetUrns": [created_score_set["urn"]], + } + ) + + return expected_response diff --git a/tests/routers/conftest.py b/tests/routers/conftest.py index 5317ee0ce..e0ef23eee 100644 --- a/tests/routers/conftest.py +++ b/tests/routers/conftest.py @@ -6,6 +6,7 @@ import pytest from mavedb.models.controlled_keyword import ControlledKeyword +from mavedb.models.contributor import Contributor from mavedb.models.enums.user_role import UserRole from mavedb.models.license import License from mavedb.models.role import Role @@ -14,9 +15,11 @@ from tests.helpers.constants import ( ADMIN_USER, EXTRA_USER, + EXTRA_CONTRIBUTOR, TEST_CDOT_TRANSCRIPT, TEST_DB_KEYWORDS, TEST_LICENSE, + EXTRA_LICENSE, TEST_TAXONOMY, TEST_USER, ) @@ -41,6 +44,8 @@ def setup_router_db(session): db.add(User(**ADMIN_USER, role_objs=[Role(name=UserRole.admin)])) db.add(Taxonomy(**TEST_TAXONOMY)) db.add(License(**TEST_LICENSE)) + db.add(License(**EXTRA_LICENSE)) + db.add(Contributor(**EXTRA_CONTRIBUTOR)) db.bulk_save_objects([ControlledKeyword(**keyword_obj) for keyword_obj in TEST_DB_KEYWORDS]) db.commit() diff --git a/tests/routers/test_score_set.py b/tests/routers/test_score_set.py index 25d13b9b4..1c5d8832b 100644 --- a/tests/routers/test_score_set.py +++ b/tests/routers/test_score_set.py @@ -4,10 +4,12 @@ from unittest.mock import patch import jsonschema +import pytest from arq import ArqRedis +from humps import camelize from sqlalchemy import select -from mavedb.lib.validation.urn_re import MAVEDB_TMP_URN_RE +from mavedb.lib.validation.urn_re import MAVEDB_TMP_URN_RE, MAVEDB_SCORE_SET_URN_RE, MAVEDB_EXPERIMENT_URN_RE from mavedb.models.enums.processing_state import ProcessingState from mavedb.models.experiment import Experiment as ExperimentDbModel from mavedb.models.score_set import ScoreSet as ScoreSetDbModel @@ -16,11 +18,21 @@ from mavedb.view_models.score_set import ScoreSet, ScoreSetCreate from tests.helpers.constants import ( EXTRA_USER, + EXTRA_LICENSE, + TEST_CROSSREF_IDENTIFIER, TEST_MINIMAL_ACC_SCORESET, TEST_MINIMAL_SEQ_SCORESET, TEST_MINIMAL_SEQ_SCORESET_RESPONSE, + TEST_PUBMED_IDENTIFIER, TEST_ORCID_ID, + TEST_SCORESET_RANGE, + TEST_SAVED_SCORESET_RANGE, + TEST_MINIMAL_ACC_SCORESET_RESPONSE, TEST_USER, + SAVED_DOI_IDENTIFIER, + SAVED_EXTRA_CONTRIBUTOR, + SAVED_PUBMED_PUBLICATION, + SAVED_SHORT_EXTRA_LICENSE, ) from tests.helpers.dependency_overrider import DependencyOverrider from tests.helpers.util import ( @@ -29,9 +41,15 @@ create_experiment, create_seq_score_set, create_seq_score_set_with_variants, + update_expected_response_for_created_resources, ) +######################################################################################################################## +# Score set schemas +######################################################################################################################## + + def test_TEST_MINIMAL_SEQ_SCORESET_is_valid(): jsonschema.validate(instance=TEST_MINIMAL_SEQ_SCORESET, schema=ScoreSetCreate.schema()) @@ -40,27 +58,31 @@ def test_TEST_MINIMAL_ACC_SCORESET_is_valid(): jsonschema.validate(instance=TEST_MINIMAL_ACC_SCORESET, schema=ScoreSetCreate.schema()) +######################################################################################################################## +# Score set creation +######################################################################################################################## + + def test_create_minimal_score_set(client, setup_router_db): experiment = create_experiment(client) score_set_post_payload = deepcopy(TEST_MINIMAL_SEQ_SCORESET) score_set_post_payload["experimentUrn"] = experiment["urn"] + response = client.post("/api/v1/score-sets/", json=score_set_post_payload) assert response.status_code == 200 response_data = response.json() + jsonschema.validate(instance=response_data, schema=ScoreSet.schema()) assert isinstance(MAVEDB_TMP_URN_RE.fullmatch(response_data["urn"]), re.Match) - expected_response = deepcopy(TEST_MINIMAL_SEQ_SCORESET_RESPONSE) - expected_response.update({"urn": response_data["urn"]}) - expected_response["experiment"].update( - { - "urn": experiment["urn"], - "experimentSetUrn": experiment["experimentSetUrn"], - "scoreSetUrns": [response_data["urn"]], - } + + expected_response = update_expected_response_for_created_resources( + deepcopy(TEST_MINIMAL_SEQ_SCORESET_RESPONSE), experiment, response_data ) + assert sorted(expected_response.keys()) == sorted(response_data.keys()) for key in expected_response: assert (key, expected_response[key]) == (key, response_data[key]) + response = client.get(f"/api/v1/score-sets/{response_data['urn']}") assert response.status_code == 200 @@ -79,16 +101,12 @@ def test_create_score_set_with_contributor(client, setup_router_db): assert response.status_code == 200 response_data = response.json() + jsonschema.validate(instance=response_data, schema=ScoreSet.schema()) assert isinstance(MAVEDB_TMP_URN_RE.fullmatch(response_data["urn"]), re.Match) - expected_response = deepcopy(TEST_MINIMAL_SEQ_SCORESET_RESPONSE) - expected_response.update({"urn": response_data["urn"]}) - expected_response["experiment"].update( - { - "urn": experiment["urn"], - "experimentSetUrn": experiment["experimentSetUrn"], - "scoreSetUrns": [response_data["urn"]], - } + + expected_response = update_expected_response_for_created_resources( + deepcopy(TEST_MINIMAL_SEQ_SCORESET_RESPONSE), experiment, response_data ) expected_response["contributors"] = [ { @@ -97,9 +115,11 @@ def test_create_score_set_with_contributor(client, setup_router_db): "familyName": "User", } ] + assert sorted(expected_response.keys()) == sorted(response_data.keys()) for key in expected_response: assert (key, expected_response[key]) == (key, response_data[key]) + response = client.get(f"/api/v1/score-sets/{response_data['urn']}") assert response.status_code == 200 @@ -108,57 +128,24 @@ def test_create_score_set_with_score_range(client, setup_router_db): experiment = create_experiment(client) score_set = deepcopy(TEST_MINIMAL_SEQ_SCORESET) score_set["experimentUrn"] = experiment["urn"] - score_set.update( - { - "score_ranges": { - "wt_score": 0.5, - "ranges": [ - {"label": "range_1", "range": (-2, 2), "classification": "normal"}, - {"label": "range_2", "range": (2, None), "classification": "abnormal"}, - { - "label": "custom_1", - "range": (None, -2), - "classification": "abnormal", - "description": "A user provided custom range", - }, - ], - } - } - ) + score_set.update({"score_ranges": TEST_SCORESET_RANGE}) response = client.post("/api/v1/score-sets/", json=score_set) assert response.status_code == 200 - response_data = response.json() + jsonschema.validate(instance=response_data, schema=ScoreSet.schema()) assert isinstance(MAVEDB_TMP_URN_RE.fullmatch(response_data["urn"]), re.Match) - expected_response = deepcopy(TEST_MINIMAL_SEQ_SCORESET_RESPONSE) - expected_response.update({"urn": response_data["urn"]}) - expected_response["experiment"].update( - { - "urn": experiment["urn"], - "experimentSetUrn": experiment["experimentSetUrn"], - "scoreSetUrns": [response_data["urn"]], - } + expected_response = update_expected_response_for_created_resources( + deepcopy(TEST_MINIMAL_SEQ_SCORESET_RESPONSE), experiment, response_data ) - expected_response["scoreRanges"] = { - "wtScore": 0.5, - "ranges": [ - {"label": "range_1", "range": [-2, 2], "classification": "normal"}, - {"label": "range_2", "range": [2, None], "classification": "abnormal"}, - { - "label": "custom_1", - "range": [None, -2], - "classification": "abnormal", - "description": "A user provided custom range", - }, - ], - } + expected_response["scoreRanges"] = TEST_SAVED_SCORESET_RANGE assert sorted(expected_response.keys()) == sorted(response_data.keys()) for key in expected_response: assert (key, expected_response[key]) == (key, response_data[key]) + response = client.get(f"/api/v1/score-sets/{response_data['urn']}") assert response.status_code == 200 @@ -185,21 +172,222 @@ def test_cannot_create_score_set_with_invalid_target_gene_category(client, setup assert "value is not a valid enumeration member;" in response_data["detail"][0]["msg"] -def test_get_own_private_score_set(client, setup_router_db): +######################################################################################################################## +# Score set updating +######################################################################################################################## + + +@pytest.mark.parametrize( + "attribute,updated_data,expected_response_data", + [ + ("title", "Updated Title", "Updated Title"), + ("method_text", "Updated Method Text", "Updated Method Text"), + ("abstract_text", "Updated Abstract Text", "Updated Abstract Text"), + ("short_description", "Updated Abstract Text", "Updated Abstract Text"), + ("extra_metadata", {"updated": "metadata"}, {"updated": "metadata"}), + ("data_usage_policy", "data_usage_policy", "data_usage_policy"), + ("contributors", [{"orcid_id": EXTRA_USER["username"]}], [SAVED_EXTRA_CONTRIBUTOR]), + ("primary_publication_identifiers", [{"identifier": TEST_PUBMED_IDENTIFIER}], [SAVED_PUBMED_PUBLICATION]), + ("secondary_publication_identifiers", [{"identifier": TEST_PUBMED_IDENTIFIER}], [SAVED_PUBMED_PUBLICATION]), + ("doi_identifiers", [{"identifier": TEST_CROSSREF_IDENTIFIER}], [SAVED_DOI_IDENTIFIER]), + ("license_id", EXTRA_LICENSE["id"], SAVED_SHORT_EXTRA_LICENSE), + ("target_genes", TEST_MINIMAL_ACC_SCORESET["targetGenes"], TEST_MINIMAL_ACC_SCORESET_RESPONSE["targetGenes"]), + ("score_ranges", TEST_SCORESET_RANGE, TEST_SAVED_SCORESET_RANGE), + ], +) +@pytest.mark.parametrize( + "mock_publication_fetch", + [({"dbName": "PubMed", "identifier": f"{TEST_PUBMED_IDENTIFIER}"})], + indirect=["mock_publication_fetch"], +) +def test_can_update_score_set_data_before_publication( + client, setup_router_db, attribute, updated_data, expected_response_data, mock_publication_fetch +): experiment = create_experiment(client) score_set = create_seq_score_set(client, experiment["urn"]) - expected_response = deepcopy(TEST_MINIMAL_SEQ_SCORESET_RESPONSE) - expected_response.update({"urn": score_set["urn"]}) - expected_response["experiment"].update( + expected_response = update_expected_response_for_created_resources( + deepcopy(TEST_MINIMAL_SEQ_SCORESET_RESPONSE), experiment, score_set + ) + + response = client.get(f"/api/v1/score-sets/{score_set['urn']}") + assert response.status_code == 200 + response_data = response.json() + + assert sorted(expected_response.keys()) == sorted(response_data.keys()) + for key in expected_response: + assert (key, expected_response[key]) == (key, response_data[key]) + + score_set_update_payload = deepcopy(TEST_MINIMAL_SEQ_SCORESET) + score_set_update_payload.update({camelize(attribute): updated_data}) + response = client.put(f"/api/v1/score-sets/{score_set['urn']}", json=score_set_update_payload) + assert response.status_code == 200 + + response = client.get(f"/api/v1/score-sets/{score_set['urn']}") + assert response.status_code == 200 + response_data = response.json() + + # Although the client provides the license id, the response includes the full license. + if attribute == "license_id": + attribute = "license" + + assert expected_response_data == response_data[camelize(attribute)] + + +@pytest.mark.parametrize( + "attribute,updated_data,expected_response_data", + [ + ("title", "Updated Title", "Updated Title"), + ("method_text", "Updated Method Text", "Updated Method Text"), + ("abstract_text", "Updated Abstract Text", "Updated Abstract Text"), + ("short_description", "Updated Abstract Text", "Updated Abstract Text"), + ("extra_metadata", {"updated": "metadata"}, {"updated": "metadata"}), + ("data_usage_policy", "data_usage_policy", "data_usage_policy"), + ("contributors", [{"orcid_id": EXTRA_USER["username"]}], [SAVED_EXTRA_CONTRIBUTOR]), + ("primary_publication_identifiers", [{"identifier": TEST_PUBMED_IDENTIFIER}], [SAVED_PUBMED_PUBLICATION]), + ("secondary_publication_identifiers", [{"identifier": TEST_PUBMED_IDENTIFIER}], [SAVED_PUBMED_PUBLICATION]), + ("doi_identifiers", [{"identifier": TEST_CROSSREF_IDENTIFIER}], [SAVED_DOI_IDENTIFIER]), + ("license_id", EXTRA_LICENSE["id"], SAVED_SHORT_EXTRA_LICENSE), + ], +) +@pytest.mark.parametrize( + "mock_publication_fetch", + [({"dbName": "PubMed", "identifier": f"{TEST_PUBMED_IDENTIFIER}"})], + indirect=["mock_publication_fetch"], +) +def test_can_update_score_set_supporting_data_after_publication( + session, + data_provider, + client, + setup_router_db, + attribute, + updated_data, + expected_response_data, + mock_publication_fetch, + data_files, +): + experiment = create_experiment(client) + score_set = create_seq_score_set_with_variants( + client, session, data_provider, experiment["urn"], data_files / "scores.csv" + ) + + response = client.post(f"/api/v1/score-sets/{score_set['urn']}/publish") + assert response.status_code == 200 + published_urn = response.json()["urn"] + response = client.get(f"/api/v1/score-sets/{published_urn}") + assert response.status_code == 200 + response_data = response.json() + + expected_response = update_expected_response_for_created_resources( + deepcopy(TEST_MINIMAL_SEQ_SCORESET_RESPONSE), response_data["experiment"], response_data + ) + expected_response["experiment"].update({"publishedDate": date.today().isoformat()}) + expected_response.update( { - "urn": experiment["urn"], - "experimentSetUrn": experiment["experimentSetUrn"], - "scoreSetUrns": [score_set["urn"]], + "urn": published_urn, + "publishedDate": date.today().isoformat(), + "numVariants": 3, + "private": False, + "datasetColumns": {"countColumns": [], "scoreColumns": ["score"]}, + "processingState": ProcessingState.success.name, } ) + + assert sorted(expected_response.keys()) == sorted(response_data.keys()) + for key in expected_response: + assert (key, expected_response[key]) == (key, response_data[key]) + + score_set_update_payload = deepcopy(TEST_MINIMAL_SEQ_SCORESET) + score_set_update_payload.update({camelize(attribute): updated_data}) + response = client.put(f"/api/v1/score-sets/{published_urn}", json=score_set_update_payload) + assert response.status_code == 200 + + response = client.get(f"/api/v1/score-sets/{published_urn}") + assert response.status_code == 200 + response_data = response.json() + + # Although the client provides the license id, the response includes the full license. + if attribute == "license_id": + attribute = "license" + + assert expected_response_data == response_data[camelize(attribute)] + + +@pytest.mark.parametrize( + "attribute,updated_data,expected_response_data", + [ + ("target_genes", TEST_MINIMAL_ACC_SCORESET["targetGenes"], TEST_MINIMAL_SEQ_SCORESET_RESPONSE["targetGenes"]), + ( + "score_ranges", + TEST_SCORESET_RANGE, + None, + ), + ], +) +def test_cannot_update_score_set_target_data_after_publication( + client, setup_router_db, attribute, expected_response_data, updated_data, session, data_provider, data_files +): + experiment = create_experiment(client) + score_set = create_seq_score_set_with_variants( + client, session, data_provider, experiment["urn"], data_files / "scores.csv" + ) + + response = client.post(f"/api/v1/score-sets/{score_set['urn']}/publish") + assert response.status_code == 200 + published_urn = response.json()["urn"] + response = client.get(f"/api/v1/score-sets/{published_urn}") + assert response.status_code == 200 + response_data = response.json() + + expected_response = update_expected_response_for_created_resources( + deepcopy(TEST_MINIMAL_SEQ_SCORESET_RESPONSE), response_data["experiment"], response_data + ) + expected_response["experiment"].update({"publishedDate": date.today().isoformat()}) + expected_response.update( + { + "urn": published_urn, + "publishedDate": date.today().isoformat(), + "numVariants": 3, + "private": False, + "datasetColumns": {"countColumns": [], "scoreColumns": ["score"]}, + "processingState": ProcessingState.success.name, + } + ) + + assert sorted(expected_response.keys()) == sorted(response_data.keys()) + for key in expected_response: + assert (key, expected_response[key]) == (key, response_data[key]) + + score_set_update_payload = deepcopy(TEST_MINIMAL_SEQ_SCORESET) + score_set_update_payload.update({camelize(attribute): updated_data}) + response = client.put(f"/api/v1/score-sets/{published_urn}", json=score_set_update_payload) + assert response.status_code == 200 + + response = client.get(f"/api/v1/score-sets/{published_urn}") + assert response.status_code == 200 + response_data = response.json() + + if expected_response_data: + assert expected_response_data == response_data[camelize(attribute)] + else: + assert camelize(attribute) not in response_data.keys() + + +######################################################################################################################## +# Score set fetching +######################################################################################################################## + + +def test_get_own_private_score_set(client, setup_router_db): + experiment = create_experiment(client) + score_set = create_seq_score_set(client, experiment["urn"]) + expected_response = update_expected_response_for_created_resources( + deepcopy(TEST_MINIMAL_SEQ_SCORESET_RESPONSE), experiment, score_set + ) + response = client.get(f"/api/v1/score-sets/{score_set['urn']}") assert response.status_code == 200 response_data = response.json() + assert sorted(expected_response.keys()) == sorted(response_data.keys()) for key in expected_response: assert (key, expected_response[key]) == (key, response_data[key]) @@ -240,14 +428,8 @@ def test_contributor_can_get_other_users_private_score_set(session, client, setu TEST_USER["last_name"], ) - expected_response = deepcopy(TEST_MINIMAL_SEQ_SCORESET_RESPONSE) - expected_response.update({"urn": score_set["urn"]}) - expected_response["experiment"].update( - { - "urn": experiment["urn"], - "experimentSetUrn": experiment["experimentSetUrn"], - "scoreSetUrns": [score_set["urn"]], - } + expected_response = update_expected_response_for_created_resources( + deepcopy(TEST_MINIMAL_SEQ_SCORESET_RESPONSE), experiment, score_set ) expected_response["contributors"] = [ { @@ -266,9 +448,11 @@ def test_contributor_can_get_other_users_private_score_set(session, client, setu "firstName": EXTRA_USER["first_name"], "lastName": EXTRA_USER["last_name"], } + response = client.get(f"/api/v1/score-sets/{score_set['urn']}") assert response.status_code == 200 response_data = response.json() + assert sorted(expected_response.keys()) == sorted(response_data.keys()) for key in expected_response: assert (key, expected_response[key]) == (key, response_data[key]) @@ -277,15 +461,10 @@ def test_contributor_can_get_other_users_private_score_set(session, client, setu def test_admin_can_get_other_user_private_score_set(session, client, admin_app_overrides, setup_router_db): experiment = create_experiment(client) score_set = create_seq_score_set(client, experiment["urn"]) - expected_response = deepcopy(TEST_MINIMAL_SEQ_SCORESET_RESPONSE) - expected_response.update({"urn": score_set["urn"]}) - expected_response["experiment"].update( - { - "urn": experiment["urn"], - "experimentSetUrn": experiment["experimentSetUrn"], - "scoreSetUrns": [score_set["urn"]], - } + expected_response = update_expected_response_for_created_resources( + deepcopy(TEST_MINIMAL_SEQ_SCORESET_RESPONSE), experiment, score_set ) + with DependencyOverrider(admin_app_overrides): response = client.get(f"/api/v1/score-sets/{score_set['urn']}") @@ -296,6 +475,11 @@ def test_admin_can_get_other_user_private_score_set(session, client, admin_app_o assert (key, expected_response[key]) == (key, response_data[key]) +######################################################################################################################## +# Adding scores to score set +######################################################################################################################## + + def test_add_score_set_variants_scores_only_endpoint(client, setup_router_db, data_files): experiment = create_experiment(client) score_set = create_seq_score_set(client, experiment["urn"]) @@ -621,6 +805,11 @@ def test_admin_can_add_scores_and_counts_to_other_user_score_set(session, client assert score_set == response_data +######################################################################################################################## +# Score set publication +######################################################################################################################## + + def test_publish_score_set(session, data_provider, client, setup_router_db, data_files): experiment = create_experiment(client) score_set = create_seq_score_set_with_variants( @@ -630,10 +819,13 @@ def test_publish_score_set(session, data_provider, client, setup_router_db, data response = client.post(f"/api/v1/score-sets/{score_set['urn']}/publish") assert response.status_code == 200 response_data = response.json() - assert response_data["urn"] == "urn:mavedb:00000001-a-1" - assert response_data["experiment"]["urn"] == "urn:mavedb:00000001-a" + assert isinstance(MAVEDB_SCORE_SET_URN_RE.fullmatch(response_data["urn"]), re.Match) + assert isinstance(MAVEDB_EXPERIMENT_URN_RE.fullmatch(response_data["experiment"]["urn"]), re.Match) - expected_response = deepcopy(TEST_MINIMAL_SEQ_SCORESET_RESPONSE) + expected_response = update_expected_response_for_created_resources( + deepcopy(TEST_MINIMAL_SEQ_SCORESET_RESPONSE), response_data["experiment"], response_data + ) + expected_response["experiment"].update({"publishedDate": date.today().isoformat()}) expected_response.update( { "urn": response_data["urn"], @@ -644,14 +836,6 @@ def test_publish_score_set(session, data_provider, client, setup_router_db, data "processingState": ProcessingState.success.name, } ) - expected_response["experiment"].update( - { - "urn": response_data["experiment"]["urn"], - "experimentSetUrn": response_data["experiment"]["experimentSetUrn"], - "scoreSetUrns": [response_data["urn"]], - "publishedDate": date.today().isoformat(), - } - ) assert sorted(expected_response.keys()) == sorted(response_data.keys()) # refresh score set to post worker state @@ -768,7 +952,10 @@ def test_contributor_can_publish_other_users_score_set(session, data_provider, c assert response_data["urn"] == "urn:mavedb:00000001-a-1" assert response_data["experiment"]["urn"] == "urn:mavedb:00000001-a" - expected_response = deepcopy(TEST_MINIMAL_SEQ_SCORESET_RESPONSE) + expected_response = update_expected_response_for_created_resources( + deepcopy(TEST_MINIMAL_SEQ_SCORESET_RESPONSE), response_data["experiment"], response_data + ) + expected_response["experiment"].update({"publishedDate": date.today().isoformat()}) expected_response.update( { "urn": response_data["urn"], @@ -779,14 +966,6 @@ def test_contributor_can_publish_other_users_score_set(session, data_provider, c "processingState": ProcessingState.success.name, } ) - expected_response["experiment"].update( - { - "urn": response_data["experiment"]["urn"], - "experimentSetUrn": response_data["experiment"]["experimentSetUrn"], - "scoreSetUrns": [response_data["urn"]], - "publishedDate": date.today().isoformat(), - } - ) expected_response["contributors"] = [ { "orcidId": TEST_USER["username"], @@ -833,6 +1012,11 @@ def test_admin_cannot_publish_other_user_private_score_set( assert f"score set with URN '{score_set['urn']}' not found" in response_data["detail"] +######################################################################################################################## +# Score set meta-analysis +######################################################################################################################## + + def test_create_single_score_set_meta_analysis(session, data_provider, client, setup_router_db, data_files): experiment = create_experiment(client) score_set = create_seq_score_set_with_variants( @@ -1057,6 +1241,11 @@ def test_multiple_score_set_meta_analysis_multiple_experiment_sets_different_sco assert meta_score_set_3["urn"] == "urn:mavedb:00000003-0-3" +######################################################################################################################## +# Score set search +######################################################################################################################## + + def test_search_score_sets_no_match(session, data_provider, client, setup_router_db, data_files): experiment_1 = create_experiment(client, {"title": "Experiment 1"}) create_seq_score_set_with_variants( @@ -1119,6 +1308,11 @@ def test_search_score_sets_urn_with_space_match(session, data_provider, client, assert response.json()[0]["urn"] == score_set_1_1["urn"] +######################################################################################################################## +# Score set deletion +######################################################################################################################## + + def test_anonymous_cannot_delete_other_users_private_scoreset( session, data_provider, client, setup_router_db, data_files, anonymous_app_overrides ): @@ -1230,6 +1424,11 @@ def test_admin_can_delete_other_users_published_scoreset( assert del_response.status_code == 200 +######################################################################################################################## +# Adding score sets to experiments +######################################################################################################################## + + def test_can_add_score_set_to_own_private_experiment(session, client, setup_router_db): experiment = create_experiment(client) score_set_post_payload = deepcopy(TEST_MINIMAL_SEQ_SCORESET)