Compare commits

..

18 Commits

99 changed files with 837 additions and 1837 deletions

View File

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

View File

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

View File

@ -11,13 +11,16 @@ import (
"strconv" "strconv"
"syscall" "syscall"
"git.defalsify.org/vise.git/asm"
"git.defalsify.org/vise.git/db"
fsdb "git.defalsify.org/vise.git/db/fs"
gdbmdb "git.defalsify.org/vise.git/db/gdbm"
"git.defalsify.org/vise.git/engine" "git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/resource"
"git.defalsify.org/vise.git/logging"
"git.grassecon.net/urdt/ussd/internal/handlers" "git.grassecon.net/urdt/ussd/internal/handlers/ussd"
httpserver "git.grassecon.net/urdt/ussd/internal/http" httpserver "git.grassecon.net/urdt/ussd/internal/http"
"git.grassecon.net/urdt/ussd/internal/storage"
) )
var ( var (
@ -25,6 +28,90 @@ var (
scriptDir = path.Join("services", "registration") scriptDir = path.Join("services", "registration")
) )
func getFlags(fp string, debug bool) (*asm.FlagParser, error) {
flagParser := asm.NewFlagParser().WithDebug()
_, err := flagParser.Load(fp)
if err != nil {
return nil, err
}
return flagParser, nil
}
func getHandler(appFlags *asm.FlagParser, rs *resource.DbResource, userdataStore db.Db) (*ussd.Handlers, error) {
ussdHandlers, err := ussd.NewHandlers(appFlags, userdataStore)
if err != nil {
return nil, err
}
rs.AddLocalFunc("select_language", ussdHandlers.SetLanguage)
rs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
rs.AddLocalFunc("save_pin", ussdHandlers.SavePin)
rs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin)
rs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier)
rs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus)
rs.AddLocalFunc("authorize_account", ussdHandlers.Authorize)
rs.AddLocalFunc("quit", ussdHandlers.Quit)
rs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance)
rs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient)
rs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset)
rs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount)
rs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount)
rs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount)
rs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient)
rs.AddLocalFunc("get_sender", ussdHandlers.GetSender)
rs.AddLocalFunc("get_amount", ussdHandlers.GetAmount)
rs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin)
rs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname)
rs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname)
rs.AddLocalFunc("save_gender", ussdHandlers.SaveGender)
rs.AddLocalFunc("save_location", ussdHandlers.SaveLocation)
rs.AddLocalFunc("save_yob", ussdHandlers.SaveYob)
rs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings)
rs.AddLocalFunc("quit_with_balance", ussdHandlers.QuitWithBalance)
rs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized)
rs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate)
rs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo)
rs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob)
rs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob)
rs.AddLocalFunc("set_reset_single_edit", ussdHandlers.SetResetSingleEdit)
rs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction)
return ussdHandlers, nil
}
func ensureDbDir(dbDir string) error {
err := os.MkdirAll(dbDir, 0700)
if err != nil {
return fmt.Errorf("state dir create exited with error: %v\n", err)
}
return nil
}
func getStateStore(dbDir string, ctx context.Context) (db.Db, error) {
store := gdbmdb.NewGdbmDb()
storeFile := path.Join(dbDir, "state.gdbm")
store.Connect(ctx, storeFile)
return store, nil
}
func getUserdataDb(dbDir string, ctx context.Context) db.Db {
store := gdbmdb.NewGdbmDb()
storeFile := path.Join(dbDir, "userdata.gdbm")
store.Connect(ctx, storeFile)
return store
}
func getResource(resourceDir string, ctx context.Context) (resource.Resource, error) {
store := fsdb.NewFsDb()
err := store.Connect(ctx, resourceDir)
if err != nil {
return nil, err
}
rfs := resource.NewDbResource(store)
return rfs, nil
}
func main() { func main() {
var dbDir string var dbDir string
var resourceDir string var resourceDir string
@ -46,6 +133,11 @@ func main() {
ctx := context.Background() ctx := context.Background()
pfp := path.Join(scriptDir, "pp.csv") pfp := path.Join(scriptDir, "pp.csv")
flagParser, err := getFlags(pfp, true)
if err != nil {
os.Exit(1)
}
cfg := engine.Config{ cfg := engine.Config{
Root: "root", Root: "root",
@ -59,20 +151,19 @@ func main() {
cfg.EngineDebug = true cfg.EngineDebug = true
} }
menuStorageService := storage.MenuStorageService{} rs, err := getResource(resourceDir, ctx)
rs, err := menuStorageService.GetResource(scriptDir, ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
} }
err = menuStorageService.EnsureDbDir(dbDir) err = ensureDbDir(dbDir)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
} }
userdataStore := menuStorageService.GetUserdataDb(dbDir, ctx) userdataStore := getUserdataDb(dbDir, ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
@ -84,21 +175,13 @@ func main() {
os.Exit(1) os.Exit(1)
} }
lhs, err := handlers.NewLocalHandlerService(pfp, true, dbResource, cfg, rs) hl, err := getHandler(flagParser, dbResource, userdataStore)
lhs.WithDataStore(&userdataStore)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
} }
hl, err := lhs.GetHandler() stateStore, err := getStateStore(dbDir, ctx)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
stateStore, err := menuStorageService.GetStateStore(dbDir, ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
@ -106,8 +189,8 @@ func main() {
defer stateStore.Close() defer stateStore.Close()
rp := &httpserver.DefaultRequestParser{} rp := &httpserver.DefaultRequestParser{}
bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl) //sh := httpserver.NewSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl.Init)
sh := httpserver.ToSessionHandler(bsh) sh := httpserver.NewSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
s := &http.Server{ s := &http.Server{
Addr: fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))), Addr: fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))),
Handler: sh, Handler: sh,

View File

