Compare commits

...

286 Commits

Author SHA1 Message Date
Carlosokumu
9f8fcf1ed0 attach account service to handler 2024-10-17 12:54:11 +03:00
Carlosokumu
4a599b902d define transaction 2024-10-17 12:49:51 +03:00
Carlosokumu
d181c34946 update handler and move transaction struct to models 2024-10-17 12:49:28 +03:00
Carlosokumu
bfa6eac4c2 define a test account service 2024-10-17 12:47:44 +03:00
Carlosokumu
667a21d950 pass account service as a param 2024-10-17 12:37:15 +03:00
Carlosokumu
4416c008fc add menu traversal tests 2024-10-17 12:36:24 +03:00
Carlosokumu
0a7389a71d explicitly reset authorize flag 2024-10-17 12:36:02 +03:00
Carlosokumu
383e64776d chore: change pin to PIN 2024-10-17 12:35:44 +03:00
Carlosokumu
a27c1790b8 setup test data file 2024-10-17 12:34:17 +03:00
Carlosokumu
f81e3508ca define engine to run tests 2024-10-17 12:33:38 +03:00
Carlosokumu
127219f510 define build for running online and offline tests 2024-10-17 12:33:09 +03:00
Carlosokumu
54cc33c819 Delete connstr in threadgdbm global channel map on close 2024-10-17 12:32:31 +03:00
Carlosokumu
a51f739d06 pass account service as a param 2024-10-17 12:31:33 +03:00
Carlosokumu
8d047ebe05 define universal group driver 2024-10-17 12:30:58 +03:00
Carlosokumu
4e840ac17c add uuid 2024-10-17 12:30:36 +03:00
e986eaa538 Merge pull request 'menu-api-errors' (#112) from menu-api-errors into master
Reviewed-on: urdt/ussd#112
2024-10-16 19:08:46 +02:00
Carlosokumu
0be570ae2d add check for api call failure 2024-10-15 16:31:31 +03:00
Carlosokumu
6a36bc43b5 add check for api call failure 2024-10-15 16:31:15 +03:00
Carlosokumu
1d27a88908 add test on validate amount 2024-10-15 16:30:29 +03:00
Carlosokumu
4889e6d18b Merge remote-tracking branch 'origin' into menu-api-errors 2024-10-15 14:04:45 +03:00
Carlosokumu
368c25125a set flag count to 128 2024-10-15 13:59:49 +03:00
Carlosokumu
283793a2ae add swahili menu option 2024-10-15 13:57:45 +03:00
Carlosokumu
bec7e5c69f update: fetch balances in a sepate function,show profile information in swahili and english 2024-10-15 13:57:05 +03:00
Carlosokumu
d638aba85e update tests 2024-10-15 13:50:28 +03:00
Carlosokumu
df7788dd0b return actual reponses on the api calls 2024-10-15 13:48:14 +03:00
Carlosokumu
4a62773098 add handler fetching custodial balances 2024-10-15 13:44:50 +03:00
Carlosokumu
26d315b032 add check for api call failure 2024-10-15 13:42:20 +03:00
Carlosokumu
1927544533 reset authorized flag 2024-10-15 13:40:40 +03:00
Carlosokumu
c641a0c669 add api failure nodes 2024-10-14 23:19:09 +03:00
Carlosokumu
952da86931 update balance nodes 2024-10-14 23:18:42 +03:00
Carlosokumu
be6391686f update return type 2024-10-14 23:17:57 +03:00
Carlosokumu
65794c1b20 add api calls flag 2024-10-14 23:17:17 +03:00
b058f9d770 Merge pull request 'Adapter to enable subdomain of db key prefixes' (#102) from lash/subprefix into master
Reviewed-on: urdt/ussd#102
2024-10-14 15:11:07 +02:00
lash
2a93ea7a0c Remove out-of-context extend key 2024-09-27 21:16:08 +01:00
lash
f89b1acc6c Merge branch 'master' into lash/subprefix 2024-09-27 21:15:17 +01:00
ddeafe015b Merge pull request 'tests-update' (#100) from tests-update into master
Reviewed-on: urdt/ussd#100
2024-09-27 22:14:50 +02:00
lash
1dc8b054eb Add sub-prefix db wrapper 2024-09-27 21:10:03 +01:00
Carlosokumu
a3ff3be5b1 clean up 2024-09-27 16:32:47 +03:00
Carlosokumu
ea52a7cf0a update tests 2024-09-26 23:11:37 +03:00
Carlosokumu
a993026380 update tests 2024-09-26 23:07:48 +03:00
lash
4ee241714b Attempt 2 revert changes from ssh 2024-09-26 15:08:15 +01:00
lash
d7dc743fa2 Merge remote-tracking branch 'origin/master' 2024-09-26 15:07:13 +01:00
lash
c220cbb767 Revert accidental merge of ssh 2024-09-26 15:05:17 +01:00
0dc322729f Merge pull request 'SSH server entry point.' (#77) from lash/ssh-2 into master
Reviewed-on: urdt/ussd#77
2024-09-26 15:55:51 +02:00
726d6dd338 Merge pull request 'menu-profile-edit' (#82) from menu-profile-edit into master
Reviewed-on: urdt/ussd#82
2024-09-26 15:48:55 +02:00
a8b202bd79 Merge branch 'master' into menu-profile-edit 2024-09-25 15:19:04 +02:00
Carlosokumu
6dbd250694 remove extra spaces 2024-09-25 09:29:26 +03:00
Carlosokumu
cc760e7698 remove move 2024-09-25 09:20:45 +03:00
Carlosokumu
8648ea599c add catch node 2024-09-25 09:20:45 +03:00
alfred-mk
77e4c5d43a reset the positon of LOAD function 2024-09-25 09:20:44 +03:00
alfred-mk
b15ba367c4 change position of LOAD function 2024-09-25 09:20:44 +03:00
alfred-mk
4db862bc97 LOAD and RELOAD get_amount 2024-09-25 09:20:43 +03:00
alfred-mk
6ad67f6adc change position of LOAD 2024-09-25 09:20:42 +03:00
0caf82a27e Merge pull request 'fix-repeated-nodes' (#81) from fix-repeated-nodes into master
Reviewed-on: urdt/ussd#81
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-09-25 01:48:34 +02:00
a430857309 Merge branch 'master' into fix-repeated-nodes 2024-09-25 01:41:43 +02:00
alfred-mk
2643c0c9ff only have a -d for the engineDebug 2024-09-24 16:11:22 +03:00
alfred-mk
b8852e1ab3 add engineDebug and stateDebug flags 2024-09-24 14:10:10 +03:00
Carlosokumu
0dea34daab resolve confilict 2024-09-24 09:18:21 +03:00
Carlosokumu
8057313c78 Merge remote-tracking branch 'remotes/origin/master' into menu-profile-edit 2024-09-24 09:10:17 +03:00
Carlosokumu
1bd96d0689 setup dedicated nodes for case selection 2024-09-24 09:02:04 +03:00
Carlosokumu
bbfe46f162 use current rendering symbol for case selection 2024-09-24 09:01:19 +03:00
Carlosokumu
eff2cbde8b setup check for unused code 2024-09-24 09:00:31 +03:00
Carlosokumu
5c85ecffd1 remove unused code 2024-09-24 09:00:13 +03:00
lash
ad1f9233ca Merge branch 'master' into lash/ssh-2 2024-09-23 20:26:47 +01:00
e9fdd1ddbe Merge pull request 'Threaded gdbm' (#76) from lash/thread-gdbm into master
Reviewed-on: urdt/ussd#76
2024-09-23 21:25:37 +02:00
alfred-mk
8a03b05c90 reset the positon of LOAD function 2024-09-23 21:20:57 +03:00
lash
c18c40b1b8 Merge branch 'lash/thread-gdbm' into lash/ssh-2 2024-09-23 18:56:38 +01:00
alfred-mk
5a38b40eaf Merge branch 'lash/thread-gdbm' into fix-repeated-nodes 2024-09-23 20:55:56 +03:00
lash
6112ca175f Merge branch 'master' into lash/thread-gdbm 2024-09-23 18:54:18 +01:00
Carlosokumu
c7ebca4729 update template text 2024-09-23 20:20:41 +03:00
Carlosokumu
fa0070b9a8 add swahili templates 2024-09-23 20:20:20 +03:00
Carlosokumu
4a447fbb45 update profile update 2024-09-23 20:10:37 +03:00
Carlosokumu
cb32cfe9d8 update dep 2024-09-23 20:09:51 +03:00
Carlosokumu
cf4e80dcb8 update profile edit nodes 2024-09-23 20:09:37 +03:00
lash
c02d70eaa4 Upgrade dep, improve logging 2024-09-23 17:35:17 +01:00
alfred-mk
909d1f3409 change position of LOAD function 2024-09-23 14:16:01 +03:00
alfred-mk
66cf90f044 LOAD and RELOAD get_amount 2024-09-23 14:13:26 +03:00
alfred-mk
74d5da3987 Merge branch 'lash/thread-gdbm' into fix-repeated-nodes 2024-09-23 11:40:29 +03:00
alfred-mk
f215159725 change position of LOAD 2024-09-23 11:38:07 +03:00
alfred-mk
a6301163eb use latest commit from go-vise 0.2.0 2024-09-23 09:59:21 +03:00
lash
a07fce527f Merge branch 'lash/thread-gdbm' into lash/ssh-2 2024-09-22 16:26:59 +01:00
lash
8a5209584d Update go module deps 2024-09-22 16:25:04 +01:00
lash
ddc8f6dad1 Update module dep 2024-09-22 16:24:09 +01:00
lash
c2b68231f5 Update gitignore 2024-09-22 16:15:40 +01:00
lash
3f3dbf414c Clarify keyfile use in readme 2024-09-22 16:13:30 +01:00
lash
e4c3e9f015 Add ssh instructions, log host key 2024-09-22 16:09:57 +01:00
lash
9b71244391 Merge branch 'lash/thread-gdbm' into lash/ssh-2 2024-09-22 15:43:18 +01:00
lash
ada1f26b68 Add keystore class, separate keystore tool executable 2024-09-22 15:41:55 +01:00
lash
935b777e57 Factor out ssh service code 2024-09-22 14:41:01 +01:00
lash
7d3ff690f0 Remove useless 'registered' member in thread gdbm 2024-09-22 03:10:58 +01:00
lash
a455cfb854 Return if duplicate connect 2024-09-22 03:09:53 +01:00
lash
19372c17f4 Reinstate ssh executable 2024-09-22 03:06:24 +01:00
lash
1cc2f58ab3 Remove ssh 2024-09-22 03:03:03 +01:00
lash
3756bdc98a Merge remote-tracking branch 'origin/lash/ssh' into lash/ssh 2024-09-22 02:58:18 +01:00
lash
e07f88b368 Settable ssh keyfile, host, port 2024-09-22 02:57:10 +01:00
472624a831 Merge branch 'master' into lash/ssh 2024-09-22 03:38:53 +02:00
lash
84422684c5 Rehabilitate all executables 2024-09-22 02:33:11 +01:00
lash
28fc7a0462 Implement threaded gdbm in storageservice 2024-09-22 02:28:00 +01:00
lash
78d349ccc7 Add threaded gdbm wrapper 2024-09-22 02:21:56 +01:00
lash
0813a619b4 Add hacky db closer function in ssh 2024-09-21 21:32:02 +01:00
lash
5ed9d2643b WIP can execute single session, but persister fails in next 2024-09-21 18:56:20 +01:00
lash
8dcba67fe9 WIP can execute single session, but persister fails in next 2024-09-21 18:49:11 +01:00
lash
5cac17676a WIP adapt ssh server sketch to urdt setup 2024-09-21 00:32:39 +01:00
4439cc249a Merge pull request 'go-vise-0.2.0' (#71) from go-vise-0.2.0 into master
Reviewed-on: urdt/ussd#71
2024-09-21 00:06:07 +02:00
alfred-mk
e754773bce remove need for initial Exec before loop 2024-09-20 22:16:43 +03:00
alfred-mk
63c7c65a00 rename all WriteResult to Flush 2024-09-20 22:15:24 +03:00
alfred-mk
9925b3a2e5 use updated latest commit from go-vise 0.2.0 2024-09-20 22:12:52 +03:00
alfred-mk
a1056cf287 removed unused code 2024-09-20 13:54:23 +03:00
alfred-mk
e6e701a609 remove init and use Exec 2024-09-20 13:13:20 +03:00
alfred-mk
b66891a646 use latest commit from go-vise 0.2.0 2024-09-20 13:10:44 +03:00
7197382911 Merge pull request 'Code refactor' (#66) from wip-main-refactor into master
Reviewed-on: urdt/ussd#66
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-09-19 20:19:16 +02:00
5c5f4cbc8e Merge branch 'master' into wip-main-refactor 2024-09-19 20:19:06 +02:00
Carlosokumu
e581a1ad2f factor out getflag 2024-09-19 21:04:09 +03:00
929e812992 Merge pull request 'at-return-output' (#63) from at-return-output into master
Reviewed-on: urdt/ussd#63
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-09-19 18:23:13 +02:00
alfred-mk
3fe66466c5 renamed test file and added server.go tests 2024-09-19 18:52:13 +03:00
alfred-mk
f6979868e5 added at_session_handler tests 2024-09-19 17:19:09 +03:00
alfred-mk
e8be64ae28 added http related mocks 2024-09-19 17:18:14 +03:00
alfred-mk
e4d8bfad7b moved the mocks package 2024-09-19 17:17:28 +03:00
Carlosokumu
96358959b4 remove unused code 2024-09-19 17:04:08 +03:00
Carlosokumu
bc9dfe4f65 remove redundant code 2024-09-19 17:01:23 +03:00
Carlosokumu
ffa00ae15c refactor code 2024-09-19 16:18:21 +03:00
Carlosokumu
13294b42d3 refactor code 2024-09-19 16:16:17 +03:00
Carlosokumu
81159e77d1 refactor code 2024-09-19 16:15:29 +03:00
Carlosokumu
5bd51b280e add storage service for menu 2024-09-19 15:57:11 +03:00
Carlosokumu
71e1ae6e3c add function registration service 2024-09-19 15:54:23 +03:00
alfred-mk
10de039da0 remove obsolete code 2024-09-18 15:42:56 +03:00
alfred-mk
27aa71e0ee only set the code to 200 if no error exists 2024-09-18 15:11:49 +03:00
alfred-mk
a9a429824c return if the sessionId cannot be retrieved from the request 2024-09-18 14:52:56 +03:00
4098ac8a19 Merge pull request 'change-language' (#58) from change-language into master
Reviewed-on: urdt/ussd#58
2024-09-17 18:31:10 +02:00
2982f08b41 Merge branch 'master' into change-language 2024-09-17 18:30:52 +02:00
alfred-mk
75ab9c66a3 run TestSetLanguage with execPath instead of input 2024-09-17 18:54:07 +03:00
95f02231b3 Merge pull request 'Unit tests' (#62) from wip-unit-tests into master
Reviewed-on: urdt/ussd#62
2024-09-17 15:55:33 +02:00
Carlosokumu
599f7a2857 add more tests 2024-09-17 15:44:22 +03:00
alfred-mk
2e7c07e6f4 added missing functions to the other getHandlers 2024-09-17 15:31:44 +03:00
alfred-mk
01f7571185 use dedicated nodes to set the language 2024-09-17 15:29:21 +03:00
alfred-mk
065c8e5c1d use a single set_language function for setting and changing language 2024-09-17 15:26:50 +03:00
alfred-mk
dc14480519 remove extra space 2024-09-16 17:29:27 +03:00
alfred-mk
aaf4923f64 ensure a PIN is set before changing the language 2024-09-16 17:29:27 +03:00
alfred-mk
e9c645bd87 added the profile info on the swa menu template 2024-09-16 17:29:27 +03:00
alfred-mk
1be6da9139 remove new lines on templates and menus to clean up the empty spaces on the menu 2024-09-16 17:29:27 +03:00
alfred-mk
fa2930d93a added quit menu to handle eng and swa 2024-09-16 17:29:27 +03:00
alfred-mk
a409e292ab add the change_language functionality to the account menu 2024-09-16 17:29:27 +03:00
alfred-mk
00c86a2850 change language 2024-09-16 17:29:27 +03:00
alfred-mk
4daac7e90b added change_language node and templates 2024-09-16 17:29:27 +03:00
alfred-mk
06e23565df added the SetNewLanguage function 2024-09-16 17:29:27 +03:00
b19188165b Merge pull request 'wip-menu-help' (#56) from wip-menu-help into master
Reviewed-on: urdt/ussd#56
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-09-16 16:21:49 +02:00
Carlosokumu
2ed9f083bb add quit with helpline info
add quit with helpline info
2024-09-16 15:17:56 +01:00
e323ffa078 Merge pull request 'Pin reset' (#59) from wip-pin-reset into master
Reviewed-on: urdt/ussd#59
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-09-16 16:14:52 +02:00
68de83af97 Merge pull request 'Allow setting go-vise path for asm executable in makefile' (#57) from lash/vise-make-var into master
Reviewed-on: urdt/ussd#57
2024-09-16 15:49:58 +02:00
Carlosokumu
a3f2b23128 add pin reset templates for swahili 2024-09-16 15:17:15 +03:00
Carlosokumu
09f970db9b navigate to main menu after successful pin reset 2024-09-16 14:59:26 +03:00
Carlosokumu
04edd90c21 fix sink issue onback option 2024-09-16 14:58:56 +03:00
Carlosokumu
a3f410875a define temporary key value 2024-09-16 14:39:42 +03:00
Carlosokumu
3f3e98e637 add pin reset handlers 2024-09-16 14:39:01 +03:00
Carlosokumu
660f8b7aa2 add pin reset nodes 2024-09-16 14:30:47 +03:00
lash
8ae3372c36 Allow setting go-vise path for asm executable in makefile 2024-09-14 21:34:11 +01:00
3a7c3ffc67 Merge pull request 'wip-unit-tests' (#55) from wip-unit-tests into master
Reviewed-on: urdt/ussd#55
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-09-14 22:29:52 +02:00
Carlosokumu
78c033e61c remove deprecated code 2024-09-14 20:59:08 +03:00
Carlosokumu
b5ef483f34 clean up tests 2024-09-14 20:07:17 +03:00
Carlosokumu
006eef0a28 add testdata loader 2024-09-14 20:02:21 +03:00
Carlosokumu
e99a788c60 add tests 2024-09-14 20:01:58 +03:00
b5d33a98f0 Merge pull request 'Enable CLI driver of async session' (#49) from lash/async-driver into master
Reviewed-on: urdt/ussd#49
2024-09-14 16:52:06 +02:00
f4f475bd45 Merge pull request 'africas-talking' (#50) from africas-talking into lash/async-driver
Reviewed-on: urdt/ussd#50
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-09-14 16:28:05 +02:00
6fe2e7287f Update the func name to Output
Signed-off-by: Alfred Kamanda <alfredkamandamw@gmail.com>
2024-09-14 16:01:18 +02:00
alfred-mk
1e9c9cf6ad removed AT specific code 2024-09-14 15:57:16 +03:00
alfred-mk
3cb0b099e4 use the new AtSessionHandler 2024-09-14 15:55:45 +03:00
alfred-mk
b53658e038 have AtOutput as an option 2024-09-13 16:03:31 +03:00
alfred-mk
512460fdeb Added Custom AtOutput to append CON or END to output 2024-09-13 16:02:04 +03:00
alfred-mk
762f90adf6 Added Continue bool to track whether the execution should be terminated 2024-09-12 23:52:35 +03:00
lash
9b4a4eeaf4 Temporary solution for make sure storage object gets put back in all cases of execution 2024-09-12 16:46:11 +01:00
alfred-mk
5e4e6a21a0 Added africastalking binary 2024-09-12 15:56:57 +03:00
alfred-mk
ffeb28e851 Use latest go-vise package 2024-09-12 15:56:34 +03:00
lash
9c751aff30 Update go-vise 2024-09-12 04:19:13 +01:00
lash
b31d3b907a Add shutdown to async, rehabilitate http cmd 2024-09-12 04:13:57 +01:00
lash
d49f866ca4 Factor out methods common to http and async cli 2024-09-12 04:07:55 +01:00
lash
dde9f552a6 Isolate http specific parts to minimal 2024-09-12 03:30:23 +01:00
0f9b5551ec Merge pull request 'Implement http server for the URDT vise engine' (#41) from lash/http-server into master
Reviewed-on: urdt/ussd#41
2024-09-12 00:13:14 +02:00
lash
514e043e38 Fix symptom of handler missing persister 2024-09-11 17:53:12 +01:00
lash
836e5fe8ee Revert africas talking changes in http 2024-09-11 17:32:55 +01:00
lash
660fcaa0b6 Import go-vise fixing missing exit on state reset 2024-09-11 17:25:56 +01:00
alfred-mk
44015b1c76 Parse the request body to get the PhoneNumber and Input text 2024-09-11 15:40:49 +03:00
lash
7438531900 Update govise to include removed dead asm code 2024-09-11 04:10:05 +01:00
lash
681f293d3c Externalize requestparser, flush persister on http request end 2024-09-10 23:09:10 +01:00
lash
8e3ff27bb8 Ensure db close on http signal shutdown, correct stores to provider 2024-09-10 20:44:10 +01:00
lash
dd2468a4d7 Http server harness
Add storage retrieval solution for http handler

Successfully executed account regisration using http

Set upstream go-vise dependency version in go.mod

Adapt menuhandler to upstream
2024-09-10 13:59:36 +01:00
63cee42261 Merge pull request 'wip-code-check' (#44) from wip-code-check into master
Reviewed-on: urdt/ussd#44
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-09-10 14:25:50 +02:00
Carlosokumu
9f034967b5 remove resource directory 2024-09-10 12:32:19 +03:00
Carlosokumu
ad48890a9f remove deprecated code 2024-09-10 11:24:09 +03:00
Carlosokumu
c0a3ad7e2b delete deprecated code 2024-09-10 11:23:41 +03:00
Carlosokumu
a3dffdf4e9 match code refactor 2024-09-10 11:23:25 +03:00
Carlosokumu
47d39a1c6f remove commented test 2024-09-09 17:24:43 +03:00
Carlosokumu
f5f1cbaaba cleanup 2024-09-09 17:18:07 +03:00
Carlosokumu
134aa96194 implement db for user datastore 2024-09-09 17:16:08 +03:00
Carlosokumu
0cdb23fbea setupmock for user datastore 2024-09-09 17:15:35 +03:00
Carlosokumu
ca655b0cdc add tests 2024-09-09 17:15:04 +03:00
Carlosokumu
da93444d3f remove deprecated code 2024-09-09 10:14:40 +03:00
Carlosokumu
c2564a9b8f remove go-vise subdirectory 2024-09-09 10:12:29 +03:00
Carlosokumu
33c6b35f8f Merge branch 'origin/master' into wip-code-check 2024-09-09 09:56:00 +03:00
Carlosokumu
ba72b3bf44 Merge branch 'master' into wip-code-check 2024-09-09 09:45:29 +03:00
Carlosokumu
e961b6cb6a Merge branch 'wip-code-check' 2024-09-09 09:22:08 +03:00
Carlosokumu
2c059afa7d resolve 2024-09-07 22:05:09 +03:00
Carlosokumu
9aab7d3a9b Merge branch 'master' into wip-code-check 2024-09-07 22:04:30 +03:00
Carlosokumu
a25beb5b80 Merge branch 'wip-code-check' 2024-09-07 21:54:00 +03:00
Carlosokumu
39d27209cd Merge remote-tracking branch 'refs/remotes/origin/wip-code-check' into wip-code-check 2024-09-07 18:10:40 +03:00
Carlosokumu
16a56ef29d add go-vise 2024-09-07 18:09:55 +03:00
Carlosokumu
6d02ea79ec add go-vise 2024-09-07 18:09:13 +03:00
alfred-mk
deb4706b1d Test commit 2024-09-07 17:51:30 +03:00
alfred-mk
e14fd5e496 Return the response and the error 2024-09-07 17:41:05 +03:00
Carlosokumu
01c13ec581 make packKey accessible from tests 2024-09-07 16:25:29 +03:00
Carlosokumu
dd97531861 remove uimplemented tests 2024-09-07 16:24:37 +03:00
Carlosokumu
be33b7458f cleanup code 2024-09-07 16:22:08 +03:00
Carlosokumu
4c3f63a48f create an account only if not found in gdbm 2024-09-07 10:36:00 +03:00
Carlosokumu
d1d5c897d1 refactor 2024-09-07 09:55:59 +03:00
Carlosokumu
04ea11dd6d setup db mock 2024-09-07 09:55:47 +03:00
Carlosokumu
5722552395 remove old mocks 2024-09-07 09:55:13 +03:00
Carlosokumu
421fbe5543 setup mock for the acccount service 2024-09-07 09:55:00 +03:00
alfred-mk
de24ca0648 Mock the gdm nd updated the TestSaveFirstname and TestSaveFamilyname 2024-09-07 09:30:20 +03:00
Carlosokumu
175cbd1983 setup template test 2024-09-06 17:51:07 +03:00
Carlosokumu
8175d6ac12 setup db mock 2024-09-06 17:50:33 +03:00
Carlosokumu
d4bae50ff0 Merge remote-tracking branch 'refs/remotes/origin/wip-code-check' into wip-code-check 2024-09-06 16:54:41 +03:00
Carlosokumu
d7376a4196 format error 2024-09-06 16:53:22 +03:00
alfred-mk
eb9f470ac5 Update the GetProfileInfo to get data from gdbm 2024-09-06 16:17:26 +03:00
Carlosokumu
7a12588744 remove deprecated code 2024-09-06 12:42:24 +03:00
Carlosokumu
6947b1e4a4 rename getflags to getparser 2024-09-06 11:03:01 +03:00
Carlosokumu
2cc85379cc update log 2024-09-06 11:02:33 +03:00
alfred-mk
d001c5a7fc Write and read data from gdbm 2024-09-06 09:33:39 +03:00
alfred-mk
de8c7ce34a Added user data to db.go util 2024-09-06 08:35:01 +03:00
lash
cad8e86c8b Remove manual init calls in handler funcs 2024-09-05 23:55:54 +01:00
lash
0feb013c78 Ensure handler init when state and cache is available 2024-09-05 20:40:40 +01:00
Carlosokumu
643b4d87a9 read account pin of gdbm store 2024-09-05 20:30:28 +03:00
Carlosokumu
5abaf28f49 remove explicit attachment of state to engine 2024-09-05 20:26:52 +03:00
alfred-mk
17804e7641 Save the actual PIN and verify it 2024-09-05 19:50:02 +03:00
lash
db3ec1991d Remove obsolete store and persister global 2024-09-05 16:45:35 +01:00
lash
62b7adb967 Fix missing byte allocation for typ serialize 2024-09-05 15:41:27 +01:00
lash
6fa5f49bc0 Remove impossible nil check in handler 2024-09-05 15:28:53 +01:00
Carlosokumu
2bf7a5c246 setup db keys 2024-09-05 17:12:38 +03:00
Carlosokumu
98c74dffc4 use state from persister 2024-09-05 17:09:30 +03:00
Carlosokumu
771a1e8169 code refactoring 2024-09-05 17:07:20 +03:00
Carlosokumu
220c5b2081 remove commented code 2024-09-04 20:22:25 +03:00
Carlosokumu
2fc8a0e5a7 use fsDb for the resources 2024-09-04 19:57:08 +03:00
Alfred Kamanda
c6c66f956a Merge pull request 'wip-flag-migration' (#28) from wip-flag-migration into master
Reviewed-on: urdt/ussd#28
2024-09-04 11:25:33 +02:00
alfred-mk
1957606bc2 Added log to show debug the flag loaded 2024-09-04 12:19:34 +03:00
alfred-mk
8ffc5d67cc Removed mutex 2024-09-04 12:01:51 +03:00
alfred-mk
2d7b6bd125 Removed unused code 2024-09-03 22:28:48 +03:00
alfred-mk
5105e902f1 Process flags once and make them available across functions 2024-09-03 19:06:37 +03:00
Carlosokumu
19b2fa65fa Merge remote-tracking branch 'refs/remotes/origin/wip-flag-migration' into wip-flag-migration 2024-09-03 18:00:06 +03:00
Carlosokumu
ee4db50e00 remove unused code 2024-09-03 17:59:03 +03:00
alfred-mk
99d18322b2 Append to the correct flag set/reset 2024-09-03 15:34:19 +03:00
Carlosokumu
a07a8703f4 remove flag setting invalid pin entry 2024-09-03 15:13:55 +03:00
Carlosokumu
94f12edeaf Merge remote-tracking branch 'refs/remotes/origin/wip-flag-migration' into wip-flag-migration 2024-09-03 15:08:45 +03:00
Carlosokumu
fee51edade append sessionid to the gdbm file 2024-09-03 15:07:49 +03:00
alfred-mk
f90fde90fb Register flags in debugger from the csv 2024-09-03 15:04:09 +03:00
alfred-mk
ab6433168a Resolve bug on transaction confirmation PIN 2024-09-03 15:02:31 +03:00
Carlosokumu
0c360c0cc4 remove panics after failed execution 2024-09-03 13:19:32 +03:00
Carlosokumu
560714838f fix menu termination on invalid pin entry 2024-09-03 11:51:14 +03:00
Carlosokumu
c365eaa1d1 remove unused code 2024-09-02 21:24:16 +03:00
Carlosokumu
337419c9f2 Merge remote-tracking branch 'refs/remotes/origin/wip-flag-migration' into wip-flag-migration 2024-09-02 21:10:41 +03:00
Carlosokumu
d4c4db09f3 load profile data from gdbm 2024-09-02 21:09:39 +03:00
alfred-mk
3a3ddc9922 Use FlagParser on main.go and delete the flags.go file 2024-09-02 21:06:42 +03:00
Carlosokumu
1c1ea74088 Merge remote-tracking branch 'refs/remotes/origin/wip-flag-migration' into wip-flag-migration 2024-09-02 17:12:01 +03:00
Carlosokumu
14c8230fd4 code cleanup 2024-09-02 17:11:08 +03:00
alfred-mk
b6e4ba7ede Verify PIN using the one in the db 2024-09-02 17:08:57 +03:00
Carlosokumu
2b511aba4c remove commented code 2024-09-02 16:39:04 +03:00
Carlosokumu
626fca1a55 Merge remote-tracking branch 'refs/remotes/origin/wip-flag-migration' into wip-flag-migration 2024-09-02 16:30:58 +03:00
Carlosokumu
0c96831378 use gdbm 2024-09-02 16:05:13 +03:00
Carlosokumu
fe4df78f80 use gdbm for user info storage 2024-09-02 16:04:53 +03:00
alfred-mk
31be1fa221 Use flags from pp.csv on the menuhandler functions and tests 2024-09-02 14:00:02 +03:00
Carlosokumu
ba4d0abcda improve doc line 2024-09-02 10:03:57 +03:00
alfred-mk
632dd860ff Use flags from pp.csv on the Authorize function 2024-08-31 17:16:57 +03:00
Carlosokumu
07c8638a6d use correct symbol name 2024-08-31 15:09:34 +03:00
Carlosokumu
c71f6e76d7 Merge remote-tracking branch 'refs/remotes/origin/wip-flag-migration' into wip-flag-migration 2024-08-31 14:56:18 +03:00
Carlosokumu
2559b4e604 add test profile 2024-08-31 14:55:54 +03:00
alfred-mk
460e6fa2c8 Renamed duplicate flag 2024-08-31 14:50:45 +03:00
alfred-mk
c3c5c83662 Change the order of flags 2024-08-31 14:48:02 +03:00
alfred-mk
143c9b14a1 Renamed the flags 2024-08-31 12:58:48 +03:00
alfred-mk
42ab2dd577 Use preprocessor flags 2024-08-31 12:16:36 +03:00
alfred-mk
26959be149 Fixed bug on transaction pin node 2024-08-31 12:09:02 +03:00
Carlosokumu
118b5577bb Merge remote-tracking branch 'refs/remotes/origin/wip-flag-migration' into wip-flag-migration 2024-08-31 12:06:42 +03:00
Carlosokumu
4e69fa454d use string flags 2024-08-31 12:02:44 +03:00
alfred-mk
9fb3ef8e2a Use preprocessor flags 2024-08-31 11:56:36 +03:00
Carlosokumu
ec4201139a add _flag suffix 2024-08-31 11:35:29 +03:00
alfred-mk
bcc73dec23 Add the flag file during build 2024-08-31 11:27:47 +03:00
alfred-mk
828f9a17fc Added the csv containing string flags 2024-08-31 10:30:10 +03:00
Carlosokumu
de4aaeb02b use script dir for parser 2024-08-31 10:26:49 +03:00
Carlosokumu
cc323707fc setup parser 2024-08-31 10:21:20 +03:00
ef0c207fc4 Merge pull request 'wip-account-creation' (#4) from wip-account-creation into master
Reviewed-on: urdt/ussd#4
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-08-30 14:37:58 +02:00
154 changed files with 6187 additions and 1469 deletions

2
.gitignore vendored
View File

@@ -4,3 +4,5 @@ go.work*
**/*/*.bin
**/*/.state/
cmd/.state/
id_*
*.gdbm

169
cmd/africastalking/main.go Normal file
View File

@@ -0,0 +1,169 @@
package main
import (
"context"
"flag"
"fmt"
"net/http"
"os"
"os/signal"
"path"
"strconv"
"strings"
"syscall"
"git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/handlers/server"
httpserver "git.grassecon.net/urdt/ussd/internal/http"
"git.grassecon.net/urdt/ussd/internal/storage"
)
var (
logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration")
)
type atRequestParser struct{}
func (arp *atRequestParser) GetSessionId(rq any) (string, error) {
rqv, ok := rq.(*http.Request)
if !ok {
return "", handlers.ErrInvalidRequest
}
if err := rqv.ParseForm(); err != nil {
return "", fmt.Errorf("failed to parse form data: %v", err)
}
phoneNumber := rqv.FormValue("phoneNumber")
if phoneNumber == "" {
return "", fmt.Errorf("no phone number found")
}
return phoneNumber, nil
}
func (arp *atRequestParser) GetInput(rq any) ([]byte, error) {
rqv, ok := rq.(*http.Request)
if !ok {
return nil, handlers.ErrInvalidRequest
}
if err := rqv.ParseForm(); err != nil {
return nil, fmt.Errorf("failed to parse form data: %v", err)
}
text := rqv.FormValue("text")
parts := strings.Split(text, "*")
if len(parts) == 0 {
return nil, fmt.Errorf("no input found")
}
return []byte(parts[len(parts)-1]), nil
}
func main() {
var dbDir string
var resourceDir string
var size uint
var engineDebug bool
var host string
var port uint
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.UintVar(&size, "s", 160, "max size of output")
flag.StringVar(&host, "h", "127.0.0.1", "http host")
flag.UintVar(&port, "p", 7123, "http port")
flag.Parse()
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
ctx := context.Background()
pfp := path.Join(scriptDir, "pp.csv")
cfg := engine.Config{
Root: "root",
OutputSize: uint32(size),
FlagCount: uint32(128),
}
if engineDebug {
cfg.EngineDebug = true
}
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
rs, err := menuStorageService.GetResource(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
err = menuStorageService.EnsureDbDir()
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
userdataStore, err := menuStorageService.GetUserdataDb(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
defer userdataStore.Close()
dbResource, ok := rs.(*resource.DbResource)
if !ok {
os.Exit(1)
}
lhs, err := handlers.NewLocalHandlerService(pfp, true, dbResource, cfg, rs)
lhs.SetDataStore(&userdataStore)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
accountService := server.AccountService{}
hl, err := lhs.GetHandler(&accountService)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
stateStore, err := menuStorageService.GetStateStore(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
defer stateStore.Close()
rp := &atRequestParser{}
bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
sh := httpserver.NewATSessionHandler(bsh)
s := &http.Server{
Addr: fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))),
Handler: sh,
}
s.RegisterOnShutdown(sh.Shutdown)
cint := make(chan os.Signal)
cterm := make(chan os.Signal)
signal.Notify(cint, os.Interrupt, syscall.SIGINT)
signal.Notify(cterm, os.Interrupt, syscall.SIGTERM)
go func() {
select {
case _ = <-cint:
case _ = <-cterm:
}
s.Shutdown(ctx)
}()
err = s.ListenAndServe()
if err != nil {
logg.Infof("Server closed with error", "err", err)
}
}

163
cmd/async/main.go Normal file
View File

@@ -0,0 +1,163 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"os/signal"
"path"
"syscall"
"git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/handlers/server"
"git.grassecon.net/urdt/ussd/internal/storage"
)
var (
logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration")
)
type asyncRequestParser struct {
sessionId string
input []byte
}
func (p *asyncRequestParser) GetSessionId(r any) (string, error) {
return p.sessionId, nil
}
func (p *asyncRequestParser) GetInput(r any) ([]byte, error) {
return p.input, nil
}
func main() {
var sessionId string
var dbDir string
var resourceDir string
var size uint
var engineDebug bool
var host string
var port uint
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.UintVar(&size, "s", 160, "max size of output")
flag.StringVar(&host, "h", "127.0.0.1", "http host")
flag.UintVar(&port, "p", 7123, "http port")
flag.Parse()
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId)
ctx := context.Background()
pfp := path.Join(scriptDir, "pp.csv")
cfg := engine.Config{
Root: "root",
OutputSize: uint32(size),
FlagCount: uint32(128),
}
if engineDebug {
cfg.EngineDebug = true
}
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
rs, err := menuStorageService.GetResource(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
err = menuStorageService.EnsureDbDir()
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
userdataStore, err := menuStorageService.GetUserdataDb(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
defer userdataStore.Close()
dbResource, ok := rs.(*resource.DbResource)
if !ok {
os.Exit(1)
}
lhs, err := handlers.NewLocalHandlerService(pfp, true, dbResource, cfg, rs)
lhs.SetDataStore(&userdataStore)
accountService := server.AccountService{}
hl, err := lhs.GetHandler(&accountService)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
stateStore, err := menuStorageService.GetStateStore(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
defer stateStore.Close()
rp := &asyncRequestParser{
sessionId: sessionId,
}
sh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
cfg.SessionId = sessionId
rqs := handlers.RequestSession{
Ctx: ctx,
Writer: os.Stdout,
Config: cfg,
}
cint := make(chan os.Signal)
cterm := make(chan os.Signal)
signal.Notify(cint, os.Interrupt, syscall.SIGINT)
signal.Notify(cterm, os.Interrupt, syscall.SIGTERM)
go func() {
select {
case _ = <-cint:
case _ = <-cterm:
}
sh.Shutdown()
}()
for true {
rqs, err = sh.Process(rqs)
if err != nil {
logg.ErrorCtxf(ctx, "error in process: %v", "err", err)
fmt.Errorf("error in process: %v", err)
os.Exit(1)
}
rqs, err = sh.Output(rqs)
if err != nil {
logg.ErrorCtxf(ctx, "error in output: %v", "err", err)
fmt.Errorf("error in output: %v", err)
os.Exit(1)
}
rqs, err = sh.Reset(rqs)
if err != nil {
logg.ErrorCtxf(ctx, "error in reset: %v", "err", err)
fmt.Errorf("error in reset: %v", err)
os.Exit(1)
}
fmt.Println("")
_, err = fmt.Scanln(&rqs.Input)
if err != nil {
logg.ErrorCtxf(ctx, "error in input", "err", err)
fmt.Errorf("error in input: %v", err)
os.Exit(1)
}
}
}

129
cmd/http/main.go Normal file
View File

@@ -0,0 +1,129 @@
package main
import (
"context"
"flag"
"fmt"
"net/http"
"os"
"os/signal"
"path"
"strconv"
"syscall"
"git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/handlers/server"
httpserver "git.grassecon.net/urdt/ussd/internal/http"
"git.grassecon.net/urdt/ussd/internal/storage"
)
var (
logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration")
)
func main() {
var dbDir string
var resourceDir string
var size uint
var engineDebug bool
var host string
var port uint
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.UintVar(&size, "s", 160, "max size of output")
flag.StringVar(&host, "h", "127.0.0.1", "http host")
flag.UintVar(&port, "p", 7123, "http port")
flag.Parse()
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
ctx := context.Background()
pfp := path.Join(scriptDir, "pp.csv")
cfg := engine.Config{
Root: "root",
OutputSize: uint32(size),
FlagCount: uint32(128),
}
if engineDebug {
cfg.EngineDebug = true
}
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
rs, err := menuStorageService.GetResource(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
err = menuStorageService.EnsureDbDir()
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
userdataStore, err := menuStorageService.GetUserdataDb(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
defer userdataStore.Close()
dbResource, ok := rs.(*resource.DbResource)
if !ok {
os.Exit(1)
}
lhs, err := handlers.NewLocalHandlerService(pfp, true, dbResource, cfg, rs)
lhs.SetDataStore(&userdataStore)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
accountService := server.AccountService{}
hl, err := lhs.GetHandler(&accountService)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
stateStore, err := menuStorageService.GetStateStore(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
defer stateStore.Close()
rp := &httpserver.DefaultRequestParser{}
bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
sh := httpserver.ToSessionHandler(bsh)
s := &http.Server{
Addr: fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))),
Handler: sh,
}
s.RegisterOnShutdown(sh.Shutdown)
cint := make(chan os.Signal)
cterm := make(chan os.Signal)
signal.Notify(cint, os.Interrupt, syscall.SIGINT)
signal.Notify(cterm, os.Interrupt, syscall.SIGTERM)
go func() {
select {
case _ = <-cint:
case _ = <-cterm:
}
s.Shutdown(ctx)
}()
err = s.ListenAndServe()
if err != nil {
logg.Infof("Server closed with error", "err", err)
}
}

View File

@@ -7,138 +7,99 @@ import (
"os"
"path"
"git.defalsify.org/vise.git/cache"
"git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/resource"
"git.defalsify.org/vise.git/state"
"git.grassecon.net/urdt/ussd/internal/handlers/ussd"
"git.grassecon.net/urdt/ussd/internal/models"
"git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/handlers/server"
"git.grassecon.net/urdt/ussd/internal/storage"
)
var (
logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration")
)
func main() {
var dir string
var root string
var dbDir string
var size uint
var sessionId string
flag.StringVar(&dir, "d", ".", "resource dir to read from")
flag.UintVar(&size, "s", 0, "max size of output")
flag.StringVar(&root, "root", "root", "entry point symbol")
flag.StringVar(&sessionId, "session-id", "default", "session id")
var engineDebug bool
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.UintVar(&size, "s", 160, "max size of output")
flag.Parse()
fmt.Fprintf(os.Stderr, "starting session at symbol '%s' using resource dir: %s\n", root, dir)
logg.Infof("start command", "dbdir", dbDir, "outputsize", size)
ctx := context.Background()
st := state.NewState(16)
st.UseDebug()
state.FlagDebugger.Register(models.USERFLAG_LANGUAGE_SET, "LANGUAGE_CHANGE")
state.FlagDebugger.Register(models.USERFLAG_ACCOUNT_CREATED, "ACCOUNT_CREATED")
state.FlagDebugger.Register(models.USERFLAG_ACCOUNT_SUCCESS, "ACCOUNT_SUCCESS")
state.FlagDebugger.Register(models.USERFLAG_ACCOUNT_PENDING, "ACCOUNT_PENDING")
state.FlagDebugger.Register(models.USERFLAG_INCORRECTPIN, "INCORRECTPIN")
state.FlagDebugger.Register(models.USERFLAG_INCORRECTDATEFORMAT, "INVALIDDATEFORMAT")
state.FlagDebugger.Register(models.USERFLAG_INVALID_RECIPIENT, "INVALIDRECIPIENT")
state.FlagDebugger.Register(models.USERFLAG_PINMISMATCH, "PINMISMATCH")
state.FlagDebugger.Register(models.USERFLAG_PIN_SET, "PIN_SET")
state.FlagDebugger.Register(models.USERFLAG_INVALID_RECIPIENT_WITH_INVITE, "INVALIDRECIPIENT_WITH_INVITE")
state.FlagDebugger.Register(models.USERFLAG_INVALID_AMOUNT, "INVALIDAMOUNT")
state.FlagDebugger.Register(models.USERFLAG_ALLOW_UPDATE, "UNLOCKFORUPDATE")
state.FlagDebugger.Register(models.USERFLAG_VALIDPIN, "VALIDPIN")
state.FlagDebugger.Register(models.USERFLAG_VALIDPIN, "ACCOUNTUNLOCKED")
state.FlagDebugger.Register(models.USERFLAG_ACCOUNT_CREATION_FAILED, "ACCOUNT_CREATION_FAILED")
state.FlagDebugger.Register(models.USERFLAG_SINGLE_EDIT, "SINGLEEDIT")
ctx = context.WithValue(ctx, "SessionId", sessionId)
pfp := path.Join(scriptDir, "pp.csv")
rfs := resource.NewFsResource(scriptDir)
ca := cache.NewCache()
cfg := engine.Config{
Root: "root",
SessionId: sessionId,
Root: "root",
SessionId: sessionId,
OutputSize: uint32(size),
FlagCount: uint32(128),
}
dp := path.Join(scriptDir, ".state")
err := os.MkdirAll(dp, 0700)
resourceDir := scriptDir
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
err := menuStorageService.EnsureDbDir()
if err != nil {
fmt.Fprintf(os.Stderr, "state dir create exited with error: %v\n", err)
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
pr := persist.NewFsPersister(dp)
en, err := engine.NewPersistedEngine(ctx, cfg, pr, rfs)
rs, err := menuStorageService.GetResource(ctx)
if err != nil {
pr = pr.WithContent(&st, ca)
err = pr.Save(cfg.SessionId)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to save state with error: %v\n", err)
}
en, err = engine.NewPersistedEngine(ctx, cfg, pr, rfs)
if err != nil {
fmt.Fprintf(os.Stderr, "engine create exited with error: %v\n", err)
os.Exit(1)
}
}
fp := path.Join(dp, sessionId)
ussdHandlers := ussd.NewHandlers(fp, &st)
rfs.AddLocalFunc("select_language", ussdHandlers.SetLanguage)
rfs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
rfs.AddLocalFunc("save_pin", ussdHandlers.SavePin)
rfs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin)
rfs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier)
rfs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus)
rfs.AddLocalFunc("authorize_account", ussdHandlers.Authorize)
rfs.AddLocalFunc("quit", ussdHandlers.Quit)
rfs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance)
rfs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient)
rfs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset)
rfs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount)
rfs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount)
rfs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount)
rfs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient)
rfs.AddLocalFunc("get_sender", ussdHandlers.GetSender)
rfs.AddLocalFunc("get_amount", ussdHandlers.GetAmount)
rfs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin)
rfs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname)
rfs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname)
rfs.AddLocalFunc("save_gender", ussdHandlers.SaveGender)
rfs.AddLocalFunc("save_location", ussdHandlers.SaveLocation)
rfs.AddLocalFunc("save_yob", ussdHandlers.SaveYob)
rfs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings)
rfs.AddLocalFunc("quit_with_balance", ussdHandlers.QuitWithBalance)
rfs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized)
rfs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate)
rfs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo)
rfs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob)
rfs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob)
rfs.AddLocalFunc("set_reset_single_edit", ussdHandlers.SetResetSingleEdit)
rfs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction)
cont, err := en.Init(ctx)
en.SetDebugger(engine.NewSimpleDebug(nil))
if err != nil {
fmt.Fprintf(os.Stderr, "engine init exited with error: %v\n", err)
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
if !cont {
_, err = en.WriteResult(ctx, os.Stdout)
if err != nil {
fmt.Fprintf(os.Stderr, "dead init write error: %v\n", err)
os.Exit(1)
}
err = en.Finish()
if err != nil {
fmt.Fprintf(os.Stderr, "engine finish error: %v\n", err)
os.Exit(1)
}
os.Stdout.Write([]byte{0x0a})
os.Exit(0)
pe, err := menuStorageService.GetPersister(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
err = engine.Loop(ctx, en, os.Stdin, os.Stdout)
userdatastore, err := menuStorageService.GetUserdataDb(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
dbResource, ok := rs.(*resource.DbResource)
if !ok {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
lhs, err := handlers.NewLocalHandlerService(pfp, true, dbResource, cfg, rs)
lhs.SetDataStore(&userdatastore)
lhs.SetPersister(pe)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
accountService := server.AccountService{}
hl, err := lhs.GetHandler(&accountService)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
en := lhs.GetEngine()
en = en.WithFirst(hl.Init)
if engineDebug {
en = en.WithDebug(nil)
}
err = engine.Loop(ctx, en, os.Stdin, os.Stdout, nil)
if err != nil {
fmt.Fprintf(os.Stderr, "loop exited with error: %v\n", err)
os.Exit(1)

111
driver/groupdriver.go Normal file
View File

@@ -0,0 +1,111 @@
package driver
import (
"encoding/json"
"log"
"os"
"regexp"
)
type Step struct {
Input string `json:"input"`
ExpectedContent string `json:"expectedContent"`
}
func (s *Step) MatchesExpectedContent(content []byte) (bool, error) {
pattern := regexp.QuoteMeta(s.ExpectedContent)
re, err := regexp.Compile(pattern)
if err != nil {
return false, err
}
if re.Match([]byte(content)) {
return true, nil
}
return false, nil
}
// Group represents a group of steps
type Group struct {
Name string `json:"name"`
Steps []Step `json:"steps"`
}
type TestCase struct {
Name string
Input string
ExpectedContent string
}
func (s *TestCase) MatchesExpectedContent(content []byte) (bool, error) {
pattern := regexp.QuoteMeta(s.ExpectedContent)
re, err := regexp.Compile(pattern)
if err != nil {
return false, err
}
// Check if the content matches the regex pattern
if re.Match(content) {
return true, nil
}
return false, nil
}
// DataGroup represents the overall structure of the JSON.
type DataGroup struct {
Groups []Group `json:"groups"`
}
type Session struct {
Name string `json:"name"`
Groups []Group `json:"groups"`
}
func ReadData() []Session {
data, err := os.ReadFile("test_setup.json")
if err != nil {
log.Fatalf("Failed to read file: %v", err)
}
// Unmarshal JSON data
var sessions []Session
err = json.Unmarshal(data, &sessions)
if err != nil {
log.Fatalf("Failed to unmarshal JSON: %v", err)
}
return sessions
}
func FilterGroupsByName(groups []Group, name string) []Group {
var filteredGroups []Group
for _, group := range groups {
if group.Name == name {
filteredGroups = append(filteredGroups, group)
}
}
return filteredGroups
}
func LoadTestGroups(filePath string) (DataGroup, error) {
var sessionsData DataGroup
data, err := os.ReadFile(filePath)
if err != nil {
return sessionsData, err
}
err = json.Unmarshal(data, &sessionsData)
return sessionsData, err
}
func CreateTestCases(group DataGroup) []TestCase {
var tests []TestCase
for _, group := range group.Groups {
for _, step := range group.Steps {
// Create a test case for each group
tests = append(tests, TestCase{
Name: group.Name,
Input: step.Input,
ExpectedContent: step.ExpectedContent,
})
}
}
return tests
}

Submodule go-vise deleted from 1f47a674d9

24
go.mod
View File

@@ -2,4 +2,26 @@ module git.grassecon.net/urdt/ussd
go 1.22.6
require github.com/stretchr/testify v1.9.0 // indirect
require (
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb
github.com/alecthomas/assert/v2 v2.2.2
github.com/peteole/testdata-loader v0.3.0
gopkg.in/leonelquinteros/gotext.v1 v1.3.1
)
require (
github.com/alecthomas/participle/v2 v2.0.0 // indirect
github.com/alecthomas/repr v0.2.0 // indirect
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
github.com/gofrs/uuid v4.4.0+incompatible
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0
github.com/x448/float16 v0.8.4 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

36
go.sum
View File

@@ -1,2 +1,38 @@
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb h1:6P4kxihcwMjDKzvUFC6t2zGNb7MDW+l/ACGlSAN1N8Y=
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M=
github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk=
github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g=
github.com/alecthomas/participle/v2 v2.0.0/go.mod h1:rAKZdJldHu8084ojcWevWAL8KmEU+AT+Olodb+WoN2Y=
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c h1:H9Nm+I7Cg/YVPpEV1RzU3Wq2pjamPc/UtHDgItcb7lE=
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c/go.mod h1:rGod7o6KPeJ+hyBpHfhi4v7blx9sf+QsHsA7KAsdN6U=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo=
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a h1:0Q3H0YXzMHiciXtRcM+j0jiCe8WKPQHoRgQiRTnfcLY=
github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a/go.mod h1:CdTTBOYzS5E4mWS1N8NWP6AHI19MP0A2B18n3hLzRMk=
github.com/peteole/testdata-loader v0.3.0 h1:8jckE9KcyNHgyv/VPoaljvKZE0Rqr8+dPVYH6rfNr9I=
github.com/peteole/testdata-loader v0.3.0/go.mod h1:Mt0ZbRtb56u8SLJpNP+BnQbENljMorYBpqlvt3cS83U=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/leonelquinteros/gotext.v1 v1.3.1 h1:8d9/fdTG0kn/B7NNGV1BsEyvektXFAbkMsTZS2sFSCc=
gopkg.in/leonelquinteros/gotext.v1 v1.3.1/go.mod h1:X1WlGDeAFIYsW6GjgMm4VwUwZ2XjI7Zan2InxSUQWrU=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

105
internal/handlers/base.go Normal file
View File

@@ -0,0 +1,105 @@
package handlers
import (
"git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/internal/handlers/ussd"
"git.grassecon.net/urdt/ussd/internal/storage"
)
type BaseSessionHandler struct {
cfgTemplate engine.Config
rp RequestParser
rs resource.Resource
hn *ussd.Handlers
provider storage.StorageProvider
}
func NewBaseSessionHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp RequestParser, hn *ussd.Handlers) *BaseSessionHandler {
return &BaseSessionHandler{
cfgTemplate: cfg,
rs: rs,
hn: hn,
rp: rp,
provider: storage.NewSimpleStorageProvider(stateDb, userdataDb),
}
}
func(f* BaseSessionHandler) Shutdown() {
err := f.provider.Close()
if err != nil {
logg.Errorf("handler shutdown error", "err", err)
}
}
func(f *BaseSessionHandler) GetEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) engine.Engine {
en := engine.NewEngine(cfg, rs)
en = en.WithPersister(pr)
return en
}
func(f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error) {
var r bool
var err error
var ok bool
logg.InfoCtxf(rqs.Ctx, "new request", "data", rqs)
rqs.Storage, err = f.provider.Get(rqs.Config.SessionId)
if err != nil {
logg.ErrorCtxf(rqs.Ctx, "", "storage get error", err)
return rqs, ErrStorage
}
f.hn = f.hn.WithPersister(rqs.Storage.Persister)
eni := f.GetEngine(rqs.Config, f.rs, rqs.Storage.Persister)
en, ok := eni.(*engine.DefaultEngine)
if !ok {
perr := f.provider.Put(rqs.Config.SessionId, rqs.Storage)
rqs.Storage = nil
if perr != nil {
logg.ErrorCtxf(rqs.Ctx, "", "storage put error", perr)
}
return rqs, ErrEngineType
}
en = en.WithFirst(f.hn.Init)
if rqs.Config.EngineDebug {
en = en.WithDebug(nil)
}
rqs.Engine = en
r, err = rqs.Engine.Exec(rqs.Ctx, rqs.Input)
if err != nil {
perr := f.provider.Put(rqs.Config.SessionId, rqs.Storage)
rqs.Storage = nil
if perr != nil {
logg.ErrorCtxf(rqs.Ctx, "", "storage put error", perr)
}
return rqs, err
}
rqs.Continue = r
return rqs, nil
}
func(f *BaseSessionHandler) Output(rqs RequestSession) (RequestSession, error) {
var err error
_, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer)
return rqs, err
}
func(f *BaseSessionHandler) Reset(rqs RequestSession) (RequestSession, error) {
defer f.provider.Put(rqs.Config.SessionId, rqs.Storage)
return rqs, rqs.Engine.Finish()
}
func(f *BaseSessionHandler) GetConfig() engine.Config {
return f.cfgTemplate
}
func(f *BaseSessionHandler) GetRequestParser() RequestParser {
return f.rp
}

View File

@@ -0,0 +1,106 @@
package handlers
import (
"git.defalsify.org/vise.git/asm"
"git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/internal/handlers/server"
"git.grassecon.net/urdt/ussd/internal/handlers/ussd"
)
type HandlerService interface {
GetHandler() (*ussd.Handlers, error)
}
func getParser(fp string, debug bool) (*asm.FlagParser, error) {
flagParser := asm.NewFlagParser().WithDebug()
_, err := flagParser.Load(fp)
if err != nil {
return nil, err
}
return flagParser, nil
}
type LocalHandlerService struct {
Parser *asm.FlagParser
DbRs *resource.DbResource
Pe *persist.Persister
UserdataStore *db.Db
Cfg engine.Config
Rs resource.Resource
}
func NewLocalHandlerService(fp string, debug bool, dbResource *resource.DbResource, cfg engine.Config, rs resource.Resource) (*LocalHandlerService, error) {
parser, err := getParser(fp, debug)
if err != nil {
return nil, err
}
return &LocalHandlerService{
Parser: parser,
DbRs: dbResource,
Cfg: cfg,
Rs: rs,
}, nil
}
func (ls *LocalHandlerService) SetPersister(Pe *persist.Persister) {
ls.Pe = Pe
}
func (ls *LocalHandlerService) SetDataStore(db *db.Db) {
ls.UserdataStore = db
}
func (ls *LocalHandlerService) GetHandler(accountService server.AccountServiceInterface) (*ussd.Handlers, error) {
ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore,accountService)
if err != nil {
return nil, err
}
ussdHandlers = ussdHandlers.WithPersister(ls.Pe)
ls.DbRs.AddLocalFunc("set_language", ussdHandlers.SetLanguage)
ls.DbRs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
ls.DbRs.AddLocalFunc("save_pin", ussdHandlers.SavePin)
ls.DbRs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin)
ls.DbRs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier)
ls.DbRs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus)
ls.DbRs.AddLocalFunc("authorize_account", ussdHandlers.Authorize)
ls.DbRs.AddLocalFunc("quit", ussdHandlers.Quit)
ls.DbRs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance)
ls.DbRs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient)
ls.DbRs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset)
ls.DbRs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount)
ls.DbRs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount)
ls.DbRs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount)
ls.DbRs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient)
ls.DbRs.AddLocalFunc("get_sender", ussdHandlers.GetSender)
ls.DbRs.AddLocalFunc("get_amount", ussdHandlers.GetAmount)
ls.DbRs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin)
ls.DbRs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname)
ls.DbRs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname)
ls.DbRs.AddLocalFunc("save_gender", ussdHandlers.SaveGender)
ls.DbRs.AddLocalFunc("save_location", ussdHandlers.SaveLocation)
ls.DbRs.AddLocalFunc("save_yob", ussdHandlers.SaveYob)
ls.DbRs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings)
ls.DbRs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized)
ls.DbRs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate)
ls.DbRs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo)
ls.DbRs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob)
ls.DbRs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob)
ls.DbRs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction)
ls.DbRs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin)
ls.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin)
ls.DbRs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange)
ls.DbRs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp)
ls.DbRs.AddLocalFunc("fetch_custodial_balances", ussdHandlers.FetchCustodialBalances)
return ussdHandlers, nil
}
// TODO: enable setting of sessionId on engine init time
func (ls *LocalHandlerService) GetEngine() *engine.DefaultEngine {
en := engine.NewEngine(ls.Cfg, ls.Rs)
en = en.WithPersister(ls.Pe)
return en
}

