Compare commits
	
		
			No commits in common. "c93a07832daa0e4ba21ea8ddb9519e4c91ffd1d5" and "80b96e9bf6c85ceb1141891c8543ff9cb6f3257f" have entirely different histories.
		
	
	
		
			c93a07832d
			...
			80b96e9bf6
		
	
		
							
								
								
									
										10
									
								
								.env.example
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								.env.example
									
									
									
									
									
								
							@ -6,9 +6,13 @@ HOST=127.0.0.1
 | 
			
		||||
AT_ENDPOINT=/ussd/africastalking
 | 
			
		||||
 | 
			
		||||
#PostgreSQL
 | 
			
		||||
DB_CONN=postgres://postgres:strongpass@localhost:5432/urdt_ussd
 | 
			
		||||
#DB_TIMEZONE=Africa/Nairobi
 | 
			
		||||
#DB_SCHEMA=vise
 | 
			
		||||
DB_HOST=localhost
 | 
			
		||||
DB_USER=postgres
 | 
			
		||||
DB_PASSWORD=strongpass
 | 
			
		||||
DB_NAME=urdt_ussd
 | 
			
		||||
DB_PORT=5432
 | 
			
		||||
DB_SSLMODE=disable
 | 
			
		||||
DB_TIMEZONE=Africa/Nairobi
 | 
			
		||||
 | 
			
		||||
#External API Calls
 | 
			
		||||
