Compare commits

..

135 Commits

Author SHA1 Message Date
004e30af31 Merge branch 'master' of gitlab.com:grassrootseconomics/cic-internal-integration into lash/improve-cache 2022-01-04 18:59:11 +03:00
nolash
7618abcda3 Bump deps 2021-12-22 21:31:39 +00:00
nolash
94d8ddb164 Amend okota dependency conflict 2021-12-22 20:34:12 +00:00
nolash
7f958d4be8 Rehabiliate test for balance incoming check (already counted from PENDING state) 2021-12-22 20:23:02 +00:00
nolash
03b06ca8c1 Merge remote-tracking branch 'origin/master' into lash/improve-cache 2021-12-22 20:09:54 +00:00
nolash
14449f5c6d Rehabilitate tests for cic-eth 2021-12-22 20:02:05 +00:00
nolash
15618fa061 Rehabilitate tests after url format change 2021-12-22 19:12:08 +00:00
nolash
3a52a78e93 Change poker method 2021-12-21 14:23:33 +00:00
nolash
6562d37a30 Add demurrage poker script 2021-12-21 14:19:26 +00:00
nolash
c5efa56885 Loosen requirements restrictions, fix missing explicit nonce in global contract deploy last step 2021-12-21 14:18:09 +00:00
d6346bb87b Merge "origin/master" into "lash/improve-cache" 2021-12-20 11:14:04 +03:00
9050d331cd Bumps lib versions 2021-12-20 11:12:39 +03:00
99997df248 Bumps deps 2021-12-20 11:11:23 +03:00
d04a4bf5c6 Adds missing import: tempfile 2021-12-20 11:10:35 +03:00
43c49dd527 Adds minor fixes:
- Added appropriate imports:
  - from okota.token_index.index import to_identifier
  - from cic_eth.error import SignerError
- cleaned up balance func.
2021-12-20 11:07:41 +03:00
511557c242 Cleaned up docker-compose.yml:
- Added ETH_MIN_FEE_LIMIT.
- Removed "" from callback task definitions.
2021-12-20 11:03:23 +03:00
887799962f Couldn't get python setup.py install to work without it fussing:
```
 => => # Processing dependencies for cic-cache==0.3.1
 => => # Searching for eth-contract-registry~=0.7.1a2
 => => # Reading https://pypi.org/simple/eth-contract-registry/
 => => # Couldn't retrieve index page for 'eth-contract-registry'
 => => # Scanning index of all packages (this may take a while)
 => => # Reading https://pypi.org/simple/
```
2021-12-20 11:01:39 +03:00
3acc3cf417 Bumps deps. 2021-12-20 10:58:21 +03:00
ceeb246ce2 Retires unused override_requirements.txt file. 2021-12-20 10:56:30 +03:00
45499ec839 Refactor to match proof checked for by verify_proofs task. 2021-12-20 10:56:01 +03:00
77bdee049c Adds token decimals to demurrage token deployment. 2021-12-20 10:54:28 +03:00
nolash
0cf6489f49 Merge remote-tracking branch 'origin/master' into lash/improve-cache 2021-12-08 06:44:31 +01:00
nolash
31256b3650 Remove custom port in pip url in dockers 2021-12-07 21:54:04 +01:00
nolash
380550cb84 Add higher fee limit in base task 2021-12-07 21:45:07 +01:00
nolash
a356585c6a Remove port from pip 2021-12-06 17:45:44 +01:00
nolash
4809bc8c22 Bump confini 2021-12-06 14:21:26 +01:00
nolash
760f618943 WIP upgrade deps 2021-12-04 11:39:13 +01:00
nolash
39de1837c2 Upgrade deps to non-prerelease (temporarily removed transfer-auth) 2021-11-15 14:23:24 +01:00
nolash
97e45c87d7 WIP move to whole patch versions in deps 2021-11-15 14:07:54 +01:00
nolash
4658a5d8e5 Bump cic-cache version 2021-11-13 07:51:01 +01:00
nolash
995d4e0bd0 Add remaining database name prefix changes 2021-11-10 09:55:30 +01:00
nolash
140b72a72b Use database prefix instead of name 2021-11-10 09:07:23 +01:00
nolash
21b0c4a48b Change query parse order 2021-11-08 09:58:22 +01:00
nolash
0b66462c11 Update openapi spec, enable queries with no ranges 2021-11-04 09:42:35 +01:00
nolash
f18f865231 WIP openapi spec for cic-cache-server 2021-11-04 07:59:38 +01:00
nolash
ad1c241a85 Reorganize url path params in cic-cache-server 2021-11-04 06:06:34 +01:00
nolash
99b0fb5aed Merge branch 'lash/verify-cache' into lash/bloxberg-seeding 2021-11-04 04:26:50 +01:00
nolash
29423449b7 Merge remote-tracking branch 'origin/master' into lash/verify-cache 2021-11-04 04:23:47 +01:00
nolash
58e766aa58 Remove explicit config in db migration 2021-11-04 04:18:27 +01:00
nolash
2ebcd3e3de Merge remote-tracking branch 'origin/master' into lash/bloxberg-seeding 2021-11-02 18:49:49 +01:00
nolash
c440b049cc Add config dirs 2021-11-02 16:35:44 +01:00
nolash
09034af5bc Bump cic-eth version 2021-11-02 16:03:29 +01:00
nolash
dc80bae673 Upgrade cic-eth in migrations 2021-11-02 15:31:00 +01:00
nolash
d88ae00b72 Add celery cli args with defaults from redis 2021-10-31 07:58:35 +01:00
nolash
7a366edb9d WIP rehabilitate cic-eth-inspect 2021-10-30 19:09:17 +02:00
nolash
0b912b99b6 Add role listing to cic-eth tag cli tool 2021-10-30 13:19:31 +02:00
nolash
cbd4aef004 Add action confirm on sweep script 2021-10-30 10:25:39 +02:00
nolash
6f7f91780b Add script to sweep gas from signer accounts 2021-10-30 09:02:04 +02:00
nolash
83ecdaf023 Connect token filter to tracker 2021-10-29 16:35:11 +02:00
nolash
e2ef9b43c8 Reactivate cic-eth-tasker dependency for bootstrap 2021-10-29 15:58:34 +02:00
nolash
6e58e4e4de Remove nasty residue from bootstrap 2021-10-29 14:40:06 +02:00
nolash
f46c9b0e7d Merge remote-tracking branch 'origin/master' into lash/bloxberg-seeding 2021-10-29 11:39:40 +02:00
nolash
6ca3fd55d7 Add gas cache oracle connection for erc20 2021-10-29 08:45:42 +02:00
nolash
258ed420b8 Merge branch 'lash/tmp-bloxberg-seeding' into lash/bloxberg-seeding 2021-10-29 07:35:08 +02:00
nolash
1c022e9853 Added changes to wrong branch 2021-10-29 07:33:38 +02:00
nolash
d35e144723 Register gas cache only for registered tokens 2021-10-29 07:00:25 +02:00
nolash
fb953d0318 Add gas cache backend, test, filter 2021-10-28 21:45:47 +02:00
nolash
858bbdb69a Merge remote-tracking branch 'origin/master' into lash/local-dev-improve 2021-10-28 14:36:45 +02:00
nolash
66e23e4e20 Test config cleanup 2021-10-28 14:11:11 +02:00
nolash
546256c86a Better gas gifting amounts and thresholds estimation, fix broken cic-eth imports 2021-10-28 13:34:39 +02:00
nolash
d9720bd0aa Merge remote-tracking branch 'origin/lash/local-dev-improve' into lash/bloxberg-seeding 2021-10-28 05:41:27 +02:00
nolash
e9e9f66d97 Correct wrong change for docker registries 2021-10-28 05:39:44 +02:00
nolash
0d640fab57 Merge remote-tracking branch 'origin/lash/local-dev-improve' into lash/bloxberg-seeding 2021-10-28 05:29:07 +02:00
nolash
4ce85bc824 Remove faulty default registry in dockerfiles 2021-10-28 05:27:13 +02:00
nolash
ce67f83457 Remove faulty default registry in docker compose 2021-10-28 05:24:11 +02:00
nolash
13f2e17931 Remove accidental 0 value override for syncer offset to trackers 2021-10-28 05:18:54 +02:00
nolash
f236234682 Merge remote-tracking branch 'origin/master' into lash/local-dev-improve 2021-10-27 16:58:38 +02:00
nolash
1f37632f0f WIP Replace env vars in data-seeding with well-known 2021-10-27 16:56:03 +02:00
nolash
03d7518f8c Merge branch 'lash/local-dev-improve' of gitlab.com:grassrootseconomics/cic-internal-integration into lash/local-dev-improve 2021-10-27 11:52:31 +02:00
nolash
67152d0df1 Replace KEYSTORE_PATH with WALLET_KEY_FILE in data seeding 2021-10-27 11:51:20 +02:00
9168322941 Revert base image changes. 2021-10-27 12:41:35 +03:00
2fbd338e24 Adds correct base image. 2021-10-27 11:44:23 +03:00
c7d7f2a64d Remove force reset. 2021-10-27 11:44:08 +03:00
16153df2f0 Resolve creation of phone dir when it already exists. 2021-10-27 11:43:35 +03:00
nolash
4391fa3aff Merge remote-tracking branch 'origin/master' into lash/local-dev-improve 2021-10-25 21:01:27 +02:00
nolash
7ce68021bd Merge remote-tracking branch 'origin/master' into lash/verify-cache 2021-10-25 20:20:40 +02:00
nolash
cd602dee49 Remove WIP docker compose file 2021-10-25 20:12:32 +02:00
nolash
a548ba6fce Chainlib upgrade to handle none receipts, rpc node debug output in bootstrap 2021-10-25 20:09:35 +02:00
nolash
a6de7e9fe0 Merge remote-tracking branch 'origin/master' into lash/local-dev-improve 2021-10-20 20:02:19 +02:00
nolash
e705a94873 Resolve notify/ussd dependency conflict 2021-10-20 10:07:19 +02:00
nolash
3923de0a81 Update pip args handling in notify 2021-10-19 23:01:55 +02:00
nolash
5c0250b5b9 Rehabilitate cic-cache db migration 2021-10-19 22:58:10 +02:00
nolash
3285d8dfe5 Implement asynchronous deploys in bootstrap 2021-10-19 22:08:17 +02:00
nolash
9d349f1579 Add debug level env var to bootstrap dev container 2021-10-19 19:54:59 +02:00
nolash
837a1770d1 Upgrade deps more chainlib in bootstrap 2021-10-19 10:10:39 +02:00
003febec9d Bumps contract migration deps. 2021-10-19 10:38:21 +03:00
f066a32ce8 Adds libffi-dev for local git-tea. 2021-10-19 10:38:08 +03:00
nolash
ad493705ad Upgrade deps 2021-10-18 17:16:28 +02:00
nolash
b765c4ab88 More wrestling with chainlib-eth deps 2021-10-18 17:06:31 +02:00
nolash
e4935d3b58 Merge branch 'lash/split-migration' of gitlab.com:grassrootseconomics/cic-internal-integration into lash/split-migration 2021-10-18 16:49:58 +02:00
nolash
f88f0e321b Upgrade chainlib-eth dep 2021-10-18 16:48:14 +02:00
31fa721397 Add cic-notify container 2021-10-18 17:17:53 +03:00
16481da193 Merge remote-tracking branch 'origin/lash/split-migration' into lash/split-migration 2021-10-18 16:54:23 +03:00
97a48cd8c6 Improves ussd deps. 2021-10-18 16:53:38 +03:00
nolash
7732412341 Merge branch 'lash/split-migration' of gitlab.com:grassrootseconomics/cic-internal-integration into lash/split-migration 2021-10-18 15:51:38 +02:00
nolash
649b124a61 Ugprade chainqueue dep 2021-10-18 15:50:45 +02:00
7601e3eeff Corrects breakages in cic-ussd 2021-10-18 15:19:32 +03:00
60a9efc88b Merge remote-tracking branch 'origin/lash/split-migration' into lash/split-migration 2021-10-18 15:18:33 +03:00
45011b58c4 Cleans up configs. 2021-10-18 15:11:31 +03:00
nolash
f1a0b4ee7c Merge branch 'lash/split-migration' of gitlab.com:grassrootseconomics/cic-internal-integration into lash/split-migration 2021-10-18 14:10:52 +02:00
nolash
c57abb7ad5 Upgrade deps in cic-eth, allow for new chain spec format 2021-10-18 14:08:39 +02:00
930a99c974 Bumps cic-types version. 2021-10-18 06:52:49 +03:00
b0935caab8 Fixes imports. 2021-10-18 06:52:28 +03:00
nolash
bdd5f6fcec Update readme in data seeding 2021-10-17 19:37:29 +02:00
nolash
a293c2460e Consolidate dir handling in data seeding scripts 2021-10-17 19:27:15 +02:00
nolash
0ee6400d7d WIP rehabilitate ussd builds 2021-10-17 18:32:08 +02:00
nolash
677fb346fd Add data seeding preparation step, rehabilitation of non-custodial seeding 2021-10-17 18:05:00 +02:00
nolash
ea3c75e755 Rehabilitate traffic script 2021-10-17 14:30:42 +02:00
nolash
0b2f22c416 Rehabilitate cic-user-server 2021-10-16 20:54:41 +02:00
nolash
24385ea27d Rehabilitate cic-cache 2021-10-16 14:03:05 +02:00
nolash
9a154a8046 WIP rehabilitate cic-cache 2021-10-16 08:23:32 +02:00
nolash
d3576c8ec7 Add eth retrier to new docker compose file 2021-10-16 07:08:44 +02:00
nolash
79ee2bf4ff Add eth tracker, dispatcher to new docker compose file 2021-10-16 07:04:19 +02:00
nolash
89ac70371a Remove single function worker in test 2021-10-16 00:18:08 +02:00
nolash
5ea0318b0b Fix default token symbol config setting for aux 2021-10-15 23:21:57 +02:00
nolash
5dfb96ec0c Add new cic-signer app 2021-10-15 23:11:00 +02:00
nolash
4634ac41df Merge remote-tracking branch 'origin/master' into lash/split-migration 2021-10-15 22:19:01 +02:00
nolash
97f4fe8ca7 refactor docker-compose cic-eth-tasker, bootstrap (aka contract migration) 2021-10-15 22:16:45 +02:00
nolash
b36529f7fa WIP local docker registry adaptations 2021-10-15 20:27:03 +02:00
nolash
a6675f2348 Add environment sourcing for cic-eth-tasker docker compose 2021-10-15 18:52:37 +02:00
nolash
e3116d74d6 No export 2021-10-15 12:54:16 +02:00
nolash
c0bbdc9bec Add missing file 2021-10-15 08:43:04 +02:00
nolash
396bd4f300 update preliminary readme 2021-10-15 08:38:01 +02:00
nolash
58547b4067 Bump cic-eth-registry 2021-10-15 07:44:50 +02:00
nolash
9009815d78 Add trust address to contract migration config, get cic-eth default token from registry 2021-10-14 21:31:04 +02:00
nolash
2da19f5819 Add basic connectivity config directives 2021-10-14 17:40:53 +02:00
nolash
3948d5aa40 Add custodial initialization 2021-10-14 17:18:49 +02:00
nolash
ed432abb23 WIP refactor custodial initialization 2021-10-14 14:37:48 +02:00
nolash
f251b8b729 Remove dead code 2021-10-14 11:35:08 +02:00
nolash
36e791e08a Split contract migration into three separate steps 2021-10-14 11:33:50 +02:00
nolash
71a7e3d3d5 Reinstate test config dir 2021-10-09 17:23:38 +02:00
nolash
335b7b30a4 Add okota dep 2021-10-09 16:40:28 +02:00
nolash
3b1f470ddf Add empty config dir 2021-10-09 16:33:40 +02:00
nolash
4c9f20aa7f Add explicit zero length tx lsit check for cic-cache verify 2021-10-08 11:26:09 +02:00
nolash
980191be4f Add verify check for cache, use chainlib cli for cic-cache 2021-10-08 11:19:21 +02:00
121 changed files with 1521 additions and 4421 deletions

1
.gitignore vendored
View File

@@ -15,4 +15,3 @@ build/
.idea .idea
**/.vim **/.vim
**/*secret.yaml **/*secret.yaml
.envrc

View File

@@ -1,36 +1 @@
# CIC-ETH # CIC-ETH
## Testing CIC-ETH locally.
### Setup a Virtual Env
```bash
python3 -m venv ./venv # Python 3.9
source ./venv/activate
```
### Running All Unit Tests
```bash
bash ./tests/run_tests.sh # This will also install required dependencies
```
### Running Specific Unit Tests
Ensure that:
- You have called `bash ./tests/run_tests.sh` at least once or run the following to install required dependencies
- You have activated the virtual environment
```
pip install --extra-index-url https://pip.grassrootseconomics.net --extra-index-url https://gitlab.com/api/v4/projects/27624814/packages/pypi/simple \
-r admin_requirements.txt \
-r services_requirements.txt \
-r test_requirements.txt
```
Then here is an example that only runs tests with the keyword(-k) `test_server`
```bash
pytest -s -v --log-cli-level DEBUG --log-level DEBUG -k test_server
```

View File

@@ -6,8 +6,11 @@
# standard imports # standard imports
import logging import logging
# external imports # external imports
import celery import celery
from chainlib.chain import ChainSpec
from hexathon import strip_0x
# local imports # local imports
from cic_eth.api.base import ApiBase from cic_eth.api.base import ApiBase
from cic_eth.enum import LockEnum from cic_eth.enum import LockEnum

View File

@@ -1,12 +0,0 @@
# standard imports
import os
import random
import uuid
def blockchain_address() -> str:
return os.urandom(20).hex().lower()

View File

@@ -1,132 +0,0 @@
# standard imports
import os
# external imports
import pytest
from celery import uuid
# test imports
from cic_eth.pytest.helpers.accounts import blockchain_address
@pytest.fixture(scope='function')
def task_uuid():
return uuid()
@pytest.fixture(scope='function')
def default_token_data(foo_token_symbol, foo_token):
return {
'symbol': foo_token_symbol,
'address': foo_token,
'name': 'Giftable Token',
'decimals': 6,
"converters": []
}
@pytest.fixture(scope='function')
def mock_account_creation_task_request(mocker, task_uuid):
def mock_request(self):
mocked_task_request = mocker.patch('celery.app.task.Task.request')
mocked_task_request.id = task_uuid
return mocked_task_request
mocker.patch('cic_eth.api.api_task.Api.create_account', mock_request)
@pytest.fixture(scope='function')
def mock_account_creation_task_result(mocker, task_uuid):
def task_result(self):
sync_res = mocker.patch('celery.result.AsyncResult')
sync_res.id = task_uuid
sync_res.get.return_value = blockchain_address()
return sync_res
mocker.patch('cic_eth.api.api_task.Api.create_account', task_result)
@pytest.fixture(scope='function')
def mock_token_api_query(foo_token_symbol, foo_token, mocker, task_uuid):
def mock_query(self, token_symbol, proof=None):
sync_res = mocker.patch('celery.result.AsyncResult')
sync_res.id = task_uuid
sync_res.get.return_value = [
{
'address': foo_token,
'converters': [],
'decimals': 6,
'name': 'Giftable Token',
'proofs': ['5b1549818725ca07c19fc47fda5d8d85bbebb1283855d5ab99785dcb7d9051d3'],
'symbol': foo_token_symbol,
},{'5b1549818725ca07c19fc47fda5d8d85bbebb1283855d5ab99785dcb7d9051d3': ['Eb3907eCad74a0013c259D5874AE7f22DcBcC95C','Eb3907eCad74a0013c259D5874AE7f22DcBcC95C']}
]
return sync_res
mocker.patch('cic_eth.api.api_task.Api.token', mock_query)
@pytest.fixture(scope='function')
def mock_tokens_api_query(foo_token_symbol, foo_token, mocker, task_uuid):
def mock_query(self, token_symbol, proof=None):
sync_res = mocker.patch('celery.result.AsyncResult')
sync_res.id = task_uuid
sync_res.get.return_value = [[
{
'address': foo_token,
'converters': [],
'decimals': 6,
'name': 'Giftable Token',
'proofs': ['5b1549818725ca07c19fc47fda5d8d85bbebb1283855d5ab99785dcb7d9051d3'],
'symbol': foo_token_symbol,
},{'5b1549818725ca07c19fc47fda5d8d85bbebb1283855d5ab99785dcb7d9051d3': ['Eb3907eCad74a0013c259D5874AE7f22DcBcC95C','Eb3907eCad74a0013c259D5874AE7f22DcBcC95C']}
], [
{
'address': foo_token,
'converters': [],
'decimals': 6,
'name': 'Giftable Token',
'proofs': ['5b1549818725ca07c19fc47fda5d8d85bbebb1283855d5ab99785dcb7d9051d3'],
'symbol': foo_token_symbol,
},{'5b1549818725ca07c19fc47fda5d8d85bbebb1283855d5ab99785dcb7d9051d3': ['Eb3907eCad74a0013c259D5874AE7f22DcBcC95C','Eb3907eCad74a0013c259D5874AE7f22DcBcC95C']}
]]
return sync_res
mocker.patch('cic_eth.api.api_task.Api.tokens', mock_query)
@pytest.fixture(scope='function')
def mock_sync_balance_api_query(balances, mocker, task_uuid):
def sync_api_query(self, address: str, token_symbol: str):
sync_res = mocker.patch('celery.result.AsyncResult')
sync_res.id = task_uuid
sync_res.get.return_value = balances
return sync_res
mocker.patch('cic_eth.api.api_task.Api.balance', sync_api_query)
@pytest.fixture(scope='function')
def mock_sync_default_token_api_query(default_token_data, mocker, task_uuid):
def mock_query(self):
sync_res = mocker.patch('celery.result.AsyncResult')
sync_res.id = task_uuid
sync_res.get.return_value = default_token_data
return sync_res
mocker.patch('cic_eth.api.api_task.Api.default_token', mock_query)
@pytest.fixture(scope='function')
def mock_transaction_list_query(mocker):
query_args = {}
def mock_query(self, address: str, limit: int):
query_args['address'] = address
query_args['limit'] = limit
mocker.patch('cic_eth.api.api_task.Api.list', mock_query)
return query_args
@pytest.fixture(scope='function')
def mock_transfer_api(mocker):
transfer_args = {}
def mock_transfer(self, from_address: str, to_address: str, value: int, token_symbol: str):
transfer_args['from_address'] = from_address
transfer_args['to_address'] = to_address
transfer_args['value'] = value
transfer_args['token_symbol'] = token_symbol
mocker.patch('cic_eth.api.api_task.Api.transfer', mock_transfer)
return transfer_args

View File

@@ -1,11 +1,15 @@
#!/usr/bin/python #!/usr/bin/python
import sys import sys
import os
import logging import logging
import uuid import uuid
import json import json
import argparse
# external imports # external imports
import redis import redis
from xdg.BaseDirectory import xdg_config_home
from chainlib.chain import ChainSpec
# local imports # local imports
import cic_eth.cli import cic_eth.cli

View File

@@ -1,38 +0,0 @@
import logging
import cic_eth.cli
from cic_eth.server.app import create_app
from cic_eth.server.celery import create_celery_wrapper
arg_flags = cic_eth.cli.argflag_std_base
local_arg_flags = cic_eth.cli.argflag_local_taskcallback
argparser = cic_eth.cli.ArgumentParser(arg_flags)
argparser.process_local_flags(local_arg_flags)
args = argparser.parse_args()
config = cic_eth.cli.Config.from_args(args, arg_flags, local_arg_flags)
# Define log levels
if args.vv:
logging.getLogger().setLevel(logging.DEBUG)
elif args.v:
logging.getLogger().setLevel(logging.INFO)
# Setup Celery App
celery_app = cic_eth.cli.CeleryApp.from_config(config)
celery_app.set_default()
chain_spec = config.get('CHAIN_SPEC')
celery_queue = config.get('CELERY_QUEUE')
redis_host = config.get('REDIS_HOST')
redis_port = config.get('REDIS_PORT')
redis_db = config.get('REDIS_DB')
redis_timeout = config.get('REDIS_TIMEOUT')
celery_wrapper = create_celery_wrapper(celery_queue=celery_queue, chain_spec=chain_spec,
redis_db=redis_db, redis_host=redis_host, redis_port=redis_port, redis_timeout=redis_timeout)
app = create_app(celery_wrapper)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=5000, log_level="info")

View File

@@ -1,3 +0,0 @@
from . import converters
from . import cache
from . import celery

View File

@@ -1,119 +0,0 @@
import logging
import sys
from typing import List, Optional, Union
from cic_eth.server import cache, converters
from cic_eth.server.cache import setup_cache
from cic_eth.server.celery import create_celery_wrapper
from cic_eth.server.models import (DefaultToken, Token, TokenBalance,
Transaction)
from fastapi import FastAPI, Query
log = logging.getLogger(__name__)
def create_app(celery_wrapper):
app = FastAPI(debug=True,
title="Grassroots Economics",
description="CIC ETH API",
version="0.0.1",
terms_of_service="https://www.grassrootseconomics.org/pages/terms-and-conditions.html",
contact={
"name": "Grassroots Economics",
"url": "https://www.grassrootseconomics.org",
"email": "will@grassecon.org"
},
license_info={
"name": "GPLv3",
})
@app.get("/transactions", response_model=List[Transaction])
def transactions(address: str, limit: Optional[str] = 10):
return celery_wrapper('list', address, limit=limit)
@app.get("/balance", response_model=List[TokenBalance])
def balance(token_symbol: str, address: str = Query(..., title="Address", min_length=40, max_length=42), include_pending: bool = True):
log.info(f"address: {address}")
log.info(f"token_symbol: {token_symbol}")
data = celery_wrapper('balance', address, token_symbol,
include_pending=include_pending)
for b in data:
token = get_token(token_symbol)
b['balance_network'] = converters.from_wei(
token.decimals, int(b['balance_network']))
b['balance_incoming'] = converters.from_wei(
token.decimals, int(b['balance_incoming']))
b['balance_outgoing'] = converters.from_wei(
token.decimals, int(b['balance_outgoing']))
b.update({
"balance_available": int(b['balance_network']) + int(b['balance_incoming']) - int(b['balance_outgoing'])
})
return data
@app.post("/create_account")
def create_account(password: Optional[str] = None, register: bool = True):
data = celery_wrapper(
'create_account', password=password, register=register)
return data
# def refill_gas(start_response, query: dict):
# address = query.pop('address')
# data = celery_wrapper('refill_gas', address)
# return data
# def ping(start_response, query: dict):
# data = celery_wrapper('ping', **query)
# return data
@app.post("/transfer")
def transfer(from_address: str, to_address: str, value: int, token_symbol: str):
token = get_token(
token_symbol)
wei_value = converters.to_wei(token.decimals, int(value))
data = celery_wrapper('transfer', from_address,
to_address, wei_value, token_symbol)
return data
@app.post("/transfer_from")
def transfer_from(from_address: str, to_address: str, value: int, token_symbol: str, spender_address: str):
token = get_token(
token_symbol)
wei_value = converters.to_wei(token.decimals, int(value))
data = celery_wrapper('transfer_from', from_address, to_address,
wei_value, token_symbol, spender_address)
return data
@app.get("/token", response_model=Token)
def token(token_symbol: str, proof: Optional[str] = None):
token = get_token(token_symbol)
if token == None:
sys.stderr.write(f"Cached Token {token_symbol} not found")
data = celery_wrapper('token', token_symbol, proof=proof)
token = Token.new(data)
sys.stderr.write(f"Token {token}")
return token
@app.get("/tokens", response_model=List[Token])
def tokens(token_symbols: Optional[List[str]] = Query(None), proof: Optional[Union[str, List[str], List[List[str]]]] = None):
data = celery_wrapper('tokens', token_symbols,
catch=len(token_symbols), proof=proof)
if data:
tokens = []
for token in data:
print(f"Token: {token}")
tokens.append(Token.new(token))
return tokens
return None
@app.get("/default_token", response_model=DefaultToken)
def default_token():
data = celery_wrapper('default_token')
return data
def get_token(token_symbol: str):
data = celery_wrapper('token', token_symbol)
return Token.new(data)
return app

View File