View File

@@ -4,21 +4,23 @@ import (
"encoding/json"
"io"
"net/http"
"time"
"git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/internal/models"
)
type AccountServiceInterface interface {
CheckBalance(publicKey string) (string, error)
CheckBalance(publicKey string) (*models.BalanceResponse, error)
CreateAccount() (*models.AccountResponse, error)
CheckAccountStatus(trackingId string) (string, error)
CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error)
}
type AccountService struct {
}
type TestAccountService struct {
}
// CheckAccountStatus retrieves the status of an account transaction based on the provided tracking ID.
//
@@ -27,64 +29,51 @@ type AccountService struct {
// CreateAccount or a similar function that returns an AccountResponse. The `trackingId` field in the
// AccountResponse struct can be used here to check the account status during a transaction.
//
//
// Returns:
// - string: The status of the transaction as a string. If there is an error during the request or processing, this will be an empty string.
// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
// If no error occurs, this will be nil.
//
func (as *AccountService) CheckAccountStatus(trackingId string) (string, error) {
func (as *AccountService) CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error) {
resp, err := http.Get(config.TrackStatusURL + trackingId)
if err != nil {
return "", err
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
return nil, err
}
var trackResp models.TrackStatusResponse
err = json.Unmarshal(body, &trackResp)
if err != nil {
return "", err
return nil, err
}
status := trackResp.Result.Transaction.Status
return status, nil
return &trackResp, nil
}
// CheckBalance retrieves the balance for a given public key from the custodial balance API endpoint.
// Parameters:
// - publicKey: The public key associated with the account whose balance needs to be checked.
func (as *AccountService) CheckBalance(publicKey string) (string, error) {
func (as *AccountService) CheckBalance(publicKey string) (*models.BalanceResponse, error) {
resp, err := http.Get(config.BalanceURL + publicKey)
if err != nil {
return "0.0", err
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "0.0", err
return nil, err
}
var balanceResp models.BalanceResponse
err = json.Unmarshal(body, &balanceResp)
if err != nil {
return "0.0", err
return nil, err
}
balance := balanceResp.Result.Balance
return balance, nil
return &balanceResp, nil
}
//CreateAccount creates a new account in the custodial system.
// CreateAccount creates a new account in the custodial system.
// Returns:
// - *models.AccountResponse: A pointer to an AccountResponse struct containing the details of the created account.
// If there is an error during the request or processing, this will be nil.
@@ -96,17 +85,69 @@ func (as *AccountService) CreateAccount() (*models.AccountResponse, error) {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var accountResp models.AccountResponse
err = json.Unmarshal(body, &accountResp)
if err != nil {
return nil, err
}
return &accountResp, nil
}
func (tas *TestAccountService) CreateAccount() (*models.AccountResponse, error) {
return &models.AccountResponse{
Ok: true,
Result: struct {
CustodialId json.Number `json:"custodialId"`
PublicKey string `json:"publicKey"`
TrackingId string `json:"trackingId"`
}{
CustodialId: json.Number("182"),
PublicKey: "0x48ADca309b5085852207FAaf2816eD72B52F527C",
TrackingId: "28ebe84d-b925-472c-87ae-bbdfa1fb97be",
},
}, nil
}
func (tas *TestAccountService) CheckBalance(publicKey string) (*models.BalanceResponse, error) {
balanceResponse := &models.BalanceResponse{
Ok: true,
Result: struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
}
return balanceResponse, nil
}
func (tas *TestAccountService) CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error) {
trackResponse := &models.TrackStatusResponse{
Ok: true,
Result: struct {
Transaction struct {
CreatedAt time.Time "json:\"createdAt\""
Status string "json:\"status\""
TransferValue json.Number "json:\"transferValue\""
TxHash string "json:\"txHash\""
TxType string "json:\"txType\""
}
}{
Transaction: models.Transaction{
CreatedAt: time.Now(),
Status: "SUCCESS",
TransferValue: json.Number("0.5"),
TxHash: "0x123abc456def",
TxType: "transfer",
},
},
}
return trackResponse, nil
}

View File

@@ -0,0 +1,54 @@
package handlers
import (
"context"
"errors"
"io"
"git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/resource"
"git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/logging"
"git.grassecon.net/urdt/ussd/internal/storage"
)
var (
logg = logging.NewVanilla().WithDomain("handlers")
)
var (
ErrInvalidRequest = errors.New("invalid request for context")
ErrSessionMissing = errors.New("missing session")
ErrInvalidInput = errors.New("invalid input")
ErrStorage = errors.New("storage retrieval fail")
ErrEngineType = errors.New("incompatible engine")
ErrEngineInit = errors.New("engine init fail")
ErrEngineExec = errors.New("engine exec fail")
)
type RequestSession struct {
Ctx context.Context
Config engine.Config
Engine engine.Engine
Input []byte
Storage *storage.Storage
Writer io.Writer
Continue bool
}
// TODO: seems like can remove this.
type RequestParser interface {
GetSessionId(rq any) (string, error)
GetInput(rq any) ([]byte, error)
}
type RequestHandler interface {
GetConfig() engine.Config
GetRequestParser() RequestParser
GetEngine(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine
Process(rs RequestSession) (RequestSession, error)
Output(rs RequestSession) (RequestSession, error)
Reset(rs RequestSession) (RequestSession, error)
Shutdown()
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,44 +0,0 @@
package mocks
import (
"git.grassecon.net/urdt/ussd/internal/models"
"github.com/stretchr/testify/mock"
)
type MockAccountFileHandler struct {
mock.Mock
}
func (m *MockAccountFileHandler) EnsureFileExists() error {
args := m.Called()
return args.Error(0)
}
func (m *MockAccountFileHandler) ReadAccountData() (map[string]string, error) {
args := m.Called()
return args.Get(0).(map[string]string), args.Error(1)
}
func (m *MockAccountFileHandler) WriteAccountData(data map[string]string) error {
args := m.Called(data)
return args.Error(0)
}
type MockAccountService struct {
mock.Mock
}
func (m *MockAccountService) CreateAccount() (*models.AccountResponse, error) {
args := m.Called()
return args.Get(0).(*models.AccountResponse), args.Error(1)
}
func (m *MockAccountService) CheckAccountStatus(TrackingId string) (string, error) {
args := m.Called()
return args.Get(0).(string), args.Error(1)
}
func (m *MockAccountService) CheckBalance(PublicKey string) (string, error) {
args := m.Called()
return args.Get(0).(string), args.Error(1)
}

View File

@@ -0,0 +1,92 @@
package http
import (
"io"
"net/http"
"git.grassecon.net/urdt/ussd/internal/handlers"
)
type ATSessionHandler struct {
*SessionHandler
}
func NewATSessionHandler(h handlers.RequestHandler) *ATSessionHandler {
return &ATSessionHandler{
SessionHandler: ToSessionHandler(h),
}
}
func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
var code int
var err error
rqs := handlers.RequestSession{
Ctx: req.Context(),
Writer: w,
}
rp := ash.GetRequestParser()
cfg := ash.GetConfig()
cfg.SessionId, err = rp.GetSessionId(req)
if err != nil {
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
ash.writeError(w, 400, err)
return
}
rqs.Config = cfg
rqs.Input, err = rp.GetInput(req)
if err != nil {
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
ash.writeError(w, 400, err)
return
}
rqs, err = ash.Process(rqs)
switch err {
case nil: // set code to 200 if no err
code = 200
case handlers.ErrStorage, handlers.ErrEngineInit, handlers.ErrEngineExec, handlers.ErrEngineType:
code = 500
default:
code = 500
}
if code != 200 {
ash.writeError(w, 500, err)
return
}
w.WriteHeader(200)
w.Header().Set("Content-Type", "text/plain")
rqs, err = ash.Output(rqs)
if err != nil {
ash.writeError(w, 500, err)
return
}
rqs, err = ash.Reset(rqs)
if err != nil {
ash.writeError(w, 500, err)
return
}
}
func (ash *ATSessionHandler) Output(rqs handlers.RequestSession) (handlers.RequestSession, error) {
var err error
var prefix string
if rqs.Continue {
prefix = "CON "
} else {
prefix = "END "
}
_, err = io.WriteString(rqs.Writer, prefix)
if err != nil {
return rqs, err
}
_, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer)
return rqs, err
}

449
internal/http/http_test.go Normal file
View File

@@ -0,0 +1,449 @@
package http
import (
"bytes"
"context"
"errors"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"git.defalsify.org/vise.git/engine"
"git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/mocks/httpmocks"
)
// invalidRequestType is a custom type to test invalid request scenarios
type invalidRequestType struct{}
// errorReader is a helper type that always returns an error when Read is called
type errorReader struct{}
func (e *errorReader) Read(p []byte) (n int, err error) {
return 0, errors.New("read error")
}
func TestNewATSessionHandler(t *testing.T) {
mockHandler := &httpmocks.MockRequestHandler{}
ash := NewATSessionHandler(mockHandler)
if ash == nil {
t.Fatal("NewATSessionHandler returned nil")
}
if ash.SessionHandler == nil {
t.Fatal("SessionHandler is nil")
}
}
func TestATSessionHandler_ServeHTTP(t *testing.T) {
tests := []struct {
name string
setupMocks func(*httpmocks.MockRequestHandler, *httpmocks.MockRequestParser, *httpmocks.MockEngine)
formData url.Values
expectedStatus int
expectedBody string
}{
{
name: "Successful request",
setupMocks: func(mh *httpmocks.MockRequestHandler, mrp *httpmocks.MockRequestParser, me *httpmocks.MockEngine) {
mrp.GetSessionIdFunc = func(rq any) (string, error) {
req := rq.(*http.Request)
return req.FormValue("phoneNumber"), nil
}
mrp.GetInputFunc = func(rq any) ([]byte, error) {
req := rq.(*http.Request)
text := req.FormValue("text")
parts := strings.Split(text, "*")
return []byte(parts[len(parts)-1]), nil
}
mh.ProcessFunc = func(rqs handlers.RequestSession) (handlers.RequestSession, error) {
rqs.Continue = true
rqs.Engine = me
return rqs, nil
}
mh.GetConfigFunc = func() engine.Config { return engine.Config{} }
mh.GetRequestParserFunc = func() handlers.RequestParser { return mrp }
mh.OutputFunc = func(rs handlers.RequestSession) (handlers.RequestSession, error) { return rs, nil }
mh.ResetFunc = func(rs handlers.RequestSession) (handlers.RequestSession, error) { return rs, nil }
me.FlushFunc = func(context.Context, io.Writer) (int, error) { return 0, nil }
},
formData: url.Values{
"phoneNumber": []string{"+1234567890"},
"text": []string{"1*2*3"},
},
expectedStatus: http.StatusOK,
expectedBody: "CON ",
},
{
name: "GetSessionId error",
setupMocks: func(mh *httpmocks.MockRequestHandler, mrp *httpmocks.MockRequestParser, me *httpmocks.MockEngine) {
mrp.GetSessionIdFunc = func(rq any) (string, error) {
return "", errors.New("no phone number found")
}
mh.GetConfigFunc = func() engine.Config { return engine.Config{} }
mh.GetRequestParserFunc = func() handlers.RequestParser { return mrp }
},
formData: url.Values{
"text": []string{"1*2*3"},
},
expectedStatus: http.StatusBadRequest,
expectedBody: "",
},
{
name: "GetInput error",
setupMocks: func(mh *httpmocks.MockRequestHandler, mrp *httpmocks.MockRequestParser, me *httpmocks.MockEngine) {
mrp.GetSessionIdFunc = func(rq any) (string, error) {
req := rq.(*http.Request)
return req.FormValue("phoneNumber"), nil
}
mrp.GetInputFunc = func(rq any) ([]byte, error) {
return nil, errors.New("no input found")
}
mh.GetConfigFunc = func() engine.Config { return engine.Config{} }
mh.GetRequestParserFunc = func() handlers.RequestParser { return mrp }
},
formData: url.Values{
"phoneNumber": []string{"+1234567890"},
},
expectedStatus: http.StatusBadRequest,
expectedBody: "",
},
{
name: "Process error",
setupMocks: func(mh *httpmocks.MockRequestHandler, mrp *httpmocks.MockRequestParser, me *httpmocks.MockEngine) {
mrp.GetSessionIdFunc = func(rq any) (string, error) {
req := rq.(*http.Request)
return req.FormValue("phoneNumber"), nil
}
mrp.GetInputFunc = func(rq any) ([]byte, error) {
req := rq.(*http.Request)
text := req.FormValue("text")
parts := strings.Split(text, "*")
return []byte(parts[len(parts)-1]), nil
}
mh.ProcessFunc = func(rqs handlers.RequestSession) (handlers.RequestSession, error) {
return rqs, handlers.ErrStorage
}
mh.GetConfigFunc = func() engine.Config { return engine.Config{} }
mh.GetRequestParserFunc = func() handlers.RequestParser { return mrp }
},
formData: url.Values{
"phoneNumber": []string{"+1234567890"},
"text": []string{"1*2*3"},
},
expectedStatus: http.StatusInternalServerError,
expectedBody: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockHandler := &httpmocks.MockRequestHandler{}
mockRequestParser := &httpmocks.MockRequestParser{}
mockEngine := &httpmocks.MockEngine{}
tt.setupMocks(mockHandler, mockRequestParser, mockEngine)
ash := NewATSessionHandler(mockHandler)
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tt.formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
ash.ServeHTTP(w, req)
if w.Code != tt.expectedStatus {
t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code)
}
if tt.expectedBody != "" && w.Body.String() != tt.expectedBody {
t.Errorf("Expected body %q, got %q", tt.expectedBody, w.Body.String())
}
})
}
}
func TestATSessionHandler_Output(t *testing.T) {
tests := []struct {
name string
input handlers.RequestSession
expectedPrefix string
expectedError bool
}{
{
name: "Continue true",
input: handlers.RequestSession{
Continue: true,
Engine: &httpmocks.MockEngine{
FlushFunc: func(context.Context, io.Writer) (int, error) {
return 0, nil
},
},
Writer: &httpmocks.MockWriter{},
},
expectedPrefix: "CON ",
expectedError: false,
},
{
name: "Continue false",
input: handlers.RequestSession{
Continue: false,
Engine: &httpmocks.MockEngine{
FlushFunc: func(context.Context, io.Writer) (int, error) {
return 0, nil
},
},
Writer: &httpmocks.MockWriter{},
},
expectedPrefix: "END ",
expectedError: false,
},
{
name: "Flush error",
input: handlers.RequestSession{
Continue: true,
Engine: &httpmocks.MockEngine{
FlushFunc: func(context.Context, io.Writer) (int, error) {
return 0, errors.New("write error")
},
},
Writer: &httpmocks.MockWriter{},
},
expectedPrefix: "CON ",
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ash := &ATSessionHandler{}
_, err := ash.Output(tt.input)
if tt.expectedError && err == nil {
t.Error("Expected an error, but got nil")
}
if !tt.expectedError && err != nil {
t.Errorf("Unexpected error: %v", err)
}
mw := tt.input.Writer.(*httpmocks.MockWriter)
if !mw.WriteStringCalled {
t.Error("WriteString was not called")
}
if mw.WrittenString != tt.expectedPrefix {
t.Errorf("Expected prefix %q, got %q", tt.expectedPrefix, mw.WrittenString)
}
})
}
}
func TestSessionHandler_ServeHTTP(t *testing.T) {
tests := []struct {
name string
sessionID string
input []byte
parserErr error
processErr error
outputErr error
resetErr error
expectedStatus int
}{
{
name: "Success",
sessionID: "123",
input: []byte("test input"),
expectedStatus: http.StatusOK,
},
{
name: "Missing Session ID",
sessionID: "",
parserErr: handlers.ErrSessionMissing,
expectedStatus: http.StatusBadRequest,
},
{
name: "Process Error",
sessionID: "123",
input: []byte("test input"),
processErr: handlers.ErrStorage,
expectedStatus: http.StatusInternalServerError,
},
{
name: "Output Error",
sessionID: "123",
input: []byte("test input"),
outputErr: errors.New("output error"),
expectedStatus: http.StatusOK,
},
{
name: "Reset Error",
sessionID: "123",
input: []byte("test input"),
resetErr: errors.New("reset error"),
expectedStatus: http.StatusOK,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRequestParser := &httpmocks.MockRequestParser{
GetSessionIdFunc: func(any) (string, error) {
return tt.sessionID, tt.parserErr
},
GetInputFunc: func(any) ([]byte, error) {
return tt.input, nil
},
}
mockRequestHandler := &httpmocks.MockRequestHandler{
ProcessFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
return rs, tt.processErr
},
OutputFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
return rs, tt.outputErr
},
ResetFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
return rs, tt.resetErr
},
GetRequestParserFunc: func() handlers.RequestParser {
return mockRequestParser
},
GetConfigFunc: func() engine.Config {
return engine.Config{}
},
}
sessionHandler := ToSessionHandler(mockRequestHandler)
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.input))
req.Header.Set("X-Vise-Session", tt.sessionID)
rr := httptest.NewRecorder()
sessionHandler.ServeHTTP(rr, req)
if status := rr.Code; status != tt.expectedStatus {
t.Errorf("handler returned wrong status code: got %v want %v",
status, tt.expectedStatus)
}
})
}
}
func TestSessionHandler_writeError(t *testing.T) {
handler := &SessionHandler{}
mockWriter := &httpmocks.MockWriter{}
err := errors.New("test error")
handler.writeError(mockWriter, http.StatusBadRequest, err)
if mockWriter.WrittenString != "" {
t.Errorf("Expected empty body, got %s", mockWriter.WrittenString)
}
}
func TestDefaultRequestParser_GetSessionId(t *testing.T) {
tests := []struct {
name string
request any
expectedID string
expectedError error
}{
{
name: "Valid Session ID",
request: func() *http.Request {
req := httptest.NewRequest(http.MethodPost, "/", nil)
req.Header.Set("X-Vise-Session", "123456")
return req
}(),
expectedID: "123456",
expectedError: nil,
},
{
name: "Missing Session ID",
request: httptest.NewRequest(http.MethodPost, "/", nil),
expectedID: "",
expectedError: handlers.ErrSessionMissing,
},
{
name: "Invalid Request Type",
request: invalidRequestType{},
expectedID: "",
expectedError: handlers.ErrInvalidRequest,
},
}
parser := &DefaultRequestParser{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
id, err := parser.GetSessionId(tt.request)
if id != tt.expectedID {
t.Errorf("Expected session ID %s, got %s", tt.expectedID, id)
}
if err != tt.expectedError {
t.Errorf("Expected error %v, got %v", tt.expectedError, err)
}
})
}
}
func TestDefaultRequestParser_GetInput(t *testing.T) {
tests := []struct {
name string
request any
expectedInput []byte
expectedError error
}{
{
name: "Valid Input",
request: func() *http.Request {
return httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString("test input"))
}(),
expectedInput: []byte("test input"),
expectedError: nil,
},
{
name: "Empty Input",
request: httptest.NewRequest(http.MethodPost, "/", nil),
expectedInput: []byte{},
expectedError: nil,
},
{
name: "Invalid Request Type",
request: invalidRequestType{},
expectedInput: nil,
expectedError: handlers.ErrInvalidRequest,
},
{
name: "Read Error",
request: func() *http.Request {
return httptest.NewRequest(http.MethodPost, "/", &errorReader{})
}(),
expectedInput: nil,
expectedError: errors.New("read error"),
},
}
parser := &DefaultRequestParser{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
input, err := parser.GetInput(tt.request)
if !bytes.Equal(input, tt.expectedInput) {
t.Errorf("Expected input %s, got %s", tt.expectedInput, input)
}
if err != tt.expectedError && (err == nil || err.Error() != tt.expectedError.Error()) {
t.Errorf("Expected error %v, got %v", tt.expectedError, err)
}
})
}
}

