From dc4308931773104afb184674569fc5c3a44b0766 Mon Sep 17 00:00:00 2001 From: Yosuke Otosu Date: Mon, 1 May 2023 20:18:12 +0900 Subject: [PATCH 1/2] feat: add token history API --- app/model/blockchain/token.py | 50 +- app/model/db/__init__.py | 2 +- app/model/db/update_token.py | 11 + app/model/schema/__init__.py | 4 + app/model/schema/token.py | 69 +- app/routers/bond.py | 94 ++ app/routers/share.py | 94 ++ .../fedec7fb783a_v23_6_0_feature_510.py | 33 + tests/test_app_routers_bond_tokens_POST.py | 10 +- ...outers_bond_tokens_{token_address}_POST.py | 210 +++- ...bond_tokens_{token_address}_history_GET.py | 1074 ++++++++++++++++ tests/test_app_routers_share_tokens_POST.py | 15 +- ...uters_share_tokens_{token_address}_POST.py | 379 ++++-- ...hare_tokens_{token_address}_history_GET.py | 1119 +++++++++++++++++ 14 files changed, 3043 insertions(+), 121 deletions(-) create mode 100644 migrations/versions/fedec7fb783a_v23_6_0_feature_510.py create mode 100644 tests/test_app_routers_bond_tokens_{token_address}_history_GET.py create mode 100644 tests/test_app_routers_share_tokens_{token_address}_history_GET.py diff --git a/app/model/blockchain/token.py b/app/model/blockchain/token.py index 436d392b..7ec779a2 100644 --- a/app/model/blockchain/token.py +++ b/app/model/blockchain/token.py @@ -58,7 +58,13 @@ from app.model.blockchain.tx_params.ibet_straight_bond import ( UpdateParams as IbetStraightBondUpdateParams, ) -from app.model.db import TokenAttrUpdate, TokenCache +from app.model.db import ( + TokenAttrUpdate, + TokenCache, + TokenType, + UpdateToken, + UpdateTokenTrigger, +) from app.utils.contract_utils import ContractUtils from app.utils.web3_utils import Web3Wrapper from config import CHAIN_ID, TOKEN_CACHE, TOKEN_CACHE_TTL, TX_GAS_LIMIT, ZERO_ADDRESS @@ -108,6 +114,24 @@ def record_attr_update(self, db_session: Session): _token_attr_update.updated_datetime = datetime.utcnow() db_session.add(_token_attr_update) + def create_history( + self, + db_session: Session, + original_contents: dict, + modified_contents: dict, + token_type: str, + trigger: UpdateTokenTrigger, + ): + update_token = UpdateToken() + update_token.token_address = self.token_address + update_token.issuer_address = self.issuer_address + update_token.type = token_type + update_token.arguments = modified_contents + update_token.original_contents = original_contents + update_token.status = 1 # succeeded + update_token.trigger = trigger + db_session.add(update_token) + def create_cache(self, db_session: Session): token_cache = TokenCache() token_cache.token_address = self.token_address @@ -559,6 +583,11 @@ def update( self, data: IbetStraightBondUpdateParams, tx_from: str, private_key: str ): """Update token""" + if data.dict(exclude_none=True) == {}: + return + + original_contents = self.get().__dict__ + contract = ContractUtils.get_contract( contract_name=self.contract_name, contract_address=self.token_address ) @@ -840,6 +869,13 @@ def update( db_session = Session(autocommit=False, autoflush=True, bind=engine) try: self.record_attr_update(db_session) + self.create_history( + db_session, + original_contents=original_contents, + modified_contents=data.dict(exclude_none=True), + token_type=TokenType.IBET_STRAIGHT_BOND.value, + trigger=UpdateTokenTrigger.UPDATE, + ) self.delete_cache(db_session) db_session.commit() except Exception as err: @@ -982,6 +1018,11 @@ def get(self): def update(self, data: IbetShareUpdateParams, tx_from: str, private_key: str): """Update token""" + if data.dict(exclude_none=True) == {}: + return + + original_contents = self.get().__dict__ + contract = ContractUtils.get_contract( contract_name=self.contract_name, contract_address=self.token_address ) @@ -1245,6 +1286,13 @@ def update(self, data: IbetShareUpdateParams, tx_from: str, private_key: str): db_session = Session(autocommit=False, autoflush=True, bind=engine) try: self.record_attr_update(db_session) + self.create_history( + db_session, + original_contents=original_contents, + modified_contents=data.dict(exclude_none=True), + token_type=TokenType.IBET_SHARE.value, + trigger=UpdateTokenTrigger.UPDATE, + ) self.delete_cache(db_session) db_session.commit() except Exception as err: diff --git a/app/model/db/__init__.py b/app/model/db/__init__.py index ac065240..d5cad5db 100644 --- a/app/model/db/__init__.py +++ b/app/model/db/__init__.py @@ -74,6 +74,6 @@ TransferApprovalOperationType, ) from .tx_management import TransactionLock -from .update_token import UpdateToken +from .update_token import UpdateToken, UpdateTokenTrigger from .upload_file import UploadFile from .utxo import UTXO, UTXOBlockNumber diff --git a/app/model/db/update_token.py b/app/model/db/update_token.py index a9172383..9027a493 100644 --- a/app/model/db/update_token.py +++ b/app/model/db/update_token.py @@ -16,6 +16,8 @@ SPDX-License-Identifier: Apache-2.0 """ +from enum import StrEnum + from sqlalchemy import JSON, Column, Integer, String from .base import Base @@ -35,7 +37,16 @@ class UpdateToken(Base): type = Column(String(40), nullable=False) # arguments arguments = Column(JSON, nullable=False) + # original contents + original_contents = Column(JSON, nullable=True) # processing status (pending:0, succeeded:1, failed:2) status = Column(Integer, nullable=False) # update trigger trigger = Column(String(40), nullable=False) + + +class UpdateTokenTrigger(StrEnum): + """Trigger of update token""" + + ISSUE = "Issue" + UPDATE = "Update" diff --git a/app/model/schema/__init__.py b/app/model/schema/__init__.py index cfd15bf7..1426dc47 100644 --- a/app/model/schema/__init__.py +++ b/app/model/schema/__init__.py @@ -114,7 +114,11 @@ ListAllTokenLockEventsQuery, ListAllTokenLockEventsResponse, ListAllTokenLockEventsSortItem, + ListTokenHistoryQuery, + ListTokenHistoryResponse, TokenAddressResponse, + TokenHistoryResponse, + UpdateTokenTrigger, ) from .token_holders import ( CreateTokenHoldersListRequest, diff --git a/app/model/schema/token.py b/app/model/schema/token.py index 59f192cd..44457959 100644 --- a/app/model/schema/token.py +++ b/app/model/schema/token.py @@ -17,8 +17,9 @@ SPDX-License-Identifier: Apache-2.0 """ import math -from enum import Enum -from typing import List, Optional +from datetime import datetime +from enum import Enum, StrEnum +from typing import Optional from fastapi import Query from pydantic import BaseModel, Field, validator @@ -47,14 +48,14 @@ class IbetStraightBondCreate(BaseModel): return_date: Optional[YYYYMMDD_constr] return_amount: Optional[str] = Field(max_length=2000) interest_rate: Optional[float] = Field(None, ge=0.0000, le=100.0000) - interest_payment_date: Optional[List[MMDD_constr]] + interest_payment_date: Optional[list[MMDD_constr]] transferable: Optional[bool] is_redeemed: Optional[bool] status: Optional[bool] is_offering: Optional[bool] tradable_exchange_contract_address: Optional[str] personal_info_contract_address: Optional[str] - image_url: Optional[List[str]] + image_url: Optional[list[str]] contact_information: Optional[str] = Field(max_length=2000) privacy_policy: Optional[str] = Field(max_length=5000) transfer_approval_required: Optional[bool] @@ -98,7 +99,7 @@ class IbetStraightBondUpdate(BaseModel): face_value: Optional[int] = Field(None, ge=0, le=5_000_000_000) interest_rate: Optional[float] = Field(None, ge=0.0000, le=100.0000) - interest_payment_date: Optional[List[MMDD_constr]] + interest_payment_date: Optional[list[MMDD_constr]] redemption_value: Optional[int] = Field(None, ge=0, le=5_000_000_000) transferable: Optional[bool] status: Optional[bool] @@ -381,6 +382,44 @@ class ListAllTokenLockEventsQuery: ) +class UpdateTokenTrigger(StrEnum): + """Trigger of update token""" + + ISSUE = "Issue" + UPDATE = "Update" + + +class ListTokenHistorySortItem(StrEnum): + """Sort item of token history""" + + created = "created" + trigger = "trigger" + + +@dataclass +class ListTokenHistoryQuery: + modified_contents: Optional[str] = Query( + default=None, description="Modified contents query" + ) + trigger: Optional[UpdateTokenTrigger] = Query( + default=None, description="Trigger of change" + ) + created_from: Optional[datetime] = Query( + default=None, description="Created datetime filter(From)" + ) + created_to: Optional[datetime] = Query( + default=None, description="Created datetime filter(To)" + ) + sort_item: ListTokenHistorySortItem = Query( + default=ListTokenHistorySortItem.created, description="Sort item" + ) + sort_order: SortOrder = Query( + default=SortOrder.DESC, description="Sort order(0: ASC, 1: DESC)" + ) + offset: Optional[int] = Query(default=None, description="Start position", ge=0) + limit: Optional[int] = Query(default=None, description="Number of set", ge=0) + + ############################ # RESPONSE ############################ @@ -408,7 +447,7 @@ class IbetStraightBondResponse(BaseModel): return_amount: str purpose: str interest_rate: float - interest_payment_date: List[str] + interest_payment_date: list[str] transferable: bool is_redeemed: bool status: bool @@ -451,8 +490,24 @@ class IbetShareResponse(BaseModel): memo: str +class TokenHistoryResponse(BaseModel): + original_contents: dict | None = Field( + default=None, nullable=True, description="original attributes before update" + ) + modified_contents: dict = Field(..., description="update attributes") + trigger: UpdateTokenTrigger + created: datetime + + +class ListTokenHistoryResponse(BaseModel): + result_set: ResultSet + history: list[TokenHistoryResponse] = Field( + default=[], description="token update histories" + ) + + class ListAllTokenLockEventsResponse(BaseModel): """List All Lock/Unlock events (Response)""" result_set: ResultSet - events: List[LockEvent] = Field(description="Lock/Unlock event list") + events: list[LockEvent] = Field(description="Lock/Unlock event list") diff --git a/app/routers/bond.py b/app/routers/bond.py index 197ab820..01559e05 100644 --- a/app/routers/bond.py +++ b/app/routers/bond.py @@ -117,6 +117,8 @@ ListAllTokenLockEventsSortItem, ListBatchIssueRedeemUploadResponse, ListBatchRegisterPersonalInfoUploadResponse, + ListTokenHistoryQuery, + ListTokenHistoryResponse, ListTransferHistoryQuery, ListTransferHistorySortItem, LockEventCategory, @@ -124,6 +126,7 @@ ScheduledEventIdResponse, ScheduledEventResponse, TokenAddressResponse, + TokenHistoryResponse, TransferApprovalHistoryResponse, TransferApprovalsResponse, TransferApprovalTokenResponse, @@ -148,6 +151,7 @@ LOG = log.get_logger() local_tz = timezone(config.TZ) +utc_tz = timezone("UTC") # POST: /bond/tokens @@ -246,6 +250,7 @@ def issue_token( _update_token.issuer_address = issuer_address _update_token.type = TokenType.IBET_STRAIGHT_BOND.value _update_token.arguments = token_dict + _update_token.original_contents = None _update_token.status = 0 # pending _update_token.trigger = "Issue" db.add(_update_token) @@ -284,6 +289,17 @@ def issue_token( _utxo.block_timestamp = datetime.utcfromtimestamp(block["timestamp"]) db.add(_utxo) + # Insert token history + _update_token = UpdateToken() + _update_token.token_address = contract_address + _update_token.issuer_address = issuer_address + _update_token.type = TokenType.IBET_STRAIGHT_BOND.value + _update_token.arguments = token_dict + _update_token.original_contents = None + _update_token.status = 1 # succeeded + _update_token.trigger = "Issue" + db.add(_update_token) + token_status = 1 # succeeded # Register token data @@ -455,6 +471,84 @@ def update_token( return +# GET: /bond/tokens/{token_address}/history +@router.get( + "/tokens/{token_address}/history", + response_model=ListTokenHistoryResponse, + responses=get_routers_responses(404, InvalidParameterError), +) +def list_bond_history( + db: DBSession, + token_address: str, + request_query: ListTokenHistoryQuery = Depends(), +): + """List token history""" + query = ( + db.query(UpdateToken) + .filter(UpdateToken.type == TokenType.IBET_STRAIGHT_BOND) + .filter(UpdateToken.token_address == token_address) + .filter(UpdateToken.status == 1) + ) + total = query.count() + + if request_query.trigger: + query = query.filter(UpdateToken.trigger == request_query.trigger) + if request_query.modified_contents: + query = query.filter( + cast(UpdateToken.arguments, String).like( + "%" + request_query.modified_contents + "%" + ) + ) + if request_query.created_from: + query = query.filter( + UpdateToken.created + >= local_tz.localize(request_query.created_from).astimezone(utc_tz) + ) + if request_query.created_to: + query = query.filter( + UpdateToken.created + <= local_tz.localize(request_query.created_to).astimezone(utc_tz) + ) + + count = query.count() + + # Sort + sort_attr = getattr(UpdateToken, request_query.sort_item, None) + if request_query.sort_order == 0: # ASC + query = query.order_by(sort_attr) + else: # DESC + query = query.order_by(desc(sort_attr)) + if request_query.sort_item != UpdateToken.created: + # NOTE: Set secondary sort for consistent results + query = query.order_by(desc(UpdateToken.created)) + + # Pagination + if request_query.limit is not None: + query = query.limit(request_query.limit) + if request_query.offset is not None: + query = query.offset(request_query.offset) + + return json_response( + { + "result_set": { + "count": count, + "offset": request_query.offset, + "limit": request_query.limit, + "total": total, + }, + "history": [ + { + "original_contents": h.original_contents, + "modified_contents": h.arguments, + "trigger": h.trigger, + "created": utc_tz.localize(h.created).astimezone(local_tz), + } + for h in query.all() + ], + } + ) + + # GET: /bond/tokens/{token_address}/additional_issue @router.get( "/tokens/{token_address}/additional_issue", diff --git a/app/routers/share.py b/app/routers/share.py index 2cacb3bf..12f9078d 100644 --- a/app/routers/share.py +++ b/app/routers/share.py @@ -118,6 +118,8 @@ ListAllTokenLockEventsSortItem, ListBatchIssueRedeemUploadResponse, ListBatchRegisterPersonalInfoUploadResponse, + ListTokenHistoryQuery, + ListTokenHistoryResponse, ListTransferHistoryQuery, ListTransferHistorySortItem, LockEventCategory, @@ -125,6 +127,7 @@ ScheduledEventIdResponse, ScheduledEventResponse, TokenAddressResponse, + TokenHistoryResponse, TransferApprovalHistoryResponse, TransferApprovalsResponse, TransferApprovalTokenResponse, @@ -149,6 +152,7 @@ LOG = log.get_logger() local_tz = timezone(config.TZ) +utc_tz = timezone("UTC") # POST: /share/tokens @@ -247,6 +251,7 @@ def issue_token( _update_token.issuer_address = issuer_address _update_token.type = TokenType.IBET_SHARE.value _update_token.arguments = token_dict + _update_token.original_contents = None _update_token.status = 0 # pending _update_token.trigger = "Issue" db.add(_update_token) @@ -285,6 +290,17 @@ def issue_token( _utxo.block_timestamp = datetime.utcfromtimestamp(block["timestamp"]) db.add(_utxo) + # Insert token history + _update_token = UpdateToken() + _update_token.token_address = contract_address + _update_token.issuer_address = issuer_address + _update_token.type = TokenType.IBET_SHARE.value + _update_token.arguments = token_dict + _update_token.original_contents = None + _update_token.status = 1 # succeeded + _update_token.trigger = "Issue" + db.add(_update_token) + token_status = 1 # succeeded # Register token data @@ -450,6 +466,84 @@ def update_token( return +# GET: /share/tokens/{token_address}/history +@router.get( + "/tokens/{token_address}/history", + response_model=ListTokenHistoryResponse, + responses=get_routers_responses(404, InvalidParameterError), +) +def list_share_history( + db: DBSession, + token_address: str, + request_query: ListTokenHistoryQuery = Depends(), +): + """List token history""" + query = ( + db.query(UpdateToken) + .filter(UpdateToken.type == TokenType.IBET_SHARE) + .filter(UpdateToken.token_address == token_address) + .filter(UpdateToken.status == 1) + ) + total = query.count() + + if request_query.trigger: + query = query.filter(UpdateToken.trigger == request_query.trigger) + if request_query.modified_contents: + query = query.filter( + cast(UpdateToken.arguments, String).like( + "%" + request_query.modified_contents + "%" + ) + ) + if request_query.created_from: + query = query.filter( + UpdateToken.created + >= local_tz.localize(request_query.created_from).astimezone(utc_tz) + ) + if request_query.created_to: + query = query.filter( + UpdateToken.created + <= local_tz.localize(request_query.created_to).astimezone(utc_tz) + ) + + count = query.count() + + # Sort + sort_attr = getattr(UpdateToken, request_query.sort_item, None) + if request_query.sort_order == 0: # ASC + query = query.order_by(sort_attr) + else: # DESC + query = query.order_by(desc(sort_attr)) + if request_query.sort_item != UpdateToken.created: + # NOTE: Set secondary sort for consistent results + query = query.order_by(desc(UpdateToken.created)) + + # Pagination + if request_query.limit is not None: + query = query.limit(request_query.limit) + if request_query.offset is not None: + query = query.offset(request_query.offset) + + return json_response( + { + "result_set": { + "count": count, + "offset": request_query.offset, + "limit": request_query.limit, + "total": total, + }, + "history": [ + { + "original_contents": h.original_contents, + "modified_contents": h.arguments, + "trigger": h.trigger, + "created": utc_tz.localize(h.created).astimezone(local_tz), + } + for h in query.all() + ], + } + ) + + # GET: /share/tokens/{token_address}/additional_issue @router.get( "/tokens/{token_address}/additional_issue", diff --git a/migrations/versions/fedec7fb783a_v23_6_0_feature_510.py b/migrations/versions/fedec7fb783a_v23_6_0_feature_510.py new file mode 100644 index 00000000..e486d8bc --- /dev/null +++ b/migrations/versions/fedec7fb783a_v23_6_0_feature_510.py @@ -0,0 +1,33 @@ +"""v23.6.0_feature_510 + +Revision ID: fedec7fb783a +Revises: 90b5d4040e8f +Create Date: 2023-05-01 19:49:40.649457 + +""" +import sqlalchemy as sa +from alembic import op + +from app.database import get_db_schema + +# revision identifiers, used by Alembic. +revision = "fedec7fb783a" +down_revision = "90b5d4040e8f" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "update_token", + sa.Column("original_contents", sa.JSON(), nullable=True), + schema=get_db_schema(), + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("update_token", "original_contents", schema=get_db_schema()) + # ### end Alembic commands ### diff --git a/tests/test_app_routers_bond_tokens_POST.py b/tests/test_app_routers_bond_tokens_POST.py index 4adf0ba1..601e230a 100644 --- a/tests/test_app_routers_bond_tokens_POST.py +++ b/tests/test_app_routers_bond_tokens_POST.py @@ -153,7 +153,10 @@ def test_normal_1(self, client, db): assert utxo.block_timestamp == datetime(2021, 4, 27, 12, 34, 56) update_token = db.query(UpdateToken).first() - assert update_token is None + assert update_token.token_address == "contract_address_test1" + assert update_token.type == TokenType.IBET_STRAIGHT_BOND.value + assert update_token.status == 1 + assert update_token.trigger == "Issue" # # include updates @@ -380,7 +383,10 @@ def test_normal_3(self, client, db): assert utxo.block_timestamp == datetime(2021, 4, 27, 12, 34, 56) update_token = db.query(UpdateToken).first() - assert update_token is None + assert update_token.token_address == "contract_address_test1" + assert update_token.type == TokenType.IBET_STRAIGHT_BOND.value + assert update_token.status == 1 + assert update_token.trigger == "Issue" ########################################################################### # Error Case diff --git a/tests/test_app_routers_bond_tokens_{token_address}_POST.py b/tests/test_app_routers_bond_tokens_{token_address}_POST.py index 928b66b1..6526b70d 100644 --- a/tests/test_app_routers_bond_tokens_{token_address}_POST.py +++ b/tests/test_app_routers_bond_tokens_{token_address}_POST.py @@ -18,14 +18,24 @@ """ import hashlib from unittest import mock -from unittest.mock import ANY, MagicMock +from unittest.mock import MagicMock +from eth_keyfile import decode_keyfile_json from web3 import Web3 from web3.middleware import geth_poa_middleware import config from app.exceptions import SendTransactionError -from app.model.db import Account, AuthToken, Token, TokenType +from app.model.blockchain import IbetStraightBondContract +from app.model.db import ( + Account, + AuthToken, + Token, + TokenAttrUpdate, + TokenType, + UpdateToken, +) +from app.utils.contract_utils import ContractUtils from app.utils.e2ee_utils import E2EEUtils from tests.account_config import config_eth_account @@ -33,6 +43,28 @@ web3.middleware_onion.inject(geth_poa_middleware, layer=0) +def deploy_bond_token_contract( + address, + private_key, +): + arguments = [ + "token.name", + "token.symbol", + 100, + 20, + "token.redemption_date", + 30, + "token.return_date", + "token.return_amount", + "token.purpose", + ] + bond_contrat = IbetStraightBondContract() + token_address, _, _ = bond_contrat.create(arguments, address, private_key) + + return ContractUtils.get_contract("IbetStraightBond", token_address) + + +@mock.patch("app.model.blockchain.token.TX_GAS_LIMIT", 8000000) class TestAppRoutersBondTokensTokenAddressPOST: # target API endpoint base_url = "/bond/tokens/{}" @@ -42,12 +74,18 @@ class TestAppRoutersBondTokensTokenAddressPOST: ########################################################################### # - @mock.patch("app.model.blockchain.token.IbetStraightBondContract.update") - def test_normal_1(self, IbetStraightBondContract_mock, client, db): + def test_normal_1(self, client, db): test_account = config_eth_account("user1") _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) _keyfile = test_account["keyfile_json"] - _token_address = "0x82b1c9374aB625380bd498a3d9dF4033B8A0E3Bb" + + # Prepare data : Token + token_contract = deploy_bond_token_contract(_issuer_address, issuer_private_key) + _token_address = token_contract.address # prepare data account = Account() @@ -64,9 +102,6 @@ def test_normal_1(self, IbetStraightBondContract_mock, client, db): token.abi = "" db.add(token) - # mock - IbetStraightBondContract_mock.side_effect = [None] - # request target API req_param = { "face_value": 10000, @@ -94,20 +129,78 @@ def test_normal_1(self, IbetStraightBondContract_mock, client, db): ) # assertion - IbetStraightBondContract_mock.assert_any_call( - data=req_param, tx_from=_issuer_address, private_key=ANY - ) assert resp.status_code == 200 assert resp.json() is None + token_attr_update = ( + db.query(TokenAttrUpdate) + .filter(TokenAttrUpdate.token_address == _token_address) + .all() + ) + assert len(token_attr_update) == 1 + + update_token = db.query(UpdateToken).first() + assert update_token.token_address == _token_address + assert update_token.issuer_address == _issuer_address + assert update_token.type == TokenType.IBET_STRAIGHT_BOND.value + assert update_token.original_contents == { + "contact_information": "", + "contract_name": "IbetStraightBond", + "face_value": 20, + "interest_payment_date": ["", "", "", "", "", "", "", "", "", "", "", ""], + "interest_rate": 0.0, + "is_offering": False, + "is_redeemed": False, + "issuer_address": _issuer_address, + "memo": "", + "name": "token.name", + "personal_info_contract_address": "0x0000000000000000000000000000000000000000", + "privacy_policy": "", + "purpose": "token.purpose", + "redemption_date": "token.redemption_date", + "redemption_value": 30, + "return_amount": "token.return_amount", + "return_date": "token.return_date", + "status": True, + "symbol": "token.symbol", + "token_address": _token_address, + "total_supply": 100, + "tradable_exchange_contract_address": "0x0000000000000000000000000000000000000000", + "transfer_approval_required": False, + "transferable": False, + } + assert update_token.arguments == { + "face_value": 10000, + "interest_rate": 0.5, + "interest_payment_date": ["0101", "0701"], + "redemption_value": 11000, + "transferable": False, + "status": False, + "is_offering": False, + "is_redeemed": True, + "tradable_exchange_contract_address": "0xe883A6f441Ad5682d37DF31d34fc012bcB07A740", + "personal_info_contract_address": "0xa4CEe3b909751204AA151860ebBE8E7A851c2A1a", + "contact_information": "問い合わせ先test", + "privacy_policy": "プライバシーポリシーtest", + "transfer_approval_required": True, + "memo": "m" * 10000, + } + assert update_token.status == 1 + # # No request parameters - @mock.patch("app.model.blockchain.token.IbetStraightBondContract.update") - def test_normal_2(self, IbetStraightBondContract_mock, client, db): + def test_normal_2(self, client, db): test_account = config_eth_account("user1") _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) _keyfile = test_account["keyfile_json"] - _token_address = "0x82b1c9374aB625380bd498a3d9dF4033B8A0E3Bb" + + # Prepare data : Token + token_contract = deploy_bond_token_contract(_issuer_address, issuer_private_key) + _token_address = token_contract.address # prepare data account = Account() @@ -124,9 +217,6 @@ def test_normal_2(self, IbetStraightBondContract_mock, client, db): token.abi = "" db.add(token) - # mock - IbetStraightBondContract_mock.side_effect = [None] - # request target API req_param = {} resp = client.post( @@ -142,14 +232,33 @@ def test_normal_2(self, IbetStraightBondContract_mock, client, db): assert resp.status_code == 200 assert resp.json() is None + token_attr_update = ( + db.query(TokenAttrUpdate) + .filter(TokenAttrUpdate.token_address == _token_address) + .all() + ) + assert len(token_attr_update) == 0 + + update_token = db.query(UpdateToken).first() + assert update_token is None + # # Authorization by auth token - @mock.patch("app.model.blockchain.token.IbetStraightBondContract.update") - def test_normal_3(self, IbetStraightBondContract_mock, client, db): + def test_normal_3(self, client, db): test_account = config_eth_account("user1") _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) _keyfile = test_account["keyfile_json"] - _token_address = "0x82b1c9374aB625380bd498a3d9dF4033B8A0E3Bb" + + # Prepare data : Token + token_contract = deploy_bond_token_contract( + _issuer_address, + issuer_private_key, + ) + _token_address = token_contract.address # prepare data account = Account() @@ -172,9 +281,6 @@ def test_normal_3(self, IbetStraightBondContract_mock, client, db): token.abi = "" db.add(token) - # mock - IbetStraightBondContract_mock.side_effect = [None] - # request target API req_param = { "face_value": 10000, @@ -202,12 +308,64 @@ def test_normal_3(self, IbetStraightBondContract_mock, client, db): ) # assertion - IbetStraightBondContract_mock.assert_any_call( - data=req_param, tx_from=_issuer_address, private_key=ANY - ) assert resp.status_code == 200 assert resp.json() is None + token_attr_update = ( + db.query(TokenAttrUpdate) + .filter(TokenAttrUpdate.token_address == _token_address) + .all() + ) + assert len(token_attr_update) == 1 + + update_token = db.query(UpdateToken).first() + assert update_token.token_address == _token_address + assert update_token.issuer_address == _issuer_address + assert update_token.type == TokenType.IBET_STRAIGHT_BOND.value + assert update_token.original_contents == { + "contact_information": "", + "contract_name": "IbetStraightBond", + "face_value": 20, + "interest_payment_date": ["", "", "", "", "", "", "", "", "", "", "", ""], + "interest_rate": 0.0, + "is_offering": False, + "is_redeemed": False, + "issuer_address": _issuer_address, + "memo": "", + "name": "token.name", + "personal_info_contract_address": "0x0000000000000000000000000000000000000000", + "privacy_policy": "", + "purpose": "token.purpose", + "redemption_date": "token.redemption_date", + "redemption_value": 30, + "return_amount": "token.return_amount", + "return_date": "token.return_date", + "status": True, + "symbol": "token.symbol", + "token_address": _token_address, + "total_supply": 100, + "tradable_exchange_contract_address": "0x0000000000000000000000000000000000000000", + "transfer_approval_required": False, + "transferable": False, + } + assert update_token.arguments == { + "face_value": 10000, + "interest_rate": 0.5, + "interest_payment_date": ["0101", "0701"], + "redemption_value": 11000, + "transferable": False, + "status": False, + "is_offering": False, + "is_redeemed": True, + "tradable_exchange_contract_address": "0xe883A6f441Ad5682d37DF31d34fc012bcB07A740", + "personal_info_contract_address": "0xa4CEe3b909751204AA151860ebBE8E7A851c2A1a", + "contact_information": "問い合わせ先test", + "privacy_policy": "プライバシーポリシーtest", + "transfer_approval_required": True, + "memo": "memo_test1", + } + assert update_token.status == 1 + ########################################################################### # Error Case ########################################################################### diff --git a/tests/test_app_routers_bond_tokens_{token_address}_history_GET.py b/tests/test_app_routers_bond_tokens_{token_address}_history_GET.py new file mode 100644 index 00000000..9f4fc7fd --- /dev/null +++ b/tests/test_app_routers_bond_tokens_{token_address}_history_GET.py @@ -0,0 +1,1074 @@ +""" +Copyright BOOSTRY Co., Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. + +You may obtain a copy of the License at +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +See the License for the specific language governing permissions and +limitations under the License. + +SPDX-License-Identifier: Apache-2.0 +""" +import json +from datetime import datetime +from unittest.mock import ANY + +from _decimal import Decimal +from eth_keyfile import decode_keyfile_json +from pytz import timezone +from starlette.testclient import TestClient +from web3 import Web3 +from web3.contract import Contract +from web3.middleware import geth_poa_middleware + +import config +from app.model.blockchain import IbetStraightBondContract +from app.model.db import Account, Token, TokenType, UpdateToken, UpdateTokenTrigger +from app.model.schema import IbetStraightBondCreate +from app.utils.contract_utils import ContractUtils +from app.utils.e2ee_utils import E2EEUtils +from tests.account_config import config_eth_account + +web3 = Web3(Web3.HTTPProvider(config.WEB3_HTTP_PROVIDER)) +web3.middleware_onion.inject(geth_poa_middleware, layer=0) + + +def deploy_bond_token_contract( + session, + address, + private_key, + personal_info_contract_address, + tradable_exchange_contract_address=config.ZERO_ADDRESS, + transfer_approval_required=True, + created: datetime | None = None, +) -> (Contract, dict): + arguments = [ + "token.name", # name + "token.symbol", # symbol + 100, # total_supply + 20, # face_value + "20230501", # redemption_date + 30, # redemption_value + "20230501", # return_date + "token.return_amount", # return_amount + "token.purpose", # purpose + ] + bond_contrat = IbetStraightBondContract() + token_address, _, _ = bond_contrat.create(arguments, address, private_key) + contract = ContractUtils.get_contract("IbetStraightBond", token_address) + token_create_param = IbetStraightBondCreate( + name="token.name", + total_supply=100, + face_value=20, + purpose="token.purpose", + symbol="token.symbol", + redemption_date="20230501", + redemption_value=30, + return_date="20230501", + return_amount="token.return_amount", + interest_rate=0.0001, # update + interest_payment_date=["0331", "0930"], # update + transferable=True, # update + is_redeemed=False, + status=False, # update + is_offering=True, # update + tradable_exchange_contract_address=tradable_exchange_contract_address, # update + personal_info_contract_address=personal_info_contract_address, # update + image_url=None, + contact_information="contact info test", # update + privacy_policy="privacy policy test", # update + transfer_approval_required=transfer_approval_required, # update + ).__dict__ + + token_create_param.pop("image_url") + update_token = UpdateToken() + update_token.token_address = token_address + update_token.type = TokenType.IBET_STRAIGHT_BOND.value + update_token.issuer_address = address + update_token.arguments = token_create_param + update_token.original_contents = None + update_token.status = 1 + update_token.trigger = UpdateTokenTrigger.ISSUE.value + if created: + update_token.created = created + session.add(update_token) + + build_tx_param = { + "chainId": config.CHAIN_ID, + "from": address, + "gas": config.TX_GAS_LIMIT, + "gasPrice": 0, + } + tx = contract.functions.setInterestRate( + int(Decimal(str(token_create_param["interest_rate"])) * Decimal("10000")) + ).build_transaction(build_tx_param) + _interest_payment_date = {} + for i, item in enumerate(token_create_param["interest_payment_date"]): + _interest_payment_date[f"interestPaymentDate{i + 1}"] = item + _interest_payment_date_string = json.dumps(_interest_payment_date) + ContractUtils.send_transaction(transaction=tx, private_key=private_key) + tx = contract.functions.setInterestPaymentDate( + _interest_payment_date_string + ).build_transaction(build_tx_param) + ContractUtils.send_transaction(transaction=tx, private_key=private_key) + tx = contract.functions.setTransferable( + token_create_param["transferable"] + ).build_transaction(build_tx_param) + ContractUtils.send_transaction(transaction=tx, private_key=private_key) + tx = contract.functions.setStatus(token_create_param["status"]).build_transaction( + build_tx_param + ) + ContractUtils.send_transaction(transaction=tx, private_key=private_key) + tx = contract.functions.changeOfferingStatus( + token_create_param["is_offering"] + ).build_transaction(build_tx_param) + ContractUtils.send_transaction(transaction=tx, private_key=private_key) + tx = contract.functions.setTradableExchange( + token_create_param["tradable_exchange_contract_address"] + ).build_transaction(build_tx_param) + ContractUtils.send_transaction(transaction=tx, private_key=private_key) + tx = contract.functions.setPersonalInfoAddress( + token_create_param["personal_info_contract_address"] + ).build_transaction(build_tx_param) + ContractUtils.send_transaction(transaction=tx, private_key=private_key) + tx = contract.functions.setContactInformation( + token_create_param["contact_information"] + ).build_transaction(build_tx_param) + ContractUtils.send_transaction(transaction=tx, private_key=private_key) + tx = contract.functions.setPrivacyPolicy( + token_create_param["privacy_policy"] + ).build_transaction(build_tx_param) + ContractUtils.send_transaction(transaction=tx, private_key=private_key) + tx = contract.functions.setTransferApprovalRequired( + token_create_param["transfer_approval_required"] + ).build_transaction(build_tx_param) + ContractUtils.send_transaction(transaction=tx, private_key=private_key) + + return contract, token_create_param + + +class TestAppRoutersBondTokensTokenAddressHistoryGET: + # target API endpoint + base_url = "/bond/tokens/{}/history" + + @staticmethod + def create_history_by_api( + client: TestClient, token_address: str, issuer_address: str + ): + client.post( + f"/bond/tokens/{token_address}", + json={"face_value": 10000, "memo": None}, + headers={ + "issuer-address": issuer_address, + "eoa-password": E2EEUtils.encrypt("password"), + }, + ) + client.post( + f"/bond/tokens/{token_address}", + json={"interest_rate": 0.5, "memo": None}, + headers={ + "issuer-address": issuer_address, + "eoa-password": E2EEUtils.encrypt("password"), + }, + ) + client.post( + f"/bond/tokens/{token_address}", + json={"interest_payment_date": ["0101", "0701"], "memo": None}, + headers={ + "issuer-address": issuer_address, + "eoa-password": E2EEUtils.encrypt("password"), + }, + ) + + @staticmethod + def expected_original_after_issue( + create_token_param: dict, issuer_address: str, token_address: str + ): + interest_payment_date = [ + create_token_param["interest_payment_date"][i] + if len(create_token_param["interest_payment_date"]) > i + else "" + for i in range(12) + ] + + return { + **create_token_param, + "contract_name": "IbetStraightBond", + "interest_payment_date": interest_payment_date, + "issuer_address": issuer_address, + "memo": "", + "token_address": token_address, + } + + ########################################################################### + # Normal Case + ########################################################################### + + # + # 0 record + def test_normal_1(self, client, db, personal_info_contract): + test_account = config_eth_account("user1") + _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) + _keyfile = test_account["keyfile_json"] + + # prepare data: Token + account = Account() + account.issuer_address = _issuer_address + account.keyfile = _keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + _token = Token() + _token.token_address = "no_record_address" + _token.issuer_address = _issuer_address + _token.type = TokenType.IBET_STRAIGHT_BOND.value + _token.tx_hash = "" + _token.abi = "" + db.add(_token) + + # request target api + resp = client.get( + self.base_url.format(_token.token_address), + ) + + # assertion + assert resp.status_code == 200 + assert resp.json() == { + "result_set": { + "count": 0, + "offset": None, + "limit": None, + "total": 0, + }, + "history": [], + } + + # + # Multiple record + def test_normal_2(self, client, db, personal_info_contract): + test_account = config_eth_account("user1") + _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) + _keyfile = test_account["keyfile_json"] + + # Prepare data : Token + token_contract, create_param = deploy_bond_token_contract( + db, _issuer_address, issuer_private_key, personal_info_contract.address + ) + _token_address = token_contract.address + + # prepare data + account = Account() + account.issuer_address = _issuer_address + account.keyfile = _keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + _token = Token() + _token.token_address = token_contract.address + _token.issuer_address = _issuer_address + _token.type = TokenType.IBET_STRAIGHT_BOND.value + _token.tx_hash = "" + _token.abi = "" + db.add(_token) + db.commit() + + # create history + self.create_history_by_api(client, _token_address, _issuer_address) + + # request target API + resp = client.get( + self.base_url.format(_token_address), + ) + original_after_issue = self.expected_original_after_issue( + create_param, _issuer_address, _token_address + ) + + # assertion + assert resp.status_code == 200 + assert resp.json() == { + "result_set": { + "count": 4, + "offset": None, + "limit": None, + "total": 4, + }, + "history": [ + { + "original_contents": { + **original_after_issue, + **{"face_value": 10000}, + **{"interest_rate": 0.5}, + }, + "modified_contents": {"interest_payment_date": ["0101", "0701"]}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + { + "original_contents": { + **original_after_issue, + **{"face_value": 10000}, + }, + "modified_contents": {"interest_rate": 0.5}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + { + "original_contents": original_after_issue, + "modified_contents": {"face_value": 10000}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + { + "original_contents": None, + "modified_contents": create_param, + "trigger": UpdateTokenTrigger.ISSUE.value, + "created": ANY, + }, + ], + } + + # + # Search filter: trigger + def test_normal_3_1(self, client, db, personal_info_contract): + test_account = config_eth_account("user1") + _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) + _keyfile = test_account["keyfile_json"] + + # Prepare data : Token + token_contract, create_param = deploy_bond_token_contract( + db, _issuer_address, issuer_private_key, personal_info_contract.address + ) + _token_address = token_contract.address + + # prepare data + account = Account() + account.issuer_address = _issuer_address + account.keyfile = _keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + _token = Token() + _token.token_address = token_contract.address + _token.issuer_address = _issuer_address + _token.type = TokenType.IBET_STRAIGHT_BOND.value + _token.tx_hash = "" + _token.abi = "" + db.add(_token) + db.commit() + + # create history + self.create_history_by_api(client, _token_address, _issuer_address) + + # request target API + resp = client.get( + self.base_url.format(_token_address), + params={ + "trigger": "Update", + }, + ) + + original_after_issue = self.expected_original_after_issue( + create_param, _issuer_address, _token_address + ) + + # assertion + assert resp.status_code == 200 + assert resp.json() == { + "result_set": { + "count": 3, + "offset": None, + "limit": None, + "total": 4, + }, + "history": [ + { + "original_contents": { + **original_after_issue, + **{"face_value": 10000}, + **{"interest_rate": 0.5}, + }, + "modified_contents": {"interest_payment_date": ["0101", "0701"]}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + { + "original_contents": { + **original_after_issue, + **{"face_value": 10000}, + }, + "modified_contents": {"interest_rate": 0.5}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + { + "original_contents": original_after_issue, + "modified_contents": {"face_value": 10000}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + ], + } + + # + # Search filter: modified_contents + def test_normal_3_2(self, client, db, personal_info_contract): + test_account = config_eth_account("user1") + _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) + _keyfile = test_account["keyfile_json"] + + # Prepare data : Token + token_contract, create_param = deploy_bond_token_contract( + db, _issuer_address, issuer_private_key, personal_info_contract.address + ) + _token_address = token_contract.address + + # prepare data + account = Account() + account.issuer_address = _issuer_address + account.keyfile = _keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + _token = Token() + _token.token_address = token_contract.address + _token.issuer_address = _issuer_address + _token.type = TokenType.IBET_STRAIGHT_BOND.value + _token.tx_hash = "" + _token.abi = "" + db.add(_token) + db.commit() + + # create history + self.create_history_by_api(client, _token_address, _issuer_address) + + # request target API + resp = client.get( + self.base_url.format(_token_address), + params={ + "modified_contents": "face_value", + }, + ) + + original_after_issue = self.expected_original_after_issue( + create_param, _issuer_address, _token_address + ) + + # assertion + assert resp.status_code == 200 + assert resp.json() == { + "result_set": { + "count": 2, + "offset": None, + "limit": None, + "total": 4, + }, + "history": [ + { + "original_contents": original_after_issue, + "modified_contents": {"face_value": 10000}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + { + "original_contents": None, + "modified_contents": create_param, + "trigger": UpdateTokenTrigger.ISSUE.value, + "created": ANY, + }, + ], + } + + # + # Search filter: created_from + def test_normal_3_3(self, client, db, personal_info_contract, monkeypatch): + test_account = config_eth_account("user1") + _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) + _keyfile = test_account["keyfile_json"] + + # Prepare data : Token + token_contract, create_param = deploy_bond_token_contract( + db, + _issuer_address, + issuer_private_key, + personal_info_contract.address, + created=datetime(2023, 5, 1, tzinfo=timezone("UTC")), + ) + _token_address = token_contract.address + + # prepare data + account = Account() + account.issuer_address = _issuer_address + account.keyfile = _keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + _token = Token() + _token.token_address = token_contract.address + _token.issuer_address = _issuer_address + _token.type = TokenType.IBET_STRAIGHT_BOND.value + _token.tx_hash = "" + _token.abi = "" + db.add(_token) + + _update_token_1 = UpdateToken() + _update_token_1.created = datetime(2023, 5, 2, tzinfo=timezone("UTC")) + _update_token_1.token_address = _token_address + _update_token_1.type = TokenType.IBET_STRAIGHT_BOND.value + _update_token_1.arguments = {"memo": "20230502"} + _update_token_1.original_contents = {} + _update_token_1.status = 1 + _update_token_1.trigger = UpdateTokenTrigger.UPDATE.value + db.add(_update_token_1) + _update_token_2 = UpdateToken() + _update_token_2.created = datetime(2023, 5, 3, tzinfo=timezone("UTC")) + _update_token_2.token_address = _token_address + _update_token_2.type = TokenType.IBET_STRAIGHT_BOND.value + _update_token_2.arguments = {"memo": "20230503"} + _update_token_2.original_contents = {} + _update_token_2.status = 1 + _update_token_2.trigger = UpdateTokenTrigger.UPDATE.value + db.add(_update_token_2) + _update_token_3 = UpdateToken() + _update_token_3.created = datetime(2023, 5, 4, tzinfo=timezone("UTC")) + _update_token_3.token_address = _token_address + _update_token_3.type = TokenType.IBET_STRAIGHT_BOND.value + _update_token_3.arguments = {"memo": "20230504"} + _update_token_3.original_contents = {} + _update_token_3.status = 1 + _update_token_3.trigger = UpdateTokenTrigger.UPDATE.value + db.add(_update_token_3) + db.commit() + + # request target API + resp = client.get( + self.base_url.format(_token_address), + params={ + "created_from": str(datetime(2023, 5, 3, 8, 0, 0)), + }, + ) + + original_after_issue = self.expected_original_after_issue( + create_param, _issuer_address, _token_address + ) + + # assertion + assert resp.status_code == 200 + assert resp.json() == { + "result_set": { + "count": 2, + "offset": None, + "limit": None, + "total": 4, + }, + "history": [ + { + "original_contents": {}, + "modified_contents": {"memo": "20230504"}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + { + "original_contents": {}, + "modified_contents": {"memo": "20230503"}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + ], + } + + # + # Search filter: created_to + def test_normal_3_4(self, client, db, personal_info_contract, monkeypatch): + test_account = config_eth_account("user1") + _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) + _keyfile = test_account["keyfile_json"] + + # Prepare data : Token + token_contract, create_param = deploy_bond_token_contract( + db, + _issuer_address, + issuer_private_key, + personal_info_contract.address, + created=datetime(2023, 5, 1, tzinfo=timezone("UTC")), + ) + _token_address = token_contract.address + + # prepare data + account = Account() + account.issuer_address = _issuer_address + account.keyfile = _keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + _token = Token() + _token.token_address = token_contract.address + _token.issuer_address = _issuer_address + _token.type = TokenType.IBET_STRAIGHT_BOND.value + _token.tx_hash = "" + _token.abi = "" + db.add(_token) + + _update_token_1 = UpdateToken() + _update_token_1.created = datetime(2023, 5, 2, tzinfo=timezone("UTC")) + _update_token_1.token_address = _token_address + _update_token_1.type = TokenType.IBET_STRAIGHT_BOND.value + _update_token_1.arguments = {"memo": "20230502"} + _update_token_1.original_contents = {} + _update_token_1.status = 1 + _update_token_1.trigger = UpdateTokenTrigger.UPDATE.value + db.add(_update_token_1) + _update_token_2 = UpdateToken() + _update_token_2.created = datetime(2023, 5, 3, tzinfo=timezone("UTC")) + _update_token_2.token_address = _token_address + _update_token_2.type = TokenType.IBET_STRAIGHT_BOND.value + _update_token_2.arguments = {"memo": "20230503"} + _update_token_2.original_contents = {} + _update_token_2.status = 1 + _update_token_2.trigger = UpdateTokenTrigger.UPDATE.value + db.add(_update_token_2) + _update_token_3 = UpdateToken() + _update_token_3.created = datetime(2023, 5, 4, tzinfo=timezone("UTC")) + _update_token_3.token_address = _token_address + _update_token_3.type = TokenType.IBET_STRAIGHT_BOND.value + _update_token_3.arguments = {"memo": "20230504"} + _update_token_3.original_contents = {} + _update_token_3.status = 1 + _update_token_3.trigger = UpdateTokenTrigger.UPDATE.value + db.add(_update_token_3) + db.commit() + + # request target API + resp = client.get( + self.base_url.format(_token_address), + params={ + "created_to": str(datetime(2023, 5, 2, 0, 0, 0)), + }, + ) + + # assertion + assert resp.status_code == 200 + assert resp.json() == { + "result_set": { + "count": 1, + "offset": None, + "limit": None, + "total": 4, + }, + "history": [ + { + "original_contents": None, + "modified_contents": create_param, + "trigger": UpdateTokenTrigger.ISSUE.value, + "created": ANY, + }, + ], + } + + # + # Sort Order + def test_normal_4_1(self, client, db, personal_info_contract): + test_account = config_eth_account("user1") + _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) + _keyfile = test_account["keyfile_json"] + + # Prepare data : Token + token_contract, create_param = deploy_bond_token_contract( + db, _issuer_address, issuer_private_key, personal_info_contract.address + ) + _token_address = token_contract.address + + # prepare data + account = Account() + account.issuer_address = _issuer_address + account.keyfile = _keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + _token = Token() + _token.token_address = token_contract.address + _token.issuer_address = _issuer_address + _token.type = TokenType.IBET_STRAIGHT_BOND.value + _token.tx_hash = "" + _token.abi = "" + db.add(_token) + db.commit() + + # create history + self.create_history_by_api(client, _token_address, _issuer_address) + + # request target API + resp = client.get( + self.base_url.format(_token_address), + params={ + "sort_order": 0, + }, + ) + + original_after_issue = self.expected_original_after_issue( + create_param, _issuer_address, _token_address + ) + + # assertion + assert resp.status_code == 200 + assert resp.json() == { + "result_set": { + "count": 4, + "offset": None, + "limit": None, + "total": 4, + }, + "history": [ + { + "original_contents": None, + "modified_contents": create_param, + "trigger": UpdateTokenTrigger.ISSUE.value, + "created": ANY, + }, + { + "original_contents": original_after_issue, + "modified_contents": {"face_value": 10000}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + { + "original_contents": { + **original_after_issue, + **{"face_value": 10000}, + }, + "modified_contents": {"interest_rate": 0.5}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + { + "original_contents": { + **original_after_issue, + **{"face_value": 10000}, + **{"interest_rate": 0.5}, + }, + "modified_contents": {"interest_payment_date": ["0101", "0701"]}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + ], + } + + # + # Sort Item + def test_normal_4_2(self, client, db, personal_info_contract): + test_account = config_eth_account("user1") + _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) + _keyfile = test_account["keyfile_json"] + + # Prepare data : Token + token_contract, create_param = deploy_bond_token_contract( + db, _issuer_address, issuer_private_key, personal_info_contract.address + ) + _token_address = token_contract.address + + # prepare data + account = Account() + account.issuer_address = _issuer_address + account.keyfile = _keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + _token = Token() + _token.token_address = token_contract.address + _token.issuer_address = _issuer_address + _token.type = TokenType.IBET_STRAIGHT_BOND.value + _token.tx_hash = "" + _token.abi = "" + db.add(_token) + db.commit() + + # create history + self.create_history_by_api(client, _token_address, _issuer_address) + + # request target API + resp = client.get( + self.base_url.format(_token_address), + params={ + "sort_order": 0, + "sort_item": "trigger", + }, + ) + + original_after_issue = self.expected_original_after_issue( + create_param, _issuer_address, _token_address + ) + + # assertion + assert resp.status_code == 200 + assert resp.json() == { + "result_set": { + "count": 4, + "offset": None, + "limit": None, + "total": 4, + }, + "history": [ + { + "original_contents": None, + "modified_contents": create_param, + "trigger": UpdateTokenTrigger.ISSUE.value, + "created": ANY, + }, + { + "original_contents": { + **original_after_issue, + **{"face_value": 10000}, + **{"interest_rate": 0.5}, + }, + "modified_contents": {"interest_payment_date": ["0101", "0701"]}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + { + "original_contents": { + **original_after_issue, + **{"face_value": 10000}, + }, + "modified_contents": {"interest_rate": 0.5}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + { + "original_contents": original_after_issue, + "modified_contents": {"face_value": 10000}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + ], + } + + # + # Pagination + def test_normal_5_1(self, client, db, personal_info_contract): + test_account = config_eth_account("user1") + _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) + _keyfile = test_account["keyfile_json"] + + # Prepare data : Token + token_contract, create_param = deploy_bond_token_contract( + db, _issuer_address, issuer_private_key, personal_info_contract.address + ) + _token_address = token_contract.address + + # prepare data + account = Account() + account.issuer_address = _issuer_address + account.keyfile = _keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + _token = Token() + _token.token_address = token_contract.address + _token.issuer_address = _issuer_address + _token.type = TokenType.IBET_STRAIGHT_BOND.value + _token.tx_hash = "" + _token.abi = "" + db.add(_token) + db.commit() + + # create history + self.create_history_by_api(client, _token_address, _issuer_address) + + # request target API + resp = client.get( + self.base_url.format(_token_address), + params={ + "limit": 2, + "offset": 1, + }, + ) + + original_after_issue = self.expected_original_after_issue( + create_param, _issuer_address, _token_address + ) + + # assertion + assert resp.status_code == 200 + assert resp.json() == { + "result_set": { + "count": 4, + "offset": 1, + "limit": 2, + "total": 4, + }, + "history": [ + { + "original_contents": { + **original_after_issue, + **{"face_value": 10000}, + }, + "modified_contents": {"interest_rate": 0.5}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + { + "original_contents": original_after_issue, + "modified_contents": {"face_value": 10000}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + ], + } + + # + # Pagination (over offset) + def test_normal_5_2(self, client, db, personal_info_contract): + test_account = config_eth_account("user1") + _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) + _keyfile = test_account["keyfile_json"] + + # Prepare data : Token + token_contract, create_param = deploy_bond_token_contract( + db, _issuer_address, issuer_private_key, personal_info_contract.address + ) + _token_address = token_contract.address + + # prepare data + account = Account() + account.issuer_address = _issuer_address + account.keyfile = _keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + _token = Token() + _token.token_address = token_contract.address + _token.issuer_address = _issuer_address + _token.type = TokenType.IBET_STRAIGHT_BOND.value + _token.tx_hash = "" + _token.abi = "" + db.add(_token) + db.commit() + + # create history + self.create_history_by_api(client, _token_address, _issuer_address) + + # request target API + resp = client.get( + self.base_url.format(_token_address), + params={ + "limit": 1, + "offset": 4, + }, + ) + + # assertion + assert resp.status_code == 200 + assert resp.json() == { + "result_set": { + "count": 4, + "offset": 4, + "limit": 1, + "total": 4, + }, + "history": [], + } + + ########################################################################### + # Error Case + ########################################################################### + + # + # RequestValidationError + # query(invalid value) + def test_error_1(self, client, db): + token_address = "0x0123456789012345678901234567890123456789" + + # request target api + resp = client.get( + self.base_url.format(token_address), + params={ + "trigger": "test", + "sort_order": "test", + "sort_item": "test", + "offset": "test", + "limit": "test", + }, + ) + + # assertion + assert resp.status_code == 422 + assert resp.json() == { + "meta": {"code": 1, "title": "RequestValidationError"}, + "detail": [ + { + "ctx": {"enum_values": ["Issue", "Update"]}, + "loc": ["query", "trigger"], + "msg": "value is not a valid enumeration member; permitted: " + "'Issue', 'Update'", + "type": "type_error.enum", + }, + { + "ctx": {"enum_values": ["created", "trigger"]}, + "loc": ["query", "sort_item"], + "msg": "value is not a valid enumeration member; permitted: " + "'created', 'trigger'", + "type": "type_error.enum", + }, + { + "loc": ["query", "sort_order"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + }, + { + "loc": ["query", "offset"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + }, + { + "loc": ["query", "limit"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + }, + ], + } diff --git a/tests/test_app_routers_share_tokens_POST.py b/tests/test_app_routers_share_tokens_POST.py index c3a89e9e..b39f2ec2 100644 --- a/tests/test_app_routers_share_tokens_POST.py +++ b/tests/test_app_routers_share_tokens_POST.py @@ -168,7 +168,10 @@ def test_normal_1_1(self, client, db): assert utxo.block_timestamp == datetime(2021, 4, 27, 12, 34, 56) update_token = db.query(UpdateToken).first() - assert update_token is None + assert update_token.token_address == "contract_address_test1" + assert update_token.type == TokenType.IBET_SHARE.value + assert update_token.status == 1 + assert update_token.trigger == "Issue" # # create only @@ -270,7 +273,10 @@ def test_normal_1_2(self, client, db): assert utxo.block_timestamp == datetime(2021, 4, 27, 12, 34, 56) update_token = db.query(UpdateToken).first() - assert update_token is None + assert update_token.token_address == "contract_address_test1" + assert update_token.type == TokenType.IBET_SHARE.value + assert update_token.status == 1 + assert update_token.trigger == "Issue" # # include updates @@ -508,7 +514,10 @@ def test_normal_3(self, client, db): assert utxo.block_timestamp == datetime(2021, 4, 27, 12, 34, 56) update_token = db.query(UpdateToken).first() - assert update_token is None + assert update_token.token_address == "contract_address_test1" + assert update_token.type == TokenType.IBET_SHARE.value + assert update_token.status == 1 + assert update_token.trigger == "Issue" # # YYYYMMDD parameter is not empty diff --git a/tests/test_app_routers_share_tokens_{token_address}_POST.py b/tests/test_app_routers_share_tokens_{token_address}_POST.py index 4ca0cad6..6451588c 100644 --- a/tests/test_app_routers_share_tokens_{token_address}_POST.py +++ b/tests/test_app_routers_share_tokens_{token_address}_POST.py @@ -18,14 +18,24 @@ """ import hashlib from unittest import mock -from unittest.mock import ANY, MagicMock +from unittest.mock import MagicMock +from eth_keyfile import decode_keyfile_json from web3 import Web3 from web3.middleware import geth_poa_middleware import config from app.exceptions import SendTransactionError -from app.model.db import Account, AuthToken, Token, TokenType +from app.model.blockchain import IbetShareContract +from app.model.db import ( + Account, + AuthToken, + Token, + TokenAttrUpdate, + TokenType, + UpdateToken, +) +from app.utils.contract_utils import ContractUtils from app.utils.e2ee_utils import E2EEUtils from tests.account_config import config_eth_account @@ -33,6 +43,28 @@ web3.middleware_onion.inject(geth_poa_middleware, layer=0) +def deploy_share_token_contract( + address, + private_key, +): + arguments = [ + "token.name", + "token.symbol", + 20, + 100, + 3, + "token.dividend_record_date", + "token.dividend_payment_date", + "token.cancellation_date", + 30, + ] + share_contract = IbetShareContract() + token_address, _, _ = share_contract.create(arguments, address, private_key) + + return ContractUtils.get_contract("IbetShare", token_address) + + +@mock.patch("app.model.blockchain.token.TX_GAS_LIMIT", 8000000) class TestAppRoutersShareTokensTokenAddressPOST: # target API endpoint base_url = "/share/tokens/{}" @@ -42,12 +74,21 @@ class TestAppRoutersShareTokensTokenAddressPOST: ########################################################################### # - @mock.patch("app.model.blockchain.token.IbetShareContract.update") - def test_normal_1(self, IbetShareContract_mock, client, db): + def test_normal_1(self, client, db): test_account = config_eth_account("user1") _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) _keyfile = test_account["keyfile_json"] - _token_address = "0x82b1c9374aB625380bd498a3d9dF4033B8A0E3Bb" + + # Prepare data : Token + token_contract = deploy_share_token_contract( + _issuer_address, + issuer_private_key, + ) + _token_address = token_contract.address # prepare data account = Account() @@ -64,9 +105,6 @@ def test_normal_1(self, IbetShareContract_mock, client, db): token.abi = "" db.add(token) - # mock - IbetShareContract_mock.side_effect = [None] - # request target API req_param = { "cancellation_date": "20221231", @@ -94,21 +132,80 @@ def test_normal_1(self, IbetShareContract_mock, client, db): }, ) - # assertion - IbetShareContract_mock.assert_any_call( - data=req_param, tx_from=_issuer_address, private_key=ANY - ) assert resp.status_code == 200 assert resp.json() is None + token_attr_update = ( + db.query(TokenAttrUpdate) + .filter(TokenAttrUpdate.token_address == _token_address) + .all() + ) + assert len(token_attr_update) == 1 + + update_token = db.query(UpdateToken).first() + assert update_token.token_address == _token_address + assert update_token.issuer_address == _issuer_address + assert update_token.type == TokenType.IBET_SHARE.value + assert update_token.original_contents == { + "cancellation_date": "token.cancellation_date", + "contact_information": "", + "contract_name": "IbetShare", + "dividend_payment_date": "token.dividend_payment_date", + "dividend_record_date": "token.dividend_record_date", + "dividends": 3e-13, + "is_canceled": False, + "is_offering": False, + "issue_price": 20, + "issuer_address": _issuer_address, + "memo": "", + "name": "token.name", + "personal_info_contract_address": "0x0000000000000000000000000000000000000000", + "principal_value": 30, + "privacy_policy": "", + "status": True, + "symbol": "token.symbol", + "token_address": _token_address, + "total_supply": 100, + "tradable_exchange_contract_address": "0x0000000000000000000000000000000000000000", + "transfer_approval_required": False, + "transferable": False, + } + assert update_token.arguments == { + "cancellation_date": "20221231", + "dividends": 345.67, + "dividend_record_date": "20211231", + "dividend_payment_date": "20211231", + "tradable_exchange_contract_address": "0xe883A6f441Ad5682d37DF31d34fc012bcB07A740", + "personal_info_contract_address": "0xa4CEe3b909751204AA151860ebBE8E7A851c2A1a", + "transferable": False, + "status": False, + "is_offering": False, + "contact_information": "問い合わせ先test", + "privacy_policy": "プライバシーポリシーtest", + "transfer_approval_required": False, + "principal_value": 1000, + "is_canceled": True, + "memo": "m" * 10000, + } + assert update_token.status == 1 + # # No request parameters - @mock.patch("app.model.blockchain.token.IbetShareContract.update") - def test_normal_2(self, IbetShareContract_mock, client, db): + def test_normal_2(self, client, db): test_account = config_eth_account("user1") _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) _keyfile = test_account["keyfile_json"] - _token_address = "0x82b1c9374aB625380bd498a3d9dF4033B8A0E3Bb" + + # Prepare data : Token + token_contract = deploy_share_token_contract( + _issuer_address, + issuer_private_key, + ) + _token_address = token_contract.address # prepare data account = Account() @@ -125,9 +222,6 @@ def test_normal_2(self, IbetShareContract_mock, client, db): token.abi = "" db.add(token) - # mock - IbetShareContract_mock.side_effect = [None] - # request target API req_param = {} resp = client.post( @@ -143,14 +237,33 @@ def test_normal_2(self, IbetShareContract_mock, client, db): assert resp.status_code == 200 assert resp.json() is None + token_attr_update = ( + db.query(TokenAttrUpdate) + .filter(TokenAttrUpdate.token_address == _token_address) + .all() + ) + assert len(token_attr_update) == 0 + + update_token = db.query(UpdateToken).first() + assert update_token is None + # # Authorization by auth-token - @mock.patch("app.model.blockchain.token.IbetShareContract.update") - def test_normal_3(self, IbetShareContract_mock, client, db): + def test_normal_3(self, client, db): test_account = config_eth_account("user1") _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) _keyfile = test_account["keyfile_json"] - _token_address = "0x82b1c9374aB625380bd498a3d9dF4033B8A0E3Bb" + + # Prepare data : Token + token_contract = deploy_share_token_contract( + _issuer_address, + issuer_private_key, + ) + _token_address = token_contract.address # prepare data account = Account() @@ -173,9 +286,6 @@ def test_normal_3(self, IbetShareContract_mock, client, db): token.abi = "" db.add(token) - # mock - IbetShareContract_mock.side_effect = [None] - # request target API req_param = { "cancellation_date": "20221231", @@ -204,20 +314,80 @@ def test_normal_3(self, IbetShareContract_mock, client, db): ) # assertion - IbetShareContract_mock.assert_any_call( - data=req_param, tx_from=_issuer_address, private_key=ANY - ) assert resp.status_code == 200 assert resp.json() is None + token_attr_update = ( + db.query(TokenAttrUpdate) + .filter(TokenAttrUpdate.token_address == _token_address) + .all() + ) + assert len(token_attr_update) == 1 + + update_token = db.query(UpdateToken).first() + assert update_token.token_address == _token_address + assert update_token.issuer_address == _issuer_address + assert update_token.type == TokenType.IBET_SHARE.value + assert update_token.original_contents == { + "cancellation_date": "token.cancellation_date", + "contact_information": "", + "contract_name": "IbetShare", + "dividend_payment_date": "token.dividend_payment_date", + "dividend_record_date": "token.dividend_record_date", + "dividends": 3e-13, + "is_canceled": False, + "is_offering": False, + "issue_price": 20, + "issuer_address": _issuer_address, + "memo": "", + "name": "token.name", + "personal_info_contract_address": "0x0000000000000000000000000000000000000000", + "principal_value": 30, + "privacy_policy": "", + "status": True, + "symbol": "token.symbol", + "token_address": _token_address, + "total_supply": 100, + "tradable_exchange_contract_address": "0x0000000000000000000000000000000000000000", + "transfer_approval_required": False, + "transferable": False, + } + assert update_token.arguments == { + "cancellation_date": "20221231", + "dividends": 345.67, + "dividend_record_date": "20211231", + "dividend_payment_date": "20211231", + "tradable_exchange_contract_address": "0xe883A6f441Ad5682d37DF31d34fc012bcB07A740", + "personal_info_contract_address": "0xa4CEe3b909751204AA151860ebBE8E7A851c2A1a", + "transferable": False, + "status": False, + "is_offering": False, + "contact_information": "問い合わせ先test", + "privacy_policy": "プライバシーポリシーtest", + "transfer_approval_required": False, + "principal_value": 1000, + "is_canceled": True, + "memo": "memo_test1", + } + assert update_token.status == 1 + # # YYYYMMDD parameter is not an empty string - @mock.patch("app.model.blockchain.token.IbetShareContract.update") - def test_normal_4_1(self, IbetShareContract_mock, client, db): + def test_normal_4_1(self, client, db): test_account = config_eth_account("user1") _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) _keyfile = test_account["keyfile_json"] - _token_address = "0x82b1c9374aB625380bd498a3d9dF4033B8A0E3Bb" + + # Prepare data : Token + token_contract = deploy_share_token_contract( + _issuer_address, + issuer_private_key, + ) + _token_address = token_contract.address # prepare data account = Account() @@ -234,9 +404,6 @@ def test_normal_4_1(self, IbetShareContract_mock, client, db): token.abi = "" db.add(token) - # mock - IbetShareContract_mock.side_effect = [None] - # request target API req_param = { "cancellation_date": "20221231", @@ -254,38 +421,69 @@ def test_normal_4_1(self, IbetShareContract_mock, client, db): ) # assertion - IbetShareContract_mock.assert_any_call( - data={ - "cancellation_date": "20221231", - "dividends": 345.67, - "dividend_record_date": "20211231", - "dividend_payment_date": "20211231", - "tradable_exchange_contract_address": None, - "personal_info_contract_address": None, - "transferable": None, - "status": None, - "is_offering": None, - "contact_information": None, - "privacy_policy": None, - "transfer_approval_required": None, - "principal_value": None, - "is_canceled": None, - "memo": None, - }, - tx_from=_issuer_address, - private_key=ANY, - ) assert resp.status_code == 200 assert resp.json() is None + token_attr_update = ( + db.query(TokenAttrUpdate) + .filter(TokenAttrUpdate.token_address == _token_address) + .all() + ) + assert len(token_attr_update) == 1 + + update_token = db.query(UpdateToken).first() + assert update_token.token_address == _token_address + assert update_token.issuer_address == _issuer_address + assert update_token.type == TokenType.IBET_SHARE.value + assert update_token.original_contents == { + "cancellation_date": "token.cancellation_date", + "contact_information": "", + "contract_name": "IbetShare", + "dividend_payment_date": "token.dividend_payment_date", + "dividend_record_date": "token.dividend_record_date", + "dividends": 3e-13, + "is_canceled": False, + "is_offering": False, + "issue_price": 20, + "issuer_address": _issuer_address, + "memo": "", + "name": "token.name", + "personal_info_contract_address": "0x0000000000000000000000000000000000000000", + "principal_value": 30, + "privacy_policy": "", + "status": True, + "symbol": "token.symbol", + "token_address": _token_address, + "total_supply": 100, + "tradable_exchange_contract_address": "0x0000000000000000000000000000000000000000", + "transfer_approval_required": False, + "transferable": False, + } + assert update_token.arguments == { + "cancellation_date": "20221231", + "dividends": 345.67, + "dividend_record_date": "20211231", + "dividend_payment_date": "20211231", + } + assert update_token.status == 1 + # # YYYYMMDD parameter is an empty string - @mock.patch("app.model.blockchain.token.IbetShareContract.update") - def test_normal_4_2(self, IbetShareContract_mock, client, db): + def test_normal_4_2(self, client, db): test_account = config_eth_account("user1") _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) _keyfile = test_account["keyfile_json"] - _token_address = "0x82b1c9374aB625380bd498a3d9dF4033B8A0E3Bb" + + # Prepare data : Token + token_contract = deploy_share_token_contract( + _issuer_address, + issuer_private_key, + ) + _token_address = token_contract.address # prepare data account = Account() @@ -302,9 +500,6 @@ def test_normal_4_2(self, IbetShareContract_mock, client, db): token.abi = "" db.add(token) - # mock - IbetShareContract_mock.side_effect = [None] - # request target API req_param = { "cancellation_date": "", @@ -322,30 +517,52 @@ def test_normal_4_2(self, IbetShareContract_mock, client, db): ) # assertion - IbetShareContract_mock.assert_any_call( - data={ - "cancellation_date": "", - "dividends": 345.67, - "dividend_record_date": "", - "dividend_payment_date": "", - "tradable_exchange_contract_address": None, - "personal_info_contract_address": None, - "transferable": None, - "status": None, - "is_offering": None, - "contact_information": None, - "privacy_policy": None, - "transfer_approval_required": None, - "principal_value": None, - "is_canceled": None, - "memo": None, - }, - tx_from=_issuer_address, - private_key=ANY, - ) assert resp.status_code == 200 assert resp.json() is None + token_attr_update = ( + db.query(TokenAttrUpdate) + .filter(TokenAttrUpdate.token_address == _token_address) + .all() + ) + assert len(token_attr_update) == 1 + + update_token = db.query(UpdateToken).first() + assert update_token.token_address == _token_address + assert update_token.issuer_address == _issuer_address + assert update_token.type == TokenType.IBET_SHARE.value + assert update_token.original_contents == { + "cancellation_date": "token.cancellation_date", + "contact_information": "", + "contract_name": "IbetShare", + "dividend_payment_date": "token.dividend_payment_date", + "dividend_record_date": "token.dividend_record_date", + "dividends": 3e-13, + "is_canceled": False, + "is_offering": False, + "issue_price": 20, + "issuer_address": _issuer_address, + "memo": "", + "name": "token.name", + "personal_info_contract_address": "0x0000000000000000000000000000000000000000", + "principal_value": 30, + "privacy_policy": "", + "status": True, + "symbol": "token.symbol", + "token_address": _token_address, + "total_supply": 100, + "tradable_exchange_contract_address": "0x0000000000000000000000000000000000000000", + "transfer_approval_required": False, + "transferable": False, + } + assert update_token.arguments == { + "cancellation_date": "", + "dividends": 345.67, + "dividend_record_date": "", + "dividend_payment_date": "", + } + assert update_token.status == 1 + ########################################################################### # Error Case ########################################################################### diff --git a/tests/test_app_routers_share_tokens_{token_address}_history_GET.py b/tests/test_app_routers_share_tokens_{token_address}_history_GET.py new file mode 100644 index 00000000..4cb8eb8a --- /dev/null +++ b/tests/test_app_routers_share_tokens_{token_address}_history_GET.py @@ -0,0 +1,1119 @@ +""" +Copyright BOOSTRY Co., Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. + +You may obtain a copy of the License at +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +See the License for the specific language governing permissions and +limitations under the License. + +SPDX-License-Identifier: Apache-2.0 +""" +import json +from datetime import datetime +from unittest import mock +from unittest.mock import ANY + +from _decimal import Decimal +from eth_keyfile import decode_keyfile_json +from pytz import timezone +from starlette.testclient import TestClient +from web3 import Web3 +from web3.contract import Contract +from web3.middleware import geth_poa_middleware + +import config +from app.model.blockchain import IbetShareContract +from app.model.db import Account, Token, TokenType, UpdateToken, UpdateTokenTrigger +from app.model.schema import IbetShareCreate +from app.utils.contract_utils import ContractUtils +from app.utils.e2ee_utils import E2EEUtils +from tests.account_config import config_eth_account + +web3 = Web3(Web3.HTTPProvider(config.WEB3_HTTP_PROVIDER)) +web3.middleware_onion.inject(geth_poa_middleware, layer=0) + + +def deploy_share_token_contract( + session, + address, + private_key, + personal_info_contract_address, + tradable_exchange_contract_address=config.ZERO_ADDRESS, + transfer_approval_required=True, + created: datetime | None = None, +) -> (Contract, dict): + arguments = [ + "token.name", + "token.symbol", + 20, + 100, + 3 * 10000000000000, + "20230501", + "20230501", + "20230501", + 30, + ] + share_contract = IbetShareContract() + token_address, _, _ = share_contract.create(arguments, address, private_key) + contract = ContractUtils.get_contract("IbetShare", token_address) + token_create_param = IbetShareCreate( + name="token.name", + symbol="token.symbol", + issue_price=20, + total_supply=100, + dividends=3, + dividend_record_date="20230501", + dividend_payment_date="20230501", + cancellation_date="20230501", + principal_value=30, + transferable=False, # update + status=True, # update + is_offering=True, # update + tradable_exchange_contract_address=tradable_exchange_contract_address, # update + personal_info_contract_address=personal_info_contract_address, # update + contact_information="contact info test", # update + privacy_policy="privacy policy test", # update + transfer_approval_required=transfer_approval_required, # update + is_canceled=True, # update + ).__dict__ + + update_token = UpdateToken() + update_token.token_address = token_address + update_token.type = TokenType.IBET_SHARE.value + update_token.issuer_address = address + update_token.arguments = token_create_param + update_token.original_contents = None + update_token.status = 1 + update_token.trigger = UpdateTokenTrigger.ISSUE.value + if created: + update_token.created = created + session.add(update_token) + + build_tx_param = { + "chainId": config.CHAIN_ID, + "from": address, + "gas": config.TX_GAS_LIMIT, + "gasPrice": 0, + } + tx = contract.functions.setTransferable( + token_create_param["transferable"] + ).build_transaction(build_tx_param) + ContractUtils.send_transaction(transaction=tx, private_key=private_key) + tx = contract.functions.setStatus(token_create_param["status"]).build_transaction( + build_tx_param + ) + ContractUtils.send_transaction(transaction=tx, private_key=private_key) + tx = contract.functions.changeOfferingStatus( + token_create_param["is_offering"] + ).build_transaction(build_tx_param) + ContractUtils.send_transaction(transaction=tx, private_key=private_key) + tx = contract.functions.setTradableExchange( + token_create_param["tradable_exchange_contract_address"] + ).build_transaction(build_tx_param) + ContractUtils.send_transaction(transaction=tx, private_key=private_key) + tx = contract.functions.setPersonalInfoAddress( + token_create_param["personal_info_contract_address"] + ).build_transaction(build_tx_param) + ContractUtils.send_transaction(transaction=tx, private_key=private_key) + tx = contract.functions.setContactInformation( + token_create_param["contact_information"] + ).build_transaction(build_tx_param) + ContractUtils.send_transaction(transaction=tx, private_key=private_key) + tx = contract.functions.setPrivacyPolicy( + token_create_param["privacy_policy"] + ).build_transaction(build_tx_param) + ContractUtils.send_transaction(transaction=tx, private_key=private_key) + tx = contract.functions.setTransferApprovalRequired( + token_create_param["transfer_approval_required"] + ).build_transaction(build_tx_param) + ContractUtils.send_transaction(transaction=tx, private_key=private_key) + tx = contract.functions.changeToCanceled().build_transaction(build_tx_param) + ContractUtils.send_transaction(transaction=tx, private_key=private_key) + + return contract, token_create_param + + +@mock.patch("app.model.blockchain.token.TX_GAS_LIMIT", 8000000) +class TestAppRoutersShareTokensTokenAddressHistoryGET: + # target API endpoint + base_url = "/share/tokens/{}/history" + + @staticmethod + def create_history_by_api( + client: TestClient, token_address: str, issuer_address: str + ): + client.post( + f"/share/tokens/{token_address}", + json={ + "dividends": 1, + "dividend_record_date": "20230502", + "dividend_payment_date": "20230502", + "memo": None, + }, + headers={ + "issuer-address": issuer_address, + "eoa-password": E2EEUtils.encrypt("password"), + }, + ) + client.post( + f"/share/tokens/{token_address}", + json={"memo": "." * 10000}, + headers={ + "issuer-address": issuer_address, + "eoa-password": E2EEUtils.encrypt("password"), + }, + ) + client.post( + f"/share/tokens/{token_address}", + json={"is_offering": False, "memo": None}, + headers={ + "issuer-address": issuer_address, + "eoa-password": E2EEUtils.encrypt("password"), + }, + ) + + @staticmethod + def expected_original_after_issue( + create_token_param: dict, issuer_address: str, token_address: str + ): + return { + **create_token_param, + "contract_name": "IbetShare", + "issuer_address": issuer_address, + "memo": "", + "token_address": token_address, + } + + ########################################################################### + # Normal Case + ########################################################################### + + # + # 0 record + def test_normal_1(self, client, db, personal_info_contract): + test_account = config_eth_account("user1") + _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) + _keyfile = test_account["keyfile_json"] + + # prepare data: Token + account = Account() + account.issuer_address = _issuer_address + account.keyfile = _keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + _token = Token() + _token.token_address = "no_record_address" + _token.issuer_address = _issuer_address + _token.type = TokenType.IBET_SHARE.value + _token.tx_hash = "" + _token.abi = "" + db.add(_token) + + # request target api + resp = client.get( + self.base_url.format(_token.token_address), + ) + + # assertion + assert resp.status_code == 200 + assert resp.json() == { + "result_set": { + "count": 0, + "offset": None, + "limit": None, + "total": 0, + }, + "history": [], + } + + # + # Multiple record + def test_normal_2(self, client, db, personal_info_contract): + test_account = config_eth_account("user1") + _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) + _keyfile = test_account["keyfile_json"] + + # Prepare data : Token + token_contract, create_param = deploy_share_token_contract( + db, _issuer_address, issuer_private_key, personal_info_contract.address + ) + _token_address = token_contract.address + + # prepare data + account = Account() + account.issuer_address = _issuer_address + account.keyfile = _keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + _token = Token() + _token.token_address = token_contract.address + _token.issuer_address = _issuer_address + _token.type = TokenType.IBET_SHARE.value + _token.tx_hash = "" + _token.abi = "" + db.add(_token) + db.commit() + + # create history + self.create_history_by_api(client, _token_address, _issuer_address) + + # request target API + resp = client.get( + self.base_url.format(_token_address), + ) + original_after_issue = self.expected_original_after_issue( + create_param, _issuer_address, _token_address + ) + + # assertion + assert resp.status_code == 200 + assert resp.json() == { + "result_set": { + "count": 4, + "offset": None, + "limit": None, + "total": 4, + }, + "history": [ + { + "original_contents": { + **original_after_issue, + **{ + "dividends": 1.0, + "dividend_record_date": "20230502", + "dividend_payment_date": "20230502", + }, + **{"memo": "." * 10000}, + }, + "modified_contents": {"is_offering": False}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + { + "original_contents": { + **original_after_issue, + **{ + "dividends": 1.0, + "dividend_record_date": "20230502", + "dividend_payment_date": "20230502", + }, + }, + "modified_contents": {"memo": "." * 10000}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + { + "original_contents": original_after_issue, + "modified_contents": { + "dividends": 1.0, + "dividend_record_date": "20230502", + "dividend_payment_date": "20230502", + }, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + { + "original_contents": None, + "modified_contents": create_param, + "trigger": UpdateTokenTrigger.ISSUE.value, + "created": ANY, + }, + ], + } + + # + # Search filter: trigger + def test_normal_3_1(self, client, db, personal_info_contract): + test_account = config_eth_account("user1") + _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) + _keyfile = test_account["keyfile_json"] + + # Prepare data : Token + token_contract, create_param = deploy_share_token_contract( + db, _issuer_address, issuer_private_key, personal_info_contract.address + ) + _token_address = token_contract.address + + # prepare data + account = Account() + account.issuer_address = _issuer_address + account.keyfile = _keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + _token = Token() + _token.token_address = token_contract.address + _token.issuer_address = _issuer_address + _token.type = TokenType.IBET_SHARE.value + _token.tx_hash = "" + _token.abi = "" + db.add(_token) + db.commit() + + # create history + self.create_history_by_api(client, _token_address, _issuer_address) + + # request target API + resp = client.get( + self.base_url.format(_token_address), + params={ + "trigger": "Update", + }, + ) + + original_after_issue = self.expected_original_after_issue( + create_param, _issuer_address, _token_address + ) + + # assertion + assert resp.status_code == 200 + assert resp.json() == { + "result_set": { + "count": 3, + "offset": None, + "limit": None, + "total": 4, + }, + "history": [ + { + "original_contents": { + **original_after_issue, + **{ + "dividends": 1.0, + "dividend_record_date": "20230502", + "dividend_payment_date": "20230502", + }, + **{"memo": "." * 10000}, + }, + "modified_contents": {"is_offering": False}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + { + "original_contents": { + **original_after_issue, + **{ + "dividends": 1.0, + "dividend_record_date": "20230502", + "dividend_payment_date": "20230502", + }, + }, + "modified_contents": {"memo": "." * 10000}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + { + "original_contents": original_after_issue, + "modified_contents": { + "dividends": 1.0, + "dividend_record_date": "20230502", + "dividend_payment_date": "20230502", + }, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + ], + } + + # + # Search filter: modified_contents + def test_normal_3_2(self, client, db, personal_info_contract): + test_account = config_eth_account("user1") + _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) + _keyfile = test_account["keyfile_json"] + + # Prepare data : Token + token_contract, create_param = deploy_share_token_contract( + db, _issuer_address, issuer_private_key, personal_info_contract.address + ) + _token_address = token_contract.address + + # prepare data + account = Account() + account.issuer_address = _issuer_address + account.keyfile = _keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + _token = Token() + _token.token_address = token_contract.address + _token.issuer_address = _issuer_address + _token.type = TokenType.IBET_SHARE.value + _token.tx_hash = "" + _token.abi = "" + db.add(_token) + db.commit() + + # create history + self.create_history_by_api(client, _token_address, _issuer_address) + + # request target API + resp = client.get( + self.base_url.format(_token_address), + params={ + "modified_contents": "is_offering", + }, + ) + + original_after_issue = self.expected_original_after_issue( + create_param, _issuer_address, _token_address + ) + + # assertion + assert resp.status_code == 200 + assert resp.json() == { + "result_set": { + "count": 2, + "offset": None, + "limit": None, + "total": 4, + }, + "history": [ + { + "original_contents": { + **original_after_issue, + **{ + "dividends": 1.0, + "dividend_record_date": "20230502", + "dividend_payment_date": "20230502", + }, + **{"memo": "." * 10000}, + }, + "modified_contents": {"is_offering": False}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + { + "original_contents": None, + "modified_contents": create_param, + "trigger": UpdateTokenTrigger.ISSUE.value, + "created": ANY, + }, + ], + } + + # + # Search filter: created_from + def test_normal_3_3(self, client, db, personal_info_contract): + test_account = config_eth_account("user1") + _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) + _keyfile = test_account["keyfile_json"] + + # Prepare data : Token + token_contract, create_param = deploy_share_token_contract( + db, + _issuer_address, + issuer_private_key, + personal_info_contract.address, + created=datetime(2023, 5, 1, tzinfo=timezone("UTC")), + ) + _token_address = token_contract.address + + # prepare data + account = Account() + account.issuer_address = _issuer_address + account.keyfile = _keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + _token = Token() + _token.token_address = token_contract.address + _token.issuer_address = _issuer_address + _token.type = TokenType.IBET_SHARE.value + _token.tx_hash = "" + _token.abi = "" + db.add(_token) + + _update_token_1 = UpdateToken() + _update_token_1.created = datetime(2023, 5, 2, tzinfo=timezone("UTC")) + _update_token_1.token_address = _token_address + _update_token_1.type = TokenType.IBET_SHARE.value + _update_token_1.arguments = {"memo": "20230502"} + _update_token_1.original_contents = {} + _update_token_1.status = 1 + _update_token_1.trigger = UpdateTokenTrigger.UPDATE.value + db.add(_update_token_1) + _update_token_2 = UpdateToken() + _update_token_2.created = datetime(2023, 5, 3, tzinfo=timezone("UTC")) + _update_token_2.token_address = _token_address + _update_token_2.type = TokenType.IBET_SHARE.value + _update_token_2.arguments = {"memo": "20230503"} + _update_token_2.original_contents = {} + _update_token_2.status = 1 + _update_token_2.trigger = UpdateTokenTrigger.UPDATE.value + db.add(_update_token_2) + _update_token_3 = UpdateToken() + _update_token_3.created = datetime(2023, 5, 4, tzinfo=timezone("UTC")) + _update_token_3.token_address = _token_address + _update_token_3.type = TokenType.IBET_SHARE.value + _update_token_3.arguments = {"memo": "20230504"} + _update_token_3.original_contents = {} + _update_token_3.status = 1 + _update_token_3.trigger = UpdateTokenTrigger.UPDATE.value + db.add(_update_token_3) + db.commit() + + # request target API + resp = client.get( + self.base_url.format(_token_address), + params={ + "created_from": str(datetime(2023, 5, 3, 8, 0, 0)), + }, + ) + + # assertion + assert resp.status_code == 200 + assert resp.json() == { + "result_set": { + "count": 2, + "offset": None, + "limit": None, + "total": 4, + }, + "history": [ + { + "original_contents": {}, + "modified_contents": {"memo": "20230504"}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + { + "original_contents": {}, + "modified_contents": {"memo": "20230503"}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + ], + } + + # + # Search filter: created_to + def test_normal_3_4(self, client, db, personal_info_contract): + test_account = config_eth_account("user1") + _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) + _keyfile = test_account["keyfile_json"] + + # Prepare data : Token + token_contract, create_param = deploy_share_token_contract( + db, + _issuer_address, + issuer_private_key, + personal_info_contract.address, + created=datetime(2023, 5, 1, tzinfo=timezone("UTC")), + ) + _token_address = token_contract.address + + # prepare data + account = Account() + account.issuer_address = _issuer_address + account.keyfile = _keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + _token = Token() + _token.token_address = token_contract.address + _token.issuer_address = _issuer_address + _token.type = TokenType.IBET_SHARE.value + _token.tx_hash = "" + _token.abi = "" + db.add(_token) + + _update_token_1 = UpdateToken() + _update_token_1.created = datetime(2023, 5, 2, tzinfo=timezone("UTC")) + _update_token_1.token_address = _token_address + _update_token_1.type = TokenType.IBET_SHARE.value + _update_token_1.arguments = {"memo": "20230502"} + _update_token_1.original_contents = {} + _update_token_1.status = 1 + _update_token_1.trigger = UpdateTokenTrigger.UPDATE.value + db.add(_update_token_1) + _update_token_2 = UpdateToken() + _update_token_2.created = datetime(2023, 5, 3, tzinfo=timezone("UTC")) + _update_token_2.token_address = _token_address + _update_token_2.type = TokenType.IBET_SHARE.value + _update_token_2.arguments = {"memo": "20230503"} + _update_token_2.original_contents = {} + _update_token_2.status = 1 + _update_token_2.trigger = UpdateTokenTrigger.UPDATE.value + db.add(_update_token_2) + _update_token_3 = UpdateToken() + _update_token_3.created = datetime(2023, 5, 4, tzinfo=timezone("UTC")) + _update_token_3.token_address = _token_address + _update_token_3.type = TokenType.IBET_SHARE.value + _update_token_3.arguments = {"memo": "20230504"} + _update_token_3.original_contents = {} + _update_token_3.status = 1 + _update_token_3.trigger = UpdateTokenTrigger.UPDATE.value + db.add(_update_token_3) + db.commit() + + # request target API + resp = client.get( + self.base_url.format(_token_address), + params={ + "created_to": str(datetime(2023, 5, 2, 0, 0, 0)), + }, + ) + + # assertion + assert resp.status_code == 200 + assert resp.json() == { + "result_set": { + "count": 1, + "offset": None, + "limit": None, + "total": 4, + }, + "history": [ + { + "original_contents": None, + "modified_contents": create_param, + "trigger": UpdateTokenTrigger.ISSUE.value, + "created": ANY, + }, + ], + } + + # + # Sort Order + def test_normal_4_1(self, client, db, personal_info_contract): + test_account = config_eth_account("user1") + _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) + _keyfile = test_account["keyfile_json"] + + # Prepare data : Token + token_contract, create_param = deploy_share_token_contract( + db, _issuer_address, issuer_private_key, personal_info_contract.address + ) + _token_address = token_contract.address + + # prepare data + account = Account() + account.issuer_address = _issuer_address + account.keyfile = _keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + _token = Token() + _token.token_address = token_contract.address + _token.issuer_address = _issuer_address + _token.type = TokenType.IBET_SHARE.value + _token.tx_hash = "" + _token.abi = "" + db.add(_token) + db.commit() + + # create history + self.create_history_by_api(client, _token_address, _issuer_address) + + # request target API + resp = client.get( + self.base_url.format(_token_address), + params={ + "sort_order": 0, + }, + ) + + original_after_issue = self.expected_original_after_issue( + create_param, _issuer_address, _token_address + ) + + # assertion + assert resp.status_code == 200 + assert resp.json() == { + "result_set": { + "count": 4, + "offset": None, + "limit": None, + "total": 4, + }, + "history": [ + { + "original_contents": None, + "modified_contents": create_param, + "trigger": UpdateTokenTrigger.ISSUE.value, + "created": ANY, + }, + { + "original_contents": original_after_issue, + "modified_contents": { + "dividends": 1.0, + "dividend_record_date": "20230502", + "dividend_payment_date": "20230502", + }, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + { + "original_contents": { + **original_after_issue, + **{ + "dividends": 1.0, + "dividend_record_date": "20230502", + "dividend_payment_date": "20230502", + }, + }, + "modified_contents": {"memo": "." * 10000}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + { + "original_contents": { + **original_after_issue, + **{ + "dividends": 1.0, + "dividend_record_date": "20230502", + "dividend_payment_date": "20230502", + }, + **{"memo": "." * 10000}, + }, + "modified_contents": {"is_offering": False}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + ], + } + + # + # Sort Item + def test_normal_4_2(self, client, db, personal_info_contract): + test_account = config_eth_account("user1") + _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) + _keyfile = test_account["keyfile_json"] + + # Prepare data : Token + token_contract, create_param = deploy_share_token_contract( + db, _issuer_address, issuer_private_key, personal_info_contract.address + ) + _token_address = token_contract.address + + # prepare data + account = Account() + account.issuer_address = _issuer_address + account.keyfile = _keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + _token = Token() + _token.token_address = token_contract.address + _token.issuer_address = _issuer_address + _token.type = TokenType.IBET_SHARE.value + _token.tx_hash = "" + _token.abi = "" + db.add(_token) + db.commit() + + # create history + self.create_history_by_api(client, _token_address, _issuer_address) + + # request target API + resp = client.get( + self.base_url.format(_token_address), + params={ + "sort_order": 0, + "sort_item": "trigger", + }, + ) + + original_after_issue = self.expected_original_after_issue( + create_param, _issuer_address, _token_address + ) + + # assertion + assert resp.status_code == 200 + assert resp.json() == { + "result_set": { + "count": 4, + "offset": None, + "limit": None, + "total": 4, + }, + "history": [ + { + "original_contents": None, + "modified_contents": create_param, + "trigger": UpdateTokenTrigger.ISSUE.value, + "created": ANY, + }, + { + "original_contents": { + **original_after_issue, + **{ + "dividends": 1.0, + "dividend_record_date": "20230502", + "dividend_payment_date": "20230502", + }, + **{"memo": "." * 10000}, + }, + "modified_contents": {"is_offering": False}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + { + "original_contents": { + **original_after_issue, + **{ + "dividends": 1.0, + "dividend_record_date": "20230502", + "dividend_payment_date": "20230502", + }, + }, + "modified_contents": {"memo": "." * 10000}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + { + "original_contents": original_after_issue, + "modified_contents": { + "dividends": 1.0, + "dividend_record_date": "20230502", + "dividend_payment_date": "20230502", + }, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + ], + } + + # + # Pagination + def test_normal_5_1(self, client, db, personal_info_contract): + test_account = config_eth_account("user1") + _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) + _keyfile = test_account["keyfile_json"] + + # Prepare data : Token + token_contract, create_param = deploy_share_token_contract( + db, _issuer_address, issuer_private_key, personal_info_contract.address + ) + _token_address = token_contract.address + + # prepare data + account = Account() + account.issuer_address = _issuer_address + account.keyfile = _keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + _token = Token() + _token.token_address = token_contract.address + _token.issuer_address = _issuer_address + _token.type = TokenType.IBET_SHARE.value + _token.tx_hash = "" + _token.abi = "" + db.add(_token) + db.commit() + + # create history + self.create_history_by_api(client, _token_address, _issuer_address) + + # request target API + resp = client.get( + self.base_url.format(_token_address), + params={ + "limit": 2, + "offset": 1, + }, + ) + + original_after_issue = self.expected_original_after_issue( + create_param, _issuer_address, _token_address + ) + + # assertion + assert resp.status_code == 200 + assert resp.json() == { + "result_set": { + "count": 4, + "offset": 1, + "limit": 2, + "total": 4, + }, + "history": [ + { + "original_contents": { + **original_after_issue, + **{ + "dividends": 1.0, + "dividend_record_date": "20230502", + "dividend_payment_date": "20230502", + }, + }, + "modified_contents": {"memo": "." * 10000}, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + { + "original_contents": original_after_issue, + "modified_contents": { + "dividends": 1.0, + "dividend_record_date": "20230502", + "dividend_payment_date": "20230502", + }, + "trigger": UpdateTokenTrigger.UPDATE.value, + "created": ANY, + }, + ], + } + + # + # Pagination (over offset) + def test_normal_5_2(self, client, db, personal_info_contract): + test_account = config_eth_account("user1") + _issuer_address = test_account["address"] + issuer_private_key = decode_keyfile_json( + raw_keyfile_json=test_account["keyfile_json"], + password="password".encode("utf-8"), + ) + _keyfile = test_account["keyfile_json"] + + # Prepare data : Token + token_contract, create_param = deploy_share_token_contract( + db, _issuer_address, issuer_private_key, personal_info_contract.address + ) + _token_address = token_contract.address + + # prepare data + account = Account() + account.issuer_address = _issuer_address + account.keyfile = _keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + _token = Token() + _token.token_address = token_contract.address + _token.issuer_address = _issuer_address + _token.type = TokenType.IBET_SHARE.value + _token.tx_hash = "" + _token.abi = "" + db.add(_token) + db.commit() + + # create history + self.create_history_by_api(client, _token_address, _issuer_address) + + # request target API + resp = client.get( + self.base_url.format(_token_address), + params={ + "limit": 1, + "offset": 4, + }, + ) + + # assertion + assert resp.status_code == 200 + assert resp.json() == { + "result_set": { + "count": 4, + "offset": 4, + "limit": 1, + "total": 4, + }, + "history": [], + } + + ########################################################################### + # Error Case + ########################################################################### + + # + # RequestValidationError + # query(invalid value) + def test_error_1(self, client, db): + token_address = "0x0123456789012345678901234567890123456789" + + # request target api + resp = client.get( + self.base_url.format(token_address), + params={ + "trigger": "test", + "sort_order": "test", + "sort_item": "test", + "offset": "test", + "limit": "test", + }, + ) + + # assertion + assert resp.status_code == 422 + assert resp.json() == { + "meta": {"code": 1, "title": "RequestValidationError"}, + "detail": [ + { + "ctx": {"enum_values": ["Issue", "Update"]}, + "loc": ["query", "trigger"], + "msg": "value is not a valid enumeration member; permitted: " + "'Issue', 'Update'", + "type": "type_error.enum", + }, + { + "ctx": {"enum_values": ["created", "trigger"]}, + "loc": ["query", "sort_item"], + "msg": "value is not a valid enumeration member; permitted: " + "'created', 'trigger'", + "type": "type_error.enum", + }, + { + "loc": ["query", "sort_order"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + }, + { + "loc": ["query", "offset"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + }, + { + "loc": ["query", "limit"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + }, + ], + } From e9fa63bfb565da6eb02f3d21ab1b130f8c53d2dd Mon Sep 17 00:00:00 2001 From: Yosuke Otosu Date: Mon, 1 May 2023 21:31:00 +0900 Subject: [PATCH 2/2] fix: tests --- tests/model/blockchain/test_token_IbetShare.py | 4 +--- tests/model/blockchain/test_token_IbetStraightBond.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/model/blockchain/test_token_IbetShare.py b/tests/model/blockchain/test_token_IbetShare.py index 5472e325..ad51967f 100644 --- a/tests/model/blockchain/test_token_IbetShare.py +++ b/tests/model/blockchain/test_token_IbetShare.py @@ -604,9 +604,7 @@ def test_normal_1(self, db): assert share_contract.memo == "" _token_attr_update = db.query(TokenAttrUpdate).first() - assert _token_attr_update.id == 1 - assert _token_attr_update.token_address == contract_address - assert _token_attr_update.updated_datetime > pre_datetime + assert _token_attr_update is None # # Update all items diff --git a/tests/model/blockchain/test_token_IbetStraightBond.py b/tests/model/blockchain/test_token_IbetStraightBond.py index c5466e50..bae405bc 100644 --- a/tests/model/blockchain/test_token_IbetStraightBond.py +++ b/tests/model/blockchain/test_token_IbetStraightBond.py @@ -682,9 +682,7 @@ def test_normal_1(self, db): assert bond_contract.memo == "" _token_attr_update = db.query(TokenAttrUpdate).first() - assert _token_attr_update.id == 1 - assert _token_attr_update.token_address == contract_address - assert _token_attr_update.updated_datetime > pre_datetime + assert _token_attr_update is None # # Update all items