Compare commits

..

191 Commits

Author SHA1 Message Date
843b0d1e7e Merge pull request 'Add reverse sessionid address lookup' (#146) from lash/reverse-session into master
Reviewed-on: #146
2024-10-30 10:35:55 +01:00
lash
cb997159f7
Add reverse sessionid address lookup 2024-10-30 00:59:59 +00:00
727f54ee57 Merge pull request 'tests-refactor' (#137) from tests-refactor into master
Reviewed-on: #137
2024-10-28 16:28:41 +01:00
4e1b2d5ddb
update account services 2024-10-25 18:01:52 +03:00
d8800a665d
Merge branch 'master' into tests-refactor 2024-10-25 17:48:40 +03:00
2dee47404d Merge pull request 'menu-voucherlist' (#101) from menu-voucherlist into master
Reviewed-on: #101
2024-10-25 15:59:46 +02:00
3792bfdc1f
run mod tidy 2024-10-25 09:38:09 +03:00
a0e97cfe5b
remove test account service implementation 2024-10-25 09:37:16 +03:00
a3be98c514
update test account service 2024-10-25 09:36:43 +03:00
2b34b3a75c
Merge branch 'master' into tests-refactor 2024-10-25 09:14:16 +03:00
b4454f7517
Added context to FetchVouchers 2024-10-24 20:44:29 +03:00
d9c660b8ea
Merge branch 'master' into menu-voucherlist 2024-10-24 20:39:43 +03:00
d113ea82fd
Add dynamic send_amount and session_id 2024-10-24 20:21:28 +03:00
a92c640cb7
Updated tests 2024-10-24 18:15:54 +03:00
728815f0c6
Include the active symbol in the send menu 2024-10-24 17:50:37 +03:00
6b5d3f74d1 Merge pull request 'api-context' (#127) from api-context into master
Reviewed-on: #127
2024-10-24 16:45:37 +02:00
bee9ad5ff5
Merge remote-tracking branch 'refs/remotes/origin/api-context' into api-context 2024-10-24 17:39:11 +03:00
6e7b46666e
ensure mod match with master 2024-10-24 17:34:42 +03:00
383f074cae
update method signatures 2024-10-24 17:32:08 +03:00
d678a639b8
Merge branch 'master' into api-context 2024-10-24 17:07:11 +03:00
453fea569a Merge pull request 'api-structs' (#117) from api-structs into master
Reviewed-on: #117
2024-10-24 15:53:46 +02:00
5c75e35fe0 Delete coverage.html
delete cover.html
2024-10-24 15:45:52 +02:00
cff50538fa Delete cover.out
delete cover.out
2024-10-24 15:45:29 +02:00
69a4530269 Delete services/registration/locale/swa/default.mo
remove dev file
2024-10-24 15:41:47 +02:00
db19e38717
Merge remote-tracking branch 'refs/remotes/origin/api-structs' into api-structs 2024-10-24 16:37:07 +03:00
c796bbdcfc
correct create endpoint 2024-10-24 16:36:09 +03:00
2b34a0900c
check for status code and unmarshal on errResponse 2024-10-24 16:35:53 +03:00
e15bac98a5 Merge branch 'master' into menu-voucherlist 2024-10-24 16:31:49 +03:00
ee9a683eb0
Merge branch 'master' into menu-voucherlist 2024-10-24 16:30:44 +03:00
9274e585bd
Merge branch 'master' into menu-voucherlist 2024-10-24 16:26:32 +03:00
57a49819f4
Merge branch 'master' into api-structs 2024-10-24 16:22:41 +03:00
abc01b7cee
change to local setup endpoint 2024-10-24 16:21:34 +03:00
d19c20a9d7 Merge branch 'master' into api-structs 2024-10-24 15:19:06 +02:00
0c6144d262 Merge pull request 'send-menu-update' (#131) from send-menu-update into master
Reviewed-on: #131
2024-10-24 15:15:11 +02:00
ec4e44a27c Merge branch 'master' into send-menu-update 2024-10-24 15:58:59 +03:00
69eb57f794 Merge branch 'master' into api-context 2024-10-24 14:51:25 +02:00
2347d64acc Merge pull request 'check-balance-update' (#132) from check-balance-update into master
Reviewed-on: #132
2024-10-24 14:50:59 +02:00
39c0560abe
Updated the test 2024-10-24 15:21:48 +03:00
75459f852b
Return the full balance string 2024-10-24 15:12:57 +03:00
6200728435
Updated the menuhander test 2024-10-24 14:45:15 +03:00
579b46db65
Replace the public key with the sessionId 2024-10-24 14:34:12 +03:00
f99486c190
correct import 2024-10-24 12:07:00 +03:00
57a07af8ca
correct import 2024-10-24 12:06:44 +03:00
5692440099
move to testutil 2024-10-24 12:05:27 +03:00
651969668f
move to testutil 2024-10-24 12:05:11 +03:00
f3a028f1fc
move to testutil 2024-10-24 12:03:32 +03:00
9f2d57ea03
update tests 2024-10-24 10:10:14 +03:00
d74a4cc33b
update service mock method signatures 2024-10-24 10:10:03 +03:00
308ca99fb0
remove reload account creation 2024-10-24 10:02:32 +03:00
08e709f1b3
check for error responses 2024-10-24 10:02:15 +03:00
9bc9d04a49
use errresponse and okresponse for deserialization 2024-10-24 10:00:38 +03:00
a553731f02
Cleaned up code 2024-10-23 17:45:41 +03:00
4011597d9c
Check specific db error 2024-10-23 14:02:13 +03:00
176473aa26
Rename prefix to vouchers 2024-10-23 13:54:42 +03:00
b41e52af63
pass context.Context 2024-10-23 12:45:54 +03:00
fb32dde136
run go mod tidy 2024-10-23 12:45:10 +03:00
1e6cf6a33a
run mod tidy 2024-10-23 11:08:51 +03:00
2725323f16
Merge branch 'master' into tests-refactor 2024-10-23 11:06:10 +03:00
5f1ee396d8
Fix PIN being requested twice 2024-10-23 06:35:19 +03:00
5f2c6cce16
add required track endpoint 2024-10-22 21:36:28 +03:00
3179ec1f62
add local track endpoint 2024-10-22 21:35:52 +03:00
b9a63f3c6f
merge dep 2024-10-22 21:30:48 +03:00
1d255372b2
Merge branch 'master' into api-structs 2024-10-22 21:05:51 +03:00
306666a15e
load and reload after halt 2024-10-22 20:37:30 +03:00
e05dbb4885
check for back option 2024-10-22 20:36:58 +03:00
637e107525
Merge branch 'master' into menu-voucherlist 2024-10-22 17:10:51 +03:00
59d0446020 Merge pull request 'postgres-switch' (#113) from postgres-switch into master
Reviewed-on: #113
2024-10-22 16:04:56 +02:00
f37ec13c75
polish code 2024-10-22 16:31:29 +03:00
0547bc7e5f
added send menu test with dynamic max_amount 2024-10-22 14:24:22 +03:00
02cb75f97a
increase the size to resolve sink error 2024-10-21 17:14:59 +03:00
9c75942b0b
chore: remove commented code 2024-10-21 16:54:16 +03:00
5909659fa9
Use dynamic balance 2024-10-21 16:52:07 +03:00
eaac771722
move into testtag package 2024-10-21 16:48:24 +03:00
f3a5178de7
update imports 2024-10-21 16:47:43 +03:00
549d09890b
update imports 2024-10-21 16:47:25 +03:00
4a9ef6b5f2
move test account service to testutil 2024-10-21 16:47:00 +03:00
961d8d1a5c
update package import 2024-10-21 16:46:29 +03:00
a904cdbf44
move driver to testutil 2024-10-21 16:45:06 +03:00
3af943f77c
move account service tags switch to test tags package 2024-10-21 16:44:02 +03:00
14455f65cb
move account service to testutil 2024-10-21 16:43:24 +03:00
0c08654df3
move driver to testutil 2024-10-21 16:42:57 +03:00
acd0af25c6
catch api call error 2024-10-21 11:11:04 +03:00
66d2e2e838
add custodial api structs 2024-10-21 11:10:42 +03:00
4beeb9986a
use importable api structs 2024-10-21 11:10:23 +03:00
d81bc0eefb
use importable api structs 2024-10-21 11:10:01 +03:00
5b0a383513
use importable api structs 2024-10-21 11:09:37 +03:00
367d3f0593
remove manually added api.go file 2024-10-21 11:07:32 +03:00
415c807464
include the Database in context 2024-10-21 09:22:07 +03:00
9f562fe53e
use postgres directly from go-vise 2024-10-19 23:06:58 +03:00
3bf2045f15
added FetchVouchers to the TestAccountService 2024-10-19 16:07:40 +03:00
aced3e4fc3 Merge branch 'master' into menu-voucherlist 2024-10-19 15:38:54 +03:00
fb0d2db156 Merge branch 'master' into postgres-switch 2024-10-19 15:28:59 +03:00
a40fc37da4
implement postgres for the state store 2024-10-19 15:27:23 +03:00
f13f5996c1
remove thread abstraction from postgres 2024-10-19 14:51:41 +03:00
00a2beae50
rename file 2024-10-19 13:41:19 +03:00
25bc7006a4
show a balance of 0 and prevent sending when no voucher exists 2024-10-18 18:31:40 +03:00
f643aa4d14
update tests 2024-10-18 17:37:04 +03:00
b8860478b6
implement expected structs 2024-10-18 17:36:51 +03:00
847b91ca9e
Merge branch 'menu-api-errors' into api-structs 2024-10-18 17:15:05 +03:00
353e24de33
define api structs 2024-10-18 17:04:43 +03:00
1c57c95d93
use api structs 2024-10-18 17:04:02 +03:00
128a354b34
use api structs 2024-10-18 17:03:48 +03:00
81c4189c8e
update tests 2024-10-18 17:02:08 +03:00
b8e12e5215
update api endpoints 2024-10-18 17:01:21 +03:00
113f1a5b34 Merge pull request 'menu-traversal-v2' (#115) from menu-traversal-v2 into master
Reviewed-on: #115
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-10-18 15:38:56 +02:00
4f04362835
pass error description to error response 2024-10-17 16:08:18 +03:00
fd8bfae8c7
updated the vise version to include the updated column 2024-10-17 15:35:35 +03:00
eea3be3a39
resolve failing test 2024-10-17 13:49:56 +03:00
849a950fbc Merge branch 'master' into menu-voucherlist 2024-10-17 13:44:31 +03:00
6727bd3769
polished code 2024-10-17 12:55:47 +03:00
9f8fcf1ed0
attach account service to handler 2024-10-17 12:54:11 +03:00
4a599b902d
define transaction 2024-10-17 12:49:51 +03:00
a759f47c8e Merge branch 'master' into postgres-switch 2024-10-17 12:49:28 +03:00
d181c34946
update handler and move transaction struct to models 2024-10-17 12:49:28 +03:00
4968cdff37
switch to postgres once the flag is set 2024-10-17 12:47:57 +03:00
bfa6eac4c2
define a test account service 2024-10-17 12:47:44 +03:00
1aeb18379c
added a db flag and Database to the context 2024-10-17 12:46:20 +03:00
667a21d950
pass account service as a param 2024-10-17 12:37:15 +03:00
4416c008fc
add menu traversal tests 2024-10-17 12:36:24 +03:00
0a7389a71d
explicitly reset authorize flag 2024-10-17 12:36:02 +03:00
383e64776d
chore: change pin to PIN 2024-10-17 12:35:44 +03:00
a27c1790b8
setup test data file 2024-10-17 12:34:17 +03:00
f81e3508ca
define engine to run tests 2024-10-17 12:33:38 +03:00
127219f510
define build for running online and offline tests 2024-10-17 12:33:09 +03:00
54cc33c819
Delete connstr in threadgdbm global channel map on close 2024-10-17 12:32:31 +03:00
a51f739d06
pass account service as a param 2024-10-17 12:31:33 +03:00
8d047ebe05
define universal group driver 2024-10-17 12:30:58 +03:00
4e840ac17c
add uuid 2024-10-17 12:30:36 +03:00
f73b7a8b04
setup api responses 2024-10-16 22:40:40 +03:00
e986eaa538 Merge pull request 'menu-api-errors' (#112) from menu-api-errors into master
Reviewed-on: #112
2024-10-16 19:08:46 +02:00
d8db8df643
use go-vise v0.2.0 2024-10-15 23:42:39 +03:00
09eac03e10
use env variables 2024-10-15 23:41:16 +03:00
51122d0fc5
added an env.example file 2024-10-15 22:31:37 +03:00
d05c666513 Merge branch 'master' into menu-voucherlist 2024-10-15 17:08:48 +03:00
a336856c9b
use separate functions to process voucher data 2024-10-15 17:02:51 +03:00
0be570ae2d
add check for api call failure 2024-10-15 16:31:31 +03:00
6a36bc43b5
add check for api call failure 2024-10-15 16:31:15 +03:00
1d27a88908
add test on validate amount 2024-10-15 16:30:29 +03:00
f378f15422
updated tests 2024-10-15 16:29:51 +03:00
b2fb9faf6c
use the active balance to validate the amount 2024-10-15 16:17:33 +03:00
859e203a00
removed debug comments 2024-10-15 15:54:43 +03:00
6c6af5ec21
set a default voucher as the active sym if none exists 2024-10-15 15:46:02 +03:00
8ed98b3e6c
resolve case issue 2024-10-15 14:53:00 +03:00
4889e6d18b
Merge remote-tracking branch 'origin' into menu-api-errors 2024-10-15 14:04:45 +03:00
368c25125a
set flag count to 128 2024-10-15 13:59:49 +03:00
283793a2ae
add swahili menu option 2024-10-15 13:57:45 +03:00
bec7e5c69f
update: fetch balances in a sepate function,show profile information in swahili and english 2024-10-15 13:57:05 +03:00
d638aba85e
update tests 2024-10-15 13:50:28 +03:00
df7788dd0b
return actual reponses on the api calls 2024-10-15 13:48:14 +03:00
4a62773098
add handler fetching custodial balances 2024-10-15 13:44:50 +03:00
26d315b032
add check for api call failure 2024-10-15 13:42:20 +03:00
1927544533
reset authorized flag 2024-10-15 13:40:40 +03:00
c641a0c669
add api failure nodes 2024-10-14 23:19:09 +03:00
952da86931
update balance nodes 2024-10-14 23:18:42 +03:00
be6391686f
update return type 2024-10-14 23:17:57 +03:00
65794c1b20
add api calls flag 2024-10-14 23:17:17 +03:00
b058f9d770 Merge pull request 'Adapter to enable subdomain of db key prefixes' (#102) from lash/subprefix into master
Reviewed-on: #102
2024-10-14 15:11:07 +02:00
7df77a1343
updated the ValidateAmount to also check the active symbol, updated tests 2024-10-12 20:07:06 +03:00
f5dbfe553d
use the check_balance for the max_amount 2024-10-12 20:06:00 +03:00
7fe8f0b7d5
updated the args under FetchVouchers 2024-10-12 18:03:13 +03:00
a9f9867976
added the TestSetVoucher 2024-10-12 17:36:00 +03:00
b6d24bf929
update the TestCheckBalance 2024-10-12 16:29:12 +03:00
e9684fcf45
check whether the active symbol is empty 2024-10-12 16:28:02 +03:00
a5b1c5b74e
cleaned up the code 2024-10-12 14:32:40 +03:00
672eebb8fb
display the balance based on the symbol 2024-10-11 09:42:08 +03:00
8f834b3d76
ensure that a user is authorized 2024-10-11 09:41:47 +03:00
6c93fb76b1
correctly added the flag and the flag count 2024-10-11 09:36:43 +03:00
ea2df55295
use go-vise v0.2.0 2024-10-10 15:18:13 +03:00
7c08a0f0af
add temp and active balance 2024-10-10 14:48:23 +03:00
9ad7d5a522
set the active symbol 2024-10-09 15:39:06 +03:00
6c904a8b3f
add the temporary and active symbol 2024-10-09 15:38:19 +03:00
9ccb6cc066
use 99 as the quit 2024-10-09 15:05:59 +03:00
2c98a8e133
read token list from json file 2024-10-08 14:34:21 +03:00
4b6fd35e7a
define importable voucher response 2024-10-08 13:43:57 +03:00
b3c7a3a337
Merge remote-tracking branch 'refs/remotes/origin/menu-voucherlist' into menu-voucherlist 2024-10-08 13:43:19 +03:00
7b374ad801
define importable token list 2024-10-08 13:41:09 +03:00
cb4a52e4f2
view a selected voucher and verify the PIN 2024-10-07 16:16:57 +03:00
06ebcc0f07
added swahili template 2024-10-07 16:15:13 +03:00
e4ed9a65bb
store the symbols and balances 2024-10-07 13:49:12 +03:00
755899be4e
store the pre-rendered vouchers list 2024-10-07 08:59:15 +03:00
4dede757d2 Merge branch 'lash/subprefix' into menu-voucherlist 2024-10-07 08:06:37 +03:00
517f980664
get the vouchers list and store in gdbm 2024-10-05 16:56:49 +03:00
31aea6b807
added model and func to fetch vouchers from the API 2024-09-28 14:07:37 +03:00
3a46fda769
use save_temporary_pin and updated tests 2024-09-28 12:26:37 +03:00
c7bdfe90b7 Merge branch 'master' into menu-voucherlist 2024-09-28 11:07:36 +03:00
60134f14e9 Merge branch 'master' into menu-voucherlist 2024-09-27 19:11:35 +03:00
4c33945081
use latest go-vise update 2024-09-27 19:10:08 +03:00
221db4e998
return a numbered list of vouchers 2024-09-25 16:06:06 +03:00
0e376e0d9e
include back and quit 2024-09-25 16:03:08 +03:00
7aa44caea2
add voucher nodes 2024-09-25 15:57:23 +03:00
188cb573dd
add dummy vouchers list 2024-09-25 13:27:13 +03:00
80 changed files with 2924 additions and 611 deletions

18
.env.example Normal file
View File

@ -0,0 +1,18 @@
#Serve Http
PORT=7123
HOST=127.0.0.1
#PostgreSQL
DB_HOST=localhost
DB_USER=postgres
DB_PASSWORD=strongpass
DB_NAME=urdt_ussd
DB_PORT=5432
DB_SSLMODE=disable
DB_TIMEZONE=Africa/Nairobi
#External API Calls
CREATE_ACCOUNT_URL=http://localhost:5003/api/v2/account/create
TRACK_STATUS_URL=https://custodial.sarafu.africa/api/track/
BALANCE_URL=https://custodial.sarafu.africa/api/account/status/
TRACK_URL=http://localhost:5003/api/v2/account/status

View File

@ -16,7 +16,10 @@ import (
"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/config"
"git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/internal/handlers" "git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/handlers/server"
httpserver "git.grassecon.net/urdt/ussd/internal/http" httpserver "git.grassecon.net/urdt/ussd/internal/http"
"git.grassecon.net/urdt/ussd/internal/storage" "git.grassecon.net/urdt/ussd/internal/storage"
) )
@ -26,6 +29,10 @@ var (
scriptDir = path.Join("services", "registration") scriptDir = path.Join("services", "registration")
) )
func init() {
initializers.LoadEnvVariables()
}
type atRequestParser struct{} type atRequestParser struct{}
func (arp *atRequestParser) GetSessionId(rq any) (string, error) { func (arp *atRequestParser) GetSessionId(rq any) (string, error) {
@ -65,29 +72,34 @@ func (arp *atRequestParser) GetInput(rq any) ([]byte, error) {
} }
func main() { func main() {
config.LoadConfig()
var dbDir string var dbDir string
var resourceDir string var resourceDir string
var size uint var size uint
var database string
var engineDebug bool var engineDebug bool
var host string var host string
var port uint var port uint
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
flag.StringVar(&database, "db", "gdbm", "database to be used")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output") flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.UintVar(&size, "s", 160, "max size of output") flag.UintVar(&size, "s", 160, "max size of output")
flag.StringVar(&host, "h", "127.0.0.1", "http host") flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
flag.UintVar(&port, "p", 7123, "http port") flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
flag.Parse() flag.Parse()
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size) logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
ctx := context.Background() ctx := context.Background()
ctx = context.WithValue(ctx, "Database", database)
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(16), FlagCount: uint32(128),
} }
if engineDebug { if engineDebug {
@ -127,7 +139,8 @@ func main() {
os.Exit(1) os.Exit(1)
} }
hl, err := lhs.GetHandler() accountService := server.AccountService{}
hl, err := lhs.GetHandler(&accountService)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)

View File

@ -13,7 +13,10 @@ import (
"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/config"
"git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/internal/handlers" "git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/handlers/server"
"git.grassecon.net/urdt/ussd/internal/storage" "git.grassecon.net/urdt/ussd/internal/storage"
) )
@ -22,6 +25,10 @@ var (
scriptDir = path.Join("services", "registration") scriptDir = path.Join("services", "registration")
) )
func init() {
initializers.LoadEnvVariables()
}
type asyncRequestParser struct { type asyncRequestParser struct {
sessionId string sessionId string
input []byte input []byte
@ -36,31 +43,36 @@ func (p *asyncRequestParser) GetInput(r any) ([]byte, error) {
} }
func main() { func main() {
config.LoadConfig()
var sessionId string var sessionId string
var dbDir string var dbDir string
var resourceDir string var resourceDir string
var size uint var size uint
var database string
var engineDebug bool var engineDebug bool
var host string var host string
var port uint var port uint
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
flag.StringVar(&database, "db", "gdbm", "database to be used")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output") flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.UintVar(&size, "s", 160, "max size of output") flag.UintVar(&size, "s", 160, "max size of output")
flag.StringVar(&host, "h", "127.0.0.1", "http host") flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
flag.UintVar(&port, "p", 7123, "http port") flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
flag.Parse() flag.Parse()
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId) logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId)
ctx := context.Background() ctx := context.Background()
ctx = context.WithValue(ctx, "Database", database)
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(16), FlagCount: uint32(128),
} }
if engineDebug { if engineDebug {
@ -94,8 +106,9 @@ func main() {
lhs, err := handlers.NewLocalHandlerService(pfp, true, dbResource, cfg, rs) lhs, err := handlers.NewLocalHandlerService(pfp, true, dbResource, cfg, rs)
lhs.SetDataStore(&userdataStore) lhs.SetDataStore(&userdataStore)
accountService := server.AccountService{}
hl, err := lhs.GetHandler() hl, err := lhs.GetHandler(&accountService)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)

View File

@ -15,7 +15,10 @@ import (
"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/config"
"git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/internal/handlers" "git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/handlers/server"
httpserver "git.grassecon.net/urdt/ussd/internal/http" httpserver "git.grassecon.net/urdt/ussd/internal/http"
"git.grassecon.net/urdt/ussd/internal/storage" "git.grassecon.net/urdt/ussd/internal/storage"
) )
@ -25,30 +28,39 @@ var (
scriptDir = path.Join("services", "registration") scriptDir = path.Join("services", "registration")
) )
func init() {
initializers.LoadEnvVariables()
}
func main() { func main() {
config.LoadConfig()
var dbDir string var dbDir string
var resourceDir string var resourceDir string
var size uint var size uint
var database string
var engineDebug bool var engineDebug bool
var host string var host string
var port uint var port uint
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
flag.StringVar(&database, "db", "gdbm", "database to be used")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output") flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.UintVar(&size, "s", 160, "max size of output") flag.UintVar(&size, "s", 160, "max size of output")
flag.StringVar(&host, "h", "127.0.0.1", "http host") flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
flag.UintVar(&port, "p", 7123, "http port") flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
flag.Parse() flag.Parse()
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size) logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
ctx := context.Background() ctx := context.Background()
ctx = context.WithValue(ctx, "Database", database)
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(16), FlagCount: uint32(128),
} }
if engineDebug { if engineDebug {
@ -87,8 +99,8 @@ func main() {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
} }
accountService := server.AccountService{}
hl, err := lhs.GetHandler() hl, err := lhs.GetHandler(&accountService)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)

View File

@ -10,7 +10,10 @@ import (
"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/config"
"git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/internal/handlers" "git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/handlers/server"
"git.grassecon.net/urdt/ussd/internal/storage" "git.grassecon.net/urdt/ussd/internal/storage"
) )
@ -19,12 +22,20 @@ var (
scriptDir = path.Join("services", "registration") scriptDir = path.Join("services", "registration")
) )
func init() {
initializers.LoadEnvVariables()
}
func main() { func main() {
config.LoadConfig()
var dbDir string var dbDir string
var size uint var size uint
var sessionId string var sessionId string
var database string
var engineDebug bool var engineDebug bool
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") 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.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output") flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.UintVar(&size, "s", 160, "max size of output") flag.UintVar(&size, "s", 160, "max size of output")
@ -34,13 +45,14 @@ func main() {
ctx := context.Background() ctx := context.Background()
ctx = context.WithValue(ctx, "SessionId", sessionId) ctx = context.WithValue(ctx, "SessionId", sessionId)
ctx = context.WithValue(ctx, "Database", database)
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(16), FlagCount: uint32(128),
} }
resourceDir := scriptDir resourceDir := scriptDir
@ -85,7 +97,8 @@ func main() {
os.Exit(1) os.Exit(1)
} }
hl, err := lhs.GetHandler() accountService := server.AccountService{}
hl, err := lhs.GetHandler(&accountService)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)

18
common/hex.go Normal file
View File

@ -0,0 +1,18 @@
package common
import (
"encoding/hex"
)
func NormalizeHex(s string) (string, error) {
if len(s) >= 2 {
if s[:2] == "0x" {
s = s[2:]
}
}
r, err := hex.DecodeString(s)
if err != nil {
return "", err
}
return hex.EncodeToString(r), nil
}

View File

@ -1,10 +1,18 @@
package config package config
import "git.grassecon.net/urdt/ussd/initializers"
var (
const ( CreateAccountURL string
CreateAccountURL = "https://custodial.sarafu.africa/api/account/create" TrackStatusURL string
TrackStatusURL = "https://custodial.sarafu.africa/api/track/" BalanceURL string
BalanceURL = "https://custodial.sarafu.africa/api/account/status/" TrackURL string
) )
// LoadConfig initializes the configuration values after environment variables are loaded.
func LoadConfig() {
CreateAccountURL = initializers.GetEnv("CREATE_ACCOUNT_URL", "http://localhost:5003/api/v2/account/create")
TrackStatusURL = initializers.GetEnv("TRACK_STATUS_URL", "https://custodial.sarafu.africa/api/track/")
BalanceURL = initializers.GetEnv("BALANCE_URL", "https://custodial.sarafu.africa/api/account/status/")
TrackURL = initializers.GetEnv("TRACK_URL", "http://localhost:5003/api/v2/account/status")
}

25
go.mod
View File

@ -1,24 +1,41 @@
module git.grassecon.net/urdt/ussd module git.grassecon.net/urdt/ussd
go 1.22.6 go 1.23.0
toolchain go1.23.2
require ( require (
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b
github.com/alecthomas/assert/v2 v2.2.2 github.com/alecthomas/assert/v2 v2.2.2
github.com/grassrootseconomics/eth-custodial v1.3.0-beta
github.com/peteole/testdata-loader v0.3.0 github.com/peteole/testdata-loader v0.3.0
gopkg.in/leonelquinteros/gotext.v1 v1.3.1 gopkg.in/leonelquinteros/gotext.v1 v1.3.1
) )
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.1 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/joho/godotenv v1.5.1
github.com/kr/text v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/text v0.18.0 // indirect
)
require ( require (
github.com/alecthomas/participle/v2 v2.0.0 // indirect github.com/alecthomas/participle/v2 v2.0.0 // indirect
github.com/alecthomas/repr v0.2.0 // indirect github.com/alecthomas/repr v0.2.0 // indirect
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // indirect github.com/fxamacker/cbor/v2 v2.4.0 // indirect
github.com/gofrs/uuid v4.4.0+incompatible
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 // indirect github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect

48
go.sum
View File

@ -1,5 +1,5 @@
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb h1:6P4kxihcwMjDKzvUFC6t2zGNb7MDW+l/ACGlSAN1N8Y= git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b h1:dxBplsIlzJHV+5EH+gzB+w08Blt7IJbb2jeRe1OEjLU=
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M= git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b/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=
@ -8,29 +8,65 @@ github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c h1:H9Nm+I7Cg/YVPpEV1RzU3Wq2pjamPc/UtHDgItcb7lE= github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c h1:H9Nm+I7Cg/YVPpEV1RzU3Wq2pjamPc/UtHDgItcb7lE=
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c/go.mod h1:rGod7o6KPeJ+hyBpHfhi4v7blx9sf+QsHsA7KAsdN6U= github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c/go.mod h1:rGod7o6KPeJ+hyBpHfhi4v7blx9sf+QsHsA7KAsdN6U=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/grassrootseconomics/eth-custodial v1.3.0-beta h1:twrMBhl89GqDUL9PlkzQxMP/6OST1BByrNDj+rqXDmU=
github.com/grassrootseconomics/eth-custodial v1.3.0-beta/go.mod h1:7uhRcdnJplX4t6GKCEFkbeDhhjlcaGJeJqevbcvGLZo=
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo= github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo=
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y= github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a h1:0Q3H0YXzMHiciXtRcM+j0jiCe8WKPQHoRgQiRTnfcLY= github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a h1:0Q3H0YXzMHiciXtRcM+j0jiCe8WKPQHoRgQiRTnfcLY=
github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a/go.mod h1:CdTTBOYzS5E4mWS1N8NWP6AHI19MP0A2B18n3hLzRMk= github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a/go.mod h1:CdTTBOYzS5E4mWS1N8NWP6AHI19MP0A2B18n3hLzRMk=
github.com/pashagolub/pgxmock/v4 v4.3.0 h1:DqT7fk0OCK6H0GvqtcMsLpv8cIwWqdxWgfZNLeHCb/s=
github.com/pashagolub/pgxmock/v4 v4.3.0/go.mod h1:9VoVHXwS3XR/yPtKGzwQvwZX1kzGB9sM8SviDcHDa3A=
github.com/peteole/testdata-loader v0.3.0 h1:8jckE9KcyNHgyv/VPoaljvKZE0Rqr8+dPVYH6rfNr9I= github.com/peteole/testdata-loader v0.3.0 h1:8jckE9KcyNHgyv/VPoaljvKZE0Rqr8+dPVYH6rfNr9I=
github.com/peteole/testdata-loader v0.3.0/go.mod h1:Mt0ZbRtb56u8SLJpNP+BnQbENljMorYBpqlvt3cS83U= github.com/peteole/testdata-loader v0.3.0/go.mod h1:Mt0ZbRtb56u8SLJpNP+BnQbENljMorYBpqlvt3cS83U=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/leonelquinteros/gotext.v1 v1.3.1 h1:8d9/fdTG0kn/B7NNGV1BsEyvektXFAbkMsTZS2sFSCc= gopkg.in/leonelquinteros/gotext.v1 v1.3.1 h1:8d9/fdTG0kn/B7NNGV1BsEyvektXFAbkMsTZS2sFSCc=
gopkg.in/leonelquinteros/gotext.v1 v1.3.1/go.mod h1:X1WlGDeAFIYsW6GjgMm4VwUwZ2XjI7Zan2InxSUQWrU= gopkg.in/leonelquinteros/gotext.v1 v1.3.1/go.mod h1:X1WlGDeAFIYsW6GjgMm4VwUwZ2XjI7Zan2InxSUQWrU=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

34
initializers/load.go Normal file
View File

@ -0,0 +1,34 @@
package initializers
import (
"log"
"os"
"strconv"
"github.com/joho/godotenv"
)
func LoadEnvVariables() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
}
// Helper to get environment variables with a default fallback
func GetEnv(key, defaultVal string) string {
if value, exists := os.LookupEnv(key); exists {
return value
}
return defaultVal
}
// Helper to safely convert environment variables to uint
func GetEnvUint(key string, defaultVal uint) uint {
if value, exists := os.LookupEnv(key); exists {
if parsed, err := strconv.Atoi(value); err == nil && parsed >= 0 {
return uint(parsed)
}
}
return defaultVal
}

View File

@ -6,6 +6,7 @@ import (
"git.defalsify.org/vise.git/engine" "git.defalsify.org/vise.git/engine"
"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/internal/handlers/server"
"git.grassecon.net/urdt/ussd/internal/handlers/ussd" "git.grassecon.net/urdt/ussd/internal/handlers/ussd"
) )
@ -52,16 +53,16 @@ func (ls *LocalHandlerService) SetDataStore(db *db.Db) {
ls.UserdataStore = db ls.UserdataStore = db
} }
func (ls *LocalHandlerService) GetHandler() (*ussd.Handlers, error) { func (ls *LocalHandlerService) GetHandler(accountService server.AccountServiceInterface) (*ussd.Handlers, error) {
ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore) ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore,accountService)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ussdHandlers = ussdHandlers.WithPersister(ls.Pe) ussdHandlers = ussdHandlers.WithPersister(ls.Pe)
ls.DbRs.AddLocalFunc("set_language", ussdHandlers.SetLanguage) ls.DbRs.AddLocalFunc("set_language", ussdHandlers.SetLanguage)
ls.DbRs.AddLocalFunc("create_account", ussdHandlers.CreateAccount) ls.DbRs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
ls.DbRs.AddLocalFunc("save_pin", ussdHandlers.SavePin) ls.DbRs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin)
ls.DbRs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin) ls.DbRs.AddLocalFunc("verify_create_pin", ussdHandlers.VerifyCreatePin)
ls.DbRs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier) ls.DbRs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier)
ls.DbRs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus) ls.DbRs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus)
ls.DbRs.AddLocalFunc("authorize_account", ussdHandlers.Authorize) ls.DbRs.AddLocalFunc("authorize_account", ussdHandlers.Authorize)
@ -82,17 +83,21 @@ func (ls *LocalHandlerService) GetHandler() (*ussd.Handlers, error) {
ls.DbRs.AddLocalFunc("save_location", ussdHandlers.SaveLocation) ls.DbRs.AddLocalFunc("save_location", ussdHandlers.SaveLocation)
ls.DbRs.AddLocalFunc("save_yob", ussdHandlers.SaveYob) ls.DbRs.AddLocalFunc("save_yob", ussdHandlers.SaveYob)
ls.DbRs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings) ls.DbRs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings)
ls.DbRs.AddLocalFunc("quit_with_balance", ussdHandlers.QuitWithBalance)
ls.DbRs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized) ls.DbRs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized)
ls.DbRs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate) ls.DbRs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate)
ls.DbRs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo) ls.DbRs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo)
ls.DbRs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob) ls.DbRs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob)
ls.DbRs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob) ls.DbRs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob)
ls.DbRs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction) ls.DbRs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction)
ls.DbRs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin)
ls.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin) ls.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin)
ls.DbRs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange) ls.DbRs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange)
ls.DbRs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp) ls.DbRs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp)
ls.DbRs.AddLocalFunc("fetch_custodial_balances", ussdHandlers.FetchCustodialBalances)
ls.DbRs.AddLocalFunc("set_default_voucher", ussdHandlers.SetDefaultVoucher)
ls.DbRs.AddLocalFunc("check_vouchers", ussdHandlers.CheckVouchers)
ls.DbRs.AddLocalFunc("get_vouchers", ussdHandlers.GetVoucherList)
ls.DbRs.AddLocalFunc("view_voucher", ussdHandlers.ViewVoucher)
ls.DbRs.AddLocalFunc("set_voucher", ussdHandlers.SetVoucher)
return ussdHandlers, nil return ussdHandlers, nil
} }