@@ -1,130 +0,0 @@
# standard imports
import hashlib
import json
import logging
from typing import Optional, Union
from cic_eth.server.models import Token
from cic_types.condiments import MetadataPointer
from redis import Redis, StrictRedis
logg = logging.getLogger(__file__)
class Cache:
store: Redis = None
def setup_cache(redis_host, redis_port, redis_db):
# Define universal redis cache access
Cache.store = StrictRedis(
host=redis_host, port=redis_port, db=redis_db, decode_responses=True)
def get_token_data(token_symbol: str):
"""
:param token_symbol:
:type token_symbol:
:return:
:rtype:
"""
identifier = [token_symbol.encode('utf-8')]
key = cache_data_key(identifier, MetadataPointer.TOKEN_DATA)
logg.debug(f'Retrieving token data for: {token_symbol} at: {key}')
token_data_str = get_cached_data(key=key)
if(token_data_str is None):
logg.debug(f'No token data found for: {token_symbol}')
return None
else:
token_data = json.loads(token_data_str)
logg.debug(f'Retrieved token data: {token_data}')
return token_data
def set_token_data(token_symbol: str, token: dict):
"""
:param token_symbol:
:type token_symbol:
:return:
:rtype:
"""
identifier = [token_symbol.encode('utf-8')]
key = cache_data_key(identifier, MetadataPointer.TOKEN_DATA)
cache_data(key, json.dumps(token))
logg.debug(f'Cached token data for: {token_symbol} at: {key}')
def get_default_token() -> Optional[str]:
"""This function attempts to retrieve the default token's data from the redis cache.
:return:
:rtype:
"""
logg.debug(f'Retrieving default token from cache')
# TODO: What should the identifier be?
key = cache_data_key(identifier="ff".encode('utf-8'),
salt=MetadataPointer.TOKEN_DEFAULT)
default_token_str = get_cached_data(key=key)
if default_token_str is None:
logg.debug(f'No cached default token found: {key}')
return None
default_token = json.loads(default_token_str)
logg.debug(f'Retrieved default token data: {default_token}')
return default_token
def set_default_token(default_token: dict):
"""
:param default_token:
:type default_token:
:return:
:rtype:
"""
logg.debug(f'Setting default token in cache')
key = cache_data_key(identifier="ff".encode('utf-8'),
salt=MetadataPointer.TOKEN_DEFAULT)
cache_data(key, json.dumps(default_token))
def cache_data(key: str, data: str):
"""
:param key:
:type key:
:param data:
:type data:
:return:
:rtype:
"""
cache = Cache.store
cache.set(name=key, value=data)
cache.persist(name=key)
logg.debug(f'caching: {data} with key: {key}.')
def get_cached_data(key: str):
"""
:param key:
:type key:
:return:
:rtype:
"""
cache = Cache.store
return cache.get(name=key)
def cache_data_key(identifier: Union[list, bytes], salt: MetadataPointer):
"""
:param identifier:
:type identifier:
:param salt:
:type salt:
:return:
:rtype:
"""
hash_object = hashlib.new("sha256")
if isinstance(identifier, list):
for identity in identifier:
hash_object.update(identity)
else:
hash_object.update(identifier)
hash_object.update(salt.value.encode(encoding="utf-8"))
return hash_object.digest().hex()

View File

@@ -1,64 +0,0 @@
import json
import logging
import sys
import uuid
import redis
from cic_eth.api.api_task import Api
log = logging.getLogger(__name__)
def create_celery_wrapper(chain_spec,
celery_queue,
redis_host,
redis_port,
redis_db,
redis_timeout):
def call(method, *args, catch=1, **kwargs):
""" Creates a redis channel and calls `cic_eth.api` with the provided `method` and `*args`. Returns the result of the api call. Catch allows you to specify how many messages to catch before returning.
"""
log.debug(f"Using redis: {redis_host}, {redis_port}, {redis_db}")
redis_channel = str(uuid.uuid4())
r = redis.Redis(redis_host, redis_port, redis_db)
ps = r.pubsub()
ps.subscribe(redis_channel)
api = Api(
chain_spec,
queue=celery_queue,
callback_param='{}:{}:{}:{}'.format(
redis_host, redis_port, redis_db, redis_channel),
callback_task='cic_eth.callbacks.redis.redis',
callback_queue=celery_queue,
)
getattr(api, method)(*args, **kwargs)
ps.get_message()
try:
data = []
if catch == 1:
message = ps.get_message(timeout=redis_timeout)
data = json.loads(message['data'])["result"]
raise data
else:
for _i in range(catch):
message = ps.get_message(
timeout=redis_timeout)
result = json.loads(message['data'])["result"]
data.append(result)
except TimeoutError as e:
sys.stderr.write(
f"cic_eth.api.{method}({args}, {kwargs}) timed out:\n {e}")
raise e
except Exception as e:
sys.stderr.write(
f'Unable to parse Data:\n{data}\n Error:\n{e}')
raise e
log.debug(
f"cic_eth.api.{method}(args={args}, kwargs={kwargs})\n {data}")
ps.unsubscribe()
return data
return call

View File

@@ -1,39 +0,0 @@
# Stolen from ussd
from math import trunc
def from_wei(decimals: int, value: int) -> float:
"""This function converts values in Wei to a token in the cic network.
:param decimals: The decimals required for wei values.
:type decimals: int
:param value: Value in Wei
:type value: int
:return: SRF equivalent of value in Wei
:rtype: float
"""
value = float(value) / (10**decimals)
return truncate(value=value, decimals=2)
def to_wei(decimals: int, value: int) -> int:
"""This functions converts values from a token in the cic network to Wei.
:param decimals: The decimals required for wei values.
:type decimals: int
:param value: Value in SRF
:type value: int
:return: Wei equivalent of value in SRF
:rtype: int
"""
return int(value * (10**decimals))
def truncate(value: float, decimals: int) -> float:
"""This function truncates a value to a specified number of decimals places.
:param value: The value to be truncated.
:type value: float
:param decimals: The number of decimals for the value to be truncated to
:type decimals: int
:return: The truncated value.
:rtype: int
"""
stepper = 10.0**decimals
return trunc(stepper*value) / stepper

View File

@@ -1,92 +0,0 @@
from __future__ import annotations
from typing import List, Optional
from pydantic import BaseModel, Field
class Transaction(BaseModel):
block_number: Optional[int] = Field(None, example=24531)
date_checked: Optional[str] = Field(
None, example='2021-11-12T09:36:40.725296')
date_created: Optional[str] = Field(
None, example='2021-11-12T09:36:40.131292')
date_updated: Optional[str] = Field(
None, example='2021-11-12T09:36:40.131292')
destination_token: Optional[str] = Field(
None, example=365185044137427460620354810422988491181438940190
)
destination_token_decimals: Optional[int] = Field(None, example=6)
destination_token_symbol: Optional[str] = Field(None, example='COFE')
from_value: Optional[int] = Field(None, example=100000000)
hash: Optional[str] = Field(
None,
example=90380195350511178677041624165156640995490505896556680958001954705731707874291,
)
nonce: Optional[int] = Field(None, example=1)
recipient: Optional[str] = Field(
None, example='872e1ec9d499b242ebfcfd0a279a4c3e0cd472c0'
)
sender: Optional[str] = Field(
None, example='1a92b05e0b880127a4c26ac0f68a52df3ac6b89d'
)
signed_tx: Optional[str] = Field(
None,
example=1601943273486236942256143665779318355236220334071247753507187634376562549990085710958441113013370129915441072693447256942510246386178938683325073160349857879326297351587330623503997011254644396580777843154770873208185332563272343361515226115860084201932230246018679661802320007832375955345977725551120479084062615799940692628221555193198194825737613358738414884130187144700126061702642574663703095161159219410608270,
)
source_token: Optional[str] = Field(
None, example=365185044137427460620354810422988491181438940190
)
source_token_decimals: Optional[int] = Field(None, example=6)
source_token_symbol: Optional[str] = Field(None, example='COFE')
status: Optional[str] = Field(None, example='SUCCESS')
status_code: Optional[int] = Field(None, example=4104)
timestamp: Optional[int] = Field(None, example=1636709800)
to_value: Optional[int] = Field(None, example=100000000)
tx_hash: Optional[str] = Field(
None,
example=90380195350511178677041624165156640995490505896556680958001954705731707874291,
)
tx_index: Optional[int] = Field(None, example=0)
class DefaultToken(BaseModel):
symbol: Optional[str] = Field(None, description='Token Symbol')
address: Optional[str] = Field(None, description='Token Address')
name: Optional[str] = Field(None, description='Token Name')
decimals: Optional[int] = Field(None, description='Decimals')
class TokenBalance(BaseModel):
address: Optional[str] = None
converters: Optional[List[str]] = None
balance_network: Optional[int] = None
balance_incoming: Optional[int] = None
balance_outgoing: Optional[int] = None
balance_available: Optional[int] = None
class Token(BaseModel):
decimals: Optional[int] = None
name: Optional[str] = None
address: Optional[str] = None
symbol: Optional[str] = None
proofs: Optional[List[str]] = None
converters: Optional[List[str]] = None
proofs_with_signers: Optional[List[Proof]] = None
@staticmethod
def new(data: List[dict]) -> Token:
proofs_with_signers = [{"proof": proof, "signers": signers}
for (proof, signers) in data[1].items()]
return Token(**data[0],
proofs_with_signers=proofs_with_signers,
)
class Proof(BaseModel):
proof: Optional[str] = None
signers: Optional[List[str]] = None
Token.update_forward_refs()

View File

@@ -17,7 +17,7 @@ from cic_eth_registry.error import UnknownContractError
# local imports # local imports
from cic_eth.error import SeppukuError from cic_eth.error import SeppukuError
from cic_eth.db.models.base import SessionBase from cic_eth.db.models.base import SessionBase
from cic_eth.eth.util import CacheGasOracle, MaxGasOracle from cic_eth.eth.util import CacheGasOracle
#logg = logging.getLogger().getChild(__name__) #logg = logging.getLogger().getChild(__name__)
logg = logging.getLogger() logg = logging.getLogger()
@@ -41,24 +41,21 @@ class BaseTask(celery.Task):
def create_gas_oracle(self, conn, address=None, *args, **kwargs): def create_gas_oracle(self, conn, address=None, *args, **kwargs):
x = None if address == None:
if address is None: return RPCGasOracle(
x = RPCGasOracle(
conn, conn,
code_callback=kwargs.get('code_callback', self.get_min_fee_limit), code_callback=kwargs.get('code_callback', self.get_min_fee_limit),
min_price=self.min_fee_price, min_price=self.min_fee_price,
id_generator=kwargs.get('id_generator'), id_generator=kwargs.get('id_generator'),
) )
else:
x = MaxGasOracle(conn) return CacheGasOracle(
x.code_callback = x.get_fee_units conn,
address,
return x method=kwargs.get('method'),
min_price=self.min_fee_price,
id_generator=kwargs.get('id_generator'),
def get_min_fee_limit(self, code): )
return self.min_fee_limit
def get_min_fee_limit(self, code): def get_min_fee_limit(self, code):
@@ -87,7 +84,7 @@ class BaseTask(celery.Task):
) )
s.apply_async() s.apply_async()
class CriticalTask(BaseTask): class CriticalTask(BaseTask):
retry_jitter = True retry_jitter = True
retry_backoff = True retry_backoff = True
@@ -99,7 +96,7 @@ class CriticalSQLAlchemyTask(CriticalTask):
sqlalchemy.exc.DatabaseError, sqlalchemy.exc.DatabaseError,
sqlalchemy.exc.TimeoutError, sqlalchemy.exc.TimeoutError,
sqlalchemy.exc.ResourceClosedError, sqlalchemy.exc.ResourceClosedError,
) )
class CriticalWeb3Task(CriticalTask): class CriticalWeb3Task(CriticalTask):
@@ -107,7 +104,7 @@ class CriticalWeb3Task(CriticalTask):
ConnectionError, ConnectionError,
) )
safe_gas_threshold_amount = 60000 * 3 safe_gas_threshold_amount = 60000 * 3
safe_gas_refill_amount = safe_gas_threshold_amount * 5 safe_gas_refill_amount = safe_gas_threshold_amount * 5
safe_gas_gifter_balance = safe_gas_threshold_amount * 5 * 100 safe_gas_gifter_balance = safe_gas_threshold_amount * 5 * 100
@@ -125,13 +122,13 @@ class CriticalSQLAlchemyAndSignerTask(CriticalTask):
sqlalchemy.exc.DatabaseError, sqlalchemy.exc.DatabaseError,
sqlalchemy.exc.TimeoutError, sqlalchemy.exc.TimeoutError,
sqlalchemy.exc.ResourceClosedError, sqlalchemy.exc.ResourceClosedError,
) )
class CriticalWeb3AndSignerTask(CriticalWeb3Task): class CriticalWeb3AndSignerTask(CriticalWeb3Task):
autoretry_for = ( autoretry_for = (
ConnectionError, ConnectionError,
) )
@celery_app.task() @celery_app.task()
def check_health(self): def check_health(self):
pass pass

View File

@@ -1,5 +0,0 @@
[redis]
host=redis
database=0
password=
port=6379

View File

@@ -5,5 +5,3 @@ urlybird~=0.0.1
cic-eth-registry~=0.6.6 cic-eth-registry~=0.6.6
cic-types~=0.2.1a8 cic-types~=0.2.1a8
cic-eth-aux-erc20-demurrage-token~=0.0.3 cic-eth-aux-erc20-demurrage-token~=0.0.3
fastapi[all]==0.70.1
uvicorn[standard]<0.16.0

View File

@@ -36,7 +36,6 @@ packages =
cic_eth.db.models cic_eth.db.models
cic_eth.queue cic_eth.queue
cic_eth.ext cic_eth.ext
cic_eth.server
cic_eth.runnable cic_eth.runnable
cic_eth.runnable.daemons cic_eth.runnable.daemons
cic_eth.runnable.daemons.filters cic_eth.runnable.daemons.filters

View File

@@ -24,8 +24,6 @@ from cic_eth.pytest.fixtures_database import *
from cic_eth.pytest.fixtures_role import * from cic_eth.pytest.fixtures_role import *
from cic_eth.pytest.fixtures_contract import * from cic_eth.pytest.fixtures_contract import *
from cic_eth.pytest.fixtures_token import * from cic_eth.pytest.fixtures_token import *
from cic_eth.pytest.patches.account import *
from chainlib.eth.pytest import * from chainlib.eth.pytest import *
from eth_contract_registry.pytest import * from eth_contract_registry.pytest import *
from cic_eth_registry.pytest.fixtures_contracts import * from cic_eth_registry.pytest.fixtures_contracts import *

View File

@@ -2,9 +2,9 @@
set -e set -e
pip install --extra-index-url https://pip.grassrootseconomics.net --extra-index-url https://gitlab.com/api/v4/projects/27624814/packages/pypi/simple \ pip install --extra-index-url https://pip.grassrootseconomics.net --extra-index-url https://gitlab.com/api/v4/projects/27624814/packages/pypi/simple
-r admin_requirements.txt \ -r admin_requirements.txt
-r services_requirements.txt \ -r services_requirements.txt
-r test_requirements.txt -r test_requirements.txt
export PYTHONPATH=. && pytest -x --cov=cic_eth --cov-fail-under=90 --cov-report term-missing tests export PYTHONPATH=. && pytest -x --cov=cic_eth --cov-fail-under=90 --cov-report term-missing tests

View File

@@ -1,171 +0,0 @@
# coding: utf-8
from __future__ import absolute_import
import logging
import time
import hexathon
import pytest
from cic_eth.server.app import create_app
from cic_eth.server.celery import create_celery_wrapper
from fastapi.testclient import TestClient
log = logging.getLogger(__name__)
@pytest.fixture(scope='function')
def celery_wrapper(api):
""" Creates a redis channel and calls `cic_eth.api` with the provided `method` and `*args`. Returns the result of the api call. Catch allows you to specify how many messages to catch before returning.
"""
def wrapper(method, *args, catch=1, **kwargs):
t = getattr(api, method)(*args, **kwargs)
return t.get()
return wrapper
@pytest.fixture(scope='function')
def client(celery_wrapper):
app = create_app(celery_wrapper)
return TestClient(app)
def test_default_token(client,mock_sync_default_token_api_query):
# Default Token
response = client.get('/default_token')
log.debug(f"balance response {response}")
default_token = response.json()
assert default_token == {'symbol': 'FOO', 'address': '0xe7c559c40B297d7f039767A2c3677E20B24F1385', 'name': 'Giftable Token', 'decimals': 6}
def test_token(client, mock_token_api_query):
# Default Token
response = client.get('/token?token_symbol=FOO')
log.debug(f"token response {response}")
token = response.json()
assert token == {
'address': '0xe7c559c40B297d7f039767A2c3677E20B24F1385',
'converters': [],
'decimals': 6,
'name': 'Giftable Token',
'proofs': ['5b1549818725ca07c19fc47fda5d8d85bbebb1283855d5ab99785dcb7d9051d3'],
'proofs_with_signers': [{'proof': '5b1549818725ca07c19fc47fda5d8d85bbebb1283855d5ab99785dcb7d9051d3',
'signers': ['Eb3907eCad74a0013c259D5874AE7f22DcBcC95C', 'Eb3907eCad74a0013c259D5874AE7f22DcBcC95C']}],
'symbol': 'FOO',
}
def test_tokens(client, mock_tokens_api_query):
# Default Token
response = client.get(
'/tokens', params={'token_symbols': ['FOO', 'FOO']})
log.debug(f"tokens response {response}")
tokens = response.json()
assert tokens == [
{
'address': '0xe7c559c40B297d7f039767A2c3677E20B24F1385',
'converters': [],
'decimals': 6,
'name': 'Giftable Token',
'proofs': ['5b1549818725ca07c19fc47fda5d8d85bbebb1283855d5ab99785dcb7d9051d3'],
'proofs_with_signers': [{'proof': '5b1549818725ca07c19fc47fda5d8d85bbebb1283855d5ab99785dcb7d9051d3',
'signers': ['Eb3907eCad74a0013c259D5874AE7f22DcBcC95C', 'Eb3907eCad74a0013c259D5874AE7f22DcBcC95C']}],
'symbol': 'FOO',
},
{
'address': '0xe7c559c40B297d7f039767A2c3677E20B24F1385',
'converters': [],
'decimals': 6,
'name': 'Giftable Token',
'proofs': ['5b1549818725ca07c19fc47fda5d8d85bbebb1283855d5ab99785dcb7d9051d3'],
'proofs_with_signers': [{'proof': '5b1549818725ca07c19fc47fda5d8d85bbebb1283855d5ab99785dcb7d9051d3',
'signers': ['Eb3907eCad74a0013c259D5874AE7f22DcBcC95C', 'Eb3907eCad74a0013c259D5874AE7f22DcBcC95C']}],
'symbol': 'FOO',
},
]
@pytest.mark.skip("Not implemented")
def test_account(client):
# Default Token
response = client.get('/default_token')
log.debug(f"balance response {response}")
default_token = response.json()
# Create Account 1
params = {
'password': '',
'register': True
}
response = client.post(
'/create_account',
params=params)
address_1 = hexathon.valid(response.json())
# Create Account 2
params = {
'password': '',
'register': True
}
response = client.post('/create_account',
params=params)
address_2 = hexathon.valid(response.json())
time.sleep(30) # Required to allow balance to show
# Balance Account 1
params = {
'address': address_1,
'token_symbol': 'COFE',
'include_pending': True
}
response = client.get('/balance',
params=params)
balance = response.json()
assert (balance[0] == {
"address": default_token.get('address').lower(),
"balance_available": 30000,
"balance_incoming": 0,
"balance_network": 30000,
"balance_outgoing": 0,
"converters": []
})
# Transfer
params = {
'from_address': address_1,
'to_address': address_2,
'value': 100,
'token_symbol': 'COFE'
}
response = client.post('/transfer',
params=params)
transfer = response.json()
# Balance Account 1
params = {
'address': address_1,
'token_symbol': 'COFE',
'include_pending': True
}
response = client.get('/balance',
params=params)
balance_after_transfer = response.json()
assert (balance_after_transfer[0] == {
"address": default_token.get('address').lower(),
"balance_available": 29900,
"balance_incoming": 0,
"balance_network": 30000,
"balance_outgoing": 100,
"converters": []
})
# Transactions Account 1
params = {
'address': address_1,
'limit': 10
}
response = client.get('/transactions',
params=params)
transactions = response.json()
# TODO: What are the other 2 transactions
assert len(transactions) == 3
# Check the transaction is correct
# TODO wtf is READSEND (Ready to send? Or already sent)
assert transactions[0].status == 'READYSEND'

View File

@@ -1,9 +1,10 @@
[database] [DATABASE]
name=cic_notify_test user = postgres
user= password =
password= host = localhost
host=localhost port = 5432
port= name = /tmp/cic-notify.db
engine=sqlite #engine = postgresql
driver=pysqlite #driver = psycopg2
debug=0 engine = sqlite
driver = pysqlite

View File

