Compare commits

...

84 Commits

Author SHA1 Message Date
d2fce05461 Merge pull request 'fix: language change' (#242) from language-change-fix into master
Some checks are pending
release / docker (push) Waiting to run
Reviewed-on: urdt/ussd#242
2025-01-03 09:30:27 +01:00
68ac237449 Merge branch 'master' into language-change-fix 2025-01-03 09:28:48 +01:00
162e6c1934
fix: language change 2025-01-03 11:26:56 +03:00
8bd025f2b2 Merge pull request 'hash-pin' (#235) from hash-pin into master
Some checks are pending
release / docker (push) Waiting to run
Reviewed-on: urdt/ussd#235
2025-01-03 09:25:26 +01:00
9d6e25e184
revert to previous state for the adminstore 2025-01-03 11:24:24 +03:00
c26f5683f6
removed second unused argument 2025-01-03 11:17:09 +03:00
91dc9ce82f
tests: add sample pin/hash pair from migration dataset 2025-01-03 11:10:07 +03:00
0fe48a30fa
Merge branch 'master' into hash-pin 2025-01-03 06:58:41 +03:00
58edfa01a2 Merge pull request 'menu-primary-selectors' (#237) from menu-primary-selectors into master
Some checks are pending
release / docker (push) Waiting to run
Reviewed-on: urdt/ussd#237
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2025-01-02 15:50:42 +01:00
3830c12a57
update tests 2025-01-02 17:42:03 +03:00
f1fd690a7b
update expected content 2025-01-02 17:37:26 +03:00
491b7424a9
point to the correct ./devtools/admin_numbers directory 2025-01-02 16:01:19 +03:00
29ce4b83bd
added tests for HashPIN and VerifyPIN 2025-01-02 15:22:07 +03:00
ca8df5989a
updated expected age in test 2025-01-02 15:15:52 +03:00
82b4365d16
hash the PIN in TestAuthorize 2025-01-02 14:38:22 +03:00
98db85511b
hash the PIN in the ResetOthersPin function 2025-01-02 14:37:45 +03:00
99a4d3ff42
verify the PIN input against the hashed PIN 2025-01-02 13:51:57 +03:00
d95c7abea4
return if the PIN is not a match, and hash the PIN before saving it 2025-01-02 13:45:18 +03:00
fd1ac85a1b
add code to Hash and Verify the PIN 2025-01-02 13:43:38 +03:00
c899c098f6
updated the expected age 2025-01-02 13:20:01 +03:00
5ca6a74274
move PIN test to the common package 2025-01-02 13:18:49 +03:00
48d63fb43f
added pin.go to contain all PIN related functionality 2025-01-02 13:16:38 +03:00
e666c58644
start primary selectors with 1 2025-01-02 12:17:28 +03:00
e980586910
chore: repeat same node on invalid menu choice 2025-01-02 12:15:57 +03:00
ffd5be1f1f
add quit option on view profile 2025-01-02 12:12:52 +03:00
ed1aeecf7d Merge pull request 'Legible dumper' (#232) from lash/dump-key-prefix into master
Some checks failed
release / docker (push) Has been cancelled
Reviewed-on: urdt/ussd#232
2024-12-31 09:56:04 +01:00
3b69f3d38d Merge branch 'master' into lash/dump-key-prefix 2024-12-31 09:55:40 +01:00
lash
cd58f5ae33
Upgrade govise 2024-12-31 08:55:25 +00:00
7a535f796a
output the value as a string 2024-12-31 11:41:04 +03:00
7c4c73125e Merge pull request 'force-restart-state' (#223) from force-restart-state into master
Reviewed-on: urdt/ussd#223
2024-12-31 09:34:44 +01:00
lash
c7dbe1d88f
Remove obsolete subprefix strings 2024-12-31 08:30:08 +00:00
4ea52bf3fb
removed unused code 2024-12-31 11:16:43 +03:00
be2ea3a2f0
removed the non-working restart_state devtool 2024-12-31 10:51:29 +03:00
8217ea8fdc Merge branch 'master' into force-restart-state 2024-12-31 05:06:26 +03:00
3c73fc7188
added a test for the Init func with the different states 2024-12-31 05:05:39 +03:00
1311a0cab9
use the 'send_with_invite' test group in the menu traversal test 2024-12-31 02:36:28 +03:00
lash
3bcd48e5a7
Update govise 2024-12-30 19:58:34 +00:00
lash
0e12c0ee4e
Add prefix for dumper, format base dump key for pg 2024-12-30 19:35:45 +00:00
3caee98cdb Merge pull request 'mixed-languages' (#228) from mixed-languages into master
Reviewed-on: urdt/ussd#228
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-12-30 09:07:13 +01:00
db7c9bf56d
chore: add colon to enhance formatting. 2024-12-30 08:07:39 +03:00
0a332ec501
chore: ensure swahili language translation. 2024-12-30 08:05:36 +03:00
90367fe53e Merge pull request 'profile-update-fix' (#226) from profile-update-fix into master
Reviewed-on: urdt/ussd#226
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-12-28 16:20:32 +01:00
50c006546c
added code to reset the state and persist it 2024-12-28 13:21:03 +03:00
e8c171a82e Merge branch 'master' into force-restart-state 2024-12-28 11:46:15 +03:00
58a60f2c81
update expected age in test 2024-12-28 08:51:38 +03:00
0820e1b9f2 Merge branch 'master' into profile-update-fix 2024-12-28 06:30:14 +01:00
46edf2b819
remove
redundant catch on pin entry
2024-12-27 16:13:36 +03:00
11eb61ba35
repeat same node on invalid selection 2024-12-27 16:11:09 +03:00
813b92af78 Merge pull request 'issue-205: added comments for menu handlers methods.' (#218) from konstantinmds/ussd:dev/issue-205 into master
Reviewed-on: urdt/ussd#218
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-12-27 12:58:00 +01:00
5579991d66
guard profile update after being set 2024-12-27 10:07:05 +03:00
f4f4fdd3ac issue-205:
added comments for menu handlers methods and changed function name to better fit function workings.
2024-12-25 15:59:28 +01:00
be215d3f75
set the code to an empty byte for it to move to the top node 2024-12-20 12:26:07 +03:00
235af3519d Merge pull request 'add-space-after-colon' (#211) from add-space-after-colon into master
Some checks failed
release / docker (push) Has been cancelled
Reviewed-on: urdt/ussd#211
2024-12-19 11:35:13 +01:00
1292851226
rename the function to ReplaceSeparatorFunc 2024-12-19 13:32:39 +03:00
dfd0a0994b Merge branch 'master' into force-restart-state 2024-12-18 22:39:20 +03:00
97fcdda12f
Merge branch 'master' into add-space-after-colon 2024-12-18 22:30:41 +03:00
055c2db790
use a common mockReplaceSeparator func 2024-12-18 22:25:47 +03:00
ecfdab47a8
updated test 2024-12-18 22:21:52 +03:00
fda68231ea
use the replaceSeparator func to format the generated menus 2024-12-18 21:59:09 +03:00
d08afff443
add the replaceSeparator func and pass it to the Handler struct 2024-12-18 21:56:37 +03:00
17ba6a06ba
remove the MenuSeparator from the context 2024-12-18 21:49:42 +03:00
dbd59a4023 Merge pull request 'add link to terms page' (#217) from link-terms-and-conditions into master
Reviewed-on: urdt/ussd#217
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-12-18 07:45:48 +01:00
5534706189
reset the state when input is nil 2024-12-17 17:58:08 +03:00
5428626c3f
cleaned up the restart_state 2024-12-17 17:56:56 +03:00
9b33117cb1
add space on expected content 2024-12-17 16:02:35 +03:00
70b2fa4ac2
add spacing after link 2024-12-17 15:46:28 +03:00
fd6ff86579
add link to terms and conditions as expected content 2024-12-17 15:24:15 +03:00
549782f230
add link to terms page 2024-12-17 15:12:38 +03:00
8cf4848b45 Merge pull request 'Userstore dumper tool' (#153) from lash/store-dumper into master
Some checks failed
release / docker (push) Has been cancelled
Reviewed-on: urdt/ussd#153
2024-12-14 13:02:48 +01:00
7aea2af9a1
updated tests 2024-12-13 11:44:04 +03:00
5cd791aae7
use the MenuSeparator 2024-12-13 11:43:47 +03:00
df5e5f1a4b
properly format the vouchers 2024-12-13 11:40:39 +03:00
64c1fe5276
set the separator as a var and add it to the context 2024-12-13 11:38:10 +03:00
f38ea59569
set the separator as a config and not an arg 2024-12-13 01:10:46 +03:00
6cc285d1e8
add the custom separator to the menu 2024-12-12 21:12:25 +03:00
0d7f7aaca1
use latest commits from go-vise 2024-12-12 21:09:48 +03:00
f8ea2daa73
initialize the restart state devtool 2024-12-12 19:55:01 +03:00
e05f8e7291
update to the latest go-vise changes 2024-12-11 19:46:52 +03:00
2383e8ead3
updated failed menuhandler_test 2024-12-11 19:35:04 +03:00
1a4ee0d3e1
updated the description of the GetTransactionsList function 2024-12-11 19:32:41 +03:00
6f3b30e2fe
Capitalize statement details and add a space after the colon 2024-12-11 19:31:17 +03:00
b1e4b63c6a
Add a space after the colon for vouchers 2024-12-11 19:28:53 +03:00
3129e8210e
Capitalize the transfer statement details 2024-12-11 19:25:38 +03:00
5d8de80a18
Write the error in the response 2024-12-11 18:58:50 +03:00
37 changed files with 607 additions and 235 deletions

View File

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

View File

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

View File

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

View File

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

33
common/pin.go Normal file
View File

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

173
common/pin_test.go Normal file
View File

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

View File

@ -84,18 +84,18 @@ func GetTransferData(ctx context.Context, db storage.PrefixDb, publicKey string,
// Adjust for 0-based indexing // Adjust for 0-based indexing
i := index - 1 i := index - 1
transactionType := "received" transactionType := "Received"
party := fmt.Sprintf("from: %s", strings.TrimSpace(senders[i])) party := fmt.Sprintf("From: %s", strings.TrimSpace(senders[i]))
if strings.TrimSpace(senders[i]) == publicKey { if strings.TrimSpace(senders[i]) == publicKey {
transactionType = "sent" transactionType = "Sent"
party = fmt.Sprintf("to: %s", strings.TrimSpace(recipients[i])) party = fmt.Sprintf("To: %s", strings.TrimSpace(recipients[i]))
} }
formattedDate := formatDate(strings.TrimSpace(dates[i])) formattedDate := formatDate(strings.TrimSpace(dates[i]))
// Build the full transaction detail // Build the full transaction detail
detail := fmt.Sprintf( detail := fmt.Sprintf(
"%s %s %s\n%s\ncontract address: %s\ntxhash: %s\ndate: %s", "%s %s %s\n%s\nContract address: %s\nTxhash: %s\nDate: %s",
transactionType, transactionType,
strings.TrimSpace(values[i]), strings.TrimSpace(values[i]),
strings.TrimSpace(syms[i]), strings.TrimSpace(syms[i]),

View File

@ -11,13 +11,9 @@ import (
func init() { func init() {
DebugCap |= 1 DebugCap |= 1
dbTypStr[db.DATATYPE_STATE] = "internal state" dbTypStr[db.DATATYPE_STATE] = "internal state"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT] = "account"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT_CREATED] = "account created"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TRACKING_ID] = "tracking id" dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TRACKING_ID] = "tracking id"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_PUBLIC_KEY] = "public key" dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_PUBLIC_KEY] = "public key"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_CUSTODIAL_ID] = "custodial id"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT_PIN] = "account pin" dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT_PIN] = "account pin"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT_STATUS] = "account status"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_FIRST_NAME] = "first name" dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_FIRST_NAME] = "first name"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_FAMILY_NAME] = "family name" dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_FAMILY_NAME] = "family name"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_YOB] = "year of birth" dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_YOB] = "year of birth"

View File

@ -11,6 +11,7 @@ import (
"git.grassecon.net/urdt/ussd/initializers" "git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/internal/storage" "git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/debug" "git.grassecon.net/urdt/ussd/debug"
"git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/logging"
) )
@ -47,13 +48,14 @@ func main() {
store, err := menuStorageService.GetUserdataDb(ctx) store, err := menuStorageService.GetUserdataDb(ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, "get userdata db: %v\n", err.Error())
os.Exit(1) os.Exit(1)
} }
store.SetPrefix(db.DATATYPE_USERDATA)
d, err := store.Dump(ctx, []byte(sessionId)) d, err := store.Dump(ctx, []byte(sessionId))
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, "store dump fail: %v\n", err.Error())
os.Exit(1) os.Exit(1)
} }
@ -67,7 +69,7 @@ func main() {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
} }
fmt.Printf("%vValue: %v\n\n", o, v) fmt.Printf("%vValue: %v\n\n", o, string(v))
} }
err = store.Close() err = store.Close()