View File

@ -1,112 +1,185 @@
package server package server
import ( import (
"context"
"encoding/json" "encoding/json"
"errors"
"fmt"
"io" "io"
"net/http" "net/http"
"os"
"git.grassecon.net/urdt/ussd/config" "git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/internal/models" "git.grassecon.net/urdt/ussd/internal/models"
"github.com/grassrootseconomics/eth-custodial/pkg/api"
)
var (
okResponse api.OKResponse
errResponse api.ErrResponse
) )
type AccountServiceInterface interface { type AccountServiceInterface interface {
CheckBalance(publicKey string) (string, error) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResponse, error)
CreateAccount() (*models.AccountResponse, error) CreateAccount(ctx context.Context) (*api.OKResponse, error)
CheckAccountStatus(trackingId string) (string, error) CheckAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResponse, error)
TrackAccountStatus(ctx context.Context, publicKey string) (*api.OKResponse, error)
FetchVouchers(ctx context.Context, publicKey string) (*models.VoucherHoldingResponse, error)
} }
type AccountService struct { type AccountService struct {
} }
// CheckAccountStatus retrieves the status of an account transaction based on the provided tracking ID.
//
// Parameters: // Parameters:
// - trackingId: A unique identifier for the account.This should be obtained from a previous call to // - trackingId: A unique identifier for the account.This should be obtained from a previous call to
// CreateAccount or a similar function that returns an AccountResponse. The `trackingId` field in the // CreateAccount or a similar function that returns an AccountResponse. The `trackingId` field in the
// AccountResponse struct can be used here to check the account status during a transaction. // AccountResponse struct can be used here to check the account status during a transaction.
// //
//
// Returns: // Returns:
// - string: The status of the transaction as a string. If there is an error during the request or processing, this will be an empty string. // - string: The status of the transaction as a string. If there is an error during the request or processing, this will be an empty string.
// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data. // - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
// If no error occurs, this will be nil. // If no error occurs, this will be nil
// func (as *AccountService) CheckAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResponse, error) {
func (as *AccountService) CheckAccountStatus(trackingId string) (string, error) { resp, err := http.Get(config.BalanceURL + trackingId)
resp, err := http.Get(config.TrackStatusURL + trackingId)
if err != nil { if err != nil {
return "", err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return "", err return nil, err
} }
var trackResp models.TrackStatusResponse var trackResp models.TrackStatusResponse
err = json.Unmarshal(body, &trackResp) err = json.Unmarshal(body, &trackResp)
if err != nil { if err != nil {
return "", err return nil, err
} }
return &trackResp, nil
status := trackResp.Result.Transaction.Status
return status, nil
} }
func (as *AccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*api.OKResponse, error) {
// CheckBalance retrieves the balance for a given public key from the custodial balance API endpoint. var err error
// Parameters: // Construct the URL with the path parameter
// - publicKey: The public key associated with the account whose balance needs to be checked. url := fmt.Sprintf("%s/%s", config.TrackURL, publicKey)
func (as *AccountService) CheckBalance(publicKey string) (string, error) { req, err := http.NewRequest("GET", url, nil)
resp, err := http.Get(config.BalanceURL + publicKey)
if err != nil { if err != nil {
return "0.0", err return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-GE-KEY", "xd")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return "0.0", err errResponse.Description = err.Error()
return nil, err
} }
if resp.StatusCode >= http.StatusBadRequest {
err := json.Unmarshal([]byte(body), &errResponse)
if err != nil {
return nil, err
}
return nil, errors.New(errResponse.Description)
}
err = json.Unmarshal([]byte(body), &okResponse)
if err != nil {
return nil, err
}
if len(okResponse.Result) == 0 {
return nil, errors.New("Empty api result")
}
return &okResponse, nil
}
// CheckBalance retrieves the balance for a given public key from the custodial balance API endpoint.
// Parameters:
// - publicKey: The public key associated with the account whose balance needs to be checked.
func (as *AccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResponse, error) {
resp, err := http.Get(config.BalanceURL + publicKey)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var balanceResp models.BalanceResponse var balanceResp models.BalanceResponse
err = json.Unmarshal(body, &balanceResp) err = json.Unmarshal(body, &balanceResp)
if err != nil { if err != nil {
return "0.0", err return nil, err
} }
return &balanceResp, nil
balance := balanceResp.Result.Balance
return balance, nil
} }
// CreateAccount creates a new account in the custodial system.
//CreateAccount creates a new account in the custodial system.
// Returns: // Returns:
// - *models.AccountResponse: A pointer to an AccountResponse struct containing the details of the created account. // - *models.AccountResponse: A pointer to an AccountResponse struct containing the details of the created account.
// If there is an error during the request or processing, this will be nil. // If there is an error during the request or processing, this will be nil.
// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data. // - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
// If no error occurs, this will be nil. // If no error occurs, this will be nil.
func (as *AccountService) CreateAccount() (*models.AccountResponse, error) { func (as *AccountService) CreateAccount(ctx context.Context) (*api.OKResponse, error) {
resp, err := http.Post(config.CreateAccountURL, "application/json", nil) var err error
// Create a new request
req, err := http.NewRequest("POST", config.CreateAccountURL, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-GE-KEY", "xd")
resp, err := http.DefaultClient.Do(req)
if err != nil {
errResponse.Description = err.Error()
return nil, err
}
defer resp.Body.Close() defer resp.Body.Close()
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if resp.StatusCode >= http.StatusBadRequest {
var accountResp models.AccountResponse err := json.Unmarshal([]byte(body), &errResponse)
err = json.Unmarshal(body, &accountResp) if err != nil {
return nil, err
}
return nil, errors.New(errResponse.Description)
}
err = json.Unmarshal([]byte(body), &okResponse)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(okResponse.Result) == 0 {
return &accountResp, nil return nil, errors.New("Empty api result")
}
return &okResponse, nil
}
// FetchVouchers retrieves the token holdings for a given public key from the custodial holdings API endpoint
// Parameters:
// - publicKey: The public key associated with the account.
func (as *AccountService) FetchVouchers(ctx context.Context, publicKey string) (*models.VoucherHoldingResponse, error) {
file, err := os.Open("sample_tokens.json")
if err != nil {
return nil, err
}
defer file.Close()
var holdings models.VoucherHoldingResponse
if err := json.NewDecoder(file).Decode(&holdings); err != nil {
return nil, err
}
return &holdings, nil
} }

View File

@ -10,6 +10,7 @@ import (
"strings" "strings"
"git.defalsify.org/vise.git/asm" "git.defalsify.org/vise.git/asm"
"github.com/grassrootseconomics/eth-custodial/pkg/api"
"git.defalsify.org/vise.git/cache" "git.defalsify.org/vise.git/cache"
"git.defalsify.org/vise.git/db" "git.defalsify.org/vise.git/db"
@ -20,13 +21,19 @@ import (
"git.defalsify.org/vise.git/state" "git.defalsify.org/vise.git/state"
"git.grassecon.net/urdt/ussd/internal/handlers/server" "git.grassecon.net/urdt/ussd/internal/handlers/server"
"git.grassecon.net/urdt/ussd/internal/utils" "git.grassecon.net/urdt/ussd/internal/utils"
"git.grassecon.net/urdt/ussd/common"
"gopkg.in/leonelquinteros/gotext.v1" "gopkg.in/leonelquinteros/gotext.v1"
"git.grassecon.net/urdt/ussd/internal/storage"
) )
var ( var (
logg = logging.NewVanilla().WithDomain("ussdmenuhandler") logg = logging.NewVanilla().WithDomain("ussdmenuhandler")
scriptDir = path.Join("services", "registration") scriptDir = path.Join("services", "registration")
translationDir = path.Join(scriptDir, "locale") translationDir = path.Join(scriptDir, "locale")
okResponse *api.OKResponse
errResponse *api.ErrResponse
backOption = []byte("0")
) )
// FlagManager handles centralized flag management // FlagManager handles centralized flag management
@ -61,7 +68,7 @@ type Handlers struct {
accountService server.AccountServiceInterface accountService server.AccountServiceInterface
} }
func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db) (*Handlers, error) { func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, accountService server.AccountServiceInterface) (*Handlers, error) {
if userdataStore == nil { if userdataStore == nil {
return nil, fmt.Errorf("cannot create handler with nil userdata store") return nil, fmt.Errorf("cannot create handler with nil userdata store")
} }
@ -71,7 +78,7 @@ func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db) (*Handlers, erro
h := &Handlers{ h := &Handlers{
userdataStore: userDb, userdataStore: userDb,
flagManager: appFlags, flagManager: appFlags,
accountService: &server.AccountService{}, accountService: accountService,
} }
return h, nil return h, nil
} }
@ -136,23 +143,35 @@ func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (r
} }
func (h *Handlers) createAccountNoExist(ctx context.Context, sessionId string, res *resource.Result) error { func (h *Handlers) createAccountNoExist(ctx context.Context, sessionId string, res *resource.Result) error {
accountResp, err := h.accountService.CreateAccount() flag_account_created, _ := h.flagManager.GetFlag("flag_account_created")
data := map[utils.DataTyp]string{ okResponse, err := h.accountService.CreateAccount(ctx)
utils.DATA_TRACKING_ID: accountResp.Result.TrackingId, if err != nil {
utils.DATA_PUBLIC_KEY: accountResp.Result.PublicKey, return err
utils.DATA_CUSTODIAL_ID: accountResp.Result.CustodialId.String(),
} }
trackingId := okResponse.Result["trackingId"].(string)
publicKey := okResponse.Result["publicKey"].(string)
data := map[utils.DataTyp]string{
utils.DATA_TRACKING_ID: trackingId,
utils.DATA_PUBLIC_KEY: publicKey,
}
store := h.userdataStore
for key, value := range data { for key, value := range data {
store := h.userdataStore err = store.WriteEntry(ctx, sessionId, key, []byte(value))
err := store.WriteEntry(ctx, sessionId, key, []byte(value))
if err != nil { if err != nil {
return err return err
} }
} }
flag_account_created, _ := h.flagManager.GetFlag("flag_account_created") publicKeyNormalized, err := common.NormalizeHex(publicKey)
if err != nil {
return err
}
err = store.WriteEntry(ctx, publicKeyNormalized, utils.DATA_PUBLIC_KEY_REVERSE, []byte(sessionId))
if err != nil {
return err
}
res.FlagSet = append(res.FlagSet, flag_account_created) res.FlagSet = append(res.FlagSet, flag_account_created)
return err return nil
} }
@ -180,34 +199,6 @@ func (h *Handlers) CreateAccount(ctx context.Context, sym string, input []byte)
return res, nil return res, nil
} }
// SavePin persists the user's PIN choice into the filesystem
func (h *Handlers) SavePin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
accountPIN := string(input)
// Validate that the PIN is a 4-digit number
if !isValidPIN(accountPIN) {
res.FlagSet = append(res.FlagSet, flag_incorrect_pin)
return res, nil
}
res.FlagReset = append(res.FlagReset, flag_incorrect_pin)
store := h.userdataStore
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(accountPIN))
if err != nil {
return res, err
}
return res, nil
}
func (h *Handlers) VerifyNewPin(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) VerifyNewPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
res := resource.Result{} res := resource.Result{}
_, ok := ctx.Value("SessionId").(string) _, ok := ctx.Value("SessionId").(string)
@ -226,6 +217,9 @@ func (h *Handlers) VerifyNewPin(ctx context.Context, sym string, input []byte) (
return res, nil return res, nil
} }
// SaveTemporaryPin saves the valid PIN input to the DATA_TEMPORARY_PIN
// during the account creation process
// and during the change PIN process
func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
var err error var err error
@ -234,6 +228,7 @@ func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byt
if !ok { if !ok {
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin") flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
accountPIN := string(input) accountPIN := string(input)
@ -243,11 +238,15 @@ func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byt
res.FlagSet = append(res.FlagSet, flag_incorrect_pin) res.FlagSet = append(res.FlagSet, flag_incorrect_pin)
return res, nil return res, nil
} }
res.FlagReset = append(res.FlagReset, flag_incorrect_pin)
store := h.userdataStore store := h.userdataStore
err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_PIN, []byte(accountPIN)) err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_PIN, []byte(accountPIN))
if err != nil { if err != nil {
return res, err return res, err
} }
return res, nil return res, nil
} }
@ -276,10 +275,10 @@ func (h *Handlers) ConfirmPinChange(ctx context.Context, sym string, input []byt
return res, nil return res, nil
} }
// VerifyPin checks whether the confirmation PIN is similar to the account PIN // VerifyCreatePin checks whether the confirmation PIN is similar to the temporary PIN
// If similar, it sets the USERFLAG_PIN_SET flag allowing the user // If similar, it sets the USERFLAG_PIN_SET flag and writes the account PIN allowing the user
// to access the main menu // to access the main menu
func (h *Handlers) VerifyPin(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) VerifyCreatePin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
flag_valid_pin, _ := h.flagManager.GetFlag("flag_valid_pin") flag_valid_pin, _ := h.flagManager.GetFlag("flag_valid_pin")
@ -290,15 +289,13 @@ func (h *Handlers) VerifyPin(ctx context.Context, sym string, input []byte) (res
if !ok { if !ok {
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
//AccountPin, _ := utils.ReadEntry(ctx, h.userdataStore, sessionId, utils.DATA_ACCOUNT_PIN)
store := h.userdataStore store := h.userdataStore
AccountPin, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN) temporaryPin, err := store.ReadEntry(ctx, sessionId, utils.DATA_TEMPORARY_PIN)
if err != nil { if err != nil {
return res, err return res, err
} }
if bytes.Equal(input, AccountPin) { if bytes.Equal(input, temporaryPin) {
res.FlagSet = []uint32{flag_valid_pin} res.FlagSet = []uint32{flag_valid_pin}
res.FlagReset = []uint32{flag_pin_mismatch} res.FlagReset = []uint32{flag_pin_mismatch}
res.FlagSet = append(res.FlagSet, flag_pin_set) res.FlagSet = append(res.FlagSet, flag_pin_set)
@ -306,6 +303,11 @@ func (h *Handlers) VerifyPin(ctx context.Context, sym string, input []byte) (res
res.FlagSet = []uint32{flag_pin_mismatch} res.FlagSet = []uint32{flag_pin_mismatch}
} }
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(temporaryPin))
if err != nil {
return res, err
}
return res, nil return res, nil
} }
@ -328,6 +330,9 @@ func (h *Handlers) SaveFirstname(ctx context.Context, sym string, input []byte)
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
if len(input) > 0 { if len(input) > 0 {
if bytes.Equal(input, backOption) {
return res, nil
}
firstName := string(input) firstName := string(input)
store := h.userdataStore store := h.userdataStore
err = store.WriteEntry(ctx, sessionId, utils.DATA_FIRST_NAME, []byte(firstName)) err = store.WriteEntry(ctx, sessionId, utils.DATA_FIRST_NAME, []byte(firstName))
@ -347,8 +352,10 @@ func (h *Handlers) SaveFamilyname(ctx context.Context, sym string, input []byte)
if !ok { if !ok {
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
if len(input) > 0 { if len(input) > 0 {
if bytes.Equal(input, backOption) {
return res, nil
}
familyName := string(input) familyName := string(input)
store := h.userdataStore store := h.userdataStore
err = store.WriteEntry(ctx, sessionId, utils.DATA_FAMILY_NAME, []byte(familyName)) err = store.WriteEntry(ctx, sessionId, utils.DATA_FAMILY_NAME, []byte(familyName))
@ -370,7 +377,6 @@ func (h *Handlers) SaveYob(ctx context.Context, sym string, input []byte) (resou
if !ok { if !ok {
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
if len(input) == 4 { if len(input) == 4 {
yob := string(input) yob := string(input)
store := h.userdataStore store := h.userdataStore
@ -391,8 +397,10 @@ func (h *Handlers) SaveLocation(ctx context.Context, sym string, input []byte) (
if !ok { if !ok {
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
if len(input) > 0 { if len(input) > 0 {
if bytes.Equal(input, backOption) {
return res, nil
}
location := string(input) location := string(input)
store := h.userdataStore store := h.userdataStore
err = store.WriteEntry(ctx, sessionId, utils.DATA_LOCATION, []byte(location)) err = store.WriteEntry(ctx, sessionId, utils.DATA_LOCATION, []byte(location))
@ -413,7 +421,9 @@ func (h *Handlers) SaveGender(ctx context.Context, sym string, input []byte) (re
if !ok { if !ok {
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
if bytes.Equal(input, backOption) {
return res, nil
}
gender := strings.Split(symbol, "_")[1] gender := strings.Split(symbol, "_")[1]
store := h.userdataStore store := h.userdataStore
err = store.WriteEntry(ctx, sessionId, utils.DATA_GENDER, []byte(gender)) err = store.WriteEntry(ctx, sessionId, utils.DATA_GENDER, []byte(gender))
@ -432,7 +442,6 @@ func (h *Handlers) SaveOfferings(ctx context.Context, sym string, input []byte)
if !ok { if !ok {
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
if len(input) > 0 { if len(input) > 0 {
offerings := string(input) offerings := string(input)
store := h.userdataStore store := h.userdataStore
@ -458,7 +467,6 @@ func (h *Handlers) ResetAllowUpdate(ctx context.Context, sym string, input []byt
// ResetAccountAuthorized resets the account authorization flag after a successful PIN entry. // ResetAccountAuthorized resets the account authorization flag after a successful PIN entry.
func (h *Handlers) ResetAccountAuthorized(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) ResetAccountAuthorized(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized") flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
res.FlagReset = append(res.FlagReset, flag_account_authorized) res.FlagReset = append(res.FlagReset, flag_account_authorized)
@ -468,12 +476,10 @@ func (h *Handlers) ResetAccountAuthorized(ctx context.Context, sym string, input
// CheckIdentifier retrieves the PublicKey from the JSON data file. // CheckIdentifier retrieves the PublicKey from the JSON data file.
func (h *Handlers) CheckIdentifier(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) CheckIdentifier(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
sessionId, ok := ctx.Value("SessionId").(string) sessionId, ok := ctx.Value("SessionId").(string)
if !ok { if !ok {
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
store := h.userdataStore store := h.userdataStore
publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY) publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
@ -487,12 +493,10 @@ func (h *Handlers) CheckIdentifier(ctx context.Context, sym string, input []byte
func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
var err error var err error
sessionId, ok := ctx.Value("SessionId").(string) sessionId, ok := ctx.Value("SessionId").(string)
if !ok { if !ok {
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin") flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized") flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update") flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update")
@ -525,9 +529,7 @@ func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (res
// ResetIncorrectPin resets the incorrect pin flag after a new PIN attempt. // ResetIncorrectPin resets the incorrect pin flag after a new PIN attempt.
func (h *Handlers) ResetIncorrectPin(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) ResetIncorrectPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin") flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
res.FlagReset = append(res.FlagReset, flag_incorrect_pin) res.FlagReset = append(res.FlagReset, flag_incorrect_pin)
return res, nil return res, nil
} }
@ -539,29 +541,28 @@ func (h *Handlers) CheckAccountStatus(ctx context.Context, sym string, input []b
flag_account_success, _ := h.flagManager.GetFlag("flag_account_success") flag_account_success, _ := h.flagManager.GetFlag("flag_account_success")
flag_account_pending, _ := h.flagManager.GetFlag("flag_account_pending") flag_account_pending, _ := h.flagManager.GetFlag("flag_account_pending")
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
sessionId, ok := ctx.Value("SessionId").(string) sessionId, ok := ctx.Value("SessionId").(string)
if !ok { if !ok {
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
store := h.userdataStore store := h.userdataStore
trackingId, err := store.ReadEntry(ctx, sessionId, utils.DATA_TRACKING_ID) publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
if err != nil { if err != nil {
return res, err return res, err
} }
okResponse, err = h.accountService.TrackAccountStatus(ctx, string(publicKey))
status, err := h.accountService.CheckAccountStatus(string(trackingId))
if err != nil { if err != nil {
fmt.Println("Error checking account status:", err) res.FlagSet = append(res.FlagSet, flag_api_error)
return res, err return res, err
} }
res.FlagReset = append(res.FlagReset, flag_api_error)
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACCOUNT_STATUS, []byte(status)) isActive := okResponse.Result["active"].(bool)
if err != nil { if !ok {
return res, nil return res, err
} }
if isActive {
if status == "SUCCESS" {
res.FlagSet = append(res.FlagSet, flag_account_success) res.FlagSet = append(res.FlagSet, flag_account_success)
res.FlagReset = append(res.FlagReset, flag_account_pending) res.FlagReset = append(res.FlagReset, flag_account_pending)
} else { } else {
@ -635,7 +636,7 @@ func (h *Handlers) ResetIncorrectYob(ctx context.Context, sym string, input []by
return res, nil return res, nil
} }
// CheckBalance retrieves the balance from the API using the "PublicKey" and sets // CheckBalance retrieves the balance of the active voucher and sets
// the balance as the result content // the balance as the result content
func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
@ -646,18 +647,70 @@ func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) (
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
store := h.userdataStore
// get the active sym and active balance
activeSym, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM)
if err != nil {
if db.IsNotFound(err) {
balance := "0.00"
res.Content = l.Get("Balance: %s\n", balance)
return res, nil
}
return res, err
}
activeBal, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_BAL)
if err != nil {
return res, err
}
res.Content = l.Get("Balance: %s\n", fmt.Sprintf("%s %s", activeBal, activeSym))
return res, nil
}
func (h *Handlers) FetchCustodialBalances(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
symbol, _ := h.st.Where()
balanceType := strings.Split(symbol, "_")[0]
store := h.userdataStore store := h.userdataStore
publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY) publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
if err != nil { if err != nil {
return res, err return res, err
} }
balance, err := h.accountService.CheckBalance(string(publicKey)) balanceResponse, err := h.accountService.CheckBalance(ctx, string(publicKey))
if err != nil { if err != nil {
return res, nil return res, nil
} }
res.Content = balance if !balanceResponse.Ok {
res.FlagSet = append(res.FlagSet, flag_api_error)
return res, nil
}
res.FlagReset = append(res.FlagReset, flag_api_error)
balance := balanceResponse.Result.Balance
switch balanceType {
case "my":
res.Content = fmt.Sprintf("Your balance is %s", balance)
case "community":
res.Content = fmt.Sprintf("Your community balance is %s", balance)
default:
break
}
return res, nil return res, nil
} }
@ -755,14 +808,13 @@ func (h *Handlers) MaxAmount(ctx context.Context, sym string, input []byte) (res
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
store := h.userdataStore store := h.userdataStore
publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
balance, err := h.accountService.CheckBalance(string(publicKey)) activeBal, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_BAL)
if err != nil { if err != nil {
return res, nil return res, err
} }
res.Content = balance res.Content = string(activeBal)
return res, nil return res, nil
} }
@ -771,47 +823,29 @@ func (h *Handlers) MaxAmount(ctx context.Context, sym string, input []byte) (res
// it is not more than the current balance. // it is not more than the current balance.
func (h *Handlers) ValidateAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) ValidateAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
var err error
sessionId, ok := ctx.Value("SessionId").(string) sessionId, ok := ctx.Value("SessionId").(string)
if !ok { if !ok {
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
flag_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount") flag_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount")
store := h.userdataStore store := h.userdataStore
publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
amountStr := string(input) var balanceValue float64
balanceStr, err := h.accountService.CheckBalance(string(publicKey))
// retrieve the active balance
activeBal, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_BAL)
if err != nil { if err != nil {
return res, err return res, err
} }
res.Content = balanceStr balanceValue, err = strconv.ParseFloat(string(activeBal), 64)
// Parse the balance
balanceParts := strings.Split(balanceStr, " ")
if len(balanceParts) != 2 {
return res, fmt.Errorf("unexpected balance format: %s", balanceStr)
}
balanceValue, err := strconv.ParseFloat(balanceParts[0], 64)
if err != nil { if err != nil {
return res, fmt.Errorf("failed to parse balance: %v", err) return res, err
} }
// Extract numeric part from input // Extract numeric part from the input amount
re := regexp.MustCompile(`^(\d+(\.\d+)?)\s*(?:CELO)?$`) amountStr := strings.TrimSpace(string(input))
matches := re.FindStringSubmatch(strings.TrimSpace(amountStr)) inputAmount, err := strconv.ParseFloat(amountStr, 64)
if len(matches) < 2 {
res.FlagSet = append(res.FlagSet, flag_invalid_amount)
res.Content = amountStr
return res, nil
}
inputAmount, err := strconv.ParseFloat(matches[1], 64)
if err != nil { if err != nil {
res.FlagSet = append(res.FlagSet, flag_invalid_amount) res.FlagSet = append(res.FlagSet, flag_invalid_amount)
res.Content = amountStr res.Content = amountStr
@ -824,12 +858,14 @@ func (h *Handlers) ValidateAmount(ctx context.Context, sym string, input []byte)
return res, nil return res, nil
} }
res.Content = fmt.Sprintf("%.3f", inputAmount) // Format to 3 decimal places // Format the amount with 2 decimal places before saving
err = store.WriteEntry(ctx, sessionId, utils.DATA_AMOUNT, []byte(amountStr)) formattedAmount := fmt.Sprintf("%.2f", inputAmount)
err = store.WriteEntry(ctx, sessionId, utils.DATA_AMOUNT, []byte(formattedAmount))
if err != nil { if err != nil {
return res, err return res, err
} }
res.Content = fmt.Sprintf("%s", formattedAmount)
return res, nil return res, nil
} }
@ -849,7 +885,7 @@ func (h *Handlers) GetRecipient(ctx context.Context, sym string, input []byte) (
return res, nil return res, nil
} }
// GetSender retrieves the public key from the Gdbm Db // GetSender returns the sessionId (phoneNumber)
func (h *Handlers) GetSender(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) GetSender(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
@ -858,10 +894,7 @@ func (h *Handlers) GetSender(ctx context.Context, sym string, input []byte) (res
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
store := h.userdataStore res.Content = string(sessionId)
publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
res.Content = string(publicKey)
return res, nil return res, nil
} }
@ -875,40 +908,17 @@ func (h *Handlers) GetAmount(ctx context.Context, sym string, input []byte) (res
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
store := h.userdataStore store := h.userdataStore
amount, _ := store.ReadEntry(ctx, sessionId, utils.DATA_AMOUNT)
res.Content = string(amount) // retrieve the active symbol
activeSym, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM)
return res, nil
}
// QuickWithBalance retrieves the balance for a given public key from the custodial balance API endpoint before
// gracefully exiting the session.
func (h *Handlers) QuitWithBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
store := h.userdataStore
publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
if err != nil { if err != nil {
return res, err return res, err
} }
balance, err := h.accountService.CheckBalance(string(publicKey))
if err != nil { amount, _ := store.ReadEntry(ctx, sessionId, utils.DATA_AMOUNT)
return res, nil
} res.Content = fmt.Sprintf("%s %s", string(amount), string(activeSym))
res.Content = l.Get("Your account balance is %s", balance)
res.FlagReset = append(res.FlagReset, flag_account_authorized)
return res, nil return res, nil
} }
@ -928,13 +938,14 @@ func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input []
// TODO // TODO
// Use the amount, recipient and sender to call the API and initialize the transaction // Use the amount, recipient and sender to call the API and initialize the transaction
store := h.userdataStore store := h.userdataStore
publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
amount, _ := store.ReadEntry(ctx, sessionId, utils.DATA_AMOUNT) amount, _ := store.ReadEntry(ctx, sessionId, utils.DATA_AMOUNT)
recipient, _ := store.ReadEntry(ctx, sessionId, utils.DATA_RECIPIENT) recipient, _ := store.ReadEntry(ctx, sessionId, utils.DATA_RECIPIENT)
res.Content = l.Get("Your request has been sent. %s will receive %s from %s.", string(recipient), string(amount), string(publicKey)) activeSym, _ := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM)
res.Content = l.Get("Your request has been sent. %s will receive %s %s from %s.", string(recipient), string(amount), string(activeSym), string(sessionId))
account_authorized_flag, err := h.flagManager.GetFlag("flag_account_authorized") account_authorized_flag, err := h.flagManager.GetFlag("flag_account_authorized")
if err != nil { if err != nil {
@ -945,16 +956,23 @@ func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input []
return res, nil return res, nil
} }
// GetProfileInfo retrieves and formats the profile information of a user from a Gdbm backed storage.
func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
var defaultValue string
sessionId, ok := ctx.Value("SessionId").(string) sessionId, ok := ctx.Value("SessionId").(string)
if !ok { if !ok {
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
language, ok := ctx.Value("Language").(lang.Language)
// Default value when an entry is not found if !ok {
defaultValue := "Not Provided" return res, fmt.Errorf("value for 'Language' is not of type lang.Language")
}
code := language.Code
if code == "swa" {
defaultValue = "Haipo"
} else {
defaultValue = "Not Provided"
}
// Helper function to handle nil byte slices and convert them to string // Helper function to handle nil byte slices and convert them to string
getEntryOrDefault := func(entry []byte, err error) string { getEntryOrDefault := func(entry []byte, err error) string {
@ -991,12 +1009,299 @@ func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte)
return res, fmt.Errorf("invalid year of birth: %v", err) return res, fmt.Errorf("invalid year of birth: %v", err)
} }
} }
switch language.Code {
// Format the result case "eng":
res.Content = fmt.Sprintf( res.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",
name, gender, age, location, offerings, name, gender, age, location, offerings,
) )
case "swa":
res.Content = fmt.Sprintf(
"Jina: %s\nJinsia: %s\nUmri: %s\nEneo: %s\nUnauza: %s\n",
name, gender, age, location, offerings,
)
default:
res.Content = fmt.Sprintf(
"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
name, gender, age, location, offerings,
)
}
return res, nil
}
// SetDefaultVoucher retrieves the current vouchers
// and sets the first as the default voucher, if no active voucher is set
func (h *Handlers) SetDefaultVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
store := h.userdataStore
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
flag_no_active_voucher, _ := h.flagManager.GetFlag("flag_no_active_voucher")
// check if the user has an active sym
_, err = store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM)
if err != nil {
if db.IsNotFound(err) {
publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
if err != nil {
return res, nil
}
// Fetch vouchers from the API using the public key
vouchersResp, err := h.accountService.FetchVouchers(ctx, string(publicKey))
if err != nil {
return res, nil
}
// Return if there is no voucher
if len(vouchersResp.Result.Holdings) == 0 {
res.FlagSet = append(res.FlagSet, flag_no_active_voucher)
return res, nil
}
// Use only the first voucher
firstVoucher := vouchersResp.Result.Holdings[0]
defaultSym := firstVoucher.TokenSymbol
defaultBal := firstVoucher.Balance
// set the active symbol
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM, []byte(defaultSym))
if err != nil {
return res, err
}
// set the active balance
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACTIVE_BAL, []byte(defaultBal))
if err != nil {
return res, err
}
return res, nil
}
return res, err
}
res.FlagReset = append(res.FlagReset, flag_no_active_voucher)
return res, nil
}
// CheckVouchers retrieves the token holdings from the API using the "PublicKey" and stores
// them to gdbm
func (h *Handlers) CheckVouchers(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
store := h.userdataStore
publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
if err != nil {
return res, nil
}
// Fetch vouchers from the API using the public key
vouchersResp, err := h.accountService.FetchVouchers(ctx, string(publicKey))
if err != nil {
return res, nil
}
// process voucher data
voucherSymbolList, voucherBalanceList := ProcessVouchers(vouchersResp.Result.Holdings)
prefixdb := storage.NewSubPrefixDb(store, []byte("vouchers"))
err = prefixdb.Put(ctx, []byte("sym"), []byte(voucherSymbolList))
if err != nil {
return res, nil
}
err = prefixdb.Put(ctx, []byte("bal"), []byte(voucherBalanceList))
if err != nil {
return res, nil
}
return res, nil
}
// ProcessVouchers formats the holdings into symbol and balance lists.
func ProcessVouchers(holdings []struct {
ContractAddress string `json:"contractAddress"`
TokenSymbol string `json:"tokenSymbol"`
TokenDecimals string `json:"tokenDecimals"`
Balance string `json:"balance"`
}) (string, string) {
var numberedSymbols, numberedBalances []string
for i, voucher := range holdings {
numberedSymbols = append(numberedSymbols, fmt.Sprintf("%d:%s", i+1, voucher.TokenSymbol))
numberedBalances = append(numberedBalances, fmt.Sprintf("%d:%s", i+1, voucher.Balance))
}
voucherSymbolList := strings.Join(numberedSymbols, "\n")
voucherBalanceList := strings.Join(numberedBalances, "\n")
return voucherSymbolList, voucherBalanceList
}
// GetVoucherList fetches the list of vouchers and formats them
func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
// Read vouchers from the store
store := h.userdataStore
prefixdb := storage.NewSubPrefixDb(store, []byte("vouchers"))
voucherData, err := prefixdb.Get(ctx, []byte("sym"))
if err != nil {
return res, nil
}
res.Content = string(voucherData)
return res, nil
}
// ViewVoucher retrieves the token holding and balance from the subprefixDB
func (h *Handlers) ViewVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
store := h.userdataStore
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
flag_incorrect_voucher, _ := h.flagManager.GetFlag("flag_incorrect_voucher")
inputStr := string(input)
if inputStr == "0" || inputStr == "99" {
res.FlagReset = append(res.FlagReset, flag_incorrect_voucher)
return res, nil
}
prefixdb := storage.NewSubPrefixDb(store, []byte("vouchers"))
// Retrieve the voucher symbol list
voucherSymbolList, err := prefixdb.Get(ctx, []byte("sym"))
if err != nil {
return res, fmt.Errorf("failed to retrieve voucher symbol list: %v", err)
}
// Retrieve the voucher balance list
voucherBalanceList, err := prefixdb.Get(ctx, []byte("bal"))
if err != nil {
return res, fmt.Errorf("failed to retrieve voucher balance list: %v", err)
}
// match the voucher symbol and balance with the input
matchedSymbol, matchedBalance := MatchVoucher(inputStr, string(voucherSymbolList), string(voucherBalanceList))
// If a match is found, write the temporary sym and balance
if matchedSymbol != "" && matchedBalance != "" {
err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_SYM, []byte(matchedSymbol))
if err != nil {
return res, err
}
err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_BAL, []byte(matchedBalance))
if err != nil {
return res, err
}
res.FlagReset = append(res.FlagReset, flag_incorrect_voucher)
res.Content = fmt.Sprintf("%s\n%s", matchedSymbol, matchedBalance)
} else {
res.FlagSet = append(res.FlagSet, flag_incorrect_voucher)
}
return res, nil
}
// MatchVoucher finds the matching voucher symbol and balance based on the input.
func MatchVoucher(inputStr string, voucherSymbols, voucherBalances string) (string, string) {
// Split the lists into slices for processing
symbols := strings.Split(voucherSymbols, "\n")
balances := strings.Split(voucherBalances, "\n")
var matchedSymbol, matchedBalance string
for i, symbol := range symbols {
symbolParts := strings.SplitN(symbol, ":", 2)
if len(symbolParts) != 2 {
continue
}
voucherNum := symbolParts[0]
voucherSymbol := symbolParts[1]
// Check if input matches either the number or the symbol
if inputStr == voucherNum || strings.EqualFold(inputStr, voucherSymbol) {
matchedSymbol = voucherSymbol
// Ensure there's a corresponding balance
if i < len(balances) {
matchedBalance = strings.SplitN(balances[i], ":", 2)[1]
}
break
}
}
return matchedSymbol, matchedBalance
}
// SetVoucher retrieves the temporary sym and balance,
// sets them as the active data and
// clears the temporary data
func (h *Handlers) SetVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
store := h.userdataStore
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
// get the current temporary symbol
temporarySym, err := store.ReadEntry(ctx, sessionId, utils.DATA_TEMPORARY_SYM)
if err != nil {
return res, err
}
// get the current temporary balance
temporaryBal, err := store.ReadEntry(ctx, sessionId, utils.DATA_TEMPORARY_BAL)
if err != nil {
return res, err
}
// set the active symbol
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM, []byte(temporarySym))
if err != nil {
return res, err
}
// set the active balance
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACTIVE_BAL, []byte(temporaryBal))
if err != nil {
return res, err
}
// reset the temporary symbol
err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_SYM, []byte(""))
if err != nil {
return res, err
}
// reset the temporary balance
err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_BAL, []byte(""))
if err != nil {
return res, err
}
res.Content = string(temporarySym)
return res, nil return res, nil
} }