@@ -1,7 +0,0 @@
[report]
omit =
venv/*
scripts/*
cic_notify/db/migrations/*
cic_notify/runnable/*
cic_notify/version.py

View File

@@ -3,7 +3,6 @@ import logging
import re import re
# third-party imports # third-party imports
import cic_notify.tasks.sms.db
from celery.app.control import Inspect from celery.app.control import Inspect
import celery import celery
@@ -14,16 +13,45 @@ app = celery.current_app
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger() logg = logging.getLogger()
sms_tasks_matcher = r"^(cic_notify.tasks.sms)(\.\w+)?"
re_q = r'^cic-notify'
def get_sms_queue_tasks(app, task_prefix='cic_notify.tasks.sms.'):
host_queues = []
i = Inspect(app=app)
qs = i.active_queues()
for host in qs.keys():
for q in qs[host]:
if re.match(re_q, q['name']):
host_queues.append((host, q['name'],))
task_prefix_len = len(task_prefix)
queue_tasks = []
for (host, queue) in host_queues:
i = Inspect(app=app, destination=[host])
for tasks in i.registered_tasks().values():
for task in tasks:
if len(task) >= task_prefix_len and task[:task_prefix_len] == task_prefix:
queue_tasks.append((queue, task,))
return queue_tasks
class Api: class Api:
def __init__(self, queue: any = 'cic-notify'): # TODO: Implement callback strategy
def __init__(self, queue=None):
""" """
:param queue: The queue on which to execute notification tasks :param queue: The queue on which to execute notification tasks
:type queue: str :type queue: str
""" """
self.queue = queue self.queue = queue
self.sms_tasks = get_sms_queue_tasks(app)
logg.debug('sms tasks {}'.format(self.sms_tasks))
def sms(self, message: str, recipient: str):
def sms(self, message, recipient):
"""This function chains all sms tasks in order to send a message, log and persist said data to disk """This function chains all sms tasks in order to send a message, log and persist said data to disk
:param message: The message to be sent to the recipient. :param message: The message to be sent to the recipient.
:type message: str :type message: str
@@ -32,9 +60,24 @@ class Api:
:return: a celery Task :return: a celery Task
:rtype: Celery.Task :rtype: Celery.Task
""" """
s_send = celery.signature('cic_notify.tasks.sms.africastalking.send', [message, recipient], queue=self.queue) signatures = []
s_log = celery.signature('cic_notify.tasks.sms.log.log', [message, recipient], queue=self.queue) for q in self.sms_tasks:
s_persist_notification = celery.signature(
'cic_notify.tasks.sms.db.persist_notification', [message, recipient], queue=self.queue) if not self.queue:
signatures = [s_send, s_log, s_persist_notification] queue = q[0]
return celery.group(signatures)() else:
queue = self.queue
signature = celery.signature(
q[1],
[
message,
recipient,
],
queue=queue,
)
signatures.append(signature)
t = celery.group(signatures)()
return t

View File

@@ -2,7 +2,7 @@
[alembic] [alembic]
# path to migration scripts # path to migration scripts
script_location = . script_location = migrations
# template used to generate migration files # template used to generate migration files
# file_template = %%(rev)s_%%(slug)s # file_template = %%(rev)s_%%(slug)s
@@ -27,17 +27,28 @@ script_location = .
# sourceless = false # sourceless = false
# version location specification; this defaults # version location specification; this defaults
# to ./versions. When using multiple version # to migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path # directories, initial revisions must be specified with --version-path
# version_locations = %(here)s/bar %(here)s/bat ./versions # version_locations = %(here)s/bar %(here)s/bat migrations/versions
# the output encoding used when revision files # the output encoding used when revision files
# are written from script.py.mako # are written from script.py.mako
# output_encoding = utf-8 # output_encoding = utf-8
sqlalchemy.url = driver://user:pass@localhost/dbname sqlalchemy.url = postgres+psycopg2://postgres@localhost/cic-notify
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks=black
# black.type=console_scripts
# black.entrypoint=black
# black.options=-l 79
# Logging configuration # Logging configuration
[loggers] [loggers]
keys = root,sqlalchemy,alembic keys = root,sqlalchemy,alembic

View File

@@ -11,7 +11,7 @@ config = context.config
# Interpret the config file for Python logging. # Interpret the config file for Python logging.
# This line sets up loggers basically. # This line sets up loggers basically.
fileConfig(config.config_file_name, disable_existing_loggers=True) fileConfig(config.config_file_name)
# add your model's MetaData object here # add your model's MetaData object here
# for 'autogenerate' support # for 'autogenerate' support
@@ -56,14 +56,11 @@ def run_migrations_online():
and associate a connection with the context. and associate a connection with the context.
""" """
connectable = context.config.attributes.get("connection", None) connectable = engine_from_config(
config.get_section(config.config_ini_section),
if connectable is None: prefix="sqlalchemy.",
connectable = engine_from_config( poolclass=pool.NullPool,
context.config.get_section(context.config.config_ini_section), )
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection: with connectable.connect() as connection:
context.configure( context.configure(

View File

@@ -7,7 +7,7 @@ import celery
celery_app = celery.current_app celery_app = celery.current_app
logg = celery_app.log.get_default_logger() logg = celery_app.log.get_default_logger()
local_logg = logging.getLogger() local_logg = logging.getLogger(__name__)
@celery_app.task @celery_app.task

View File

@@ -1,9 +1,5 @@
Faker==11.1.0 pytest~=6.0.1
faker-e164==0.1.0 pytest-celery~=0.0.0a1
pytest==6.2.5 pytest-mock~=3.3.1
pytest-celery~=0.0.0 pysqlite3~=0.4.3
pytest-mock==3.6.1 pytest-cov==2.10.1
pysqlite3~=0.4.6
pytest-cov==3.0.0
pytest-alembic==0.7.0
requests-mock==1.9.3

View File

@@ -1,28 +0,0 @@
import pytest
def test_single_head_revision(alembic_runner):
heads = alembic_runner.heads
head_count = len(heads)
assert head_count == 1
def test_upgrade(alembic_runner):
try:
alembic_runner.migrate_up_to("head")
except RuntimeError:
pytest.fail('Failed to upgrade to the head revision.')
def test_up_down_consistency(alembic_runner):
try:
for revision in alembic_runner.history.revisions:
alembic_runner.migrate_up_to(revision)
except RuntimeError:
pytest.fail('Failed to upgrade through each revision individually.')
try:
for revision in reversed(alembic_runner.history.revisions):
alembic_runner.migrate_down_to(revision)
except RuntimeError:
pytest.fail('Failed to downgrade through each revision individually.')

View File

@@ -1,27 +0,0 @@
# standard imports
# external imports
from faker import Faker
from faker_e164.providers import E164Provider
# local imports
from cic_notify.db.enum import NotificationStatusEnum, NotificationTransportEnum
from cic_notify.db.models.notification import Notification
# test imports
from tests.helpers.phone import phone_number
def test_notification(init_database):
message = 'Hello world'
recipient = phone_number()
notification = Notification(NotificationTransportEnum.SMS, recipient, message)
init_database.add(notification)
init_database.commit()
notification = init_database.query(Notification).get(1)
assert notification.status == NotificationStatusEnum.UNKNOWN
assert notification.recipient == recipient
assert notification.message == message
assert notification.transport == NotificationTransportEnum.SMS

View File

@@ -1,38 +0,0 @@
# standard imports
import os
# third-party imports
# local imports
from cic_notify.db import dsn_from_config
def test_dsn_from_config(load_config):
"""
"""
# test dsn for other db formats
overrides = {
'DATABASE_PASSWORD': 'password',
'DATABASE_DRIVER': 'psycopg2',
'DATABASE_ENGINE': 'postgresql'
}
load_config.dict_override(dct=overrides, dct_description='Override values to test different db formats.')
scheme = f'{load_config.get("DATABASE_ENGINE")}+{load_config.get("DATABASE_DRIVER")}'
dsn = dsn_from_config(load_config)
assert dsn == f"{scheme}://{load_config.get('DATABASE_USER')}:{load_config.get('DATABASE_PASSWORD')}@{load_config.get('DATABASE_HOST')}:{load_config.get('DATABASE_PORT')}/{load_config.get('DATABASE_NAME')}"
# undoes overrides to revert engine and drivers to sqlite
overrides = {
'DATABASE_PASSWORD': '',
'DATABASE_DRIVER': 'pysqlite',
'DATABASE_ENGINE': 'sqlite'
}
load_config.dict_override(dct=overrides, dct_description='Override values to test different db formats.')
# test dsn for sqlite engine
dsn = dsn_from_config(load_config)
scheme = f'{load_config.get("DATABASE_ENGINE")}+{load_config.get("DATABASE_DRIVER")}'
assert dsn == f'{scheme}:///{load_config.get("DATABASE_NAME")}'

View File

@@ -1,75 +0,0 @@
# standard imports
import logging
import os
# external imports
import pytest
import requests_mock
# local imports
from cic_notify.error import NotInitializedError, AlreadyInitializedError, NotificationSendError
from cic_notify.tasks.sms.africastalking import AfricasTalkingNotifier
# test imports
from tests.helpers.phone import phone_number
def test_africas_talking_notifier(africastalking_response, caplog):
caplog.set_level(logging.DEBUG)
with pytest.raises(NotInitializedError) as error:
AfricasTalkingNotifier()
assert str(error.value) == ''
api_key = os.urandom(24).hex()
sender_id = 'bar'
username = 'sandbox'
AfricasTalkingNotifier.initialize(username, api_key, sender_id)
africastalking_notifier = AfricasTalkingNotifier()
assert africastalking_notifier.sender_id == sender_id
assert africastalking_notifier.initiated is True
with pytest.raises(AlreadyInitializedError) as error:
AfricasTalkingNotifier.initialize(username, api_key, sender_id)
assert str(error.value) == ''
with requests_mock.Mocker(real_http=False) as request_mocker:
message = 'Hello world.'
recipient = phone_number()
africastalking_response.get('SMSMessageData').get('Recipients')[0]['number'] = recipient
request_mocker.register_uri(method='POST',
headers={'content-type': 'application/json'},
json=africastalking_response,
url='https://api.sandbox.africastalking.com/version1/messaging',
status_code=200)
africastalking_notifier.send(message, recipient)
assert f'Africastalking response sender-id {africastalking_response}' in caplog.text
africastalking_notifier.sender_id = None
africastalking_notifier.send(message, recipient)
assert f'africastalking response no-sender-id {africastalking_response}' in caplog.text
with pytest.raises(NotificationSendError) as error:
status = 'InvalidPhoneNumber'
status_code = 403
africastalking_response.get('SMSMessageData').get('Recipients')[0]['status'] = status
africastalking_response.get('SMSMessageData').get('Recipients')[0]['statusCode'] = status_code
request_mocker.register_uri(method='POST',
headers={'content-type': 'application/json'},
json=africastalking_response,
url='https://api.sandbox.africastalking.com/version1/messaging',
status_code=200)
africastalking_notifier.send(message, recipient)
assert str(error.value) == f'Sending notification failed due to: {status}'
with pytest.raises(NotificationSendError) as error:
recipients = []
status = 'InsufficientBalance'
africastalking_response.get('SMSMessageData')['Recipients'] = recipients
africastalking_response.get('SMSMessageData')['Message'] = status
request_mocker.register_uri(method='POST',
headers={'content-type': 'application/json'},
json=africastalking_response,
url='https://api.sandbox.africastalking.com/version1/messaging',
status_code=200)
africastalking_notifier.send(message, recipient)
assert str(error.value) == f'Unexpected number of recipients: {len(recipients)}. Status: {status}'

View File

@@ -1,26 +0,0 @@
# standard imports
# external imports
import celery
# local imports
from cic_notify.db.enum import NotificationStatusEnum, NotificationTransportEnum
from cic_notify.db.models.notification import Notification
# test imports
from tests.helpers.phone import phone_number
def test_persist_notification(celery_session_worker, init_database):
message = 'Hello world.'
recipient = phone_number()
s_persist_notification = celery.signature(
'cic_notify.tasks.sms.db.persist_notification', (message, recipient)
)
s_persist_notification.apply_async().get()
notification = Notification.session.query(Notification).filter_by(recipient=recipient).first()
assert notification.status == NotificationStatusEnum.UNKNOWN
assert notification.recipient == recipient
assert notification.message == message
assert notification.transport == NotificationTransportEnum.SMS

View File

@@ -1,21 +0,0 @@
# standard imports
import logging
# external imports
import celery
# local imports
# test imports
from tests.helpers.phone import phone_number
def test_log(caplog, celery_session_worker):
message = 'Hello world.'
recipient = phone_number()
caplog.set_level(logging.INFO)
s_log = celery.signature(
'cic_notify.tasks.sms.log.log', [message, recipient]
)
s_log.apply_async().get()
assert f'message to {recipient}: {message}' in caplog.text

View File

@@ -1,24 +0,0 @@
# standard imports
# external imports
import celery
# local imports
from cic_notify.api import Api
# test imports
from tests.helpers.phone import phone_number
def test_api(celery_session_worker, mocker):
mocked_group = mocker.patch('celery.group')
message = 'Hello world.'
recipient = phone_number()
s_send = celery.signature('cic_notify.tasks.sms.africastalking.send', [message, recipient], queue=None)
s_log = celery.signature('cic_notify.tasks.sms.log.log', [message, recipient], queue=None)
s_persist_notification = celery.signature(
'cic_notify.tasks.sms.db.persist_notification', [message, recipient], queue=None)
signatures = [s_send, s_log, s_persist_notification]
api = Api(queue=None)
api.sms(message, recipient)
mocked_group.assert_called_with(signatures)

View File

@@ -1,13 +1,31 @@
# standard imports # standard imports
import sys
import os
import pytest
import logging import logging
# third party imports # third party imports
import confini
script_dir = os.path.dirname(os.path.realpath(__file__))
root_dir = os.path.dirname(script_dir)
sys.path.insert(0, root_dir)
# local imports # local imports
from cic_notify.db.models.base import SessionBase
#from transport.notification import AfricastalkingNotification
# test imports # fixtures
from tests.fixtures_config import *
from tests.fixtures_celery import *
from tests.fixtures_database import *
from .fixtures.celery import * logg = logging.getLogger()
from .fixtures.config import *
from .fixtures.database import *
from .fixtures.result import * #@pytest.fixture(scope='session')
#def africastalking_notification(
# load_config,
# ):
# return AfricastalkingNotificationTransport(load_config)
#

View File

@@ -1,32 +0,0 @@
# standard imports
import os
import logging
# external imports
import pytest
from confini import Config
logg = logging.getLogger(__file__)
fixtures_dir = os.path.dirname(__file__)
root_directory = os.path.dirname(os.path.dirname(fixtures_dir))
@pytest.fixture(scope='session')
def alembic_config():
migrations_directory = os.path.join(root_directory, 'cic_notify', 'db', 'migrations', 'default')
file = os.path.join(migrations_directory, 'alembic.ini')
return {
'file': file,
'script_location': migrations_directory
}
@pytest.fixture(scope='session')
def load_config():
config_directory = os.path.join(root_directory, '.config/test')
config = Config(default_dir=config_directory)
config.process()
logg.debug('config loaded\n{}'.format(config))
return config

View File

@@ -1,54 +0,0 @@
# standard imports
import os
# third-party imports
import pytest
import alembic
from alembic.config import Config as AlembicConfig
# local imports
from cic_notify.db import dsn_from_config
from cic_notify.db.models.base import SessionBase, create_engine
from .config import root_directory
@pytest.fixture(scope='session')
def alembic_engine(load_config):
data_source_name = dsn_from_config(load_config)
return create_engine(data_source_name)
@pytest.fixture(scope='session')
def database_engine(load_config):
if load_config.get('DATABASE_ENGINE') == 'sqlite':
try:
os.unlink(load_config.get('DATABASE_NAME'))
except FileNotFoundError:
pass
dsn = dsn_from_config(load_config)
SessionBase.connect(dsn)
return dsn
@pytest.fixture(scope='function')
def init_database(load_config, database_engine):
db_directory = os.path.join(root_directory, 'cic_notify', 'db')
migrations_directory = os.path.join(db_directory, 'migrations', load_config.get('DATABASE_ENGINE'))
if not os.path.isdir(migrations_directory):
migrations_directory = os.path.join(db_directory, 'migrations', 'default')
session = SessionBase.create_session()
alembic_config = AlembicConfig(os.path.join(migrations_directory, 'alembic.ini'))
alembic_config.set_main_option('sqlalchemy.url', database_engine)
alembic_config.set_main_option('script_location', migrations_directory)
alembic.command.downgrade(alembic_config, 'base')
alembic.command.upgrade(alembic_config, 'head')
yield session
session.commit()
session.close()

View File

@@ -1,24 +0,0 @@
# standard imports
# external imports
import pytest
# local imports
# test imports
@pytest.fixture(scope="function")
def africastalking_response():
return {
"SMSMessageData": {
"Message": "Sent to 1/1 Total Cost: KES 0.8000",
"Recipients": [{
"statusCode": 101,
"number": "+254711XXXYYY",
"status": "Success",
"cost": "KES 0.8000",
"messageId": "ATPid_SampleTxnId123"
}]
}
}

View File

@@ -37,6 +37,12 @@ def celery_config():
shutil.rmtree(rq) shutil.rmtree(rq)
@pytest.fixture(scope='session')
def celery_worker_parameters():
return {
# 'queues': ('cic-notify'),
}
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
def celery_enable_logging(): def celery_enable_logging():
return True return True

View File

@@ -0,0 +1,20 @@
# standard imports
import os
import logging
# third-party imports
import pytest
import confini
script_dir = os.path.dirname(os.path.realpath(__file__))
root_dir = os.path.dirname(script_dir)
logg = logging.getLogger(__file__)
@pytest.fixture(scope='session')
def load_config():
config_dir = os.path.join(root_dir, '.config/test')
conf = confini.Config(config_dir, 'CICTEST')
conf.process()
logg.debug('config {}'.format(conf))
return conf

View File

@@ -0,0 +1,48 @@
# standard imports
import os
# third-party imports
import pytest
import alembic
from alembic.config import Config as AlembicConfig
# local imports
from cic_notify.db import SessionBase
from cic_notify.db import dsn_from_config
@pytest.fixture(scope='session')
def database_engine(
load_config,
):
dsn = dsn_from_config(load_config)
SessionBase.connect(dsn)
return dsn
@pytest.fixture(scope='function')
def init_database(
load_config,
database_engine,
):
rootdir = os.path.dirname(os.path.dirname(__file__))
dbdir = os.path.join(rootdir, 'cic_notify', 'db')
migrationsdir = os.path.join(dbdir, 'migrations', load_config.get('DATABASE_ENGINE'))
if not os.path.isdir(migrationsdir):
migrationsdir = os.path.join(dbdir, 'migrations', 'default')
session = SessionBase.create_session()
ac = AlembicConfig(os.path.join(migrationsdir, 'alembic.ini'))
ac.set_main_option('sqlalchemy.url', database_engine)
ac.set_main_option('script_location', migrationsdir)
alembic.command.downgrade(ac, 'base')
alembic.command.upgrade(ac, 'head')
yield session
session.commit()
session.close()

View File

@@ -1,16 +0,0 @@
# standard imports
# external imports
from faker import Faker
from faker_e164.providers import E164Provider
# local imports
# test imports
fake = Faker()
fake.add_provider(E164Provider)
def phone_number() -> str:
return fake.e164('KE')

View File

@@ -0,0 +1,34 @@
# standard imports
import json
# third party imports
import pytest
import celery
# local imports
from cic_notify.tasks.sms import db
from cic_notify.tasks.sms import log
def test_log_notification(
celery_session_worker,
):
recipient = '+25412121212'
content = 'bar'
s_log = celery.signature('cic_notify.tasks.sms.log.log')
t = s_log.apply_async(args=[recipient, content])
r = t.get()
def test_db_notification(
init_database,
celery_session_worker,
):
recipient = '+25412121213'
content = 'foo'
s_db = celery.signature('cic_notify.tasks.sms.db.persist_notification')
t = s_db.apply_async(args=[recipient, content])
r = t.get()

View File

@@ -3,5 +3,4 @@ omit =
venv/* venv/*
scripts/* scripts/*
cic_ussd/db/migrations/* cic_ussd/db/migrations/*
cic_ussd/runnable/* cic_ussd/runnable/*
cic_ussd/version.py

View File

@@ -1,22 +0,0 @@
# standard imports
# external imports
# local imports
class Guardianship:
guardians: list = []
@classmethod
def load_system_guardians(cls, guardians_file: str):
with open(guardians_file, 'r') as system_guardians:
cls.guardians = [line.strip() for line in system_guardians]
def is_system_guardian(self, phone_number: str):
"""
:param phone_number:
:type phone_number:
:return:
:rtype:
"""
return phone_number in self.guardians

View File

@@ -13,6 +13,7 @@ from cic_types.condiments import MetadataPointer
from cic_ussd.account.chain import Chain from cic_ussd.account.chain import Chain
from cic_ussd.account.transaction import from_wei from cic_ussd.account.transaction import from_wei
from cic_ussd.cache import cache_data_key, get_cached_data from cic_ussd.cache import cache_data_key, get_cached_data
from cic_ussd.translation import translation_for
logg = logging.getLogger(__name__) logg = logging.getLogger(__name__)
@@ -96,3 +97,17 @@ def query_statement(blockchain_address: str, limit: int = 9):
callback_param=blockchain_address callback_param=blockchain_address
) )
cic_eth_api.list(address=blockchain_address, limit=limit) cic_eth_api.list(address=blockchain_address, limit=limit)
def statement_transaction_set(preferred_language: str, transaction_reprs: list):
"""
:param preferred_language:
:type preferred_language:
:param transaction_reprs:
:type transaction_reprs:
:return:
:rtype:
"""
if not transaction_reprs:
return translation_for('helpers.no_transaction_history', preferred_language)
return ''.join(f'{transaction_repr}\n' for transaction_repr in transaction_reprs)

View File

@@ -15,6 +15,7 @@ from cic_ussd.cache import cache_data, cache_data_key, get_cached_data
from cic_ussd.error import CachedDataNotFoundError, SeppukuError from cic_ussd.error import CachedDataNotFoundError, SeppukuError
from cic_ussd.metadata.tokens import query_token_info, query_token_metadata from cic_ussd.metadata.tokens import query_token_info, query_token_metadata
from cic_ussd.processor.util import wait_for_cache from cic_ussd.processor.util import wait_for_cache
from cic_ussd.translation import translation_for
logg = logging.getLogger(__file__) logg = logging.getLogger(__file__)
@@ -325,3 +326,16 @@ def set_active_token(blockchain_address: str, token_symbol: str):
cache_data(key=key, data=token_symbol) cache_data(key=key, data=token_symbol)
def token_list_set(preferred_language: str, token_data_reprs: list):
"""
:param preferred_language:
:type preferred_language:
:param token_data_reprs:
:type token_data_reprs:
:return:
:rtype:
"""
if not token_data_reprs:
return translation_for('helpers.no_tokens_list', preferred_language)
return ''.join(f'{token_data_repr}\n' for token_data_repr in token_data_reprs)

View File

@@ -14,7 +14,7 @@ class Cache:
store: Redis = None store: Redis = None
def cache_data(key: str, data: [bytes, float, int, str]): def cache_data(key: str, data: str):
""" """
:param key: :param key:
:type key: :type key:
@@ -55,6 +55,5 @@ def cache_data_key(identifier: Union[list, bytes], salt: MetadataPointer):
hash_object.update(identity) hash_object.update(identity)
else: else:
hash_object.update(identifier) hash_object.update(identifier)
if salt != MetadataPointer.NONE: hash_object.update(salt.value.encode(encoding="utf-8"))
hash_object.update(salt.value.encode(encoding="utf-8"))
return hash_object.digest().hex() return hash_object.digest().hex()

View File

@@ -63,7 +63,10 @@ class Account(SessionBase):
def remove_guardian(self, phone_number: str): def remove_guardian(self, phone_number: str):
set_guardians = self.guardians.split(',') set_guardians = self.guardians.split(',')
set_guardians.remove(phone_number) set_guardians.remove(phone_number)
self.guardians = ','.join(set_guardians) if len(set_guardians) > 1:
self.guardians = ','.join(set_guardians)
else:
self.guardians = set_guardians[0]
def get_guardians(self) -> list: def get_guardians(self) -> list:
return self.guardians.split(',') if self.guardians else [] return self.guardians.split(',') if self.guardians else []
@@ -168,7 +171,7 @@ class Account(SessionBase):
return check_password_hash(password, self.password_hash) return check_password_hash(password, self.password_hash)
def create(chain_str: str, phone_number: str, session: Session, preferred_language: str): def create(chain_str: str, phone_number: str, session: Session):
""" """
:param chain_str: :param chain_str:
:type chain_str: :type chain_str:
@@ -176,14 +179,12 @@ def create(chain_str: str, phone_number: str, session: Session, preferred_langua
:type phone_number: :type phone_number:
:param session: :param session:
:type session: :type session:
:param preferred_language:
:type preferred_language:
:return: :return:
:rtype: :rtype:
""" """
api = Api(callback_task='cic_ussd.tasks.callback_handler.account_creation_callback', api = Api(callback_task='cic_ussd.tasks.callback_handler.account_creation_callback',
callback_queue='cic-ussd', callback_queue='cic-ussd',
callback_param=preferred_language, callback_param='',
chain_str=chain_str) chain_str=chain_str)
task_uuid = api.create_account().id task_uuid = api.create_account().id
TaskTracker.add(session=session, task_uuid=task_uuid) TaskTracker.add(session=session, task_uuid=task_uuid)

View File

@@ -2,441 +2,417 @@
"ussd_menu": { "ussd_menu": {
"1": { "1": {
"description": "Entry point for users to select their preferred language.", "description": "Entry point for users to select their preferred language.",
"display_key": "ussd.initial_language_selection", "display_key": "ussd.kenya.initial_language_selection",
"name": "initial_language_selection", "name": "initial_language_selection",
"parent": null "parent": null
}, },
"2": { "2": {
"description": "Entry point for users to enter a pin to secure their account.", "description": "Entry point for users to enter a pin to secure their account.",
"display_key": "ussd.initial_pin_entry", "display_key": "ussd.kenya.initial_pin_entry",
"name": "initial_pin_entry", "name": "initial_pin_entry",
"parent": null "parent": null
}, },
"3": { "3": {
"description": "Pin confirmation entry menu.", "description": "Pin confirmation entry menu.",
"display_key": "ussd.initial_pin_confirmation", "display_key": "ussd.kenya.initial_pin_confirmation",
"name": "initial_pin_confirmation", "name": "initial_pin_confirmation",
"parent": "initial_pin_entry" "parent": "initial_pin_entry"
}, },
"4": { "4": {
"description": "The signup process has been initiated and the account is being created.", "description": "The signup process has been initiated and the account is being created.",
"display_key": "ussd.account_creation_prompt", "display_key": "ussd.kenya.account_creation_prompt",
"name": "account_creation_prompt", "name": "account_creation_prompt",
"parent": null "parent": null
}, },
"5": { "5": {
"description": "Entry point for activated users.", "description": "Entry point for activated users.",
"display_key": "ussd.start", "display_key": "ussd.kenya.start",
"name": "start", "name": "start",
"parent": null "parent": null
}, },
"6": { "6": {
"description": "Given name entry menu.", "description": "Given name entry menu.",
"display_key": "ussd.enter_given_name", "display_key": "ussd.kenya.enter_given_name",
"name": "enter_given_name", "name": "enter_given_name",
"parent": "metadata_management" "parent": "metadata_management"
}, },
"7": { "7": {
"description": "Family name entry menu.", "description": "Family name entry menu.",
"display_key": "ussd.enter_family_name", "display_key": "ussd.kenya.enter_family_name",
"name": "enter_family_name", "name": "enter_family_name",
"parent": "metadata_management" "parent": "metadata_management"
}, },
"8": { "8": {
"description": "Gender entry menu.", "description": "Gender entry menu.",
"display_key": "ussd.enter_gender", "display_key": "ussd.kenya.enter_gender",
"name": "enter_gender", "name": "enter_gender",
"parent": "metadata_management" "parent": "metadata_management"
}, },
"9": { "9": {
"description": "Age entry menu.", "description": "Age entry menu.",
"display_key": "ussd.enter_gender", "display_key": "ussd.kenya.enter_gender",
"name": "enter_gender", "name": "enter_gender",
"parent": "metadata_management" "parent": "metadata_management"
}, },
"10": { "10": {
"description": "Location entry menu.", "description": "Location entry menu.",
"display_key": "ussd.enter_location", "display_key": "ussd.kenya.enter_location",
"name": "enter_location", "name": "enter_location",
"parent": "metadata_management" "parent": "metadata_management"
}, },
"11": { "11": {
"description": "Products entry menu.", "description": "Products entry menu.",
"display_key": "ussd.enter_products", "display_key": "ussd.kenya.enter_products",
"name": "enter_products", "name": "enter_products",
"parent": "metadata_management" "parent": "metadata_management"
}, },
"12": { "12": {
"description": "Entry point for activated users.", "description": "Entry point for activated users.",
"display_key": "ussd.start", "display_key": "ussd.kenya.start",
"name": "start", "name": "start",
"parent": null "parent": null
}, },
"13": { "13": {
"description": "Send Token recipient entry.", "description": "Send Token recipient entry.",
"display_key": "ussd.enter_transaction_recipient", "display_key": "ussd.kenya.enter_transaction_recipient",
"name": "enter_transaction_recipient", "name": "enter_transaction_recipient",
"parent": "start" "parent": "start"
}, },
"14": { "14": {
"description": "Send Token amount prompt menu.", "description": "Send Token amount prompt menu.",
"display_key": "ussd.enter_transaction_amount", "display_key": "ussd.kenya.enter_transaction_amount",
"name": "enter_transaction_amount", "name": "enter_transaction_amount",
"parent": "start" "parent": "start"
}, },
"15": { "15": {
"description": "Pin entry for authorization to send token.", "description": "Pin entry for authorization to send token.",
"display_key": "ussd.transaction_pin_authorization", "display_key": "ussd.kenya.transaction_pin_authorization",
"name": "transaction_pin_authorization", "name": "transaction_pin_authorization",
"parent": "start" "parent": "start"
}, },
"16": { "16": {
"description": "Manage account menu.", "description": "Manage account menu.",
"display_key": "ussd.account_management", "display_key": "ussd.kenya.account_management",
"name": "account_management", "name": "account_management",
"parent": "start" "parent": "start"
}, },
"17": { "17": {
"description": "Manage metadata menu.", "description": "Manage metadata menu.",
"display_key": "ussd.metadata_management", "display_key": "ussd.kenya.metadata_management",
"name": "metadata_management", "name": "metadata_management",
"parent": "start" "parent": "start"
}, },
"18": { "18": {
"description": "Manage user's preferred language menu.", "description": "Manage user's preferred language menu.",
"display_key": "ussd.select_preferred_language", "display_key": "ussd.kenya.select_preferred_language",
"name": "select_preferred_language", "name": "select_preferred_language",
"parent": "account_management" "parent": "account_management"
}, },
"19": { "19": {
"description": "Retrieve mini-statement menu.", "description": "Retrieve mini-statement menu.",
"display_key": "ussd.mini_statement_pin_authorization", "display_key": "ussd.kenya.mini_statement_pin_authorization",
"name": "mini_statement_pin_authorization", "name": "mini_statement_pin_authorization",
"parent": "account_management" "parent": "account_management"
}, },
"20": { "20": {
"description": "Manage user's pin menu.", "description": "Manage user's pin menu.",
"display_key": "ussd.enter_current_pin", "display_key": "ussd.kenya.enter_current_pin",
"name": "enter_current_pin", "name": "enter_current_pin",
"parent": "account_management" "parent": "account_management"
}, },
"21": { "21": {
"description": "New pin entry menu.", "description": "New pin entry menu.",
"display_key": "ussd.enter_new_pin", "display_key": "ussd.kenya.enter_new_pin",
"name": "enter_new_pin", "name": "enter_new_pin",
"parent": "account_management" "parent": "account_management"
}, },
"22": { "22": {
"description": "Pin entry menu.", "description": "Pin entry menu.",
"display_key": "ussd.display_metadata_pin_authorization", "display_key": "ussd.kenya.display_metadata_pin_authorization",
"name": "display_metadata_pin_authorization", "name": "display_metadata_pin_authorization",
"parent": "start" "parent": "start"
}, },
"23": { "23": {
"description": "Exit menu.", "description": "Exit menu.",
"display_key": "ussd.exit", "display_key": "ussd.kenya.exit",
"name": "exit", "name": "exit",
"parent": null "parent": null
}, },
"24": { "24": {
"description": "Invalid menu option.", "description": "Invalid menu option.",
"display_key": "ussd.exit_invalid_menu_option", "display_key": "ussd.kenya.exit_invalid_menu_option",
"name": "exit_invalid_menu_option", "name": "exit_invalid_menu_option",
"parent": null "parent": null
}, },
"25": { "25": {
"description": "Pin policy violation.", "description": "Pin policy violation.",
"display_key": "ussd.exit_invalid_pin", "display_key": "ussd.kenya.exit_invalid_pin",
"name": "exit_invalid_pin", "name": "exit_invalid_pin",
"parent": null "parent": null
}, },
"26": { "26": {
"description": "Pin mismatch. New pin and the new pin confirmation do not match", "description": "Pin mismatch. New pin and the new pin confirmation do not match",
"display_key": "ussd.exit_pin_mismatch", "display_key": "ussd.kenya.exit_pin_mismatch",
"name": "exit_pin_mismatch", "name": "exit_pin_mismatch",
"parent": null "parent": null
}, },
"27": { "27": {
"description": "Ussd pin blocked Menu", "description": "Ussd pin blocked Menu",
"display_key": "ussd.exit_pin_blocked", "display_key": "ussd.kenya.exit_pin_blocked",
"name": "exit_pin_blocked", "name": "exit_pin_blocked",
"parent": null "parent": null
}, },
"28": { "28": {
"description": "Key params missing in request.", "description": "Key params missing in request.",
"display_key": "ussd.exit_invalid_request", "display_key": "ussd.kenya.exit_invalid_request",
"name": "exit_invalid_request", "name": "exit_invalid_request",
"parent": null "parent": null
}, },
"29": { "29": {
"description": "The user did not select a choice.", "description": "The user did not select a choice.",
"display_key": "ussd.exit_invalid_input", "display_key": "ussd.kenya.exit_invalid_input",
"name": "exit_invalid_input", "name": "exit_invalid_input",
"parent": null "parent": null
}, },
"30": { "30": {
"description": "Exit following unsuccessful transaction due to insufficient account balance.", "description": "Exit following unsuccessful transaction due to insufficient account balance.",
"display_key": "ussd.exit_insufficient_balance", "display_key": "ussd.kenya.exit_insufficient_balance",
"name": "exit_insufficient_balance", "name": "exit_insufficient_balance",
"parent": null "parent": null
}, },
"31": { "31": {
"description": "Exit following a successful transaction.", "description": "Exit following a successful transaction.",
"display_key": "ussd.exit_successful_transaction", "display_key": "ussd.kenya.exit_successful_transaction",
"name": "exit_successful_transaction", "name": "exit_successful_transaction",
"parent": null "parent": null
}, },
"32": { "32": {
"description": "End of a menu flow.", "description": "End of a menu flow.",
"display_key": "ussd.complete", "display_key": "ussd.kenya.complete",
"name": "complete", "name": "complete",
"parent": null "parent": null
}, },
"33": { "33": {
"description": "Pin entry menu to view account balances.", "description": "Pin entry menu to view account balances.",
"display_key": "ussd.account_balances_pin_authorization", "display_key": "ussd.kenya.account_balances_pin_authorization",
"name": "account_balances_pin_authorization", "name": "account_balances_pin_authorization",
"parent": "account_management" "parent": "account_management"
}, },
"34": { "34": {
"description": "Pin entry menu to view account statement.", "description": "Pin entry menu to view account statement.",
"display_key": "ussd.account_statement_pin_authorization", "display_key": "ussd.kenya.account_statement_pin_authorization",
"name": "account_statement_pin_authorization", "name": "account_statement_pin_authorization",
"parent": "account_management" "parent": "account_management"
}, },
"35": { "35": {
"description": "Menu to display account balances.", "description": "Menu to display account balances.",
"display_key": "ussd.account_balances", "display_key": "ussd.kenya.account_balances",
"name": "account_balances", "name": "account_balances",
"parent": "account_management" "parent": "account_management"
}, },
"36": { "36": {
"description": "Menu to display first set of transactions in statement.", "description": "Menu to display first set of transactions in statement.",
"display_key": "ussd.first_transaction_set", "display_key": "ussd.kenya.first_transaction_set",
"name": "first_transaction_set", "name": "first_transaction_set",
"parent": "account_management" "parent": null
}, },
"37": { "37": {
"description": "Menu to display middle set of transactions in statement.", "description": "Menu to display middle set of transactions in statement.",
"display_key": "ussd.middle_transaction_set", "display_key": "ussd.kenya.middle_transaction_set",
"name": "middle_transaction_set", "name": "middle_transaction_set",
"parent": null "parent": null
}, },
"38": { "38": {
"description": "Menu to display last set of transactions in statement.", "description": "Menu to display last set of transactions in statement.",
"display_key": "ussd.last_transaction_set", "display_key": "ussd.kenya.last_transaction_set",
"name": "last_transaction_set", "name": "last_transaction_set",
"parent": null "parent": null
}, },
"39": { "39": {
"description": "Menu to instruct users to call the office.", "description": "Menu to instruct users to call the office.",
"display_key": "ussd.help", "display_key": "ussd.kenya.help",
"name": "help", "name": "help",
"parent": null "parent": null
}, },
"40": { "40": {
"description": "Menu to display a user's entire profile", "description": "Menu to display a user's entire profile",
"display_key": "ussd.display_user_metadata", "display_key": "ussd.kenya.display_user_metadata",
"name": "display_user_metadata", "name": "display_user_metadata",
"parent": "metadata_management" "parent": "metadata_management"
}, },
"41": { "41": {
"description": "The recipient is not in the system", "description": "The recipient is not in the system",
"display_key": "ussd.exit_invalid_recipient", "display_key": "ussd.kenya.exit_invalid_recipient",
"name": "exit_invalid_recipient", "name": "exit_invalid_recipient",
"parent": null "parent": null
}, },
"42": { "42": {
"description": "Pin entry menu for changing name data.", "description": "Pin entry menu for changing name data.",
"display_key": "ussd.name_edit_pin_authorization", "display_key": "ussd.kenya.name_edit_pin_authorization",
"name": "name_edit_pin_authorization", "name": "name_edit_pin_authorization",
"parent": "metadata_management" "parent": "metadata_management"
}, },
"43": { "43": {
"description": "Pin entry menu for changing gender data.", "description": "Pin entry menu for changing gender data.",
"display_key": "ussd.gender_edit_pin_authorization", "display_key": "ussd.kenya.gender_edit_pin_authorization",
"name": "gender_edit_pin_authorization", "name": "gender_edit_pin_authorization",
"parent": "metadata_management" "parent": "metadata_management"
}, },
"44": { "44": {
"description": "Pin entry menu for changing location data.", "description": "Pin entry menu for changing location data.",
"display_key": "ussd.location_edit_pin_authorization", "display_key": "ussd.kenya.location_edit_pin_authorization",
"name": "location_edit_pin_authorization", "name": "location_edit_pin_authorization",
"parent": "metadata_management" "parent": "metadata_management"
}, },
"45": { "45": {
"description": "Pin entry menu for changing products data.", "description": "Pin entry menu for changing products data.",
"display_key": "ussd.products_edit_pin_authorization", "display_key": "ussd.kenya.products_edit_pin_authorization",
"name": "products_edit_pin_authorization", "name": "products_edit_pin_authorization",
"parent": "metadata_management" "parent": "metadata_management"
}, },
"46": { "46": {
"description": "Pin confirmation for pin change.", "description": "Pin confirmation for pin change.",
"display_key": "ussd.new_pin_confirmation", "display_key": "ussd.kenya.new_pin_confirmation",
"name": "new_pin_confirmation", "name": "new_pin_confirmation",
"parent": "metadata_management" "parent": "metadata_management"
}, },
"47": { "47": {
"description": "Year of birth entry menu.", "description": "Year of birth entry menu.",
"display_key": "ussd.enter_date_of_birth", "display_key": "ussd.kenya.enter_date_of_birth",
"name": "enter_date_of_birth", "name": "enter_date_of_birth",
"parent": "metadata_management" "parent": "metadata_management"
}, },
"48": { "48": {
"description": "Pin entry menu for changing year of birth data.", "description": "Pin entry menu for changing year of birth data.",
"display_key": "ussd.dob_edit_pin_authorization", "display_key": "ussd.kenya.dob_edit_pin_authorization",
"name": "dob_edit_pin_authorization", "name": "dob_edit_pin_authorization",
"parent": "metadata_management" "parent": "metadata_management"
}, },
"49": { "49": {
"description": "Menu to display first set of tokens in the account's token list.", "description": "Menu to display first set of tokens in the account's token list.",
"display_key": "ussd.first_account_tokens_set", "display_key": "ussd.kenya.first_account_tokens_set",
"name": "first_account_tokens_set", "name": "first_account_tokens_set",
"parent": "start" "parent": null
}, },
"50": { "50": {
"description": "Menu to display middle set of tokens in the account's token list.", "description": "Menu to display middle set of tokens in the account's token list.",
"display_key": "ussd.middle_account_tokens_set", "display_key": "ussd.kenya.middle_account_tokens_set",
"name": "middle_account_tokens_set", "name": "middle_account_tokens_set",
"parent": null "parent": null
}, },
"51": { "51": {
"description": "Menu to display last set of tokens in the account's token list.", "description": "Menu to display last set of tokens in the account's token list.",
"display_key": "ussd.last_account_tokens_set", "display_key": "ussd.kenya.last_account_tokens_set",
"name": "last_account_tokens_set", "name": "last_account_tokens_set",
"parent": null "parent": null
}, },
"52": { "52": {
"description": "Pin entry menu for setting an active token.", "description": "Pin entry menu for setting an active token.",
"display_key": "ussd.token_selection_pin_authorization", "display_key": "ussd.kenya.token_selection_pin_authorization",
"name": "token_selection_pin_authorization", "name": "token_selection_pin_authorization",
"parent": "first_account_tokens_set" "parent": null
}, },
"53": { "53": {
"description": "Exit following a successful active token setting.", "description": "Exit following a successful active token setting.",
"display_key": "ussd.exit_successful_token_selection", "display_key": "ussd.kenya.exit_successful_token_selection",
"name": "exit_successful_token_selection", "name": "exit_successful_token_selection",
"parent": null "parent": null
}, },
"54": { "54": {
"description": "Pin management menu for operations related to an account's pin.", "description": "Pin management menu for operations related to an account's pin.",
"display_key": "ussd.pin_management", "display_key": "ussd.kenya.pin_management",
"name": "pin_management", "name": "pin_management",
"parent": "start" "parent": "start"
}, },
"55": { "55": {
"description": "Phone number entry for account whose pin is being reset.", "description": "Phone number entry for account whose pin is being reset.",
"display_key": "ussd.reset_guarded_pin", "display_key": "ussd.kenya.reset_guarded_pin",
"name": "reset_guarded_pin", "name": "reset_guarded_pin",
"parent": "pin_management" "parent": "pin_management"
}, },
"56": { "56": {
"description": "Pin entry for initiating request to reset an account's pin.", "description": "Pin entry for initiating request to reset an account's pin.",
"display_key": "ussd.reset_guarded_pin_authorization", "display_key": "ussd.kenya.reset_guarded_pin_authorization",
"name": "reset_guarded_pin_authorization", "name": "reset_guarded_pin_authorization",
"parent": "pin_management" "parent": "pin_management"
}, },
"57": { "57": {
"description": "Exit menu following successful pin reset initiation.", "description": "Exit menu following successful pin reset initiation.",
"display_key": "ussd.exit_pin_reset_initiated_success", "display_key": "ussd.kenya.exit_pin_reset_initiated_success",
"name": "exit_pin_reset_initiated_success", "name": "exit_pin_reset_initiated_success",
"parent": "pin_management" "parent": "pin_management"
}, },
"58": { "58": {
"description": "Exit menu in the event that an account is not a set guardian.", "description": "Exit menu in the event that an account is not a set guardian.",
"display_key": "ussd.exit_not_authorized_for_pin_reset", "display_key": "ussd.kenya.exit_not_authorized_for_pin_reset",
"name": "exit_not_authorized_for_pin_reset", "name": "exit_not_authorized_for_pin_reset",
"parent": "pin_management" "parent": "pin_management"
}, },
"59": { "59": {
"description": "Pin guard menu for handling guardianship operations.", "description": "Pin guard menu for handling guardianship operations.",
"display_key": "ussd.guard_pin", "display_key": "ussd.kenya.guard_pin",
"name": "guard_pin", "name": "guard_pin",
"parent": "pin_management" "parent": "pin_management"
}, },
"60": { "60": {
"description": "Pin entry to display a list of set guardians.", "description": "Pin entry to display a list of set guardians.",
"display_key": "ussd.guardian_list_pin_authorization", "display_key": "ussd.kenya.guardian_list_pin_authorization",
"name": "guardian_list_pin_authorization", "name": "guardian_list_pin_authorization",
"parent": "guard_pin" "parent": "guard_pin"
}, },
"61": { "61": {
"description": "Menu to display list of set guardians.", "description": "Menu to display list of set guardians.",
"display_key": "ussd.guardian_list", "display_key": "ussd.kenya.guardian_list",
"name": "guardian_list", "name": "guardian_list",
"parent": "guard_pin" "parent": "guard_pin"
}, },
"62": { "62": {
"description": "Phone number entry to add an account as a guardian to reset pin.", "description": "Phone number entry to add an account as a guardian to reset pin.",
"display_key": "ussd.add_guardian", "display_key": "ussd.kenya.add_guardian",
"name": "add_guardian", "name": "add_guardian",
"parent": "guard_pin" "parent": "guard_pin"
}, },
"63": { "63": {
"description": "Pin entry to confirm addition of an account as a guardian.", "description": "Pin entry to confirm addition of an account as a guardian.",
"display_key": "ussd.add_guardian_pin_authorization", "display_key": "ussd.kenya.add_guardian_pin_authorization",
"name": "add_guardian_pin_authorization", "name": "add_guardian_pin_authorization",
"parent": "guard_pin" "parent": "guard_pin"
}, },
"64": { "64": {
"description": "Exit menu when an account is successfully added as pin reset guardian.", "description": "Exit menu when an account is successfully added as pin reset guardian.",
"display_key": "ussd.exit_guardian_addition_success", "display_key": "ussd.kenya.exit_guardian_addition_success",
"name": "exit_guardian_addition_success", "name": "exit_guardian_addition_success",
"parent": "guard_pin" "parent": "guard_pin"
}, },
"65": { "65": {
"description": "Phone number entry to remove an account as a guardian to reset pin.", "description": "Phone number entry to remove an account as a guardian to reset pin.",
"display_key": "ussd.remove_guardian", "display_key": "ussd.kenya.remove_guardian",
"name": "remove_guardian", "name": "remove_guardian",
"parent": "guard_pin" "parent": "guard_pin"
}, },
"66": { "66": {
"description": "Pin entry to confirm removal of an account as a guardian.", "description": "Pin entry to confirm removal of an account as a guardian.",
"display_key": "ussd.remove_guardian_pin_authorization", "display_key": "ussd.kenya.remove_guardian_pin_authorization",
"name": "remove_guardian_pin_authorization", "name": "remove_guardian_pin_authorization",
"parent": "guard_pin" "parent": "guard_pin"
}, },
"67": { "67": {
"description": "Exit menu when an account is successfully removed as pin reset guardian.", "description": "Exit menu when an account is successfully removed as pin reset guardian.",
"display_key": "ussd.exit_guardian_removal_success", "display_key": "ussd.kenya.exit_guardian_removal_success",
"name": "exit_guardian_removal_success", "name": "exit_guardian_removal_success",
"parent": "guard_pin" "parent": "guard_pin"
}, },
"68": { "68": {
"description": "Exit menu when invalid phone number entry for guardian addition.", "description": "Exit menu when invalid phone number entry for guardian addition. ",
"display_key": "ussd.exit_invalid_guardian_addition", "display_key": "ussd.kenya.exit_invalid_guardian_addition",
"name": "exit_invalid_guardian_addition", "name": "exit_invalid_guardian_addition",
"parent": "guard_pin" "parent": "guard_pin"
}, },
"69": { "69": {
"description": "Exit menu when invalid phone number entry for guardian removal.", "description": "Exit menu when invalid phone number entry for guardian removal. ",
"display_key": "ussd.exit_invalid_guardian_removal", "display_key": "ussd.kenya.exit_invalid_guardian_removal",
"name": "exit_invalid_guardian_removal", "name": "exit_invalid_guardian_removal",
"parent": "guard_pin" "parent": "guard_pin"
},
"70": {
"description": "Menu to display middle set of languages to select.",
"display_key": "ussd.initial_middle_language_set",
"name": "initial_middle_language_set",
"parent": null
},
"71": {
"description": "Menu to display last set of languages to select.",
"display_key": "ussd.initial_last_language_set",
"name": "initial_last_language_set",
"parent": null
},
"72": {
"description": "Menu to display middle set of languages to select.",
"display_key": "ussd.middle_language_set",
"name": "middle_language_set",
"parent": null
},
"73": {
"description": "Menu to display last set of languages to select.",
"display_key": "ussd.last_language_set",
"name": "last_language_set",
"parent": null
} }
} }
} }

View File

@@ -7,4 +7,3 @@ from .custom import CustomMetadata
from .person import PersonMetadata from .person import PersonMetadata
from .phone import PhonePointerMetadata from .phone import PhonePointerMetadata
from .preferences import PreferencesMetadata from .preferences import PreferencesMetadata
from .tokens import TokenMetadata

View File

@@ -19,33 +19,34 @@ from cic_ussd.account.metadata import get_cached_preferred_language
from cic_ussd.account.statement import ( from cic_ussd.account.statement import (
get_cached_statement, get_cached_statement,
parse_statement_transactions, parse_statement_transactions,
query_statement) query_statement,
statement_transaction_set
)
from cic_ussd.account.tokens import (create_account_tokens_list, from cic_ussd.account.tokens import (create_account_tokens_list,
get_active_token_symbol, get_active_token_symbol,
get_cached_token_data, get_cached_token_data,
get_cached_token_symbol_list, get_cached_token_symbol_list,
get_cached_token_data_list, get_cached_token_data_list,
parse_token_list) parse_token_list,
token_list_set)
from cic_ussd.account.transaction import from_wei, to_wei from cic_ussd.account.transaction import from_wei, to_wei
from cic_ussd.cache import cache_data_key, cache_data, get_cached_data from cic_ussd.cache import cache_data_key, cache_data
from cic_ussd.db.models.account import Account from cic_ussd.db.models.account import Account
from cic_ussd.metadata import PersonMetadata from cic_ussd.metadata import PersonMetadata
from cic_ussd.phone_number import Support from cic_ussd.phone_number import Support
from cic_ussd.processor.util import parse_person_metadata, ussd_menu_list, wait_for_session_data from cic_ussd.processor.util import parse_person_metadata
from cic_ussd.session.ussd_session import save_session_data from cic_ussd.session.ussd_session import save_session_data
from cic_ussd.state_machine.logic.language import preferred_langauge_from_selection
from cic_ussd.translation import translation_for from cic_ussd.translation import translation_for
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
logg = logging.getLogger(__file__) logg = logging.getLogger(__name__)
class MenuProcessor: class MenuProcessor:
def __init__(self, account: Account, display_key: str, menu_name: str, session: Session, ussd_session: dict): def __init__(self, account: Account, display_key: str, menu_name: str, session: Session, ussd_session: dict):
self.account = account self.account = account
self.display_key = display_key self.display_key = display_key
if account: self.identifier = bytes.fromhex(self.account.blockchain_address)
self.identifier = bytes.fromhex(self.account.blockchain_address)
self.menu_name = menu_name self.menu_name = menu_name
self.session = session self.session = session
self.ussd_session = ussd_session self.ussd_session = ussd_session
@@ -88,29 +89,36 @@ class MenuProcessor:
:rtype: :rtype:
""" """
cached_statement = get_cached_statement(self.account.blockchain_address) cached_statement = get_cached_statement(self.account.blockchain_address)
transaction_sets = []
if cached_statement:
statement = json.loads(cached_statement)
statement_transactions = parse_statement_transactions(statement)
transaction_sets = [statement_transactions[tx:tx + 3] for tx in range(0, len(statement_transactions), 3)]
preferred_language = get_cached_preferred_language(self.account.blockchain_address) preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language: if not preferred_language:
preferred_language = i18n.config.get('fallback') preferred_language = i18n.config.get('fallback')
no_transaction_history = statement_transaction_set(preferred_language, transaction_sets)
first_transaction_set = no_transaction_history
middle_transaction_set = no_transaction_history
last_transaction_set = no_transaction_history
if transaction_sets:
first_transaction_set = statement_transaction_set(preferred_language, transaction_sets[0])
if len(transaction_sets) >= 2:
middle_transaction_set = statement_transaction_set(preferred_language, transaction_sets[1])
if len(transaction_sets) >= 3:
last_transaction_set = statement_transaction_set(preferred_language, transaction_sets[2])
statement_list = [] if self.display_key == 'ussd.kenya.first_transaction_set':
if cached_statement:
statement_list = parse_statement_transactions(statement=json.loads(cached_statement))
fallback = translation_for('helpers.no_transaction_history', preferred_language)
transaction_sets = ussd_menu_list(fallback=fallback, menu_list=statement_list, split=3)
if self.display_key == 'ussd.first_transaction_set':
return translation_for( return translation_for(
self.display_key, preferred_language, first_transaction_set=transaction_sets[0] self.display_key, preferred_language, first_transaction_set=first_transaction_set
) )
if self.display_key == 'ussd.middle_transaction_set': if self.display_key == 'ussd.kenya.middle_transaction_set':
return translation_for( return translation_for(
self.display_key, preferred_language, middle_transaction_set=transaction_sets[1] self.display_key, preferred_language, middle_transaction_set=middle_transaction_set
) )
if self.display_key == 'ussd.last_transaction_set': if self.display_key == 'ussd.kenya.last_transaction_set':
return translation_for( return translation_for(
self.display_key, preferred_language, last_transaction_set=transaction_sets[2] self.display_key, preferred_language, last_transaction_set=last_transaction_set
) )
def add_guardian_pin_authorization(self): def add_guardian_pin_authorization(self):
@@ -121,7 +129,7 @@ class MenuProcessor:
preferred_language = get_cached_preferred_language(self.account.blockchain_address) preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language: if not preferred_language:
preferred_language = i18n.config.get('fallback') preferred_language = i18n.config.get('fallback')
set_guardians = self.account.get_guardians()[:3] set_guardians = self.account.get_guardians()
if set_guardians: if set_guardians:
guardians_list = '' guardians_list = ''
guardians_list_header = translation_for('helpers.guardians_list_header', preferred_language) guardians_list_header = translation_for('helpers.guardians_list_header', preferred_language)
@@ -137,30 +145,36 @@ class MenuProcessor:
def account_tokens(self) -> str: def account_tokens(self) -> str:
cached_token_data_list = get_cached_token_data_list(self.account.blockchain_address) cached_token_data_list = get_cached_token_data_list(self.account.blockchain_address)
token_data_list = parse_token_list(cached_token_data_list) token_data_list = parse_token_list(cached_token_data_list)
token_list_sets = [token_data_list[tds:tds + 3] for tds in range(0, len(token_data_list), 3)]
preferred_language = get_cached_preferred_language(self.account.blockchain_address) preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language: if not preferred_language:
preferred_language = i18n.config.get('fallback') preferred_language = i18n.config.get('fallback')
no_token_list = token_list_set(preferred_language, [])
first_account_tokens_set = no_token_list
middle_account_tokens_set = no_token_list
last_account_tokens_set = no_token_list
if token_list_sets:
data = {
'account_tokens_list': cached_token_data_list
}
save_session_data(data=data, queue='cic-ussd', session=self.session, ussd_session=self.ussd_session)
first_account_tokens_set = token_list_set(preferred_language, token_list_sets[0])
fallback = translation_for('helpers.no_tokens_list', preferred_language) if len(token_list_sets) >= 2:
token_list_sets = ussd_menu_list(fallback=fallback, menu_list=token_data_list, split=3) middle_account_tokens_set = token_list_set(preferred_language, token_list_sets[1])
if len(token_list_sets) >= 3:
data = { last_account_tokens_set = token_list_set(preferred_language, token_list_sets[2])
'account_tokens_list': cached_token_data_list if self.display_key == 'ussd.kenya.first_account_tokens_set':
}
save_session_data(data=data, queue='cic-ussd', session=self.session, ussd_session=self.ussd_session)
if self.display_key == 'ussd.first_account_tokens_set':
return translation_for( return translation_for(
self.display_key, preferred_language, first_account_tokens_set=token_list_sets[0] self.display_key, preferred_language, first_account_tokens_set=first_account_tokens_set
) )
if self.display_key == 'ussd.middle_account_tokens_set': if self.display_key == 'ussd.kenya.middle_account_tokens_set':
return translation_for( return translation_for(
self.display_key, preferred_language, middle_account_tokens_set=token_list_sets[1] self.display_key, preferred_language, middle_account_tokens_set=middle_account_tokens_set
) )
if self.display_key == 'ussd.last_account_tokens_set': if self.display_key == 'ussd.kenya.last_account_tokens_set':
return translation_for( return translation_for(
self.display_key, preferred_language, last_account_tokens_set=token_list_sets[2] self.display_key, preferred_language, last_account_tokens_set=last_account_tokens_set
) )
def help(self) -> str: def help(self) -> str:
@@ -208,7 +222,7 @@ class MenuProcessor:
remaining_attempts = 3 remaining_attempts = 3
remaining_attempts -= self.account.failed_pin_attempts remaining_attempts -= self.account.failed_pin_attempts
retry_pin_entry = translation_for( retry_pin_entry = translation_for(
'ussd.retry_pin_entry', preferred_language, remaining_attempts=remaining_attempts 'ussd.kenya.retry_pin_entry', preferred_language, remaining_attempts=remaining_attempts
) )
return translation_for( return translation_for(
f'{self.display_key}.retry', preferred_language, retry_pin_entry=retry_pin_entry f'{self.display_key}.retry', preferred_language, retry_pin_entry=retry_pin_entry
@@ -224,38 +238,6 @@ class MenuProcessor:
guardian = Account.get_by_phone_number(guardian_phone_number, self.session) guardian = Account.get_by_phone_number(guardian_phone_number, self.session)
return guardian.standard_metadata_id() return guardian.standard_metadata_id()
def language(self):
key = cache_data_key('system:languages'.encode('utf-8'), MetadataPointer.NONE)
cached_system_languages = get_cached_data(key)
language_list: list = json.loads(cached_system_languages)
if self.account:
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
else:
preferred_language = i18n.config.get('fallback')
fallback = translation_for('helpers.no_language_list', preferred_language)
language_list_sets = ussd_menu_list(fallback=fallback, menu_list=language_list, split=3)
if self.display_key in ['ussd.initial_language_selection', 'ussd.select_preferred_language']:
return translation_for(
self.display_key, preferred_language, first_language_set=language_list_sets[0]
)
if 'middle_language_set' in self.display_key:
return translation_for(
self.display_key, preferred_language, middle_language_set=language_list_sets[1]
)
if 'last_language_set' in self.display_key:
return translation_for(
self.display_key, preferred_language, last_language_set=language_list_sets[2]
)
def account_creation_prompt(self):
preferred_language = preferred_langauge_from_selection(self.ussd_session.get('user_input'))
return translation_for(self.display_key, preferred_language)
def reset_guarded_pin_authorization(self): def reset_guarded_pin_authorization(self):
guarded_account_information = self.guarded_account_metadata() guarded_account_information = self.guarded_account_metadata()
return self.pin_authorization(guarded_account_information=guarded_account_information) return self.pin_authorization(guarded_account_information=guarded_account_information)
@@ -399,9 +381,8 @@ class MenuProcessor:
) )
def exit_invalid_menu_option(self): def exit_invalid_menu_option(self):
if self.account: preferred_language = get_cached_preferred_language(self.account.blockchain_address)
preferred_language = get_cached_preferred_language(self.account.blockchain_address) if not preferred_language:
else:
preferred_language = i18n.config.get('fallback') preferred_language = i18n.config.get('fallback')
return translation_for(self.display_key, preferred_language, support_phone=Support.phone_number) return translation_for(self.display_key, preferred_language, support_phone=Support.phone_number)
@@ -409,7 +390,7 @@ class MenuProcessor:
preferred_language = get_cached_preferred_language(self.account.blockchain_address) preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language: if not preferred_language:
preferred_language = i18n.config.get('fallback') preferred_language = i18n.config.get('fallback')
return translation_for('ussd.exit_pin_blocked', preferred_language, support_phone=Support.phone_number) return translation_for('ussd.kenya.exit_pin_blocked', preferred_language, support_phone=Support.phone_number)
def exit_successful_token_selection(self) -> str: def exit_successful_token_selection(self) -> str:
selected_token = self.ussd_session.get('data').get('selected_token') selected_token = self.ussd_session.get('data').get('selected_token')
@@ -417,7 +398,7 @@ class MenuProcessor:
preferred_language = get_cached_preferred_language(self.account.blockchain_address) preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language: if not preferred_language:
preferred_language = i18n.config.get('fallback') preferred_language = i18n.config.get('fallback')
return translation_for(self.display_key, preferred_language, token_symbol=token_symbol) return translation_for(self.display_key,preferred_language,token_symbol=token_symbol)
def exit_successful_transaction(self): def exit_successful_transaction(self):
""" """
@@ -464,9 +445,6 @@ def response(account: Account, display_key: str, menu_name: str, session: Sessio
""" """
menu_processor = MenuProcessor(account, display_key, menu_name, session, ussd_session) menu_processor = MenuProcessor(account, display_key, menu_name, session, ussd_session)
if menu_name == 'account_creation_prompt':
return menu_processor.account_creation_prompt()
if menu_name == 'start': if menu_name == 'start':
return menu_processor.start_menu() return menu_processor.start_menu()
@@ -524,9 +502,6 @@ def response(account: Account, display_key: str, menu_name: str, session: Sessio
if 'account_tokens_set' in menu_name: if 'account_tokens_set' in menu_name:
return menu_processor.account_tokens() return menu_processor.account_tokens()
if 'language' in menu_name:
return menu_processor.language()
if menu_name == 'display_user_metadata': if menu_name == 'display_user_metadata':
return menu_processor.person_metadata() return menu_processor.person_metadata()
@@ -540,4 +515,5 @@ def response(account: Account, display_key: str, menu_name: str, session: Sessio
return menu_processor.exit_successful_token_selection() return menu_processor.exit_successful_token_selection()
preferred_language = get_cached_preferred_language(account.blockchain_address) preferred_language = get_cached_preferred_language(account.blockchain_address)
return translation_for(display_key, preferred_language) return translation_for(display_key, preferred_language)

View File

@@ -8,7 +8,7 @@ from sqlalchemy.orm.session import Session
from tinydb.table import Document from tinydb.table import Document
# local imports # local imports
from cic_ussd.db.models.account import Account from cic_ussd.db.models.account import Account, create
from cic_ussd.db.models.base import SessionBase from cic_ussd.db.models.base import SessionBase
from cic_ussd.db.models.ussd_session import UssdSession from cic_ussd.db.models.ussd_session import UssdSession
from cic_ussd.menu.ussd_menu import UssdMenu from cic_ussd.menu.ussd_menu import UssdMenu
@@ -16,6 +16,7 @@ from cic_ussd.processor.menu import response
from cic_ussd.processor.util import latest_input, resume_last_ussd_session from cic_ussd.processor.util import latest_input, resume_last_ussd_session
from cic_ussd.session.ussd_session import create_or_update_session, persist_ussd_session from cic_ussd.session.ussd_session import create_or_update_session, persist_ussd_session
from cic_ussd.state_machine import UssdStateMachine from cic_ussd.state_machine import UssdStateMachine
from cic_ussd.translation import translation_for
from cic_ussd.validator import is_valid_response from cic_ussd.validator import is_valid_response
@@ -35,6 +36,9 @@ def handle_menu(account: Account, session: Session) -> Document:
last_ussd_session = UssdSession.last_ussd_session(account.phone_number, session) last_ussd_session = UssdSession.last_ussd_session(account.phone_number, session)
if last_ussd_session: if last_ussd_session:
return resume_last_ussd_session(last_ussd_session.state) return resume_last_ussd_session(last_ussd_session.state)
elif not account.has_preferred_language():
return UssdMenu.find_by_name('initial_language_selection')
else: else:
return UssdMenu.find_by_name('initial_pin_entry') return UssdMenu.find_by_name('initial_pin_entry')
@@ -67,13 +71,16 @@ def get_menu(account: Account,
return UssdMenu.find_by_name(state) return UssdMenu.find_by_name(state)
def handle_menu_operations(external_session_id: str, def handle_menu_operations(chain_str: str,
external_session_id: str,
phone_number: str, phone_number: str,
queue: str, queue: str,
service_code: str, service_code: str,
session, session,
user_input: str): user_input: str):
""" """
:param chain_str:
:type chain_str:
:param external_session_id: :param external_session_id:
:type external_session_id: :type external_session_id:
:param phone_number: :param phone_number:
@@ -93,38 +100,10 @@ def handle_menu_operations(external_session_id: str,
account: Account = Account.get_by_phone_number(phone_number, session) account: Account = Account.get_by_phone_number(phone_number, session)
if account: if account:
return handle_account_menu_operations(account, external_session_id, queue, session, service_code, user_input) return handle_account_menu_operations(account, external_session_id, queue, session, service_code, user_input)
else: create(chain_str, phone_number, session)
return handle_no_account_menu_operations( menu = UssdMenu.find_by_name('account_creation_prompt')
account, external_session_id, phone_number, queue, session, service_code, user_input) preferred_language = i18n.config.get('fallback')
create_or_update_session(
def handle_no_account_menu_operations(account: Optional[Account],
external_session_id: str,
phone_number: str,
queue: str,
session: Session,
service_code: str,
user_input: str):
"""
:param account:
:type account:
:param external_session_id:
:type external_session_id:
:param phone_number:
:type phone_number:
:param queue:
:type queue:
:param session:
:type session:
:param service_code:
:type service_code:
:param user_input:
:type user_input:
:return:
:rtype:
"""
menu = UssdMenu.find_by_name('initial_language_selection')
ussd_session = create_or_update_session(
external_session_id=external_session_id, external_session_id=external_session_id,
msisdn=phone_number, msisdn=phone_number,
service_code=service_code, service_code=service_code,
@@ -132,20 +111,7 @@ def handle_no_account_menu_operations(account: Optional[Account],
session=session, session=session,
user_input=user_input) user_input=user_input)
persist_ussd_session(external_session_id, queue) persist_ussd_session(external_session_id, queue)
last_ussd_session: UssdSession = UssdSession.last_ussd_session(phone_number, session) return translation_for('ussd.kenya.account_creation_prompt', preferred_language)
if last_ussd_session:
if not user_input:
menu = resume_last_ussd_session(last_ussd_session.state)
else:
session = SessionBase.bind_session(session)
state = next_state(account, session, user_input, last_ussd_session.to_json())
menu = UssdMenu.find_by_name(state)
return response(account=account,
display_key=menu.get('display_key'),
menu_name=menu.get('name'),
session=session,
ussd_session=ussd_session.to_json())
def handle_account_menu_operations(account: Account, def handle_account_menu_operations(account: Account,
@@ -186,12 +152,15 @@ def handle_account_menu_operations(account: Account,
if last_ussd_session: if last_ussd_session:
ussd_session = create_or_update_session( ussd_session = create_or_update_session(
external_session_id, phone_number, service_code, user_input, menu.get('name'), session, external_session_id, phone_number, service_code, user_input, menu.get('name'), session,
last_ussd_session.data) last_ussd_session.data
)
else: else:
ussd_session = create_or_update_session( ussd_session = create_or_update_session(
external_session_id, phone_number, service_code, user_input, menu.get('name'), session, {}) external_session_id, phone_number, service_code, user_input, menu.get('name'), session, None
)
menu_response = response( menu_response = response(
account, menu.get('display_key'), menu.get('name'), session, ussd_session.to_json()) account, menu.get('display_key'), menu.get('name'), session, ussd_session.to_json()
)
if not is_valid_response(menu_response): if not is_valid_response(menu_response):
raise ValueError(f'Invalid response: {response}') raise ValueError(f'Invalid response: {response}')
persist_ussd_session(external_session_id, queue) persist_ussd_session(external_session_id, queue)

View File

@@ -3,7 +3,7 @@ import datetime
import json import json
import logging import logging
import time import time
from typing import List, Union from typing import Union
# external imports # external imports
from cic_types.condiments import MetadataPointer from cic_types.condiments import MetadataPointer
@@ -21,7 +21,9 @@ logg = logging.getLogger(__file__)
def latest_input(user_input: str) -> str: def latest_input(user_input: str) -> str:
""" """
:param user_input: :param user_input:
:type user_input:
:return: :return:
:rtype:
""" """
return user_input.split('*')[-1] return user_input.split('*')[-1]
@@ -83,27 +85,6 @@ def resume_last_ussd_session(last_state: str) -> Document:
return UssdMenu.find_by_name(last_state) return UssdMenu.find_by_name(last_state)
def ussd_menu_list(fallback: str, menu_list: list, split: int = 3) -> List[str]:
"""
:param fallback:
:type fallback:
:param menu_list:
:type menu_list:
:param split:
:type split:
:return:
:rtype:
"""
menu_list_sets = [menu_list[item:item + split] for item in range(0, len(menu_list), split)]
menu_list_reprs = []
for i in range(split):
try:
menu_list_reprs.append(''.join(f'{list_set_item}\n' for list_set_item in menu_list_sets[i]).rstrip('\n'))
except IndexError:
menu_list_reprs.append(fallback)
return menu_list_reprs
def wait_for_cache(identifier: Union[list, bytes], resource_name: str, salt: MetadataPointer, interval: int = 1, max_retry: int = 5): def wait_for_cache(identifier: Union[list, bytes], resource_name: str, salt: MetadataPointer, interval: int = 1, max_retry: int = 5):
""" """
:param identifier: :param identifier:
@@ -151,28 +132,17 @@ def wait_for_session_data(resource_name: str, session_data_key: str, ussd_sessio
:return: :return:
:rtype: :rtype:
""" """
data = ussd_session.get('data') session_data = ussd_session.get('data').get(session_data_key)
data_poller = 0 counter = 0
while not data: while session_data is None:
logg.debug(f'Waiting for data object on ussd session: {ussd_session.get("external_session_id")}') logg.debug(f'Waiting for: {resource_name}. Checking after: {interval} ...')
logg.debug(f'Data poller at: {data_poller}. Checking again after: {interval} secs...')
time.sleep(interval) time.sleep(interval)
data_poller += 1 counter += 1
if data: session_data = ussd_session.get('data').get(session_data_key)
logg.debug(f'Data object found, proceeding to poll for: {session_data_key}') if session_data is not None:
logg.debug(f'{resource_name} now available.')
break break
if data: else:
session_data_poller = 0 if counter == max_retry:
session_data = data.get(session_data_key) logg.debug(f'Could not find: {resource_name} within: {max_retry}')
while not session_data_key:
logg.debug(
f'Session data poller at: {data_poller} with max retry at: {max_retry}. Checking again after: {interval} secs...')
time.sleep(interval)
session_data_poller += 1
if session_data:
logg.debug(f'{resource_name} now available.')
break break
elif session_data_poller >= max_retry:
logg.debug(f'Could not find data object within: {max_retry}')

View File

@@ -20,7 +20,6 @@ from cic_ussd.db import dsn_from_config
from cic_ussd.db.models.base import SessionBase from cic_ussd.db.models.base import SessionBase
from cic_ussd.phone_number import Support from cic_ussd.phone_number import Support
from cic_ussd.session.ussd_session import UssdSession as InMemoryUssdSession from cic_ussd.session.ussd_session import UssdSession as InMemoryUssdSession
from cic_ussd.translation import generate_locale_files
from cic_ussd.validator import validate_presence from cic_ussd.validator import validate_presence
logging.basicConfig(level=logging.WARNING) logging.basicConfig(level=logging.WARNING)
@@ -84,10 +83,6 @@ if key_file_path:
validate_presence(path=key_file_path) validate_presence(path=key_file_path)
Signer.key_file_path = key_file_path Signer.key_file_path = key_file_path
generate_locale_files(locale_dir=config.get('LOCALE_PATH'),
schema_file_path=config.get('SCHEMA_FILE_PATH'),
translation_builder_path=config.get('LOCALE_FILE_BUILDERS'))
# set up translations # set up translations
i18n.load_path.append(config.get('LOCALE_PATH')) i18n.load_path.append(config.get('LOCALE_PATH'))
i18n.set('fallback', config.get('LOCALE_FALLBACK')) i18n.set('fallback', config.get('LOCALE_FALLBACK'))

View File

@@ -18,7 +18,6 @@ from cic_types.ext.metadata.signer import Signer
# local imports # local imports
from cic_ussd.account.chain import Chain from cic_ussd.account.chain import Chain
from cic_ussd.account.guardianship import Guardianship
from cic_ussd.account.tokens import query_default_token from cic_ussd.account.tokens import query_default_token
from cic_ussd.cache import cache_data, cache_data_key, Cache from cic_ussd.cache import cache_data, cache_data_key, Cache
from cic_ussd.db import dsn_from_config from cic_ussd.db import dsn_from_config
@@ -34,7 +33,7 @@ from cic_ussd.processor.ussd import handle_menu_operations
from cic_ussd.runnable.server_base import exportable_parser, logg from cic_ussd.runnable.server_base import exportable_parser, logg
from cic_ussd.session.ussd_session import UssdSession as InMemoryUssdSession from cic_ussd.session.ussd_session import UssdSession as InMemoryUssdSession
from cic_ussd.state_machine import UssdStateMachine from cic_ussd.state_machine import UssdStateMachine
from cic_ussd.translation import generate_locale_files, Languages, translation_for from cic_ussd.translation import translation_for
from cic_ussd.validator import check_ip, check_request_content_length, validate_phone_number, validate_presence from cic_ussd.validator import check_ip, check_request_content_length, validate_phone_number, validate_presence
args = exportable_parser.parse_args() args = exportable_parser.parse_args()
@@ -57,6 +56,10 @@ SessionBase.connect(data_source_name,
pool_size=int(config.get('DATABASE_POOL_SIZE')), pool_size=int(config.get('DATABASE_POOL_SIZE')),
debug=config.true('DATABASE_DEBUG')) debug=config.true('DATABASE_DEBUG'))
# set up translations
i18n.load_path.append(config.get('LOCALE_PATH'))
i18n.set('fallback', config.get('LOCALE_FALLBACK'))
# set Fernet key # set Fernet key
PasswordEncoder.set_key(config.get('APP_PASSWORD_PEPPER')) PasswordEncoder.set_key(config.get('APP_PASSWORD_PEPPER'))
@@ -118,22 +121,6 @@ valid_service_codes = config.get('USSD_SERVICE_CODE').split(",")
E164Format.region = config.get('E164_REGION') E164Format.region = config.get('E164_REGION')
Support.phone_number = config.get('OFFICE_SUPPORT_PHONE') Support.phone_number = config.get('OFFICE_SUPPORT_PHONE')
validate_presence(config.get('SYSTEM_GUARDIANS_FILE'))
Guardianship.load_system_guardians(config.get('SYSTEM_GUARDIANS_FILE'))
generate_locale_files(locale_dir=config.get('LOCALE_PATH'),
schema_file_path=config.get('SCHEMA_FILE_PATH'),
translation_builder_path=config.get('LOCALE_FILE_BUILDERS'))
# set up translations
i18n.load_path.append(config.get('LOCALE_PATH'))
i18n.set('fallback', config.get('LOCALE_FALLBACK'))
validate_presence(config.get('LANGUAGES_FILE'))
Languages.load_languages_dict(config.get('LANGUAGES_FILE'))
languages = Languages()
languages.cache_system_languages()
def application(env, start_response): def application(env, start_response):
"""Loads python code for application to be accessible over web server """Loads python code for application to be accessible over web server
@@ -188,7 +175,7 @@ def application(env, start_response):
if service_code not in valid_service_codes: if service_code not in valid_service_codes:
response = translation_for( response = translation_for(
'ussd.invalid_service_code', 'ussd.kenya.invalid_service_code',
i18n.config.get('fallback'), i18n.config.get('fallback'),
valid_service_code=valid_service_codes[0] valid_service_code=valid_service_codes[0]
) )
@@ -202,7 +189,9 @@ def application(env, start_response):
return [] return []
logg.debug('session {} started for {}'.format(external_session_id, phone_number)) logg.debug('session {} started for {}'.format(external_session_id, phone_number))
response = handle_menu_operations(external_session_id, phone_number, args.q, service_code, session, user_input) response = handle_menu_operations(
chain_str, external_session_id, phone_number, args.q, service_code, session, user_input
)
response_bytes, headers = with_content_headers(headers, response) response_bytes, headers = with_content_headers(headers, response)
start_response('200 OK,', headers) start_response('200 OK,', headers)
session.commit() session.commit()

View File

@@ -11,20 +11,46 @@ from cic_types.models.person import get_contact_data_from_vcard, generate_vcard_
# local imports # local imports
from cic_ussd.account.chain import Chain from cic_ussd.account.chain import Chain
from cic_ussd.account.maps import gender from cic_ussd.account.maps import gender, language
from cic_ussd.account.metadata import get_cached_preferred_language from cic_ussd.account.metadata import get_cached_preferred_language
from cic_ussd.db.models.account import Account, create from cic_ussd.db.models.account import Account
from cic_ussd.db.models.base import SessionBase from cic_ussd.db.models.base import SessionBase
from cic_ussd.error import MetadataNotFoundError from cic_ussd.error import MetadataNotFoundError
from cic_ussd.metadata import PersonMetadata from cic_ussd.metadata import PersonMetadata
from cic_ussd.session.ussd_session import save_session_data from cic_ussd.session.ussd_session import save_session_data
from cic_ussd.state_machine.logic.language import preferred_langauge_from_selection
from cic_ussd.translation import translation_for from cic_ussd.translation import translation_for
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
logg = logging.getLogger(__file__) logg = logging.getLogger(__file__)
def change_preferred_language(state_machine_data: Tuple[str, dict, Account, Session]):
"""
:param state_machine_data:
:type state_machine_data:
:return:
:rtype:
"""
user_input, ussd_session, account, session = state_machine_data
r_user_input = language().get(user_input)
session = SessionBase.bind_session(session)
account.preferred_language = r_user_input
session.add(account)
session.flush()
SessionBase.release_session(session)
preferences_data = {
'preferred_language': r_user_input
}
s = celery.signature(
'cic_ussd.tasks.metadata.add_preferences_metadata',
[account.blockchain_address, preferences_data],
queue='cic-ussd'
)
return s.apply_async()
def update_account_status_to_active(state_machine_data: Tuple[str, dict, Account, Session]): def update_account_status_to_active(state_machine_data: Tuple[str, dict, Account, Session]):
"""This function sets user's account to active. """This function sets user's account to active.
:param state_machine_data: A tuple containing user input, a ussd session and user object. :param state_machine_data: A tuple containing user input, a ussd session and user object.
@@ -219,16 +245,3 @@ def edit_user_metadata_attribute(state_machine_data: Tuple[str, dict, Account, S
[blockchain_address, parsed_person_metadata] [blockchain_address, parsed_person_metadata]
) )
s_edit_person_metadata.apply_async(queue='cic-ussd') s_edit_person_metadata.apply_async(queue='cic-ussd')
def process_account_creation(state_machine_data: Tuple[str, dict, Account, Session]):
"""
:param state_machine_data:
:type state_machine_data:
:return:
:rtype:
"""
user_input, ussd_session, account, session = state_machine_data
preferred_language = preferred_langauge_from_selection(user_input=user_input)
chain_str = Chain.spec.__str__()
create(chain_str, ussd_session.get('msisdn'), session, preferred_language)

View File

@@ -1,95 +0,0 @@
# standard imports
import json
from typing import Tuple
# external imports
import celery
import i18n
from cic_types.condiments import MetadataPointer
from sqlalchemy.orm.session import Session
# local imports
from cic_ussd.cache import cache_data_key, get_cached_data
from cic_ussd.db.models.account import Account
from cic_ussd.processor.util import wait_for_cache, wait_for_session_data
from cic_ussd.session.ussd_session import save_session_data
from cic_ussd.translation import Languages
def is_valid_language_selection(state_machine_data: Tuple[str, dict, Account, Session]):
"""
:param state_machine_data:
:type state_machine_data:
:return:
:rtype:
"""
user_input, ussd_session, account, session = state_machine_data
key = cache_data_key('system:languages'.encode('utf-8'), MetadataPointer.NONE)
cached_system_languages = get_cached_data(key)
language_list = json.loads(cached_system_languages)
if not language_list:
wait_for_cache(identifier='system:languages'.encode('utf-8'), resource_name='Languages list', salt=MetadataPointer.NONE)
if user_input in ['00', '11', '22']:
return False
user_input = int(user_input)
return user_input <= len(language_list)
def change_preferred_language(state_machine_data: Tuple[str, dict, Account, Session]):
"""
:param state_machine_data:
:type state_machine_data:
:return:
:rtype:
"""
process_language_selection(state_machine_data=state_machine_data)
user_input, ussd_session, account, session = state_machine_data
wait_for_session_data(resource_name='Preferred language', session_data_key='preferred_language', ussd_session=ussd_session)
preferred_language = ussd_session.get('data').get('preferred_language')
preferences_data = {
'preferred_language': preferred_language
}
s = celery.signature(
'cic_ussd.tasks.metadata.add_preferences_metadata',
[account.blockchain_address, preferences_data],
queue='cic-ussd'
)
return s.apply_async()
def process_language_selection(state_machine_data: Tuple[str, dict, Account, Session]):
"""
:param state_machine_data:
:type state_machine_data:
:return:
:rtype:
"""
user_input, ussd_session, account, session = state_machine_data
preferred_language = preferred_langauge_from_selection(user_input=user_input)
data = {
'preferred_language': preferred_language
}
save_session_data(queue='cic-ussd', session=session, data=data, ussd_session=ussd_session)
def preferred_langauge_from_selection(user_input: str):
"""
:param user_input:
:type user_input:
:return:
:rtype:
"""
key = cache_data_key('system:languages'.encode('utf-8'), MetadataPointer.NONE)
cached_system_languages = get_cached_data(key)
language_list = json.loads(cached_system_languages)
user_input = int(user_input)
selected_language = language_list[user_input - 1]
preferred_language = i18n.config.get('fallback')
for key, value in Languages.languages_dict.items():
if selected_language[3:] == value:
preferred_language = key
return preferred_language

View File

@@ -9,11 +9,9 @@ from phonenumbers.phonenumberutil import NumberParseException
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
# local imports # local imports
from cic_ussd.account.guardianship import Guardianship
from cic_ussd.account.metadata import get_cached_preferred_language from cic_ussd.account.metadata import get_cached_preferred_language
from cic_ussd.db.models.account import Account from cic_ussd.db.models.account import Account
from cic_ussd.db.models.base import SessionBase from cic_ussd.db.models.base import SessionBase
from cic_ussd.notifications import Notifier
from cic_ussd.phone_number import process_phone_number, E164Format from cic_ussd.phone_number import process_phone_number, E164Format
from cic_ussd.session.ussd_session import save_session_data from cic_ussd.session.ussd_session import save_session_data
from cic_ussd.translation import translation_for from cic_ussd.translation import translation_for
@@ -84,10 +82,8 @@ def is_valid_guardian_addition(state_machine_data: Tuple[str, dict, Account, Ses
preferred_language = i18n.config.get('fallback') preferred_language = i18n.config.get('fallback')
is_valid_account = Account.get_by_phone_number(phone_number, session) is not None is_valid_account = Account.get_by_phone_number(phone_number, session) is not None
guardianship = Guardianship()
is_system_guardian = guardianship.is_system_guardian(phone_number)
is_initiator = phone_number == account.phone_number is_initiator = phone_number == account.phone_number
is_existent_guardian = phone_number in account.get_guardians() or is_system_guardian is_existent_guardian = phone_number in account.get_guardians()
failure_reason = '' failure_reason = ''
if not is_valid_account: if not is_valid_account:
@@ -104,7 +100,7 @@ def is_valid_guardian_addition(state_machine_data: Tuple[str, dict, Account, Ses
session_data['failure_reason'] = failure_reason session_data['failure_reason'] = failure_reason
save_session_data('cic-ussd', session, session_data, ussd_session) save_session_data('cic-ussd', session, session_data, ussd_session)
return phone_number is not None and is_valid_account and not is_existent_guardian and not is_initiator and not is_system_guardian return phone_number is not None and is_valid_account and not is_existent_guardian and not is_initiator
def add_pin_guardian(state_machine_data: Tuple[str, dict, Account, Session]): def add_pin_guardian(state_machine_data: Tuple[str, dict, Account, Session]):
@@ -134,9 +130,6 @@ def is_set_pin_guardian(account: Account, checked_number: str, preferred_languag
is_set_guardian = checked_number in set_guardians is_set_guardian = checked_number in set_guardians
is_initiator = checked_number == account.phone_number is_initiator = checked_number == account.phone_number
guardianship = Guardianship()
is_system_guardian = guardianship.is_system_guardian(checked_number)
if not is_set_guardian: if not is_set_guardian:
failure_reason = translation_for('helpers.error.is_not_existent_guardian', preferred_language) failure_reason = translation_for('helpers.error.is_not_existent_guardian', preferred_language)
@@ -148,7 +141,7 @@ def is_set_pin_guardian(account: Account, checked_number: str, preferred_languag
session_data['failure_reason'] = failure_reason session_data['failure_reason'] = failure_reason
save_session_data('cic-ussd', session, session_data, ussd_session) save_session_data('cic-ussd', session, session_data, ussd_session)
return (is_set_guardian or is_system_guardian) and not is_initiator return is_set_guardian and not is_initiator
def is_dialers_pin_guardian(state_machine_data: Tuple[str, dict, Account, Session]): def is_dialers_pin_guardian(state_machine_data: Tuple[str, dict, Account, Session]):
@@ -200,20 +193,8 @@ def initiate_pin_reset(state_machine_data: Tuple[str, dict, Account, Session]):
save_session_data('cic-ussd', session, session_data, ussd_session) save_session_data('cic-ussd', session, session_data, ussd_session)
guarded_account_phone_number = session_data.get('guarded_account_phone_number') guarded_account_phone_number = session_data.get('guarded_account_phone_number')
guarded_account = Account.get_by_phone_number(guarded_account_phone_number, session) guarded_account = Account.get_by_phone_number(guarded_account_phone_number, session)
if quorum_count >= guarded_account.guardian_quora: if quorum_count >= guarded_account.guardian_quora:
guarded_account.reset_pin(session) guarded_account.reset_pin(session)
logg.debug(f'Reset initiated for: {guarded_account.phone_number}') logg.debug(f'Reset initiated for: {guarded_account.phone_number}')
session_data['quorum_count'] = 0 session_data['quorum_count'] = 0
save_session_data('cic-ussd', session, session_data, ussd_session) save_session_data('cic-ussd', session, session_data, ussd_session)
preferred_language = get_cached_preferred_language(guarded_account.blockchain_address)
if not preferred_language:
preferred_language = i18n.config.get('fallback')
notifier = Notifier()
notifier.send_sms_notification(
key='sms.pin_reset_initiated',
phone_number=guarded_account.phone_number,
preferred_language=preferred_language,
pin_initiator=account.standard_metadata_id())

View File

@@ -23,7 +23,7 @@ def is_valid_token_selection(state_machine_data: Tuple[str, dict, Account, Sessi
account_tokens_list = session_data.get('account_tokens_list') account_tokens_list = session_data.get('account_tokens_list')
if not account_tokens_list: if not account_tokens_list:
wait_for_session_data('Account token list', session_data_key='account_tokens_list', ussd_session=ussd_session) wait_for_session_data('Account token list', session_data_key='account_tokens_list', ussd_session=ussd_session)
if user_input not in ['00', '11', '22']: if user_input not in ['00', '22']:
try: try:
user_input = int(user_input) user_input = int(user_input)
return user_input <= len(account_tokens_list) return user_input <= len(account_tokens_list)

View File

@@ -32,14 +32,14 @@ celery_app = celery.current_app
@celery_app.task(bind=True, base=CriticalSQLAlchemyTask) @celery_app.task(bind=True, base=CriticalSQLAlchemyTask)
def account_creation_callback(self, result: str, param: str, status_code: int): def account_creation_callback(self, result: str, url: str, status_code: int):
"""This function defines a task that creates a user and """This function defines a task that creates a user and
:param self: Reference providing access to the callback task instance. :param self: Reference providing access to the callback task instance.
:type self: celery.Task :type self: celery.Task
:param result: The blockchain address for the created account :param result: The blockchain address for the created account
:type result: str :type result: str
:param param: URL provided to callback task in cic-eth should http be used for callback. :param url: URL provided to callback task in cic-eth should http be used for callback.
:type param: str :type url: str
:param status_code: The status of the task to create an account :param status_code: The status of the task to create an account
:type status_code: int :type status_code: int
""" """
@@ -69,15 +69,6 @@ def account_creation_callback(self, result: str, param: str, status_code: int):
set_active_token(blockchain_address=result, token_symbol=token_symbol) set_active_token(blockchain_address=result, token_symbol=token_symbol)
queue = self.request.delivery_info.get('routing_key') queue = self.request.delivery_info.get('routing_key')
preferences_data = {"preferred_language": param}
# temporarily caching selected language
key = cache_data_key(bytes.fromhex(result), MetadataPointer.PREFERENCES)
cache_data(key, json.dumps(preferences_data))
s_preferences_metadata = celery.signature(
'cic_ussd.tasks.metadata.add_preferences_metadata', [result, preferences_data], queue=queue
)
s_preferences_metadata.apply_async()
s_phone_pointer = celery.signature( s_phone_pointer = celery.signature(
'cic_ussd.tasks.metadata.add_phone_pointer', [result, phone_number], queue=queue 'cic_ussd.tasks.metadata.add_phone_pointer', [result, phone_number], queue=queue
) )

View File

@@ -1,56 +1,9 @@
""" """
This module is responsible for translation of ussd menu text based on a user's set preferred language. This module is responsible for translation of ussd menu text based on a user's set preferred language.
""" """
# standard imports
import json
import i18n import i18n
import os
from pathlib import Path
from typing import Optional from typing import Optional
# external imports
from cic_translations.processor import generate_translation_files, parse_csv
from cic_types.condiments import MetadataPointer
# local imports
from cic_ussd.cache import cache_data, cache_data_key
from cic_ussd.validator import validate_presence
def generate_locale_files(locale_dir: str, schema_file_path: str, translation_builder_path: str):
""""""
translation_builder_files = os.listdir(translation_builder_path)
for file in translation_builder_files:
props = Path(file)
if props.suffix == '.csv':
parsed_csv = parse_csv(os.path.join(translation_builder_path, file))
generate_translation_files(
parsed_csv=parsed_csv,
schema_file_path=schema_file_path,
translation_file_type=props.stem,
translation_file_path=locale_dir
)
class Languages:
languages_dict: dict = None
@classmethod
def load_languages_dict(cls, languages_file: str):
with open(languages_file, "r") as languages_file:
cls.languages_dict = json.load(languages_file)
def cache_system_languages(self):
system_languages: list = list(self.languages_dict.values())
languages_list = []
for i in range(len(system_languages)):
language = f'{i + 1}. {system_languages[i]}'
languages_list.append(language)
key = cache_data_key('system:languages'.encode('utf-8'), MetadataPointer.NONE)
cache_data(key, json.dumps(languages_list))
def translation_for(key: str, preferred_language: Optional[str] = None, **kwargs) -> str: def translation_for(key: str, preferred_language: Optional[str] = None, **kwargs) -> str:
""" """

View File

@@ -11,6 +11,3 @@ transitions=transitions/
host = host =
port = port =
ssl = ssl =
[system]
guardians_file = var/lib/sys/guardians.txt

View File

@@ -1,5 +1,5 @@
[PIP] [PIP]
extra_index_host = pip.grassrootseconomics.net extra_index_host = pip.grassrootseconomics.net
extra_index_port = extra_index_port = 8433
extra_index_path = / extra_index_path = /
extra_index_proto = https extra_index_proto = https

View File

@@ -6,6 +6,3 @@ password_pepper=QYbzKff6NhiQzY3ygl2BkiKOpER8RE/Upqs/5aZWW+I=
[machine] [machine]
states=states/ states=states/
transitions=transitions/ transitions=transitions/
[system]
guardians_file = var/lib/sys/guardians.txt

View File

@@ -1,2 +1,2 @@
[chain] [chain]
spec = evm:foo:1:bar spec = 'evm:foo:1:bar'

View File

@@ -1,5 +1,5 @@
[PIP] [PIP]
extra_index_host = pip.grassrootseconomics.net extra_index_host = pip.grassrootseconomics.net
extra_index_port = extra_index_port = 8433
extra_index_path = / extra_index_path = /
extra_index_proto = https extra_index_proto = https

View File

@@ -1,10 +1,3 @@
[locale] [locale]
fallback=sw fallback=sw
path= path=var/lib/locale/
file_builders=var/lib/sys/
[schema]
file_path = data/schema
[languages]
file = var/lib/sys/languages.json

View File

@@ -1,10 +1,3 @@
[locale] [locale]
fallback=sw fallback=sw
path=var/lib/locale/ path=var/lib/locale/
file_builders=var/lib/sys/
[schema]
file_path = /usr/local/lib/python3.8/site-packages/cic_translations/data/schema
[languages]
file = var/lib/sys/languages.json

View File

@@ -4,9 +4,8 @@ billiard==3.6.4.0
bcrypt==3.2.0 bcrypt==3.2.0
celery==4.4.7 celery==4.4.7
cffi==1.14.6 cffi==1.14.6
cic-eth~=0.12.7 cic-eth~=0.12.6
cic-notify~=0.4.0a12 cic-notify~=0.4.0a12
cic-translations~=0.0.3
cic-types~=0.2.1a8 cic-types~=0.2.1a8
confini~=0.5.2 confini~=0.5.2
cic-eth-aux-erc20-demurrage-token~=0.0.3 cic-eth-aux-erc20-demurrage-token~=0.0.3

View File

@@ -13,7 +13,5 @@
"products_edit_pin_authorization", "products_edit_pin_authorization",
"account_balances_pin_authorization", "account_balances_pin_authorization",
"account_statement_pin_authorization", "account_statement_pin_authorization",
"account_balances", "account_balances"
"middle_language_set",
"last_language_set"
] ]

View File

@@ -2,8 +2,6 @@
"start", "start",
"scan_data", "scan_data",
"initial_language_selection", "initial_language_selection",
"initial_middle_language_set",
"initial_last_language_set",
"initial_pin_entry", "initial_pin_entry",
"initial_pin_confirmation", "initial_pin_confirmation",
"change_preferred_language" "change_preferred_language"

View File

@@ -1,12 +1,12 @@
cic-eth[services]~=0.12.7 cic-eth[services]~=0.12.4a13
Faker==11.1.0 Faker==8.1.2
faker-e164==0.1.0 faker-e164==0.1.0
pytest==6.2.5 pytest==6.2.4
pytest-alembic==0.7.0 pytest-alembic==0.2.5
pytest-celery==0.0.0a1 pytest-celery==0.0.0a1
pytest-cov==3.0.0 pytest-cov==2.10.1
pytest-mock==3.6.1 pytest-mock==3.3.1
pytest-ordering==0.6 pytest-ordering==0.6
pytest-redis==2.3.0 pytest-redis==2.0.0
requests-mock==1.9.3 requests-mock==1.8.0
tavern==1.18.0 tavern==1.14.2

View File

@@ -2,16 +2,10 @@
# external imports # external imports
import pytest import pytest
from cic_types.condiments import MetadataPointer
# local imports # local imports
from cic_ussd.account.balance import (calculate_available_balance, from cic_ussd.account.balance import calculate_available_balance, get_balances, get_cached_available_balance
get_balances,
get_cached_adjusted_balance,
get_cached_available_balance)
from cic_ussd.account.chain import Chain from cic_ussd.account.chain import Chain
from cic_ussd.account.tokens import get_cached_token_data_list
from cic_ussd.cache import cache_data_key, get_cached_data
from cic_ussd.error import CachedDataNotFoundError from cic_ussd.error import CachedDataNotFoundError
# test imports # test imports
@@ -63,45 +57,19 @@ def test_calculate_available_balance(activated_account,
'balance_outgoing': balance_outgoing, 'balance_outgoing': balance_outgoing,
'balance_incoming': balance_incoming 'balance_incoming': balance_incoming
} }
assert calculate_available_balance(balances, 6) == available_balance assert calculate_available_balance(balances) == available_balance
def test_get_cached_available_balance(activated_account, def test_get_cached_available_balance(activated_account,
balances, balances,
cache_balances, cache_balances,
cache_default_token_data, cache_default_token_data,
load_chain_spec, load_chain_spec):
token_symbol): cached_available_balance = get_cached_available_balance(activated_account.blockchain_address)
identifier = [bytes.fromhex(activated_account.blockchain_address), token_symbol.encode('utf-8')] available_balance = calculate_available_balance(balances[0])
cached_available_balance = get_cached_available_balance(6, identifier)
available_balance = calculate_available_balance(balances[0], 6)
assert cached_available_balance == available_balance assert cached_available_balance == available_balance
address = blockchain_address() address = blockchain_address()
with pytest.raises(CachedDataNotFoundError) as error: with pytest.raises(CachedDataNotFoundError) as error:
identifier = [bytes.fromhex(address), token_symbol.encode('utf-8')] cached_available_balance = get_cached_available_balance(address)
key = cache_data_key(identifier=identifier, salt=MetadataPointer.BALANCES)
cached_available_balance = get_cached_available_balance(6, identifier)
assert cached_available_balance is None assert cached_available_balance is None
assert str(error.value) == f'No cached available balance at {key}' assert str(error.value) == f'No cached available balance for address: {address}'
def test_get_cached_adjusted_balance(activated_account, cache_adjusted_balances, token_symbol):
identifier = bytes.fromhex(activated_account.blockchain_address)
balances_identifier = [identifier, token_symbol.encode('utf-8')]
key = cache_data_key(balances_identifier, MetadataPointer.BALANCES_ADJUSTED)
adjusted_balances = get_cached_data(key)
assert get_cached_adjusted_balance(balances_identifier) == adjusted_balances
def test_get_account_tokens_balance(activated_account,
cache_token_data_list,
celery_session_worker,
load_chain_spec,
load_config,
mock_async_balance_api_query,
token_symbol):
blockchain_address = activated_account.blockchain_address
chain_str = Chain.spec.__str__()
get_balances(blockchain_address, chain_str, token_symbol, asynchronous=True)
assert mock_async_balance_api_query.get('address') == blockchain_address
assert mock_async_balance_api_query.get('token_symbol') == token_symbol

View File

@@ -1,21 +0,0 @@
# standard imports
import os
# external imports
# local imports
from cic_ussd.account.guardianship import Guardianship
# test imports
from tests.fixtures.config import root_directory
def test_guardianship(load_config, setup_guardianship):
guardians_file = os.path.join(root_directory, load_config.get('SYSTEM_GUARDIANS_FILE'))
with open(guardians_file, 'r') as system_guardians:
guardians = [line.strip() for line in system_guardians]
assert Guardianship.guardians == guardians
guardianship = Guardianship()
assert guardianship.is_system_guardian(Guardianship.guardians[0]) is True
assert guardianship.is_system_guardian('+254712345678') is False

View File

@@ -11,7 +11,8 @@ from cic_ussd.account.statement import (filter_statement_transactions,
generate, generate,
get_cached_statement, get_cached_statement,
parse_statement_transactions, parse_statement_transactions,
query_statement) query_statement,
statement_transaction_set)
from cic_ussd.account.transaction import transaction_actors from cic_ussd.account.transaction import transaction_actors
from cic_ussd.cache import cache_data_key, get_cached_data from cic_ussd.cache import cache_data_key, get_cached_data
@@ -73,3 +74,12 @@ def test_query_statement(blockchain_address, limit, load_chain_spec, activated_a
query_statement(blockchain_address, limit) query_statement(blockchain_address, limit)
assert mock_transaction_list_query.get('address') == blockchain_address assert mock_transaction_list_query.get('address') == blockchain_address
assert mock_transaction_list_query.get('limit') == limit assert mock_transaction_list_query.get('limit') == limit
def test_statement_transaction_set(cache_default_token_data, load_chain_spec, preferences, set_locale_files, statement):
parsed_transactions = parse_statement_transactions(statement)
preferred_language = preferences.get('preferred_language')
transaction_set = statement_transaction_set(preferred_language, parsed_transactions)
transaction_set.startswith('Sent')
transaction_set = statement_transaction_set(preferred_language, [])
transaction_set.startswith('No')

View File

@@ -1,80 +1,17 @@
# standard imports # standard imports
import hashlib
import json import json
# external imports # external imports
import pytest import pytest
from cic_types.condiments import MetadataPointer
# local imports # local imports
from cic_ussd.account.chain import Chain from cic_ussd.account.chain import Chain
from cic_ussd.account.tokens import (collate_token_metadata, from cic_ussd.account.tokens import get_cached_default_token, get_default_token_symbol, query_default_token
create_account_tokens_list,
get_active_token_symbol,
get_default_token_symbol,
get_cached_default_token,
get_cached_token_data,
get_cached_token_data_list,
get_cached_token_symbol_list,
hashed_token_proof,
handle_token_symbol_list,
order_account_tokens_list,
parse_token_list,
process_token_data,
query_default_token,
query_token_data,
remove_from_account_tokens_list,
set_active_token)
from cic_ussd.cache import cache_data, cache_data_key, get_cached_data
from cic_ussd.error import CachedDataNotFoundError
# test imports # test imports
def test_collate_token_metadata(token_meta_symbol, token_proof_symbol):
description = token_proof_symbol.get('description')
issuer = token_proof_symbol.get('issuer')
location = token_meta_symbol.get('location')
contact = token_meta_symbol.get('contact')
data = {
'description': description,
'issuer': issuer,
'location': location,
'contact': contact
}
assert collate_token_metadata(token_proof_symbol, token_meta_symbol) == data
def test_create_account_tokens_list(activated_account,
cache_balances,
cache_token_data,
cache_token_symbol_list,
init_cache):
create_account_tokens_list(activated_account.blockchain_address)
key = cache_data_key(bytes.fromhex(activated_account.blockchain_address), MetadataPointer.TOKEN_DATA_LIST)
cached_data_list = json.loads(get_cached_data(key))
data = get_cached_token_data_list(activated_account.blockchain_address)
assert cached_data_list == data
def test_get_active_token_symbol(activated_account, set_active_token, valid_recipient):
identifier = bytes.fromhex(activated_account.blockchain_address)
key = cache_data_key(identifier=identifier, salt=MetadataPointer.TOKEN_ACTIVE)
active_token_symbol = get_cached_data(key)
assert active_token_symbol == get_active_token_symbol(activated_account.blockchain_address)
with pytest.raises(CachedDataNotFoundError) as error:
get_active_token_symbol(valid_recipient.blockchain_address)
assert str(error.value) == 'No active token set.'
def test_get_cached_token_data(activated_account, cache_token_data, token_symbol):
identifier = [bytes.fromhex(activated_account.blockchain_address), token_symbol.encode('utf-8')]
key = cache_data_key(identifier, MetadataPointer.TOKEN_DATA)
token_data = json.loads(get_cached_data(key))
assert token_data == get_cached_token_data(activated_account.blockchain_address, token_symbol)
def test_get_cached_default_token(cache_default_token_data, default_token_data, load_chain_spec): def test_get_cached_default_token(cache_default_token_data, default_token_data, load_chain_spec):
chain_str = Chain.spec.__str__() chain_str = Chain.spec.__str__()
cached_default_token = get_cached_default_token(chain_str) cached_default_token = get_cached_default_token(chain_str)
@@ -90,84 +27,6 @@ def test_get_default_token_symbol_from_api(default_token_data, load_chain_spec,
assert default_token_symbol == default_token_data['symbol'] assert default_token_symbol == default_token_data['symbol']
def test_get_cached_token_data_list(activated_account, cache_token_data_list):
blockchain_address = activated_account.blockchain_address
key = cache_data_key(identifier=bytes.fromhex(blockchain_address), salt=MetadataPointer.TOKEN_DATA_LIST)
token_symbols_list = json.loads(get_cached_data(key))
assert token_symbols_list == get_cached_token_data_list(blockchain_address)
def test_get_cached_token_symbol_list(activated_account, cache_token_symbol_list):
blockchain_address = activated_account.blockchain_address
key = cache_data_key(identifier=bytes.fromhex(blockchain_address), salt=MetadataPointer.TOKEN_SYMBOLS_LIST)
token_symbols_list = json.loads(get_cached_data(key))
assert token_symbols_list == get_cached_token_symbol_list(blockchain_address)
def test_hashed_token_proof(token_proof_symbol):
hash_object = hashlib.new("sha256")
token_proof = json.dumps(token_proof_symbol)
hash_object.update(token_proof.encode('utf-8'))
assert hash_object.digest().hex() == hashed_token_proof(token_proof_symbol)
def test_handle_token_symbol_list(activated_account, init_cache):
handle_token_symbol_list(activated_account.blockchain_address, 'GFT')
cached_token_symbol_list = get_cached_token_symbol_list(activated_account.blockchain_address)
assert len(cached_token_symbol_list) == 1
handle_token_symbol_list(activated_account.blockchain_address, 'DET')
cached_token_symbol_list = get_cached_token_symbol_list(activated_account.blockchain_address)
assert len(cached_token_symbol_list) == 2
def test_order_account_tokens_list(activated_account, token_list_entries):
identifier = bytes.fromhex(activated_account.blockchain_address)
last_sent_token_key = cache_data_key(identifier=identifier, salt=MetadataPointer.TOKEN_LAST_SENT)
cache_data(last_sent_token_key, 'FII')
last_received_token_key = cache_data_key(identifier=identifier, salt=MetadataPointer.TOKEN_LAST_RECEIVED)
cache_data(last_received_token_key, 'DET')
ordered_list = order_account_tokens_list(token_list_entries, identifier)
assert ordered_list == [
{
'name': 'Fee',
'symbol': 'FII',
'issuer': 'Foo',
'contact': {
'phone': '+254712345678'
},
'location': 'Fum',
'balance': 50.0
},
{
'name': 'Demurrage Token',
'symbol': 'DET',
'issuer': 'Grassroots Economics',
'contact': {
'phone': '+254700000000',
'email': 'info@grassrootseconomics.org'},
'location': 'Fum',
'balance': 49.99
},
{
'name': 'Giftable Token',
'symbol': 'GFT',
'issuer': 'Grassroots Economics',
'contact': {
'phone': '+254700000000',
'email': 'info@grassrootseconomics.org'},
'location': 'Fum',
'balance': 60.0
}
]
def test_parse_token_list(token_list_entries):
parsed_token_list = ['1. FII 50.0', '2. GFT 60.0', '3. DET 49.99']
assert parsed_token_list == parse_token_list(token_list_entries)
def test_query_default_token(default_token_data, load_chain_spec, mock_sync_default_token_api_query): def test_query_default_token(default_token_data, load_chain_spec, mock_sync_default_token_api_query):
chain_str = Chain.spec.__str__() chain_str = Chain.spec.__str__()
queried_default_token_data = query_default_token(chain_str) queried_default_token_data = query_default_token(chain_str)
@@ -181,38 +40,3 @@ def test_get_default_token_symbol_from_cache(cache_default_token_data, default_t
default_token_symbol = get_default_token_symbol() default_token_symbol = get_default_token_symbol()
assert default_token_symbol is not None assert default_token_symbol is not None
assert default_token_symbol == default_token_data.get('symbol') assert default_token_symbol == default_token_data.get('symbol')
def test_remove_from_account_tokens_list(token_list_entries):
assert remove_from_account_tokens_list(token_list_entries, 'GFT') == ([{
'name': 'Giftable Token',
'symbol': 'GFT',
'issuer': 'Grassroots Economics',
'contact': {
'phone': '+254700000000',
'email': 'info@grassrootseconomics.org'
},
'location': 'Fum',
'balance': 60.0
}],
[
{
'name': 'Fee',
'symbol': 'FII',
'issuer': 'Foo',
'contact': {'phone': '+254712345678'},
'location': 'Fum',
'balance': 50.0
},
{
'name': 'Demurrage Token',
'symbol': 'DET',
'issuer': 'Grassroots Economics',
'contact': {
'phone': '+254700000000',
'email': 'info@grassrootseconomics.org'
},
'location': 'Fum',
'balance': 49.99
}
])

View File

@@ -1,4 +1,5 @@
# standard imports # standard imports
from decimal import Decimal
# external imports # external imports
import pytest import pytest
@@ -36,11 +37,11 @@ def test_aux_transaction_data(preferences, set_locale_files, transactions_list):
@pytest.mark.parametrize("value, expected_result", [ @pytest.mark.parametrize("value, expected_result", [
(50000000, 50.0), (50000000, Decimal('50.00')),
(100000, 0.1) (100000, Decimal('0.10'))
]) ])
def test_from_wei(cache_default_token_data, expected_result, value): def test_from_wei(cache_default_token_data, expected_result, value):
assert from_wei(6, value) == expected_result assert from_wei(value) == expected_result
@pytest.mark.parametrize("value, expected_result", [ @pytest.mark.parametrize("value, expected_result", [
@@ -48,7 +49,7 @@ def test_from_wei(cache_default_token_data, expected_result, value):
(0.10, 100000) (0.10, 100000)
]) ])
def test_to_wei(cache_default_token_data, expected_result, value): def test_to_wei(cache_default_token_data, expected_result, value):
assert to_wei(6, value) == expected_result assert to_wei(value) == expected_result
@pytest.mark.parametrize("decimals, value, expected_result", [ @pytest.mark.parametrize("decimals, value, expected_result", [
@@ -107,8 +108,8 @@ def test_outgoing_transaction_processor(activated_account,
activated_account.blockchain_address, activated_account.blockchain_address,
valid_recipient.blockchain_address) valid_recipient.blockchain_address)
outgoing_tx_processor.transfer(amount, 6, token_symbol) outgoing_tx_processor.transfer(amount, token_symbol)
assert mock_transfer_api.get('from_address') == activated_account.blockchain_address assert mock_transfer_api.get('from_address') == activated_account.blockchain_address
assert mock_transfer_api.get('to_address') == valid_recipient.blockchain_address assert mock_transfer_api.get('to_address') == valid_recipient.blockchain_address
assert mock_transfer_api.get('value') == to_wei(6, amount) assert mock_transfer_api.get('value') == to_wei(amount)
assert mock_transfer_api.get('token_symbol') == token_symbol assert mock_transfer_api.get('token_symbol') == token_symbol

View File

@@ -90,7 +90,7 @@ def test_standard_metadata_id(activated_account, cache_person_metadata, pending_
def test_account_create(init_cache, init_database, load_chain_spec, mock_account_creation_task_result, task_uuid): def test_account_create(init_cache, init_database, load_chain_spec, mock_account_creation_task_result, task_uuid):
chain_str = Chain.spec.__str__() chain_str = Chain.spec.__str__()
create(chain_str, phone_number(), init_database, 'en') create(chain_str, phone_number(), init_database)
assert len(init_database.query(TaskTracker).all()) == 1 assert len(init_database.query(TaskTracker).all()) == 1
account_creation_data = get_cached_data(task_uuid) account_creation_data = get_cached_data(task_uuid)
assert json.loads(account_creation_data).get('status') == AccountStatus.PENDING.name assert json.loads(account_creation_data).get('status') == AccountStatus.PENDING.name

View File

@@ -23,7 +23,7 @@ def test_ussd_metadata_handler(activated_account,
setup_metadata_signer): setup_metadata_signer):
identifier = bytes.fromhex(strip_0x(activated_account.blockchain_address)) identifier = bytes.fromhex(strip_0x(activated_account.blockchain_address))
cic_type = MetadataPointer.PERSON cic_type = MetadataPointer.PERSON
metadata_client = UssdMetadataHandler(cic_type=cic_type, identifier=identifier) metadata_client = UssdMetadataHandler(cic_type, identifier)
assert metadata_client.cic_type == cic_type assert metadata_client.cic_type == cic_type
assert metadata_client.engine == 'pgp' assert metadata_client.engine == 'pgp'
assert metadata_client.identifier == identifier assert metadata_client.identifier == identifier

View File

@@ -1,72 +0,0 @@
# standard imports
import json
# external imports
import pytest
import requests_mock
from cic_types.condiments import MetadataPointer
from requests.exceptions import HTTPError
# local imports
from cic_ussd.cache import cache_data_key, get_cached_data
from cic_ussd.metadata import TokenMetadata
from cic_ussd.metadata.tokens import token_metadata_handler, query_token_metadata, query_token_info
# test imports
def test_token_metadata_handler(activated_account,
init_cache,
setup_metadata_request_handler,
setup_metadata_signer,
token_meta_symbol,
token_symbol):
with requests_mock.Mocker(real_http=False) as request_mocker:
with pytest.raises(HTTPError) as error:
metadata_client = TokenMetadata(identifier=b'foo', cic_type=MetadataPointer.TOKEN_META_SYMBOL)
reason = 'Not Found'
status_code = 401
request_mocker.register_uri('GET', metadata_client.url, status_code=status_code, reason=reason)
token_metadata_handler(metadata_client)
assert str(error.value) == f'Client Error: {status_code}, reason: {reason}'
identifier = token_symbol.encode('utf-8')
metadata_client = TokenMetadata(identifier, cic_type=MetadataPointer.TOKEN_META_SYMBOL)
request_mocker.register_uri('GET', metadata_client.url, json=token_meta_symbol, status_code=200, reason='OK')
token_metadata_handler(metadata_client)
key = cache_data_key(identifier, MetadataPointer.TOKEN_META_SYMBOL)
cached_token_meta_symbol = get_cached_data(key)
assert json.loads(cached_token_meta_symbol) == token_meta_symbol
def test_query_token_metadata(init_cache,
setup_metadata_request_handler,
setup_metadata_signer,
token_meta_symbol,
token_proof_symbol,
token_symbol):
with requests_mock.Mocker(real_http=False) as request_mocker:
identifier = token_symbol.encode('utf-8')
metadata_client = TokenMetadata(identifier, cic_type=MetadataPointer.TOKEN_META_SYMBOL)
request_mocker.register_uri('GET', metadata_client.url, json=token_meta_symbol, status_code=200, reason='OK')
query_token_metadata(identifier)
key = cache_data_key(identifier, MetadataPointer.TOKEN_META_SYMBOL)
cached_token_meta_symbol = get_cached_data(key)
assert json.loads(cached_token_meta_symbol) == token_meta_symbol
def test_query_token_info(init_cache,
setup_metadata_request_handler,
setup_metadata_signer,
token_meta_symbol,
token_proof_symbol,
token_symbol):
with requests_mock.Mocker(real_http=False) as request_mocker:
identifier = token_symbol.encode('utf-8')
metadata_client = TokenMetadata(identifier, cic_type=MetadataPointer.TOKEN_PROOF_SYMBOL)
request_mocker.register_uri('GET', metadata_client.url, json=token_proof_symbol, status_code=200, reason='OK')
query_token_info(identifier)
key = cache_data_key(identifier, MetadataPointer.TOKEN_PROOF_SYMBOL)
cached_token_proof_symbol = get_cached_data(key)
assert json.loads(cached_token_proof_symbol) == token_proof_symbol

View File

@@ -1,6 +1,6 @@
# standard imports # standard imports
import json import json
import os import datetime
# external imports # external imports
from cic_types.condiments import MetadataPointer from cic_types.condiments import MetadataPointer
@@ -10,464 +10,195 @@ from cic_ussd.account.balance import get_cached_available_balance
from cic_ussd.account.metadata import get_cached_preferred_language from cic_ussd.account.metadata import get_cached_preferred_language
from cic_ussd.account.statement import ( from cic_ussd.account.statement import (
get_cached_statement, get_cached_statement,
parse_statement_transactions parse_statement_transactions,
statement_transaction_set
) )
from cic_ussd.account.tokens import (get_active_token_symbol, from cic_ussd.account.tokens import get_default_token_symbol
get_cached_token_data)
from cic_ussd.account.transaction import from_wei, to_wei from cic_ussd.account.transaction import from_wei, to_wei
from cic_ussd.cache import cache_data, cache_data_key, get_cached_data from cic_ussd.cache import cache_data, cache_data_key
from cic_ussd.menu.ussd_menu import UssdMenu
from cic_ussd.metadata import PersonMetadata from cic_ussd.metadata import PersonMetadata
from cic_ussd.phone_number import Support from cic_ussd.phone_number import Support
from cic_ussd.processor.menu import response, MenuProcessor from cic_ussd.processor.menu import response
from cic_ussd.processor.util import parse_person_metadata, ussd_menu_list from cic_ussd.processor.util import parse_person_metadata
from cic_ussd.translation import translation_for from cic_ussd.translation import translation_for
# test imports # test imports
def test_account_balance(activated_account, cache_balances, cache_preferences, cache_token_data,
generic_ussd_session, init_database, set_active_token): def test_menu_processor(activated_account,
"""blockchain_address = activated_account.blockchain_address balances,
token_symbol = get_active_token_symbol(blockchain_address) cache_balances,
token_data = get_cached_token_data(blockchain_address, token_symbol) cache_default_token_data,
preferred_language = get_cached_preferred_language(blockchain_address) cache_preferences,
decimals = token_data.get("decimals") cache_person_metadata,
identifier = bytes.fromhex(blockchain_address) cache_statement,
balances_identifier = [identifier, token_symbol.encode('utf-8')] celery_session_worker,
available_balance = get_cached_available_balance(decimals, balances_identifier) generic_ussd_session,
with_available_balance = 'ussd.account_balances.available_balance' init_database,
resp = response(activated_account, with_available_balance, with_available_balance[5:], init_database, load_chain_spec,
generic_ussd_session) load_support_phone,
load_ussd_menu,
mock_get_adjusted_balance,
mock_sync_balance_api_query,
mock_transaction_list_query,
valid_recipient):
preferred_language = get_cached_preferred_language(activated_account.blockchain_address)
available_balance = get_cached_available_balance(activated_account.blockchain_address)
token_symbol = get_default_token_symbol()
with_available_balance = 'ussd.kenya.account_balances.available_balance'
with_fees = 'ussd.kenya.account_balances.with_fees'
ussd_menu = UssdMenu.find_by_name('account_balances')
name = ussd_menu.get('name')
resp = response(activated_account, 'ussd.kenya.account_balances', name, init_database, generic_ussd_session)
assert resp == translation_for(with_available_balance, assert resp == translation_for(with_available_balance,
preferred_language, preferred_language,
available_balance=available_balance, available_balance=available_balance,
token_symbol=token_symbol) token_symbol=token_symbol)
with_fees = 'ussd.account_balances.with_fees' identifier = bytes.fromhex(activated_account.blockchain_address)
key = cache_data_key(balances_identifier, MetadataPointer.BALANCES_ADJUSTED) key = cache_data_key(identifier, MetadataPointer.BALANCES_ADJUSTED)
adjusted_balance = 45931650.64654012 adjusted_balance = 45931650.64654012
cache_data(key, json.dumps(adjusted_balance)) cache_data(key, json.dumps(adjusted_balance))
resp = response(activated_account, with_fees, with_fees[5:], init_database, generic_ussd_session) resp = response(activated_account, 'ussd.kenya.account_balances', name, init_database, generic_ussd_session)
tax_wei = to_wei(decimals, int(available_balance)) - int(adjusted_balance) tax_wei = to_wei(int(available_balance)) - int(adjusted_balance)
tax = from_wei(decimals, int(tax_wei)) tax = from_wei(int(tax_wei))
assert resp == translation_for(key=with_fees, assert resp == translation_for(key=with_fees,
preferred_language=preferred_language, preferred_language=preferred_language,
available_balance=available_balance, available_balance=available_balance,
tax=tax, tax=tax,
token_symbol=token_symbol)""" token_symbol=token_symbol)
pass
cached_statement = get_cached_statement(activated_account.blockchain_address)
statement = json.loads(cached_statement)
statement_transactions = parse_statement_transactions(statement)
transaction_sets = [statement_transactions[tx:tx + 3] for tx in range(0, len(statement_transactions), 3)]
first_transaction_set = []
middle_transaction_set = []
last_transaction_set = []
if transaction_sets:
first_transaction_set = statement_transaction_set(preferred_language, transaction_sets[0])
if len(transaction_sets) >= 2:
middle_transaction_set = statement_transaction_set(preferred_language, transaction_sets[1])
if len(transaction_sets) >= 3:
last_transaction_set = statement_transaction_set(preferred_language, transaction_sets[2])
def test_account_statement(activated_account, display_key = 'ussd.kenya.first_transaction_set'
cache_preferences, ussd_menu = UssdMenu.find_by_name('first_transaction_set')
cache_statement, name = ussd_menu.get('name')
generic_ussd_session, resp = response(activated_account, display_key, name, init_database, generic_ussd_session)
init_database,
set_active_token,
set_locale_files):
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
cached_statement = get_cached_statement(blockchain_address)
statement_list = parse_statement_transactions(statement=json.loads(cached_statement))
first_transaction_set = 'ussd.first_transaction_set'
middle_transaction_set = 'ussd.middle_transaction_set'
last_transaction_set = 'ussd.last_transaction_set'
fallback = translation_for('helpers.no_transaction_history', preferred_language)
transaction_sets = ussd_menu_list(fallback=fallback, menu_list=statement_list, split=3)
resp = response(activated_account, first_transaction_set, first_transaction_set[5:], init_database,
generic_ussd_session)
assert resp == translation_for(first_transaction_set, preferred_language, first_transaction_set=transaction_sets[0])
resp = response(activated_account, middle_transaction_set, middle_transaction_set[5:], init_database,
generic_ussd_session)
assert resp == translation_for(middle_transaction_set, preferred_language,
middle_transaction_set=transaction_sets[1])
resp = response(activated_account, last_transaction_set, last_transaction_set[5:], init_database,
generic_ussd_session)
assert resp == translation_for(last_transaction_set, preferred_language, last_transaction_set=transaction_sets[2])
assert resp == translation_for(display_key, preferred_language, first_transaction_set=first_transaction_set)
def test_add_guardian_pin_authorization(activated_account, display_key = 'ussd.kenya.middle_transaction_set'
cache_preferences, ussd_menu = UssdMenu.find_by_name('middle_transaction_set')
guardian_account, name = ussd_menu.get('name')
generic_ussd_session, resp = response(activated_account, display_key, name, init_database, generic_ussd_session)
init_database):
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
add_guardian_pin_authorization = 'ussd.add_guardian_pin_authorization'
activated_account.add_guardian(guardian_account.phone_number)
init_database.flush()
generic_ussd_session['external_session_id'] = os.urandom(20).hex()
generic_ussd_session['msisdn'] = guardian_account.phone_number
generic_ussd_session['data'] = {'guardian_phone_number': guardian_account.phone_number}
generic_ussd_session['state'] = 'add_guardian_pin_authorization'
resp = response(activated_account,
add_guardian_pin_authorization,
add_guardian_pin_authorization[5:],
init_database,
generic_ussd_session)
assert resp == translation_for(f'{add_guardian_pin_authorization}.first', preferred_language,
guardian_information=guardian_account.standard_metadata_id())
assert resp == translation_for(display_key, preferred_language, middle_transaction_set=middle_transaction_set)
def test_guardian_list(activated_account, display_key = 'ussd.kenya.last_transaction_set'
cache_preferences, ussd_menu = UssdMenu.find_by_name('last_transaction_set')
generic_ussd_session, name = ussd_menu.get('name')
guardian_account, resp = response(activated_account, display_key, name, init_database, generic_ussd_session)
init_database):
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
guardians_list = 'ussd.guardian_list'
guardians_list_header = translation_for('helpers.guardians_list_header', preferred_language)
guardian_information = guardian_account.standard_metadata_id()
guardians = guardians_list_header + '\n' + f'{guardian_information}\n'
activated_account.add_guardian(guardian_account.phone_number)
init_database.flush()
resp = response(activated_account, guardians_list, guardians_list[5:], init_database, generic_ussd_session)
assert resp == translation_for(guardians_list, preferred_language, guardians_list=guardians)
guardians = translation_for('helpers.no_guardians_list', preferred_language)
identifier = bytes.fromhex(guardian_account.blockchain_address)
key = cache_data_key(identifier, MetadataPointer.PREFERENCES)
cache_data(key, json.dumps({'preferred_language': preferred_language}))
resp = response(guardian_account, guardians_list, guardians_list[5:], init_database, generic_ussd_session)
assert resp == translation_for(guardians_list, preferred_language, guardians_list=guardians)
assert resp == translation_for(display_key, preferred_language, last_transaction_set=last_transaction_set)
def test_account_tokens(activated_account, cache_token_data_list, celery_session_worker, generic_ussd_session, display_key = 'ussd.kenya.display_user_metadata'
init_cache, init_database): ussd_menu = UssdMenu.find_by_name('display_user_metadata')
"""blockchain_address = activated_account.blockchain_address name = ussd_menu.get('name')
preferred_language = get_cached_preferred_language(blockchain_address) identifier = bytes.fromhex(activated_account.blockchain_address)
cached_token_data_list = get_cached_token_data_list(blockchain_address)
token_data_list = ['1. GFT 50.0']
fallback = translation_for('helpers.no_tokens_list', preferred_language)
token_list_sets = ussd_menu_list(fallback=fallback, menu_list=token_data_list, split=3)
first_account_tokens_set = 'ussd.first_account_tokens_set'
middle_account_tokens_set = 'ussd.middle_account_tokens_set'
last_account_tokens_set = 'ussd.last_account_tokens_set'
resp = response(activated_account, first_account_tokens_set, first_account_tokens_set[5:], init_database,
generic_ussd_session)
assert resp == translation_for(first_account_tokens_set, preferred_language,
first_account_tokens_set=token_list_sets[0])
assert generic_ussd_session.get('data').get('account_tokens_list') == cached_token_data_list
resp = response(activated_account, middle_account_tokens_set, middle_account_tokens_set[5:], init_database,
generic_ussd_session)
assert resp == translation_for(middle_account_tokens_set, preferred_language,
middle_account_tokens_set=token_list_sets[1])
resp = response(activated_account, last_account_tokens_set, last_account_tokens_set[5:], init_database,
generic_ussd_session)
assert resp == translation_for(last_account_tokens_set, preferred_language,
last_account_tokens_set=token_list_sets[2])"""
pass
def test_help(activated_account, cache_preferences, generic_ussd_session, init_database):
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
help = 'ussd.help'
resp = response(activated_account, help, help[5:], init_database, generic_ussd_session)
assert resp == translation_for(help, preferred_language, support_phone=Support.phone_number)
def test_person_data(activated_account, cache_person_metadata, cache_preferences, cached_ussd_session,
generic_ussd_session, init_database):
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
identifier = bytes.fromhex(blockchain_address)
display_user_metadata = 'ussd.display_user_metadata'
person_metadata = PersonMetadata(identifier) person_metadata = PersonMetadata(identifier)
cached_person_metadata = person_metadata.get_cached_metadata() cached_person_metadata = person_metadata.get_cached_metadata()
resp = response(activated_account, display_user_metadata, display_user_metadata[5:], init_database, resp = response(activated_account, display_key, name, init_database, generic_ussd_session)
generic_ussd_session) assert resp == parse_person_metadata(cached_person_metadata, display_key, preferred_language)
assert resp == parse_person_metadata(cached_person_metadata, display_user_metadata, preferred_language)
display_key = 'ussd.kenya.account_balances_pin_authorization'
ussd_menu = UssdMenu.find_by_name('account_balances_pin_authorization')
name = ussd_menu.get('name')
resp = response(activated_account, display_key, name, init_database, generic_ussd_session)
assert resp == translation_for(f'{display_key}.first', preferred_language)
def test_guarded_account_metadata(activated_account, generic_ussd_session, init_database): activated_account.failed_pin_attempts = 1
reset_guarded_pin_authorization = 'ussd.reset_guarded_pin_authorization' resp = response(activated_account, display_key, name, init_database, generic_ussd_session)
generic_ussd_session['data'] = {'guarded_account_phone_number': activated_account.phone_number} retry_pin_entry = translation_for('ussd.kenya.retry_pin_entry', preferred_language, remaining_attempts=2)
menu_processor = MenuProcessor(activated_account, reset_guarded_pin_authorization, assert resp == translation_for(f'{display_key}.retry', preferred_language, retry_pin_entry=retry_pin_entry)
reset_guarded_pin_authorization[5:], init_database, generic_ussd_session) activated_account.failed_pin_attempts = 0
assert menu_processor.guarded_account_metadata() == activated_account.standard_metadata_id()
display_key = 'ussd.kenya.start'
def test_guardian_metadata(activated_account, generic_ussd_session, guardian_account, init_database): ussd_menu = UssdMenu.find_by_name('start')
add_guardian_pin_authorization = 'ussd.add_guardian_pin_authorization' name = ussd_menu.get('name')
generic_ussd_session['data'] = {'guardian_phone_number': guardian_account.phone_number} resp = response(activated_account, display_key, name, init_database, generic_ussd_session)
menu_processor = MenuProcessor(activated_account, add_guardian_pin_authorization, assert resp == translation_for(display_key,
add_guardian_pin_authorization[5:], init_database, generic_ussd_session)
assert menu_processor.guardian_metadata() == guardian_account.standard_metadata_id()
def test_language(activated_account, cache_preferences, generic_ussd_session, init_database, load_languages):
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
initial_language_selection = 'ussd.initial_language_selection'
select_preferred_language = 'ussd.select_preferred_language'
initial_middle_language_set = 'ussd.initial_middle_language_set'
middle_language_set = 'ussd.middle_language_set'
initial_last_language_set = 'ussd.initial_last_language_set'
last_language_set = 'ussd.last_language_set'
key = cache_data_key('system:languages'.encode('utf-8'), MetadataPointer.NONE)
cached_system_languages = get_cached_data(key)
language_list: list = json.loads(cached_system_languages)
fallback = translation_for('helpers.no_language_list', preferred_language)
language_list_sets = ussd_menu_list(fallback=fallback, menu_list=language_list, split=3)
resp = response(activated_account, initial_language_selection, initial_language_selection[5:], init_database,
generic_ussd_session)
assert resp == translation_for(initial_language_selection, preferred_language,
first_language_set=language_list_sets[0])
resp = response(activated_account, select_preferred_language, select_preferred_language[5:], init_database,
generic_ussd_session)
assert resp == translation_for(select_preferred_language, preferred_language,
first_language_set=language_list_sets[0])
resp = response(activated_account, initial_middle_language_set, initial_middle_language_set[5:], init_database,
generic_ussd_session)
assert resp == translation_for(initial_middle_language_set, preferred_language,
middle_language_set=language_list_sets[1])
resp = response(activated_account, initial_last_language_set, initial_last_language_set[5:], init_database,
generic_ussd_session)
assert resp == translation_for(initial_last_language_set, preferred_language,
last_language_set=language_list_sets[2])
resp = response(activated_account, middle_language_set, middle_language_set[5:], init_database,
generic_ussd_session)
assert resp == translation_for(middle_language_set, preferred_language, middle_language_set=language_list_sets[1])
resp = response(activated_account, last_language_set, last_language_set[5:], init_database, generic_ussd_session)
assert resp == translation_for(last_language_set, preferred_language, last_language_set=language_list_sets[2])
def test_account_creation_prompt(activated_account, cache_preferences, generic_ussd_session, init_database,
load_languages):
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
user_input = ''
if preferred_language == 'en':
user_input = '1'
elif preferred_language == 'sw':
user_input = '2'
account_creation_prompt = 'ussd.account_creation_prompt'
generic_ussd_session['user_input'] = user_input
resp = response(activated_account, account_creation_prompt, account_creation_prompt[5:], init_database,
generic_ussd_session)
assert resp == translation_for(account_creation_prompt, preferred_language)
def test_reset_guarded_pin_authorization(activated_account, cache_preferences, generic_ussd_session, guardian_account,
init_database):
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
reset_guarded_pin_authorization = 'ussd.reset_guarded_pin_authorization'
generic_ussd_session['external_session_id'] = os.urandom(20).hex()
generic_ussd_session['msisdn'] = guardian_account.phone_number
generic_ussd_session['data'] = {'guarded_account_phone_number': activated_account.phone_number}
resp = response(activated_account,
reset_guarded_pin_authorization,
reset_guarded_pin_authorization[5:],
init_database,
generic_ussd_session)
assert resp == translation_for(f'{reset_guarded_pin_authorization}.first', preferred_language,
guarded_account_information=activated_account.phone_number)
def test_start(activated_account, cache_balances, cache_preferences, cache_token_data, cache_token_data_list,
cache_token_symbol_list, celery_session_worker, generic_ussd_session, init_database, load_chain_spec,
mock_sync_balance_api_query, set_active_token):
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
token_symbol = get_active_token_symbol(blockchain_address)
token_data = get_cached_token_data(blockchain_address, token_symbol)
decimals = token_data.get("decimals")
identifier = bytes.fromhex(blockchain_address)
balances_identifier = [identifier, token_symbol.encode('utf-8')]
available_balance = get_cached_available_balance(decimals, balances_identifier)
start = 'ussd.start'
resp = response(activated_account, start, start[5:], init_database, generic_ussd_session)
assert resp == translation_for(start,
preferred_language, preferred_language,
account_balance=available_balance, account_balance=available_balance,
account_token_name=token_symbol) account_token_name=token_symbol)
display_key = 'ussd.kenya.start'
ussd_menu = UssdMenu.find_by_name('start')
name = ussd_menu.get('name')
older_timestamp = (activated_account.created - datetime.timedelta(days=35))
activated_account.created = older_timestamp
init_database.flush()
response(activated_account, display_key, name, init_database, generic_ussd_session)
assert mock_get_adjusted_balance['timestamp'] == int((datetime.datetime.now() - datetime.timedelta(days=30)).timestamp())
def test_token_selection_pin_authorization(activated_account, cache_preferences, cache_token_data, generic_ussd_session, display_key = 'ussd.kenya.transaction_pin_authorization'
init_database, set_active_token): ussd_menu = UssdMenu.find_by_name('transaction_pin_authorization')
blockchain_address = activated_account.blockchain_address name = ussd_menu.get('name')
token_symbol = get_active_token_symbol(blockchain_address)
token_data = get_cached_token_data(blockchain_address, token_symbol)
preferred_language = get_cached_preferred_language(blockchain_address)
token_selection_pin_authorization = 'ussd.token_selection_pin_authorization'
generic_ussd_session['data'] = {'selected_token': token_data}
resp = response(activated_account,
token_selection_pin_authorization,
token_selection_pin_authorization[5:],
init_database,
generic_ussd_session)
token_name = token_data.get('name')
token_symbol = token_data.get('symbol')
token_issuer = token_data.get('issuer')
token_contact = token_data.get('contact')
token_location = token_data.get('location')
data = f'{token_name} ({token_symbol})\n{token_issuer}\n{token_contact}\n{token_location}\n'
assert resp == translation_for(f'{token_selection_pin_authorization}.first', preferred_language,
token_data=data)
def test_transaction_pin_authorization(activated_account, cache_preferences, cache_token_data, generic_ussd_session,
init_database, set_active_token, valid_recipient):
blockchain_address = activated_account.blockchain_address
token_symbol = get_active_token_symbol(blockchain_address)
token_data = get_cached_token_data(blockchain_address, token_symbol)
preferred_language = get_cached_preferred_language(blockchain_address)
decimals = token_data.get("decimals")
transaction_pin_authorization = 'ussd.transaction_pin_authorization'
generic_ussd_session['data'] = { generic_ussd_session['data'] = {
'recipient_phone_number': valid_recipient.phone_number, 'recipient_phone_number': valid_recipient.phone_number,
'transaction_amount': '15' 'transaction_amount': '15'
} }
resp = response(activated_account, transaction_pin_authorization, transaction_pin_authorization[5:], init_database, resp = response(activated_account, display_key, name, init_database, generic_ussd_session)
generic_ussd_session)
user_input = generic_ussd_session.get('data').get('transaction_amount') user_input = generic_ussd_session.get('data').get('transaction_amount')
transaction_amount = to_wei(decimals, int(user_input)) transaction_amount = to_wei(value=int(user_input))
tx_recipient_information = valid_recipient.standard_metadata_id() tx_recipient_information = valid_recipient.standard_metadata_id()
tx_sender_information = activated_account.standard_metadata_id() tx_sender_information = activated_account.standard_metadata_id()
assert resp == translation_for(f'{transaction_pin_authorization}.first', assert resp == translation_for(f'{display_key}.first',
preferred_language, preferred_language,
recipient_information=tx_recipient_information, recipient_information=tx_recipient_information,
transaction_amount=from_wei(decimals, transaction_amount), transaction_amount=from_wei(transaction_amount),
token_symbol=token_symbol, token_symbol=token_symbol,
sender_information=tx_sender_information) sender_information=tx_sender_information)
display_key = 'ussd.kenya.exit_insufficient_balance'
def test_guardian_exits(activated_account, cache_preferences, cache_token_data, generic_ussd_session, guardian_account, ussd_menu = UssdMenu.find_by_name('exit_insufficient_balance')
init_database, set_active_token): name = ussd_menu.get('name')
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
generic_ussd_session['data'] = {'guardian_phone_number': guardian_account.phone_number}
# testing exit guardian addition success
exit_guardian_addition_success = 'ussd.exit_guardian_addition_success'
resp = response(activated_account, exit_guardian_addition_success, exit_guardian_addition_success[5:],
init_database, generic_ussd_session)
assert resp == translation_for(exit_guardian_addition_success, preferred_language,
guardian_information=guardian_account.standard_metadata_id())
# testing exit guardian removal success
exit_guardian_removal_success = 'ussd.exit_guardian_removal_success'
resp = response(activated_account, exit_guardian_removal_success, exit_guardian_removal_success[5:],
init_database, generic_ussd_session)
assert resp == translation_for(exit_guardian_removal_success, preferred_language,
guardian_information=guardian_account.standard_metadata_id())
generic_ussd_session['data'] = {'failure_reason': 'foo'}
# testing exit invalid guardian addition
exit_invalid_guardian_addition = 'ussd.exit_invalid_guardian_addition'
resp = response(activated_account, exit_invalid_guardian_addition, exit_invalid_guardian_addition[5:],
init_database, generic_ussd_session)
assert resp == translation_for(exit_invalid_guardian_addition, preferred_language, error_exit='foo')
# testing exit invalid guardian removal
exit_invalid_guardian_removal = 'ussd.exit_invalid_guardian_removal'
resp = response(activated_account, exit_invalid_guardian_removal, exit_invalid_guardian_removal[5:],
init_database, generic_ussd_session)
assert resp == translation_for(exit_invalid_guardian_removal, preferred_language, error_exit='foo')
def test_exit_pin_reset_initiated_success(activated_account, cache_preferences, generic_ussd_session, init_database):
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
exit_pin_reset_initiated_success = 'ussd.exit_pin_reset_initiated_success'
generic_ussd_session['data'] = {'guarded_account_phone_number': activated_account.phone_number}
resp = response(activated_account, exit_pin_reset_initiated_success, exit_pin_reset_initiated_success[5:],
init_database, generic_ussd_session)
assert resp == translation_for(exit_pin_reset_initiated_success,
preferred_language,
guarded_account_information=activated_account.standard_metadata_id())
def test_exit_insufficient_balance(activated_account, cache_balances, cache_preferences, cache_token_data,
generic_ussd_session, init_database, set_active_token, valid_recipient):
blockchain_address = activated_account.blockchain_address
token_symbol = get_active_token_symbol(blockchain_address)
token_data = get_cached_token_data(blockchain_address, token_symbol)
preferred_language = get_cached_preferred_language(blockchain_address)
decimals = token_data.get("decimals")
identifier = bytes.fromhex(blockchain_address)
balances_identifier = [identifier, token_symbol.encode('utf-8')]
available_balance = get_cached_available_balance(decimals, balances_identifier)
tx_recipient_information = valid_recipient.standard_metadata_id()
exit_insufficient_balance = 'ussd.exit_insufficient_balance'
generic_ussd_session['data'] = { generic_ussd_session['data'] = {
'recipient_phone_number': valid_recipient.phone_number, 'recipient_phone_number': valid_recipient.phone_number,
'transaction_amount': '85' 'transaction_amount': '85'
} }
transaction_amount = generic_ussd_session.get('data').get('transaction_amount') transaction_amount = generic_ussd_session.get('data').get('transaction_amount')
transaction_amount = to_wei(decimals, int(transaction_amount)) transaction_amount = to_wei(value=int(transaction_amount))
resp = response(activated_account, exit_insufficient_balance, exit_insufficient_balance[5:], init_database, resp = response(activated_account, display_key, name, init_database, generic_ussd_session)
generic_ussd_session) assert resp == translation_for(display_key,
assert resp == translation_for(exit_insufficient_balance,
preferred_language, preferred_language,
amount=from_wei(decimals, transaction_amount), amount=from_wei(transaction_amount),
token_symbol=token_symbol, token_symbol=token_symbol,
recipient_information=tx_recipient_information, recipient_information=tx_recipient_information,
token_balance=available_balance) token_balance=available_balance)
display_key = 'ussd.kenya.exit_invalid_menu_option'
ussd_menu = UssdMenu.find_by_name('exit_invalid_menu_option')
name = ussd_menu.get('name')
resp = response(activated_account, display_key, name, init_database, generic_ussd_session)
assert resp == translation_for(display_key, preferred_language, support_phone=Support.phone_number)
def test_exit_invalid_menu_option(activated_account, cache_preferences, generic_ussd_session, init_database, display_key = 'ussd.kenya.exit_successful_transaction'
load_support_phone): ussd_menu = UssdMenu.find_by_name('exit_successful_transaction')
blockchain_address = activated_account.blockchain_address name = ussd_menu.get('name')
preferred_language = get_cached_preferred_language(blockchain_address)
exit_invalid_menu_option = 'ussd.exit_invalid_menu_option'
resp = response(activated_account, exit_invalid_menu_option, exit_invalid_menu_option[5:], init_database,
generic_ussd_session)
assert resp == translation_for(exit_invalid_menu_option, preferred_language, support_phone=Support.phone_number)
def test_exit_pin_blocked(activated_account, cache_preferences, generic_ussd_session, init_database,
load_support_phone):
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
exit_pin_blocked = 'ussd.exit_pin_blocked'
resp = response(activated_account, exit_pin_blocked, exit_pin_blocked[5:], init_database, generic_ussd_session)
assert resp == translation_for(exit_pin_blocked, preferred_language, support_phone=Support.phone_number)
def test_exit_successful_token_selection(activated_account, cache_preferences, cache_token_data, generic_ussd_session,
init_database, set_active_token):
blockchain_address = activated_account.blockchain_address
token_symbol = get_active_token_symbol(blockchain_address)
token_data = get_cached_token_data(blockchain_address, token_symbol)
preferred_language = get_cached_preferred_language(blockchain_address)
exit_successful_token_selection = 'ussd.exit_successful_token_selection'
generic_ussd_session['data'] = {'selected_token': token_data}
resp = response(activated_account, exit_successful_token_selection, exit_successful_token_selection[5:],
init_database, generic_ussd_session)
assert resp == translation_for(exit_successful_token_selection, preferred_language, token_symbol=token_symbol)
def test_exit_successful_transaction(activated_account, cache_preferences, cache_token_data, generic_ussd_session,
init_database, set_active_token, valid_recipient):
blockchain_address = activated_account.blockchain_address
token_symbol = get_active_token_symbol(blockchain_address)
token_data = get_cached_token_data(blockchain_address, token_symbol)
preferred_language = get_cached_preferred_language(blockchain_address)
decimals = token_data.get("decimals")
tx_recipient_information = valid_recipient.standard_metadata_id()
tx_sender_information = activated_account.standard_metadata_id()
exit_successful_transaction = 'ussd.exit_successful_transaction'
generic_ussd_session['data'] = { generic_ussd_session['data'] = {
'recipient_phone_number': valid_recipient.phone_number, 'recipient_phone_number': valid_recipient.phone_number,
'transaction_amount': '15' 'transaction_amount': '15'
} }
transaction_amount = generic_ussd_session.get('data').get('transaction_amount') transaction_amount = generic_ussd_session.get('data').get('transaction_amount')
transaction_amount = to_wei(decimals, int(transaction_amount)) transaction_amount = to_wei(value=int(transaction_amount))
resp = response(activated_account, exit_successful_transaction, exit_successful_transaction[5:], init_database, resp = response(activated_account, display_key, name, init_database, generic_ussd_session)
generic_ussd_session) assert resp == translation_for(display_key,
assert resp == translation_for(exit_successful_transaction,
preferred_language, preferred_language,
transaction_amount=from_wei(decimals, transaction_amount), transaction_amount=from_wei(transaction_amount),
token_symbol=token_symbol, token_symbol=token_symbol,
recipient_information=tx_recipient_information, recipient_information=tx_recipient_information,
sender_information=tx_sender_information) sender_information=tx_sender_information)

View File

@@ -10,16 +10,13 @@ from chainlib.hash import strip_0x
from cic_types.condiments import MetadataPointer from cic_types.condiments import MetadataPointer
# local imports # local imports
from cic_ussd.account.chain import Chain
from cic_ussd.account.metadata import get_cached_preferred_language from cic_ussd.account.metadata import get_cached_preferred_language
from cic_ussd.cache import cache_data, cache_data_key, get_cached_data from cic_ussd.cache import cache_data, cache_data_key, get_cached_data
from cic_ussd.db.models.task_tracker import TaskTracker from cic_ussd.db.models.task_tracker import TaskTracker
from cic_ussd.menu.ussd_menu import UssdMenu from cic_ussd.menu.ussd_menu import UssdMenu
from cic_ussd.metadata import PersonMetadata from cic_ussd.metadata import PersonMetadata
from cic_ussd.processor.ussd import (get_menu, from cic_ussd.processor.ussd import get_menu, handle_menu, handle_menu_operations
handle_menu,
handle_menu_operations)
from cic_ussd.processor.util import ussd_menu_list
from cic_ussd.state_machine.logic.language import preferred_langauge_from_selection
from cic_ussd.translation import translation_for from cic_ussd.translation import translation_for
# test imports # test imports
@@ -46,7 +43,7 @@ def test_handle_menu(activated_account,
ussd_menu = UssdMenu.find_by_name('exit_pin_blocked') ussd_menu = UssdMenu.find_by_name('exit_pin_blocked')
assert menu_resp.get('name') == ussd_menu.get('name') assert menu_resp.get('name') == ussd_menu.get('name')
menu_resp = handle_menu(pending_account, init_database) menu_resp = handle_menu(pending_account, init_database)
ussd_menu = UssdMenu.find_by_name('initial_pin_entry') ussd_menu = UssdMenu.find_by_name('initial_language_selection')
assert menu_resp.get('name') == ussd_menu.get('name') assert menu_resp.get('name') == ussd_menu.get('name')
identifier = bytes.fromhex(strip_0x(pending_account.blockchain_address)) identifier = bytes.fromhex(strip_0x(pending_account.blockchain_address))
key = cache_data_key(identifier, MetadataPointer.PREFERENCES) key = cache_data_key(identifier, MetadataPointer.PREFERENCES)
@@ -78,62 +75,38 @@ def test_get_menu(activated_account,
assert menu_resp.get('name') == ussd_menu.get('name') assert menu_resp.get('name') == ussd_menu.get('name')
def test_handle_no_account_menu_operations(celery_session_worker, def test_handle_menu_operations(activated_account,
init_cache, cache_preferences,
init_database, celery_session_worker,
load_chain_spec, generic_ussd_session,
load_config, init_database,
load_languages, init_cache,
load_ussd_menu, load_chain_spec,
mock_account_creation_task_result, load_config,
pending_account, mock_account_creation_task_result,
persisted_ussd_session, persisted_ussd_session,
set_locale_files, person_metadata,
task_uuid): set_locale_files,
initial_language_selection = 'ussd.initial_language_selection' setup_metadata_request_handler,
setup_metadata_signer,
task_uuid):
# sourcery skip: extract-duplicate-method
chain_str = Chain.spec.__str__()
phone = phone_number() phone = phone_number()
external_session_id = os.urandom(20).hex() external_session_id = os.urandom(20).hex()
valid_service_codes = load_config.get('USSD_SERVICE_CODE').split(",") valid_service_codes = load_config.get('USSD_SERVICE_CODE').split(",")
preferred_language = i18n.config.get('fallback') preferred_language = i18n.config.get('fallback')
key = cache_data_key('system:languages'.encode('utf-8'), MetadataPointer.NONE) resp = handle_menu_operations(chain_str, external_session_id, phone, None, valid_service_codes[0], init_database, '4444')
cached_system_languages = get_cached_data(key) assert resp == translation_for('ussd.kenya.account_creation_prompt', preferred_language)
language_list: list = json.loads(cached_system_languages)
fallback = translation_for('helpers.no_language_list', preferred_language)
language_list_sets = ussd_menu_list(fallback=fallback, menu_list=language_list, split=3)
resp = handle_menu_operations(external_session_id, phone, None, valid_service_codes[0], init_database, '')
assert resp == translation_for(initial_language_selection, preferred_language,
first_language_set=language_list_sets[0])
cached_ussd_session = get_cached_data(external_session_id) cached_ussd_session = get_cached_data(external_session_id)
ussd_session = json.loads(cached_ussd_session) ussd_session = json.loads(cached_ussd_session)
assert ussd_session['msisdn'] == phone assert ussd_session['msisdn'] == phone
persisted_ussd_session.external_session_id = external_session_id
persisted_ussd_session.msisdn = phone
persisted_ussd_session.state = initial_language_selection[5:]
init_database.add(persisted_ussd_session)
init_database.commit()
account_creation_prompt = 'ussd.account_creation_prompt'
user_input = '2'
resp = handle_menu_operations(external_session_id, phone, None, valid_service_codes[0], init_database, user_input)
preferred_language = preferred_langauge_from_selection(user_input)
assert resp == translation_for(account_creation_prompt, preferred_language)
task_tracker = init_database.query(TaskTracker).filter_by(task_uuid=task_uuid).first() task_tracker = init_database.query(TaskTracker).filter_by(task_uuid=task_uuid).first()
assert task_tracker.task_uuid == task_uuid assert task_tracker.task_uuid == task_uuid
cached_creation_task_uuid = get_cached_data(task_uuid) cached_creation_task_uuid = get_cached_data(task_uuid)
creation_task_uuid_data = json.loads(cached_creation_task_uuid) creation_task_uuid_data = json.loads(cached_creation_task_uuid)
assert creation_task_uuid_data['status'] == 'PENDING' assert creation_task_uuid_data['status'] == 'PENDING'
def test_handle_account_menu_operations(activated_account,
cache_preferences,
celery_session_worker,
init_database,
load_config,
persisted_ussd_session,
person_metadata,
set_locale_files,
setup_metadata_request_handler,
setup_metadata_signer, ):
valid_service_codes = load_config.get('USSD_SERVICE_CODE').split(",")
identifier = bytes.fromhex(strip_0x(activated_account.blockchain_address)) identifier = bytes.fromhex(strip_0x(activated_account.blockchain_address))
person_metadata_client = PersonMetadata(identifier) person_metadata_client = PersonMetadata(identifier)
with requests_mock.Mocker(real_http=False) as request_mocker: with requests_mock.Mocker(real_http=False) as request_mocker:
@@ -144,5 +117,6 @@ def test_handle_account_menu_operations(activated_account,
phone = activated_account.phone_number phone = activated_account.phone_number
preferred_language = get_cached_preferred_language(activated_account.blockchain_address) preferred_language = get_cached_preferred_language(activated_account.blockchain_address)
persisted_ussd_session.state = 'enter_transaction_recipient' persisted_ussd_session.state = 'enter_transaction_recipient'
resp = handle_menu_operations(external_session_id, phone, None, valid_service_codes[0], init_database, '1') resp = handle_menu_operations(chain_str, external_session_id, phone, None, valid_service_codes[0], init_database, '1')
assert resp == translation_for('ussd.enter_transaction_recipient', preferred_language) assert resp == translation_for('ussd.kenya.enter_transaction_recipient', preferred_language)

View File

@@ -10,10 +10,7 @@ from cic_types.models.person import get_contact_data_from_vcard
# local imports # local imports
from cic_ussd.account.metadata import get_cached_preferred_language from cic_ussd.account.metadata import get_cached_preferred_language
from cic_ussd.metadata import PersonMetadata from cic_ussd.metadata import PersonMetadata
from cic_ussd.processor.util import (latest_input, from cic_ussd.processor.util import latest_input, parse_person_metadata, resume_last_ussd_session
parse_person_metadata,
resume_last_ussd_session,
ussd_menu_list)
from cic_ussd.translation import translation_for from cic_ussd.translation import translation_for
@@ -35,7 +32,7 @@ def test_parse_person_metadata(activated_account, cache_person_metadata, cache_p
cached_person_metadata = person_metadata.get_cached_metadata() cached_person_metadata = person_metadata.get_cached_metadata()
person_metadata = json.loads(cached_person_metadata) person_metadata = json.loads(cached_person_metadata)
preferred_language = get_cached_preferred_language(activated_account.blockchain_address) preferred_language = get_cached_preferred_language(activated_account.blockchain_address)
display_key = 'ussd.display_person_metadata' display_key = 'ussd.kenya.display_person_metadata'
parsed_person_metadata = parse_person_metadata(cached_person_metadata, parsed_person_metadata = parse_person_metadata(cached_person_metadata,
display_key, display_key,
preferred_language) preferred_language)
@@ -63,20 +60,3 @@ def test_parse_person_metadata(activated_account, cache_person_metadata, cache_p
]) ])
def test_resume_last_ussd_session(expected_menu_name, last_state, load_ussd_menu): def test_resume_last_ussd_session(expected_menu_name, last_state, load_ussd_menu):
assert resume_last_ussd_session(last_state).get('name') == expected_menu_name assert resume_last_ussd_session(last_state).get('name') == expected_menu_name
def test_ussd_menu_list(activated_account, cache_preferences, load_ussd_menu, set_locale_files):
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
fallback = translation_for('helpers.no_transaction_history', preferred_language)
menu_list_sets = ['1. FII 50.0', '2. GFT 60.0', '3. DET 49.99']
split = 3
menu_list = ussd_menu_list(fallback=fallback, menu_list=menu_list_sets, split=split)
menu_list_sets = [menu_list_sets[item:item + split] for item in range(0, len(menu_list), split)]
menu_list_reprs = []
for i in range(split):
try:
menu_list_reprs.append(''.join(f'{list_set_item}\n' for list_set_item in menu_list_sets[i]).rstrip('\n'))
except IndexError:
menu_list_reprs.append(fallback)
assert menu_list == menu_list_reprs

View File

@@ -3,7 +3,8 @@ import json
# external imports # external imports
import pytest import pytest
import requests_mock
from chainlib.hash import strip_0x
from cic_types.models.person import Person, get_contact_data_from_vcard from cic_types.models.person import Person, get_contact_data_from_vcard
# local imports # local imports
@@ -11,7 +12,9 @@ from cic_ussd.cache import get_cached_data
from cic_ussd.account.maps import gender from cic_ussd.account.maps import gender
from cic_ussd.account.metadata import get_cached_preferred_language from cic_ussd.account.metadata import get_cached_preferred_language
from cic_ussd.db.enum import AccountStatus from cic_ussd.db.enum import AccountStatus
from cic_ussd.state_machine.logic.account import (edit_user_metadata_attribute, from cic_ussd.metadata import PreferencesMetadata
from cic_ussd.state_machine.logic.account import (change_preferred_language,
edit_user_metadata_attribute,
parse_gender, parse_gender,
parse_person_metadata, parse_person_metadata,
save_complete_person_metadata, save_complete_person_metadata,
@@ -23,6 +26,32 @@ from cic_ussd.translation import translation_for
# test imports # test imports
@pytest.mark.parametrize('user_input, expected_preferred_language', [
('1', 'en'),
('2', 'sw')
])
def test_change_preferred_language(activated_account,
celery_session_worker,
expected_preferred_language,
init_database,
generic_ussd_session,
mock_response,
preferences,
setup_metadata_request_handler,
user_input):
identifier = bytes.fromhex(strip_0x(activated_account.blockchain_address))
preferences_metadata_client = PreferencesMetadata(identifier)
with requests_mock.Mocker(real_http=False) as requests_mocker:
requests_mocker.register_uri(
'POST', preferences_metadata_client.url, status_code=200, reason='OK', json=mock_response
)
state_machine_data = (user_input, generic_ussd_session, activated_account, init_database)
res = change_preferred_language(state_machine_data)
init_database.commit()
assert res.id is not None
assert activated_account.preferred_language == expected_preferred_language
@pytest.mark.parametrize('user_input', [ @pytest.mark.parametrize('user_input', [
'1', '1',
'2', '2',

View File

@@ -1,52 +0,0 @@
# standard imports
import json
# external imports
import requests_mock
from cic_types.condiments import MetadataPointer
# local imports
from cic_ussd.cache import cache_data_key, get_cached_data
from cic_ussd.metadata import PreferencesMetadata
from cic_ussd.state_machine.logic.language import (change_preferred_language,
is_valid_language_selection,
preferred_langauge_from_selection,
process_language_selection)
# test imports
def test_change_preferred_language(activated_account,
cached_ussd_session,
celery_session_worker,
init_database,
load_languages,
mocker,
setup_metadata_signer,
setup_metadata_request_handler):
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
preferences = {
'preferred_language': 'en'
}
ussd_session['data'] = preferences
mock_add_preferences_metadata = mocker.patch('cic_ussd.tasks.metadata.add_preferences_metadata.apply_async')
with requests_mock.Mocker(real_http=False) as request_mocker:
identifier = bytes.fromhex(activated_account.blockchain_address)
metadata_client = PreferencesMetadata(identifier=identifier)
request_mocker.register_uri('POST', metadata_client.url, status_code=201, reason='CREATED', json=preferences)
state_machine_data = ('1', ussd_session, activated_account, init_database)
change_preferred_language(state_machine_data)
mock_add_preferences_metadata.assert_called_with(
(activated_account.blockchain_address, preferences), {}, queue='cic-ussd')
def test_is_valid_language_selection(activated_account,
generic_ussd_session,
init_cache,
init_database,
load_languages):
state_machine_data = ('1', generic_ussd_session, activated_account, init_database)
assert is_valid_language_selection(state_machine_data) is True
state_machine_data = ('12', generic_ussd_session, activated_account, init_database)
assert is_valid_language_selection(state_machine_data) is False

View File

@@ -9,10 +9,7 @@ from cic_ussd.state_machine.logic.menu import (menu_one_selected,
menu_four_selected, menu_four_selected,
menu_five_selected, menu_five_selected,
menu_six_selected, menu_six_selected,
menu_nine_selected,
menu_zero_zero_selected, menu_zero_zero_selected,
menu_eleven_selected,
menu_twenty_two_selected,
menu_ninety_nine_selected) menu_ninety_nine_selected)
# test imports # test imports
@@ -32,14 +29,8 @@ def test_menu_selection(init_database, pending_account, persisted_ussd_session):
assert menu_five_selected(('e', ussd_session, pending_account, init_database)) is False assert menu_five_selected(('e', ussd_session, pending_account, init_database)) is False
assert menu_six_selected(('6', ussd_session, pending_account, init_database)) is True assert menu_six_selected(('6', ussd_session, pending_account, init_database)) is True
assert menu_six_selected(('8', ussd_session, pending_account, init_database)) is False assert menu_six_selected(('8', ussd_session, pending_account, init_database)) is False
assert menu_nine_selected(('9', ussd_session, pending_account, init_database)) is True
assert menu_nine_selected(('-', ussd_session, pending_account, init_database)) is False
assert menu_zero_zero_selected(('00', ussd_session, pending_account, init_database)) is True assert menu_zero_zero_selected(('00', ussd_session, pending_account, init_database)) is True
assert menu_zero_zero_selected(('/', ussd_session, pending_account, init_database)) is False assert menu_zero_zero_selected(('/', ussd_session, pending_account, init_database)) is False
assert menu_eleven_selected(('11', ussd_session, pending_account, init_database)) is True
assert menu_eleven_selected(('*', ussd_session, pending_account, init_database)) is False
assert menu_twenty_two_selected(('22', ussd_session, pending_account, init_database)) is True
assert menu_twenty_two_selected(('5', ussd_session, pending_account, init_database)) is False
assert menu_ninety_nine_selected(('99', ussd_session, pending_account, init_database)) is True assert menu_ninety_nine_selected(('99', ussd_session, pending_account, init_database)) is True
assert menu_ninety_nine_selected(('d', ussd_session, pending_account, init_database)) is False assert menu_ninety_nine_selected(('d', ussd_session, pending_account, init_database)) is False

View File

@@ -1,221 +0,0 @@
# standard imports
import json
# external imports
import requests_mock
# local imports
from cic_ussd.account.guardianship import Guardianship
from cic_ussd.account.metadata import get_cached_preferred_language
from cic_ussd.cache import cache_data_key, get_cached_data
from cic_ussd.db.models.account import Account
from cic_ussd.metadata import PersonMetadata
from cic_ussd.state_machine.logic.pin_guard import (add_pin_guardian,
is_dialers_pin_guardian,
is_others_pin_guardian,
is_set_pin_guardian,
remove_pin_guardian,
initiate_pin_reset,
save_guardian_to_session_data,
save_guarded_account_session_data,
retrieve_person_metadata,
is_valid_guardian_addition)
from cic_ussd.translation import translation_for
def test_save_guardian_to_session_data(activated_account,
cached_ussd_session,
celery_session_worker,
guardian_account,
init_cache,
init_database):
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
ussd_session['msisdn'] = activated_account.phone_number
state_machine_data = (guardian_account.phone_number, ussd_session, activated_account, init_database)
save_guardian_to_session_data(state_machine_data)
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
assert ussd_session.get('data').get('guardian_phone_number') == guardian_account.phone_number
def test_save_guarded_account_session_data(activated_account,
cached_ussd_session,
celery_session_worker,
guardian_account,
init_cache,
init_database):
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
ussd_session['msisdn'] = guardian_account.phone_number
state_machine_data = (activated_account.phone_number, ussd_session, guardian_account, init_database)
save_guarded_account_session_data(state_machine_data)
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
assert ussd_session.get('data').get('guarded_account_phone_number') == activated_account.phone_number
def test_retrieve_person_metadata(activated_account,
cached_ussd_session,
celery_session_worker,
guardian_account,
init_cache,
init_database,
mocker,
person_metadata,
setup_metadata_request_handler,
setup_metadata_signer):
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
ussd_session['msisdn'] = activated_account.phone_number
state_machine_data = (guardian_account.phone_number, ussd_session, activated_account, init_database)
mocker_query_person_metadata = mocker.patch('cic_ussd.tasks.metadata.query_person_metadata.apply_async')
with requests_mock.Mocker(real_http=False) as request_mocker:
identifier = bytes.fromhex(activated_account.blockchain_address)
metadata_client = PersonMetadata(identifier)
request_mocker.register_uri('GET', metadata_client.url, json=person_metadata, reason='OK', status_code=200)
retrieve_person_metadata(state_machine_data)
mocker_query_person_metadata.assert_called_with((guardian_account.blockchain_address,), {}, queue='cic-ussd')
def test_is_valid_guardian_addition(activated_account,
cache_preferences,
cached_ussd_session,
celery_session_worker,
init_cache,
init_database,
guardian_account,
load_languages,
load_ussd_menu,
set_locale_files,
setup_guardianship):
blockchain_address = activated_account.blockchain_address
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
state_machine_data = (guardian_account.phone_number, ussd_session, activated_account, init_database)
assert is_valid_guardian_addition(state_machine_data) is True
state_machine_data = (activated_account.phone_number, ussd_session, activated_account, init_database)
assert is_valid_guardian_addition(state_machine_data) is False
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
preferred_language = get_cached_preferred_language(blockchain_address)
failure_reason = translation_for('helpers.error.is_initiator', preferred_language)
assert ussd_session.get('data').get('failure_reason') == failure_reason
state_machine_data = (Guardianship.guardians[0], ussd_session, activated_account, init_database)
assert is_valid_guardian_addition(state_machine_data) is False
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
preferred_language = get_cached_preferred_language(blockchain_address)
failure_reason = translation_for('helpers.error.is_existent_guardian', preferred_language)
assert ussd_session.get('data').get('failure_reason') == failure_reason
def test_add_pin_guardian(activated_account, generic_ussd_session, guardian_account, init_database):
generic_ussd_session['data'] = {'guardian_phone_number': guardian_account.phone_number}
state_machine_data = ('', generic_ussd_session, activated_account, init_database)
add_pin_guardian(state_machine_data)
account = Account.get_by_phone_number(activated_account.phone_number, init_database)
assert account.get_guardians()[0] == guardian_account.phone_number
def test_is_set_pin_guardian(activated_account,
cache_preferences,
cached_ussd_session,
celery_session_worker,
init_cache,
init_database,
guardian_account,
load_languages,
load_ussd_menu,
set_locale_files,
setup_guardianship):
blockchain_address = activated_account.blockchain_address
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
preferred_language = get_cached_preferred_language(blockchain_address)
assert is_set_pin_guardian(activated_account, guardian_account.phone_number, preferred_language, init_database,
ussd_session) is False
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
failure_reason = translation_for('helpers.error.is_not_existent_guardian', preferred_language)
assert ussd_session.get('data').get('failure_reason') == failure_reason
assert is_set_pin_guardian(activated_account, Guardianship.guardians[0], preferred_language, init_database,
ussd_session) is True
assert is_set_pin_guardian(activated_account, activated_account.phone_number, preferred_language, init_database,
ussd_session) is False
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
failure_reason = translation_for('helpers.error.is_initiator', preferred_language)
assert ussd_session.get('data').get('failure_reason') == failure_reason
def test_is_dialers_pin_guardian(activated_account,
cache_preferences,
cached_ussd_session,
celery_session_worker,
init_database,
guardian_account):
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
state_machine_data = (guardian_account.phone_number, ussd_session, activated_account, init_database)
assert is_dialers_pin_guardian(state_machine_data) is False
activated_account.add_guardian(guardian_account.phone_number)
init_database.flush()
state_machine_data = (guardian_account.phone_number, ussd_session, activated_account, init_database)
assert is_dialers_pin_guardian(state_machine_data) is True
def test_is_others_pin_guardian(activated_account,
cache_preferences,
cached_ussd_session,
celery_session_worker,
init_database,
guardian_account):
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
state_machine_data = (activated_account.phone_number, ussd_session, guardian_account, init_database)
assert is_others_pin_guardian(state_machine_data) is False
activated_account.add_guardian(guardian_account.phone_number)
init_database.flush()
state_machine_data = (activated_account.phone_number, ussd_session, guardian_account, init_database)
assert is_others_pin_guardian(state_machine_data) is True
def test_remove_pin_guardian(activated_account, generic_ussd_session, guardian_account, init_database):
generic_ussd_session['data'] = {'guardian_phone_number': guardian_account.phone_number}
activated_account.add_guardian(guardian_account.phone_number)
init_database.flush()
assert activated_account.get_guardians()[0] == guardian_account.phone_number
state_machine_data = ('', generic_ussd_session, activated_account, init_database)
remove_pin_guardian(state_machine_data)
assert len(activated_account.get_guardians()) == 0
def test_initiate_pin_reset(activated_account,
cache_preferences,
celery_session_worker,
cached_ussd_session,
guardian_account,
init_cache,
init_database,
load_ussd_menu,
mock_notifier_api,
set_locale_files):
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
ussd_session['data'] = {'guarded_account_phone_number': activated_account.phone_number}
state_machine_data = ('', ussd_session, guardian_account, init_database)
initiate_pin_reset(state_machine_data)
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
message = translation_for('sms.pin_reset_initiated', preferred_language, pin_initiator=guardian_account.standard_metadata_id())
assert mock_notifier_api.get('message') == message
assert mock_notifier_api.get('recipient') == activated_account.phone_number

View File

@@ -23,7 +23,6 @@ def test_upsell_unregistered_recipient(activated_account,
load_support_phone, load_support_phone,
mock_notifier_api, mock_notifier_api,
set_locale_files, set_locale_files,
set_active_token,
valid_recipient): valid_recipient):
cached_ussd_session.set_data('recipient_phone_number', valid_recipient.phone_number) cached_ussd_session.set_data('recipient_phone_number', valid_recipient.phone_number)
state_machine_data = ('', cached_ussd_session.to_json(), activated_account, init_database) state_machine_data = ('', cached_ussd_session.to_json(), activated_account, init_database)

View File

@@ -1,69 +0,0 @@
# standard imports
import json
# external imports
from cic_types.condiments import MetadataPointer
# local imports
from cic_ussd.cache import cache_data_key, get_cached_data
from cic_ussd.state_machine.logic.tokens import (is_valid_token_selection,
process_token_selection,
set_selected_active_token)
from cic_ussd.account.tokens import get_cached_token_data_list
# test imports
def test_is_valid_token_selection(activated_account,
cache_token_data_list,
cache_token_symbol_list,
cached_ussd_session,
init_cache,
init_database):
cached_token_data_list = get_cached_token_data_list(activated_account.blockchain_address)
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
ussd_session['data'] = {'account_tokens_list': cached_token_data_list}
state_machine_data = ('GFT', ussd_session, activated_account, init_database)
assert is_valid_token_selection(state_machine_data) is True
state_machine_data = ('1', ussd_session, activated_account, init_database)
assert is_valid_token_selection(state_machine_data) is True
state_machine_data = ('3', ussd_session, activated_account, init_database)
assert is_valid_token_selection(state_machine_data) is False
def test_process_token_selection(activated_account,
cache_token_data_list,
cache_token_symbol_list,
cached_ussd_session,
celery_session_worker,
init_cache,
init_database):
cached_token_data_list = get_cached_token_data_list(activated_account.blockchain_address)
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
ussd_session['data'] = {'account_tokens_list': cached_token_data_list}
state_machine_data = ('GFT', ussd_session, activated_account, init_database)
process_token_selection(state_machine_data)
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
assert ussd_session.get('data').get('selected_token').get('symbol') == 'GFT'
def test_set_selected_active_token(activated_account,
cache_token_data_list,
cache_token_symbol_list,
cached_ussd_session,
init_cache,
init_database):
cached_token_data_list = get_cached_token_data_list(activated_account.blockchain_address)
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
ussd_session['data'] = {'selected_token': cached_token_data_list[0]}
state_machine_data = ('GFT', ussd_session, activated_account, init_database)
set_selected_active_token(state_machine_data)
identifier = bytes.fromhex(activated_account.blockchain_address)
key = cache_data_key(identifier=identifier, salt=MetadataPointer.TOKEN_ACTIVE)
active_token = get_cached_data(key)
assert active_token == 'GFT'

View File

@@ -3,12 +3,13 @@ import json
# external imports # external imports
import pytest import pytest
import requests_mock
from chainlib.hash import strip_0x
# local imports # local imports
from cic_ussd.account.metadata import get_cached_preferred_language
from cic_ussd.account.tokens import get_active_token_symbol, get_cached_token_data
from cic_ussd.account.transaction import to_wei from cic_ussd.account.transaction import to_wei
from cic_ussd.cache import get_cached_data from cic_ussd.cache import get_cached_data
from cic_ussd.metadata import PersonMetadata
from cic_ussd.state_machine.logic.transaction import (is_valid_recipient, from cic_ussd.state_machine.logic.transaction import (is_valid_recipient,
is_valid_transaction_amount, is_valid_transaction_amount,
has_sufficient_balance, has_sufficient_balance,
@@ -17,6 +18,7 @@ from cic_ussd.state_machine.logic.transaction import (is_valid_recipient,
save_recipient_phone_to_session_data, save_recipient_phone_to_session_data,
save_transaction_amount_to_session_data) save_transaction_amount_to_session_data)
# test imports # test imports
@@ -47,18 +49,17 @@ def test_is_valid_transaction_amount(activated_account, amount, expected_result,
]) ])
def test_has_sufficient_balance(activated_account, def test_has_sufficient_balance(activated_account,
cache_balances, cache_balances,
cache_token_data, cache_default_token_data,
expected_result, expected_result,
generic_ussd_session, generic_ussd_session,
init_database, init_database,
set_active_token,
value): value):
state_machine_data = (value, generic_ussd_session, activated_account, init_database) state_machine_data = (value, generic_ussd_session, activated_account, init_database)
assert has_sufficient_balance(state_machine_data=state_machine_data) == expected_result assert has_sufficient_balance(state_machine_data=state_machine_data) == expected_result
def test_process_transaction_request(activated_account, def test_process_transaction_request(activated_account,
cache_token_data, cache_default_token_data,
cached_ussd_session, cached_ussd_session,
celery_session_worker, celery_session_worker,
init_cache, init_cache,
@@ -66,12 +67,7 @@ def test_process_transaction_request(activated_account,
load_chain_spec, load_chain_spec,
load_config, load_config,
mock_transfer_api, mock_transfer_api,
set_active_token,
valid_recipient): valid_recipient):
blockchain_address = activated_account.blockchain_address
token_symbol = get_active_token_symbol(blockchain_address)
token_data = get_cached_token_data(blockchain_address, token_symbol)
decimals = token_data.get("decimals")
cached_ussd_session.set_data('recipient_phone_number', valid_recipient.phone_number) cached_ussd_session.set_data('recipient_phone_number', valid_recipient.phone_number)
cached_ussd_session.set_data('transaction_amount', '50') cached_ussd_session.set_data('transaction_amount', '50')
ussd_session = get_cached_data(cached_ussd_session.external_session_id) ussd_session = get_cached_data(cached_ussd_session.external_session_id)
@@ -80,7 +76,7 @@ def test_process_transaction_request(activated_account,
process_transaction_request(state_machine_data) process_transaction_request(state_machine_data)
assert mock_transfer_api['from_address'] == activated_account.blockchain_address assert mock_transfer_api['from_address'] == activated_account.blockchain_address
assert mock_transfer_api['to_address'] == valid_recipient.blockchain_address assert mock_transfer_api['to_address'] == valid_recipient.blockchain_address
assert mock_transfer_api['value'] == to_wei(decimals, 50) assert mock_transfer_api['value'] == to_wei(50)
assert mock_transfer_api['token_symbol'] == load_config.get('TEST_TOKEN_SYMBOL') assert mock_transfer_api['token_symbol'] == load_config.get('TEST_TOKEN_SYMBOL')

View File

@@ -6,10 +6,8 @@ def test_state_machine(activated_account_ussd_session,
celery_session_worker, celery_session_worker,
init_database, init_database,
init_state_machine, init_state_machine,
load_languages, pending_account):
pending_account,
set_locale_files):
state_machine = UssdStateMachine(activated_account_ussd_session) state_machine = UssdStateMachine(activated_account_ussd_session)
state_machine.scan_data(('1', activated_account_ussd_session, pending_account, init_database)) state_machine.scan_data(('1', activated_account_ussd_session, pending_account, init_database))
assert state_machine.__repr__() == f'<KenyaUssdStateMachine: {state_machine.state}>' assert state_machine.__repr__() == f'<KenyaUssdStateMachine: {state_machine.state}>'
assert state_machine.state == 'account_creation_prompt' assert state_machine.state == 'initial_pin_entry'

View File

@@ -4,18 +4,15 @@ import json
# external imports # external imports
import celery import celery
import pytest import pytest
import requests_mock
from chainlib.hash import strip_0x from chainlib.hash import strip_0x
from cic_types.condiments import MetadataPointer from cic_types.condiments import MetadataPointer
# local imports # local imports
from cic_ussd.account.statement import filter_statement_transactions from cic_ussd.account.statement import filter_statement_transactions
from cic_ussd.account.tokens import collate_token_metadata
from cic_ussd.account.transaction import transaction_actors from cic_ussd.account.transaction import transaction_actors
from cic_ussd.cache import cache_data_key, get_cached_data from cic_ussd.cache import cache_data_key, get_cached_data
from cic_ussd.db.models.account import Account from cic_ussd.db.models.account import Account
from cic_ussd.error import AccountCreationDataNotFound from cic_ussd.error import AccountCreationDataNotFound
from cic_ussd.metadata import TokenMetadata
# test imports # test imports
@@ -25,13 +22,11 @@ from tests.helpers.accounts import blockchain_address
def test_account_creation_callback(account_creation_data, def test_account_creation_callback(account_creation_data,
cache_account_creation_data, cache_account_creation_data,
celery_session_worker, celery_session_worker,
cache_default_token_data,
custom_metadata, custom_metadata,
init_cache, init_cache,
init_database, init_database,
load_chain_spec, load_chain_spec,
mocker, mocker,
preferences,
setup_metadata_request_handler, setup_metadata_request_handler,
setup_metadata_signer): setup_metadata_signer):
phone_number = account_creation_data.get('phone_number') phone_number = account_creation_data.get('phone_number')
@@ -53,12 +48,10 @@ def test_account_creation_callback(account_creation_data,
cached_account_creation_data = get_cached_data(task_uuid) cached_account_creation_data = get_cached_data(task_uuid)
cached_account_creation_data = json.loads(cached_account_creation_data) cached_account_creation_data = json.loads(cached_account_creation_data)
assert cached_account_creation_data.get('status') == account_creation_data.get('status') assert cached_account_creation_data.get('status') == account_creation_data.get('status')
mock_add_preferences_metadata = mocker.patch('cic_ussd.tasks.metadata.add_preferences_metadata.apply_async')
mock_add_phone_pointer = mocker.patch('cic_ussd.tasks.metadata.add_phone_pointer.apply_async') mock_add_phone_pointer = mocker.patch('cic_ussd.tasks.metadata.add_phone_pointer.apply_async')
mock_add_custom_metadata = mocker.patch('cic_ussd.tasks.metadata.add_custom_metadata.apply_async') mock_add_custom_metadata = mocker.patch('cic_ussd.tasks.metadata.add_custom_metadata.apply_async')
preferred_language = preferences.get('preferred_language')
s_account_creation_callback = celery.signature( s_account_creation_callback = celery.signature(
'cic_ussd.tasks.callback_handler.account_creation_callback', [result, preferred_language, 0] 'cic_ussd.tasks.callback_handler.account_creation_callback', [result, '', 0]
) )
s_account_creation_callback.apply_async().get() s_account_creation_callback.apply_async().get()
account = init_database.query(Account).filter_by(phone_number=phone_number).first() account = init_database.query(Account).filter_by(phone_number=phone_number).first()
@@ -66,7 +59,6 @@ def test_account_creation_callback(account_creation_data,
cached_account_creation_data = get_cached_data(task_uuid) cached_account_creation_data = get_cached_data(task_uuid)
cached_account_creation_data = json.loads(cached_account_creation_data) cached_account_creation_data = json.loads(cached_account_creation_data)
assert cached_account_creation_data.get('status') == 'CREATED' assert cached_account_creation_data.get('status') == 'CREATED'
mock_add_preferences_metadata.assert_called_with((result, preferences), {}, queue='cic-ussd')
mock_add_phone_pointer.assert_called_with((result, phone_number), {}, queue='cic-ussd') mock_add_phone_pointer.assert_called_with((result, phone_number), {}, queue='cic-ussd')
mock_add_custom_metadata.assert_called_with((result, custom_metadata), {}, queue='cic-ussd') mock_add_custom_metadata.assert_called_with((result, custom_metadata), {}, queue='cic-ussd')
@@ -125,46 +117,12 @@ def test_statement_callback(activated_account, mocker, transactions_list):
(activated_account.blockchain_address, sender_transaction), {}, queue='cic-ussd') (activated_account.blockchain_address, sender_transaction), {}, queue='cic-ussd')
def test_token_data_callback(activated_account,
cache_token_data,
cache_token_meta_symbol,
cache_token_proof_symbol,
celery_session_worker,
default_token_data,
init_cache,
token_meta_symbol,
token_symbol):
blockchain_address = activated_account.blockchain_address
identifier = token_symbol.encode('utf-8')
status_code = 1
with pytest.raises(ValueError) as error:
s_token_data_callback = celery.signature(
'cic_ussd.tasks.callback_handler.token_data_callback',
[[default_token_data], blockchain_address, status_code])
s_token_data_callback.apply_async().get()
assert str(error.value) == f'Unexpected status code: {status_code}.'
token_data_key = cache_data_key([bytes.fromhex(blockchain_address), identifier], MetadataPointer.TOKEN_DATA)
token_meta_key = cache_data_key(identifier, MetadataPointer.TOKEN_META_SYMBOL)
token_info_key = cache_data_key(identifier, MetadataPointer.TOKEN_PROOF_SYMBOL)
token_meta = get_cached_data(token_meta_key)
token_meta = json.loads(token_meta)
token_info = get_cached_data(token_info_key)
token_info = json.loads(token_info)
token_data = collate_token_metadata(token_info=token_info, token_metadata=token_meta)
token_data = {**token_data, **default_token_data}
cached_token_data = json.loads(get_cached_data(token_data_key))
for key, value in token_data.items():
assert token_data[key] == cached_token_data[key]
def test_transaction_balances_callback(activated_account, def test_transaction_balances_callback(activated_account,
balances, balances,
cache_balances, cache_balances,
cache_token_data, cache_default_token_data,
cache_person_metadata, cache_person_metadata,
cache_preferences, cache_preferences,
celery_session_worker,
load_chain_spec, load_chain_spec,
mocker, mocker,
preferences, preferences,
@@ -199,16 +157,7 @@ def test_transaction_balances_callback(activated_account,
mocked_chain.assert_called() mocked_chain.assert_called()
def test_transaction_callback(cache_token_data, def test_transaction_callback(load_chain_spec, mock_async_balance_api_query, transaction_result):
celery_session_worker,
default_token_data,
init_cache,
load_chain_spec,
mock_async_balance_api_query,
token_symbol,
token_meta_symbol,
token_proof_symbol,
transaction_result):
status_code = 1 status_code = 1
with pytest.raises(ValueError) as error: with pytest.raises(ValueError) as error:
s_transaction_callback = celery.signature( s_transaction_callback = celery.signature(
@@ -217,19 +166,13 @@ def test_transaction_callback(cache_token_data,
s_transaction_callback.apply_async().get() s_transaction_callback.apply_async().get()
assert str(error.value) == f'Unexpected status code: {status_code}.' assert str(error.value) == f'Unexpected status code: {status_code}.'
with requests_mock.Mocker(real_http=False) as request_mocker: status_code = 0
identifier = token_symbol.encode('utf-8') s_transaction_callback = celery.signature(
metadata_client = TokenMetadata(identifier, cic_type=MetadataPointer.TOKEN_META_SYMBOL) 'cic_ussd.tasks.callback_handler.transaction_callback',
request_mocker.register_uri('GET', metadata_client.url, json=token_meta_symbol, status_code=200, reason='OK') [transaction_result, 'transfer', status_code])
metadata_client = TokenMetadata(identifier, cic_type=MetadataPointer.TOKEN_PROOF_SYMBOL) s_transaction_callback.apply_async().get()
request_mocker.register_uri('GET', metadata_client.url, json=token_proof_symbol, status_code=200, reason='OK') recipient_transaction, sender_transaction = transaction_actors(transaction_result)
status_code = 0 assert mock_async_balance_api_query.get('address') == recipient_transaction.get('blockchain_address') or sender_transaction.get('blockchain_address')
s_transaction_callback = celery.signature( assert mock_async_balance_api_query.get('token_symbol') == recipient_transaction.get('token_symbol') or sender_transaction.get('token_symbol')
'cic_ussd.tasks.callback_handler.transaction_callback',
[transaction_result, 'transfer', status_code])
s_transaction_callback.apply_async().get()
recipient_transaction, sender_transaction = transaction_actors(transaction_result)
assert mock_async_balance_api_query.get('address') == recipient_transaction.get('blockchain_address') or sender_transaction.get('blockchain_address')
assert mock_async_balance_api_query.get('token_symbol') == recipient_transaction.get('token_symbol') or sender_transaction.get('token_symbol')

View File

@@ -14,14 +14,13 @@ from cic_ussd.translation import translation_for
def test_transaction(cache_default_token_data, def test_transaction(cache_default_token_data,
cache_token_data,
celery_session_worker, celery_session_worker,
load_support_phone, load_support_phone,
mock_notifier_api, mock_notifier_api,
notification_data, notification_data,
set_locale_files): set_locale_files):
notification_data['transaction_type'] = 'transfer' notification_data['transaction_type'] = 'transfer'
amount = from_wei(6, notification_data.get('token_value')) amount = from_wei(notification_data.get('token_value'))
balance = notification_data.get('available_balance') balance = notification_data.get('available_balance')
phone_number = notification_data.get('phone_number') phone_number = notification_data.get('phone_number')
preferred_language = notification_data.get('preferred_language') preferred_language = notification_data.get('preferred_language')

View File

@@ -52,11 +52,6 @@ def test_cache_statement(activated_account,
cached_statement = get_cached_data(key) cached_statement = get_cached_data(key)
cached_statement = json.loads(cached_statement) cached_statement = json.loads(cached_statement)
assert len(cached_statement) == 1 assert len(cached_statement) == 1
sender_transaction['token_value'] = 60.0
s_parse_transaction = celery.signature(
'cic_ussd.tasks.processor.parse_transaction', [sender_transaction])
result = s_parse_transaction.apply_async().get()
s_cache_statement = celery.signature( s_cache_statement = celery.signature(
'cic_ussd.tasks.processor.cache_statement', [result, activated_account.blockchain_address] 'cic_ussd.tasks.processor.cache_statement', [result, activated_account.blockchain_address]
) )

View File

@@ -8,8 +8,8 @@ from cic_ussd.notifications import Notifier
@pytest.mark.parametrize("key, preferred_language, recipient, expected_message", [ @pytest.mark.parametrize("key, preferred_language, recipient, expected_message", [
("ussd.exit", "en", "+254712345678", "END Thank you for using the service."), ("ussd.kenya.exit", "en", "+254712345678", "END Thank you for using the service."),
("ussd.exit", "sw", "+254712345678", "END Asante kwa kutumia huduma.") ("ussd.kenya.exit", "sw", "+254712345678", "END Asante kwa kutumia huduma.")
]) ])
def test_send_sms_notification(celery_session_worker, def test_send_sms_notification(celery_session_worker,
expected_message, expected_message,

Some files were not shown because too many files have changed in this diff Show More