Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: extending test coverage (#38) #40

Merged
merged 15 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pytest = "*"
pytest-asyncio = "*"
pytest-cov = "*"
pytest-httpx = "*"
pytest-subprocess = "*"
sphinx = "*"
sphinx-rtd-theme = "*"
starlette = "*"
Expand Down
370 changes: 189 additions & 181 deletions backend/Pipfile.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
45 changes: 45 additions & 0 deletions backend/tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,48 @@ 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, 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
assert response.text == "v0.0.0-16-g7a4205d-dirty"


@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"
54 changes: 51 additions & 3 deletions frontend/src/api/__tests__/annonars.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ 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)

describe('Annonars Client', () => {
describe.concurrent('Annonars Client', () => {
beforeEach(() => {
fetchMocker.enableMocks()
fetchMocker.resetMocks()
Expand All @@ -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 }))
Expand All @@ -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 }))
})
})
10 changes: 8 additions & 2 deletions frontend/src/api/__tests__/common.spec.ts
Original file line number Diff line number Diff line change
@@ -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.concurrent('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
})

describe('API_BASE_PREFIX constants', () => {
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')
Expand Down
43 changes: 43 additions & 0 deletions frontend/src/api/__tests__/mehari.spec.ts
Original file line number Diff line number Diff line change
@@ -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.concurrent('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 }))
})
})
2 changes: 1 addition & 1 deletion frontend/src/api/__tests__/misc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { MiscClient } from '../misc'

const fetchMocker = createFetchMock(vi)

describe('Misc Client', () => {
describe.concurrent('Misc Client', () => {
beforeEach(() => {
fetchMocker.enableMocks()
fetchMocker.resetMocks()
Expand Down
121 changes: 118 additions & 3 deletions frontend/src/api/__tests__/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { describe, it, expect } from 'vitest'

import { roundIt, search } from '../utils'
import {
roundIt,
separateIt,
isVariantMt,
isVariantMtHomopolymer,
search,
infoFromQuery
} from '../utils'

describe('roundIt method', () => {
describe.concurrent('roundIt method', () => {
it('should round a positive value with default digits', () => {
const result = roundIt(3.14159)
expect(result).toBe('<abbr title="3.14159">3.14</abbr>')
Expand Down Expand Up @@ -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',
Expand All @@ -46,6 +115,52 @@ describe('search method', () => {
})
})

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
})
})

it('should return null if no entry', () => {
const result = search('', 'foo37')
expect(result).toBe(null)
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/api/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -75,6 +75,8 @@ export const isVariantMtHomopolymer = (smallVar: any): any => {
}
if (isVariantMt(smallVar)) {
return positionCheck(start) || positionCheck(end)
} else {
return false
}
}

Expand Down Expand Up @@ -141,7 +143,6 @@ export const search = (searchTerm: string, genomeRelease: string) => {
return routeLocation
}
}
return null
}

/**
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/assets/__tests__/BRCA1TxInfo.json
Git LFS file not shown
3 changes: 3 additions & 0 deletions frontend/src/assets/__tests__/BRCA1VariantInfo.json
Git LFS file not shown
3 changes: 3 additions & 0 deletions frontend/src/assets/__tests__/BRCA1VariantValidator.json
Git LFS file not shown
3 changes: 3 additions & 0 deletions frontend/src/assets/__tests__/EMPSearchInfo.json
Git LFS file not shown
Loading