View File

@ -7,15 +7,22 @@ import (
"log" "log"
"path" "path"
"testing" "testing"
"time"
"git.defalsify.org/vise.git/asm"
"git.defalsify.org/vise.git/db" "git.defalsify.org/vise.git/db"
"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/mocks"
"git.grassecon.net/urdt/ussd/internal/models" "git.grassecon.net/urdt/ussd/internal/models"
"git.grassecon.net/urdt/ussd/internal/testutil/mocks"
"git.grassecon.net/urdt/ussd/internal/testutil/testservice"
"git.grassecon.net/urdt/ussd/internal/utils" "git.grassecon.net/urdt/ussd/internal/utils"
"git.grassecon.net/urdt/ussd/common"
"github.com/alecthomas/assert/v2" "github.com/alecthomas/assert/v2"
"github.com/grassrootseconomics/eth-custodial/pkg/api"
testdataloader "github.com/peteole/testdata-loader" testdataloader "github.com/peteole/testdata-loader"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -25,75 +32,124 @@ var (
flagsPath = path.Join(baseDir, "services", "registration", "pp.csv") flagsPath = path.Join(baseDir, "services", "registration", "pp.csv")
) )
func TestCreateAccount(t *testing.T) { func TestNewHandlers(t *testing.T) {
fm, err := NewFlagManager(flagsPath) fm, err := NewFlagManager(flagsPath)
accountService := testservice.TestAccountService{}
if err != nil { if err != nil {
t.Logf(err.Error()) t.Logf(err.Error())
} }
t.Run("Valid UserDataStore", func(t *testing.T) {
mockStore := &mocks.MockUserDataStore{}
handlers, err := NewHandlers(fm.parser, mockStore, &accountService)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if handlers == nil {
t.Fatal("expected handlers to be non-nil")
}
if handlers.userdataStore == nil {
t.Fatal("expected userdataStore to be set in handlers")
}
})
// Test case for nil userdataStore
t.Run("Nil UserDataStore", func(t *testing.T) {
appFlags := &asm.FlagParser{}
handlers, err := NewHandlers(appFlags, nil, &accountService)
if err == nil {
t.Fatal("expected an error, got none")
}
if handlers != nil {
t.Fatal("expected handlers to be nil")
}
if err.Error() != "cannot create handler with nil userdata store" {
t.Fatalf("expected specific error, got %v", err)
}
})
}
func TestCreateAccount(t *testing.T) {
fm, err := NewFlagManager(flagsPath)
if err != nil {
t.Logf(err.Error())
}
// Create required mocks // Create required mocks
mockDataStore := new(mocks.MockUserDataStore) flag_account_created, err := fm.GetFlag("flag_account_created")
mockCreateAccountService := new(mocks.MockAccountService)
expectedResult := resource.Result{}
accountCreatedFlag, err := fm.GetFlag("flag_account_created")
if err != nil { if err != nil {
t.Logf(err.Error()) t.Logf(err.Error())
} }
expectedResult.FlagSet = append(expectedResult.FlagSet, accountCreatedFlag)
// Define session ID and mock data // Define session ID and mock data
sessionId := "session123" sessionId := "session123"
typ := utils.DATA_ACCOUNT_CREATED notFoundErr := db.ErrNotFound{}
fakeError := db.ErrNotFound{}
// Create context with session ID
ctx := context.WithValue(context.Background(), "SessionId", sessionId) ctx := context.WithValue(context.Background(), "SessionId", sessionId)
// Define expected interactions with the mock tests := []struct {
mockDataStore.On("ReadEntry", ctx, sessionId, typ).Return([]byte("123"), fakeError) name string
expectedAccountResp := &models.AccountResponse{ serverResponse *api.OKResponse
Ok: true, expectedResult resource.Result
Result: struct { }{
CustodialId json.Number `json:"custodialId"` {
PublicKey string `json:"publicKey"` name: "Test account creation success",
TrackingId string `json:"trackingId"` serverResponse: &api.OKResponse{
}{ Ok: true,
CustodialId: "12", Description: "Account creation successed",
PublicKey: "0x8E0XSCSVA", Result: map[string]any{
TrackingId: "d95a7e83-196c-4fd0-866fSGAGA", "trackingId": "1234567890",
"publicKey": "0xD3adB33f",
},
},
expectedResult: resource.Result{
FlagSet: []uint32{flag_account_created},
},
}, },
} }
mockCreateAccountService.On("CreateAccount").Return(expectedAccountResp, nil) for _, tt := range tests {
data := map[utils.DataTyp]string{ t.Run(tt.name, func(t *testing.T) {
utils.DATA_TRACKING_ID: expectedAccountResp.Result.TrackingId,
utils.DATA_PUBLIC_KEY: expectedAccountResp.Result.PublicKey, mockDataStore := new(mocks.MockUserDataStore)
utils.DATA_CUSTODIAL_ID: expectedAccountResp.Result.CustodialId.String(), mockCreateAccountService := new(mocks.MockAccountService)
// Create a Handlers instance with the mock data store
h := &Handlers{
userdataStore: mockDataStore,
accountService: mockCreateAccountService,
flagManager: fm.parser,
}
publicKey := tt.serverResponse.Result["publicKey"].(string)
data := map[utils.DataTyp]string{
utils.DATA_TRACKING_ID: tt.serverResponse.Result["trackingId"].(string),
utils.DATA_PUBLIC_KEY: publicKey,
}
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_ACCOUNT_CREATED).Return([]byte(""), notFoundErr)
mockCreateAccountService.On("CreateAccount").Return(tt.serverResponse, nil)
for key, value := range data {
mockDataStore.On("WriteEntry", ctx, sessionId, key, []byte(value)).Return(nil)
}
publicKeyNormalized, err := common.NormalizeHex(publicKey)
if err != nil {
t.Fatal(err)
}
mockDataStore.On("WriteEntry", ctx, publicKeyNormalized, utils.DATA_PUBLIC_KEY_REVERSE, []byte(sessionId)).Return(nil)
// Call the method you want to test
res, err := h.CreateAccount(ctx, "create_account", []byte("some-input"))
// Assert that no errors occurred
assert.NoError(t, err)
// Assert that the account created flag has been set to the result
assert.Equal(t, res, tt.expectedResult, "Expected result should be equal to the actual result")
// Assert that expectations were met
mockDataStore.AssertExpectations(t)
})
} }
for key, value := range data {
mockDataStore.On("WriteEntry", ctx, sessionId, key, []byte(value)).Return(nil)
}
// Create a Handlers instance with the mock data store
h := &Handlers{
userdataStore: mockDataStore,
accountService: mockCreateAccountService,
flagManager: fm.parser,
}
// Call the method you want to test
res, err := h.CreateAccount(ctx, "create_account", []byte("some-input"))
// Assert that no errors occurred
assert.NoError(t, err)
//Assert that the account created flag has been set to the result
assert.Equal(t, res, expectedResult, "Expected result should be equal to the actual result")
// Assert that expectations were met
mockDataStore.AssertExpectations(t)
} }
func TestWithPersister(t *testing.T) { func TestWithPersister(t *testing.T) {
@ -171,7 +227,7 @@ func TestSaveFamilyname(t *testing.T) {
mockStore.AssertExpectations(t) mockStore.AssertExpectations(t)
} }
func TestSavePin(t *testing.T) { func TestSaveTemporaryPin(t *testing.T) {
fm, err := NewFlagManager(flagsPath) fm, err := NewFlagManager(flagsPath)
mockStore := new(mocks.MockUserDataStore) mockStore := new(mocks.MockUserDataStore)
if err != nil { if err != nil {
@ -213,10 +269,10 @@ func TestSavePin(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
// Set up the expected behavior of the mock // Set up the expected behavior of the mock
mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(tt.input)).Return(nil) mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_TEMPORARY_PIN, []byte(tt.input)).Return(nil)
// Call the method // Call the method
res, err := h.SavePin(ctx, "save_pin", tt.input) res, err := h.SaveTemporaryPin(ctx, "save_pin", tt.input)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
@ -434,44 +490,12 @@ func TestCheckIdentifier(t *testing.T) {
} }
} }
func TestMaxAmount(t *testing.T) {
mockStore := new(mocks.MockUserDataStore)
mockCreateAccountService := new(mocks.MockAccountService)
// Define test data
sessionId := "session123"
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
publicKey := "0xcasgatweksalw1018221"
expectedBalance := "0.003CELO"
// Set up the expected behavior of the mock
mockStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil)
mockCreateAccountService.On("CheckBalance", publicKey).Return(expectedBalance, nil)
// Create the Handlers instance with the mock store
h := &Handlers{
userdataStore: mockStore,
accountService: mockCreateAccountService,
}
// Call the method
res, _ := h.MaxAmount(ctx, "max_amount", []byte("check_balance"))
//Assert that the balance that was set as the result content is what was returned by Check Balance
assert.Equal(t, expectedBalance, res.Content)
}
func TestGetSender(t *testing.T) { func TestGetSender(t *testing.T) {
mockStore := new(mocks.MockUserDataStore) mockStore := new(mocks.MockUserDataStore)
// Define test data // Define test data
sessionId := "session123" sessionId := "254712345678"
ctx := context.WithValue(context.Background(), "SessionId", sessionId) ctx := context.WithValue(context.Background(), "SessionId", sessionId)
publicKey := "0xcasgatweksalw1018221"
// Set up the expected behavior of the mock
mockStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil)
// Create the Handlers instance with the mock store // Create the Handlers instance with the mock store
h := &Handlers{ h := &Handlers{
@ -479,34 +503,37 @@ func TestGetSender(t *testing.T) {
} }
// Call the method // Call the method
res, _ := h.GetSender(ctx, "max_amount", []byte("check_balance")) res, _ := h.GetSender(ctx, "get_sender", []byte(""))
//Assert that the public key from readentry operation is what was set as the result content.
assert.Equal(t, publicKey, res.Content)
//Assert that the sessionId is what was set as the result content.
assert.Equal(t, sessionId, res.Content)
} }
func TestGetAmount(t *testing.T) { func TestGetAmount(t *testing.T) {
mockStore := new(mocks.MockUserDataStore) mockDataStore := new(mocks.MockUserDataStore)
// Define test data // Define test data
sessionId := "session123" sessionId := "session123"
ctx := context.WithValue(context.Background(), "SessionId", sessionId) ctx := context.WithValue(context.Background(), "SessionId", sessionId)
Amount := "0.03CELO" amount := "0.03"
activeSym := "SRF"
// Set up the expected behavior of the mock // Set up the expected behavior of the mock
mockStore.On("ReadEntry", ctx, sessionId, utils.DATA_AMOUNT).Return([]byte(Amount), nil) mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_ACTIVE_SYM).Return([]byte(activeSym), nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_AMOUNT).Return([]byte(amount), nil)
// Create the Handlers instance with the mock store // Create the Handlers instance with the mock store
h := &Handlers{ h := &Handlers{
userdataStore: mockStore, userdataStore: mockDataStore,
} }
// Call the method // Call the method
res, _ := h.GetAmount(ctx, "get_amount", []byte("Getting amount...")) res, _ := h.GetAmount(ctx, "get_amount", []byte(""))
formattedAmount := fmt.Sprintf("%s %s", amount, activeSym)
//Assert that the retrieved amount is what was set as the content //Assert that the retrieved amount is what was set as the content
assert.Equal(t, Amount, res.Content) assert.Equal(t, formattedAmount, res.Content)
} }
@ -537,12 +564,10 @@ func TestGetRecipient(t *testing.T) {
func TestGetFlag(t *testing.T) { func TestGetFlag(t *testing.T) {
fm, err := NewFlagManager(flagsPath) fm, err := NewFlagManager(flagsPath)
expectedFlag := uint32(9) expectedFlag := uint32(9)
if err != nil { if err != nil {
t.Logf(err.Error()) t.Logf(err.Error())
} }
flag, err := fm.GetFlag("flag_account_created") flag, err := fm.GetFlag("flag_account_created")
if err != nil { if err != nil {
t.Logf(err.Error()) t.Logf(err.Error())
} }
@ -937,7 +962,7 @@ func TestVerifyYob(t *testing.T) {
} }
} }
func TestVerifyPin(t *testing.T) { func TestVerifyCreatePin(t *testing.T) {
fm, err := NewFlagManager(flagsPath) fm, err := NewFlagManager(flagsPath)
if err != nil { if err != nil {
@ -986,7 +1011,7 @@ func TestVerifyPin(t *testing.T) {
}, },
} }
typ := utils.DATA_ACCOUNT_PIN typ := utils.DATA_TEMPORARY_PIN
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -994,8 +1019,11 @@ func TestVerifyPin(t *testing.T) {
// Define expected interactions with the mock // Define expected interactions with the mock
mockDataStore.On("ReadEntry", ctx, sessionId, typ).Return([]byte(firstSetPin), nil) mockDataStore.On("ReadEntry", ctx, sessionId, typ).Return([]byte(firstSetPin), nil)
// Set up the expected behavior of the mock
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(firstSetPin)).Return(nil)
// Call the method under test // Call the method under test
res, err := h.VerifyPin(ctx, "verify_pin", []byte(tt.input)) res, err := h.VerifyCreatePin(ctx, "verify_create_pin", []byte(tt.input))
// Assert that no errors occurred // Assert that no errors occurred
assert.NoError(t, err) assert.NoError(t, err)
@ -1012,53 +1040,115 @@ func TestVerifyPin(t *testing.T) {
func TestCheckAccountStatus(t *testing.T) { func TestCheckAccountStatus(t *testing.T) {
fm, err := NewFlagManager(flagsPath) fm, err := NewFlagManager(flagsPath)
if err != nil { if err != nil {
t.Logf(err.Error()) t.Logf(err.Error())
} }
mockDataStore := new(mocks.MockUserDataStore)
mockCreateAccountService := new(mocks.MockAccountService)
sessionId := "session123" sessionId := "session123"
flag_account_success, _ := fm.GetFlag("flag_account_success") flag_account_success, _ := fm.GetFlag("flag_account_success")
flag_account_pending, _ := fm.GetFlag("flag_account_pending") flag_account_pending, _ := fm.GetFlag("flag_account_pending")
flag_api_error, _ := fm.GetFlag("flag_api_call_error")
ctx := context.WithValue(context.Background(), "SessionId", sessionId) ctx := context.WithValue(context.Background(), "SessionId", sessionId)
h := &Handlers{
userdataStore: mockDataStore,
accountService: mockCreateAccountService,
flagManager: fm.parser,
}
tests := []struct { tests := []struct {
name string name string
input []byte input []byte
status string serverResponse *api.OKResponse
response *models.TrackStatusResponse
expectedResult resource.Result expectedResult resource.Result
}{ }{
{ {
name: "Test when account status is Success", name: "Test when account is on the Sarafu network",
input: []byte("TrackingId1234"), input: []byte("TrackingId1234"),
status: "SUCCESS", serverResponse: &api.OKResponse{
Ok: true,
Description: "Account creation succeeded",
Result: map[string]any{
"active": true,
},
},
response: &models.TrackStatusResponse{
Ok: true,
Result: struct {
Transaction struct {
CreatedAt time.Time "json:\"createdAt\""
Status string "json:\"status\""
TransferValue json.Number "json:\"transferValue\""
TxHash string "json:\"txHash\""
TxType string "json:\"txType\""
}
}{
Transaction: models.Transaction{
CreatedAt: time.Now(),
Status: "SUCCESS",
TransferValue: json.Number("0.5"),
TxHash: "0x123abc456def",
TxType: "transfer",
},
},
},
expectedResult: resource.Result{ expectedResult: resource.Result{
FlagSet: []uint32{flag_account_success}, FlagSet: []uint32{flag_account_success},
FlagReset: []uint32{flag_account_pending}, FlagReset: []uint32{flag_api_error, flag_account_pending},
},
},
{
name: "Test when the account is not yet on the sarafu network",
input: []byte("TrackingId1234"),
response: &models.TrackStatusResponse{
Ok: true,
Result: struct {
Transaction struct {
CreatedAt time.Time "json:\"createdAt\""
Status string "json:\"status\""
TransferValue json.Number "json:\"transferValue\""
TxHash string "json:\"txHash\""
TxType string "json:\"txType\""
}
}{
Transaction: models.Transaction{
CreatedAt: time.Now(),
Status: "SUCCESS",
TransferValue: json.Number("0.5"),
TxHash: "0x123abc456def",
TxType: "transfer",
},
},
},
serverResponse: &api.OKResponse{
Ok: true,
Description: "Account creation succeeded",
Result: map[string]any{
"active": false,
},
},
expectedResult: resource.Result{
FlagSet: []uint32{flag_account_pending},
FlagReset: []uint32{flag_api_error, flag_account_success},
}, },
}, },
} }
typ := utils.DATA_TRACKING_ID
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
mockDataStore := new(mocks.MockUserDataStore)
mockCreateAccountService := new(mocks.MockAccountService)
h := &Handlers{
userdataStore: mockDataStore,
accountService: mockCreateAccountService,
flagManager: fm.parser,
}
status := tt.response.Result.Transaction.Status
// Define expected interactions with the mock // Define expected interactions with the mock
mockDataStore.On("ReadEntry", ctx, sessionId, typ).Return(tt.input, nil) mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return(tt.input, nil)
mockCreateAccountService.On("CheckAccountStatus", string(tt.input)).Return(tt.status, nil) mockCreateAccountService.On("CheckAccountStatus", string(tt.input)).Return(tt.response, nil)
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACCOUNT_STATUS, []byte(tt.status)).Return(nil) mockCreateAccountService.On("TrackAccountStatus", string(tt.input)).Return(tt.serverResponse, nil)
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACCOUNT_STATUS, []byte(status)).Return(nil).Maybe()
// Call the method under test // Call the method under test
res, _ := h.CheckAccountStatus(ctx, "check_status", tt.input) res, _ := h.CheckAccountStatus(ctx, "check_account_status", tt.input)
// Assert that no errors occurred // Assert that no errors occurred
assert.NoError(t, err) assert.NoError(t, err)
@ -1187,7 +1277,7 @@ func TestResetInvalidAmount(t *testing.T) {
} }
func TestInitiateTransaction(t *testing.T) { func TestInitiateTransaction(t *testing.T) {
sessionId := "session123" sessionId := "254712345678"
fm, err := NewFlagManager(flagsPath) fm, err := NewFlagManager(flagsPath)
@ -1210,30 +1300,29 @@ func TestInitiateTransaction(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input []byte input []byte
PublicKey []byte
Recipient []byte Recipient []byte
Amount []byte Amount []byte
ActiveSym []byte
status string status string
expectedResult resource.Result expectedResult resource.Result
}{ }{
{ {
name: "Test amount reset", name: "Test initiate transaction",
PublicKey: []byte("0x1241527192"), Amount: []byte("0.002"),
Amount: []byte("0.002CELO"), ActiveSym: []byte("SRF"),
Recipient: []byte("0x12415ass27192"), Recipient: []byte("0x12415ass27192"),
expectedResult: resource.Result{ expectedResult: resource.Result{
FlagReset: []uint32{account_authorized_flag}, FlagReset: []uint32{account_authorized_flag},
Content: "Your request has been sent. 0x12415ass27192 will receive 0.002CELO from 0x1241527192.", Content: "Your request has been sent. 0x12415ass27192 will receive 0.002 SRF from 254712345678.",
}, },
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
// Define expected interactions with the mock // Define expected interactions with the mock
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return(tt.PublicKey, nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_AMOUNT).Return(tt.Amount, nil) mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_AMOUNT).Return(tt.Amount, nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_RECIPIENT).Return(tt.Recipient, nil) mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_RECIPIENT).Return(tt.Recipient, nil)
//mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_AMOUNT, []byte("")).Return(nil) mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_ACTIVE_SYM).Return(tt.ActiveSym, nil)
// Call the method under test // Call the method under test
res, _ := h.InitiateTransaction(ctx, "transaction_reset_amount", tt.input) res, _ := h.InitiateTransaction(ctx, "transaction_reset_amount", tt.input)
@ -1246,10 +1335,8 @@ func TestInitiateTransaction(t *testing.T) {
// Assert that expectations were met // Assert that expectations were met
mockDataStore.AssertExpectations(t) mockDataStore.AssertExpectations(t)
}) })
} }
} }
func TestQuit(t *testing.T) { func TestQuit(t *testing.T) {
@ -1358,66 +1445,6 @@ func TestIsValidPIN(t *testing.T) {
} }
} }
func TestQuitWithBalance(t *testing.T) {
fm, err := NewFlagManager(flagsPath)
if err != nil {
t.Logf(err.Error())
}
flag_account_authorized, _ := fm.parser.GetFlag("flag_account_authorized")
mockDataStore := new(mocks.MockUserDataStore)
mockCreateAccountService := new(mocks.MockAccountService)
sessionId := "session123"
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
h := &Handlers{
userdataStore: mockDataStore,
accountService: mockCreateAccountService,
flagManager: fm.parser,
}
tests := []struct {
name string
input []byte
publicKey []byte
balance string
expectedResult resource.Result
}{
{
name: "Test quit with balance",
balance: "0.02CELO",
publicKey: []byte("0xrqeqrequuq"),
expectedResult: resource.Result{
FlagReset: []uint32{flag_account_authorized},
Content: fmt.Sprintf("Your account balance is %s", "0.02CELO"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return(tt.publicKey, nil)
mockCreateAccountService.On("CheckBalance", string(tt.publicKey)).Return(tt.balance, nil)
// Call the method under test
res, _ := h.QuitWithBalance(ctx, "test_quit_with_balance", tt.input)
// Assert that no errors occurred
assert.NoError(t, err)
//Assert that the account created flag has been set to the result
assert.Equal(t, res, tt.expectedResult, "Expected result should be equal to the actual result")
// Assert that expectations were met
mockDataStore.AssertExpectations(t)
})
}
}
func TestValidateAmount(t *testing.T) { func TestValidateAmount(t *testing.T) {
fm, err := NewFlagManager(flagsPath) fm, err := NewFlagManager(flagsPath)
@ -1440,34 +1467,31 @@ func TestValidateAmount(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input []byte input []byte
publicKey []byte activeBal []byte
balance string balance string
expectedResult resource.Result expectedResult resource.Result
}{ }{
{ {
name: "Test with valid amount", name: "Test with valid amount",
input: []byte("0.001"), input: []byte("4.10"),
balance: "0.003 CELO", activeBal: []byte("5"),
publicKey: []byte("0xrqeqrequuq"),
expectedResult: resource.Result{ expectedResult: resource.Result{
Content: "0.001", Content: "4.10",
}, },
}, },
{ {
name: "Test with amount larger than balance", name: "Test with amount larger than active balance",
input: []byte("0.02"), input: []byte("5.02"),
balance: "0.003 CELO", activeBal: []byte("5"),
publicKey: []byte("0xrqeqrequuq"),
expectedResult: resource.Result{ expectedResult: resource.Result{
FlagSet: []uint32{flag_invalid_amount}, FlagSet: []uint32{flag_invalid_amount},
Content: "0.02", Content: "5.02",
}, },
}, },
{ {
name: "Test with invalid amount", name: "Test with invalid amount format",
input: []byte("0.02ms"), input: []byte("0.02ms"),
balance: "0.003 CELO", activeBal: []byte("5"),
publicKey: []byte("0xrqeqrequuq"),
expectedResult: resource.Result{ expectedResult: resource.Result{
FlagSet: []uint32{flag_invalid_amount}, FlagSet: []uint32{flag_invalid_amount},
Content: "0.02ms", Content: "0.02ms",
@ -1477,23 +1501,23 @@ func TestValidateAmount(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) {
// Mock behavior for active balance retrieval
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_ACTIVE_BAL).Return(tt.activeBal, nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return(tt.publicKey, nil) // Mock behavior for storing the amount (if valid)
mockCreateAccountService.On("CheckBalance", string(tt.publicKey)).Return(tt.balance, nil)
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_AMOUNT, tt.input).Return(nil).Maybe() mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_AMOUNT, tt.input).Return(nil).Maybe()
// Call the method under test // Call the method under test
res, _ := h.ValidateAmount(ctx, "test_validate_amount", tt.input) res, _ := h.ValidateAmount(ctx, "test_validate_amount", tt.input)
// Assert that no errors occurred // Assert no errors occurred
assert.NoError(t, err) assert.NoError(t, err)
//Assert that the account created flag has been set to the result // Assert the result matches the expected result
assert.Equal(t, res, tt.expectedResult, "Expected result should be equal to the actual result") assert.Equal(t, tt.expectedResult, res, "Expected result should match actual result")
// Assert that expectations were met // Assert all expectations were met
mockDataStore.AssertExpectations(t) mockDataStore.AssertExpectations(t)
}) })
} }
} }
@ -1557,33 +1581,54 @@ func TestValidateRecipient(t *testing.T) {
} }
func TestCheckBalance(t *testing.T) { func TestCheckBalance(t *testing.T) {
tests := []struct {
mockDataStore := new(mocks.MockUserDataStore) name string
sessionId := "session123" sessionId string
publicKey := "0X13242618721" publicKey string
balance := "0.003 CELO" activeSym string
activeBal string
expectedResult := resource.Result{ expectedResult resource.Result
Content: "0.003 CELO", expectError bool
}{
{
name: "User with active sym",
sessionId: "session456",
publicKey: "0X98765432109",
activeSym: "ETH",
activeBal: "1.5",
expectedResult: resource.Result{Content: "Balance: 1.5 ETH\n"},
expectError: false,
},
} }
mockCreateAccountService := new(mocks.MockAccountService) for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockDataStore := new(mocks.MockUserDataStore)
mockAccountService := new(mocks.MockAccountService)
ctx := context.WithValue(context.Background(), "SessionId", tt.sessionId)
ctx := context.WithValue(context.Background(), "SessionId", sessionId) h := &Handlers{
userdataStore: mockDataStore,
accountService: mockAccountService,
}
h := &Handlers{ // Mock for user with active sym
userdataStore: mockDataStore, mockDataStore.On("ReadEntry", ctx, tt.sessionId, utils.DATA_ACTIVE_SYM).Return([]byte(tt.activeSym), nil)
accountService: mockCreateAccountService, mockDataStore.On("ReadEntry", ctx, tt.sessionId, utils.DATA_ACTIVE_BAL).Return([]byte(tt.activeBal), nil)
//flagManager: fm.parser,
res, err := h.CheckBalance(ctx, "check_balance", []byte(""))
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedResult, res, "Result should match expected output")
}
mockDataStore.AssertExpectations(t)
mockAccountService.AssertExpectations(t)
})
} }
//mock call operations
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil)
mockCreateAccountService.On("CheckBalance", string(publicKey)).Return(balance, nil)
res, _ := h.CheckBalance(ctx, "check_balance", []byte("123456"))
assert.Equal(t, res, expectedResult, "Result should contain flag(s) that have been reset")
} }
func TestGetProfile(t *testing.T) { func TestGetProfile(t *testing.T) {
@ -1592,23 +1637,50 @@ func TestGetProfile(t *testing.T) {
mockDataStore := new(mocks.MockUserDataStore) mockDataStore := new(mocks.MockUserDataStore)
mockCreateAccountService := new(mocks.MockAccountService) mockCreateAccountService := new(mocks.MockAccountService)
mockState := state.NewState(16)
h := &Handlers{ h := &Handlers{
userdataStore: mockDataStore, userdataStore: mockDataStore,
accountService: mockCreateAccountService, accountService: mockCreateAccountService,
st: mockState,
} }
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
tests := []struct { tests := []struct {
name string name string
keys []utils.DataTyp languageCode string
profileInfo []string keys []utils.DataTyp
result resource.Result profileInfo []string
result resource.Result
}{ }{
{ {
name: "Test with full profile information", name: "Test with full profile information in eng",
keys: []utils.DataTyp{utils.DATA_FAMILY_NAME, utils.DATA_FIRST_NAME, utils.DATA_GENDER, utils.DATA_OFFERINGS, utils.DATA_LOCATION, utils.DATA_YOB}, keys: []utils.DataTyp{utils.DATA_FAMILY_NAME, utils.DATA_FIRST_NAME, utils.DATA_GENDER, utils.DATA_OFFERINGS, utils.DATA_LOCATION, utils.DATA_YOB},
profileInfo: []string{"Doee", "John", "Male", "Bananas", "Kilifi", "1976"}, profileInfo: []string{"Doee", "John", "Male", "Bananas", "Kilifi", "1976"},
languageCode: "eng",
result: resource.Result{
Content: fmt.Sprintf(
"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
"John Doee", "Male", "48", "Kilifi", "Bananas",
),
},
},
{
name: "Test with with profile information in swa ",
keys: []utils.DataTyp{utils.DATA_FAMILY_NAME, utils.DATA_FIRST_NAME, utils.DATA_GENDER, utils.DATA_OFFERINGS, utils.DATA_LOCATION, utils.DATA_YOB},
profileInfo: []string{"Doee", "John", "Male", "Bananas", "Kilifi", "1976"},
languageCode: "swa",
result: resource.Result{
Content: fmt.Sprintf(
"Jina: %s\nJinsia: %s\nUmri: %s\nEneo: %s\nUnauza: %s\n",
"John Doee", "Male", "48", "Kilifi", "Bananas",
),
},
},
{
name: "Test with with profile information with language that is not yet supported",
keys: []utils.DataTyp{utils.DATA_FAMILY_NAME, utils.DATA_FIRST_NAME, utils.DATA_GENDER, utils.DATA_OFFERINGS, utils.DATA_LOCATION, utils.DATA_YOB},
profileInfo: []string{"Doee", "John", "Male", "Bananas", "Kilifi", "1976"},
languageCode: "nor",
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",
@ -1619,9 +1691,14 @@ func TestGetProfile(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) {
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
ctx = context.WithValue(ctx, "Language", lang.Language{
Code: tt.languageCode,
})
for index, key := range tt.keys { for index, key := range tt.keys {
mockDataStore.On("ReadEntry", ctx, sessionId, key).Return([]byte(tt.profileInfo[index]), nil) mockDataStore.On("ReadEntry", ctx, sessionId, key).Return([]byte(tt.profileInfo[index]), nil).Maybe()
} }
res, _ := h.GetProfileInfo(ctx, "get_profile_info", []byte("")) res, _ := h.GetProfileInfo(ctx, "get_profile_info", []byte(""))
// Assert that expectations were met // Assert that expectations were met
@ -1693,42 +1770,6 @@ func TestVerifyNewPin(t *testing.T) {
} }
func TestSaveTemporaryPIn(t *testing.T) {
fm, err := NewFlagManager(flagsPath)
if err != nil {
t.Logf(err.Error())
}
// Create a new instance of UserDataStore
mockStore := new(mocks.MockUserDataStore)
// Define test data
sessionId := "session123"
PIN := "1234"
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
// Set up the expected behavior of the mock
mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_TEMPORARY_PIN, []byte(PIN)).Return(nil)
// Create the Handlers instance with the mock store
h := &Handlers{
userdataStore: mockStore,
flagManager: fm.parser,
}
// Call the method
res, err := h.SaveTemporaryPin(ctx, "save_temporary_pin", []byte(PIN))
// Assert results
assert.NoError(t, err)
assert.Equal(t, resource.Result{}, res)
// Assert all expectations were met
mockStore.AssertExpectations(t)
}
func TestConfirmPin(t *testing.T) { func TestConfirmPin(t *testing.T) {
sessionId := "session123" sessionId := "session123"
@ -1776,5 +1817,120 @@ func TestConfirmPin(t *testing.T) {
}) })
} }
}
func TestSetVoucher(t *testing.T) {
mockDataStore := new(mocks.MockUserDataStore)
sessionId := "session123"
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
temporarySym := []byte("tempSym")
temporaryBal := []byte("tempBal")
// Set expectations for the mock data store
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_TEMPORARY_SYM).Return(temporarySym, nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_TEMPORARY_BAL).Return(temporaryBal, nil)
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACTIVE_SYM, temporarySym).Return(nil)
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACTIVE_BAL, temporaryBal).Return(nil)
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_TEMPORARY_SYM, []byte("")).Return(nil)
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_TEMPORARY_BAL, []byte("")).Return(nil)
h := &Handlers{
userdataStore: mockDataStore,
}
// Call the method under test
res, err := h.SetVoucher(ctx, "someSym", []byte{})
// Assert that no errors occurred
assert.NoError(t, err)
// Assert that the result content is correct
assert.Equal(t, string(temporarySym), res.Content)
// Assert that expectations were met
mockDataStore.AssertExpectations(t)
}
func TestFetchCustodialBalances(t *testing.T) {
fm, err := NewFlagManager(flagsPath)
if err != nil {
t.Logf(err.Error())
}
flag_api_error, _ := fm.GetFlag("flag_api_call_error")
// Define test data
sessionId := "session123"
publicKey := "0X13242618721"
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
tests := []struct {
name string
balanceResonse *models.BalanceResponse
expectedResult resource.Result
}{
{
name: "Test when fetch custodial balances is not a success",
balanceResonse: &models.BalanceResponse{
Ok: false,
Result: struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
},
expectedResult: resource.Result{
FlagSet: []uint32{flag_api_error},
},
},
{
name: "Test when fetch custodial balances is a success",
balanceResonse: &models.BalanceResponse{
Ok: true,
Result: struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
},
expectedResult: resource.Result{
FlagReset: []uint32{flag_api_error},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockDataStore := new(mocks.MockUserDataStore)
mockCreateAccountService := new(mocks.MockAccountService)
mockState := state.NewState(16)
// Create the Handlers instance with the mock store
h := &Handlers{
userdataStore: mockDataStore,
flagManager: fm.parser,
st: mockState,
accountService: mockCreateAccountService,
}
// Set up the expected behavior of the mock
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil)
mockCreateAccountService.On("CheckBalance", string(publicKey)).Return(tt.balanceResonse, nil)
// Call the method
res, _ := h.FetchCustodialBalances(ctx, "fetch_custodial_balances", []byte(""))
// Assert that expectations were met
mockDataStore.AssertExpectations(t)
//Assert that the result set to content is what was expected
assert.Equal(t, res, tt.expectedResult, "Result should contain flags set according to user input")
})
}
} }