2
go.mod
View File

@ -3,7 +3,7 @@ module git.grassecon.net/urdt/ussd
go 1.23.0 go 1.23.0
require ( require (
git.defalsify.org/vise.git v0.2.1-0.20241212145627-683015d4df80 git.defalsify.org/vise.git v0.2.3-0.20241231085136-8582c7e157d9
github.com/alecthomas/assert/v2 v2.2.2 github.com/alecthomas/assert/v2 v2.2.2
github.com/gofrs/uuid v4.4.0+incompatible github.com/gofrs/uuid v4.4.0+incompatible
github.com/grassrootseconomics/eth-custodial v1.3.0-beta github.com/grassrootseconomics/eth-custodial v1.3.0-beta

4
go.sum
View File

@ -1,5 +1,5 @@
git.defalsify.org/vise.git v0.2.1-0.20241212145627-683015d4df80 h1:GYUVXRUtMpA40T4COeAduoay6CIgXjD5cfDYZOTFIKw= git.defalsify.org/vise.git v0.2.3-0.20241231085136-8582c7e157d9 h1:O3m+NgWDWtJm8OculT99c4bDMAO4xLe2c8hpCKpsd9g=
git.defalsify.org/vise.git v0.2.1-0.20241212145627-683015d4df80/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck= git.defalsify.org/vise.git v0.2.3-0.20241231085136-8582c7e157d9/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk=
github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g= github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g=

View File

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

View File

@ -5,7 +5,6 @@ import (
"context" "context"
"fmt" "fmt"
"path" "path"
"regexp"
"strconv" "strconv"
"strings" "strings"
@ -34,17 +33,6 @@ var (
translationDir = path.Join(scriptDir, "locale") translationDir = path.Join(scriptDir, "locale")
) )
// Define the regex patterns as constants
const (
pinPattern = `^\d{4}$`
)
// isValidPIN checks whether the given input is a 4 digit number
func isValidPIN(pin string) bool {
match, _ := regexp.MatchString(pinPattern, pin)
return match
}
// FlagManager handles centralized flag management // FlagManager handles centralized flag management
type FlagManager struct { type FlagManager struct {
parser *asm.FlagParser parser *asm.FlagParser
@ -69,18 +57,20 @@ func (fm *FlagManager) GetFlag(label string) (uint32, error) {
} }
type Handlers struct { type Handlers struct {
pe *persist.Persister pe *persist.Persister
st *state.State st *state.State
ca cache.Memory ca cache.Memory
userdataStore common.DataStore userdataStore common.DataStore
adminstore *utils.AdminStore adminstore *utils.AdminStore
flagManager *asm.FlagParser flagManager *asm.FlagParser
accountService remote.AccountServiceInterface accountService remote.AccountServiceInterface
prefixDb storage.PrefixDb prefixDb storage.PrefixDb
profile *models.Profile profile *models.Profile
ReplaceSeparatorFunc func(string) string
} }
func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, adminstore *utils.AdminStore, accountService remote.AccountServiceInterface) (*Handlers, error) { // NewHandlers creates a new instance of the Handlers struct with the provided dependencies.
func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, adminstore *utils.AdminStore, accountService remote.AccountServiceInterface, replaceSeparatorFunc func(string) string) (*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")
} }
@ -93,16 +83,18 @@ func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, adminstore *util
prefixDb := storage.NewSubPrefixDb(userdataStore, prefix) prefixDb := storage.NewSubPrefixDb(userdataStore, prefix)
h := &Handlers{ h := &Handlers{
userdataStore: userDb, userdataStore: userDb,
flagManager: appFlags, flagManager: appFlags,
adminstore: adminstore, adminstore: adminstore,
accountService: accountService, accountService: accountService,
prefixDb: prefixDb, prefixDb: prefixDb,
profile: &models.Profile{Max: 6}, profile: &models.Profile{Max: 6},
ReplaceSeparatorFunc: replaceSeparatorFunc,
} }
return h, nil return h, nil
} }
// WithPersister sets persister instance to the handlers.
func (h *Handlers) WithPersister(pe *persist.Persister) *Handlers { func (h *Handlers) WithPersister(pe *persist.Persister) *Handlers {
if h.pe != nil { if h.pe != nil {
panic("persister already set") panic("persister already set")
@ -111,6 +103,7 @@ func (h *Handlers) WithPersister(pe *persist.Persister) *Handlers {
return h return h
} }
// Init initializes the handler for a new session.
func (h *Handlers) Init(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) Init(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var r resource.Result var r resource.Result
if h.pe == nil { if h.pe == nil {
@ -124,6 +117,11 @@ func (h *Handlers) Init(ctx context.Context, sym string, input []byte) (resource
h.st = h.pe.GetState() h.st = h.pe.GetState()
h.ca = h.pe.GetMemory() h.ca = h.pe.GetMemory()
if len(input) == 0 {
// move to the top node
h.st.Code = []byte{}
}
sessionId, _ := ctx.Value("SessionId").(string) sessionId, _ := ctx.Value("SessionId").(string)
flag_admin_privilege, _ := h.flagManager.GetFlag("flag_admin_privilege") flag_admin_privilege, _ := h.flagManager.GetFlag("flag_admin_privilege")
@ -149,7 +147,7 @@ func (h *Handlers) Exit() {
h.pe = nil h.pe = nil
} }
// SetLanguage sets the language across the menu // SetLanguage sets the language across the menu.
func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
@ -173,6 +171,7 @@ func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (r
return res, nil return res, nil
} }
// handles the account creation when no existing account is present for the session and stores associated data in the user data store.
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 {
flag_account_created, _ := h.flagManager.GetFlag("flag_account_created") flag_account_created, _ := h.flagManager.GetFlag("flag_account_created")
r, err := h.accountService.CreateAccount(ctx) r, err := h.accountService.CreateAccount(ctx)
@ -205,9 +204,9 @@ func (h *Handlers) createAccountNoExist(ctx context.Context, sessionId string, r
return nil return nil
} }
// CreateAccount checks if any account exists on the JSON data file, and if not // CreateAccount checks if any account exists on the JSON data file, and if not,
// creates an account on the API, // creates an account on the API,
// sets the default values and flags // sets the default values and flags.
func (h *Handlers) CreateAccount(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) CreateAccount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
var err error var err error
@ -231,19 +230,22 @@ func (h *Handlers) CreateAccount(ctx context.Context, sym string, input []byte)
return res, nil return res, nil
} }
func (h *Handlers) CheckPinMisMatch(ctx context.Context, sym string, input []byte) (resource.Result, error) { // CheckBlockedNumPinMisMatch checks if the provided PIN matches a temporary PIN stored for a blocked number.
func (h *Handlers) CheckBlockedNumPinMisMatch(ctx context.Context, sym string, input []byte) (resource.Result, error) {
res := resource.Result{} res := resource.Result{}
flag_pin_mismatch, _ := h.flagManager.GetFlag("flag_pin_mismatch") flag_pin_mismatch, _ := h.flagManager.GetFlag("flag_pin_mismatch")
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")
} }
// Get blocked number from storage.
store := h.userdataStore store := h.userdataStore
blockedNumber, err := store.ReadEntry(ctx, sessionId, common.DATA_BLOCKED_NUMBER) blockedNumber, err := store.ReadEntry(ctx, sessionId, common.DATA_BLOCKED_NUMBER)
if err != nil { if err != nil {
logg.ErrorCtxf(ctx, "failed to read blockedNumber entry with", "key", common.DATA_BLOCKED_NUMBER, "error", err) logg.ErrorCtxf(ctx, "failed to read blockedNumber entry with", "key", common.DATA_BLOCKED_NUMBER, "error", err)
return res, err return res, err
} }
// Get temporary PIN for the blocked number.
temporaryPin, err := store.ReadEntry(ctx, string(blockedNumber), common.DATA_TEMPORARY_VALUE) temporaryPin, err := store.ReadEntry(ctx, string(blockedNumber), common.DATA_TEMPORARY_VALUE)
if err != nil { if err != nil {
logg.ErrorCtxf(ctx, "failed to read temporaryPin entry with", "key", common.DATA_TEMPORARY_VALUE, "error", err) logg.ErrorCtxf(ctx, "failed to read temporaryPin entry with", "key", common.DATA_TEMPORARY_VALUE, "error", err)
@ -257,6 +259,7 @@ func (h *Handlers) CheckPinMisMatch(ctx context.Context, sym string, input []byt
return res, nil return res, nil
} }
// VerifyNewPin checks if a new PIN meets the required format criteria.
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)
@ -265,8 +268,8 @@ func (h *Handlers) VerifyNewPin(ctx context.Context, sym string, input []byte) (
} }
flag_valid_pin, _ := h.flagManager.GetFlag("flag_valid_pin") flag_valid_pin, _ := h.flagManager.GetFlag("flag_valid_pin")
pinInput := string(input) pinInput := string(input)
// Validate that the PIN is a 4-digit number // Validate that the PIN is a 4-digit number.
if isValidPIN(pinInput) { if common.IsValidPIN(pinInput) {
res.FlagSet = append(res.FlagSet, flag_valid_pin) res.FlagSet = append(res.FlagSet, flag_valid_pin)
} else { } else {
res.FlagReset = append(res.FlagReset, flag_valid_pin) res.FlagReset = append(res.FlagReset, flag_valid_pin)
@ -275,9 +278,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_VALUE // SaveTemporaryPin saves the valid PIN input to the DATA_TEMPORARY_VALUE,
// during the account creation process // during the account creation process
// and during the change PIN 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
@ -290,8 +293,8 @@ func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byt
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin") flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
accountPIN := string(input) accountPIN := string(input)
// Validate that the PIN is a 4-digit number // Validate that the PIN is a 4-digit number.
if !isValidPIN(accountPIN) { if !common.IsValidPIN(accountPIN) {
res.FlagSet = append(res.FlagSet, flag_incorrect_pin) res.FlagSet = append(res.FlagSet, flag_incorrect_pin)
return res, nil return res, nil
} }
@ -306,6 +309,7 @@ func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byt
return res, nil return res, nil
} }
// SaveOthersTemporaryPin allows authorized users to set temporary PINs for blocked numbers.
func (h *Handlers) SaveOthersTemporaryPin(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) SaveOthersTemporaryPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
var err error var err error
@ -316,12 +320,14 @@ func (h *Handlers) SaveOthersTemporaryPin(ctx context.Context, sym string, input
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
temporaryPin := string(input) temporaryPin := string(input)
// First, we retrieve the blocked number associated with this session
blockedNumber, err := store.ReadEntry(ctx, sessionId, common.DATA_BLOCKED_NUMBER) blockedNumber, err := store.ReadEntry(ctx, sessionId, common.DATA_BLOCKED_NUMBER)
if err != nil { if err != nil {
logg.ErrorCtxf(ctx, "failed to read blockedNumber entry with", "key", common.DATA_BLOCKED_NUMBER, "error", err) logg.ErrorCtxf(ctx, "failed to read blockedNumber entry with", "key", common.DATA_BLOCKED_NUMBER, "error", err)
return res, err return res, err
} }
// Then we save the temporary PIN for that blocked number
err = store.WriteEntry(ctx, string(blockedNumber), common.DATA_TEMPORARY_VALUE, []byte(temporaryPin)) err = store.WriteEntry(ctx, string(blockedNumber), common.DATA_TEMPORARY_VALUE, []byte(temporaryPin))
if err != nil { if err != nil {
logg.ErrorCtxf(ctx, "failed to write temporaryPin entry with", "key", common.DATA_TEMPORARY_VALUE, "value", temporaryPin, "error", err) logg.ErrorCtxf(ctx, "failed to write temporaryPin entry with", "key", common.DATA_TEMPORARY_VALUE, "value", temporaryPin, "error", err)
@ -331,6 +337,7 @@ func (h *Handlers) SaveOthersTemporaryPin(ctx context.Context, sym string, input
return res, nil return res, nil
} }
// ConfirmPinChange validates user's new PIN. If input matches the temporary PIN, saves it as the new account PIN.
func (h *Handlers) ConfirmPinChange(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) ConfirmPinChange(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)
@ -349,10 +356,20 @@ func (h *Handlers) ConfirmPinChange(ctx context.Context, sym string, input []byt
res.FlagReset = append(res.FlagReset, flag_pin_mismatch) res.FlagReset = append(res.FlagReset, flag_pin_mismatch)
} else { } else {
res.FlagSet = append(res.FlagSet, flag_pin_mismatch) res.FlagSet = append(res.FlagSet, flag_pin_mismatch)
return res, nil
} }
err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(temporaryPin))
// Hash the PIN
hashedPIN, err := common.HashPIN(string(temporaryPin))
if err != nil { if err != nil {
logg.ErrorCtxf(ctx, "failed to write temporaryPin entry with", "key", common.DATA_ACCOUNT_PIN, "value", temporaryPin, "error", err) logg.ErrorCtxf(ctx, "failed to hash temporaryPin", "error", err)
return res, err
}
// save the hashed PIN as the new account PIN
err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(hashedPIN))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write DATA_ACCOUNT_PIN entry with", "key", common.DATA_ACCOUNT_PIN, "hashedPIN value", hashedPIN, "error", err)
return res, err return res, err
} }
return res, nil return res, nil
@ -360,7 +377,7 @@ func (h *Handlers) ConfirmPinChange(ctx context.Context, sym string, input []byt
// VerifyCreatePin checks whether the confirmation PIN is similar to the temporary PIN // VerifyCreatePin checks whether the confirmation PIN is similar to the temporary PIN
// If similar, it sets the USERFLAG_PIN_SET flag and writes the account PIN 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) VerifyCreatePin(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
@ -384,18 +401,26 @@ func (h *Handlers) VerifyCreatePin(ctx context.Context, sym string, input []byte
res.FlagSet = append(res.FlagSet, flag_pin_set) res.FlagSet = append(res.FlagSet, flag_pin_set)
} else { } else {
res.FlagSet = []uint32{flag_pin_mismatch} res.FlagSet = []uint32{flag_pin_mismatch}
return res, nil
} }
err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(temporaryPin)) // Hash the PIN
hashedPIN, err := common.HashPIN(string(temporaryPin))
if err != nil { if err != nil {
logg.ErrorCtxf(ctx, "failed to write temporaryPin entry with", "key", common.DATA_ACCOUNT_PIN, "value", temporaryPin, "error", err) logg.ErrorCtxf(ctx, "failed to hash temporaryPin", "error", err)
return res, err
}
err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(hashedPIN))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write DATA_ACCOUNT_PIN entry with", "key", common.DATA_ACCOUNT_PIN, "value", hashedPIN, "error", err)
return res, err return res, err
} }
return res, nil return res, nil
} }
// codeFromCtx retrieves language codes from the context that can be used for handling translations // retrieves language codes from the context that can be used for handling translations.
func codeFromCtx(ctx context.Context) string { func codeFromCtx(ctx context.Context) string {
var code string var code string
if ctx.Value("Language") != nil { if ctx.Value("Language") != nil {
@ -702,7 +727,7 @@ func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (res
return res, err return res, err
} }
if len(input) == 4 { if len(input) == 4 {
if bytes.Equal(input, AccountPin) { if common.VerifyPIN(string(AccountPin), string(input)) {
if h.st.MatchFlag(flag_account_authorized, false) { if h.st.MatchFlag(flag_account_authorized, false) {
res.FlagReset = append(res.FlagReset, flag_incorrect_pin) res.FlagReset = append(res.FlagReset, flag_incorrect_pin)
res.FlagSet = append(res.FlagSet, flag_allow_update, flag_account_authorized) res.FlagSet = append(res.FlagSet, flag_allow_update, flag_account_authorized)
@ -729,7 +754,7 @@ func (h *Handlers) ResetIncorrectPin(ctx context.Context, sym string, input []by
return res, nil return res, nil
} }
// Setback sets the flag_back_set flag when the navigation is back // Setback sets the flag_back_set flag when the navigation is back.
func (h *Handlers) SetBack(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) SetBack(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
//TODO: //TODO:
@ -742,7 +767,7 @@ func (h *Handlers) SetBack(ctx context.Context, sym string, input []byte) (resou
} }
// CheckAccountStatus queries the API using the TrackingId and sets flags // CheckAccountStatus queries the API using the TrackingId and sets flags
// based on the account status // based on the account status.
func (h *Handlers) CheckAccountStatus(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) CheckAccountStatus(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
@ -782,7 +807,7 @@ func (h *Handlers) CheckAccountStatus(ctx context.Context, sym string, input []b
return res, nil return res, nil
} }
// Quit displays the Thank you message and exits the menu // Quit displays the Thank you message and exits the menu.
func (h *Handlers) Quit(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) Quit(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
@ -797,7 +822,7 @@ func (h *Handlers) Quit(ctx context.Context, sym string, input []byte) (resource
return res, nil return res, nil
} }
// QuitWithHelp displays helpline information then exits the menu // QuitWithHelp displays helpline information then exits the menu.
func (h *Handlers) QuitWithHelp(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) QuitWithHelp(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
@ -812,7 +837,7 @@ func (h *Handlers) QuitWithHelp(ctx context.Context, sym string, input []byte) (
return res, nil return res, nil
} }
// VerifyYob verifies the length of the given input // VerifyYob verifies the length of the given input.
func (h *Handlers) VerifyYob(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) VerifyYob(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
var err error var err error
@ -834,7 +859,7 @@ func (h *Handlers) VerifyYob(ctx context.Context, sym string, input []byte) (res
return res, nil return res, nil
} }
// ResetIncorrectYob resets the incorrect date format flag after a new attempt // ResetIncorrectYob resets the incorrect date format flag after a new attempt.
func (h *Handlers) ResetIncorrectYob(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) ResetIncorrectYob(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
@ -844,7 +869,7 @@ func (h *Handlers) ResetIncorrectYob(ctx context.Context, sym string, input []by
} }
// CheckBalance retrieves the balance of the active voucher 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
var err error var err error
@ -894,9 +919,12 @@ func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) (
return res, nil return res, nil
} }
// FetchCommunityBalance retrieves and displays the balance for community accounts in user's preferred language.
func (h *Handlers) FetchCommunityBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) FetchCommunityBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
// retrieve the language code from the context
code := codeFromCtx(ctx) code := codeFromCtx(ctx)
// Initialize the localization system with the appropriate translation directory
l := gotext.NewLocale(translationDir, code) l := gotext.NewLocale(translationDir, code)
l.AddDomain("default") l.AddDomain("default")
//TODO: //TODO:
@ -905,6 +933,10 @@ func (h *Handlers) FetchCommunityBalance(ctx context.Context, sym string, input
return res, nil return res, nil
} }
// ResetOthersPin handles the PIN reset process for other users' accounts by:
// 1. Retrieving the blocked phone number from the session
// 2. Fetching the temporary PIN associated with that number
// 3. Updating the account PIN with the temporary PIN
func (h *Handlers) ResetOthersPin(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) ResetOthersPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
store := h.userdataStore store := h.userdataStore
@ -922,7 +954,15 @@ func (h *Handlers) ResetOthersPin(ctx context.Context, sym string, input []byte)
logg.ErrorCtxf(ctx, "failed to read temporaryPin entry with", "key", common.DATA_TEMPORARY_VALUE, "error", err) logg.ErrorCtxf(ctx, "failed to read temporaryPin entry with", "key", common.DATA_TEMPORARY_VALUE, "error", err)
return res, err return res, err
} }
err = store.WriteEntry(ctx, string(blockedPhonenumber), common.DATA_ACCOUNT_PIN, []byte(temporaryPin))
// Hash the PIN
hashedPIN, err := common.HashPIN(string(temporaryPin))
if err != nil {
logg.ErrorCtxf(ctx, "failed to hash temporaryPin", "error", err)
return res, err
}
err = store.WriteEntry(ctx, string(blockedPhonenumber), common.DATA_ACCOUNT_PIN, []byte(hashedPIN))
if err != nil { if err != nil {
return res, nil return res, nil
} }
@ -930,6 +970,8 @@ func (h *Handlers) ResetOthersPin(ctx context.Context, sym string, input []byte)
return res, nil return res, nil
} }
// ResetUnregisteredNumber clears the unregistered number flag in the system,
// indicating that a number's registration status should no longer be marked as unregistered.
func (h *Handlers) ResetUnregisteredNumber(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) ResetUnregisteredNumber(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
flag_unregistered_number, _ := h.flagManager.GetFlag("flag_unregistered_number") flag_unregistered_number, _ := h.flagManager.GetFlag("flag_unregistered_number")
@ -937,6 +979,8 @@ func (h *Handlers) ResetUnregisteredNumber(ctx context.Context, sym string, inpu
return res, nil return res, nil
} }
// ValidateBlockedNumber performs validation of phone numbers, specifically for blocked numbers in the system.
// It checks phone number format and verifies registration status.
func (h *Handlers) ValidateBlockedNumber(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) ValidateBlockedNumber(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
var err error var err error
@ -1065,7 +1109,7 @@ func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []by
} }
// TransactionReset resets the previous transaction data (Recipient and Amount) // TransactionReset resets the previous transaction data (Recipient and Amount)
// as well as the invalid flags // as well as the invalid flags.
func (h *Handlers) TransactionReset(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) TransactionReset(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
var err error var err error
@ -1118,7 +1162,7 @@ func (h *Handlers) InviteValidRecipient(ctx context.Context, sym string, input [
return res, nil return res, nil
} }
// ResetTransactionAmount resets the transaction amount and invalid flag // ResetTransactionAmount resets the transaction amount and invalid flag.
func (h *Handlers) ResetTransactionAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) ResetTransactionAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
var err error var err error
@ -1248,7 +1292,7 @@ func (h *Handlers) RetrieveBlockedNumber(ctx context.Context, sym string, input
return res, nil return res, nil
} }
// GetSender returns the sessionId (phoneNumber) // 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
@ -1262,7 +1306,7 @@ func (h *Handlers) GetSender(ctx context.Context, sym string, input []byte) (res
return res, nil return res, nil
} }
// GetAmount retrieves the amount from teh Gdbm Db // GetAmount retrieves the amount from teh Gdbm Db.
func (h *Handlers) GetAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) GetAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
@ -1286,7 +1330,7 @@ func (h *Handlers) GetAmount(ctx context.Context, sym string, input []byte) (res
return res, nil return res, nil
} }
// InitiateTransaction calls the TokenTransfer and returns a confirmation based on the result // InitiateTransaction calls the TokenTransfer and returns a confirmation based on the result.
func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
var err error var err error
@ -1336,9 +1380,12 @@ func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input []
return res, nil return res, nil
} }
// GetCurrentProfileInfo retrieves specific profile fields based on the current state of the USSD session.
// Uses flag management system to track profile field status and handle menu navigation.
func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
var profileInfo []byte var profileInfo []byte
var defaultValue string
var err error var err error
flag_firstname_set, _ := h.flagManager.GetFlag("flag_firstname_set") flag_firstname_set, _ := h.flagManager.GetFlag("flag_firstname_set")
@ -1355,6 +1402,17 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
if !ok { if !ok {
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
language, ok := ctx.Value("Language").(lang.Language)
if !ok {
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"
}
sm, _ := h.st.Where() sm, _ := h.st.Where()
parts := strings.SplitN(sm, "_", 2) parts := strings.SplitN(sm, "_", 2)
filename := parts[1] filename := parts[1]
@ -1371,7 +1429,7 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_FIRST_NAME) profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_FIRST_NAME)
if err != nil { if err != nil {
if db.IsNotFound(err) { if db.IsNotFound(err) {
res.Content = "Not provided" res.Content = defaultValue
break break
} }
logg.ErrorCtxf(ctx, "Failed to read first name entry with", "key", "error", common.DATA_FIRST_NAME, err) logg.ErrorCtxf(ctx, "Failed to read first name entry with", "key", "error", common.DATA_FIRST_NAME, err)
@ -1383,7 +1441,7 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_FAMILY_NAME) profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_FAMILY_NAME)
if err != nil { if err != nil {
if db.IsNotFound(err) { if db.IsNotFound(err) {
res.Content = "Not provided" res.Content = defaultValue
break break
} }
logg.ErrorCtxf(ctx, "Failed to read family name entry with", "key", "error", common.DATA_FAMILY_NAME, err) logg.ErrorCtxf(ctx, "Failed to read family name entry with", "key", "error", common.DATA_FAMILY_NAME, err)
@ -1396,7 +1454,7 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_GENDER) profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_GENDER)
if err != nil { if err != nil {
if db.IsNotFound(err) { if db.IsNotFound(err) {
res.Content = "Not provided" res.Content = defaultValue
break break
} }
logg.ErrorCtxf(ctx, "Failed to read gender entry with", "key", "error", common.DATA_GENDER, err) logg.ErrorCtxf(ctx, "Failed to read gender entry with", "key", "error", common.DATA_GENDER, err)
@ -1408,7 +1466,7 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_YOB) profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_YOB)
if err != nil { if err != nil {
if db.IsNotFound(err) { if db.IsNotFound(err) {
res.Content = "Not provided" res.Content = defaultValue
break break
} }
logg.ErrorCtxf(ctx, "Failed to read year of birth(yob) entry with", "key", "error", common.DATA_YOB, err) logg.ErrorCtxf(ctx, "Failed to read year of birth(yob) entry with", "key", "error", common.DATA_YOB, err)
@ -1420,7 +1478,7 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_LOCATION) profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_LOCATION)
if err != nil { if err != nil {
if db.IsNotFound(err) { if db.IsNotFound(err) {
res.Content = "Not provided" res.Content = defaultValue
break break
} }
logg.ErrorCtxf(ctx, "Failed to read location entry with", "key", "error", common.DATA_LOCATION, err) logg.ErrorCtxf(ctx, "Failed to read location entry with", "key", "error", common.DATA_LOCATION, err)
@ -1432,7 +1490,7 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_OFFERINGS) profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_OFFERINGS)
if err != nil { if err != nil {
if db.IsNotFound(err) { if db.IsNotFound(err) {
res.Content = "Not provided" res.Content = defaultValue
break break
} }
logg.ErrorCtxf(ctx, "Failed to read offerings entry with", "key", "error", common.DATA_OFFERINGS, err) logg.ErrorCtxf(ctx, "Failed to read offerings entry with", "key", "error", common.DATA_OFFERINGS, err)
@ -1447,6 +1505,7 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
return res, nil return res, nil
} }
// GetProfileInfo provides a comprehensive view of a user's profile.
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 var defaultValue string
@ -1515,7 +1574,7 @@ func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte)
} }
// SetDefaultVoucher retrieves the current vouchers // SetDefaultVoucher retrieves the current vouchers
// and sets the first as the default voucher, if no active voucher is set // 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) { func (h *Handlers) SetDefaultVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
var err error var err error
@ -1600,7 +1659,7 @@ func (h *Handlers) SetDefaultVoucher(ctx context.Context, sym string, input []by
} }
// CheckVouchers retrieves the token holdings from the API using the "PublicKey" and stores // CheckVouchers retrieves the token holdings from the API using the "PublicKey" and stores
// them to gdbm // them to gdbm.
func (h *Handlers) CheckVouchers(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) CheckVouchers(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)
@ -1672,7 +1731,7 @@ func (h *Handlers) CheckVouchers(ctx context.Context, sym string, input []byte)
return res, nil return res, nil
} }
// GetVoucherList fetches the list of vouchers and formats them // GetVoucherList fetches the list of vouchers and formats them.
func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
@ -1683,13 +1742,15 @@ func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte)
return res, err return res, err
} }
res.Content = string(voucherData) formattedData := h.ReplaceSeparatorFunc(string(voucherData))
res.Content = string(formattedData)
return res, nil return res, nil
} }
// ViewVoucher retrieves the token holding and balance from the subprefixDB // ViewVoucher retrieves the token holding and balance from the subprefixDB
// and displays it to the user for them to select it // and displays it to the user for them to select it.
func (h *Handlers) ViewVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) ViewVoucher(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)
@ -1730,7 +1791,7 @@ func (h *Handlers) ViewVoucher(ctx context.Context, sym string, input []byte) (r
return res, nil return res, nil
} }
// SetVoucher retrieves the temp voucher data and sets it as the active data // SetVoucher retrieves the temp voucher data and sets it as the active data.
func (h *Handlers) SetVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) SetVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
@ -1756,7 +1817,7 @@ func (h *Handlers) SetVoucher(ctx context.Context, sym string, input []byte) (re
return res, nil return res, nil
} }
// GetVoucherDetails retrieves the voucher details // GetVoucherDetails retrieves the voucher details.
func (h *Handlers) GetVoucherDetails(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) GetVoucherDetails(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
store := h.userdataStore store := h.userdataStore
@ -1788,7 +1849,7 @@ func (h *Handlers) GetVoucherDetails(ctx context.Context, sym string, input []by
return res, nil return res, nil
} }
// CheckTransactions retrieves the transactions from the API using the "PublicKey" and stores to prefixDb // CheckTransactions retrieves the transactions from the API using the "PublicKey" and stores to prefixDb.
func (h *Handlers) CheckTransactions(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) CheckTransactions(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)
@ -1846,13 +1907,14 @@ func (h *Handlers) CheckTransactions(ctx context.Context, sym string, input []by
return res, nil return res, nil
} }
// GetTransactionsList fetches the list of transactions and formats them // GetTransactionsList fetches the list of transactions and formats them.
func (h *Handlers) GetTransactionsList(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) GetTransactionsList(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, err := store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY) publicKey, err := store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY)
if err != nil { if err != nil {
@ -1895,12 +1957,14 @@ func (h *Handlers) GetTransactionsList(ctx context.Context, sym string, input []
value := strings.TrimSpace(values[i]) value := strings.TrimSpace(values[i])
date := strings.Split(strings.TrimSpace(dates[i]), " ")[0] date := strings.Split(strings.TrimSpace(dates[i]), " ")[0]
status := "received" status := "Received"
if sender == string(publicKey) { if sender == string(publicKey) {
status = "sent" status = "Sent"
} }
formattedTransactions = append(formattedTransactions, fmt.Sprintf("%d:%s %s %s %s", i+1, status, value, sym, date)) // Use the ReplaceSeparator function for the menu separator
transactionLine := fmt.Sprintf("%d%s%s %s %s %s", i+1, h.ReplaceSeparatorFunc(":"), status, value, sym, date)
formattedTransactions = append(formattedTransactions, transactionLine)
} }
res.Content = strings.Join(formattedTransactions, "\n") res.Content = strings.Join(formattedTransactions, "\n")
@ -1909,7 +1973,7 @@ func (h *Handlers) GetTransactionsList(ctx context.Context, sym string, input []
} }
// ViewTransactionStatement retrieves the transaction statement // ViewTransactionStatement retrieves the transaction statement
// and displays it to the user // and displays it to the user.
func (h *Handlers) ViewTransactionStatement(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) ViewTransactionStatement(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)
@ -1957,6 +2021,7 @@ func (h *Handlers) ViewTransactionStatement(ctx context.Context, sym string, inp
return res, nil return res, nil
} }
// handles bulk updates of profile information.
func (h *Handlers) insertProfileItems(ctx context.Context, sessionId string, res *resource.Result) error { func (h *Handlers) insertProfileItems(ctx context.Context, sessionId string, res *resource.Result) error {
var err error var err error
store := h.userdataStore store := h.userdataStore
@ -1979,21 +2044,22 @@ func (h *Handlers) insertProfileItems(ctx context.Context, sessionId string, res
for index, profileItem := range h.profile.ProfileItems { for index, profileItem := range h.profile.ProfileItems {
// Ensure the profileItem is not "0"(is set) // Ensure the profileItem is not "0"(is set)
if profileItem != "0" { if profileItem != "0" {
err = store.WriteEntry(ctx, sessionId, profileDataKeys[index], []byte(profileItem))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write profile entry with", "key", profileDataKeys[index], "value", profileItem, "error", err)
return err
}
// Get the flag for the current index
flag, _ := h.flagManager.GetFlag(profileFlagNames[index]) flag, _ := h.flagManager.GetFlag(profileFlagNames[index])
res.FlagSet = append(res.FlagSet, flag) isProfileItemSet := h.st.MatchFlag(flag, true)
if !isProfileItemSet {
err = store.WriteEntry(ctx, sessionId, profileDataKeys[index], []byte(profileItem))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write profile entry with", "key", profileDataKeys[index], "value", profileItem, "error", err)
return err
}
res.FlagSet = append(res.FlagSet, flag)
}
} }
} }
return nil return nil
} }
// UpdateAllProfileItems is used to persist all the new profile information and setup the required profile flags // UpdateAllProfileItems is used to persist all the new profile information and setup the required profile flags.
func (h *Handlers) UpdateAllProfileItems(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) UpdateAllProfileItems(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)

