From 7a4205d0c11eb0532f02a48ec70ac5d424d44656 Mon Sep 17 00:00:00 2001 From: gromdimon Date: Tue, 5 Sep 2023 16:34:55 +0200 Subject: [PATCH 01/14] feat: backend tests --- backend/app/main.py | 2 +- backend/tests/test_main.py | 43 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/backend/app/main.py b/backend/app/main.py index cf9a7b57..7a6824e2 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -31,7 +31,7 @@ #: The REEV version from the file (``None`` if to load dynamically from git) REEV_VERSION = None # Try to obtain version from file, otherwise keep it at ``None`` -if os.path.exists(VERSION_FILE): +if os.path.exists(VERSION_FILE): # pragma: no cover with open(VERSION_FILE) as f: REEV_VERSION = f.read().strip() or None diff --git a/backend/tests/test_main.py b/backend/tests/test_main.py index 81db8155..1f7effcc 100644 --- a/backend/tests/test_main.py +++ b/backend/tests/test_main.py @@ -1,3 +1,4 @@ +import subprocess import typing import pytest @@ -74,3 +75,45 @@ async def test_invalid_proxy_route(monkeypatch, httpx_mock): response = client.get("/proxy/some-other-path") assert response.status_code == 404 assert response.text == "Reverse proxy route not found" + + +@pytest.mark.asyncio +async def test_version(monkeypatch): + """Test version endpoint.""" + monkeypatch.setattr(main, "REEV_VERSION", "1.2.3") + response = client.get("/version") + assert response.status_code == 200 + assert response.text == "1.2.3" + + +@pytest.mark.asyncio +async def test_version_no_version(monkeypatch): + """Test version endpoint with no version.""" + monkeypatch.setattr(main, "REEV_VERSION", None) + response = client.get("/version") + assert response.status_code == 200 + expected = subprocess.check_output(["git", "describe", "--tags", "--dirty"]).strip().decode() + assert response.text == expected + + +@pytest.mark.asyncio +async def test_variantvalidator(monkeypatch, httpx_mock): + """Test variant validator endpoint.""" + variantvalidator_url = "https://rest.variantvalidator.org/VariantValidator/variantvalidator" + httpx_mock.add_response( + url=f"{variantvalidator_url}/{MOCKED_URL_TOKEN}", + method="GET", + text="Mocked response", + ) + + response = client.get(f"/variantvalidator/{MOCKED_URL_TOKEN}") + assert response.status_code == 200 + assert response.text == "Mocked response" + + +@pytest.mark.asyncio +async def test_favicon(): + """Test favicon endpoint.""" + response = client.get("/favicon.ico") + assert response.status_code == 200 + assert response.headers["content-type"] == "image/vnd.microsoft.icon" From 811e2a41cf014536d904138e0c2bab85007dbd9e Mon Sep 17 00:00:00 2001 From: gromdimon Date: Tue, 5 Sep 2023 17:36:35 +0200 Subject: [PATCH 02/14] feat: api tests --- frontend/src/api/__tests__/annonars.spec.ts | 52 +++++++- frontend/src/api/__tests__/common.spec.ts | 8 +- frontend/src/api/__tests__/mehari.spec.ts | 43 +++++++ frontend/src/api/__tests__/utils.spec.ts | 120 +++++++++++++++++- frontend/src/api/utils.ts | 5 +- .../src/assets/__tests__/BRCA1TxInfo.json | 3 + .../assets/__tests__/BRCA1VariantInfo.json | 3 + .../src/assets/__tests__/EMPSearchInfo.json | 3 + frontend/tsconfig.vitest.json | 1 - 9 files changed, 227 insertions(+), 11 deletions(-) create mode 100644 frontend/src/api/__tests__/mehari.spec.ts create mode 100644 frontend/src/assets/__tests__/BRCA1TxInfo.json create mode 100644 frontend/src/assets/__tests__/BRCA1VariantInfo.json create mode 100644 frontend/src/assets/__tests__/EMPSearchInfo.json diff --git a/frontend/src/api/__tests__/annonars.spec.ts b/frontend/src/api/__tests__/annonars.spec.ts index b3710fbf..1f3a359d 100644 --- a/frontend/src/api/__tests__/annonars.spec.ts +++ b/frontend/src/api/__tests__/annonars.spec.ts @@ -3,6 +3,8 @@ import createFetchMock from 'vitest-fetch-mock' import { AnnonarsClient } from '../annonars' import * as BRCA1geneInfo from '@/assets/__tests__/BRCA1GeneInfo.json' +import * as BRCA1VariantInfo from '@/assets/__tests__/BRCA1VariantInfo.json' +import * as EMPSearchInfo from '@/assets/__tests__/EMPSearchInfo.json' const fetchMocker = createFetchMock(vi) @@ -20,9 +22,9 @@ describe('Annonars Client', () => { expect(JSON.stringify(result)).toEqual(JSON.stringify(BRCA1geneInfo)) }) - it('fails to fetch gene info with wrong hgnc-id', async () => { + it('fails to fetch gene info with wrong HGNC id', async () => { fetchMocker.mockResponse((req) => { - if (req.url.includes('hgnc-id=BRCA1')) { + if (req.url.includes('hgnc_id=BRCA1')) { return Promise.resolve(JSON.stringify(BRCA1geneInfo)) } return Promise.resolve(JSON.stringify({ status: 400 })) @@ -32,4 +34,50 @@ describe('Annonars Client', () => { const result = await client.fetchGeneInfo('123') expect(JSON.stringify(result)).toEqual(JSON.stringify({ status: 400 })) }) + + it('fetches variant info correctly', async () => { + fetchMocker.mockResponseOnce(JSON.stringify(BRCA1VariantInfo)) + + const client = new AnnonarsClient() + const result = await client.fetchVariantInfo('grch37', 'chr17', 43044295, 'A', 'G') + expect(JSON.stringify(result)).toEqual(JSON.stringify(BRCA1VariantInfo)) + }) + + it('fails to fetch variant info with wrong variant', async () => { + fetchMocker.mockResponse((req) => { + if (req.url.includes('alternative=G')) { + return Promise.resolve(JSON.stringify(BRCA1VariantInfo)) + } + return Promise.resolve(JSON.stringify({ status: 400 })) + }) + + const client = new AnnonarsClient() + const result = await client.fetchVariantInfo('grch37', 'chr17', 43044295, 'A', 'T') + expect(JSON.stringify(result)).toEqual(JSON.stringify({ status: 400 })) + }) + + it('fetches genes correctly', async () => { + fetchMocker.mockResponseOnce(JSON.stringify(EMPSearchInfo)) + + const client = new AnnonarsClient() + const result = await client.fetchGenes( + 'q=BRCA1&fields=hgnc_id,ensembl_gene_id,ncbi_gene_id,symbol' + ) + expect(JSON.stringify(result)).toEqual(JSON.stringify(EMPSearchInfo)) + }) + + it('fails to fetch genes with wrong query', async () => { + fetchMocker.mockResponse((req) => { + if (req.url.includes('q=BRCA1')) { + return Promise.resolve(JSON.stringify(EMPSearchInfo)) + } + return Promise.resolve(JSON.stringify({ status: 400 })) + }) + + const client = new AnnonarsClient() + const result = await client.fetchGenes( + 'q=BRCA2&fields=hgnc_id,ensembl_gene_id,ncbi_gene_id,symbol' + ) + expect(JSON.stringify(result)).toEqual(JSON.stringify({ status: 400 })) + }) }) diff --git a/frontend/src/api/__tests__/common.spec.ts b/frontend/src/api/__tests__/common.spec.ts index fc081656..28d1df10 100644 --- a/frontend/src/api/__tests__/common.spec.ts +++ b/frontend/src/api/__tests__/common.spec.ts @@ -1,8 +1,14 @@ import { describe, it, expect } from 'vitest' -import { API_BASE_PREFIX_ANNONARS, API_BASE_PREFIX_MEHARI } from '../common' +import { API_BASE_PREFIX, API_BASE_PREFIX_ANNONARS, API_BASE_PREFIX_MEHARI } from '../common' describe('API_BASE_PREFIX constants', () => { + it('returns the correct API base prefix in production mode', () => { + const originalMode = import.meta.env.MODE + expect(API_BASE_PREFIX).toBe('/') + import.meta.env.MODE = originalMode + }) + it('returns the correct API base prefix for annonars in production mode', () => { const originalMode = import.meta.env.MODE expect(API_BASE_PREFIX_ANNONARS).toBe('/proxy/annonars') diff --git a/frontend/src/api/__tests__/mehari.spec.ts b/frontend/src/api/__tests__/mehari.spec.ts new file mode 100644 index 00000000..6254ecf9 --- /dev/null +++ b/frontend/src/api/__tests__/mehari.spec.ts @@ -0,0 +1,43 @@ +import { beforeEach, describe, it, expect, vi } from 'vitest' +import createFetchMock from 'vitest-fetch-mock' + +import { MehariClient } from '../mehari' +import * as BRCA1TxInfo from '@/assets/__tests__/BRCA1TxInfo.json' + +const fetchMocker = createFetchMock(vi) + +describe('Mehari Client', () => { + beforeEach(() => { + fetchMocker.enableMocks() + fetchMocker.resetMocks() + }) + + it('fetches TxCsq info correctly', async () => { + fetchMocker.mockResponseOnce(JSON.stringify(BRCA1TxInfo)) + + const client = new MehariClient() + const result = await client.retrieveTxCsq('grch37', 'chr17', 43044295, 'A', 'G', 'HGNC:1100') + expect(JSON.stringify(result)).toEqual(JSON.stringify(BRCA1TxInfo)) + }) + + it('fetches TxCsq info correctly without HGNC id', async () => { + fetchMocker.mockResponseOnce(JSON.stringify(BRCA1TxInfo)) + + const client = new MehariClient() + const result = await client.retrieveTxCsq('grch37', 'chr17', 43044295, 'A', 'G') + expect(JSON.stringify(result)).toEqual(JSON.stringify(BRCA1TxInfo)) + }) + + it('fails to fetch variant info with wrong variant', async () => { + fetchMocker.mockResponse((req) => { + if (req.url.includes('alternative=G')) { + return Promise.resolve(JSON.stringify(BRCA1TxInfo)) + } + return Promise.resolve(JSON.stringify({ status: 400 })) + }) + + const client = new MehariClient() + const result = await client.retrieveTxCsq('grch37', 'chr17', 43044295, 'A', 'T') + expect(JSON.stringify(result)).toEqual(JSON.stringify({ status: 400 })) + }) +}) diff --git a/frontend/src/api/__tests__/utils.spec.ts b/frontend/src/api/__tests__/utils.spec.ts index a4766e0f..2aa6e205 100644 --- a/frontend/src/api/__tests__/utils.spec.ts +++ b/frontend/src/api/__tests__/utils.spec.ts @@ -1,6 +1,13 @@ import { describe, it, expect } from 'vitest' -import { roundIt, search } from '../utils' +import { + roundIt, + separateIt, + isVariantMt, + isVariantMtHomopolymer, + search, + infoFromQuery +} from '../utils' describe('roundIt method', () => { it('should round a positive value with default digits', () => { @@ -34,8 +41,70 @@ describe('roundIt method', () => { }) }) +describe('separateIt method', () => { + it('should separate a positive value with default separator', () => { + const result = separateIt(123456789) + expect(result).toBe(' 123 456 789 ') + }) + + it('should separate a positive value with specified separator', () => { + const result = separateIt(123456789, ',') + expect(result).toBe(',123,456,789,') + }) + + it('should handle zero value', () => { + const result = separateIt(0) + expect(result).toBe('0 ') + }) + + it('should handle float value', () => { + const result = separateIt(123456789.12345) + expect(result).toBe(' 123 456 789 ') + }) +}) + +describe('isVariantMt method', () => { + it('should return true if mitochondrial chromosome', () => { + const result_MT = isVariantMt({ chromosome: 'MT' }) + const result_M = isVariantMt({ chromosome: 'M' }) + const result_chrMT = isVariantMt({ chromosome: 'chrMT' }) + const result_chrM = isVariantMt({ chromosome: 'chrM' }) + expect(result_MT).toBe(true) + expect(result_M).toBe(true) + expect(result_chrMT).toBe(true) + expect(result_chrM).toBe(true) + }) + + it('should return false if not mitochondrial chromosome', () => { + const result = isVariantMt({ chromosome: '1' }) + expect(result).toBe(false) + }) +}) + +describe('isVariantMtHomopolymer method', () => { + it('should return true if mitochondrial homopolymer', () => { + const result = isVariantMtHomopolymer({ chromosome: 'MT', start: 70 }) + expect(result).toBe(true) + }) + + it('should return false if not mitochondrial homopolymer (chromosome)', () => { + const result = isVariantMtHomopolymer({ chromosome: '1', start: 70 }) + expect(result).toBe(false) + }) + + it('should return false if not mitochondrial homopolymer (position)', () => { + const result = isVariantMtHomopolymer({ chromosome: 'MT', start: 1 }) + expect(result).toBe(false) + }) + + it('should return false for NaN', () => { + const result = isVariantMtHomopolymer(NaN) + expect(result).toBe(false) + }) +}) + describe('search method', () => { - it('should return route location if match', () => { + it('should return "gene" route location for HGNC queries', () => { const result = search('HGNC:1100', 'ghcr37') expect(result).toEqual({ name: 'gene', @@ -46,8 +115,49 @@ describe('search method', () => { }) }) - it.skip('should return null if no match', () => { - const result = search('foo', 'foo37') - expect(result).toBe(null) + it('should return "variant" route location for Variant queries', () => { + const result = search('chr37:12345:A:G', 'ghcr37') + expect(result).toEqual({ + name: 'variant', + params: { + searchTerm: 'chr37:12345:A:G', + genomeRelease: 'ghcr37' + } + }) + }) + + it('should return "genes" route location for general queries', () => { + const result = search('TP53', 'ghcr37') + expect(result).toEqual({ + name: 'genes', + query: { + q: 'TP53', + fields: 'hgnc_id,ensembl_gene_id,ncbi_gene_id,symbol' + } + }) + }) +}) + +describe('infoFromQuery method', () => { + it('should return info from query', () => { + const result = infoFromQuery('chr37:12345:A:G') + expect(result).toEqual({ + chromosome: 'chr37', + pos: '12345', + reference: 'A', + alternative: 'G', + hgnc_id: undefined + }) + }) + + it('should return empty object if no query', () => { + const result = infoFromQuery('') + expect(result).toEqual({ + chromosome: '', + pos: undefined, + reference: undefined, + alternative: undefined, + hgnc_id: undefined + }) }) }) diff --git a/frontend/src/api/utils.ts b/frontend/src/api/utils.ts index 4b794025..f93f23d7 100644 --- a/frontend/src/api/utils.ts +++ b/frontend/src/api/utils.ts @@ -58,7 +58,7 @@ export const isVariantMt = (smallVar: any): boolean => { * @param smallVar Small variant to check. * @returns whether the position is in a mitochondrial homopolymer */ -export const isVariantMtHomopolymer = (smallVar: any): any => { +export const isVariantMtHomopolymer = (smallVar: any): boolean => { if (!smallVar) { return false } @@ -75,6 +75,8 @@ export const isVariantMtHomopolymer = (smallVar: any): any => { } if (isVariantMt(smallVar)) { return positionCheck(start) || positionCheck(end) + } else { + return false } } @@ -136,7 +138,6 @@ export const search = (searchTerm: string, genomeRelease: string) => { return routeLocation } } - return null } /** diff --git a/frontend/src/assets/__tests__/BRCA1TxInfo.json b/frontend/src/assets/__tests__/BRCA1TxInfo.json new file mode 100644 index 00000000..03f9e16d --- /dev/null +++ b/frontend/src/assets/__tests__/BRCA1TxInfo.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1076fda8fd2bccd3d4ef2af3bb96acf1fa252bfa55efa982e9651f3ada0f0f6 +size 3710 diff --git a/frontend/src/assets/__tests__/BRCA1VariantInfo.json b/frontend/src/assets/__tests__/BRCA1VariantInfo.json new file mode 100644 index 00000000..1302674e --- /dev/null +++ b/frontend/src/assets/__tests__/BRCA1VariantInfo.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1cd6ea9f62f8621bbf78d2a54dc311d91cbdce8286e10599160b8d1c0d0862f2 +size 117222 diff --git a/frontend/src/assets/__tests__/EMPSearchInfo.json b/frontend/src/assets/__tests__/EMPSearchInfo.json new file mode 100644 index 00000000..433bfe15 --- /dev/null +++ b/frontend/src/assets/__tests__/EMPSearchInfo.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5689d3a4388ec55904f0ec592f1cd7066708cbbae78c6dcabf2fb2d8506c830f +size 3503 diff --git a/frontend/tsconfig.vitest.json b/frontend/tsconfig.vitest.json index d080d611..5e40cbb3 100644 --- a/frontend/tsconfig.vitest.json +++ b/frontend/tsconfig.vitest.json @@ -2,7 +2,6 @@ "extends": "./tsconfig.app.json", "exclude": [], "compilerOptions": { - "composite": true, "lib": [], "types": ["node", "jsdom"] } From 53e79cea1c87540b8b963c192ce80966f095e282 Mon Sep 17 00:00:00 2001 From: gromdimon Date: Tue, 5 Sep 2023 17:38:32 +0200 Subject: [PATCH 03/14] fix: backend test --- backend/tests/test_main.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/tests/test_main.py b/backend/tests/test_main.py index 1f7effcc..23000117 100644 --- a/backend/tests/test_main.py +++ b/backend/tests/test_main.py @@ -92,7 +92,11 @@ async def test_version_no_version(monkeypatch): monkeypatch.setattr(main, "REEV_VERSION", None) response = client.get("/version") assert response.status_code == 200 - expected = subprocess.check_output(["git", "describe", "--tags", "--dirty"]).strip().decode() + # The following lines do not work in a GitHub Actions workflow, as the + # tests are running not in the repository. + # expected = subprocess.check_output(["git", "describe", "--tags", "--dirty"]).strip().decode() + # The following line is a workaround for the above. + expected = "v0.0.0-16-g7a4205d-dirty" assert response.text == expected From b06ed33168f31a6a7da553e7af666e38db3fa370 Mon Sep 17 00:00:00 2001 From: gromdimon Date: Tue, 5 Sep 2023 18:13:25 +0200 Subject: [PATCH 04/14] wip --- backend/Pipfile | 1 + backend/Pipfile.lock | 264 +++++++++--------- backend/tests/test_main.py | 13 +- frontend/src/components/HeaderDefault.vue | 1 + .../src/stores/__tests__/genesList.spec.ts | 78 ++++++ 5 files changed, 222 insertions(+), 135 deletions(-) create mode 100644 frontend/src/stores/__tests__/genesList.spec.ts diff --git a/backend/Pipfile b/backend/Pipfile index 892cbfaa..1885c004 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -20,6 +20,7 @@ pytest = "*" pytest-asyncio = "*" pytest-cov = "*" pytest-httpx = "*" +pytest-subprocess = "*" sphinx = "*" sphinx-rtd-theme = "*" starlette = "*" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 7ce66281..4be494f2 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "3483bfb1650286c17d19cf1dd690354f5869b28cf2c9a10f7708f2a388404ecc" + "sha256": "75758868d20e4fa7fb7618ab07ef00f73cd6e0215275661cfb4fb4e8932ad72f" }, "pipfile-spec": 6, "requires": { @@ -139,11 +139,11 @@ }, "fastapi": { "hashes": [ - "sha256:7b32000d14ca9992f7461117b81e4ef9ff0c07936af641b4fe40e67d5f9d63cb", - "sha256:aef5f8676eb1b8389952e1fe734abe20f04b71f6936afcc53b320ba79b686a4b" + "sha256:345844e6a82062f06a096684196aaf96c1198b25c06b72c1311b882aa2d8a35d", + "sha256:5e5f17e826dbd9e9b5a5145976c5cd90bcaa61f2bf9a69aca423f2bcebe44d83" ], "index": "pypi", - "version": "==0.101.1" + "version": "==0.103.1" }, "h11": { "hashes": [ @@ -179,123 +179,123 @@ }, "pydantic": { "hashes": [ - "sha256:0c88bd2b63ed7a5109c75ab180d55f58f80a4b559682406812d0684d3f4b9192", - "sha256:31b5cada74b2320999fb2577e6df80332a200ff92e7775a52448b6b036fce24a" + "sha256:1607cc106602284cd4a00882986570472f193fde9cb1259bceeaedb26aa79a6d", + "sha256:45b5e446c6dfaad9444819a293b921a40e1db1aa61ea08aede0522529ce90e81" ], "index": "pypi", - "version": "==2.2.1" + "version": "==2.3.0" }, "pydantic-core": { "hashes": [ - "sha256:043212f21c75cb6ee3a92fffbc747410e32b08e1a419ce16a9da98a16d660a7c", - "sha256:09e4ebd11a0b333b1fca75c1004c76dc9719f3aaf83ae38c42358754d8a76148", - "sha256:1281c940f47e5c89b594ef7580045647df1f9ad687edd503bcc0485be94576f4", - "sha256:136de286abf53f326b90389aaaca8a8050c2570adfc74afe06ab1c35d5d242bf", - "sha256:153a5dd24c09ab7544beda967366afbaae8350b327a4ebd5807ed45ec791baa0", - "sha256:17ab25bb24e98b61d120b7248c2b49ea56ce754a050d6b348be42015fcb7aa25", - "sha256:1f4327fa6a1ac3da62b27d43bb0f27657ed4e601b141ecbfcf8523814b6c33b6", - "sha256:200704f6824f8014bdccb1ce57cbd328666e6de4ecd77f0b8ab472cdea9c49ce", - "sha256:2034d9b83a59b3b74b9dbf97ddb99de86c08863c1c33aabf80bc95791c7d50c3", - "sha256:20e850f3242d7836a5e15453f798d8569b9754350c8e184ba32d102c515dd507", - "sha256:26b81017aeae0d96f776fbce34a3a763d26ac575d8ad3f1202bdfdd2b935954b", - "sha256:27ba58bbfd1b2b9da45bfe524e680e2bc747a1ca9738ee5aa18d8cbdcc08e5e6", - "sha256:2b8ccec2189d8a8b83929f79e5bc00c0656f6c2ba4345125c0c82d1b77e15a26", - "sha256:2d41701c88d8b678c16c10562949f2d28aceacd767cbe51dac9c8c41e6e609fb", - "sha256:2da1d21a4f2675d5b8a749674993a65c0537e2066e7ab7b1a4a54ef0b3ac8efd", - "sha256:2effc71653247e76c5b95d15c58d4ca3f591f42f714eb3b32df9d6ec613794a5", - "sha256:3210eb73707e3487c16ef25cfd1663660f4e7d647a181d6c2fb18bc6167985fb", - "sha256:33b9343aa464d60c31937b361abde08d3af9943f3eb09d3216211b6236bd40c4", - "sha256:34734d486d059f0f6f5bfa9ba4a41449f666e2abbde002e9fa8b050bc50e3347", - "sha256:364c13ef48c9e2f8c2ea8ee0da5ea23db5e218f99e796cbf360a2a7cab511439", - "sha256:3679b9a1f41eb1b699e9556f91281d78c416cdc59ae90d5733fbe2017f1effe9", - "sha256:382d40843ae759d43ef65b67dec713390f9417135c1dd730afbf03cf2f450f45", - "sha256:3bdf293b6304bc451678b7016c2505b7d97aa85ff13dac4420027b1b69e15d3d", - "sha256:3c8f3aebaf92f088b1dafd7101d1ccca0459ae0f5b26017411b9969667d289a9", - "sha256:3d14ae98a8d251402ef8ed017039d2fc3e29fb155f909cd3816ba259fd30fb48", - "sha256:420a76a62dd20a6ef08445abf7cf04dcd8a845a5bb15932c2e88a8e518c70d43", - "sha256:4223e8bdad41d846a84cda400cd538e1cdc63d98eb4d41951396bfdb88fd8ce9", - "sha256:4525b8498d362e4e324e3e175239b364768f52bd3563ac4ef9750160f5789de8", - "sha256:45d248c3c5c5c23a8d048cfdebc8151ae7b32a6dc6d68fbca995521e54692207", - "sha256:4902300e763a2fcc49ae14366493ef1fdbd3c7128b9acf37aef505f671aa681f", - "sha256:494b211b12b8fedd184dbba609f6ed582e23561db57c1996fd6773989dbaef9b", - "sha256:4a3c20808d3ced90e29439f72a563eadf21d29560935cc818b2dab80b92c114a", - "sha256:51ffa985b874ca7d0dc199bb75c67b77907379291c91532a9e2d981f7b681527", - "sha256:5482d692ae37857695feccb179022728b275b7bfcc1c85bcdf7b556e76bffcd8", - "sha256:55701608e60418a423db2486b5c64d790f86eb78a11b9077efb6302c50e62564", - "sha256:55aac69d7339a63e37164f0a629c3034becc6746d68d126118a3ee4493514bed", - "sha256:56672429f8a89d2a0f4402d912f0dad68c2d05f7c278d3152c6fb4a76c2a429a", - "sha256:56e4953cd911293d6d755e2a97c651826aca76201db8f1ee298939e703721390", - "sha256:588a5ffd8bbf1b2230611ed1b45221adcf05b981037b2f853b5f20465849b5c1", - "sha256:5a12520a6d502a25f6e47319874e47056b290f1b3c2ed9391444ce81c8cc5b83", - "sha256:5b4efa68bcfa6f2b93624c6660b6cf4b7b4336d4225afb314254a0ed9c9f4153", - "sha256:5f253d20314e53ba0fb2b95541b6ed23f44fbcd927fe7674de341545c3327c3d", - "sha256:60a238bb4ab09a81a6b25c9a0bb12756cfab2d9f3a7a471f857a179f83da0df6", - "sha256:6221c97d6d58f2370650cfe3d81408901a1951c99960e1df9f6f9f8482d73d08", - "sha256:64ff7a4b7ee2a56735af28da76c5dacbba6995801080f739d14610f4aa3de35d", - "sha256:66eda8ac48ac33e9e5c6541c8e30c702924b70a6f2e9732b74230d9b2dd35fb6", - "sha256:6916b27072c957947919fb32551f08486562bb8616f2e3db9e4e9c1d83d36886", - "sha256:6a839c95d5cc91eed053d8dafde4e200c4bc82f56fb1cf7bbfaeb03e2d907929", - "sha256:6dd6c9f47e26779bf1f7da4d6ccd60f66973e63b0a143438f1e20bae296c3fde", - "sha256:6ea8dd2854fe6cee5ea0d60304ee7877dffe487cf118f221e85029269dd1235d", - "sha256:707e3005e8c129bdac117285b71717c13b9ed81a81eae0b1642f4ddc60028e63", - "sha256:7188359b95a2b1aef5744a2ee6af2d9cfc733dd823f8840f4c896129477a172b", - "sha256:734864605d722a6f8db3b9c96371710f7cb591fbfca40cfeaedf5b67df282438", - "sha256:760f8a0aeb43ceeff1e536859e071a72e91075d4d37d51470812c4f49e682702", - "sha256:775098e3629a959dfec8444667a53e0916839e9fbf6b55e07d6e2aadde006400", - "sha256:7888b3ee7566865cff3e9edab5d6cdf2e7cf793df17fe53d5e7be3e57eae45ec", - "sha256:78eadd8d7d5cd8c3616e363c394d721437c339feaa4c28404e2eda79add69781", - "sha256:7c3a2b4d1636446dc71da1e949d2cf9ac1ee691ca63a640b77fce0360b4b75be", - "sha256:7ddaa2c3c66682f0ff4ebc8c85ef2d8305f32deba79416464c47c93d94ca3740", - "sha256:7ef56a05bb60336d5e795bf166d6712b2362e6478522c77e8336cb0da8909913", - "sha256:7f03541c25a77fb5445055e070b69d292c9818a9195ffbfd3962c0ad0da983e8", - "sha256:81424dc05c4342a19fb64323bb9d4468e7407b745c00377ccc4d3dd96d5e02fe", - "sha256:8714e958d01342d08e520ffec6c1acf66cdec83ce51302f9a1a6efb2f784d0b6", - "sha256:92321582e59da185b76b2eca4488ea95e41800672e57107509d32ebf8ad550f8", - "sha256:9b623e09239ed333d14c02c9fcd1a7bb350b95eca8383f6e9b0d8e373d5a14b5", - "sha256:9bf3ba6b4878ee692f6e24230801f682807fd97356bc2064f630fc0a2ad2ead6", - "sha256:a1ad48e77935d7dbbc2d75aeb638abbfbd0df0cfacf774dbe98d52271468f00c", - "sha256:a32ed5a794918a61bf77b967c197eb78f31ad4e3145860193dc381bde040717e", - "sha256:a4536d132a8bbd05bf368fb802a264cb9828f6c85e4029a6a3670bc98ba97323", - "sha256:a5127b811c6a26deb85f5b17a06c26c28ce204e51e0a963b75bdf8612b22546d", - "sha256:a809498dceb0cd1cd1e57a2bfdc70ea82f424776e0196f4d63c4b6fcdaeb5aab", - "sha256:aadc84f5bd7b1421b5a6b389ceff46062dd4a58c44cfb75990e9ca2d9d8270df", - "sha256:b1a01dce87507b9a8f1b71933ade85c573a22c9bd4649590e28d8a497afb68bd", - "sha256:b1aed20778092f8334c8eaf91550fa2805221d5e9b40ebdd1f46ee7efc159a48", - "sha256:b974d65692333931b4c7f730e7a3135ff854a1e5384bc260de3327ea364c835a", - "sha256:bb6273068e9450c5c91f58dd277fbd406b896ffa30f0ef312edc5519d07f16ae", - "sha256:c07cdb2e02733e5f26b9b004a1a8b99814d175f8953fa9f59e4293de2b8e9787", - "sha256:c1e44b77442fb5b1b6fccea30e3359b14d0a2e5896801243defe54482a591500", - "sha256:c22e4fbfb5823d0fcb2c20ed164b39c3588554f9635f70765e8c9cff0fef67ad", - "sha256:c5be947ad41a7602f941dc834d03e64dd1c7fae65fa85cb4f1004a95c5d50df1", - "sha256:c7b89b2875b967ad5c3c980bf72773851554f80c2529796e815a10c99295d872", - "sha256:c82fb25f965f6777032fc2f2856c86149f7709c8f7fd0c020a8631b8211f2bab", - "sha256:ca5606bd82e255b1d704a4334e5ebf05ae966b69686fae02dcd31c057bdcb113", - "sha256:cb5131d75d69b0547ef9a8f46f7b94857411c9badcdd5092de61a3b4943f08c7", - "sha256:cc7fc3e81b4ea6bce7e0e1d9797f496e957c5e66adf483f89afdce2d81d19986", - "sha256:cd163109047ab41ef1ea34258b35beb3ccac90af2012927ee8ab6ff122fef671", - "sha256:cd6f05f3e237ed6b3949464e7679e55843645fe0fe8d3b33277c321386836f6a", - "sha256:cd9f14454b4bc89c705ce17951f9c783db82efd2b44a424487c593e2269eef61", - "sha256:d0bf1c2545ab253732229c7fe8294d98eb08f99aa25a388267e1bc4d2d7e0a34", - "sha256:d1141f18414aee8865c7917ae1432e419c1983272f53625152493692ff3d6783", - "sha256:d6971131de66d1a37293f2e032206b6984b0dec44f568b453dfe89a84a2de0cc", - "sha256:da240bbd8191edc6009e7793d5d4d67c55f56225c4788f068d6286c20e5a2038", - "sha256:db0c12f1e9d3bf658634621f3423486803d749fef77a64cfb4252f9d619e1817", - "sha256:de1a3e56e34264d5216c67d2a48185216ada8f5f35a7f4c96a3971847c0de897", - "sha256:dfc8f534a21b60b00f87e5a4fc36b8b8945160a6cc9e7b6e67db541c766c9597", - "sha256:dfdb1617af455a551be4cc0471f0bf3bfb1e882db71afad0e587c821326bb749", - "sha256:e1c69334bb843c9bff98f52a1fa6c06420081a561fcecb03c6b9376960bd7de2", - "sha256:e2d8faedb138c704957642fdf154c94f1b3d2a15cbd2472e45665f80463e85ee", - "sha256:e3ff36f945342086ee917d4219dd0e59660a2dfcdb86a07696c2791f5d59c07d", - "sha256:e55514a022c768cccf07a675d20d07b847980dcd9250f6b516a86bab5612fc01", - "sha256:e84812b1ca989b2e9f4913d7b75ae0eece2a90154de35b4c5411ad640bfd387c", - "sha256:f2fed4ad60ccf2698bd04e95dfc3bd84149ced9605a29fd27d624701e1da300c", - "sha256:f34f26d8a5f1a45366189ec30a57f43b21e2172d0d3b62822638dd885cc8eaab", - "sha256:f55001a689111a297c0006c46c0589cfd559261baaa9a37bc35eff05b8cae1a6", - "sha256:f5b51ec04743c94288c46e3759769611ab7c5ce0f941113363da96d20d345fb6", - "sha256:f7ec4c6edafa3f0eb1aa461e31ea263736cc541b2459dddfbda7085b30844801" + "sha256:002d0ea50e17ed982c2d65b480bd975fc41086a5a2f9c924ef8fc54419d1dea3", + "sha256:02e1c385095efbd997311d85c6021d32369675c09bcbfff3b69d84e59dc103f6", + "sha256:046af9cfb5384f3684eeb3f58a48698ddab8dd870b4b3f67f825353a14441418", + "sha256:04fe5c0a43dec39aedba0ec9579001061d4653a9b53a1366b113aca4a3c05ca7", + "sha256:07a1aec07333bf5adebd8264047d3dc518563d92aca6f2f5b36f505132399efc", + "sha256:1480fa4682e8202b560dcdc9eeec1005f62a15742b813c88cdc01d44e85308e5", + "sha256:1508f37ba9e3ddc0189e6ff4e2228bd2d3c3a4641cbe8c07177162f76ed696c7", + "sha256:171a4718860790f66d6c2eda1d95dd1edf64f864d2e9f9115840840cf5b5713f", + "sha256:19e20f8baedd7d987bd3f8005c146e6bcbda7cdeefc36fad50c66adb2dd2da48", + "sha256:1a0ddaa723c48af27d19f27f1c73bdc615c73686d763388c8683fe34ae777bad", + "sha256:1aa712ba150d5105814e53cb141412217146fedc22621e9acff9236d77d2a5ef", + "sha256:1ac1750df1b4339b543531ce793b8fd5c16660a95d13aecaab26b44ce11775e9", + "sha256:1c721bfc575d57305dd922e6a40a8fe3f762905851d694245807a351ad255c58", + "sha256:1ce8c84051fa292a5dc54018a40e2a1926fd17980a9422c973e3ebea017aa8da", + "sha256:1fa1f6312fb84e8c281f32b39affe81984ccd484da6e9d65b3d18c202c666149", + "sha256:22134a4453bd59b7d1e895c455fe277af9d9d9fbbcb9dc3f4a97b8693e7e2c9b", + "sha256:23470a23614c701b37252618e7851e595060a96a23016f9a084f3f92f5ed5881", + "sha256:240a015102a0c0cc8114f1cba6444499a8a4d0333e178bc504a5c2196defd456", + "sha256:252851b38bad3bfda47b104ffd077d4f9604a10cb06fe09d020016a25107bf98", + "sha256:2a20c533cb80466c1d42a43a4521669ccad7cf2967830ac62c2c2f9cece63e7e", + "sha256:2dd50d6a1aef0426a1d0199190c6c43ec89812b1f409e7fe44cb0fbf6dfa733c", + "sha256:340e96c08de1069f3d022a85c2a8c63529fd88709468373b418f4cf2c949fb0e", + "sha256:3796a6152c545339d3b1652183e786df648ecdf7c4f9347e1d30e6750907f5bb", + "sha256:37a822f630712817b6ecc09ccc378192ef5ff12e2c9bae97eb5968a6cdf3b862", + "sha256:3a750a83b2728299ca12e003d73d1264ad0440f60f4fc9cee54acc489249b728", + "sha256:3c8945a105f1589ce8a693753b908815e0748f6279959a4530f6742e1994dcb6", + "sha256:3ccc13afee44b9006a73d2046068d4df96dc5b333bf3509d9a06d1b42db6d8bf", + "sha256:3f90e5e3afb11268628c89f378f7a1ea3f2fe502a28af4192e30a6cdea1e7d5e", + "sha256:4292ca56751aebbe63a84bbfc3b5717abb09b14d4b4442cc43fd7c49a1529efd", + "sha256:430ddd965ffd068dd70ef4e4d74f2c489c3a313adc28e829dd7262cc0d2dd1e8", + "sha256:439a0de139556745ae53f9cc9668c6c2053444af940d3ef3ecad95b079bc9987", + "sha256:44b4f937b992394a2e81a5c5ce716f3dcc1237281e81b80c748b2da6dd5cf29a", + "sha256:48c1ed8b02ffea4d5c9c220eda27af02b8149fe58526359b3c07eb391cb353a2", + "sha256:4ef724a059396751aef71e847178d66ad7fc3fc969a1a40c29f5aac1aa5f8784", + "sha256:50555ba3cb58f9861b7a48c493636b996a617db1a72c18da4d7f16d7b1b9952b", + "sha256:522a9c4a4d1924facce7270c84b5134c5cabcb01513213662a2e89cf28c1d309", + "sha256:5493a7027bfc6b108e17c3383959485087d5942e87eb62bbac69829eae9bc1f7", + "sha256:56ea80269077003eaa59723bac1d8bacd2cd15ae30456f2890811efc1e3d4413", + "sha256:5a2a3c9ef904dcdadb550eedf3291ec3f229431b0084666e2c2aa8ff99a103a2", + "sha256:5cfde4fab34dd1e3a3f7f3db38182ab6c95e4ea91cf322242ee0be5c2f7e3d2f", + "sha256:5e4a2cf8c4543f37f5dc881de6c190de08096c53986381daebb56a355be5dfe6", + "sha256:5e9c068f36b9f396399d43bfb6defd4cc99c36215f6ff33ac8b9c14ba15bdf6b", + "sha256:5ed7ceca6aba5331ece96c0e328cd52f0dcf942b8895a1ed2642de50800b79d3", + "sha256:5fa159b902d22b283b680ef52b532b29554ea2a7fc39bf354064751369e9dbd7", + "sha256:615a31b1629e12445c0e9fc8339b41aaa6cc60bd53bf802d5fe3d2c0cda2ae8d", + "sha256:621afe25cc2b3c4ba05fff53525156d5100eb35c6e5a7cf31d66cc9e1963e378", + "sha256:6656a0ae383d8cd7cc94e91de4e526407b3726049ce8d7939049cbfa426518c8", + "sha256:672174480a85386dd2e681cadd7d951471ad0bb028ed744c895f11f9d51b9ebe", + "sha256:692b4ff5c4e828a38716cfa92667661a39886e71136c97b7dac26edef18767f7", + "sha256:6bcc1ad776fffe25ea5c187a028991c031a00ff92d012ca1cc4714087e575973", + "sha256:6bf7d610ac8f0065a286002a23bcce241ea8248c71988bda538edcc90e0c39ad", + "sha256:75c0ebbebae71ed1e385f7dfd9b74c1cff09fed24a6df43d326dd7f12339ec34", + "sha256:788be9844a6e5c4612b74512a76b2153f1877cd845410d756841f6c3420230eb", + "sha256:7dc2ce039c7290b4ef64334ec7e6ca6494de6eecc81e21cb4f73b9b39991408c", + "sha256:813aab5bfb19c98ae370952b6f7190f1e28e565909bfc219a0909db168783465", + "sha256:8421cf496e746cf8d6b677502ed9a0d1e4e956586cd8b221e1312e0841c002d5", + "sha256:84e87c16f582f5c753b7f39a71bd6647255512191be2d2dbf49458c4ef024588", + "sha256:84f8bb34fe76c68c9d96b77c60cef093f5e660ef8e43a6cbfcd991017d375950", + "sha256:85cc4d105747d2aa3c5cf3e37dac50141bff779545ba59a095f4a96b0a460e70", + "sha256:883daa467865e5766931e07eb20f3e8152324f0adf52658f4d302242c12e2c32", + "sha256:8b2b1bfed698fa410ab81982f681f5b1996d3d994ae8073286515ac4d165c2e7", + "sha256:8ecbac050856eb6c3046dea655b39216597e373aa8e50e134c0e202f9c47efec", + "sha256:930bfe73e665ebce3f0da2c6d64455098aaa67e1a00323c74dc752627879fc67", + "sha256:9616567800bdc83ce136e5847d41008a1d602213d024207b0ff6cab6753fe645", + "sha256:9680dd23055dd874173a3a63a44e7f5a13885a4cfd7e84814be71be24fba83db", + "sha256:99faba727727b2e59129c59542284efebbddade4f0ae6a29c8b8d3e1f437beb7", + "sha256:9a718d56c4d55efcfc63f680f207c9f19c8376e5a8a67773535e6f7e80e93170", + "sha256:9b33bf9658cb29ac1a517c11e865112316d09687d767d7a0e4a63d5c640d1b17", + "sha256:9e8b374ef41ad5c461efb7a140ce4730661aadf85958b5c6a3e9cf4e040ff4bb", + "sha256:9e9b65a55bbabda7fccd3500192a79f6e474d8d36e78d1685496aad5f9dbd92c", + "sha256:a0b7486d85293f7f0bbc39b34e1d8aa26210b450bbd3d245ec3d732864009819", + "sha256:a53e3195f134bde03620d87a7e2b2f2046e0e5a8195e66d0f244d6d5b2f6d31b", + "sha256:a87c54e72aa2ef30189dc74427421e074ab4561cf2bf314589f6af5b37f45e6d", + "sha256:a892b5b1871b301ce20d40b037ffbe33d1407a39639c2b05356acfef5536d26a", + "sha256:a8acc9dedd304da161eb071cc7ff1326aa5b66aadec9622b2574ad3ffe225525", + "sha256:aaafc776e5edc72b3cad1ccedb5fd869cc5c9a591f1213aa9eba31a781be9ac1", + "sha256:acafc4368b289a9f291e204d2c4c75908557d4f36bd3ae937914d4529bf62a76", + "sha256:b0a5d7edb76c1c57b95df719af703e796fc8e796447a1da939f97bfa8a918d60", + "sha256:b25afe9d5c4f60dcbbe2b277a79be114e2e65a16598db8abee2a2dcde24f162b", + "sha256:b44c42edc07a50a081672e25dfe6022554b47f91e793066a7b601ca290f71e42", + "sha256:b594b64e8568cf09ee5c9501ede37066b9fc41d83d58f55b9952e32141256acd", + "sha256:b962700962f6e7a6bd77e5f37320cabac24b4c0f76afeac05e9f93cf0c620014", + "sha256:bb128c30cf1df0ab78166ded1ecf876620fb9aac84d2413e8ea1594b588c735d", + "sha256:bf9d42a71a4d7a7c1f14f629e5c30eac451a6fc81827d2beefd57d014c006c4a", + "sha256:c6595b0d8c8711e8e1dc389d52648b923b809f68ac1c6f0baa525c6440aa0daa", + "sha256:c8c6660089a25d45333cb9db56bb9e347241a6d7509838dbbd1931d0e19dbc7f", + "sha256:c9d469204abcca28926cbc28ce98f28e50e488767b084fb3fbdf21af11d3de26", + "sha256:d38bbcef58220f9c81e42c255ef0bf99735d8f11edef69ab0b499da77105158a", + "sha256:d4eb77df2964b64ba190eee00b2312a1fd7a862af8918ec70fc2d6308f76ac64", + "sha256:d63b7545d489422d417a0cae6f9898618669608750fc5e62156957e609e728a5", + "sha256:d7050899026e708fb185e174c63ebc2c4ee7a0c17b0a96ebc50e1f76a231c057", + "sha256:d79f1f2f7ebdb9b741296b69049ff44aedd95976bfee38eb4848820628a99b50", + "sha256:d85463560c67fc65cd86153a4975d0b720b6d7725cf7ee0b2d291288433fc21b", + "sha256:d9140ded382a5b04a1c030b593ed9bf3088243a0a8b7fa9f071a5736498c5483", + "sha256:d9b4916b21931b08096efed090327f8fe78e09ae8f5ad44e07f5c72a7eedb51b", + "sha256:df14f6332834444b4a37685810216cc8fe1fe91f447332cd56294c984ecbff1c", + "sha256:e49ce7dc9f925e1fb010fc3d555250139df61fa6e5a0a95ce356329602c11ea9", + "sha256:e61eae9b31799c32c5f9b7be906be3380e699e74b2db26c227c50a5fc7988698", + "sha256:ea053cefa008fda40f92aab937fb9f183cf8752e41dbc7bc68917884454c6362", + "sha256:f06e21ad0b504658a3a9edd3d8530e8cea5723f6ea5d280e8db8efc625b47e49", + "sha256:f14546403c2a1d11a130b537dda28f07eb6c1805a43dae4617448074fd49c282", + "sha256:f1a5d8f18877474c80b7711d870db0eeef9442691fcdb00adabfc97e183ee0b0", + "sha256:f2969e8f72c6236c51f91fbb79c33821d12a811e2a94b7aa59c65f8dbdfad34a", + "sha256:f468d520f47807d1eb5d27648393519655eadc578d5dd862d06873cce04c4d1b", + "sha256:f70dc00a91311a1aea124e5f64569ea44c011b58433981313202c46bccbec0e1", + "sha256:f93255b3e4d64785554e544c1c76cd32f4a354fa79e2eeca5d16ac2e7fdd57aa" ], "markers": "python_version >= '3.7'", - "version": "==2.6.1" + "version": "==2.6.3" }, "python-dotenv": { "hashes": [ @@ -800,11 +800,11 @@ }, "pluggy": { "hashes": [ - "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849", - "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3" + "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", + "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" ], - "markers": "python_version >= '3.7'", - "version": "==1.2.0" + "markers": "python_version >= '3.8'", + "version": "==1.3.0" }, "pycodestyle": { "hashes": [ @@ -832,11 +832,11 @@ }, "pytest": { "hashes": [ - "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32", - "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a" + "sha256:2f2301e797521b23e4d2585a0a3d7b5e50fdddaaf7e7d6773ea26ddb17c213ab", + "sha256:460c9a59b14e27c602eb5ece2e47bec99dc5fc5f6513cf924a7d03a578991b1f" ], "index": "pypi", - "version": "==7.4.0" + "version": "==7.4.1" }, "pytest-asyncio": { "hashes": [ @@ -856,11 +856,19 @@ }, "pytest-httpx": { "hashes": [ - "sha256:ba38a9e6c685d3cf6197551a79bf7e41f8bbc57a6d1de65b537f77e87f56ecd3", - "sha256:cfed19eb8b13cbdf464bbb1c4ef88717d88d42334aa9ce516e56e46975c77f74" + "sha256:193cecb57a005eb15288f68986f328d4c8d06c0b7c4ef1ce512e024cbb1d5961", + "sha256:259e6266cf3e04eb8fcc18dff262657ad96f6b8668dc2171fb353eaec5571889" + ], + "index": "pypi", + "version": "==0.24.0" + }, + "pytest-subprocess": { + "hashes": [ + "sha256:d7693b96f588f39b84c7b2b5c04287459246dfae6be1dd4098937a728ad4fbe3", + "sha256:dfd75b10af6800a89a9b758f2e2eceff9de082a27bd1388521271b6e8bde298b" ], "index": "pypi", - "version": "==0.23.1" + "version": "==1.5.0" }, "requests": { "hashes": [ @@ -887,11 +895,11 @@ }, "sphinx": { "hashes": [ - "sha256:1c0abe6d4de7a6b2c2b109a2e18387bf27b240742e1b34ea42ac3ed2ac99978c", - "sha256:ed33bc597dd8f05cd37118f64cbac0b8bf773389a628ddfe95ab9e915c9308dc" + "sha256:1a9290001b75c497fd087e92b0334f1bbfa1a1ae7fddc084990c4b7bd1130b88", + "sha256:9269f9ed2821c9ebd30e4204f5c2339f5d4980e377bc89cb2cb6f9b17409c20a" ], "index": "pypi", - "version": "==7.2.2" + "version": "==7.2.5" }, "sphinx-rtd-theme": { "hashes": [ @@ -990,4 +998,4 @@ "version": "==2.0.4" } } -} \ No newline at end of file +} diff --git a/backend/tests/test_main.py b/backend/tests/test_main.py index 23000117..1639107f 100644 --- a/backend/tests/test_main.py +++ b/backend/tests/test_main.py @@ -87,17 +87,16 @@ async def test_version(monkeypatch): @pytest.mark.asyncio -async def test_version_no_version(monkeypatch): +async def test_version_no_version(monkeypatch, fp): """Test version endpoint with no version.""" monkeypatch.setattr(main, "REEV_VERSION", None) + # We mock the output of ``git describe`` as subprocesses will be triggered + # internally. + fp.register(["git", "describe", "--tags", "--dirty"], stdout="v0.0.0-16-g7a4205d-dirty") response = client.get("/version") + assert response.status_code == 200 - # The following lines do not work in a GitHub Actions workflow, as the - # tests are running not in the repository. - # expected = subprocess.check_output(["git", "describe", "--tags", "--dirty"]).strip().decode() - # The following line is a workaround for the above. - expected = "v0.0.0-16-g7a4205d-dirty" - assert response.text == expected + assert response.text == "v0.0.0-16-g7a4205d-dirty" @pytest.mark.asyncio diff --git a/frontend/src/components/HeaderDefault.vue b/frontend/src/components/HeaderDefault.vue index 05e0da3d..864c1e41 100644 --- a/frontend/src/components/HeaderDefault.vue +++ b/frontend/src/components/HeaderDefault.vue @@ -24,6 +24,7 @@ onMounted(() => { /> REEV: Explanation and Evaluation of Variants + EXPERIMENTAL diff --git a/frontend/src/stores/__tests__/genesList.spec.ts b/frontend/src/stores/__tests__/genesList.spec.ts new file mode 100644 index 00000000..1ad9b610 --- /dev/null +++ b/frontend/src/stores/__tests__/genesList.spec.ts @@ -0,0 +1,78 @@ +import { beforeEach, describe, it, expect, vi } from 'vitest' +import createFetchMock from 'vitest-fetch-mock' + +import { setActivePinia, createPinia } from 'pinia' + +import { StoreState } from '../misc' +import { useGenesListStore } from '../genesList' + +const fetchMocker = createFetchMock(vi) + +describe('geneInfo Store', () => { + beforeEach(() => { + setActivePinia(createPinia()) + fetchMocker.enableMocks() + fetchMocker.resetMocks() + }) + + it('should have initial state', () => { + const store = useGenesListStore() + + expect(store.storeState).toBe(StoreState.Initial) + expect(store.query).toBe(null) + expect(store.genesList).toBe(null) + expect(store.redirectHgncId).toBe(null) + }) + + it('should clear state', () => { + const store = useGenesListStore() + store.storeState = StoreState.Active + store.query = 'q=BRCA1&fields=symbol' + store.genesList = JSON.parse(JSON.stringify({ gene: 'info' })) + + store.clearData() + + expect(store.storeState).toBe(StoreState.Initial) + expect(store.query).toBe(null) + expect(store.genesList).toBe(null) + }) + + // it('should load data', async () => { + // const store = useGenesListStore() + // fetchMocker.mockResponses(JSON.stringify({ result: { BRCA1: { gene: 'info' } } })) + + // await store.loadData('q=BRCA1&fields=symbol') + + // expect(store.storeState).toBe(StoreState.Active) + // expect(store.query).toBe('q=BRCA1&fields=symbol') + // expect(store.genesList).toEqual({ gene: 'info' }) + // }) + + // it('should fail to load data with invalid request', async () => { + // // Disable error logging + // vi.spyOn(console, 'error').mockImplementation(() => {}) + // const store = useGeneInfoStore() + // fetchMocker.mockResponseOnce(JSON.stringify({ foo: 'bar' }), { status: 400 }) + + // await store.loadData('invalid') + + // expect(store.storeState).toBe(StoreState.Error) + // expect(store.geneSymbol).toBe(null) + // expect(store.geneInfo).toBe(null) + // }) + + // it('should not load data if gene symbol is the same', async () => { + // const store = useGeneInfoStore() + // fetchMocker.mockResponse(JSON.stringify({ genes: { BRCA1: { gene: 'info' } } })) + + // await store.loadData('BRCA1') + + // expect(store.storeState).toBe(StoreState.Active) + // expect(store.geneSymbol).toBe('BRCA1') + // expect(store.geneInfo).toEqual({ gene: 'info' }) + + // await store.loadData('BRCA1') + + // expect(fetchMocker.mock.calls.length).toBe(1) + // }) +}) From 6f2710a5ce40e459bf073238741a95f205bee22c Mon Sep 17 00:00:00 2001 From: gromdimon Date: Wed, 6 Sep 2023 10:40:33 +0200 Subject: [PATCH 05/14] feat: frontend stores tests --- backend/tests/test_main.py | 1 - .../src/stores/__tests__/genesList.spec.ts | 89 ++++++++---- .../src/stores/__tests__/variantInfo.spec.ts | 128 ++++++++++++++++++ frontend/src/stores/variantInfo.ts | 9 +- frontend/src/views/VariantDetailView.vue | 3 +- 5 files changed, 193 insertions(+), 37 deletions(-) create mode 100644 frontend/src/stores/__tests__/variantInfo.spec.ts diff --git a/backend/tests/test_main.py b/backend/tests/test_main.py index 1639107f..744d4eb6 100644 --- a/backend/tests/test_main.py +++ b/backend/tests/test_main.py @@ -1,4 +1,3 @@ -import subprocess import typing import pytest diff --git a/frontend/src/stores/__tests__/genesList.spec.ts b/frontend/src/stores/__tests__/genesList.spec.ts index 1ad9b610..1b806561 100644 --- a/frontend/src/stores/__tests__/genesList.spec.ts +++ b/frontend/src/stores/__tests__/genesList.spec.ts @@ -8,6 +8,25 @@ import { useGenesListStore } from '../genesList' const fetchMocker = createFetchMock(vi) +const exampleGenesList = { + genes: [ + { + score: 0.75, + data: { + hgnc_id: 'HGNC:3333', + symbol: 'EMP1' + } + }, + { + score: 0.75, + data: { + hgnc_id: 'HGNC:3334', + symbol: 'EMP2' + } + } + ] +} + describe('geneInfo Store', () => { beforeEach(() => { setActivePinia(createPinia()) @@ -37,42 +56,56 @@ describe('geneInfo Store', () => { expect(store.genesList).toBe(null) }) - // it('should load data', async () => { - // const store = useGenesListStore() - // fetchMocker.mockResponses(JSON.stringify({ result: { BRCA1: { gene: 'info' } } })) + it('should load data', async () => { + const store = useGenesListStore() + fetchMocker.mockResponse(JSON.stringify(exampleGenesList)) - // await store.loadData('q=BRCA1&fields=symbol') + await store.loadData({ q: 'EMP', fields: 'hgnc_id,ensembl_gene_id,ncbi_gene_id,symbol' }) - // expect(store.storeState).toBe(StoreState.Active) - // expect(store.query).toBe('q=BRCA1&fields=symbol') - // expect(store.genesList).toEqual({ gene: 'info' }) - // }) + expect(store.storeState).toBe(StoreState.Active) + expect(store.query).toBe('q=EMP&fields=hgnc_id,ensembl_gene_id,ncbi_gene_id,symbol') + expect(store.genesList).toEqual(exampleGenesList.genes) + }) - // it('should fail to load data with invalid request', async () => { - // // Disable error logging - // vi.spyOn(console, 'error').mockImplementation(() => {}) - // const store = useGeneInfoStore() - // fetchMocker.mockResponseOnce(JSON.stringify({ foo: 'bar' }), { status: 400 }) + it('should fail to load data with invalid request', async () => { + // Disable error logging + vi.spyOn(console, 'error').mockImplementation(() => {}) + const store = useGenesListStore() + fetchMocker.mockResponseOnce(JSON.stringify({ foo: 'bar' }), { status: 400 }) - // await store.loadData('invalid') + await store.loadData('invalid') - // expect(store.storeState).toBe(StoreState.Error) - // expect(store.geneSymbol).toBe(null) - // expect(store.geneInfo).toBe(null) - // }) + expect(store.storeState).toBe(StoreState.Error) + expect(store.query).toBe(null) + expect(store.genesList).toBe(null) + }) - // it('should not load data if gene symbol is the same', async () => { - // const store = useGeneInfoStore() - // fetchMocker.mockResponse(JSON.stringify({ genes: { BRCA1: { gene: 'info' } } })) + it('should not load data if gene symbol is the same', async () => { + const store = useGenesListStore() + fetchMocker.mockResponse(JSON.stringify(exampleGenesList)) - // await store.loadData('BRCA1') + await store.loadData({ q: 'EMP', fields: 'hgnc_id,ensembl_gene_id,ncbi_gene_id,symbol' }) - // expect(store.storeState).toBe(StoreState.Active) - // expect(store.geneSymbol).toBe('BRCA1') - // expect(store.geneInfo).toEqual({ gene: 'info' }) + expect(store.storeState).toBe(StoreState.Active) + expect(store.query).toBe('q=EMP&fields=hgnc_id,ensembl_gene_id,ncbi_gene_id,symbol') + expect(store.genesList).toEqual(exampleGenesList.genes) - // await store.loadData('BRCA1') + await store.loadData({ q: 'EMP', fields: 'hgnc_id,ensembl_gene_id,ncbi_gene_id,symbol' }) - // expect(fetchMocker.mock.calls.length).toBe(1) - // }) + expect(fetchMocker.mock.calls.length).toBe(1) + }) + + it('should redirect if the searchTerm has match', async () => { + const store = useGenesListStore() + fetchMocker.mockResponse(JSON.stringify(exampleGenesList)) + + await store.loadData({ q: 'EMP1', fields: 'hgnc_id,ensembl_gene_id,ncbi_gene_id,symbol' }) + + expect(store.storeState).toBe(StoreState.Redirect) + expect(store.redirectHgncId).toBe('HGNC:3333') + expect(store.query).toBe('q=EMP1&fields=hgnc_id,ensembl_gene_id,ncbi_gene_id,symbol') + expect(store.genesList).toEqual(exampleGenesList.genes) + + expect(fetchMocker.mock.calls.length).toBe(1) + }) }) diff --git a/frontend/src/stores/__tests__/variantInfo.spec.ts b/frontend/src/stores/__tests__/variantInfo.spec.ts new file mode 100644 index 00000000..e0a6627d --- /dev/null +++ b/frontend/src/stores/__tests__/variantInfo.spec.ts @@ -0,0 +1,128 @@ +import { beforeEach, describe, it, expect, vi } from 'vitest' +import createFetchMock from 'vitest-fetch-mock' + +import { setActivePinia, createPinia } from 'pinia' + +import { StoreState } from '@/stores/misc' +import { useVariantInfoStore } from '../variantInfo' +import * as BRCA1GeneInfo from '@/assets/__tests__/BRCA1GeneInfo.json' +import * as BRCA1VariantInfo from '@/assets/__tests__/BRCA1VariantInfo.json' +import * as BRCA1TxInfo from '@/assets/__tests__/BRCA1TxInfo.json' + +const fetchMocker = createFetchMock(vi) + +const smallVariantInfo = { + release: 'grch37', + chromosome: 'chr17', + start: '43044295', + end: '43044295', + reference: 'G', + alternative: 'A', + hgnc_id: 'HGNC:1100' +} + +describe('geneInfo Store', () => { + beforeEach(() => { + setActivePinia(createPinia()) + fetchMocker.enableMocks() + fetchMocker.resetMocks() + }) + + it('should have initial state', () => { + const store = useVariantInfoStore() + + expect(store.storeState).toBe(StoreState.Initial) + expect(store.variantTerm).toBe(null) + expect(store.smallVariant).toBe(null) + expect(store.varAnnos).toBe(null) + expect(store.geneInfo).toBe(null) + expect(store.txCsq).toBe(null) + }) + + it('should clear state', () => { + const store = useVariantInfoStore() + store.storeState = StoreState.Active + store.variantTerm = 'chr1:12345:A:T' + store.smallVariant = JSON.parse(JSON.stringify(smallVariantInfo)) + store.varAnnos = JSON.parse(JSON.stringify(BRCA1VariantInfo)) + store.geneInfo = JSON.parse(JSON.stringify(BRCA1GeneInfo)) + store.txCsq = JSON.parse(JSON.stringify(BRCA1TxInfo)) + + store.clearData() + + expect(store.storeState).toBe(StoreState.Initial) + expect(store.variantTerm).toBe(null) + expect(store.smallVariant).toBe(null) + expect(store.varAnnos).toBe(null) + expect(store.geneInfo).toBe(null) + expect(store.txCsq).toBe(null) + }) + + it('should load data', async () => { + const store = useVariantInfoStore() + fetchMocker.mockResponse((req) => { + if (req.url.includes('annos/variant')) { + return Promise.resolve(JSON.stringify(BRCA1VariantInfo)) + } else if (req.url.includes('tx/csq')) { + return Promise.resolve(JSON.stringify(BRCA1TxInfo)) + } else if (req.url.includes('genes/info')) { + return Promise.resolve(JSON.stringify(BRCA1GeneInfo)) + } else { + return Promise.resolve(JSON.stringify({ status: 400 })) + } + }) + + await store.loadData('chr17:43044295:G:A', 'grch37') + + expect(store.storeState).toBe(StoreState.Active) + expect(store.variantTerm).toBe('chr17:43044295:G:A') + expect(store.smallVariant).toEqual(smallVariantInfo) + expect(store.varAnnos).toEqual(BRCA1VariantInfo.result) + expect(store.geneInfo).toEqual(BRCA1GeneInfo.genes['HGNC:1100']) + expect(store.txCsq).toEqual(BRCA1TxInfo.result) + }) + + it('should fail to load data with invalid request', async () => { + // Disable error logging + vi.spyOn(console, 'error').mockImplementation(() => {}) + const store = useVariantInfoStore() + fetchMocker.mockResponseOnce(JSON.stringify({ foo: 'bar' }), { status: 400 }) + + await store.loadData('invalid', 'grch37') + + expect(store.storeState).toBe(StoreState.Error) + expect(store.variantTerm).toBe(null) + expect(store.smallVariant).toBe(null) + expect(store.varAnnos).toBe(null) + expect(store.geneInfo).toBe(null) + expect(store.txCsq).toBe(null) + }) + + it('should not load data if variant is the same', async () => { + const store = useVariantInfoStore() + fetchMocker.mockResponse((req) => { + if (req.url.includes('annos/variant')) { + return Promise.resolve(JSON.stringify(BRCA1VariantInfo)) + } else if (req.url.includes('tx/csq')) { + return Promise.resolve(JSON.stringify(BRCA1TxInfo)) + } else if (req.url.includes('genes/info')) { + return Promise.resolve(JSON.stringify(BRCA1GeneInfo)) + } else { + return Promise.resolve(JSON.stringify({ status: 400 })) + } + }) + + await store.loadData('chr17:43044295:G:A', 'grch37') + + expect(store.storeState).toBe(StoreState.Active) + expect(store.variantTerm).toBe('chr17:43044295:G:A') + expect(store.smallVariant).toEqual(smallVariantInfo) + expect(store.varAnnos).toEqual(BRCA1VariantInfo.result) + expect(store.geneInfo).toEqual(BRCA1GeneInfo.genes['HGNC:1100']) + expect(store.txCsq).toEqual(BRCA1TxInfo.result) + + await store.loadData('chr17:43044295:G:A', 'grch37') + + expect(fetchMocker.mock.calls.length).toBe(3) + }) +}) diff --git a/frontend/src/stores/variantInfo.ts b/frontend/src/stores/variantInfo.ts index 4caafa7a..6640098b 100644 --- a/frontend/src/stores/variantInfo.ts +++ b/frontend/src/stores/variantInfo.ts @@ -9,13 +9,7 @@ import { ref } from 'vue' import { AnnonarsClient } from '@/api/annonars' import { MehariClient } from '@/api/mehari' import { infoFromQuery } from '@/api/utils' - -export enum StoreState { - Initial = 'initial', - Loading = 'loading', - Active = 'active', - Error = 'error' -} +import { StoreState } from '@/stores/misc' export const useVariantInfoStore = defineStore('variantInfo', () => { /** The current store state. */ @@ -39,6 +33,7 @@ export const useVariantInfoStore = defineStore('variantInfo', () => { function clearData() { storeState.value = StoreState.Initial variantTerm.value = null + smallVariant.value = null varAnnos.value = null txCsq.value = null geneInfo.value = null diff --git a/frontend/src/views/VariantDetailView.vue b/frontend/src/views/VariantDetailView.vue index e5ec2620..123ce583 100644 --- a/frontend/src/views/VariantDetailView.vue +++ b/frontend/src/views/VariantDetailView.vue @@ -2,7 +2,8 @@ import { watch, onMounted, ref } from 'vue' import { useRoute, useRouter } from 'vue-router' -import { StoreState, useVariantInfoStore } from '@/stores/variantInfo' +import { useVariantInfoStore } from '@/stores/variantInfo' +import { StoreState } from '@/stores/misc' import HeaderDetailPage from '@/components/HeaderDetailPage.vue' import VariantDetailsGene from '@/components/VariantDetails/VariantGene.vue' From dee8989f2526668591d334cd2365c051b67d722e Mon Sep 17 00:00:00 2001 From: gromdimon Date: Wed, 6 Sep 2023 12:23:28 +0200 Subject: [PATCH 06/14] feat: components tests --- frontend/src/components/HeaderDefault.vue | 13 ---- .../__tests__/FooterDefault.spec.ts | 60 ++++++++++++++++++ .../__tests__/HeaderDefault.spec.ts | 14 +---- .../__tests__/HeaderDetailPage.spec.ts | 63 +++++++++++++++---- frontend/src/views/__tests__/HomeView.spec.ts | 2 +- 5 files changed, 113 insertions(+), 39 deletions(-) create mode 100644 frontend/src/components/__tests__/FooterDefault.spec.ts diff --git a/frontend/src/components/HeaderDefault.vue b/frontend/src/components/HeaderDefault.vue index 864c1e41..6aca72b3 100644 --- a/frontend/src/components/HeaderDefault.vue +++ b/frontend/src/components/HeaderDefault.vue @@ -1,16 +1,3 @@ - -