View File

@ -13,7 +13,7 @@ import (
"git.defalsify.org/vise.git/engine" "git.defalsify.org/vise.git/engine"
"git.grassecon.net/urdt/ussd/internal/handlers" "git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/mocks/httpmocks" "git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks"
) )
// invalidRequestType is a custom type to test invalid request scenarios // invalidRequestType is a custom type to test invalid request scenarios

View File

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

View File

@ -1,15 +1,10 @@
package models package models
import (
"encoding/json"
)
type AccountResponse struct { type AccountResponse struct {
Ok bool `json:"ok"` Ok bool `json:"ok"`
Result struct { Description string `json:"description"` // Include the description field
CustodialId json.Number `json:"custodialId"` Result struct {
PublicKey string `json:"publicKey"` PublicKey string `json:"publicKey"`
TrackingId string `json:"trackingId"` TrackingId string `json:"trackingId"`
} `json:"result"` } `json:"result"`
} }

View File

@ -0,0 +1,18 @@
package models
type ApiResponse struct {
OK bool `json:"ok"`
Description string `json:"description"`
Result Result `json:"result"`
}
type Result struct {
Holdings []Holding `json:"holdings"`
}
type Holding struct {
ContractAddress string `json:"contractAddress"`
TokenSymbol string `json:"tokenSymbol"`
TokenDecimals string `json:"tokenDecimals"`
Balance string `json:"balance"`
}