CUSTODIAL_URL_BASE=http://localhost:5003
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										165
									
								
								cmd/africastalking/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								cmd/africastalking/main.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,165 @@
 | 
			
		||||
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/logging"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
	"git.defalsify.org/vise.git/lang"
 | 
			
		||||
 | 
			
		||||
	"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/http/at"
 | 
			
		||||
	httpserver "git.grassecon.net/urdt/ussd/internal/http/at"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/remote"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/args"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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 dbDir string
 | 
			
		||||
	var resourceDir string
 | 
			
		||||
	var size uint
 | 
			
		||||
	var database string
 | 
			
		||||
	var engineDebug bool
 | 
			
		||||
	var host string
 | 
			
		||||
	var port uint
 | 
			
		||||
	var gettextDir string
 | 
			
		||||
	var langs args.LangVar
 | 
			
		||||
	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
 | 
			
		||||
	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
 | 
			
		||||
	flag.StringVar(&database, "db", "gdbm", "database to be used")
 | 
			
		||||
	flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
 | 
			
		||||
	flag.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()
 | 
			
		||||
 | 
			
		||||
	logg.Infof("start command", "build", build, "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	ctx = context.WithValue(ctx, "Database", database)
 | 
			
		||||
	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(dbDir, resourceDir)
 | 
			
		||||
	rs, err := menuStorageService.GetResource(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = menuStorageService.EnsureDbDir()
 | 
			
		||||
	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 := httpserver.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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										190
									
								
								cmd/async/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								cmd/async/main.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,190 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/signal"
 | 
			
		||||
	"path"
 | 
			
		||||
	"syscall"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
	"git.defalsify.org/vise.git/lang"
 | 
			
		||||
 | 
			
		||||
	"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/remote"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/args"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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 sessionId string
 | 
			
		||||
	var dbDir string
 | 
			
		||||
	var resourceDir string
 | 
			
		||||
	var size uint
 | 
			
		||||
	var database string
 | 
			
		||||
	var engineDebug bool
 | 
			
		||||
	var host string
 | 
			
		||||
	var port uint
 | 
			
		||||
	var gettextDir string
 | 
			
		||||
	var langs args.LangVar
 | 
			
		||||
	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
 | 
			
		||||
	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
 | 
			
		||||
	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
 | 
			
		||||
	flag.StringVar(&database, "db", "gdbm", "database to be used")
 | 
			
		||||
	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()
 | 
			
		||||
 | 
			
		||||
	logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId)
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	ctx = context.WithValue(ctx, "Database", database)
 | 
			
		||||
 | 
			
		||||
	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(dbDir, resourceDir)
 | 
			
		||||
	rs, err := menuStorageService.GetResource(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = menuStorageService.EnsureDbDir()
 | 
			
		||||
	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)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										157
									
								
								cmd/http/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								cmd/http/main.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,157 @@
 | 
			
		||||
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/logging"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
	"git.defalsify.org/vise.git/lang"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/config"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
			
		||||
	"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"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/args"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg      = logging.NewVanilla()
 | 
			
		||||
	scriptDir = path.Join("services", "registration")
 | 
			
		||||
	menuSeparator = ": "
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	initializers.LoadEnvVariables()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	config.LoadConfig()
 | 
			
		||||
 | 
			
		||||
	var dbDir string
 | 
			
		||||
	var resourceDir string
 | 
			
		||||
	var size uint
 | 
			
		||||
	var database string
 | 
			
		||||
	var engineDebug bool
 | 
			
		||||
	var host string
 | 
			
		||||
	var port uint
 | 
			
		||||
	var gettextDir string
 | 
			
		||||
	var langs args.LangVar
 | 
			
		||||
	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
 | 
			
		||||
	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
 | 
			
		||||
	flag.StringVar(&database, "db", "gdbm", "database to be used")
 | 
			
		||||
	flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
 | 
			
		||||
	flag.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()
 | 
			
		||||
 | 
			
		||||
	logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	ctx = context.WithValue(ctx, "Database", database)
 | 
			
		||||
 | 
			
		||||
	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(dbDir, resourceDir)
 | 
			
		||||
	rs, err := menuStorageService.GetResource(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = menuStorageService.EnsureDbDir()
 | 
			
		||||
	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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										142
									
								
								cmd/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								cmd/main.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,142 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
	"git.defalsify.org/vise.git/lang"
 | 
			
		||||
	"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/args"
 | 
			
		||||
	"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 dbDir string
 | 
			
		||||
	var size uint
 | 
			
		||||
	var sessionId string
 | 
			
		||||
	var database string
 | 
			
		||||
	var engineDebug bool
 | 
			
		||||
	var gettextDir string
 | 
			
		||||
	var langs args.LangVar
 | 
			
		||||
	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
 | 
			
		||||
	flag.StringVar(&database, "db", "gdbm", "database to be used")
 | 
			
		||||
	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
 | 
			
		||||
	flag.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()
 | 
			
		||||
 | 
			
		||||
	logg.Infof("start command", "dbdir", dbDir, "outputsize", size)
 | 
			
		||||
 | 
			
		||||
	if len(langs.Langs()) == 0 {
 | 
			
		||||
		langs.Set(config.DefaultLanguage)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	ctx = context.WithValue(ctx, "SessionId", sessionId)
 | 
			
		||||
	ctx = context.WithValue(ctx, "Database", database)
 | 
			
		||||
 | 
			
		||||
	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,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resourceDir := scriptDir
 | 
			
		||||
	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
 | 
			
		||||
	if gettextDir != "" {
 | 
			
		||||
		menuStorageService = menuStorageService.WithGettext(gettextDir, langs.Langs())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = menuStorageService.EnsureDbDir()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rs, err := menuStorageService.GetResource(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pe, err := menuStorageService.GetPersister(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userdatastore, err := menuStorageService.GetUserdataDb(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dbResource, ok := rs.(*resource.DbResource)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	lhs, err := handlers.NewLocalHandlerService(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>
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										117
									
								
								cmd/ssh/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								cmd/ssh/main.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,117 @@
 | 
			
		||||
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/internal/ssh"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	wg        sync.WaitGroup
 | 
			
		||||
	keyStore  db.Db
 | 
			
		||||
	logg      = logging.NewVanilla()
 | 
			
		||||
	scriptDir = path.Join("services", "registration")
 | 
			
		||||
 | 
			
		||||
	build = "dev"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	var dbDir string
 | 
			
		||||
	var resourceDir string
 | 
			
		||||
	var size uint
 | 
			
		||||
	var engineDebug bool
 | 
			
		||||
	var stateDebug bool
 | 
			
		||||
	var host string
 | 
			
		||||
	var port uint
 | 
			
		||||
	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
 | 
			
		||||
	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
 | 
			
		||||
	flag.BoolVar(&engineDebug, "engine-debug", false, "use engine debug output")
 | 
			
		||||
	flag.BoolVar(&stateDebug, "state-debug", false, "use engine debug output")
 | 
			
		||||
	flag.UintVar(&size, "s", 160, "max size of output")
 | 
			
		||||
	flag.StringVar(&host, "h", "127.0.0.1", "http host")
 | 
			
		||||
	flag.UintVar(&port, "p", 7122, "http port")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	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", "dbdir", dbDir, "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, dbDir)
 | 
			
		||||
	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,
 | 
			
		||||
		DbDir:       dbDir,
 | 
			
		||||
		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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -55,10 +55,6 @@ const (
 | 
			
		||||
	DATA_ACTIVE_DECIMAL
 | 
			
		||||
	// EVM address of the currently active voucher
 | 
			
		||||
	DATA_ACTIVE_ADDRESS
 | 
			
		||||
	//Holds count of the number of incorrect PIN attempts
 | 
			
		||||
	DATA_INCORRECT_PIN_ATTEMPTS
 | 
			
		||||
	//ISO 639 code for the selected language.
 | 
			
		||||
	DATA_SELECTED_LANGUAGE_CODE
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
 | 
			
		||||
@ -6,13 +6,9 @@ import (
 | 
			
		||||
	"golang.org/x/crypto/bcrypt"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Define the regex pattern as a constant
 | 
			
		||||
const (
 | 
			
		||||
	// Define the regex pattern as a constant
 | 
			
		||||
	pinPattern = `^\d{4}$`
 | 
			
		||||
 | 
			
		||||
	//Allowed incorrect  PIN attempts
 | 
			
		||||
	AllowedPINAttempts = uint8(3)
 | 
			
		||||
	
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// checks whether the given input is a 4 digit number
 | 
			
		||||
 | 
			
		||||
@ -7,13 +7,8 @@ import (
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
	"git.defalsify.org/vise.git/persist"
 | 
			
		||||
	"git.defalsify.org/vise.git/lang"
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/storage"
 | 
			
		||||
	dbstorage "git.grassecon.net/grassrootseconomics/visedriver/storage/db"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	ToConnData = storage.ToConnData
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
	dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func StoreToDb(store *UserDataStore) db.Db {
 | 
			
		||||
@ -28,28 +23,17 @@ type StorageServices interface {
 | 
			
		||||
	GetPersister(ctx context.Context) (*persist.Persister, error)
 | 
			
		||||
	GetUserdataDb(ctx context.Context) (db.Db, error)
 | 
			
		||||
	GetResource(ctx context.Context) (resource.Resource, error)
 | 
			
		||||
	EnsureDbDir() error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type StorageService struct {
 | 
			
		||||
	svc *storage.MenuStorageService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewStorageService(conn storage.ConnData) (*StorageService, error) {
 | 
			
		||||
	svc := &StorageService{
 | 
			
		||||
		svc: storage.NewMenuStorageService(conn, ""),
 | 
			
		||||
func NewStorageService(dbDir string) *StorageService {
 | 
			
		||||
	return &StorageService{
 | 
			
		||||
		svc: storage.NewMenuStorageService(dbDir, ""),
 | 
			
		||||
	}
 | 
			
		||||
	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) {
 | 
			
		||||
@ -64,6 +48,6 @@ func(ss *StorageService) GetResource(ctx context.Context) (resource.Resource, er
 | 
			
		||||
	return nil, errors.New("not implemented")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(ss *StorageService) GetStateStore(ctx context.Context) (db.Db, error) {
 | 
			
		||||
	return ss.svc.GetStateStore(ctx)
 | 
			
		||||
func(ss *StorageService) EnsureDbDir() error {
 | 
			
		||||
	return ss.svc.EnsureDbDir()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
	"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"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ import (
 | 
			
		||||
	"math/big"
 | 
			
		||||
	"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"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@ import (
 | 
			
		||||
 | 
			
		||||
	visedb "git.defalsify.org/vise.git/db"
 | 
			
		||||
	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"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ import (
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/initializers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
@ -40,7 +40,6 @@ var (
 | 
			
		||||
	VoucherTransfersURL string
 | 
			
		||||
	VoucherDataURL      string
 | 
			
		||||
	CheckAliasURL       string
 | 
			
		||||
	DbConn		string
 | 
			
		||||
	DefaultLanguage	    string
 | 
			
		||||
	Languages	[]string
 | 
			
		||||
)
 | 
			
		||||
@ -70,20 +69,14 @@ func setBase() error {
 | 
			
		||||
	dataURLBase = initializers.GetEnv("DATA_URL_BASE", "http://localhost:5006")
 | 
			
		||||
	BearerToken = initializers.GetEnv("BEARER_TOKEN", "")
 | 
			
		||||
 | 
			
		||||
	_, err = url.Parse(custodialURLBase)
 | 
			
		||||
	_, err = url.JoinPath(custodialURLBase, "/foo")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	_, err = url.Parse(dataURLBase)
 | 
			
		||||
	_, err = url.JoinPath(dataURLBase, "/bar")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func setConn() error {
 | 
			
		||||
	DbConn = initializers.GetEnv("DB_CONN", "")
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -93,10 +86,6 @@ func LoadConfig() error {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	err = setConn()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	err = setLanguage()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"encoding/binary"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/common"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/common"
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ package debug
 | 
			
		||||
import (
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/common"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/common"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@ package debug
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/common"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/common"
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ import (
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/utils"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/utils"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
@ -4,7 +4,7 @@ import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"log"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/cmd/admin/commands"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/devtools/admin/commands"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
@ -10,8 +10,8 @@ import (
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"git.defalsify.org/vise.git/lang"
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/config"
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/initializers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/config"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
@ -7,10 +7,10 @@ import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/config"
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/initializers"
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/storage"
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/debug"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/config"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/debug"
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
)
 | 
			
		||||
@ -37,34 +37,23 @@ func formatItem(k []byte, v []byte) (string, error) {
 | 
			
		||||
func main() {
 | 
			
		||||
	config.LoadConfig()
 | 
			
		||||
 | 
			
		||||
	var connStr string
 | 
			
		||||
	var dbDir string
 | 
			
		||||
	var sessionId string
 | 
			
		||||
	var database string
 | 
			
		||||
	var engineDebug bool
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
 | 
			
		||||
	flag.StringVar(&connStr, "c", ".state", "connection string")
 | 
			
		||||
	flag.StringVar(&database, "db", "gdbm", "database to be used")
 | 
			
		||||
	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
 | 
			
		||||
	flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	if connStr != "" {
 | 
			
		||||
		connStr = config.DbConn
 | 
			
		||||
	}
 | 
			
		||||
	connData, err := storage.ToConnData(config.DbConn)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "connstr err: %v", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logg.Infof("start command", "conn", connData)
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	ctx = context.WithValue(ctx, "SessionId", sessionId)
 | 
			
		||||
	ctx = context.WithValue(ctx, "Database", database)
 | 
			
		||||
 | 
			
		||||
	resourceDir := scriptDir
 | 
			
		||||
	menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
 | 
			
		||||
	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
 | 
			
		||||
 | 
			
		||||
	store, err := menuStorageService.GetUserdataDb(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@ -8,17 +8,15 @@ import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
 | 
			
		||||
	testdataloader "github.com/peteole/testdata-loader"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/config"
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/storage"
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/initializers"
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/common"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/config"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/common"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg      = logging.NewVanilla()
 | 
			
		||||
	baseDir   = testdataloader.GetBasePath()
 | 
			
		||||
	scriptDir = path.Join("services", "registration")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -26,37 +24,27 @@ func init() {
 | 
			
		||||
	initializers.LoadEnvVariables()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	config.LoadConfig()
 | 
			
		||||
 | 
			
		||||
	var connStr string
 | 
			
		||||
	var dbDir string
 | 
			
		||||
	var sessionId string
 | 
			
		||||
	var database string
 | 
			
		||||
	var engineDebug bool
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
 | 
			
		||||
	flag.StringVar(&connStr, "c", "", "connection string")
 | 
			
		||||
	flag.StringVar(&database, "db", "gdbm", "database to be used")
 | 
			
		||||
	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
 | 
			
		||||
	flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	if connStr != "" {
 | 
			
		||||
		connStr = config.DbConn
 | 
			
		||||
	}
 | 
			
		||||
	connData, err := storage.ToConnData(config.DbConn)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "connstr err: %v", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logg.Infof("start command", "conn", connData)
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	ctx = context.WithValue(ctx, "SessionId", sessionId)
 | 
			
		||||
	ctx = context.WithValue(ctx, "Database", database)
 | 
			
		||||
 | 
			
		||||
	resourceDir := scriptDir
 | 
			
		||||
	menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
 | 
			
		||||
	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
 | 
			
		||||
 | 
			
		||||
	store, err := menuStorageService.GetUserdataDb(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@ -87,4 +75,5 @@ func main() {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -8,14 +8,15 @@ require (
 | 
			
		||||
	github.com/gofrs/uuid v4.4.0+incompatible
 | 
			
		||||
	github.com/grassrootseconomics/eth-custodial v1.3.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/peteole/testdata-loader v0.3.0
 | 
			
		||||
	github.com/stretchr/testify v1.9.0
 | 
			
		||||
	golang.org/x/crypto v0.27.0
 | 
			
		||||
	gopkg.in/leonelquinteros/gotext.v1 v1.3.1
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/alecthomas/participle/v2 v2.0.0 // indirect
 | 
			
		||||
	github.com/alecthomas/repr v0.2.0 // indirect
 | 
			
		||||
	github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // 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/jackc/pgpassfile v1.0.0 // indirect
 | 
			
		||||
	github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
 | 
			
		||||
	github.com/jackc/pgx/v5 v5.7.1 // indirect
 | 
			
		||||
	github.com/jackc/puddle/v2 v2.2.2 // indirect
 | 
			
		||||
	github.com/kr/text v0.2.0 // 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/x448/float16 v0.8.4 // 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
 | 
			
		||||
	gopkg.in/leonelquinteros/gotext.v1 v1.3.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=
 | 
			
		||||
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/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/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
 | 
			
		||||
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/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
 | 
			
		||||
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/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
 | 
			
		||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
 | 
			
		||||
@ -3,30 +3,24 @@ package initializers
 | 
			
		||||
import (
 | 
			
		||||
	"log"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"github.com/joho/godotenv"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func LoadEnvVariables() {
 | 
			
		||||
	LoadEnvVariablesPath(".")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func LoadEnvVariablesPath(dir string) {
 | 
			
		||||
	fp := path.Join(dir, ".env")
 | 
			
		||||
	err := godotenv.Load(fp)
 | 
			
		||||
	err := godotenv.Load()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error loading .env file", err)
 | 
			
		||||
		log.Fatal("Error loading .env file")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper to get environment variables with a default fallback
 | 
			
		||||
func GetEnv(key, defaultVal string) string {
 | 
			
		||||
	if value, exists := os.LookupEnv(key); exists {
 | 
			
		||||
		return value
 | 
			
		||||
  if value, exists := os.LookupEnv(key); exists {
 | 
			
		||||
   	return value
 | 
			
		||||
	}
 | 
			
		||||
	return defaultVal
 | 
			
		||||
  return defaultVal
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper to safely convert environment variables to uint
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,54 +1,47 @@
 | 
			
		||||
package session
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.defalsify.org/vise.git/persist"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
	"git.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 (
 | 
			
		||||
	logg = logging.NewVanilla().WithDomain("visedriver.session")
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers/ussd"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type BaseSessionHandler struct {
 | 
			
		||||
	cfgTemplate engine.Config
 | 
			
		||||
	rp request.RequestParser
 | 
			
		||||
	rp RequestParser
 | 
			
		||||
	rs resource.Resource
 | 
			
		||||
	hn entry.EntryHandler
 | 
			
		||||
	hn *ussd.Handlers
 | 
			
		||||
	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 request.RequestParser, hn entry.EntryHandler) *BaseSessionHandler {
 | 
			
		||||
func NewBaseSessionHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp RequestParser, hn *ussd.Handlers) *BaseSessionHandler {
 | 
			
		||||
	return &BaseSessionHandler{
 | 
			
		||||
		cfgTemplate: cfg,
 | 
			
		||||
		rs:          rs,
 | 
			
		||||
		hn:          hn,
 | 
			
		||||
		rp:          rp,
 | 
			
		||||
		provider:    storage.NewSimpleStorageProvider(stateDb, userdataDb),
 | 
			
		||||
		rs: rs,
 | 
			
		||||
		hn: hn,
 | 
			
		||||
		rp: rp,
 | 
			
		||||
		provider: storage.NewSimpleStorageProvider(stateDb, userdataDb),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *BaseSessionHandler) Shutdown() {
 | 
			
		||||
func(f* BaseSessionHandler) Shutdown() {
 | 
			
		||||
	err := f.provider.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logg.Errorf("handler shutdown error", "err", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *BaseSessionHandler) GetEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) engine.Engine {
 | 
			
		||||
func(f *BaseSessionHandler) GetEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) engine.Engine {
 | 
			
		||||
	en := engine.NewEngine(cfg, rs)
 | 
			
		||||
	en = en.WithPersister(pr)
 | 
			
		||||
	return en
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(f *BaseSessionHandler) Process(rqs request.RequestSession) (request.RequestSession, error) {
 | 
			
		||||
func(f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error) {
 | 
			
		||||
	var r bool
 | 
			
		||||
	var err error
 | 
			
		||||
	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)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		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.SetPersister(rqs.Storage.Persister)
 | 
			
		||||
	f.hn = f.hn.WithPersister(rqs.Storage.Persister)
 | 
			
		||||
	defer func() {
 | 
			
		||||
		f.hn.Exit()
 | 
			
		||||
	}()
 | 
			
		||||
@ -74,7 +66,7 @@ func(f *BaseSessionHandler) Process(rqs request.RequestSession) (request.Request
 | 
			
		||||
		if perr != nil {
 | 
			
		||||
			logg.ErrorCtxf(rqs.Ctx, "", "storage put error", perr)
 | 
			
		||||
		}
 | 
			
		||||
		return rqs, errors.ErrEngineType
 | 
			
		||||
		return rqs, ErrEngineType
 | 
			
		||||
	}
 | 
			
		||||
	en = en.WithFirst(f.hn.Init)
 | 
			
		||||
	if rqs.Config.EngineDebug {
 | 
			
		||||
@ -96,21 +88,21 @@ func(f *BaseSessionHandler) Process(rqs request.RequestSession) (request.Request
 | 
			
		||||
	return rqs, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(f *BaseSessionHandler) Output(rqs request.RequestSession) (request.RequestSession,  error) {
 | 
			
		||||
func(f *BaseSessionHandler) Output(rqs RequestSession) (RequestSession,  error) {
 | 
			
		||||
	var err error
 | 
			
		||||
	_, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer)
 | 
			
		||||
	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)
 | 
			
		||||
	return rqs, rqs.Engine.Finish()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *BaseSessionHandler) GetConfig() engine.Config {
 | 
			
		||||
func(f *BaseSessionHandler) GetConfig() engine.Config {
 | 
			
		||||
	return f.cfgTemplate
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(f *BaseSessionHandler) GetRequestParser() request.RequestParser {
 | 
			
		||||
func(f *BaseSessionHandler) GetRequestParser() RequestParser {
 | 
			
		||||
	return f.rp
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										140
									
								
								internal/handlers/handlerservice.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								internal/handlers/handlerservice.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,140 @@
 | 
			
		||||
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/ussd"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/utils"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/remote"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type HandlerService interface {
 | 
			
		||||
	GetHandler() (*ussd.Handlers, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getParser(fp string, debug bool) (*asm.FlagParser, error) {
 | 
			
		||||
	flagParser := asm.NewFlagParser().WithDebug()
 | 
			
		||||
	_, err := flagParser.Load(fp)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return flagParser, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type LocalHandlerService struct {
 | 
			
		||||
	Parser        *asm.FlagParser
 | 
			
		||||
	DbRs          *resource.DbResource
 | 
			
		||||
	Pe            *persist.Persister
 | 
			
		||||
	UserdataStore *db.Db
 | 
			
		||||
	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) (*ussd.Handlers, error) {
 | 
			
		||||
	replaceSeparatorFunc := func(input string) string {
 | 
			
		||||
		return strings.ReplaceAll(input, ":", ls.Cfg.MenuSeparator)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore, ls.AdminStore, accountService, replaceSeparatorFunc)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	ussdHandlers = ussdHandlers.WithPersister(ls.Pe)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("set_language", ussdHandlers.SetLanguage)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("verify_create_pin", ussdHandlers.VerifyCreatePin)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("authorize_account", ussdHandlers.Authorize)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("quit", ussdHandlers.Quit)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("invite_valid_recipient", ussdHandlers.InviteValidRecipient)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("get_sender", ussdHandlers.GetSender)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("get_amount", ussdHandlers.GetAmount)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("save_gender", ussdHandlers.SaveGender)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("save_location", ussdHandlers.SaveLocation)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("save_yob", ussdHandlers.SaveYob)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("fetch_community_balance", ussdHandlers.FetchCommunityBalance)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("set_default_voucher", ussdHandlers.SetDefaultVoucher)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("check_vouchers", ussdHandlers.CheckVouchers)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("get_vouchers", ussdHandlers.GetVoucherList)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("view_voucher", ussdHandlers.ViewVoucher)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("set_voucher", ussdHandlers.SetVoucher)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("get_voucher_details", ussdHandlers.GetVoucherDetails)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("reset_valid_pin", ussdHandlers.ResetValidPin)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("check_pin_mismatch", ussdHandlers.CheckBlockedNumPinMisMatch)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("validate_blocked_number", ussdHandlers.ValidateBlockedNumber)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("retrieve_blocked_number", ussdHandlers.RetrieveBlockedNumber)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("reset_unregistered_number", ussdHandlers.ResetUnregisteredNumber)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("reset_others_pin", ussdHandlers.ResetOthersPin)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("save_others_temporary_pin", ussdHandlers.SaveOthersTemporaryPin)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("get_current_profile_info", ussdHandlers.GetCurrentProfileInfo)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("check_transactions", ussdHandlers.CheckTransactions)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("get_transactions", ussdHandlers.GetTransactionsList)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("view_statement", ussdHandlers.ViewTransactionStatement)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("update_all_profile_items", ussdHandlers.UpdateAllProfileItems)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("set_back", ussdHandlers.SetBack)
 | 
			
		||||
 | 
			
		||||
	return ussdHandlers, 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/resource"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/storage"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2077
									
								
								internal/handlers/ussd/menuhandler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2077
									
								
								internal/handlers/ussd/menuhandler.go
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										2192
									
								
								internal/handlers/ussd/menuhandler_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2192
									
								
								internal/handlers/ussd/menuhandler_test.go
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										119
									
								
								internal/http/at/parse.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								internal/http/at/parse.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,119 @@
 | 
			
		||||
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")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return []byte(parts[len(parts)-1]), 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"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/internal/handlers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/internal/handlers"
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/testutil/mocks/httpmocks"
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/request"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// invalidRequestType is a custom type to test invalid request scenarios
 | 
			
		||||
@ -82,16 +81,16 @@ func TestSessionHandler_ServeHTTP(t *testing.T) {
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			mockRequestHandler := &httpmocks.MockRequestHandler{
 | 
			
		||||
				ProcessFunc: func(rs request.RequestSession) (request.RequestSession, error) {
 | 
			
		||||
				ProcessFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
 | 
			
		||||
					return rs, tt.processErr
 | 
			
		||||
				},
 | 
			
		||||
				OutputFunc: func(rs request.RequestSession) (request.RequestSession, error) {
 | 
			
		||||
				OutputFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
 | 
			
		||||
					return rs, tt.outputErr
 | 
			
		||||
				},
 | 
			
		||||
				ResetFunc: func(rs request.RequestSession) (request.RequestSession, error) {
 | 
			
		||||
				ResetFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
 | 
			
		||||
					return rs, tt.resetErr
 | 
			
		||||
				},
 | 
			
		||||
				GetRequestParserFunc: func() request.RequestParser {
 | 
			
		||||
				GetRequestParserFunc: func() handlers.RequestParser {
 | 
			
		||||
					return mockRequestParser
 | 
			
		||||
				},
 | 
			
		||||
				GetConfigFunc: func() engine.Config {
 | 
			
		||||
@ -99,9 +98,7 @@ func TestSessionHandler_ServeHTTP(t *testing.T) {
 | 
			
		||||
				},
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			sessionHandler := &HTTPSessionHandler{
 | 
			
		||||
				RequestHandler: mockRequestHandler,
 | 
			
		||||
			}
 | 
			
		||||
			sessionHandler := ToSessionHandler(mockRequestHandler)
 | 
			
		||||
 | 
			
		||||
			req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.input))
 | 
			
		||||
			req.Header.Set("X-Vise-Session", tt.sessionID)
 | 
			
		||||
@ -119,7 +116,7 @@ func TestSessionHandler_ServeHTTP(t *testing.T) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestSessionHandler_WriteError(t *testing.T) {
 | 
			
		||||
	handler := &HTTPSessionHandler{}
 | 
			
		||||
	handler := &SessionHandler{}
 | 
			
		||||
	mockWriter := &httpmocks.MockWriter{}
 | 
			
		||||
	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()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										287
									
								
								internal/ssh/ssh.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								internal/ssh/ssh.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,287 @@
 | 
			
		||||
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) {
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SshRunner struct {
 | 
			
		||||
	Ctx context.Context
 | 
			
		||||
	Cfg engine.Config
 | 
			
		||||
	FlagFile string
 | 
			
		||||
	DbDir string
 | 
			
		||||
	ResourceDir string
 | 
			
		||||
	Debug bool
 | 
			
		||||
	SrvKeyFile string
 | 
			
		||||
	Host string
 | 
			
		||||
	Port uint
 | 
			
		||||
	wg sync.WaitGroup
 | 
			
		||||
	lst net.Listener
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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.DbDir, s.ResourceDir)
 | 
			
		||||
 | 
			
		||||
	err := menuStorageService.EnsureDbDir()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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) {
 | 
			
		||||
	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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -6,7 +6,6 @@ import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
 | 
			
		||||
	"github.com/jackc/pgx/v5/pgxpool"
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
	fsdb "git.defalsify.org/vise.git/db/fs"
 | 
			
		||||
	"git.defalsify.org/vise.git/db/postgres"
 | 
			
		||||
@ -14,7 +13,8 @@ import (
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"git.defalsify.org/vise.git/persist"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
	gdbmstorage "git.grassecon.net/grassrootseconomics/visedriver/storage/db/gdbm"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
			
		||||
	gdbmstorage "git.grassecon.net/urdt/ussd/internal/storage/db/gdbm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
@ -25,10 +25,11 @@ type StorageService interface {
 | 
			
		||||
	GetPersister(ctx context.Context) (*persist.Persister, error)
 | 
			
		||||
	GetUserdataDb(ctx context.Context) db.Db
 | 
			
		||||
	GetResource(ctx context.Context) (resource.Resource, error)
 | 
			
		||||
	EnsureDbDir() error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MenuStorageService struct {
 | 
			
		||||
	conn ConnData
 | 
			
		||||
	dbDir         string
 | 
			
		||||
	resourceDir   string
 | 
			
		||||
	poResource    resource.Resource
 | 
			
		||||
	resourceStore db.Db
 | 
			
		||||
@ -36,55 +37,29 @@ type MenuStorageService struct {
 | 
			
		||||
	userDataStore db.Db
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewMenuStorageService(conn ConnData, resourceDir string) *MenuStorageService {
 | 
			
		||||
func buildConnStr() string {
 | 
			
		||||
	host := initializers.GetEnv("DB_HOST", "localhost")
 | 
			
		||||
	user := initializers.GetEnv("DB_USER", "postgres")
 | 
			
		||||
	password := initializers.GetEnv("DB_PASSWORD", "")
 | 
			
		||||
	dbName := initializers.GetEnv("DB_NAME", "")
 | 
			
		||||
	port := initializers.GetEnv("DB_PORT", "5432")
 | 
			
		||||
 | 
			
		||||
	connString := fmt.Sprintf(
 | 
			
		||||
		"postgres://%s:%s@%s:%s/%s",
 | 
			
		||||
		user, password, host, port, dbName,
 | 
			
		||||
	)
 | 
			
		||||
	logg.Debugf("pg conn string", "conn", connString)
 | 
			
		||||
 | 
			
		||||
	return connString
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewMenuStorageService(dbDir string, resourceDir string) *MenuStorageService {
 | 
			
		||||
	return &MenuStorageService{
 | 
			
		||||
		conn: conn,
 | 
			
		||||
		dbDir:       dbDir,
 | 
			
		||||
		resourceDir: resourceDir,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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) {
 | 
			
		||||
	var newDb db.Db
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	if existingDb != nil {
 | 
			
		||||
		return existingDb, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	connStr := ms.conn.String()
 | 
			
		||||
	dbTyp := ms.conn.DbType()
 | 
			
		||||
	if dbTyp == DBTYPE_POSTGRES {
 | 
			
		||||
		// TODO: move to vise
 | 
			
		||||
		err = ensureSchemaExists(ctx, ms.conn)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		newDb = postgres.NewPgDb().WithSchema(ms.conn.Domain())
 | 
			
		||||
	} else if dbTyp == DBTYPE_GDBM {
 | 
			
		||||
		err = ms.ensureDbDir()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		connStr = path.Join(connStr, section)
 | 
			
		||||
		newDb = gdbmstorage.NewThreadGdbmDb()
 | 
			
		||||
	} else {
 | 
			
		||||
		return nil, fmt.Errorf("unsupported connection string: '%s'\n", ms.conn.String())
 | 
			
		||||
	}
 | 
			
		||||
	logg.DebugCtxf(ctx, "connecting to db", "conn", connStr, "conndata", ms.conn)
 | 
			
		||||
	err = newDb.Connect(ctx, connStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return newDb, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WithGettext triggers use of gettext for translation of templates and menus.
 | 
			
		||||
//
 | 
			
		||||
// The first language in `lns` will be used as default language, to resolve node keys to 
 | 
			
		||||
@ -107,21 +82,34 @@ func (ms *MenuStorageService) WithGettext(path string, lns []lang.Language) *Men
 | 
			
		||||
	return ms
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ensureSchemaExists creates a new schema if it does not exist
 | 
			
		||||
func ensureSchemaExists(ctx context.Context, conn ConnData) error {
 | 
			
		||||
	h, err := pgxpool.New(ctx, conn.Path())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to connect to the database: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer h.Close()
 | 
			
		||||
 | 
			
		||||
	query := fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", conn.Domain())
 | 
			
		||||
	_, err = h.Exec(ctx, query)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to create schema: %w", err)
 | 
			
		||||
func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.Db, fileName string) (db.Db, error) {
 | 
			
		||||
	database, ok := ctx.Value("Database").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return nil, fmt.Errorf("failed to select the database")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
	if existingDb != nil {
 | 
			
		||||
		return existingDb, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var newDb db.Db
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	if database == "postgres" {
 | 
			
		||||
		newDb = postgres.NewPgDb()
 | 
			
		||||
		connStr := buildConnStr()
 | 
			
		||||
		err = newDb.Connect(ctx, connStr)
 | 
			
		||||
	} else {
 | 
			
		||||
		newDb = gdbmstorage.NewThreadGdbmDb()
 | 
			
		||||
		storeFile := path.Join(ms.dbDir, fileName)
 | 
			
		||||
		err = newDb.Connect(ctx, storeFile)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return newDb, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ms *MenuStorageService) GetPersister(ctx context.Context) (*persist.Persister, error) {
 | 
			
		||||
@ -178,8 +166,8 @@ func (ms *MenuStorageService) GetStateStore(ctx context.Context) (db.Db, error)
 | 
			
		||||
	return ms.stateStore, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ms *MenuStorageService) ensureDbDir() error {
 | 
			
		||||
	err := os.MkdirAll(ms.conn.String(), 0700)
 | 
			
		||||
func (ms *MenuStorageService) EnsureDbDir() error {
 | 
			
		||||
	err := os.MkdirAll(ms.dbDir, 0700)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("state dir create exited with error: %v\n", err)
 | 
			
		||||
	}
 | 
			
		||||
							
								
								
									
										124
									
								
								internal/testutil/TestEngine.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								internal/testutil/TestEngine.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,124 @@
 | 
			
		||||
package testutil
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/testutil/testservice"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/testutil/testtag"
 | 
			
		||||
	testdataloader "github.com/peteole/testdata-loader"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/remote"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	baseDir   = testdataloader.GetBasePath()
 | 
			
		||||
	logg      = logging.NewVanilla()
 | 
			
		||||
	scriptDir = path.Join(baseDir, "services", "registration")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestEngine(sessionId string) (engine.Engine, func(), chan bool) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	ctx = context.WithValue(ctx, "SessionId", sessionId)
 | 
			
		||||
	ctx = context.WithValue(ctx, "Database", "gdbm")
 | 
			
		||||
	pfp := path.Join(scriptDir, "pp.csv")
 | 
			
		||||
 | 
			
		||||
	var eventChannel = make(chan bool)
 | 
			
		||||
 | 
			
		||||
	cfg := engine.Config{
 | 
			
		||||
		Root:       "root",
 | 
			
		||||
		SessionId:  sessionId,
 | 
			
		||||
		OutputSize: uint32(160),
 | 
			
		||||
		FlagCount:  uint32(128),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dbDir := ".test_state"
 | 
			
		||||
	resourceDir := scriptDir
 | 
			
		||||
	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
 | 
			
		||||
 | 
			
		||||
	err := menuStorageService.EnsureDbDir()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rs, err := menuStorageService.GetResource(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pe, err := menuStorageService.GetPersister(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userDataStore, err := menuStorageService.GetUserdataDb(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dbResource, ok := rs.(*resource.DbResource)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	lhs, err := handlers.NewLocalHandlerService(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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										47
									
								
								internal/testutil/mocks/httpmocks/requesthandlermock.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								internal/testutil/mocks/httpmocks/requesthandlermock.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/servicemock.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								internal/testutil/mocks/servicemock.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/TestAccountService.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								internal/testutil/testservice/TestAccountService.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
 | 
			
		||||
)
 | 
			
		||||
							
								
								
									
										452
									
								
								menutraversal_test/group_test.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										452
									
								
								menutraversal_test/group_test.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,452 @@
 | 
			
		||||
{
 | 
			
		||||
    "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\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\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\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"
 | 
			
		||||
                }
 | 
			
		||||
            ]
 | 
			
		||||
        }
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
      
 | 
			
		||||
        
 | 
			
		||||
       
 | 
			
		||||
        
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										383
									
								
								menutraversal_test/menu_traversal_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										383
									
								
								menutraversal_test/menu_traversal_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,383 @@
 | 
			
		||||
package menutraversaltest
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"log"
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"os"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/testutil"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/testutil/driver"
 | 
			
		||||
	"github.com/gofrs/uuid"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	testData  = driver.ReadData()
 | 
			
		||||
	testStore = ".test_state"
 | 
			
		||||
	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")
 | 
			
		||||
 | 
			
		||||
func GenerateSessionId() string {
 | 
			
		||||
	uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g))
 | 
			
		||||
	v, err := uu.NewV4()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
	return v.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Extract the public key from the engine response
 | 
			
		||||
func extractPublicKey(response []byte) string {
 | 
			
		||||
	// Regex pattern to match the public key starting with 0x and 40 characters
 | 
			
		||||
	re := regexp.MustCompile(`0x[a-fA-F0-9]{40}`)
 | 
			
		||||
	match := re.Find(response)
 | 
			
		||||
	if match != nil {
 | 
			
		||||
		return string(match)
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Extracts the balance value from the engine response.
 | 
			
		||||
func extractBalance(response []byte) string {
 | 
			
		||||
	// Regex to match "Balance: <amount> <symbol>" followed by a newline
 | 
			
		||||
	re := regexp.MustCompile(`(?m)^Balance:\s+(\d+(\.\d+)?)\s+([A-Z]+)`)
 | 
			
		||||
	match := re.FindSubmatch(response)
 | 
			
		||||
	if match != nil {
 | 
			
		||||
		return string(match[1]) + " " + string(match[3]) // "<amount> <symbol>"
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Extracts the Maximum amount value from the engine response.
 | 
			
		||||
func extractMaxAmount(response []byte) string {
 | 
			
		||||
	// Regex to match "Maximum amount: <amount>" followed by a newline
 | 
			
		||||
	re := regexp.MustCompile(`(?m)^Maximum amount:\s+(\d+(\.\d+)?)`)
 | 
			
		||||
	match := re.FindSubmatch(response)
 | 
			
		||||
	if match != nil {
 | 
			
		||||
		return string(match[1]) // "<amount>"
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Extracts the send amount value from the engine response.
 | 
			
		||||
func extractSendAmount(response []byte) string {
 | 
			
		||||
	// Regex to match the pattern "will receive X.XX SYM from"
 | 
			
		||||
	re := regexp.MustCompile(`will receive (\d+\.\d{2}\s+[A-Z]+) from`)
 | 
			
		||||
	match := re.FindSubmatch(response)
 | 
			
		||||
	if match != nil {
 | 
			
		||||
		return string(match[1]) // Returns "X.XX SYM"
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestMain(m *testing.M) {
 | 
			
		||||
	sessionID = GenerateSessionId()
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if err := os.RemoveAll(testStore); err != nil {
 | 
			
		||||
			log.Fatalf("Failed to delete state store %s: %v", testStore, err)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
	m.Run()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestAccountCreationSuccessful(t *testing.T) {
 | 
			
		||||
	en, fn, eventChannel := testutil.TestEngine(sessionID)
 | 
			
		||||
	defer fn()
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	sessions := testData
 | 
			
		||||
	for _, session := range sessions {
 | 
			
		||||
		groups := driver.FilterGroupsByName(session.Groups, "account_creation_successful")
 | 
			
		||||
		for _, group := range groups {
 | 
			
		||||
			for _, step := range group.Steps {
 | 
			
		||||
				cont, err := en.Exec(ctx, []byte(step.Input))
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
 | 
			
		||||
				}
 | 
			
		||||
				if !cont {
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
				w := bytes.NewBuffer(nil)
 | 
			
		||||
				_, err = en.Flush(ctx, w)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
 | 
			
		||||
				}
 | 
			
		||||
				b := w.Bytes()
 | 
			
		||||
				match, err := step.MatchesExpectedContent(b)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
 | 
			
		||||
				}
 | 
			
		||||
				if !match {
 | 
			
		||||
					t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	<-eventChannel
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestAccountRegistrationRejectTerms(t *testing.T) {
 | 
			
		||||
	// Generate a new UUID for this edge case test
 | 
			
		||||
	uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g))
 | 
			
		||||
	v, err := uu.NewV4()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fail()
 | 
			
		||||
	}
 | 
			
		||||
	edgeCaseSessionID := v.String()
 | 
			
		||||
	en, fn, _ := testutil.TestEngine(edgeCaseSessionID)
 | 
			
		||||
	defer fn()
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	sessions := testData
 | 
			
		||||
	for _, session := range sessions {
 | 
			
		||||
		groups := driver.FilterGroupsByName(session.Groups, "account_creation_reject_terms")
 | 
			
		||||
		for _, group := range groups {
 | 
			
		||||
			for _, step := range group.Steps {
 | 
			
		||||
				cont, err := en.Exec(ctx, []byte(step.Input))
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				if !cont {
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
				w := bytes.NewBuffer(nil)
 | 
			
		||||
				if _, err := en.Flush(ctx, w); err != nil {
 | 
			
		||||
					t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				b := w.Bytes()
 | 
			
		||||
				match, err := step.MatchesExpectedContent(b)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
 | 
			
		||||
				}
 | 
			
		||||
				if !match {
 | 
			
		||||
					t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestMainMenuHelp(t *testing.T) {
 | 
			
		||||
	en, fn, _ := testutil.TestEngine(sessionID)
 | 
			
		||||
	defer fn()
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	sessions := testData
 | 
			
		||||
	for _, session := range sessions {
 | 
			
		||||
		groups := driver.FilterGroupsByName(session.Groups, "main_menu_help")
 | 
			
		||||
		for _, group := range groups {
 | 
			
		||||
			for _, step := range group.Steps {
 | 
			
		||||
				cont, err := en.Exec(ctx, []byte(step.Input))
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				if !cont {
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
				w := bytes.NewBuffer(nil)
 | 
			
		||||
				if _, err := en.Flush(ctx, w); err != nil {
 | 
			
		||||
					t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				b := w.Bytes()
 | 
			
		||||
				balance := extractBalance(b)
 | 
			
		||||
 | 
			
		||||
				expectedContent := []byte(step.ExpectedContent)
 | 
			
		||||
				expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
 | 
			
		||||
 | 
			
		||||
				step.ExpectedContent = string(expectedContent)
 | 
			
		||||
				match, err := step.MatchesExpectedContent(b)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
 | 
			
		||||
				}
 | 
			
		||||
				if !match {
 | 
			
		||||
					t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestMainMenuQuit(t *testing.T) {
 | 
			
		||||
	en, fn, _ := testutil.TestEngine(sessionID)
 | 
			
		||||
	defer fn()
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	sessions := testData
 | 
			
		||||
	for _, session := range sessions {
 | 
			
		||||
		groups := driver.FilterGroupsByName(session.Groups, "main_menu_quit")
 | 
			
		||||
		for _, group := range groups {
 | 
			
		||||
			for _, step := range group.Steps {
 | 
			
		||||
				cont, err := en.Exec(ctx, []byte(step.Input))
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				if !cont {
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
				w := bytes.NewBuffer(nil)
 | 
			
		||||
				if _, err := en.Flush(ctx, w); err != nil {
 | 
			
		||||
					t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				b := w.Bytes()
 | 
			
		||||
				balance := extractBalance(b)
 | 
			
		||||
 | 
			
		||||
				expectedContent := []byte(step.ExpectedContent)
 | 
			
		||||
				expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
 | 
			
		||||
 | 
			
		||||
				step.ExpectedContent = string(expectedContent)
 | 
			
		||||
				match, err := step.MatchesExpectedContent(b)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
 | 
			
		||||
				}
 | 
			
		||||
				if !match {
 | 
			
		||||
					t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestMyAccount_MyAddress(t *testing.T) {
 | 
			
		||||
	en, fn, _ := testutil.TestEngine(sessionID)
 | 
			
		||||
	defer fn()
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	sessions := testData
 | 
			
		||||
	for _, session := range sessions {
 | 
			
		||||
		groups := driver.FilterGroupsByName(session.Groups, "menu_my_account_my_address")
 | 
			
		||||
		for _, group := range groups {
 | 
			
		||||
			for index, step := range group.Steps {
 | 
			
		||||
				t.Logf("step %v with input %v", index, step.Input)
 | 
			
		||||
				cont, err := en.Exec(ctx, []byte(step.Input))
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Errorf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				if !cont {
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
				w := bytes.NewBuffer(nil)
 | 
			
		||||
				if _, err := en.Flush(ctx, w); err != nil {
 | 
			
		||||
					t.Errorf("Test case '%s' failed during Flush: %v", group.Name, err)
 | 
			
		||||
				}
 | 
			
		||||
				b := w.Bytes()
 | 
			
		||||
 | 
			
		||||
				balance := extractBalance(b)
 | 
			
		||||
				publicKey := extractPublicKey(b)
 | 
			
		||||
 | 
			
		||||
				expectedContent := []byte(step.ExpectedContent)
 | 
			
		||||
				expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
 | 
			
		||||
				expectedContent = bytes.Replace(expectedContent, []byte("{public_key}"), []byte(publicKey), -1)
 | 
			
		||||
 | 
			
		||||
				step.ExpectedContent = string(expectedContent)
 | 
			
		||||
				match, err := step.MatchesExpectedContent(b)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
 | 
			
		||||
				}
 | 
			
		||||
				if !match {
 | 
			
		||||
					t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expectedContent, b)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestMainMenuSend(t *testing.T) {
 | 
			
		||||
	en, fn, _ := testutil.TestEngine(sessionID)
 | 
			
		||||
	defer fn()
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	sessions := testData
 | 
			
		||||
	for _, session := range sessions {
 | 
			
		||||
		groups := driver.FilterGroupsByName(session.Groups, "send_with_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/accountresponse.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								models/accountresponse.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
			
		||||
package models
 | 
			
		||||
 | 
			
		||||
type AccountResult struct {
 | 
			
		||||
	PublicKey  string `json:"publicKey"`
 | 
			
		||||
	TrackingId string `json:"trackingId"`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										8
									
								
								models/balanceresponse.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								models/balanceresponse.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/trackstatusresponse.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								models/trackstatusresponse.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/accountservice.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										294
									
								
								remote/accountservice.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 *
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue
	
	Block a user