@ -7,11 +7,15 @@ import (
"os" "os"
"path" "path"
"git.defalsify.org/vise.git/asm"
"git.defalsify.org/vise.git/db"
fsdb "git.defalsify.org/vise.git/db/fs"
gdbmdb "git.defalsify.org/vise.git/db/gdbm"
"git.defalsify.org/vise.git/engine" "git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/internal/handlers" "git.grassecon.net/urdt/ussd/internal/handlers/ussd"
"git.grassecon.net/urdt/ussd/internal/storage"
) )
var ( var (
@ -19,6 +23,103 @@ var (
scriptDir = path.Join("services", "registration") scriptDir = path.Join("services", "registration")
) )
func getParser(fp string, debug bool) (*asm.FlagParser, error) {
flagParser := asm.NewFlagParser().WithDebug()
_, err := flagParser.Load(fp)
if err != nil {
return nil, err
}
return flagParser, nil
}
func getHandler(appFlags *asm.FlagParser, rs *resource.DbResource, pe *persist.Persister, userdataStore db.Db) (*ussd.Handlers, error) {
ussdHandlers, err := ussd.NewHandlers(appFlags, userdataStore)
if err != nil {
return nil, err
}
ussdHandlers = ussdHandlers.WithPersister(pe)
rs.AddLocalFunc("select_language", ussdHandlers.SetLanguage)
rs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
rs.AddLocalFunc("save_pin", ussdHandlers.SavePin)
rs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin)
rs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier)
rs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus)
rs.AddLocalFunc("authorize_account", ussdHandlers.Authorize)
rs.AddLocalFunc("quit", ussdHandlers.Quit)
rs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance)
rs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient)
rs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset)
rs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount)
rs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount)
rs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount)
rs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient)
rs.AddLocalFunc("get_sender", ussdHandlers.GetSender)
rs.AddLocalFunc("get_amount", ussdHandlers.GetAmount)
rs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin)
rs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname)
rs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname)
rs.AddLocalFunc("save_gender", ussdHandlers.SaveGender)
rs.AddLocalFunc("save_location", ussdHandlers.SaveLocation)
rs.AddLocalFunc("save_yob", ussdHandlers.SaveYob)
rs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings)
rs.AddLocalFunc("quit_with_balance", ussdHandlers.QuitWithBalance)
rs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized)
rs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate)
rs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo)
rs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob)
rs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob)
rs.AddLocalFunc("set_reset_single_edit", ussdHandlers.SetResetSingleEdit)
rs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction)
rs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp)
return ussdHandlers, nil
}
func ensureDbDir(dbDir string) error {
err := os.MkdirAll(dbDir, 0700)
if err != nil {
return fmt.Errorf("state dir create exited with error: %v\n", err)
}
return nil
}
func getPersister(dbDir string, ctx context.Context) (*persist.Persister, error) {
err := ensureDbDir(dbDir)
if err != nil {
return nil, err
}
store := gdbmdb.NewGdbmDb()
storeFile := path.Join(dbDir, "state.gdbm")
store.Connect(ctx, storeFile)
pr := persist.NewPersister(store)
return pr, nil
}
func getUserdataDb(dbDir string, ctx context.Context) db.Db {
store := gdbmdb.NewGdbmDb()
storeFile := path.Join(dbDir, "userdata.gdbm")
store.Connect(ctx, storeFile)
return store
}
func getResource(resourceDir string, ctx context.Context) (resource.Resource, error) {
store := fsdb.NewFsDb()
err := store.Connect(ctx, resourceDir)
if err != nil {
return nil, err
}
rfs := resource.NewDbResource(store)
return rfs, nil
}
func getEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) *engine.DefaultEngine {
en := engine.NewEngine(cfg, rs)
en = en.WithPersister(pr)
return en
}
func main() { func main() {
var dbDir string var dbDir string
var size uint var size uint
@ -35,6 +136,11 @@ func main() {
ctx := context.Background() ctx := context.Background()
ctx = context.WithValue(ctx, "SessionId", sessionId) ctx = context.WithValue(ctx, "SessionId", sessionId)
pfp := path.Join(scriptDir, "pp.csv") pfp := path.Join(scriptDir, "pp.csv")
flagParser, err := getParser(pfp, true)
if err != nil {
os.Exit(1)
}
cfg := engine.Config{ cfg := engine.Config{
Root: "root", Root: "root",
@ -43,27 +149,19 @@ func main() {
FlagCount: uint32(16), FlagCount: uint32(16),
} }
menuStorageService := storage.MenuStorageService{} rs, err := getResource(scriptDir, ctx)
err := menuStorageService.EnsureDbDir(dbDir)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
} }
rs, err := menuStorageService.GetResource(scriptDir, ctx) pe, err := getPersister(dbDir, ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
} }
pe, err := menuStorageService.GetPersister(dbDir, ctx) store := getUserdataDb(dbDir, ctx)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
userdatastore := menuStorageService.GetUserdataDb(dbDir, ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
@ -75,22 +173,13 @@ func main() {
os.Exit(1) os.Exit(1)
} }
lhs, err := handlers.NewLocalHandlerService(pfp, true, dbResource, cfg, rs) hl, err := getHandler(flagParser, dbResource, pe, store)
lhs.WithDataStore(&userdatastore)
lhs.WithPersister(pe)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
} }
hl, err := lhs.GetHandler() en := getEngine(cfg, rs, pe)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
en := lhs.GetEngine()
en = en.WithFirst(hl.Init) en = en.WithFirst(hl.Init)
if debug { if debug {
en = en.WithDebug(nil) en = en.WithDebug(nil)

4
go.mod
View File

@ -3,12 +3,14 @@ module git.grassecon.net/urdt/ussd
go 1.22.6 go 1.22.6
require ( require (
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240911231817-0d23e0dbb57f git.defalsify.org/vise.git v0.1.0-rc.3.0.20240911162138-1f2af8672dc7
github.com/alecthomas/assert/v2 v2.2.2 github.com/alecthomas/assert/v2 v2.2.2
github.com/peteole/testdata-loader v0.3.0 github.com/peteole/testdata-loader v0.3.0
gopkg.in/leonelquinteros/gotext.v1 v1.3.1 gopkg.in/leonelquinteros/gotext.v1 v1.3.1
) )
require gopkg.in/dnaeon/go-vcr.v4 v4.0.1 // indirect
require ( require (
github.com/alecthomas/participle/v2 v2.0.0 // indirect github.com/alecthomas/participle/v2 v2.0.0 // indirect
github.com/alecthomas/repr v0.2.0 // indirect github.com/alecthomas/repr v0.2.0 // indirect

6
go.sum
View File

@ -1,5 +1,5 @@
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240911231817-0d23e0dbb57f h1:CuJvG3NyMoRtHUim4aZdrfjjJBg2AId7z0yp7Q97bRM= git.defalsify.org/vise.git v0.1.0-rc.3.0.20240911162138-1f2af8672dc7 h1:embPZDx0Sgpq6jp9vcZ1GVI0eum3PsPCmAfxAa/1KLI=
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240911231817-0d23e0dbb57f/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M= git.defalsify.org/vise.git v0.1.0-rc.3.0.20240911162138-1f2af8672dc7/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M=
github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk=
github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g= github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g=
@ -30,6 +30,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/dnaeon/go-vcr.v4 v4.0.1 h1:dIFuOqqDZIJ9BTcK+DXmElzypQ6PV9fBQZSIwY+J1yM=
gopkg.in/dnaeon/go-vcr.v4 v4.0.1/go.mod h1:65yxh9goQVrudqofKtHA4JNFWd6XZRkWfKN4YpMx7KI=
gopkg.in/leonelquinteros/gotext.v1 v1.3.1 h1:8d9/fdTG0kn/B7NNGV1BsEyvektXFAbkMsTZS2sFSCc= gopkg.in/leonelquinteros/gotext.v1 v1.3.1 h1:8d9/fdTG0kn/B7NNGV1BsEyvektXFAbkMsTZS2sFSCc=
gopkg.in/leonelquinteros/gotext.v1 v1.3.1/go.mod h1:X1WlGDeAFIYsW6GjgMm4VwUwZ2XjI7Zan2InxSUQWrU= gopkg.in/leonelquinteros/gotext.v1 v1.3.1/go.mod h1:X1WlGDeAFIYsW6GjgMm4VwUwZ2XjI7Zan2InxSUQWrU=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

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

View File

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

View File

@ -0,0 +1,129 @@
//go:build !online
// +build !online
package server
import (
"testing"
"github.com/alecthomas/assert/v2"
"gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
)
func TestCheckBalanceOffline(t *testing.T) {
r, err := recorder.New("custodial/balance")
if err != nil {
t.Fatal(err)
}
defer r.Stop()
client := r.GetDefaultClient()
as := AccountService{
Client: client,
}
tests := []struct {
name string
balance string
publicKey string
}{
{
name: "Test check balance with correct public key",
publicKey: "0x216a4A64E1e699F9d65Dd9CbD0058dAB21DeF002",
balance: "3.06000000003 CELO",
},
{
name: "Test check balance with public key that doesn't exist in the custodial system",
balance: "",
publicKey: "0x216a4A64E1e699F9d65Dd9CbD0058dAB21DeF00",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
balance, err := as.CheckBalance(tt.publicKey)
if err != nil {
t.Fatalf("Failed to get balance with error %s", err)
}
if err != nil {
return
}
assert.NoError(t, err)
assert.Equal(t, balance, tt.balance, "Expected balance and actual balance should be equal")
})
}
}
func TestCheckAccountStatusOffline(t *testing.T) {
r, err := recorder.New("custodial/status")
if err != nil {
t.Fatal(err)
}
defer r.Stop()
client := r.GetDefaultClient()
as := AccountService{
Client: client,
}
tests := []struct {
name string
status string
trackingId string
}{
{
name: "Test check status with tracking id that exists in the custodial system",
trackingId: "bb23945b-65cd-4110-ac2e-a5df40572e18",
status: "SUCCESS",
},
{
name: "Test check status with tracking id that doesn't exist in the custodial system",
status: "",
trackingId: "bb23945b-65cd-4110-ac2e-a5df40572e1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
status, err := as.CheckAccountStatus(tt.trackingId)
if err != nil {
t.Fatalf("Failed to account status with error %s", err)
}
if err != nil {
return
}
assert.NoError(t, err)
assert.Equal(t, status, tt.status, "Expected status and actual status should be equal")
})
}
}
func TestCreateAccountOffline(t *testing.T) {
r, err := recorder.New("custodial/create")
if err != nil {
t.Fatal(err)
}
defer r.Stop()
client := r.GetDefaultClient()
as := AccountService{
Client: client,
}
accountRes, err := as.CreateAccount()
if err != nil {
t.Fatalf("Failed to create an account with error %s", err)
}
if err != nil {
return
}
assert.NoError(t, err)
assert.Equal(t, accountRes.Ok, true, "account response status is true")
}

View File

@ -0,0 +1,130 @@
//go:build online
// +build online
package server
import (
"net/http"
"testing"
"github.com/alecthomas/assert/v2"
"gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
)
func TestCheckBalance(t *testing.T) {
r, err := recorder.New("custodial/balance")
if err != nil {
t.Fatal(err)
}
defer r.Stop()
client := &http.Client{}
as := AccountService{
Client: client,
}
tests := []struct {
name string
balance string
publicKey string
}{
{
name: "Test check balance with correct public key",
publicKey: "0x216a4A64E1e699F9d65Dd9CbD0058dAB21DeF002",
balance: "3.06000000003 CELO",
},
{
name: "Test check balance with public key that doesn't exist in the custodial system",
balance: "",
publicKey: "0x216a4A64E1e699F9d65Dd9CbD0058dAB21DeF00",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
balance, err := as.CheckBalance(tt.publicKey)
if err != nil {
t.Fatalf("Failed to get balance with error %s", err)
}
if err != nil {
return
}
assert.NoError(t, err)
assert.Equal(t, balance, tt.balance, "Expected balance and actual balance should be equal")
})
}
}
func TestCheckAccountStatus(t *testing.T) {
r, err := recorder.New("custodial/status")
if err != nil {
t.Fatal(err)
}
defer r.Stop()
client := &http.Client{}
as := AccountService{
Client: client,
}
tests := []struct {
name string
status string
trackingId string
}{
{
name: "Test check status with tracking id that exists in the custodial system",
trackingId: "bb23945b-65cd-4110-ac2e-a5df40572e18",
status: "SUCCESS",
},
{
name: "Test check status with tracking id that doesn't exist in the custodial system",
status: "",
trackingId: "bb23945b-65cd-4110-ac2e-a5df40572e1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
status, err := as.CheckAccountStatus(tt.trackingId)
if err != nil {
t.Fatalf("Failed to account status with error %s", err)
}
if err != nil {
return
}
assert.NoError(t, err)
assert.Equal(t, status, tt.status, "Expected status and actual status should be equal")
})
}
}
func TestCreateAccount(t *testing.T) {
r, err := recorder.New("custodial/create")
if err != nil {
t.Fatal(err)
}
defer r.Stop()
client := &http.Client{}
as := AccountService{
Client: client,
}
accountRes, err := as.CreateAccount()
if err != nil {
t.Fatalf("Failed to create an account with error %s", err)
}
if err != nil {
return
}
assert.NoError(t, err)
assert.Equal(t, accountRes.Ok, true, "account response status is true")
}