View File

@ -5,8 +5,10 @@ import (
"fmt" "fmt"
"log" "log"
"path" "path"
"strings"
"testing" "testing"
"git.defalsify.org/vise.git/cache"
"git.defalsify.org/vise.git/lang" "git.defalsify.org/vise.git/lang"
"git.defalsify.org/vise.git/persist" "git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/resource"
@ -14,6 +16,7 @@ import (
"git.grassecon.net/urdt/ussd/internal/storage" "git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/internal/testutil/mocks" "git.grassecon.net/urdt/ussd/internal/testutil/mocks"
"git.grassecon.net/urdt/ussd/internal/testutil/testservice" "git.grassecon.net/urdt/ussd/internal/testutil/testservice"
"git.grassecon.net/urdt/ussd/internal/utils"
"git.grassecon.net/urdt/ussd/models" "git.grassecon.net/urdt/ussd/models"
"git.grassecon.net/urdt/ussd/common" "git.grassecon.net/urdt/ussd/common"
@ -32,6 +35,11 @@ var (
flagsPath = path.Join(baseDir, "services", "registration", "pp.csv") flagsPath = path.Join(baseDir, "services", "registration", "pp.csv")
) )
// mockReplaceSeparator function
var mockReplaceSeparator = func(input string) string {
return strings.ReplaceAll(input, ":", ": ")
}
// InitializeTestStore sets up and returns an in-memory database and store. // InitializeTestStore sets up and returns an in-memory database and store.
func InitializeTestStore(t *testing.T) (context.Context, *common.UserDataStore) { func InitializeTestStore(t *testing.T) (context.Context, *common.UserDataStore) {
ctx := context.Background() ctx := context.Background()
@ -67,12 +75,15 @@ func TestNewHandlers(t *testing.T) {
_, store := InitializeTestStore(t) _, store := InitializeTestStore(t)
fm, err := NewFlagManager(flagsPath) fm, err := NewFlagManager(flagsPath)
accountService := testservice.TestAccountService{}
if err != nil { if err != nil {
t.Logf(err.Error()) log.Fatal(err)
} }
accountService := testservice.TestAccountService{}
// Test case for valid UserDataStore
t.Run("Valid UserDataStore", func(t *testing.T) { t.Run("Valid UserDataStore", func(t *testing.T) {
handlers, err := NewHandlers(fm.parser, store, nil, &accountService) handlers, err := NewHandlers(fm.parser, store, nil, &accountService, mockReplaceSeparator)
if err != nil { if err != nil {
t.Fatalf("expected no error, got %v", err) t.Fatalf("expected no error, got %v", err)
} }
@ -82,23 +93,130 @@ func TestNewHandlers(t *testing.T) {
if handlers.userdataStore == nil { if handlers.userdataStore == nil {
t.Fatal("expected userdataStore to be set in handlers") t.Fatal("expected userdataStore to be set in handlers")
} }
if handlers.ReplaceSeparatorFunc == nil {
t.Fatal("expected ReplaceSeparatorFunc to be set in handlers")
}
// Test ReplaceSeparatorFunc functionality
input := "1:Menu item"
expectedOutput := "1: Menu item"
if handlers.ReplaceSeparatorFunc(input) != expectedOutput {
t.Fatalf("ReplaceSeparatorFunc function did not return expected output: got %v, want %v", handlers.ReplaceSeparatorFunc(input), expectedOutput)
}
}) })
// Test case for nil userdataStore // Test case for nil UserDataStore
t.Run("Nil UserDataStore", func(t *testing.T) { t.Run("Nil UserDataStore", func(t *testing.T) {
handlers, err := NewHandlers(fm.parser, nil, nil, &accountService) handlers, err := NewHandlers(fm.parser, nil, nil, &accountService, mockReplaceSeparator)
if err == nil { if err == nil {
t.Fatal("expected an error, got none") t.Fatal("expected an error, got none")
} }
if handlers != nil { if handlers != nil {
t.Fatal("expected handlers to be nil") t.Fatal("expected handlers to be nil")
} }
if err.Error() != "cannot create handler with nil userdata store" { expectedError := "cannot create handler with nil userdata store"
t.Fatalf("expected specific error, got %v", err) if err.Error() != expectedError {
t.Fatalf("expected error '%s', got '%v'", expectedError, err)
} }
}) })
} }
func TestInit(t *testing.T) {
sessionId := "session123"
ctx, store := InitializeTestStore(t)
ctx = context.WithValue(ctx, "SessionId", sessionId)
fm, err := NewFlagManager(flagsPath)
if err != nil {
t.Fatal(err.Error())
}
adminstore, err := utils.NewAdminStore(ctx, "admin_numbers")
if err != nil {
t.Fatal(err.Error())
}
st := state.NewState(128)
ca := cache.NewCache()
flag_admin_privilege, _ := fm.GetFlag("flag_admin_privilege")
tests := []struct {
name string
setup func() (*Handlers, context.Context)
input []byte
expectedResult resource.Result
}{
{
name: "Handler not ready",
setup: func() (*Handlers, context.Context) {
return &Handlers{}, ctx
},
input: []byte("1"),
expectedResult: resource.Result{},
},
{
name: "State and memory initialization",
setup: func() (*Handlers, context.Context) {
pe := persist.NewPersister(store).WithSession(sessionId).WithContent(st, ca)
h := &Handlers{
flagManager: fm.parser,
adminstore: adminstore,
pe: pe,
}
return h, context.WithValue(ctx, "SessionId", sessionId)
},
input: []byte("1"),
expectedResult: resource.Result{
FlagReset: []uint32{flag_admin_privilege},
},
},
{
name: "Non-admin session initialization",
setup: func() (*Handlers, context.Context) {
pe := persist.NewPersister(store).WithSession("0712345678").WithContent(st, ca)
h := &Handlers{
flagManager: fm.parser,
adminstore: adminstore,
pe: pe,
}
return h, context.WithValue(context.Background(), "SessionId", "0712345678")
},
input: []byte("1"),
expectedResult: resource.Result{
FlagReset: []uint32{flag_admin_privilege},
},
},
{
name: "Move to top node on empty input",
setup: func() (*Handlers, context.Context) {
pe := persist.NewPersister(store).WithSession(sessionId).WithContent(st, ca)
h := &Handlers{
flagManager: fm.parser,
adminstore: adminstore,
pe: pe,
}
st.Code = []byte("some pending bytecode")
return h, context.WithValue(ctx, "SessionId", sessionId)
},
input: []byte(""),
expectedResult: resource.Result{
FlagReset: []uint32{flag_admin_privilege},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h, testCtx := tt.setup()
res, err := h.Init(testCtx, "", tt.input)
assert.NoError(t, err, "Unexpected error occurred")
assert.Equal(t, res, tt.expectedResult, "Expected result should match actual result")
})
}
}
func TestCreateAccount(t *testing.T) { func TestCreateAccount(t *testing.T) {
sessionId := "session123" sessionId := "session123"
ctx, store := InitializeTestStore(t) ctx, store := InitializeTestStore(t)
@ -929,7 +1047,14 @@ func TestAuthorize(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(accountPIN)) // Hash the PIN
hashedPIN, err := common.HashPIN(accountPIN)
if err != nil {
logg.ErrorCtxf(ctx, "failed to hash temporaryPin", "error", err)
t.Fatal(err)
}
err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(hashedPIN))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -1381,59 +1506,6 @@ func TestQuit(t *testing.T) {
} }
} }
func TestIsValidPIN(t *testing.T) {
tests := []struct {
name string
pin string
expected bool
}{
{
name: "Valid PIN with 4 digits",
pin: "1234",
expected: true,
},
{
name: "Valid PIN with leading zeros",
pin: "0001",
expected: true,
},
{
name: "Invalid PIN with less than 4 digits",
pin: "123",
expected: false,
},
{
name: "Invalid PIN with more than 4 digits",
pin: "12345",
expected: false,
},
{
name: "Invalid PIN with letters",
pin: "abcd",
expected: false,
},
{
name: "Invalid PIN with special characters",
pin: "12@#",
expected: false,
},
{
name: "Empty PIN",
pin: "",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := isValidPIN(tt.pin)
if actual != tt.expected {
t.Errorf("isValidPIN(%q) = %v; expected %v", tt.pin, actual, tt.expected)
}
})
}
}
func TestValidateAmount(t *testing.T) { func TestValidateAmount(t *testing.T) {
fm, err := NewFlagManager(flagsPath) fm, err := NewFlagManager(flagsPath)
if err != nil { if err != nil {
@ -1680,7 +1752,7 @@ func TestGetProfile(t *testing.T) {
result: resource.Result{ result: resource.Result{
Content: fmt.Sprintf( Content: fmt.Sprintf(
"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n", "Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
"John Doee", "Male", "48", "Kilifi", "Bananas", "John Doee", "Male", "49", "Kilifi", "Bananas",
), ),
}, },
}, },
@ -1692,7 +1764,7 @@ func TestGetProfile(t *testing.T) {
result: resource.Result{ result: resource.Result{
Content: fmt.Sprintf( Content: fmt.Sprintf(
"Jina: %s\nJinsia: %s\nUmri: %s\nEneo: %s\nUnauza: %s\n", "Jina: %s\nJinsia: %s\nUmri: %s\nEneo: %s\nUnauza: %s\n",
"John Doee", "Male", "48", "Kilifi", "Bananas", "John Doee", "Male", "49", "Kilifi", "Bananas",
), ),
}, },
}, },
@ -1704,7 +1776,7 @@ func TestGetProfile(t *testing.T) {
result: resource.Result{ result: resource.Result{
Content: fmt.Sprintf( Content: fmt.Sprintf(
"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n", "Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
"John Doee", "Male", "48", "Kilifi", "Bananas", "John Doee", "Male", "49", "Kilifi", "Bananas",
), ),
}, },
}, },
@ -1982,26 +2054,31 @@ func TestCheckVouchers(t *testing.T) {
func TestGetVoucherList(t *testing.T) { func TestGetVoucherList(t *testing.T) {
sessionId := "session123" sessionId := "session123"
ctx := context.WithValue(context.Background(), "SessionId", sessionId) ctx := context.WithValue(context.Background(), "SessionId", sessionId)
spdb := InitializeTestSubPrefixDb(t, ctx) spdb := InitializeTestSubPrefixDb(t, ctx)
// Initialize Handlers
h := &Handlers{ h := &Handlers{
prefixDb: spdb, prefixDb: spdb,
ReplaceSeparatorFunc: mockReplaceSeparator,
} }
expectedSym := []byte("1:SRF\n2:MILO") mockSyms := []byte("1:SRF\n2:MILO")
// Put voucher sym data from the store // Put voucher sym data from the store
err := spdb.Put(ctx, common.ToBytes(common.DATA_VOUCHER_SYMBOLS), expectedSym) err := spdb.Put(ctx, common.ToBytes(common.DATA_VOUCHER_SYMBOLS), mockSyms)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
expectedSyms := []byte("1: SRF\n2: MILO")
res, err := h.GetVoucherList(ctx, "", []byte("")) res, err := h.GetVoucherList(ctx, "", []byte(""))
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, res.Content, string(expectedSym)) assert.Equal(t, res.Content, string(expectedSyms))
} }
func TestViewVoucher(t *testing.T) { func TestViewVoucher(t *testing.T) {

View File

@ -17,8 +17,7 @@ var (
type DefaultRequestParser struct { type DefaultRequestParser struct {
} }
func (rp *DefaultRequestParser) GetSessionId(rq any) (string, error) {
func(rp *DefaultRequestParser) GetSessionId(rq any) (string, error) {
rqv, ok := rq.(*http.Request) rqv, ok := rq.(*http.Request)
if !ok { if !ok {
return "", handlers.ErrInvalidRequest return "", handlers.ErrInvalidRequest
@ -30,7 +29,7 @@ func(rp *DefaultRequestParser) GetSessionId(rq any) (string, error) {
return v, nil return v, nil
} }
func(rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) { func (rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) {
rqv, ok := rq.(*http.Request) rqv, ok := rq.(*http.Request)
if !ok { if !ok {
return nil, handlers.ErrInvalidRequest return nil, handlers.ErrInvalidRequest
@ -53,25 +52,24 @@ func ToSessionHandler(h handlers.RequestHandler) *SessionHandler {
} }
} }
func(f *SessionHandler) writeError(w http.ResponseWriter, code int, err error) { func (f *SessionHandler) writeError(w http.ResponseWriter, code int, err error) {
s := err.Error() s := err.Error()
w.Header().Set("Content-Length", strconv.Itoa(len(s))) w.Header().Set("Content-Length", strconv.Itoa(len(s)))
w.WriteHeader(code) w.WriteHeader(code)
_, err = w.Write([]byte{}) _, err = w.Write([]byte(s))
if err != nil { if err != nil {
logg.Errorf("error writing error!!", "err", err, "olderr", s) logg.Errorf("error writing error!!", "err", err, "olderr", s)
w.WriteHeader(500) w.WriteHeader(500)
} }
return
} }
func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
var code int var code int
var err error var err error
var perr error var perr error
rqs := handlers.RequestSession{ rqs := handlers.RequestSession{
Ctx: req.Context(), Ctx: req.Context(),
Writer: w, Writer: w,
} }

View File

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

View File

@ -62,10 +62,10 @@
}, },
{ {
"input": "1234", "input": "1234",
"expectedContent": "Select language:\n0:English\n1:Kiswahili" "expectedContent": "Select language:\n1:English\n2:Kiswahili"
}, },
{ {
"input": "0", "input": "1",
"expectedContent": "Your language change request was successful.\n0:Back\n9:Quit" "expectedContent": "Your language change request was successful.\n0:Back\n9:Quit"
}, },
{ {
@ -430,7 +430,7 @@
}, },
{ {
"input": "1234", "input": "1234",
"expectedContent": "My profile:\nName: foo bar\nGender: male\nAge: 84\nLocation: Kilifi\nYou provide: Bananas\n\n0:Back" "expectedContent": "My profile:\nName: foo bar\nGender: male\nAge: 80\nLocation: Kilifi\nYou provide: Bananas\n\n0:Back\n9:Quit"
}, },
{ {
"input": "0", "input": "0",

View File

@ -298,9 +298,10 @@ func TestMainMenuSend(t *testing.T) {
ctx := context.Background() ctx := context.Background()
sessions := testData sessions := testData
for _, session := range sessions { for _, session := range sessions {
groups := driver.FilterGroupsByName(session.Groups, "send_with_invalid_inputs") groups := driver.FilterGroupsByName(session.Groups, "send_with_invite")
for _, group := range groups { for _, group := range groups {
for _, step := range group.Steps { for index, step := range group.Steps {
t.Logf("step %v with input %v", index, step.Input)
cont, err := en.Exec(ctx, []byte(step.Input)) cont, err := en.Exec(ctx, []byte(step.Input))
if err != nil { if err != nil {
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err) t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,5 +10,4 @@ CATCH _ flag_back_set 1
RELOAD save_offerings RELOAD save_offerings
INCMP _ 0 INCMP _ 0
CATCH pin_entry flag_offerings_set 1 CATCH pin_entry flag_offerings_set 1
CATCH pin_entry flag_offerings_set 0
INCMP update_profile_items * INCMP update_profile_items *

View File

@ -20,3 +20,4 @@ INCMP edit_yob 4
INCMP edit_location 5 INCMP edit_location 5
INCMP edit_offerings 6 INCMP edit_offerings 6
INCMP view_profile 7 INCMP view_profile 7
INCMP . *

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +1,2 @@
Do you agree to terms and conditions? Do you agree to terms and conditions?
https://grassecon.org/pages/terms-and-conditions

View File

@ -1,5 +1,5 @@
MOUT yes 0 MOUT yes 1
MOUT no 1 MOUT no 2
HALT HALT
INCMP create_pin 0 INCMP create_pin 1
INCMP quit * INCMP quit *

View File

@ -1 +1,2 @@
Kwa kutumia hii huduma umekubali sheria na masharti? Kwa kutumia hii huduma umekubali sheria na masharti?
https://grassecon.org/pages/terms-and-conditions

View File

@ -4,5 +4,8 @@ LOAD reset_incorrect 6
CATCH incorrect_pin flag_incorrect_pin 1 CATCH incorrect_pin flag_incorrect_pin 1
CATCH pin_entry flag_account_authorized 0 CATCH pin_entry flag_account_authorized 0
MOUT back 0 MOUT back 0
MOUT quit 9
HALT HALT
INCMP _ 0 INCMP _ 0
INCMP quit 9
INCMP . *