Compare commits
No commits in common. "lash/purify-max" and "master" have entirely different histories.
lash/purif
...
master
172
cmd/africastalking/main.go
Normal file
172
cmd/africastalking/main.go
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"git.defalsify.org/vise.git/engine"
|
||||||
|
"git.defalsify.org/vise.git/lang"
|
||||||
|
"git.defalsify.org/vise.git/logging"
|
||||||
|
"git.defalsify.org/vise.git/resource"
|
||||||
|
|
||||||
|
"git.grassecon.net/urdt/ussd/config"
|
||||||
|
"git.grassecon.net/urdt/ussd/initializers"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/args"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/http/at"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||||
|
"git.grassecon.net/urdt/ussd/remote"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
logg = logging.NewVanilla().WithDomain("AfricasTalking").WithContextKey("at-session-id")
|
||||||
|
scriptDir = path.Join("services", "registration")
|
||||||
|
build = "dev"
|
||||||
|
menuSeparator = ": "
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
initializers.LoadEnvVariables()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config.LoadConfig()
|
||||||
|
|
||||||
|
var connStr string
|
||||||
|
var resourceDir string
|
||||||
|
var size uint
|
||||||
|
var engineDebug bool
|
||||||
|
var host string
|
||||||
|
var port uint
|
||||||
|
var err error
|
||||||
|
var gettextDir string
|
||||||
|
var langs args.LangVar
|
||||||
|
|
||||||
|
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
|
||||||
|
flag.StringVar(&connStr, "c", "", "connection string")
|
||||||
|
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||||
|
flag.UintVar(&size, "s", 160, "max size of output")
|
||||||
|
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
|
||||||
|
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
|
||||||
|
flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory")
|
||||||
|
flag.Var(&langs, "language", "add symbol resolution for language")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if connStr == "" {
|
||||||
|
connStr = config.DbConn
|
||||||
|
}
|
||||||
|
connData, err := storage.ToConnData(connStr)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
logg.Infof("start command", "build", build, "conn", connData, "resourcedir", resourceDir, "outputsize", size)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
ln, err := lang.LanguageFromCode(config.DefaultLanguage)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "default language set error: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
ctx = context.WithValue(ctx, "Language", ln)
|
||||||
|
|
||||||
|
pfp := path.Join(scriptDir, "pp.csv")
|
||||||
|
|
||||||
|
cfg := engine.Config{
|
||||||
|
Root: "root",
|
||||||
|
OutputSize: uint32(size),
|
||||||
|
FlagCount: uint32(128),
|
||||||
|
MenuSeparator: menuSeparator,
|
||||||
|
}
|
||||||
|
|
||||||
|
if engineDebug {
|
||||||
|
cfg.EngineDebug = true
|
||||||
|
}
|
||||||
|
|
||||||
|
menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
userdataStore, err := menuStorageService.GetUserdataDb(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(ctx, pfp, true, dbResource, cfg, rs)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
lhs.SetDataStore(&userdataStore)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
accountService := remote.AccountService{}
|
||||||
|
hl, err := lhs.GetHandler(&accountService)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
stateStore, err := menuStorageService.GetStateStore(ctx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer stateStore.Close()
|
||||||
|
|
||||||
|
rp := &at.ATRequestParser{}
|
||||||
|
bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
|
||||||
|
sh := at.NewATSessionHandler(bsh)
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.Handle(initializers.GetEnv("AT_ENDPOINT", "/"), sh)
|
||||||
|
|
||||||
|
s := &http.Server{
|
||||||
|
Addr: fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))),
|
||||||
|
Handler: mux,
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
197
cmd/async/main.go
Normal file
197
cmd/async/main.go
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"git.defalsify.org/vise.git/engine"
|
||||||
|
"git.defalsify.org/vise.git/lang"
|
||||||
|
"git.defalsify.org/vise.git/logging"
|
||||||
|
"git.defalsify.org/vise.git/resource"
|
||||||
|
|
||||||
|
"git.grassecon.net/urdt/ussd/config"
|
||||||
|
"git.grassecon.net/urdt/ussd/initializers"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/args"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||||
|
"git.grassecon.net/urdt/ussd/remote"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
logg = logging.NewVanilla()
|
||||||
|
scriptDir = path.Join("services", "registration")
|
||||||
|
menuSeparator = ": "
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
initializers.LoadEnvVariables()
|
||||||
|
}
|
||||||
|
|
||||||
|
type asyncRequestParser struct {
|
||||||
|
sessionId string
|
||||||
|
input []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *asyncRequestParser) GetSessionId(ctx context.Context, r any) (string, error) {
|
||||||
|
return p.sessionId, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *asyncRequestParser) GetInput(r any) ([]byte, error) {
|
||||||
|
return p.input, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config.LoadConfig()
|
||||||
|
|
||||||
|
var connStr string
|
||||||
|
var sessionId string
|
||||||
|
var resourceDir string
|
||||||
|
var size uint
|
||||||
|
var engineDebug bool
|
||||||
|
var host string
|
||||||
|
var port uint
|
||||||
|
var err error
|
||||||
|
var gettextDir string
|
||||||
|
var langs args.LangVar
|
||||||
|
|
||||||
|
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
|
||||||
|
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
|
||||||
|
flag.StringVar(&connStr, "c", "", "connection string")
|
||||||
|
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||||
|
flag.UintVar(&size, "s", 160, "max size of output")
|
||||||
|
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
|
||||||
|
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
|
||||||
|
flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory")
|
||||||
|
flag.Var(&langs, "language", "add symbol resolution for language")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if connStr == "" {
|
||||||
|
connStr = config.DbConn
|
||||||
|
}
|
||||||
|
connData, err := storage.ToConnData(connStr)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
logg.Infof("start command", "conn", connData, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
ln, err := lang.LanguageFromCode(config.DefaultLanguage)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "default language set error: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
ctx = context.WithValue(ctx, "Language", ln)
|
||||||
|
|
||||||
|
pfp := path.Join(scriptDir, "pp.csv")
|
||||||
|
|
||||||
|
cfg := engine.Config{
|
||||||
|
Root: "root",
|
||||||
|
OutputSize: uint32(size),
|
||||||
|
FlagCount: uint32(128),
|
||||||
|
MenuSeparator: menuSeparator,
|
||||||
|
}
|
||||||
|
|
||||||
|
if engineDebug {
|
||||||
|
cfg.EngineDebug = true
|
||||||
|
}
|
||||||
|
|
||||||
|
menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
userdataStore, err := menuStorageService.GetUserdataDb(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(ctx, pfp, true, dbResource, cfg, rs)
|
||||||
|
lhs.SetDataStore(&userdataStore)
|
||||||
|
accountService := remote.AccountService{}
|
||||||
|
|
||||||
|
hl, err := lhs.GetHandler(&accountService)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
stateStore, err := menuStorageService.GetStateStore(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 {
|
||||||
|
logg.ErrorCtxf(ctx, "error in process: %v", "err", err)
|
||||||
|
fmt.Errorf("error in process: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
rqs, err = sh.Output(rqs)
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(ctx, "error in output: %v", "err", err)
|
||||||
|
fmt.Errorf("error in output: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
rqs, err = sh.Reset(rqs)
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(ctx, "error in reset: %v", "err", err)
|
||||||
|
fmt.Errorf("error in reset: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println("")
|
||||||
|
_, err = fmt.Scanln(&rqs.Input)
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(ctx, "error in input", "err", err)
|
||||||
|
fmt.Errorf("error in input: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
160
cmd/http/main.go
Normal file
160
cmd/http/main.go
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"git.defalsify.org/vise.git/engine"
|
||||||
|
"git.defalsify.org/vise.git/lang"
|
||||||
|
"git.defalsify.org/vise.git/logging"
|
||||||
|
"git.defalsify.org/vise.git/resource"
|
||||||
|
|
||||||
|
"git.grassecon.net/urdt/ussd/config"
|
||||||
|
"git.grassecon.net/urdt/ussd/initializers"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/args"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||||
|
httpserver "git.grassecon.net/urdt/ussd/internal/http"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||||
|
"git.grassecon.net/urdt/ussd/remote"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
logg = logging.NewVanilla()
|
||||||
|
scriptDir = path.Join("services", "registration")
|
||||||
|
menuSeparator = ": "
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
initializers.LoadEnvVariables()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config.LoadConfig()
|
||||||
|
|
||||||
|
var connStr string
|
||||||
|
var resourceDir string
|
||||||
|
var size uint
|
||||||
|
var engineDebug bool
|
||||||
|
var host string
|
||||||
|
var port uint
|
||||||
|
var err error
|
||||||
|
var gettextDir string
|
||||||
|
var langs args.LangVar
|
||||||
|
|
||||||
|
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
|
||||||
|
flag.StringVar(&connStr, "c", "", "connection string")
|
||||||
|
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||||
|
flag.UintVar(&size, "s", 160, "max size of output")
|
||||||
|
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
|
||||||
|
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
|
||||||
|
flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory")
|
||||||
|
flag.Var(&langs, "language", "add symbol resolution for language")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if connStr == "" {
|
||||||
|
connStr = config.DbConn
|
||||||
|
}
|
||||||
|
connData, err := storage.ToConnData(connStr)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
logg.Infof("start command", "conn", connData, "resourcedir", resourceDir, "outputsize", size)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
ln, err := lang.LanguageFromCode(config.DefaultLanguage)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "default language set error: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
ctx = context.WithValue(ctx, "Language", ln)
|
||||||
|
|
||||||
|
pfp := path.Join(scriptDir, "pp.csv")
|
||||||
|
|
||||||
|
cfg := engine.Config{
|
||||||
|
Root: "root",
|
||||||
|
OutputSize: uint32(size),
|
||||||
|
FlagCount: uint32(128),
|
||||||
|
MenuSeparator: menuSeparator,
|
||||||
|
}
|
||||||
|
|
||||||
|
if engineDebug {
|
||||||
|
cfg.EngineDebug = true
|
||||||
|
}
|
||||||
|
|
||||||
|
menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
|
||||||
|
|
||||||
|
rs, err := menuStorageService.GetResource(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)
|
||||||
|
}
|
||||||
|
defer userdataStore.Close()
|
||||||
|
|
||||||
|
dbResource, ok := rs.(*resource.DbResource)
|
||||||
|
if !ok {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs)
|
||||||
|
lhs.SetDataStore(&userdataStore)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
accountService := remote.AccountService{}
|
||||||
|
hl, err := lhs.GetHandler(&accountService)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
stateStore, err := menuStorageService.GetStateStore(ctx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer stateStore.Close()
|
||||||
|
|
||||||
|
rp := &httpserver.DefaultRequestParser{}
|
||||||
|
bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
|
||||||
|
sh := httpserver.ToSessionHandler(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)
|
||||||
|
}
|
||||||
|
}
|
146
cmd/main.go
Normal file
146
cmd/main.go
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"git.defalsify.org/vise.git/engine"
|
||||||
|
"git.defalsify.org/vise.git/lang"
|
||||||
|
"git.defalsify.org/vise.git/logging"
|
||||||
|
"git.defalsify.org/vise.git/resource"
|
||||||
|
"git.grassecon.net/urdt/ussd/config"
|
||||||
|
"git.grassecon.net/urdt/ussd/initializers"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/args"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||||
|
"git.grassecon.net/urdt/ussd/remote"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
logg = logging.NewVanilla()
|
||||||
|
scriptDir = path.Join("services", "registration")
|
||||||
|
menuSeparator = ": "
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
initializers.LoadEnvVariables()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: external script automatically generate language handler list from select language vise code OR consider dynamic menu generation script possibility
|
||||||
|
func main() {
|
||||||
|
config.LoadConfig()
|
||||||
|
|
||||||
|
var connStr string
|
||||||
|
var size uint
|
||||||
|
var sessionId string
|
||||||
|
var engineDebug bool
|
||||||
|
var resourceDir string
|
||||||
|
var err error
|
||||||
|
var gettextDir string
|
||||||
|
var langs args.LangVar
|
||||||
|
|
||||||
|
flag.StringVar(&resourceDir, "resourcedir", scriptDir, "resource dir")
|
||||||
|
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
|
||||||
|
flag.StringVar(&connStr, "c", "", "connection string")
|
||||||
|
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||||
|
flag.UintVar(&size, "s", 160, "max size of output")
|
||||||
|
flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory")
|
||||||
|
flag.Var(&langs, "language", "add symbol resolution for language")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if connStr == "" {
|
||||||
|
connStr = config.DbConn
|
||||||
|
}
|
||||||
|
connData, err := storage.ToConnData(connStr)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
logg.Infof("start command", "conn", connData, "outputsize", size)
|
||||||
|
|
||||||
|
if len(langs.Langs()) == 0 {
|
||||||
|
langs.Set(config.DefaultLanguage)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||||
|
|
||||||
|
ln, err := lang.LanguageFromCode(config.DefaultLanguage)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "default language set error: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
ctx = context.WithValue(ctx, "Language", ln)
|
||||||
|
|
||||||
|
pfp := path.Join(scriptDir, "pp.csv")
|
||||||
|
|
||||||
|
cfg := engine.Config{
|
||||||
|
Root: "root",
|
||||||
|
SessionId: sessionId,
|
||||||
|
OutputSize: uint32(size),
|
||||||
|
FlagCount: uint32(128),
|
||||||
|
MenuSeparator: menuSeparator,
|
||||||
|
}
|
||||||
|
|
||||||
|
menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
|
||||||
|
|
||||||
|
if gettextDir != "" {
|
||||||
|
menuStorageService = menuStorageService.WithGettext(gettextDir, langs.Langs())
|
||||||
|
}
|
||||||
|
|
||||||
|
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(ctx, pfp, true, dbResource, cfg, rs)
|
||||||
|
lhs.SetDataStore(&userdatastore)
|
||||||
|
lhs.SetPersister(pe)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
accountService := remote.AccountService{}
|
||||||
|
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)
|
||||||
|
if engineDebug {
|
||||||
|
en = en.WithDebug(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = engine.Loop(ctx, en, os.Stdin, os.Stdout, nil)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "loop exited with error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
34
cmd/ssh/README.md
Normal file
34
cmd/ssh/README.md
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# URDT-USSD SSH server
|
||||||
|
|
||||||
|
An SSH server entry point for the vise engine.
|
||||||
|
|
||||||
|
|
||||||
|
## Adding public keys for access
|
||||||
|
|
||||||
|
Map your (client) public key to a session identifier (e.g. phone number)
|
||||||
|
|
||||||
|
```
|
||||||
|
go run -v -tags logtrace ./cmd/ssh/sshkey/main.go -i <session_id> [--dbdir <dbpath>] <client_publickey_filepath>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Create a private key for the server
|
||||||
|
|
||||||
|
```
|
||||||
|
ssh-keygen -N "" -f <server_privatekey_filepath>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Run the server
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
go run -v -tags logtrace ./cmd/ssh/main.go -h <host> -p <port> [--dbdir <dbpath>] <server_privatekey_filepath>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Connect to the server
|
||||||
|
|
||||||
|
```
|
||||||
|
ssh [-v] -T -p <port> -i <client_publickey_filepath> <host>
|
||||||
|
```
|
144
cmd/ssh/main.go
Normal file
144
cmd/ssh/main.go
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"git.defalsify.org/vise.git/db"
|
||||||
|
"git.defalsify.org/vise.git/engine"
|
||||||
|
"git.defalsify.org/vise.git/logging"
|
||||||
|
|
||||||
|
"git.grassecon.net/urdt/ussd/config"
|
||||||
|
"git.grassecon.net/urdt/ussd/initializers"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/ssh"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
wg sync.WaitGroup
|
||||||
|
keyStore db.Db
|
||||||
|
logg = logging.NewVanilla()
|
||||||
|
scriptDir = path.Join("services", "registration")
|
||||||
|
|
||||||
|
build = "dev"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
initializers.LoadEnvVariables()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config.LoadConfig()
|
||||||
|
|
||||||
|
var connStr string
|
||||||
|
var authConnStr string
|
||||||
|
var resourceDir string
|
||||||
|
var size uint
|
||||||
|
var engineDebug bool
|
||||||
|
var stateDebug bool
|
||||||
|
var host string
|
||||||
|
var port uint
|
||||||
|
flag.StringVar(&connStr, "c", "", "connection string")
|
||||||
|
flag.StringVar(&authConnStr, "authdb", "", "auth connection string")
|
||||||
|
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
|
||||||
|
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||||
|
flag.UintVar(&size, "s", 160, "max size of output")
|
||||||
|
flag.StringVar(&host, "h", "127.0.0.1", "socket host")
|
||||||
|
flag.UintVar(&port, "p", 7122, "socket port")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if connStr == "" {
|
||||||
|
connStr = config.DbConn
|
||||||
|
}
|
||||||
|
if authConnStr == "" {
|
||||||
|
authConnStr = connStr
|
||||||
|
}
|
||||||
|
connData, err := storage.ToConnData(connStr)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
authConnData, err := storage.ToConnData(authConnStr)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "auth connstr err: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
sshKeyFile := flag.Arg(0)
|
||||||
|
_, err = os.Stat(sshKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "cannot open ssh server private key file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
logg.WarnCtxf(ctx, "!!!!! WARNING WARNING WARNING")
|
||||||
|
logg.WarnCtxf(ctx, "!!!!! =======================")
|
||||||
|
logg.WarnCtxf(ctx, "!!!!! This is not a production ready server!")
|
||||||
|
logg.WarnCtxf(ctx, "!!!!! Do not expose to internet and only use with tunnel!")
|
||||||
|
logg.WarnCtxf(ctx, "!!!!! (See ssh -L <...>)")
|
||||||
|
|
||||||
|
logg.Infof("start command", "conn", connData, "authconn", authConnData, "resourcedir", resourceDir, "outputsize", size, "keyfile", sshKeyFile, "host", host, "port", port)
|
||||||
|
|
||||||
|
pfp := path.Join(scriptDir, "pp.csv")
|
||||||
|
|
||||||
|
cfg := engine.Config{
|
||||||
|
Root: "root",
|
||||||
|
OutputSize: uint32(size),
|
||||||
|
FlagCount: uint32(16),
|
||||||
|
}
|
||||||
|
if stateDebug {
|
||||||
|
cfg.StateDebug = true
|
||||||
|
}
|
||||||
|
if engineDebug {
|
||||||
|
cfg.EngineDebug = true
|
||||||
|
}
|
||||||
|
|
||||||
|
authKeyStore, err := ssh.NewSshKeyStore(ctx, authConnData.String())
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "keystore file open error: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
logg.TraceCtxf(ctx, "shutdown auth key store reached")
|
||||||
|
err = authKeyStore.Close()
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(ctx, "keystore close error", "err", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
cint := make(chan os.Signal)
|
||||||
|
cterm := make(chan os.Signal)
|
||||||
|
signal.Notify(cint, os.Interrupt, syscall.SIGINT)
|
||||||
|
signal.Notify(cterm, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
|
runner := &ssh.SshRunner{
|
||||||
|
Cfg: cfg,
|
||||||
|
Debug: engineDebug,
|
||||||
|
FlagFile: pfp,
|
||||||
|
Conn: connData,
|
||||||
|
ResourceDir: resourceDir,
|
||||||
|
SrvKeyFile: sshKeyFile,
|
||||||
|
Host: host,
|
||||||
|
Port: port,
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case _ = <-cint:
|
||||||
|
case _ = <-cterm:
|
||||||
|
}
|
||||||
|
logg.TraceCtxf(ctx, "shutdown runner reached")
|
||||||
|
err := runner.Stop()
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(ctx, "runner stop error", "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}()
|
||||||
|
runner.Run(ctx, authKeyStore)
|
||||||
|
}
|
44
cmd/ssh/sshkey/main.go
Normal file
44
cmd/ssh/sshkey/main.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var dbDir string
|
||||||
|
var sessionId string
|
||||||
|
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
||||||
|
flag.StringVar(&sessionId, "i", "", "session id")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if sessionId == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "empty session id\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
sshKeyFile := flag.Arg(0)
|
||||||
|
if sshKeyFile == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "missing key file argument\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
store, err := ssh.NewSshKeyStore(ctx, dbDir)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer store.Close()
|
||||||
|
|
||||||
|
err = store.AddFromFile(ctx, sshKeyFile, sessionId)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
@ -7,13 +7,8 @@ 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/persist"
|
"git.defalsify.org/vise.git/persist"
|
||||||
"git.defalsify.org/vise.git/lang"
|
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/storage"
|
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||||
dbstorage "git.grassecon.net/grassrootseconomics/visedriver/storage/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ToConnData = storage.ToConnData
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func StoreToDb(store *UserDataStore) db.Db {
|
func StoreToDb(store *UserDataStore) db.Db {
|
||||||
@ -41,17 +36,6 @@ func NewStorageService(conn storage.ConnData) (*StorageService, error) {
|
|||||||
return svc, nil
|
return svc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *StorageService) WithGettext(path string, lns []lang.Language) *StorageService {
|
|
||||||
ss.svc = ss.svc.WithGettext(path, lns)
|
|
||||||
return ss
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: simplify enable poresource, conndata instead
|
|
||||||
func(ss *StorageService) SetResourceDir(resourceDir string) error {
|
|
||||||
ss.svc = ss.svc.WithResourceDir(resourceDir)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func(ss *StorageService) GetPersister(ctx context.Context) (*persist.Persister, error) {
|
func(ss *StorageService) GetPersister(ctx context.Context) (*persist.Persister, error) {
|
||||||
return ss.svc.GetPersister(ctx)
|
return ss.svc.GetPersister(ctx)
|
||||||
}
|
}
|
||||||
@ -63,7 +47,3 @@ func(ss *StorageService) GetUserdataDb(ctx context.Context) (db.Db, error) {
|
|||||||
func(ss *StorageService) GetResource(ctx context.Context) (resource.Resource, error) {
|
func(ss *StorageService) GetResource(ctx context.Context) (resource.Resource, error) {
|
||||||
return nil, errors.New("not implemented")
|
return nil, errors.New("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func(ss *StorageService) GetStateStore(ctx context.Context) (db.Db, error) {
|
|
||||||
return ss.svc.GetStateStore(ctx)
|
|
||||||
}
|
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
dbstorage "git.grassecon.net/grassrootseconomics/visedriver/storage/db"
|
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||||
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
"math/big"
|
"math/big"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
dbstorage "git.grassecon.net/grassrootseconomics/visedriver/storage/db"
|
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||||
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
visedb "git.defalsify.org/vise.git/db"
|
visedb "git.defalsify.org/vise.git/db"
|
||||||
memdb "git.defalsify.org/vise.git/db/mem"
|
memdb "git.defalsify.org/vise.git/db/mem"
|
||||||
dbstorage "git.grassecon.net/grassrootseconomics/visedriver/storage/db"
|
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||||
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/initializers"
|
"git.grassecon.net/urdt/ussd/initializers"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/common"
|
"git.grassecon.net/urdt/ussd/common"
|
||||||
"git.defalsify.org/vise.git/db"
|
"git.defalsify.org/vise.git/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ package debug
|
|||||||
import (
|
import (
|
||||||
"git.defalsify.org/vise.git/db"
|
"git.defalsify.org/vise.git/db"
|
||||||
|
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/common"
|
"git.grassecon.net/urdt/ussd/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -3,7 +3,7 @@ package debug
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/common"
|
"git.grassecon.net/urdt/ussd/common"
|
||||||
"git.defalsify.org/vise.git/db"
|
"git.defalsify.org/vise.git/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.defalsify.org/vise.git/logging"
|
"git.defalsify.org/vise.git/logging"
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/utils"
|
"git.grassecon.net/urdt/ussd/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
@ -4,7 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/cmd/admin/commands"
|
"git.grassecon.net/urdt/ussd/devtools/admin/commands"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
@ -10,8 +10,8 @@ import (
|
|||||||
|
|
||||||
"git.defalsify.org/vise.git/logging"
|
"git.defalsify.org/vise.git/logging"
|
||||||
"git.defalsify.org/vise.git/lang"
|
"git.defalsify.org/vise.git/lang"
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/config"
|
"git.grassecon.net/urdt/ussd/config"
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/initializers"
|
"git.grassecon.net/urdt/ussd/initializers"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
@ -7,10 +7,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/config"
|
"git.grassecon.net/urdt/ussd/config"
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/initializers"
|
"git.grassecon.net/urdt/ussd/initializers"
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/storage"
|
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/debug"
|
"git.grassecon.net/urdt/ussd/debug"
|
||||||
"git.defalsify.org/vise.git/db"
|
"git.defalsify.org/vise.git/db"
|
||||||
"git.defalsify.org/vise.git/logging"
|
"git.defalsify.org/vise.git/logging"
|
||||||
)
|
)
|
@ -8,12 +8,12 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
testdataloader "github.com/peteole/testdata-loader"
|
|
||||||
"git.defalsify.org/vise.git/logging"
|
"git.defalsify.org/vise.git/logging"
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/config"
|
"git.grassecon.net/urdt/ussd/common"
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/storage"
|
"git.grassecon.net/urdt/ussd/config"
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/initializers"
|
"git.grassecon.net/urdt/ussd/initializers"
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/common"
|
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||||
|
testdataloader "github.com/peteole/testdata-loader"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
@ -1,13 +0,0 @@
|
|||||||
package entry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"git.defalsify.org/vise.git/resource"
|
|
||||||
"git.defalsify.org/vise.git/persist"
|
|
||||||
)
|
|
||||||
|
|
||||||
type EntryHandler interface {
|
|
||||||
Init(context.Context, string, []byte) (resource.Result, error) // HandlerFunc
|
|
||||||
Exit()
|
|
||||||
SetPersister(*persist.Persister)
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
package errors
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/internal/handlers"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrInvalidRequest = handlers.ErrInvalidRequest
|
|
||||||
ErrSessionMissing = handlers.ErrSessionMissing
|
|
||||||
ErrInvalidInput = handlers.ErrInvalidInput
|
|
||||||
ErrStorage = handlers.ErrStorage
|
|
||||||
ErrEngineType = handlers.ErrEngineType
|
|
||||||
ErrEngineInit = handlers.ErrEngineInit
|
|
||||||
ErrEngineExec = handlers.ErrEngineExec
|
|
||||||
)
|
|
8
go.mod
8
go.mod
@ -1,4 +1,4 @@
|
|||||||
module git.grassecon.net/grassrootseconomics/visedriver
|
module git.grassecon.net/urdt/ussd
|
||||||
|
|
||||||
go 1.23.0
|
go 1.23.0
|
||||||
|
|
||||||
@ -8,14 +8,15 @@ require (
|
|||||||
github.com/gofrs/uuid v4.4.0+incompatible
|
github.com/gofrs/uuid v4.4.0+incompatible
|
||||||
github.com/grassrootseconomics/eth-custodial v1.3.0-beta
|
github.com/grassrootseconomics/eth-custodial v1.3.0-beta
|
||||||
github.com/grassrootseconomics/ussd-data-service v1.2.0-beta
|
github.com/grassrootseconomics/ussd-data-service v1.2.0-beta
|
||||||
github.com/jackc/pgx/v5 v5.7.1
|
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/peteole/testdata-loader v0.3.0
|
github.com/peteole/testdata-loader v0.3.0
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
golang.org/x/crypto v0.27.0
|
golang.org/x/crypto v0.27.0
|
||||||
|
gopkg.in/leonelquinteros/gotext.v1 v1.3.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/alecthomas/participle/v2 v2.0.0 // indirect
|
||||||
github.com/alecthomas/repr v0.2.0 // indirect
|
github.com/alecthomas/repr v0.2.0 // indirect
|
||||||
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect
|
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
@ -24,6 +25,7 @@ require (
|
|||||||
github.com/hexops/gotextdiff v1.0.3 // indirect
|
github.com/hexops/gotextdiff v1.0.3 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.7.1 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect
|
github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect
|
||||||
@ -32,8 +34,6 @@ require (
|
|||||||
github.com/stretchr/objx v0.5.2 // indirect
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
golang.org/x/sync v0.8.0 // indirect
|
golang.org/x/sync v0.8.0 // indirect
|
||||||
golang.org/x/sys v0.25.0 // indirect
|
|
||||||
golang.org/x/text v0.18.0 // indirect
|
golang.org/x/text v0.18.0 // indirect
|
||||||
gopkg.in/leonelquinteros/gotext.v1 v1.3.1 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
6
go.sum
6
go.sum
@ -2,6 +2,8 @@ git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d h1:bPAOVZOX4frSG
|
|||||||
git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
|
git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
|
||||||
github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk=
|
github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk=
|
||||||
github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
|
github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
|
||||||
|
github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g=
|
||||||
|
github.com/alecthomas/participle/v2 v2.0.0/go.mod h1:rAKZdJldHu8084ojcWevWAL8KmEU+AT+Olodb+WoN2Y=
|
||||||
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
|
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
|
||||||
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c h1:H9Nm+I7Cg/YVPpEV1RzU3Wq2pjamPc/UtHDgItcb7lE=
|
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c h1:H9Nm+I7Cg/YVPpEV1RzU3Wq2pjamPc/UtHDgItcb7lE=
|
||||||
@ -60,10 +62,6 @@ golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
|||||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
|
||||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
|
|
||||||
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
|
|
||||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
34
internal/args/lang.go
Normal file
34
internal/args/lang.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package args
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.defalsify.org/vise.git/lang"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LangVar struct {
|
||||||
|
v []lang.Language
|
||||||
|
}
|
||||||
|
|
||||||
|
func(lv *LangVar) Set(s string) error {
|
||||||
|
v, err := lang.LanguageFromCode(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
lv.v = append(lv.v, v)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func(lv *LangVar) String() string {
|
||||||
|
var s []string
|
||||||
|
for _, v := range(lv.v) {
|
||||||
|
s = append(s, v.Code)
|
||||||
|
}
|
||||||
|
return strings.Join(s, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func(lv *LangVar) Langs() []lang.Language {
|
||||||
|
return lv.v
|
||||||
|
}
|
||||||
|
|
||||||
|
|
2193
internal/handlers/application/menu_handler.go
Normal file
2193
internal/handlers/application/menu_handler.go
Normal file
File diff suppressed because it is too large
Load Diff
2331
internal/handlers/application/menu_handler_test.go
Normal file
2331
internal/handlers/application/menu_handler_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,31 +1,24 @@
|
|||||||
package session
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.defalsify.org/vise.git/db"
|
"git.defalsify.org/vise.git/db"
|
||||||
"git.defalsify.org/vise.git/engine"
|
"git.defalsify.org/vise.git/engine"
|
||||||
"git.defalsify.org/vise.git/persist"
|
"git.defalsify.org/vise.git/persist"
|
||||||
"git.defalsify.org/vise.git/resource"
|
"git.defalsify.org/vise.git/resource"
|
||||||
"git.defalsify.org/vise.git/logging"
|
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/request"
|
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/storage"
|
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/errors"
|
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/entry"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
"git.grassecon.net/urdt/ussd/internal/handlers/application"
|
||||||
logg = logging.NewVanilla().WithDomain("visedriver.session")
|
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BaseSessionHandler struct {
|
type BaseSessionHandler struct {
|
||||||
cfgTemplate engine.Config
|
cfgTemplate engine.Config
|
||||||
rp request.RequestParser
|
rp RequestParser
|
||||||
rs resource.Resource
|
rs resource.Resource
|
||||||
hn entry.EntryHandler
|
hn *application.Handlers
|
||||||
provider storage.StorageProvider
|
provider storage.StorageProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
//func NewBaseSessionHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp request.RequestParser, hn *handlers.Handlers) *BaseSessionHandler {
|
func NewBaseSessionHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp RequestParser, hn *application.Handlers) *BaseSessionHandler {
|
||||||
func NewBaseSessionHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp request.RequestParser, hn entry.EntryHandler) *BaseSessionHandler {
|
|
||||||
return &BaseSessionHandler{
|
return &BaseSessionHandler{
|
||||||
cfgTemplate: cfg,
|
cfgTemplate: cfg,
|
||||||
rs: rs,
|
rs: rs,
|
||||||
@ -48,7 +41,7 @@ func (f *BaseSessionHandler) GetEngine(cfg engine.Config, rs resource.Resource,
|
|||||||
return en
|
return en
|
||||||
}
|
}
|
||||||
|
|
||||||
func(f *BaseSessionHandler) Process(rqs request.RequestSession) (request.RequestSession, error) {
|
func (f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error) {
|
||||||
var r bool
|
var r bool
|
||||||
var err error
|
var err error
|
||||||
var ok bool
|
var ok bool
|
||||||
@ -58,11 +51,10 @@ func(f *BaseSessionHandler) Process(rqs request.RequestSession) (request.Request
|
|||||||
rqs.Storage, err = f.provider.Get(rqs.Config.SessionId)
|
rqs.Storage, err = f.provider.Get(rqs.Config.SessionId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logg.ErrorCtxf(rqs.Ctx, "", "storage get error", err)
|
logg.ErrorCtxf(rqs.Ctx, "", "storage get error", err)
|
||||||
return rqs, errors.ErrStorage
|
return rqs, ErrStorage
|
||||||
}
|
}
|
||||||
|
|
||||||
//f.hn = f.hn.WithPersister(rqs.Storage.Persister)
|
f.hn = f.hn.WithPersister(rqs.Storage.Persister)
|
||||||
f.hn.SetPersister(rqs.Storage.Persister)
|
|
||||||
defer func() {
|
defer func() {
|
||||||
f.hn.Exit()
|
f.hn.Exit()
|
||||||
}()
|
}()
|
||||||
@ -74,7 +66,7 @@ func(f *BaseSessionHandler) Process(rqs request.RequestSession) (request.Request
|
|||||||
if perr != nil {
|
if perr != nil {
|
||||||
logg.ErrorCtxf(rqs.Ctx, "", "storage put error", perr)
|
logg.ErrorCtxf(rqs.Ctx, "", "storage put error", perr)
|
||||||
}
|
}
|
||||||
return rqs, errors.ErrEngineType
|
return rqs, ErrEngineType
|
||||||
}
|
}
|
||||||
en = en.WithFirst(f.hn.Init)
|
en = en.WithFirst(f.hn.Init)
|
||||||
if rqs.Config.EngineDebug {
|
if rqs.Config.EngineDebug {
|
||||||
@ -96,13 +88,13 @@ func(f *BaseSessionHandler) Process(rqs request.RequestSession) (request.Request
|
|||||||
return rqs, nil
|
return rqs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func(f *BaseSessionHandler) Output(rqs request.RequestSession) (request.RequestSession, error) {
|
func (f *BaseSessionHandler) Output(rqs RequestSession) (RequestSession, error) {
|
||||||
var err error
|
var err error
|
||||||
_, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer)
|
_, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer)
|
||||||
return rqs, err
|
return rqs, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func(f *BaseSessionHandler) Reset(rqs request.RequestSession) (request.RequestSession, error) {
|
func (f *BaseSessionHandler) Reset(rqs RequestSession) (RequestSession, error) {
|
||||||
defer f.provider.Put(rqs.Config.SessionId, rqs.Storage)
|
defer f.provider.Put(rqs.Config.SessionId, rqs.Storage)
|
||||||
return rqs, rqs.Engine.Finish()
|
return rqs, rqs.Engine.Finish()
|
||||||
}
|
}
|
||||||
@ -111,6 +103,6 @@ func (f *BaseSessionHandler) GetConfig() engine.Config {
|
|||||||
return f.cfgTemplate
|
return f.cfgTemplate
|
||||||
}
|
}
|
||||||
|
|
||||||
func(f *BaseSessionHandler) GetRequestParser() request.RequestParser {
|
func (f *BaseSessionHandler) GetRequestParser() RequestParser {
|
||||||
return f.rp
|
return f.rp
|
||||||
}
|
}
|
141
internal/handlers/handler_service.go
Normal file
141
internal/handlers/handler_service.go
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"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/application"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/utils"
|
||||||
|
"git.grassecon.net/urdt/ussd/remote"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HandlerService interface {
|
||||||
|
GetHandler() (*application.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
|
||||||
|
AdminStore *utils.AdminStore
|
||||||
|
Cfg engine.Config
|
||||||
|
Rs resource.Resource
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLocalHandlerService(ctx context.Context, 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
|
||||||
|
}
|
||||||
|
adminstore, err := utils.NewAdminStore(ctx, "admin_numbers")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &LocalHandlerService{
|
||||||
|
Parser: parser,
|
||||||
|
DbRs: dbResource,
|
||||||
|
AdminStore: adminstore,
|
||||||
|
Cfg: cfg,
|
||||||
|
Rs: rs,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LocalHandlerService) SetPersister(Pe *persist.Persister) {
|
||||||
|
ls.Pe = Pe
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LocalHandlerService) SetDataStore(db *db.Db) {
|
||||||
|
ls.UserdataStore = db
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceInterface) (*application.Handlers, error) {
|
||||||
|
replaceSeparatorFunc := func(input string) string {
|
||||||
|
return strings.ReplaceAll(input, ":", ls.Cfg.MenuSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
appHandlers, err := application.NewHandlers(ls.Parser, *ls.UserdataStore, ls.AdminStore, accountService, replaceSeparatorFunc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
appHandlers = appHandlers.WithPersister(ls.Pe)
|
||||||
|
ls.DbRs.AddLocalFunc("set_language", appHandlers.SetLanguage)
|
||||||
|
ls.DbRs.AddLocalFunc("create_account", appHandlers.CreateAccount)
|
||||||
|
ls.DbRs.AddLocalFunc("save_temporary_pin", appHandlers.SaveTemporaryPin)
|
||||||
|
ls.DbRs.AddLocalFunc("verify_create_pin", appHandlers.VerifyCreatePin)
|
||||||
|
ls.DbRs.AddLocalFunc("check_identifier", appHandlers.CheckIdentifier)
|
||||||
|
ls.DbRs.AddLocalFunc("check_account_status", appHandlers.CheckAccountStatus)
|
||||||
|
ls.DbRs.AddLocalFunc("authorize_account", appHandlers.Authorize)
|
||||||
|
ls.DbRs.AddLocalFunc("quit", appHandlers.Quit)
|
||||||
|
ls.DbRs.AddLocalFunc("check_balance", appHandlers.CheckBalance)
|
||||||
|
ls.DbRs.AddLocalFunc("validate_recipient", appHandlers.ValidateRecipient)
|
||||||
|
ls.DbRs.AddLocalFunc("transaction_reset", appHandlers.TransactionReset)
|
||||||
|
ls.DbRs.AddLocalFunc("invite_valid_recipient", appHandlers.InviteValidRecipient)
|
||||||
|
ls.DbRs.AddLocalFunc("max_amount", appHandlers.MaxAmount)
|
||||||
|
ls.DbRs.AddLocalFunc("validate_amount", appHandlers.ValidateAmount)
|
||||||
|
ls.DbRs.AddLocalFunc("reset_transaction_amount", appHandlers.ResetTransactionAmount)
|
||||||
|
ls.DbRs.AddLocalFunc("get_recipient", appHandlers.GetRecipient)
|
||||||
|
ls.DbRs.AddLocalFunc("get_sender", appHandlers.GetSender)
|
||||||
|
ls.DbRs.AddLocalFunc("get_amount", appHandlers.GetAmount)
|
||||||
|
ls.DbRs.AddLocalFunc("reset_incorrect", appHandlers.ResetIncorrectPin)
|
||||||
|
ls.DbRs.AddLocalFunc("save_firstname", appHandlers.SaveFirstname)
|
||||||
|
ls.DbRs.AddLocalFunc("save_familyname", appHandlers.SaveFamilyname)
|
||||||
|
ls.DbRs.AddLocalFunc("save_gender", appHandlers.SaveGender)
|
||||||
|
ls.DbRs.AddLocalFunc("save_location", appHandlers.SaveLocation)
|
||||||
|
ls.DbRs.AddLocalFunc("save_yob", appHandlers.SaveYob)
|
||||||
|
ls.DbRs.AddLocalFunc("save_offerings", appHandlers.SaveOfferings)
|
||||||
|
ls.DbRs.AddLocalFunc("reset_account_authorized", appHandlers.ResetAccountAuthorized)
|
||||||
|
ls.DbRs.AddLocalFunc("reset_allow_update", appHandlers.ResetAllowUpdate)
|
||||||
|
ls.DbRs.AddLocalFunc("get_profile_info", appHandlers.GetProfileInfo)
|
||||||
|
ls.DbRs.AddLocalFunc("verify_yob", appHandlers.VerifyYob)
|
||||||
|
ls.DbRs.AddLocalFunc("reset_incorrect_date_format", appHandlers.ResetIncorrectYob)
|
||||||
|
ls.DbRs.AddLocalFunc("initiate_transaction", appHandlers.InitiateTransaction)
|
||||||
|
ls.DbRs.AddLocalFunc("verify_new_pin", appHandlers.VerifyNewPin)
|
||||||
|
ls.DbRs.AddLocalFunc("confirm_pin_change", appHandlers.ConfirmPinChange)
|
||||||
|
ls.DbRs.AddLocalFunc("quit_with_help", appHandlers.QuitWithHelp)
|
||||||
|
ls.DbRs.AddLocalFunc("fetch_community_balance", appHandlers.FetchCommunityBalance)
|
||||||
|
ls.DbRs.AddLocalFunc("set_default_voucher", appHandlers.SetDefaultVoucher)
|
||||||
|
ls.DbRs.AddLocalFunc("check_vouchers", appHandlers.CheckVouchers)
|
||||||
|
ls.DbRs.AddLocalFunc("get_vouchers", appHandlers.GetVoucherList)
|
||||||
|
ls.DbRs.AddLocalFunc("view_voucher", appHandlers.ViewVoucher)
|
||||||
|
ls.DbRs.AddLocalFunc("set_voucher", appHandlers.SetVoucher)
|
||||||
|
ls.DbRs.AddLocalFunc("get_voucher_details", appHandlers.GetVoucherDetails)
|
||||||
|
ls.DbRs.AddLocalFunc("reset_valid_pin", appHandlers.ResetValidPin)
|
||||||
|
ls.DbRs.AddLocalFunc("check_pin_mismatch", appHandlers.CheckBlockedNumPinMisMatch)
|
||||||
|
ls.DbRs.AddLocalFunc("validate_blocked_number", appHandlers.ValidateBlockedNumber)
|
||||||
|
ls.DbRs.AddLocalFunc("retrieve_blocked_number", appHandlers.RetrieveBlockedNumber)
|
||||||
|
ls.DbRs.AddLocalFunc("reset_unregistered_number", appHandlers.ResetUnregisteredNumber)
|
||||||
|
ls.DbRs.AddLocalFunc("reset_others_pin", appHandlers.ResetOthersPin)
|
||||||
|
ls.DbRs.AddLocalFunc("save_others_temporary_pin", appHandlers.SaveOthersTemporaryPin)
|
||||||
|
ls.DbRs.AddLocalFunc("get_current_profile_info", appHandlers.GetCurrentProfileInfo)
|
||||||
|
ls.DbRs.AddLocalFunc("check_transactions", appHandlers.CheckTransactions)
|
||||||
|
ls.DbRs.AddLocalFunc("get_transactions", appHandlers.GetTransactionsList)
|
||||||
|
ls.DbRs.AddLocalFunc("view_statement", appHandlers.ViewTransactionStatement)
|
||||||
|
ls.DbRs.AddLocalFunc("update_all_profile_items", appHandlers.UpdateAllProfileItems)
|
||||||
|
ls.DbRs.AddLocalFunc("set_back", appHandlers.SetBack)
|
||||||
|
ls.DbRs.AddLocalFunc("show_blocked_account", appHandlers.ShowBlockedAccount)
|
||||||
|
|
||||||
|
return appHandlers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: enable setting of sessionId on engine init time
|
||||||
|
func (ls *LocalHandlerService) GetEngine() *engine.DefaultEngine {
|
||||||
|
en := engine.NewEngine(ls.Cfg, ls.Rs)
|
||||||
|
en = en.WithPersister(ls.Pe)
|
||||||
|
return en
|
||||||
|
}
|
@ -10,7 +10,7 @@ import (
|
|||||||
"git.defalsify.org/vise.git/persist"
|
"git.defalsify.org/vise.git/persist"
|
||||||
"git.defalsify.org/vise.git/resource"
|
"git.defalsify.org/vise.git/resource"
|
||||||
|
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/storage"
|
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
120
internal/http/at/parse.go
Normal file
120
internal/http/at/parse.go
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
package at
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.grassecon.net/urdt/ussd/common"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ATRequestParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (arp *ATRequestParser) GetSessionId(ctx context.Context, rq any) (string, error) {
|
||||||
|
rqv, ok := rq.(*http.Request)
|
||||||
|
if !ok {
|
||||||
|
logg.Warnf("got an invalid request", "req", rq)
|
||||||
|
return "", handlers.ErrInvalidRequest
|
||||||
|
}
|
||||||
|
// Capture body (if any) for logging
|
||||||
|
body, err := io.ReadAll(rqv.Body)
|
||||||
|
if err != nil {
|
||||||
|
logg.Warnf("failed to read request body", "err", err)
|
||||||
|
return "", fmt.Errorf("failed to read request body: %v", err)
|
||||||
|
}
|
||||||
|
// Reset the body for further reading
|
||||||
|
rqv.Body = io.NopCloser(bytes.NewReader(body))
|
||||||
|
|
||||||
|
// Log the body as JSON
|
||||||
|
bodyLog := map[string]string{"body": string(body)}
|
||||||
|
logBytes, err := json.Marshal(bodyLog)
|
||||||
|
if err != nil {
|
||||||
|
logg.Warnf("failed to marshal request body", "err", err)
|
||||||
|
} else {
|
||||||
|
decodedStr := string(logBytes)
|
||||||
|
sessionId, err := extractATSessionId(decodedStr)
|
||||||
|
if err != nil {
|
||||||
|
ctx = context.WithValue(ctx, "AT-SessionId", sessionId)
|
||||||
|
}
|
||||||
|
logg.DebugCtxf(ctx, "Received request:", decodedStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rqv.ParseForm(); err != nil {
|
||||||
|
logg.Warnf("failed to parse form data", "err", err)
|
||||||
|
return "", fmt.Errorf("failed to parse form data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
phoneNumber := rqv.FormValue("phoneNumber")
|
||||||
|
if phoneNumber == "" {
|
||||||
|
return "", fmt.Errorf("no phone number found")
|
||||||
|
}
|
||||||
|
|
||||||
|
formattedNumber, err := common.FormatPhoneNumber(phoneNumber)
|
||||||
|
if err != nil {
|
||||||
|
logg.Warnf("failed to format phone number", "err", err)
|
||||||
|
return "", fmt.Errorf("failed to format number")
|
||||||
|
}
|
||||||
|
|
||||||
|
return formattedNumber, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (arp *ATRequestParser) GetInput(rq any) ([]byte, error) {
|
||||||
|
rqv, ok := rq.(*http.Request)
|
||||||
|
if !ok {
|
||||||
|
return nil, handlers.ErrInvalidRequest
|
||||||
|
}
|
||||||
|
if err := rqv.ParseForm(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse form data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
text := rqv.FormValue("text")
|
||||||
|
|
||||||
|
parts := strings.Split(text, "*")
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return nil, fmt.Errorf("no input found")
|
||||||
|
}
|
||||||
|
|
||||||
|
trimmedInput := strings.TrimSpace(parts[len(parts)-1])
|
||||||
|
return []byte(trimmedInput), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseQueryParams(query string) map[string]string {
|
||||||
|
params := make(map[string]string)
|
||||||
|
|
||||||
|
queryParams := strings.Split(query, "&")
|
||||||
|
for _, param := range queryParams {
|
||||||
|
// Split each key-value pair by '='
|
||||||
|
parts := strings.SplitN(param, "=", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
params[parts[0]] = parts[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractATSessionId(decodedStr string) (string, error) {
|
||||||
|
var data map[string]string
|
||||||
|
err := json.Unmarshal([]byte(decodedStr), &data)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logg.Errorf("Error unmarshalling JSON: %v", err)
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
decodedBody, err := url.QueryUnescape(data["body"])
|
||||||
|
if err != nil {
|
||||||
|
logg.Errorf("Error URL-decoding body: %v", err)
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
params := parseQueryParams(decodedBody)
|
||||||
|
|
||||||
|
sessionId := params["sessionId"]
|
||||||
|
return sessionId, nil
|
||||||
|
|
||||||
|
}
|
98
internal/http/at/server.go
Normal file
98
internal/http/at/server.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package at
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.defalsify.org/vise.git/logging"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||||
|
httpserver "git.grassecon.net/urdt/ussd/internal/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
logg = logging.NewVanilla().WithDomain("atserver").WithContextKey("SessionId").WithContextKey("AT-SessionId")
|
||||||
|
)
|
||||||
|
|
||||||
|
type ATSessionHandler struct {
|
||||||
|
*httpserver.SessionHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewATSessionHandler(h handlers.RequestHandler) *ATSessionHandler {
|
||||||
|
return &ATSessionHandler{
|
||||||
|
SessionHandler: httpserver.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.Context(), 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.Flush(rqs.Ctx, rqs.Writer)
|
||||||
|
return rqs, err
|
||||||
|
}
|
234
internal/http/at/server_test.go
Normal file
234
internal/http/at/server_test.go
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
package at
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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/testutil/mocks/httpmocks"
|
||||||
|
)
|
||||||
|
|
||||||
|
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.FlushFunc = 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{
|
||||||
|
FlushFunc: 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{
|
||||||
|
FlushFunc: func(context.Context, io.Writer) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Writer: &httpmocks.MockWriter{},
|
||||||
|
},
|
||||||
|
expectedPrefix: "END ",
|
||||||
|
expectedError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Flush error",
|
||||||
|
input: handlers.RequestSession{
|
||||||
|
Continue: true,
|
||||||
|
Engine: &httpmocks.MockEngine{
|
||||||
|
FlushFunc: 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -5,7 +5,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/internal/handlers"
|
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DefaultRequestParser struct {
|
type DefaultRequestParser struct {
|
91
internal/http/server.go
Normal file
91
internal/http/server.go
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.defalsify.org/vise.git/logging"
|
||||||
|
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
logg = logging.NewVanilla().WithDomain("httpserver")
|
||||||
|
)
|
||||||
|
|
||||||
|
type SessionHandler struct {
|
||||||
|
handlers.RequestHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToSessionHandler(h handlers.RequestHandler) *SessionHandler {
|
||||||
|
return &SessionHandler{
|
||||||
|
RequestHandler: h,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *SessionHandler) WriteError(w http.ResponseWriter, code int, err error) {
|
||||||
|
s := err.Error()
|
||||||
|
w.Header().Set("Content-Length", strconv.Itoa(len(s)))
|
||||||
|
w.WriteHeader(code)
|
||||||
|
_, err = w.Write([]byte(s))
|
||||||
|
if err != nil {
|
||||||
|
logg.Errorf("error writing error!!", "err", err, "olderr", s)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
var code int
|
||||||
|
var err error
|
||||||
|
var perr error
|
||||||
|
|
||||||
|
rqs := handlers.RequestSession{
|
||||||
|
Ctx: req.Context(),
|
||||||
|
Writer: w,
|
||||||
|
}
|
||||||
|
|
||||||
|
rp := f.GetRequestParser()
|
||||||
|
cfg := f.GetConfig()
|
||||||
|
cfg.SessionId, err = rp.GetSessionId(req.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", 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
|
||||||
|
}
|
||||||
|
|
||||||
|
rqs, err = f.Process(rqs)
|
||||||
|
switch err {
|
||||||
|
case handlers.ErrStorage:
|
||||||
|
code = 500
|
||||||
|
case handlers.ErrEngineInit:
|
||||||
|
code = 500
|
||||||
|
case handlers.ErrEngineExec:
|
||||||
|
code = 500
|
||||||
|
default:
|
||||||
|
code = 200
|
||||||
|
}
|
||||||
|
|
||||||
|
if code != 200 {
|
||||||
|
f.WriteError(w, 500, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(200)
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
rqs, err = f.Output(rqs)
|
||||||
|
rqs, perr = f.Reset(rqs)
|
||||||
|
if err != nil {
|
||||||
|
f.WriteError(w, 500, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if perr != nil {
|
||||||
|
f.WriteError(w, 500, perr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
@ -9,9 +9,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.defalsify.org/vise.git/engine"
|
"git.defalsify.org/vise.git/engine"
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/internal/handlers"
|
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/testutil/mocks/httpmocks"
|
"git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks"
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/request"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// invalidRequestType is a custom type to test invalid request scenarios
|
// invalidRequestType is a custom type to test invalid request scenarios
|
||||||
@ -82,16 +81,16 @@ func TestSessionHandler_ServeHTTP(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mockRequestHandler := &httpmocks.MockRequestHandler{
|
mockRequestHandler := &httpmocks.MockRequestHandler{
|
||||||
ProcessFunc: func(rs request.RequestSession) (request.RequestSession, error) {
|
ProcessFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||||
return rs, tt.processErr
|
return rs, tt.processErr
|
||||||
},
|
},
|
||||||
OutputFunc: func(rs request.RequestSession) (request.RequestSession, error) {
|
OutputFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||||
return rs, tt.outputErr
|
return rs, tt.outputErr
|
||||||
},
|
},
|
||||||
ResetFunc: func(rs request.RequestSession) (request.RequestSession, error) {
|
ResetFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||||
return rs, tt.resetErr
|
return rs, tt.resetErr
|
||||||
},
|
},
|
||||||
GetRequestParserFunc: func() request.RequestParser {
|
GetRequestParserFunc: func() handlers.RequestParser {
|
||||||
return mockRequestParser
|
return mockRequestParser
|
||||||
},
|
},
|
||||||
GetConfigFunc: func() engine.Config {
|
GetConfigFunc: func() engine.Config {
|
||||||
@ -99,9 +98,7 @@ func TestSessionHandler_ServeHTTP(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionHandler := &HTTPSessionHandler{
|
sessionHandler := ToSessionHandler(mockRequestHandler)
|
||||||
RequestHandler: mockRequestHandler,
|
|
||||||
}
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.input))
|
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.input))
|
||||||
req.Header.Set("X-Vise-Session", tt.sessionID)
|
req.Header.Set("X-Vise-Session", tt.sessionID)
|
||||||
@ -119,7 +116,7 @@ func TestSessionHandler_ServeHTTP(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSessionHandler_WriteError(t *testing.T) {
|
func TestSessionHandler_WriteError(t *testing.T) {
|
||||||
handler := &HTTPSessionHandler{}
|
handler := &SessionHandler{}
|
||||||
mockWriter := &httpmocks.MockWriter{}
|
mockWriter := &httpmocks.MockWriter{}
|
||||||
err := errors.New("test error")
|
err := errors.New("test error")
|
||||||
|
|
65
internal/ssh/keystore.go
Normal file
65
internal/ssh/keystore.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
|
||||||
|
"git.defalsify.org/vise.git/db"
|
||||||
|
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||||
|
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db/gdbm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SshKeyStore struct {
|
||||||
|
store db.Db
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSshKeyStore(ctx context.Context, dbDir string) (*SshKeyStore, error) {
|
||||||
|
keyStore := &SshKeyStore{}
|
||||||
|
keyStoreFile := path.Join(dbDir, "ssh_authorized_keys.gdbm")
|
||||||
|
keyStore.store = dbstorage.NewThreadGdbmDb()
|
||||||
|
err := keyStore.store.Connect(ctx, keyStoreFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return keyStore, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func(s *SshKeyStore) AddFromFile(ctx context.Context, fp string, sessionId string) error {
|
||||||
|
_, err := os.Stat(fp)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot open ssh server public key file: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
publicBytes, err := os.ReadFile(fp)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to load public key: %v", err)
|
||||||
|
}
|
||||||
|
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(publicBytes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to parse public key: %v", err)
|
||||||
|
}
|
||||||
|
k := append([]byte{0x01}, pubKey.Marshal()...)
|
||||||
|
s.store.SetPrefix(storage.DATATYPE_EXTEND)
|
||||||
|
logg.Infof("Added key", "sessionId", sessionId, "public key", string(publicBytes))
|
||||||
|
return s.store.Put(ctx, k, []byte(sessionId))
|
||||||
|
}
|
||||||
|
|
||||||
|
func(s *SshKeyStore) Get(ctx context.Context, pubKey ssh.PublicKey) (string, error) {
|
||||||
|
s.store.SetLanguage(nil)
|
||||||
|
s.store.SetPrefix(storage.DATATYPE_EXTEND)
|
||||||
|
k := append([]byte{0x01}, pubKey.Marshal()...)
|
||||||
|
v, err := s.store.Get(ctx, k)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(v), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func(s *SshKeyStore) Close() error {
|
||||||
|
return s.store.Close()
|
||||||
|
}
|
284
internal/ssh/ssh.go
Normal file
284
internal/ssh/ssh.go
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
|
||||||
|
"git.defalsify.org/vise.git/engine"
|
||||||
|
"git.defalsify.org/vise.git/logging"
|
||||||
|
"git.defalsify.org/vise.git/resource"
|
||||||
|
"git.defalsify.org/vise.git/state"
|
||||||
|
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||||
|
"git.grassecon.net/urdt/ussd/remote"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
logg = logging.NewVanilla().WithDomain("ssh")
|
||||||
|
)
|
||||||
|
|
||||||
|
type auther struct {
|
||||||
|
Ctx context.Context
|
||||||
|
keyStore *SshKeyStore
|
||||||
|
auth map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuther(ctx context.Context, keyStore *SshKeyStore) *auther {
|
||||||
|
return &auther{
|
||||||
|
Ctx: ctx,
|
||||||
|
keyStore: keyStore,
|
||||||
|
auth: make(map[string]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func(a *auther) Check(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
|
||||||
|
logg.TraceCtxf(a.Ctx, "looking for publickey", "pubkey", fmt.Sprintf("%x", pubKey))
|
||||||
|
va, err := a.keyStore.Get(a.Ctx, pubKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ka := hex.EncodeToString(conn.SessionID())
|
||||||
|
a.auth[ka] = va
|
||||||
|
fmt.Fprintf(os.Stderr, "connect: %s -> %s\n", ka, va)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func(a *auther) FromConn(c *ssh.ServerConn) (string, error) {
|
||||||
|
if c == nil {
|
||||||
|
return "", errors.New("nil server conn")
|
||||||
|
}
|
||||||
|
if c.Conn == nil {
|
||||||
|
return "", errors.New("nil underlying conn")
|
||||||
|
}
|
||||||
|
return a.Get(c.Conn.SessionID())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func(a *auther) Get(k []byte) (string, error) {
|
||||||
|
ka := hex.EncodeToString(k)
|
||||||
|
v, ok := a.auth[ka]
|
||||||
|
if !ok {
|
||||||
|
return "", errors.New("not found")
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type SshRunner struct {
|
||||||
|
Ctx context.Context
|
||||||
|
Cfg engine.Config
|
||||||
|
FlagFile string
|
||||||
|
Conn storage.ConnData
|
||||||
|
ResourceDir string
|
||||||
|
Debug bool
|
||||||
|
SrvKeyFile string
|
||||||
|
Host string
|
||||||
|
Port uint
|
||||||
|
wg sync.WaitGroup
|
||||||
|
lst net.Listener
|
||||||
|
}
|
||||||
|
|
||||||
|
func(s *SshRunner) serve(ctx context.Context, sessionId string, ch ssh.NewChannel, en engine.Engine) error {
|
||||||
|
if ch == nil {
|
||||||
|
return errors.New("nil channel")
|
||||||
|
}
|
||||||
|
if ch.ChannelType() != "session" {
|
||||||
|
ch.Reject(ssh.UnknownChannelType, "that is not the channel you are looking for")
|
||||||
|
return errors.New("not a session")
|
||||||
|
}
|
||||||
|
channel, requests, err := ch.Accept()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer channel.Close()
|
||||||
|
s.wg.Add(1)
|
||||||
|
go func(reqIn <-chan *ssh.Request) {
|
||||||
|
defer s.wg.Done()
|
||||||
|
for req := range reqIn {
|
||||||
|
req.Reply(req.Type == "shell", nil)
|
||||||
|
}
|
||||||
|
_ = requests
|
||||||
|
}(requests)
|
||||||
|
|
||||||
|
cont, err := en.Exec(ctx, []byte{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("initial engine exec err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var input [state.INPUT_LIMIT]byte
|
||||||
|
for cont {
|
||||||
|
c, err := en.Flush(ctx, channel)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("flush err: %v", err)
|
||||||
|
}
|
||||||
|
_, err = channel.Write([]byte{0x0a})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("newline err: %v", err)
|
||||||
|
}
|
||||||
|
c, err = channel.Read(input[:])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read input fail: %v", err)
|
||||||
|
}
|
||||||
|
logg.TraceCtxf(ctx, "input read", "c", c, "input", input[:c-1])
|
||||||
|
cont, err = en.Exec(ctx, input[:c-1])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("engine exec err: %v", err)
|
||||||
|
}
|
||||||
|
logg.TraceCtxf(ctx, "exec cont", "cont", cont, "en", en)
|
||||||
|
_ = c
|
||||||
|
}
|
||||||
|
c, err := en.Flush(ctx, channel)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("last flush err: %v", err)
|
||||||
|
}
|
||||||
|
_ = c
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func(s *SshRunner) Stop() error {
|
||||||
|
return s.lst.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func(s *SshRunner) GetEngine(sessionId string) (engine.Engine, func(), error) {
|
||||||
|
ctx := s.Ctx
|
||||||
|
menuStorageService := storage.NewMenuStorageService(s.Conn, s.ResourceDir)
|
||||||
|
|
||||||
|
rs, err := menuStorageService.GetResource(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pe, err := menuStorageService.GetPersister(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
userdatastore, err := menuStorageService.GetUserdataDb(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dbResource, ok := rs.(*resource.DbResource)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lhs, err := handlers.NewLocalHandlerService(ctx, s.FlagFile, true, dbResource, s.Cfg, rs)
|
||||||
|
lhs.SetDataStore(&userdatastore)
|
||||||
|
lhs.SetPersister(pe)
|
||||||
|
lhs.Cfg.SessionId = sessionId
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: clear up why pointer here and by-value other cmds
|
||||||
|
accountService := &remote.AccountService{}
|
||||||
|
hl, err := lhs.GetHandler(accountService)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
en := lhs.GetEngine()
|
||||||
|
en = en.WithFirst(hl.Init)
|
||||||
|
if s.Debug {
|
||||||
|
en = en.WithDebug(nil)
|
||||||
|
}
|
||||||
|
// TODO: this is getting very hacky!
|
||||||
|
closer := func() {
|
||||||
|
err := menuStorageService.Close()
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(ctx, "menu storage service cleanup fail", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return en, closer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// adapted example from crypto/ssh package, NewServerConn doc
|
||||||
|
func(s *SshRunner) Run(ctx context.Context, keyStore *SshKeyStore) {
|
||||||
|
s.Ctx = ctx
|
||||||
|
running := true
|
||||||
|
|
||||||
|
// TODO: waitgroup should probably not be global
|
||||||
|
defer s.wg.Wait()
|
||||||
|
|
||||||
|
auth := NewAuther(ctx, keyStore)
|
||||||
|
cfg := ssh.ServerConfig{
|
||||||
|
PublicKeyCallback: auth.Check,
|
||||||
|
}
|
||||||
|
|
||||||
|
privateBytes, err := os.ReadFile(s.SrvKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(ctx, "Failed to load private key", "err", err)
|
||||||
|
}
|
||||||
|
private, err := ssh.ParsePrivateKey(privateBytes)
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(ctx, "Failed to parse private key", "err", err)
|
||||||
|
}
|
||||||
|
srvPub := private.PublicKey()
|
||||||
|
srvPubStr := base64.StdEncoding.EncodeToString(srvPub.Marshal())
|
||||||
|
logg.InfoCtxf(ctx, "have server key", "type", srvPub.Type(), "public", srvPubStr)
|
||||||
|
cfg.AddHostKey(private)
|
||||||
|
|
||||||
|
s.lst, err = net.Listen("tcp", fmt.Sprintf("%s:%d", s.Host, s.Port))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for running {
|
||||||
|
conn, err := s.lst.Accept()
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(ctx, "ssh accept error", "err", err)
|
||||||
|
running = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
go func(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
for true {
|
||||||
|
srvConn, nC, rC, err := ssh.NewServerConn(conn, &cfg)
|
||||||
|
if err != nil {
|
||||||
|
logg.InfoCtxf(ctx, "rejected client", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logg.DebugCtxf(ctx, "ssh client connected", "conn", srvConn)
|
||||||
|
|
||||||
|
s.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
ssh.DiscardRequests(rC)
|
||||||
|
s.wg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
sessionId, err := auth.FromConn(srvConn)
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(ctx, "Cannot find authentication")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
en, closer, err := s.GetEngine(sessionId)
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(ctx, "engine won't start", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := en.Finish()
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(ctx, "engine won't stop", "err", err)
|
||||||
|
}
|
||||||
|
closer()
|
||||||
|
}()
|
||||||
|
for ch := range nC {
|
||||||
|
err = s.serve(ctx, sessionId, ch, en)
|
||||||
|
logg.ErrorCtxf(ctx, "ssh server finish", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(conn)
|
||||||
|
}
|
||||||
|
}
|
@ -5,8 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
|
||||||
"git.defalsify.org/vise.git/db"
|
"git.defalsify.org/vise.git/db"
|
||||||
fsdb "git.defalsify.org/vise.git/db/fs"
|
fsdb "git.defalsify.org/vise.git/db/fs"
|
||||||
"git.defalsify.org/vise.git/db/postgres"
|
"git.defalsify.org/vise.git/db/postgres"
|
||||||
@ -14,7 +13,8 @@ import (
|
|||||||
"git.defalsify.org/vise.git/logging"
|
"git.defalsify.org/vise.git/logging"
|
||||||
"git.defalsify.org/vise.git/persist"
|
"git.defalsify.org/vise.git/persist"
|
||||||
"git.defalsify.org/vise.git/resource"
|
"git.defalsify.org/vise.git/resource"
|
||||||
gdbmstorage "git.grassecon.net/grassrootseconomics/visedriver/storage/db/gdbm"
|
gdbmstorage "git.grassecon.net/urdt/ussd/internal/storage/db/gdbm"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -43,11 +43,6 @@ func NewMenuStorageService(conn ConnData, resourceDir string) *MenuStorageServic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ms *MenuStorageService) WithResourceDir(resourceDir string) *MenuStorageService {
|
|
||||||
ms.resourceDir = resourceDir
|
|
||||||
return ms
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.Db, section string) (db.Db, error) {
|
func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.Db, section string) (db.Db, error) {
|
||||||
var newDb db.Db
|
var newDb db.Db
|
||||||
var err error
|
var err error
|
209
internal/testutil/engine.go
Normal file
209
internal/testutil/engine.go
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
package testutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.defalsify.org/vise.git/engine"
|
||||||
|
"git.defalsify.org/vise.git/logging"
|
||||||
|
"git.defalsify.org/vise.git/resource"
|
||||||
|
"git.grassecon.net/urdt/ussd/config"
|
||||||
|
"git.grassecon.net/urdt/ussd/initializers"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/testutil/testservice"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/testutil/testtag"
|
||||||
|
"git.grassecon.net/urdt/ussd/remote"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
testdataloader "github.com/peteole/testdata-loader"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
logg = logging.NewVanilla()
|
||||||
|
baseDir = testdataloader.GetBasePath()
|
||||||
|
scriptDir = path.Join(baseDir, "services", "registration")
|
||||||
|
setDbType string
|
||||||
|
setConnStr string
|
||||||
|
setDbSchema string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
initializers.LoadEnvVariablesPath(baseDir)
|
||||||
|
config.LoadConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDatabase updates the database used by TestEngine
|
||||||
|
func SetDatabase(database, connStr, dbSchema string) {
|
||||||
|
setDbType = database
|
||||||
|
setConnStr = connStr
|
||||||
|
setDbSchema = dbSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanDatabase removes all test data from the database
|
||||||
|
func CleanDatabase() {
|
||||||
|
if setDbType == "postgres" {
|
||||||
|
ctx := context.Background()
|
||||||
|
// Update the connection string with the new search path
|
||||||
|
updatedConnStr, err := updateSearchPath(setConnStr, setDbSchema)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to update search path: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbConn, err := pgxpool.New(ctx, updatedConnStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to connect to database for cleanup: %v", err)
|
||||||
|
}
|
||||||
|
defer dbConn.Close()
|
||||||
|
|
||||||
|
query := fmt.Sprintf("DELETE FROM %s.kv_vise;", setDbSchema)
|
||||||
|
_, execErr := dbConn.Exec(ctx, query)
|
||||||
|
if execErr != nil {
|
||||||
|
log.Printf("Failed to cleanup table %s.kv_vise: %v", setDbSchema, execErr)
|
||||||
|
} else {
|
||||||
|
log.Printf("Successfully cleaned up table %s.kv_vise", setDbSchema)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setConnStr, _ := filepath.Abs(setConnStr)
|
||||||
|
if err := os.RemoveAll(setConnStr); err != nil {
|
||||||
|
log.Fatalf("Failed to delete state store %s: %v", setConnStr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateSearchPath updates the search_path (schema) to be used in the connection
|
||||||
|
func updateSearchPath(connStr string, newSearchPath string) (string, error) {
|
||||||
|
u, err := url.Parse(connStr)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("invalid connection string: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the query parameters
|
||||||
|
q := u.Query()
|
||||||
|
|
||||||
|
// Update or add the search_path parameter
|
||||||
|
q.Set("search_path", newSearchPath)
|
||||||
|
|
||||||
|
// Rebuild the connection string with updated parameters
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
return u.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEngine(sessionId string) (engine.Engine, func(), chan bool) {
|
||||||
|
var err error
|
||||||
|
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(128),
|
||||||
|
}
|
||||||
|
|
||||||
|
if setDbType == "postgres" {
|
||||||
|
setConnStr = config.DbConn
|
||||||
|
setConnStr, err = updateSearchPath(setConnStr, setDbSchema)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setConnStr, err = filepath.Abs(setConnStr)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := storage.ToConnData(setConnStr)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "connstr parse err: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceDir := scriptDir
|
||||||
|
menuStorageService := storage.NewMenuStorageService(conn, resourceDir)
|
||||||
|
|
||||||
|
rs, err := menuStorageService.GetResource(ctx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "resource error: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pe, err := menuStorageService.GetPersister(ctx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "persister error: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
userDataStore, err := menuStorageService.GetUserdataDb(ctx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "userdb error: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbResource, ok := rs.(*resource.DbResource)
|
||||||
|
if !ok {
|
||||||
|
fmt.Fprintf(os.Stderr, "dbresource cast error")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs)
|
||||||
|
lhs.SetDataStore(&userDataStore)
|
||||||
|
lhs.SetPersister(pe)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if testtag.AccountService == nil {
|
||||||
|
testtag.AccountService = &remote.AccountService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch testtag.AccountService.(type) {
|
||||||
|
case *testservice.TestAccountService:
|
||||||
|
go func() {
|
||||||
|
eventChannel <- false
|
||||||
|
}()
|
||||||
|
case *remote.AccountService:
|
||||||
|
go func() {
|
||||||
|
time.Sleep(5 * time.Second) // Wait for 5 seconds
|
||||||
|
eventChannel <- true
|
||||||
|
}()
|
||||||
|
default:
|
||||||
|
panic("Unknown account service type")
|
||||||
|
}
|
||||||
|
|
||||||
|
hl, err := lhs.GetHandler(testtag.AccountService)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
en := lhs.GetEngine()
|
||||||
|
en = en.WithFirst(hl.Init)
|
||||||
|
cleanFn := func() {
|
||||||
|
err := en.Finish()
|
||||||
|
if err != nil {
|
||||||
|
logg.Errorf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = menuStorageService.Close()
|
||||||
|
if err != nil {
|
||||||
|
logg.Errorf(err.Error())
|
||||||
|
}
|
||||||
|
logg.Infof("testengine storage closed")
|
||||||
|
}
|
||||||
|
return en, cleanFn, eventChannel
|
||||||
|
}
|
15
internal/testutil/engine_test.go
Normal file
15
internal/testutil/engine_test.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package testutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateEngine(t *testing.T) {
|
||||||
|
o, clean, eventC := TestEngine("foo")
|
||||||
|
defer clean()
|
||||||
|
defer func() {
|
||||||
|
<-eventC
|
||||||
|
close(eventC)
|
||||||
|
}()
|
||||||
|
_ = o
|
||||||
|
}
|
47
internal/testutil/mocks/httpmocks/request_handler_mock.go
Normal file
47
internal/testutil/mocks/httpmocks/request_handler_mock.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
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()
|
||||||
|
}
|
54
internal/testutil/mocks/service_mock.go
Normal file
54
internal/testutil/mocks/service_mock.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"git.grassecon.net/urdt/ussd/models"
|
||||||
|
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockAccountService implements AccountServiceInterface for testing
|
||||||
|
type MockAccountService struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockAccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Get(0).(*models.AccountResult), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) {
|
||||||
|
args := m.Called(publicKey)
|
||||||
|
return args.Get(0).(*models.BalanceResult), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockAccountService) TrackAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResult, error) {
|
||||||
|
args := m.Called(trackingId)
|
||||||
|
return args.Get(0).(*models.TrackStatusResult), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) {
|
||||||
|
args := m.Called(publicKey)
|
||||||
|
return args.Get(0).([]dataserviceapi.TokenHoldings), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockAccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) {
|
||||||
|
args := m.Called(publicKey)
|
||||||
|
return args.Get(0).([]dataserviceapi.Last10TxResponse), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockAccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) {
|
||||||
|
args := m.Called(address)
|
||||||
|
return args.Get(0).(*models.VoucherDataResult), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockAccountService) TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Get(0).(*models.TokenTransferResponse), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockAccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) {
|
||||||
|
args := m.Called(alias)
|
||||||
|
return args.Get(0).(*dataserviceapi.AliasAddress), args.Error(1)
|
||||||
|
}
|
62
internal/testutil/testservice/account_service.go
Normal file
62
internal/testutil/testservice/account_service.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package testservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"git.grassecon.net/urdt/ussd/models"
|
||||||
|
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestAccountService struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tas *TestAccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) {
|
||||||
|
return &models.AccountResult{
|
||||||
|
TrackingId: "075ccc86-f6ef-4d33-97d5-e91cfb37aa0d",
|
||||||
|
PublicKey: "0x623EFAFa8868df4B934dd12a8B26CB3Dd75A7AdD",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tas *TestAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) {
|
||||||
|
balanceResponse := &models.BalanceResult{
|
||||||
|
Balance: "0.003 CELO",
|
||||||
|
Nonce: json.Number("0"),
|
||||||
|
}
|
||||||
|
return balanceResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tas *TestAccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error) {
|
||||||
|
return &models.TrackStatusResult{
|
||||||
|
Active: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) {
|
||||||
|
return []dataserviceapi.TokenHoldings{
|
||||||
|
dataserviceapi.TokenHoldings{
|
||||||
|
ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee",
|
||||||
|
TokenSymbol: "SRF",
|
||||||
|
TokenDecimals: "6",
|
||||||
|
Balance: "2745987",
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tas *TestAccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) {
|
||||||
|
return []dataserviceapi.Last10TxResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m TestAccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) {
|
||||||
|
return &models.VoucherDataResult{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tas *TestAccountService) TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) {
|
||||||
|
return &models.TokenTransferResponse{
|
||||||
|
TrackingId: "e034d147-747d-42ea-928d-b5a7cb3426af",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m TestAccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) {
|
||||||
|
return &dataserviceapi.AliasAddress{}, nil
|
||||||
|
}
|
12
internal/testutil/testtag/offlinetest.go
Normal file
12
internal/testutil/testtag/offlinetest.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// +build !online
|
||||||
|
|
||||||
|
package testtag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.grassecon.net/urdt/ussd/remote"
|
||||||
|
accountservice "git.grassecon.net/urdt/ussd/internal/testutil/testservice"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
AccountService remote.AccountServiceInterface = &accountservice.TestAccountService{}
|
||||||
|
)
|
9
internal/testutil/testtag/onlinetest.go
Normal file
9
internal/testutil/testtag/onlinetest.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// +build online
|
||||||
|
|
||||||
|
package testtag
|
||||||
|
|
||||||
|
import "git.grassecon.net/urdt/ussd/remote"
|
||||||
|
|
||||||
|
var (
|
||||||
|
AccountService remote.AccountServiceInterface
|
||||||
|
)
|
443
menutraversal_test/group_test.json
Normal file
443
menutraversal_test/group_test.json
Normal file
@ -0,0 +1,443 @@
|
|||||||
|
{
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"name": "my_account_change_pin",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"input": "",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "3",
|
||||||
|
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "5",
|
||||||
|
"expectedContent": "PIN Management\n1:Change PIN\n2:Reset other's PIN\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "Enter your old PIN\n\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1234",
|
||||||
|
"expectedContent": "Enter a new four number PIN:\n\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1234",
|
||||||
|
"expectedContent": "Confirm your new PIN:\n\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1234",
|
||||||
|
"expectedContent": "Your PIN change request has been successful\n\n0:Back\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "menu_my_account_language_change",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"input": "",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "3",
|
||||||
|
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "2",
|
||||||
|
"expectedContent": "Please enter your PIN:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1235",
|
||||||
|
"expectedContent": "Incorrect PIN. You have: 2 remaining attempt(s).\n1:Retry\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "Please enter your PIN:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1234",
|
||||||
|
"expectedContent": "Select language:\n1:English\n2:Kiswahili"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "Your language change request was successful.\n0:Back\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "menu_my_account_check_my_balance",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"input": "",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "3",
|
||||||
|
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "3",
|
||||||
|
"expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "Please enter your PIN:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1235",
|
||||||
|
"expectedContent": "Incorrect PIN. You have: 2 remaining attempt(s).\n1:Retry\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "Please enter your PIN:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1234",
|
||||||
|
"expectedContent": "Balance: {balance}\n\n0:Back\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "menu_my_account_check_community_balance",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"input": "",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "3",
|
||||||
|
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "3",
|
||||||
|
"expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "2",
|
||||||
|
"expectedContent": "Please enter your PIN:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1235",
|
||||||
|
"expectedContent": "Incorrect PIN. You have: 2 remaining attempt(s).\n1:Retry\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "Please enter your PIN:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1234",
|
||||||
|
"expectedContent": "{balance}\n0:Back\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "menu_my_account_edit_all_account_details_starting_from_firstname",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"input": "",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "3",
|
||||||
|
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "Enter your first names:\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "foo",
|
||||||
|
"expectedContent": "Enter family name:\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "bar",
|
||||||
|
"expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "Enter your year of birth\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1940",
|
||||||
|
"expectedContent": "Enter your location:\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "Kilifi",
|
||||||
|
"expectedContent": "Enter the services or goods you offer: \n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "Bananas",
|
||||||
|
"expectedContent": "Please enter your PIN:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1234",
|
||||||
|
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "menu_my_account_edit_familyname_when_all_account__details_have_been_set",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"input": "",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "3",
|
||||||
|
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "2",
|
||||||
|
"expectedContent": "Enter family name:\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "bar",
|
||||||
|
"expectedContent": "Please enter your PIN:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1234",
|
||||||
|
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "menu_my_account_edit_gender_when_all_account__details_have_been_set",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"input": "",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "3",
|
||||||
|
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "3",
|
||||||
|
"expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "Please enter your PIN:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1234",
|
||||||
|
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "menu_my_account_edit_yob_when_all_account__details_have_been_set",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"input": "",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "3",
|
||||||
|
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "4",
|
||||||
|
"expectedContent": "Enter your year of birth\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1945",
|
||||||
|
"expectedContent": "Please enter your PIN:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1234",
|
||||||
|
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "menu_my_account_edit_location_when_all_account_details_have_been_set",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"input": "",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "3",
|
||||||
|
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "5",
|
||||||
|
"expectedContent": "Enter your location:\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "Kilifi",
|
||||||
|
"expectedContent": "Please enter your PIN:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1234",
|
||||||
|
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "menu_my_account_edit_offerings_when_all_account__details_have_been_set",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"input": "",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "3",
|
||||||
|
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "6",
|
||||||
|
"expectedContent": "Enter the services or goods you offer: \n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "Bananas",
|
||||||
|
"expectedContent": "Please enter your PIN:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1234",
|
||||||
|
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "menu_my_account_view_profile",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"input": "",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "3",
|
||||||
|
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "7",
|
||||||
|
"expectedContent": "Please enter your PIN:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1234",
|
||||||
|
"expectedContent": "My profile:\nName: foo bar\nGender: male\nAge: 80\nLocation: Kilifi\nYou provide: Bananas\n\n0:Back\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
386
menutraversal_test/menu_traversal_test.go
Normal file
386
menutraversal_test/menu_traversal_test.go
Normal file
@ -0,0 +1,386 @@
|
|||||||
|
package menutraversaltest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/testutil"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/testutil/driver"
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testData = driver.ReadData()
|
||||||
|
sessionID string
|
||||||
|
src = rand.NewSource(42)
|
||||||
|
g = rand.New(src)
|
||||||
|
)
|
||||||
|
|
||||||
|
var groupTestFile = flag.String("test-file", "group_test.json", "The test file to use for running the group tests")
|
||||||
|
var database = flag.String("db", "gdbm", "Specify the database (gdbm or postgres)")
|
||||||
|
var connStr = flag.String("conn", ".test_state", "connection string")
|
||||||
|
var dbSchema = flag.String("schema", "test", "Specify the database schema (default test)")
|
||||||
|
|
||||||
|
func GenerateSessionId() string {
|
||||||
|
uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g))
|
||||||
|
v, err := uu.NewV4()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return v.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the public key from the engine response
|
||||||
|
func extractPublicKey(response []byte) string {
|
||||||
|
// Regex pattern to match the public key starting with 0x and 40 characters
|
||||||
|
re := regexp.MustCompile(`0x[a-fA-F0-9]{40}`)
|
||||||
|
match := re.Find(response)
|
||||||
|
if match != nil {
|
||||||
|
return string(match)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extracts the balance value from the engine response.
|
||||||
|
func extractBalance(response []byte) string {
|
||||||
|
// Regex to match "Balance: <amount> <symbol>" followed by a newline
|
||||||
|
re := regexp.MustCompile(`(?m)^Balance:\s+(\d+(\.\d+)?)\s+([A-Z]+)`)
|
||||||
|
match := re.FindSubmatch(response)
|
||||||
|
if match != nil {
|
||||||
|
return string(match[1]) + " " + string(match[3]) // "<amount> <symbol>"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extracts the Maximum amount value from the engine response.
|
||||||
|
func extractMaxAmount(response []byte) string {
|
||||||
|
// Regex to match "Maximum amount: <amount>" followed by a newline
|
||||||
|
re := regexp.MustCompile(`(?m)^Maximum amount:\s+(\d+(\.\d+)?)`)
|
||||||
|
match := re.FindSubmatch(response)
|
||||||
|
if match != nil {
|
||||||
|
return string(match[1]) // "<amount>"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extracts the send amount value from the engine response.
|
||||||
|
func extractSendAmount(response []byte) string {
|
||||||
|
// Regex to match the pattern "will receive X.XX SYM from"
|
||||||
|
re := regexp.MustCompile(`will receive (\d+\.\d{2}\s+[A-Z]+) from`)
|
||||||
|
match := re.FindSubmatch(response)
|
||||||
|
if match != nil {
|
||||||
|
return string(match[1]) // Returns "X.XX SYM"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
// Parse the flags
|
||||||
|
flag.Parse()
|
||||||
|
sessionID = GenerateSessionId()
|
||||||
|
// set the db
|
||||||
|
testutil.SetDatabase(*database, *connStr, *dbSchema)
|
||||||
|
|
||||||
|
// Cleanup the db after tests
|
||||||
|
defer testutil.CleanDatabase()
|
||||||
|
|
||||||
|
m.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccountCreationSuccessful(t *testing.T) {
|
||||||
|
en, fn, eventChannel := testutil.TestEngine(sessionID)
|
||||||
|
defer fn()
|
||||||
|
ctx := context.Background()
|
||||||
|
sessions := testData
|
||||||
|
for _, session := range sessions {
|
||||||
|
groups := driver.FilterGroupsByName(session.Groups, "account_creation_successful")
|
||||||
|
for _, group := range groups {
|
||||||
|
for _, step := range group.Steps {
|
||||||
|
cont, err := en.Exec(ctx, []byte(step.Input))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
|
||||||
|
}
|
||||||
|
if !cont {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
w := bytes.NewBuffer(nil)
|
||||||
|
_, err = en.Flush(ctx, w)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
|
||||||
|
}
|
||||||
|
b := w.Bytes()
|
||||||
|
match, err := step.MatchesExpectedContent(b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
|
||||||
|
}
|
||||||
|
if !match {
|
||||||
|
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<-eventChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccountRegistrationRejectTerms(t *testing.T) {
|
||||||
|
// Generate a new UUID for this edge case test
|
||||||
|
uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g))
|
||||||
|
v, err := uu.NewV4()
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
edgeCaseSessionID := v.String()
|
||||||
|
en, fn, _ := testutil.TestEngine(edgeCaseSessionID)
|
||||||
|
defer fn()
|
||||||
|
ctx := context.Background()
|
||||||
|
sessions := testData
|
||||||
|
for _, session := range sessions {
|
||||||
|
groups := driver.FilterGroupsByName(session.Groups, "account_creation_reject_terms")
|
||||||
|
for _, group := range groups {
|
||||||
|
for _, step := range group.Steps {
|
||||||
|
cont, err := en.Exec(ctx, []byte(step.Input))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !cont {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
w := bytes.NewBuffer(nil)
|
||||||
|
if _, err := en.Flush(ctx, w); err != nil {
|
||||||
|
t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b := w.Bytes()
|
||||||
|
match, err := step.MatchesExpectedContent(b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
|
||||||
|
}
|
||||||
|
if !match {
|
||||||
|
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMainMenuHelp(t *testing.T) {
|
||||||
|
en, fn, _ := testutil.TestEngine(sessionID)
|
||||||
|
defer fn()
|
||||||
|
ctx := context.Background()
|
||||||
|
sessions := testData
|
||||||
|
for _, session := range sessions {
|
||||||
|
groups := driver.FilterGroupsByName(session.Groups, "main_menu_help")
|
||||||
|
for _, group := range groups {
|
||||||
|
for _, step := range group.Steps {
|
||||||
|
cont, err := en.Exec(ctx, []byte(step.Input))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !cont {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
w := bytes.NewBuffer(nil)
|
||||||
|
if _, err := en.Flush(ctx, w); err != nil {
|
||||||
|
t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b := w.Bytes()
|
||||||
|
balance := extractBalance(b)
|
||||||
|
|
||||||
|
expectedContent := []byte(step.ExpectedContent)
|
||||||
|
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
|
||||||
|
|
||||||
|
step.ExpectedContent = string(expectedContent)
|
||||||
|
match, err := step.MatchesExpectedContent(b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
|
||||||
|
}
|
||||||
|
if !match {
|
||||||
|
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMainMenuQuit(t *testing.T) {
|
||||||
|
en, fn, _ := testutil.TestEngine(sessionID)
|
||||||
|
defer fn()
|
||||||
|
ctx := context.Background()
|
||||||
|
sessions := testData
|
||||||
|
for _, session := range sessions {
|
||||||
|
groups := driver.FilterGroupsByName(session.Groups, "main_menu_quit")
|
||||||
|
for _, group := range groups {
|
||||||
|
for _, step := range group.Steps {
|
||||||
|
cont, err := en.Exec(ctx, []byte(step.Input))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !cont {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
w := bytes.NewBuffer(nil)
|
||||||
|
if _, err := en.Flush(ctx, w); err != nil {
|
||||||
|
t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b := w.Bytes()
|
||||||
|
balance := extractBalance(b)
|
||||||
|
|
||||||
|
expectedContent := []byte(step.ExpectedContent)
|
||||||
|
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
|
||||||
|
|
||||||
|
step.ExpectedContent = string(expectedContent)
|
||||||
|
match, err := step.MatchesExpectedContent(b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
|
||||||
|
}
|
||||||
|
if !match {
|
||||||
|
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMyAccount_MyAddress(t *testing.T) {
|
||||||
|
en, fn, _ := testutil.TestEngine(sessionID)
|
||||||
|
defer fn()
|
||||||
|
ctx := context.Background()
|
||||||
|
sessions := testData
|
||||||
|
for _, session := range sessions {
|
||||||
|
groups := driver.FilterGroupsByName(session.Groups, "menu_my_account_my_address")
|
||||||
|
for _, group := range groups {
|
||||||
|
for index, step := range group.Steps {
|
||||||
|
t.Logf("step %v with input %v", index, step.Input)
|
||||||
|
cont, err := en.Exec(ctx, []byte(step.Input))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !cont {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
w := bytes.NewBuffer(nil)
|
||||||
|
if _, err := en.Flush(ctx, w); err != nil {
|
||||||
|
t.Errorf("Test case '%s' failed during Flush: %v", group.Name, err)
|
||||||
|
}
|
||||||
|
b := w.Bytes()
|
||||||
|
|
||||||
|
balance := extractBalance(b)
|
||||||
|
publicKey := extractPublicKey(b)
|
||||||
|
|
||||||
|
expectedContent := []byte(step.ExpectedContent)
|
||||||
|
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
|
||||||
|
expectedContent = bytes.Replace(expectedContent, []byte("{public_key}"), []byte(publicKey), -1)
|
||||||
|
|
||||||
|
step.ExpectedContent = string(expectedContent)
|
||||||
|
match, err := step.MatchesExpectedContent(b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
|
||||||
|
}
|
||||||
|
if !match {
|
||||||
|
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expectedContent, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMainMenuSend(t *testing.T) {
|
||||||
|
en, fn, _ := testutil.TestEngine(sessionID)
|
||||||
|
defer fn()
|
||||||
|
ctx := context.Background()
|
||||||
|
sessions := testData
|
||||||
|
for _, session := range sessions {
|
||||||
|
groups := driver.FilterGroupsByName(session.Groups, "send_with_invite")
|
||||||
|
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.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !cont {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
w := bytes.NewBuffer(nil)
|
||||||
|
if _, err := en.Flush(ctx, w); err != nil {
|
||||||
|
t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b := w.Bytes()
|
||||||
|
balance := extractBalance(b)
|
||||||
|
max_amount := extractMaxAmount(b)
|
||||||
|
send_amount := extractSendAmount(b)
|
||||||
|
|
||||||
|
expectedContent := []byte(step.ExpectedContent)
|
||||||
|
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
|
||||||
|
expectedContent = bytes.Replace(expectedContent, []byte("{max_amount}"), []byte(max_amount), -1)
|
||||||
|
expectedContent = bytes.Replace(expectedContent, []byte("{send_amount}"), []byte(send_amount), -1)
|
||||||
|
expectedContent = bytes.Replace(expectedContent, []byte("{session_id}"), []byte(sessionID), -1)
|
||||||
|
|
||||||
|
step.ExpectedContent = string(expectedContent)
|
||||||
|
match, err := step.MatchesExpectedContent(b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
|
||||||
|
}
|
||||||
|
if !match {
|
||||||
|
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGroups(t *testing.T) {
|
||||||
|
groups, err := driver.LoadTestGroups(*groupTestFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load test groups: %v", err)
|
||||||
|
}
|
||||||
|
en, fn, _ := testutil.TestEngine(sessionID)
|
||||||
|
defer fn()
|
||||||
|
ctx := context.Background()
|
||||||
|
// Create test cases from loaded groups
|
||||||
|
tests := driver.CreateTestCases(groups)
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.Name, func(t *testing.T) {
|
||||||
|
cont, err := en.Exec(ctx, []byte(tt.Input))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Test case '%s' failed at input '%s': %v", tt.Name, tt.Input, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !cont {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w := bytes.NewBuffer(nil)
|
||||||
|
if _, err := en.Flush(ctx, w); err != nil {
|
||||||
|
t.Errorf("Test case '%s' failed during Flush: %v", tt.Name, err)
|
||||||
|
}
|
||||||
|
b := w.Bytes()
|
||||||
|
balance := extractBalance(b)
|
||||||
|
|
||||||
|
expectedContent := []byte(tt.ExpectedContent)
|
||||||
|
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
|
||||||
|
|
||||||
|
tt.ExpectedContent = string(expectedContent)
|
||||||
|
|
||||||
|
match, err := tt.MatchesExpectedContent(b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error compiling regex for step '%s': %v", tt.Input, err)
|
||||||
|
}
|
||||||
|
if !match {
|
||||||
|
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", tt.ExpectedContent, b)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
68
menutraversal_test/profile_edit_start_familyname.json
Normal file
68
menutraversal_test/profile_edit_start_familyname.json
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"name": "menu_my_account_edit_all_account_details_starting_from_family_name",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"input": "",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "3",
|
||||||
|
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "2",
|
||||||
|
"expectedContent": "Enter family name:\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "bar",
|
||||||
|
"expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "Enter your year of birth\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1940",
|
||||||
|
"expectedContent": "Enter your location:\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "Kilifi",
|
||||||
|
"expectedContent": "Enter the services or goods you offer: \n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "Bananas",
|
||||||
|
"expectedContent": "Please enter your PIN:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1234",
|
||||||
|
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
61
menutraversal_test/profile_edit_start_firstname.json
Normal file
61
menutraversal_test/profile_edit_start_firstname.json
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"name": "menu_my_account_edit_all_account_details_starting_from_firstname",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"input": "",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "3",
|
||||||
|
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "Enter your first names:\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "foo",
|
||||||
|
"expectedContent": "Enter family name:\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "bar",
|
||||||
|
"expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "Enter your year of birth\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1940",
|
||||||
|
"expectedContent": "Enter your location:\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "Kilifi",
|
||||||
|
"expectedContent": "Enter the services or goods you offer: \n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "Bananas",
|
||||||
|
"expectedContent": "Please enter your PIN:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1234",
|
||||||
|
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
55
menutraversal_test/profile_edit_start_gender.json
Normal file
55
menutraversal_test/profile_edit_start_gender.json
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"name": "menu_my_account_edit_all_account_details_starting_from_gender",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"input": "",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "3",
|
||||||
|
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "3",
|
||||||
|
"expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "Enter your year of birth\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1940",
|
||||||
|
"expectedContent": "Enter your location:\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "Kilifi",
|
||||||
|
"expectedContent": "Enter the services or goods you offer: \n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "Bananas",
|
||||||
|
"expectedContent": "Please enter your PIN:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1234",
|
||||||
|
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
46
menutraversal_test/profile_edit_start_location.json
Normal file
46
menutraversal_test/profile_edit_start_location.json
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"name": "menu_my_account_edit_all_account_details_starting_from_location",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"input": "",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "3",
|
||||||
|
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "5",
|
||||||
|
"expectedContent": "Enter your location:\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "Kilifi",
|
||||||
|
"expectedContent": "Enter the services or goods you offer: \n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "Bananas",
|
||||||
|
"expectedContent": "Please enter your PIN:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1234",
|
||||||
|
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
42
menutraversal_test/profile_edit_start_offerings.json
Normal file
42
menutraversal_test/profile_edit_start_offerings.json
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"name": "menu_my_account_edit_all_account_details_starting_from_offerings",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"input": "",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "3",
|
||||||
|
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "6",
|
||||||
|
"expectedContent": "Enter the services or goods you offer: \n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "Bananas",
|
||||||
|
"expectedContent": "Please enter your PIN:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1234",
|
||||||
|
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
50
menutraversal_test/profile_edit_start_yob.json
Normal file
50
menutraversal_test/profile_edit_start_yob.json
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"name": "menu_my_account_edit_all_account_details_starting_from_yob",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"input": "",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "3",
|
||||||
|
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "4",
|
||||||
|
"expectedContent": "Enter your year of birth\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1940",
|
||||||
|
"expectedContent": "Enter your location:\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "Kilifi",
|
||||||
|
"expectedContent": "Enter the services or goods you offer: \n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "Bananas",
|
||||||
|
"expectedContent": "Please enter your PIN:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1234",
|
||||||
|
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
70
menutraversal_test/profile_edit_when_adjacent_item_set.json
Normal file
70
menutraversal_test/profile_edit_when_adjacent_item_set.json
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"name": "menu_my_account_edit_familyname_when_adjacent_profile_information_set",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"input": "",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "3",
|
||||||
|
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "3",
|
||||||
|
"expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "Enter your year of birth\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1940",
|
||||||
|
"expectedContent": "Enter your location:\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "Kilifi",
|
||||||
|
"expectedContent": "Enter the services or goods you offer: \n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "Bananas",
|
||||||
|
"expectedContent": "Please enter your PIN:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1234",
|
||||||
|
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "2",
|
||||||
|
"expectedContent": "Enter family name:\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "foo2",
|
||||||
|
"expectedContent": "Please enter your PIN:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1234",
|
||||||
|
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
133
menutraversal_test/test_setup.json
Normal file
133
menutraversal_test/test_setup.json
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "session one",
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"name": "account_creation_successful",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"input": "",
|
||||||
|
"expectedContent": "Welcome to Sarafu Network\nPlease select a language\n1:English\n2:Kiswahili"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "Do you agree to terms and conditions?\nhttps://grassecon.org/pages/terms-and-conditions\n\n1:Yes\n2:No"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"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\n1:English\n2:Kiswahili"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "Do you agree to terms and conditions?\nhttps://grassecon.org/pages/terms-and-conditions\n\n1:Yes\n2:No"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "2",
|
||||||
|
"expectedContent": "Thank you for using Sarafu. Goodbye!"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "send_with_invite",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"input": "",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "Enter recipient's phone number/address/alias:\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0@0",
|
||||||
|
"expectedContent": "0@0 is invalid, please try again:\n1:Retry\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "1",
|
||||||
|
"expectedContent": "Enter recipient's phone number/address/alias:\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "0712345678",
|
||||||
|
"expectedContent": "0712345678 is not registered, please try again:\n1:Retry\n2:Invite to Sarafu Network\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "2",
|
||||||
|
"expectedContent": "Your invite request for 0712345678 to Sarafu Network failed. Please try again later."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "main_menu_help",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"input": "",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "4",
|
||||||
|
"expectedContent": "For more help,please call: 0757628885"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "main_menu_quit",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"input": "",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "9",
|
||||||
|
"expectedContent": "Thank you for using Sarafu. Goodbye!"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "menu_my_account_my_address",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"input": "",
|
||||||
|
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "3",
|
||||||
|
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "6",
|
||||||
|
"expectedContent": "Address: {public_key}\n0:Back\n9:Quit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "9",
|
||||||
|
"expectedContent": "Thank you for using Sarafu. Goodbye!"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
6
models/account_response.go
Normal file
6
models/account_response.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type AccountResult struct {
|
||||||
|
PublicKey string `json:"publicKey"`
|
||||||
|
TrackingId string `json:"trackingId"`
|
||||||
|
}
|
8
models/balance_response.go
Normal file
8
models/balance_response.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
type BalanceResult struct {
|
||||||
|
Balance string `json:"balance"`
|
||||||
|
Nonce json.Number `json:"nonce"`
|
||||||
|
}
|
18
models/profile.go
Normal file
18
models/profile.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type Profile struct {
|
||||||
|
ProfileItems []string
|
||||||
|
Max int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Profile) InsertOrShift(index int, value string) {
|
||||||
|
if index < len(p.ProfileItems) {
|
||||||
|
p.ProfileItems = append(p.ProfileItems[:index], value)
|
||||||
|
} else {
|
||||||
|
for len(p.ProfileItems) < index {
|
||||||
|
p.ProfileItems = append(p.ProfileItems, "0")
|
||||||
|
}
|
||||||
|
p.ProfileItems = append(p.ProfileItems, "0")
|
||||||
|
p.ProfileItems[index] = value
|
||||||
|
}
|
||||||
|
}
|
5
models/token_transfer_response.go
Normal file
5
models/token_transfer_response.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type TokenTransferResponse struct {
|
||||||
|
TrackingId string `json:"trackingId"`
|
||||||
|
}
|
18
models/track_status_response.go
Normal file
18
models/track_status_response.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Transaction struct {
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
TransferValue json.Number `json:"transferValue"`
|
||||||
|
TxHash string `json:"txHash"`
|
||||||
|
TxType string `json:"txType"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TrackStatusResult struct {
|
||||||
|
Active bool `json:"active"`
|
||||||
|
}
|
10
models/voucher_data_result.go
Normal file
10
models/voucher_data_result.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type VoucherDataResult struct {
|
||||||
|
TokenName string `json:"tokenName"`
|
||||||
|
TokenSymbol string `json:"tokenSymbol"`
|
||||||
|
TokenDecimals int `json:"tokenDecimals"`
|
||||||
|
SinkAddress string `json:"sinkAddress"`
|
||||||
|
TokenCommodity string `json:"tokenCommodity"`
|
||||||
|
TokenLocation string `json:"tokenLocation"`
|
||||||
|
}
|
294
remote/account_service.go
Normal file
294
remote/account_service.go
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
package remote
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"git.grassecon.net/urdt/ussd/config"
|
||||||
|
"git.grassecon.net/urdt/ussd/models"
|
||||||
|
"github.com/grassrootseconomics/eth-custodial/pkg/api"
|
||||||
|
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AccountServiceInterface interface {
|
||||||
|
CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error)
|
||||||
|
CreateAccount(ctx context.Context) (*models.AccountResult, error)
|
||||||
|
TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error)
|
||||||
|
FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error)
|
||||||
|
FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error)
|
||||||
|
VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error)
|
||||||
|
TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error)
|
||||||
|
CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccountService struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// AccountResponse struct can be used here to check the account status during a transaction.
|
||||||
|
//
|
||||||
|
// 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) TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error) {
|
||||||
|
var r models.TrackStatusResult
|
||||||
|
|
||||||
|
ep, err := url.JoinPath(config.TrackURL, publicKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", ep, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = doRequest(ctx, req, &r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckBalance retrieves the balance for a given public key from the custodial balance API endpoint.
|
||||||
|
// Parameters:
|
||||||
|
// - publicKey: The public key associated with the account whose balance needs to be checked.
|
||||||
|
func (as *AccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) {
|
||||||
|
var balanceResult models.BalanceResult
|
||||||
|
|
||||||
|
ep, err := url.JoinPath(config.BalanceURL, publicKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", ep, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = doRequest(ctx, req, &balanceResult)
|
||||||
|
return &balanceResult, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateAccount creates a new account in the custodial system.
|
||||||
|
// Returns:
|
||||||
|
// - *models.AccountResponse: A pointer to an AccountResponse struct containing the details of the created account.
|
||||||
|
// If there is an error during the request or processing, this will be nil.
|
||||||
|
// - 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(ctx context.Context) (*models.AccountResult, error) {
|
||||||
|
var r models.AccountResult
|
||||||
|
// Create a new request
|
||||||
|
req, err := http.NewRequest("POST", config.CreateAccountURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = doRequest(ctx, req, &r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchVouchers retrieves the token holdings for a given public key from the data indexer API endpoint
|
||||||
|
// Parameters:
|
||||||
|
// - publicKey: The public key associated with the account.
|
||||||
|
func (as *AccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) {
|
||||||
|
var r struct {
|
||||||
|
Holdings []dataserviceapi.TokenHoldings `json:"holdings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ep, err := url.JoinPath(config.VoucherHoldingsURL, publicKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", ep, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = doRequest(ctx, req, &r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Holdings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchTransactions retrieves the last 10 transactions for a given public key from the data indexer API endpoint
|
||||||
|
// Parameters:
|
||||||
|
// - publicKey: The public key associated with the account.
|
||||||
|
func (as *AccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) {
|
||||||
|
var r struct {
|
||||||
|
Transfers []dataserviceapi.Last10TxResponse `json:"transfers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ep, err := url.JoinPath(config.VoucherTransfersURL, publicKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", ep, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = doRequest(ctx, req, &r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Transfers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VoucherData retrieves voucher metadata from the data indexer API endpoint.
|
||||||
|
// Parameters:
|
||||||
|
// - address: The voucher address.
|
||||||
|
func (as *AccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) {
|
||||||
|
var r struct {
|
||||||
|
TokenDetails models.VoucherDataResult `json:"tokenDetails"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ep, err := url.JoinPath(config.VoucherDataURL, address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", ep, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = doRequest(ctx, req, &r)
|
||||||
|
return &r.TokenDetails, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenTransfer creates a new token transfer in the custodial system.
|
||||||
|
// Returns:
|
||||||
|
// - *models.TokenTransferResponse: A pointer to an TokenTransferResponse struct containing the trackingId.
|
||||||
|
// 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) TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) {
|
||||||
|
var r models.TokenTransferResponse
|
||||||
|
|
||||||
|
// Create request payload
|
||||||
|
payload := map[string]string{
|
||||||
|
"amount": amount,
|
||||||
|
"from": from,
|
||||||
|
"to": to,
|
||||||
|
"tokenAddress": tokenAddress,
|
||||||
|
}
|
||||||
|
|
||||||
|
payloadBytes, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new request
|
||||||
|
req, err := http.NewRequest("POST", config.TokenTransferURL, bytes.NewBuffer(payloadBytes))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = doRequest(ctx, req, &r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckAliasAddress retrieves the address of an alias from the API endpoint.
|
||||||
|
// Parameters:
|
||||||
|
// - alias: The alias of the user.
|
||||||
|
func (as *AccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) {
|
||||||
|
var r dataserviceapi.AliasAddress
|
||||||
|
|
||||||
|
ep, err := url.JoinPath(config.CheckAliasURL, alias)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", ep, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = doRequest(ctx, req, &r)
|
||||||
|
return &r, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) {
|
||||||
|
var okResponse api.OKResponse
|
||||||
|
var errResponse api.ErrResponse
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", "Bearer "+config.BearerToken)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
logRequestDetails(req)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to make %s request to endpoint: %s with reason: %s", req.Method, req.URL, err.Error())
|
||||||
|
errResponse.Description = err.Error()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
log.Printf("Received response for %s: Status Code: %d | Content-Type: %s", req.URL, resp.StatusCode, resp.Header.Get("Content-Type"))
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if resp.StatusCode >= http.StatusBadRequest {
|
||||||
|
err := json.Unmarshal([]byte(body), &errResponse)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, errors.New(errResponse.Description)
|
||||||
|
}
|
||||||
|
err = json.Unmarshal([]byte(body), &okResponse)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(okResponse.Result) == 0 {
|
||||||
|
return nil, errors.New("Empty api result")
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := json.Marshal(okResponse.Result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(v, &rcpt)
|
||||||
|
return &okResponse, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func logRequestDetails(req *http.Request) {
|
||||||
|
var bodyBytes []byte
|
||||||
|
contentType := req.Header.Get("Content-Type")
|
||||||
|
if req.Body != nil {
|
||||||
|
bodyBytes, err := io.ReadAll(req.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error reading request body: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||||
|
} else {
|
||||||
|
bodyBytes = []byte("-")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("URL: %s | Content-Type: %s | Method: %s| Request Body: %s", req.URL, contentType, req.Method, string(bodyBytes))
|
||||||
|
}
|
@ -1,42 +0,0 @@
|
|||||||
package request
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"git.defalsify.org/vise.git/resource"
|
|
||||||
"git.defalsify.org/vise.git/persist"
|
|
||||||
"git.defalsify.org/vise.git/engine"
|
|
||||||
"git.defalsify.org/vise.git/logging"
|
|
||||||
"git.grassecon.net/grassrootseconomics/visedriver/storage"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
logg = logging.NewVanilla().WithDomain("visedriver.request")
|
|
||||||
)
|
|
||||||
|
|
||||||
type RequestSession struct {
|
|
||||||
Ctx context.Context
|
|
||||||
Config engine.Config
|
|
||||||
Engine engine.Engine
|
|
||||||
Input []byte
|
|
||||||
Storage *storage.Storage
|
|
||||||
Writer io.Writer
|
|
||||||
Continue bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: seems like can remove this.
|
|
||||||
type RequestParser interface {
|
|
||||||
GetSessionId(ctx context.Context, 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()
|
|
||||||
}
|
|
18
services/registration/Makefile
Normal file
18
services/registration/Makefile
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Variables to match files in the current directory
|
||||||
|
INPUTS = $(wildcard ./*.vis)
|
||||||
|
TXTS = $(wildcard ./*.txt.orig)
|
||||||
|
VISE_PATH := ../../go-vise
|
||||||
|
|
||||||
|
# Rule to build .bin files from .vis files
|
||||||
|
%.vis:
|
||||||
|
go run $(VISE_PATH)/dev/asm/main.go -f pp.csv $(basename $@).vis > $(basename $@).bin
|
||||||
|
@echo "Built $(basename $@).bin from $(basename $@).vis"
|
||||||
|
|
||||||
|
# Rule to copy .orig files to .txt
|
||||||
|
%.txt.orig:
|
||||||
|
cp -v $(basename $@).orig $(basename $@)
|
||||||
|
@echo "Copied $(basename $@).orig to $(basename $@)"
|
||||||
|
|
||||||
|
# 'all' target depends on all .vis and .txt.orig files
|
||||||
|
all: $(INPUTS) $(TXTS)
|
||||||
|
@echo "Running all: $(INPUTS) $(TXTS)"
|
1
services/registration/_catch
Normal file
1
services/registration/_catch
Normal file
@ -0,0 +1 @@
|
|||||||
|
Something went wrong.Please try again
|
1
services/registration/_catch.vis
Normal file
1
services/registration/_catch.vis
Normal file
@ -0,0 +1 @@
|
|||||||
|
HALT
|
1
services/registration/_catch_swa
Normal file
1
services/registration/_catch_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Tatizo la kimtambo limetokea,tafadhali jaribu tena baadaye.
|
1
services/registration/account_creation
Normal file
1
services/registration/account_creation
Normal file
@ -0,0 +1 @@
|
|||||||
|
Your account is being created...
|
4
services/registration/account_creation.vis
Normal file
4
services/registration/account_creation.vis
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
RELOAD verify_create_pin
|
||||||
|
CATCH create_pin_mismatch flag_pin_mismatch 1
|
||||||
|
LOAD quit 0
|
||||||
|
HALT
|
1
services/registration/account_creation_failed
Normal file
1
services/registration/account_creation_failed
Normal file
@ -0,0 +1 @@
|
|||||||
|
Your account creation request failed. Please try again later.
|
3
services/registration/account_creation_failed.vis
Normal file
3
services/registration/account_creation_failed.vis
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
MOUT quit 9
|
||||||
|
HALT
|
||||||
|
INCMP quit 9
|
1
services/registration/account_creation_failed_swa
Normal file
1
services/registration/account_creation_failed_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Ombi lako la kusajiliwa haliwezi kukamilishwa. Tafadhali jaribu tena baadaye.
|
1
services/registration/account_creation_swa
Normal file
1
services/registration/account_creation_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Akaunti yako inatengenezwa...
|
1
services/registration/account_menu
Normal file
1
services/registration/account_menu
Normal file
@ -0,0 +1 @@
|
|||||||
|
My Account
|
1
services/registration/account_menu_swa
Normal file
1
services/registration/account_menu_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Akaunti yangu
|
1
services/registration/account_pending
Normal file
1
services/registration/account_pending
Normal file
@ -0,0 +1 @@
|
|||||||
|
Your account is still being created.
|
3
services/registration/account_pending.vis
Normal file
3
services/registration/account_pending.vis
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
RELOAD check_account_status
|
||||||
|
CATCH main flag_account_success 1
|
||||||
|
HALT
|
1
services/registration/account_pending_swa
Normal file
1
services/registration/account_pending_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Akaunti yako bado inatengenezwa
|
1
services/registration/address
Normal file
1
services/registration/address
Normal file
@ -0,0 +1 @@
|
|||||||
|
Address: {{.check_identifier}}
|
8
services/registration/address.vis
Normal file
8
services/registration/address.vis
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
LOAD check_identifier 0
|
||||||
|
RELOAD check_identifier
|
||||||
|
MAP check_identifier
|
||||||
|
MOUT back 0
|
||||||
|
MOUT quit 9
|
||||||
|
HALT
|
||||||
|
INCMP _ 0
|
||||||
|
INCMP quit 9
|
1
services/registration/address_swa
Normal file
1
services/registration/address_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Anwani:{{.check_identifier}}
|
2
services/registration/amount
Normal file
2
services/registration/amount
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Maximum amount: {{.max_amount}}
|
||||||
|
Enter amount:
|
15
services/registration/amount.vis
Normal file
15
services/registration/amount.vis
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
LOAD reset_transaction_amount 0
|
||||||
|
LOAD max_amount 10
|
||||||
|
RELOAD max_amount
|
||||||
|
MAP max_amount
|
||||||
|
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 0
|
||||||
|
LOAD get_sender 64
|
||||||
|
LOAD get_amount 32
|
||||||
|
INCMP transaction_pin *
|
2
services/registration/amount_swa
Normal file
2
services/registration/amount_swa
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Kiwango cha juu: {{.max_amount}}
|
||||||
|
Weka kiwango:
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user