Compare commits
	
		
			111 Commits
		
	
	
		
			80b96e9bf6
			...
			c93a07832d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					c93a07832d | ||
| 
						 | 
					46bf21b7b8 | ||
| 
						 | 
					dff662663d | ||
| 
						 | 
					977d14c529 | ||
| 
						 | 
					fd6e5caf53 | ||
| 
						 | 
					840c22ca89 | ||
| 
						 | 
					f939a20543 | ||
| 
						 | 
					8387644019 | ||
| 
						 | 
					c535938dbc | ||
| 
						 | 
					b60d2648ea | ||
| 
						 | 
					85ede15613 | ||
| b11f11b5fa | |||
| 3d35a5de78 | |||
| a19ace85f8 | |||
| 
						 | 
					a7a8a482ab | ||
| 
						 | 
					d8e7c443b5 | ||
| 
						 | 
					5216a9383e | ||
| 
						 | 
					6cd639fd19 | ||
| 
						 | 
					a7ca280964 | ||
| 8f5ed0cd4f | |||
| 
						 | 
					f1664a43a8 | ||
| c29abfe21e | |||
| 9a6d8e5158 | |||
| 
						 | 
					9ca5091692 | ||
| db431c750e | |||
| 2b9c6d641e | |||
| 
						 | 
					b9712098ef | ||
| 
						 | 
					bcf1965a6c | ||
| 3747f87a7c | |||
| 1f0568df32 | |||
| 
						 | 
					24c513d4f0 | ||
| b3fd6f5c1a | |||
| 73eb765408 | |||
| f660f6c19a | |||
| 3fccfaab61 | |||
| 
						 | 
					b50a51df9b | ||
| 
						 | 
					df8c9aab0c | ||
| 
						 | 
					ddefdd7fb3 | ||
| 5734011f96 | |||
| 
						 | 
					379d98ccd5 | ||
| f40e11c267 | |||
| b698f08136 | |||
| 4d7589ad95 | |||
| efdb52bccd | |||
| 2ff9fed3c5 | |||
| 477b4cf8f6 | |||
| ed6651697a | |||
| c359d99075 | |||
| 8d477356f3 | |||
| 7f3294a8a2 | |||
| 4b5f08e25e | |||
| ea9cab930e | |||
| a37f6e6da3 | |||
| f59c3a53ef | |||
| 81c3378ea6 | |||
| 46a6d2bc6e | |||
| 
						 | 
					721f80d0f2 | ||
| f49e54a562 | |||
| 
						 | 
					5081b6d4ce | ||
| 4d72ae0313 | |||
| 4fe64a7747 | |||
| 3004698d5b | |||
| 50c7ff1046 | |||
| 07b061a68b | |||
| 6339f0c2e5 | |||
| 
						 | 
					1fa830f286 | ||
| 64fba91670 | |||
| c15958a1ad | |||
| ee442daefa | |||
| 656052dc74 | |||
| 6c5873da6f | |||
| 11d30583a4 | |||
| f83f539046 | |||
| 562bd4fa24 | |||
| 90df0eefc3 | |||
| b37f2a0a11 | |||
| 68e4c9af03 | |||
| c12e867ac3 | |||
| 79de0a9092 | |||
| 3ee15497a5 | |||
| 
						 | 
					e09e324a50 | ||
| 
						 | 
					599815c343 | ||
| 
						 | 
					462c0d7677 | ||
| 
						 | 
					02823fd64e | ||
| 
						 | 
					cd575c2edb | ||
| 
						 | 
					f8d8f265f1 | ||
| 
						 | 
					9371b52f3e | ||
| 
						 | 
					563000ec15 | ||
| 
						 | 
					52fd1eced2 | ||
| 
						 | 
					5c7a535288 | ||
| 
						 | 
					cc2f7b41df | ||
| 
						 | 
					d39740a09a | ||
| 
						 | 
					739fd90dfd | ||
| 
						 | 
					6789c4f550 | ||
| 
						 | 
					437f73827d | ||
| 
						 | 
					f0a4a0df61 | ||
| 
						 | 
					bd604219b8 | ||
| 
						 | 
					51b6fc0dde | ||
| 
						 | 
					cc9760125a | ||
| 
						 | 
					3a9f3fa373 | ||
| 
						 | 
					89c21847b9 | ||
| 
						 | 
					450dfa02cc | ||
| 
						 | 
					f61e65f4fe | ||
| 
						 | 
					a4d6cef9c0 | ||
| 
						 | 
					2992f7ae8e | ||
| 
						 | 
					dc61d05584 | ||
| 
						 | 
					e92e498726 | ||
| 
						 | 
					c3cbe1cd92 | ||
| 
						 | 
					418080d093 | ||
| 
						 | 
					2e30739ec9 | ||
| 
						 | 
					dc1674ec55 | 
							
								
								
									
										10
									
								
								.env.example
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								.env.example
									
									
									
									
									
								
							@ -6,13 +6,9 @@ HOST=127.0.0.1
 | 
				
			|||||||
AT_ENDPOINT=/ussd/africastalking
 | 
					AT_ENDPOINT=/ussd/africastalking
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#PostgreSQL
 | 
					#PostgreSQL
 | 
				
			||||||
