Compare commits

..

218 Commits

Author SHA1 Message Date
52787bdb4d Merge branch 'master' into lash/dump-format 2025-01-06 09:42:26 +01:00
824d39908b ci: fix missing ssh dir
Some checks failed
release / docker (push) Has been cancelled
2025-01-06 11:19:36 +03:00
a312ea5b84 feat: inject build string in ssh binary, expose default ssh port
Some checks failed
release / docker (push) Has been cancelled
2025-01-06 11:09:51 +03:00
4836162f40 ci: add ssh build
Some checks failed
release / docker (push) Has been cancelled
2025-01-06 10:51:20 +03:00
lash
2024cc96e2 Bring up-to-date with refactor
Some checks failed
release / docker (push) Has been cancelled
2025-01-06 07:22:58 +00:00
lash
d2d878d5d7 Merge branch 'master' into lash/ssh-4 2025-01-06 07:12:00 +00:00
c995143543 Merge pull request 'log-session-id-at-sessionid' (#251) from log-session-id-at-sessionid into master
Reviewed-on: #251
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2025-01-06 08:01:20 +01:00
Carlosokumu
44570e20ef remove unused context key :- at-session-id 2025-01-06 09:59:47 +03:00
Carlosokumu
362eb209ef add SessionId to context key 2025-01-06 09:54:28 +03:00
Carlosokumu
c69d3896f1 pass context as an argument,rename context keys 2025-01-06 08:52:53 +03:00
Carlosokumu
974af6b2a7 pass context as an argument 2025-01-06 08:50:53 +03:00
lash
83857026d3 Merge branch 'master' into lash/dump-format 2025-01-04 10:00:25 +00:00
47b5ff0435 Merge pull request 'Improve separation of concerns in all modules, phase 1' (#246) from lash/purify into master
Reviewed-on: #246
2025-01-04 10:56:17 +01:00
lash
25867cf05e Rehabilitate voucher test 2025-01-04 09:42:36 +00:00
Carlosokumu
d5a2680500 make context accessible 2025-01-04 12:02:45 +03:00
lash
d950b10b50 Move prefix db spec to separate package 2025-01-04 08:37:28 +00:00
lash
bcb3ab905e Move db related to own package 2025-01-04 08:09:18 +00:00
lash
3ed9caf16d Factor out request parsers 2025-01-04 08:02:44 +00:00
lash
86464c31d2 Merge branch 'master' into lash/purify 2025-01-04 07:57:54 +00:00
5ee10d8e14 Merge pull request 'logs-at-sessionid' (#245) from logs-at-sessionid into master
Some checks failed
release / docker (push) Has been cancelled
Reviewed-on: #245
2025-01-04 08:56:09 +01:00
Carlosokumu
62f3681b9e define context keysessionid using go-vise --withcontext 2025-01-04 10:40:26 +03:00
Carlosokumu
3ce1435591 extract session id from africastalking request 2025-01-04 10:38:25 +03:00
Carlosokumu
f65c458daa update go-vise. 2025-01-04 10:35:59 +03:00
lash
67007fcd48 Factor out gdbm package 2025-01-04 07:35:28 +00:00
lash
f1b258fa6d Factor out at code 2025-01-04 07:29:22 +00:00
lash
daec816a3e Move store devtools location 2025-01-03 17:21:52 +00:00
lash
ac0c43cb43 Factor out formatting method 2025-01-03 17:18:23 +00:00
d2fce05461 Merge pull request 'fix: language change' (#242) from language-change-fix into master
Some checks failed
release / docker (push) Has been cancelled
Reviewed-on: #242
2025-01-03 09:30:27 +01:00
carlos
68ac237449 Merge branch 'master' into language-change-fix 2025-01-03 09:28:48 +01:00
Carlosokumu
162e6c1934 fix: language change 2025-01-03 11:26:56 +03:00
8bd025f2b2 Merge pull request 'hash-pin' (#235) from hash-pin into master
Some checks failed
release / docker (push) Has been cancelled
Reviewed-on: #235
2025-01-03 09:25:26 +01:00
alfred-mk
9d6e25e184 revert to previous state for the adminstore 2025-01-03 11:24:24 +03:00
alfred-mk
c26f5683f6 removed second unused argument 2025-01-03 11:17:09 +03:00
91dc9ce82f tests: add sample pin/hash pair from migration dataset 2025-01-03 11:10:07 +03:00
alfred-mk
0fe48a30fa Merge branch 'master' into hash-pin 2025-01-03 06:58:41 +03:00
58edfa01a2 Merge pull request 'menu-primary-selectors' (#237) from menu-primary-selectors into master
Some checks failed
release / docker (push) Has been cancelled
Reviewed-on: #237
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2025-01-02 15:50:42 +01:00
Carlosokumu
3830c12a57 update tests 2025-01-02 17:42:03 +03:00
Carlosokumu
f1fd690a7b update expected content 2025-01-02 17:37:26 +03:00
alfred-mk
491b7424a9 point to the correct ./devtools/admin_numbers directory 2025-01-02 16:01:19 +03:00
alfred-mk
29ce4b83bd added tests for HashPIN and VerifyPIN 2025-01-02 15:22:07 +03:00
alfred-mk
ca8df5989a updated expected age in test 2025-01-02 15:15:52 +03:00
alfred-mk
82b4365d16 hash the PIN in TestAuthorize 2025-01-02 14:38:22 +03:00
alfred-mk
98db85511b hash the PIN in the ResetOthersPin function 2025-01-02 14:37:45 +03:00
alfred-mk
99a4d3ff42 verify the PIN input against the hashed PIN 2025-01-02 13:51:57 +03:00
alfred-mk
d95c7abea4 return if the PIN is not a match, and hash the PIN before saving it 2025-01-02 13:45:18 +03:00
alfred-mk
fd1ac85a1b add code to Hash and Verify the PIN 2025-01-02 13:43:38 +03:00
alfred-mk
c899c098f6 updated the expected age 2025-01-02 13:20:01 +03:00
alfred-mk
5ca6a74274 move PIN test to the common package 2025-01-02 13:18:49 +03:00
alfred-mk
48d63fb43f added pin.go to contain all PIN related functionality 2025-01-02 13:16:38 +03:00
Carlosokumu
e666c58644 start primary selectors with 1 2025-01-02 12:17:28 +03:00
Carlosokumu
e980586910 chore: repeat same node on invalid menu choice 2025-01-02 12:15:57 +03:00
Carlosokumu
ffd5be1f1f add quit option on view profile 2025-01-02 12:12:52 +03:00
ed1aeecf7d Merge pull request 'Legible dumper' (#232) from lash/dump-key-prefix into master
Some checks failed
release / docker (push) Has been cancelled
Reviewed-on: #232
2024-12-31 09:56:04 +01:00
3b69f3d38d Merge branch 'master' into lash/dump-key-prefix 2024-12-31 09:55:40 +01:00
lash
cd58f5ae33 Upgrade govise 2024-12-31 08:55:25 +00:00
alfred-mk
7a535f796a output the value as a string 2024-12-31 11:41:04 +03:00
7c4c73125e Merge pull request 'force-restart-state' (#223) from force-restart-state into master
Reviewed-on: #223
2024-12-31 09:34:44 +01:00
lash
c7dbe1d88f Remove obsolete subprefix strings 2024-12-31 08:30:08 +00:00
alfred-mk
4ea52bf3fb removed unused code 2024-12-31 11:16:43 +03:00
alfred-mk
be2ea3a2f0 removed the non-working restart_state devtool 2024-12-31 10:51:29 +03:00
alfred-mk
8217ea8fdc Merge branch 'master' into force-restart-state 2024-12-31 05:06:26 +03:00
alfred-mk
3c73fc7188 added a test for the Init func with the different states 2024-12-31 05:05:39 +03:00
alfred-mk
1311a0cab9 use the 'send_with_invite' test group in the menu traversal test 2024-12-31 02:36:28 +03:00
lash
3bcd48e5a7 Update govise 2024-12-30 19:58:34 +00:00
lash
0e12c0ee4e Add prefix for dumper, format base dump key for pg 2024-12-30 19:35:45 +00:00
3caee98cdb Merge pull request 'mixed-languages' (#228) from mixed-languages into master
Reviewed-on: #228
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-12-30 09:07:13 +01:00
Carlosokumu
db7c9bf56d chore: add colon to enhance formatting. 2024-12-30 08:07:39 +03:00
Carlosokumu
0a332ec501 chore: ensure swahili language translation. 2024-12-30 08:05:36 +03:00
90367fe53e Merge pull request 'profile-update-fix' (#226) from profile-update-fix into master
Reviewed-on: #226
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-12-28 16:20:32 +01:00
alfred-mk
50c006546c added code to reset the state and persist it 2024-12-28 13:21:03 +03:00
alfred-mk
e8c171a82e Merge branch 'master' into force-restart-state 2024-12-28 11:46:15 +03:00
Carlosokumu
58a60f2c81 update expected age in test 2024-12-28 08:51:38 +03:00
carlos
0820e1b9f2 Merge branch 'master' into profile-update-fix 2024-12-28 06:30:14 +01:00
Carlosokumu
46edf2b819 remove
redundant catch on pin entry
2024-12-27 16:13:36 +03:00
Carlosokumu
11eb61ba35 repeat same node on invalid selection 2024-12-27 16:11:09 +03:00
813b92af78 Merge pull request 'issue-205: added comments for menu handlers methods.' (#218) from konstantinmds/ussd:dev/issue-205 into master
Reviewed-on: #218
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-12-27 12:58:00 +01:00
Carlosokumu
5579991d66 guard profile update after being set 2024-12-27 10:07:05 +03:00
konstantinmds
f4f4fdd3ac issue-205:
added comments for menu handlers methods and changed function name to better fit function workings.
2024-12-25 15:59:28 +01:00
alfred-mk
be215d3f75 set the code to an empty byte for it to move to the top node 2024-12-20 12:26:07 +03:00
235af3519d Merge pull request 'add-space-after-colon' (#211) from add-space-after-colon into master
Some checks failed
release / docker (push) Has been cancelled
Reviewed-on: #211
2024-12-19 11:35:13 +01:00
alfred-mk
1292851226 rename the function to ReplaceSeparatorFunc 2024-12-19 13:32:39 +03:00
alfred-mk
dfd0a0994b Merge branch 'master' into force-restart-state 2024-12-18 22:39:20 +03:00
alfred-mk
97fcdda12f Merge branch 'master' into add-space-after-colon 2024-12-18 22:30:41 +03:00
alfred-mk
055c2db790 use a common mockReplaceSeparator func 2024-12-18 22:25:47 +03:00
alfred-mk
ecfdab47a8 updated test 2024-12-18 22:21:52 +03:00
alfred-mk
fda68231ea use the replaceSeparator func to format the generated menus 2024-12-18 21:59:09 +03:00
alfred-mk
d08afff443 add the replaceSeparator func and pass it to the Handler struct 2024-12-18 21:56:37 +03:00
alfred-mk
17ba6a06ba remove the MenuSeparator from the context 2024-12-18 21:49:42 +03:00
dbd59a4023 Merge pull request 'add link to terms page' (#217) from link-terms-and-conditions into master
Reviewed-on: #217
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-12-18 07:45:48 +01:00
alfred-mk
5534706189 reset the state when input is nil 2024-12-17 17:58:08 +03:00
alfred-mk
5428626c3f cleaned up the restart_state 2024-12-17 17:56:56 +03:00
Carlosokumu
9b33117cb1 add space on expected content 2024-12-17 16:02:35 +03:00
Carlosokumu
70b2fa4ac2 add spacing after link 2024-12-17 15:46:28 +03:00
Carlosokumu
fd6ff86579 add link to terms and conditions as expected content 2024-12-17 15:24:15 +03:00
Carlosokumu
549782f230 add link to terms page 2024-12-17 15:12:38 +03:00
8cf4848b45 Merge pull request 'Userstore dumper tool' (#153) from lash/store-dumper into master
Some checks failed
release / docker (push) Has been cancelled
Reviewed-on: #153
2024-12-14 13:02:48 +01:00
9f6c0a1111 Merge branch 'master' into lash/store-dumper 2024-12-14 13:02:38 +01:00
lash
1ab49647f6 Upgrade vise 2024-12-14 12:02:13 +00:00
lash
8d4d8a48e0 Fix compile errors, test errors 2024-12-14 11:56:31 +00:00
alfred-mk
7aea2af9a1 updated tests 2024-12-13 11:44:04 +03:00
alfred-mk
5cd791aae7 use the MenuSeparator 2024-12-13 11:43:47 +03:00
alfred-mk
df5e5f1a4b properly format the vouchers 2024-12-13 11:40:39 +03:00
alfred-mk
64c1fe5276 set the separator as a var and add it to the context 2024-12-13 11:38:10 +03:00
alfred-mk
f38ea59569 set the separator as a config and not an arg 2024-12-13 01:10:46 +03:00
alfred-mk
6cc285d1e8 add the custom separator to the menu 2024-12-12 21:12:25 +03:00
alfred-mk
0d7f7aaca1 use latest commits from go-vise 2024-12-12 21:09:48 +03:00
alfred-mk
f8ea2daa73 initialize the restart state devtool 2024-12-12 19:55:01 +03:00
lash
c820e89cb7 Segment tx userdata types, add internals docs 2024-12-11 19:13:13 +00:00
alfred-mk
e05f8e7291 update to the latest go-vise changes 2024-12-11 19:46:52 +03:00
alfred-mk
2383e8ead3 updated failed menuhandler_test 2024-12-11 19:35:04 +03:00
alfred-mk
1a4ee0d3e1 updated the description of the GetTransactionsList function 2024-12-11 19:32:41 +03:00
alfred-mk
6f3b30e2fe Capitalize statement details and add a space after the colon 2024-12-11 19:31:17 +03:00
alfred-mk
b1e4b63c6a Add a space after the colon for vouchers 2024-12-11 19:28:53 +03:00
alfred-mk
3129e8210e Capitalize the transfer statement details 2024-12-11 19:25:38 +03:00
alfred-mk
5d8de80a18 Write the error in the response 2024-12-11 18:58:50 +03:00
lash
604c16ec90 Habilitate store dumper 2024-12-08 22:48:39 +00:00
lash
a3e821fb16 Clarify gen cmd example with reverse public key lookup 2024-12-08 21:57:53 +00:00
lash
890f50704f Add documentation comments for db subprefix types 2024-12-08 21:21:48 +00:00
lash
3416fdf50c Add keyinfo restore 2024-12-07 23:06:03 +00:00
43892f0d8c Merge pull request 'profile-edit-traverse' (#199) from profile-edit-traverse into master
Some checks failed
release / docker (push) Has been cancelled
Reviewed-on: #199
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-12-05 16:37:37 +01:00
Carlosokumu
8e6b1e6f52 Merge branch 'master' into profile-edit-traverse 2024-12-05 18:34:29 +03:00
lash
ff943a125c Merge branch 'master' into lash/store-dumper 2024-12-05 15:30:29 +00:00
Carlosokumu
9cbbdff993 chore: use profileDataKeys as an array 2024-12-05 18:25:51 +03:00
316358765d Merge pull request 'data-items-cleanup' (#203) from data-items-cleanup into master
Reviewed-on: #203
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-12-05 16:03:50 +01:00
alfred-mk
14737b5f12 Removed redundant naming of transfer related data 2024-12-05 17:58:51 +03:00
alfred-mk
caff27b43d Replace IntToBytes(value int) and ToBytes() with a single ToBytes() function 2024-12-05 17:50:40 +03:00
alfred-mk
589a94216b Use the DATATYPE_USERDATA as the prefix for the NewSubPrefixDb func 2024-12-05 17:02:26 +03:00
alfred-mk
a659fb06fa Added a function to convert int to []byte 2024-12-05 16:58:03 +03:00
alfred-mk
f733fe5636 Start the sub prefix data at 256 (0x0100) 2024-12-05 16:31:47 +03:00
alfred-mk
18423fcd9c updated the name of the voucher related data 2024-12-05 16:26:56 +03:00
Carlosokumu
72a3681767 add test data. 2024-12-05 16:03:08 +03:00
Carlosokumu
160ccbb220 read test file from test run arg 2024-12-05 14:05:32 +03:00
Carlosokumu
22f96363ba add test files 2024-12-05 14:04:56 +03:00
Carlosokumu
3e7f90733e update test names 2024-12-05 12:22:29 +03:00
Carlosokumu
321f038c7c iterate over a map for the set profile items 2024-12-05 10:52:45 +03:00
alfred-mk
7a9de79aae return nil with error 2024-12-04 21:07:11 +03:00
alfred-mk
862830e9de renamed internal/storage/db.go -> internal/storage/sub_prefix_db.go for clarity 2024-12-04 20:59:46 +03:00
alfred-mk
bc0e536d3d updated failing tests 2024-12-04 20:55:03 +03:00
alfred-mk
82884a75a3 Merge branch 'master' into data-items-cleanup 2024-12-04 20:45:38 +03:00
alfred-mk
93c44861e0 Use numeric prefixes 2024-12-04 20:42:47 +03:00
alfred-mk
4ecfc9de38 removed DATATYPE_USERSUB and replaced with DATATYPE_USERDATA 2024-12-04 20:38:52 +03:00
Carlosokumu
a84c3e0852 update menu traversal test data 2024-12-04 09:46:16 +03:00
Carlosokumu
8efed966a0 set flag when location is set 2024-12-04 09:08:47 +03:00
Carlosokumu
e7c4b5bca7 update tests 2024-12-04 09:08:25 +03:00
Carlosokumu
ed632248c5 add doc lines and check for back naviagtions 2024-12-04 08:30:07 +03:00
Carlosokumu
5c202741d6 add handler for catching back navigations 2024-12-04 08:20:43 +03:00
Carlosokumu
c5ebdbf85b catch back navigations 2024-12-04 08:20:09 +03:00
Carlosokumu
c4282a870e add flag to catch back navigations 2024-12-04 08:19:20 +03:00
Carlosokumu
91cd6077ce Merge branch 'master' into profile-edit-traverse 2024-12-03 22:25:47 +03:00
Carlosokumu
c0ed6fa9c8 catch incorrect pin entries 2024-12-03 21:43:24 +03:00
22c9c3e0f0 Merge pull request 'minor-bug-fixes' (#177) from minor-bug-fixes into master
Reviewed-on: #177
2024-12-03 18:18:23 +01:00
a709d24520 Merge branch 'master' into minor-bug-fixes 2024-12-03 18:16:19 +01:00
e56138e416 Merge pull request 'Clear persister from handler in outer code aswell' (#200) from lash/persister-freakout into master
Reviewed-on: #200
2024-12-03 17:18:11 +01:00
lash
d516584d90 Clear persister from handler in outer code aswell 2024-12-03 16:16:53 +00:00
Carlosokumu
b420a9bba0 set flag if profile data is set 2024-12-03 17:41:06 +03:00
Carlosokumu
a20ab79355 explicit reload save gender 2024-12-03 17:38:20 +03:00
Carlosokumu
e0ec15b272 allow sequential profile edit 2024-12-03 14:40:57 +03:00
Carlosokumu
9e998f9a29 add a zero pad value to unfilled profile item 2024-12-03 14:37:55 +03:00
Carlosokumu
1c7c0af712 catch next unset profile item 2024-12-03 14:36:48 +03:00
alfred-mk
d40a4a171f formatted code 2024-12-03 14:12:47 +03:00
alfred-mk
ba430a5849 add a separate function to handle ConstructName 2024-12-03 14:10:05 +03:00
alfred-mk
0f21b01813 resolved error in the TestViewVoucher 2024-12-03 13:37:00 +03:00
alfred-mk
10586baf0d resolved error in the TestCheckBalance 2024-12-03 13:35:14 +03:00
alfred-mk
e979742424 resolved error in the TestValidateRecipient 2024-12-03 13:32:18 +03:00
alfred-mk
ff3f049226 updated the CheckAliasAddress mock 2024-12-03 13:31:30 +03:00
alfred-mk
13b45c49da Merge branch 'master' into minor-bug-fixes 2024-12-03 12:58:26 +03:00
Carlosokumu
a72fb08dc8 allow sequential profile edit 2024-12-03 11:20:35 +03:00
Carlosokumu
944fa89b3c add profile holder struct 2024-12-03 11:19:38 +03:00
Carlosokumu
48e1b02e0e allow all item profile edit 2024-12-02 20:50:21 +03:00
Carlosokumu
3e0bbe5ffe add handler to update profile items 2024-12-02 20:35:56 +03:00
Carlosokumu
fd586d09c3 add required profile edit flags 2024-12-02 20:35:04 +03:00
lash
c2a4efde2b Merge branch 'master' into lash/store-dumper 2024-12-02 15:39:29 +00:00
b615c27cf6 Merge pull request 'trigger-balance-reload' (#193) from trigger-balance-reload into master
Some checks failed
release / docker (push) Has been cancelled
Reviewed-on: #193
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-12-02 16:36:26 +01:00
22e870b3e5 Merge branch 'master' into trigger-balance-reload 2024-12-02 16:36:08 +01:00
da462346f9 Merge pull request 'Always reset persister in handler' (#194) from lash/no-persister-deadlock into master
Reviewed-on: #194
2024-12-02 16:17:36 +01:00
lash
406bd84875 Always reset persister in handler 2024-12-02 14:53:18 +00:00
lash
c9deca1180 change userdata prefix on subprefix debug 2024-12-02 14:36:33 +00:00
alfred-mk
419cd185fc Merge branch 'master' into minor-bug-fixes 2024-12-02 15:25:34 +03:00
7976e237ca Merge pull request 'voucher-details' (#179) from voucher-details into master
Reviewed-on: #179
Reviewed-by: Alfred Kamanda <alfredkamandamw@gmail.com>
2024-12-02 11:43:30 +01:00
alfred-mk
19ec8f0817 Merge branch 'master' into voucher-details 2024-12-02 13:32:32 +03:00
alfred-mk
ef3a3d6717 updated the TestGetVoucherDetails 2024-12-02 13:30:33 +03:00
alfred-mk
c2019267d1 capitalize the voucher descriptions 2024-12-02 13:26:46 +03:00
Carlosokumu
0091fbcabb fix: looped navigation 2024-12-02 09:03:04 +03:00
lash
6d4f3109f8 Consolidate subtyp and typ debug 2024-12-01 23:12:58 +00:00
lash
35cf3a1cd1 Add debug label test, capability debug flag 2024-12-01 18:41:28 +00:00
lash
1a782c1db9 Add debug package 2024-12-01 17:32:06 +00:00
lash
3d2ca606ca Merge branch 'master' into lash/store-dumper 2024-12-01 16:34:10 +00:00
alfred-mk
aa7497573e removed unused code 2024-11-30 15:29:28 +03:00
alfred-mk
54c1fe51ef update the active voucher data when checking the current vouchers 2024-11-30 15:28:21 +03:00
alfred-mk
7a86b2ad3b updated the UpdateVoucherData description 2024-11-30 15:26:13 +03:00
alfred-mk
6b23c284e5 check vouchers before checking the balance 2024-11-30 15:24:14 +03:00
alfred-mk
aab6660edd Capitalize menu items 2024-11-29 15:39:27 +03:00
alfred-mk
c46f41e25f Format the balance to 2 decimal places 2024-11-29 14:47:22 +03:00
Carlosokumu
00c0445eed show name without depending on family name being set 2024-11-28 11:42:47 +03:00
alfred-mk
c8c6b05b8a Merge branch 'master' into minor-bug-fixes 2024-11-26 15:31:45 +03:00
Carlosokumu
a17cf78d29 Merge remote-tracking branch 'refs/remotes/origin/minor-bug-fixes' into minor-bug-fixes 2024-11-22 11:35:57 +03:00
Carlosokumu
9847433e0a use _ for back navigation 2024-11-22 11:30:13 +03:00
alfred-mk
7ce50398d1 use the language translation instead of hardcoded eng 2024-11-21 15:54:00 +03:00
alfred-mk
e30bc177e9 fixed typo and added a new translation 2024-11-21 15:52:07 +03:00
alfred-mk
b9ff467c0c use the correct balance 2024-11-21 15:51:04 +03:00
Carlosokumu
1174500e3f add test for voucher details 2024-11-21 15:19:36 +03:00
alfred-mk
07df450b3c include labels to define the symbol and balance while selecting a voucher 2024-11-21 15:15:15 +03:00
Carlosokumu
b8d938d3aa add voucher details 2024-11-21 13:04:19 +03:00
Carlosokumu
d1e9340ea9 add voucher details 2024-11-21 13:03:43 +03:00
Carlosokumu
8925e26c4c refactor check for valid yob 2024-11-21 11:25:47 +03:00
Carlosokumu
9b89462797 add function to check validity of provided yob 2024-11-21 11:21:16 +03:00
Carlosokumu
7880294c6f set eng as default language 2024-11-20 17:14:25 +03:00
Carlosokumu
451b15fb6b explicit set_language reload 2024-11-20 17:13:14 +03:00
Carlosokumu
d20700ca74 fix size limit error 2024-11-20 16:39:50 +03:00
Carlosokumu
9cf1cbe425 add swahili translation for catch node 2024-11-20 16:36:18 +03:00
584d02db29 Merge branch 'master' into lash/store-dumper 2024-11-05 00:19:06 +01:00
5937c6bf5c Merge branch 'master' into lash/store-dumper 2024-11-04 13:56:44 +01:00
lash
10b3083647 WIP Add db dumper tool 2024-11-02 20:29:32 +00:00
lash
bb1a846cb3 Merge remote-tracking branch 'origin/master' into lash/ssh-4 2024-10-31 20:52:09 +00:00
lash
967e53d83b Merge branch 'master' into lash/ssh-4 2024-10-14 14:50:12 +01:00
lash
d246cdee51 Rename datatype const name for ssh prefix 2024-09-27 21:25:21 +01:00
lash
d518a76536 Merge branch 'lash/subprefix' into lash/ssh-4 2024-09-27 21:18:25 +01:00
lash
6f65c33be4 Re-add ssh 2024-09-26 15:15:06 +01:00
107 changed files with 3078 additions and 811 deletions

View File

@@ -1,5 +1,6 @@
/** /**
!/cmd/africastalking !/cmd/africastalking
!/cmd/ssh
!/common !/common
!/config !/config
!/initializers !/initializers

View File

@@ -19,6 +19,7 @@ WORKDIR /build
RUN echo "Building on $BUILDPLATFORM, building for $TARGETPLATFORM" RUN echo "Building on $BUILDPLATFORM, building for $TARGETPLATFORM"
RUN go mod download RUN go mod download
RUN go build -tags logtrace -o ussd-africastalking -ldflags="-X main.build=${BUILD} -s -w" cmd/africastalking/main.go RUN go build -tags logtrace -o ussd-africastalking -ldflags="-X main.build=${BUILD} -s -w" cmd/africastalking/main.go
RUN go build -tags logtrace -o ussd-ssh -ldflags="-X main.build=${BUILD} -s -w" cmd/ssh/main.go
FROM debian:bookworm-slim FROM debian:bookworm-slim
@@ -30,6 +31,7 @@ RUN apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /service WORKDIR /service
COPY --from=build /build/ussd-africastalking . COPY --from=build /build/ussd-africastalking .
COPY --from=build /build/ussd-ssh .
COPY --from=build /build/LICENSE . COPY --from=build /build/LICENSE .
COPY --from=build /build/README.md . COPY --from=build /build/README.md .
COPY --from=build /build/services ./services COPY --from=build /build/services ./services
@@ -37,5 +39,6 @@ COPY --from=build /build/.env.example .
RUN mv .env.example .env RUN mv .env.example .env
EXPOSE 7123 EXPOSE 7123
EXPOSE 7122
CMD ["./ussd-africastalking"] CMD ["./ussd-africastalking"]

View File

@@ -1,110 +1,39 @@
package main package main
import ( import (
"bytes"
"context" "context"
"encoding/json"
"flag" "flag"
"fmt" "fmt"
"io"
"log"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"path" "path"
"strconv" "strconv"
"strings"
"syscall" "syscall"
"git.defalsify.org/vise.git/engine" "git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/common"
"git.grassecon.net/urdt/ussd/config" "git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/initializers" "git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/internal/handlers" "git.grassecon.net/urdt/ussd/internal/handlers"
httpserver "git.grassecon.net/urdt/ussd/internal/http" "git.grassecon.net/urdt/ussd/internal/http/at"
httpserver "git.grassecon.net/urdt/ussd/internal/http/at"
"git.grassecon.net/urdt/ussd/internal/storage" "git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/remote" "git.grassecon.net/urdt/ussd/remote"
) )
var ( var (
logg = logging.NewVanilla() logg = logging.NewVanilla().WithDomain("AfricasTalking").WithContextKey("at-session-id")
scriptDir = path.Join("services", "registration") scriptDir = path.Join("services", "registration")
build = "dev"
build = "dev" menuSeparator = ": "
) )
func init() { func init() {
initializers.LoadEnvVariables() initializers.LoadEnvVariables()
} }
type atRequestParser struct{}
func (arp *atRequestParser) GetSessionId(rq any) (string, error) {
rqv, ok := rq.(*http.Request)
if !ok {
log.Printf("got an invalid request:", rq)
return "", handlers.ErrInvalidRequest
}
// Capture body (if any) for logging
body, err := io.ReadAll(rqv.Body)
if err != nil {
log.Printf("failed to read request body:", err)
return "", fmt.Errorf("failed to read request body: %v", err)
}
// Reset the body for further reading
rqv.Body = io.NopCloser(bytes.NewReader(body))
// Log the body as JSON
bodyLog := map[string]string{"body": string(body)}
logBytes, err := json.Marshal(bodyLog)
if err != nil {
log.Printf("failed to marshal request body:", err)
} else {
log.Printf("Received request:", string(logBytes))
}
if err := rqv.ParseForm(); err != nil {
log.Printf("failed to parse form data: %v", err)
return "", fmt.Errorf("failed to parse form data: %v", err)
}
phoneNumber := rqv.FormValue("phoneNumber")
if phoneNumber == "" {
return "", fmt.Errorf("no phone number found")
}
formattedNumber, err := common.FormatPhoneNumber(phoneNumber)
if err != nil {
fmt.Printf("Error: %v\n", err)
return "", fmt.Errorf("failed to format number")
}
return formattedNumber, 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() { func main() {
config.LoadConfig() config.LoadConfig()
@@ -131,9 +60,10 @@ func main() {
pfp := path.Join(scriptDir, "pp.csv") pfp := path.Join(scriptDir, "pp.csv")
cfg := engine.Config{ cfg := engine.Config{
Root: "root", Root: "root",
OutputSize: uint32(size), OutputSize: uint32(size),
FlagCount: uint32(128), FlagCount: uint32(128),
MenuSeparator: menuSeparator,
} }
if engineDebug { if engineDebug {
@@ -191,7 +121,7 @@ func main() {
} }
defer stateStore.Close() defer stateStore.Close()
rp := &atRequestParser{} rp := &at.ATRequestParser{}
bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl) bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
sh := httpserver.NewATSessionHandler(bsh) sh := httpserver.NewATSessionHandler(bsh)

View File

@@ -21,8 +21,9 @@ import (
) )
var ( var (
logg = logging.NewVanilla() logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration") scriptDir = path.Join("services", "registration")
menuSeparator = ": "
) )
func init() { func init() {
@@ -34,7 +35,7 @@ type asyncRequestParser struct {
input []byte input []byte
} }
func (p *asyncRequestParser) GetSessionId(r any) (string, error) { func (p *asyncRequestParser) GetSessionId(ctx context.Context, r any) (string, error) {
return p.sessionId, nil return p.sessionId, nil
} }
@@ -70,9 +71,10 @@ func main() {
pfp := path.Join(scriptDir, "pp.csv") pfp := path.Join(scriptDir, "pp.csv")
cfg := engine.Config{ cfg := engine.Config{
Root: "root", Root: "root",
OutputSize: uint32(size), OutputSize: uint32(size),
FlagCount: uint32(128), FlagCount: uint32(128),
MenuSeparator: menuSeparator,
} }
if engineDebug { if engineDebug {

View File

@@ -26,6 +26,7 @@ import (
var ( var (
logg = logging.NewVanilla() logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration") scriptDir = path.Join("services", "registration")
menuSeparator = ": "
) )
func init() { func init() {
@@ -58,9 +59,10 @@ func main() {
pfp := path.Join(scriptDir, "pp.csv") pfp := path.Join(scriptDir, "pp.csv")
cfg := engine.Config{ cfg := engine.Config{
Root: "root", Root: "root",
OutputSize: uint32(size), OutputSize: uint32(size),
FlagCount: uint32(128), FlagCount: uint32(128),
MenuSeparator: menuSeparator,
} }
if engineDebug { if engineDebug {

View File

@@ -18,8 +18,9 @@ import (
) )
var ( var (
logg = logging.NewVanilla() logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration") scriptDir = path.Join("services", "registration")
menuSeparator = ": "
) )
func init() { func init() {
@@ -49,10 +50,11 @@ func main() {
pfp := path.Join(scriptDir, "pp.csv") pfp := path.Join(scriptDir, "pp.csv")
cfg := engine.Config{ cfg := engine.Config{
Root: "root", Root: "root",
SessionId: sessionId, SessionId: sessionId,
OutputSize: uint32(size), OutputSize: uint32(size),
FlagCount: uint32(128), FlagCount: uint32(128),
MenuSeparator: menuSeparator,
} }
resourceDir := scriptDir resourceDir := scriptDir

34
cmd/ssh/README.md Normal file
View File

@@ -0,0 +1,34 @@
# URDT-USSD SSH server
An SSH server entry point for the vise engine.
## Adding public keys for access
Map your (client) public key to a session identifier (e.g. phone number)
```
go run -v -tags logtrace ./cmd/ssh/sshkey/main.go -i <session_id> [--dbdir <dbpath>] <client_publickey_filepath>
```
## Create a private key for the server
```
ssh-keygen -N "" -f <server_privatekey_filepath>
```
## Run the server
```
go run -v -tags logtrace ./cmd/ssh/main.go -h <host> -p <port> [--dbdir <dbpath>] <server_privatekey_filepath>
```
## Connect to the server
```
ssh [-v] -T -p <port> -i <client_publickey_filepath> <host>
```

117
cmd/ssh/main.go Normal file
View File

@@ -0,0 +1,117 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"os/signal"
"path"
"sync"
"syscall"
"git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/logging"
"git.grassecon.net/urdt/ussd/internal/ssh"
)
var (
wg sync.WaitGroup
keyStore db.Db
logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration")
build = "dev"
)
func main() {
var dbDir string
var resourceDir string
var size uint
var engineDebug bool
var stateDebug 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, "engine-debug", false, "use engine debug output")
flag.BoolVar(&stateDebug, "state-debug", 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", 7122, "http port")
flag.Parse()
sshKeyFile := flag.Arg(0)
_, err := os.Stat(sshKeyFile)
if err != nil {
fmt.Fprintf(os.Stderr, "cannot open ssh server private key file: %v\n", err)
os.Exit(1)
}
ctx := context.Background()
logg.WarnCtxf(ctx, "!!!!! WARNING WARNING WARNING")
logg.WarnCtxf(ctx, "!!!!! =======================")
logg.WarnCtxf(ctx, "!!!!! This is not a production ready server!")
logg.WarnCtxf(ctx, "!!!!! Do not expose to internet and only use with tunnel!")
logg.WarnCtxf(ctx, "!!!!! (See ssh -L <...>)")
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "keyfile", sshKeyFile, "host", host, "port", port)
pfp := path.Join(scriptDir, "pp.csv")
cfg := engine.Config{
Root: "root",
OutputSize: uint32(size),
FlagCount: uint32(16),
}
if stateDebug {
cfg.StateDebug = true
}
if engineDebug {
cfg.EngineDebug = true
}
authKeyStore, err := ssh.NewSshKeyStore(ctx, dbDir)
if err != nil {
fmt.Fprintf(os.Stderr, "keystore file open error: %v", err)
os.Exit(1)
}
defer func() {
logg.TraceCtxf(ctx, "shutdown auth key store reached")
err = authKeyStore.Close()
if err != nil {
logg.ErrorCtxf(ctx, "keystore close error", "err", err)
}
}()
cint := make(chan os.Signal)
cterm := make(chan os.Signal)
signal.Notify(cint, os.Interrupt, syscall.SIGINT)
signal.Notify(cterm, os.Interrupt, syscall.SIGTERM)
runner := &ssh.SshRunner{
Cfg: cfg,
Debug: engineDebug,
FlagFile: pfp,
DbDir: dbDir,
ResourceDir: resourceDir,
SrvKeyFile: sshKeyFile,
Host: host,
Port: port,
}
go func() {
select {
case _ = <-cint:
case _ = <-cterm:
}
logg.TraceCtxf(ctx, "shutdown runner reached")
err := runner.Stop()
if err != nil {
logg.ErrorCtxf(ctx, "runner stop error", "err", err)
}
}()
runner.Run(ctx, authKeyStore)
}

44
cmd/ssh/sshkey/main.go Normal file
View File

@@ -0,0 +1,44 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"git.grassecon.net/urdt/ussd/internal/ssh"
)
func main() {
var dbDir string
var sessionId string
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
flag.StringVar(&sessionId, "i", "", "session id")
flag.Parse()
if sessionId == "" {
fmt.Fprintf(os.Stderr, "empty session id\n")
os.Exit(1)
}
ctx := context.Background()
sshKeyFile := flag.Arg(0)
if sshKeyFile == "" {
fmt.Fprintf(os.Stderr, "missing key file argument\n")
os.Exit(1)
}
store, err := ssh.NewSshKeyStore(ctx, dbDir)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
defer store.Close()
err = store.AddFromFile(ctx, sshKeyFile, sessionId)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}

View File

@@ -7,32 +7,84 @@ import (
"git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/logging"
) )
// DataType is a subprefix value used in association with vise/db.DATATYPE_USERDATA.
//
// All keys are used only within the context of a single account. Unless otherwise specified, the user context is the session id.
//
// * The first byte is vise/db.DATATYPE_USERDATA
// * The last 2 bytes are the DataTyp value, big-endian.
// * The intermediate bytes are the id of the user context.
//
// All values are strings
type DataTyp uint16 type DataTyp uint16
const ( const (
DATA_ACCOUNT DataTyp = iota // API Tracking id to follow status of account creation
DATA_ACCOUNT_CREATED DATA_TRACKING_ID = iota
DATA_TRACKING_ID // EVM address returned from API on account creation
DATA_PUBLIC_KEY DATA_PUBLIC_KEY
DATA_CUSTODIAL_ID // Currently active PIN used to authenticate ussd state change requests
DATA_ACCOUNT_PIN DATA_ACCOUNT_PIN
DATA_ACCOUNT_STATUS // The first name of the user
DATA_FIRST_NAME DATA_FIRST_NAME
// The last name of the user
DATA_FAMILY_NAME DATA_FAMILY_NAME
// The year-of-birth of the user
DATA_YOB DATA_YOB
// The location of the user
DATA_LOCATION DATA_LOCATION
// The gender of the user
DATA_GENDER DATA_GENDER
// The offerings description of the user
DATA_OFFERINGS DATA_OFFERINGS
// The ethereum address of the recipient of an ongoing send request
DATA_RECIPIENT DATA_RECIPIENT
// The voucher value amount of an ongoing send request
DATA_AMOUNT DATA_AMOUNT
// A general swap field for temporary values
DATA_TEMPORARY_VALUE DATA_TEMPORARY_VALUE
// Currently active voucher symbol of user
DATA_ACTIVE_SYM DATA_ACTIVE_SYM
// Voucher balance of user's currently active voucher
DATA_ACTIVE_BAL DATA_ACTIVE_BAL
// String boolean indicating whether use of PIN is blocked
DATA_BLOCKED_NUMBER DATA_BLOCKED_NUMBER
// Reverse mapping of a user's evm address to a session id.
DATA_PUBLIC_KEY_REVERSE DATA_PUBLIC_KEY_REVERSE
// Decimal count of the currently active voucher
DATA_ACTIVE_DECIMAL DATA_ACTIVE_DECIMAL
// EVM address of the currently active voucher
DATA_ACTIVE_ADDRESS DATA_ACTIVE_ADDRESS
DATA_TRANSACTIONS )
const (
// List of valid voucher symbols in the user context.
DATA_VOUCHER_SYMBOLS DataTyp = 256 + iota
// List of voucher balances for vouchers valid in the user context.
DATA_VOUCHER_BALANCES
// List of voucher decimal counts for vouchers valid in the user context.
DATA_VOUCHER_DECIMALS
// List of voucher EVM addresses for vouchers valid in the user context.
DATA_VOUCHER_ADDRESSES
// List of senders for valid transactions in the user context.
)
const (
DATA_TX_SENDERS = 512 + iota
// List of recipients for valid transactions in the user context.
DATA_TX_RECIPIENTS
// List of voucher values for valid transactions in the user context.
DATA_TX_VALUES
// List of voucher EVM addresses for valid transactions in the user context.
DATA_TX_ADDRESSES
// List of valid transaction hashes in the user context.
DATA_TX_HASHES
// List of transaction dates for valid transactions in the user context.
DATA_TX_DATES
// List of voucher symbols for valid transactions in the user context.
DATA_TX_SYMBOLS
// List of voucher decimal counts for valid transactions in the user context.
DATA_TX_DECIMALS
) )
var ( var (
@@ -69,3 +121,10 @@ func StringToDataTyp(str string) (DataTyp, error) {
return 0, errors.New("invalid DataTyp string") return 0, errors.New("invalid DataTyp string")
} }
} }
// ToBytes converts DataTyp or int to a byte slice
func ToBytes[T ~uint16 | int](value T) []byte {
bytes := make([]byte, 2)
binary.BigEndian.PutUint16(bytes, uint16(value))
return bytes
}

33
common/pin.go Normal file
View File

@@ -0,0 +1,33 @@
package common
import (
"regexp"
"golang.org/x/crypto/bcrypt"
)
// Define the regex pattern as a constant
const (
pinPattern = `^\d{4}$`
)
// checks whether the given input is a 4 digit number
func IsValidPIN(pin string) bool {
match, _ := regexp.MatchString(pinPattern, pin)
return match
}
// HashPIN uses bcrypt with 8 salt rounds to hash the PIN
func HashPIN(pin string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(pin), 8)
if err != nil {
return "", err
}
return string(hash), nil
}
// VerifyPIN compareS the hashed PIN with the plaintext PIN
func VerifyPIN(hashedPIN, pin string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPIN), []byte(pin))
return err == nil
}

173
common/pin_test.go Normal file
View File

@@ -0,0 +1,173 @@
package common
import (
"testing"
"golang.org/x/crypto/bcrypt"
)
func TestIsValidPIN(t *testing.T) {
tests := []struct {
name string
pin string
expected bool
}{
{
name: "Valid PIN with 4 digits",
pin: "1234",
expected: true,
},
{
name: "Valid PIN with leading zeros",
pin: "0001",
expected: true,
},
{
name: "Invalid PIN with less than 4 digits",
pin: "123",
expected: false,
},
{
name: "Invalid PIN with more than 4 digits",
pin: "12345",
expected: false,
},
{
name: "Invalid PIN with letters",
pin: "abcd",
expected: false,
},
{
name: "Invalid PIN with special characters",
pin: "12@#",
expected: false,
},
{
name: "Empty PIN",
pin: "",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := IsValidPIN(tt.pin)
if actual != tt.expected {
t.Errorf("IsValidPIN(%q) = %v; expected %v", tt.pin, actual, tt.expected)
}
})
}
}
func TestHashPIN(t *testing.T) {
tests := []struct {
name string
pin string
}{
{
name: "Valid PIN with 4 digits",
pin: "1234",
},
{
name: "Valid PIN with leading zeros",
pin: "0001",
},
{
name: "Empty PIN",
pin: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hashedPIN, err := HashPIN(tt.pin)
if err != nil {
t.Errorf("HashPIN(%q) returned an error: %v", tt.pin, err)
return
}
if hashedPIN == "" {
t.Errorf("HashPIN(%q) returned an empty hash", tt.pin)
}
// Ensure the hash can be verified with bcrypt
err = bcrypt.CompareHashAndPassword([]byte(hashedPIN), []byte(tt.pin))
if tt.pin != "" && err != nil {
t.Errorf("HashPIN(%q) produced a hash that does not match: %v", tt.pin, err)
}
})
}
}
func TestVerifyMigratedHashPin(t *testing.T) {
tests := []struct {
pin string
hash string
}{
{
pin: "1234",
hash: "$2b$08$dTvIGxCCysJtdvrSnaLStuylPoOS/ZLYYkxvTeR5QmTFY3TSvPQC6",
},
}
for _, tt := range tests {
t.Run(tt.pin, func(t *testing.T) {
ok := VerifyPIN(tt.hash, tt.pin)
if !ok {
t.Errorf("VerifyPIN could not verify migrated PIN: %v", tt.pin)
}
})
}
}
func TestVerifyPIN(t *testing.T) {
tests := []struct {
name string
pin string
hashedPIN string
shouldPass bool
}{
{
name: "Valid PIN verification",
pin: "1234",
hashedPIN: hashPINHelper("1234"),
shouldPass: true,
},
{
name: "Invalid PIN verification with incorrect PIN",
pin: "5678",
hashedPIN: hashPINHelper("1234"),
shouldPass: false,
},
{
name: "Invalid PIN verification with empty PIN",
pin: "",
hashedPIN: hashPINHelper("1234"),
shouldPass: false,
},
{
name: "Invalid PIN verification with invalid hash",
pin: "1234",
hashedPIN: "invalidhash",
shouldPass: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := VerifyPIN(tt.hashedPIN, tt.pin)
if result != tt.shouldPass {
t.Errorf("VerifyPIN(%q, %q) = %v; expected %v", tt.hashedPIN, tt.pin, result, tt.shouldPass)
}
})
}
}
// Helper function to hash a PIN for testing purposes
func hashPINHelper(pin string) string {
hashedPIN, err := HashPIN(pin)
if err != nil {
panic("Failed to hash PIN for test setup: " + err.Error())
}
return hashedPIN
}

View File

@@ -8,14 +8,15 @@ import (
"git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/resource"
"git.defalsify.org/vise.git/persist" "git.defalsify.org/vise.git/persist"
"git.grassecon.net/urdt/ussd/internal/storage" "git.grassecon.net/urdt/ussd/internal/storage"
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
) )
func StoreToDb(store *UserDataStore) db.Db { func StoreToDb(store *UserDataStore) db.Db {
return store.Db return store.Db
} }
func StoreToPrefixDb(store *UserDataStore, pfx []byte) storage.PrefixDb { func StoreToPrefixDb(store *UserDataStore, pfx []byte) dbstorage.PrefixDb {
return storage.NewSubPrefixDb(store.Db, pfx) return dbstorage.NewSubPrefixDb(store.Db, pfx)
} }
type StorageServices interface { type StorageServices interface {

View File

@@ -6,7 +6,7 @@ import (
"strings" "strings"
"time" "time"
"git.grassecon.net/urdt/ussd/internal/storage" dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
) )
@@ -56,26 +56,26 @@ func ProcessTransfers(transfers []dataserviceapi.Last10TxResponse) TransferMetad
// GetTransferData retrieves and matches transfer data // GetTransferData retrieves and matches transfer data
// returns a formatted string of the full transaction/statement // returns a formatted string of the full transaction/statement
func GetTransferData(ctx context.Context, db storage.PrefixDb, publicKey string, index int) (string, error) { func GetTransferData(ctx context.Context, db dbstorage.PrefixDb, publicKey string, index int) (string, error) {
keys := []string{"txfrom", "txto", "txval", "txaddr", "txhash", "txdate", "txsym"} keys := []DataTyp{DATA_TX_SENDERS, DATA_TX_RECIPIENTS, DATA_TX_VALUES, DATA_TX_ADDRESSES, DATA_TX_HASHES, DATA_TX_DATES, DATA_TX_SYMBOLS}
data := make(map[string]string) data := make(map[DataTyp]string)
for _, key := range keys { for _, key := range keys {
value, err := db.Get(ctx, []byte(key)) value, err := db.Get(ctx, ToBytes(key))
if err != nil { if err != nil {
return "", fmt.Errorf("failed to get %s: %v", key, err) return "", fmt.Errorf("failed to get %s: %v", ToBytes(key), err)
} }
data[key] = string(value) data[key] = string(value)
} }
// Split the data // Split the data
senders := strings.Split(string(data["txfrom"]), "\n") senders := strings.Split(string(data[DATA_TX_SENDERS]), "\n")
recipients := strings.Split(string(data["txto"]), "\n") recipients := strings.Split(string(data[DATA_TX_RECIPIENTS]), "\n")
values := strings.Split(string(data["txval"]), "\n") values := strings.Split(string(data[DATA_TX_VALUES]), "\n")
addresses := strings.Split(string(data["txaddr"]), "\n") addresses := strings.Split(string(data[DATA_TX_ADDRESSES]), "\n")
hashes := strings.Split(string(data["txhash"]), "\n") hashes := strings.Split(string(data[DATA_TX_HASHES]), "\n")
dates := strings.Split(string(data["txdate"]), "\n") dates := strings.Split(string(data[DATA_TX_DATES]), "\n")
syms := strings.Split(string(data["txsym"]), "\n") syms := strings.Split(string(data[DATA_TX_SYMBOLS]), "\n")
// Check if index is within range // Check if index is within range
if index < 1 || index > len(senders) { if index < 1 || index > len(senders) {
@@ -84,18 +84,18 @@ func GetTransferData(ctx context.Context, db storage.PrefixDb, publicKey string,
// Adjust for 0-based indexing // Adjust for 0-based indexing
i := index - 1 i := index - 1
transactionType := "received" transactionType := "Received"
party := fmt.Sprintf("from: %s", strings.TrimSpace(senders[i])) party := fmt.Sprintf("From: %s", strings.TrimSpace(senders[i]))
if strings.TrimSpace(senders[i]) == publicKey { if strings.TrimSpace(senders[i]) == publicKey {
transactionType = "sent" transactionType = "Sent"
party = fmt.Sprintf("to: %s", strings.TrimSpace(recipients[i])) party = fmt.Sprintf("To: %s", strings.TrimSpace(recipients[i]))
} }
formattedDate := formatDate(strings.TrimSpace(dates[i])) formattedDate := formatDate(strings.TrimSpace(dates[i]))
// Build the full transaction detail // Build the full transaction detail
detail := fmt.Sprintf( detail := fmt.Sprintf(
"%s %s %s\n%s\ncontract address: %s\ntxhash: %s\ndate: %s", "%s %s %s\n%s\nContract address: %s\nTxhash: %s\nDate: %s",
transactionType, transactionType,
strings.TrimSpace(values[i]), strings.TrimSpace(values[i]),
strings.TrimSpace(syms[i]), strings.TrimSpace(syms[i]),

View File

@@ -20,7 +20,7 @@ type UserDataStore struct {
func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error) { func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error) {
store.SetPrefix(db.DATATYPE_USERDATA) store.SetPrefix(db.DATATYPE_USERDATA)
store.SetSession(sessionId) store.SetSession(sessionId)
k := PackKey(typ, []byte(sessionId)) k := ToBytes(typ)
return store.Get(ctx, k) return store.Get(ctx, k)
} }
@@ -29,6 +29,6 @@ func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ
func (store *UserDataStore) WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error { func (store *UserDataStore) WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error {
store.SetPrefix(db.DATATYPE_USERDATA) store.SetPrefix(db.DATATYPE_USERDATA)
store.SetSession(sessionId) store.SetSession(sessionId)
k := PackKey(typ, []byte(sessionId)) k := ToBytes(typ)
return store.Put(ctx, k, value) return store.Put(ctx, k, value)
} }

View File

@@ -6,7 +6,7 @@ import (
"math/big" "math/big"
"strings" "strings"
"git.grassecon.net/urdt/ussd/internal/storage" dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
) )
@@ -63,23 +63,24 @@ func ScaleDownBalance(balance, decimals string) string {
} }
// GetVoucherData retrieves and matches voucher data // GetVoucherData retrieves and matches voucher data
func GetVoucherData(ctx context.Context, db storage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) { func GetVoucherData(ctx context.Context, db dbstorage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) {
keys := []string{"sym", "bal", "deci", "addr"} keys := []DataTyp{DATA_VOUCHER_SYMBOLS, DATA_VOUCHER_BALANCES, DATA_VOUCHER_DECIMALS, DATA_VOUCHER_ADDRESSES}
data := make(map[string]string) data := make(map[DataTyp]string)
for _, key := range keys { for _, key := range keys {
value, err := db.Get(ctx, []byte(key)) value, err := db.Get(ctx, ToBytes(key))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get %s: %v", key, err) return nil, fmt.Errorf("failed to get %s: %v", ToBytes(key), err)
} }
data[key] = string(value) data[key] = string(value)
} }
symbol, balance, decimal, address := MatchVoucher(input, symbol, balance, decimal, address := MatchVoucher(input,
data["sym"], data[DATA_VOUCHER_SYMBOLS],
data["bal"], data[DATA_VOUCHER_BALANCES],
data["deci"], data[DATA_VOUCHER_DECIMALS],
data["addr"]) data[DATA_VOUCHER_ADDRESSES],
)
if symbol == "" { if symbol == "" {
return nil, nil return nil, nil
@@ -151,7 +152,7 @@ func GetTemporaryVoucherData(ctx context.Context, store DataStore, sessionId str
return data, nil return data, nil
} }
// UpdateVoucherData sets the active voucher data in the DataStore. // UpdateVoucherData updates the active voucher data in the DataStore.
func UpdateVoucherData(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error { func UpdateVoucherData(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error {
logg.TraceCtxf(ctx, "dtal", "data", data) logg.TraceCtxf(ctx, "dtal", "data", data)
// Active voucher data entries // Active voucher data entries

View File

@@ -8,8 +8,9 @@ import (
"github.com/alecthomas/assert/v2" "github.com/alecthomas/assert/v2"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"git.grassecon.net/urdt/ussd/internal/storage" visedb "git.defalsify.org/vise.git/db"
memdb "git.defalsify.org/vise.git/db/mem" memdb "git.defalsify.org/vise.git/db/mem"
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
) )
@@ -83,19 +84,21 @@ func TestGetVoucherData(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
spdb := storage.NewSubPrefixDb(db, []byte("vouchers"))
prefix := ToBytes(visedb.DATATYPE_USERDATA)
spdb := dbstorage.NewSubPrefixDb(db, prefix)
// Test voucher data // Test voucher data
mockData := map[string][]byte{ mockData := map[DataTyp][]byte{
"sym": []byte("1:SRF\n2:MILO"), DATA_VOUCHER_SYMBOLS: []byte("1:SRF\n2:MILO"),
"bal": []byte("1:100\n2:200"), DATA_VOUCHER_BALANCES: []byte("1:100\n2:200"),
"deci": []byte("1:6\n2:4"), DATA_VOUCHER_DECIMALS: []byte("1:6\n2:4"),
"addr": []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"), DATA_VOUCHER_ADDRESSES: []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"),
} }
// Put the data // Put the data
for key, value := range mockData { for key, value := range mockData {
err = spdb.Put(ctx, []byte(key), []byte(value)) err = spdb.Put(ctx, []byte(ToBytes(key)), []byte(value))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

5
debug/cap.go Normal file
View File

@@ -0,0 +1,5 @@
package debug
var (
DebugCap uint32
)

84
debug/db.go Normal file
View File

@@ -0,0 +1,84 @@
package debug
import (
"fmt"
"encoding/binary"
"git.grassecon.net/urdt/ussd/common"
"git.defalsify.org/vise.git/db"
)
var (
dbTypStr map[common.DataTyp]string = make(map[common.DataTyp]string)
)
type KeyInfo struct {
SessionId string
Typ uint8
SubTyp common.DataTyp
Label string
Description string
}
func (k KeyInfo) String() string {
v := uint16(k.SubTyp)
s := subTypToString(k.SubTyp)
if s == "" {
v = uint16(k.Typ)
s = typToString(k.Typ)
}
return fmt.Sprintf("Session Id: %s\nTyp: %s (%d)\n", k.SessionId, s, v)
}
func ToKeyInfo(k []byte, sessionId string) (KeyInfo, error) {
o := KeyInfo{}
b := []byte(sessionId)
if len(k) <= len(b) {
return o, fmt.Errorf("storage key missing")
}
o.SessionId = sessionId
o.Typ = uint8(k[0])
k = k[1:]
o.SessionId = string(k[:len(b)])
k = k[len(b):]
if o.Typ == db.DATATYPE_USERDATA {
if len(k) == 0 {
return o, fmt.Errorf("missing subtype key")
}
v := binary.BigEndian.Uint16(k[:2])
o.SubTyp = common.DataTyp(v)
o.Label = subTypToString(o.SubTyp)
k = k[2:]
} else {
o.Label = typToString(o.Typ)
}
if len(k) != 0 {
return o, fmt.Errorf("excess key information")
}
return o, nil
}
func FromKey(k []byte) (KeyInfo, error) {
o := KeyInfo{}
if len(k) < 4 {
return o, fmt.Errorf("insufficient key length")
}
sessionIdBytes := k[1:len(k)-2]
return ToKeyInfo(k, string(sessionIdBytes))
}
func subTypToString(v common.DataTyp) string {
return dbTypStr[v + db.DATATYPE_USERDATA + 1]
}
func typToString(v uint8) string {
return dbTypStr[common.DataTyp(uint16(v))]
}

44
debug/db_debug.go Normal file
View File

@@ -0,0 +1,44 @@
// +build debugdb
package debug
import (
"git.defalsify.org/vise.git/db"
"git.grassecon.net/urdt/ussd/common"
)
func init() {
DebugCap |= 1
dbTypStr[db.DATATYPE_STATE] = "internal state"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TRACKING_ID] = "tracking id"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_PUBLIC_KEY] = "public key"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT_PIN] = "account pin"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_FIRST_NAME] = "first name"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_FAMILY_NAME] = "family name"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_YOB] = "year of birth"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_LOCATION] = "location"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_GENDER] = "gender"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_OFFERINGS] = "offerings"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_RECIPIENT] = "recipient"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_AMOUNT] = "amount"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TEMPORARY_VALUE] = "temporary value"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_SYM] = "active sym"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_BAL] = "active bal"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_BLOCKED_NUMBER] = "blocked number"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_PUBLIC_KEY_REVERSE] = "public_key_reverse"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_DECIMAL] = "active decimal"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_ADDRESS] = "active address"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_SYMBOLS] = "voucher symbols"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_BALANCES] = "voucher balances"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_DECIMALS] = "voucher decimals"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_ADDRESSES] = "voucher addresses"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_SENDERS] = "tx senders"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_RECIPIENTS] = "tx recipients"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_VALUES] = "tx values"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_ADDRESSES] = "tx addresses"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_HASHES] = "tx hashes"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_DATES] = "tx dates"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_SYMBOLS] = "tx symbols"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_DECIMALS] = "tx decimals"
}

78
debug/db_test.go Normal file
View File

@@ -0,0 +1,78 @@
package debug
import (
"testing"
"git.grassecon.net/urdt/ussd/common"
"git.defalsify.org/vise.git/db"
)
func TestDebugDbSubKeyInfo(t *testing.T) {
s := "foo"
b := []byte{0x20}
b = append(b, []byte(s)...)
b = append(b, []byte{0x00, 0x02}...)
r, err := ToKeyInfo(b, s)
if err != nil {
t.Fatal(err)
}
if r.SessionId != s {
t.Fatalf("expected %s, got %s", s, r.SessionId)
}
if r.Typ != 32 {
t.Fatalf("expected 64, got %d", r.Typ)
}
if r.SubTyp != 2 {
t.Fatalf("expected 2, got %d", r.SubTyp)
}
if DebugCap & 1 > 0 {
if r.Label != "tracking id" {
t.Fatalf("expected 'tracking id', got '%s'", r.Label)
}
}
}
func TestDebugDbKeyInfo(t *testing.T) {
s := "bar"
b := []byte{0x10}
b = append(b, []byte(s)...)
r, err := ToKeyInfo(b, s)
if err != nil {
t.Fatal(err)
}
if r.SessionId != s {
t.Fatalf("expected %s, got %s", s, r.SessionId)
}
if r.Typ != 16 {
t.Fatalf("expected 16, got %d", r.Typ)
}
if DebugCap & 1 > 0 {
if r.Label != "internal state" {
t.Fatalf("expected 'internal_state', got '%s'", r.Label)
}
}
}
func TestDebugDbKeyInfoRestore(t *testing.T) {
s := "bar"
b := []byte{db.DATATYPE_USERDATA}
b = append(b, []byte(s)...)
k := common.ToBytes(common.DATA_ACTIVE_SYM)
b = append(b, k...)
r, err := ToKeyInfo(b, s)
if err != nil {
t.Fatal(err)
}
if r.SessionId != s {
t.Fatalf("expected %s, got %s", s, r.SessionId)
}
if r.Typ != 32 {
t.Fatalf("expected 32, got %d", r.Typ)
}
if DebugCap & 1 > 0 {
if r.Label != "active sym" {
t.Fatalf("expected 'active sym', got '%s'", r.Label)
}
}
}

View File

@@ -0,0 +1,89 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"path"
"git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/debug"
"git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/logging"
)
var (
logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration")
)
func init() {
initializers.LoadEnvVariables()
}
func formatItem(k []byte, v []byte) (string, error) {
o, err := debug.FromKey(k)
if err != nil {
return "", err
}
s := fmt.Sprintf("%vValue: %v\n\n", o, string(v))
return s, nil
}
func main() {
config.LoadConfig()
var dbDir string
var sessionId string
var database string
var engineDebug bool
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
flag.StringVar(&database, "db", "gdbm", "database to be used")
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.Parse()
ctx := context.Background()
ctx = context.WithValue(ctx, "SessionId", sessionId)
ctx = context.WithValue(ctx, "Database", database)
resourceDir := scriptDir
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
store, err := menuStorageService.GetUserdataDb(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "get userdata db: %v\n", err.Error())
os.Exit(1)
}
store.SetPrefix(db.DATATYPE_USERDATA)
d, err := store.Dump(ctx, []byte(sessionId))
if err != nil {
fmt.Fprintf(os.Stderr, "store dump fail: %v\n", err.Error())
os.Exit(1)
}
for true {
k, v := d.Next(ctx)
if k == nil {
break
}
r, err := formatItem(k, v)
if err != nil {
fmt.Fprintf(os.Stderr, "format db item error: %v", err)
os.Exit(1)
}
fmt.Printf(r)
}
err = store.Close()
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
}

View File

@@ -0,0 +1,79 @@
package main
import (
"context"
"crypto/sha1"
"flag"
"fmt"
"os"
"path"
"git.defalsify.org/vise.git/logging"
"git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/common"
)
var (
logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration")
)
func init() {
initializers.LoadEnvVariables()
}
func main() {
config.LoadConfig()
var dbDir string
var sessionId string
var database string
var engineDebug bool
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
flag.StringVar(&database, "db", "gdbm", "database to be used")
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.Parse()
ctx := context.Background()
ctx = context.WithValue(ctx, "SessionId", sessionId)
ctx = context.WithValue(ctx, "Database", database)
resourceDir := scriptDir
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
store, err := menuStorageService.GetUserdataDb(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
userStore := common.UserDataStore{store}
h := sha1.New()
h.Write([]byte(sessionId))
address := h.Sum(nil)
addressString := fmt.Sprintf("%x", address)
err = userStore.WriteEntry(ctx, sessionId, common.DATA_PUBLIC_KEY, []byte(addressString))
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
err = userStore.WriteEntry(ctx, addressString, common.DATA_PUBLIC_KEY_REVERSE, []byte(sessionId))
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
err = store.Close()
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
}

28
doc/data.md Normal file
View File

@@ -0,0 +1,28 @@
# Internals
## Version
This document describes component versions:
* `urdt-ussd` `v0.5.0-beta`
* `go-vise` `v0.2.2`
## User profile data
All user profile items are stored under keys matching the user's session id, prefixed with the 8-bit value `git.defalsify.org/vise.git/db.DATATYPE_USERDATA` (32), and followed with a 16-big big-endian value subprefix.
For example, given the sessionId `+254123` and the key `git.grassecon.net/urdt-ussd/common.DATA_PUBLIC_KEY` (2) will be stored under the key:
```
0x322b3235343132330002
prefix sessionid subprefix
32 2b323534313233 0002
```
### Sub-prefixes
All sub-prefixes are defined as constants in the `git.grassecon.net/urdt-ussd/common` package. The constant names have the prefix `DATA_`
Please refer to inline godoc documentation for the `git.grassecon.net/urdt-ussd/common` package for details on each data item.

4
go.mod
View File

@@ -3,7 +3,7 @@ module git.grassecon.net/urdt/ussd
go 1.23.0 go 1.23.0
require ( require (
git.defalsify.org/vise.git v0.2.1-0.20241122120231-9e9ee5bdfa7a git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d
github.com/alecthomas/assert/v2 v2.2.2 github.com/alecthomas/assert/v2 v2.2.2
github.com/gofrs/uuid v4.4.0+incompatible github.com/gofrs/uuid v4.4.0+incompatible
github.com/grassrootseconomics/eth-custodial v1.3.0-beta github.com/grassrootseconomics/eth-custodial v1.3.0-beta
@@ -11,6 +11,7 @@ require (
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/peteole/testdata-loader v0.3.0 github.com/peteole/testdata-loader v0.3.0
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
golang.org/x/crypto v0.27.0
gopkg.in/leonelquinteros/gotext.v1 v1.3.1 gopkg.in/leonelquinteros/gotext.v1 v1.3.1
) )
@@ -32,7 +33,6 @@ require (
github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.2 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/sync v0.8.0 // indirect golang.org/x/sync v0.8.0 // indirect
golang.org/x/text v0.18.0 // indirect golang.org/x/text v0.18.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect

4
go.sum
View File

@@ -1,5 +1,5 @@
git.defalsify.org/vise.git v0.2.1-0.20241122120231-9e9ee5bdfa7a h1:LvGKktk0kUnuRN3nF9r15D8OoV0sFaMmQr52kGq2gtE= git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d h1:bPAOVZOX4frSGhfOdcj7kc555f8dc9DmMd2YAyC2AMw=
git.defalsify.org/vise.git v0.2.1-0.20241122120231-9e9ee5bdfa7a/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck= git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= 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/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 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g=

View File

@@ -55,6 +55,9 @@ func(f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error)
} }
f.hn = f.hn.WithPersister(rqs.Storage.Persister) f.hn = f.hn.WithPersister(rqs.Storage.Persister)
defer func() {
f.hn.Exit()
}()
eni := f.GetEngine(rqs.Config, f.rs, rqs.Storage.Persister) eni := f.GetEngine(rqs.Config, f.rs, rqs.Storage.Persister)
en, ok := eni.(*engine.DefaultEngine) en, ok := eni.(*engine.DefaultEngine)
if !ok { if !ok {

View File

@@ -2,6 +2,7 @@ package handlers
import ( import (
"context" "context"
"strings"
"git.defalsify.org/vise.git/asm" "git.defalsify.org/vise.git/asm"
"git.defalsify.org/vise.git/db" "git.defalsify.org/vise.git/db"
@@ -64,7 +65,11 @@ func (ls *LocalHandlerService) SetDataStore(db *db.Db) {
} }
func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceInterface) (*ussd.Handlers, error) { func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceInterface) (*ussd.Handlers, error) {
ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore, ls.AdminStore, accountService) replaceSeparatorFunc := func(input string) string {
return strings.ReplaceAll(input, ":", ls.Cfg.MenuSeparator)
}
ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore, ls.AdminStore, accountService, replaceSeparatorFunc)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -111,7 +116,7 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceIn
ls.DbRs.AddLocalFunc("set_voucher", ussdHandlers.SetVoucher) ls.DbRs.AddLocalFunc("set_voucher", ussdHandlers.SetVoucher)
ls.DbRs.AddLocalFunc("get_voucher_details", ussdHandlers.GetVoucherDetails) ls.DbRs.AddLocalFunc("get_voucher_details", ussdHandlers.GetVoucherDetails)
ls.DbRs.AddLocalFunc("reset_valid_pin", ussdHandlers.ResetValidPin) ls.DbRs.AddLocalFunc("reset_valid_pin", ussdHandlers.ResetValidPin)
ls.DbRs.AddLocalFunc("check_pin_mismatch", ussdHandlers.CheckPinMisMatch) ls.DbRs.AddLocalFunc("check_pin_mismatch", ussdHandlers.CheckBlockedNumPinMisMatch)
ls.DbRs.AddLocalFunc("validate_blocked_number", ussdHandlers.ValidateBlockedNumber) ls.DbRs.AddLocalFunc("validate_blocked_number", ussdHandlers.ValidateBlockedNumber)
ls.DbRs.AddLocalFunc("retrieve_blocked_number", ussdHandlers.RetrieveBlockedNumber) ls.DbRs.AddLocalFunc("retrieve_blocked_number", ussdHandlers.RetrieveBlockedNumber)
ls.DbRs.AddLocalFunc("reset_unregistered_number", ussdHandlers.ResetUnregisteredNumber) ls.DbRs.AddLocalFunc("reset_unregistered_number", ussdHandlers.ResetUnregisteredNumber)
@@ -121,6 +126,8 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceIn
ls.DbRs.AddLocalFunc("check_transactions", ussdHandlers.CheckTransactions) ls.DbRs.AddLocalFunc("check_transactions", ussdHandlers.CheckTransactions)
ls.DbRs.AddLocalFunc("get_transactions", ussdHandlers.GetTransactionsList) ls.DbRs.AddLocalFunc("get_transactions", ussdHandlers.GetTransactionsList)
ls.DbRs.AddLocalFunc("view_statement", ussdHandlers.ViewTransactionStatement) ls.DbRs.AddLocalFunc("view_statement", ussdHandlers.ViewTransactionStatement)
ls.DbRs.AddLocalFunc("update_all_profile_items", ussdHandlers.UpdateAllProfileItems)
ls.DbRs.AddLocalFunc("set_back", ussdHandlers.SetBack)
return ussdHandlers, nil return ussdHandlers, nil
} }

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -5,15 +5,18 @@ import (
"fmt" "fmt"
"log" "log"
"path" "path"
"strings"
"testing" "testing"
"git.defalsify.org/vise.git/cache"
"git.defalsify.org/vise.git/lang" "git.defalsify.org/vise.git/lang"
"git.defalsify.org/vise.git/persist" "git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/resource"
"git.defalsify.org/vise.git/state" "git.defalsify.org/vise.git/state"
"git.grassecon.net/urdt/ussd/internal/storage" dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
"git.grassecon.net/urdt/ussd/internal/testutil/mocks" "git.grassecon.net/urdt/ussd/internal/testutil/mocks"
"git.grassecon.net/urdt/ussd/internal/testutil/testservice" "git.grassecon.net/urdt/ussd/internal/testutil/testservice"
"git.grassecon.net/urdt/ussd/internal/utils"
"git.grassecon.net/urdt/ussd/models" "git.grassecon.net/urdt/ussd/models"
"git.grassecon.net/urdt/ussd/common" "git.grassecon.net/urdt/ussd/common"
@@ -22,6 +25,7 @@ import (
testdataloader "github.com/peteole/testdata-loader" testdataloader "github.com/peteole/testdata-loader"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
visedb "git.defalsify.org/vise.git/db"
memdb "git.defalsify.org/vise.git/db/mem" memdb "git.defalsify.org/vise.git/db/mem"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
) )
@@ -31,6 +35,11 @@ var (
flagsPath = path.Join(baseDir, "services", "registration", "pp.csv") flagsPath = path.Join(baseDir, "services", "registration", "pp.csv")
) )
// mockReplaceSeparator function
var mockReplaceSeparator = func(input string) string {
return strings.ReplaceAll(input, ":", ": ")
}
// InitializeTestStore sets up and returns an in-memory database and store. // InitializeTestStore sets up and returns an in-memory database and store.
func InitializeTestStore(t *testing.T) (context.Context, *common.UserDataStore) { func InitializeTestStore(t *testing.T) (context.Context, *common.UserDataStore) {
ctx := context.Background() ctx := context.Background()
@@ -50,13 +59,14 @@ func InitializeTestStore(t *testing.T) (context.Context, *common.UserDataStore)
return ctx, store return ctx, store
} }
func InitializeTestSubPrefixDb(t *testing.T, ctx context.Context) *storage.SubPrefixDb { func InitializeTestSubPrefixDb(t *testing.T, ctx context.Context) *dbstorage.SubPrefixDb {
db := memdb.NewMemDb() db := memdb.NewMemDb()
err := db.Connect(ctx, "") err := db.Connect(ctx, "")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
spdb := storage.NewSubPrefixDb(db, []byte("vouchers")) prefix := common.ToBytes(visedb.DATATYPE_USERDATA)
spdb := dbstorage.NewSubPrefixDb(db, prefix)
return spdb return spdb
} }
@@ -65,12 +75,15 @@ func TestNewHandlers(t *testing.T) {
_, store := InitializeTestStore(t) _, store := InitializeTestStore(t)
fm, err := NewFlagManager(flagsPath) fm, err := NewFlagManager(flagsPath)
accountService := testservice.TestAccountService{}
if err != nil { if err != nil {
t.Logf(err.Error()) log.Fatal(err)
} }
accountService := testservice.TestAccountService{}
// Test case for valid UserDataStore
t.Run("Valid UserDataStore", func(t *testing.T) { t.Run("Valid UserDataStore", func(t *testing.T) {
handlers, err := NewHandlers(fm.parser, store, nil, &accountService) handlers, err := NewHandlers(fm.parser, store, nil, &accountService, mockReplaceSeparator)
if err != nil { if err != nil {
t.Fatalf("expected no error, got %v", err) t.Fatalf("expected no error, got %v", err)
} }
@@ -80,23 +93,130 @@ func TestNewHandlers(t *testing.T) {
if handlers.userdataStore == nil { if handlers.userdataStore == nil {
t.Fatal("expected userdataStore to be set in handlers") t.Fatal("expected userdataStore to be set in handlers")
} }
if handlers.ReplaceSeparatorFunc == nil {
t.Fatal("expected ReplaceSeparatorFunc to be set in handlers")
}
// Test ReplaceSeparatorFunc functionality
input := "1:Menu item"
expectedOutput := "1: Menu item"
if handlers.ReplaceSeparatorFunc(input) != expectedOutput {
t.Fatalf("ReplaceSeparatorFunc function did not return expected output: got %v, want %v", handlers.ReplaceSeparatorFunc(input), expectedOutput)
}
}) })
// Test case for nil userdataStore // Test case for nil UserDataStore
t.Run("Nil UserDataStore", func(t *testing.T) { t.Run("Nil UserDataStore", func(t *testing.T) {
handlers, err := NewHandlers(fm.parser, nil, nil, &accountService) handlers, err := NewHandlers(fm.parser, nil, nil, &accountService, mockReplaceSeparator)
if err == nil { if err == nil {
t.Fatal("expected an error, got none") t.Fatal("expected an error, got none")
} }
if handlers != nil { if handlers != nil {
t.Fatal("expected handlers to be nil") t.Fatal("expected handlers to be nil")
} }
if err.Error() != "cannot create handler with nil userdata store" { expectedError := "cannot create handler with nil userdata store"
t.Fatalf("expected specific error, got %v", err) if err.Error() != expectedError {
t.Fatalf("expected error '%s', got '%v'", expectedError, err)
} }
}) })
} }
func TestInit(t *testing.T) {
sessionId := "session123"
ctx, store := InitializeTestStore(t)
ctx = context.WithValue(ctx, "SessionId", sessionId)
fm, err := NewFlagManager(flagsPath)
if err != nil {
t.Fatal(err.Error())
}
adminstore, err := utils.NewAdminStore(ctx, "admin_numbers")
if err != nil {
t.Fatal(err.Error())
}
st := state.NewState(128)
ca := cache.NewCache()
flag_admin_privilege, _ := fm.GetFlag("flag_admin_privilege")
tests := []struct {
name string
setup func() (*Handlers, context.Context)
input []byte
expectedResult resource.Result
}{
{
name: "Handler not ready",
setup: func() (*Handlers, context.Context) {
return &Handlers{}, ctx
},
input: []byte("1"),
expectedResult: resource.Result{},
},
{
name: "State and memory initialization",
setup: func() (*Handlers, context.Context) {
pe := persist.NewPersister(store).WithSession(sessionId).WithContent(st, ca)
h := &Handlers{
flagManager: fm.parser,
adminstore: adminstore,
pe: pe,
}
return h, context.WithValue(ctx, "SessionId", sessionId)
},
input: []byte("1"),
expectedResult: resource.Result{
FlagReset: []uint32{flag_admin_privilege},
},
},
{
name: "Non-admin session initialization",
setup: func() (*Handlers, context.Context) {
pe := persist.NewPersister(store).WithSession("0712345678").WithContent(st, ca)
h := &Handlers{
flagManager: fm.parser,
adminstore: adminstore,
pe: pe,
}
return h, context.WithValue(context.Background(), "SessionId", "0712345678")
},
input: []byte("1"),
expectedResult: resource.Result{
FlagReset: []uint32{flag_admin_privilege},
},
},
{
name: "Move to top node on empty input",
setup: func() (*Handlers, context.Context) {
pe := persist.NewPersister(store).WithSession(sessionId).WithContent(st, ca)
h := &Handlers{
flagManager: fm.parser,
adminstore: adminstore,
pe: pe,
}
st.Code = []byte("some pending bytecode")
return h, context.WithValue(ctx, "SessionId", sessionId)
},
input: []byte(""),
expectedResult: resource.Result{
FlagReset: []uint32{flag_admin_privilege},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h, testCtx := tt.setup()
res, err := h.Init(testCtx, "", tt.input)
assert.NoError(t, err, "Unexpected error occurred")
assert.Equal(t, res, tt.expectedResult, "Expected result should match actual result")
})
}
}
func TestCreateAccount(t *testing.T) { func TestCreateAccount(t *testing.T) {
sessionId := "session123" sessionId := "session123"
ctx, store := InitializeTestStore(t) ctx, store := InitializeTestStore(t)
@@ -179,11 +299,14 @@ func TestSaveFirstname(t *testing.T) {
fm, _ := NewFlagManager(flagsPath) fm, _ := NewFlagManager(flagsPath)
flag_allow_update, _ := fm.GetFlag("flag_allow_update") flag_allow_update, _ := fm.GetFlag("flag_allow_update")
flag_firstname_set, _ := fm.GetFlag("flag_firstname_set")
// Set the flag in the State // Set the flag in the State
mockState := state.NewState(16) mockState := state.NewState(128)
mockState.SetFlag(flag_allow_update) mockState.SetFlag(flag_allow_update)
expectedResult := resource.Result{}
// Define test data // Define test data
firstName := "John" firstName := "John"
@@ -191,6 +314,8 @@ func TestSaveFirstname(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
expectedResult.FlagSet = []uint32{flag_firstname_set}
// Create the Handlers instance with the mock store // Create the Handlers instance with the mock store
h := &Handlers{ h := &Handlers{
userdataStore: store, userdataStore: store,
@@ -203,7 +328,7 @@ func TestSaveFirstname(t *testing.T) {
// Assert results // Assert results
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, resource.Result{}, res) assert.Equal(t, expectedResult, res)
// Verify that the DATA_FIRST_NAME entry has been updated with the temporary value // Verify that the DATA_FIRST_NAME entry has been updated with the temporary value
storedFirstName, _ := store.ReadEntry(ctx, sessionId, common.DATA_FIRST_NAME) storedFirstName, _ := store.ReadEntry(ctx, sessionId, common.DATA_FIRST_NAME)
@@ -218,11 +343,16 @@ func TestSaveFamilyname(t *testing.T) {
fm, _ := NewFlagManager(flagsPath) fm, _ := NewFlagManager(flagsPath)
flag_allow_update, _ := fm.GetFlag("flag_allow_update") flag_allow_update, _ := fm.GetFlag("flag_allow_update")
flag_firstname_set, _ := fm.GetFlag("flag_familyname_set")
// Set the flag in the State // Set the flag in the State
mockState := state.NewState(16) mockState := state.NewState(128)
mockState.SetFlag(flag_allow_update) mockState.SetFlag(flag_allow_update)
expectedResult := resource.Result{}
expectedResult.FlagSet = []uint32{flag_firstname_set}
// Define test data // Define test data
familyName := "Doeee" familyName := "Doeee"
@@ -242,7 +372,7 @@ func TestSaveFamilyname(t *testing.T) {
// Assert results // Assert results
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, resource.Result{}, res) assert.Equal(t, expectedResult, res)
// Verify that the DATA_FAMILY_NAME entry has been updated with the temporary value // Verify that the DATA_FAMILY_NAME entry has been updated with the temporary value
storedFamilyName, _ := store.ReadEntry(ctx, sessionId, common.DATA_FAMILY_NAME) storedFamilyName, _ := store.ReadEntry(ctx, sessionId, common.DATA_FAMILY_NAME)
@@ -257,11 +387,14 @@ func TestSaveYoB(t *testing.T) {
fm, _ := NewFlagManager(flagsPath) fm, _ := NewFlagManager(flagsPath)
flag_allow_update, _ := fm.GetFlag("flag_allow_update") flag_allow_update, _ := fm.GetFlag("flag_allow_update")
flag_yob_set, _ := fm.GetFlag("flag_yob_set")
// Set the flag in the State // Set the flag in the State
mockState := state.NewState(16) mockState := state.NewState(108)
mockState.SetFlag(flag_allow_update) mockState.SetFlag(flag_allow_update)
expectedResult := resource.Result{}
// Define test data // Define test data
yob := "1980" yob := "1980"
@@ -269,6 +402,8 @@ func TestSaveYoB(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
expectedResult.FlagSet = []uint32{flag_yob_set}
// Create the Handlers instance with the mock store // Create the Handlers instance with the mock store
h := &Handlers{ h := &Handlers{
userdataStore: store, userdataStore: store,
@@ -281,7 +416,7 @@ func TestSaveYoB(t *testing.T) {
// Assert results // Assert results
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, resource.Result{}, res) assert.Equal(t, expectedResult, res)
// Verify that the DATA_YOB entry has been updated with the temporary value // Verify that the DATA_YOB entry has been updated with the temporary value
storedYob, _ := store.ReadEntry(ctx, sessionId, common.DATA_YOB) storedYob, _ := store.ReadEntry(ctx, sessionId, common.DATA_YOB)
@@ -296,11 +431,14 @@ func TestSaveLocation(t *testing.T) {
fm, _ := NewFlagManager(flagsPath) fm, _ := NewFlagManager(flagsPath)
flag_allow_update, _ := fm.GetFlag("flag_allow_update") flag_allow_update, _ := fm.GetFlag("flag_allow_update")
flag_location_set, _ := fm.GetFlag("flag_location_set")
// Set the flag in the State // Set the flag in the State
mockState := state.NewState(16) mockState := state.NewState(108)
mockState.SetFlag(flag_allow_update) mockState.SetFlag(flag_allow_update)
expectedResult := resource.Result{}
// Define test data // Define test data
location := "Kilifi" location := "Kilifi"
@@ -308,6 +446,8 @@ func TestSaveLocation(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
expectedResult.FlagSet = []uint32{flag_location_set}
// Create the Handlers instance with the mock store // Create the Handlers instance with the mock store
h := &Handlers{ h := &Handlers{
userdataStore: store, userdataStore: store,
@@ -320,7 +460,7 @@ func TestSaveLocation(t *testing.T) {
// Assert results // Assert results
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, resource.Result{}, res) assert.Equal(t, expectedResult, res)
// Verify that the DATA_LOCATION entry has been updated with the temporary value // Verify that the DATA_LOCATION entry has been updated with the temporary value
storedLocation, _ := store.ReadEntry(ctx, sessionId, common.DATA_LOCATION) storedLocation, _ := store.ReadEntry(ctx, sessionId, common.DATA_LOCATION)
@@ -335,11 +475,14 @@ func TestSaveOfferings(t *testing.T) {
fm, _ := NewFlagManager(flagsPath) fm, _ := NewFlagManager(flagsPath)
flag_allow_update, _ := fm.GetFlag("flag_allow_update") flag_allow_update, _ := fm.GetFlag("flag_allow_update")
flag_offerings_set, _ := fm.GetFlag("flag_offerings_set")
// Set the flag in the State // Set the flag in the State
mockState := state.NewState(16) mockState := state.NewState(108)
mockState.SetFlag(flag_allow_update) mockState.SetFlag(flag_allow_update)
expectedResult := resource.Result{}
// Define test data // Define test data
offerings := "Bananas" offerings := "Bananas"
@@ -347,6 +490,8 @@ func TestSaveOfferings(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
expectedResult.FlagSet = []uint32{flag_offerings_set}
// Create the Handlers instance with the mock store // Create the Handlers instance with the mock store
h := &Handlers{ h := &Handlers{
userdataStore: store, userdataStore: store,
@@ -359,7 +504,7 @@ func TestSaveOfferings(t *testing.T) {
// Assert results // Assert results
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, resource.Result{}, res) assert.Equal(t, expectedResult, res)
// Verify that the DATA_OFFERINGS entry has been updated with the temporary value // Verify that the DATA_OFFERINGS entry has been updated with the temporary value
storedOfferings, _ := store.ReadEntry(ctx, sessionId, common.DATA_OFFERINGS) storedOfferings, _ := store.ReadEntry(ctx, sessionId, common.DATA_OFFERINGS)
@@ -374,9 +519,10 @@ func TestSaveGender(t *testing.T) {
fm, _ := NewFlagManager(flagsPath) fm, _ := NewFlagManager(flagsPath)
flag_allow_update, _ := fm.GetFlag("flag_allow_update") flag_allow_update, _ := fm.GetFlag("flag_allow_update")
flag_gender_set, _ := fm.GetFlag("flag_gender_set")
// Set the flag in the State // Set the flag in the State
mockState := state.NewState(16) mockState := state.NewState(108)
mockState.SetFlag(flag_allow_update) mockState.SetFlag(flag_allow_update)
// Define test cases // Define test cases
@@ -420,12 +566,16 @@ func TestSaveGender(t *testing.T) {
flagManager: fm.parser, flagManager: fm.parser,
} }
expectedResult := resource.Result{}
// Call the method // Call the method
res, err := h.SaveGender(ctx, "save_gender", tt.input) res, err := h.SaveGender(ctx, "save_gender", tt.input)
expectedResult.FlagSet = []uint32{flag_gender_set}
// Assert results // Assert results
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, resource.Result{}, res) assert.Equal(t, expectedResult, res)
// Verify that the DATA_GENDER entry has been updated with the temporary value // Verify that the DATA_GENDER entry has been updated with the temporary value
storedGender, _ := store.ReadEntry(ctx, sessionId, common.DATA_GENDER) storedGender, _ := store.ReadEntry(ctx, sessionId, common.DATA_GENDER)
@@ -897,7 +1047,14 @@ func TestAuthorize(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(accountPIN)) // Hash the PIN
hashedPIN, err := common.HashPIN(accountPIN)
if err != nil {
logg.ErrorCtxf(ctx, "failed to hash temporaryPin", "error", err)
t.Fatal(err)
}
err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(hashedPIN))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -1349,59 +1506,6 @@ func TestQuit(t *testing.T) {
} }
} }
func TestIsValidPIN(t *testing.T) {
tests := []struct {
name string
pin string
expected bool
}{
{
name: "Valid PIN with 4 digits",
pin: "1234",
expected: true,
},
{
name: "Valid PIN with leading zeros",
pin: "0001",
expected: true,
},
{
name: "Invalid PIN with less than 4 digits",
pin: "123",
expected: false,
},
{
name: "Invalid PIN with more than 4 digits",
pin: "12345",
expected: false,
},
{
name: "Invalid PIN with letters",
pin: "abcd",
expected: false,
},
{
name: "Invalid PIN with special characters",
pin: "12@#",
expected: false,
},
{
name: "Empty PIN",
pin: "",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := isValidPIN(tt.pin)
if actual != tt.expected {
t.Errorf("isValidPIN(%q) = %v; expected %v", tt.pin, actual, tt.expected)
}
})
}
}
func TestValidateAmount(t *testing.T) { func TestValidateAmount(t *testing.T) {
fm, err := NewFlagManager(flagsPath) fm, err := NewFlagManager(flagsPath)
if err != nil { if err != nil {
@@ -1498,10 +1602,10 @@ func TestValidateRecipient(t *testing.T) {
}{ }{
{ {
name: "Test with invalid recepient", name: "Test with invalid recepient",
input: []byte("9234adf5"), input: []byte("7?1234"),
expectedResult: resource.Result{ expectedResult: resource.Result{
FlagSet: []uint32{flag_invalid_recipient}, FlagSet: []uint32{flag_invalid_recipient},
Content: "9234adf5", Content: "7?1234",
}, },
}, },
{ {
@@ -1517,22 +1621,40 @@ func TestValidateRecipient(t *testing.T) {
input: []byte("0711223344"), input: []byte("0711223344"),
expectedResult: resource.Result{}, expectedResult: resource.Result{},
}, },
{
name: "Test with address",
input: []byte("0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9"),
expectedResult: resource.Result{},
},
{
name: "Test with alias recepient",
input: []byte("alias123"),
expectedResult: resource.Result{},
},
} }
// store a public key for the valid recipient // store a public key for the valid recipient
err = store.WriteEntry(ctx, "0711223344", common.DATA_PUBLIC_KEY, []byte(publicKey)) err = store.WriteEntry(ctx, "+254711223344", common.DATA_PUBLIC_KEY, []byte(publicKey))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
mockAccountService := new(mocks.MockAccountService)
// Create the Handlers instance // Create the Handlers instance
h := &Handlers{ h := &Handlers{
flagManager: fm.parser, flagManager: fm.parser,
userdataStore: store, userdataStore: store,
accountService: mockAccountService,
} }
aliasResponse := &dataserviceapi.AliasAddress{
Address: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9",
}
mockAccountService.On("CheckAliasAddress", string(tt.input)).Return(aliasResponse, nil)
// Call the method // Call the method
res, err := h.ValidateRecipient(ctx, "validate_recepient", tt.input) res, err := h.ValidateRecipient(ctx, "validate_recepient", tt.input)
@@ -1564,7 +1686,7 @@ func TestCheckBalance(t *testing.T) {
publicKey: "0X98765432109", publicKey: "0X98765432109",
activeSym: "ETH", activeSym: "ETH",
activeBal: "1.5", activeBal: "1.5",
expectedResult: resource.Result{Content: "Balance: 1.5 ETH\n"}, expectedResult: resource.Result{Content: "Balance: 1.50 ETH\n"},
expectError: false, expectError: false,
}, },
} }
@@ -1630,7 +1752,7 @@ func TestGetProfile(t *testing.T) {
result: resource.Result{ result: resource.Result{
Content: fmt.Sprintf( Content: fmt.Sprintf(
"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n", "Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
"John Doee", "Male", "48", "Kilifi", "Bananas", "John Doee", "Male", "49", "Kilifi", "Bananas",
), ),
}, },
}, },
@@ -1642,7 +1764,7 @@ func TestGetProfile(t *testing.T) {
result: resource.Result{ result: resource.Result{
Content: fmt.Sprintf( Content: fmt.Sprintf(
"Jina: %s\nJinsia: %s\nUmri: %s\nEneo: %s\nUnauza: %s\n", "Jina: %s\nJinsia: %s\nUmri: %s\nEneo: %s\nUnauza: %s\n",
"John Doee", "Male", "48", "Kilifi", "Bananas", "John Doee", "Male", "49", "Kilifi", "Bananas",
), ),
}, },
}, },
@@ -1654,7 +1776,7 @@ func TestGetProfile(t *testing.T) {
result: resource.Result{ result: resource.Result{
Content: fmt.Sprintf( Content: fmt.Sprintf(
"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n", "Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
"John Doee", "Male", "48", "Kilifi", "Bananas", "John Doee", "Male", "49", "Kilifi", "Bananas",
), ),
}, },
}, },
@@ -1919,7 +2041,7 @@ func TestCheckVouchers(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
// Read voucher sym data from the store // Read voucher sym data from the store
voucherData, err := spdb.Get(ctx, []byte("sym")) voucherData, err := spdb.Get(ctx, common.ToBytes(common.DATA_VOUCHER_SYMBOLS))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -1932,26 +2054,31 @@ func TestCheckVouchers(t *testing.T) {
func TestGetVoucherList(t *testing.T) { func TestGetVoucherList(t *testing.T) {
sessionId := "session123" sessionId := "session123"
ctx := context.WithValue(context.Background(), "SessionId", sessionId) ctx := context.WithValue(context.Background(), "SessionId", sessionId)
spdb := InitializeTestSubPrefixDb(t, ctx) spdb := InitializeTestSubPrefixDb(t, ctx)
// Initialize Handlers
h := &Handlers{ h := &Handlers{
prefixDb: spdb, prefixDb: spdb,
ReplaceSeparatorFunc: mockReplaceSeparator,
} }
expectedSym := []byte("1:SRF\n2:MILO") mockSyms := []byte("1:SRF\n2:MILO")
// Put voucher sym data from the store // Put voucher sym data from the store
err := spdb.Put(ctx, []byte("sym"), expectedSym) err := spdb.Put(ctx, common.ToBytes(common.DATA_VOUCHER_SYMBOLS), mockSyms)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
expectedSyms := []byte("1: SRF\n2: MILO")
res, err := h.GetVoucherList(ctx, "", []byte("")) res, err := h.GetVoucherList(ctx, "", []byte(""))
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, res.Content, string(expectedSym)) assert.Equal(t, res.Content, string(expectedSyms))
} }
func TestViewVoucher(t *testing.T) { func TestViewVoucher(t *testing.T) {
@@ -1973,16 +2100,16 @@ func TestViewVoucher(t *testing.T) {
} }
// Define mock voucher data // Define mock voucher data
mockData := map[string][]byte{ mockData := map[common.DataTyp][]byte{
"sym": []byte("1:SRF\n2:MILO"), common.DATA_VOUCHER_SYMBOLS: []byte("1:SRF\n2:MILO"),
"bal": []byte("1:100\n2:200"), common.DATA_VOUCHER_BALANCES: []byte("1:100\n2:200"),
"deci": []byte("1:6\n2:4"), common.DATA_VOUCHER_DECIMALS: []byte("1:6\n2:4"),
"addr": []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"), common.DATA_VOUCHER_ADDRESSES: []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"),
} }
// Put the data // Put the data
for key, value := range mockData { for key, value := range mockData {
err = spdb.Put(ctx, []byte(key), []byte(value)) err = spdb.Put(ctx, []byte(common.ToBytes(key)), []byte(value))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -1990,7 +2117,7 @@ func TestViewVoucher(t *testing.T) {
res, err := h.ViewVoucher(ctx, "view_voucher", []byte("1")) res, err := h.ViewVoucher(ctx, "view_voucher", []byte("1"))
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, res.Content, "SRF\n100") assert.Equal(t, res.Content, "Symbol: SRF\nBalance: 100")
} }
func TestSetVoucher(t *testing.T) { func TestSetVoucher(t *testing.T) {
@@ -2024,3 +2151,42 @@ func TestSetVoucher(t *testing.T) {
assert.Equal(t, string(tempData.TokenSymbol), res.Content) assert.Equal(t, string(tempData.TokenSymbol), res.Content)
} }
func TestGetVoucherDetails(t *testing.T) {
ctx, store := InitializeTestStore(t)
fm, err := NewFlagManager(flagsPath)
if err != nil {
t.Logf(err.Error())
}
mockAccountService := new(mocks.MockAccountService)
sessionId := "session123"
ctx = context.WithValue(ctx, "SessionId", sessionId)
expectedResult := resource.Result{}
tokA_AAddress := "0x0000000000000000000000000000000000000000"
h := &Handlers{
userdataStore: store,
flagManager: fm.parser,
accountService: mockAccountService,
}
err = store.WriteEntry(ctx, sessionId, common.DATA_ACTIVE_ADDRESS, []byte(tokA_AAddress))
if err != nil {
t.Fatal(err)
}
tokenDetails := &models.VoucherDataResult{
TokenName: "Token A",
TokenSymbol: "TOKA",
TokenLocation: "Kilifi,Kenya",
TokenCommodity: "Farming",
}
expectedResult.Content = fmt.Sprintf(
"Name: %s\nSymbol: %s\nCommodity: %s\nLocation: %s", tokenDetails.TokenName, tokenDetails.TokenSymbol, tokenDetails.TokenCommodity, tokenDetails.TokenLocation,
)
mockAccountService.On("VoucherData", string(tokA_AAddress)).Return(tokenDetails, nil)
res, err := h.GetVoucherDetails(ctx, "SessionId", []byte(""))
assert.NoError(t, err)
assert.Equal(t, expectedResult, res)
}

119
internal/http/at/parse.go Normal file
View File

@@ -0,0 +1,119 @@
package at
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"git.grassecon.net/urdt/ussd/common"
"git.grassecon.net/urdt/ussd/internal/handlers"
)
type ATRequestParser struct {
}
func (arp *ATRequestParser) GetSessionId(ctx context.Context, rq any) (string, error) {
rqv, ok := rq.(*http.Request)
if !ok {
logg.Warnf("got an invalid request", "req", rq)
return "", handlers.ErrInvalidRequest
}
// Capture body (if any) for logging
body, err := io.ReadAll(rqv.Body)
if err != nil {
logg.Warnf("failed to read request body", "err", err)
return "", fmt.Errorf("failed to read request body: %v", err)
}
// Reset the body for further reading
rqv.Body = io.NopCloser(bytes.NewReader(body))
// Log the body as JSON
bodyLog := map[string]string{"body": string(body)}
logBytes, err := json.Marshal(bodyLog)
if err != nil {
logg.Warnf("failed to marshal request body", "err", err)
} else {
decodedStr := string(logBytes)
sessionId, err := extractATSessionId(decodedStr)
if err != nil {
ctx = context.WithValue(ctx, "AT-SessionId", sessionId)
}
logg.DebugCtxf(ctx, "Received request:", decodedStr)
}
if err := rqv.ParseForm(); err != nil {
logg.Warnf("failed to parse form data", "err", err)
return "", fmt.Errorf("failed to parse form data: %v", err)
}
phoneNumber := rqv.FormValue("phoneNumber")
if phoneNumber == "" {
return "", fmt.Errorf("no phone number found")
}
formattedNumber, err := common.FormatPhoneNumber(phoneNumber)
if err != nil {
logg.Warnf("failed to format phone number", "err", err)
return "", fmt.Errorf("failed to format number")
}
return formattedNumber, 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 parseQueryParams(query string) map[string]string {
params := make(map[string]string)
queryParams := strings.Split(query, "&")
for _, param := range queryParams {
// Split each key-value pair by '='
parts := strings.SplitN(param, "=", 2)
if len(parts) == 2 {
params[parts[0]] = parts[1]
}
}
return params
}
func extractATSessionId(decodedStr string) (string, error) {
var data map[string]string
err := json.Unmarshal([]byte(decodedStr), &data)
if err != nil {
logg.Errorf("Error unmarshalling JSON: %v", err)
return "", nil
}
decodedBody, err := url.QueryUnescape(data["body"])
if err != nil {
logg.Errorf("Error URL-decoding body: %v", err)
return "", nil
}
params := parseQueryParams(decodedBody)
sessionId := params["sessionId"]
return sessionId, nil
}

View File

@@ -1,19 +1,25 @@
package http package at
import ( import (
"io" "io"
"net/http" "net/http"
"git.defalsify.org/vise.git/logging"
"git.grassecon.net/urdt/ussd/internal/handlers" "git.grassecon.net/urdt/ussd/internal/handlers"
httpserver "git.grassecon.net/urdt/ussd/internal/http"
)
var (
logg = logging.NewVanilla().WithDomain("atserver").WithContextKey("SessionId").WithContextKey("AT-SessionId")
) )
type ATSessionHandler struct { type ATSessionHandler struct {
*SessionHandler *httpserver.SessionHandler
} }
func NewATSessionHandler(h handlers.RequestHandler) *ATSessionHandler { func NewATSessionHandler(h handlers.RequestHandler) *ATSessionHandler {
return &ATSessionHandler{ return &ATSessionHandler{
SessionHandler: ToSessionHandler(h), SessionHandler: httpserver.ToSessionHandler(h),
} }
} }
@@ -28,21 +34,21 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
rp := ash.GetRequestParser() rp := ash.GetRequestParser()
cfg := ash.GetConfig() cfg := ash.GetConfig()
cfg.SessionId, err = rp.GetSessionId(req) cfg.SessionId, err = rp.GetSessionId(req.Context(), req)
if err != nil { if err != nil {
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
ash.writeError(w, 400, err) ash.WriteError(w, 400, err)
return return
} }
rqs.Config = cfg rqs.Config = cfg
rqs.Input, err = rp.GetInput(req) rqs.Input, err = rp.GetInput(req)
if err != nil { if err != nil {
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
ash.writeError(w, 400, err) ash.WriteError(w, 400, err)
return return
} }
rqs, err = ash.Process(rqs) rqs, err = ash.Process(rqs)
switch err { switch err {
case nil: // set code to 200 if no err case nil: // set code to 200 if no err
code = 200 code = 200
@@ -53,7 +59,7 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
} }
if code != 200 { if code != 200 {
ash.writeError(w, 500, err) ash.WriteError(w, 500, err)
return return
} }
@@ -61,13 +67,13 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Type", "text/plain")
rqs, err = ash.Output(rqs) rqs, err = ash.Output(rqs)
if err != nil { if err != nil {
ash.writeError(w, 500, err) ash.WriteError(w, 500, err)
return return
} }
rqs, err = ash.Reset(rqs) rqs, err = ash.Reset(rqs)
if err != nil { if err != nil {
ash.writeError(w, 500, err) ash.WriteError(w, 500, err)
return return
} }
} }
@@ -89,4 +95,4 @@ func (ash *ATSessionHandler) Output(rqs handlers.RequestSession) (handlers.Reque
_, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer) _, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer)
return rqs, err return rqs, err
} }

View File

@@ -1,7 +1,6 @@
package http package at
import ( import (
"bytes"
"context" "context"
"errors" "errors"
"io" "io"
@@ -16,16 +15,6 @@ import (
"git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks" "git.grassecon.net/urdt/ussd/internal/testutil/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) { func TestNewATSessionHandler(t *testing.T) {
mockHandler := &httpmocks.MockRequestHandler{} mockHandler := &httpmocks.MockRequestHandler{}
ash := NewATSessionHandler(mockHandler) ash := NewATSessionHandler(mockHandler)
@@ -242,208 +231,4 @@ func TestATSessionHandler_Output(t *testing.T) {
} }
} }
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)
}
})
}
}

37
internal/http/parse.go Normal file
View File

@@ -0,0 +1,37 @@
package http
import (
"context"
"io/ioutil"
"net/http"
"git.grassecon.net/urdt/ussd/internal/handlers"
)
type DefaultRequestParser struct {
}
func (rp *DefaultRequestParser) GetSessionId(ctx context.Context, 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
}

View File

@@ -1,7 +1,6 @@
package http package http
import ( import (
"io/ioutil"
"net/http" "net/http"
"strconv" "strconv"
@@ -14,35 +13,6 @@ var (
logg = logging.NewVanilla().WithDomain("httpserver") 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 { type SessionHandler struct {
handlers.RequestHandler handlers.RequestHandler
} }
@@ -53,40 +23,39 @@ func ToSessionHandler(h handlers.RequestHandler) *SessionHandler {
} }
} }
func(f *SessionHandler) writeError(w http.ResponseWriter, code int, err error) { func (f *SessionHandler) WriteError(w http.ResponseWriter, code int, err error) {
s := err.Error() s := err.Error()
w.Header().Set("Content-Length", strconv.Itoa(len(s))) w.Header().Set("Content-Length", strconv.Itoa(len(s)))
w.WriteHeader(code) w.WriteHeader(code)
_, err = w.Write([]byte{}) _, err = w.Write([]byte(s))
if err != nil { if err != nil {
logg.Errorf("error writing error!!", "err", err, "olderr", s) logg.Errorf("error writing error!!", "err", err, "olderr", s)
w.WriteHeader(500) w.WriteHeader(500)
} }
return
} }
func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
var code int var code int
var err error var err error
var perr error var perr error
rqs := handlers.RequestSession{ rqs := handlers.RequestSession{
Ctx: req.Context(), Ctx: req.Context(),
Writer: w, Writer: w,
} }
rp := f.GetRequestParser() rp := f.GetRequestParser()
cfg := f.GetConfig() cfg := f.GetConfig()
cfg.SessionId, err = rp.GetSessionId(req) cfg.SessionId, err = rp.GetSessionId(req.Context(), req)
if err != nil { if err != nil {
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
f.writeError(w, 400, err) f.WriteError(w, 400, err)
} }
rqs.Config = cfg rqs.Config = cfg
rqs.Input, err = rp.GetInput(req) rqs.Input, err = rp.GetInput(req)
if err != nil { if err != nil {
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
f.writeError(w, 400, err) f.WriteError(w, 400, err)
return return
} }
@@ -103,7 +72,7 @@ func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
if code != 200 { if code != 200 {
f.writeError(w, 500, err) f.WriteError(w, 500, err)
return return
} }
@@ -112,11 +81,11 @@ func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
rqs, err = f.Output(rqs) rqs, err = f.Output(rqs)
rqs, perr = f.Reset(rqs) rqs, perr = f.Reset(rqs)
if err != nil { if err != nil {
f.writeError(w, 500, err) f.WriteError(w, 500, err)
return return
} }
if perr != nil { if perr != nil {
f.writeError(w, 500, perr) f.WriteError(w, 500, perr)
return return
} }
} }

View File

@@ -0,0 +1,230 @@
package http
import (
"bytes"
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
"git.defalsify.org/vise.git/engine"
"git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/testutil/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 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(context.Background(),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)
}
})
}
}

65
internal/ssh/keystore.go Normal file
View File

@@ -0,0 +1,65 @@
package ssh
import (
"context"
"fmt"
"os"
"path"
"golang.org/x/crypto/ssh"
"git.defalsify.org/vise.git/db"
"git.grassecon.net/urdt/ussd/internal/storage"
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db/gdbm"
)
type SshKeyStore struct {
store db.Db
}
func NewSshKeyStore(ctx context.Context, dbDir string) (*SshKeyStore, error) {
keyStore := &SshKeyStore{}
keyStoreFile := path.Join(dbDir, "ssh_authorized_keys.gdbm")
keyStore.store = dbstorage.NewThreadGdbmDb()
err := keyStore.store.Connect(ctx, keyStoreFile)
if err != nil {
return nil, err
}
return keyStore, nil
}
func(s *SshKeyStore) AddFromFile(ctx context.Context, fp string, sessionId string) error {
_, err := os.Stat(fp)
if err != nil {
return fmt.Errorf("cannot open ssh server public key file: %v\n", err)
}
publicBytes, err := os.ReadFile(fp)
if err != nil {
return fmt.Errorf("Failed to load public key: %v", err)
}
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(publicBytes)
if err != nil {
return fmt.Errorf("Failed to parse public key: %v", err)
}
k := append([]byte{0x01}, pubKey.Marshal()...)
s.store.SetPrefix(storage.DATATYPE_EXTEND)
logg.Infof("Added key", "sessionId", sessionId, "public key", string(publicBytes))
return s.store.Put(ctx, k, []byte(sessionId))
}
func(s *SshKeyStore) Get(ctx context.Context, pubKey ssh.PublicKey) (string, error) {
s.store.SetLanguage(nil)
s.store.SetPrefix(storage.DATATYPE_EXTEND)
k := append([]byte{0x01}, pubKey.Marshal()...)
v, err := s.store.Get(ctx, k)
if err != nil {
return "", err
}
return string(v), nil
}
func(s *SshKeyStore) Close() error {
return s.store.Close()
}

287
internal/ssh/ssh.go Normal file
View File

@@ -0,0 +1,287 @@
package ssh
import (
"context"
"encoding/hex"
"encoding/base64"
"errors"
"fmt"
"net"
"os"
"sync"
"golang.org/x/crypto/ssh"
"git.defalsify.org/vise.git/engine"
"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"
"git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/remote"
)
var (
logg = logging.NewVanilla().WithDomain("ssh")
)
type auther struct {
Ctx context.Context
keyStore *SshKeyStore
auth map[string]string
}
func NewAuther(ctx context.Context, keyStore *SshKeyStore) *auther {
return &auther{
Ctx: ctx,
keyStore: keyStore,
auth: make(map[string]string),
}
}
func(a *auther) Check(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
va, err := a.keyStore.Get(a.Ctx, pubKey)
if err != nil {
return nil, err
}
ka := hex.EncodeToString(conn.SessionID())
a.auth[ka] = va
fmt.Fprintf(os.Stderr, "connect: %s -> %s\n", ka, va)
return nil, nil
}
func(a *auther) FromConn(c *ssh.ServerConn) (string, error) {
if c == nil {
return "", errors.New("nil server conn")
}
if c.Conn == nil {
return "", errors.New("nil underlying conn")
}
return a.Get(c.Conn.SessionID())
}
func(a *auther) Get(k []byte) (string, error) {
ka := hex.EncodeToString(k)
v, ok := a.auth[ka]
if !ok {
return "", errors.New("not found")
}
return v, nil
}
func(s *SshRunner) serve(ctx context.Context, sessionId string, ch ssh.NewChannel, en engine.Engine) error {
if ch == nil {
return errors.New("nil channel")
}
if ch.ChannelType() != "session" {
ch.Reject(ssh.UnknownChannelType, "that is not the channel you are looking for")
return errors.New("not a session")
}
channel, requests, err := ch.Accept()
if err != nil {
panic(err)
}
defer channel.Close()
s.wg.Add(1)
go func(reqIn <-chan *ssh.Request) {
defer s.wg.Done()
for req := range reqIn {
req.Reply(req.Type == "shell", nil)
}
_ = requests
}(requests)
cont, err := en.Exec(ctx, []byte{})
if err != nil {
return fmt.Errorf("initial engine exec err: %v", err)
}
var input [state.INPUT_LIMIT]byte
for cont {
c, err := en.Flush(ctx, channel)
if err != nil {
return fmt.Errorf("flush err: %v", err)
}
_, err = channel.Write([]byte{0x0a})
if err != nil {
return fmt.Errorf("newline err: %v", err)
}
c, err = channel.Read(input[:])
if err != nil {
return fmt.Errorf("read input fail: %v", err)
}
logg.TraceCtxf(ctx, "input read", "c", c, "input", input[:c-1])
cont, err = en.Exec(ctx, input[:c-1])
if err != nil {
return fmt.Errorf("engine exec err: %v", err)
}
logg.TraceCtxf(ctx, "exec cont", "cont", cont, "en", en)
_ = c
}
c, err := en.Flush(ctx, channel)
if err != nil {
return fmt.Errorf("last flush err: %v", err)
}
_ = c
return nil
}
type SshRunner struct {
Ctx context.Context
Cfg engine.Config
FlagFile string
DbDir string
ResourceDir string
Debug bool
SrvKeyFile string
Host string
Port uint
wg sync.WaitGroup
lst net.Listener
}
func(s *SshRunner) Stop() error {
return s.lst.Close()
}
func(s *SshRunner) GetEngine(sessionId string) (engine.Engine, func(), error) {
ctx := s.Ctx
menuStorageService := storage.NewMenuStorageService(s.DbDir, s.ResourceDir)
err := menuStorageService.EnsureDbDir()
if err != nil {
return nil, nil, err
}
rs, err := menuStorageService.GetResource(ctx)
if err != nil {
return nil, nil, err
}
pe, err := menuStorageService.GetPersister(ctx)
if err != nil {
return nil, nil, err
}
userdatastore, err := menuStorageService.GetUserdataDb(ctx)
if err != nil {
return nil, nil, err
}
dbResource, ok := rs.(*resource.DbResource)
if !ok {
return nil, nil, err
}
lhs, err := handlers.NewLocalHandlerService(ctx, s.FlagFile, true, dbResource, s.Cfg, rs)
lhs.SetDataStore(&userdatastore)
lhs.SetPersister(pe)
lhs.Cfg.SessionId = sessionId
if err != nil {
return nil, nil, err
}
// TODO: clear up why pointer here and by-value other cmds
accountService := &remote.AccountService{}
hl, err := lhs.GetHandler(accountService)
if err != nil {
return nil, nil, err
}
en := lhs.GetEngine()
en = en.WithFirst(hl.Init)
if s.Debug {
en = en.WithDebug(nil)
}
// TODO: this is getting very hacky!
closer := func() {
err := menuStorageService.Close()
if err != nil {
logg.ErrorCtxf(ctx, "menu storage service cleanup fail", "err", err)
}
}
return en, closer, nil
}
// adapted example from crypto/ssh package, NewServerConn doc
func(s *SshRunner) Run(ctx context.Context, keyStore *SshKeyStore) {
running := true
// TODO: waitgroup should probably not be global
defer s.wg.Wait()
auth := NewAuther(ctx, keyStore)
cfg := ssh.ServerConfig{
PublicKeyCallback: auth.Check,
}
privateBytes, err := os.ReadFile(s.SrvKeyFile)
if err != nil {
logg.ErrorCtxf(ctx, "Failed to load private key", "err", err)
}
private, err := ssh.ParsePrivateKey(privateBytes)
if err != nil {
logg.ErrorCtxf(ctx, "Failed to parse private key", "err", err)
}
srvPub := private.PublicKey()
srvPubStr := base64.StdEncoding.EncodeToString(srvPub.Marshal())
logg.InfoCtxf(ctx, "have server key", "type", srvPub.Type(), "public", srvPubStr)
cfg.AddHostKey(private)
s.lst, err = net.Listen("tcp", fmt.Sprintf("%s:%d", s.Host, s.Port))
if err != nil {
panic(err)
}
for running {
conn, err := s.lst.Accept()
if err != nil {
logg.ErrorCtxf(ctx, "ssh accept error", "err", err)
running = false
continue
}
go func(conn net.Conn) {
defer conn.Close()
for true {
srvConn, nC, rC, err := ssh.NewServerConn(conn, &cfg)
if err != nil {
logg.InfoCtxf(ctx, "rejected client", "err", err)
return
}
logg.DebugCtxf(ctx, "ssh client connected", "conn", srvConn)
s.wg.Add(1)
go func() {
ssh.DiscardRequests(rC)
s.wg.Done()
}()
sessionId, err := auth.FromConn(srvConn)
if err != nil {
logg.ErrorCtxf(ctx, "Cannot find authentication")
return
}
en, closer, err := s.GetEngine(sessionId)
if err != nil {
logg.ErrorCtxf(ctx, "engine won't start", "err", err)
return
}
defer func() {
err := en.Finish()
if err != nil {
logg.ErrorCtxf(ctx, "engine won't stop", "err", err)
}
closer()
}()
for ch := range nC {
err = s.serve(ctx, sessionId, ch, en)
logg.ErrorCtxf(ctx, "ssh server finish", "err", err)
}
}
}(conn)
}
}

View File

@@ -6,6 +6,11 @@ import (
"git.defalsify.org/vise.git/db" "git.defalsify.org/vise.git/db"
gdbmdb "git.defalsify.org/vise.git/db/gdbm" gdbmdb "git.defalsify.org/vise.git/db/gdbm"
"git.defalsify.org/vise.git/lang" "git.defalsify.org/vise.git/lang"
"git.defalsify.org/vise.git/logging"
)
var (
logg = logging.NewVanilla().WithDomain("gdbmstorage")
) )
var ( var (
@@ -115,7 +120,8 @@ func(tdb *ThreadGdbmDb) Close() error {
return err return err
} }
func(tdb *ThreadGdbmDb) Dump(_ context.Context, _ []byte) (*db.Dumper, error) { func(tdb *ThreadGdbmDb) Dump(ctx context.Context, key []byte) (*db.Dumper, error) {
logg.Warnf("method not implemented for thread gdbm db") tdb.reserve()
return nil, nil defer tdb.release()
return tdb.db.Dump(ctx, key)
} }

View File

@@ -6,10 +6,6 @@ import (
"git.defalsify.org/vise.git/db" "git.defalsify.org/vise.git/db"
) )
const (
DATATYPE_USERSUB = 64
)
// PrefixDb interface abstracts the database operations. // PrefixDb interface abstracts the database operations.
type PrefixDb interface { type PrefixDb interface {
Get(ctx context.Context, key []byte) ([]byte, error) Get(ctx context.Context, key []byte) ([]byte, error)
@@ -35,13 +31,13 @@ func (s *SubPrefixDb) toKey(k []byte) []byte {
} }
func (s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) { func (s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) {
s.store.SetPrefix(DATATYPE_USERSUB) s.store.SetPrefix(db.DATATYPE_USERDATA)
key = s.toKey(key) key = s.toKey(key)
return s.store.Get(ctx, key) return s.store.Get(ctx, key)
} }
func (s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error { func (s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error {
s.store.SetPrefix(DATATYPE_USERSUB) s.store.SetPrefix(db.DATATYPE_USERDATA)
key = s.toKey(key) key = s.toKey(key)
return s.store.Put(ctx, key, val) return s.store.Put(ctx, key, val)
} }

View File

@@ -5,6 +5,10 @@ import (
"git.defalsify.org/vise.git/persist" "git.defalsify.org/vise.git/persist"
) )
const (
DATATYPE_EXTEND = 128
)
type Storage struct { type Storage struct {
Persister *persist.Persister Persister *persist.Persister
UserdataDb db.Db UserdataDb db.Db

View File

@@ -13,6 +13,7 @@ import (
"git.defalsify.org/vise.git/persist" "git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/initializers" "git.grassecon.net/urdt/ussd/initializers"
gdbmstorage "git.grassecon.net/urdt/ussd/internal/storage/db/gdbm"
) )
var ( var (
@@ -75,7 +76,7 @@ func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.D
connStr := buildConnStr() connStr := buildConnStr()
err = newDb.Connect(ctx, connStr) err = newDb.Connect(ctx, connStr)
} else { } else {
newDb = NewThreadGdbmDb() newDb = gdbmstorage.NewThreadGdbmDb()
storeFile := path.Join(ms.dbDir, fileName) storeFile := path.Join(ms.dbDir, fileName)
err = newDb.Connect(ctx, storeFile) err = newDb.Connect(ctx, storeFile)
} }

View File

@@ -1,12 +1,14 @@
package httpmocks package httpmocks
import "context"
// MockRequestParser implements the handlers.RequestParser interface for testing // MockRequestParser implements the handlers.RequestParser interface for testing
type MockRequestParser struct { type MockRequestParser struct {
GetSessionIdFunc func(any) (string, error) GetSessionIdFunc func(any) (string, error)
GetInputFunc func(any) ([]byte, error) GetInputFunc func(any) ([]byte, error)
} }
func (m *MockRequestParser) GetSessionId(rq any) (string, error) { func (m *MockRequestParser) GetSessionId(ctx context.Context, rq any) (string, error) {
return m.GetSessionIdFunc(rq) return m.GetSessionIdFunc(rq)
} }

View File

@@ -49,6 +49,6 @@ func (m *MockAccountService) TokenTransfer(ctx context.Context, amount, from, to
} }
func (m *MockAccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) { func (m *MockAccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) {
args := m.Called() args := m.Called(alias)
return args.Get(0).(*dataserviceapi.AliasAddress), args.Error(1) return args.Get(0).(*dataserviceapi.AliasAddress), args.Error(1)
} }

View File

@@ -1,6 +1,9 @@
package utils package utils
import "time" import (
"strconv"
"time"
)
// CalculateAge calculates the age based on a given birthdate and the current date in the format dd/mm/yy // CalculateAge calculates the age based on a given birthdate and the current date in the format dd/mm/yy
// It adjusts for cases where the current date is before the birthday in the current year. // It adjusts for cases where the current date is before the birthday in the current year.
@@ -25,11 +28,29 @@ func CalculateAge(birthdate, today time.Time) int {
// It subtracts the YOB from the current year to determine the age. // It subtracts the YOB from the current year to determine the age.
// //
// Parameters: // Parameters:
// yob: The year of birth as an integer. //
// yob: The year of birth as an integer.
// //
// Returns: // Returns:
// The calculated age as an integer. //
// The calculated age as an integer.
func CalculateAgeWithYOB(yob int) int { func CalculateAgeWithYOB(yob int) int {
currentYear := time.Now().Year() currentYear := time.Now().Year()
return currentYear - yob return currentYear - yob
} }
//IsValidYob checks if the provided yob can be considered valid
func IsValidYOb(yob string) bool {
currentYear := time.Now().Year()
yearOfBirth, err := strconv.ParseInt(yob, 10, 64)
if err != nil {
return false
}
if yearOfBirth >= 1900 && int(yearOfBirth) <= currentYear {
return true
} else {
return false
}
}

View File

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

17
internal/utils/name.go Normal file
View File

@@ -0,0 +1,17 @@
package utils
func ConstructName(firstName, familyName, defaultValue string) string {
name := defaultValue
if familyName != defaultValue {
if firstName != defaultValue {
name = firstName + " " + familyName
} else {
name = familyName
}
} else {
if firstName != defaultValue {
name = firstName
}
}
return name
}

View File

@@ -54,7 +54,7 @@
}, },
{ {
"input": "1235", "input": "1235",
"expectedContent": "Incorrect pin\n1:Retry\n9:Quit" "expectedContent": "Incorrect PIN\n1:Retry\n9:Quit"
}, },
{ {
"input": "1", "input": "1",
@@ -62,10 +62,10 @@
}, },
{ {
"input": "1234", "input": "1234",
"expectedContent": "Select language:\n0:english\n1:kiswahili" "expectedContent": "Select language:\n1:English\n2:Kiswahili"
}, },
{ {
"input": "0", "input": "1",
"expectedContent": "Your language change request was successful.\n0:Back\n9:Quit" "expectedContent": "Your language change request was successful.\n0:Back\n9:Quit"
}, },
{ {
@@ -95,7 +95,7 @@
}, },
{ {
"input": "1235", "input": "1235",
"expectedContent": "Incorrect pin\n1:Retry\n9:Quit" "expectedContent": "Incorrect PIN\n1:Retry\n9:Quit"
}, },
{ {
"input": "1", "input": "1",
@@ -141,7 +141,7 @@
}, },
{ {
"input": "1235", "input": "1235",
"expectedContent": "Incorrect pin\n1:Retry\n9:Quit" "expectedContent": "Incorrect PIN\n1:Retry\n9:Quit"
}, },
{ {
"input": "1", "input": "1",
@@ -167,7 +167,7 @@
] ]
}, },
{ {
"name": "menu_my_account_edit_firstname", "name": "menu_my_account_edit_all_account_details_starting_from_firstname",
"steps": [ "steps": [
{ {
"input": "", "input": "",
@@ -187,6 +187,26 @@
}, },
{ {
"input": "foo", "input": "foo",
"expectedContent": "Enter family name:\n0:Back"
},
{
"input": "bar",
"expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back"
},
{
"input": "1",
"expectedContent": "Enter your year of birth\n0:Back"
},
{
"input": "1940",
"expectedContent": "Enter your location:\n0:Back"
},
{
"input": "Kilifi",
"expectedContent": "Enter the services or goods you offer: \n0:Back"
},
{
"input": "Bananas",
"expectedContent": "Please enter your PIN:" "expectedContent": "Please enter your PIN:"
}, },
{ {
@@ -197,10 +217,6 @@
"input": "0", "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" "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", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
@@ -208,7 +224,7 @@
] ]
}, },
{ {
"name": "menu_my_account_edit_familyname", "name": "menu_my_account_edit_familyname_when_all_account__details_have_been_set",
"steps": [ "steps": [
{ {
"input": "", "input": "",
@@ -238,10 +254,6 @@
"input": "0", "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" "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", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
@@ -250,7 +262,7 @@
] ]
}, },
{ {
"name": "menu_my_account_edit_gender", "name": "menu_my_account_edit_gender_when_all_account__details_have_been_set",
"steps": [ "steps": [
{ {
"input": "", "input": "",
@@ -280,10 +292,6 @@
"input": "0", "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" "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", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
@@ -291,7 +299,7 @@
] ]
}, },
{ {
"name": "menu_my_account_edit_yob", "name": "menu_my_account_edit_yob_when_all_account__details_have_been_set",
"steps": [ "steps": [
{ {
"input": "", "input": "",
@@ -321,10 +329,6 @@
"input": "0", "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" "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", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
@@ -332,7 +336,7 @@
] ]
}, },
{ {
"name": "menu_my_account_edit_location", "name": "menu_my_account_edit_location_when_all_account_details_have_been_set",
"steps": [ "steps": [
{ {
"input": "", "input": "",
@@ -362,10 +366,6 @@
"input": "0", "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" "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", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
@@ -373,7 +373,7 @@
] ]
}, },
{ {
"name": "menu_my_account_edit_offerings", "name": "menu_my_account_edit_offerings_when_all_account__details_have_been_set",
"steps": [ "steps": [
{ {
"input": "", "input": "",
@@ -403,10 +403,6 @@
"input": "0", "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" "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", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
@@ -434,16 +430,12 @@
}, },
{ {
"input": "1234", "input": "1234",
"expectedContent": "My profile:\nName: foo bar\nGender: male\nAge: 79\nLocation: Kilifi\nYou provide: Bananas\n\n0:Back" "expectedContent": "My profile:\nName: foo bar\nGender: male\nAge: 80\nLocation: Kilifi\nYou provide: Bananas\n\n0:Back\n9:Quit"
}, },
{ {
"input": "0", "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" "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", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"

View File

@@ -3,6 +3,7 @@ package menutraversaltest
import ( import (
"bytes" "bytes"
"context" "context"
"flag"
"log" "log"
"math/rand" "math/rand"
"os" "os"
@@ -15,14 +16,15 @@ import (
) )
var ( var (
testData = driver.ReadData() testData = driver.ReadData()
testStore = ".test_state" testStore = ".test_state"
groupTestFile = "group_test.json" sessionID string
sessionID string src = rand.NewSource(42)
src = rand.NewSource(42) g = rand.New(src)
g = rand.New(src)
) )
var groupTestFile = flag.String("test-file", "group_test.json", "The test file to use for running the group tests")
func GenerateSessionId() string { func GenerateSessionId() string {
uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g)) uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g))
v, err := uu.NewV4() v, err := uu.NewV4()
@@ -296,9 +298,10 @@ func TestMainMenuSend(t *testing.T) {
ctx := context.Background() ctx := context.Background()
sessions := testData sessions := testData
for _, session := range sessions { for _, session := range sessions {
groups := driver.FilterGroupsByName(session.Groups, "send_with_invalid_inputs") groups := driver.FilterGroupsByName(session.Groups, "send_with_invite")
for _, group := range groups { for _, group := range groups {
for _, step := range group.Steps { for index, step := range group.Steps {
t.Logf("step %v with input %v", index, step.Input)
cont, err := en.Exec(ctx, []byte(step.Input)) cont, err := en.Exec(ctx, []byte(step.Input))
if err != nil { if err != nil {
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err) t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
@@ -337,7 +340,7 @@ func TestMainMenuSend(t *testing.T) {
} }
func TestGroups(t *testing.T) { func TestGroups(t *testing.T) {
groups, err := driver.LoadTestGroups(groupTestFile) groups, err := driver.LoadTestGroups(*groupTestFile)
if err != nil { if err != nil {
log.Fatalf("Failed to load test groups: %v", err) log.Fatalf("Failed to load test groups: %v", err)
} }

View File

@@ -0,0 +1,68 @@
{
"groups": [
{
"name": "menu_my_account_edit_all_account_details_starting_from_family_name",
"steps": [
{
"input": "",
"expectedContent": "{balance}\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": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back"
},
{
"input": "1",
"expectedContent": "Enter your year of birth\n0:Back"
},
{
"input": "1940",
"expectedContent": "Enter your location:\n0:Back"
},
{
"input": "Kilifi",
"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": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}
]
}

View File

@@ -0,0 +1,61 @@
{
"groups": [
{
"name": "menu_my_account_edit_all_account_details_starting_from_firstname",
"steps": [
{
"input": "",
"expectedContent": "{balance}\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": "Enter family name:\n0:Back"
},
{
"input": "bar",
"expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back"
},
{
"input": "1",
"expectedContent": "Enter your year of birth\n0:Back"
},
{
"input": "1940",
"expectedContent": "Enter your location:\n0:Back"
},
{
"input": "Kilifi",
"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": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}
]
}

View File

@@ -0,0 +1,55 @@
{
"groups": [
{
"name": "menu_my_account_edit_all_account_details_starting_from_gender",
"steps": [
{
"input": "",
"expectedContent": "{balance}\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": "Enter your year of birth\n0:Back"
},
{
"input": "1940",
"expectedContent": "Enter your location:\n0:Back"
},
{
"input": "Kilifi",
"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": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}
]
}

View File

@@ -0,0 +1,46 @@
{
"groups": [
{
"name": "menu_my_account_edit_all_account_details_starting_from_location",
"steps": [
{
"input": "",
"expectedContent": "{balance}\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": "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": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}
]
}

View File

@@ -0,0 +1,42 @@
{
"groups": [
{
"name": "menu_my_account_edit_all_account_details_starting_from_offerings",
"steps": [
{
"input": "",
"expectedContent": "{balance}\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": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}
]
}

View File

@@ -0,0 +1,50 @@
{
"groups": [
{
"name": "menu_my_account_edit_all_account_details_starting_from_yob",
"steps": [
{
"input": "",
"expectedContent": "{balance}\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": "1940",
"expectedContent": "Enter your location:\n0:Back"
},
{
"input": "Kilifi",
"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": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}
]
}

View File

@@ -0,0 +1,70 @@
{
"groups": [
{
"name": "menu_my_account_edit_familyname_when_adjacent_profile_information_set",
"steps": [
{
"input": "",
"expectedContent": "{balance}\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": "Enter your year of birth\n0:Back"
},
{
"input": "1940",
"expectedContent": "Enter your location:\n0:Back"
},
{
"input": "Kilifi",
"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": "2",
"expectedContent": "Enter family name:\n0:Back"
},
{
"input": "foo2",
"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": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}
]
}

View File

@@ -7,14 +7,14 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:english\n1:kiswahili" "expectedContent": "Welcome to Sarafu Network\nPlease select a language\n1:English\n2:Kiswahili"
}, },
{ {
"input": "0", "input": "1",
"expectedContent": "Do you agree to terms and conditions?\n0:yes\n1:no" "expectedContent": "Do you agree to terms and conditions?\nhttps://grassecon.org/pages/terms-and-conditions\n\n1:Yes\n2:No"
}, },
{ {
"input": "0", "input": "1",
"expectedContent": "Please enter a new four number PIN for your account:\n0:Exit" "expectedContent": "Please enter a new four number PIN for your account:\n0:Exit"
}, },
{ {
@@ -40,14 +40,14 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:english\n1:kiswahili" "expectedContent": "Welcome to Sarafu Network\nPlease select a language\n1:English\n2:Kiswahili"
},
{
"input": "0",
"expectedContent": "Do you agree to terms and conditions?\n0:yes\n1:no"
}, },
{ {
"input": "1", "input": "1",
"expectedContent": "Do you agree to terms and conditions?\nhttps://grassecon.org/pages/terms-and-conditions\n\n1:Yes\n2:No"
},
{
"input": "2",
"expectedContent": "Thank you for using Sarafu. Goodbye!" "expectedContent": "Thank you for using Sarafu. Goodbye!"
} }
] ]
@@ -64,8 +64,8 @@
"expectedContent": "Enter recipient's phone number/address/alias:\n0:Back" "expectedContent": "Enter recipient's phone number/address/alias:\n0:Back"
}, },
{ {
"input": "000", "input": "0@0",
"expectedContent": "000 is invalid, please try again:\n1:Retry\n9:Quit" "expectedContent": "0@0 is invalid, please try again:\n1:Retry\n9:Quit"
}, },
{ {
"input": "1", "input": "1",

18
models/profile.go Normal file
View File

@@ -0,0 +1,18 @@
package models
type Profile struct {
ProfileItems []string
Max int
}
func (p *Profile) InsertOrShift(index int, value string) {
if index < len(p.ProfileItems) {
p.ProfileItems = append(p.ProfileItems[:index], value)
} else {
for len(p.ProfileItems) < index {
p.ProfileItems = append(p.ProfileItems, "0")
}
p.ProfileItems = append(p.ProfileItems, "0")
p.ProfileItems[index] = value
}
}

View File

@@ -1,8 +1,10 @@
package models package models
type VoucherDataResult struct { type VoucherDataResult struct {
TokenName string `json:"tokenName"` TokenName string `json:"tokenName"`
TokenSymbol string `json:"tokenSymbol"` TokenSymbol string `json:"tokenSymbol"`
TokenDecimals int `json:"tokenDecimals"` TokenDecimals int `json:"tokenDecimals"`
SinkAddress string `json:"sinkAddress"` SinkAddress string `json:"sinkAddress"`
TokenCommodity string `json:"tokenCommodity"`
TokenLocation string `json:"tokenLocation"`
} }

View File

@@ -0,0 +1 @@
Tatizo la kimtambo limetokea,tafadhali jaribu tena baadaye.

View File

@@ -7,3 +7,4 @@ HALT
INCMP _ 0 INCMP _ 0
INCMP my_balance 1 INCMP my_balance 1
INCMP community_balance 2 INCMP community_balance 2
INCMP . *

View File

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

View File

@@ -9,3 +9,4 @@ MOUT quit 9
HALT HALT
INCMP _ 0 INCMP _ 0
INCMP quit 9 INCMP quit 9
INCMP . *

View File

@@ -2,9 +2,17 @@ CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_familyname flag_allow_update 1 CATCH update_familyname flag_allow_update 1
LOAD get_current_profile_info 0 LOAD get_current_profile_info 0
RELOAD get_current_profile_info RELOAD get_current_profile_info
MAP get_current_profile_info
MOUT back 0 MOUT back 0
HALT HALT
LOAD save_familyname 0 RELOAD set_back
CATCH _ flag_back_set 1
LOAD save_familyname 64
RELOAD save_familyname RELOAD save_familyname
INCMP _ 0 CATCH pin_entry flag_familyname_set 1
INCMP pin_entry * CATCH select_gender flag_gender_set 0
CATCH edit_yob flag_yob_set 0
CATCH edit_location flag_location_set 0
CATCH edit_offerings flag_offerings_set 0
CATCH pin_entry flag_familyname_set 0
INCMP select_gender *

View File

@@ -5,7 +5,14 @@ RELOAD get_current_profile_info
MAP get_current_profile_info MAP get_current_profile_info
MOUT back 0 MOUT back 0
HALT HALT
LOAD save_firstname 0 RELOAD set_back
CATCH _ flag_back_set 1
LOAD save_firstname 128
RELOAD save_firstname RELOAD save_firstname
INCMP _ 0 CATCH pin_entry flag_firstname_set 1
INCMP pin_entry * CATCH edit_family_name flag_familyname_set 0
CATCH edit_gender flag_gender_set 0
CATCH edit_yob flag_yob_set 0
CATCH edit_location flag_location_set 0
CATCH edit_offerings flag_offerings_set 0
CATCH pin_entry flag_firstname_set 0

View File

@@ -1,2 +1,2 @@
Jina la kwanza la sasa {{.get_current_profile_info}} Jina la kwanza la sasa: {{.get_current_profile_info}}
Weka majina yako ya kwanza: Weka majina yako ya kwanza:

View File

@@ -2,9 +2,14 @@ CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_location flag_allow_update 1 CATCH update_location flag_allow_update 1
LOAD get_current_profile_info 0 LOAD get_current_profile_info 0
RELOAD get_current_profile_info RELOAD get_current_profile_info
LOAD save_location 16
MOUT back 0 MOUT back 0
HALT HALT
LOAD save_location 0 RELOAD set_back
CATCH _ flag_back_set 1
RELOAD save_location RELOAD save_location
INCMP _ 0 INCMP _ 0
INCMP pin_entry * CATCH pin_entry flag_location_set 1
CATCH edit_offerings flag_offerings_set 0
CATCH pin_entry flag_location_set 0
INCMP edit_offerings *

View File

@@ -1,2 +1,2 @@
Eneo la sasa {{.get_current_profile_info}} Eneo la sasa: {{.get_current_profile_info}}
Weka eneo: Weka eneo:

View File

@@ -2,9 +2,12 @@ CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_offerings flag_allow_update 1 CATCH update_offerings flag_allow_update 1
LOAD get_current_profile_info 0 LOAD get_current_profile_info 0
RELOAD get_current_profile_info RELOAD get_current_profile_info
LOAD save_offerings 0 LOAD save_offerings 8
MOUT back 0 MOUT back 0
HALT HALT
RELOAD set_back
CATCH _ flag_back_set 1
RELOAD save_offerings RELOAD save_offerings
INCMP _ 0 INCMP _ 0
INCMP pin_entry * CATCH pin_entry flag_offerings_set 1
INCMP update_profile_items *

View File

@@ -11,7 +11,8 @@ MOUT edit_offerings 6
MOUT view 7 MOUT view 7
MOUT back 0 MOUT back 0
HALT HALT
INCMP my_account 0 LOAD set_back 6
INCMP ^ 0
INCMP edit_first_name 1 INCMP edit_first_name 1
INCMP edit_family_name 2 INCMP edit_family_name 2
INCMP select_gender 3 INCMP select_gender 3
@@ -19,3 +20,4 @@ INCMP edit_yob 4
INCMP edit_location 5 INCMP edit_location 5
INCMP edit_offerings 6 INCMP edit_offerings 6
INCMP view_profile 7 INCMP view_profile 7
INCMP . *

View File

@@ -5,10 +5,15 @@ RELOAD get_current_profile_info
MAP get_current_profile_info MAP get_current_profile_info
MOUT back 0 MOUT back 0
HALT HALT
RELOAD set_back
CATCH _ flag_back_set 1
LOAD verify_yob 6 LOAD verify_yob 6
RELOAD verify_yob RELOAD verify_yob
CATCH incorrect_date_format flag_incorrect_date_format 1 CATCH incorrect_date_format flag_incorrect_date_format 1
LOAD save_yob 0 LOAD save_yob 32
RELOAD save_yob RELOAD save_yob
INCMP _ 0 CATCH pin_entry flag_yob_set 1
INCMP pin_entry * CATCH edit_location flag_location_set 0
CATCH edit_offerings flag_offerings_set 0
CATCH pin_entry flag_yob_set 0
INCMP edit_location *

View File

@@ -1,2 +1,2 @@
Mwaka wa sasa wa kuzaliwa {{.get_current_profile_info}} Mwaka wa sasa wa kuzaliwa: {{.get_current_profile_info}}
Weka mwaka wa kuzaliwa Weka mwaka wa kuzaliwa

View File

@@ -0,0 +1 @@
English

View File

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

View File

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

View File

@@ -0,0 +1 @@
Kiswahili

View File

@@ -13,7 +13,7 @@ msgstr "Kwa usaidizi zaidi,piga: 0757628885"
msgid "Balance: %s\n" msgid "Balance: %s\n"
msgstr "Salio: %s\n" msgstr "Salio: %s\n"
msid "Your invite request for %s to Sarafu Network failed. Please try again later." msgid "Your invite request for %s to Sarafu Network failed. Please try again later."
msgstr "Ombi lako la kumwalika %s kwa matandao wa Sarafu halikufaulu. Tafadhali jaribu tena baadaye." msgstr "Ombi lako la kumwalika %s kwa matandao wa Sarafu halikufaulu. Tafadhali jaribu tena baadaye."
msgid "Your invitation to %s to join Sarafu Network has been sent." msgid "Your invitation to %s to join Sarafu Network has been sent."
@@ -23,4 +23,7 @@ msgid "Your request failed. Please try again later."
msgstr "Ombi lako halikufaulu. Tafadhali jaribu tena baadaye." msgstr "Ombi lako halikufaulu. Tafadhali jaribu tena baadaye."
msgid "Community Balance: 0.00" msgid "Community Balance: 0.00"
msgid "Salio la Kikundi: 0.00" msgstr "Salio la Kikundi: 0.00"
msgid "Symbol: %s\nBalance: %s"
msgstr "Sarafu: %s\nSalio: %s"

View File

@@ -1,10 +1,10 @@
LOAD set_default_voucher 8 LOAD set_default_voucher 8
RELOAD set_default_voucher RELOAD set_default_voucher
LOAD check_balance 64
RELOAD check_balance
LOAD check_vouchers 10 LOAD check_vouchers 10
RELOAD check_vouchers RELOAD check_vouchers
CATCH api_failure flag_api_call_error 1 LOAD check_balance 128
RELOAD check_balance
CATCH api_failure flag_api_call_error 1
MAP check_balance MAP check_balance
MOUT send 1 MOUT send 1
MOUT vouchers 2 MOUT vouchers 2

View File

@@ -14,3 +14,4 @@ INCMP balances 3
INCMP check_statement 4 INCMP check_statement 4
INCMP pin_management 5 INCMP pin_management 5
INCMP address 6 INCMP address 6
INCMP . *

View File

@@ -9,3 +9,4 @@ MOUT quit 9
HALT HALT
INCMP _ 0 INCMP _ 0
INCMP quit 9 INCMP quit 9
INCMP . *

View File

@@ -1 +1 @@
Salio lako ni: 0.00 SRF {{.check_balance}}

View File

@@ -0,0 +1 @@
Next

View File

@@ -0,0 +1 @@
Mbele

View File

@@ -1 +1 @@
no No

View File

@@ -1 +1 @@
la La

View File

@@ -21,3 +21,10 @@ flag,flag_admin_privilege,27,this is set when a user has admin privileges.
flag,flag_unregistered_number,28,this is set when an unregistered phonenumber tries to perform an action flag,flag_unregistered_number,28,this is set when an unregistered phonenumber tries to perform an action
flag,flag_no_transfers,29,this is set when a user does not have any transactions flag,flag_no_transfers,29,this is set when a user does not have any transactions
flag,flag_incorrect_statement,30,this is set when the selected statement is invalid flag,flag_incorrect_statement,30,this is set when the selected statement is invalid
flag,flag_firstname_set,31,this is set when the first name of the profile is set
flag,flag_familyname_set,32,this is set when the family name of the profile is set
flag,flag_yob_set,33,this is set when the yob of the profile is set
flag,flag_gender_set,34,this is set when the gender of the profile is set
flag,flag_location_set,35,this is set when the location of the profile is set
flag,flag_offerings_set,36,this is set when the offerings of the profile is set
flag,flag_back_set,37,this is set when it is a back navigation
1 flag flag_language_set 8 checks whether the user has set their prefered language
21 flag flag_unregistered_number 28 this is set when an unregistered phonenumber tries to perform an action
22 flag flag_no_transfers 29 this is set when a user does not have any transactions
23 flag flag_incorrect_statement 30 this is set when the selected statement is invalid
24 flag flag_firstname_set 31 this is set when the first name of the profile is set
25 flag flag_familyname_set 32 this is set when the family name of the profile is set
26 flag flag_yob_set 33 this is set when the yob of the profile is set
27 flag flag_gender_set 34 this is set when the gender of the profile is set
28 flag flag_location_set 35 this is set when the location of the profile is set
29 flag flag_offerings_set 36 this is set when the offerings of the profile is set
30 flag flag_back_set 37 this is set when it is a back navigation

View File

@@ -0,0 +1 @@
Prev

View File

@@ -0,0 +1 @@
Nyuma

View File

@@ -1,3 +1,5 @@
LOAD update_all_profile_items 0
RELOAD update_all_profile_items
MOUT back 0 MOUT back 0
MOUT quit 9 MOUT quit 9
HALT HALT

View File

@@ -11,3 +11,4 @@ INCMP _ 0
INCMP set_male 1 INCMP set_male 1
INCMP set_female 2 INCMP set_female 2
INCMP set_unspecified 3 INCMP set_unspecified 3
INCMP . *

View File

@@ -1,2 +1,2 @@
Jinsia ya sasa {{.get_current_profile_info}} Jinsia ya sasa: {{.get_current_profile_info}}
Chagua jinsia Chagua jinsia

View File

@@ -1,6 +1,6 @@
MOUT english 0 MOUT english 1
MOUT kiswahili 1 MOUT kiswahili 2
HALT HALT
INCMP set_eng 0 INCMP set_eng 1
INCMP set_swa 1 INCMP set_swa 2
INCMP . * INCMP . *

View File

@@ -0,0 +1,4 @@
LOAD set_language 6
RELOAD set_language
CATCH terms flag_account_created 0
MOVE language_changed

View File

@@ -1,3 +1,4 @@
LOAD set_language 6 LOAD set_language 6
RELOAD set_language
CATCH terms flag_account_created 0 CATCH terms flag_account_created 0
MOVE language_changed MOVE language_changed

View File

@@ -1,4 +1,10 @@
LOAD save_gender 0 LOAD save_gender 32
RELOAD save_gender
CATCH incorrect_pin flag_incorrect_pin 1 CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_gender flag_allow_update 1 CATCH update_gender flag_allow_update 1
MOVE pin_entry CATCH pin_entry flag_gender_set 1
CATCH edit_yob flag_yob_set 0
CATCH edit_location flag_location_set 0
CATCH edit_offerings flag_offerings_set 0
CATCH pin_entry flag_gender_set 0
MOVE edit_yob

View File

@@ -1,4 +1,10 @@
LOAD save_gender 0 LOAD save_gender 16
RELOAD save_gender
CATCH incorrect_pin flag_incorrect_pin 1 CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_gender flag_allow_update 1 CATCH update_gender flag_allow_update 1
MOVE pin_entry CATCH pin_entry flag_gender_set 1
CATCH edit_yob flag_yob_set 0
CATCH edit_location flag_location_set 0
CATCH edit_offerings flag_offerings_set 0
CATCH pin_entry flag_gender_set 0
MOVE edit_yob

View File

@@ -1,3 +1,4 @@
LOAD set_language 6 LOAD set_language 6
RELOAD set_language
CATCH terms flag_account_created 0 CATCH terms flag_account_created 0
MOVE language_changed MOVE language_changed

View File

@@ -1,4 +1,10 @@
LOAD save_gender 0 LOAD save_gender 8
RELOAD save_gender
CATCH incorrect_pin flag_incorrect_pin 1 CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_gender flag_allow_update 1 CATCH update_gender flag_allow_update 1
MOVE pin_entry CATCH pin_entry flag_gender_set 1
CATCH edit_yob flag_yob_set 0
CATCH edit_location flag_location_set 0
CATCH edit_offerings flag_offerings_set 0
CATCH pin_entry flag_gender_set 0
MOVE edit_yob

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