122
internal/http/server.go Normal file
View File

@@ -0,0 +1,122 @@
package http
import (
"io/ioutil"
"net/http"
"strconv"
"git.defalsify.org/vise.git/logging"
"git.grassecon.net/urdt/ussd/internal/handlers"
)
var (
logg = logging.NewVanilla().WithDomain("httpserver")
)
type DefaultRequestParser struct {
}
func(rp *DefaultRequestParser) GetSessionId(rq any) (string, error) {
rqv, ok := rq.(*http.Request)
if !ok {
return "", handlers.ErrInvalidRequest
}
v := rqv.Header.Get("X-Vise-Session")
if v == "" {
return "", handlers.ErrSessionMissing
}
return v, nil
}
func(rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) {
rqv, ok := rq.(*http.Request)
if !ok {
return nil, handlers.ErrInvalidRequest
}
defer rqv.Body.Close()
v, err := ioutil.ReadAll(rqv.Body)
if err != nil {
return nil, err
}
return v, nil
}
type SessionHandler struct {
handlers.RequestHandler
}
func ToSessionHandler(h handlers.RequestHandler) *SessionHandler {
return &SessionHandler{
RequestHandler: h,
}
}
func(f *SessionHandler) writeError(w http.ResponseWriter, code int, err error) {
s := err.Error()
w.Header().Set("Content-Length", strconv.Itoa(len(s)))
w.WriteHeader(code)
_, err = w.Write([]byte{})
if err != nil {
logg.Errorf("error writing error!!", "err", err, "olderr", s)
w.WriteHeader(500)
}
return
}
func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
var code int
var err error
var perr error
rqs := handlers.RequestSession{
Ctx: req.Context(),
Writer: w,
}
rp := f.GetRequestParser()
cfg := f.GetConfig()
cfg.SessionId, err = rp.GetSessionId(req)
if err != nil {
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
f.writeError(w, 400, err)
}
rqs.Config = cfg
rqs.Input, err = rp.GetInput(req)
if err != nil {
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
f.writeError(w, 400, err)
return
}
rqs, err = f.Process(rqs)
switch err {
case handlers.ErrStorage:
code = 500
case handlers.ErrEngineInit:
code = 500
case handlers.ErrEngineExec:
code = 500
default:
code = 200
}
if code != 200 {
f.writeError(w, 500, err)
return
}
w.WriteHeader(200)
w.Header().Set("Content-Type", "text/plain")
rqs, err = f.Output(rqs)
rqs, perr = f.Reset(rqs)
if err != nil {
f.writeError(w, 500, err)
return
}
if perr != nil {
f.writeError(w, 500, perr)
return
}
}

