Skip to content

Commit

Permalink
OCT-2249: Add unit tests for /projects & improve searching (#627)
Browse files Browse the repository at this point in the history
## Description

## Definition of Done

1. [ ] If required, the desciption of your change is added to the [QA
changelog](https://www.notion.so/octantapp/Changelog-for-the-QA-d96fa3b411cf488bb1d8d9a598d88281)
2. [ ] Acceptance criteria are met.
3. [ ] PR is manually tested before the merge by developer(s).
    - [ ] Happy path is manually checked.
4. [ ] PR is manually tested by QA when their assistance is required
(1).
- [ ] Octant Areas & Test Cases are checked for impact and updated if
required (2).
5. [ ] Unit tests are added unless there is a reason to omit them.
6. [ ] Automated tests are added when required.
7. [ ] The code is merged.
8. [ ] Tech documentation is added / updated, reviewed and approved
(including mandatory approval by a code owner, should such exist for
changed files).
    - [ ] BE: Swagger documentation is updated.
9. [ ] When required by QA:
    - [ ] Deployed to the relevant environment.
    - [ ] Passed system tests.

---

(1) Developer(s) in coordination with QA decide whether it's required.
For small tickets introducing small changes QA assistance is most
probably not required.

(2) [Octant Areas & Test
Cases](https://docs.google.com/spreadsheets/d/1cRe6dxuKJV3a4ZskAwWEPvrFkQm6rEfyUCYwLTYw_Cc).
  • Loading branch information
kgarbacinski authored and chris-calo committed Jan 16, 2025
1 parent 562f1b1 commit 4c860fd
Show file tree
Hide file tree
Showing 12 changed files with 413 additions and 3 deletions.
1 change: 1 addition & 0 deletions backend/app/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
SAFE_API_SEPOLIA = "https://safe-transaction-sepolia.safe.global/api/v1"

DEFAULT_MAINNET_PROJECT_CIDS = "QmSQEFD35gKxdPEmngNt1CWe3kSwiiGqBn1Z3FZvWb8mvK,Qmds9N5y2vkMuPTD6M4EBxNXnf3bjTDmzWBGnCkQGsMMGe,QmSXcT18anMXKACTueom8GXw8zrxTBbHGB71atitf6gZ9V,QmXomSdCCwt4FtBp3pidqSz3PtaiV2EyQikU6zRGWeCAsf,QmdtFLK3sB7EwQTNaqtmBnZqnN2pYZcu6GmUSTrpvb9wcq"
DEFAULT_PROJECTS_CONTRACT_ADDRESS = "0xB259fe6EC190cffF893b247AE688eFBF4261D2fc"

EPOCH0_SYBILS = [
"0xde19a6ce83cc934e5d4c4573f0f026c02c984fb2",
Expand Down
3 changes: 3 additions & 0 deletions backend/tests/v2/factories/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from tests.v2.factories.allocation_requests import AllocationRequestFactorySet
from tests.v2.factories.allocations import AllocationFactorySet
from tests.v2.factories.projects_details import ProjectsDetailsFactorySet
from tests.v2.factories.users import UserFactorySet
from dataclasses import dataclass

Expand All @@ -21,6 +22,7 @@ class FactoriesAggregator:
users: UserFactorySet
allocation_requests: AllocationRequestFactorySet
allocations: AllocationFactorySet
projects_details: ProjectsDetailsFactorySet

def __init__(self, fast_session: AsyncSession):
"""
Expand All @@ -29,3 +31,4 @@ def __init__(self, fast_session: AsyncSession):
self.users = UserFactorySet(fast_session)
self.allocation_requests = AllocationRequestFactorySet(fast_session)
self.allocations = AllocationFactorySet(fast_session)
self.projects_details = ProjectsDetailsFactorySet(fast_session)
40 changes: 40 additions & 0 deletions backend/tests/v2/factories/projects_details.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from async_factory_boy.factory.sqlalchemy import AsyncSQLAlchemyFactory
from factory import LazyAttribute

from app.infrastructure.database.models import ProjectsDetails
from tests.v2.factories.base import FactorySetBase
from tests.v2.factories.helpers import generate_random_eip55_address
from v2.core.types import Address


class ProjectsDetailsFactory(AsyncSQLAlchemyFactory):
class Meta:
model = ProjectsDetails
sqlalchemy_session_persistence = "commit"

address = LazyAttribute(lambda _: generate_random_eip55_address())
name = None
epoch = None


class ProjectsDetailsFactorySet(FactorySetBase):
_factories = {"projects_details": ProjectsDetailsFactory}

async def create(
self,
name: str,
epoch: int,
address: Address | None = None,
) -> ProjectsDetails:
factory_kwargs = {
"address": address,
"name": name,
"epoch": epoch,
}

if address is not None:
factory_kwargs["address"] = address

projects_details = await ProjectsDetailsFactory.create(**factory_kwargs)

return projects_details
26 changes: 25 additions & 1 deletion backend/tests/v2/fake_contracts/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@
from fastapi import FastAPI

from tests.v2.fake_contracts.epochs import FakeEpochsContract
from tests.v2.fake_contracts.helpers import FakeEpochsContractDetails
from tests.v2.fake_contracts.helpers import (
FakeEpochsContractDetails,
FakeProjectsContractDetails,
)
from tests.v2.fake_contracts.projects import FakeProjectsContract
from v2.epochs.dependencies import get_epochs_contracts
from v2.projects.dependencies import get_projects_contracts


@pytest.fixture(scope="function")
Expand All @@ -26,6 +31,25 @@ def _create_fake_epochs_contract(
return _create_fake_epochs_contract


@pytest.fixture(scope="function")
def fake_projects_contract_factory(fast_app: FastAPI) -> FakeProjectsContractCallable:
def _create_fake_projects_contract(
projects_details_for_contract: FakeProjectsContractDetails | None = None,
):
fake_projects_contract = FakeProjectsContract(projects_details_for_contract)

fast_app.dependency_overrides[
get_projects_contracts
] = lambda: fake_projects_contract

return fake_projects_contract

return _create_fake_projects_contract


FakeEpochsContractCallable = Callable[
[FakeEpochsContractDetails | None], FakeEpochsContract
]
FakeProjectsContractCallable = Callable[
[FakeProjectsContractDetails | None], FakeProjectsContract
]
14 changes: 14 additions & 0 deletions backend/tests/v2/fake_contracts/helpers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from dataclasses import dataclass

from v2.core.types import Address


@dataclass
class FakeEpochsContractDetails:
Expand All @@ -13,3 +15,15 @@ class FakeEpochsContractDetails:
future_epoch_props: dict = None
is_started: bool = False
started: int = 0


@dataclass
class FakeProjectDetails:
address: Address
epoch_number: int


@dataclass
class FakeProjectsContractDetails:
projects_cid: str
projects_details: list[FakeProjectDetails]
23 changes: 23 additions & 0 deletions backend/tests/v2/fake_contracts/projects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from tests.v2.fake_contracts.helpers import FakeProjectsContractDetails


class FakeProjectsContract:
def __init__(self, projects_details_for_contract: FakeProjectsContractDetails):
self.projects_details_for_contract = projects_details_for_contract

async def get_project_addresses(self, epoch_number: int) -> list[str]:
filtered_projects_details = list(
filter(
lambda project_details: project_details.epoch_number == epoch_number,
self.projects_details_for_contract.projects_details,
)
)
return list(
map(
lambda project_details: project_details.address,
filtered_projects_details,
)
)

async def get_project_cid(self) -> str:
return self.projects_details_for_contract.projects_cid
52 changes: 52 additions & 0 deletions backend/tests/v2/projects/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import pytest

from tests.v2.fake_contracts.conftest import ( # noqa: F401
fake_projects_contract_factory,
)

from tests.v2.factories.helpers import generate_random_eip55_address


@pytest.fixture(scope="module")
def multiple_project_details_simple() -> list:
details = [
{
"name": "test_project1",
"epoch": 3,
"address": generate_random_eip55_address(),
},
{
"name": "test_project2",
"epoch": 2,
"address": generate_random_eip55_address(),
},
{
"name": "test_project3",
"epoch": 1,
"address": generate_random_eip55_address(),
},
]

return details


@pytest.fixture(scope="module")
def multiple_project_details_various() -> list:
details = [
{
"name": "test_project1",
"epoch": 3,
"address": generate_random_eip55_address(),
},
{
"name": "octant_project2",
"epoch": 2,
"address": generate_random_eip55_address(),
},
{
"name": "great_project3",
"epoch": 1,
"address": generate_random_eip55_address(),
},
]
return details
144 changes: 144 additions & 0 deletions backend/tests/v2/projects/test_projects_details.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
from http import HTTPStatus

import pytest
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession

from tests.v2.factories import FactoriesAggregator
from tests.v2.factories.helpers import generate_random_eip55_address


@pytest.mark.asyncio
async def test_returns_empty_project_details(
fast_client: AsyncClient, fast_session: AsyncSession
):
async with fast_client as client:
resp = await client.get("projects/details?epochs=1&searchPhrases=test")
assert resp.status_code == HTTPStatus.OK
assert resp.json() == {"projectsDetails": []}


@pytest.mark.asyncio
async def test_returns_correct_project_details_for_single_filter(
fast_client: AsyncClient, factories: FactoriesAggregator
):
single_details = {
"name": "test_project1",
"epoch": 3,
"address": generate_random_eip55_address(),
}

await factories.projects_details.create(**single_details)

async with fast_client as client:
resp = await client.get("projects/details?epochs=3&searchPhrases=test_project")
assert resp.status_code == HTTPStatus.OK
assert resp.json() == {
"projectsDetails": [
{
"name": single_details["name"],
"epoch": str(single_details["epoch"]),
"address": single_details["address"],
}
]
}


@pytest.mark.asyncio
async def test_returns_correct_project_details_for_many_epochs(
fast_client: AsyncClient,
factories: FactoriesAggregator,
multiple_project_details_simple: list,
):
details = multiple_project_details_simple

for detail in details:
await factories.projects_details.create(**detail)

async with fast_client as client:
resp = await client.get("projects/details?epochs=1,2,3&searchPhrases=test")
assert resp.status_code == HTTPStatus.OK

assert resp.json() == {
"projectsDetails": [
{
"name": details[2]["name"],
"epoch": str(details[2]["epoch"]),
"address": details[2]["address"],
},
{
"name": details[1]["name"],
"epoch": str(details[1]["epoch"]),
"address": details[1]["address"],
},
{
"name": details[0]["name"],
"epoch": str(details[0]["epoch"]),
"address": details[0]["address"],
},
]
}


@pytest.mark.asyncio
async def test_returns_correct_project_details_for_many_epochs_and_search_phrases(
fast_client: AsyncClient,
factories: FactoriesAggregator,
multiple_project_details_various: list,
):
details = multiple_project_details_various

for detail in details:
await factories.projects_details.create(**detail)

async with fast_client as client:
resp = await client.get(
"projects/details?epochs=1,2,3&searchPhrases=test,octant,great"
)
assert resp.status_code == HTTPStatus.OK

assert resp.json() == {
"projectsDetails": [
{
"name": details[2]["name"],
"epoch": str(details[2]["epoch"]),
"address": details[2]["address"],
},
{
"name": details[1]["name"],
"epoch": str(details[1]["epoch"]),
"address": details[1]["address"],
},
{
"name": details[0]["name"],
"epoch": str(details[0]["epoch"]),
"address": details[0]["address"],
},
]
}


@pytest.mark.asyncio
async def test_returns_not_duplicated_project_details_for_words_from_the_same_project(
fast_client: AsyncClient,
factories: FactoriesAggregator,
multiple_project_details_various: list,
):
details = multiple_project_details_various
words = "test,project1"

for detail in details:
await factories.projects_details.create(**detail)

async with fast_client as client:
resp = await client.get(f"projects/details?epochs=3&searchPhrases={words}")
assert resp.status_code == HTTPStatus.OK
assert resp.json() == {
"projectsDetails": [
{
"name": details[0]["name"],
"epoch": str(details[0]["epoch"]),
"address": details[0]["address"],
}
]
}
Loading

0 comments on commit 4c860fd

Please sign in to comment.