View File

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

View File

@ -0,0 +1,15 @@
package models
// VoucherHoldingResponse represents a single voucher holding
type VoucherHoldingResponse struct {
Ok bool `json:"ok"`
Description string `json:"description"`
Result struct {
Holdings []struct {
ContractAddress string `json:"contractAddress"`
TokenSymbol string `json:"tokenSymbol"`
TokenDecimals string `json:"tokenDecimals"`
Balance string `json:"balance"`
} `json:"holdings"`
} `json:"result"`
}

View File

@ -12,32 +12,32 @@ const (
type SubPrefixDb struct { type SubPrefixDb struct {
store db.Db store db.Db
pfx []byte pfx []byte
} }
func NewSubPrefixDb(store db.Db, pfx []byte) *SubPrefixDb { func NewSubPrefixDb(store db.Db, pfx []byte) *SubPrefixDb {
return &SubPrefixDb{ return &SubPrefixDb{
store: store, store: store,
pfx: pfx, pfx: pfx,
} }
} }
func(s *SubPrefixDb) toKey(k []byte) []byte { func (s *SubPrefixDb) toKey(k []byte) []byte {
return append(s.pfx, k...) return append(s.pfx, k...)
} }
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(DATATYPE_USERSUB)
key = s.toKey(key) key = s.toKey(key)
v, err := s.store.Get(ctx, key) v, err := s.store.Get(ctx, key)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return v, nil return v, nil
} }
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(DATATYPE_USERSUB)
key = s.toKey(key) key = s.toKey(key)
return s.store.Put(ctx, key, val) return s.store.Put(ctx, key, val)
} }