59
internal/mocks/dbmock.go Normal file
View File

@@ -0,0 +1,59 @@
package mocks
import (
"context"
"git.defalsify.org/vise.git/lang"
"github.com/stretchr/testify/mock"
)
type MockDb struct {
mock.Mock
}
func (m *MockDb) SetPrefix(prefix uint8) {
m.Called(prefix)
}
func (m *MockDb) Prefix() uint8 {
args := m.Called()
return args.Get(0).(uint8)
}
func (m *MockDb) Safe() bool {
args := m.Called()
return args.Get(0).(bool)
}
func (m *MockDb) SetLanguage(language *lang.Language) {
m.Called(language)
}
func (m *MockDb) SetLock(uint8, bool) error {
args := m.Called()
return args.Error(0)
}
func (m *MockDb) Connect(ctx context.Context, connectionStr string) error {
args := m.Called(ctx, connectionStr)
return args.Error(0)
}
func (m *MockDb) SetSession(sessionId string) {
m.Called(sessionId)
}
func (m *MockDb) Put(ctx context.Context, key, value []byte) error {
args := m.Called(ctx, key, value)
return args.Error(0)
}
func (m *MockDb) Get(ctx context.Context, key []byte) ([]byte, error) {
args := m.Called(ctx, key)
return nil, args.Error(0)
}
func (m *MockDb) Close() error {
args := m.Called(nil)
return args.Error(0)
}

