diff --git a/backend/alembic/versions/d10fec1c88fc_init_acmgseqvar.py b/backend/alembic/versions/d10fec1c88fc_init_acmgseqvar.py new file mode 100644 index 00000000..085d6af8 --- /dev/null +++ b/backend/alembic/versions/d10fec1c88fc_init_acmgseqvar.py @@ -0,0 +1,39 @@ +"""init acmgseqvar +Revision ID: d10fec1c88fc +Revises: 4f3b20f156c1 +Create Date: 2023-11-29 11:16:25.636296+01:00 + +""" +import fastapi_users_db_sqlalchemy.generics # noqa +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "d10fec1c88fc" +down_revision = "4f3b20f156c1" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "acmgseqvar", + sa.Column("id", fastapi_users_db_sqlalchemy.generics.GUID(), nullable=False), + sa.Column("user", sa.Uuid(), nullable=False), + sa.Column("seqvar_name", sa.String(length=255), nullable=False), + sa.Column("acmg_rank", sa.JSON(), nullable=True), + sa.ForeignKeyConstraint(["user"], ["user.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("user", "seqvar_name", name="uq_acmgseqvar"), + ) + op.create_index(op.f("ix_acmgseqvar_id"), "acmgseqvar", ["id"], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f("ix_acmgseqvar_id"), table_name="acmgseqvar") + op.drop_table("acmgseqvar") + # ### end Alembic commands ### diff --git a/backend/app/api/api_v1/api.py b/backend/app/api/api_v1/api.py index ff0ac97a..84d7339e 100644 --- a/backend/app/api/api_v1/api.py +++ b/backend/app/api/api_v1/api.py @@ -4,12 +4,13 @@ from httpx_oauth.clients.openid import OpenID from httpx_oauth.errors import GetIdEmailError -from app.api.api_v1.endpoints import adminmsgs, auth, bookmarks, caseinfo, utils +from app.api.api_v1.endpoints import acmgseqvar, adminmsgs, auth, bookmarks, caseinfo, utils from app.core.auth import auth_backend_bearer, auth_backend_cookie, fastapi_users from app.core.config import settings from app.schemas.user import UserRead, UserUpdate api_router = APIRouter() +api_router.include_router(acmgseqvar.router, prefix="/acmgseqvar", tags=["acmgseqvar"]) api_router.include_router(adminmsgs.router, prefix="/adminmsgs", tags=["adminmsgs"]) api_router.include_router(bookmarks.router, prefix="/bookmarks", tags=["bookmarks"]) api_router.include_router(utils.router, prefix="/utils", tags=["utils"]) diff --git a/backend/app/api/api_v1/endpoints/acmgseqvar.py b/backend/app/api/api_v1/endpoints/acmgseqvar.py new file mode 100644 index 00000000..c85e8d4d --- /dev/null +++ b/backend/app/api/api_v1/endpoints/acmgseqvar.py @@ -0,0 +1,173 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession + +from app import crud, schemas +from app.api import deps +from app.api.deps import current_active_superuser, current_active_user +from app.models.user import User + +router = APIRouter() + + +@router.post("/create", response_model=schemas.AcmgSeqVarCreate) +async def create_acmgseqvar( + acmgseqvar: schemas.AcmgSeqVarCreate, + db: AsyncSession = Depends(deps.get_db), + user: User = Depends(current_active_user), +): + """ + Create a new ACMG Sequence Variant. + + :param acmgseqvar: ACMG Sequence Variant to create + :type acmgseqvar: dict or :class:`.schemas.AcmgSeqVarCreate` + :return: ACMG Sequence Variant + :rtype: dict + """ + acmgseqvar.user = user.id + return await crud.acmgseqvar.create(db, obj_in=acmgseqvar) + + +@router.get( + "/list-all", + dependencies=[Depends(current_active_superuser)], + response_model=list[schemas.AcmgSeqVarRead], +) +async def list_acmgseqvars( + skip: int = 0, limit: int = 100, db: AsyncSession = Depends(deps.get_db) +): + """ + List all ACMG Sequence Variants. Available only for superusers. + + :param skip: number of ACMG Sequence Variants to skip + :type skip: int + :param limit: maximum number of ACMG Sequence Variants to return + :type limit: int + :return: list of ACMG Sequence Variants + :rtype: list + """ + return await crud.acmgseqvar.get_multi(db, skip=skip, limit=limit) + + +@router.get( + "/get-by-id", + dependencies=[Depends(current_active_superuser)], + response_model=schemas.AcmgSeqVarRead, +) +async def get_acmgseqvar(id: str, db: AsyncSession = Depends(deps.get_db)): + """ + Get a ACMG Sequence Variant by id. Available only for superusers. + + :param id: ACMG Sequence Variant id + :type id: uuid + :return: ACMG Sequence Variant + :rtype: dict + """ + response = await crud.acmgseqvar.get(db, id) + if not response: + raise HTTPException(status_code=404, detail="ACMG Sequence Variant not found") + else: + return response + + +@router.get("/list", response_model=list[schemas.AcmgSeqVarRead]) +async def list_acmgseqvars_by_user( + skip: int = 0, + limit: int = 100, + db: AsyncSession = Depends(deps.get_db), + user: User = Depends(current_active_user), +): + """ + List ACMG Sequence Variants by user. + + :return: list of ACMG Sequence Variants + :rtype: list + """ + return await crud.acmgseqvar.get_multi_by_user(db, user_id=user.id, skip=skip, limit=limit) + + +@router.get("/get", response_model=schemas.AcmgSeqVarRead) +async def get_acmgseqvar_by_user( + seqvar: str, + db: AsyncSession = Depends(deps.get_db), + user: User = Depends(current_active_user), +): + """ + Get a ACMG Sequence Variant by id. + + :param id: ACMG Sequence Variant id + :type id: uuid + :return: ACMG Sequence Variant + :rtype: dict + """ + response = await crud.acmgseqvar.get_by_user(db, user_id=user.id, seqvar_name=seqvar) + if not response: + raise HTTPException(status_code=404, detail="ACMG Sequence Variant not found") + else: + return response + + +@router.put("/update", response_model=schemas.AcmgSeqVarRead) +@router.patch("/update", response_model=schemas.AcmgSeqVarRead) +async def update_acmgseqvar( + acmgseqvar: schemas.AcmgSeqVarUpdate, + db: AsyncSession = Depends(deps.get_db), + user: User = Depends(current_active_user), +): + """ + Update a ACMG Sequence Variant. + + :param acmgseqvar: ACMG Sequence Variant to update + :type acmgseqvar: dict or :class:`.schemas.AcmgSeqVarUpdate` + :return: ACMG Sequence Variant + :rtype: dict + """ + acmgseqvar.user = user.id + response = await crud.acmgseqvar.get_by_user( + db, user_id=user.id, seqvar_name=acmgseqvar.seqvar_name + ) + if not response: + raise HTTPException(status_code=404, detail="ACMG Sequence Variant not found") + else: + return await crud.acmgseqvar.update(db, db_obj=response, obj_in=acmgseqvar) + + +@router.delete( + "/delete-by-id", + dependencies=[Depends(current_active_superuser)], + response_model=schemas.AcmgSeqVarRead, +) +async def delete_acmgseqvar(id: str, db: AsyncSession = Depends(deps.get_db)): + """ + Delete a ACMG Sequence Variant by id. Available only for superusers. + + :param id: ACMG Sequence Variant id + :type id: uuid + :return: ACMG Sequence Variant + :rtype: dict + """ + response = await crud.acmgseqvar.get(db, id) + if not response: + raise HTTPException(status_code=404, detail="ACMG Sequence Variant not found") + else: + return await crud.acmgseqvar.remove(db, id=id) + + +@router.delete("/delete", response_model=schemas.AcmgSeqVarRead) +async def delete_acmgseqvar_by_user( + seqvar: str, + db: AsyncSession = Depends(deps.get_db), + user: User = Depends(current_active_user), +): + """ + Delete a ACMG Sequence Variant by id. + + :param id: ACMG Sequence Variant id + :type id: uuid + :return: ACMG Sequence Variant + :rtype: dict + """ + response = await crud.acmgseqvar.get_by_user(db, user_id=user.id, seqvar_name=seqvar) + if not response: + raise HTTPException(status_code=404, detail="ACMG Sequence Variant not found") + else: + return await crud.acmgseqvar.remove(db, id=response.id) diff --git a/backend/app/api/api_v1/endpoints/caseinfo.py b/backend/app/api/api_v1/endpoints/caseinfo.py index e909b96e..c837a7c9 100644 --- a/backend/app/api/api_v1/endpoints/caseinfo.py +++ b/backend/app/api/api_v1/endpoints/caseinfo.py @@ -67,27 +67,6 @@ async def get_caseinfo(id: str, db: AsyncSession = Depends(deps.get_db)): return response -@router.delete( - "/delete-by-id", - dependencies=[Depends(current_active_superuser)], - response_model=schemas.CaseInfoRead, -) -async def delete_caseinfo(id: str, db: AsyncSession = Depends(deps.get_db)): - """ - Delete a Case Information by id. Available only for superusers. - - :param id: Case Information id - :type id: uuid - :return: Case Information - :rtype: dict - """ - response = await crud.caseinfo.remove(db, id=id) - if not response: - raise HTTPException(status_code=404, detail="Case Information not found") - else: - return response - - @router.get("/list", response_model=list[schemas.CaseInfoRead]) async def list_caseinfos_for_user( db: AsyncSession = Depends(deps.get_db), @@ -140,6 +119,27 @@ async def update_caseinfo_for_user( return await crud.caseinfo.update(db, db_obj=caseinfo, obj_in=caseinfoupdate) +@router.delete( + "/delete-by-id", + dependencies=[Depends(current_active_superuser)], + response_model=schemas.CaseInfoRead, +) +async def delete_caseinfo(id: str, db: AsyncSession = Depends(deps.get_db)): + """ + Delete a Case Information by id. Available only for superusers. + + :param id: Case Information id + :type id: uuid + :return: Case Information + :rtype: dict + """ + response = await crud.caseinfo.remove(db, id=id) + if not response: + raise HTTPException(status_code=404, detail="Case Information not found") + else: + return response + + @router.delete("/delete", response_model=schemas.CaseInfoRead) async def delete_caseinfo_for_user( db: AsyncSession = Depends(deps.get_db), user: User = Depends(current_active_user) diff --git a/backend/app/crud/__init__.py b/backend/app/crud/__init__.py index e7dfc7a8..506c6f2a 100644 --- a/backend/app/crud/__init__.py +++ b/backend/app/crud/__init__.py @@ -1,6 +1,8 @@ +from app.crud.acmgseqvar import CrudAcmgSeqVar from app.crud.base import CrudBase from app.crud.bookmarks import CrudBookmark from app.crud.caseinfo import CrudCaseInfo +from app.models.acmgseqvar import AcmgSeqVar from app.models.adminmsg import AdminMessage from app.models.bookmark import Bookmark from app.models.caseinfo import CaseInfo @@ -9,3 +11,4 @@ adminmessage = CrudBase[AdminMessage, AdminMessageCreate, AdminMessageUpdate](AdminMessage) bookmark = CrudBookmark(Bookmark) caseinfo = CrudCaseInfo(CaseInfo) +acmgseqvar = CrudAcmgSeqVar(AcmgSeqVar) diff --git a/backend/app/crud/acmgseqvar.py b/backend/app/crud/acmgseqvar.py new file mode 100644 index 00000000..f3647651 --- /dev/null +++ b/backend/app/crud/acmgseqvar.py @@ -0,0 +1,27 @@ +from typing import Any, Sequence + +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select + +from app.crud.base import CrudBase +from app.models.acmgseqvar import AcmgSeqVar +from app.schemas.acmgseqvar import AcmgSeqVarCreate, AcmgSeqVarUpdate + + +class CrudAcmgSeqVar(CrudBase[AcmgSeqVar, AcmgSeqVarCreate, AcmgSeqVarUpdate]): + async def get_multi_by_user( + self, session: AsyncSession, *, skip: int = 0, limit: int = 100, user_id: Any + ) -> Sequence[AcmgSeqVar]: + query = select(self.model).filter(self.model.user == user_id).offset(skip).limit(limit) + result = await session.execute(query) + all_scalars: Sequence[AcmgSeqVar] = result.scalars().all() # type: ignore[assignment] + return all_scalars + + async def get_by_user( + self, session: AsyncSession, *, user_id: Any, seqvar_name: Any + ) -> AcmgSeqVar | None: + query = select(self.model).filter( + self.model.user == user_id, self.model.seqvar_name == seqvar_name + ) + result = await session.execute(query) + return result.scalars().first() diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index a779ebe4..7302c49a 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -1,3 +1,4 @@ +from app.models.acmgseqvar import AcmgSeqVar # noqa from app.models.adminmsg import AdminMessage # noqa from app.models.bookmark import Bookmark # noqa from app.models.caseinfo import CaseInfo # noqa diff --git a/backend/app/models/acmgseqvar.py b/backend/app/models/acmgseqvar.py new file mode 100644 index 00000000..a715b803 --- /dev/null +++ b/backend/app/models/acmgseqvar.py @@ -0,0 +1,38 @@ +"""Models for ACMG sequence variant.""" + +import uuid as uuid_module +from typing import TYPE_CHECKING + +from fastapi_users_db_sqlalchemy.generics import GUID # noqa +from sqlalchemy import JSON, Column, ForeignKey, String, UniqueConstraint, Uuid +from sqlalchemy.orm import Mapped, mapped_column + +from app.db.session import Base +from app.schemas.acmgseqvar import AcmgRank + +UUID_ID = uuid_module.UUID + + +class AcmgSeqVar(Base): + """ACMG sequence variant.""" + + __tablename__ = "acmgseqvar" + + __table_args__ = (UniqueConstraint("user", "seqvar_name", name="uq_acmgseqvar"),) + + if TYPE_CHECKING: # pragma: no cover + id: UUID_ID + user: UUID_ID + seqvar_name: str + acmg_rank: AcmgRank + else: + #: UUID of the ACMG sequence variant. + id: Mapped[UUID_ID] = mapped_column( + GUID, primary_key=True, index=True, default=uuid_module.uuid4 + ) + #: User who created the ACMG sequence variant. + user = Column(Uuid, ForeignKey("user.id", ondelete="CASCADE"), nullable=False) + #: Sequence variant ID. + seqvar_name = Column(String(255), nullable=False) + #: ACMG criteria. + acmg_rank = Column(JSON, nullable=True) diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py index 1fe27737..bce0f66e 100644 --- a/backend/app/schemas/__init__.py +++ b/backend/app/schemas/__init__.py @@ -1,3 +1,4 @@ +from app.schemas.acmgseqvar import AcmgSeqVarCreate, AcmgSeqVarRead, AcmgSeqVarUpdate # noqa from app.schemas.adminmsg import AdminMessageCreate, AdminMessageRead, AdminMessageUpdate # noqa from app.schemas.auth import OAuth2ProviderConfig, OAuth2ProviderPublic # noqa from app.schemas.bookmark import BookmarkCreate, BookmarkRead, BookmarkUpdate # noqa diff --git a/backend/app/schemas/acmgseqvar.py b/backend/app/schemas/acmgseqvar.py new file mode 100644 index 00000000..931f1e84 --- /dev/null +++ b/backend/app/schemas/acmgseqvar.py @@ -0,0 +1,91 @@ +from enum import Enum +from uuid import UUID + +from pydantic import BaseModel, ConfigDict + + +class Presence(str, Enum): + Present = "Present" + Absent = "Absent" + Unknown = "Unknown" + + +class Evidence(str, Enum): + PathogenicVeryStrong = "Pathogenic Very Strong" + PathogenicStrong = "Pathogenic Strong" + PathogenicModerate = "Pathogenic Moderate" + PathogenicSupporting = "Pathogenic Supporting" + BenignStandalone = "Benign Standalone" + BenignStrong = "Benign Strong" + BenignSupporting = "Benign Supporting" + NotSet = "Not Set" + + +class Criteria(str, Enum): + Pvs1 = "Pvs1" + Ps1 = "Ps1" + Ps2 = "Ps2" + Ps3 = "Ps3" + Ps4 = "Ps4" + Pm1 = "Pm1" + Pm2 = "Pm2" + Pm3 = "Pm3" + Pm4 = "Pm4" + Pm5 = "Pm5" + Pm6 = "Pm6" + Pp1 = "Pp1" + Pp2 = "Pp2" + Pp3 = "Pp3" + Pp4 = "Pp4" + Pp5 = "Pp5" + Ba1 = "Ba1" + Bs1 = "Bs1" + Bs2 = "Bs2" + Bs3 = "Bs3" + Bs4 = "Bs4" + Bp1 = "Bp1" + Bp2 = "Bp2" + Bp3 = "Bp3" + Bp4 = "Bp4" + Bp5 = "Bp5" + Bp6 = "Bp6" + Bp7 = "Bp7" + + +class SeqVarCriteria(BaseModel): + criteria: Criteria + presence: Presence + evidence: Evidence + + +class AcmgRank(BaseModel): + criterias: list[SeqVarCriteria] + comment: str + + +class AcmgSeqVar(BaseModel): + user: UUID | None = None + seqvar_name: str | None = None + acmg_rank: AcmgRank | None = None + + +class AcmgSeqVarCreate(AcmgSeqVar): + pass + + +class AcmgSeqVarUpdate(AcmgSeqVar): + pass + + +class AcmgSeqVarInDbBase(AcmgSeqVar): + model_config = ConfigDict(from_attributes=True) + + id: UUID + + +class AcmgSeqVarRead(AcmgSeqVarInDbBase): + pass + + +class AcmgSeqVarInDb(AcmgSeqVarInDbBase): + pass diff --git a/backend/tests/api/api_v1/test_acmgseqvar.py b/backend/tests/api/api_v1/test_acmgseqvar.py new file mode 100644 index 00000000..26480e0e --- /dev/null +++ b/backend/tests/api/api_v1/test_acmgseqvar.py @@ -0,0 +1,822 @@ +import uuid +from typing import Any + +import pytest +from fastapi.testclient import TestClient +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.config import settings +from app.models.user import User + +# ------------------------------------------------------------------------------ +# # /api/v1/acmgseqvar/create +# ------------------------------------------------------------------------------ + + +@pytest.fixture +def acmgseqvar_post_data() -> dict[str, Any]: + return { + "seqvar_name": "chr0:123:A:C", + "acmg_rank": { + "comment": "No comment", + "criterias": [ + { + "criteria": "Pm4", + "presence": "Absent", + "evidence": "Pathogenic Moderate", + } + ], + }, + } + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(False, False)], indirect=True) +async def test_create_acmgseqvar( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, + acmgseqvar_post_data: dict[str, Any], +): + """Test creating a acmgseqvar.""" + _ = db_session + response = client_user.post( + f"{settings.API_V1_STR}/acmgseqvar/create", + json=acmgseqvar_post_data, + ) + content = response.json() + assert response.status_code == 200 + assert content["seqvar_name"] == acmgseqvar_post_data["seqvar_name"] + assert content["acmg_rank"] == acmgseqvar_post_data["acmg_rank"] + assert content["user"] == str(test_user.id) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(True, True)], indirect=True) +async def test_create_acmgseqvar_superuser( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, + acmgseqvar_post_data: dict[str, Any], +): + """Test creating a acmgseqvar as superuser.""" + _ = db_session + response = client_user.post( + f"{settings.API_V1_STR}/acmgseqvar/create", + json=acmgseqvar_post_data, + ) + content = response.json() + assert response.status_code == 200 + assert content["seqvar_name"] == acmgseqvar_post_data["seqvar_name"] + assert content["acmg_rank"] == acmgseqvar_post_data["acmg_rank"] + assert content["user"] == str(test_user.id) + + +@pytest.mark.asyncio +async def test_create_acmgseqvar_anon( + db_session: AsyncSession, client: TestClient, acmgseqvar_post_data: dict[str, Any] +): + """Test creating a acmgseqvar as anonymous user.""" + _ = db_session + response = client.post( + f"{settings.API_V1_STR}/acmgseqvar/create", + json=acmgseqvar_post_data, + ) + assert response.status_code == 401 + assert response.json() == {"detail": "Unauthorized"} + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(True, True)], indirect=True) +async def test_create_acmgseqvar_invalid_data( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, +): + """Test creating a acmgseqvar with invalid data.""" + _ = db_session + response = client_user.post( + f"{settings.API_V1_STR}/acmgseqvar/create", + json={"seqvar_name": "chr0:123:A:C", "acmg_rank": {"comment": "No comment"}}, + ) + assert response.status_code == 422 + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(True, True)], indirect=True) +async def test_create_acmgseqvar_invalid_enums( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, + acmgseqvar_post_data: dict[str, Any], +): + """Test creating a acmgseqvar with invalid enums.""" + _ = db_session + post_data = acmgseqvar_post_data.copy() + post_data["acmg_rank"]["criterias"][0]["criteria"] = "Pppm4" + response = client_user.post( + f"{settings.API_V1_STR}/acmgseqvar/create", + json=post_data, + ) + assert response.status_code == 422 + + +# ------------------------------------------------------------------------------ +# /api/v1/acmgseqvar/list-all +# ------------------------------------------------------------------------------ + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(False, False)], indirect=True) +async def test_list_all_acmgseqvars( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, + acmgseqvar_post_data: dict[str, Any], +): + """Test listing all acmgseqvars.""" + _ = db_session + # Create acmgseqvar + response = client_user.post( + f"{settings.API_V1_STR}/acmgseqvar/create", + json=acmgseqvar_post_data, + ) + assert response.status_code == 200 + + response = client_user.get( + f"{settings.API_V1_STR}/acmgseqvar/list-all", + ) + content = response.json() + assert response.status_code == 401 + assert content == {"detail": "Unauthorized"} + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(True, True)], indirect=True) +async def test_list_all_acmgseqvars_superuser( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, + acmgseqvar_post_data: dict[str, Any], +): + """Test listing all acmgseqvars as superuser.""" + _ = db_session + # Create acmgseqvar + response = client_user.post( + f"{settings.API_V1_STR}/acmgseqvar/create", + json=acmgseqvar_post_data, + ) + assert response.status_code == 200 + + response = client_user.get( + f"{settings.API_V1_STR}/acmgseqvar/list-all", + ) + content = response.json() + assert response.status_code == 200 + assert content[0]["seqvar_name"] == acmgseqvar_post_data["seqvar_name"] + assert content[0]["acmg_rank"] == acmgseqvar_post_data["acmg_rank"] + assert content[0]["user"] == str(test_user.id) + + +@pytest.mark.asyncio +async def test_list_all_acmgseqvars_anon(db_session: AsyncSession, client: TestClient): + """Test listing all acmgseqvars as anonymous user.""" + _ = db_session + response = client.get( + f"{settings.API_V1_STR}/acmgseqvar/list-all", + ) + assert response.status_code == 401 + assert response.json() == {"detail": "Unauthorized"} + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(True, True)], indirect=True) +async def test_list_all_no_acmgseqvars( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, +): + """Test listing all acmgseqvars as superuser.""" + _ = db_session + response = client_user.get( + f"{settings.API_V1_STR}/acmgseqvar/list-all", + ) + content = response.json() + assert response.status_code == 200 + assert content == [] + + +# ------------------------------------------------------------------------------ +# /api/v1/acmgseqvar/get-by-id +# ------------------------------------------------------------------------------ + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(False, False)], indirect=True) +async def test_get_by_id_acmgseqvar( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, + acmgseqvar_post_data: dict[str, Any], +): + """Test getting a acmgseqvar.""" + _ = db_session + # Create acmgseqvar + response = client_user.post( + f"{settings.API_V1_STR}/acmgseqvar/create", + json=acmgseqvar_post_data, + ) + assert response.status_code == 200 + # Get the acmgseqvar id + response = client_user.get(f"{settings.API_V1_STR}/acmgseqvar/list") + acmgseqvar_id = response.json()[0]["id"] + + response = client_user.get( + f"{settings.API_V1_STR}/acmgseqvar/get-by-id?id={acmgseqvar_id}", + ) + content = response.json() + assert response.status_code == 401 + assert content == {"detail": "Unauthorized"} + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(True, True)], indirect=True) +async def test_get_by_id_acmgseqvar_superuser( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, + acmgseqvar_post_data: dict[str, Any], +): + """Test getting a acmgseqvar as superuser.""" + _ = db_session + # Create acmgseqvar + response = client_user.post( + f"{settings.API_V1_STR}/acmgseqvar/create", + json=acmgseqvar_post_data, + ) + assert response.status_code == 200 + # Get the acmgseqvar id + response = client_user.get(f"{settings.API_V1_STR}/acmgseqvar/list") + acmgseqvar_id = response.json()[0]["id"] + + response = client_user.get( + f"{settings.API_V1_STR}/acmgseqvar/get-by-id?id={acmgseqvar_id}", + ) + content = response.json() + assert response.status_code == 200 + assert content["seqvar_name"] == acmgseqvar_post_data["seqvar_name"] + assert content["acmg_rank"] == acmgseqvar_post_data["acmg_rank"] + assert content["user"] == str(test_user.id) + + +@pytest.mark.asyncio +async def test_get_by_id_acmgseqvar_anon(db_session: AsyncSession, client: TestClient): + """Test getting a acmgseqvar as anonymous user.""" + _ = db_session + response = client.get( + f"{settings.API_V1_STR}/acmgseqvar/get-by-id?id={uuid.uuid4()}", + ) + assert response.status_code == 401 + assert response.json() == {"detail": "Unauthorized"} + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(True, True)], indirect=True) +async def test_get_acmgseqvar_by_invalid_id( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, +): + """Test getting a acmgseqvar with invalid id.""" + caseinfo_id = uuid.uuid4() # Invalid id + response = client_user.get( + f"{settings.API_V1_STR}/acmgseqvar/get-by-id?id={caseinfo_id}", + ) + assert response.status_code == 404 + assert response.json() == {"detail": "ACMG Sequence Variant not found"} + + +# ------------------------------------------------------------------------------ +# /api/v1/acmgseqvar/list +# ------------------------------------------------------------------------------ + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(False, False)], indirect=True) +async def test_list_acmgseqvars( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, + acmgseqvar_post_data: dict[str, Any], +): + """Test listing acmgseqvars.""" + _ = db_session + # Create acmgseqvar + response = client_user.post( + f"{settings.API_V1_STR}/acmgseqvar/create", + json=acmgseqvar_post_data, + ) + assert response.status_code == 200 + + response = client_user.get( + f"{settings.API_V1_STR}/acmgseqvar/list", + ) + content = response.json() + assert response.status_code == 200 + assert content[0]["seqvar_name"] == acmgseqvar_post_data["seqvar_name"] + assert content[0]["acmg_rank"] == acmgseqvar_post_data["acmg_rank"] + assert content[0]["user"] == str(test_user.id) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(True, True)], indirect=True) +async def test_list_acmgseqvars_superuser( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, + acmgseqvar_post_data: dict[str, Any], +): + """Test listing acmgseqvars as superuser.""" + _ = db_session + # Create acmgseqvar + response = client_user.post( + f"{settings.API_V1_STR}/acmgseqvar/create", + json=acmgseqvar_post_data, + ) + assert response.status_code == 200 + + response = client_user.get( + f"{settings.API_V1_STR}/acmgseqvar/list", + ) + content = response.json() + assert response.status_code == 200 + assert content[0]["seqvar_name"] == acmgseqvar_post_data["seqvar_name"] + assert content[0]["acmg_rank"] == acmgseqvar_post_data["acmg_rank"] + assert content[0]["user"] == str(test_user.id) + + +@pytest.mark.asyncio +async def test_list_acmgseqvars_anon(db_session: AsyncSession, client: TestClient): + """Test listing acmgseqvars as anonymous user.""" + _ = db_session + response = client.get( + f"{settings.API_V1_STR}/acmgseqvar/list", + ) + assert response.status_code == 401 + assert response.json() == {"detail": "Unauthorized"} + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(True, True)], indirect=True) +async def test_list_no_acmgseqvars( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, +): + """Test listing acmgseqvars with no records.""" + _ = db_session + response = client_user.get( + f"{settings.API_V1_STR}/acmgseqvar/list", + ) + content = response.json() + assert response.status_code == 200 + assert content == [] + + +# ------------------------------------------------------------------------------ +# /api/v1/acmgseqvar/get +# ------------------------------------------------------------------------------ + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(False, False)], indirect=True) +async def test_get_acmgseqvar( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, + acmgseqvar_post_data: dict[str, Any], +): + """Test getting a acmgseqvar.""" + _ = db_session + # Create acmgseqvar + response = client_user.post( + f"{settings.API_V1_STR}/acmgseqvar/create", + json=acmgseqvar_post_data, + ) + assert response.status_code == 200 + + response = client_user.get( + f"{settings.API_V1_STR}/acmgseqvar/get?seqvar={acmgseqvar_post_data['seqvar_name']}", + ) + content = response.json() + assert response.status_code == 200 + assert content["seqvar_name"] == acmgseqvar_post_data["seqvar_name"] + assert content["acmg_rank"] == acmgseqvar_post_data["acmg_rank"] + assert content["user"] == str(test_user.id) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(True, True)], indirect=True) +async def test_get_acmgseqvar_superuser( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, + acmgseqvar_post_data: dict[str, Any], +): + """Test getting a acmgseqvar as superuser.""" + _ = db_session + # Create acmgseqvar + response = client_user.post( + f"{settings.API_V1_STR}/acmgseqvar/create", + json=acmgseqvar_post_data, + ) + assert response.status_code == 200 + + response = client_user.get( + f"{settings.API_V1_STR}/acmgseqvar/get?seqvar={acmgseqvar_post_data['seqvar_name']}", + ) + content = response.json() + assert response.status_code == 200 + assert content["seqvar_name"] == acmgseqvar_post_data["seqvar_name"] + assert content["acmg_rank"] == acmgseqvar_post_data["acmg_rank"] + assert content["user"] == str(test_user.id) + + +@pytest.mark.asyncio +async def test_get_acmgseqvar_anon(db_session: AsyncSession, client: TestClient): + """Test getting a acmgseqvar as anonymous user.""" + _ = db_session + response = client.get( + f"{settings.API_V1_STR}/acmgseqvar/get?seqvar={uuid.uuid4()}", + ) + assert response.status_code == 401 + assert response.json() == {"detail": "Unauthorized"} + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(True, True)], indirect=True) +async def test_get_no_acmgseqvar( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, +): + """Test getting a acmgseqvar with no acmgseqvar.""" + _ = db_session + response = client_user.get( + f"{settings.API_V1_STR}/acmgseqvar/get?seqvar=invalid", + ) + assert response.status_code == 404 + assert response.json() == {"detail": "ACMG Sequence Variant not found"} + + +# ------------------------------------------------------------------------------ +# /api/v1/acmgseqvar/update +# ------------------------------------------------------------------------------ + + +@pytest.fixture +def acmgseqvar_update_data() -> dict[str, Any]: + return { + "seqvar_name": "chr0:123:A:C", + "acmg_rank": { + "comment": "Update", + "criterias": [ + { + "criteria": "Pm4", + "presence": "Present", + "evidence": "Pathogenic Moderate", + } + ], + }, + } + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(False, False)], indirect=True) +async def test_update_acmgseqvar( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, + acmgseqvar_post_data: dict[str, Any], + acmgseqvar_update_data: dict[str, Any], +): + """Test updating a acmgseqvar.""" + _ = db_session + # Create acmgseqvar + response = client_user.post( + f"{settings.API_V1_STR}/acmgseqvar/create", + json=acmgseqvar_post_data, + ) + assert response.status_code == 200 + # Update acmgseqvar + response = client_user.put( + f"{settings.API_V1_STR}/acmgseqvar/update", + json=acmgseqvar_update_data, + ) + content = response.json() + assert response.status_code == 200 + assert content["seqvar_name"] == acmgseqvar_update_data["seqvar_name"] + assert content["acmg_rank"] == acmgseqvar_update_data["acmg_rank"] + assert content["user"] == str(test_user.id) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(True, True)], indirect=True) +async def test_update_acmgseqvar_superuser( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, + acmgseqvar_post_data: dict[str, Any], + acmgseqvar_update_data: dict[str, Any], +): + """Test updating a acmgseqvar as superuser.""" + _ = db_session + # Create acmgseqvar + response = client_user.post( + f"{settings.API_V1_STR}/acmgseqvar/create", + json=acmgseqvar_post_data, + ) + assert response.status_code == 200 + # Update acmgseqvar + response = client_user.put( + f"{settings.API_V1_STR}/acmgseqvar/update", + json=acmgseqvar_update_data, + ) + content = response.json() + assert response.status_code == 200 + assert content["seqvar_name"] == acmgseqvar_update_data["seqvar_name"] + assert content["acmg_rank"] == acmgseqvar_update_data["acmg_rank"] + assert content["user"] == str(test_user.id) + + +@pytest.mark.asyncio +async def test_update_acmgseqvar_anon( + db_session: AsyncSession, client: TestClient, acmgseqvar_update_data: dict[str, Any] +): + """Test updating a acmgseqvar as anonymous user.""" + _ = db_session + response = client.put( + f"{settings.API_V1_STR}/acmgseqvar/update", + json=acmgseqvar_update_data, + ) + assert response.status_code == 401 + assert response.json() == {"detail": "Unauthorized"} + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(True, True)], indirect=True) +async def test_update_acmgseqvar_patch( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, + acmgseqvar_post_data: dict[str, Any], + acmgseqvar_update_data: dict[str, Any], +): + """Test updating a acmgseqvar with invalid data.""" + _ = db_session + # Create acmgseqvar + response = client_user.post( + f"{settings.API_V1_STR}/acmgseqvar/create", + json=acmgseqvar_post_data, + ) + assert response.status_code == 200 + # Update acmgseqvar + response = client_user.patch( + f"{settings.API_V1_STR}/acmgseqvar/update", + json=acmgseqvar_update_data, + ) + content = response.json() + assert response.status_code == 200 + assert content["seqvar_name"] == acmgseqvar_update_data["seqvar_name"] + assert content["acmg_rank"] == acmgseqvar_update_data["acmg_rank"] + assert content["user"] == str(test_user.id) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(True, True)], indirect=True) +async def test_update_acmgseqvar_no_acmgseqvar( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, + acmgseqvar_update_data: dict[str, Any], +): + """Test updating a acmgseqvar with invalid data.""" + _ = db_session + response = client_user.put( + f"{settings.API_V1_STR}/acmgseqvar/update", + json=acmgseqvar_update_data, + ) + assert response.status_code == 404 + assert response.json() == {"detail": "ACMG Sequence Variant not found"} + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(True, True)], indirect=True) +async def test_update_acmgseqvar_invalid_enum( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, + acmgseqvar_post_data: dict[str, Any], + acmgseqvar_update_data: dict[str, Any], +): + """Test updating a acmgseqvar with invalid enums.""" + _ = db_session + # Create acmgseqvar + response = client_user.post( + f"{settings.API_V1_STR}/acmgseqvar/create", + json=acmgseqvar_post_data, + ) + assert response.status_code == 200 + # Update acmgseqvar + post_data = acmgseqvar_update_data.copy() + post_data["acmg_rank"]["criterias"][0]["criteria"] = "Pppm4" + response = client_user.put( + f"{settings.API_V1_STR}/acmgseqvar/update", + json=post_data, + ) + assert response.status_code == 422 + + +# ------------------------------------------------------------------------------ +# /api/v1/acmgseqvar/delete-by-id +# ------------------------------------------------------------------------------ + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(False, False)], indirect=True) +async def test_delete_acmgseqvar_by_id( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, + acmgseqvar_post_data: dict[str, Any], +): + """Test deleting a acmgseqvar by id.""" + _ = db_session + # Create acmgseqvar + response = client_user.post( + f"{settings.API_V1_STR}/acmgseqvar/create", + json=acmgseqvar_post_data, + ) + + # Get the acmgseqvar id + response = client_user.get(f"{settings.API_V1_STR}/acmgseqvar/list") + acmgseqvar_id = response.json()[0]["id"] + + response = client_user.delete( + f"{settings.API_V1_STR}/acmgseqvar/delete-by-id?id={acmgseqvar_id}", + ) + assert response.status_code == 401 + assert response.json() == {"detail": "Unauthorized"} + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(True, True)], indirect=True) +async def test_delete_acmgseqvar_by_id_superuser( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, + acmgseqvar_post_data: dict[str, Any], +): + """Test deleting a acmgseqvar by id as superuser.""" + _ = db_session + # Create acmgseqvar + response = client_user.post( + f"{settings.API_V1_STR}/acmgseqvar/create", + json=acmgseqvar_post_data, + ) + + # Get the acmgseqvar id + response = client_user.get(f"{settings.API_V1_STR}/acmgseqvar/list") + acmgseqvar_id = response.json()[0]["id"] + + response = client_user.delete( + f"{settings.API_V1_STR}/acmgseqvar/delete-by-id?id={acmgseqvar_id}", + ) + assert response.status_code == 200 + content = response.json() + assert content["seqvar_name"] == acmgseqvar_post_data["seqvar_name"] + assert content["acmg_rank"] == acmgseqvar_post_data["acmg_rank"] + assert content["user"] == str(test_user.id) + + +@pytest.mark.asyncio +async def test_delete_acmgseqvar_by_id_anon(db_session: AsyncSession, client: TestClient): + """Test deleting a acmgseqvar by id as anonymous user.""" + _ = db_session + response = client.delete( + f"{settings.API_V1_STR}/acmgseqvar/delete-by-id?id={uuid.uuid4()}", + ) + assert response.status_code == 401 + assert response.json() == {"detail": "Unauthorized"} + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(True, True)], indirect=True) +async def test_delete_acmgseqvar_by_invalid_id( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, +): + """Test deleting a acmgseqvar by id with invalid id.""" + _ = db_session + response = client_user.delete( + f"{settings.API_V1_STR}/acmgseqvar/delete-by-id?id={uuid.uuid4()}", + ) + assert response.status_code == 404 + assert response.json() == {"detail": "ACMG Sequence Variant not found"} + + +# ------------------------------------------------------------------------------ +# /api/v1/acmgseqvar/delete +# ------------------------------------------------------------------------------ + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(False, False)], indirect=True) +async def test_delete_acmgseqvar( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, + acmgseqvar_post_data: dict[str, Any], +): + """Test deleting a acmgseqvar.""" + _ = db_session + # Create acmgseqvar + response = client_user.post( + f"{settings.API_V1_STR}/acmgseqvar/create", + json=acmgseqvar_post_data, + ) + + response = client_user.delete( + f"{settings.API_V1_STR}/acmgseqvar/delete?seqvar={acmgseqvar_post_data['seqvar_name']}", + ) + assert response.status_code == 200 + content = response.json() + assert content["seqvar_name"] == acmgseqvar_post_data["seqvar_name"] + assert content["acmg_rank"] == acmgseqvar_post_data["acmg_rank"] + assert content["user"] == str(test_user.id) + + # Verify that the acmgseqvar is deleted + response = client_user.get( + f"{settings.API_V1_STR}/acmgseqvar/get?seqvar={acmgseqvar_post_data['seqvar_name']}", + ) + assert response.status_code == 404 + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(True, True)], indirect=True) +async def test_delete_acmgseqvar_superuser( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, + acmgseqvar_post_data: dict[str, Any], +): + """Test deleting a acmgseqvar as superuser.""" + _ = db_session + # Create acmgseqvar + response = client_user.post( + f"{settings.API_V1_STR}/acmgseqvar/create", + json=acmgseqvar_post_data, + ) + + response = client_user.delete( + f"{settings.API_V1_STR}/acmgseqvar/delete?seqvar={acmgseqvar_post_data['seqvar_name']}", + ) + assert response.status_code == 200 + content = response.json() + assert content["seqvar_name"] == acmgseqvar_post_data["seqvar_name"] + assert content["acmg_rank"] == acmgseqvar_post_data["acmg_rank"] + assert content["user"] == str(test_user.id) + + # Verify that the acmgseqvar is deleted + response = client_user.get( + f"{settings.API_V1_STR}/acmgseqvar/get?seqvar={acmgseqvar_post_data['seqvar_name']}", + ) + assert response.status_code == 404 + + +@pytest.mark.asyncio +async def test_delete_acmgseqvar_anon(db_session: AsyncSession, client: TestClient): + """Test deleting a acmgseqvar as anonymous user.""" + _ = db_session + response = client.delete( + f"{settings.API_V1_STR}/acmgseqvar/delete?seqvar={uuid.uuid4()}", + ) + assert response.status_code == 401 + assert response.json() == {"detail": "Unauthorized"} + + +@pytest.mark.asyncio +@pytest.mark.parametrize("test_user, client_user", [(True, True)], indirect=True) +async def test_delete_acmgseqvar_no_acmgseqvar( + db_session: AsyncSession, + client_user: TestClient, + test_user: User, +): + """Test deleting a acmgseqvar with invalid data.""" + _ = db_session + response = client_user.delete( + f"{settings.API_V1_STR}/acmgseqvar/delete?seqvar=invalid", + ) + assert response.status_code == 404 + assert response.json() == {"detail": "ACMG Sequence Variant not found"} diff --git a/backend/tests/crud/test_acmgseqvar.py b/backend/tests/crud/test_acmgseqvar.py new file mode 100644 index 00000000..375b6edc --- /dev/null +++ b/backend/tests/crud/test_acmgseqvar.py @@ -0,0 +1,91 @@ +import uuid + +import pytest +from sqlalchemy.ext.asyncio import AsyncSession + +from app import crud +from app.schemas.acmgseqvar import ( + AcmgRank, + AcmgSeqVarCreate, + Criteria, + Evidence, + Presence, + SeqVarCriteria, +) + + +@pytest.fixture +def acmgseqvar_create() -> AcmgSeqVarCreate: + """Create a AcmgSeqVarCreate object.""" + pm4 = SeqVarCriteria( + criteria=Criteria.Pm4, + presence=Presence.Absent, + evidence=Evidence.PathogenicModerate, + ) + rank = AcmgRank( + comment="No comment", + criterias=[pm4], + ) + return AcmgSeqVarCreate( + user=uuid.uuid4(), + seqvar_name="chr0:123:A:C", + acmg_rank=rank, + ) + + +@pytest.mark.asyncio +async def test_create_get_acmgseqvar(db_session: AsyncSession, acmgseqvar_create: AcmgSeqVarCreate): + """Test creating and retrieving a acmgseqvar.""" + acmgseqvar_postcreate = await crud.acmgseqvar.create( + session=db_session, obj_in=acmgseqvar_create + ) + stored_item = await crud.acmgseqvar.get( + session=db_session, + id=acmgseqvar_postcreate.id, + ) + assert stored_item + assert acmgseqvar_postcreate.user == stored_item.user + assert acmgseqvar_postcreate.seqvar_name == stored_item.seqvar_name + assert acmgseqvar_postcreate.acmg_rank == stored_item.acmg_rank + + +@pytest.mark.asyncio +async def test_delete_acmgseqvar(db_session: AsyncSession, acmgseqvar_create: AcmgSeqVarCreate): + """Test deleting a acmgseqvar.""" + acmgseqvar_postcreate = await crud.acmgseqvar.create( + session=db_session, obj_in=acmgseqvar_create + ) + await crud.acmgseqvar.remove(session=db_session, id=acmgseqvar_postcreate.id) + + +@pytest.mark.asyncio +async def test_get_multi_by_user(db_session: AsyncSession, acmgseqvar_create: AcmgSeqVarCreate): + """Test get_multi_by_user.""" + acmgseqvar_postcreate = await crud.acmgseqvar.create( + session=db_session, obj_in=acmgseqvar_create + ) + stored_item = await crud.acmgseqvar.get_multi_by_user( + session=db_session, + user_id=acmgseqvar_postcreate.user, + ) + assert stored_item + assert acmgseqvar_postcreate.user == stored_item[0].user + assert acmgseqvar_postcreate.seqvar_name == stored_item[0].seqvar_name + assert acmgseqvar_postcreate.acmg_rank == stored_item[0].acmg_rank + + +@pytest.mark.asyncio +async def test_get_by_user(db_session: AsyncSession, acmgseqvar_create: AcmgSeqVarCreate): + """Test get_by_user.""" + acmgseqvar_postcreate = await crud.acmgseqvar.create( + session=db_session, obj_in=acmgseqvar_create + ) + stored_item = await crud.acmgseqvar.get_by_user( + session=db_session, + user_id=acmgseqvar_postcreate.user, + seqvar_name=acmgseqvar_postcreate.seqvar_name, + ) + assert stored_item + assert acmgseqvar_postcreate.user == stored_item.user + assert acmgseqvar_postcreate.seqvar_name == stored_item.seqvar_name + assert acmgseqvar_postcreate.acmg_rank == stored_item.acmg_rank diff --git a/frontend/src/api/__tests__/acmgseqvar.spec.ts b/frontend/src/api/__tests__/acmgseqvar.spec.ts new file mode 100644 index 00000000..7fce01cc --- /dev/null +++ b/frontend/src/api/__tests__/acmgseqvar.spec.ts @@ -0,0 +1,118 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import createFetchMock from 'vitest-fetch-mock' + +import { AcmgSeqVarClient } from '@/api/acmgseqvar' +import { type AcmgRatingBackend } from '@/stores/variantAcmgRating' + +const fetchMocker = createFetchMock(vi) + +const mockVariantName = 'chr0:1234:A:C' +const mockAcmgRating: AcmgRatingBackend = { + comment: 'exampleComment', + criterias: [ + { + criteria: 'Pm1', + presence: 'Present', + evidence: 'Pathogenic Moderate' + } + ] +} + +describe.concurrent('AcmgSeqVar Client', () => { + beforeEach(() => { + fetchMocker.enableMocks() + fetchMocker.resetMocks() + }) + + it('fetches ACMG rating correctly', async () => { + fetchMocker.mockResponse(JSON.stringify(mockAcmgRating)) + + const client = new AcmgSeqVarClient() + const result = await client.fetchAcmgRating(mockVariantName) + + expect(result).toEqual(mockAcmgRating) + }) + + it('fails to fetch ACMG rating', async () => { + fetchMocker.mockResponse((req) => { + if (req.url.includes('acmgseqvar/get')) { + return Promise.resolve(JSON.stringify({ status: 500 })) + } + return Promise.resolve(JSON.stringify({ status: 400 })) + }) + + const client = new AcmgSeqVarClient() + const result = await client.fetchAcmgRating(mockVariantName) + + expect(result).toEqual({ status: 500 }) + }) + + it('saves ACMG rating correctly', async () => { + fetchMocker.mockResponse(JSON.stringify(mockAcmgRating)) + + const client = new AcmgSeqVarClient() + const result = await client.saveAcmgRating(mockVariantName, mockAcmgRating) + + expect(result).toEqual(mockAcmgRating) + }) + + it('fails to save ACMG rating', async () => { + fetchMocker.mockResponse((req) => { + if (req.url.includes('acmgseqvar/create')) { + return Promise.resolve(JSON.stringify({ status: 500 })) + } + return Promise.resolve(JSON.stringify({ status: 400 })) + }) + + const client = new AcmgSeqVarClient() + const result = await client.saveAcmgRating(mockVariantName, mockAcmgRating) + + expect(result).toEqual({ status: 500 }) + }) + + it('updates ACMG rating correctly', async () => { + fetchMocker.mockResponse(JSON.stringify(mockAcmgRating)) + + const client = new AcmgSeqVarClient() + const result = await client.updateAcmgRating(mockVariantName, mockAcmgRating) + + expect(result).toEqual(mockAcmgRating) + }) + + it('fails to update ACMG rating', async () => { + fetchMocker.mockResponse((req) => { + if (req.url.includes('acmgseqvar/update')) { + return Promise.resolve(JSON.stringify({ status: 500 })) + } + return Promise.resolve(JSON.stringify({ status: 400 })) + }) + + const client = new AcmgSeqVarClient() + const result = await client.updateAcmgRating(mockVariantName, mockAcmgRating) + + expect(result).toEqual({ status: 500 }) + }) + + it('deletes ACMG rating correctly', async () => { + fetchMocker.mockResponse(JSON.stringify({})) + + const client = new AcmgSeqVarClient() + const result = await client.deleteAcmgRating(mockVariantName) + + expect(result).toEqual({}) + }) + + it('fails to delete ACMG rating', async () => { + fetchMocker.mockResponse((req) => { + if (req.url.includes('acmgseqvar/delete')) { + return Promise.resolve(JSON.stringify({ status: 500 })) + } + return Promise.resolve(JSON.stringify({ status: 400 })) + }) + + const client = new AcmgSeqVarClient() + const result = await client.deleteAcmgRating(mockVariantName) + + expect(result).toEqual({ status: 500 }) + }) +}) diff --git a/frontend/src/api/acmgseqvar.ts b/frontend/src/api/acmgseqvar.ts new file mode 100644 index 00000000..b6acbb46 --- /dev/null +++ b/frontend/src/api/acmgseqvar.ts @@ -0,0 +1,93 @@ +import { API_V1_BASE_PREFIX } from '@/api/common' +import { type AcmgRatingBackend } from '@/stores/variantAcmgRating' + +/** + * Access to the seqvar part of the API. + */ +export class AcmgSeqVarClient { + private apiBaseUrl: string + private csrfToken: string | null + private currentUserId: string | null + + constructor(apiBaseUrl?: string, csrfToken?: string) { + this.apiBaseUrl = apiBaseUrl ?? API_V1_BASE_PREFIX + this.csrfToken = csrfToken ?? null + this.currentUserId = null + } + + /** + * Obtains the ACMG rating for a variant. + * + * @param variantName The variant to retrieve the ACMG rating for. + * @returns The ACMG rating for the variant. + */ + async fetchAcmgRating(variantName: string): Promise { + const url = `${this.apiBaseUrl}acmgseqvar/get?seqvar=${variantName}` + const response = await fetch(url, { + method: 'GET', + mode: 'cors', + credentials: 'include' + }) + return await response.json() + } + + /** + * Save the ACMG rating for a variant. + */ + async saveAcmgRating( + variantName: string, + acmgRating: AcmgRatingBackend + ): Promise { + const postData = `{ + "seqvar_name": "${variantName}", + "acmg_rank": ${JSON.stringify(acmgRating)} + }` + const response = await fetch(`${this.apiBaseUrl}acmgseqvar/create`, { + method: 'POST', + mode: 'cors', + credentials: 'include', + headers: { + accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: postData + }) + return await response.json() + } + + /** + * Update the ACMG rating for a variant. + */ + async updateAcmgRating( + variantName: string, + acmgRating: AcmgRatingBackend + ): Promise { + const postData = `{ + "seqvar_name": "${variantName}", + "acmg_rank": ${JSON.stringify(acmgRating)} + }` + const response = await fetch(`${this.apiBaseUrl}acmgseqvar/update`, { + method: 'PUT', + mode: 'cors', + credentials: 'include', + headers: { + accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: postData + }) + return await response.json() + } + + /** + * Delete the ACMG rating for a variant. + */ + async deleteAcmgRating(variantName: string): Promise { + const response = await fetch(`${this.apiBaseUrl}acmgseqvar/delete?seqvar=${variantName}`, { + method: 'DELETE', + mode: 'cors', + credentials: 'include' + }) + return await response.json() + } +} diff --git a/frontend/src/assets/__tests__/ExampleAcmgSeqVarRank.json b/frontend/src/assets/__tests__/ExampleAcmgSeqVarRank.json new file mode 100644 index 00000000..7c613044 --- /dev/null +++ b/frontend/src/assets/__tests__/ExampleAcmgSeqVarRank.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be2876be4d45d1ffcf61a11cf3a5f88fa92ef37d3b593646f4f29bf66f850996 +size 3419 diff --git a/frontend/src/components/VariantDetails/AcmgRating.vue b/frontend/src/components/VariantDetails/AcmgRating.vue index 96de65ea..dab22c93 100644 --- a/frontend/src/components/VariantDetails/AcmgRating.vue +++ b/frontend/src/components/VariantDetails/AcmgRating.vue @@ -29,10 +29,22 @@ const unsetAcmgRating = () => { acmgRatingStore.acmgRating.setUserPresenceAbsent() } -const resetAcmgRating = () => { +const resetAcmgRatingInterVar = () => { acmgRatingStore.acmgRating.setUserPresenceInterVar() } +const resetAcmgRatingServer = () => { + acmgRatingStore.acmgRating.setUserPresenceServer() +} + +const saveAcmgRating = () => { + acmgRatingStore.saveAcmgRating() +} + +const deleteAcmgRating = () => { + acmgRatingStore.deleteAcmgRating() +} + const updateAcmgConflicting = (isConflicting: boolean) => { acmgRatingConflicting.value = isConflicting } @@ -54,7 +66,11 @@ watch( async () => { if (props.smallVariant && acmgRatingStore.storeState === StoreState.Active) { await acmgRatingStore.setAcmgRating(props.smallVariant) - resetAcmgRating() + if (acmgRatingStore.acmgRatingStatus === false) { + resetAcmgRatingInterVar() + } else { + resetAcmgRatingServer() + } } } ) @@ -70,9 +86,8 @@ onMounted(async () => { ACMG Rating - - - + +

@@ -86,10 +101,6 @@ onMounted(async () => { Further documentation mdi-open-in-new

-
- Clear - Reset -
@@ -108,16 +119,44 @@ onMounted(async () => {
- + + + + With the buttons below you can twick the presented ACMG rating: +
+ Clear + + Reset to InterVar + +
+ + Reset to server + +
+
+ Reset to server +
+
+ With this buttons below you can + {{ acmgRatingStore.acmgRatingStatus === false ? 'save' : 'update' }} ACMG rating on the + server: +
+ + {{ acmgRatingStore.acmgRatingStatus === false ? 'Save' : 'Update' }} + + Delete +
+
+
- + {{ showSwitches ? 'Hide' : 'Show' }} summary view - +

Pathogenic:

@@ -160,7 +199,7 @@ onMounted(async () => {
- + {{ showFailed ? 'Hide' : 'Show' }} failed criteria @@ -203,17 +242,6 @@ onMounted(async () => {