Compare commits

..

83 Commits

Author SHA1 Message Date
Carlosokumu
fc85bd7eed return an event channel for the test engine 2024-10-09 14:06:16 +03:00
Carlosokumu
5869324c16 remove unused variable 2024-10-09 14:03:36 +03:00
Carlosokumu
1d67fb694b use regex for vm output and expected content 2024-10-09 12:18:56 +03:00
Carlosokumu
3d409a7a59 remove blank line 2024-10-08 22:44:07 +03:00
Carlosokumu
22047ba703 update account service initialization based on the tag 2024-10-08 22:43:11 +03:00
Carlosokumu
8751c4f5bd use just one driver for the tests 2024-10-08 22:19:48 +03:00
Carlosokumu
fc935535f9 remove deprecated code 2024-10-08 22:19:21 +03:00
Carlosokumu
ef4846b11f move online,offline and test engine into the testutil package 2024-10-08 22:18:44 +03:00
Carlosokumu
827b99e48d Revert "add dummy vouchers list"
This reverts commit 188cb573dd.
2024-10-07 20:36:02 +03:00
Carlosokumu
e09c1e1e98 Revert "add dummy vouchers list"
This reverts commit 188cb573dd.
2024-10-07 20:35:18 +03:00
Carlosokumu
87c6029199 Revert "add dummy vouchers list"
This reverts commit 188cb573dd.
2024-10-07 20:33:02 +03:00
Carlosokumu
992dd0bd54 factor out setup test 2024-10-07 19:05:36 +03:00
Carlosokumu
4cd7c742ef factor out tested groups 2024-10-07 18:36:34 +03:00
Carlosokumu
0d18eec39f remove debug message 2024-10-07 16:55:07 +03:00
Carlosokumu
bb009fa1e2 add group test cases 2024-10-07 16:43:13 +03:00
Carlosokumu
2ffe749c9d add driver to group tests 2024-10-07 16:42:48 +03:00
Carlosokumu
0a92444ae8 add group tests 2024-10-07 16:41:45 +03:00
Carlosokumu
2703e6d1df add tags to toggle between online and offline tests 2024-10-05 21:47:17 +03:00
Carlosokumu
86ba03b375 update tests 2024-10-04 17:04:33 +03:00
Carlosokumu
f91480f3de pass account service as a param 2024-10-04 16:58:47 +03:00
Carlosokumu
79a127cea9 pass account service as a parameter 2024-10-04 16:58:18 +03:00
Carlosokumu
6178425355 create a mocked account service 2024-10-04 16:57:25 +03:00
Carlosokumu
09bf266644 pass account service 2024-10-04 16:56:23 +03:00
Carlosokumu
bf1df95af1 pass account service implementation 2024-10-04 16:53:52 +03:00
Carlosokumu
9647d29bd7 resolve failing test 2024-10-04 11:17:19 +03:00
Carlosokumu
64cc9bc752 Merge remote-tracking branch 'refs/remotes/origin/menu-traversals' into menu-traversals 2024-10-04 11:13:58 +03:00
Carlosokumu
ddfd9d4e89 fix failing test 2024-10-04 11:11:41 +03:00
alfred-mk
b7d93c2249 use a UUID for the sessionId 2024-10-03 17:47:48 +03:00
Carlosokumu
a1201e2525 Merge remote-tracking branch 'remotes/origin/master' into menu-traversals 2024-10-03 17:13:40 +03:00
alfred-mk
d6cd2766df add a Random SessionID for each test run 2024-10-03 16:54:30 +03:00
Carlosokumu
e2316b38a8 reset account authorized and perform clean quit on view profile test 2024-10-03 14:55:25 +03:00
alfred-mk
d94037d499 combine the invalid PIN case and remove unused tests 2024-10-03 14:07:36 +03:00
Carlosokumu
0f2ec2f2bd reset authorized flag,update test data file 2024-10-03 13:47:31 +03:00
alfred-mk
089691db4c remove combined test 2024-10-03 12:51:46 +03:00
alfred-mk
1b8c8a12c0 combine the invalid PIN case 2024-10-03 12:51:03 +03:00
alfred-mk
c965d3083d renamed sym to symbol to avoid confusion 2024-10-03 11:53:46 +03:00
alfred-mk
59aa4eae77 removed unused code 2024-10-03 11:45:56 +03:00
alfred-mk
587cfb5a36 use the pp.csv from the scriptDir 2024-10-03 11:45:37 +03:00
alfred-mk
d94758c32c moved the test to test_engine folder 2024-10-03 11:44:34 +03:00
alfred-mk
0fa4a81826 updated the TestAuthorize 2024-10-03 11:42:41 +03:00
alfred-mk
73013f53bb added engine Finish 2024-10-02 15:52:55 +03:00
alfred-mk
986f3979fb use a dynamic public_key 2024-10-02 15:50:52 +03:00
Carlosokumu
96ec3919b2 update test data file 2024-10-02 14:39:48 +03:00
Carlosokumu
079529ebac Merge remote-tracking branch 'refs/remotes/origin/menu-traversals' into menu-traversals 2024-10-02 14:32:08 +03:00
alfred-mk
94bf4ffaa6 combine some test cases and rename the account creation test 2024-10-02 14:28:21 +03:00
alfred-mk
24380e1449 update the templates 2024-10-02 14:25:29 +03:00
Carlosokumu
7f6405d356 Merge remote-tracking branch 'refs/remotes/origin/menu-traversals' into menu-traversals 2024-10-02 13:54:00 +03:00
alfred-mk
2a1e48e5e8 update the test_data with expected output 2024-10-02 13:31:47 +03:00
alfred-mk
6b8f62fbc6 code cleanup - removed unused new lines 2024-10-02 13:26:19 +03:00
alfred-mk
948a9d3a93 allow one to go back during the change PIN 2024-10-02 13:24:03 +03:00
Carlosokumu
01476d5408 update tests 2024-10-02 13:22:00 +03:00
Carlosokumu
ab02a8882a update tests 2024-10-02 12:10:32 +03:00
Carlosokumu
74fa2c3d65 update tests 2024-10-02 11:23:34 +03:00
Carlosokumu
204ce3ce56 update tests 2024-10-02 10:18:04 +03:00
Carlosokumu
abbf22086f update tests 2024-10-02 09:55:56 +03:00
Carlosokumu
6eec9571c7 update test 2024-10-02 09:01:27 +03:00
Carlosokumu
7c0ae80bbf Merge remote-tracking branch 'refs/remotes/origin/menu-traversals' into menu-traversals 2024-10-01 23:23:22 +03:00
Carlosokumu
1ff2c3602f update tests 2024-10-01 23:21:29 +03:00
alfred-mk
a0ef57ca15 add Change PIN test 2024-10-01 21:43:52 +03:00
Carlosokumu
dddb7d7309 merge 2024-10-01 16:54:44 +03:00
Carlosokumu
c5b2348f5e add tests 2024-10-01 16:54:16 +03:00
alfred-mk
48c608ad4d add Help and Quit menu tests 2024-10-01 15:36:59 +03:00
alfred-mk
14218ffb88 add a dynamic value 2024-10-01 14:50:45 +03:00
alfred-mk
3e74c1d939 test with all invalid inputs 2024-10-01 13:26:47 +03:00
alfred-mk
285002738f added send_with_invalid_recipient test 2024-10-01 13:15:05 +03:00
lash
f267aa2b41 Delete connstr in threadgdbm global channel map on close 2024-10-01 00:18:54 +01:00
Carlosokumu
1b2c6933e1 clean up test 2024-09-30 22:27:37 +03:00
Carlosokumu
94d1271dbb update 2024-09-30 22:22:00 +03:00
alfred-mk
f3f276c549 check the status of cont 2024-09-30 22:12:49 +03:00
alfred-mk
9eb35ec52d added en.Finish 2024-09-30 22:00:28 +03:00
alfred-mk
be165e5033 added tests and test cases 2024-09-30 19:55:13 +03:00
Carlosokumu
e7a3de526c remove log 2024-09-30 18:09:53 +03:00
Carlosokumu
cf5b4ec744 call finish on engine 2024-09-30 18:05:20 +03:00
Carlosokumu
80ce141b80 reference correct test data file 2024-09-30 13:31:49 +03:00
Carlosokumu
170d075545 update tests 2024-09-30 13:25:38 +03:00
Carlosokumu
4aad23ab30 update tests 2024-09-30 13:21:17 +03:00
Carlosokumu
7d1db50294 update 2024-09-27 16:26:00 +03:00
Carlosokumu
afe98b8695 setup traversal tests 2024-09-27 15:02:29 +03:00
Carlosokumu
ece80b31f6 start menu traversal tests setup 2024-09-26 21:27:54 +03:00
alfred-mk
221db4e998 return a numbered list of vouchers 2024-09-25 16:06:06 +03:00
alfred-mk
0e376e0d9e include back and quit 2024-09-25 16:03:08 +03:00
Carlosokumu
7aa44caea2 add voucher nodes 2024-09-25 15:57:23 +03:00
Carlosokumu
188cb573dd add dummy vouchers list 2024-09-25 13:27:13 +03:00
64 changed files with 1673 additions and 948 deletions