View File

@@ -0,0 +1,30 @@
package httpmocks
import (
"context"
"io"
)
// MockEngine implements the engine.Engine interface for testing
type MockEngine struct {
InitFunc func(context.Context) (bool, error)
ExecFunc func(context.Context, []byte) (bool, error)
FlushFunc func(context.Context, io.Writer) (int, error)
FinishFunc func() error
}
func (m *MockEngine) Init(ctx context.Context) (bool, error) {
return m.InitFunc(ctx)
}
func (m *MockEngine) Exec(ctx context.Context, input []byte) (bool, error) {
return m.ExecFunc(ctx, input)
}
func (m *MockEngine) Flush(ctx context.Context, w io.Writer) (int, error) {
return m.FlushFunc(ctx, w)
}
func (m *MockEngine) Finish() error {
return m.FinishFunc()
}

View File

@@ -0,0 +1,47 @@
package httpmocks
import (
"git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/internal/handlers"
)
// MockRequestHandler implements handlers.RequestHandler interface for testing
type MockRequestHandler struct {
ProcessFunc func(handlers.RequestSession) (handlers.RequestSession, error)
GetConfigFunc func() engine.Config
GetEngineFunc func(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine
OutputFunc func(rs handlers.RequestSession) (handlers.RequestSession, error)
ResetFunc func(rs handlers.RequestSession) (handlers.RequestSession, error)
ShutdownFunc func()
GetRequestParserFunc func() handlers.RequestParser
}
func (m *MockRequestHandler) Process(rqs handlers.RequestSession) (handlers.RequestSession, error) {
return m.ProcessFunc(rqs)
}
func (m *MockRequestHandler) GetConfig() engine.Config {
return m.GetConfigFunc()
}
func (m *MockRequestHandler) GetEngine(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine {
return m.GetEngineFunc(cfg, rs, pe)
}
func (m *MockRequestHandler) Output(rs handlers.RequestSession) (handlers.RequestSession, error) {
return m.OutputFunc(rs)
}
func (m *MockRequestHandler) Reset(rs handlers.RequestSession) (handlers.RequestSession, error) {
return m.ResetFunc(rs)
}
func (m *MockRequestHandler) Shutdown() {
m.ShutdownFunc()
}
func (m *MockRequestHandler) GetRequestParser() handlers.RequestParser {
return m.GetRequestParserFunc()
}

View File

@@ -0,0 +1,15 @@
package httpmocks
// MockRequestParser implements the handlers.RequestParser interface for testing
type MockRequestParser struct {
GetSessionIdFunc func(any) (string, error)
GetInputFunc func(any) ([]byte, error)
}
func (m *MockRequestParser) GetSessionId(rq any) (string, error) {
return m.GetSessionIdFunc(rq)
}
func (m *MockRequestParser) GetInput(rq any) ([]byte, error) {
return m.GetInputFunc(rq)
}

View File

@@ -0,0 +1,25 @@
package httpmocks
import "net/http"
// MockWriter implements a mock io.Writer for testing
type MockWriter struct {
WriteStringCalled bool
WrittenString string
}
func (m *MockWriter) Write(p []byte) (n int, err error) {
return len(p), nil
}
func (m *MockWriter) WriteString(s string) (n int, err error) {
m.WriteStringCalled = true
m.WrittenString = s
return len(s), nil
}
func (m *MockWriter) Header() http.Header {
return http.Header{}
}
func (m *MockWriter) WriteHeader(statusCode int) {}

View File

@@ -0,0 +1,26 @@
package mocks
import (
"git.grassecon.net/urdt/ussd/internal/models"
"github.com/stretchr/testify/mock"
)
// MockAccountService implements AccountServiceInterface for testing
type MockAccountService struct {
mock.Mock
}
func (m *MockAccountService) CreateAccount() (*models.AccountResponse, error) {
args := m.Called()
return args.Get(0).(*models.AccountResponse), args.Error(1)
}
func (m *MockAccountService) CheckBalance(publicKey string) (*models.BalanceResponse, error) {
args := m.Called(publicKey)
return args.Get(0).(*models.BalanceResponse), args.Error(1)
}
func (m *MockAccountService) CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error) {
args := m.Called(trackingId)
return args.Get(0).(*models.TrackStatusResponse), args.Error(1)
}

View File

@@ -0,0 +1,24 @@
package mocks
import (
"context"
"git.defalsify.org/vise.git/db"
"git.grassecon.net/urdt/ussd/internal/utils"
"github.com/stretchr/testify/mock"
)
type MockUserDataStore struct {
db.Db
mock.Mock
}
func (m *MockUserDataStore) ReadEntry(ctx context.Context, sessionId string, typ utils.DataTyp) ([]byte, error) {
args := m.Called(ctx, sessionId, typ)
return args.Get(0).([]byte), args.Error(1)
}
func (m *MockUserDataStore) WriteEntry(ctx context.Context, sessionId string, typ utils.DataTyp, value []byte) error {
args := m.Called(ctx, sessionId, typ, value)
return args.Error(0)
}

View File

@@ -1,22 +0,0 @@
package models
import "git.defalsify.org/vise.git/state"
const (
USERFLAG_LANGUAGE_SET = iota + state.FLAG_USERSTART
USERFLAG_ACCOUNT_CREATED
USERFLAG_ACCOUNT_PENDING
USERFLAG_ACCOUNT_SUCCESS
USERFLAG_ACCOUNT_AUTHORIZED
USERFLAG_INVALID_RECIPIENT
USERFLAG_INVALID_RECIPIENT_WITH_INVITE
USERFLAG_INCORRECTPIN
USERFLAG_ALLOW_UPDATE
USERFLAG_INVALID_AMOUNT
USERFLAG_PIN_SET
USERFLAG_VALIDPIN
USERFLAG_PINMISMATCH
USERFLAG_INCORRECTDATEFORMAT
USERFLAG_ACCOUNT_CREATION_FAILED
USERFLAG_SINGLE_EDIT
)

View File

@@ -5,6 +5,13 @@ import (
"time"
)
type Transaction struct {
CreatedAt time.Time `json:"createdAt"`
Status string `json:"status"`
TransferValue json.Number `json:"transferValue"`
TxHash string `json:"txHash"`
TxType string `json:"txType"`
}
type TrackStatusResponse struct {
Ok bool `json:"ok"`
@@ -17,4 +24,4 @@ type TrackStatusResponse struct {
TxType string `json:"txType"`
}
} `json:"result"`
}
}