View File

@ -16,6 +16,7 @@ type AccountServiceInterface interface {
} }
type AccountService struct { type AccountService struct {
Client *http.Client
} }
@ -34,7 +35,8 @@ type AccountService struct {
// If no error occurs, this will be nil. // If no error occurs, this will be nil.
// //
func (as *AccountService) CheckAccountStatus(trackingId string) (string, error) { func (as *AccountService) CheckAccountStatus(trackingId string) (string, error) {
resp, err := http.Get(config.TrackStatusURL + trackingId) resp,err := as.Client.Get(config.TrackStatusURL + trackingId)
// resp, err := http.Get(config.TrackStatusURL + trackingId)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -62,7 +64,8 @@ func (as *AccountService) CheckAccountStatus(trackingId string) (string, error)
// - publicKey: The public key associated with the account whose balance needs to be checked. // - publicKey: The public key associated with the account whose balance needs to be checked.
func (as *AccountService) CheckBalance(publicKey string) (string, error) { func (as *AccountService) CheckBalance(publicKey string) (string, error) {
resp, err := http.Get(config.BalanceURL + publicKey) //resp, err := http.Get(config.BalanceURL + publicKey)
resp, err := as.Client.Get(config.BalanceURL + publicKey)
if err != nil { if err != nil {
return "0.0", err return "0.0", err
} }
@ -91,7 +94,8 @@ func (as *AccountService) CheckBalance(publicKey string) (string, error) {
// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data. // - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
// If no error occurs, this will be nil. // If no error occurs, this will be nil.
func (as *AccountService) CreateAccount() (*models.AccountResponse, error) { func (as *AccountService) CreateAccount() (*models.AccountResponse, error) {
resp, err := http.Post(config.CreateAccountURL, "application/json", nil) //resp, err := http.Post(config.CreateAccountURL, "application/json", nil)
resp, err := as.Client.Post(config.CreateAccountURL, "application/json", nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -0,0 +1,126 @@
package server
import (
"testing"
"github.com/alecthomas/assert/v2"
"gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
)
func TestCheckBalance(t *testing.T) {
r, err := recorder.New("custodial/balance")
if err != nil {
t.Fatal(err)
}
defer r.Stop()
client := r.GetDefaultClient()
as := AccountService{
Client: client,
}
tests := []struct {
name string
balance string
publicKey string
}{
{
name: "Test check balance with correct public key",
publicKey: "0x216a4A64E1e699F9d65Dd9CbD0058dAB21DeF002",
balance: "3.06000000003 CELO",
},
{
name: "Test check balance with public key that doesn't exist in the custodial system",
balance: "",
publicKey: "0x216a4A64E1e699F9d65Dd9CbD0058dAB21DeF00",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
balance, err := as.CheckBalance(tt.publicKey)
if err != nil {
t.Fatalf("Failed to get balance with error %s", err)
}
if err != nil {
return
}
assert.NoError(t, err)
assert.Equal(t, balance, tt.balance, "Expected balance and actual balance should be equal")
})
}
}
func TestCheckAccountStatus(t *testing.T) {
r, err := recorder.New("custodial/status")
if err != nil {
t.Fatal(err)
}
defer r.Stop()
client := r.GetDefaultClient()
as := AccountService{
Client: client,
}
tests := []struct {
name string
status string
trackingId string
}{
{
name: "Test check status with tracking id that exists in the custodial system",
trackingId: "bb23945b-65cd-4110-ac2e-a5df40572e18",
status: "SUCCESS",
},
{
name: "Test check status with tracking id that doesn't exist in the custodial system",
status: "",
trackingId: "bb23945b-65cd-4110-ac2e-a5df40572e1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
status, err := as.CheckAccountStatus(tt.trackingId)
if err != nil {
t.Fatalf("Failed to account status with error %s", err)
}
if err != nil {
return
}
assert.NoError(t, err)
assert.Equal(t, status, tt.status, "Expected status and actual status should be equal")
})
}
}
func TestCreateAccount(t *testing.T) {
r, err := recorder.New("custodial/create")
if err != nil {
t.Fatal(err)
}
defer r.Stop()
client := r.GetDefaultClient()
as := AccountService{
Client: client,
}
accountRes, err := as.CreateAccount()
if err != nil {
t.Fatalf("Failed to create an account with error %s", err)
}
if err != nil {
return
}
assert.NoError(t, err)
assert.Equal(t, accountRes.Ok, true, "account response status is true")
}

View File

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

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"net/http"
"path" "path"
"regexp" "regexp"
"strconv" "strconv"
@ -29,6 +30,11 @@ var (
translationDir = path.Join(scriptDir, "locale") translationDir = path.Join(scriptDir, "locale")
) )
type FSData struct {
Path string
St *state.State
}
// FlagManager handles centralized flag management // FlagManager handles centralized flag management
type FlagManager struct { type FlagManager struct {
parser *asm.FlagParser parser *asm.FlagParser
@ -68,10 +74,13 @@ func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db) (*Handlers, erro
userDb := &utils.UserDataStore{ userDb := &utils.UserDataStore{
Db: userdataStore, Db: userdataStore,
} }
client := &http.Client{}
h := &Handlers{ h := &Handlers{
userdataStore: userDb, userdataStore: userDb,
flagManager: appFlags, flagManager: appFlags,
accountService: &server.AccountService{}, accountService: &server.AccountService{
Client: client,
},
} }
return h, nil return h, nil
} }
@ -116,15 +125,15 @@ func (h *Handlers) Init(ctx context.Context, sym string, input []byte) (resource
// SetLanguage sets the language across the menu // SetLanguage sets the language across the menu
func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
var err error
sym, _ = h.st.Where() inputStr := string(input)
switch inputStr {
switch sym { case "0":
case "set_default": res.FlagSet = []uint32{state.FLAG_LANG}
res.FlagSet = append(res.FlagSet, state.FLAG_LANG)
res.Content = "eng" res.Content = "eng"
case "set_swa": case "1":
res.FlagSet = append(res.FlagSet, state.FLAG_LANG) res.FlagSet = []uint32{state.FLAG_LANG}
res.Content = "swa" res.Content = "swa"
default: default:
} }
@ -211,74 +220,6 @@ func (h *Handlers) SavePin(ctx context.Context, sym string, input []byte) (resou
return res, nil return res, nil
} }
func (h *Handlers) VerifyNewPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
res := resource.Result{}
_, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
flag_valid_pin, _ := h.flagManager.GetFlag("flag_valid_pin")
pinInput := string(input)
// Validate that the PIN is a 4-digit number
if isValidPIN(pinInput) {
res.FlagSet = append(res.FlagSet, flag_valid_pin)
} else {
res.FlagReset = append(res.FlagReset, flag_valid_pin)
}
return res, nil
}
func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
accountPIN := string(input)
// Validate that the PIN is a 4-digit number
if !isValidPIN(accountPIN) {
res.FlagSet = append(res.FlagSet, flag_incorrect_pin)
return res, nil
}
store := h.userdataStore
err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_PIN, []byte(accountPIN))
if err != nil {
return res, err
}
return res, nil
}
func (h *Handlers) ConfirmPinChange(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")
}
flag_pin_mismatch, _ := h.flagManager.GetFlag("flag_pin_mismatch")
store := h.userdataStore
temporaryPin, err := store.ReadEntry(ctx, sessionId, utils.DATA_TEMPORARY_PIN)
if err != nil {
return res, err
}
if bytes.Equal(temporaryPin, input) {
res.FlagReset = append(res.FlagReset, flag_pin_mismatch)
} else {
res.FlagSet = append(res.FlagSet, flag_pin_mismatch)
}
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(temporaryPin))
if err != nil {
return res, err
}
return res, nil
}
// SetResetSingleEdit sets and resets flags to allow gradual editing of profile information. // SetResetSingleEdit sets and resets flags to allow gradual editing of profile information.
func (h *Handlers) SetResetSingleEdit(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) SetResetSingleEdit(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
@ -385,6 +326,9 @@ func (h *Handlers) SaveFamilyname(ctx context.Context, sym string, input []byte)
if err != nil { if err != nil {
return res, err return res, err
} }
if err != nil {
return res, nil
}
} else { } else {
return res, fmt.Errorf("a family name cannot be less than one character") return res, fmt.Errorf("a family name cannot be less than one character")
} }
@ -558,6 +502,7 @@ func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (res
} else { } else {
return res, nil return res, nil
} }
return res, nil return res, nil
} }
@ -599,7 +544,6 @@ func (h *Handlers) CheckAccountStatus(ctx context.Context, sym string, input []b
if err != nil { if err != nil {
return res, nil return res, nil
} }
if status == "SUCCESS" { if status == "SUCCESS" {
res.FlagSet = append(res.FlagSet, flag_account_success) res.FlagSet = append(res.FlagSet, flag_account_success)
res.FlagReset = append(res.FlagReset, flag_account_pending) res.FlagReset = append(res.FlagReset, flag_account_pending)
@ -625,22 +569,6 @@ func (h *Handlers) Quit(ctx context.Context, sym string, input []byte) (resource
return res, nil return res, nil
} }
// QuitWithHelp displays helpline information then exits the menu
func (h *Handlers) QuitWithHelp(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
res.Content = l.Get("For more help,please call: 0757628885")
res.FlagReset = append(res.FlagReset, flag_account_authorized)
return res, nil
}
// VerifyYob verifies the length of the given input // VerifyYob verifies the length of the given input
func (h *Handlers) VerifyYob(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) VerifyYob(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
@ -830,6 +758,7 @@ func (h *Handlers) ValidateAmount(ctx context.Context, sym string, input []byte)
if err != nil { if err != nil {
return res, err return res, err
} }
res.Content = balanceStr res.Content = balanceStr
// Parse the balance // Parse the balance
@ -837,6 +766,7 @@ func (h *Handlers) ValidateAmount(ctx context.Context, sym string, input []byte)
if len(balanceParts) != 2 { if len(balanceParts) != 2 {
return res, fmt.Errorf("unexpected balance format: %s", balanceStr) return res, fmt.Errorf("unexpected balance format: %s", balanceStr)
} }
balanceValue, err := strconv.ParseFloat(balanceParts[0], 64) balanceValue, err := strconv.ParseFloat(balanceParts[0], 64)
if err != nil { if err != nil {
return res, fmt.Errorf("failed to parse balance: %v", err) return res, fmt.Errorf("failed to parse balance: %v", err)
@ -846,6 +776,7 @@ func (h *Handlers) ValidateAmount(ctx context.Context, sym string, input []byte)
re := regexp.MustCompile(`^(\d+(\.\d+)?)\s*(?:CELO)?$`) re := regexp.MustCompile(`^(\d+(\.\d+)?)\s*(?:CELO)?$`)
matches := re.FindStringSubmatch(strings.TrimSpace(amountStr)) matches := re.FindStringSubmatch(strings.TrimSpace(amountStr))
if len(matches) < 2 { if len(matches) < 2 {
res.FlagSet = append(res.FlagSet, flag_invalid_amount) res.FlagSet = append(res.FlagSet, flag_invalid_amount)
res.Content = amountStr res.Content = amountStr
return res, nil return res, nil
@ -853,6 +784,7 @@ func (h *Handlers) ValidateAmount(ctx context.Context, sym string, input []byte)
inputAmount, err := strconv.ParseFloat(matches[1], 64) inputAmount, err := strconv.ParseFloat(matches[1], 64)
if err != nil { if err != nil {
res.FlagSet = append(res.FlagSet, flag_invalid_amount) res.FlagSet = append(res.FlagSet, flag_invalid_amount)
res.Content = amountStr res.Content = amountStr
return res, nil return res, nil
@ -865,6 +797,7 @@ func (h *Handlers) ValidateAmount(ctx context.Context, sym string, input []byte)
} }
res.Content = fmt.Sprintf("%.3f", inputAmount) // Format to 3 decimal places res.Content = fmt.Sprintf("%.3f", inputAmount) // Format to 3 decimal places
store = h.userdataStore
err = store.WriteEntry(ctx, sessionId, utils.DATA_AMOUNT, []byte(amountStr)) err = store.WriteEntry(ctx, sessionId, utils.DATA_AMOUNT, []byte(amountStr))
if err != nil { if err != nil {
return res, err return res, err

View File

@ -11,7 +11,7 @@ import (
"git.defalsify.org/vise.git/db" "git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/resource"
"git.defalsify.org/vise.git/state" "git.defalsify.org/vise.git/state"
"git.grassecon.net/urdt/ussd/internal/mocks" "git.grassecon.net/urdt/ussd/internal/handlers/ussd/mocks"
"git.grassecon.net/urdt/ussd/internal/models" "git.grassecon.net/urdt/ussd/internal/models"
"git.grassecon.net/urdt/ussd/internal/utils" "git.grassecon.net/urdt/ussd/internal/utils"
"github.com/alecthomas/assert/v2" "github.com/alecthomas/assert/v2"
@ -539,55 +539,63 @@ func TestSetLanguage(t *testing.T) {
// Define test cases // Define test cases
tests := []struct { tests := []struct {
name string name string
execPath []string input []byte
expectedFlags []uint32
expectedResult resource.Result expectedResult resource.Result
flagManagerResponse uint32
flagManagerError error
}{ }{
{ {
name: "Set Default Language (English)", name: "English language",
execPath: []string{"set_default"}, input: []byte("0"),
expectedFlags: []uint32{state.FLAG_LANG, 123},
expectedResult: resource.Result{ expectedResult: resource.Result{
FlagSet: []uint32{state.FLAG_LANG, 8}, FlagSet: []uint32{state.FLAG_LANG, 8},
Content: "eng", Content: "eng",
}, },
flagManagerResponse: 123,
flagManagerError: nil,
}, },
{ {
name: "Set Swahili Language", name: "Swahili language",
execPath: []string{"set_swa"}, input: []byte("1"),
expectedFlags: []uint32{state.FLAG_LANG, 123},
expectedResult: resource.Result{ expectedResult: resource.Result{
FlagSet: []uint32{state.FLAG_LANG, 8}, FlagSet: []uint32{state.FLAG_LANG, 8},
Content: "swa", Content: "swa",
}, },
flagManagerResponse: 123,
flagManagerError: nil,
}, },
{ {
name: "Unhandled path", name: "Unhandled Input",
execPath: []string{""}, input: []byte("3"),
expectedFlags: []uint32{123},
expectedResult: resource.Result{ expectedResult: resource.Result{
FlagSet: []uint32{8}, FlagSet: []uint32{8},
}, },
flagManagerResponse: 123,
flagManagerError: nil,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
mockState := state.NewState(16)
// Set the ExecPath
mockState.ExecPath = tt.execPath
// Create the Handlers instance with the mock flag manager // Create the Handlers instance with the mock flag manager
h := &Handlers{ h := &Handlers{
flagManager: fm.parser, flagManager: fm.parser,
st: mockState,
} }
// Call the method // Call the method
res, err := h.SetLanguage(context.Background(), "set_language", nil) res, err := h.SetLanguage(context.Background(), "set_language", tt.input)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
// Assert that the Result FlagSet has the required flags after language switch // Assert that the Result FlagSet has the required flags after language switch
assert.Equal(t, res, tt.expectedResult, "Result should match expected result") assert.Equal(t, res, tt.expectedResult, "Flags should be equal to account created")
}) })
} }
@ -1101,6 +1109,15 @@ func TestCheckAccountStatus(t *testing.T) {
FlagReset: []uint32{flag_account_pending}, FlagReset: []uint32{flag_account_pending},
}, },
}, },
// {
// name: "Test when account status is not Success",
// input: []byte("TrackingId1234"),
// status: "REVERTED",
// expectedResult: resource.Result{
// FlagReset: []uint32{flag_account_pending},
// FlagSet: []uint32{flag_account_success},
// },
// },
} }
typ := utils.DATA_TRACKING_ID typ := utils.DATA_TRACKING_ID
@ -1671,6 +1688,17 @@ func TestGetProfile(t *testing.T) {
), ),
}, },
}, },
// {
// name: "Test with yob not provided",
// 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", "Not Provided"},
// result: resource.Result{
// Content: fmt.Sprintf(
// "Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
// "John Doee", "Male", "Not Provided", "Kilifi", "Bananas",
// ),
// },
// },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -1688,148 +1716,3 @@ func TestGetProfile(t *testing.T) {
}) })
} }
} }
func TestVerifyNewPin(t *testing.T) {
sessionId := "session123"
fm, _ := NewFlagManager(flagsPath)
flag_valid_pin, _ := fm.parser.GetFlag("flag_valid_pin")
mockDataStore := new(mocks.MockUserDataStore)
mockCreateAccountService := new(mocks.MockAccountService)
h := &Handlers{
userdataStore: mockDataStore,
flagManager: fm.parser,
accountService: mockCreateAccountService,
}
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
tests := []struct {
name string
input []byte
expectedResult resource.Result
}{
{
name: "Test with valid pin",
input: []byte("1234"),
expectedResult: resource.Result{
FlagSet: []uint32{flag_valid_pin},
},
},
{
name: "Test with invalid pin",
input: []byte("123"),
expectedResult: resource.Result{
FlagReset: []uint32{flag_valid_pin},
},
},
{
name: "Test with invalid pin",
input: []byte("12345"),
expectedResult: resource.Result{
FlagReset: []uint32{flag_valid_pin},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
//Call the function under test
res, _ := h.VerifyNewPin(ctx, "verify_new_pin", tt.input)
// 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")
})
}
}
func TestSaveTemporaryPIn(t *testing.T) {
fm, err := NewFlagManager(flagsPath)
if err != nil {
t.Logf(err.Error())
}
// Create a new instance of UserDataStore
mockStore := new(mocks.MockUserDataStore)
// Define test data
sessionId := "session123"
PIN := "1234"
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
// Set up the expected behavior of the mock
mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_TEMPORARY_PIN, []byte(PIN)).Return(nil)
// Create the Handlers instance with the mock store
h := &Handlers{
userdataStore: mockStore,
flagManager: fm.parser,
}
// Call the method
res, err := h.SaveTemporaryPin(ctx, "save_temporary_pin", []byte(PIN))
// Assert results
assert.NoError(t, err)
assert.Equal(t, resource.Result{}, res)
// Assert all expectations were met
mockStore.AssertExpectations(t)
}
func TestConfirmPin(t *testing.T) {
sessionId := "session123"
fm, _ := NewFlagManager(flagsPath)
flag_pin_mismatch, _ := fm.parser.GetFlag("flag_pin_mismatch")
mockDataStore := new(mocks.MockUserDataStore)
mockCreateAccountService := new(mocks.MockAccountService)
h := &Handlers{
userdataStore: mockDataStore,
flagManager: fm.parser,
accountService: mockCreateAccountService,
}
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
tests := []struct {
name string
input []byte
temporarypin []byte
expectedResult resource.Result
}{
{
name: "Test with correct pin confirmation",
input: []byte("1234"),
temporarypin: []byte("1234"),
expectedResult: resource.Result{
FlagReset: []uint32{flag_pin_mismatch},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Set up the expected behavior of the mock
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(tt.temporarypin)).Return(nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_TEMPORARY_PIN).Return(tt.temporarypin, nil)
//Call the function under test
res, _ := h.ConfirmPinChange(ctx, "confirm_pin_change", tt.temporarypin)
// 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,92 +0,0 @@
package http
import (
"io"
"net/http"
"git.grassecon.net/urdt/ussd/internal/handlers"
)
type ATSessionHandler struct {
*SessionHandler
}
func NewATSessionHandler(h handlers.RequestHandler) *ATSessionHandler {
return &ATSessionHandler{
SessionHandler: ToSessionHandler(h),
}
}
func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
var code int
var err error
rqs := handlers.RequestSession{
Ctx: req.Context(),
Writer: w,
}
rp := ash.GetRequestParser()
cfg := ash.GetConfig()
cfg.SessionId, err = rp.GetSessionId(req)
if err != nil {
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
ash.writeError(w, 400, err)
return
}
rqs.Config = cfg
rqs.Input, err = rp.GetInput(req)
if err != nil {
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
ash.writeError(w, 400, err)
return
}
rqs, err = ash.Process(rqs)
switch err {
case nil: // set code to 200 if no err
code = 200
case handlers.ErrStorage, handlers.ErrEngineInit, handlers.ErrEngineExec, handlers.ErrEngineType:
code = 500
default:
code = 500
}
if code != 200 {
ash.writeError(w, 500, err)
return
}
w.WriteHeader(200)
w.Header().Set("Content-Type", "text/plain")
rqs, err = ash.Output(rqs)
if err != nil {
ash.writeError(w, 500, err)
return
}
rqs, err = ash.Reset(rqs)
if err != nil {
ash.writeError(w, 500, err)
return
}
}
func (ash *ATSessionHandler) Output(rqs handlers.RequestSession) (handlers.RequestSession, error) {
var err error
var prefix string
if rqs.Continue {
prefix = "CON "
} else {
prefix = "END "
}
_, err = io.WriteString(rqs.Writer, prefix)
if err != nil {
return rqs, err
}
_, err = rqs.Engine.WriteResult(rqs.Ctx, rqs.Writer)
return rqs, err
}

View File

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

View File

@ -1,42 +1,42 @@
package http package http
import ( import (
"fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"strconv"
"git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/internal/handlers" "git.grassecon.net/urdt/ussd/internal/handlers/ussd"
) )
var ( var (
logg = logging.NewVanilla().WithDomain("httpserver") logg = logging.NewVanilla().WithDomain("httpserver")
) )
type RequestParser interface {
GetSessionId(rq *http.Request) (string, error)
GetInput(rq *http.Request) ([]byte, error)
}
type DefaultRequestParser struct { type DefaultRequestParser struct {
} }
func(rp *DefaultRequestParser) GetSessionId(rq *http.Request) (string, error) {
func(rp *DefaultRequestParser) GetSessionId(rq any) (string, error) { v := rq.Header.Get("X-Vise-Session")
rqv, ok := rq.(*http.Request)
if !ok {
return "", handlers.ErrInvalidRequest
}
v := rqv.Header.Get("X-Vise-Session")
if v == "" { if v == "" {
return "", handlers.ErrSessionMissing return "", fmt.Errorf("no session found")
} }
return v, nil return v, nil
} }
func(rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) { func(rp *DefaultRequestParser) GetInput(rq *http.Request) ([]byte, error) {
rqv, ok := rq.(*http.Request) defer rq.Body.Close()
if !ok { v, err := ioutil.ReadAll(rq.Body)
return nil, handlers.ErrInvalidRequest
}
defer rqv.Body.Close()
v, err := ioutil.ReadAll(rqv.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -44,79 +44,107 @@ func(rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) {
} }
type SessionHandler struct { type SessionHandler struct {
handlers.RequestHandler cfgTemplate engine.Config
rp RequestParser
rs resource.Resource
//first resource.EntryFunc
hn *ussd.Handlers
provider StorageProvider
} }
func ToSessionHandler(h handlers.RequestHandler) *SessionHandler { //func NewSessionHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp RequestParser, first resource.EntryFunc) *SessionHandler {
func NewSessionHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp RequestParser, hn *ussd.Handlers) *SessionHandler {
return &SessionHandler{ return &SessionHandler{
RequestHandler: h, cfgTemplate: cfg,
rs: rs,
//first: first,
hn: hn,
rp: rp,
provider: NewSimpleStorageProvider(stateDb, userdataDb),
} }
} }
func(f *SessionHandler) writeError(w http.ResponseWriter, code int, err error) { func(f *SessionHandler) writeError(w http.ResponseWriter, code int, msg string, err error) {
s := err.Error() w.Header().Set("X-Vise", msg + ": " + err.Error())
w.Header().Set("Content-Length", strconv.Itoa(len(s))) w.Header().Set("Content-Length", "0")
w.WriteHeader(code) w.WriteHeader(code)
_, err = w.Write([]byte{}) _, err = w.Write([]byte{})
if err != nil { if err != nil {
logg.Errorf("error writing error!!", "err", err, "olderr", s)
w.WriteHeader(500) w.WriteHeader(500)
w.Header().Set("X-Vise", err.Error())
} }
return return
} }
func(f* SessionHandler) Shutdown() {
err := f.provider.Close()
if err != nil {
logg.Errorf("handler shutdown error", "err", err)
}
}
func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
var code int var r bool
var err error sessionId, err := f.rp.GetSessionId(req)
var perr error
rqs := handlers.RequestSession{
Ctx: req.Context(),
Writer: w,
}
rp := f.GetRequestParser()
cfg := f.GetConfig()
cfg.SessionId, err = rp.GetSessionId(req)
if err != nil { if err != nil {
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) f.writeError(w, 400, "Session missing", err)
f.writeError(w, 400, err)
}
rqs.Config = cfg
rqs.Input, err = rp.GetInput(req)
if err != nil {
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
f.writeError(w, 400, err)
return return
} }
input, err := f.rp.GetInput(req)
if err != nil {
f.writeError(w, 400, "Input read fail", err)
return
}
ctx := req.Context()
cfg := f.cfgTemplate
cfg.SessionId = sessionId
rqs, err = f.Process(rqs) logg.InfoCtxf(ctx, "new request", "session", cfg.SessionId, "input", input)
switch err {
case handlers.ErrStorage: storage, err := f.provider.Get(cfg.SessionId)
code = 500 if err != nil {
case handlers.ErrEngineInit: f.writeError(w, 500, "Storage retrieval fail", err)
code = 500 return
case handlers.ErrEngineExec: }
code = 500 f.hn = f.hn.WithPersister(storage.Persister)
default: defer f.provider.Put(cfg.SessionId, storage)
code = 200 en := getEngine(cfg, f.rs, storage.Persister)
en = en.WithFirst(f.hn.Init)
if cfg.EngineDebug {
en = en.WithDebug(nil)
} }
if code != 200 { r, err = en.Init(ctx)
f.writeError(w, 500, err) if err != nil {
f.writeError(w, 500, "Engine init fail", err)
return
}
if r && len(input) > 0 {
r, err = en.Exec(ctx, input)
}
if err != nil {
f.writeError(w, 500, "Engine exec fail", err)
return return
} }
w.WriteHeader(200) w.WriteHeader(200)
w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Type", "text/plain")
rqs, err = f.Output(rqs) _, err = en.WriteResult(ctx, w)
rqs, perr = f.Reset(rqs)
if err != nil { if err != nil {
f.writeError(w, 500, err) f.writeError(w, 500, "Write result fail", err)
return return
} }
if perr != nil { err = en.Finish()
f.writeError(w, 500, perr) if err != nil {
f.writeError(w, 500, "Engine finish fail", err)
return return
} }
_ = r
}
func getEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) *engine.DefaultEngine {
en := engine.NewEngine(cfg, rs)
en = en.WithPersister(pr)
return en
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,64 +0,0 @@
package storage
import (
"context"
"fmt"
"os"
"path"
"git.defalsify.org/vise.git/db"
fsdb "git.defalsify.org/vise.git/db/fs"
gdbmdb "git.defalsify.org/vise.git/db/gdbm"
"git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/resource"
)
type StorageService interface {
GetPersister(dbDir string, ctx context.Context) (*persist.Persister, error)
GetUserdataDb(dbDir string, ctx context.Context) db.Db
GetResource(resourceDir string, ctx context.Context) (resource.Resource, error)
EnsureDbDir(dbDir string) error
}
type MenuStorageService struct{}
func (menuStorageService *MenuStorageService) GetPersister(dbDir string, ctx context.Context) (*persist.Persister, error) {
store := gdbmdb.NewGdbmDb()
storeFile := path.Join(dbDir, "state.gdbm")
store.Connect(ctx, storeFile)
pr := persist.NewPersister(store)
return pr, nil
}
func (menuStorageService *MenuStorageService) GetUserdataDb(dbDir string, ctx context.Context) db.Db {
store := gdbmdb.NewGdbmDb()
storeFile := path.Join(dbDir, "userdata.gdbm")
store.Connect(ctx, storeFile)
return store
}
func (menuStorageService *MenuStorageService) GetResource(resourceDir string, ctx context.Context) (resource.Resource, error) {
store := fsdb.NewFsDb()
err := store.Connect(ctx, resourceDir)
if err != nil {
return nil, err
}
rfs := resource.NewDbResource(store)
return rfs, nil
}
func (menuStorageService *MenuStorageService) GetStateStore(dbDir string, ctx context.Context) (db.Db, error) {
store := gdbmdb.NewGdbmDb()
storeFile := path.Join(dbDir, "state.gdbm")
store.Connect(ctx, storeFile)
return store, nil
}
func (menuStorageService *MenuStorageService) EnsureDbDir(dbDir string) error {
err := os.MkdirAll(dbDir, 0700)
if err != nil {
return fmt.Errorf("state dir create exited with error: %v\n", err)
}
return nil
}

View File

@ -22,7 +22,6 @@ const (
DATA_OFFERINGS DATA_OFFERINGS
DATA_RECIPIENT DATA_RECIPIENT
DATA_AMOUNT DATA_AMOUNT
DATA_TEMPORARY_PIN
) )
func typToBytes(typ DataTyp) []byte { func typToBytes(typ DataTyp) []byte {

View File

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

View File

@ -1 +1 @@
Salio: Salio

View File

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

View File

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

View File

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

View File

@ -1 +1,2 @@
Your community balance is: 0.00SRF Your community balance is: 0.00SRF

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +0,0 @@
MOUT retry 1
MOUT quit 9
HALT
INCMP confirm_pin_change 1
INCMP quit 9

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@
Quit

View File

@ -1 +0,0 @@
Ondoka

View File

@ -1,6 +1,6 @@
MOUT english 0 MOUT english 0
MOUT kiswahili 1 MOUT kiswahili 1
HALT HALT
INCMP set_default 0 INCMP terms 0
INCMP set_swa 1 INCMP terms 1
INCMP . * INCMP . *

View File

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

View File

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

View File

@ -1,3 +1,5 @@
LOAD select_language 0
RELOAD select_language
MOUT yes 0 MOUT yes 0
MOUT no 1 MOUT no 1
HALT HALT

View File

@ -1,3 +1,4 @@
LOAD authorize_account 6
MAP validate_amount MAP validate_amount
RELOAD get_recipient RELOAD get_recipient
MAP get_recipient MAP get_recipient
@ -6,9 +7,9 @@ MAP get_sender
MOUT back 0 MOUT back 0
MOUT quit 9 MOUT quit 9
HALT HALT
LOAD authorize_account 6
RELOAD authorize_account RELOAD authorize_account
CATCH incorrect_pin flag_incorrect_pin 1 CATCH incorrect_pin flag_incorrect_pin 1
CATCH transaction_initiated flag_account_authorized 1
INCMP _ 0 INCMP _ 0
INCMP quit 9 INCMP quit 9
INCMP transaction_initiated *

View File

@ -1,2 +1 @@
Wasifu wangu: Wasifu wangu
{{.get_profile_info}}