View File

@ -109,6 +109,7 @@ func(tdb *ThreadGdbmDb) Get(ctx context.Context, key []byte) ([]byte, error) {
func(tdb *ThreadGdbmDb) Close() error { func(tdb *ThreadGdbmDb) Close() error {
tdb.reserve() tdb.reserve()
close(dbC[tdb.connStr]) close(dbC[tdb.connStr])
delete(dbC, tdb.connStr)
err := tdb.db.Close() err := tdb.db.Close()
tdb.db = nil tdb.db = nil
return err return err

View File

@ -8,14 +8,16 @@ import (
"git.defalsify.org/vise.git/db" "git.defalsify.org/vise.git/db"
fsdb "git.defalsify.org/vise.git/db/fs" fsdb "git.defalsify.org/vise.git/db/fs"
"git.defalsify.org/vise.git/db/postgres"
"git.defalsify.org/vise.git/logging"
"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/logging" "git.grassecon.net/urdt/ussd/initializers"
) )
var ( var (
logg = logging.NewVanilla().WithDomain("storage") logg = logging.NewVanilla().WithDomain("storage")
) )
type StorageService interface { type StorageService interface {
GetPersister(ctx context.Context) (*persist.Persister, error) GetPersister(ctx context.Context) (*persist.Persister, error)
@ -24,40 +26,86 @@ type StorageService interface {
EnsureDbDir() error EnsureDbDir() error
} }
type MenuStorageService struct{ type MenuStorageService struct {
dbDir string dbDir string
resourceDir string resourceDir string
resourceStore db.Db resourceStore db.Db
stateStore db.Db stateStore db.Db
userDataStore db.Db userDataStore db.Db
} }
func buildConnStr() string {
host := initializers.GetEnv("DB_HOST", "localhost")
user := initializers.GetEnv("DB_USER", "postgres")
password := initializers.GetEnv("DB_PASSWORD", "")
dbName := initializers.GetEnv("DB_NAME", "")
port := initializers.GetEnv("DB_PORT", "5432")
return fmt.Sprintf(
"postgres://%s:%s@%s:%s/%s",
user, password, host, port, dbName,
)
}
func NewMenuStorageService(dbDir string, resourceDir string) *MenuStorageService { func NewMenuStorageService(dbDir string, resourceDir string) *MenuStorageService {
return &MenuStorageService{ return &MenuStorageService{
dbDir: dbDir, dbDir: dbDir,
resourceDir: resourceDir, resourceDir: resourceDir,
} }
} }
func (ms *MenuStorageService) GetPersister(ctx context.Context) (*persist.Persister, error) { func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.Db, fileName string) (db.Db, error) {
ms.stateStore = NewThreadGdbmDb() database, ok := ctx.Value("Database").(string)
storeFile := path.Join(ms.dbDir, "state.gdbm") if !ok {
err := ms.stateStore.Connect(ctx, storeFile) return nil, fmt.Errorf("failed to select the database")
}
if existingDb != nil {
return existingDb, nil
}
var newDb db.Db
var err error
if database == "postgres" {
newDb = postgres.NewPgDb()
connStr := buildConnStr()
err = newDb.Connect(ctx, connStr)
} else {
newDb = NewThreadGdbmDb()
storeFile := path.Join(ms.dbDir, fileName)
err = newDb.Connect(ctx, storeFile)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
pr := persist.NewPersister(ms.stateStore)
logg.TraceCtxf(ctx, "menu storage service", "persist", pr, "store", ms.stateStore) return newDb, nil
}
func (ms *MenuStorageService) GetPersister(ctx context.Context) (*persist.Persister, error) {
stateStore, err := ms.GetStateStore(ctx)
if err != nil {
return nil, err
}
pr := persist.NewPersister(stateStore)
logg.TraceCtxf(ctx, "menu storage service", "persist", pr, "store", stateStore)
return pr, nil return pr, nil
} }
func (ms *MenuStorageService) GetUserdataDb(ctx context.Context) (db.Db, error) { func (ms *MenuStorageService) GetUserdataDb(ctx context.Context) (db.Db, error) {
ms.userDataStore = NewThreadGdbmDb() if ms.userDataStore != nil {
storeFile := path.Join(ms.dbDir, "userdata.gdbm") return ms.userDataStore, nil
err := ms.userDataStore.Connect(ctx, storeFile) }
userDataStore, err := ms.getOrCreateDb(ctx, ms.userDataStore, "userdata.gdbm")
if err != nil { if err != nil {
return nil, err return nil, err
} }
ms.userDataStore = userDataStore
return ms.userDataStore, nil return ms.userDataStore, nil
} }
@ -73,14 +121,15 @@ func (ms *MenuStorageService) GetResource(ctx context.Context) (resource.Resourc
func (ms *MenuStorageService) GetStateStore(ctx context.Context) (db.Db, error) { func (ms *MenuStorageService) GetStateStore(ctx context.Context) (db.Db, error) {
if ms.stateStore != nil { if ms.stateStore != nil {
panic("set up store when already exists") return ms.stateStore, nil
} }
ms.stateStore = NewThreadGdbmDb()
storeFile := path.Join(ms.dbDir, "state.gdbm") stateStore, err := ms.getOrCreateDb(ctx, ms.stateStore, "state.gdbm")
err := ms.stateStore.Connect(ctx, storeFile)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ms.stateStore = stateStore
return ms.stateStore, nil return ms.stateStore, nil
} }

View File

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

View File

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

View File

@ -0,0 +1,40 @@
package mocks
import (
"context"
"git.grassecon.net/urdt/ussd/internal/models"
"github.com/grassrootseconomics/eth-custodial/pkg/api"
"github.com/stretchr/testify/mock"
)
// MockAccountService implements AccountServiceInterface for testing
type MockAccountService struct {
mock.Mock
}
func (m *MockAccountService) CreateAccount(ctx context.Context) (*api.OKResponse, error) {
args := m.Called()
return args.Get(0).(*api.OKResponse), args.Error(1)
}
func (m *MockAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResponse, error) {
args := m.Called(publicKey)
return args.Get(0).(*models.BalanceResponse), args.Error(1)
}
func (m *MockAccountService) CheckAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResponse, error) {
args := m.Called(trackingId)
return args.Get(0).(*models.TrackStatusResponse), args.Error(1)
}
func (m *MockAccountService) TrackAccountStatus(ctx context.Context,publicKey string) (*api.OKResponse, error) {
args := m.Called(publicKey)
return args.Get(0).(*api.OKResponse), args.Error(1)
}
func (m *MockAccountService) FetchVouchers(ctx context.Context, publicKey string) (*models.VoucherHoldingResponse, error) {
args := m.Called(publicKey)
return args.Get(0).(*models.VoucherHoldingResponse), args.Error(1)
}

View File

@ -0,0 +1,101 @@
package testservice
import (
"context"
"encoding/json"
"time"
"git.grassecon.net/urdt/ussd/internal/models"
"github.com/grassrootseconomics/eth-custodial/pkg/api"
)
type TestAccountService struct {
}
func (tas *TestAccountService) CreateAccount(ctx context.Context) (*api.OKResponse, error) {
return &api.OKResponse{
Ok: true,
Description: "Account creation succeeded",
Result: map[string]any{
"trackingId": "075ccc86-f6ef-4d33-97d5-e91cfb37aa0d",
"publicKey": "0x623EFAFa8868df4B934dd12a8B26CB3Dd75A7AdD",
},
}, nil
}
func (tas *TestAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResponse, error) {
balanceResponse := &models.BalanceResponse{
Ok: true,
Result: struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
}
return balanceResponse, nil
}
func (tas *TestAccountService) CheckAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResponse, error) {
trackResponse := &models.TrackStatusResponse{
Ok: true,
Result: struct {
Transaction struct {
CreatedAt time.Time "json:\"createdAt\""
Status string "json:\"status\""
TransferValue json.Number "json:\"transferValue\""
TxHash string "json:\"txHash\""
TxType string "json:\"txType\""
}
}{
Transaction: models.Transaction{
CreatedAt: time.Now(),
Status: "SUCCESS",
TransferValue: json.Number("0.5"),
TxHash: "0x123abc456def",
TxType: "transfer",
},
},
}
return trackResponse, nil
}
func (tas *TestAccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*api.OKResponse, error) {
return &api.OKResponse{
Ok: true,
Description: "Account creation succeeded",
Result: map[string]any{
"active": true,
},
}, nil
}
func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) (*models.VoucherHoldingResponse, error) {
return &models.VoucherHoldingResponse{
Ok: true,
Result: struct {
Holdings []struct {
ContractAddress string `json:"contractAddress"`
TokenSymbol string `json:"tokenSymbol"`
TokenDecimals string `json:"tokenDecimals"`
Balance string `json:"balance"`
} `json:"holdings"`
}{
Holdings: []struct {
ContractAddress string `json:"contractAddress"`
TokenSymbol string `json:"tokenSymbol"`
TokenDecimals string `json:"tokenDecimals"`
Balance string `json:"balance"`
}{
{
ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee",
TokenSymbol: "SRF",
TokenDecimals: "6",
Balance: "2745987",
},
},
},
}, nil
}

View File

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

View File

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

View File

@ -23,6 +23,12 @@ const (
DATA_RECIPIENT DATA_RECIPIENT
DATA_AMOUNT DATA_AMOUNT
DATA_TEMPORARY_PIN DATA_TEMPORARY_PIN
DATA_VOUCHER_LIST
DATA_TEMPORARY_SYM
DATA_ACTIVE_SYM
DATA_TEMPORARY_BAL
DATA_ACTIVE_BAL
DATA_PUBLIC_KEY_REVERSE
) )
func typToBytes(typ DataTyp) []byte { func typToBytes(typ DataTyp) []byte {

View File

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

View File

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

View File

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

44
sample_tokens.json Normal file
View File

@ -0,0 +1,44 @@
{
"ok": true,
"description": "Token holdings with current balances",
"result": {
"holdings": [
{
"contractAddress": "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee",
"tokenSymbol": "FSPTST",
"tokenDecimals": "6",
"balance": "8869964242"
},
{
"contractAddress": "0x724F2910D790B54A39a7638282a45B1D83564fFA",
"tokenSymbol": "GEO",
"tokenDecimals": "6",
"balance": "9884"
},
{
"contractAddress": "0x2105a206B7bec31E2F90acF7385cc8F7F5f9D273",
"tokenSymbol": "MFNK",
"tokenDecimals": "6",
"balance": "19788697"
},
{
"contractAddress": "0x63DE2Ac8D1008351Cc69Fb8aCb94Ba47728a7E83",
"tokenSymbol": "MILO",
"tokenDecimals": "6",
"balance": "75"
},
{
"contractAddress": "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9",
"tokenSymbol": "SOHAIL",
"tokenDecimals": "6",
"balance": "27874115"
},
{
"contractAddress": "0x45d747172e77d55575c197CbA9451bC2CD8F4958",
"tokenSymbol": "SRF",
"tokenDecimals": "6",
"balance": "2745987"
}
]
}
}

View File

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

View File

@ -1,13 +1,15 @@
LOAD reset_transaction_amount 0 LOAD reset_transaction_amount 0
LOAD max_amount 10 LOAD max_amount 10
RELOAD max_amount
MAP max_amount MAP max_amount
MOUT back 0 MOUT back 0
HALT HALT
LOAD validate_amount 64 LOAD validate_amount 64
RELOAD validate_amount RELOAD validate_amount
CATCH api_failure flag_api_call_error 1
CATCH invalid_amount flag_invalid_amount 1 CATCH invalid_amount flag_invalid_amount 1
INCMP _ 0 INCMP _ 0
LOAD get_recipient 12 LOAD get_recipient 12
LOAD get_sender 64 LOAD get_sender 64
LOAD get_amount 12 LOAD get_amount 32
INCMP transaction_pin * INCMP transaction_pin *

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
LOAD save_pin 0 LOAD save_temporary_pin 6
HALT HALT
LOAD verify_pin 8 LOAD verify_create_pin 8
INCMP account_creation * INCMP account_creation *

View File

@ -2,8 +2,8 @@ LOAD create_account 0
CATCH account_creation_failed flag_account_creation_failed 1 CATCH account_creation_failed flag_account_creation_failed 1
MOUT exit 0 MOUT exit 0
HALT HALT
LOAD save_pin 0 LOAD save_temporary_pin 6
RELOAD save_pin RELOAD save_temporary_pin
CATCH . flag_incorrect_pin 1 CATCH . flag_incorrect_pin 1
INCMP quit 0 INCMP quit 0
INCMP confirm_create_pin * INCMP confirm_create_pin *

View File

@ -1,4 +1,5 @@
LOAD reset_account_authorized 16 LOAD reset_account_authorized 16
RELOAD reset_account_authorized
LOAD reset_allow_update 0 LOAD reset_allow_update 0
RELOAD reset_allow_update RELOAD reset_allow_update
MOUT edit_name 1 MOUT edit_name 1

View File

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

View File

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

View File

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

View File

@ -1,12 +1,14 @@
msgid "Your account balance is %s" msgid "Your account balance is %s"
msgstr "Salio lako ni %s" msgstr "Salio lako ni %s"
msgid "Your request has been sent. %s will receive %s from %s." msgid "Your request has been sent. %s will receive %s %s from %s."
msgstr "Ombi lako limetumwa. %s atapokea %s kutoka kwa %s." msgstr "Ombi lako limetumwa. %s atapokea %s %s kutoka kwa %s."
msgid "Thank you for using Sarafu. Goodbye!" msgid "Thank you for using Sarafu. Goodbye!"
msgstr "Asante kwa kutumia huduma ya Sarafu. Kwaheri!" msgstr "Asante kwa kutumia huduma ya Sarafu. Kwaheri!"
msgid "For more help,please call: 0757628885" msgid "For more help,please call: 0757628885"
msgstr "Kwa usaidizi zaidi,piga: 0757628885" msgstr "Kwa usaidizi zaidi,piga: 0757628885"
msgid "Balance: %s\n"
msgstr "Salio: %s\n"

View File

@ -1 +1 @@
Balance: {{.check_balance}} {{.check_balance}}

View File

@ -1,5 +1,10 @@
LOAD set_default_voucher 8
RELOAD set_default_voucher
LOAD check_balance 64 LOAD check_balance 64
RELOAD check_balance RELOAD check_balance
LOAD check_vouchers 10
RELOAD check_vouchers
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
@ -8,7 +13,7 @@ MOUT help 4
MOUT quit 9 MOUT quit 9
HALT HALT
INCMP send 1 INCMP send 1
INCMP quit 2 INCMP my_vouchers 2
INCMP my_account 3 INCMP my_account 3
INCMP help 4 INCMP help 4
INCMP quit 9 INCMP quit 9

View File

@ -1 +1 @@
Salio: {{.check_balance}} {{.check_balance}}

View File

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

View File

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

View File

@ -0,0 +1 @@
My vouchers

View File

@ -0,0 +1,8 @@
LOAD reset_account_authorized 16
RELOAD reset_account_authorized
MOUT select_voucher 1
MOUT voucher_details 2
MOUT back 0
HALT
INCMP _ 0
INCMP select_voucher 1

View File

@ -1 +1 @@
Enter a new four number pin Enter a new four number PIN:

View File

@ -0,0 +1 @@
You need a voucher to send

View File

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

View File

@ -0,0 +1 @@
Unahitaji sarafu kutuma

View File

@ -14,3 +14,6 @@ flag,flag_valid_pin,20,this is set when the given PIN is valid
flag,flag_allow_update,21,this is set to allow a user to update their profile data flag,flag_allow_update,21,this is set to allow a user to update their profile data
flag,flag_single_edit,22,this is set to allow a user to edit a single profile item such as year of birth flag,flag_single_edit,22,this is set to allow a user to edit a single profile item such as year of birth
flag,flag_incorrect_date_format,23,this is set when the given year of birth is invalid flag,flag_incorrect_date_format,23,this is set when the given year of birth is invalid
flag,flag_incorrect_voucher,24,this is set when the selected voucher is invalid
flag,flag_api_call_error,25,this is set when communication to an external service fails
flag,flag_no_active_voucher,26,this is set when a user does not have an active voucher

1 flag flag_language_set 8 checks whether the user has set their prefered language
14 flag flag_allow_update 21 this is set to allow a user to update their profile data
15 flag flag_single_edit 22 this is set to allow a user to edit a single profile item such as year of birth
16 flag flag_incorrect_date_format 23 this is set when the given year of birth is invalid
17 flag flag_incorrect_voucher 24 this is set when the selected voucher is invalid
18 flag flag_api_call_error 25 this is set when communication to an external service fails
19 flag flag_no_active_voucher 26 this is set when a user does not have an active voucher

View File

@ -1,6 +1,8 @@
CATCH select_language flag_language_set 0 CATCH select_language flag_language_set 0
CATCH terms flag_account_created 0 CATCH terms flag_account_created 0
LOAD check_account_status 0 LOAD check_account_status 0
RELOAD check_account_status
CATCH api_failure flag_api_call_error 1
CATCH account_pending flag_account_pending 1 CATCH account_pending flag_account_pending 1
CATCH create_pin flag_pin_set 0 CATCH create_pin flag_pin_set 0
CATCH main flag_account_success 1 CATCH main flag_account_success 1

View File

@ -0,0 +1,2 @@
Select number or symbol from your vouchers:
{{.get_vouchers}}

View File

@ -0,0 +1,15 @@
LOAD get_vouchers 0
MAP get_vouchers
MOUT back 0
MOUT quit 99
MNEXT next 11
MPREV prev 22
HALT
LOAD view_voucher 80
RELOAD view_voucher
CATCH . flag_incorrect_voucher 1
INCMP _ 0
INCMP quit 99
INCMP > 11
INCMP < 22
INCMP view_voucher *

View File

@ -0,0 +1 @@
Select voucher

View File

@ -0,0 +1,2 @@
Chagua nambari au ishara kutoka kwa salio zako:
{{.get_vouchers}}

View File

@ -1,4 +1,5 @@
LOAD transaction_reset 0 LOAD transaction_reset 0
CATCH no_voucher flag_no_active_voucher 1
MOUT back 0 MOUT back 0
HALT HALT
LOAD validate_recipient 20 LOAD validate_recipient 20

View File

@ -0,0 +1 @@
Angalia Wasifu

View File

@ -0,0 +1,2 @@
Enter PIN to confirm selection:
{{.view_voucher}}

View File

@ -0,0 +1,10 @@
MAP view_voucher
MOUT back 0
MOUT quit 9
LOAD authorize_account 6
HALT
RELOAD authorize_account
CATCH incorrect_pin flag_incorrect_pin 1
INCMP _ 0
INCMP quit 9
INCMP voucher_set *

View File

@ -0,0 +1,2 @@
Weka PIN ili kuthibitisha chaguo:
{{.view_voucher}}

View File

@ -0,0 +1 @@
Voucher details

View File

@ -0,0 +1 @@
Success! {{.set_voucher}} is now your active voucher.

View File

@ -0,0 +1,10 @@
LOAD reset_incorrect 6
CATCH incorrect_pin flag_incorrect_pin 1
CATCH _ flag_account_authorized 0
LOAD set_voucher 12
MAP set_voucher
MOUT back 0
MOUT quit 9
HALT
INCMP ^ 0
INCMP quit 9

View File

@ -0,0 +1 @@
Hongera! {{.set_voucher}} ni Sarafu inayotumika sasa.