DB_HOST=localhost
 | 
					DB_CONN=postgres://postgres:strongpass@localhost:5432/urdt_ussd
 | 
				
			||||||
DB_USER=postgres
 | 
					#DB_TIMEZONE=Africa/Nairobi
 | 
				
			||||||
DB_PASSWORD=strongpass
 | 
					#DB_SCHEMA=vise
 | 
				
			||||||
DB_NAME=urdt_ussd
 | 
					 | 
				
			||||||
DB_PORT=5432
 | 
					 | 
				
			||||||
DB_SSLMODE=disable
 | 
					 | 
				
			||||||
DB_TIMEZONE=Africa/Nairobi
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
#External API Calls
 | 
					#External API Calls
 | 
				
			||||||
CUSTODIAL_URL_BASE=http://localhost:5003
 | 
					CUSTODIAL_URL_BASE=http://localhost:5003
 | 
				
			||||||
 | 
				
			|||||||
@ -6,7 +6,7 @@ import (
 | 
				
			|||||||
	"os"
 | 
						"os"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.defalsify.org/vise.git/logging"
 | 
						"git.defalsify.org/vise.git/logging"
 | 
				
			||||||
	"git.grassecon.net/urdt/ussd/internal/utils"
 | 
						"git.grassecon.net/grassrootseconomics/visedriver/utils"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					var (
 | 
				
			||||||
@ -4,7 +4,7 @@ import (
 | 
				
			|||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.grassecon.net/urdt/ussd/devtools/admin/commands"
 | 
						"git.grassecon.net/grassrootseconomics/visedriver/cmd/admin/commands"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func main() {
 | 
					func main() {
 | 
				
			||||||
@ -1,165 +0,0 @@
 | 
				
			|||||||
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)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,190 +0,0 @@
 | 
				
			|||||||
package main
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"flag"
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"os/signal"
 | 
					 | 
				
			||||||
	"path"
 | 
					 | 
				
			||||||
	"syscall"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.defalsify.org/vise.git/engine"
 | 
					 | 
				
			||||||
	"git.defalsify.org/vise.git/logging"
 | 
					 | 
				
			||||||
	"git.defalsify.org/vise.git/resource"
 | 
					 | 
				
			||||||
	"git.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
									
									
									
									
									
								
							
							
						
						
									
										157
									
								
								cmd/http/main.go
									
									
									
									
									
								
							@ -1,157 +0,0 @@
 | 
				
			|||||||
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)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -10,8 +10,8 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	"git.defalsify.org/vise.git/logging"
 | 
						"git.defalsify.org/vise.git/logging"
 | 
				
			||||||
	"git.defalsify.org/vise.git/lang"
 | 
						"git.defalsify.org/vise.git/lang"
 | 
				
			||||||
	"git.grassecon.net/urdt/ussd/config"
 | 
						"git.grassecon.net/grassrootseconomics/visedriver/config"
 | 
				
			||||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
						"git.grassecon.net/grassrootseconomics/visedriver/initializers"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
							
								
								
									
										142
									
								
								cmd/main.go
									
									
									
									
									
								
							
							
						
						
									
										142
									
								
								cmd/main.go
									
									
									
									
									
								
							@ -1,142 +0,0 @@
 | 
				
			|||||||
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)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,34 +0,0 @@
 | 
				
			|||||||
# 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
									
									
									
									
									
								
							
							
						
						
									
										117
									
								
								cmd/ssh/main.go
									
									
									
									
									
								
							@ -1,117 +0,0 @@
 | 
				
			|||||||
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)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,44 +0,0 @@
 | 
				
			|||||||
package main
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"flag"
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.grassecon.net/urdt/ussd/internal/ssh"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func main() {
 | 
					 | 
				
			||||||
	var dbDir string
 | 
					 | 
				
			||||||
	var sessionId string
 | 
					 | 
				
			||||||
	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
 | 
					 | 
				
			||||||
	flag.StringVar(&sessionId, "i", "", "session id")
 | 
					 | 
				
			||||||
	flag.Parse()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if sessionId == "" {
 | 
					 | 
				
			||||||
		fmt.Fprintf(os.Stderr, "empty session id\n")
 | 
					 | 
				
			||||||
		os.Exit(1)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	ctx := context.Background()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	sshKeyFile := flag.Arg(0)
 | 
					 | 
				
			||||||
	if sshKeyFile == "" {
 | 
					 | 
				
			||||||
		fmt.Fprintf(os.Stderr, "missing key file argument\n")
 | 
					 | 
				
			||||||
		os.Exit(1)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	store, err := ssh.NewSshKeyStore(ctx, dbDir)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		fmt.Fprintf(os.Stderr, "%v\n", err)
 | 
					 | 
				
			||||||
		os.Exit(1)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	defer store.Close()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	err = store.AddFromFile(ctx, sshKeyFile, sessionId)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		fmt.Fprintf(os.Stderr, "%v\n", err)
 | 
					 | 
				
			||||||
		os.Exit(1)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -7,10 +7,10 @@ import (
 | 
				
			|||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.grassecon.net/urdt/ussd/config"
 | 
						"git.grassecon.net/grassrootseconomics/visedriver/config"
 | 
				
			||||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
						"git.grassecon.net/grassrootseconomics/visedriver/initializers"
 | 
				
			||||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
						"git.grassecon.net/grassrootseconomics/visedriver/storage"
 | 
				
			||||||
	"git.grassecon.net/urdt/ussd/debug"
 | 
						"git.grassecon.net/grassrootseconomics/visedriver/debug"
 | 
				
			||||||
	"git.defalsify.org/vise.git/db"
 | 
						"git.defalsify.org/vise.git/db"
 | 
				
			||||||
	"git.defalsify.org/vise.git/logging"
 | 
						"git.defalsify.org/vise.git/logging"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@ -37,23 +37,34 @@ func formatItem(k []byte, v []byte) (string, error) {
 | 
				
			|||||||
func main() {
 | 
					func main() {
 | 
				
			||||||
	config.LoadConfig()
 | 
						config.LoadConfig()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var dbDir string
 | 
						var connStr string
 | 
				
			||||||
	var sessionId string
 | 
						var sessionId string
 | 
				
			||||||
	var database string
 | 
						var database string
 | 
				
			||||||
	var engineDebug bool
 | 
						var engineDebug bool
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
 | 
						flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
 | 
				
			||||||
	flag.StringVar(&database, "db", "gdbm", "database to be used")
 | 
						flag.StringVar(&connStr, "c", ".state", "connection string")
 | 
				
			||||||
	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
 | 
					 | 
				
			||||||
	flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
 | 
						flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
 | 
				
			||||||
	flag.Parse()
 | 
						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.Background()
 | 
				
			||||||
	ctx = context.WithValue(ctx, "SessionId", sessionId)
 | 
						ctx = context.WithValue(ctx, "SessionId", sessionId)
 | 
				
			||||||
	ctx = context.WithValue(ctx, "Database", database)
 | 
						ctx = context.WithValue(ctx, "Database", database)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	resourceDir := scriptDir
 | 
						resourceDir := scriptDir
 | 
				
			||||||
	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
 | 
						menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	store, err := menuStorageService.GetUserdataDb(ctx)
 | 
						store, err := menuStorageService.GetUserdataDb(ctx)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@ -8,15 +8,17 @@ import (
 | 
				
			|||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						testdataloader "github.com/peteole/testdata-loader"
 | 
				
			||||||
	"git.defalsify.org/vise.git/logging"
 | 
						"git.defalsify.org/vise.git/logging"
 | 
				
			||||||
	"git.grassecon.net/urdt/ussd/config"
 | 
						"git.grassecon.net/grassrootseconomics/visedriver/config"
 | 
				
			||||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
						"git.grassecon.net/grassrootseconomics/visedriver/storage"
 | 
				
			||||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
						"git.grassecon.net/grassrootseconomics/visedriver/initializers"
 | 
				
			||||||
	"git.grassecon.net/urdt/ussd/common"
 | 
						"git.grassecon.net/grassrootseconomics/visedriver/common"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					var (
 | 
				
			||||||
	logg      = logging.NewVanilla()
 | 
						logg      = logging.NewVanilla()
 | 
				
			||||||
 | 
						baseDir   = testdataloader.GetBasePath()
 | 
				
			||||||
	scriptDir = path.Join("services", "registration")
 | 
						scriptDir = path.Join("services", "registration")
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -24,27 +26,37 @@ func init() {
 | 
				
			|||||||
	initializers.LoadEnvVariables()
 | 
						initializers.LoadEnvVariables()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
func main() {
 | 
					func main() {
 | 
				
			||||||
	config.LoadConfig()
 | 
						config.LoadConfig()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var dbDir string
 | 
						var connStr string
 | 
				
			||||||
	var sessionId string
 | 
						var sessionId string
 | 
				
			||||||
	var database string
 | 
						var database string
 | 
				
			||||||
	var engineDebug bool
 | 
						var engineDebug bool
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
 | 
						flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
 | 
				
			||||||
	flag.StringVar(&database, "db", "gdbm", "database to be used")
 | 
						flag.StringVar(&connStr, "c", "", "connection string")
 | 
				
			||||||
	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
 | 
					 | 
				
			||||||
	flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
 | 
						flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
 | 
				
			||||||
	flag.Parse()
 | 
						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.Background()
 | 
				
			||||||
	ctx = context.WithValue(ctx, "SessionId", sessionId)
 | 
						ctx = context.WithValue(ctx, "SessionId", sessionId)
 | 
				
			||||||
	ctx = context.WithValue(ctx, "Database", database)
 | 
						ctx = context.WithValue(ctx, "Database", database)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	resourceDir := scriptDir
 | 
						resourceDir := scriptDir
 | 
				
			||||||
	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
 | 
						menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	store, err := menuStorageService.GetUserdataDb(ctx)
 | 
						store, err := menuStorageService.GetUserdataDb(ctx)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@ -75,5 +87,4 @@ func main() {
 | 
				
			|||||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
							fmt.Fprintf(os.Stderr, err.Error())
 | 
				
			||||||
		os.Exit(1)
 | 
							os.Exit(1)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -55,6 +55,10 @@ const (
 | 
				
			|||||||
	DATA_ACTIVE_DECIMAL
 | 
						DATA_ACTIVE_DECIMAL
 | 
				
			||||||
	// EVM address of the currently active voucher
 | 
						// EVM address of the currently active voucher
 | 
				
			||||||
	DATA_ACTIVE_ADDRESS
 | 
						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 (
 | 
					const (
 | 
				
			||||||
 | 
				
			|||||||
@ -6,9 +6,13 @@ import (
 | 
				
			|||||||
	"golang.org/x/crypto/bcrypt"
 | 
						"golang.org/x/crypto/bcrypt"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Define the regex pattern as a constant
 | 
					 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
 | 
						// Define the regex pattern as a constant
 | 
				
			||||||
	pinPattern = `^\d{4}$`
 | 
						pinPattern = `^\d{4}$`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						//Allowed incorrect  PIN attempts
 | 
				
			||||||
 | 
						AllowedPINAttempts = uint8(3)
 | 
				
			||||||
 | 
						
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// checks whether the given input is a 4 digit number
 | 
					// checks whether the given input is a 4 digit number
 | 
				
			||||||
 | 
				
			|||||||
@ -7,8 +7,13 @@ import (
 | 
				
			|||||||
	"git.defalsify.org/vise.git/db"
 | 
						"git.defalsify.org/vise.git/db"
 | 
				
			||||||
	"git.defalsify.org/vise.git/resource"
 | 
						"git.defalsify.org/vise.git/resource"
 | 
				
			||||||
	"git.defalsify.org/vise.git/persist"
 | 
						"git.defalsify.org/vise.git/persist"
 | 
				
			||||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
						"git.defalsify.org/vise.git/lang"
 | 
				
			||||||
	dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
 | 
						"git.grassecon.net/grassrootseconomics/visedriver/storage"
 | 
				
			||||||
 | 
						dbstorage "git.grassecon.net/grassrootseconomics/visedriver/storage/db"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						ToConnData = storage.ToConnData
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func StoreToDb(store *UserDataStore) db.Db {
 | 
					func StoreToDb(store *UserDataStore) db.Db {
 | 
				
			||||||
@ -23,17 +28,28 @@ type StorageServices interface {
 | 
				
			|||||||
	GetPersister(ctx context.Context) (*persist.Persister, error)
 | 
						GetPersister(ctx context.Context) (*persist.Persister, error)
 | 
				
			||||||
	GetUserdataDb(ctx context.Context) (db.Db, error)
 | 
						GetUserdataDb(ctx context.Context) (db.Db, error)
 | 
				
			||||||
	GetResource(ctx context.Context) (resource.Resource, error)
 | 
						GetResource(ctx context.Context) (resource.Resource, error)
 | 
				
			||||||
	EnsureDbDir() error
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type StorageService struct {
 | 
					type StorageService struct {
 | 
				
			||||||
	svc *storage.MenuStorageService
 | 
						svc *storage.MenuStorageService
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func NewStorageService(dbDir string) *StorageService {
 | 
					func NewStorageService(conn storage.ConnData) (*StorageService, error) {
 | 
				
			||||||
	return &StorageService{
 | 
						svc := &StorageService{
 | 
				
			||||||
		svc: storage.NewMenuStorageService(dbDir, ""),
 | 
							svc: storage.NewMenuStorageService(conn, ""),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						return svc, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (ss *StorageService) WithGettext(path string, lns []lang.Language) *StorageService {
 | 
				
			||||||
 | 
						ss.svc = ss.svc.WithGettext(path, lns)
 | 
				
			||||||
 | 
						return ss
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TODO: simplify enable poresource, conndata instead
 | 
				
			||||||
 | 
					func(ss *StorageService) SetResourceDir(resourceDir string) error {
 | 
				
			||||||
 | 
						ss.svc = ss.svc.WithResourceDir(resourceDir)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func(ss *StorageService) GetPersister(ctx context.Context) (*persist.Persister, error) {
 | 
					func(ss *StorageService) GetPersister(ctx context.Context) (*persist.Persister, error) {
 | 
				
			||||||
@ -48,6 +64,6 @@ func(ss *StorageService) GetResource(ctx context.Context) (resource.Resource, er
 | 
				
			|||||||
	return nil, errors.New("not implemented")
 | 
						return nil, errors.New("not implemented")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func(ss *StorageService) EnsureDbDir() error {
 | 
					func(ss *StorageService) GetStateStore(ctx context.Context) (db.Db, error) {
 | 
				
			||||||
	return ss.svc.EnsureDbDir()
 | 
						return ss.svc.GetStateStore(ctx)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -6,7 +6,7 @@ import (
 | 
				
			|||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
 | 
						dbstorage "git.grassecon.net/grassrootseconomics/visedriver/storage/db"
 | 
				
			||||||
	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
 | 
						dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -6,7 +6,7 @@ import (
 | 
				
			|||||||
	"math/big"
 | 
						"math/big"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
 | 
						dbstorage "git.grassecon.net/grassrootseconomics/visedriver/storage/db"
 | 
				
			||||||
	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
 | 
						dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -10,7 +10,7 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	visedb "git.defalsify.org/vise.git/db"
 | 
						visedb "git.defalsify.org/vise.git/db"
 | 
				
			||||||
	memdb "git.defalsify.org/vise.git/db/mem"
 | 
						memdb "git.defalsify.org/vise.git/db/mem"
 | 
				
			||||||
	dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
 | 
						dbstorage "git.grassecon.net/grassrootseconomics/visedriver/storage/db"
 | 
				
			||||||
	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
 | 
						dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,7 @@ import (
 | 
				
			|||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
						"git.grassecon.net/grassrootseconomics/visedriver/initializers"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
@ -40,6 +40,7 @@ var (
 | 
				
			|||||||
	VoucherTransfersURL string
 | 
						VoucherTransfersURL string
 | 
				
			||||||
	VoucherDataURL      string
 | 
						VoucherDataURL      string
 | 
				
			||||||
	CheckAliasURL       string
 | 
						CheckAliasURL       string
 | 
				
			||||||
 | 
						DbConn		string
 | 
				
			||||||
	DefaultLanguage	    string
 | 
						DefaultLanguage	    string
 | 
				
			||||||
	Languages	[]string
 | 
						Languages	[]string
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@ -69,14 +70,20 @@ func setBase() error {
 | 
				
			|||||||
	dataURLBase = initializers.GetEnv("DATA_URL_BASE", "http://localhost:5006")
 | 
						dataURLBase = initializers.GetEnv("DATA_URL_BASE", "http://localhost:5006")
 | 
				
			||||||
	BearerToken = initializers.GetEnv("BEARER_TOKEN", "")
 | 
						BearerToken = initializers.GetEnv("BEARER_TOKEN", "")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	_, err = url.JoinPath(custodialURLBase, "/foo")
 | 
						_, err = url.Parse(custodialURLBase)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	_, err = url.JoinPath(dataURLBase, "/bar")
 | 
						_, err = url.Parse(dataURLBase)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func setConn() error {
 | 
				
			||||||
 | 
						DbConn = initializers.GetEnv("DB_CONN", "")
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -86,6 +93,10 @@ func LoadConfig() error {
 | 
				
			|||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						err = setConn()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	err = setLanguage()
 | 
						err = setLanguage()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,7 @@ import (
 | 
				
			|||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"encoding/binary"
 | 
						"encoding/binary"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.grassecon.net/urdt/ussd/common"
 | 
						"git.grassecon.net/grassrootseconomics/visedriver/common"
 | 
				
			||||||
	"git.defalsify.org/vise.git/db"
 | 
						"git.defalsify.org/vise.git/db"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -5,7 +5,7 @@ package debug
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"git.defalsify.org/vise.git/db"
 | 
						"git.defalsify.org/vise.git/db"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.grassecon.net/urdt/ussd/common"
 | 
						"git.grassecon.net/grassrootseconomics/visedriver/common"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func init() {
 | 
					func init() {
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@ package debug
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.grassecon.net/urdt/ussd/common"
 | 
						"git.grassecon.net/grassrootseconomics/visedriver/common"
 | 
				
			||||||
	"git.defalsify.org/vise.git/db"
 | 
						"git.defalsify.org/vise.git/db"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										13
									
								
								entry/handlers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								entry/handlers.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					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)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										15
									
								
								errors/errors.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								errors/errors.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					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/urdt/ussd
 | 
					module git.grassecon.net/grassrootseconomics/visedriver
 | 
				
			||||||
 | 
					
 | 
				
			||||||
go 1.23.0
 | 
					go 1.23.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -8,15 +8,14 @@ require (
 | 
				
			|||||||
	github.com/gofrs/uuid v4.4.0+incompatible
 | 
						github.com/gofrs/uuid v4.4.0+incompatible
 | 
				
			||||||
	github.com/grassrootseconomics/eth-custodial v1.3.0-beta
 | 
						github.com/grassrootseconomics/eth-custodial v1.3.0-beta
 | 
				
			||||||
	github.com/grassrootseconomics/ussd-data-service v1.2.0-beta
 | 
						github.com/grassrootseconomics/ussd-data-service v1.2.0-beta
 | 
				
			||||||
 | 
						github.com/jackc/pgx/v5 v5.7.1
 | 
				
			||||||
	github.com/joho/godotenv v1.5.1
 | 
						github.com/joho/godotenv v1.5.1
 | 
				
			||||||
	github.com/peteole/testdata-loader v0.3.0
 | 
						github.com/peteole/testdata-loader v0.3.0
 | 
				
			||||||
	github.com/stretchr/testify v1.9.0
 | 
						github.com/stretchr/testify v1.9.0
 | 
				
			||||||
	golang.org/x/crypto v0.27.0
 | 
						golang.org/x/crypto v0.27.0
 | 
				
			||||||
	gopkg.in/leonelquinteros/gotext.v1 v1.3.1
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require (
 | 
					require (
 | 
				
			||||||
	github.com/alecthomas/participle/v2 v2.0.0 // indirect
 | 
					 | 
				
			||||||
	github.com/alecthomas/repr v0.2.0 // indirect
 | 
						github.com/alecthomas/repr v0.2.0 // indirect
 | 
				
			||||||
	github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect
 | 
						github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect
 | 
				
			||||||
	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 | 
						github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 | 
				
			||||||
@ -25,7 +24,6 @@ require (
 | 
				
			|||||||
	github.com/hexops/gotextdiff v1.0.3 // indirect
 | 
						github.com/hexops/gotextdiff v1.0.3 // indirect
 | 
				
			||||||
	github.com/jackc/pgpassfile v1.0.0 // indirect
 | 
						github.com/jackc/pgpassfile v1.0.0 // indirect
 | 
				
			||||||
	github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
 | 
						github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
 | 
				
			||||||
	github.com/jackc/pgx/v5 v5.7.1 // indirect
 | 
					 | 
				
			||||||
	github.com/jackc/puddle/v2 v2.2.2 // indirect
 | 
						github.com/jackc/puddle/v2 v2.2.2 // indirect
 | 
				
			||||||
	github.com/kr/text v0.2.0 // indirect
 | 
						github.com/kr/text v0.2.0 // indirect
 | 
				
			||||||
	github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect
 | 
						github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect
 | 
				
			||||||
@ -34,6 +32,8 @@ require (
 | 
				
			|||||||
	github.com/stretchr/objx v0.5.2 // indirect
 | 
						github.com/stretchr/objx v0.5.2 // indirect
 | 
				
			||||||
	github.com/x448/float16 v0.8.4 // indirect
 | 
						github.com/x448/float16 v0.8.4 // indirect
 | 
				
			||||||
	golang.org/x/sync v0.8.0 // indirect
 | 
						golang.org/x/sync v0.8.0 // indirect
 | 
				
			||||||
 | 
						golang.org/x/sys v0.25.0 // indirect
 | 
				
			||||||
	golang.org/x/text v0.18.0 // indirect
 | 
						golang.org/x/text v0.18.0 // indirect
 | 
				
			||||||
 | 
						gopkg.in/leonelquinteros/gotext.v1 v1.3.1 // indirect
 | 
				
			||||||
	gopkg.in/yaml.v3 v3.0.1 // indirect
 | 
						gopkg.in/yaml.v3 v3.0.1 // indirect
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										6
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								go.sum
									
									
									
									
									
								
							@ -2,8 +2,6 @@ git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d h1:bPAOVZOX4frSG
 | 
				
			|||||||
git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
 | 
					git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
 | 
				
			||||||
github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk=
 | 
					github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk=
 | 
				
			||||||
github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
 | 
					github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
 | 
				
			||||||
github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g=
 | 
					 | 
				
			||||||
github.com/alecthomas/participle/v2 v2.0.0/go.mod h1:rAKZdJldHu8084ojcWevWAL8KmEU+AT+Olodb+WoN2Y=
 | 
					 | 
				
			||||||
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
 | 
					github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
 | 
				
			||||||
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
 | 
					github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
 | 
				
			||||||
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c h1:H9Nm+I7Cg/YVPpEV1RzU3Wq2pjamPc/UtHDgItcb7lE=
 | 
					github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c h1:H9Nm+I7Cg/YVPpEV1RzU3Wq2pjamPc/UtHDgItcb7lE=
 | 
				
			||||||
@ -62,6 +60,10 @@ golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
 | 
				
			|||||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
 | 
					golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
 | 
				
			||||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
 | 
					golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
 | 
				
			||||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 | 
					golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 | 
				
			||||||
 | 
					golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
 | 
				
			||||||
 | 
					golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
 | 
				
			||||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
 | 
					golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
 | 
				
			||||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
 | 
					golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
 | 
				
			||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
					gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
				
			||||||
 | 
				
			|||||||
@ -3,24 +3,30 @@ package initializers
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/joho/godotenv"
 | 
						"github.com/joho/godotenv"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func LoadEnvVariables() {
 | 
					func LoadEnvVariables() {
 | 
				
			||||||
	err := godotenv.Load()
 | 
						LoadEnvVariablesPath(".")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func LoadEnvVariablesPath(dir string) {
 | 
				
			||||||
 | 
						fp := path.Join(dir, ".env")
 | 
				
			||||||
 | 
						err := godotenv.Load(fp)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Fatal("Error loading .env file")
 | 
							log.Fatal("Error loading .env file", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Helper to get environment variables with a default fallback
 | 
					// Helper to get environment variables with a default fallback
 | 
				
			||||||
func GetEnv(key, defaultVal string) string {
 | 
					func GetEnv(key, defaultVal string) string {
 | 
				
			||||||
  if value, exists := os.LookupEnv(key); exists {
 | 
						if value, exists := os.LookupEnv(key); exists {
 | 
				
			||||||
   	return value
 | 
							return value
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
  return defaultVal
 | 
						return defaultVal
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Helper to safely convert environment variables to uint
 | 
					// Helper to safely convert environment variables to uint
 | 
				
			||||||
 | 
				
			|||||||
@ -1,34 +0,0 @@
 | 
				
			|||||||
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,140 +0,0 @@
 | 
				
			|||||||
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/persist"
 | 
				
			||||||
	"git.defalsify.org/vise.git/resource"
 | 
						"git.defalsify.org/vise.git/resource"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
						"git.grassecon.net/grassrootseconomics/visedriver/storage"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					var (
 | 
				
			||||||
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,119 +0,0 @@
 | 
				
			|||||||
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
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,98 +0,0 @@
 | 
				
			|||||||
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
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,234 +0,0 @@
 | 
				
			|||||||
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)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@ -1,91 +0,0 @@
 | 
				
			|||||||
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
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,65 +0,0 @@
 | 
				
			|||||||
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()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,287 +0,0 @@
 | 
				
			|||||||
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)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,124 +0,0 @@
 | 
				
			|||||||
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
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,47 +0,0 @@
 | 
				
			|||||||
package httpmocks
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"git.defalsify.org/vise.git/engine"
 | 
					 | 
				
			||||||
	"git.defalsify.org/vise.git/persist"
 | 
					 | 
				
			||||||
	"git.defalsify.org/vise.git/resource"
 | 
					 | 
				
			||||||
	"git.grassecon.net/urdt/ussd/internal/handlers"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// MockRequestHandler implements handlers.RequestHandler interface for testing
 | 
					 | 
				
			||||||
type MockRequestHandler struct {
 | 
					 | 
				
			||||||
	ProcessFunc          func(handlers.RequestSession) (handlers.RequestSession, error)
 | 
					 | 
				
			||||||
	GetConfigFunc        func() engine.Config
 | 
					 | 
				
			||||||
	GetEngineFunc        func(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine
 | 
					 | 
				
			||||||
	OutputFunc           func(rs handlers.RequestSession) (handlers.RequestSession, error)
 | 
					 | 
				
			||||||
	ResetFunc            func(rs handlers.RequestSession) (handlers.RequestSession, error)
 | 
					 | 
				
			||||||
	ShutdownFunc         func()
 | 
					 | 
				
			||||||
	GetRequestParserFunc func() handlers.RequestParser
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m *MockRequestHandler) Process(rqs handlers.RequestSession) (handlers.RequestSession, error) {
 | 
					 | 
				
			||||||
	return m.ProcessFunc(rqs)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m *MockRequestHandler) GetConfig() engine.Config {
 | 
					 | 
				
			||||||
	return m.GetConfigFunc()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m *MockRequestHandler) GetEngine(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine {
 | 
					 | 
				
			||||||
	return m.GetEngineFunc(cfg, rs, pe)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m *MockRequestHandler) Output(rs handlers.RequestSession) (handlers.RequestSession, error) {
 | 
					 | 
				
			||||||
	return m.OutputFunc(rs)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m *MockRequestHandler) Reset(rs handlers.RequestSession) (handlers.RequestSession, error) {
 | 
					 | 
				
			||||||
	return m.ResetFunc(rs)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m *MockRequestHandler) Shutdown() {
 | 
					 | 
				
			||||||
	m.ShutdownFunc()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m *MockRequestHandler) GetRequestParser() handlers.RequestParser {
 | 
					 | 
				
			||||||
	return m.GetRequestParserFunc()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,54 +0,0 @@
 | 
				
			|||||||
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)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,62 +0,0 @@
 | 
				
			|||||||
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
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,12 +0,0 @@
 | 
				
			|||||||
// +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{}
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
@ -1,9 +0,0 @@
 | 
				
			|||||||
// +build online
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package testtag
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import "git.grassecon.net/urdt/ussd/remote"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var (
 | 
					 | 
				
			||||||
	AccountService remote.AccountServiceInterface
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
@ -1,452 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
    "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"
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            ]
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
      
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
       
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@ -1,383 +0,0 @@
 | 
				
			|||||||
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)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,68 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
    "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"
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
      
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
       
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
@ -1,61 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
    "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"
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            ]
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,55 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
    "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"
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
      
 | 
					 | 
				
			||||||
      
 | 
					 | 
				
			||||||
@ -1,46 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
    "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"
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
     
 | 
					 | 
				
			||||||
@ -1,42 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
    "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"
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
     
 | 
					 | 
				
			||||||
@ -1,50 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
    "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"
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
     
 | 
					 | 
				
			||||||
@ -1,70 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
    "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"
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
@ -1,133 +0,0 @@
 | 
				
			|||||||
[
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        "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!"
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
@ -1,6 +0,0 @@
 | 
				
			|||||||
package models
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type AccountResult struct {
 | 
					 | 
				
			||||||
	PublicKey  string `json:"publicKey"`
 | 
					 | 
				
			||||||
	TrackingId string `json:"trackingId"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,8 +0,0 @@
 | 
				
			|||||||
package models
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import "encoding/json"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type BalanceResult struct {
 | 
					 | 
				
			||||||
	Balance string      `json:"balance"`
 | 
					 | 
				
			||||||
	Nonce   json.Number `json:"nonce"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,18 +0,0 @@
 | 
				
			|||||||
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
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,5 +0,0 @@
 | 
				
			|||||||
package models
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type TokenTransferResponse struct {
 | 
					 | 
				
			||||||
	TrackingId string `json:"trackingId"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,18 +0,0 @@
 | 
				
			|||||||
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"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,10 +0,0 @@
 | 
				
			|||||||
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"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,294 +0,0 @@
 | 
				
			|||||||
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))
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										42
									
								
								request/request.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								request/request.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					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()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,18 +0,0 @@
 | 
				
			|||||||
# 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 +0,0 @@
 | 
				
			|||||||
Something went wrong.Please try again
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
HALT
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
Tatizo la kimtambo limetokea,tafadhali jaribu tena baadaye.
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
Your account is being created...
 | 
					 | 
				
			||||||
@ -1,4 +0,0 @@
 | 
				
			|||||||
RELOAD verify_create_pin
 | 
					 | 
				
			||||||
CATCH create_pin_mismatch flag_pin_mismatch 1
 | 
					 | 
				
			||||||
LOAD quit 0
 | 
					 | 
				
			||||||
HALT
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
Your account creation request failed. Please try again later.
 | 
					 | 
				
			||||||
@ -1,3 +0,0 @@
 | 
				
			|||||||
MOUT quit 9
 | 
					 | 
				
			||||||
HALT
 | 
					 | 
				
			||||||
INCMP quit 9
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
Ombi lako la kusajiliwa haliwezi kukamilishwa. Tafadhali jaribu tena baadaye.
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
Akaunti yako inatengenezwa...
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
My Account
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
Akaunti yangu
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
Your account is still being created.
 | 
					 | 
				
			||||||
@ -1,3 +0,0 @@
 | 
				
			|||||||
RELOAD check_account_status
 | 
					 | 
				
			||||||
CATCH main flag_account_success 1
 | 
					 | 
				
			||||||
HALT
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
Akaunti yako bado inatengenezwa
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
Address: {{.check_identifier}}
 | 
					 | 
				
			||||||
@ -1,8 +0,0 @@
 | 
				
			|||||||
LOAD check_identifier 0
 | 
					 | 
				
			||||||
RELOAD check_identifier
 | 
					 | 
				
			||||||
MAP check_identifier
 | 
					 | 
				
			||||||
MOUT back 0
 | 
					 | 
				
			||||||
MOUT quit 9
 | 
					 | 
				
			||||||
HALT
 | 
					 | 
				
			||||||
INCMP _ 0
 | 
					 | 
				
			||||||
INCMP quit 9
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
Anwani:{{.check_identifier}}
 | 
					 | 
				
			||||||
@ -1,2 +0,0 @@
 | 
				
			|||||||
Maximum amount: {{.max_amount}}
 | 
					 | 
				
			||||||
Enter amount:
 | 
					 | 
				
			||||||
@ -1,15 +0,0 @@
 | 
				
			|||||||
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 *
 | 
					 | 
				
			||||||
@ -1,2 +0,0 @@
 | 
				
			|||||||
Kiwango cha juu: {{.max_amount}}
 | 
					 | 
				
			||||||
Weka kiwango:
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
Failed to connect to the custodial service.Please try again.
 | 
					 | 
				
			||||||
@ -1,5 +0,0 @@
 | 
				
			|||||||
MOUT retry 1
 | 
					 | 
				
			||||||
MOUT quit 9
 | 
					 | 
				
			||||||
HALT
 | 
					 | 
				
			||||||
INCMP _ 1
 | 
					 | 
				
			||||||
INCMP quit 9
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
Back
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
Rudi
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
Balances:
 | 
					 | 
				
			||||||
@ -1,10 +0,0 @@
 | 
				
			|||||||
LOAD reset_account_authorized 0
 | 
					 | 
				
			||||||
RELOAD reset_account_authorized
 | 
					 | 
				
			||||||
MOUT my_balance 1
 | 
					 | 
				
			||||||
MOUT community_balance 2
 | 
					 | 
				
			||||||
MOUT back 0
 | 
					 | 
				
			||||||
HALT
 | 
					 | 
				
			||||||
INCMP _ 0
 | 
					 | 
				
			||||||
INCMP my_balance 1
 | 
					 | 
				
			||||||
INCMP community_balance 2
 | 
					 | 
				
			||||||
INCMP . * 
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
Salio:
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
Select language:
 | 
					 | 
				
			||||||
@ -1,10 +0,0 @@
 | 
				
			|||||||
LOAD reset_account_authorized 0
 | 
					 | 
				
			||||||
LOAD reset_incorrect 0
 | 
					 | 
				
			||||||
CATCH incorrect_pin flag_incorrect_pin 1
 | 
					 | 
				
			||||||
CATCH pin_entry flag_account_authorized 0
 | 
					 | 
				
			||||||
MOUT english 1
 | 
					 | 
				
			||||||
MOUT kiswahili 2
 | 
					 | 
				
			||||||
HALT
 | 
					 | 
				
			||||||
INCMP set_eng 1
 | 
					 | 
				
			||||||
INCMP set_swa 2
 | 
					 | 
				
			||||||
INCMP . *
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
Change language
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
Badili lugha
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
Chagua lugha:
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
Change PIN
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
Badili PIN
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
Check balances
 | 
					 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue
	
	Block a user