43
internal/storage/db.go Normal file
View File

@@ -0,0 +1,43 @@
package storage
import (
"context"
"git.defalsify.org/vise.git/db"
)
const (
DATATYPE_USERSUB = 64
)
type SubPrefixDb struct {
store db.Db
pfx []byte
}
func NewSubPrefixDb(store db.Db, pfx []byte) *SubPrefixDb {
return &SubPrefixDb{
store: store,
pfx: pfx,
}
}
func(s *SubPrefixDb) toKey(k []byte) []byte {
return append(s.pfx, k...)
}
func(s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) {
s.store.SetPrefix(DATATYPE_USERSUB)
key = s.toKey(key)
v, err := s.store.Get(ctx, key)
if err != nil {
return nil, err
}
return v, nil
}
func(s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error {
s.store.SetPrefix(DATATYPE_USERSUB)
key = s.toKey(key)
return s.store.Put(ctx, key, val)
}

View File

@@ -0,0 +1,54 @@
package storage
import (
"bytes"
"context"
"testing"
memdb "git.defalsify.org/vise.git/db/mem"
)
func TestSubPrefix(t *testing.T) {
ctx := context.Background()
db := memdb.NewMemDb()
err := db.Connect(ctx, "")
if err != nil {
t.Fatal(err)
}
sdba := NewSubPrefixDb(db, []byte("tinkywinky"))
err = sdba.Put(ctx, []byte("foo"), []byte("dipsy"))
if err != nil {
t.Fatal(err)
}
r, err := sdba.Get(ctx, []byte("foo"))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(r, []byte("dipsy")) {
t.Fatalf("expected 'dipsy', got %s", r)
}
sdbb := NewSubPrefixDb(db, []byte("lala"))
r, err = sdbb.Get(ctx, []byte("foo"))
if err == nil {
t.Fatal("expected not found")
}
err = sdbb.Put(ctx, []byte("foo"), []byte("pu"))
if err != nil {
t.Fatal(err)
}
r, err = sdbb.Get(ctx, []byte("foo"))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(r, []byte("pu")) {
t.Fatalf("expected 'pu', got %s", r)
}
r, err = sdba.Get(ctx, []byte("foo"))
if !bytes.Equal(r, []byte("dipsy")) {
t.Fatalf("expected 'dipsy', got %s", r)
}
}

116
internal/storage/gdbm.go Normal file
View File

@@ -0,0 +1,116 @@
package storage
import (
"context"
"git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/lang"
gdbmdb "git.defalsify.org/vise.git/db/gdbm"
)
var (
dbC map[string]chan db.Db
)
type ThreadGdbmDb struct {
db db.Db
connStr string
}
func NewThreadGdbmDb() *ThreadGdbmDb {
if dbC == nil {
dbC = make(map[string]chan db.Db)
}
return &ThreadGdbmDb{}
}
func(tdb *ThreadGdbmDb) Connect(ctx context.Context, connStr string) error {
var ok bool
_, ok = dbC[connStr]
if ok {
logg.WarnCtxf(ctx, "already registered thread gdbm, skipping", "connStr", connStr)
return nil
}
gdb := gdbmdb.NewGdbmDb()
err := gdb.Connect(ctx, connStr)
if err != nil {
return err
}
dbC[connStr] = make(chan db.Db, 1)
dbC[connStr]<- gdb
tdb.connStr = connStr
return nil
}
func(tdb *ThreadGdbmDb) reserve() {
if tdb.db == nil {
tdb.db = <-dbC[tdb.connStr]
}
}
func(tdb *ThreadGdbmDb) release() {
if tdb.db == nil {
return
}
dbC[tdb.connStr] <- tdb.db
tdb.db = nil
}
func(tdb *ThreadGdbmDb) SetPrefix(pfx uint8) {
tdb.reserve()
tdb.db.SetPrefix(pfx)
}
func(tdb *ThreadGdbmDb) SetSession(sessionId string) {
tdb.reserve()
tdb.db.SetSession(sessionId)
}
func(tdb *ThreadGdbmDb) SetLanguage(lng *lang.Language) {
tdb.reserve()
tdb.db.SetLanguage(lng)
}
func(tdb *ThreadGdbmDb) Safe() bool {
tdb.reserve()
v := tdb.db.Safe()
tdb.release()
return v
}
func(tdb *ThreadGdbmDb) Prefix() uint8 {
tdb.reserve()
v := tdb.db.Prefix()
tdb.release()
return v
}
func(tdb *ThreadGdbmDb) SetLock(typ uint8, locked bool) error {
tdb.reserve()
err := tdb.db.SetLock(typ, locked)
tdb.release()
return err
}
func(tdb *ThreadGdbmDb) Put(ctx context.Context, key []byte, val []byte) error {
tdb.reserve()
err := tdb.db.Put(ctx, key, val)
tdb.release()
return err
}
func(tdb *ThreadGdbmDb) Get(ctx context.Context, key []byte) ([]byte, error) {
tdb.reserve()
v, err := tdb.db.Get(ctx, key)
tdb.release()
return v, err
}
func(tdb *ThreadGdbmDb) Close() error {
tdb.reserve()
close(dbC[tdb.connStr])
delete(dbC, tdb.connStr)
err := tdb.db.Close()
tdb.db = nil
return err
}

View File

@@ -0,0 +1,44 @@
package storage
import (
"git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/persist"
)
type Storage struct {
Persister *persist.Persister
UserdataDb db.Db
}
type StorageProvider interface {
Get(sessionId string) (*Storage, error)
Put(sessionId string, storage *Storage) error
Close() error
}
type SimpleStorageProvider struct {
*Storage
}
func NewSimpleStorageProvider(stateStore db.Db, userdataStore db.Db) StorageProvider {
pe := persist.NewPersister(stateStore)
pe = pe.WithFlush()
return &SimpleStorageProvider{
Storage: &Storage{
Persister: pe,
UserdataDb: userdataStore,
},
}
}
func (p *SimpleStorageProvider) Get(sessionId string) (*Storage, error) {
return p.Storage, nil
}
func (p *SimpleStorageProvider) Put(sessionId string, storage *Storage) error {
return nil
}
func (p *SimpleStorageProvider) Close() error {
return p.Storage.UserdataDb.Close()
}

View File

@@ -0,0 +1,103 @@
package storage
import (
"context"
"fmt"
"os"
"path"
"git.defalsify.org/vise.git/db"
fsdb "git.defalsify.org/vise.git/db/fs"
"git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/resource"
"git.defalsify.org/vise.git/logging"
)
var (
logg = logging.NewVanilla().WithDomain("storage")
)
type StorageService interface {
GetPersister(ctx context.Context) (*persist.Persister, error)
GetUserdataDb(ctx context.Context) db.Db
GetResource(ctx context.Context) (resource.Resource, error)
EnsureDbDir() error
}
type MenuStorageService struct{
dbDir string
resourceDir string
resourceStore db.Db
stateStore db.Db
userDataStore db.Db
}
func NewMenuStorageService(dbDir string, resourceDir string) *MenuStorageService {
return &MenuStorageService{
dbDir: dbDir,
resourceDir: resourceDir,
}
}
func (ms *MenuStorageService) GetPersister(ctx context.Context) (*persist.Persister, error) {
ms.stateStore = NewThreadGdbmDb()
storeFile := path.Join(ms.dbDir, "state.gdbm")
err := ms.stateStore.Connect(ctx, storeFile)
if err != nil {
return nil, err
}
pr := persist.NewPersister(ms.stateStore)
logg.TraceCtxf(ctx, "menu storage service", "persist", pr, "store", ms.stateStore)
return pr, nil
}
func (ms *MenuStorageService) GetUserdataDb(ctx context.Context) (db.Db, error) {
ms.userDataStore = NewThreadGdbmDb()
storeFile := path.Join(ms.dbDir, "userdata.gdbm")
err := ms.userDataStore.Connect(ctx, storeFile)
if err != nil {
return nil, err
}
return ms.userDataStore, nil
}
func (ms *MenuStorageService) GetResource(ctx context.Context) (resource.Resource, error) {
ms.resourceStore = fsdb.NewFsDb()
err := ms.resourceStore.Connect(ctx, ms.resourceDir)
if err != nil {
return nil, err
}
rfs := resource.NewDbResource(ms.resourceStore)
return rfs, nil
}
func (ms *MenuStorageService) GetStateStore(ctx context.Context) (db.Db, error) {
if ms.stateStore != nil {
panic("set up store when already exists")
}
ms.stateStore = NewThreadGdbmDb()
storeFile := path.Join(ms.dbDir, "state.gdbm")
err := ms.stateStore.Connect(ctx, storeFile)
if err != nil {
return nil, err
}
return ms.stateStore, nil
}
func (ms *MenuStorageService) EnsureDbDir() error {
err := os.MkdirAll(ms.dbDir, 0700)
if err != nil {
return fmt.Errorf("state dir create exited with error: %v\n", err)
}
return nil
}
func (ms *MenuStorageService) Close() error {
errA := ms.stateStore.Close()
errB := ms.userDataStore.Close()
errC := ms.resourceStore.Close()
if errA != nil || errB != nil || errC != nil {
return fmt.Errorf("%v %v %v", errA, errB, errC)
}
return nil
}

View File

@@ -0,0 +1,121 @@
package testutil
import (
"context"
"fmt"
"os"
"path"
"time"
"git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/handlers/server"
"git.grassecon.net/urdt/ussd/internal/storage"
testdataloader "github.com/peteole/testdata-loader"
)
var (
baseDir = testdataloader.GetBasePath()
logg = logging.NewVanilla()
scriptDir = path.Join(baseDir, "services", "registration")
)
func TestEngine(sessionId string) (engine.Engine, func(), chan bool) {
ctx := context.Background()
ctx = context.WithValue(ctx, "SessionId", sessionId)
pfp := path.Join(scriptDir, "pp.csv")
var eventChannel = make(chan bool)
cfg := engine.Config{
Root: "root",
SessionId: sessionId,
OutputSize: uint32(160),
FlagCount: uint32(128),
}
dbDir := ".test_state"
resourceDir := scriptDir
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
err := menuStorageService.EnsureDbDir()
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
rs, err := menuStorageService.GetResource(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
pe, err := menuStorageService.GetPersister(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
userDataStore, err := menuStorageService.GetUserdataDb(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
dbResource, ok := rs.(*resource.DbResource)
if !ok {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
lhs, err := handlers.NewLocalHandlerService(pfp, true, dbResource, cfg, rs)
lhs.SetDataStore(&userDataStore)
lhs.SetPersister(pe)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
if AccountService == nil {
AccountService = &server.AccountService{}
}
switch AccountService.(type) {
case *server.TestAccountService:
go func() {
eventChannel <- false
}()
case *server.AccountService:
go func() {
time.Sleep(5 * time.Second) // Wait for 5 seconds
eventChannel <- true
}()
default:
panic("Unknown account service type")
}
hl, err := lhs.GetHandler(AccountService)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
en := lhs.GetEngine()
en = en.WithFirst(hl.Init)
cleanFn := func() {
err := en.Finish()
if err != nil {
logg.Errorf(err.Error())
}
err = menuStorageService.Close()
if err != nil {
logg.Errorf(err.Error())
}
logg.Infof("testengine storage closed")
}
return en, cleanFn, eventChannel
}

View File

@@ -0,0 +1,11 @@
// +build !online
package testutil
import (
"git.grassecon.net/urdt/ussd/internal/handlers/server"
)
var (
AccountService server.AccountServiceInterface = &server.TestAccountService{}
)

View File

@@ -0,0 +1,9 @@
// +build online
package testutil
import "git.grassecon.net/urdt/ussd/internal/handlers/server"
var (
AccountService server.AccountServiceInterface
)

View File

@@ -1,46 +0,0 @@
package utils
import (
"encoding/json"
"os"
)
type AccountFileHandler struct {
FilePath string
}
func NewAccountFileHandler(path string) *AccountFileHandler {
return &AccountFileHandler{FilePath: path}
}
func (afh *AccountFileHandler) ReadAccountData() (map[string]string, error) {
jsonData, err := os.ReadFile(afh.FilePath)
if err != nil {
return nil, err
}
var accountData map[string]string
err = json.Unmarshal(jsonData, &accountData)
if err != nil {
return nil, err
}
return accountData, nil
}
func (afh *AccountFileHandler) WriteAccountData(accountData map[string]string) error {
jsonData, err := json.Marshal(accountData)
if err != nil {
return err
}
return os.WriteFile(afh.FilePath, jsonData, 0644)
}
func (afh *AccountFileHandler) EnsureFileExists() error {
f, err := os.OpenFile(afh.FilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
return f.Close()
}

37
internal/utils/db.go Normal file
View File

@@ -0,0 +1,37 @@
package utils
import (
"encoding/binary"
)
type DataTyp uint16
const (
DATA_ACCOUNT DataTyp = iota
DATA_ACCOUNT_CREATED
DATA_TRACKING_ID
DATA_PUBLIC_KEY
DATA_CUSTODIAL_ID
DATA_ACCOUNT_PIN
DATA_ACCOUNT_STATUS
DATA_FIRST_NAME
DATA_FAMILY_NAME
DATA_YOB
DATA_LOCATION
DATA_GENDER
DATA_OFFERINGS
DATA_RECIPIENT
DATA_AMOUNT
DATA_TEMPORARY_PIN
)
func typToBytes(typ DataTyp) []byte {
var b [2]byte
binary.BigEndian.PutUint16(b[:], uint16(typ))
return b[:]
}
func PackKey(typ DataTyp, data []byte) []byte {
v := typToBytes(typ)
return append(v, data...)
}

View File

@@ -1,13 +0,0 @@
package utils
type AccountFileHandlerInterface interface {
EnsureFileExists() error
ReadAccountData() (map[string]string, error)
WriteAccountData(data map[string]string) error
}

11
internal/utils/isocode.go Normal file
View File

@@ -0,0 +1,11 @@
package utils
var isoCodes = map[string]bool{
"eng": true, // English
"swa": true, // Swahili
}
func IsValidISO639(code string) bool {
return isoCodes[code]
}

View File

@@ -0,0 +1,32 @@
package utils
import (
"context"
"git.defalsify.org/vise.git/db"
)
type DataStore interface {
db.Db
ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error)
WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error
}
type UserDataStore struct {
db.Db
}
// ReadEntry retrieves an entry from the store based on the provided parameters.
func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error) {
store.SetPrefix(db.DATATYPE_USERDATA)
store.SetSession(sessionId)
k := PackKey(typ, []byte(sessionId))
return store.Get(ctx, k)
}
func (store *UserDataStore) WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error {
store.SetPrefix(db.DATATYPE_USERDATA)
store.SetSession(sessionId)
k := PackKey(typ, []byte(sessionId))
return store.Put(ctx, k, value)
}

View File

@@ -0,0 +1,460 @@
{
"groups": [
{
"name": "my_account_change_pin",
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "5",
"expectedContent": "PIN Management\n1:Change PIN\n2:Reset other's PIN\n3:Guard my PIN\n0:Back"
},
{
"input": "1",
"expectedContent": "Enter your old PIN\n\n0:Back"
},
{
"input": "1234",
"expectedContent": "Enter a new four number PIN:\n\n0:Back"
},
{
"input": "1234",
"expectedContent": "Confirm your new PIN:\n\n0:Back"
},
{
"input": "1234",
"expectedContent": "Your PIN change request has been successful\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
{
"name": "menu_my_account_language_change",
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "2",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1235",
"expectedContent": "Incorrect pin\n1:retry\n9:Quit"
},
{
"input": "1",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Select language:\n0:english\n1:kiswahili"
},
{
"input": "0",
"expectedContent": "Your language change request was successful.\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
{
"name": "menu_my_account_check_my_balance",
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "3",
"expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
},
{
"input": "1",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1235",
"expectedContent": "Incorrect pin\n1:retry\n9:Quit"
},
{
"input": "1",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Your balance is 0.003 CELO\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
},
{
"input": "0",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
{
"name": "menu_my_account_check_community_balance",
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "3",
"expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
},
{
"input": "2",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1235",
"expectedContent": "Incorrect pin\n1:retry\n9:Quit"
},
{
"input": "1",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Your community balance is 0.003 CELO\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
},
{
"input": "0",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
{
"name": "menu_my_account_edit_firstname",
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "1",
"expectedContent": "Enter your first names:\n0:Back"
},
{
"input": "foo",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
{
"name": "menu_my_account_edit_familyname",
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "2",
"expectedContent": "Enter family name:\n0:Back"
},
{
"input": "bar",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
{
"name": "menu_my_account_edit_gender",
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "3",
"expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back"
},
{
"input": "1",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
{
"name": "menu_my_account_edit_yob",
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "4",
"expectedContent": "Enter your year of birth\n0:Back"
},
{
"input": "1945",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
{
"name": "menu_my_account_edit_location",
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "5",
"expectedContent": "Enter your location:\n0:Back"
},
{
"input": "Kilifi",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
{
"name": "menu_my_account_edit_offerings",
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "6",
"expectedContent": "Enter the services or goods you offer: \n0:Back"
},
{
"input": "Bananas",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
{
"name": "menu_my_account_view_profile",
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "7",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "My profile:\nName: foo bar\nGender: male\nAge: 79\nLocation: Kilifi\nYou provide: Bananas\n\n0:Back"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}
]
}

View File

@@ -0,0 +1,278 @@
package menutraversaltest
import (
"bytes"
"context"
"log"
"math/rand"
"os"
"regexp"
"testing"
"git.grassecon.net/urdt/ussd/driver"
"git.grassecon.net/urdt/ussd/internal/testutil"
"github.com/gofrs/uuid"
)
var (
testData = driver.ReadData()
testStore = ".test_state"
groupTestFile = "group_test.json"
sessionID string
src = rand.NewSource(42)
g = rand.New(src)
)
func GenerateSessionId() string {
uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g))
v, err := uu.NewV4()
if err != nil {
panic(err)
}
return v.String()
}
// Extract the public key from the engine response
func extractPublicKey(response []byte) string {
// Regex pattern to match the public key starting with 0x and 40 characters
re := regexp.MustCompile(`0x[a-fA-F0-9]{40}`)
match := re.Find(response)
if match != nil {
return string(match)
}
return ""
}
func TestMain(m *testing.M) {
sessionID = GenerateSessionId()
defer func() {
if err := os.RemoveAll(testStore); err != nil {
log.Fatalf("Failed to delete state store %s: %v", testStore, err)
}
}()
m.Run()
}
func TestAccountCreationSuccessful(t *testing.T) {
en, fn, eventChannel := testutil.TestEngine(sessionID)
defer fn()
ctx := context.Background()
sessions := testData
for _, session := range sessions {
groups := driver.FilterGroupsByName(session.Groups, "account_creation_successful")
for _, group := range groups {
for _, step := range group.Steps {
cont, err := en.Exec(ctx, []byte(step.Input))
if err != nil {
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
}
if !cont {
break
}
w := bytes.NewBuffer(nil)
_, err = en.Flush(ctx, w)
if err != nil {
t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
}
b := w.Bytes()
match, err := step.MatchesExpectedContent(b)
if err != nil {
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
}
if !match {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
}
}
}
}
<-eventChannel
}
func TestAccountRegistrationRejectTerms(t *testing.T) {
// Generate a new UUID for this edge case test
uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g))
v, err := uu.NewV4()
if err != nil {
t.Fail()
}
edgeCaseSessionID := v.String()
en, fn, _ := testutil.TestEngine(edgeCaseSessionID)
defer fn()
ctx := context.Background()
sessions := testData
for _, session := range sessions {
groups := driver.FilterGroupsByName(session.Groups, "account_creation_reject_terms")
for _, group := range groups {
for _, step := range group.Steps {
cont, err := en.Exec(ctx, []byte(step.Input))
if err != nil {
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
return
}
if !cont {
break
}
w := bytes.NewBuffer(nil)
if _, err := en.Flush(ctx, w); err != nil {
t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
}
b := w.Bytes()
match, err := step.MatchesExpectedContent(b)
if err != nil {
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
}
if !match {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
}
}
}
}
}
func TestMainMenuHelp(t *testing.T) {
en, fn, _ := testutil.TestEngine(sessionID)
defer fn()
ctx := context.Background()
sessions := testData
for _, session := range sessions {
groups := driver.FilterGroupsByName(session.Groups, "main_menu_help")
for _, group := range groups {
for _, step := range group.Steps {
cont, err := en.Exec(ctx, []byte(step.Input))
if err != nil {
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
return
}
if !cont {
break
}
w := bytes.NewBuffer(nil)
if _, err := en.Flush(ctx, w); err != nil {
t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
}
b := w.Bytes()
match, err := step.MatchesExpectedContent(b)
if err != nil {
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
}
if !match {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
}
}
}
}
}
func TestMainMenuQuit(t *testing.T) {
en, fn, _ := testutil.TestEngine(sessionID)
defer fn()
ctx := context.Background()
sessions := testData
for _, session := range sessions {
groups := driver.FilterGroupsByName(session.Groups, "main_menu_quit")
for _, group := range groups {
for _, step := range group.Steps {
cont, err := en.Exec(ctx, []byte(step.Input))
if err != nil {
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
return
}
if !cont {
break
}
w := bytes.NewBuffer(nil)
if _, err := en.Flush(ctx, w); err != nil {
t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
}
b := w.Bytes()
match, err := step.MatchesExpectedContent(b)
if err != nil {
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
}
if !match {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
}
}
}
}
}
func TestMyAccount_MyAddress(t *testing.T) {
en, fn, _ := testutil.TestEngine(sessionID)
defer fn()
ctx := context.Background()
sessions := testData
for _, session := range sessions {
groups := driver.FilterGroupsByName(session.Groups, "menu_my_account_my_address")
for _, group := range groups {
for index, step := range group.Steps {
t.Logf("step %v with input %v", index, step.Input)
cont, err := en.Exec(ctx, []byte(step.Input))
if err != nil {
t.Errorf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
return
}
if !cont {
break
}
w := bytes.NewBuffer(nil)
if _, err := en.Flush(ctx, w); err != nil {
t.Errorf("Test case '%s' failed during Flush: %v", group.Name, err)
}
b := w.Bytes()
publicKey := extractPublicKey(b)
expectedContent := bytes.Replace([]byte(step.ExpectedContent), []byte("{public_key}"), []byte(publicKey), -1)
step.ExpectedContent = string(expectedContent)
match, err := step.MatchesExpectedContent(b)
if err != nil {
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
}
if !match {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expectedContent, b)
}
}
}
}
}
func TestGroups(t *testing.T) {
groups, err := driver.LoadTestGroups(groupTestFile)
if err != nil {
log.Fatalf("Failed to load test groups: %v", err)
}
en, fn, _ := testutil.TestEngine(sessionID)
defer fn()
ctx := context.Background()
// Create test cases from loaded groups
tests := driver.CreateTestCases(groups)
for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
cont, err := en.Exec(ctx, []byte(tt.Input))
if err != nil {
t.Errorf("Test case '%s' failed at input '%s': %v", tt.Name, tt.Input, err)
return
}
if !cont {
return
}
w := bytes.NewBuffer(nil)
if _, err := en.Flush(ctx, w); err != nil {
t.Errorf("Test case '%s' failed during Flush: %v", tt.Name, err)
}
b := w.Bytes()
match, err := tt.MatchesExpectedContent(b)
if err != nil {
t.Fatalf("Error compiling regex for step '%s': %v", tt.Input, err)
}
if !match {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", tt.ExpectedContent, b)
}
})
}
}

View File

@@ -0,0 +1,153 @@
[
{
"name": "session one",
"groups": [
{
"name": "account_creation_successful",
"steps": [
{
"input": "",
"expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:english\n1:kiswahili"
},
{
"input": "0",
"expectedContent": "Do you agree to terms and conditions?\n0:yes\n1:no"
},
{
"input": "0",
"expectedContent": "Please enter a new four number PIN for your account:\n0:Exit"
},
{
"input": "1234",
"expectedContent": "Enter your four number PIN again:"
},
{
"input": "1111",
"expectedContent": "The PIN is not a match. Try again\n1:retry\n9:Quit"
},
{
"input": "1",
"expectedContent": "Enter your four number PIN again:"
},
{
"input": "1234",
"expectedContent": "Your account is being created...Thank you for using Sarafu. Goodbye!"
}
]
},
{
"name": "account_creation_reject_terms",
"steps": [
{
"input": "",
"expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:english\n1:kiswahili"
},
{
"input": "0",
"expectedContent": "Do you agree to terms and conditions?\n0:yes\n1:no"
},
{
"input": "1",
"expectedContent": "Thank you for using Sarafu. Goodbye!"
}
]
},
{
"name": "send_with_invalid_inputs",
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "1",
"expectedContent": "Enter recipient's phone number:\n0:Back"
},
{
"input": "000",
"expectedContent": "000 is not registered or invalid, please try again:\n1:retry\n9:Quit"
},
{
"input": "1",
"expectedContent": "Enter recipient's phone number:\n0:Back"
},
{
"input": "065656",
"expectedContent": "Maximum amount: 0.003 CELO\nEnter amount:\n0:Back"
},
{
"input": "0.1",
"expectedContent": "Amount 0.1 is invalid, please try again:\n1:retry\n9:Quit"
},
{
"input": "1",
"expectedContent": "Maximum amount: 0.003 CELO\nEnter amount:\n0:Back"
},
{
"input": "0.001",
"expectedContent": "065656 will receive 0.001 from {public_key}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit"
},
{
"input": "1222",
"expectedContent": "Incorrect pin\n1:retry\n9:Quit"
},
{
"input": "1",
"expectedContent": "065656 will receive 0.001 from {public_key}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit"
},
{
"input": "1234",
"expectedContent": "Your request has been sent. 065656 will receive 0.001 from {public_key}."
}
]
},
{
"name": "main_menu_help",
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "4",
"expectedContent": "For more help,please call: 0757628885"
}
]
},
{
"name": "main_menu_quit",
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "9",
"expectedContent": "Thank you for using Sarafu. Goodbye!"
}
]
},
{
"name": "menu_my_account_my_address",
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "6",
"expectedContent": "Address: {public_key}\n9:Quit"
},
{
"input": "9",
"expectedContent": "Thank you for using Sarafu. Goodbye!"
}
]
}
]
}
]