View File

@@ -17,6 +17,7 @@ import (
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/handlers/server"
httpserver "git.grassecon.net/urdt/ussd/internal/http"
"git.grassecon.net/urdt/ussd/internal/storage"
)
@@ -87,7 +88,7 @@ func main() {
cfg := engine.Config{
Root: "root",
OutputSize: uint32(size),
FlagCount: uint32(128),
FlagCount: uint32(16),
}
if engineDebug {
@@ -127,7 +128,8 @@ func main() {
os.Exit(1)
}
hl, err := lhs.GetHandler()
accountService := server.AccountService{}
hl, err := lhs.GetHandler(&accountService)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)

View File

@@ -14,6 +14,7 @@ import (
"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"
)
@@ -60,7 +61,7 @@ func main() {
cfg := engine.Config{
Root: "root",
OutputSize: uint32(size),
FlagCount: uint32(128),
FlagCount: uint32(16),
}
if engineDebug {
@@ -95,7 +96,9 @@ func main() {
lhs, err := handlers.NewLocalHandlerService(pfp, true, dbResource, cfg, rs)
lhs.SetDataStore(&userdataStore)
hl, err := lhs.GetHandler()
accountService := server.AccountService{}
hl, err := lhs.GetHandler(&accountService)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)

View File

@@ -16,6 +16,7 @@ import (
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/handlers/server"
httpserver "git.grassecon.net/urdt/ussd/internal/http"
"git.grassecon.net/urdt/ussd/internal/storage"
)
@@ -48,7 +49,7 @@ func main() {
cfg := engine.Config{
Root: "root",
OutputSize: uint32(size),
FlagCount: uint32(128),
FlagCount: uint32(16),
}
if engineDebug {
@@ -88,7 +89,8 @@ func main() {
os.Exit(1)
}
hl, err := lhs.GetHandler()
accountService := server.AccountService{}
hl, err := lhs.GetHandler(&accountService)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)

View File

@@ -11,6 +11,7 @@ import (
"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"
)
@@ -40,7 +41,7 @@ func main() {
Root: "root",
SessionId: sessionId,
OutputSize: uint32(size),
FlagCount: uint32(128),
FlagCount: uint32(16),
}
resourceDir := scriptDir
@@ -84,8 +85,8 @@ func main() {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
hl, err := lhs.GetHandler()
accountService := server.AccountService{}
hl, err := lhs.GetHandler(&accountService)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)

View File

@@ -1,7 +1,10 @@
package config
const (
CreateAccountURL = "http://localhost:5003/api/v2/account/create"
BalanceURL = "https://custodial.sarafu.africa/api/account/status/"
TrackURL = "http://localhost:5003/api/v2/account/status"
CreateAccountURL = "https://custodial.sarafu.africa/api/account/create"
TrackStatusURL = "https://custodial.sarafu.africa/api/track/"
BalanceURL = "https://custodial.sarafu.africa/api/account/status/"
)

111
driver/testdriver.go Normal file
View File

@@ -0,0 +1,111 @@
package driver
import (
"encoding/json"
"log"
"os"
"regexp"
)
type Step struct {
Input string `json:"input"`
ExpectedContent string `json:"expectedContent"`
}
func (s *Step) MatchesExpectedContent(content []byte) (bool, error) {
pattern := `.*\?.*|.*`
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
}
// 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) {
re, err := regexp.Compile(s.ExpectedContent)
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
}

1
go.mod
View File

@@ -15,6 +15,7 @@ require (
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
github.com/gofrs/uuid v4.4.0+incompatible
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect

2
go.sum
View File

@@ -12,6 +12,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo=
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=

View File

@@ -6,6 +6,7 @@ import (
"git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/internal/handlers/server"
"git.grassecon.net/urdt/ussd/internal/handlers/ussd"
)
@@ -52,8 +53,8 @@ func (ls *LocalHandlerService) SetDataStore(db *db.Db) {
ls.UserdataStore = db
}
func (ls *LocalHandlerService) GetHandler() (*ussd.Handlers, error) {
ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore)
func (ls *LocalHandlerService) GetHandler(accountService server.AccountServiceInterface) (*ussd.Handlers, error) {
ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore, accountService)
if err != nil {
return nil, err
}
@@ -82,6 +83,7 @@ func (ls *LocalHandlerService) GetHandler() (*ussd.Handlers, error) {
ls.DbRs.AddLocalFunc("save_location", ussdHandlers.SaveLocation)
ls.DbRs.AddLocalFunc("save_yob", ussdHandlers.SaveYob)
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_allow_update", ussdHandlers.ResetAllowUpdate)
ls.DbRs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo)
@@ -92,7 +94,6 @@ func (ls *LocalHandlerService) GetHandler() (*ussd.Handlers, error) {
ls.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin)
ls.DbRs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange)
ls.DbRs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp)
ls.DbRs.AddLocalFunc("fetch_custodial_balances", ussdHandlers.FetchCustodialBalances)
return ussdHandlers, nil
}

View File

@@ -2,7 +2,6 @@ package server
import (
"encoding/json"
"fmt"
"io"
"net/http"
@@ -10,21 +9,20 @@ import (
"git.grassecon.net/urdt/ussd/internal/models"
)
var apiResponse struct {
Ok bool `json:"ok"`
Description string `json:"description"`
}
type AccountServiceInterface interface {
CheckBalance(publicKey string) (*models.BalanceResponse, error)
CreateAccount() (*OKResponse, *ErrResponse)
CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error)
TrackAccountStatus(publicKey string) (*OKResponse, *ErrResponse)
CheckBalance(publicKey string) (string, error)
CreateAccount() (*models.AccountResponse, error)
CheckAccountStatus(trackingId string) (string, error)
}
type AccountService struct {
}
type MockAccountService struct {
}
// CheckAccountStatus retrieves the status of an account transaction based on the provided tracking ID.
//
// Parameters:
// - 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
@@ -33,100 +31,54 @@ type AccountService struct {
// Returns:
// - string: The status of the transaction as a string. If there is an error during the request or processing, this will be an empty string.
// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
// If no error occurs, this will be nil
func (as *AccountService) CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error) {
resp, err := http.Get(config.BalanceURL + trackingId)
// If no error occurs, this will be nil.
func (as *AccountService) CheckAccountStatus(trackingId string) (string, error) {
resp, err := http.Get(config.TrackStatusURL + trackingId)
if err != nil {
return nil, err
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
return "", err
}
var trackResp models.TrackStatusResponse
err = json.Unmarshal(body, &trackResp)
if err != nil {
return nil, err
}
return &trackResp, nil
}
func (as *AccountService) TrackAccountStatus(publicKey string) (*OKResponse, *ErrResponse) {
var errResponse ErrResponse
var okResponse OKResponse
var err error
// Construct the URL with the path parameter
url := fmt.Sprintf("%s/%s", config.TrackURL, publicKey)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
errResponse.Description = err.Error()
return nil, &errResponse
}
// Set headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-GE-KEY", "xd")
// Send the request
resp, err := http.DefaultClient.Do(req)
if err != nil {
errResponse.Description = err.Error()
return nil, &errResponse
}
defer resp.Body.Close()
// Read the response body
body, err := io.ReadAll(resp.Body)
if err != nil {
errResponse.Description = err.Error()
return nil, &errResponse
return "", err
}
// Step 2: Unmarshal into the generic struct
err = json.Unmarshal([]byte(body), &apiResponse)
if err != nil {
errResponse.Description = err.Error()
return nil, &errResponse
}
if apiResponse.Ok {
err = json.Unmarshal([]byte(body), &okResponse)
if err != nil {
errResponse.Description = err.Error()
return nil, &errResponse
}
return &okResponse, nil
} else {
err := json.Unmarshal([]byte(body), &errResponse)
if err != nil {
errResponse.Description = err.Error()
return nil, &errResponse
}
return nil, &errResponse
}
status := trackResp.Result.Transaction.Status
return status, nil
}
// CheckBalance retrieves the balance for a given public key from the custodial balance API endpoint.
// Parameters:
// - publicKey: The public key associated with the account whose balance needs to be checked.
func (as *AccountService) CheckBalance(publicKey string) (*models.BalanceResponse, error) {
func (as *AccountService) CheckBalance(publicKey string) (string, error) {
resp, err := http.Get(config.BalanceURL + publicKey)
if err != nil {
return nil, err
return "0.0", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
return "0.0", err
}
var balanceResp models.BalanceResponse
err = json.Unmarshal(body, &balanceResp)
if err != nil {
return nil, err
return "0.0", err
}
return &balanceResp, nil
balance := balanceResp.Result.Balance
return balance, nil
}
// CreateAccount creates a new account in the custodial system.
@@ -135,50 +87,58 @@ func (as *AccountService) CheckBalance(publicKey string) (*models.BalanceRespons
// 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.
// If no error occurs, this will be nil.
func (as *AccountService) CreateAccount() (*OKResponse, *ErrResponse) {
var errResponse ErrResponse
var okResponse OKResponse
var err error
// Create a new request
req, err := http.NewRequest("POST", config.CreateAccountURL, nil)
func (as *AccountService) CreateAccount() (*models.AccountResponse, error) {
resp, err := http.Post(config.CreateAccountURL, "application/json", nil)
if err != nil {
errResponse.Description = err.Error()
return nil, &errResponse
}
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, &errResponse
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
errResponse.Description = err.Error()
return nil, &errResponse
return nil, err
}
err = json.Unmarshal([]byte(body), &apiResponse)
var accountResp models.AccountResponse
err = json.Unmarshal(body, &accountResp)
if err != nil {
return nil, &errResponse
}
if apiResponse.Ok {
err = json.Unmarshal([]byte(body), &okResponse)
if err != nil {
errResponse.Description = err.Error()
return nil, &errResponse
}
return &okResponse, nil
} else {
err := json.Unmarshal([]byte(body), &errResponse)
if err != nil {
errResponse.Description = err.Error()
return nil, &errResponse
}
return nil, &errResponse
return nil, err
}
return &accountResp, nil
}
func (mas *MockAccountService) CreateAccount() (*models.AccountResponse, error) {
return &models.AccountResponse{
Ok: true,
Result: struct {
CustodialId json.Number `json:"custodialId"`
PublicKey string `json:"publicKey"`
TrackingId string `json:"trackingId"`
}{
CustodialId: json.Number("182"),
PublicKey: "0x48ADca309b5085852207FAaf2816eD72B52F527C",
TrackingId: "28ebe84d-b925-472c-87ae-bbdfa1fb97be",
},
}, nil
}
func (mas *MockAccountService) CheckBalance(publicKey string) (string, 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.Result.Balance, nil
}
func (mas *MockAccountService) CheckAccountStatus(trackingId string) (string, error) {
return "SUCCESS", nil
}

View File

@@ -1,60 +0,0 @@
package server
type (
OKResponse struct {
Ok bool `json:"ok"`
Description string `json:"description"`
Result map[string]any `json:"result"`
}
ErrResponse struct {
Ok bool `json:"ok"`
Description string `json:"description"`
ErrCode string `json:"errorCode"`
}
TransferRequest struct {
From string `json:"from" validate:"required,eth_addr_checksum"`
To string `json:"to" validate:"required,eth_addr_checksum"`
TokenAddress string `json:"tokenAddress" validate:"required,eth_addr_checksum"`
Amount string `json:"amount" validate:"required,number,gt=0"`
}
PoolSwapRequest struct {
From string `json:"from" validate:"required,eth_addr_checksum"`
FromTokenAddress string `json:"fromTokenAddress" validate:"required,eth_addr_checksum"`
ToTokenAddress string `json:"toTokenAddress" validate:"required,eth_addr_checksum"`
PoolAddress string `json:"poolAddress" validate:"required,eth_addr_checksum"`
Amount string `json:"amount" validate:"required,number,gt=0"`
}
PoolDepositRequest struct {
From string `json:"from" validate:"required,eth_addr_checksum"`
TokenAddress string `json:"tokenAddress" validate:"required,eth_addr_checksum"`
PoolAddress string `json:"poolAddress" validate:"required,eth_addr_checksum"`
Amount string `json:"amount" validate:"required,number,gt=0"`
}
AccountAddressParam struct {
Address string `param:"address" validate:"required,eth_addr_checksum"`
}
TrackingIDParam struct {
TrackingID string `param:"trackingId" validate:"required,uuid"`
}
OTXByAccountRequest struct {
Address string `param:"address" validate:"required,eth_addr_checksum"`
PerPage int `query:"perPage" validate:"required,number,gt=0"`
Cursor int `query:"cursor" validate:"number"`
Next bool `query:"next"`
}
)
const (
ErrCodeInternalServerError = "E01"
ErrCodeInvalidJSON = "E02"
ErrCodeInvalidAPIKey = "E03"
ErrCodeValidationFailed = "E04"
ErrCodeAccountNotExists = "E05"
)

View File

@@ -27,8 +27,6 @@ var (
logg = logging.NewVanilla().WithDomain("ussdmenuhandler")
scriptDir = path.Join("services", "registration")
translationDir = path.Join(scriptDir, "locale")
okResponse *server.OKResponse
errResponse *server.ErrResponse
)
// FlagManager handles centralized flag management
@@ -63,7 +61,7 @@ type Handlers struct {
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 {
return nil, fmt.Errorf("cannot create handler with nil userdata store")
}
@@ -73,7 +71,7 @@ func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db) (*Handlers, erro
h := &Handlers{
userdataStore: userDb,
flagManager: appFlags,
accountService: &server.AccountService{},
accountService: accountService,
}
return h, nil
}
@@ -138,16 +136,11 @@ 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 {
okResponse, errResponse := h.accountService.CreateAccount()
if errResponse != nil {
return nil
}
trackingId := okResponse.Result["trackingId"].(string)
publicKey := okResponse.Result["publicKey"].(string)
accountResp, err := h.accountService.CreateAccount()
data := map[utils.DataTyp]string{
utils.DATA_TRACKING_ID: trackingId,
utils.DATA_PUBLIC_KEY: publicKey,
utils.DATA_TRACKING_ID: accountResp.Result.TrackingId,
utils.DATA_PUBLIC_KEY: accountResp.Result.PublicKey,
utils.DATA_CUSTODIAL_ID: accountResp.Result.CustodialId.String(),
}
for key, value := range data {
@@ -159,7 +152,7 @@ func (h *Handlers) createAccountNoExist(ctx context.Context, sessionId string, r
}
flag_account_created, _ := h.flagManager.GetFlag("flag_account_created")
res.FlagSet = append(res.FlagSet, flag_account_created)
return nil
return err
}
@@ -198,6 +191,7 @@ func (h *Handlers) SavePin(ctx context.Context, sym string, input []byte) (resou
}
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
accountPIN := string(input)
// Validate that the PIN is a 4-digit number
if !isValidPIN(accountPIN) {
@@ -296,6 +290,8 @@ func (h *Handlers) VerifyPin(ctx context.Context, sym string, input []byte) (res
if !ok {
return res, fmt.Errorf("missing session")
}
//AccountPin, _ := utils.ReadEntry(ctx, h.userdataStore, sessionId, utils.DATA_ACCOUNT_PIN)
store := h.userdataStore
AccountPin, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN)
if err != nil {
@@ -374,6 +370,7 @@ func (h *Handlers) SaveYob(ctx context.Context, sym string, input []byte) (resou
if !ok {
return res, fmt.Errorf("missing session")
}
if len(input) == 4 {
yob := string(input)
store := h.userdataStore
@@ -416,6 +413,7 @@ func (h *Handlers) SaveGender(ctx context.Context, sym string, input []byte) (re
if !ok {
return res, fmt.Errorf("missing session")
}
gender := strings.Split(symbol, "_")[1]
store := h.userdataStore
err = store.WriteEntry(ctx, sessionId, utils.DATA_GENDER, []byte(gender))
@@ -434,6 +432,7 @@ func (h *Handlers) SaveOfferings(ctx context.Context, sym string, input []byte)
if !ok {
return res, fmt.Errorf("missing session")
}
if len(input) > 0 {
offerings := string(input)
store := h.userdataStore
@@ -459,6 +458,7 @@ func (h *Handlers) ResetAllowUpdate(ctx context.Context, sym string, input []byt
// 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) {
var res resource.Result
flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
res.FlagReset = append(res.FlagReset, flag_account_authorized)
@@ -468,10 +468,12 @@ func (h *Handlers) ResetAccountAuthorized(ctx context.Context, sym string, input
// CheckIdentifier retrieves the PublicKey from the JSON data file.
func (h *Handlers) CheckIdentifier(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, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
@@ -485,10 +487,12 @@ 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) {
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")
flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update")
@@ -498,7 +502,7 @@ func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (res
if err != nil {
return res, err
}
if len(input) == 4 {
if len(input) > 1 {
if bytes.Equal(input, AccountPin) {
if h.st.MatchFlag(flag_account_authorized, false) {
res.FlagReset = append(res.FlagReset, flag_incorrect_pin)
@@ -521,7 +525,9 @@ func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (res
// 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) {
var res resource.Result
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
res.FlagReset = append(res.FlagReset, flag_incorrect_pin)
return res, nil
}
@@ -533,27 +539,29 @@ func (h *Handlers) CheckAccountStatus(ctx context.Context, sym string, input []b
flag_account_success, _ := h.flagManager.GetFlag("flag_account_success")
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)
if !ok {
return res, fmt.Errorf("missing session")
}
store := h.userdataStore
publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
trackingId, err := store.ReadEntry(ctx, sessionId, utils.DATA_TRACKING_ID)
if err != nil {
return res, err
}
okResponse, errResponse = h.accountService.TrackAccountStatus(string(publicKey))
if errResponse != nil {
status, err := h.accountService.CheckAccountStatus(string(trackingId))
if err != nil {
fmt.Println("Error checking account status:", err)
return res, err
}
res.FlagReset = append(res.FlagReset, flag_api_error)
isActive := okResponse.Result["active"].(bool)
if !ok {
return res, err
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACCOUNT_STATUS, []byte(status))
if err != nil {
return res, nil
}
if isActive {
if status == "SUCCESS" {
res.FlagSet = append(res.FlagSet, flag_account_success)
res.FlagReset = append(res.FlagReset, flag_account_pending)
} else {
@@ -633,8 +641,6 @@ func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) (
var res resource.Result
var err error
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
@@ -646,60 +652,15 @@ func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) (
return res, err
}
balanceResponse, err := h.accountService.CheckBalance(string(publicKey))
balance, err := h.accountService.CheckBalance(string(publicKey))
if err != nil {
return res, nil
}
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
res.Content = balance
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
publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
if err != nil {
return res, err
}
balanceResponse, err := h.accountService.CheckBalance(string(publicKey))
if err != nil {
return res, nil
}
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
}
// ValidateRecipient validates that the given input is a valid phone number.
func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
@@ -796,11 +757,10 @@ func (h *Handlers) MaxAmount(ctx context.Context, sym string, input []byte) (res
store := h.userdataStore
publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
balanceResp, err := h.accountService.CheckBalance(string(publicKey))
balance, err := h.accountService.CheckBalance(string(publicKey))
if err != nil {
return res, nil
}
balance := balanceResp.Result.Balance
res.Content = balance
@@ -819,25 +779,18 @@ func (h *Handlers) ValidateAmount(ctx context.Context, sym string, input []byte)
}
flag_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount")
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
store := h.userdataStore
publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
amountStr := string(input)
balanceRes, err := h.accountService.CheckBalance(string(publicKey))
balanceStr := balanceRes.Result.Balance
balanceStr, err := h.accountService.CheckBalance(string(publicKey))
if !balanceRes.Ok {
res.FlagSet = append(res.FlagSet, flag_api_error)
return res, nil
}
if err != nil {
return res, err
}
res.Content = balanceStr
res.FlagReset = append(res.FlagReset, flag_api_error)
// Parse the balance
balanceParts := strings.Split(balanceStr, " ")
@@ -929,6 +882,36 @@ func (h *Handlers) GetAmount(ctx context.Context, sym string, input []byte) (res
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 {
return res, err
}
balance, err := h.accountService.CheckBalance(string(publicKey))
if err != nil {
return res, nil
}
res.Content = l.Get("Your account balance is %s", balance)
res.FlagReset = append(res.FlagReset, flag_account_authorized)
return res, nil
}
// InitiateTransaction returns a confirmation and resets the transaction data
// on the gdbm store.
func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input []byte) (resource.Result, error) {
@@ -962,23 +945,16 @@ func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input []
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) {
var res resource.Result
var defaultValue string
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
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"
}
// Default value when an entry is not found
defaultValue := "Not Provided"
// Helper function to handle nil byte slices and convert them to string
getEntryOrDefault := func(entry []byte, err error) string {
@@ -1015,23 +991,12 @@ func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte)
return res, fmt.Errorf("invalid year of birth: %v", err)
}
}
switch language.Code {
case "eng":
res.Content = fmt.Sprintf(
"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
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,
)
}
// Format the result
res.Content = fmt.Sprintf(
"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
name, gender, age, location, offerings,
)
return res, nil
}

View File

@@ -7,15 +7,11 @@ import (
"log"
"path"
"testing"
"time"
"git.defalsify.org/vise.git/asm"
"git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/lang"
"git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/resource"
"git.defalsify.org/vise.git/state"
"git.grassecon.net/urdt/ussd/internal/handlers/server"
"git.grassecon.net/urdt/ussd/internal/mocks"
"git.grassecon.net/urdt/ussd/internal/models"
"git.grassecon.net/urdt/ussd/internal/utils"
@@ -29,125 +25,75 @@ var (
flagsPath = path.Join(baseDir, "services", "registration", "pp.csv")
)
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"`
}
func TestNewHandlers(t *testing.T) {
fm, err := NewFlagManager(flagsPath)
if err != nil {
t.Logf(err.Error())
}
t.Run("Valid UserDataStore", func(t *testing.T) {
mockStore := &mocks.MockUserDataStore{}
handlers, err := NewHandlers(fm.parser, mockStore)
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)
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
flag_account_created, err := fm.GetFlag("flag_account_created")
mockDataStore := new(mocks.MockUserDataStore)
mockCreateAccountService := new(mocks.MockAccountService)
expectedResult := resource.Result{}
accountCreatedFlag, err := fm.GetFlag("flag_account_created")
if err != nil {
t.Logf(err.Error())
}
expectedResult.FlagSet = append(expectedResult.FlagSet, accountCreatedFlag)
// Define session ID and mock data
sessionId := "session123"
notFoundErr := db.ErrNotFound{}
typ := utils.DATA_ACCOUNT_CREATED
fakeError := db.ErrNotFound{}
// Create context with session ID
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
tests := []struct {
name string
serverResponse *server.OKResponse
expectedResult resource.Result
}{
{
name: "Test account creation success",
serverResponse: &server.OKResponse{
Ok: true,
Description: "Account creation successed",
Result: map[string]any{
"trackingId": "1234567890",
"publicKey": "1235QERYU",
},
},
expectedResult: resource.Result{
FlagSet: []uint32{flag_account_created},
},
// Define expected interactions with the mock
mockDataStore.On("ReadEntry", ctx, sessionId, typ).Return([]byte("123"), fakeError)
expectedAccountResp := &models.AccountResponse{
Ok: true,
Result: struct {
CustodialId json.Number `json:"custodialId"`
PublicKey string `json:"publicKey"`
TrackingId string `json:"trackingId"`
}{
CustodialId: "12",
PublicKey: "0x8E0XSCSVA",
TrackingId: "d95a7e83-196c-4fd0-866fSGAGA",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockDataStore := new(mocks.MockUserDataStore)
mockCreateAccountService := new(mocks.MockAccountService)
// Create a Handlers instance with the mock data store
h := &Handlers{
userdataStore: mockDataStore,
accountService: mockCreateAccountService,
flagManager: fm.parser,
}
data := map[utils.DataTyp]string{
utils.DATA_TRACKING_ID: tt.serverResponse.Result["trackingId"].(string),
utils.DATA_PUBLIC_KEY: tt.serverResponse.Result["publicKey"].(string),
}
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)
}
// 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)
})
mockCreateAccountService.On("CreateAccount").Return(expectedAccountResp, nil)
data := map[utils.DataTyp]string{
utils.DATA_TRACKING_ID: expectedAccountResp.Result.TrackingId,
utils.DATA_PUBLIC_KEY: expectedAccountResp.Result.PublicKey,
utils.DATA_CUSTODIAL_ID: expectedAccountResp.Result.CustodialId.String(),
}
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) {
@@ -496,10 +442,7 @@ func TestMaxAmount(t *testing.T) {
sessionId := "session123"
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
publicKey := "0xcasgatweksalw1018221"
expectedBalance := &models.BalanceResponse{
Ok: true,
}
expectedBalance := "0.003CELO"
// Set up the expected behavior of the mock
mockStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil)
@@ -515,7 +458,7 @@ func TestMaxAmount(t *testing.T) {
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.Result.Balance, res.Content)
assert.Equal(t, expectedBalance, res.Content)
}
@@ -594,10 +537,12 @@ func TestGetRecipient(t *testing.T) {
func TestGetFlag(t *testing.T) {
fm, err := NewFlagManager(flagsPath)
expectedFlag := uint32(9)
if err != nil {
t.Logf(err.Error())
}
flag, err := fm.GetFlag("flag_account_created")
if err != nil {
t.Logf(err.Error())
}
@@ -891,7 +836,10 @@ func TestAuthorize(t *testing.T) {
{
name: "Test with pin that is not a 4 digit",
input: []byte("1235aqds"),
expectedResult: resource.Result{},
expectedResult: resource.Result{
FlagReset: []uint32{flag_account_authorized},
FlagSet: []uint32{flag_incorrect_pin},
},
},
}
@@ -1067,115 +1015,53 @@ func TestVerifyPin(t *testing.T) {
func TestCheckAccountStatus(t *testing.T) {
fm, err := NewFlagManager(flagsPath)
if err != nil {
t.Logf(err.Error())
}
mockDataStore := new(mocks.MockUserDataStore)
mockCreateAccountService := new(mocks.MockAccountService)
sessionId := "session123"
flag_account_success, _ := fm.GetFlag("flag_account_success")
flag_account_pending, _ := fm.GetFlag("flag_account_pending")
flag_api_error, _ := fm.GetFlag("flag_api_call_error")
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
h := &Handlers{
userdataStore: mockDataStore,
accountService: mockCreateAccountService,
flagManager: fm.parser,
}
tests := []struct {
name string
input []byte
serverResponse *server.OKResponse
response *models.TrackStatusResponse
status string
expectedResult resource.Result
}{
{
name: "Test when account is on the Sarafu network",
input: []byte("TrackingId1234"),
serverResponse: &server.OKResponse{
Ok: true,
Description: "Account creation successed",
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: Transaction{
CreatedAt: time.Now(),
Status: "SUCCESS",
TransferValue: json.Number("0.5"),
TxHash: "0x123abc456def",
TxType: "transfer",
},
},
},
name: "Test when account status is Success",
input: []byte("TrackingId1234"),
status: "SUCCESS",
expectedResult: resource.Result{
FlagSet: []uint32{flag_account_success},
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: Transaction{
CreatedAt: time.Now(),
Status: "SUCCESS",
TransferValue: json.Number("0.5"),
TxHash: "0x123abc456def",
TxType: "transfer",
},
},
},
serverResponse: &server.OKResponse{
Ok: true,
Description: "Account creation successed",
Result: map[string]any{
"active": false,
},
},
expectedResult: resource.Result{
FlagSet: []uint32{flag_account_pending},
FlagReset: []uint32{flag_api_error, flag_account_success},
FlagReset: []uint32{flag_account_pending},
},
},
}
typ := utils.DATA_TRACKING_ID
for _, tt := range tests {
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
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return(tt.input, nil)
mockDataStore.On("ReadEntry", ctx, sessionId, typ).Return(tt.input, nil)
mockCreateAccountService.On("CheckAccountStatus", string(tt.input)).Return(tt.response, 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()
mockCreateAccountService.On("CheckAccountStatus", string(tt.input)).Return(tt.status, nil)
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACCOUNT_STATUS, []byte(tt.status)).Return(nil)
// Call the method under test
res, _ := h.CheckAccountStatus(ctx, "check_account_status", tt.input)
res, _ := h.CheckAccountStatus(ctx, "check_status", tt.input)
// Assert that no errors occurred
assert.NoError(t, err)
@@ -1475,14 +1361,14 @@ func TestIsValidPIN(t *testing.T) {
}
}
func TestValidateAmount(t *testing.T) {
func TestQuitWithBalance(t *testing.T) {
fm, err := NewFlagManager(flagsPath)
if err != nil {
t.Logf(err.Error())
}
flag_invalid_amount, _ := fm.parser.GetFlag("flag_invalid_amount")
flag_api_error, _ := fm.GetFlag("flag_api_call_error")
flag_account_authorized, _ := fm.parser.GetFlag("flag_account_authorized")
mockDataStore := new(mocks.MockUserDataStore)
mockCreateAccountService := new(mocks.MockAccountService)
@@ -1496,69 +1382,19 @@ func TestValidateAmount(t *testing.T) {
flagManager: fm.parser,
}
tests := []struct {
name string
input []byte
publicKey []byte
balanceResponse *models.BalanceResponse
expectedResult resource.Result
name string
input []byte
publicKey []byte
balance string
expectedResult resource.Result
}{
{
name: "Test with valid amount",
input: []byte("0.001"),
balanceResponse: &models.BalanceResponse{
Ok: true,
Result: struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
},
name: "Test quit with balance",
balance: "0.02CELO",
publicKey: []byte("0xrqeqrequuq"),
expectedResult: resource.Result{
Content: "0.001",
FlagReset: []uint32{flag_api_error},
},
},
{
name: "Test with amount larger than balance",
input: []byte("0.02"),
balanceResponse: &models.BalanceResponse{
Ok: true,
Result: struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
},
publicKey: []byte("0xrqeqrequuq"),
expectedResult: resource.Result{
FlagSet: []uint32{flag_invalid_amount},
FlagReset: []uint32{flag_api_error},
Content: "0.02",
},
},
{
name: "Test with invalid amount",
input: []byte("0.02ms"),
balanceResponse: &models.BalanceResponse{
Ok: true,
Result: struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
},
publicKey: []byte("0xrqeqrequuq"),
expectedResult: resource.Result{
FlagSet: []uint32{flag_invalid_amount},
FlagReset: []uint32{flag_api_error},
Content: "0.02ms",
FlagReset: []uint32{flag_account_authorized},
Content: fmt.Sprintf("Your account balance is %s", "0.02CELO"),
},
},
}
@@ -1567,7 +1403,86 @@ func TestValidateAmount(t *testing.T) {
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.balanceResponse, 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) {
fm, err := NewFlagManager(flagsPath)
if err != nil {
t.Logf(err.Error())
}
flag_invalid_amount, _ := fm.parser.GetFlag("flag_invalid_amount")
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 with valid amount",
input: []byte("0.001"),
balance: "0.003 CELO",
publicKey: []byte("0xrqeqrequuq"),
expectedResult: resource.Result{
Content: "0.001",
},
},
{
name: "Test with amount larger than balance",
input: []byte("0.02"),
balance: "0.003 CELO",
publicKey: []byte("0xrqeqrequuq"),
expectedResult: resource.Result{
FlagSet: []uint32{flag_invalid_amount},
Content: "0.02",
},
},
{
name: "Test with invalid amount",
input: []byte("0.02ms"),
balance: "0.003 CELO",
publicKey: []byte("0xrqeqrequuq"),
expectedResult: resource.Result{
FlagSet: []uint32{flag_invalid_amount},
Content: "0.02ms",
},
},
}
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)
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_AMOUNT, tt.input).Return(nil).Maybe()
// Call the method under test
@@ -1646,83 +1561,31 @@ func TestValidateRecipient(t *testing.T) {
func TestCheckBalance(t *testing.T) {
mockDataStore := new(mocks.MockUserDataStore)
sessionId := "session123"
publicKey := "0X13242618721"
fm, _ := NewFlagManager(flagsPath)
flag_api_error, _ := fm.GetFlag("flag_api_call_error")
balance := "0.003 CELO"
expectedResult := resource.Result{
Content: "0.003 CELO",
}
mockCreateAccountService := new(mocks.MockAccountService)
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
tests := []struct {
name string
balanceResonse *models.BalanceResponse
expectedResult resource.Result
}{
{
name: "Test when checking a balance 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 checking a balance 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{
Content: "0.003 CELO",
FlagReset: []uint32{flag_api_error},
},
},
h := &Handlers{
userdataStore: mockDataStore,
accountService: mockCreateAccountService,
//flagManager: fm.parser,
}
//mock call operations
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil)
mockCreateAccountService.On("CheckBalance", string(publicKey)).Return(balance, nil)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
res, _ := h.CheckBalance(ctx, "check_balance", []byte("123456"))
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.CheckBalance(ctx, "check_balance", []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")
})
}
assert.Equal(t, res, expectedResult, "Result should contain flag(s) that have been reset")
}
@@ -1732,50 +1595,23 @@ func TestGetProfile(t *testing.T) {
mockDataStore := new(mocks.MockUserDataStore)
mockCreateAccountService := new(mocks.MockAccountService)
mockState := state.NewState(16)
h := &Handlers{
userdataStore: mockDataStore,
accountService: mockCreateAccountService,
st: mockState,
}
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
tests := []struct {
name string
languageCode string
keys []utils.DataTyp
profileInfo []string
result resource.Result
name string
keys []utils.DataTyp
profileInfo []string
result resource.Result
}{
{
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},
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",
name: "Test with full profile information",
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"},
result: resource.Result{
Content: fmt.Sprintf(
"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
@@ -1786,14 +1622,9 @@ func TestGetProfile(t *testing.T) {
}
for _, tt := range tests {
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 {
mockDataStore.On("ReadEntry", ctx, sessionId, key).Return([]byte(tt.profileInfo[index]), nil).Maybe()
mockDataStore.On("ReadEntry", ctx, sessionId, key).Return([]byte(tt.profileInfo[index]), nil)
}
res, _ := h.GetProfileInfo(ctx, "get_profile_info", []byte(""))
// Assert that expectations were met
@@ -1950,85 +1781,3 @@ func TestConfirmPin(t *testing.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

@@ -1,7 +1,6 @@
package mocks
import (
"git.grassecon.net/urdt/ussd/internal/handlers/server"
"git.grassecon.net/urdt/ussd/internal/models"
"github.com/stretchr/testify/mock"
)
@@ -11,40 +10,17 @@ type MockAccountService struct {
mock.Mock
}
func (m *MockAccountService) CreateAccount() (*server.OKResponse, *server.ErrResponse) {
func (m *MockAccountService) CreateAccount() (*models.AccountResponse, error) {
args := m.Called()
okResponse, ok := args.Get(0).(*server.OKResponse)
errResponse, err := args.Get(1).(*server.ErrResponse)
if ok {
return okResponse, nil
}
if err {
return nil, errResponse
}
return nil, nil
return args.Get(0).(*models.AccountResponse), args.Error(1)
}
func (m *MockAccountService) CheckBalance(publicKey string) (*models.BalanceResponse, error) {
func (m *MockAccountService) CheckBalance(publicKey string) (string, error) {
args := m.Called(publicKey)
return args.Get(0).(*models.BalanceResponse), args.Error(1)
return args.String(0), args.Error(1)
}
func (m *MockAccountService) CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error) {
func (m *MockAccountService) CheckAccountStatus(trackingId string) (string, error) {
args := m.Called(trackingId)
return args.Get(0).(*models.TrackStatusResponse), args.Error(1)
}
func (m *MockAccountService) TrackAccountStatus(publicKey string) (*server.OKResponse, *server.ErrResponse) {
args := m.Called(publicKey)
okResponse, ok := args.Get(0).(*server.OKResponse)
errResponse, err := args.Get(1).(*server.ErrResponse)
if ok {
return okResponse, nil
}
if err {
return nil, errResponse
}
return nil, nil
}
return args.String(0), args.Error(1)
}

View File

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

View File

@@ -5,6 +5,7 @@ import (
"time"
)
type TrackStatusResponse struct {
Ok bool `json:"ok"`
Result struct {
@@ -16,4 +17,4 @@ type TrackStatusResponse struct {
TxType string `json:"txType"`
}
} `json:"result"`
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,119 @@
package testutil
import (
"context"
"fmt"
"os"
"path"
"time"
"git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/handlers/server"
"git.grassecon.net/urdt/ussd/internal/storage"
testdataloader "github.com/peteole/testdata-loader"
)
var (
baseDir = testdataloader.GetBasePath()
logg = logging.NewVanilla()
scriptDir = path.Join(baseDir, "services", "registration")
)
func TestEngine(sessionId string) (engine.Engine, func(), chan bool) {
ctx := context.Background()
ctx = context.WithValue(ctx, "SessionId", sessionId)
pfp := path.Join(scriptDir, "pp.csv")
var eventChannel = make(chan bool)
cfg := engine.Config{
Root: "root",
SessionId: sessionId,
OutputSize: uint32(160),
FlagCount: uint32(16),
}
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)
}
switch AccountService.(type) {
case *server.MockAccountService:
go func() {
eventChannel <- false
}()
case *server.AccountService:
go func() {
time.Sleep(5 * time.Second) // Wait for 5 seconds
eventChannel <- true
}()
default:
panic("Unknown account service type")
}
hl, err := lhs.GetHandler(AccountService)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
en := lhs.GetEngine()
en = en.WithFirst(hl.Init)
cleanFn := func() {
err := en.Finish()
if err != nil {
logg.Errorf(err.Error())
}
err = menuStorageService.Close()
if err != nil {
logg.Errorf(err.Error())
}
logg.Infof("testengine storage closed")
}
//en = en.WithDebug(nil)
return en, cleanFn, eventChannel
}

View File

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

View File

@@ -0,0 +1,12 @@
//go:build online
// +build online
package testutil
import "git.grassecon.net/urdt/ussd/internal/handlers/server"
var AccountService server.AccountServiceInterface
func init() {
AccountService = &server.AccountService{}
}

View File

@@ -5,7 +5,6 @@ MOUT back 0
HALT
LOAD validate_amount 64
RELOAD validate_amount
CATCH api_failure flag_api_call_error 1
CATCH invalid_amount flag_invalid_amount 1
INCMP _ 0
LOAD get_recipient 12

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,4 @@ CATCH invalid_pin flag_valid_pin 0
MOUT back 0
HALT
INCMP _ 0
INCMP * pin_reset_success
INCMP pin_reset_success *

View File

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

View File

@@ -1,4 +1,5 @@
LOAD reset_account_authorized 16
RELOAD reset_account_authorized
LOAD reset_allow_update 0
RELOAD reset_allow_update
MOUT edit_name 1
@@ -17,4 +18,4 @@ INCMP select_gender 3
INCMP enter_yob 4
INCMP enter_location 5
INCMP enter_offerings 6
INCMP view_profile 7
INCMP view_profile 7

View File

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

View File

@@ -7,6 +7,3 @@ HALT
RELOAD save_firstname
INCMP _ 0
INCMP pin_entry *

View File

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

View File

@@ -1,3 +1,4 @@
RELOAD reset_account_authorized
MOUT back 0
MOUT quit 9
HALT

View File

@@ -1,6 +1,5 @@
LOAD check_balance 64
RELOAD check_balance
CATCH api_failure flag_api_call_error 1
MAP check_balance
MOUT send 1
MOUT vouchers 2

View File

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

View File

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

View File

@@ -0,0 +1 @@
My vouchers

View File

@@ -0,0 +1,6 @@
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

@@ -1,13 +1,10 @@
LOAD authorize_account 12
RELOAD authorize_account
CATCH incorrect_pin flag_incorrect_pin 1
CATCH old_pin flag_allow_update 0
CATCH _ flag_allow_update 0
MOUT back 0
HALT
INCMP _ 0
LOAD save_temporary_pin 6
LOAD verify_new_pin 0
RELOAD save_temporary_pin
LOAD verify_new_pin 8
RELOAD verify_new_pin
INCMP * confirm_pin_change
CATCH incorrect_pin flag_incorrect_pin 1
INCMP confirm_pin_change *

View File

@@ -1,2 +1 @@
Weka PIN mpya ya nne nambari:
Weka PIN mpya ya nne nambari:

View File

@@ -1 +1 @@
Enter your old PIN
Enter your old PIN

View File

@@ -1,7 +1,9 @@
LOAD reset_allow_update 0
RELOAD reset_allow_update
MOUT back 0
HALT
RELOAD reset_allow_update
LOAD authorize_account 12
RELOAD authorize_account
CATCH incorrect_pin flag_incorrect_pin 1
INCMP _ 0
INCMP new_pin *

View File

@@ -1 +1 @@
Weka PIN yako ya zamani:
Weka PIN yako ya zamani:

View File

@@ -4,5 +4,4 @@ MOUT guard_pin 3
MOUT back 0
HALT
INCMP _ 0
INCMP old_pin 1
INCMP old_pin 1

View File

@@ -1 +1 @@
The PIN is not a match. Try again
The PIN is not a match. Try again

View File

@@ -3,4 +3,3 @@ MOUT quit 9
HALT
INCMP confirm_pin_change 1
INCMP quit 9

View File

@@ -1 +1 @@
Your PIN change request has been successful
Your PIN change request has been successful

View File

@@ -1,10 +1,8 @@
LOAD confirm_pin_change 0
RELOAD confirm_pin_change
CATCH pin_reset_mismatch flag_pin_mismatch 1
CATCH pin_reset_mismatch flag_pin_mismatch 1
MOUT back 0
MOUT quit 9
HALT
INCMP main 0
INCMP quit 9

View File

@@ -1 +1 @@
Ombi lako la kubadili PIN limefanikiwa
Ombi lako la kubadili PIN limefanikiwa

View File

@@ -14,4 +14,3 @@ 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_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_api_call_error,25,this is set when communication to an external service fails
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
flag flag_api_call_error 25 this is set when communication to an external service fails

View File

@@ -1 +1 @@
Profile updated successfully
Profile updated successfully

View File

@@ -1 +1 @@
Ombi la Kuweka wasifu limefanikiwa
Ombi la Kuweka wasifu limefanikiwa

View File

@@ -1,8 +1,6 @@
CATCH select_language flag_language_set 0
CATCH terms flag_account_created 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 create_pin flag_pin_set 0
CATCH main flag_account_success 1

View File

@@ -0,0 +1 @@
Select voucher

View File

@@ -1 +0,0 @@
Angalia Wasifu

View File

@@ -0,0 +1 @@
Voucher details

368
test_engine/group_test.json Normal file
View File

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

View File

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

219
test_engine/test_setup.json Normal file
View File

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