Compare commits
	
		
			No commits in common. "master" and "lash/persister-freakout" have entirely different histories.
		
	
	
		
			master
			...
			lash/persi
		
	
		
@ -1,6 +1,5 @@
 | 
			
		||||
/**
 | 
			
		||||
!/cmd/africastalking
 | 
			
		||||
!/cmd/ssh
 | 
			
		||||
!/common
 | 
			
		||||
!/config
 | 
			
		||||
!/initializers
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										14
									
								
								.env.example
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								.env.example
									
									
									
									
									
								
							@ -6,15 +6,15 @@ HOST=127.0.0.1
 | 
			
		||||
AT_ENDPOINT=/ussd/africastalking
 | 
			
		||||
 | 
			
		||||
#PostgreSQL
 | 
			
		||||
DB_CONN=postgres://postgres:strongpass@localhost:5432/urdt_ussd
 | 
			
		||||
#DB_TIMEZONE=Africa/Nairobi
 | 
			
		||||
#DB_SCHEMA=vise
 | 
			
		||||
DB_HOST=localhost
 | 
			
		||||
DB_USER=postgres
 | 
			
		||||
DB_PASSWORD=strongpass
 | 
			
		||||
DB_NAME=urdt_ussd
 | 
			
		||||
DB_PORT=5432
 | 
			
		||||
DB_SSLMODE=disable
 | 
			
		||||
DB_TIMEZONE=Africa/Nairobi
 | 
			
		||||
 | 
			
		||||
#External API Calls
 | 
			
		||||
CUSTODIAL_URL_BASE=http://localhost:5003
 | 
			
		||||
BEARER_TOKEN=eyJeSIsInRcCI6IkpXVCJ.yJwdWJsaWNLZXkiOiIwrrrrrr
 | 
			
		||||
DATA_URL_BASE=http://localhost:5006
 | 
			
		||||
 | 
			
		||||
#Language
 | 
			
		||||
DEFAULT_LANGUAGE=eng
 | 
			
		||||
LANGUAGES=eng, swa
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,6 @@ WORKDIR /build
 | 
			
		||||
RUN echo "Building on $BUILDPLATFORM, building for $TARGETPLATFORM"
 | 
			
		||||
RUN go mod download
 | 
			
		||||
RUN go build -tags logtrace -o ussd-africastalking -ldflags="-X main.build=${BUILD} -s -w" cmd/africastalking/main.go
 | 
			
		||||
RUN go build -tags logtrace -o ussd-ssh -ldflags="-X main.build=${BUILD} -s -w" cmd/ssh/main.go
 | 
			
		||||
 | 
			
		||||
FROM debian:bookworm-slim
 | 
			
		||||
 | 
			
		||||
@ -31,7 +30,6 @@ RUN apt-get clean && rm -rf /var/lib/apt/lists/*
 | 
			
		||||
WORKDIR /service
 | 
			
		||||
 | 
			
		||||
COPY --from=build /build/ussd-africastalking .
 | 
			
		||||
COPY --from=build /build/ussd-ssh .
 | 
			
		||||
COPY --from=build /build/LICENSE .
 | 
			
		||||
COPY --from=build /build/README.md .
 | 
			
		||||
COPY --from=build /build/services ./services
 | 
			
		||||
@ -39,6 +37,5 @@ COPY --from=build /build/.env.example .
 | 
			
		||||
RUN mv .env.example .env
 | 
			
		||||
 | 
			
		||||
EXPOSE 7123
 | 
			
		||||
EXPOSE 7122
 | 
			
		||||
 | 
			
		||||
CMD ["./ussd-africastalking"]
 | 
			
		||||
@ -1,103 +1,153 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/signal"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"syscall"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.defalsify.org/vise.git/lang"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/common"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/config"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/args"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/http/at"
 | 
			
		||||
	httpserver "git.grassecon.net/urdt/ussd/internal/http"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/remote"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg          = logging.NewVanilla().WithDomain("AfricasTalking").WithContextKey("at-session-id")
 | 
			
		||||
	scriptDir     = path.Join("services", "registration")
 | 
			
		||||
	build         = "dev"
 | 
			
		||||
	menuSeparator = ": "
 | 
			
		||||
	logg      = logging.NewVanilla()
 | 
			
		||||
	scriptDir = path.Join("services", "registration")
 | 
			
		||||
 | 
			
		||||
	build = "dev"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	initializers.LoadEnvVariables()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type atRequestParser struct{}
 | 
			
		||||
 | 
			
		||||
func (arp *atRequestParser) GetSessionId(rq any) (string, error) {
 | 
			
		||||
	rqv, ok := rq.(*http.Request)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		log.Printf("got an invalid request:", rq)
 | 
			
		||||
		return "", handlers.ErrInvalidRequest
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Capture body (if any) for logging
 | 
			
		||||
	body, err := io.ReadAll(rqv.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Printf("failed to read request body:", 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 {
 | 
			
		||||
		log.Printf("failed to marshal request body:", err)
 | 
			
		||||
	} else {
 | 
			
		||||
		log.Printf("Received request:", string(logBytes))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := rqv.ParseForm(); err != nil {
 | 
			
		||||
		log.Printf("failed to parse form data: %v", 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 {
 | 
			
		||||
		fmt.Printf("Error: %v\n", 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 main() {
 | 
			
		||||
	config.LoadConfig()
 | 
			
		||||
 | 
			
		||||
	var connStr string
 | 
			
		||||
	var dbDir string
 | 
			
		||||
	var resourceDir string
 | 
			
		||||
	var size uint
 | 
			
		||||
	var database string
 | 
			
		||||
	var engineDebug bool
 | 
			
		||||
	var host string
 | 
			
		||||
	var port uint
 | 
			
		||||
	var err error
 | 
			
		||||
	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(&connStr, "c", "", "connection string")
 | 
			
		||||
	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()
 | 
			
		||||
 | 
			
		||||
	if connStr == "" {
 | 
			
		||||
		connStr = config.DbConn
 | 
			
		||||
	}
 | 
			
		||||
	connData, err := storage.ToConnData(connStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "connstr err: %v", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logg.Infof("start command", "build", build, "conn", connData, "resourcedir", resourceDir, "outputsize", size)
 | 
			
		||||
	logg.Infof("start command", "build", build, "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	ln, err := lang.LanguageFromCode(config.DefaultLanguage)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "default language set error: %v", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	ctx = context.WithValue(ctx, "Language", ln)
 | 
			
		||||
 | 
			
		||||
	ctx = context.WithValue(ctx, "Database", database)
 | 
			
		||||
	pfp := path.Join(scriptDir, "pp.csv")
 | 
			
		||||
 | 
			
		||||
	cfg := engine.Config{
 | 
			
		||||
		Root:          "root",
 | 
			
		||||
		OutputSize:    uint32(size),
 | 
			
		||||
		FlagCount:     uint32(128),
 | 
			
		||||
		MenuSeparator: menuSeparator,
 | 
			
		||||
		Root:       "root",
 | 
			
		||||
		OutputSize: uint32(size),
 | 
			
		||||
		FlagCount:  uint32(128),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if engineDebug {
 | 
			
		||||
		cfg.EngineDebug = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
 | 
			
		||||
	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
 | 
			
		||||
	rs, err := menuStorageService.GetResource(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rs, err := menuStorageService.GetResource(ctx)
 | 
			
		||||
	err = menuStorageService.EnsureDbDir()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
@ -141,9 +191,9 @@ func main() {
 | 
			
		||||
	}
 | 
			
		||||
	defer stateStore.Close()
 | 
			
		||||
 | 
			
		||||
	rp := &at.ATRequestParser{}
 | 
			
		||||
	rp := &atRequestParser{}
 | 
			
		||||
	bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
 | 
			
		||||
	sh := at.NewATSessionHandler(bsh)
 | 
			
		||||
	sh := httpserver.NewATSessionHandler(bsh)
 | 
			
		||||
 | 
			
		||||
	mux := http.NewServeMux()
 | 
			
		||||
	mux.Handle(initializers.GetEnv("AT_ENDPOINT", "/"), sh)
 | 
			
		||||
 | 
			
		||||
@ -10,22 +10,19 @@ import (
 | 
			
		||||
	"syscall"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.defalsify.org/vise.git/lang"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/config"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/args"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/remote"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg          = logging.NewVanilla()
 | 
			
		||||
	scriptDir     = path.Join("services", "registration")
 | 
			
		||||
	menuSeparator = ": "
 | 
			
		||||
	logg      = logging.NewVanilla()
 | 
			
		||||
	scriptDir = path.Join("services", "registration")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
@ -37,7 +34,7 @@ type asyncRequestParser struct {
 | 
			
		||||
	input     []byte
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *asyncRequestParser) GetSessionId(ctx context.Context, r any) (string, error) {
 | 
			
		||||
func (p *asyncRequestParser) GetSessionId(r any) (string, error) {
 | 
			
		||||
	return p.sessionId, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -48,68 +45,48 @@ func (p *asyncRequestParser) GetInput(r any) ([]byte, error) {
 | 
			
		||||
func main() {
 | 
			
		||||
	config.LoadConfig()
 | 
			
		||||
 | 
			
		||||
	var connStr string
 | 
			
		||||
	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 err error
 | 
			
		||||
	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(&connStr, "c", "", "connection string")
 | 
			
		||||
	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()
 | 
			
		||||
 | 
			
		||||
	if connStr == "" {
 | 
			
		||||
		connStr = config.DbConn
 | 
			
		||||
	}
 | 
			
		||||
	connData, err := storage.ToConnData(connStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "connstr err: %v", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logg.Infof("start command", "conn", connData, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId)
 | 
			
		||||
	logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId)
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	ln, err := lang.LanguageFromCode(config.DefaultLanguage)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "default language set error: %v", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	ctx = context.WithValue(ctx, "Language", ln)
 | 
			
		||||
 | 
			
		||||
	ctx = context.WithValue(ctx, "Database", database)
 | 
			
		||||
	pfp := path.Join(scriptDir, "pp.csv")
 | 
			
		||||
 | 
			
		||||
	cfg := engine.Config{
 | 
			
		||||
		Root:          "root",
 | 
			
		||||
		OutputSize:    uint32(size),
 | 
			
		||||
		FlagCount:     uint32(128),
 | 
			
		||||
		MenuSeparator: menuSeparator,
 | 
			
		||||
		Root:       "root",
 | 
			
		||||
		OutputSize: uint32(size),
 | 
			
		||||
		FlagCount:  uint32(128),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if engineDebug {
 | 
			
		||||
		cfg.EngineDebug = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
 | 
			
		||||
	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
 | 
			
		||||
	rs, err := menuStorageService.GetResource(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rs, err := menuStorageService.GetResource(ctx)
 | 
			
		||||
	err = menuStorageService.EnsureDbDir()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
 | 
			
		||||
@ -12,13 +12,11 @@ import (
 | 
			
		||||
	"syscall"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.defalsify.org/vise.git/lang"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/config"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/args"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers"
 | 
			
		||||
	httpserver "git.grassecon.net/urdt/ussd/internal/http"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
@ -26,9 +24,8 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg          = logging.NewVanilla()
 | 
			
		||||
	scriptDir     = path.Join("services", "registration")
 | 
			
		||||
	menuSeparator = ": "
 | 
			
		||||
	logg      = logging.NewVanilla()
 | 
			
		||||
	scriptDir = path.Join("services", "registration")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
@ -38,67 +35,51 @@ func init() {
 | 
			
		||||
func main() {
 | 
			
		||||
	config.LoadConfig()
 | 
			
		||||
 | 
			
		||||
	var connStr string
 | 
			
		||||
	var dbDir string
 | 
			
		||||
	var resourceDir string
 | 
			
		||||
	var size uint
 | 
			
		||||
	var database string
 | 
			
		||||
	var engineDebug bool
 | 
			
		||||
	var host string
 | 
			
		||||
	var port uint
 | 
			
		||||
	var err error
 | 
			
		||||
	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(&connStr, "c", "", "connection string")
 | 
			
		||||
	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()
 | 
			
		||||
 | 
			
		||||
	if connStr == "" {
 | 
			
		||||
		connStr = config.DbConn
 | 
			
		||||
	}
 | 
			
		||||
	connData, err := storage.ToConnData(connStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "connstr err: %v", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logg.Infof("start command", "conn", connData, "resourcedir", resourceDir, "outputsize", size)
 | 
			
		||||
	logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	ln, err := lang.LanguageFromCode(config.DefaultLanguage)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "default language set error: %v", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	ctx = context.WithValue(ctx, "Language", ln)
 | 
			
		||||
 | 
			
		||||
	ctx = context.WithValue(ctx, "Database", database)
 | 
			
		||||
	pfp := path.Join(scriptDir, "pp.csv")
 | 
			
		||||
 | 
			
		||||
	cfg := engine.Config{
 | 
			
		||||
		Root:          "root",
 | 
			
		||||
		OutputSize:    uint32(size),
 | 
			
		||||
		FlagCount:     uint32(128),
 | 
			
		||||
		MenuSeparator: menuSeparator,
 | 
			
		||||
		Root:       "root",
 | 
			
		||||
		OutputSize: uint32(size),
 | 
			
		||||
		FlagCount:  uint32(128),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if engineDebug {
 | 
			
		||||
		cfg.EngineDebug = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
 | 
			
		||||
 | 
			
		||||
	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())
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										64
									
								
								cmd/main.go
									
									
									
									
									
								
							
							
						
						
									
										64
									
								
								cmd/main.go
									
									
									
									
									
								
							@ -8,88 +8,60 @@ import (
 | 
			
		||||
	"path"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.defalsify.org/vise.git/lang"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/config"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/args"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/remote"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg          = logging.NewVanilla()
 | 
			
		||||
	scriptDir     = path.Join("services", "registration")
 | 
			
		||||
	menuSeparator = ": "
 | 
			
		||||
	logg      = logging.NewVanilla()
 | 
			
		||||
	scriptDir = path.Join("services", "registration")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	initializers.LoadEnvVariables()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO: external script automatically generate language handler list from select language vise code OR consider dynamic menu generation script possibility
 | 
			
		||||
func main() {
 | 
			
		||||
	config.LoadConfig()
 | 
			
		||||
 | 
			
		||||
	var connStr string
 | 
			
		||||
	var dbDir string
 | 
			
		||||
	var size uint
 | 
			
		||||
	var sessionId string
 | 
			
		||||
	var database string
 | 
			
		||||
	var engineDebug bool
 | 
			
		||||
	var resourceDir string
 | 
			
		||||
	var err error
 | 
			
		||||
	var gettextDir string
 | 
			
		||||
	var langs args.LangVar
 | 
			
		||||
 | 
			
		||||
	flag.StringVar(&resourceDir, "resourcedir", scriptDir, "resource dir")
 | 
			
		||||
	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
 | 
			
		||||
	flag.StringVar(&connStr, "c", "", "connection string")
 | 
			
		||||
	flag.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()
 | 
			
		||||
 | 
			
		||||
	if connStr == "" {
 | 
			
		||||
		connStr = config.DbConn
 | 
			
		||||
	}
 | 
			
		||||
	connData, err := storage.ToConnData(connStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "connstr err: %v", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logg.Infof("start command", "conn", connData, "outputsize", size)
 | 
			
		||||
 | 
			
		||||
	if len(langs.Langs()) == 0 {
 | 
			
		||||
		langs.Set(config.DefaultLanguage)
 | 
			
		||||
	}
 | 
			
		||||
	logg.Infof("start command", "dbdir", dbDir, "outputsize", size)
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	ctx = context.WithValue(ctx, "SessionId", sessionId)
 | 
			
		||||
 | 
			
		||||
	ln, err := lang.LanguageFromCode(config.DefaultLanguage)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "default language set error: %v", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	ctx = context.WithValue(ctx, "Language", ln)
 | 
			
		||||
 | 
			
		||||
	ctx = context.WithValue(ctx, "Database", database)
 | 
			
		||||
	pfp := path.Join(scriptDir, "pp.csv")
 | 
			
		||||
 | 
			
		||||
	cfg := engine.Config{
 | 
			
		||||
		Root:          "root",
 | 
			
		||||
		SessionId:     sessionId,
 | 
			
		||||
		OutputSize:    uint32(size),
 | 
			
		||||
		FlagCount:     uint32(128),
 | 
			
		||||
		MenuSeparator: menuSeparator,
 | 
			
		||||
		Root:       "root",
 | 
			
		||||
		SessionId:  sessionId,
 | 
			
		||||
		OutputSize: uint32(size),
 | 
			
		||||
		FlagCount:  uint32(128),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
 | 
			
		||||
	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)
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										144
									
								
								cmd/ssh/main.go
									
									
									
									
									
								
							
							
						
						
									
										144
									
								
								cmd/ssh/main.go
									
									
									
									
									
								
							@ -1,144 +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/config"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/ssh"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	wg        sync.WaitGroup
 | 
			
		||||
	keyStore  db.Db
 | 
			
		||||
	logg      = logging.NewVanilla()
 | 
			
		||||
	scriptDir = path.Join("services", "registration")
 | 
			
		||||
 | 
			
		||||
	build = "dev"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	initializers.LoadEnvVariables()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	config.LoadConfig()
 | 
			
		||||
 | 
			
		||||
	var connStr string
 | 
			
		||||
	var authConnStr string
 | 
			
		||||
	var resourceDir string
 | 
			
		||||
	var size uint
 | 
			
		||||
	var engineDebug bool
 | 
			
		||||
	var stateDebug bool
 | 
			
		||||
	var host string
 | 
			
		||||
	var port uint
 | 
			
		||||
	flag.StringVar(&connStr, "c", "", "connection string")
 | 
			
		||||
	flag.StringVar(&authConnStr, "authdb", "", "auth connection string")
 | 
			
		||||
	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
 | 
			
		||||
	flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
 | 
			
		||||
	flag.UintVar(&size, "s", 160, "max size of output")
 | 
			
		||||
	flag.StringVar(&host, "h", "127.0.0.1", "socket host")
 | 
			
		||||
	flag.UintVar(&port, "p", 7122, "socket port")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	if connStr == "" {
 | 
			
		||||
		connStr = config.DbConn
 | 
			
		||||
	}
 | 
			
		||||
	if authConnStr == "" {
 | 
			
		||||
		authConnStr = connStr
 | 
			
		||||
	}
 | 
			
		||||
	connData, err := storage.ToConnData(connStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "connstr err: %v", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	authConnData, err := storage.ToConnData(authConnStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "auth connstr err: %v", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sshKeyFile := flag.Arg(0)
 | 
			
		||||
	_, err = os.Stat(sshKeyFile)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "cannot open ssh server private key file: %v\n", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	logg.WarnCtxf(ctx, "!!!!! WARNING WARNING WARNING")
 | 
			
		||||
	logg.WarnCtxf(ctx, "!!!!! =======================")
 | 
			
		||||
	logg.WarnCtxf(ctx, "!!!!! This is not a production ready server!")
 | 
			
		||||
	logg.WarnCtxf(ctx, "!!!!! Do not expose to internet and only use with tunnel!")
 | 
			
		||||
	logg.WarnCtxf(ctx, "!!!!! (See ssh -L <...>)")
 | 
			
		||||
 | 
			
		||||
	logg.Infof("start command", "conn", connData, "authconn", authConnData, "resourcedir", resourceDir, "outputsize", size, "keyfile", sshKeyFile, "host", host, "port", port)
 | 
			
		||||
 | 
			
		||||
	pfp := path.Join(scriptDir, "pp.csv")
 | 
			
		||||
 | 
			
		||||
	cfg := engine.Config{
 | 
			
		||||
		Root:       "root",
 | 
			
		||||
		OutputSize: uint32(size),
 | 
			
		||||
		FlagCount:  uint32(16),
 | 
			
		||||
	}
 | 
			
		||||
	if stateDebug {
 | 
			
		||||
		cfg.StateDebug = true
 | 
			
		||||
	}
 | 
			
		||||
	if engineDebug {
 | 
			
		||||
		cfg.EngineDebug = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	authKeyStore, err := ssh.NewSshKeyStore(ctx, authConnData.String())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "keystore file open error: %v", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	defer func() {
 | 
			
		||||
		logg.TraceCtxf(ctx, "shutdown auth key store reached")
 | 
			
		||||
		err = authKeyStore.Close()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logg.ErrorCtxf(ctx, "keystore close error", "err", err)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	cint := make(chan os.Signal)
 | 
			
		||||
	cterm := make(chan os.Signal)
 | 
			
		||||
	signal.Notify(cint, os.Interrupt, syscall.SIGINT)
 | 
			
		||||
	signal.Notify(cterm, os.Interrupt, syscall.SIGTERM)
 | 
			
		||||
 | 
			
		||||
	runner := &ssh.SshRunner{
 | 
			
		||||
		Cfg: cfg,
 | 
			
		||||
		Debug: engineDebug,
 | 
			
		||||
		FlagFile: pfp,
 | 
			
		||||
		Conn: connData,
 | 
			
		||||
		ResourceDir: resourceDir,
 | 
			
		||||
		SrvKeyFile:  sshKeyFile,
 | 
			
		||||
		Host:        host,
 | 
			
		||||
		Port:        port,
 | 
			
		||||
	}
 | 
			
		||||
	go func() {
 | 
			
		||||
		select {
 | 
			
		||||
		case _ = <-cint:
 | 
			
		||||
		case _ = <-cterm:
 | 
			
		||||
		}
 | 
			
		||||
		logg.TraceCtxf(ctx, "shutdown runner reached")
 | 
			
		||||
		err := runner.Stop()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logg.ErrorCtxf(ctx, "runner stop error", "err", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}()
 | 
			
		||||
	runner.Run(ctx, authKeyStore)
 | 
			
		||||
}
 | 
			
		||||
@ -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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										75
									
								
								common/db.go
									
									
									
									
									
								
							
							
						
						
									
										75
									
								
								common/db.go
									
									
									
									
									
								
							@ -7,88 +7,32 @@ import (
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// DataType is a subprefix value used in association with vise/db.DATATYPE_USERDATA.
 | 
			
		||||
//
 | 
			
		||||
// All keys are used only within the context of a single account. Unless otherwise specified, the user context is the session id.
 | 
			
		||||
//
 | 
			
		||||
// * The first byte is vise/db.DATATYPE_USERDATA
 | 
			
		||||
// * The last 2 bytes are the DataTyp value, big-endian.
 | 
			
		||||
// * The intermediate bytes are the id of the user context.
 | 
			
		||||
//
 | 
			
		||||
// All values are strings
 | 
			
		||||
type DataTyp uint16
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	// API Tracking id to follow status of account creation
 | 
			
		||||
	DATA_TRACKING_ID = iota
 | 
			
		||||
	// EVM address returned from API on account creation
 | 
			
		||||
	DATA_ACCOUNT DataTyp = iota
 | 
			
		||||
	DATA_ACCOUNT_CREATED
 | 
			
		||||
	DATA_TRACKING_ID
 | 
			
		||||
	DATA_PUBLIC_KEY
 | 
			
		||||
	// Currently active PIN used to authenticate ussd state change requests
 | 
			
		||||
	DATA_CUSTODIAL_ID
 | 
			
		||||
	DATA_ACCOUNT_PIN
 | 
			
		||||
	// The first name of the user
 | 
			
		||||
	DATA_ACCOUNT_STATUS
 | 
			
		||||
	DATA_FIRST_NAME
 | 
			
		||||
	// The last name of the user
 | 
			
		||||
	DATA_FAMILY_NAME
 | 
			
		||||
	// The year-of-birth of the user
 | 
			
		||||
	DATA_YOB
 | 
			
		||||
	// The location of the user
 | 
			
		||||
	DATA_LOCATION
 | 
			
		||||
	// The gender of the user
 | 
			
		||||
	DATA_GENDER
 | 
			
		||||
	// The offerings description of the user
 | 
			
		||||
	DATA_OFFERINGS
 | 
			
		||||
	// The ethereum address of the recipient of an ongoing send request
 | 
			
		||||
	DATA_RECIPIENT
 | 
			
		||||
	// The voucher value amount of an ongoing send request
 | 
			
		||||
	DATA_AMOUNT
 | 
			
		||||
	// A general swap field for temporary values
 | 
			
		||||
	DATA_TEMPORARY_VALUE
 | 
			
		||||
	// Currently active voucher symbol of user
 | 
			
		||||
	DATA_ACTIVE_SYM
 | 
			
		||||
	// Voucher balance of user's currently active voucher
 | 
			
		||||
	DATA_ACTIVE_BAL
 | 
			
		||||
	// String boolean indicating whether use of PIN is blocked
 | 
			
		||||
	DATA_BLOCKED_NUMBER
 | 
			
		||||
	// Reverse mapping of a user's evm address to a session id.
 | 
			
		||||
	DATA_PUBLIC_KEY_REVERSE
 | 
			
		||||
	// Decimal count of the currently active voucher
 | 
			
		||||
	DATA_ACTIVE_DECIMAL
 | 
			
		||||
	// EVM address of the currently active voucher
 | 
			
		||||
	DATA_ACTIVE_ADDRESS
 | 
			
		||||
	//Holds count of the number of incorrect PIN attempts
 | 
			
		||||
	DATA_INCORRECT_PIN_ATTEMPTS
 | 
			
		||||
	//ISO 639 code for the selected language.
 | 
			
		||||
	DATA_SELECTED_LANGUAGE_CODE
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	// List of valid voucher symbols in the user context.
 | 
			
		||||
	DATA_VOUCHER_SYMBOLS DataTyp = 256 + iota
 | 
			
		||||
	// List of voucher balances for vouchers valid in the user context.
 | 
			
		||||
	DATA_VOUCHER_BALANCES
 | 
			
		||||
	// List of voucher decimal counts for vouchers valid in the user context.
 | 
			
		||||
	DATA_VOUCHER_DECIMALS
 | 
			
		||||
	// List of voucher EVM addresses for vouchers valid in the user context.
 | 
			
		||||
	DATA_VOUCHER_ADDRESSES
 | 
			
		||||
	// List of senders for valid transactions in the user context.
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	DATA_TX_SENDERS = 512 + iota
 | 
			
		||||
	// List of recipients for valid transactions in the user context.
 | 
			
		||||
	DATA_TX_RECIPIENTS
 | 
			
		||||
	// List of voucher values for valid transactions in the user context.
 | 
			
		||||
	DATA_TX_VALUES
 | 
			
		||||
	// List of voucher EVM addresses for valid transactions in the user context.
 | 
			
		||||
	DATA_TX_ADDRESSES
 | 
			
		||||
	// List of valid transaction hashes in the user context.
 | 
			
		||||
	DATA_TX_HASHES
 | 
			
		||||
	// List of transaction dates for valid transactions in the user context.
 | 
			
		||||
	DATA_TX_DATES
 | 
			
		||||
	// List of voucher symbols for valid transactions in the user context.
 | 
			
		||||
	DATA_TX_SYMBOLS
 | 
			
		||||
	// List of voucher decimal counts for valid transactions in the user context.
 | 
			
		||||
	DATA_TX_DECIMALS
 | 
			
		||||
	DATA_TRANSACTIONS
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
@ -125,10 +69,3 @@ func StringToDataTyp(str string) (DataTyp, error) {
 | 
			
		||||
		return 0, errors.New("invalid DataTyp string")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ToBytes converts DataTyp or int to a byte slice
 | 
			
		||||
func ToBytes[T ~uint16 | int](value T) []byte {
 | 
			
		||||
	bytes := make([]byte, 2)
 | 
			
		||||
	binary.BigEndian.PutUint16(bytes, uint16(value))
 | 
			
		||||
	return bytes
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,37 +0,0 @@
 | 
			
		||||
package common
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"regexp"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/crypto/bcrypt"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	// Define the regex pattern as a constant
 | 
			
		||||
	pinPattern = `^\d{4}$`
 | 
			
		||||
 | 
			
		||||
	//Allowed incorrect  PIN attempts
 | 
			
		||||
	AllowedPINAttempts = uint8(3)
 | 
			
		||||
	
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// checks whether the given input is a 4 digit number
 | 
			
		||||
func IsValidPIN(pin string) bool {
 | 
			
		||||
	match, _ := regexp.MatchString(pinPattern, pin)
 | 
			
		||||
	return match
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HashPIN uses bcrypt with 8 salt rounds to hash the PIN
 | 
			
		||||
func HashPIN(pin string) (string, error) {
 | 
			
		||||
	hash, err := bcrypt.GenerateFromPassword([]byte(pin), 8)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return string(hash), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// VerifyPIN compareS the hashed PIN with the plaintext PIN
 | 
			
		||||
func VerifyPIN(hashedPIN, pin string) bool {
 | 
			
		||||
	err := bcrypt.CompareHashAndPassword([]byte(hashedPIN), []byte(pin))
 | 
			
		||||
	return err == nil
 | 
			
		||||
}
 | 
			
		||||
@ -1,173 +0,0 @@
 | 
			
		||||
package common
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/crypto/bcrypt"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestIsValidPIN(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name     string
 | 
			
		||||
		pin      string
 | 
			
		||||
		expected bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:     "Valid PIN with 4 digits",
 | 
			
		||||
			pin:      "1234",
 | 
			
		||||
			expected: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "Valid PIN with leading zeros",
 | 
			
		||||
			pin:      "0001",
 | 
			
		||||
			expected: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "Invalid PIN with less than 4 digits",
 | 
			
		||||
			pin:      "123",
 | 
			
		||||
			expected: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "Invalid PIN with more than 4 digits",
 | 
			
		||||
			pin:      "12345",
 | 
			
		||||
			expected: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "Invalid PIN with letters",
 | 
			
		||||
			pin:      "abcd",
 | 
			
		||||
			expected: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "Invalid PIN with special characters",
 | 
			
		||||
			pin:      "12@#",
 | 
			
		||||
			expected: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "Empty PIN",
 | 
			
		||||
			pin:      "",
 | 
			
		||||
			expected: false,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			actual := IsValidPIN(tt.pin)
 | 
			
		||||
			if actual != tt.expected {
 | 
			
		||||
				t.Errorf("IsValidPIN(%q) = %v; expected %v", tt.pin, actual, tt.expected)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestHashPIN(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name string
 | 
			
		||||
		pin  string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "Valid PIN with 4 digits",
 | 
			
		||||
			pin:  "1234",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Valid PIN with leading zeros",
 | 
			
		||||
			pin:  "0001",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Empty PIN",
 | 
			
		||||
			pin:  "",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			hashedPIN, err := HashPIN(tt.pin)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Errorf("HashPIN(%q) returned an error: %v", tt.pin, err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if hashedPIN == "" {
 | 
			
		||||
				t.Errorf("HashPIN(%q) returned an empty hash", tt.pin)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Ensure the hash can be verified with bcrypt
 | 
			
		||||
			err = bcrypt.CompareHashAndPassword([]byte(hashedPIN), []byte(tt.pin))
 | 
			
		||||
			if tt.pin != "" && err != nil {
 | 
			
		||||
				t.Errorf("HashPIN(%q) produced a hash that does not match: %v", tt.pin, err)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestVerifyMigratedHashPin(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		pin  string
 | 
			
		||||
		hash string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			pin:  "1234",
 | 
			
		||||
			hash: "$2b$08$dTvIGxCCysJtdvrSnaLStuylPoOS/ZLYYkxvTeR5QmTFY3TSvPQC6",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.pin, func(t *testing.T) {
 | 
			
		||||
			ok := VerifyPIN(tt.hash, tt.pin)
 | 
			
		||||
			if !ok {
 | 
			
		||||
				t.Errorf("VerifyPIN could not verify migrated PIN: %v", tt.pin)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestVerifyPIN(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name       string
 | 
			
		||||
		pin        string
 | 
			
		||||
		hashedPIN  string
 | 
			
		||||
		shouldPass bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:       "Valid PIN verification",
 | 
			
		||||
			pin:        "1234",
 | 
			
		||||
			hashedPIN:  hashPINHelper("1234"),
 | 
			
		||||
			shouldPass: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:       "Invalid PIN verification with incorrect PIN",
 | 
			
		||||
			pin:        "5678",
 | 
			
		||||
			hashedPIN:  hashPINHelper("1234"),
 | 
			
		||||
			shouldPass: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:       "Invalid PIN verification with empty PIN",
 | 
			
		||||
			pin:        "",
 | 
			
		||||
			hashedPIN:  hashPINHelper("1234"),
 | 
			
		||||
			shouldPass: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:       "Invalid PIN verification with invalid hash",
 | 
			
		||||
			pin:        "1234",
 | 
			
		||||
			hashedPIN:  "invalidhash",
 | 
			
		||||
			shouldPass: false,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			result := VerifyPIN(tt.hashedPIN, tt.pin)
 | 
			
		||||
			if result != tt.shouldPass {
 | 
			
		||||
				t.Errorf("VerifyPIN(%q, %q) = %v; expected %v", tt.hashedPIN, tt.pin, result, tt.shouldPass)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper function to hash a PIN for testing purposes
 | 
			
		||||
func hashPINHelper(pin string) string {
 | 
			
		||||
	hashedPIN, err := HashPIN(pin)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic("Failed to hash PIN for test setup: " + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	return hashedPIN
 | 
			
		||||
}
 | 
			
		||||
@ -8,32 +8,31 @@ import (
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
	"git.defalsify.org/vise.git/persist"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
	dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func StoreToDb(store *UserDataStore) db.Db {
 | 
			
		||||
	return store.Db
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func StoreToPrefixDb(store *UserDataStore, pfx []byte) dbstorage.PrefixDb {
 | 
			
		||||
	return dbstorage.NewSubPrefixDb(store.Db, pfx)	
 | 
			
		||||
func StoreToPrefixDb(store *UserDataStore, pfx []byte) storage.PrefixDb {
 | 
			
		||||
	return storage.NewSubPrefixDb(store.Db, pfx)	
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type StorageServices interface {
 | 
			
		||||
	GetPersister(ctx context.Context) (*persist.Persister, error)
 | 
			
		||||
	GetUserdataDb(ctx context.Context) (db.Db, error)
 | 
			
		||||
	GetResource(ctx context.Context) (resource.Resource, error)
 | 
			
		||||
	EnsureDbDir() error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type StorageService struct {
 | 
			
		||||
	svc *storage.MenuStorageService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewStorageService(conn storage.ConnData) (*StorageService, error) {
 | 
			
		||||
	svc := &StorageService{
 | 
			
		||||
		svc: storage.NewMenuStorageService(conn, ""),
 | 
			
		||||
func NewStorageService(dbDir string) *StorageService {
 | 
			
		||||
	return &StorageService{
 | 
			
		||||
		svc: storage.NewMenuStorageService(dbDir, ""),
 | 
			
		||||
	}
 | 
			
		||||
	return svc, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(ss *StorageService) GetPersister(ctx context.Context) (*persist.Persister, error) {
 | 
			
		||||
@ -47,3 +46,7 @@ func(ss *StorageService) GetUserdataDb(ctx context.Context) (db.Db, error) {
 | 
			
		||||
func(ss *StorageService) GetResource(ctx context.Context) (resource.Resource, error) {
 | 
			
		||||
	return nil, errors.New("not implemented")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(ss *StorageService) EnsureDbDir() error {
 | 
			
		||||
	return ss.svc.EnsureDbDir()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -56,26 +56,26 @@ func ProcessTransfers(transfers []dataserviceapi.Last10TxResponse) TransferMetad
 | 
			
		||||
 | 
			
		||||
// GetTransferData retrieves and matches transfer data
 | 
			
		||||
// returns a formatted string of the full transaction/statement
 | 
			
		||||
func GetTransferData(ctx context.Context, db dbstorage.PrefixDb, publicKey string, index int) (string, error) {
 | 
			
		||||
	keys := []DataTyp{DATA_TX_SENDERS, DATA_TX_RECIPIENTS, DATA_TX_VALUES, DATA_TX_ADDRESSES, DATA_TX_HASHES, DATA_TX_DATES, DATA_TX_SYMBOLS}
 | 
			
		||||
	data := make(map[DataTyp]string)
 | 
			
		||||
func GetTransferData(ctx context.Context, db storage.PrefixDb, publicKey string, index int) (string, error) {
 | 
			
		||||
	keys := []string{"txfrom", "txto", "txval", "txaddr", "txhash", "txdate", "txsym"}
 | 
			
		||||
	data := make(map[string]string)
 | 
			
		||||
 | 
			
		||||
	for _, key := range keys {
 | 
			
		||||
		value, err := db.Get(ctx, ToBytes(key))
 | 
			
		||||
		value, err := db.Get(ctx, []byte(key))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", fmt.Errorf("failed to get %s: %v", ToBytes(key), err)
 | 
			
		||||
			return "", fmt.Errorf("failed to get %s: %v", key, err)
 | 
			
		||||
		}
 | 
			
		||||
		data[key] = string(value)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Split the data
 | 
			
		||||
	senders := strings.Split(string(data[DATA_TX_SENDERS]), "\n")
 | 
			
		||||
	recipients := strings.Split(string(data[DATA_TX_RECIPIENTS]), "\n")
 | 
			
		||||
	values := strings.Split(string(data[DATA_TX_VALUES]), "\n")
 | 
			
		||||
	addresses := strings.Split(string(data[DATA_TX_ADDRESSES]), "\n")
 | 
			
		||||
	hashes := strings.Split(string(data[DATA_TX_HASHES]), "\n")
 | 
			
		||||
	dates := strings.Split(string(data[DATA_TX_DATES]), "\n")
 | 
			
		||||
	syms := strings.Split(string(data[DATA_TX_SYMBOLS]), "\n")
 | 
			
		||||
	senders := strings.Split(string(data["txfrom"]), "\n")
 | 
			
		||||
	recipients := strings.Split(string(data["txto"]), "\n")
 | 
			
		||||
	values := strings.Split(string(data["txval"]), "\n")
 | 
			
		||||
	addresses := strings.Split(string(data["txaddr"]), "\n")
 | 
			
		||||
	hashes := strings.Split(string(data["txhash"]), "\n")
 | 
			
		||||
	dates := strings.Split(string(data["txdate"]), "\n")
 | 
			
		||||
	syms := strings.Split(string(data["txsym"]), "\n")
 | 
			
		||||
 | 
			
		||||
	// Check if index is within range
 | 
			
		||||
	if index < 1 || index > len(senders) {
 | 
			
		||||
@ -84,18 +84,18 @@ func GetTransferData(ctx context.Context, db dbstorage.PrefixDb, publicKey strin
 | 
			
		||||
 | 
			
		||||
	// Adjust for 0-based indexing
 | 
			
		||||
	i := index - 1
 | 
			
		||||
	transactionType := "Received"
 | 
			
		||||
	party := fmt.Sprintf("From: %s", strings.TrimSpace(senders[i]))
 | 
			
		||||
	transactionType := "received"
 | 
			
		||||
	party := fmt.Sprintf("from: %s", strings.TrimSpace(senders[i]))
 | 
			
		||||
	if strings.TrimSpace(senders[i]) == publicKey {
 | 
			
		||||
		transactionType = "Sent"
 | 
			
		||||
		party = fmt.Sprintf("To: %s", strings.TrimSpace(recipients[i]))
 | 
			
		||||
		transactionType = "sent"
 | 
			
		||||
		party = fmt.Sprintf("to: %s", strings.TrimSpace(recipients[i]))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	formattedDate := formatDate(strings.TrimSpace(dates[i]))
 | 
			
		||||
 | 
			
		||||
	// Build the full transaction detail
 | 
			
		||||
	detail := fmt.Sprintf(
 | 
			
		||||
		"%s %s %s\n%s\nContract address: %s\nTxhash: %s\nDate: %s",
 | 
			
		||||
		"%s %s %s\n%s\ncontract address: %s\ntxhash: %s\ndate: %s",
 | 
			
		||||
		transactionType,
 | 
			
		||||
		strings.TrimSpace(values[i]),
 | 
			
		||||
		strings.TrimSpace(syms[i]),
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@ type UserDataStore struct {
 | 
			
		||||
func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error) {
 | 
			
		||||
	store.SetPrefix(db.DATATYPE_USERDATA)
 | 
			
		||||
	store.SetSession(sessionId)
 | 
			
		||||
	k := ToBytes(typ)
 | 
			
		||||
	k := PackKey(typ, []byte(sessionId))
 | 
			
		||||
	return store.Get(ctx, k)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,6 @@ func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ
 | 
			
		||||
func (store *UserDataStore) WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error {
 | 
			
		||||
	store.SetPrefix(db.DATATYPE_USERDATA)
 | 
			
		||||
	store.SetSession(sessionId)
 | 
			
		||||
	k := ToBytes(typ)
 | 
			
		||||
	k := PackKey(typ, []byte(sessionId))
 | 
			
		||||
	return store.Put(ctx, k, value)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ import (
 | 
			
		||||
	"math/big"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -63,24 +63,23 @@ func ScaleDownBalance(balance, decimals string) string {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetVoucherData retrieves and matches voucher data
 | 
			
		||||
func GetVoucherData(ctx context.Context, db dbstorage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) {
 | 
			
		||||
	keys := []DataTyp{DATA_VOUCHER_SYMBOLS, DATA_VOUCHER_BALANCES, DATA_VOUCHER_DECIMALS, DATA_VOUCHER_ADDRESSES}
 | 
			
		||||
	data := make(map[DataTyp]string)
 | 
			
		||||
func GetVoucherData(ctx context.Context, db storage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) {
 | 
			
		||||
	keys := []string{"sym", "bal", "deci", "addr"}
 | 
			
		||||
	data := make(map[string]string)
 | 
			
		||||
 | 
			
		||||
	for _, key := range keys {
 | 
			
		||||
		value, err := db.Get(ctx, ToBytes(key))
 | 
			
		||||
		value, err := db.Get(ctx, []byte(key))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("failed to get %s: %v", ToBytes(key), err)
 | 
			
		||||
			return nil, fmt.Errorf("failed to get %s: %v", key, err)
 | 
			
		||||
		}
 | 
			
		||||
		data[key] = string(value)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	symbol, balance, decimal, address := MatchVoucher(input,
 | 
			
		||||
		data[DATA_VOUCHER_SYMBOLS],
 | 
			
		||||
		data[DATA_VOUCHER_BALANCES],
 | 
			
		||||
		data[DATA_VOUCHER_DECIMALS],
 | 
			
		||||
		data[DATA_VOUCHER_ADDRESSES],
 | 
			
		||||
	)
 | 
			
		||||
		data["sym"],
 | 
			
		||||
		data["bal"],
 | 
			
		||||
		data["deci"],
 | 
			
		||||
		data["addr"])
 | 
			
		||||
 | 
			
		||||
	if symbol == "" {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
 | 
			
		||||
@ -8,9 +8,8 @@ import (
 | 
			
		||||
	"github.com/alecthomas/assert/v2"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
 | 
			
		||||
	visedb "git.defalsify.org/vise.git/db"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
	memdb "git.defalsify.org/vise.git/db/mem"
 | 
			
		||||
	dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
 | 
			
		||||
	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -84,21 +83,19 @@ func TestGetVoucherData(t *testing.T) {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	prefix := ToBytes(visedb.DATATYPE_USERDATA)
 | 
			
		||||
	spdb := dbstorage.NewSubPrefixDb(db, prefix)
 | 
			
		||||
	spdb := storage.NewSubPrefixDb(db, []byte("vouchers"))
 | 
			
		||||
 | 
			
		||||
	// Test voucher data
 | 
			
		||||
	mockData := map[DataTyp][]byte{
 | 
			
		||||
		DATA_VOUCHER_SYMBOLS:   []byte("1:SRF\n2:MILO"),
 | 
			
		||||
		DATA_VOUCHER_BALANCES:  []byte("1:100\n2:200"),
 | 
			
		||||
		DATA_VOUCHER_DECIMALS:  []byte("1:6\n2:4"),
 | 
			
		||||
		DATA_VOUCHER_ADDRESSES: []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"),
 | 
			
		||||
	mockData := map[string][]byte{
 | 
			
		||||
		"sym":  []byte("1:SRF\n2:MILO"),
 | 
			
		||||
		"bal":  []byte("1:100\n2:200"),
 | 
			
		||||
		"deci": []byte("1:6\n2:4"),
 | 
			
		||||
		"addr": []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Put the data
 | 
			
		||||
	for key, value := range mockData {
 | 
			
		||||
		err = spdb.Put(ctx, []byte(ToBytes(key)), []byte(value))
 | 
			
		||||
		err = spdb.Put(ctx, []byte(key), []byte(value))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,6 @@ package config
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
			
		||||
)
 | 
			
		||||
@ -19,11 +18,6 @@ const (
 | 
			
		||||
	AliasPrefix                = "api/v1/alias"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	defaultLanguage		   = "eng"
 | 
			
		||||
	languages []string
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	custodialURLBase string
 | 
			
		||||
	dataURLBase      string
 | 
			
		||||
@ -40,29 +34,8 @@ var (
 | 
			
		||||
	VoucherTransfersURL string
 | 
			
		||||
	VoucherDataURL      string
 | 
			
		||||
	CheckAliasURL       string
 | 
			
		||||
	DbConn		string
 | 
			
		||||
	DefaultLanguage	    string
 | 
			
		||||
	Languages	[]string
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func setLanguage() error {
 | 
			
		||||
	defaultLanguage = initializers.GetEnv("DEFAULT_LANGUAGE", defaultLanguage)
 | 
			
		||||
	languages = strings.Split(initializers.GetEnv("LANGUAGES", defaultLanguage), ",")
 | 
			
		||||
	haveDefaultLanguage := false
 | 
			
		||||
	for i, v := range(languages) {
 | 
			
		||||
		languages[i] = strings.ReplaceAll(v, " ", "")
 | 
			
		||||
		if languages[i] == defaultLanguage {
 | 
			
		||||
			haveDefaultLanguage = true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !haveDefaultLanguage {
 | 
			
		||||
		languages = append([]string{defaultLanguage}, languages...)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func setBase() error {
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
@ -70,20 +43,14 @@ func setBase() error {
 | 
			
		||||
	dataURLBase = initializers.GetEnv("DATA_URL_BASE", "http://localhost:5006")
 | 
			
		||||
	BearerToken = initializers.GetEnv("BEARER_TOKEN", "")
 | 
			
		||||
 | 
			
		||||
	_, err = url.Parse(custodialURLBase)
 | 
			
		||||
	_, err = url.JoinPath(custodialURLBase, "/foo")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	_, err = url.Parse(dataURLBase)
 | 
			
		||||
	_, err = url.JoinPath(dataURLBase, "/bar")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func setConn() error {
 | 
			
		||||
	DbConn = initializers.GetEnv("DB_CONN", "")
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -93,14 +60,6 @@ func LoadConfig() error {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	err = setConn()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	err = setLanguage()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	CreateAccountURL, _ = url.JoinPath(custodialURLBase, createAccountPath)
 | 
			
		||||
	TrackStatusURL, _ = url.JoinPath(custodialURLBase, trackStatusPath)
 | 
			
		||||
	BalanceURL, _ = url.JoinPath(custodialURLBase, balancePathPrefix)
 | 
			
		||||
@ -110,8 +69,6 @@ func LoadConfig() error {
 | 
			
		||||
	VoucherTransfersURL, _ = url.JoinPath(dataURLBase, voucherTransfersPathPrefix)
 | 
			
		||||
	VoucherDataURL, _ = url.JoinPath(dataURLBase, voucherDataPathPrefix)
 | 
			
		||||
	CheckAliasURL, _ = url.JoinPath(dataURLBase, AliasPrefix)
 | 
			
		||||
	DefaultLanguage = defaultLanguage
 | 
			
		||||
	Languages = languages
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +0,0 @@
 | 
			
		||||
package debug
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	DebugCap uint32
 | 
			
		||||
)
 | 
			
		||||
							
								
								
									
										84
									
								
								debug/db.go
									
									
									
									
									
								
							
							
						
						
									
										84
									
								
								debug/db.go
									
									
									
									
									
								
							@ -1,84 +0,0 @@
 | 
			
		||||
package debug
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"encoding/binary"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/common"
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	dbTypStr map[common.DataTyp]string = make(map[common.DataTyp]string)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type KeyInfo struct {
 | 
			
		||||
	SessionId string
 | 
			
		||||
	Typ uint8
 | 
			
		||||
	SubTyp common.DataTyp
 | 
			
		||||
	Label string
 | 
			
		||||
	Description string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (k KeyInfo) String() string {
 | 
			
		||||
	v := uint16(k.SubTyp)
 | 
			
		||||
	s := subTypToString(k.SubTyp)
 | 
			
		||||
	if s == "" {
 | 
			
		||||
		v = uint16(k.Typ)
 | 
			
		||||
		s = typToString(k.Typ)
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Sprintf("Session Id: %s\nTyp: %s (%d)\n", k.SessionId, s, v)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ToKeyInfo(k []byte, sessionId string) (KeyInfo, error) {
 | 
			
		||||
	o := KeyInfo{}
 | 
			
		||||
	b := []byte(sessionId)
 | 
			
		||||
 | 
			
		||||
	if len(k) <= len(b) {
 | 
			
		||||
		return o, fmt.Errorf("storage key missing")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	o.SessionId = sessionId
 | 
			
		||||
 | 
			
		||||
	o.Typ = uint8(k[0])
 | 
			
		||||
	k = k[1:]
 | 
			
		||||
	o.SessionId = string(k[:len(b)])
 | 
			
		||||
	k = k[len(b):]
 | 
			
		||||
 | 
			
		||||
	if o.Typ == db.DATATYPE_USERDATA {
 | 
			
		||||
		if len(k) == 0 {
 | 
			
		||||
			return o, fmt.Errorf("missing subtype key")
 | 
			
		||||
		}
 | 
			
		||||
		v := binary.BigEndian.Uint16(k[:2])
 | 
			
		||||
		o.SubTyp = common.DataTyp(v)
 | 
			
		||||
		o.Label = subTypToString(o.SubTyp)
 | 
			
		||||
		k = k[2:]
 | 
			
		||||
	} else {
 | 
			
		||||
		o.Label = typToString(o.Typ)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(k) != 0 {
 | 
			
		||||
		return o, fmt.Errorf("excess key information")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return o, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func FromKey(k []byte) (KeyInfo, error) {
 | 
			
		||||
	o := KeyInfo{}
 | 
			
		||||
 | 
			
		||||
	if len(k) < 4 {
 | 
			
		||||
		return o, fmt.Errorf("insufficient key length")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sessionIdBytes := k[1:len(k)-2]
 | 
			
		||||
	return ToKeyInfo(k, string(sessionIdBytes))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func subTypToString(v common.DataTyp) string {
 | 
			
		||||
	return dbTypStr[v + db.DATATYPE_USERDATA + 1]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func typToString(v uint8) string {
 | 
			
		||||
	return dbTypStr[common.DataTyp(uint16(v))]
 | 
			
		||||
}
 | 
			
		||||
@ -1,44 +0,0 @@
 | 
			
		||||
// +build debugdb
 | 
			
		||||
 | 
			
		||||
package debug
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/common"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	DebugCap |= 1
 | 
			
		||||
	dbTypStr[db.DATATYPE_STATE] = "internal state"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TRACKING_ID] = "tracking id"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_PUBLIC_KEY] = "public key"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT_PIN] = "account pin"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_FIRST_NAME] = "first name"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_FAMILY_NAME] = "family name"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_YOB] = "year of birth"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_LOCATION] = "location"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_GENDER] = "gender"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_OFFERINGS] = "offerings"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_RECIPIENT] = "recipient"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_AMOUNT] = "amount"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TEMPORARY_VALUE] = "temporary value"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_SYM] = "active sym"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_BAL] = "active bal"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_BLOCKED_NUMBER] = "blocked number"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_PUBLIC_KEY_REVERSE] = "public_key_reverse"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_DECIMAL] = "active decimal"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_ADDRESS] = "active address"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_SYMBOLS] = "voucher symbols"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_BALANCES] = "voucher balances"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_DECIMALS] = "voucher decimals"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_ADDRESSES] = "voucher addresses"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_SENDERS] = "tx senders"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_RECIPIENTS] = "tx recipients"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_VALUES] = "tx values"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_ADDRESSES] = "tx addresses"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_HASHES] = "tx hashes"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_DATES] = "tx dates"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_SYMBOLS] = "tx symbols"
 | 
			
		||||
	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_DECIMALS] = "tx decimals"
 | 
			
		||||
}
 | 
			
		||||
@ -1,78 +0,0 @@
 | 
			
		||||
package debug
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/common"
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestDebugDbSubKeyInfo(t *testing.T) {
 | 
			
		||||
	s := "foo"
 | 
			
		||||
	b := []byte{0x20}
 | 
			
		||||
	b = append(b, []byte(s)...)
 | 
			
		||||
	b = append(b, []byte{0x00, 0x02}...)
 | 
			
		||||
	r, err := ToKeyInfo(b, s)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	if r.SessionId != s {
 | 
			
		||||
		t.Fatalf("expected %s, got %s", s, r.SessionId)
 | 
			
		||||
	}
 | 
			
		||||
	if r.Typ != 32 {
 | 
			
		||||
		t.Fatalf("expected 64, got %d", r.Typ)
 | 
			
		||||
	}
 | 
			
		||||
	if r.SubTyp != 2 {
 | 
			
		||||
		t.Fatalf("expected 2, got %d", r.SubTyp)
 | 
			
		||||
	}
 | 
			
		||||
	if DebugCap & 1 > 0 {
 | 
			
		||||
		if r.Label != "tracking id" {
 | 
			
		||||
			t.Fatalf("expected 'tracking id', got '%s'", r.Label)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDebugDbKeyInfo(t *testing.T) {
 | 
			
		||||
	s := "bar"
 | 
			
		||||
	b := []byte{0x10}
 | 
			
		||||
	b = append(b, []byte(s)...)
 | 
			
		||||
	r, err := ToKeyInfo(b, s)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	if r.SessionId != s {
 | 
			
		||||
		t.Fatalf("expected %s, got %s", s, r.SessionId)
 | 
			
		||||
	}
 | 
			
		||||
	if r.Typ != 16 {
 | 
			
		||||
		t.Fatalf("expected 16, got %d", r.Typ)
 | 
			
		||||
	}
 | 
			
		||||
	if DebugCap & 1 > 0 {
 | 
			
		||||
		if r.Label != "internal state" {
 | 
			
		||||
			t.Fatalf("expected 'internal_state', got '%s'", r.Label)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDebugDbKeyInfoRestore(t *testing.T) {
 | 
			
		||||
	s := "bar"
 | 
			
		||||
	b := []byte{db.DATATYPE_USERDATA}
 | 
			
		||||
	b = append(b, []byte(s)...)
 | 
			
		||||
	k := common.ToBytes(common.DATA_ACTIVE_SYM)
 | 
			
		||||
	b = append(b, k...)
 | 
			
		||||
 | 
			
		||||
	r, err := ToKeyInfo(b, s)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	if r.SessionId != s {
 | 
			
		||||
		t.Fatalf("expected %s, got %s", s, r.SessionId)
 | 
			
		||||
	}
 | 
			
		||||
	if r.Typ != 32 {
 | 
			
		||||
		t.Fatalf("expected 32, got %d", r.Typ)
 | 
			
		||||
	}
 | 
			
		||||
	if DebugCap & 1 > 0 {
 | 
			
		||||
		if r.Label != "active sym" {
 | 
			
		||||
			t.Fatalf("expected 'active sym', got '%s'", r.Label)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,126 +0,0 @@
 | 
			
		||||
// create language files from environment
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"git.defalsify.org/vise.git/lang"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/config"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	
 | 
			
		||||
	changeHeadSrc = `LOAD reset_account_authorized 0
 | 
			
		||||
LOAD reset_incorrect 0
 | 
			
		||||
CATCH incorrect_pin flag_incorrect_pin 1
 | 
			
		||||
CATCH pin_entry flag_account_authorized 0
 | 
			
		||||
` 
 | 
			
		||||
 | 
			
		||||
	selectSrc = `LOAD set_language 6
 | 
			
		||||
RELOAD set_language
 | 
			
		||||
CATCH terms flag_account_created 0
 | 
			
		||||
MOVE language_changed
 | 
			
		||||
`
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg = logging.NewVanilla()
 | 
			
		||||
	mouts string
 | 
			
		||||
	incmps string
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	initializers.LoadEnvVariables()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func toLanguageLabel(ln lang.Language) string {
 | 
			
		||||
	s := ln.Name
 | 
			
		||||
	v := strings.Split(s, " (")
 | 
			
		||||
	if len(v) > 1 {
 | 
			
		||||
		s = v[0]
 | 
			
		||||
	}
 | 
			
		||||
	return s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func toLanguageKey(ln lang.Language) string {
 | 
			
		||||
	s := toLanguageLabel(ln)
 | 
			
		||||
	return strings.ToLower(s)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	var srcDir string
 | 
			
		||||
 | 
			
		||||
	flag.StringVar(&srcDir, "o", ".", "resource dir write to")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	logg.Infof("start command", "dir", srcDir)
 | 
			
		||||
 | 
			
		||||
	err := config.LoadConfig()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "config load error: %v", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	logg.Tracef("using languages", "lang", config.Languages)
 | 
			
		||||
 | 
			
		||||
	for i, v := range(config.Languages) {
 | 
			
		||||
		ln, err := lang.LanguageFromCode(v)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			fmt.Fprintf(os.Stderr, "error parsing language: %s\n", v)
 | 
			
		||||
			os.Exit(1)
 | 
			
		||||
		}
 | 
			
		||||
		n := i + 1
 | 
			
		||||
		s := toLanguageKey(ln)
 | 
			
		||||
		mouts += fmt.Sprintf("MOUT %s %v\n", s, n)
 | 
			
		||||
		v = "set_" + ln.Code
 | 
			
		||||
		incmps += fmt.Sprintf("INCMP %s %v\n", v, n)
 | 
			
		||||
 | 
			
		||||
		p := path.Join(srcDir, v)
 | 
			
		||||
		w, err := os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			fmt.Fprintf(os.Stderr, "failed open language set template output: %v\n", err)
 | 
			
		||||
			os.Exit(1)
 | 
			
		||||
		}
 | 
			
		||||
		s = toLanguageLabel(ln)
 | 
			
		||||
		defer w.Close()
 | 
			
		||||
		_, err = w.Write([]byte(s))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err)
 | 
			
		||||
			os.Exit(1)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	src := mouts + "HALT\n" + incmps
 | 
			
		||||
	src += "INCMP . *\n"
 | 
			
		||||
 | 
			
		||||
	p := path.Join(srcDir, "select_language.vis")
 | 
			
		||||
	w, err := os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "failed open select language vis output: %v\n", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	defer w.Close()
 | 
			
		||||
	_, err = w.Write([]byte(src))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	src = changeHeadSrc + src
 | 
			
		||||
	p = path.Join(srcDir, "change_language.vis")
 | 
			
		||||
	w, err = os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "failed open select language vis output: %v\n", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	defer w.Close()
 | 
			
		||||
	_, err = w.Write([]byte(src))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,100 +0,0 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/config"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/debug"
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg      = logging.NewVanilla()
 | 
			
		||||
	scriptDir = path.Join("services", "registration")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	initializers.LoadEnvVariables()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func formatItem(k []byte, v []byte) (string, error) {
 | 
			
		||||
	o, err := debug.FromKey(k)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	s := fmt.Sprintf("%vValue: %v\n\n", o, string(v))
 | 
			
		||||
	return s, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	config.LoadConfig()
 | 
			
		||||
 | 
			
		||||
	var connStr string
 | 
			
		||||
	var sessionId string
 | 
			
		||||
	var database string
 | 
			
		||||
	var engineDebug bool
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
 | 
			
		||||
	flag.StringVar(&connStr, "c", ".state", "connection string")
 | 
			
		||||
	flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	if connStr != "" {
 | 
			
		||||
		connStr = config.DbConn
 | 
			
		||||
	}
 | 
			
		||||
	connData, err := storage.ToConnData(config.DbConn)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "connstr err: %v", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logg.Infof("start command", "conn", connData)
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	ctx = context.WithValue(ctx, "SessionId", sessionId)
 | 
			
		||||
	ctx = context.WithValue(ctx, "Database", database)
 | 
			
		||||
 | 
			
		||||
	resourceDir := scriptDir
 | 
			
		||||
	menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
 | 
			
		||||
 | 
			
		||||
	store, err := menuStorageService.GetUserdataDb(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "get userdata db: %v\n", err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	store.SetPrefix(db.DATATYPE_USERDATA)
 | 
			
		||||
 | 
			
		||||
	d, err := store.Dump(ctx, []byte(sessionId))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "store dump fail: %v\n", err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for true {
 | 
			
		||||
		k, v := d.Next(ctx)
 | 
			
		||||
		if k == nil {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		r, err := formatItem(k, v)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			fmt.Fprintf(os.Stderr, "format db item error: %v", err)
 | 
			
		||||
			os.Exit(1)
 | 
			
		||||
		}
 | 
			
		||||
		fmt.Printf(r)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = store.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,90 +0,0 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"crypto/sha1"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/common"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/config"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
	testdataloader "github.com/peteole/testdata-loader"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg      = logging.NewVanilla()
 | 
			
		||||
	baseDir   = testdataloader.GetBasePath()
 | 
			
		||||
	scriptDir = path.Join("services", "registration")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	initializers.LoadEnvVariables()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	config.LoadConfig()
 | 
			
		||||
 | 
			
		||||
	var connStr string
 | 
			
		||||
	var sessionId string
 | 
			
		||||
	var database string
 | 
			
		||||
	var engineDebug bool
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
 | 
			
		||||
	flag.StringVar(&connStr, "c", "", "connection string")
 | 
			
		||||
	flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	if connStr != "" {
 | 
			
		||||
		connStr = config.DbConn
 | 
			
		||||
	}
 | 
			
		||||
	connData, err := storage.ToConnData(config.DbConn)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "connstr err: %v", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logg.Infof("start command", "conn", connData)
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	ctx = context.WithValue(ctx, "SessionId", sessionId)
 | 
			
		||||
	ctx = context.WithValue(ctx, "Database", database)
 | 
			
		||||
 | 
			
		||||
	resourceDir := scriptDir
 | 
			
		||||
	menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
 | 
			
		||||
	
 | 
			
		||||
	store, err := menuStorageService.GetUserdataDb(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	userStore := common.UserDataStore{store}
 | 
			
		||||
 | 
			
		||||
	h := sha1.New()
 | 
			
		||||
	h.Write([]byte(sessionId))
 | 
			
		||||
	address := h.Sum(nil)
 | 
			
		||||
	addressString := fmt.Sprintf("%x", address)
 | 
			
		||||
 | 
			
		||||
	err = userStore.WriteEntry(ctx, sessionId, common.DATA_PUBLIC_KEY, []byte(addressString))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = userStore.WriteEntry(ctx, addressString, common.DATA_PUBLIC_KEY_REVERSE, []byte(sessionId))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = store.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										28
									
								
								doc/data.md
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								doc/data.md
									
									
									
									
									
								
							@ -1,28 +0,0 @@
 | 
			
		||||
# Internals
 | 
			
		||||
 | 
			
		||||
## Version
 | 
			
		||||
 | 
			
		||||
This document describes component versions:
 | 
			
		||||
 | 
			
		||||
* `urdt-ussd` `v0.5.0-beta`
 | 
			
		||||
* `go-vise` `v0.2.2`
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## User profile data
 | 
			
		||||
 | 
			
		||||
All user profile items are stored under keys matching the user's session id, prefixed with the 8-bit value `git.defalsify.org/vise.git/db.DATATYPE_USERDATA` (32), and followed with a 16-big big-endian value subprefix.
 | 
			
		||||
 | 
			
		||||
For example, given the sessionId `+254123` and the key `git.grassecon.net/urdt-ussd/common.DATA_PUBLIC_KEY` (2) will be stored under the key:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
0x322b3235343132330002
 | 
			
		||||
 | 
			
		||||
prefix   sessionid       subprefix
 | 
			
		||||
32       2b323534313233  0002
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Sub-prefixes
 | 
			
		||||
 | 
			
		||||
All sub-prefixes are defined as constants in the `git.grassecon.net/urdt-ussd/common` package. The constant names have the prefix `DATA_`
 | 
			
		||||
 | 
			
		||||
Please refer to inline godoc documentation for the `git.grassecon.net/urdt-ussd/common` package for details on each data item.
 | 
			
		||||
							
								
								
									
										4
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.mod
									
									
									
									
									
								
							@ -3,7 +3,7 @@ module git.grassecon.net/urdt/ussd
 | 
			
		||||
go 1.23.0
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d
 | 
			
		||||
	git.defalsify.org/vise.git v0.2.1-0.20241122120231-9e9ee5bdfa7a
 | 
			
		||||
	github.com/alecthomas/assert/v2 v2.2.2
 | 
			
		||||
	github.com/gofrs/uuid v4.4.0+incompatible
 | 
			
		||||
	github.com/grassrootseconomics/eth-custodial v1.3.0-beta
 | 
			
		||||
@ -11,7 +11,6 @@ require (
 | 
			
		||||
	github.com/joho/godotenv v1.5.1
 | 
			
		||||
	github.com/peteole/testdata-loader v0.3.0
 | 
			
		||||
	github.com/stretchr/testify v1.9.0
 | 
			
		||||
	golang.org/x/crypto v0.27.0
 | 
			
		||||
	gopkg.in/leonelquinteros/gotext.v1 v1.3.1
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -33,6 +32,7 @@ require (
 | 
			
		||||
	github.com/rogpeppe/go-internal v1.13.1 // indirect
 | 
			
		||||
	github.com/stretchr/objx v0.5.2 // indirect
 | 
			
		||||
	github.com/x448/float16 v0.8.4 // indirect
 | 
			
		||||
	golang.org/x/crypto v0.27.0 // indirect
 | 
			
		||||
	golang.org/x/sync v0.8.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.18.0 // indirect
 | 
			
		||||
	gopkg.in/yaml.v3 v3.0.1 // indirect
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
									
									
									
									
								
							@ -1,5 +1,5 @@
 | 
			
		||||
git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d h1:bPAOVZOX4frSGhfOdcj7kc555f8dc9DmMd2YAyC2AMw=
 | 
			
		||||
git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
 | 
			
		||||
git.defalsify.org/vise.git v0.2.1-0.20241122120231-9e9ee5bdfa7a h1:LvGKktk0kUnuRN3nF9r15D8OoV0sFaMmQr52kGq2gtE=
 | 
			
		||||
git.defalsify.org/vise.git v0.2.1-0.20241122120231-9e9ee5bdfa7a/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
 | 
			
		||||
github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk=
 | 
			
		||||
github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
 | 
			
		||||
github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g=
 | 
			
		||||
 | 
			
		||||
@ -3,30 +3,24 @@ package initializers
 | 
			
		||||
import (
 | 
			
		||||
	"log"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"github.com/joho/godotenv"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func LoadEnvVariables() {
 | 
			
		||||
	LoadEnvVariablesPath(".")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func LoadEnvVariablesPath(dir string) {
 | 
			
		||||
	fp := path.Join(dir, ".env")
 | 
			
		||||
	err := godotenv.Load(fp)
 | 
			
		||||
	err := godotenv.Load()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error loading .env file", err)
 | 
			
		||||
		log.Fatal("Error loading .env file")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper to get environment variables with a default fallback
 | 
			
		||||
func GetEnv(key, defaultVal string) string {
 | 
			
		||||
	if value, exists := os.LookupEnv(key); exists {
 | 
			
		||||
		return value
 | 
			
		||||
  if value, exists := os.LookupEnv(key); exists {
 | 
			
		||||
   	return value
 | 
			
		||||
	}
 | 
			
		||||
	return defaultVal
 | 
			
		||||
  return defaultVal
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper to safely convert environment variables to uint
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -6,42 +6,42 @@ import (
 | 
			
		||||
	"git.defalsify.org/vise.git/persist"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers/application"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers/ussd"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type BaseSessionHandler struct {
 | 
			
		||||
	cfgTemplate engine.Config
 | 
			
		||||
	rp          RequestParser
 | 
			
		||||
	rs          resource.Resource
 | 
			
		||||
	hn          *application.Handlers
 | 
			
		||||
	provider    storage.StorageProvider
 | 
			
		||||
	rp RequestParser
 | 
			
		||||
	rs resource.Resource
 | 
			
		||||
	hn *ussd.Handlers
 | 
			
		||||
	provider storage.StorageProvider
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewBaseSessionHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp RequestParser, hn *application.Handlers) *BaseSessionHandler {
 | 
			
		||||
func NewBaseSessionHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp RequestParser, hn *ussd.Handlers) *BaseSessionHandler {
 | 
			
		||||
	return &BaseSessionHandler{
 | 
			
		||||
		cfgTemplate: cfg,
 | 
			
		||||
		rs:          rs,
 | 
			
		||||
		hn:          hn,
 | 
			
		||||
		rp:          rp,
 | 
			
		||||
		provider:    storage.NewSimpleStorageProvider(stateDb, userdataDb),
 | 
			
		||||
		rs: rs,
 | 
			
		||||
		hn: hn,
 | 
			
		||||
		rp: rp,
 | 
			
		||||
		provider: storage.NewSimpleStorageProvider(stateDb, userdataDb),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *BaseSessionHandler) Shutdown() {
 | 
			
		||||
func(f* BaseSessionHandler) Shutdown() {
 | 
			
		||||
	err := f.provider.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logg.Errorf("handler shutdown error", "err", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *BaseSessionHandler) GetEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) engine.Engine {
 | 
			
		||||
func(f *BaseSessionHandler) GetEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) engine.Engine {
 | 
			
		||||
	en := engine.NewEngine(cfg, rs)
 | 
			
		||||
	en = en.WithPersister(pr)
 | 
			
		||||
	return en
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error) {
 | 
			
		||||
func(f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error) {
 | 
			
		||||
	var r bool
 | 
			
		||||
	var err error
 | 
			
		||||
	var ok bool
 | 
			
		||||
@ -88,21 +88,21 @@ func (f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error)
 | 
			
		||||
	return rqs, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *BaseSessionHandler) Output(rqs RequestSession) (RequestSession, error) {
 | 
			
		||||
func(f *BaseSessionHandler) Output(rqs RequestSession) (RequestSession,  error) {
 | 
			
		||||
	var err error
 | 
			
		||||
	_, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer)
 | 
			
		||||
	return rqs, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *BaseSessionHandler) Reset(rqs RequestSession) (RequestSession, error) {
 | 
			
		||||
func(f *BaseSessionHandler) Reset(rqs RequestSession) (RequestSession, error) {
 | 
			
		||||
	defer f.provider.Put(rqs.Config.SessionId, rqs.Storage)
 | 
			
		||||
	return rqs, rqs.Engine.Finish()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *BaseSessionHandler) GetConfig() engine.Config {
 | 
			
		||||
func(f *BaseSessionHandler) GetConfig() engine.Config {
 | 
			
		||||
	return f.cfgTemplate
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *BaseSessionHandler) GetRequestParser() RequestParser {
 | 
			
		||||
func(f *BaseSessionHandler) GetRequestParser() RequestParser {
 | 
			
		||||
	return f.rp
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,141 +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/application"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/utils"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/remote"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type HandlerService interface {
 | 
			
		||||
	GetHandler() (*application.Handlers, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getParser(fp string, debug bool) (*asm.FlagParser, error) {
 | 
			
		||||
	flagParser := asm.NewFlagParser().WithDebug()
 | 
			
		||||
	_, err := flagParser.Load(fp)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return flagParser, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type LocalHandlerService struct {
 | 
			
		||||
	Parser        *asm.FlagParser
 | 
			
		||||
	DbRs          *resource.DbResource
 | 
			
		||||
	Pe            *persist.Persister
 | 
			
		||||
	UserdataStore *db.Db
 | 
			
		||||
	AdminStore    *utils.AdminStore
 | 
			
		||||
	Cfg           engine.Config
 | 
			
		||||
	Rs            resource.Resource
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewLocalHandlerService(ctx context.Context, fp string, debug bool, dbResource *resource.DbResource, cfg engine.Config, rs resource.Resource) (*LocalHandlerService, error) {
 | 
			
		||||
	parser, err := getParser(fp, debug)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	adminstore, err := utils.NewAdminStore(ctx, "admin_numbers")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return &LocalHandlerService{
 | 
			
		||||
		Parser:     parser,
 | 
			
		||||
		DbRs:       dbResource,
 | 
			
		||||
		AdminStore: adminstore,
 | 
			
		||||
		Cfg:        cfg,
 | 
			
		||||
		Rs:         rs,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ls *LocalHandlerService) SetPersister(Pe *persist.Persister) {
 | 
			
		||||
	ls.Pe = Pe
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ls *LocalHandlerService) SetDataStore(db *db.Db) {
 | 
			
		||||
	ls.UserdataStore = db
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceInterface) (*application.Handlers, error) {
 | 
			
		||||
	replaceSeparatorFunc := func(input string) string {
 | 
			
		||||
		return strings.ReplaceAll(input, ":", ls.Cfg.MenuSeparator)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	appHandlers, err := application.NewHandlers(ls.Parser, *ls.UserdataStore, ls.AdminStore, accountService, replaceSeparatorFunc)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	appHandlers = appHandlers.WithPersister(ls.Pe)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("set_language", appHandlers.SetLanguage)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("create_account", appHandlers.CreateAccount)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("save_temporary_pin", appHandlers.SaveTemporaryPin)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("verify_create_pin", appHandlers.VerifyCreatePin)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("check_identifier", appHandlers.CheckIdentifier)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("check_account_status", appHandlers.CheckAccountStatus)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("authorize_account", appHandlers.Authorize)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("quit", appHandlers.Quit)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("check_balance", appHandlers.CheckBalance)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("validate_recipient", appHandlers.ValidateRecipient)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("transaction_reset", appHandlers.TransactionReset)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("invite_valid_recipient", appHandlers.InviteValidRecipient)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("max_amount", appHandlers.MaxAmount)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("validate_amount", appHandlers.ValidateAmount)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("reset_transaction_amount", appHandlers.ResetTransactionAmount)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("get_recipient", appHandlers.GetRecipient)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("get_sender", appHandlers.GetSender)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("get_amount", appHandlers.GetAmount)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("reset_incorrect", appHandlers.ResetIncorrectPin)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("save_firstname", appHandlers.SaveFirstname)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("save_familyname", appHandlers.SaveFamilyname)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("save_gender", appHandlers.SaveGender)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("save_location", appHandlers.SaveLocation)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("save_yob", appHandlers.SaveYob)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("save_offerings", appHandlers.SaveOfferings)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("reset_account_authorized", appHandlers.ResetAccountAuthorized)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("reset_allow_update", appHandlers.ResetAllowUpdate)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("get_profile_info", appHandlers.GetProfileInfo)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("verify_yob", appHandlers.VerifyYob)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("reset_incorrect_date_format", appHandlers.ResetIncorrectYob)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("initiate_transaction", appHandlers.InitiateTransaction)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("verify_new_pin", appHandlers.VerifyNewPin)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("confirm_pin_change", appHandlers.ConfirmPinChange)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("quit_with_help", appHandlers.QuitWithHelp)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("fetch_community_balance", appHandlers.FetchCommunityBalance)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("set_default_voucher", appHandlers.SetDefaultVoucher)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("check_vouchers", appHandlers.CheckVouchers)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("get_vouchers", appHandlers.GetVoucherList)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("view_voucher", appHandlers.ViewVoucher)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("set_voucher", appHandlers.SetVoucher)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("get_voucher_details", appHandlers.GetVoucherDetails)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("reset_valid_pin", appHandlers.ResetValidPin)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("check_pin_mismatch", appHandlers.CheckBlockedNumPinMisMatch)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("validate_blocked_number", appHandlers.ValidateBlockedNumber)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("retrieve_blocked_number", appHandlers.RetrieveBlockedNumber)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("reset_unregistered_number", appHandlers.ResetUnregisteredNumber)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("reset_others_pin", appHandlers.ResetOthersPin)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("save_others_temporary_pin", appHandlers.SaveOthersTemporaryPin)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("get_current_profile_info", appHandlers.GetCurrentProfileInfo)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("check_transactions", appHandlers.CheckTransactions)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("get_transactions", appHandlers.GetTransactionsList)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("view_statement", appHandlers.ViewTransactionStatement)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("update_all_profile_items", appHandlers.UpdateAllProfileItems)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("set_back", appHandlers.SetBack)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("show_blocked_account", appHandlers.ShowBlockedAccount)
 | 
			
		||||
 | 
			
		||||
	return appHandlers, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO: enable setting of sessionId on engine init time
 | 
			
		||||
func (ls *LocalHandlerService) GetEngine() *engine.DefaultEngine {
 | 
			
		||||
	en := engine.NewEngine(ls.Cfg, ls.Rs)
 | 
			
		||||
	en = en.WithPersister(ls.Pe)
 | 
			
		||||
	return en
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										133
									
								
								internal/handlers/handlerservice.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								internal/handlers/handlerservice.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,133 @@
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
 | 
			
		||||
	"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) {
 | 
			
		||||
	ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore, ls.AdminStore, accountService)
 | 
			
		||||
	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.CheckPinMisMatch)
 | 
			
		||||
	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)
 | 
			
		||||
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
@ -6,9 +6,9 @@ import (
 | 
			
		||||
	"io"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"git.defalsify.org/vise.git/persist"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
	"git.defalsify.org/vise.git/persist"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
)
 | 
			
		||||
@ -20,26 +20,26 @@ var (
 | 
			
		||||
var (
 | 
			
		||||
	ErrInvalidRequest = errors.New("invalid request for context")
 | 
			
		||||
	ErrSessionMissing = errors.New("missing session")
 | 
			
		||||
	ErrInvalidInput   = errors.New("invalid input")
 | 
			
		||||
	ErrStorage        = errors.New("storage retrieval fail")
 | 
			
		||||
	ErrEngineType     = errors.New("incompatible engine")
 | 
			
		||||
	ErrEngineInit     = errors.New("engine init fail")
 | 
			
		||||
	ErrEngineExec     = errors.New("engine exec fail")
 | 
			
		||||
	ErrInvalidInput = errors.New("invalid input")
 | 
			
		||||
	ErrStorage = errors.New("storage retrieval fail")
 | 
			
		||||
	ErrEngineType = errors.New("incompatible engine")
 | 
			
		||||
	ErrEngineInit = errors.New("engine init fail")
 | 
			
		||||
	ErrEngineExec = errors.New("engine exec fail")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type RequestSession struct {
 | 
			
		||||
	Ctx      context.Context
 | 
			
		||||
	Config   engine.Config
 | 
			
		||||
	Engine   engine.Engine
 | 
			
		||||
	Input    []byte
 | 
			
		||||
	Storage  *storage.Storage
 | 
			
		||||
	Writer   io.Writer
 | 
			
		||||
	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(context context.Context, rq any) (string, error)
 | 
			
		||||
	GetSessionId(rq any) (string, error)
 | 
			
		||||
	GetInput(rq any) ([]byte, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,23 +1,19 @@
 | 
			
		||||
package application
 | 
			
		||||
package ussd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/cache"
 | 
			
		||||
	"git.defalsify.org/vise.git/lang"
 | 
			
		||||
	"git.defalsify.org/vise.git/persist"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
	"git.defalsify.org/vise.git/state"
 | 
			
		||||
	dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/testutil/mocks"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/testutil/testservice"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/utils"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/models"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/common"
 | 
			
		||||
@ -26,7 +22,6 @@ import (
 | 
			
		||||
	testdataloader "github.com/peteole/testdata-loader"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
 | 
			
		||||
	visedb "git.defalsify.org/vise.git/db"
 | 
			
		||||
	memdb "git.defalsify.org/vise.git/db/mem"
 | 
			
		||||
	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
 | 
			
		||||
)
 | 
			
		||||
@ -36,11 +31,6 @@ var (
 | 
			
		||||
	flagsPath = path.Join(baseDir, "services", "registration", "pp.csv")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// mockReplaceSeparator function
 | 
			
		||||
var mockReplaceSeparator = func(input string) string {
 | 
			
		||||
	return strings.ReplaceAll(input, ":", ": ")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// InitializeTestStore sets up and returns an in-memory database and store.
 | 
			
		||||
func InitializeTestStore(t *testing.T) (context.Context, *common.UserDataStore) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
@ -60,14 +50,13 @@ func InitializeTestStore(t *testing.T) (context.Context, *common.UserDataStore)
 | 
			
		||||
	return ctx, store
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func InitializeTestSubPrefixDb(t *testing.T, ctx context.Context) *dbstorage.SubPrefixDb {
 | 
			
		||||
func InitializeTestSubPrefixDb(t *testing.T, ctx context.Context) *storage.SubPrefixDb {
 | 
			
		||||
	db := memdb.NewMemDb()
 | 
			
		||||
	err := db.Connect(ctx, "")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	prefix := common.ToBytes(visedb.DATATYPE_USERDATA)
 | 
			
		||||
	spdb := dbstorage.NewSubPrefixDb(db, prefix)
 | 
			
		||||
	spdb := storage.NewSubPrefixDb(db, []byte("vouchers"))
 | 
			
		||||
 | 
			
		||||
	return spdb
 | 
			
		||||
}
 | 
			
		||||
@ -76,15 +65,12 @@ func TestNewHandlers(t *testing.T) {
 | 
			
		||||
	_, store := InitializeTestStore(t)
 | 
			
		||||
 | 
			
		||||
	fm, err := NewFlagManager(flagsPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	accountService := testservice.TestAccountService{}
 | 
			
		||||
 | 
			
		||||
	// Test case for valid UserDataStore
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Logf(err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	t.Run("Valid UserDataStore", func(t *testing.T) {
 | 
			
		||||
		handlers, err := NewHandlers(fm.parser, store, nil, &accountService, mockReplaceSeparator)
 | 
			
		||||
		handlers, err := NewHandlers(fm.parser, store, nil, &accountService)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatalf("expected no error, got %v", err)
 | 
			
		||||
		}
 | 
			
		||||
@ -94,130 +80,23 @@ func TestNewHandlers(t *testing.T) {
 | 
			
		||||
		if handlers.userdataStore == nil {
 | 
			
		||||
			t.Fatal("expected userdataStore to be set in handlers")
 | 
			
		||||
		}
 | 
			
		||||
		if handlers.ReplaceSeparatorFunc == nil {
 | 
			
		||||
			t.Fatal("expected ReplaceSeparatorFunc to be set in handlers")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Test ReplaceSeparatorFunc functionality
 | 
			
		||||
		input := "1:Menu item"
 | 
			
		||||
		expectedOutput := "1: Menu item"
 | 
			
		||||
		if handlers.ReplaceSeparatorFunc(input) != expectedOutput {
 | 
			
		||||
			t.Fatalf("ReplaceSeparatorFunc function did not return expected output: got %v, want %v", handlers.ReplaceSeparatorFunc(input), expectedOutput)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	// Test case for nil UserDataStore
 | 
			
		||||
	// Test case for nil userdataStore
 | 
			
		||||
	t.Run("Nil UserDataStore", func(t *testing.T) {
 | 
			
		||||
		handlers, err := NewHandlers(fm.parser, nil, nil, &accountService, mockReplaceSeparator)
 | 
			
		||||
		handlers, err := NewHandlers(fm.parser, nil, nil, &accountService)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			t.Fatal("expected an error, got none")
 | 
			
		||||
		}
 | 
			
		||||
		if handlers != nil {
 | 
			
		||||
			t.Fatal("expected handlers to be nil")
 | 
			
		||||
		}
 | 
			
		||||
		expectedError := "cannot create handler with nil userdata store"
 | 
			
		||||
		if err.Error() != expectedError {
 | 
			
		||||
			t.Fatalf("expected error '%s', got '%v'", expectedError, err)
 | 
			
		||||
		if err.Error() != "cannot create handler with nil userdata store" {
 | 
			
		||||
			t.Fatalf("expected specific error, got %v", err)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestInit(t *testing.T) {
 | 
			
		||||
	sessionId := "session123"
 | 
			
		||||
	ctx, store := InitializeTestStore(t)
 | 
			
		||||
	ctx = context.WithValue(ctx, "SessionId", sessionId)
 | 
			
		||||
 | 
			
		||||
	fm, err := NewFlagManager(flagsPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	adminstore, err := utils.NewAdminStore(ctx, "admin_numbers")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	st := state.NewState(128)
 | 
			
		||||
	ca := cache.NewCache()
 | 
			
		||||
 | 
			
		||||
	flag_admin_privilege, _ := fm.GetFlag("flag_admin_privilege")
 | 
			
		||||
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name           string
 | 
			
		||||
		setup          func() (*Handlers, context.Context)
 | 
			
		||||
		input          []byte
 | 
			
		||||
		expectedResult resource.Result
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "Handler not ready",
 | 
			
		||||
			setup: func() (*Handlers, context.Context) {
 | 
			
		||||
				return &Handlers{}, ctx
 | 
			
		||||
			},
 | 
			
		||||
			input:          []byte("1"),
 | 
			
		||||
			expectedResult: resource.Result{},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "State and memory initialization",
 | 
			
		||||
			setup: func() (*Handlers, context.Context) {
 | 
			
		||||
				pe := persist.NewPersister(store).WithSession(sessionId).WithContent(st, ca)
 | 
			
		||||
				h := &Handlers{
 | 
			
		||||
					flagManager: fm.parser,
 | 
			
		||||
					adminstore:  adminstore,
 | 
			
		||||
					pe:          pe,
 | 
			
		||||
				}
 | 
			
		||||
				return h, context.WithValue(ctx, "SessionId", sessionId)
 | 
			
		||||
			},
 | 
			
		||||
			input: []byte("1"),
 | 
			
		||||
			expectedResult: resource.Result{
 | 
			
		||||
				FlagReset: []uint32{flag_admin_privilege},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Non-admin session initialization",
 | 
			
		||||
			setup: func() (*Handlers, context.Context) {
 | 
			
		||||
				pe := persist.NewPersister(store).WithSession("0712345678").WithContent(st, ca)
 | 
			
		||||
				h := &Handlers{
 | 
			
		||||
					flagManager: fm.parser,
 | 
			
		||||
					adminstore:  adminstore,
 | 
			
		||||
					pe:          pe,
 | 
			
		||||
				}
 | 
			
		||||
				return h, context.WithValue(context.Background(), "SessionId", "0712345678")
 | 
			
		||||
			},
 | 
			
		||||
			input: []byte("1"),
 | 
			
		||||
			expectedResult: resource.Result{
 | 
			
		||||
				FlagReset: []uint32{flag_admin_privilege},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Move to top node on empty input",
 | 
			
		||||
			setup: func() (*Handlers, context.Context) {
 | 
			
		||||
				pe := persist.NewPersister(store).WithSession(sessionId).WithContent(st, ca)
 | 
			
		||||
				h := &Handlers{
 | 
			
		||||
					flagManager: fm.parser,
 | 
			
		||||
					adminstore:  adminstore,
 | 
			
		||||
					pe:          pe,
 | 
			
		||||
				}
 | 
			
		||||
				st.Code = []byte("some pending bytecode")
 | 
			
		||||
				return h, context.WithValue(ctx, "SessionId", sessionId)
 | 
			
		||||
			},
 | 
			
		||||
			input: []byte(""),
 | 
			
		||||
			expectedResult: resource.Result{
 | 
			
		||||
				FlagReset: []uint32{flag_admin_privilege},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			h, testCtx := tt.setup()
 | 
			
		||||
			res, err := h.Init(testCtx, "", tt.input)
 | 
			
		||||
 | 
			
		||||
			assert.NoError(t, err, "Unexpected error occurred")
 | 
			
		||||
			assert.Equal(t, res, tt.expectedResult, "Expected result should match actual result")
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestCreateAccount(t *testing.T) {
 | 
			
		||||
	sessionId := "session123"
 | 
			
		||||
	ctx, store := InitializeTestStore(t)
 | 
			
		||||
@ -300,14 +179,11 @@ func TestSaveFirstname(t *testing.T) {
 | 
			
		||||
	fm, _ := NewFlagManager(flagsPath)
 | 
			
		||||
 | 
			
		||||
	flag_allow_update, _ := fm.GetFlag("flag_allow_update")
 | 
			
		||||
	flag_firstname_set, _ := fm.GetFlag("flag_firstname_set")
 | 
			
		||||
 | 
			
		||||
	// Set the flag in the State
 | 
			
		||||
	mockState := state.NewState(128)
 | 
			
		||||
	mockState := state.NewState(16)
 | 
			
		||||
	mockState.SetFlag(flag_allow_update)
 | 
			
		||||
 | 
			
		||||
	expectedResult := resource.Result{}
 | 
			
		||||
 | 
			
		||||
	// Define test data
 | 
			
		||||
	firstName := "John"
 | 
			
		||||
 | 
			
		||||
@ -315,8 +191,6 @@ func TestSaveFirstname(t *testing.T) {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expectedResult.FlagSet = []uint32{flag_firstname_set}
 | 
			
		||||
 | 
			
		||||
	// Create the Handlers instance with the mock store
 | 
			
		||||
	h := &Handlers{
 | 
			
		||||
		userdataStore: store,
 | 
			
		||||
@ -329,7 +203,7 @@ func TestSaveFirstname(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
	// Assert results
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Equal(t, expectedResult, res)
 | 
			
		||||
	assert.Equal(t, resource.Result{}, res)
 | 
			
		||||
 | 
			
		||||
	// Verify that the DATA_FIRST_NAME entry has been updated with the temporary value
 | 
			
		||||
	storedFirstName, _ := store.ReadEntry(ctx, sessionId, common.DATA_FIRST_NAME)
 | 
			
		||||
@ -344,16 +218,11 @@ func TestSaveFamilyname(t *testing.T) {
 | 
			
		||||
	fm, _ := NewFlagManager(flagsPath)
 | 
			
		||||
 | 
			
		||||
	flag_allow_update, _ := fm.GetFlag("flag_allow_update")
 | 
			
		||||
	flag_firstname_set, _ := fm.GetFlag("flag_familyname_set")
 | 
			
		||||
 | 
			
		||||
	// Set the flag in the State
 | 
			
		||||
	mockState := state.NewState(128)
 | 
			
		||||
	mockState := state.NewState(16)
 | 
			
		||||
	mockState.SetFlag(flag_allow_update)
 | 
			
		||||
 | 
			
		||||
	expectedResult := resource.Result{}
 | 
			
		||||
 | 
			
		||||
	expectedResult.FlagSet = []uint32{flag_firstname_set}
 | 
			
		||||
 | 
			
		||||
	// Define test data
 | 
			
		||||
	familyName := "Doeee"
 | 
			
		||||
 | 
			
		||||
@ -373,7 +242,7 @@ func TestSaveFamilyname(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
	// Assert results
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Equal(t, expectedResult, res)
 | 
			
		||||
	assert.Equal(t, resource.Result{}, res)
 | 
			
		||||
 | 
			
		||||
	// Verify that the DATA_FAMILY_NAME entry has been updated with the temporary value
 | 
			
		||||
	storedFamilyName, _ := store.ReadEntry(ctx, sessionId, common.DATA_FAMILY_NAME)
 | 
			
		||||
@ -388,14 +257,11 @@ func TestSaveYoB(t *testing.T) {
 | 
			
		||||
	fm, _ := NewFlagManager(flagsPath)
 | 
			
		||||
 | 
			
		||||
	flag_allow_update, _ := fm.GetFlag("flag_allow_update")
 | 
			
		||||
	flag_yob_set, _ := fm.GetFlag("flag_yob_set")
 | 
			
		||||
 | 
			
		||||
	// Set the flag in the State
 | 
			
		||||
	mockState := state.NewState(108)
 | 
			
		||||
	mockState := state.NewState(16)
 | 
			
		||||
	mockState.SetFlag(flag_allow_update)
 | 
			
		||||
 | 
			
		||||
	expectedResult := resource.Result{}
 | 
			
		||||
 | 
			
		||||
	// Define test data
 | 
			
		||||
	yob := "1980"
 | 
			
		||||
 | 
			
		||||
@ -403,8 +269,6 @@ func TestSaveYoB(t *testing.T) {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expectedResult.FlagSet = []uint32{flag_yob_set}
 | 
			
		||||
 | 
			
		||||
	// Create the Handlers instance with the mock store
 | 
			
		||||
	h := &Handlers{
 | 
			
		||||
		userdataStore: store,
 | 
			
		||||
@ -417,7 +281,7 @@ func TestSaveYoB(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
	// Assert results
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Equal(t, expectedResult, res)
 | 
			
		||||
	assert.Equal(t, resource.Result{}, res)
 | 
			
		||||
 | 
			
		||||
	// Verify that the DATA_YOB entry has been updated with the temporary value
 | 
			
		||||
	storedYob, _ := store.ReadEntry(ctx, sessionId, common.DATA_YOB)
 | 
			
		||||
@ -432,14 +296,11 @@ func TestSaveLocation(t *testing.T) {
 | 
			
		||||
	fm, _ := NewFlagManager(flagsPath)
 | 
			
		||||
 | 
			
		||||
	flag_allow_update, _ := fm.GetFlag("flag_allow_update")
 | 
			
		||||
	flag_location_set, _ := fm.GetFlag("flag_location_set")
 | 
			
		||||
 | 
			
		||||
	// Set the flag in the State
 | 
			
		||||
	mockState := state.NewState(108)
 | 
			
		||||
	mockState := state.NewState(16)
 | 
			
		||||
	mockState.SetFlag(flag_allow_update)
 | 
			
		||||
 | 
			
		||||
	expectedResult := resource.Result{}
 | 
			
		||||
 | 
			
		||||
	// Define test data
 | 
			
		||||
	location := "Kilifi"
 | 
			
		||||
 | 
			
		||||
@ -447,8 +308,6 @@ func TestSaveLocation(t *testing.T) {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expectedResult.FlagSet = []uint32{flag_location_set}
 | 
			
		||||
 | 
			
		||||
	// Create the Handlers instance with the mock store
 | 
			
		||||
	h := &Handlers{
 | 
			
		||||
		userdataStore: store,
 | 
			
		||||
@ -461,7 +320,7 @@ func TestSaveLocation(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
	// Assert results
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Equal(t, expectedResult, res)
 | 
			
		||||
	assert.Equal(t, resource.Result{}, res)
 | 
			
		||||
 | 
			
		||||
	// Verify that the DATA_LOCATION entry has been updated with the temporary value
 | 
			
		||||
	storedLocation, _ := store.ReadEntry(ctx, sessionId, common.DATA_LOCATION)
 | 
			
		||||
@ -476,14 +335,11 @@ func TestSaveOfferings(t *testing.T) {
 | 
			
		||||
	fm, _ := NewFlagManager(flagsPath)
 | 
			
		||||
 | 
			
		||||
	flag_allow_update, _ := fm.GetFlag("flag_allow_update")
 | 
			
		||||
	flag_offerings_set, _ := fm.GetFlag("flag_offerings_set")
 | 
			
		||||
 | 
			
		||||
	// Set the flag in the State
 | 
			
		||||
	mockState := state.NewState(108)
 | 
			
		||||
	mockState := state.NewState(16)
 | 
			
		||||
	mockState.SetFlag(flag_allow_update)
 | 
			
		||||
 | 
			
		||||
	expectedResult := resource.Result{}
 | 
			
		||||
 | 
			
		||||
	// Define test data
 | 
			
		||||
	offerings := "Bananas"
 | 
			
		||||
 | 
			
		||||
@ -491,8 +347,6 @@ func TestSaveOfferings(t *testing.T) {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expectedResult.FlagSet = []uint32{flag_offerings_set}
 | 
			
		||||
 | 
			
		||||
	// Create the Handlers instance with the mock store
 | 
			
		||||
	h := &Handlers{
 | 
			
		||||
		userdataStore: store,
 | 
			
		||||
@ -505,7 +359,7 @@ func TestSaveOfferings(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
	// Assert results
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Equal(t, expectedResult, res)
 | 
			
		||||
	assert.Equal(t, resource.Result{}, res)
 | 
			
		||||
 | 
			
		||||
	// Verify that the DATA_OFFERINGS entry has been updated with the temporary value
 | 
			
		||||
	storedOfferings, _ := store.ReadEntry(ctx, sessionId, common.DATA_OFFERINGS)
 | 
			
		||||
@ -520,10 +374,9 @@ func TestSaveGender(t *testing.T) {
 | 
			
		||||
	fm, _ := NewFlagManager(flagsPath)
 | 
			
		||||
 | 
			
		||||
	flag_allow_update, _ := fm.GetFlag("flag_allow_update")
 | 
			
		||||
	flag_gender_set, _ := fm.GetFlag("flag_gender_set")
 | 
			
		||||
 | 
			
		||||
	// Set the flag in the State
 | 
			
		||||
	mockState := state.NewState(108)
 | 
			
		||||
	mockState := state.NewState(16)
 | 
			
		||||
	mockState.SetFlag(flag_allow_update)
 | 
			
		||||
 | 
			
		||||
	// Define test cases
 | 
			
		||||
@ -567,16 +420,12 @@ func TestSaveGender(t *testing.T) {
 | 
			
		||||
				flagManager:   fm.parser,
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			expectedResult := resource.Result{}
 | 
			
		||||
 | 
			
		||||
			// Call the method
 | 
			
		||||
			res, err := h.SaveGender(ctx, "save_gender", tt.input)
 | 
			
		||||
 | 
			
		||||
			expectedResult.FlagSet = []uint32{flag_gender_set}
 | 
			
		||||
 | 
			
		||||
			// Assert results
 | 
			
		||||
			assert.NoError(t, err)
 | 
			
		||||
			assert.Equal(t, expectedResult, res)
 | 
			
		||||
			assert.Equal(t, resource.Result{}, res)
 | 
			
		||||
 | 
			
		||||
			// Verify that the DATA_GENDER entry has been updated with the temporary value
 | 
			
		||||
			storedGender, _ := store.ReadEntry(ctx, sessionId, common.DATA_GENDER)
 | 
			
		||||
@ -775,11 +624,6 @@ func TestSetLanguage(t *testing.T) {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sessionId := "session123"
 | 
			
		||||
	ctx, store := InitializeTestStore(t)
 | 
			
		||||
 | 
			
		||||
	ctx = context.WithValue(ctx, "SessionId", sessionId)
 | 
			
		||||
 | 
			
		||||
	// Define test cases
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name           string
 | 
			
		||||
@ -812,13 +656,12 @@ func TestSetLanguage(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
			// Create the Handlers instance with the mock flag manager
 | 
			
		||||
			h := &Handlers{
 | 
			
		||||
				flagManager:   fm.parser,
 | 
			
		||||
				userdataStore: store,
 | 
			
		||||
				st:            mockState,
 | 
			
		||||
				flagManager: fm.parser,
 | 
			
		||||
				st:          mockState,
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Call the method
 | 
			
		||||
			res, err := h.SetLanguage(ctx, "set_language", nil)
 | 
			
		||||
			res, err := h.SetLanguage(context.Background(), "set_language", nil)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Error(err)
 | 
			
		||||
			}
 | 
			
		||||
@ -914,79 +757,37 @@ func TestResetAccountAuthorized(t *testing.T) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestIncorrectPinReset(t *testing.T) {
 | 
			
		||||
	sessionId := "session123"
 | 
			
		||||
	ctx, store := InitializeTestStore(t)
 | 
			
		||||
	fm, err := NewFlagManager(flagsPath)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	flag_incorrect_pin, _ := fm.parser.GetFlag("flag_incorrect_pin")
 | 
			
		||||
	flag_account_blocked, _ := fm.parser.GetFlag("flag_account_blocked")
 | 
			
		||||
 | 
			
		||||
	ctx = context.WithValue(ctx, "SessionId", sessionId)
 | 
			
		||||
 | 
			
		||||
	// Define test cases
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name           string
 | 
			
		||||
		input          []byte
 | 
			
		||||
		attempts       uint8
 | 
			
		||||
		expectedResult resource.Result
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:  "Test when incorrect PIN attempts is 2",
 | 
			
		||||
			name:  "Test incorrect pin reset",
 | 
			
		||||
			input: []byte(""),
 | 
			
		||||
			expectedResult: resource.Result{
 | 
			
		||||
				FlagReset: []uint32{flag_incorrect_pin},
 | 
			
		||||
				Content:   "1", //Expected remaining PIN attempts
 | 
			
		||||
			},
 | 
			
		||||
			attempts: 2,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "Test incorrect pin reset when incorrect PIN attempts is 1",
 | 
			
		||||
			input: []byte(""),
 | 
			
		||||
			expectedResult: resource.Result{
 | 
			
		||||
				FlagReset: []uint32{flag_incorrect_pin},
 | 
			
		||||
				Content:   "2", //Expected remaining PIN attempts
 | 
			
		||||
			},
 | 
			
		||||
			attempts: 1,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "Test incorrect pin reset when incorrect PIN attempts is 1",
 | 
			
		||||
			input: []byte(""),
 | 
			
		||||
			expectedResult: resource.Result{
 | 
			
		||||
				FlagReset: []uint32{flag_incorrect_pin},
 | 
			
		||||
				Content:   "2", //Expected remaining PIN attempts
 | 
			
		||||
			},
 | 
			
		||||
			attempts: 1,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "Test incorrect pin reset when incorrect PIN attempts is 3(account expected to be blocked)",
 | 
			
		||||
			input: []byte(""),
 | 
			
		||||
			expectedResult: resource.Result{
 | 
			
		||||
				FlagReset: []uint32{flag_incorrect_pin},
 | 
			
		||||
				FlagSet:   []uint32{flag_account_blocked},
 | 
			
		||||
			},
 | 
			
		||||
			attempts: 3,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
			if err := store.WriteEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS, []byte(strconv.Itoa(int(tt.attempts)))); err != nil {
 | 
			
		||||
				t.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Create the Handlers instance with the mock flag manager
 | 
			
		||||
			h := &Handlers{
 | 
			
		||||
				flagManager:   fm.parser,
 | 
			
		||||
				userdataStore: store,
 | 
			
		||||
				flagManager: fm.parser,
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Call the method
 | 
			
		||||
			res, err := h.ResetIncorrectPin(ctx, "reset_incorrect_pin", tt.input)
 | 
			
		||||
			res, err := h.ResetIncorrectPin(context.Background(), "reset_incorrect_pin", tt.input)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Error(err)
 | 
			
		||||
			}
 | 
			
		||||
@ -1096,14 +897,7 @@ func TestAuthorize(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			// Hash the PIN
 | 
			
		||||
			hashedPIN, err := common.HashPIN(accountPIN)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logg.ErrorCtxf(ctx, "failed to hash temporaryPin", "error", err)
 | 
			
		||||
				t.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(hashedPIN))
 | 
			
		||||
			err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(accountPIN))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
@ -1555,6 +1349,59 @@ func TestQuit(t *testing.T) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestIsValidPIN(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name     string
 | 
			
		||||
		pin      string
 | 
			
		||||
		expected bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:     "Valid PIN with 4 digits",
 | 
			
		||||
			pin:      "1234",
 | 
			
		||||
			expected: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "Valid PIN with leading zeros",
 | 
			
		||||
			pin:      "0001",
 | 
			
		||||
			expected: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "Invalid PIN with less than 4 digits",
 | 
			
		||||
			pin:      "123",
 | 
			
		||||
			expected: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "Invalid PIN with more than 4 digits",
 | 
			
		||||
			pin:      "12345",
 | 
			
		||||
			expected: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "Invalid PIN with letters",
 | 
			
		||||
			pin:      "abcd",
 | 
			
		||||
			expected: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "Invalid PIN with special characters",
 | 
			
		||||
			pin:      "12@#",
 | 
			
		||||
			expected: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "Empty PIN",
 | 
			
		||||
			pin:      "",
 | 
			
		||||
			expected: false,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			actual := isValidPIN(tt.pin)
 | 
			
		||||
			if actual != tt.expected {
 | 
			
		||||
				t.Errorf("isValidPIN(%q) = %v; expected %v", tt.pin, actual, tt.expected)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestValidateAmount(t *testing.T) {
 | 
			
		||||
	fm, err := NewFlagManager(flagsPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@ -1651,10 +1498,10 @@ func TestValidateRecipient(t *testing.T) {
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:  "Test with invalid recepient",
 | 
			
		||||
			input: []byte("7?1234"),
 | 
			
		||||
			input: []byte("9234adf5"),
 | 
			
		||||
			expectedResult: resource.Result{
 | 
			
		||||
				FlagSet: []uint32{flag_invalid_recipient},
 | 
			
		||||
				Content: "7?1234",
 | 
			
		||||
				Content: "9234adf5",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
@ -1670,40 +1517,22 @@ func TestValidateRecipient(t *testing.T) {
 | 
			
		||||
			input:          []byte("0711223344"),
 | 
			
		||||
			expectedResult: resource.Result{},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:           "Test with address",
 | 
			
		||||
			input:          []byte("0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9"),
 | 
			
		||||
			expectedResult: resource.Result{},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:           "Test with alias recepient",
 | 
			
		||||
			input:          []byte("alias123"),
 | 
			
		||||
			expectedResult: resource.Result{},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// store a public key for the valid recipient
 | 
			
		||||
	err = store.WriteEntry(ctx, "+254711223344", common.DATA_PUBLIC_KEY, []byte(publicKey))
 | 
			
		||||
	err = store.WriteEntry(ctx, "0711223344", common.DATA_PUBLIC_KEY, []byte(publicKey))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			mockAccountService := new(mocks.MockAccountService)
 | 
			
		||||
			// Create the Handlers instance
 | 
			
		||||
			h := &Handlers{
 | 
			
		||||
				flagManager:    fm.parser,
 | 
			
		||||
				userdataStore:  store,
 | 
			
		||||
				accountService: mockAccountService,
 | 
			
		||||
				flagManager:   fm.parser,
 | 
			
		||||
				userdataStore: store,
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			aliasResponse := &dataserviceapi.AliasAddress{
 | 
			
		||||
				Address: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9",
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			mockAccountService.On("CheckAliasAddress", string(tt.input)).Return(aliasResponse, nil)
 | 
			
		||||
 | 
			
		||||
			// Call the method
 | 
			
		||||
			res, err := h.ValidateRecipient(ctx, "validate_recepient", tt.input)
 | 
			
		||||
 | 
			
		||||
@ -1735,7 +1564,7 @@ func TestCheckBalance(t *testing.T) {
 | 
			
		||||
			publicKey:      "0X98765432109",
 | 
			
		||||
			activeSym:      "ETH",
 | 
			
		||||
			activeBal:      "1.5",
 | 
			
		||||
			expectedResult: resource.Result{Content: "Balance: 1.50 ETH\n"},
 | 
			
		||||
			expectedResult: resource.Result{Content: "Balance: 1.5 ETH\n"},
 | 
			
		||||
			expectError:    false,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
@ -1801,7 +1630,7 @@ func TestGetProfile(t *testing.T) {
 | 
			
		||||
			result: resource.Result{
 | 
			
		||||
				Content: fmt.Sprintf(
 | 
			
		||||
					"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
 | 
			
		||||
					"John Doee", "Male", "49", "Kilifi", "Bananas",
 | 
			
		||||
					"John Doee", "Male", "48", "Kilifi", "Bananas",
 | 
			
		||||
				),
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
@ -1813,7 +1642,7 @@ func TestGetProfile(t *testing.T) {
 | 
			
		||||
			result: resource.Result{
 | 
			
		||||
				Content: fmt.Sprintf(
 | 
			
		||||
					"Jina: %s\nJinsia: %s\nUmri: %s\nEneo: %s\nUnauza: %s\n",
 | 
			
		||||
					"John Doee", "Male", "49", "Kilifi", "Bananas",
 | 
			
		||||
					"John Doee", "Male", "48", "Kilifi", "Bananas",
 | 
			
		||||
				),
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
@ -1825,7 +1654,7 @@ func TestGetProfile(t *testing.T) {
 | 
			
		||||
			result: resource.Result{
 | 
			
		||||
				Content: fmt.Sprintf(
 | 
			
		||||
					"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
 | 
			
		||||
					"John Doee", "Male", "49", "Kilifi", "Bananas",
 | 
			
		||||
					"John Doee", "Male", "48", "Kilifi", "Bananas",
 | 
			
		||||
				),
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
@ -2090,7 +1919,7 @@ func TestCheckVouchers(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	// Read voucher sym data from the store
 | 
			
		||||
	voucherData, err := spdb.Get(ctx, common.ToBytes(common.DATA_VOUCHER_SYMBOLS))
 | 
			
		||||
	voucherData, err := spdb.Get(ctx, []byte("sym"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
@ -2103,31 +1932,26 @@ func TestCheckVouchers(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
func TestGetVoucherList(t *testing.T) {
 | 
			
		||||
	sessionId := "session123"
 | 
			
		||||
 | 
			
		||||
	ctx := context.WithValue(context.Background(), "SessionId", sessionId)
 | 
			
		||||
 | 
			
		||||
	spdb := InitializeTestSubPrefixDb(t, ctx)
 | 
			
		||||
 | 
			
		||||
	// Initialize Handlers
 | 
			
		||||
	h := &Handlers{
 | 
			
		||||
		prefixDb:             spdb,
 | 
			
		||||
		ReplaceSeparatorFunc: mockReplaceSeparator,
 | 
			
		||||
		prefixDb: spdb,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	mockSyms := []byte("1:SRF\n2:MILO")
 | 
			
		||||
	expectedSym := []byte("1:SRF\n2:MILO")
 | 
			
		||||
 | 
			
		||||
	// Put voucher sym data from the store
 | 
			
		||||
	err := spdb.Put(ctx, common.ToBytes(common.DATA_VOUCHER_SYMBOLS), mockSyms)
 | 
			
		||||
	err := spdb.Put(ctx, []byte("sym"), expectedSym)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expectedSyms := []byte("1: SRF\n2: MILO")
 | 
			
		||||
 | 
			
		||||
	res, err := h.GetVoucherList(ctx, "", []byte(""))
 | 
			
		||||
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Equal(t, res.Content, string(expectedSyms))
 | 
			
		||||
	assert.Equal(t, res.Content, string(expectedSym))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestViewVoucher(t *testing.T) {
 | 
			
		||||
@ -2149,16 +1973,16 @@ func TestViewVoucher(t *testing.T) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Define mock voucher data
 | 
			
		||||
	mockData := map[common.DataTyp][]byte{
 | 
			
		||||
		common.DATA_VOUCHER_SYMBOLS:   []byte("1:SRF\n2:MILO"),
 | 
			
		||||
		common.DATA_VOUCHER_BALANCES:  []byte("1:100\n2:200"),
 | 
			
		||||
		common.DATA_VOUCHER_DECIMALS:  []byte("1:6\n2:4"),
 | 
			
		||||
		common.DATA_VOUCHER_ADDRESSES: []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"),
 | 
			
		||||
	mockData := map[string][]byte{
 | 
			
		||||
		"sym":  []byte("1:SRF\n2:MILO"),
 | 
			
		||||
		"bal":  []byte("1:100\n2:200"),
 | 
			
		||||
		"deci": []byte("1:6\n2:4"),
 | 
			
		||||
		"addr": []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Put the data
 | 
			
		||||
	for key, value := range mockData {
 | 
			
		||||
		err = spdb.Put(ctx, []byte(common.ToBytes(key)), []byte(value))
 | 
			
		||||
		err = spdb.Put(ctx, []byte(key), []byte(value))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
@ -2166,7 +1990,7 @@ func TestViewVoucher(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
	res, err := h.ViewVoucher(ctx, "view_voucher", []byte("1"))
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Equal(t, res.Content, "Symbol: SRF\nBalance: 100")
 | 
			
		||||
	assert.Equal(t, res.Content, "SRF\n100")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestSetVoucher(t *testing.T) {
 | 
			
		||||
@ -2239,93 +2063,3 @@ func TestGetVoucherDetails(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Equal(t, expectedResult, res)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestCountIncorrectPINAttempts(t *testing.T) {
 | 
			
		||||
	ctx, store := InitializeTestStore(t)
 | 
			
		||||
	sessionId := "session123"
 | 
			
		||||
	ctx = context.WithValue(ctx, "SessionId", sessionId)
 | 
			
		||||
	attempts := uint8(2)
 | 
			
		||||
 | 
			
		||||
	h := &Handlers{
 | 
			
		||||
		userdataStore: store,
 | 
			
		||||
	}
 | 
			
		||||
	err := store.WriteEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS, []byte(strconv.Itoa(int(attempts))))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Logf(err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	err = h.incrementIncorrectPINAttempts(ctx, sessionId)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Logf(err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	attemptsAfterCount, err := store.ReadEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Logf(err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	pinAttemptsValue, _ := strconv.ParseUint(string(attemptsAfterCount), 0, 64)
 | 
			
		||||
	pinAttemptsCount := uint8(pinAttemptsValue)
 | 
			
		||||
	expectedAttempts := attempts + 1
 | 
			
		||||
	assert.Equal(t, pinAttemptsCount, expectedAttempts)
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestResetIncorrectPINAttempts(t *testing.T) {
 | 
			
		||||
	ctx, store := InitializeTestStore(t)
 | 
			
		||||
	sessionId := "session123"
 | 
			
		||||
	ctx = context.WithValue(ctx, "SessionId", sessionId)
 | 
			
		||||
 | 
			
		||||
	err := store.WriteEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS, []byte(string("2")))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Logf(err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h := &Handlers{
 | 
			
		||||
		userdataStore: store,
 | 
			
		||||
	}
 | 
			
		||||
	h.resetIncorrectPINAttempts(ctx, sessionId)
 | 
			
		||||
	incorrectAttempts, err := store.ReadEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Logf(err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	assert.Equal(t, "0", string(incorrectAttempts))
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestPersistLanguageCode(t *testing.T) {
 | 
			
		||||
	ctx, store := InitializeTestStore(t)
 | 
			
		||||
 | 
			
		||||
	sessionId := "session123"
 | 
			
		||||
	ctx = context.WithValue(ctx, "SessionId", sessionId)
 | 
			
		||||
 | 
			
		||||
	h := &Handlers{
 | 
			
		||||
		userdataStore: store,
 | 
			
		||||
	}
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name                 string
 | 
			
		||||
		code                 string
 | 
			
		||||
		expectedLanguageCode string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:                 "Set Default Language (English)",
 | 
			
		||||
			code:                 "eng",
 | 
			
		||||
			expectedLanguageCode: "eng",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:                 "Set Swahili Language",
 | 
			
		||||
			code:                 "swa",
 | 
			
		||||
			expectedLanguageCode: "swa",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, test := range tests {
 | 
			
		||||
		err := h.persistLanguageCode(ctx, test.code)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Logf(err.Error())
 | 
			
		||||
		}
 | 
			
		||||
		code, err := store.ReadEntry(ctx, sessionId, common.DATA_SELECTED_LANGUAGE_CODE)
 | 
			
		||||
 | 
			
		||||
		assert.Equal(t, test.expectedLanguageCode, string(code))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -1,120 +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")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	trimmedInput := strings.TrimSpace(parts[len(parts)-1])
 | 
			
		||||
	return []byte(trimmedInput), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func parseQueryParams(query string) map[string]string {
 | 
			
		||||
	params := make(map[string]string)
 | 
			
		||||
 | 
			
		||||
	queryParams := strings.Split(query, "&")
 | 
			
		||||
	for _, param := range queryParams {
 | 
			
		||||
		// Split each key-value pair by '='
 | 
			
		||||
		parts := strings.SplitN(param, "=", 2)
 | 
			
		||||
		if len(parts) == 2 {
 | 
			
		||||
			params[parts[0]] = parts[1]
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return params
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func extractATSessionId(decodedStr string) (string, error) {
 | 
			
		||||
	var data map[string]string
 | 
			
		||||
	err := json.Unmarshal([]byte(decodedStr), &data)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logg.Errorf("Error unmarshalling JSON: %v", err)
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
	decodedBody, err := url.QueryUnescape(data["body"])
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logg.Errorf("Error URL-decoding body: %v", err)
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
	params := parseQueryParams(decodedBody)
 | 
			
		||||
 | 
			
		||||
	sessionId := params["sessionId"]
 | 
			
		||||
	return sessionId, nil
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -1,25 +1,19 @@
 | 
			
		||||
package at
 | 
			
		||||
package http
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
	*SessionHandler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewATSessionHandler(h handlers.RequestHandler) *ATSessionHandler {
 | 
			
		||||
	return &ATSessionHandler{
 | 
			
		||||
		SessionHandler: httpserver.ToSessionHandler(h),
 | 
			
		||||
		SessionHandler: ToSessionHandler(h),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -34,17 +28,17 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
 | 
			
		||||
 | 
			
		||||
	rp := ash.GetRequestParser()
 | 
			
		||||
	cfg := ash.GetConfig()
 | 
			
		||||
	cfg.SessionId, err = rp.GetSessionId(req.Context(), req)
 | 
			
		||||
	cfg.SessionId, err = rp.GetSessionId(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
 | 
			
		||||
		ash.WriteError(w, 400, 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)
 | 
			
		||||
		ash.writeError(w, 400, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -59,7 +53,7 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if code != 200 {
 | 
			
		||||
		ash.WriteError(w, 500, err)
 | 
			
		||||
		ash.writeError(w, 500, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -67,13 +61,13 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
 | 
			
		||||
	w.Header().Set("Content-Type", "text/plain")
 | 
			
		||||
	rqs, err = ash.Output(rqs)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ash.WriteError(w, 500, err)
 | 
			
		||||
		ash.writeError(w, 500, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rqs, err = ash.Reset(rqs)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ash.WriteError(w, 500, err)
 | 
			
		||||
		ash.writeError(w, 500, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
package at
 | 
			
		||||
package http
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
@ -15,6 +16,16 @@ import (
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// invalidRequestType is a custom type to test invalid request scenarios
 | 
			
		||||
type invalidRequestType struct{}
 | 
			
		||||
 | 
			
		||||
// errorReader is a helper type that always returns an error when Read is called
 | 
			
		||||
type errorReader struct{}
 | 
			
		||||
 | 
			
		||||
func (e *errorReader) Read(p []byte) (n int, err error) {
 | 
			
		||||
	return 0, errors.New("read error")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestNewATSessionHandler(t *testing.T) {
 | 
			
		||||
	mockHandler := &httpmocks.MockRequestHandler{}
 | 
			
		||||
	ash := NewATSessionHandler(mockHandler)
 | 
			
		||||
@ -231,4 +242,208 @@ func TestATSessionHandler_Output(t *testing.T) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestSessionHandler_ServeHTTP(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name           string
 | 
			
		||||
		sessionID      string
 | 
			
		||||
		input          []byte
 | 
			
		||||
		parserErr      error
 | 
			
		||||
		processErr     error
 | 
			
		||||
		outputErr      error
 | 
			
		||||
		resetErr       error
 | 
			
		||||
		expectedStatus int
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:           "Success",
 | 
			
		||||
			sessionID:      "123",
 | 
			
		||||
			input:          []byte("test input"),
 | 
			
		||||
			expectedStatus: http.StatusOK,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:           "Missing Session ID",
 | 
			
		||||
			sessionID:      "",
 | 
			
		||||
			parserErr:      handlers.ErrSessionMissing,
 | 
			
		||||
			expectedStatus: http.StatusBadRequest,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:           "Process Error",
 | 
			
		||||
			sessionID:      "123",
 | 
			
		||||
			input:          []byte("test input"),
 | 
			
		||||
			processErr:     handlers.ErrStorage,
 | 
			
		||||
			expectedStatus: http.StatusInternalServerError,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:           "Output Error",
 | 
			
		||||
			sessionID:      "123",
 | 
			
		||||
			input:          []byte("test input"),
 | 
			
		||||
			outputErr:      errors.New("output error"),
 | 
			
		||||
			expectedStatus: http.StatusOK,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:           "Reset Error",
 | 
			
		||||
			sessionID:      "123",
 | 
			
		||||
			input:          []byte("test input"),
 | 
			
		||||
			resetErr:       errors.New("reset error"),
 | 
			
		||||
			expectedStatus: http.StatusOK,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			mockRequestParser := &httpmocks.MockRequestParser{
 | 
			
		||||
				GetSessionIdFunc: func(any) (string, error) {
 | 
			
		||||
					return tt.sessionID, tt.parserErr
 | 
			
		||||
				},
 | 
			
		||||
				GetInputFunc: func(any) ([]byte, error) {
 | 
			
		||||
					return tt.input, nil
 | 
			
		||||
				},
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			mockRequestHandler := &httpmocks.MockRequestHandler{
 | 
			
		||||
				ProcessFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
 | 
			
		||||
					return rs, tt.processErr
 | 
			
		||||
				},
 | 
			
		||||
				OutputFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
 | 
			
		||||
					return rs, tt.outputErr
 | 
			
		||||
				},
 | 
			
		||||
				ResetFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
 | 
			
		||||
					return rs, tt.resetErr
 | 
			
		||||
				},
 | 
			
		||||
				GetRequestParserFunc: func() handlers.RequestParser {
 | 
			
		||||
					return mockRequestParser
 | 
			
		||||
				},
 | 
			
		||||
				GetConfigFunc: func() engine.Config {
 | 
			
		||||
					return engine.Config{}
 | 
			
		||||
				},
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			sessionHandler := ToSessionHandler(mockRequestHandler)
 | 
			
		||||
 | 
			
		||||
			req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.input))
 | 
			
		||||
			req.Header.Set("X-Vise-Session", tt.sessionID)
 | 
			
		||||
 | 
			
		||||
			rr := httptest.NewRecorder()
 | 
			
		||||
 | 
			
		||||
			sessionHandler.ServeHTTP(rr, req)
 | 
			
		||||
 | 
			
		||||
			if status := rr.Code; status != tt.expectedStatus {
 | 
			
		||||
				t.Errorf("handler returned wrong status code: got %v want %v",
 | 
			
		||||
					status, tt.expectedStatus)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestSessionHandler_writeError(t *testing.T) {
 | 
			
		||||
	handler := &SessionHandler{}
 | 
			
		||||
	mockWriter := &httpmocks.MockWriter{}
 | 
			
		||||
	err := errors.New("test error")
 | 
			
		||||
 | 
			
		||||
	handler.writeError(mockWriter, http.StatusBadRequest, err)
 | 
			
		||||
 | 
			
		||||
	if mockWriter.WrittenString != "" {
 | 
			
		||||
		t.Errorf("Expected empty body, got %s", mockWriter.WrittenString)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDefaultRequestParser_GetSessionId(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name          string
 | 
			
		||||
		request       any
 | 
			
		||||
		expectedID    string
 | 
			
		||||
		expectedError error
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "Valid Session ID",
 | 
			
		||||
			request: func() *http.Request {
 | 
			
		||||
				req := httptest.NewRequest(http.MethodPost, "/", nil)
 | 
			
		||||
				req.Header.Set("X-Vise-Session", "123456")
 | 
			
		||||
				return req
 | 
			
		||||
			}(),
 | 
			
		||||
			expectedID:    "123456",
 | 
			
		||||
			expectedError: nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:          "Missing Session ID",
 | 
			
		||||
			request:       httptest.NewRequest(http.MethodPost, "/", nil),
 | 
			
		||||
			expectedID:    "",
 | 
			
		||||
			expectedError: handlers.ErrSessionMissing,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:          "Invalid Request Type",
 | 
			
		||||
			request:       invalidRequestType{},
 | 
			
		||||
			expectedID:    "",
 | 
			
		||||
			expectedError: handlers.ErrInvalidRequest,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	parser := &DefaultRequestParser{}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			id, err := parser.GetSessionId(tt.request)
 | 
			
		||||
 | 
			
		||||
			if id != tt.expectedID {
 | 
			
		||||
				t.Errorf("Expected session ID %s, got %s", tt.expectedID, id)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if err != tt.expectedError {
 | 
			
		||||
				t.Errorf("Expected error %v, got %v", tt.expectedError, err)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDefaultRequestParser_GetInput(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name          string
 | 
			
		||||
		request       any
 | 
			
		||||
		expectedInput []byte
 | 
			
		||||
		expectedError error
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "Valid Input",
 | 
			
		||||
			request: func() *http.Request {
 | 
			
		||||
				return httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString("test input"))
 | 
			
		||||
			}(),
 | 
			
		||||
			expectedInput: []byte("test input"),
 | 
			
		||||
			expectedError: nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:          "Empty Input",
 | 
			
		||||
			request:       httptest.NewRequest(http.MethodPost, "/", nil),
 | 
			
		||||
			expectedInput: []byte{},
 | 
			
		||||
			expectedError: nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:          "Invalid Request Type",
 | 
			
		||||
			request:       invalidRequestType{},
 | 
			
		||||
			expectedInput: nil,
 | 
			
		||||
			expectedError: handlers.ErrInvalidRequest,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Read Error",
 | 
			
		||||
			request: func() *http.Request {
 | 
			
		||||
				return httptest.NewRequest(http.MethodPost, "/", &errorReader{})
 | 
			
		||||
			}(),
 | 
			
		||||
			expectedInput: nil,
 | 
			
		||||
			expectedError: errors.New("read error"),
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	parser := &DefaultRequestParser{}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			input, err := parser.GetInput(tt.request)
 | 
			
		||||
 | 
			
		||||
			if !bytes.Equal(input, tt.expectedInput) {
 | 
			
		||||
				t.Errorf("Expected input %s, got %s", tt.expectedInput, input)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if err != tt.expectedError && (err == nil || err.Error() != tt.expectedError.Error()) {
 | 
			
		||||
				t.Errorf("Expected error %v, got %v", tt.expectedError, err)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,37 +0,0 @@
 | 
			
		||||
package http
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type DefaultRequestParser struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (rp *DefaultRequestParser) GetSessionId(ctx context.Context, rq any) (string, error) {
 | 
			
		||||
	rqv, ok := rq.(*http.Request)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return "", handlers.ErrInvalidRequest
 | 
			
		||||
	}
 | 
			
		||||
	v := rqv.Header.Get("X-Vise-Session")
 | 
			
		||||
	if v == "" {
 | 
			
		||||
		return "", handlers.ErrSessionMissing
 | 
			
		||||
	}
 | 
			
		||||
	return v, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) {
 | 
			
		||||
	rqv, ok := rq.(*http.Request)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return nil, handlers.ErrInvalidRequest
 | 
			
		||||
	}
 | 
			
		||||
	defer rqv.Body.Close()
 | 
			
		||||
	v, err := ioutil.ReadAll(rqv.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return v, nil
 | 
			
		||||
}
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
package http
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
@ -13,6 +14,35 @@ var (
 | 
			
		||||
	logg = logging.NewVanilla().WithDomain("httpserver")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type DefaultRequestParser struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func(rp *DefaultRequestParser) GetSessionId(rq any) (string, error) {
 | 
			
		||||
	rqv, ok := rq.(*http.Request)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return "", handlers.ErrInvalidRequest
 | 
			
		||||
	}
 | 
			
		||||
	v := rqv.Header.Get("X-Vise-Session")
 | 
			
		||||
	if v == "" {
 | 
			
		||||
		return "", handlers.ErrSessionMissing
 | 
			
		||||
	}
 | 
			
		||||
	return v, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) {
 | 
			
		||||
	rqv, ok := rq.(*http.Request)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return nil, handlers.ErrInvalidRequest
 | 
			
		||||
	}
 | 
			
		||||
	defer rqv.Body.Close()
 | 
			
		||||
	v, err := ioutil.ReadAll(rqv.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return v, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SessionHandler struct {
 | 
			
		||||
	handlers.RequestHandler
 | 
			
		||||
}
 | 
			
		||||
@ -23,39 +53,40 @@ func ToSessionHandler(h handlers.RequestHandler) *SessionHandler {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *SessionHandler) WriteError(w http.ResponseWriter, code int, err error) {
 | 
			
		||||
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))
 | 
			
		||||
	_, err = w.Write([]byte{})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logg.Errorf("error writing error!!", "err", err, "olderr", s)
 | 
			
		||||
		w.WriteHeader(500)
 | 
			
		||||
	}
 | 
			
		||||
	return 
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 | 
			
		||||
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(),
 | 
			
		||||
		Ctx: req.Context(),
 | 
			
		||||
		Writer: w,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rp := f.GetRequestParser()
 | 
			
		||||
	cfg := f.GetConfig()
 | 
			
		||||
	cfg.SessionId, err = rp.GetSessionId(req.Context(), req)
 | 
			
		||||
	cfg.SessionId, err = rp.GetSessionId(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
 | 
			
		||||
		f.WriteError(w, 400, 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)
 | 
			
		||||
		f.writeError(w, 400, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -72,7 +103,7 @@ func (f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if code != 200 {
 | 
			
		||||
		f.WriteError(w, 500, err)
 | 
			
		||||
		f.writeError(w, 500, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -81,11 +112,11 @@ func (f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 | 
			
		||||
	rqs, err = f.Output(rqs)
 | 
			
		||||
	rqs, perr = f.Reset(rqs)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		f.WriteError(w, 500, err)
 | 
			
		||||
		f.writeError(w, 500, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if perr != nil {
 | 
			
		||||
		f.WriteError(w, 500, perr)
 | 
			
		||||
		f.writeError(w, 500, perr)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,230 +0,0 @@
 | 
			
		||||
package http
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/http/httptest"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// invalidRequestType is a custom type to test invalid request scenarios
 | 
			
		||||
type invalidRequestType struct{}
 | 
			
		||||
 | 
			
		||||
// errorReader is a helper type that always returns an error when Read is called
 | 
			
		||||
type errorReader struct{}
 | 
			
		||||
 | 
			
		||||
func (e *errorReader) Read(p []byte) (n int, err error) {
 | 
			
		||||
	return 0, errors.New("read error")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestSessionHandler_ServeHTTP(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name           string
 | 
			
		||||
		sessionID      string
 | 
			
		||||
		input          []byte
 | 
			
		||||
		parserErr      error
 | 
			
		||||
		processErr     error
 | 
			
		||||
		outputErr      error
 | 
			
		||||
		resetErr       error
 | 
			
		||||
		expectedStatus int
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:           "Success",
 | 
			
		||||
			sessionID:      "123",
 | 
			
		||||
			input:          []byte("test input"),
 | 
			
		||||
			expectedStatus: http.StatusOK,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:           "Missing Session ID",
 | 
			
		||||
			sessionID:      "",
 | 
			
		||||
			parserErr:      handlers.ErrSessionMissing,
 | 
			
		||||
			expectedStatus: http.StatusBadRequest,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:           "Process Error",
 | 
			
		||||
			sessionID:      "123",
 | 
			
		||||
			input:          []byte("test input"),
 | 
			
		||||
			processErr:     handlers.ErrStorage,
 | 
			
		||||
			expectedStatus: http.StatusInternalServerError,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:           "Output Error",
 | 
			
		||||
			sessionID:      "123",
 | 
			
		||||
			input:          []byte("test input"),
 | 
			
		||||
			outputErr:      errors.New("output error"),
 | 
			
		||||
			expectedStatus: http.StatusOK,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:           "Reset Error",
 | 
			
		||||
			sessionID:      "123",
 | 
			
		||||
			input:          []byte("test input"),
 | 
			
		||||
			resetErr:       errors.New("reset error"),
 | 
			
		||||
			expectedStatus: http.StatusOK,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			mockRequestParser := &httpmocks.MockRequestParser{
 | 
			
		||||
				GetSessionIdFunc: func(any) (string, error) {
 | 
			
		||||
					return tt.sessionID, tt.parserErr
 | 
			
		||||
				},
 | 
			
		||||
				GetInputFunc: func(any) ([]byte, error) {
 | 
			
		||||
					return tt.input, nil
 | 
			
		||||
				},
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			mockRequestHandler := &httpmocks.MockRequestHandler{
 | 
			
		||||
				ProcessFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
 | 
			
		||||
					return rs, tt.processErr
 | 
			
		||||
				},
 | 
			
		||||
				OutputFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
 | 
			
		||||
					return rs, tt.outputErr
 | 
			
		||||
				},
 | 
			
		||||
				ResetFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
 | 
			
		||||
					return rs, tt.resetErr
 | 
			
		||||
				},
 | 
			
		||||
				GetRequestParserFunc: func() handlers.RequestParser {
 | 
			
		||||
					return mockRequestParser
 | 
			
		||||
				},
 | 
			
		||||
				GetConfigFunc: func() engine.Config {
 | 
			
		||||
					return engine.Config{}
 | 
			
		||||
				},
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			sessionHandler := ToSessionHandler(mockRequestHandler)
 | 
			
		||||
 | 
			
		||||
			req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.input))
 | 
			
		||||
			req.Header.Set("X-Vise-Session", tt.sessionID)
 | 
			
		||||
 | 
			
		||||
			rr := httptest.NewRecorder()
 | 
			
		||||
 | 
			
		||||
			sessionHandler.ServeHTTP(rr, req)
 | 
			
		||||
 | 
			
		||||
			if status := rr.Code; status != tt.expectedStatus {
 | 
			
		||||
				t.Errorf("handler returned wrong status code: got %v want %v",
 | 
			
		||||
					status, tt.expectedStatus)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestSessionHandler_WriteError(t *testing.T) {
 | 
			
		||||
	handler := &SessionHandler{}
 | 
			
		||||
	mockWriter := &httpmocks.MockWriter{}
 | 
			
		||||
	err := errors.New("test error")
 | 
			
		||||
 | 
			
		||||
	handler.WriteError(mockWriter, http.StatusBadRequest, err)
 | 
			
		||||
 | 
			
		||||
	if mockWriter.WrittenString != "" {
 | 
			
		||||
		t.Errorf("Expected empty body, got %s", mockWriter.WrittenString)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDefaultRequestParser_GetSessionId(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name          string
 | 
			
		||||
		request       any
 | 
			
		||||
		expectedID    string
 | 
			
		||||
		expectedError error
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "Valid Session ID",
 | 
			
		||||
			request: func() *http.Request {
 | 
			
		||||
				req := httptest.NewRequest(http.MethodPost, "/", nil)
 | 
			
		||||
				req.Header.Set("X-Vise-Session", "123456")
 | 
			
		||||
				return req
 | 
			
		||||
			}(),
 | 
			
		||||
			expectedID:    "123456",
 | 
			
		||||
			expectedError: nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:          "Missing Session ID",
 | 
			
		||||
			request:       httptest.NewRequest(http.MethodPost, "/", nil),
 | 
			
		||||
			expectedID:    "",
 | 
			
		||||
			expectedError: handlers.ErrSessionMissing,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:          "Invalid Request Type",
 | 
			
		||||
			request:       invalidRequestType{},
 | 
			
		||||
			expectedID:    "",
 | 
			
		||||
			expectedError: handlers.ErrInvalidRequest,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	parser := &DefaultRequestParser{}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			id, err := parser.GetSessionId(context.Background(),tt.request)
 | 
			
		||||
 | 
			
		||||
			if id != tt.expectedID {
 | 
			
		||||
				t.Errorf("Expected session ID %s, got %s", tt.expectedID, id)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if err != tt.expectedError {
 | 
			
		||||
				t.Errorf("Expected error %v, got %v", tt.expectedError, err)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDefaultRequestParser_GetInput(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name          string
 | 
			
		||||
		request       any
 | 
			
		||||
		expectedInput []byte
 | 
			
		||||
		expectedError error
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "Valid Input",
 | 
			
		||||
			request: func() *http.Request {
 | 
			
		||||
				return httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString("test input"))
 | 
			
		||||
			}(),
 | 
			
		||||
			expectedInput: []byte("test input"),
 | 
			
		||||
			expectedError: nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:          "Empty Input",
 | 
			
		||||
			request:       httptest.NewRequest(http.MethodPost, "/", nil),
 | 
			
		||||
			expectedInput: []byte{},
 | 
			
		||||
			expectedError: nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:          "Invalid Request Type",
 | 
			
		||||
			request:       invalidRequestType{},
 | 
			
		||||
			expectedInput: nil,
 | 
			
		||||
			expectedError: handlers.ErrInvalidRequest,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Read Error",
 | 
			
		||||
			request: func() *http.Request {
 | 
			
		||||
				return httptest.NewRequest(http.MethodPost, "/", &errorReader{})
 | 
			
		||||
			}(),
 | 
			
		||||
			expectedInput: nil,
 | 
			
		||||
			expectedError: errors.New("read error"),
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	parser := &DefaultRequestParser{}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			input, err := parser.GetInput(tt.request)
 | 
			
		||||
 | 
			
		||||
			if !bytes.Equal(input, tt.expectedInput) {
 | 
			
		||||
				t.Errorf("Expected input %s, got %s", tt.expectedInput, input)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if err != tt.expectedError && (err == nil || err.Error() != tt.expectedError.Error()) {
 | 
			
		||||
				t.Errorf("Expected error %v, got %v", tt.expectedError, err)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -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,284 +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) {
 | 
			
		||||
	logg.TraceCtxf(a.Ctx, "looking for publickey", "pubkey", fmt.Sprintf("%x", pubKey))
 | 
			
		||||
	va, err := a.keyStore.Get(a.Ctx, pubKey)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	ka := hex.EncodeToString(conn.SessionID())
 | 
			
		||||
	a.auth[ka] = va 
 | 
			
		||||
	fmt.Fprintf(os.Stderr, "connect: %s -> %s\n", ka, va)
 | 
			
		||||
	return nil, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(a *auther) FromConn(c *ssh.ServerConn) (string, error) {
 | 
			
		||||
	if c == nil {
 | 
			
		||||
		return "", errors.New("nil server conn")
 | 
			
		||||
	}
 | 
			
		||||
	if c.Conn == nil {
 | 
			
		||||
		return "", errors.New("nil underlying conn")
 | 
			
		||||
	}
 | 
			
		||||
	return a.Get(c.Conn.SessionID())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func(a *auther) Get(k []byte) (string, error) {
 | 
			
		||||
	ka := hex.EncodeToString(k)
 | 
			
		||||
	v, ok := a.auth[ka]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return "", errors.New("not found")
 | 
			
		||||
	}
 | 
			
		||||
	return v, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SshRunner struct {
 | 
			
		||||
	Ctx context.Context
 | 
			
		||||
	Cfg engine.Config
 | 
			
		||||
	FlagFile string
 | 
			
		||||
	Conn storage.ConnData
 | 
			
		||||
	ResourceDir string
 | 
			
		||||
	Debug bool
 | 
			
		||||
	SrvKeyFile string
 | 
			
		||||
	Host string
 | 
			
		||||
	Port uint
 | 
			
		||||
	wg sync.WaitGroup
 | 
			
		||||
	lst net.Listener
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(s *SshRunner) serve(ctx context.Context, sessionId string, ch ssh.NewChannel, en engine.Engine) error {
 | 
			
		||||
	if ch == nil {
 | 
			
		||||
		return errors.New("nil channel")
 | 
			
		||||
	}
 | 
			
		||||
	if ch.ChannelType() != "session" {
 | 
			
		||||
		ch.Reject(ssh.UnknownChannelType, "that is not the channel you are looking for")
 | 
			
		||||
		return errors.New("not a session")
 | 
			
		||||
	}
 | 
			
		||||
	channel, requests, err := ch.Accept()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
	defer channel.Close()
 | 
			
		||||
	s.wg.Add(1)
 | 
			
		||||
	go func(reqIn <-chan *ssh.Request) {
 | 
			
		||||
		defer s.wg.Done()
 | 
			
		||||
		for req := range reqIn {
 | 
			
		||||
			req.Reply(req.Type == "shell", nil)	
 | 
			
		||||
		}
 | 
			
		||||
		_ = requests
 | 
			
		||||
	}(requests)
 | 
			
		||||
 | 
			
		||||
	cont, err := en.Exec(ctx, []byte{})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("initial engine exec err: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var input [state.INPUT_LIMIT]byte
 | 
			
		||||
	for cont {
 | 
			
		||||
		c, err := en.Flush(ctx, channel)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("flush err: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
		_, err = channel.Write([]byte{0x0a})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("newline err: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
		c, err = channel.Read(input[:])
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("read input fail: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
		logg.TraceCtxf(ctx, "input read", "c", c, "input", input[:c-1])
 | 
			
		||||
		cont, err = en.Exec(ctx, input[:c-1])
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("engine exec err: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
		logg.TraceCtxf(ctx, "exec cont", "cont", cont, "en", en)
 | 
			
		||||
		_ = c
 | 
			
		||||
	}
 | 
			
		||||
	c, err := en.Flush(ctx, channel)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("last flush err: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	_ = c
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(s *SshRunner) Stop() error {
 | 
			
		||||
	return s.lst.Close()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(s *SshRunner) GetEngine(sessionId string) (engine.Engine, func(), error) {
 | 
			
		||||
	ctx := s.Ctx
 | 
			
		||||
	menuStorageService := storage.NewMenuStorageService(s.Conn, s.ResourceDir)
 | 
			
		||||
 | 
			
		||||
	rs, err := menuStorageService.GetResource(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pe, err := menuStorageService.GetPersister(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userdatastore, err := menuStorageService.GetUserdataDb(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dbResource, ok := rs.(*resource.DbResource)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	lhs, err := handlers.NewLocalHandlerService(ctx, s.FlagFile, true, dbResource, s.Cfg, rs)
 | 
			
		||||
	lhs.SetDataStore(&userdatastore)
 | 
			
		||||
	lhs.SetPersister(pe)
 | 
			
		||||
	lhs.Cfg.SessionId = sessionId
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO: clear up why pointer here and by-value other cmds
 | 
			
		||||
	accountService := &remote.AccountService{}
 | 
			
		||||
	hl, err := lhs.GetHandler(accountService)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	en := lhs.GetEngine()
 | 
			
		||||
	en = en.WithFirst(hl.Init)
 | 
			
		||||
	if s.Debug {
 | 
			
		||||
		en = en.WithDebug(nil)
 | 
			
		||||
	}
 | 
			
		||||
	// TODO: this is getting very hacky!
 | 
			
		||||
	closer := func() {
 | 
			
		||||
		err := menuStorageService.Close()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logg.ErrorCtxf(ctx, "menu storage service cleanup fail", "err", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return en, closer, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// adapted example from crypto/ssh package, NewServerConn doc
 | 
			
		||||
func(s *SshRunner) Run(ctx context.Context, keyStore *SshKeyStore) {
 | 
			
		||||
	s.Ctx = ctx
 | 
			
		||||
	running := true
 | 
			
		||||
 | 
			
		||||
	// TODO: waitgroup should probably not be global
 | 
			
		||||
	defer s.wg.Wait()
 | 
			
		||||
 | 
			
		||||
	auth := NewAuther(ctx, keyStore)
 | 
			
		||||
	cfg := ssh.ServerConfig{
 | 
			
		||||
		PublicKeyCallback: auth.Check,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	privateBytes, err := os.ReadFile(s.SrvKeyFile)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logg.ErrorCtxf(ctx, "Failed to load private key", "err", err)
 | 
			
		||||
	}
 | 
			
		||||
	private, err := ssh.ParsePrivateKey(privateBytes)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logg.ErrorCtxf(ctx, "Failed to parse private key", "err", err)
 | 
			
		||||
	}
 | 
			
		||||
	srvPub := private.PublicKey()
 | 
			
		||||
	srvPubStr := base64.StdEncoding.EncodeToString(srvPub.Marshal())
 | 
			
		||||
	logg.InfoCtxf(ctx, "have server key", "type", srvPub.Type(), "public", srvPubStr)
 | 
			
		||||
	cfg.AddHostKey(private)
 | 
			
		||||
 | 
			
		||||
	s.lst, err = net.Listen("tcp", fmt.Sprintf("%s:%d", s.Host, s.Port))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for running {
 | 
			
		||||
		conn, err := s.lst.Accept()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logg.ErrorCtxf(ctx, "ssh accept error", "err", err)
 | 
			
		||||
			running = false
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		go func(conn net.Conn) {
 | 
			
		||||
			defer conn.Close()
 | 
			
		||||
			for true {
 | 
			
		||||
				srvConn, nC, rC, err := ssh.NewServerConn(conn, &cfg)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					logg.InfoCtxf(ctx, "rejected client", "err", err)
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				logg.DebugCtxf(ctx, "ssh client connected", "conn", srvConn)
 | 
			
		||||
 | 
			
		||||
				s.wg.Add(1)
 | 
			
		||||
				go func() {
 | 
			
		||||
					ssh.DiscardRequests(rC)
 | 
			
		||||
					s.wg.Done()
 | 
			
		||||
				}()
 | 
			
		||||
				
 | 
			
		||||
				sessionId, err := auth.FromConn(srvConn)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					logg.ErrorCtxf(ctx, "Cannot find authentication")
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				en, closer, err := s.GetEngine(sessionId)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					logg.ErrorCtxf(ctx, "engine won't start", "err", err)
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				defer func() {
 | 
			
		||||
					err := en.Finish()
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						logg.ErrorCtxf(ctx, "engine won't stop", "err", err)
 | 
			
		||||
					}
 | 
			
		||||
					closer()
 | 
			
		||||
				}()
 | 
			
		||||
				for ch := range nC {
 | 
			
		||||
					err = s.serve(ctx, sessionId, ch, en)
 | 
			
		||||
					logg.ErrorCtxf(ctx, "ssh server finish", "err", err)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}(conn)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -6,6 +6,10 @@ import (
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	DATATYPE_USERSUB = 64
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// PrefixDb interface abstracts the database operations.
 | 
			
		||||
type PrefixDb interface {
 | 
			
		||||
	Get(ctx context.Context, key []byte) ([]byte, error)
 | 
			
		||||
@ -31,13 +35,13 @@ func (s *SubPrefixDb) toKey(k []byte) []byte {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) {
 | 
			
		||||
	s.store.SetPrefix(db.DATATYPE_USERDATA)
 | 
			
		||||
	s.store.SetPrefix(DATATYPE_USERSUB)
 | 
			
		||||
	key = s.toKey(key)
 | 
			
		||||
	return s.store.Get(ctx, key)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error {
 | 
			
		||||
	s.store.SetPrefix(db.DATATYPE_USERDATA)
 | 
			
		||||
	s.store.SetPrefix(DATATYPE_USERSUB)
 | 
			
		||||
	key = s.toKey(key)
 | 
			
		||||
	return s.store.Put(ctx, key, val)
 | 
			
		||||
}
 | 
			
		||||
@ -6,11 +6,6 @@ import (
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
	gdbmdb "git.defalsify.org/vise.git/db/gdbm"
 | 
			
		||||
	"git.defalsify.org/vise.git/lang"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg = logging.NewVanilla().WithDomain("gdbmstorage")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
@ -120,8 +115,7 @@ func(tdb *ThreadGdbmDb) Close() error {
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(tdb *ThreadGdbmDb) Dump(ctx context.Context, key []byte) (*db.Dumper, error) {
 | 
			
		||||
	tdb.reserve()
 | 
			
		||||
	defer tdb.release()
 | 
			
		||||
	return tdb.db.Dump(ctx, key)
 | 
			
		||||
func(tdb *ThreadGdbmDb) Dump(_ context.Context, _ []byte) (*db.Dumper, error) {
 | 
			
		||||
	logg.Warnf("method not implemented for thread gdbm db")
 | 
			
		||||
	return nil, nil
 | 
			
		||||
}
 | 
			
		||||
@ -1,86 +0,0 @@
 | 
			
		||||
package storage
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"path"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	DBTYPE_MEM = iota
 | 
			
		||||
	DBTYPE_GDBM
 | 
			
		||||
	DBTYPE_POSTGRES
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ConnData struct {
 | 
			
		||||
	typ int
 | 
			
		||||
	str string
 | 
			
		||||
	domain string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cd *ConnData) DbType() int {
 | 
			
		||||
	return cd.typ
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cd *ConnData) String() string {
 | 
			
		||||
	return cd.str
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cd *ConnData) Domain() string {
 | 
			
		||||
	return cd.domain
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cd *ConnData) Path() string {
 | 
			
		||||
	v, _ := url.Parse(cd.str)
 | 
			
		||||
	v.RawQuery = ""
 | 
			
		||||
	return v.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func probePostgres(s string) (string, string, bool) {
 | 
			
		||||
	domain := "public"
 | 
			
		||||
	v, err := url.Parse(s)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", "", false
 | 
			
		||||
	}
 | 
			
		||||
	if v.Scheme != "postgres" {
 | 
			
		||||
		return "", "", false
 | 
			
		||||
	}
 | 
			
		||||
	vv := v.Query()
 | 
			
		||||
	if vv.Has("search_path") {
 | 
			
		||||
		domain = vv.Get("search_path")
 | 
			
		||||
	}
 | 
			
		||||
	return s, domain, true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func probeGdbm(s string) (string, string, bool) {
 | 
			
		||||
	if !path.IsAbs(s) {
 | 
			
		||||
		return "", "", false
 | 
			
		||||
	}
 | 
			
		||||
	s = path.Clean(s)
 | 
			
		||||
	return s, "", true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ToConnData(connStr string) (ConnData, error) {
 | 
			
		||||
	var o ConnData
 | 
			
		||||
 | 
			
		||||
	if connStr == "" {
 | 
			
		||||
		return o, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	v, domain, ok := probePostgres(connStr)
 | 
			
		||||
	if ok {
 | 
			
		||||
		o.typ = DBTYPE_POSTGRES
 | 
			
		||||
		o.str = v
 | 
			
		||||
		o.domain = domain
 | 
			
		||||
		return o, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	v, _, ok = probeGdbm(connStr)
 | 
			
		||||
	if ok {
 | 
			
		||||
		o.typ = DBTYPE_GDBM
 | 
			
		||||
		o.str = v
 | 
			
		||||
		return o, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return o, fmt.Errorf("invalid connection string: %s", connStr)
 | 
			
		||||
}
 | 
			
		||||
@ -1,28 +0,0 @@
 | 
			
		||||
package storage
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestParseConnStr(t *testing.T) {
 | 
			
		||||
	_, err := ToConnData("postgres://foo:bar@localhost:5432/baz")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)	
 | 
			
		||||
	}
 | 
			
		||||
	_, err = ToConnData("/foo/bar")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)	
 | 
			
		||||
	}
 | 
			
		||||
	_, err = ToConnData("/foo/bar/")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)	
 | 
			
		||||
	}
 | 
			
		||||
	_, err = ToConnData("foo/bar")
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		t.Fatalf("expected error")
 | 
			
		||||
	}
 | 
			
		||||
	_, err = ToConnData("http://foo/bar")
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		t.Fatalf("expected error")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -5,10 +5,6 @@ import (
 | 
			
		||||
	"git.defalsify.org/vise.git/persist"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	DATATYPE_EXTEND = 128
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Storage struct {
 | 
			
		||||
	Persister *persist.Persister
 | 
			
		||||
	UserdataDb db.Db	
 | 
			
		||||
 | 
			
		||||
@ -9,12 +9,10 @@ import (
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
	fsdb "git.defalsify.org/vise.git/db/fs"
 | 
			
		||||
	"git.defalsify.org/vise.git/db/postgres"
 | 
			
		||||
	"git.defalsify.org/vise.git/lang"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"git.defalsify.org/vise.git/persist"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
	gdbmstorage "git.grassecon.net/urdt/ussd/internal/storage/db/gdbm"
 | 
			
		||||
	"github.com/jackc/pgx/v5/pgxpool"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
@ -25,54 +23,63 @@ type StorageService interface {
 | 
			
		||||
	GetPersister(ctx context.Context) (*persist.Persister, error)
 | 
			
		||||
	GetUserdataDb(ctx context.Context) db.Db
 | 
			
		||||
	GetResource(ctx context.Context) (resource.Resource, error)
 | 
			
		||||
	EnsureDbDir() error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MenuStorageService struct {
 | 
			
		||||
	conn ConnData
 | 
			
		||||
	dbDir         string
 | 
			
		||||
	resourceDir   string
 | 
			
		||||
	poResource    resource.Resource
 | 
			
		||||
	resourceStore db.Db
 | 
			
		||||
	stateStore    db.Db
 | 
			
		||||
	userDataStore db.Db
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewMenuStorageService(conn ConnData, resourceDir string) *MenuStorageService {
 | 
			
		||||
func buildConnStr() string {
 | 
			
		||||
	host := initializers.GetEnv("DB_HOST", "localhost")
 | 
			
		||||
	user := initializers.GetEnv("DB_USER", "postgres")
 | 
			
		||||
	password := initializers.GetEnv("DB_PASSWORD", "")
 | 
			
		||||
	dbName := initializers.GetEnv("DB_NAME", "")
 | 
			
		||||
	port := initializers.GetEnv("DB_PORT", "5432")
 | 
			
		||||
 | 
			
		||||
	connString := fmt.Sprintf(
 | 
			
		||||
		"postgres://%s:%s@%s:%s/%s",
 | 
			
		||||
		user, password, host, port, dbName,
 | 
			
		||||
	)
 | 
			
		||||
	logg.Debugf("pg conn string", "conn", connString)
 | 
			
		||||
 | 
			
		||||
	return connString
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewMenuStorageService(dbDir string, resourceDir string) *MenuStorageService {
 | 
			
		||||
	return &MenuStorageService{
 | 
			
		||||
		conn: conn,
 | 
			
		||||
		dbDir:       dbDir,
 | 
			
		||||
		resourceDir: resourceDir,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.Db, section string) (db.Db, error) {
 | 
			
		||||
	var newDb db.Db
 | 
			
		||||
	var err error
 | 
			
		||||
func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.Db, fileName string) (db.Db, error) {
 | 
			
		||||
	database, ok := ctx.Value("Database").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return nil, fmt.Errorf("failed to select the database")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if existingDb != nil {
 | 
			
		||||
		return existingDb, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var newDb db.Db
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	connStr := ms.conn.String()
 | 
			
		||||
	dbTyp := ms.conn.DbType()
 | 
			
		||||
	if dbTyp == DBTYPE_POSTGRES {
 | 
			
		||||
		// TODO: move to vise
 | 
			
		||||
		err = ensureSchemaExists(ctx, ms.conn)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		newDb = postgres.NewPgDb().WithSchema(ms.conn.Domain())
 | 
			
		||||
	} else if dbTyp == DBTYPE_GDBM {
 | 
			
		||||
		err = ms.ensureDbDir()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		connStr = path.Join(connStr, section)
 | 
			
		||||
		newDb = gdbmstorage.NewThreadGdbmDb()
 | 
			
		||||
	if database == "postgres" {
 | 
			
		||||
		newDb = postgres.NewPgDb()
 | 
			
		||||
		connStr := buildConnStr()
 | 
			
		||||
		err = newDb.Connect(ctx, connStr)
 | 
			
		||||
	} else {
 | 
			
		||||
		return nil, fmt.Errorf("unsupported connection string: '%s'\n", ms.conn.String())
 | 
			
		||||
		newDb = NewThreadGdbmDb()
 | 
			
		||||
		storeFile := path.Join(ms.dbDir, fileName)
 | 
			
		||||
		err = newDb.Connect(ctx, storeFile)
 | 
			
		||||
	}
 | 
			
		||||
	logg.DebugCtxf(ctx, "connecting to db", "conn", connStr, "conndata", ms.conn)
 | 
			
		||||
	err = newDb.Connect(ctx, connStr)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
@ -80,45 +87,6 @@ func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.D
 | 
			
		||||
	return newDb, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WithGettext triggers use of gettext for translation of templates and menus.
 | 
			
		||||
//
 | 
			
		||||
// The first language in `lns` will be used as default language, to resolve node keys to 
 | 
			
		||||
// language strings.
 | 
			
		||||
//
 | 
			
		||||
// If `lns` is an empty array, gettext will not be used.
 | 
			
		||||
func (ms *MenuStorageService) WithGettext(path string, lns []lang.Language) *MenuStorageService {
 | 
			
		||||
	if len(lns) == 0 {
 | 
			
		||||
		logg.Warnf("Gettext requested but no languages supplied")
 | 
			
		||||
		return ms
 | 
			
		||||
	}
 | 
			
		||||
	rs := resource.NewPoResource(lns[0], path)
 | 
			
		||||
 | 
			
		||||
	for _, ln := range(lns) {
 | 
			
		||||
		rs = rs.WithLanguage(ln)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ms.poResource = rs
 | 
			
		||||
 | 
			
		||||
	return ms
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ensureSchemaExists creates a new schema if it does not exist
 | 
			
		||||
func ensureSchemaExists(ctx context.Context, conn ConnData) error {
 | 
			
		||||
	h, err := pgxpool.New(ctx, conn.Path())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to connect to the database: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer h.Close()
 | 
			
		||||
 | 
			
		||||
	query := fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", conn.Domain())
 | 
			
		||||
	_, err = h.Exec(ctx, query)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to create schema: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ms *MenuStorageService) GetPersister(ctx context.Context) (*persist.Persister, error) {
 | 
			
		||||
	stateStore, err := ms.GetStateStore(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@ -151,11 +119,6 @@ func (ms *MenuStorageService) GetResource(ctx context.Context) (resource.Resourc
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	rfs := resource.NewDbResource(ms.resourceStore)
 | 
			
		||||
	if ms.poResource != nil {
 | 
			
		||||
		logg.InfoCtxf(ctx, "using poresource for menu and template")
 | 
			
		||||
		rfs.WithMenuGetter(ms.poResource.GetMenu)
 | 
			
		||||
		rfs.WithTemplateGetter(ms.poResource.GetTemplate)
 | 
			
		||||
	}
 | 
			
		||||
	return rfs, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -173,8 +136,8 @@ func (ms *MenuStorageService) GetStateStore(ctx context.Context) (db.Db, error)
 | 
			
		||||
	return ms.stateStore, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ms *MenuStorageService) ensureDbDir() error {
 | 
			
		||||
	err := os.MkdirAll(ms.conn.String(), 0700)
 | 
			
		||||
func (ms *MenuStorageService) EnsureDbDir() error {
 | 
			
		||||
	err := os.MkdirAll(ms.dbDir, 0700)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("state dir create exited with error: %v\n", err)
 | 
			
		||||
	}
 | 
			
		||||
							
								
								
									
										124
									
								
								internal/testutil/TestEngine.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								internal/testutil/TestEngine.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,124 @@
 | 
			
		||||
package testutil
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/testutil/testservice"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/testutil/testtag"
 | 
			
		||||
	testdataloader "github.com/peteole/testdata-loader"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/remote"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	baseDir   = testdataloader.GetBasePath()
 | 
			
		||||
	logg      = logging.NewVanilla()
 | 
			
		||||
	scriptDir = path.Join(baseDir, "services", "registration")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestEngine(sessionId string) (engine.Engine, func(), chan bool) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	ctx = context.WithValue(ctx, "SessionId", sessionId)
 | 
			
		||||
	ctx = context.WithValue(ctx, "Database", "gdbm")
 | 
			
		||||
	pfp := path.Join(scriptDir, "pp.csv")
 | 
			
		||||
 | 
			
		||||
	var eventChannel = make(chan bool)
 | 
			
		||||
 | 
			
		||||
	cfg := engine.Config{
 | 
			
		||||
		Root:       "root",
 | 
			
		||||
		SessionId:  sessionId,
 | 
			
		||||
		OutputSize: uint32(160),
 | 
			
		||||
		FlagCount:  uint32(128),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dbDir := ".test_state"
 | 
			
		||||
	resourceDir := scriptDir
 | 
			
		||||
	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
 | 
			
		||||
 | 
			
		||||
	err := menuStorageService.EnsureDbDir()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rs, err := menuStorageService.GetResource(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pe, err := menuStorageService.GetPersister(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userDataStore, err := menuStorageService.GetUserdataDb(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dbResource, ok := rs.(*resource.DbResource)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs)
 | 
			
		||||
	lhs.SetDataStore(&userDataStore)
 | 
			
		||||
	lhs.SetPersister(pe)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if testtag.AccountService == nil {
 | 
			
		||||
		testtag.AccountService = &remote.AccountService{}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch testtag.AccountService.(type) {
 | 
			
		||||
	case *testservice.TestAccountService:
 | 
			
		||||
		go func() {
 | 
			
		||||
			eventChannel <- false
 | 
			
		||||
		}()
 | 
			
		||||
	case *remote.AccountService:
 | 
			
		||||
		go func() {
 | 
			
		||||
			time.Sleep(5 * time.Second) // Wait for 5 seconds
 | 
			
		||||
			eventChannel <- true
 | 
			
		||||
		}()
 | 
			
		||||
	default:
 | 
			
		||||
		panic("Unknown account service type")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hl, err := lhs.GetHandler(testtag.AccountService)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	en := lhs.GetEngine()
 | 
			
		||||
	en = en.WithFirst(hl.Init)
 | 
			
		||||
	cleanFn := func() {
 | 
			
		||||
		err := en.Finish()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logg.Errorf(err.Error())
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = menuStorageService.Close()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logg.Errorf(err.Error())
 | 
			
		||||
		}
 | 
			
		||||
		logg.Infof("testengine storage closed")
 | 
			
		||||
	}
 | 
			
		||||
	return en, cleanFn, eventChannel
 | 
			
		||||
}
 | 
			
		||||
@ -1,209 +0,0 @@
 | 
			
		||||
package testutil
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/config"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/testutil/testservice"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/testutil/testtag"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/remote"
 | 
			
		||||
	"github.com/jackc/pgx/v5/pgxpool"
 | 
			
		||||
	testdataloader "github.com/peteole/testdata-loader"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg        = logging.NewVanilla()
 | 
			
		||||
	baseDir     = testdataloader.GetBasePath()
 | 
			
		||||
	scriptDir   = path.Join(baseDir, "services", "registration")
 | 
			
		||||
	setDbType   string
 | 
			
		||||
	setConnStr  string
 | 
			
		||||
	setDbSchema string
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	initializers.LoadEnvVariablesPath(baseDir)
 | 
			
		||||
	config.LoadConfig()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetDatabase updates the database used by TestEngine
 | 
			
		||||
func SetDatabase(database, connStr, dbSchema string) {
 | 
			
		||||
	setDbType = database
 | 
			
		||||
	setConnStr = connStr
 | 
			
		||||
	setDbSchema = dbSchema
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CleanDatabase removes all test data from the database
 | 
			
		||||
func CleanDatabase() {
 | 
			
		||||
	if setDbType == "postgres" {
 | 
			
		||||
		ctx := context.Background()
 | 
			
		||||
		// Update the connection string with the new search path
 | 
			
		||||
		updatedConnStr, err := updateSearchPath(setConnStr, setDbSchema)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatalf("Failed to update search path: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		dbConn, err := pgxpool.New(ctx, updatedConnStr)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatalf("Failed to connect to database for cleanup: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
		defer dbConn.Close()
 | 
			
		||||
 | 
			
		||||
		query := fmt.Sprintf("DELETE FROM %s.kv_vise;", setDbSchema)
 | 
			
		||||
		_, execErr := dbConn.Exec(ctx, query)
 | 
			
		||||
		if execErr != nil {
 | 
			
		||||
			log.Printf("Failed to cleanup table %s.kv_vise: %v", setDbSchema, execErr)
 | 
			
		||||
		} else {
 | 
			
		||||
			log.Printf("Successfully cleaned up table %s.kv_vise", setDbSchema)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		setConnStr, _ := filepath.Abs(setConnStr)
 | 
			
		||||
		if err := os.RemoveAll(setConnStr); err != nil {
 | 
			
		||||
			log.Fatalf("Failed to delete state store %s: %v", setConnStr, err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// updateSearchPath updates the search_path (schema) to be used in the connection
 | 
			
		||||
func updateSearchPath(connStr string, newSearchPath string) (string, error) {
 | 
			
		||||
	u, err := url.Parse(connStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("invalid connection string: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Parse the query parameters
 | 
			
		||||
	q := u.Query()
 | 
			
		||||
 | 
			
		||||
	// Update or add the search_path parameter
 | 
			
		||||
	q.Set("search_path", newSearchPath)
 | 
			
		||||
 | 
			
		||||
	// Rebuild the connection string with updated parameters
 | 
			
		||||
	u.RawQuery = q.Encode()
 | 
			
		||||
 | 
			
		||||
	return u.String(), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestEngine(sessionId string) (engine.Engine, func(), chan bool) {
 | 
			
		||||
	var err error
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	ctx = context.WithValue(ctx, "SessionId", sessionId)
 | 
			
		||||
	pfp := path.Join(scriptDir, "pp.csv")
 | 
			
		||||
 | 
			
		||||
	var eventChannel = make(chan bool)
 | 
			
		||||
 | 
			
		||||
	cfg := engine.Config{
 | 
			
		||||
		Root:       "root",
 | 
			
		||||
		SessionId:  sessionId,
 | 
			
		||||
		OutputSize: uint32(160),
 | 
			
		||||
		FlagCount:  uint32(128),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if setDbType == "postgres" {
 | 
			
		||||
		setConnStr = config.DbConn
 | 
			
		||||
		setConnStr, err = updateSearchPath(setConnStr, setDbSchema)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			fmt.Println("Error:", err)
 | 
			
		||||
			os.Exit(1)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		setConnStr, err = filepath.Abs(setConnStr)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			fmt.Fprintf(os.Stderr, "connstr err: %v", err)
 | 
			
		||||
			os.Exit(1)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	conn, err := storage.ToConnData(setConnStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "connstr parse err: %v", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resourceDir := scriptDir
 | 
			
		||||
	menuStorageService := storage.NewMenuStorageService(conn, resourceDir)
 | 
			
		||||
 | 
			
		||||
	rs, err := menuStorageService.GetResource(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "resource error: %v", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pe, err := menuStorageService.GetPersister(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "persister error: %v", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userDataStore, err := menuStorageService.GetUserdataDb(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "userdb error: %v", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dbResource, ok := rs.(*resource.DbResource)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "dbresource cast error")
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs)
 | 
			
		||||
	lhs.SetDataStore(&userDataStore)
 | 
			
		||||
	lhs.SetPersister(pe)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if testtag.AccountService == nil {
 | 
			
		||||
		testtag.AccountService = &remote.AccountService{}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch testtag.AccountService.(type) {
 | 
			
		||||
	case *testservice.TestAccountService:
 | 
			
		||||
		go func() {
 | 
			
		||||
			eventChannel <- false
 | 
			
		||||
		}()
 | 
			
		||||
	case *remote.AccountService:
 | 
			
		||||
		go func() {
 | 
			
		||||
			time.Sleep(5 * time.Second) // Wait for 5 seconds
 | 
			
		||||
			eventChannel <- true
 | 
			
		||||
		}()
 | 
			
		||||
	default:
 | 
			
		||||
		panic("Unknown account service type")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hl, err := lhs.GetHandler(testtag.AccountService)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	en := lhs.GetEngine()
 | 
			
		||||
	en = en.WithFirst(hl.Init)
 | 
			
		||||
	cleanFn := func() {
 | 
			
		||||
		err := en.Finish()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logg.Errorf(err.Error())
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = menuStorageService.Close()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logg.Errorf(err.Error())
 | 
			
		||||
		}
 | 
			
		||||
		logg.Infof("testengine storage closed")
 | 
			
		||||
	}
 | 
			
		||||
	return en, cleanFn, eventChannel
 | 
			
		||||
}
 | 
			
		||||
@ -1,15 +0,0 @@
 | 
			
		||||
package testutil
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestCreateEngine(t *testing.T) {
 | 
			
		||||
	o, clean, eventC := TestEngine("foo")
 | 
			
		||||
	defer clean()
 | 
			
		||||
	defer func() {
 | 
			
		||||
		<-eventC
 | 
			
		||||
		close(eventC)
 | 
			
		||||
	}()
 | 
			
		||||
	_ = o
 | 
			
		||||
}
 | 
			
		||||
@ -1,14 +1,12 @@
 | 
			
		||||
package httpmocks
 | 
			
		||||
 | 
			
		||||
import "context"
 | 
			
		||||
 | 
			
		||||
// MockRequestParser implements the handlers.RequestParser interface for testing
 | 
			
		||||
type MockRequestParser struct {
 | 
			
		||||
	GetSessionIdFunc func(any) (string, error)
 | 
			
		||||
	GetInputFunc     func(any) ([]byte, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockRequestParser) GetSessionId(ctx context.Context, rq any) (string, error) {
 | 
			
		||||
func (m *MockRequestParser) GetSessionId(rq any) (string, error) {
 | 
			
		||||
	return m.GetSessionIdFunc(rq)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -49,6 +49,6 @@ func (m *MockAccountService) TokenTransfer(ctx context.Context, amount, from, to
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockAccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) {
 | 
			
		||||
	args := m.Called(alias)
 | 
			
		||||
	args := m.Called()
 | 
			
		||||
	return args.Get(0).(*dataserviceapi.AliasAddress), args.Error(1)
 | 
			
		||||
}
 | 
			
		||||
@ -1,9 +1,6 @@
 | 
			
		||||
package utils
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
import "time"
 | 
			
		||||
 | 
			
		||||
// CalculateAge calculates the age based on a given birthdate and the current date in the format dd/mm/yy
 | 
			
		||||
// It adjusts for cases where the current date is before the birthday in the current year.
 | 
			
		||||
@ -28,29 +25,11 @@ func CalculateAge(birthdate, today time.Time) int {
 | 
			
		||||
// It subtracts the YOB from the current year to determine the age.
 | 
			
		||||
//
 | 
			
		||||
// Parameters:
 | 
			
		||||
//
 | 
			
		||||
//	yob: The year of birth as an integer.
 | 
			
		||||
//   yob: The year of birth as an integer.
 | 
			
		||||
//
 | 
			
		||||
// Returns:
 | 
			
		||||
//
 | 
			
		||||
//	The calculated age as an integer.
 | 
			
		||||
//   The calculated age as an integer.
 | 
			
		||||
func CalculateAgeWithYOB(yob int) int {
 | 
			
		||||
	currentYear := time.Now().Year()
 | 
			
		||||
	return currentYear - yob
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
//IsValidYob checks if the provided yob can be considered valid 
 | 
			
		||||
func IsValidYOb(yob string) bool {
 | 
			
		||||
	currentYear := time.Now().Year()
 | 
			
		||||
	yearOfBirth, err := strconv.ParseInt(yob, 10, 64)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	if yearOfBirth >= 1900 && int(yearOfBirth) <= currentYear {
 | 
			
		||||
		return true
 | 
			
		||||
	} else {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
    currentYear := time.Now().Year()
 | 
			
		||||
    return currentYear - yob
 | 
			
		||||
}
 | 
			
		||||
@ -1,9 +1,9 @@
 | 
			
		||||
package utils
 | 
			
		||||
 | 
			
		||||
var isoCodes = map[string]bool{
 | 
			
		||||
	"eng":     true, // English
 | 
			
		||||
	"swa":     true, // Swahili
 | 
			
		||||
	"default": true, // Default language: English
 | 
			
		||||
	"eng": true, // English
 | 
			
		||||
	"swa": true, // Swahili
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func IsValidISO639(code string) bool {
 | 
			
		||||
 | 
			
		||||
@ -1,17 +0,0 @@
 | 
			
		||||
package utils
 | 
			
		||||
 | 
			
		||||
func ConstructName(firstName, familyName, defaultValue string) string {
 | 
			
		||||
	name := defaultValue
 | 
			
		||||
	if familyName != defaultValue {
 | 
			
		||||
		if firstName != defaultValue {
 | 
			
		||||
			name = firstName + " " + familyName
 | 
			
		||||
		} else {
 | 
			
		||||
			name = familyName
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		if firstName != defaultValue {
 | 
			
		||||
			name = firstName
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return name
 | 
			
		||||
}
 | 
			
		||||
@ -54,7 +54,7 @@
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1235",
 | 
			
		||||
                    "expectedContent": "Incorrect PIN. You have: 2 remaining attempt(s).\n1:Retry\n9:Quit"
 | 
			
		||||
                    "expectedContent": "Incorrect pin\n1:Retry\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1",
 | 
			
		||||
@ -62,10 +62,10 @@
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1234",
 | 
			
		||||
                    "expectedContent": "Select language:\n1:English\n2:Kiswahili"
 | 
			
		||||
                    "expectedContent": "Select language:\n0:english\n1:kiswahili"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1",
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "Your language change request was successful.\n0:Back\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
@ -95,7 +95,7 @@
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1235",
 | 
			
		||||
                    "expectedContent": "Incorrect PIN. You have: 2 remaining attempt(s).\n1:Retry\n9:Quit"
 | 
			
		||||
                    "expectedContent": "Incorrect pin\n1:Retry\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1",
 | 
			
		||||
@ -107,7 +107,8 @@
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
 | 
			
		||||
                     "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
 | 
			
		||||
                    
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
@ -140,7 +141,7 @@
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1235",
 | 
			
		||||
                    "expectedContent": "Incorrect PIN. You have: 2 remaining attempt(s).\n1:Retry\n9:Quit"
 | 
			
		||||
                    "expectedContent": "Incorrect pin\n1:Retry\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1",
 | 
			
		||||
@ -152,7 +153,8 @@
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
 | 
			
		||||
                     "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
 | 
			
		||||
                    
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
@ -165,7 +167,7 @@
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "menu_my_account_edit_all_account_details_starting_from_firstname",
 | 
			
		||||
            "name": "menu_my_account_edit_firstname",
 | 
			
		||||
            "steps": [
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "",
 | 
			
		||||
@ -185,26 +187,6 @@
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "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:"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
@ -215,6 +197,10 @@
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
@ -222,7 +208,7 @@
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "menu_my_account_edit_familyname_when_all_account__details_have_been_set",
 | 
			
		||||
            "name": "menu_my_account_edit_familyname",
 | 
			
		||||
            "steps": [
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "",
 | 
			
		||||
@ -252,14 +238,19 @@
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "{balance}\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",
 | 
			
		||||
            "name": "menu_my_account_edit_gender",
 | 
			
		||||
            "steps": [
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "",
 | 
			
		||||
@ -289,6 +280,10 @@
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
@ -296,7 +291,7 @@
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "menu_my_account_edit_yob_when_all_account__details_have_been_set",
 | 
			
		||||
            "name": "menu_my_account_edit_yob",
 | 
			
		||||
            "steps": [
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "",
 | 
			
		||||
@ -326,6 +321,10 @@
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
@ -333,7 +332,7 @@
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "menu_my_account_edit_location_when_all_account_details_have_been_set",
 | 
			
		||||
            "name": "menu_my_account_edit_location",
 | 
			
		||||
            "steps": [
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "",
 | 
			
		||||
@ -363,6 +362,10 @@
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
@ -370,7 +373,7 @@
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "menu_my_account_edit_offerings_when_all_account__details_have_been_set",
 | 
			
		||||
            "name": "menu_my_account_edit_offerings",
 | 
			
		||||
            "steps": [
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "",
 | 
			
		||||
@ -400,6 +403,10 @@
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
@ -427,12 +434,16 @@
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1234",
 | 
			
		||||
                    "expectedContent": "My profile:\nName: foo bar\nGender: male\nAge: 80\nLocation: Kilifi\nYou provide: Bananas\n\n0:Back\n9:Quit"
 | 
			
		||||
                    "expectedContent": "My profile:\nName: foo bar\nGender: male\nAge: 79\nLocation: Kilifi\nYou provide: Bananas\n\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
@ -441,3 +452,9 @@
 | 
			
		||||
        }
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
      
 | 
			
		||||
        
 | 
			
		||||
       
 | 
			
		||||
        
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,9 +3,9 @@ package menutraversaltest
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"log"
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"os"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
@ -15,17 +15,14 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	testData  = driver.ReadData()
 | 
			
		||||
	sessionID string
 | 
			
		||||
	src       = rand.NewSource(42)
 | 
			
		||||
	g         = rand.New(src)
 | 
			
		||||
	testData      = driver.ReadData()
 | 
			
		||||
	testStore     = ".test_state"
 | 
			
		||||
	groupTestFile = "group_test.json"
 | 
			
		||||
	sessionID     string
 | 
			
		||||
	src           = rand.NewSource(42)
 | 
			
		||||
	g             = rand.New(src)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var groupTestFile = flag.String("test-file", "group_test.json", "The test file to use for running the group tests")
 | 
			
		||||
var database = flag.String("db", "gdbm", "Specify the database (gdbm or postgres)")
 | 
			
		||||
var connStr = flag.String("conn", ".test_state", "connection string")
 | 
			
		||||
var dbSchema = flag.String("schema", "test", "Specify the database schema (default test)")
 | 
			
		||||
 | 
			
		||||
func GenerateSessionId() string {
 | 
			
		||||
	uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g))
 | 
			
		||||
	v, err := uu.NewV4()
 | 
			
		||||
@ -80,15 +77,12 @@ func extractSendAmount(response []byte) string {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestMain(m *testing.M) {
 | 
			
		||||
	// Parse the flags
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
	sessionID = GenerateSessionId()
 | 
			
		||||
	// set the db
 | 
			
		||||
	testutil.SetDatabase(*database, *connStr, *dbSchema)
 | 
			
		||||
 | 
			
		||||
	// Cleanup the db after tests
 | 
			
		||||
	defer testutil.CleanDatabase()
 | 
			
		||||
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if err := os.RemoveAll(testStore); err != nil {
 | 
			
		||||
			log.Fatalf("Failed to delete state store %s: %v", testStore, err)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
	m.Run()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -125,6 +119,7 @@ func TestAccountCreationSuccessful(t *testing.T) {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	<-eventChannel
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestAccountRegistrationRejectTerms(t *testing.T) {
 | 
			
		||||
@ -301,10 +296,9 @@ func TestMainMenuSend(t *testing.T) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	sessions := testData
 | 
			
		||||
	for _, session := range sessions {
 | 
			
		||||
		groups := driver.FilterGroupsByName(session.Groups, "send_with_invite")
 | 
			
		||||
		groups := driver.FilterGroupsByName(session.Groups, "send_with_invalid_inputs")
 | 
			
		||||
		for _, group := range groups {
 | 
			
		||||
			for index, step := range group.Steps {
 | 
			
		||||
				t.Logf("step %v with input %v", index, step.Input)
 | 
			
		||||
			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)
 | 
			
		||||
@ -343,7 +337,7 @@ func TestMainMenuSend(t *testing.T) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGroups(t *testing.T) {
 | 
			
		||||
	groups, err := driver.LoadTestGroups(*groupTestFile)
 | 
			
		||||
	groups, err := driver.LoadTestGroups(groupTestFile)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalf("Failed to load test groups: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -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"
 | 
			
		||||
                    }
 | 
			
		||||
                ]
 | 
			
		||||
            }
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
    
 | 
			
		||||
@ -7,14 +7,14 @@
 | 
			
		||||
                "steps": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "",
 | 
			
		||||
                        "expectedContent": "Welcome to Sarafu Network\nPlease select a language\n1:English\n2:Kiswahili"
 | 
			
		||||
                        "expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:english\n1:kiswahili"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "1",
 | 
			
		||||
                        "expectedContent": "Do you agree to terms and conditions?\nhttps://grassecon.org/pages/terms-and-conditions\n\n1:Yes\n2:No"
 | 
			
		||||
                        "input": "0",
 | 
			
		||||
                        "expectedContent": "Do you agree to terms and conditions?\n0:yes\n1:no"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "1",
 | 
			
		||||
                        "input": "0",
 | 
			
		||||
                        "expectedContent": "Please enter a new four number PIN for your account:\n0:Exit"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
@ -40,14 +40,14 @@
 | 
			
		||||
                "steps": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "",
 | 
			
		||||
                        "expectedContent": "Welcome to Sarafu Network\nPlease select a language\n1:English\n2:Kiswahili"
 | 
			
		||||
                        "expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:english\n1:kiswahili"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "0",
 | 
			
		||||
                        "expectedContent": "Do you agree to terms and conditions?\n0:yes\n1:no"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "1",
 | 
			
		||||
                        "expectedContent": "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!"
 | 
			
		||||
                    }
 | 
			
		||||
                ]
 | 
			
		||||
@ -64,8 +64,8 @@
 | 
			
		||||
                        "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": "000",
 | 
			
		||||
                        "expectedContent": "000 is invalid, please try again:\n1:Retry\n9:Quit"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "1",
 | 
			
		||||
 | 
			
		||||
@ -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 +0,0 @@
 | 
			
		||||
Tatizo la kimtambo limetokea,tafadhali jaribu tena baadaye.
 | 
			
		||||
@ -7,4 +7,3 @@ HALT
 | 
			
		||||
INCMP _ 0
 | 
			
		||||
INCMP my_balance 1
 | 
			
		||||
INCMP community_balance 2
 | 
			
		||||
INCMP . * 
 | 
			
		||||
 | 
			
		||||
@ -1,2 +0,0 @@
 | 
			
		||||
LOAD show_blocked_account 0
 | 
			
		||||
HALT 
 | 
			
		||||
@ -2,9 +2,9 @@ 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
 | 
			
		||||
MOUT english 0
 | 
			
		||||
MOUT kiswahili 1
 | 
			
		||||
HALT
 | 
			
		||||
INCMP set_eng 1
 | 
			
		||||
INCMP set_swa 2
 | 
			
		||||
INCMP set_default 0
 | 
			
		||||
INCMP set_swa 1
 | 
			
		||||
INCMP . *
 | 
			
		||||
 | 
			
		||||
@ -9,4 +9,3 @@ MOUT quit 9
 | 
			
		||||
HALT
 | 
			
		||||
INCMP _ 0
 | 
			
		||||
INCMP quit 9
 | 
			
		||||
INCMP . * 
 | 
			
		||||
 | 
			
		||||
@ -2,17 +2,9 @@ CATCH incorrect_pin flag_incorrect_pin 1
 | 
			
		||||
CATCH update_familyname flag_allow_update 1
 | 
			
		||||
LOAD get_current_profile_info 0
 | 
			
		||||
RELOAD get_current_profile_info
 | 
			
		||||
MAP get_current_profile_info
 | 
			
		||||
MOUT back 0
 | 
			
		||||
HALT
 | 
			
		||||
RELOAD set_back 
 | 
			
		||||
CATCH _ flag_back_set 1
 | 
			
		||||
LOAD save_familyname 64
 | 
			
		||||
LOAD save_familyname 0
 | 
			
		||||
RELOAD save_familyname
 | 
			
		||||
CATCH pin_entry flag_familyname_set 1
 | 
			
		||||
CATCH select_gender flag_gender_set 0
 | 
			
		||||
CATCH edit_yob flag_yob_set 0
 | 
			
		||||
CATCH edit_location flag_location_set 0
 | 
			
		||||
CATCH edit_offerings flag_offerings_set 0
 | 
			
		||||
CATCH pin_entry flag_familyname_set 0
 | 
			
		||||
INCMP select_gender *
 | 
			
		||||
INCMP _ 0
 | 
			
		||||
INCMP pin_entry *
 | 
			
		||||
 | 
			
		||||
@ -5,14 +5,7 @@ RELOAD get_current_profile_info
 | 
			
		||||
MAP get_current_profile_info
 | 
			
		||||
MOUT back 0
 | 
			
		||||
HALT
 | 
			
		||||
RELOAD set_back 
 | 
			
		||||
CATCH _ flag_back_set 1
 | 
			
		||||
LOAD save_firstname 128
 | 
			
		||||
LOAD save_firstname 0
 | 
			
		||||
RELOAD save_firstname
 | 
			
		||||
CATCH pin_entry flag_firstname_set 1
 | 
			
		||||
CATCH edit_family_name flag_familyname_set 0
 | 
			
		||||
CATCH edit_gender flag_gender_set 0
 | 
			
		||||
CATCH edit_yob flag_yob_set 0
 | 
			
		||||
CATCH edit_location flag_location_set 0
 | 
			
		||||
CATCH edit_offerings flag_offerings_set 0
 | 
			
		||||
CATCH pin_entry flag_firstname_set 0
 | 
			
		||||
INCMP _ 0
 | 
			
		||||
INCMP pin_entry *
 | 
			
		||||
 | 
			
		||||
@ -1,2 +1,2 @@
 | 
			
		||||
Jina la kwanza la sasa: {{.get_current_profile_info}}
 | 
			
		||||
Jina la kwanza la sasa {{.get_current_profile_info}}
 | 
			
		||||
Weka majina yako ya kwanza:
 | 
			
		||||
@ -2,14 +2,9 @@ CATCH incorrect_pin flag_incorrect_pin 1
 | 
			
		||||
CATCH update_location flag_allow_update 1
 | 
			
		||||
LOAD get_current_profile_info 0
 | 
			
		||||
RELOAD get_current_profile_info
 | 
			
		||||
LOAD save_location 16
 | 
			
		||||
MOUT back 0
 | 
			
		||||
HALT
 | 
			
		||||
RELOAD set_back 
 | 
			
		||||
CATCH _ flag_back_set 1
 | 
			
		||||
LOAD save_location 0
 | 
			
		||||
RELOAD save_location
 | 
			
		||||
INCMP _ 0
 | 
			
		||||
CATCH pin_entry flag_location_set 1
 | 
			
		||||
CATCH edit_offerings flag_offerings_set 0
 | 
			
		||||
CATCH pin_entry flag_location_set 0
 | 
			
		||||
INCMP edit_offerings *
 | 
			
		||||
INCMP pin_entry *
 | 
			
		||||
 | 
			
		||||
@ -1,2 +1,2 @@
 | 
			
		||||
Eneo la sasa: {{.get_current_profile_info}}
 | 
			
		||||
Eneo la sasa {{.get_current_profile_info}}
 | 
			
		||||
Weka eneo:
 | 
			
		||||
@ -2,12 +2,9 @@ CATCH incorrect_pin flag_incorrect_pin 1
 | 
			
		||||
CATCH update_offerings flag_allow_update 1
 | 
			
		||||
LOAD get_current_profile_info 0
 | 
			
		||||
RELOAD get_current_profile_info
 | 
			
		||||
LOAD save_offerings 8
 | 
			
		||||
LOAD save_offerings 0
 | 
			
		||||
MOUT back 0
 | 
			
		||||
HALT
 | 
			
		||||
RELOAD set_back 
 | 
			
		||||
CATCH _ flag_back_set 1
 | 
			
		||||
RELOAD save_offerings
 | 
			
		||||
INCMP _ 0
 | 
			
		||||
CATCH pin_entry flag_offerings_set 1
 | 
			
		||||
INCMP update_profile_items *
 | 
			
		||||
INCMP pin_entry *
 | 
			
		||||
 | 
			
		||||
@ -11,8 +11,7 @@ MOUT edit_offerings 6
 | 
			
		||||
MOUT view 7
 | 
			
		||||
MOUT back 0
 | 
			
		||||
HALT
 | 
			
		||||
LOAD set_back 6
 | 
			
		||||
INCMP ^ 0
 | 
			
		||||
INCMP my_account 0
 | 
			
		||||
INCMP edit_first_name 1
 | 
			
		||||
INCMP edit_family_name 2
 | 
			
		||||
INCMP select_gender 3
 | 
			
		||||
@ -20,4 +19,3 @@ INCMP edit_yob 4
 | 
			
		||||
INCMP edit_location 5
 | 
			
		||||
INCMP edit_offerings 6
 | 
			
		||||
INCMP view_profile 7
 | 
			
		||||
INCMP . * 
 | 
			
		||||
 | 
			
		||||
@ -5,15 +5,10 @@ RELOAD get_current_profile_info
 | 
			
		||||
MAP get_current_profile_info
 | 
			
		||||
MOUT back 0
 | 
			
		||||
HALT
 | 
			
		||||
RELOAD set_back 
 | 
			
		||||
CATCH _ flag_back_set 1
 | 
			
		||||
LOAD verify_yob 6
 | 
			
		||||
RELOAD verify_yob
 | 
			
		||||
CATCH incorrect_date_format flag_incorrect_date_format 1
 | 
			
		||||
LOAD save_yob 32
 | 
			
		||||
LOAD save_yob 0
 | 
			
		||||
RELOAD save_yob
 | 
			
		||||
CATCH pin_entry flag_yob_set 1
 | 
			
		||||
CATCH edit_location flag_location_set 0
 | 
			
		||||
CATCH edit_offerings flag_offerings_set 0
 | 
			
		||||
CATCH pin_entry flag_yob_set 0
 | 
			
		||||
INCMP edit_location *
 | 
			
		||||
INCMP _ 0
 | 
			
		||||
INCMP pin_entry *
 | 
			
		||||
 | 
			
		||||
@ -1,2 +1,2 @@
 | 
			
		||||
Mwaka wa sasa wa kuzaliwa: {{.get_current_profile_info}}
 | 
			
		||||
Mwaka wa sasa wa kuzaliwa {{.get_current_profile_info}}
 | 
			
		||||
Weka mwaka wa kuzaliwa
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
English
 | 
			
		||||
@ -1 +1 @@
 | 
			
		||||
Incorrect PIN. You have: {{.reset_incorrect}} remaining attempt(s).
 | 
			
		||||
Incorrect pin
 | 
			
		||||
@ -1,7 +1,5 @@
 | 
			
		||||
LOAD reset_incorrect 0
 | 
			
		||||
RELOAD reset_incorrect
 | 
			
		||||
MAP reset_incorrect
 | 
			
		||||
CATCH blocked_account flag_account_blocked 1
 | 
			
		||||
MOUT retry 1
 | 
			
		||||
MOUT quit 9
 | 
			
		||||
HALT
 | 
			
		||||
 | 
			
		||||
@ -1 +1 @@
 | 
			
		||||
PIN ulioeka sio sahihi, una majaribio: {{.reset_incorrect}} yaliyobaki
 | 
			
		||||
PIN ulioeka sio sahihi
 | 
			
		||||
@ -1 +1 @@
 | 
			
		||||
PIN mpya na udhibitisho wa PIN mpya hazilingani.Tafadhali jaribu tena.Kwa usaidizi piga simu +254757628885.
 | 
			
		||||
PIN mpya na udhibitisho wa pin mpya hazilingani.Tafadhali jaribu tena.Kwa usaidizi piga simu +254757628885.
 | 
			
		||||
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
Kiswahili
 | 
			
		||||
@ -7,16 +7,13 @@ msgstr "Ombi lako limetumwa. %s atapokea %s %s kutoka kwa %s."
 | 
			
		||||
msgid "Thank you for using Sarafu. Goodbye!"
 | 
			
		||||
msgstr "Asante kwa kutumia huduma ya Sarafu. Kwaheri!"
 | 
			
		||||
 | 
			
		||||
msgid "For more help, please call: 0757628885"
 | 
			
		||||
msgstr "Kwa usaidizi zaidi, piga: 0757628885"
 | 
			
		||||
 | 
			
		||||
msgid "Your account has been locked. For help on how to unblock your account, contact support at: 0757628885"
 | 
			
		||||
msgstr "Akaunti yako imefungwa. Kwa usaidizi wa jinsi ya kufungua akaunti yako, wasiliana na usaidizi kwa: 0757628885"
 | 
			
		||||
msgid "For more help,please call: 0757628885"
 | 
			
		||||
msgstr "Kwa usaidizi zaidi,piga: 0757628885"
 | 
			
		||||
 | 
			
		||||
msgid "Balance: %s\n"
 | 
			
		||||
msgstr "Salio: %s\n"
 | 
			
		||||
 | 
			
		||||
msgid "Your invite request for %s to Sarafu Network failed. Please try again later."
 | 
			
		||||
msid "Your invite request for %s to Sarafu Network failed. Please try again later."
 | 
			
		||||
msgstr "Ombi lako la kumwalika %s kwa matandao wa Sarafu halikufaulu. Tafadhali jaribu tena baadaye."
 | 
			
		||||
 | 
			
		||||
msgid "Your invitation to %s to join Sarafu Network has been sent."
 | 
			
		||||
@ -26,7 +23,4 @@ msgid "Your request failed. Please try again later."
 | 
			
		||||
msgstr "Ombi lako halikufaulu. Tafadhali jaribu tena baadaye."
 | 
			
		||||
 | 
			
		||||
msgid "Community Balance: 0.00"
 | 
			
		||||
msgstr "Salio la Kikundi: 0.00"
 | 
			
		||||
 | 
			
		||||
msgid "Symbol: %s\nBalance: %s"
 | 
			
		||||
msgstr "Sarafu: %s\nSalio: %s"
 | 
			
		||||
msgid "Salio la Kikundi: 0.00"
 | 
			
		||||
 | 
			
		||||
@ -2,9 +2,9 @@ LOAD set_default_voucher 8
 | 
			
		||||
RELOAD set_default_voucher
 | 
			
		||||
LOAD check_vouchers 10
 | 
			
		||||
RELOAD check_vouchers
 | 
			
		||||
LOAD check_balance 128
 | 
			
		||||
LOAD check_balance 64
 | 
			
		||||
RELOAD check_balance
 | 
			
		||||
CATCH api_failure flag_api_call_error 1
 | 
			
		||||
CATCH api_failure flag_api_call_error  1
 | 
			
		||||
MAP check_balance
 | 
			
		||||
MOUT send 1
 | 
			
		||||
MOUT vouchers 2
 | 
			
		||||
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue
	
	Block a user