diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/fixtures/rdf.py | 7 | ||||
| -rw-r--r-- | tests/integration/conftest.py | 15 | ||||
| -rw-r--r-- | tests/integration/test_gemma.py | 66 | ||||
| -rw-r--r-- | tests/integration/test_lmdb_sample_data.py | 31 | ||||
| -rw-r--r-- | tests/integration/test_partial_correlations.py | 2 | ||||
| -rwxr-xr-x | tests/test_data/lmdb-test-data/7308efbd84b33ad3d69d14b5b1f19ccc/data.mdb | bin | 0 -> 32768 bytes | |||
| -rwxr-xr-x | tests/test_data/lmdb-test-data/7308efbd84b33ad3d69d14b5b1f19ccc/lock.mdb | bin | 0 -> 8192 bytes | |||
| -rw-r--r-- | tests/test_data/ttl-files/test-data.ttl | 273 | ||||
| -rw-r--r-- | tests/unit/computations/test_partial_correlations.py | 4 | ||||
| -rw-r--r-- | tests/unit/computations/test_wgcna.py | 10 | ||||
| -rw-r--r-- | tests/unit/conftest.py | 14 | ||||
| -rw-r--r-- | tests/unit/db/rdf/data.py | 199 | ||||
| -rw-r--r-- | tests/unit/db/rdf/test_wiki.py | 50 | ||||
| -rw-r--r-- | tests/unit/db/test_case_attributes.py | 471 | ||||
| -rw-r--r-- | tests/unit/db/test_gen_menu.py | 4 | ||||
| -rw-r--r-- | tests/unit/test_db_utils.py | 70 | ||||
| -rw-r--r-- | tests/unit/test_llm.py | 138 | ||||
| -rw-r--r-- | tests/unit/test_rqtl2.py | 123 |
18 files changed, 1229 insertions, 248 deletions
diff --git a/tests/fixtures/rdf.py b/tests/fixtures/rdf.py index 98c4058..0811d3c 100644 --- a/tests/fixtures/rdf.py +++ b/tests/fixtures/rdf.py @@ -59,7 +59,8 @@ def rdf_setup(): # Make sure this graph does not exist before running anything requests.delete( - SPARQL_CONF["sparql_crud_auth_uri"], params=params, auth=auth + SPARQL_CONF["sparql_crud_auth_uri"], params=params, auth=auth, + timeout=300 ) # Open the file in binary mode and send the request @@ -69,9 +70,11 @@ def rdf_setup(): params=params, auth=auth, data=file, + timeout=300 ) yield response requests.delete( - SPARQL_CONF["sparql_crud_auth_uri"], params=params, auth=auth + SPARQL_CONF["sparql_crud_auth_uri"], params=params, auth=auth, + timeout=300 ) pid.terminate() diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 8e39726..bdbab09 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,4 +1,5 @@ """Module that holds fixtures for integration tests""" +from pathlib import Path import pytest import MySQLdb @@ -6,19 +7,25 @@ from gn3.app import create_app from gn3.chancy import random_string from gn3.db_utils import parse_db_url, database_connection + @pytest.fixture(scope="session") def client(): """Create a test client fixture for tests""" # Do some setup - app = create_app() - app.config.update({"TESTING": True}) - app.testing = True + app = create_app({ + "TESTING": True, + "LMDB_DATA_PATH": str( + Path(__file__).parent.parent / + Path("test_data/lmdb-test-data") + ), + "AUTH_SERVER_URL": "http://127.0.0.1:8081", + }) yield app.test_client() # Do some teardown/cleanup @pytest.fixture(scope="session") -def db_conn(client): # pylint: disable=[redefined-outer-name] +def db_conn(client): # pylint: disable=[redefined-outer-name] """Create a db connection fixture for tests""" # 01) Generate random string to append to all test db artifacts for the session live_db_uri = client.application.config["SQL_URI"] diff --git a/tests/integration/test_gemma.py b/tests/integration/test_gemma.py index 53a1596..7bc1df9 100644 --- a/tests/integration/test_gemma.py +++ b/tests/integration/test_gemma.py @@ -63,10 +63,9 @@ class GemmaAPITest(unittest.TestCase): @mock.patch("gn3.api.gemma.assert_paths_exist") @mock.patch("gn3.api.gemma.redis.Redis") @mock.patch("gn3.api.gemma.cache_ipfs_file") - def test_k_compute(self, mock_ipfs_cache, - mock_redis, - mock_path_exist, mock_json, mock_hash, - mock_queue_cmd): + def test_k_compute(# pylint: disable=[too-many-positional-arguments] + self, mock_ipfs_cache, mock_redis, mock_path_exist, mock_json, + mock_hash, mock_queue_cmd): """Test /gemma/k-compute/<token>""" mock_ipfs_cache.return_value = ("/tmp/cache/" "QmQPeNsJPyVWPFDVHb" @@ -106,9 +105,9 @@ class GemmaAPITest(unittest.TestCase): @mock.patch("gn3.api.gemma.assert_paths_exist") @mock.patch("gn3.api.gemma.redis.Redis") @mock.patch("gn3.api.gemma.cache_ipfs_file") - def test_k_compute_loco(self, mock_ipfs_cache, - mock_redis, mock_path_exist, mock_json, - mock_hash, mock_queue_cmd): + def test_k_compute_loco(# pylint: disable=[too-many-positional-arguments] + self, mock_ipfs_cache, mock_redis, mock_path_exist, mock_json, + mock_hash, mock_queue_cmd): """Test /gemma/k-compute/loco/<chromosomes>/<token>""" mock_ipfs_cache.return_value = ("/tmp/cache/" "QmQPeNsJPyVWPFDVHb" @@ -150,9 +149,9 @@ class GemmaAPITest(unittest.TestCase): @mock.patch("gn3.api.gemma.assert_paths_exist") @mock.patch("gn3.api.gemma.redis.Redis") @mock.patch("gn3.api.gemma.cache_ipfs_file") - def test_gwa_compute(self, mock_ipfs_cache, - mock_redis, mock_path_exist, mock_json, - mock_hash, mock_queue_cmd): + def test_gwa_compute(# pylint: disable=[too-many-positional-arguments] + self, mock_ipfs_cache, mock_redis, mock_path_exist, mock_json, + mock_hash, mock_queue_cmd): """Test /gemma/gwa-compute/<k-inputfile>/<token>""" mock_ipfs_cache.return_value = ("/tmp/cache/" "QmQPeNsJPyVWPFDVHb" @@ -201,9 +200,9 @@ class GemmaAPITest(unittest.TestCase): @mock.patch("gn3.api.gemma.assert_paths_exist") @mock.patch("gn3.api.gemma.redis.Redis") @mock.patch("gn3.api.gemma.cache_ipfs_file") - def test_gwa_compute_with_covars(self, mock_ipfs_cache, - mock_redis, mock_path_exist, - mock_json, mock_hash, mock_queue_cmd): + def test_gwa_compute_with_covars(# pylint: disable=[too-many-positional-arguments] + self, mock_ipfs_cache, mock_redis, mock_path_exist, mock_json, + mock_hash, mock_queue_cmd): """Test /gemma/gwa-compute/covars/<k-inputfile>/<token>""" mock_ipfs_cache.return_value = ("/tmp/cache/" "QmQPeNsJPyVWPFDVHb" @@ -255,9 +254,9 @@ class GemmaAPITest(unittest.TestCase): @mock.patch("gn3.api.gemma.assert_paths_exist") @mock.patch("gn3.api.gemma.redis.Redis") @mock.patch("gn3.api.gemma.cache_ipfs_file") - def test_gwa_compute_with_loco_only(self, mock_ipfs_cache, - mock_redis, mock_path_exist, - mock_json, mock_hash, mock_queue_cmd): + def test_gwa_compute_with_loco_only(# pylint: disable=[too-many-positional-arguments] + self, mock_ipfs_cache, mock_redis, mock_path_exist, mock_json, + mock_hash, mock_queue_cmd): """Test /gemma/gwa-compute/<k-inputfile>/loco/maf/<maf>/<token> """ @@ -308,10 +307,9 @@ class GemmaAPITest(unittest.TestCase): @mock.patch("gn3.api.gemma.assert_paths_exist") @mock.patch("gn3.api.gemma.redis.Redis") @mock.patch("gn3.api.gemma.cache_ipfs_file") - def test_gwa_compute_with_loco_covars(self, mock_ipfs_cache, - mock_redis, mock_path_exist, - mock_json, mock_hash, - mock_queue_cmd): + def test_gwa_compute_with_loco_covars(# pylint: disable=[too-many-positional-arguments] + self, mock_ipfs_cache, mock_redis, mock_path_exist, mock_json, + mock_hash, mock_queue_cmd): """Test /gemma/gwa-compute/<k-inputfile>/loco/covars/maf/<maf>/<token> """ @@ -363,10 +361,9 @@ class GemmaAPITest(unittest.TestCase): @mock.patch("gn3.api.gemma.assert_paths_exist") @mock.patch("gn3.api.gemma.redis.Redis") @mock.patch("gn3.api.gemma.cache_ipfs_file") - def test_k_gwa_compute_without_loco_covars(self, mock_ipfs_cache, - mock_redis, - mock_path_exist, mock_json, - mock_hash, mock_queue_cmd): + def test_k_gwa_compute_without_loco_covars(# pylint: disable=[too-many-positional-arguments] + self, mock_ipfs_cache, mock_redis, mock_path_exist, mock_json, + mock_hash, mock_queue_cmd): """Test /gemma/k-gwa-compute/<token> """ @@ -419,10 +416,9 @@ class GemmaAPITest(unittest.TestCase): @mock.patch("gn3.api.gemma.assert_paths_exist") @mock.patch("gn3.api.gemma.redis.Redis") @mock.patch("gn3.api.gemma.cache_ipfs_file") - def test_k_gwa_compute_with_covars_only(self, mock_ipfs_cache, - mock_redis, mock_path_exist, - mock_json, mock_hash, - mock_queue_cmd): + def test_k_gwa_compute_with_covars_only(# pylint: disable=[too-many-positional-arguments] + self, mock_ipfs_cache, mock_redis, mock_path_exist, mock_json, + mock_hash, mock_queue_cmd): """Test /gemma/k-gwa-compute/covars/<token> """ @@ -484,10 +480,9 @@ class GemmaAPITest(unittest.TestCase): @mock.patch("gn3.api.gemma.assert_paths_exist") @mock.patch("gn3.api.gemma.redis.Redis") @mock.patch("gn3.api.gemma.cache_ipfs_file") - def test_k_gwa_compute_with_loco_only(self, mock_ipfs_cache, - mock_redis, mock_path_exist, - mock_json, mock_hash, - mock_queue_cmd): + def test_k_gwa_compute_with_loco_only(# pylint: disable=[too-many-positional-arguments] + self, mock_ipfs_cache, mock_redis, mock_path_exist, mock_json, + mock_hash, mock_queue_cmd): """Test /gemma/k-gwa-compute/loco/<chromosomes>/maf/<maf>/<token> """ @@ -550,10 +545,9 @@ class GemmaAPITest(unittest.TestCase): @mock.patch("gn3.api.gemma.assert_paths_exist") @mock.patch("gn3.api.gemma.redis.Redis") @mock.patch("gn3.api.gemma.cache_ipfs_file") - def test_k_gwa_compute_with_loco_and_covar(self, mock_ipfs_cache, - mock_redis, - mock_path_exist, mock_json, - mock_hash, mock_queue_cmd): + def test_k_gwa_compute_with_loco_and_covar(# pylint: disable=[too-many-positional-arguments] + self, mock_ipfs_cache, mock_redis, mock_path_exist, mock_json, + mock_hash, mock_queue_cmd): """Test /k-gwa-compute/covars/loco/<chromosomes>/maf/<maf>/<token> """ diff --git a/tests/integration/test_lmdb_sample_data.py b/tests/integration/test_lmdb_sample_data.py new file mode 100644 index 0000000..30a23f4 --- /dev/null +++ b/tests/integration/test_lmdb_sample_data.py @@ -0,0 +1,31 @@ +"""Tests for the LMDB sample data API endpoint""" +import pytest + + +@pytest.mark.unit_test +def test_nonexistent_data(client): + """Test endpoint returns 404 when data doesn't exist""" + response = client.get("/api/lmdb/sample-data/nonexistent/123") + assert response.status_code == 404 + assert response.json["error"] == "No data found for given dataset and trait" + + +@pytest.mark.unit_test +def test_successful_retrieval(client): + """Test successful data retrieval using test LMDB data""" + # Use known test data hash: 7308efbd84b33ad3d69d14b5b1f19ccc + response = client.get("/api/lmdb/sample-data/BXDPublish/10007") + assert response.status_code == 200 + + data = response.json + assert len(data) == 31 + # Verify some known values from the test database + assert data["BXD1"] == 18.700001 + assert data["BXD11"] == 18.9 + + +@pytest.mark.unit_test +def test_invalid_trait_id(client): + """Test endpoint handles invalid trait IDs appropriately""" + response = client.get("/api/lmdb/sample-data/BXDPublish/999999") + assert response.status_code == 404 diff --git a/tests/integration/test_partial_correlations.py b/tests/integration/test_partial_correlations.py index fc9f64f..56af260 100644 --- a/tests/integration/test_partial_correlations.py +++ b/tests/integration/test_partial_correlations.py @@ -221,4 +221,4 @@ def test_part_corr_api_with_mix_of_existing_and_non_existing_control_traits( criteria = 10 with pytest.warns(UserWarning): partial_correlations_with_target_db( - db_conn, primary, controls, method, criteria, target) + db_conn, primary, controls, method, criteria, target, "/tmp") diff --git a/tests/test_data/lmdb-test-data/7308efbd84b33ad3d69d14b5b1f19ccc/data.mdb b/tests/test_data/lmdb-test-data/7308efbd84b33ad3d69d14b5b1f19ccc/data.mdb new file mode 100755 index 0000000..5fa213b --- /dev/null +++ b/tests/test_data/lmdb-test-data/7308efbd84b33ad3d69d14b5b1f19ccc/data.mdb Binary files differdiff --git a/tests/test_data/lmdb-test-data/7308efbd84b33ad3d69d14b5b1f19ccc/lock.mdb b/tests/test_data/lmdb-test-data/7308efbd84b33ad3d69d14b5b1f19ccc/lock.mdb new file mode 100755 index 0000000..116d824 --- /dev/null +++ b/tests/test_data/lmdb-test-data/7308efbd84b33ad3d69d14b5b1f19ccc/lock.mdb Binary files differdiff --git a/tests/test_data/ttl-files/test-data.ttl b/tests/test_data/ttl-files/test-data.ttl index 3e27652..c570484 100644 --- a/tests/test_data/ttl-files/test-data.ttl +++ b/tests/test_data/ttl-files/test-data.ttl @@ -1054,3 +1054,276 @@ gn:wiki-7273-0 dct:created "2022-08-24 18:34:41"^^xsd:datetime . gn:wiki-7273-0 foaf:mbox <XXX@XXX.com> . gn:wiki-7273-0 dct:identifier "7273"^^xsd:integer . gn:wiki-7273-0 dct:hasVersion "0"^^xsd:integer . + +gnc:NCBIWikiEntry rdfs:subClassOf gnc:GeneWikiEntry . +gnc:NCBIWikiEntry rdfs:comment "Represents GeneRIF Entries obtained from NCBI" . +gn:rif-12709-37156912-2023-05-17T20:43:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'Creatine kinase B suppresses ferroptosis by phosphorylating GPX4 through a moonlighting function.'@en ; + gnt:belongsToSpecies gn:Mus_musculus ; + gnt:symbol "Ckb" ; + gnt:hasGeneId generif:12709 ; + skos:notation taxon:10090 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:37156912 ; + dct:created "2023-05-17 20:43:00"^^xsd:datetime . +gn:rif-13176-36456775-2023-03-01T20:36:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'DCC/netrin-1 regulates cell death in oligodendrocytes after brain injury.'@en ; + gnt:belongsToSpecies gn:Mus_musculus ; + gnt:symbol "Dcc" ; + gnt:hasGeneId generif:13176 ; + skos:notation taxon:10090 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:36456775 ; + dct:created "2023-03-01 20:36:00"^^xsd:datetime . +gn:rif-13176-37541362-2023-09-21T20:40:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'Prefrontal cortex-specific Dcc deletion induces schizophrenia-related behavioral phenotypes and fail to be rescued by olanzapine treatment.'@en ; + gnt:belongsToSpecies gn:Mus_musculus ; + gnt:symbol "Dcc" ; + gnt:hasGeneId generif:13176 ; + skos:notation taxon:10090 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:37541362 ; + dct:created "2023-09-21 20:40:00"^^xsd:datetime . +gn:rif-16956-36519761-2023-04-27T20:33:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'Parkin regulates neuronal lipid homeostasis through SREBP2-lipoprotein lipase pathway-implications for Parkinson\'s disease.'@en ; + gnt:belongsToSpecies gn:Mus_musculus ; + gnt:symbol "Lpl" ; + gnt:hasGeneId generif:16956 ; + skos:notation taxon:10090 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:36519761 ; + dct:created "2023-04-27 20:33:00"^^xsd:datetime . +gn:rif-20423-36853961-2023-03-08T20:38:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'IHH, SHH, and primary cilia mediate epithelial-stromal cross-talk during decidualization in mice.'@en ; + gnt:belongsToSpecies gn:Mus_musculus ; + gnt:symbol "Shh" ; + gnt:hasGeneId generif:20423 ; + skos:notation taxon:10090 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:36853961 ; + dct:created "2023-03-08 20:38:00"^^xsd:datetime . +gn:rif-20423-37190906-2023-07-12T09:09:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'The SHH-GLI1 pathway is required in skin expansion and angiogenesis.'@en ; + gnt:belongsToSpecies gn:Mus_musculus ; + gnt:symbol "Shh" ; + gnt:hasGeneId generif:20423 ; + skos:notation taxon:10090 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:37190906 ; + dct:created "2023-07-12 09:09:00"^^xsd:datetime . +gn:rif-20423-37460185-2023-07-20T20:39:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label '[Effect study of Sonic hedgehog overexpressed hair follicle stem cells in hair follicle regeneration].'@en ; + gnt:belongsToSpecies gn:Mus_musculus ; + gnt:symbol "Shh" ; + gnt:hasGeneId generif:20423 ; + skos:notation taxon:10090 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:37460185 ; + dct:created "2023-07-20 20:39:00"^^xsd:datetime . +gn:rif-20423-37481204-2023-09-26T20:37:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'Sonic Hedgehog and WNT Signaling Regulate a Positive Feedback Loop Between Intestinal Epithelial and Stromal Cells to Promote Epithelial Regeneration.'@en ; + gnt:belongsToSpecies gn:Mus_musculus ; + gnt:symbol "Shh" ; + gnt:hasGeneId generif:20423 ; + skos:notation taxon:10090 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:37481204 ; + dct:created "2023-09-26 20:37:00"^^xsd:datetime . +gn:rif-24539-38114521-2023-12-29T20:33:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'Developing a model to predict the early risk of hypertriglyceridemia based on inhibiting lipoprotein lipase (LPL): a translational study.'@en ; + gnt:belongsToSpecies gn:Rattus_norvegicus ; + gnt:symbol "Lpl" ; + gnt:hasGeneId generif:24539 ; + skos:notation taxon:10116 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:38114521 ; + dct:created "2023-12-29 20:33:00"^^xsd:datetime . +gn:rif-29499-36906487-2023-06-23T20:38:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'Regulation of Shh/Bmp4 Signaling Pathway by DNA Methylation in Rectal Nervous System Development of Fetal Rats with Anorectal Malformation.'@en ; + gnt:belongsToSpecies gn:Rattus_norvegicus ; + gnt:symbol "Shh" ; + gnt:hasGeneId generif:29499 ; + skos:notation taxon:10116 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:36906487 ; + dct:created "2023-06-23 20:38:00"^^xsd:datetime . +gn:rif-29499-37815888-2023-10-24T20:38:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'Sonic hedgehog signaling promotes angiogenesis of endothelial progenitor cells to improve pressure ulcers healing by PI3K/AKT/eNOS signaling.'@en ; + gnt:belongsToSpecies gn:Rattus_norvegicus ; + gnt:symbol "Shh" ; + gnt:hasGeneId generif:29499 ; + skos:notation taxon:10116 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:37815888 ; + dct:created "2023-10-24 20:38:00"^^xsd:datetime . +gn:rif-1152-37156912-2023-05-17T20:43:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'Creatine kinase B suppresses ferroptosis by phosphorylating GPX4 through a moonlighting function.'@en ; + gnt:belongsToSpecies gn:Homo_sapiens ; + gnt:symbol "CKB" ; + gnt:hasGeneId generif:1152 ; + skos:notation taxon:9606 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:37156912 ; + dct:created "2023-05-17 20:43:00"^^xsd:datetime . +gn:rif-1630-36889039-2023-04-04T09:45:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'Mirror movements and callosal dysgenesis in a family with a DCC mutation: Neuropsychological and neuroimaging outcomes.'@en ; + gnt:belongsToSpecies gn:Homo_sapiens ; + gnt:symbol "DCC" ; + gnt:hasGeneId generif:1630 ; + skos:notation taxon:9606 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:36889039 ; + dct:created "2023-04-04 09:45:00"^^xsd:datetime . +gn:rif-1630-36852451-2023-07-07T20:37:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'An imbalance of netrin-1 and DCC during nigral degeneration in experimental models and patients with Parkinson\'s disease.'@en ; + gnt:belongsToSpecies gn:Homo_sapiens ; + gnt:symbol "DCC" ; + gnt:hasGeneId generif:1630 ; + skos:notation taxon:9606 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:36852451 ; + dct:created "2023-07-07 20:37:00"^^xsd:datetime . +gn:rif-4023-36763533-2023-02-23T20:40:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'Angiopoietin-like protein 4/8 complex-mediated plasmin generation leads to cleavage of the complex and restoration of LPL activity.'@en ; + gnt:belongsToSpecies gn:Homo_sapiens ; + gnt:symbol "LPL" ; + gnt:hasGeneId generif:4023 ; + skos:notation taxon:9606 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:36763533 ; + dct:created "2023-02-23 20:40:00"^^xsd:datetime . +gn:rif-4023-36652113-2023-04-07T20:39:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'The breast cancer microenvironment and lipoprotein lipase: Another negative notch for a beneficial enzyme?'@en ; + gnt:belongsToSpecies gn:Homo_sapiens ; + gnt:symbol "LPL" ; + gnt:hasGeneId generif:4023 ; + skos:notation taxon:9606 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:36652113 ; + dct:created "2023-04-07 20:39:00"^^xsd:datetime . +gn:rif-4023-36519761-2023-04-27T20:33:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'Parkin regulates neuronal lipid homeostasis through SREBP2-lipoprotein lipase pathway-implications for Parkinson\'s disease.'@en ; + gnt:belongsToSpecies gn:Homo_sapiens ; + gnt:symbol "LPL" ; + gnt:hasGeneId generif:4023 ; + skos:notation taxon:9606 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:36519761 ; + dct:created "2023-04-27 20:33:00"^^xsd:datetime . +gn:rif-4023-36708756-2023-05-22T20:32:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'Plasma Lipoprotein Lipase Is Associated with Risk of Future Major Adverse Cardiovascular Events in Patients Following Carotid Endarterectomy.'@en ; + gnt:belongsToSpecies gn:Homo_sapiens ; + gnt:symbol "LPL" ; + gnt:hasGeneId generif:4023 ; + skos:notation taxon:9606 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:36708756 ; + dct:created "2023-05-22 20:32:00"^^xsd:datetime . +gn:rif-4023-37155355-2023-07-04T21:12:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'Inverse association between apolipoprotein C-II and cardiovascular mortality: role of lipoprotein lipase activity modulation.'@en ; + gnt:belongsToSpecies gn:Homo_sapiens ; + gnt:symbol "LPL" ; + gnt:hasGeneId generif:4023 ; + skos:notation taxon:9606 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:37155355 ; + dct:created "2023-07-04 21:12:00"^^xsd:datetime . +gn:rif-4023-37432202-2023-07-13T20:35:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'Effect of the Interaction between Seaweed Intake and LPL Polymorphisms on Metabolic Syndrome in Middle-Aged Korean Adults.'@en ; + gnt:belongsToSpecies gn:Homo_sapiens ; + gnt:symbol "LPL" ; + gnt:hasGeneId generif:4023 ; + skos:notation taxon:9606 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:37432202 ; + dct:created "2023-07-13 20:35:00"^^xsd:datetime . +gn:rif-4023-37568214-2023-08-14T20:37:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'Frameshift coding sequence variants in the LPL gene: identification of two novel events and exploration of the genotype-phenotype relationship for variants reported to date.'@en ; + gnt:belongsToSpecies gn:Homo_sapiens ; + gnt:symbol "LPL" ; + gnt:hasGeneId generif:4023 ; + skos:notation taxon:9606 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:37568214 ; + dct:created "2023-08-14 20:37:00"^^xsd:datetime . +gn:rif-4023-37550668-2023-08-22T20:29:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'The East Asian-specific LPL p.Ala288Thr (c.862G > A) missense variant exerts a mild effect on protein function.'@en ; + gnt:belongsToSpecies gn:Homo_sapiens ; + gnt:symbol "LPL" ; + gnt:hasGeneId generif:4023 ; + skos:notation taxon:9606 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:37550668 ; + dct:created "2023-08-22 20:29:00"^^xsd:datetime . +gn:rif-4023-37128695-2023-09-12T20:35:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'Interaction between APOE, APOA1, and LPL Gene Polymorphisms and Variability in Changes in Lipid and Blood Pressure following Orange Juice Intake: A Pilot Study.'@en ; + gnt:belongsToSpecies gn:Homo_sapiens ; + gnt:symbol "LPL" ; + gnt:hasGeneId generif:4023 ; + skos:notation taxon:9606 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:37128695 ; + dct:created "2023-09-12 20:35:00"^^xsd:datetime . +gn:rif-4023-37427758-2023-09-25T09:33:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'Variants within the LPL gene confer susceptility to diabetic kidney disease and rapid decline in kidney function in Chinese patients with type 2 diabetes.'@en ; + gnt:belongsToSpecies gn:Homo_sapiens ; + gnt:symbol "LPL" ; + gnt:hasGeneId generif:4023 ; + skos:notation taxon:9606 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:37427758 ; + dct:created "2023-09-25 09:33:00"^^xsd:datetime . +gn:rif-4023-37901192-2023-11-01T08:55:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'The Association of Adipokines and Myokines in the Blood of Obese Children and Adolescents with Lipoprotein Lipase rs328 Gene Variants.'@en ; + gnt:belongsToSpecies gn:Homo_sapiens ; + gnt:symbol "LPL" ; + gnt:hasGeneId generif:4023 ; + skos:notation taxon:9606 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:37901192 ; + dct:created "2023-11-01 08:55:00"^^xsd:datetime . +gn:rif-4023-37871217-2023-11-10T08:44:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'The lipoprotein lipase that is shuttled into capillaries by GPIHBP1 enters the glycocalyx where it mediates lipoprotein processing.'@en ; + gnt:belongsToSpecies gn:Homo_sapiens ; + gnt:symbol "LPL" ; + gnt:hasGeneId generif:4023 ; + skos:notation taxon:9606 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:37871217 ; + dct:created "2023-11-10 08:44:00"^^xsd:datetime . +gn:rif-4023-37858495-2023-12-28T20:33:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'Clinical profile, genetic spectrum and therapy evaluation of 19 Chinese pediatric patients with lipoprotein lipase deficiency.'@en ; + gnt:belongsToSpecies gn:Homo_sapiens ; + gnt:symbol "LPL" ; + gnt:hasGeneId generif:4023 ; + skos:notation taxon:9606 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:37858495 ; + dct:created "2023-12-28 20:33:00"^^xsd:datetime . +gn:rif-4023-38114521-2023-12-29T20:33:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'Developing a model to predict the early risk of hypertriglyceridemia based on inhibiting lipoprotein lipase (LPL): a translational study.'@en ; + gnt:belongsToSpecies gn:Homo_sapiens ; + gnt:symbol "LPL" ; + gnt:hasGeneId generif:4023 ; + skos:notation taxon:9606 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:38114521 ; + dct:created "2023-12-29 20:33:00"^^xsd:datetime . +gn:rif-6469-37511358-2023-08-19T08:35:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'Low Expression of the NRP1 Gene Is Associated with Shorter Overall Survival in Patients with Sonic Hedgehog and Group 3 Medulloblastoma.'@en ; + gnt:belongsToSpecies gn:Homo_sapiens ; + gnt:symbol "SHH" ; + gnt:hasGeneId generif:6469 ; + skos:notation taxon:9606 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:37511358 ; + dct:created "2023-08-19 08:35:00"^^xsd:datetime . +gn:rif-6469-37307020-2023-09-21T20:40:00-5 rdf:type gnc:NCBIWikiEntry ; + rdfs:label 'Activation of Sonic Hedgehog Signaling Pathway Regulates Human Trabecular Meshwork Cell Function.'@en ; + gnt:belongsToSpecies gn:Homo_sapiens ; + gnt:symbol "SHH" ; + gnt:hasGeneId generif:6469 ; + skos:notation taxon:9606 ; + dct:hasVersion "5"^^xsd:integer ; + dct:references pubmed:37307020 ; + dct:created "2023-09-21 20:40:00"^^xsd:datetime . diff --git a/tests/unit/computations/test_partial_correlations.py b/tests/unit/computations/test_partial_correlations.py index 066c650..6364701 100644 --- a/tests/unit/computations/test_partial_correlations.py +++ b/tests/unit/computations/test_partial_correlations.py @@ -159,8 +159,8 @@ class TestPartialCorrelations(TestCase): "variance": None}}}, dictified_control_samples), (("BXD2",), (7.80944,), - (7.51879, 7.77141, 8.39265, 8.17443, 8.30401, 7.80944, 8.39265, - 8.17443, 8.30401, 7.80944, 7.51879, 7.77141, 7.80944), + ((7.51879, 7.77141, 8.39265, 8.17443, 8.30401, 7.80944, 8.39265, + 8.17443, 8.30401, 7.80944, 7.51879, 7.77141, 7.80944),), (None,), (None, None, None, None, None, None, None, None, None, None, None, None, None))) diff --git a/tests/unit/computations/test_wgcna.py b/tests/unit/computations/test_wgcna.py index 55432af..325bd5a 100644 --- a/tests/unit/computations/test_wgcna.py +++ b/tests/unit/computations/test_wgcna.py @@ -85,9 +85,9 @@ class TestWgcna(TestCase): mock_img.return_value = b"AFDSFNBSDGJJHH" results = call_wgcna_script( - "Rscript/GUIX_PATH/scripts/r_file.R", request_data) + "Rscript/GUIX_PATH/scripts/r_file.R", request_data, "/tmp") - mock_dumping_data.assert_called_once_with(request_data) + mock_dumping_data.assert_called_once_with(request_data, "/tmp") mock_compose_wgcna.assert_called_once_with( "Rscript/GUIX_PATH/scripts/r_file.R", @@ -119,7 +119,7 @@ class TestWgcna(TestCase): mock_run_cmd.return_value = expected_error self.assertEqual(call_wgcna_script( - "input_file.R", ""), expected_error) + "input_file.R", "", "/tmp"), expected_error) @pytest.mark.skip( "This test assumes that the system will always be invoked from the root" @@ -134,7 +134,6 @@ class TestWgcna(TestCase): wgcna_cmd, "Rscript scripts/wgcna.r /tmp/wgcna.json") @pytest.mark.unit_test - @mock.patch("gn3.computations.wgcna.TMPDIR", "/tmp") @mock.patch("gn3.computations.wgcna.uuid.uuid4") def test_create_json_file(self, file_name_generator): """test for writing the data to a csv file""" @@ -166,8 +165,7 @@ class TestWgcna(TestCase): file_name_generator.return_value = "facb73ff-7eef-4053-b6ea-e91d3a22a00c" - results = dump_wgcna_data( - expected_input) + results = dump_wgcna_data(expected_input, "/tmp") file_handler.assert_called_once_with( "/tmp/facb73ff-7eef-4053-b6ea-e91d3a22a00c.json", 'w', encoding='utf-8') diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 8005c8e..5526d16 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -7,6 +7,7 @@ import pytest from gn3.app import create_app + @pytest.fixture(scope="session") def fxtr_app(): """Fixture: setup the test app""" @@ -15,7 +16,12 @@ def fxtr_app(): testdb = Path(testdir).joinpath( f'testdb_{datetime.now().strftime("%Y%m%dT%H%M%S")}') app = create_app({ - "TESTING": True, "AUTH_DB": testdb, + "TESTING": True, + "LMDB_DATA_PATH": str( + Path(__file__).parent.parent / + Path("test_data/lmdb-test-data") + ), + "AUTH_SERVER_URL": "http://127.0.0.1:8081", "OAUTH2_ACCESS_TOKEN_GENERATOR": "tests.unit.auth.test_token.gen_token" }) app.testing = True @@ -23,13 +29,15 @@ def fxtr_app(): # Clean up after ourselves testdb.unlink(missing_ok=True) + @pytest.fixture(scope="session") -def client(fxtr_app): # pylint: disable=redefined-outer-name +def client(fxtr_app): # pylint: disable=redefined-outer-name """Create a test client fixture for tests""" with fxtr_app.app_context(): yield fxtr_app.test_client() + @pytest.fixture(scope="session") -def fxtr_app_config(client): # pylint: disable=redefined-outer-name +def fxtr_app_config(client): # pylint: disable=redefined-outer-name """Return the test application's configuration object""" return client.application.config diff --git a/tests/unit/db/rdf/data.py b/tests/unit/db/rdf/data.py new file mode 100644 index 0000000..6bc612f --- /dev/null +++ b/tests/unit/db/rdf/data.py @@ -0,0 +1,199 @@ +"""Some test data to be used in RDF data.""" + +LPL_RIF_ENTRIES = { + "@context": { + "dct": "http://purl.org/dc/terms/", + "gnt": "http://genenetwork.org/term/", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "skos": "http://www.w3.org/2004/02/skos/core#", + "symbol": "gnt:symbol", + "species": "gnt:species", + "taxonomic_id": "skos:notation", + "gene_id": "gnt:hasGeneId", + "pubmed_id": "dct:references", + "created": "dct:created", + "comment": "rdfs:comment", + "version": "dct:hasVersion", + }, + "data": [ + { + "gene_id": 4023, + "version": 5, + "species": "Homo sapiens", + "symbol": "LPL", + "created": "2023-02-23 20:40:00", + "pubmed_id": 36763533, + "comment": "Angiopoietin-like protein 4/8 complex-mediated plasmin generation \ +leads to cleavage of the complex and restoration of LPL activity.", + "taxonomic_id": 9606, + }, + { + "gene_id": 4023, + "version": 5, + "species": "Homo sapiens", + "symbol": "LPL", + "created": "2023-04-07 20:39:00", + "pubmed_id": 36652113, + "comment": "The breast cancer microenvironment and lipoprotein lipase: \ +Another negative notch for a beneficial enzyme?", + "taxonomic_id": 9606, + }, + { + "gene_id": 4023, + "version": 5, + "species": "Homo sapiens", + "symbol": "LPL", + "created": "2023-04-27 20:33:00", + "pubmed_id": 36519761, + "comment": "Parkin regulates neuronal lipid homeostasis through \ +SREBP2-lipoprotein lipase pathway-implications for Parkinson's disease.", + "taxonomic_id": 9606, + }, + { + "gene_id": 4023, + "version": 5, + "species": "Homo sapiens", + "symbol": "LPL", + "created": "2023-05-22 20:32:00", + "pubmed_id": 36708756, + "comment": "Plasma Lipoprotein Lipase Is Associated with Risk of \ +Future Major Adverse Cardiovascular Events in Patients Following Carotid Endarterectomy.", + "taxonomic_id": 9606, + }, + { + "gene_id": 4023, + "version": 5, + "species": "Homo sapiens", + "symbol": "LPL", + "created": "2023-07-04 21:12:00", + "pubmed_id": 37155355, + "comment": "Inverse association between apolipoprotein C-II and \ +cardiovascular mortality: role of lipoprotein lipase activity modulation.", + "taxonomic_id": 9606, + }, + { + "gene_id": 4023, + "version": 5, + "species": "Homo sapiens", + "symbol": "LPL", + "created": "2023-07-13 20:35:00", + "pubmed_id": 37432202, + "comment": "Effect of the Interaction between Seaweed Intake and LPL \ +Polymorphisms on Metabolic Syndrome in Middle-Aged Korean Adults.", + "taxonomic_id": 9606, + }, + { + "gene_id": 4023, + "version": 5, + "species": "Homo sapiens", + "symbol": "LPL", + "created": "2023-08-14 20:37:00", + "pubmed_id": 37568214, + "comment": "Frameshift coding sequence variants in the LPL gene: identification \ +of two novel events and exploration of the genotype-phenotype relationship for \ +variants reported to date.", + "taxonomic_id": 9606, + }, + { + "gene_id": 4023, + "version": 5, + "species": "Homo sapiens", + "symbol": "LPL", + "created": "2023-08-22 20:29:00", + "pubmed_id": 37550668, + "comment": "The East Asian-specific LPL p.Ala288Thr (c.862G > A) missense \ +variant exerts a mild effect on protein function.", + "taxonomic_id": 9606, + }, + { + "gene_id": 4023, + "version": 5, + "species": "Homo sapiens", + "symbol": "LPL", + "created": "2023-09-12 20:35:00", + "pubmed_id": 37128695, + "comment": "Interaction between APOE, APOA1, and LPL Gene Polymorphisms \ +and Variability in Changes in Lipid and Blood Pressure following Orange Juice Intake: \ +A Pilot Study.", + "taxonomic_id": 9606, + }, + { + "gene_id": 4023, + "version": 5, + "species": "Homo sapiens", + "symbol": "LPL", + "created": "2023-09-25 09:33:00", + "pubmed_id": 37427758, + "comment": "Variants within the LPL gene confer susceptility to \ +diabetic kidney disease and rapid decline in kidney function in Chinese patients \ +with type 2 diabetes.", + "taxonomic_id": 9606, + }, + { + "gene_id": 4023, + "version": 5, + "species": "Homo sapiens", + "symbol": "LPL", + "created": "2023-11-01 08:55:00", + "pubmed_id": 37901192, + "comment": "The Association of Adipokines and Myokines in the \ +Blood of Obese Children and Adolescents with Lipoprotein Lipase rs328 Gene Variants.", + "taxonomic_id": 9606, + }, + { + "gene_id": 4023, + "version": 5, + "species": "Homo sapiens", + "symbol": "LPL", + "created": "2023-11-10 08:44:00", + "pubmed_id": 37871217, + "comment": "The lipoprotein lipase that is shuttled into \ +capillaries by GPIHBP1 enters the glycocalyx where it mediates lipoprotein processing.", + "taxonomic_id": 9606, + }, + { + "gene_id": 4023, + "version": 5, + "species": "Homo sapiens", + "symbol": "LPL", + "created": "2023-12-28 20:33:00", + "pubmed_id": 37858495, + "comment": "Clinical profile, genetic spectrum and therapy \ +evaluation of 19 Chinese pediatric patients with lipoprotein lipase deficiency.", + "taxonomic_id": 9606, + }, + { + "gene_id": 4023, + "version": 5, + "species": "Homo sapiens", + "symbol": "LPL", + "created": "2023-12-29 20:33:00", + "pubmed_id": 38114521, + "comment": "Developing a model to predict the early risk of \ +hypertriglyceridemia based on inhibiting lipoprotein lipase (LPL): a translational study.", + "taxonomic_id": 9606, + }, + { + "gene_id": 16956, + "version": 5, + "species": "Mus musculus", + "symbol": "Lpl", + "created": "2023-04-27 20:33:00", + "pubmed_id": 36519761, + "comment": "Parkin regulates neuronal lipid homeostasis through \ +SREBP2-lipoprotein lipase pathway-implications for Parkinson's disease.", + "taxonomic_id": 10090, + }, + { + "gene_id": 24539, + "version": 5, + "species": "Rattus norvegicus", + "symbol": "Lpl", + "created": "2023-12-29 20:33:00", + "pubmed_id": 38114521, + "comment": "Developing a model to predict the early risk of \ +hypertriglyceridemia based on inhibiting lipoprotein lipase (LPL): a translational study.", + "taxonomic_id": 10116, + }, + ], +} diff --git a/tests/unit/db/rdf/test_wiki.py b/tests/unit/db/rdf/test_wiki.py index 3abf3ad..bab37ce 100644 --- a/tests/unit/db/rdf/test_wiki.py +++ b/tests/unit/db/rdf/test_wiki.py @@ -22,11 +22,15 @@ from tests.fixtures.rdf import ( SPARQL_CONF, ) +from tests.unit.db.rdf.data import LPL_RIF_ENTRIES + from gn3.db.rdf.wiki import ( __sanitize_result, get_wiki_entries_by_symbol, get_comment_history, update_wiki_comment, + get_rif_entries_by_symbol, + delete_wiki_entries_by_id, ) GRAPH = "<http://cd-test.genenetwork.org>" @@ -396,3 +400,49 @@ def test_update_wiki_comment(rdf_setup): # pylint: disable=W0613,W0621 "version": 3, "web_url": "http://some-website.com", }) + + +@pytest.mark.rdf +def test_get_rif_entries_by_symbol(rdf_setup): # pylint: disable=W0613,W0621 + """Test fetching NCBI Rif Metadata from RDF""" + sparql_conf = SPARQL_CONF + entries = get_rif_entries_by_symbol( + symbol="Lpl", + sparql_uri=sparql_conf["sparql_endpoint"], + graph=GRAPH, + ) + assert len(LPL_RIF_ENTRIES["data"]) == len(entries["data"]) + for result, expected in zip(LPL_RIF_ENTRIES["data"], entries["data"]): + TestCase().assertDictEqual(result, expected) + + +@pytest.mark.rdf +def test_delete_wiki_entries_by_id(rdf_setup): # pylint: disable=W0613,W0621 + """Test deleting a given RIF Wiki entry""" + sparql_conf = SPARQL_CONF + delete_wiki_entries_by_id( + 230, + sparql_user=sparql_conf["sparql_user"], + sparql_password=sparql_conf["sparql_password"], + sparql_auth_uri=sparql_conf["sparql_auth_uri"], + graph=GRAPH) + entries = get_comment_history( + comment_id=230, + sparql_uri=sparql_conf["sparql_endpoint"], + graph=GRAPH, + ) + assert len(entries["data"]) == 0 + + # Deleting a non-existent entry has no effect + delete_wiki_entries_by_id( + 199999, + sparql_user=sparql_conf["sparql_user"], + sparql_password=sparql_conf["sparql_password"], + sparql_auth_uri=sparql_conf["sparql_auth_uri"], + graph=GRAPH) + entries = get_comment_history( + comment_id=230, + sparql_uri=sparql_conf["sparql_endpoint"], + graph=GRAPH, + ) + assert len(entries["data"]) == 0 diff --git a/tests/unit/db/test_case_attributes.py b/tests/unit/db/test_case_attributes.py index 97a0703..998b58d 100644 --- a/tests/unit/db/test_case_attributes.py +++ b/tests/unit/db/test_case_attributes.py @@ -1,205 +1,326 @@ """Test cases for gn3.db.case_attributes.py""" +import pickle +import tempfile +import os +import json +from pathlib import Path import pytest from pytest_mock import MockFixture -from gn3.db.case_attributes import get_unreviewed_diffs -from gn3.db.case_attributes import get_case_attributes -from gn3.db.case_attributes import insert_case_attribute_audit -from gn3.db.case_attributes import approve_case_attribute -from gn3.db.case_attributes import reject_case_attribute +from gn3.db.case_attributes import queue_edit +from gn3.db.case_attributes import ( + CaseAttributeEdit, + EditStatus, + apply_change, + get_changes, + view_change +) @pytest.mark.unit_test -def test_get_case_attributes(mocker: MockFixture) -> None: - """Test that all the case attributes are fetched correctly""" +def test_queue_edit(mocker: MockFixture) -> None: + """Test queueing an edit.""" mock_conn = mocker.MagicMock() with mock_conn.cursor() as cursor: - cursor.fetchall.return_value = ( - (1, "Condition", None), - (2, "Tissue", None), - (3, "Age", "Cum sociis natoque penatibus et magnis dis"), - (4, "Condition", "Description A"), - (5, "Condition", "Description B"), - ) - results = get_case_attributes(mock_conn) + type(cursor).lastrowid = 28 + tmpdir = Path(os.environ.get("TMPDIR", tempfile.gettempdir())) + caseattr_id = queue_edit( + cursor, + directory=tmpdir, + edit=CaseAttributeEdit( + inbredset_id=1, status=EditStatus.review, + user_id="xxxx", changes={"a": 1, "b": 2} + )) cursor.execute.assert_called_once_with( - "SELECT Id, Name, Description FROM CaseAttribute" - ) - assert results == ( - (1, "Condition", None), - (2, "Tissue", None), - (3, "Age", "Cum sociis natoque penatibus et magnis dis"), - (4, "Condition", "Description A"), - (5, "Condition", "Description B"), - ) + "INSERT INTO " + "caseattributes_audit(status, editor, json_diff_data) " + "VALUES (%s, %s, %s) " + "ON DUPLICATE KEY UPDATE status=%s", + ('review', 'xxxx', '{"a": 1, "b": 2}', 'review')) + assert 28 == caseattr_id @pytest.mark.unit_test -def test_get_unreviewed_diffs(mocker: MockFixture) -> None: - """Test that the correct query is called when fetching unreviewed - case-attributes diff""" - mock_conn = mocker.MagicMock() - with mock_conn.cursor() as cursor: - _ = get_unreviewed_diffs(mock_conn) - cursor.fetchall.return_value = ((1, "editor", "diff_data_1"),) - cursor.execute.assert_called_once_with( - "SELECT id, editor, json_diff_data FROM " - "caseattributes_audit WHERE status = 'review'" - ) +def test_view_change(mocker: MockFixture) -> None: + """Test view_change function.""" + sample_json_diff = { + "inbredset_id": 1, + "Modifications": { + "Original": { + "B6D2F1": {"Epoch": "10au"}, + "BXD100": {"Epoch": "3b"}, + "BXD101": {"SeqCvge": "29"}, + "BXD102": {"Epoch": "3b"}, + "BXD108": {"SeqCvge": ""} + }, + "Current": { + "B6D2F1": {"Epoch": "10"}, + "BXD100": {"Epoch": "3"}, + "BXD101": {"SeqCvge": "2"}, + "BXD102": {"Epoch": "3"}, + "BXD108": {"SeqCvge": "oo"} + } + } + } + change_id = 28 + mock_cursor, mock_conn = mocker.MagicMock(), mocker.MagicMock() + mock_conn.cursor.return_value = mock_cursor + mock_cursor.fetchone.return_value = (json.dumps(sample_json_diff), None) + assert view_change(mock_cursor, change_id) == sample_json_diff + mock_cursor.execute.assert_called_once_with( + "SELECT json_diff_data FROM caseattributes_audit WHERE id = %s", + (change_id,)) + mock_cursor.fetchone.assert_called_once() @pytest.mark.unit_test -def test_insert_case_attribute_audit(mocker: MockFixture) -> None: - """Test that the updating case attributes uses the correct query""" - mock_conn = mocker.MagicMock() - with mock_conn.cursor() as cursor: - _ = insert_case_attribute_audit( - mock_conn, status="review", author="Author", data="diff_data" - ) - cursor.execute.assert_called_once_with( - "INSERT INTO caseattributes_audit " - "(status, editor, json_diff_data) " - "VALUES (%s, %s, %s)", - ("review", "Author", "diff_data"), - ) +def test_view_change_invalid_json(mocker: MockFixture) -> None: + """Test invalid json when view_change is called""" + change_id = 28 + mock_cursor, mock_conn = mocker.MagicMock(), mocker.MagicMock() + mock_conn.cursor.return_value = mock_cursor + mock_cursor.fetchone.return_value = ("invalid_json_string", None) + with pytest.raises(json.JSONDecodeError): + view_change(mock_cursor, change_id) + mock_cursor.execute.assert_called_once_with( + "SELECT json_diff_data FROM caseattributes_audit WHERE id = %s", + (change_id,)) @pytest.mark.unit_test -def test_reject_case_attribute(mocker: MockFixture) -> None: - """Test rejecting a case-attribute""" - mock_conn = mocker.MagicMock() - with mock_conn.cursor() as cursor: - _ = reject_case_attribute( - mock_conn, - case_attr_audit_id=1, - ) - cursor.execute.assert_called_once_with( - "UPDATE caseattributes_audit SET " - "status = 'rejected' WHERE id = %s", - (1,), - ) +def test_view_change_no_data(mocker: MockFixture) -> None: + "Test no result when view_change is called" + change_id = 28 + mock_cursor, mock_conn = mocker.MagicMock(), mocker.MagicMock() + mock_conn.cursor.return_value = mock_cursor + mock_cursor.fetchone.return_value = (None, None) + assert view_change(mock_cursor, change_id) == {} + mock_cursor.execute.assert_called_once_with( + "SELECT json_diff_data FROM caseattributes_audit WHERE id = %s", + (change_id,)) @pytest.mark.unit_test -def test_approve_inserting_case_attribute(mocker: MockFixture) -> None: - """Test approving inserting a case-attribute""" - mock_conn = mocker.MagicMock() - with mock_conn.cursor() as cursor: - type(cursor).rowcount = 1 - cursor.fetchone.return_value = ( - """ - {"Insert": {"name": "test", "description": "Random Description"}} - """, - ) - _ = approve_case_attribute( - mock_conn, - case_attr_audit_id=3, - ) - calls = [ - mocker.call( - "SELECT json_diff_data FROM caseattributes_audit " - "WHERE id = %s", - (3,), - ), - mocker.call( - "INSERT INTO CaseAttribute " - "(Name, Description) VALUES " - "(%s, %s)", - ( - "test", - "Random Description", - ), - ), - mocker.call( - "UPDATE caseattributes_audit SET " - "status = 'approved' WHERE id = %s", - (3,), - ), +def test_apply_change_approved(mocker: MockFixture) -> None: + """Test approving a change""" + mock_cursor, mock_conn = mocker.MagicMock(), mocker.MagicMock() + mock_conn.cursor.return_value = mock_cursor + mock_lmdb = mocker.patch("gn3.db.case_attributes.lmdb") + mock_env, mock_txn = mocker.MagicMock(), mocker.MagicMock() + mock_lmdb.open.return_value = mock_env + mock_env.begin.return_value.__enter__.return_value = mock_txn + change_id, review_ids = 1, {1, 2, 3} + mock_txn.get.side_effect = ( + pickle.dumps(review_ids), # b"review" key + None, # b"approved" key + ) + tmpdir = Path(os.environ.get("TMPDIR", tempfile.gettempdir())) + mock_cursor.fetchone.return_value = (json.dumps({ + "inbredset_id": 1, + "Modifications": { + "Current": { + "B6D2F1": {"Epoch": "10"}, + "BXD100": {"Epoch": "3"}, + "BXD101": {"SeqCvge": "2"}, + "BXD102": {"Epoch": "3"}, + "BXD108": {"SeqCvge": "oo"} + } + } + }), None) + mock_cursor.fetchall.side_effect = [ + [ # Strain query + ("B6D2F1", 1), ("BXD100", 2), + ("BXD101", 3), ("BXD102", 4), + ("BXD108", 5)], + [ # CaseAttribute query + ("Epoch", 101), ("SeqCvge", 102)] + ] + assert apply_change(mock_cursor, EditStatus.approved, + change_id, tmpdir) is True + assert mock_cursor.execute.call_count == 4 + mock_cursor.execute.assert_has_calls([ + mocker.call( + "SELECT json_diff_data FROM caseattributes_audit WHERE id = %s", + (change_id,)), + mocker.call( + "SELECT Name, Id FROM Strain WHERE Name IN (%s, %s, %s, %s, %s)", + ("B6D2F1", "BXD100", "BXD101", "BXD102", "BXD108")), + mocker.call( + "SELECT Name, CaseAttributeId FROM CaseAttribute " + "WHERE InbredSetId = %s AND Name IN (%s, %s)", + (1, "Epoch", "SeqCvge")), + mocker.call( + "UPDATE caseattributes_audit SET status = %s WHERE id = %s", + ("approved", change_id)) + ]) + mock_cursor.executemany.assert_called_once_with( + "INSERT INTO CaseAttributeXRefNew (InbredSetId, StrainId, CaseAttributeId, Value) " + "VALUES (%(inbredset_id)s, %(strain_id)s, %(caseattr_id)s, %(value)s) " + "ON DUPLICATE KEY UPDATE Value = VALUES(Value)", + [ + {"inbredset_id": 1, "strain_id": 1, "caseattr_id": 101, "value": "10"}, + {"inbredset_id": 1, "strain_id": 2, "caseattr_id": 101, "value": "3"}, + {"inbredset_id": 1, "strain_id": 3, "caseattr_id": 102, "value": "2"}, + {"inbredset_id": 1, "strain_id": 4, "caseattr_id": 101, "value": "3"}, + {"inbredset_id": 1, "strain_id": 5, "caseattr_id": 102, "value": "oo"} ] - cursor.execute.assert_has_calls(calls, any_order=False) + ) @pytest.mark.unit_test -def test_approve_deleting_case_attribute(mocker: MockFixture) -> None: - """Test deleting a case-attribute""" - mock_conn = mocker.MagicMock() - with mock_conn.cursor() as cursor: - type(cursor).rowcount = 1 - cursor.fetchone.return_value = ( - """ - {"Deletion": {"id": "12", "name": "test", "description": ""}} - """, - ) - _ = approve_case_attribute( - mock_conn, - case_attr_audit_id=3, - ) - calls = [ - mocker.call( - "SELECT json_diff_data FROM caseattributes_audit " - "WHERE id = %s", - (3,), - ), - mocker.call("DELETE FROM CaseAttribute WHERE Id = %s", ("12",)), - mocker.call( - "UPDATE caseattributes_audit SET " - "status = 'approved' WHERE id = %s", - (3,), - ), - ] - cursor.execute.assert_has_calls(calls, any_order=False) +def test_apply_change_rejected(mocker: MockFixture) -> None: + """Test rejecting a change""" + mock_cursor, mock_conn = mocker.MagicMock(), mocker.MagicMock() + mock_conn.cursor.return_value = mock_cursor + mock_lmdb = mocker.patch("gn3.db.case_attributes.lmdb") + mock_env, mock_txn = mocker.MagicMock(), mocker.MagicMock() + mock_lmdb.open.return_value = mock_env + mock_env.begin.return_value.__enter__.return_value = mock_txn + tmpdir = Path(os.environ.get("TMPDIR", tempfile.gettempdir())) + change_id, review_ids = 3, {1, 2, 3} + mock_txn.get.side_effect = [ + pickle.dumps(review_ids), # review_ids + None # rejected_ids (initially empty) + ] + + assert apply_change(mock_cursor, EditStatus.rejected, + change_id, tmpdir) is True + + # Verify SQL query call sequence + mock_cursor.execute.assert_called_once_with( + "UPDATE caseattributes_audit SET status = %s WHERE id = %s", + (str(EditStatus.rejected), change_id)) + mock_cursor.executemany.assert_not_called() + + # Verify LMDB operations + mock_env.begin.assert_called_once_with(write=True) + expected_txn_calls = [ + mocker.call(b"review", pickle.dumps({1, 2})), + mocker.call(b"rejected", pickle.dumps({3})) + ] + mock_txn.put.assert_has_calls(expected_txn_calls, any_order=False) @pytest.mark.unit_test -def test_approve_modifying_case_attribute(mocker: MockFixture) -> None: - """Test modifying a case-attribute""" - mock_conn = mocker.MagicMock() - with mock_conn.cursor() as cursor: - type(cursor).rowcount = 1 - cursor.fetchone.return_value = ( - """ -{ - "id": "12", - "Modification": { - "description": { - "Current": "Test", - "Original": "A" - }, - "name": { - "Current": "Height (A)", - "Original": "Height" +def test_apply_change_non_existent_change_id(mocker: MockFixture) -> None: + """Test that there's a missing change_id from the returned LMDB rejected set.""" + mock_env, mock_txn = mocker.MagicMock(), mocker.MagicMock() + mock_cursor, mock_conn = mocker.MagicMock(), mocker.MagicMock() + mock_lmdb = mocker.patch("gn3.db.case_attributes.lmdb") + mock_lmdb.open.return_value = mock_env + mock_conn.cursor.return_value = mock_cursor + mock_env.begin.return_value.__enter__.return_value = mock_txn + change_id, review_ids = 28, {1, 2, 3} + mock_txn.get.side_effect = [ + pickle.dumps(review_ids), # b"review" key + None, # b"approved" key + ] + tmpdir = Path(os.environ.get("TMPDIR", tempfile.gettempdir())) + assert apply_change(mock_cursor, EditStatus.approved, + change_id, tmpdir) is False + + +@pytest.mark.unit_test +def test_get_changes(mocker: MockFixture) -> None: + """Test that reviews are correctly fetched""" + mock_fetch_case_attrs_changes = mocker.patch( + "gn3.db.case_attributes.__fetch_case_attrs_changes__" + ) + mock_fetch_case_attrs_changes.return_value = [ + { + "editor": "user1", + "json_diff_data": { + "inbredset_id": 1, + "Modifications": { + "Original": { + "B6D2F1": {"Epoch": "10au"}, + "BXD100": {"Epoch": "3b"}, + "BXD101": {"SeqCvge": "29"}, + "BXD102": {"Epoch": "3b"}, + "BXD108": {"SeqCvge": ""} + }, + "Current": { + "B6D2F1": {"Epoch": "10"}, + "BXD100": {"Epoch": "3"}, + "BXD101": {"SeqCvge": "2"}, + "BXD102": {"Epoch": "3"}, + "BXD108": {"SeqCvge": "oo"} + } + } + }, + "time_stamp": "2025-07-01 12:00:00" + }, + { + "editor": "user2", + "json_diff_data": { + "inbredset_id": 1, + "Modifications": { + "Original": {"BXD200": {"Epoch": "5a"}}, + "Current": {"BXD200": {"Epoch": "5"}} + } + }, + "time_stamp": "2025-07-01 12:01:00" + } + ] + mock_lmdb = mocker.patch("gn3.db.case_attributes.lmdb") + mock_env, mock_txn = mocker.MagicMock(), mocker.MagicMock() + mock_lmdb.open.return_value = mock_env + mock_env.begin.return_value.__enter__.return_value = mock_txn + review_ids, approved_ids, rejected_ids = {1, 4}, {2, 3}, {5, 6, 7, 10} + mock_txn.get.side_effect = ( + pickle.dumps(review_ids), # b"review" key + pickle.dumps(approved_ids), # b"approved" key + pickle.dumps(rejected_ids) # b"rejected" key + ) + result = get_changes(cursor=mocker.MagicMock(), + change_type=EditStatus.review, + directory=Path("/tmp")) + expected = { + "change-type": "review", + "count": { + "reviews": 2, + "approvals": 2, + "rejections": 4 + }, + "data": { + 1: { + "editor": "user1", + "json_diff_data": { + "inbredset_id": 1, + "Modifications": { + "Original": { + "B6D2F1": {"Epoch": "10au"}, + "BXD100": {"Epoch": "3b"}, + "BXD101": {"SeqCvge": "29"}, + "BXD102": {"Epoch": "3b"}, + "BXD108": {"SeqCvge": ""} + }, + "Current": { + "B6D2F1": {"Epoch": "10"}, + "BXD100": {"Epoch": "3"}, + "BXD101": {"SeqCvge": "2"}, + "BXD102": {"Epoch": "3"}, + "BXD108": {"SeqCvge": "oo"} + } + } + }, + "time_stamp": "2025-07-01 12:00:00" + }, + 4: { + 'editor': 'user2', + 'json_diff_data': { + 'inbredset_id': 1, + 'Modifications': { + 'Original': { + 'BXD200': {'Epoch': '5a'} + }, + 'Current': { + 'BXD200': {'Epoch': '5'} + } + } + }, + "time_stamp": "2025-07-01 12:01:00" + } + } } - } -}""", - ) - _ = approve_case_attribute( - mock_conn, - case_attr_audit_id=3, - ) - calls = [ - mocker.call( - "SELECT json_diff_data FROM caseattributes_audit " - "WHERE id = %s", - (3,), - ), - mocker.call( - "UPDATE CaseAttribute SET Description = %s WHERE Id = %s", - ( - "Test", - "12", - ), - ), - mocker.call( - "UPDATE CaseAttribute SET Name = %s WHERE Id = %s", - ( - "Height (A)", - "12", - ), - ), - mocker.call( - "UPDATE caseattributes_audit SET " - "status = 'approved' WHERE id = %s", - (3,), - ), - ] - cursor.execute.assert_has_calls(calls, any_order=False) + assert result == expected diff --git a/tests/unit/db/test_gen_menu.py b/tests/unit/db/test_gen_menu.py index e6b5711..f64b4d3 100644 --- a/tests/unit/db/test_gen_menu.py +++ b/tests/unit/db/test_gen_menu.py @@ -120,7 +120,7 @@ class TestGenMenu(unittest.TestCase): with db_mock.cursor() as conn: with conn.cursor() as cursor: for item in ["x", ("result"), ["result"], [1]]: - cursor.fetchone.return_value = (item) + cursor.fetchone.return_value = item self.assertTrue(phenotypes_exist(db_mock, "test")) @pytest.mark.unit_test @@ -140,7 +140,7 @@ class TestGenMenu(unittest.TestCase): db_mock = mock.MagicMock() with db_mock.cursor() as cursor: for item in ["x", ("result"), ["result"], [1]]: - cursor.fetchone.return_value = (item) + cursor.fetchone.return_value = item self.assertTrue(phenotypes_exist(db_mock, "test")) @pytest.mark.unit_test diff --git a/tests/unit/test_db_utils.py b/tests/unit/test_db_utils.py index beb7169..51f4296 100644 --- a/tests/unit/test_db_utils.py +++ b/tests/unit/test_db_utils.py @@ -1,25 +1,61 @@ """module contains test for db_utils""" -from unittest import mock - import pytest -from gn3.db_utils import parse_db_url, database_connection +from gn3.db_utils import parse_db_url + @pytest.mark.unit_test -@mock.patch("gn3.db_utils.mdb") -@mock.patch("gn3.db_utils.parse_db_url") -def test_database_connection(mock_db_parser, mock_sql): - """test for creating database connection""" - mock_db_parser.return_value = ("localhost", "guest", "4321", "users", None) +@pytest.mark.parametrize( + "sql_uri,expected", + (("mysql://theuser:passwd@thehost:3306/thedb", + { + "host": "thehost", + "port": 3306, + "user": "theuser", + "password": "passwd", + "database": "thedb" + }), + (("mysql://auser:passwd@somehost:3307/thedb?" + "unix_socket=/run/mysqld/mysqld.sock&connect_timeout=30"), + { + "host": "somehost", + "port": 3307, + "user": "auser", + "password": "passwd", + "database": "thedb", + "unix_socket": "/run/mysqld/mysqld.sock", + "connect_timeout": 30 + }), + ("mysql://guest:4321@localhost/users", + { + "host": "localhost", + "port": 3306, + "user": "guest", + "password": "4321", + "database": "users" + }), + ("mysql://localhost/users", + { + "host": "localhost", + "port": 3306, + "user": None, + "password": None, + "database": "users" + }))) +def test_parse_db_url(sql_uri, expected): + """Test that valid URIs are passed into valid connection dicts""" + assert parse_db_url(sql_uri) == expected - with database_connection("mysql://guest:4321@localhost/users") as _conn: - mock_sql.connect.assert_called_with( - db="users", user="guest", passwd="4321", host="localhost", - port=3306) @pytest.mark.unit_test -def test_parse_db_url(): - """test for parsing db_uri env variable""" - results = parse_db_url("mysql://username:4321@localhost/test") - expected_results = ("localhost", "username", "4321", "test", None) - assert results == expected_results +@pytest.mark.parametrize( + "sql_uri,invalidopt", + (("mysql://localhost/users?socket=/run/mysqld/mysqld.sock", "socket"), + ("mysql://localhost/users?connect_timeout=30¬avalidoption=value", + "notavalidoption"))) +def test_parse_db_url_with_invalid_options(sql_uri, invalidopt): + """Test that invalid options cause the function to raise an exception.""" + with pytest.raises(AssertionError) as exc_info: + parse_db_url(sql_uri) + + assert exc_info.value.args[0] == f"Invalid database connection option ({invalidopt}) provided." diff --git a/tests/unit/test_llm.py b/tests/unit/test_llm.py index 8fbaba6..3a79486 100644 --- a/tests/unit/test_llm.py +++ b/tests/unit/test_llm.py @@ -1,11 +1,22 @@ """Test cases for procedures defined in llms """ # pylint: disable=C0301 +# pylint: disable=W0613 +from datetime import datetime, timedelta +from unittest.mock import patch +from unittest.mock import MagicMock + import pytest from gn3.llms.process import fetch_pubmed from gn3.llms.process import parse_context from gn3.llms.process import format_bibliography_info +from gn3.llms.errors import LLMError +from gn3.api.llm import clean_query +from gn3.api.llm import is_verified_anonymous_user +from gn3.api.llm import is_valid_address +from gn3.api.llm import check_rate_limiter +FAKE_NOW = datetime(2025, 1, 1, 12, 0, 0) @pytest.mark.unit_test def test_parse_context(): """test for parsing doc id context""" @@ -104,3 +115,130 @@ def test_fetching_pubmed_info(monkeypatch): assert (fetch_pubmed(data, "/pubmed.json", "data/") == expected_results) + + +@pytest.mark.unit_test +def test_clean_query(): + """Test function for cleaning up query""" + assert clean_query("!what is genetics.") == "what is genetics" + assert clean_query("hello test?") == "hello test" + assert clean_query(" hello test with space?") == "hello test with space" + + +@pytest.mark.unit_test +def test_is_verified_anonymous_user(): + """Test function for verifying anonymous user metadata""" + assert is_verified_anonymous_user({}) is False + assert is_verified_anonymous_user({"Anonymous-Id" : "qws2121dwsdwdwe", + "Anonymous-Status" : "verified"}) is True + +@pytest.mark.unit_test +def test_is_valid_address() : + """Test function checks if is a valid ip address is valid""" + assert is_valid_address("invalid_ip") is False + assert is_valid_address("127.0.0.1") is True + + +@patch("gn3.api.llm.datetime") +@patch("gn3.api.llm.db.connection") +@patch("gn3.api.llm.is_valid_address", return_value=True) +@pytest.mark.unit_test +def test_first_time_visitor(mock_is_valid, mock_db_conn, mock_datetime): + """Test rate limiting for first-time visitor""" + mock_datetime.utcnow.return_value = FAKE_NOW + mock_datetime.strptime = datetime.strptime # keep real one + mock_datetime.strftime = datetime.strftime # keep real one + + # Set up DB mock + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_conn.__enter__.return_value = mock_conn + mock_conn.cursor.return_value = mock_cursor + mock_cursor.fetchone.return_value = None + mock_db_conn.return_value = mock_conn + + result = check_rate_limiter("127.0.0.1", "test/llm.db", "Chromosome x") + assert result is True + mock_cursor.execute.assert_any_call(""" + INSERT INTO Limiter(identifier, tokens, expiry_time) + VALUES (?, ?, ?) + """, ("127.0.0.1", 4, "2025-01-01 12:24:00")) + + +@patch("gn3.api.llm.datetime") +@patch("gn3.api.llm.db.connection") +@patch("gn3.api.llm.is_valid_address", return_value=True) +@pytest.mark.unit_test +def test_visitor_at_limit(mock_is_valid, mock_db_conn, mock_datetime): + """Test rate limiting for Visitor at limit""" + mock_datetime.utcnow.return_value = FAKE_NOW + mock_datetime.strptime = datetime.strptime # keep real one + mock_datetime.strftime = datetime.strftime + + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_conn.__enter__.return_value = mock_conn + mock_conn.cursor.return_value = mock_cursor + fake_expiry = (FAKE_NOW + timedelta(minutes=10)).strftime("%Y-%m-%d %H:%M:%S") + mock_cursor.fetchone.return_value = (0, fake_expiry) #token returned are 0 + mock_db_conn.return_value = mock_conn + with pytest.raises(LLMError) as exc_info: + check_rate_limiter("127.0.0.1", "test/llm.db", "Chromosome x") + # assert llm error with correct message is raised + assert exc_info.value.args == ('Rate limit exceeded. Please try again later.', 'Chromosome x') + + +@patch("gn3.api.llm.datetime") +@patch("gn3.api.llm.db.connection") +@patch("gn3.api.llm.is_valid_address", return_value=True) +@pytest.mark.unit_test +def test_visitor_with_tokens(mock_is_valid, mock_db_conn, mock_datetime): + """Test rate limiting for user with valid tokens""" + + mock_datetime.utcnow.return_value = FAKE_NOW + mock_datetime.strptime = datetime.strptime # Use real versions + mock_datetime.strftime = datetime.strftime + + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_conn.__enter__.return_value = mock_conn + mock_conn.cursor.return_value = mock_cursor + + fake_expiry = (FAKE_NOW + timedelta(minutes=10)).strftime("%Y-%m-%d %H:%M:%S") + mock_cursor.fetchone.return_value = (3, fake_expiry) # Simulate 3 tokens + + mock_db_conn.return_value = mock_conn + + results = check_rate_limiter("127.0.0.1", "test/llm.db", "Chromosome x") + assert results is True + mock_cursor.execute.assert_any_call(""" + UPDATE Limiter + SET tokens = tokens - 1 + WHERE identifier = ? AND tokens > 0 + """, ("127.0.0.1",)) + +@patch("gn3.api.llm.datetime") +@patch("gn3.api.llm.db.connection") +@patch("gn3.api.llm.is_valid_address", return_value=True) +@pytest.mark.unit_test +def test_visitor_token_expired(mock_is_valid, mock_db_conn, mock_datetime): + """Test rate limiting for expired tokens""" + + mock_datetime.utcnow.return_value = FAKE_NOW + mock_datetime.strptime = datetime.strptime + mock_datetime.strftime = datetime.strftime + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_conn.__enter__.return_value = mock_conn + mock_conn.cursor.return_value = mock_cursor + fake_expiry = (FAKE_NOW - timedelta(minutes=10)).strftime("%Y-%m-%d %H:%M:%S") + mock_cursor.fetchone.return_value = (3, fake_expiry) # Simulate 3 tokens + mock_db_conn.return_value = mock_conn + + result = check_rate_limiter("127.0.0.1", "test/llm.db", "Chromosome x") + assert result is True + mock_cursor.execute.assert_any_call(""" + UPDATE Limiter + SET tokens = ?, expiry_time = ? + WHERE identifier = ? + """, (4, "2025-01-01 12:24:00", "127.0.0.1")) diff --git a/tests/unit/test_rqtl2.py b/tests/unit/test_rqtl2.py new file mode 100644 index 0000000..ddce91b --- /dev/null +++ b/tests/unit/test_rqtl2.py @@ -0,0 +1,123 @@ +"""Module contains the unittest for rqtl2 functions """ +# pylint: disable=C0301 +from unittest import mock +import pytest +from gn3.computations.rqtl2 import compose_rqtl2_cmd +from gn3.computations.rqtl2 import generate_rqtl2_files +from gn3.computations.rqtl2 import prepare_files +from gn3.computations.rqtl2 import validate_required_keys + + +@pytest.mark.unit_test +@mock.patch("gn3.computations.rqtl2.write_to_csv") +def test_generate_rqtl2_files(mock_write_to_csv): + """Test for generating rqtl2 files from set of inputs""" + + mock_write_to_csv.side_effect = ( + "/tmp/workspace/geno_file.csv", + "/tmp/workspace/pheno_file.csv" + ) + data = {"crosstype": "riself", + "geno_data": [{"NAME": "Ge_code_1"}], + "pheno_data": [{"NAME": "14343_at"}], + "alleles": ["L", "C"], + "geno_codes": { + "L": 1, + "C": 2 + }, + "na.strings": ["-", "NA"] + } + + test_results = generate_rqtl2_files(data, "/tmp/workspace") + expected_results = {"geno_file": "/tmp/workspace/geno_file.csv", + "pheno_file": "/tmp/workspace/pheno_file.csv", + **data + } + assert test_results == expected_results + + # assert data is written to the csv + expected_calls = [mock.call( + "/tmp/workspace", + "geno_file.csv", + [{"NAME": "Ge_code_1"}] + ), + mock.call( + "/tmp/workspace", + "pheno_file.csv", + [{"NAME": "14343_at"}] + )] + mock_write_to_csv.assert_has_calls(expected_calls) + + +@pytest.mark.unit_test +def test_validate_required_keys(): + """Test to validate required keys are in a dataset""" + required_keys = ["geno_data", "pheno_data", "geno_codes"] + assert ((False, + "Required key(s) missing: geno_data, pheno_data, geno_codes") + == validate_required_keys(required_keys, {}) + ) + assert ((True, + "") + == validate_required_keys(required_keys, { + "geno_data": [], + "pheno_data": [], + "geno_codes": {} + }) + ) + + +@pytest.mark.unit_test +def test_compose_rqtl2_cmd(): + """Test for composing rqtl2 command""" + input_file = "/tmp/575732e-691e-49e5-8d82-30c564927c95/input_file.json" + output_file = "/tmp/575732e-691e-49e5-8d82-30c564927c95/output_file.json" + directory = "/tmp/575732e-691e-49e5-8d82-30c564927c95" + expected_results = f"Rscript /rqtl2_wrapper.R --input_file {input_file} --directory {directory} --output_file {output_file} --nperm 12 --method LMM --threshold 0.05 --cores 1" + + # test for using default configs + assert compose_rqtl2_cmd(rqtl_path="/rqtl2_wrapper.R", + input_file=input_file, + output_file=output_file, + workspace_dir=directory, + data={ + "nperm": 12, + "threshold": 0.05, + "method" : "LMM" + }, + config={}) == expected_results + + # test for default permutation, method and threshold and custom configs + expected_results = f"/bin/rscript /rqtl2_wrapper.R --input_file {input_file} --directory {directory} --output_file {output_file} --nperm 0 --method HK --threshold 1 --cores 12" + assert (compose_rqtl2_cmd(rqtl_path="/rqtl2_wrapper.R", + input_file=input_file, + output_file=output_file, + workspace_dir=directory, + data={}, + config={"MULTIPROCESSOR_PROCS": 12, "RSCRIPT": "/bin/rscript"}) + == expected_results) + + +@pytest.mark.unit_test +@mock.patch("gn3.computations.rqtl2.os.makedirs") +@mock.patch("gn3.computations.rqtl2.create_file") +@mock.patch("gn3.computations.rqtl2.uuid") +def test_preparing_rqtl_files(mock_uuid, mock_create_file, mock_mkdir): + """test to create required rqtl files""" + mock_create_file.return_value = None + mock_mkdir.return_value = None + mock_uuid.uuid4.return_value = "2fc75611-1524-418e-970f-67f94ea09846" + assert ( + ( + "/tmp/2fc75611-1524-418e-970f-67f94ea09846", + "/tmp/2fc75611-1524-418e-970f-67f94ea09846/rqtl2-input-2fc75611-1524-418e-970f-67f94ea09846.json", + "/tmp/2fc75611-1524-418e-970f-67f94ea09846/rqtl2-output-2fc75611-1524-418e-970f-67f94ea09846.json", + "/tmp/rqtl2-log-2fc75611-1524-418e-970f-67f94ea09846" + ) == prepare_files(tmpdir="/tmp/") + ) + # assert method to create files is called + expected_calls = [mock.call("/tmp/2fc75611-1524-418e-970f-67f94ea09846/rqtl2-input-2fc75611-1524-418e-970f-67f94ea09846.json"), + mock.call( + "/tmp/2fc75611-1524-418e-970f-67f94ea09846/rqtl2-output-2fc75611-1524-418e-970f-67f94ea09846.json"), + mock.call("/tmp/rqtl2-log-2fc75611-1524-418e-970f-67f94ea09846")] + mock_create_file.assert_has_calls(expected_calls) |