View File

@@ -1,10 +1,11 @@
# Variables to match files in the current directory
INPUTS = $(wildcard ./*.vis)
TXTS = $(wildcard ./*.txt.orig)
VISE_PATH := ../../go-vise
# Rule to build .bin files from .vis files
%.vis:
go run ../../go-vise/dev/asm $(basename $@).vis > $(basename $@).bin
go run $(VISE_PATH)/dev/asm/main.go -f pp.csv $(basename $@).vis > $(basename $@).bin
@echo "Built $(basename $@).bin from $(basename $@).vis"
# Rule to copy .orig files to .txt

View File

@@ -0,0 +1 @@
Something went wrong.Please try again

View File

@@ -0,0 +1 @@
HALT

View File

@@ -1,4 +1,4 @@
RELOAD verify_pin
CATCH create_pin_mismatch 20 1
CATCH create_pin_mismatch flag_pin_mismatch 1
LOAD quit 0
HALT

View File

@@ -1,3 +1,3 @@
RELOAD check_account_status
CATCH main 11 1
CATCH main flag_account_success 1
HALT

View File

@@ -5,8 +5,10 @@ MOUT back 0
HALT
LOAD validate_amount 64
RELOAD validate_amount
CATCH invalid_amount 17 1
CATCH api_failure flag_api_call_error 1
CATCH invalid_amount flag_invalid_amount 1
INCMP _ 0
LOAD get_recipient 12
LOAD get_sender 64
LOAD get_amount 12
INCMP transaction_pin *

View File

@@ -0,0 +1 @@
Failed to connect to the custodial service.Please try again.

View File

@@ -0,0 +1,5 @@
MOUT retry 0
MOUT quit 9
HALT
INCMP _ 0
INCMP quit 9

View File

@@ -1 +1 @@
Rudi
Rudi

View File

@@ -1 +1 @@
Balances:
Balances:

View File

@@ -1,4 +1,5 @@
LOAD reset_unlocked 0
LOAD reset_account_authorized 0
RELOAD reset_account_authorized
MOUT my_balance 1
MOUT community_balance 2
MOUT back 0

View File

@@ -1 +1 @@
Salio
Salio:

View File

@@ -0,0 +1 @@
Select language:

View File

@@ -0,0 +1,10 @@
LOAD reset_account_authorized 0
LOAD reset_incorrect 0
CATCH incorrect_pin flag_incorrect_pin 1
CATCH pin_entry flag_account_authorized 0
MOUT english 0
MOUT kiswahili 1
HALT
INCMP set_default 0
INCMP set_swa 1
INCMP . *

View File

@@ -1 +1 @@
Badili lugha
Badili lugha

View File

@@ -0,0 +1 @@
Chagua lugha:

View File

@@ -1 +1 @@
Change PIN
Change PIN

View File

@@ -1 +1 @@
Badili PIN
Badili PIN

View File

@@ -1 +1 @@
Check statement
Check statement

View File

@@ -1,2 +1 @@
Your community balance is: 0.00SRF
{{.fetch_custodial_balances}}

View File

@@ -1,5 +1,11 @@
LOAD reset_incorrect 0
CATCH incorrect_pin 15 1
CATCH pin_entry 12 0
LOAD quit_with_balance 0
LOAD reset_incorrect 6
LOAD fetch_custodial_balances 0
CATCH api_failure flag_api_call_error 1
MAP fetch_custodial_balances
CATCH incorrect_pin flag_incorrect_pin 1
CATCH pin_entry flag_account_authorized 0
MOUT back 0
MOUT quit 9
HALT
INCMP _ 0
INCMP quit 9

View File

@@ -1 +1 @@
Community balance
Community balance

View File

@@ -0,0 +1 @@
Confirm your new PIN:

View File

@@ -0,0 +1,7 @@
CATCH invalid_pin flag_valid_pin 0
MOUT back 0
HALT
INCMP _ 0
INCMP * pin_reset_success

View File

@@ -0,0 +1 @@
Thibitisha PIN yako mpya:

View File

@@ -1,9 +1,9 @@
LOAD create_account 0
CATCH account_creation_failed 22 1
CATCH account_creation_failed flag_account_creation_failed 1
MOUT exit 0
HALT
LOAD save_pin 0
RELOAD save_pin
CATCH . 15 1
CATCH . flag_incorrect_pin 1
INCMP quit 0
INCMP confirm_create_pin *

View File

@@ -0,0 +1 @@
Edit family name

View File

@@ -0,0 +1 @@
Weka jina la familia

View File

@@ -1 +1 @@
Edit name
Edit name

View File

@@ -1 +1 @@
Weka jina
Weka jina

View File

@@ -1 +1 @@
Edit offerings
Edit offerings

View File

@@ -1,20 +1,21 @@
LOAD reset_account_authorized 16
RELOAD reset_account_authorized
LOAD reset_allow_update 0
RELOAD reset_allow_update
MOUT edit_name 1
MOUT edit_gender 2
MOUT edit_yob 3
MOUT edit_location 4
MOUT edit_offerings 5
MOUT view 6
MOUT edit_familyname 2
MOUT edit_gender 3
MOUT edit_yob 4
MOUT edit_location 5
MOUT edit_offerings 6
MOUT view 7
MOUT back 0
HALT
INCMP _ 0
LOAD set_reset_single_edit 0
RELOAD set_reset_single_edit
INCMP my_account 0
INCMP enter_name 1
INCMP select_gender 2
INCMP enter_yob 3
INCMP enter_location 4
INCMP enter_offerings 5
INCMP view_profile 6
INCMP enter_familyname 2
INCMP select_gender 3
INCMP enter_yob 4
INCMP enter_location 5
INCMP enter_offerings 6
INCMP view_profile 7

View File

@@ -1 +1 @@
Enter family name:
Enter family name:

View File

@@ -1,5 +1,9 @@
LOAD save_firstname 0
CATCH incorrect_pin flag_incorrect_pin 1
CATCH profile_update_success flag_allow_update 1
LOAD save_familyname 0
RELOAD save_familyname
MOUT back 0
HALT
RELOAD save_familyname
INCMP _ 0
INCMP select_gender *
INCMP pin_entry *

View File

@@ -0,0 +1 @@
Weka jina la familia

View File

@@ -1 +1 @@
Enter your location:
Enter your location:

View File

@@ -1,12 +1,8 @@
CATCH incorrect_date_format 21 1
LOAD save_yob 0
CATCH update_success 16 1
CATCH incorrect_pin flag_incorrect_pin 1
CATCH profile_update_success flag_allow_update 1
LOAD save_location 0
MOUT back 0
HALT
RELOAD save_location
INCMP _ 0
LOAD save_location 0
CATCH pin_entry 23 1
INCMP enter_offerings *
INCMP pin_entry *

View File

@@ -1,4 +1,12 @@
CATCH incorrect_pin flag_incorrect_pin 1
CATCH profile_update_success flag_allow_update 1
LOAD save_firstname 0
RELOAD save_firstname
MOUT back 0
HALT
RELOAD save_firstname
INCMP _ 0
INCMP enter_familyname *
INCMP pin_entry *

View File

@@ -1 +1 @@
Weka majina yako ya kwanza:
Weka majina yako ya kwanza:

View File

@@ -1,8 +1,8 @@
LOAD save_location 0
CATCH incorrect_pin 15 1
CATCH update_success 16 1
CATCH incorrect_pin flag_incorrect_pin 1
CATCH profile_update_success flag_allow_update 1
LOAD save_offerings 0
MOUT back 0
HALT
LOAD save_offerings 0
RELOAD save_offerings
INCMP _ 0
INCMP pin_entry *

View File

@@ -1,9 +1,10 @@
LOAD save_gender 0
CATCH update_success 16 1
CATCH incorrect_pin flag_incorrect_pin 1
CATCH profile_update_success flag_allow_update 1
LOAD save_yob 0
MOUT back 0
HALT
LOAD verify_yob 0
CATCH incorrect_date_format flag_incorrect_date_format 1
RELOAD save_yob
INCMP _ 0
LOAD verify_yob 8
LOAD save_yob 0
CATCH pin_entry 23 1
INCMP enter_location *
INCMP pin_entry *

View File

@@ -1 +1 @@
Female
Female

View File

@@ -1 +1 @@
Guard my PIN
Guard my PIN

View File

@@ -1 +1 @@
Linda PIN yangu
Linda PIN yangu

View File

@@ -0,0 +1,2 @@
LOAD quit_with_help 0
HALT

View File

@@ -1,2 +1,2 @@
The year of birth you entered is invalid.
Please try again.
Please try again.

View File

@@ -1 +1 @@
Incorrect pin
Incorrect pin

View File

@@ -0,0 +1 @@
The PIN you entered is invalid.The PIN must be different from your current PIN.For help call +254757628885

View File

@@ -0,0 +1,3 @@
MOUT back 0
HALT
INCMP _ 0

View File

@@ -0,0 +1 @@
PIN mpya na udhibitisho wa pin mpya hazilingani.Tafadhali jaribu tena.Kwa usaidizi piga simu +254757628885.

View File

@@ -0,0 +1 @@
Your language change request was successful.

View File

@@ -0,0 +1,5 @@
MOUT back 0
MOUT quit 9
HALT
INCMP ^ 0
INCMP quit 9

View File

@@ -0,0 +1 @@
Ombi lako la kubadilisha lugha limefanikiwa.

View File

@@ -6,3 +6,7 @@ msgstr "Ombi lako limetumwa. %s atapokea %s kutoka kwa %s."
msgid "Thank you for using Sarafu. Goodbye!"
msgstr "Asante kwa kutumia huduma ya Sarafu. Kwaheri!"
msgid "For more help,please call: 0757628885"
msgstr "Kwa usaidizi zaidi,piga: 0757628885"

View File

@@ -1,5 +1,6 @@
LOAD check_balance 64
RELOAD check_balance
CATCH api_failure flag_api_call_error 1
MAP check_balance
MOUT send 1
MOUT vouchers 2
@@ -10,6 +11,6 @@ HALT
INCMP send 1
INCMP quit 2
INCMP my_account 3
INCMP quit 4
INCMP help 4
INCMP quit 9
INCMP . *

View File

@@ -7,8 +7,9 @@ MOUT pin_options 5
MOUT my_address 6
MOUT back 0
HALT
INCMP _ 0
INCMP main 0
INCMP edit_profile 1
INCMP change_language 2
INCMP balances 3
INCMP pin_management 5
INCMP address 6

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