Merge branch 'lash/purify-more' into postgres-switch-for-tests

This commit is contained in:
lash 2025-01-08 22:05:12 +00:00
commit ddefdd7fb3
31 changed files with 584 additions and 212 deletions

View File

@ -6,13 +6,9 @@ HOST=127.0.0.1
AT_ENDPOINT=/ussd/africastalking AT_ENDPOINT=/ussd/africastalking
#PostgreSQL #PostgreSQL
DB_HOST=localhost DB_CONN=postgres://postgres:strongpass@localhost:5432/urdt_ussd
DB_USER=postgres #DB_TIMEZONE=Africa/Nairobi
DB_PASSWORD=strongpass #DB_SCHEMA=vise
DB_NAME=urdt_ussd
DB_PORT=5432
DB_SSLMODE=disable
DB_TIMEZONE=Africa/Nairobi
#External API Calls #External API Calls
CUSTODIAL_URL_BASE=http://localhost:5003 CUSTODIAL_URL_BASE=http://localhost:5003

View File

@ -21,7 +21,6 @@ import (
"git.grassecon.net/urdt/ussd/initializers" "git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/internal/handlers" "git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/http/at" "git.grassecon.net/urdt/ussd/internal/http/at"
httpserver "git.grassecon.net/urdt/ussd/internal/http/at"
"git.grassecon.net/urdt/ussd/internal/storage" "git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/remote" "git.grassecon.net/urdt/ussd/remote"
"git.grassecon.net/urdt/ussd/internal/args" "git.grassecon.net/urdt/ussd/internal/args"
@ -38,10 +37,11 @@ var (
func init() { func init() {
initializers.LoadEnvVariables(baseDir) initializers.LoadEnvVariables(baseDir)
} }
func main() { func main() {
config.LoadConfig() config.LoadConfig()
var dbDir string var connStr string
var resourceDir string var resourceDir string
var size uint var size uint
var database string var database string
@ -49,12 +49,12 @@ func main() {
var engineDebug bool var engineDebug bool
var host string var host string
var port uint var port uint
var err error
var gettextDir string var gettextDir string
var langs args.LangVar 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(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
flag.StringVar(&database, "db", "gdbm", "database to be used") flag.StringVar(&connStr, "c", "", "connection string")
flag.StringVar(&dbSchema, "schema", "public", "database schema to be used")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output") flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.UintVar(&size, "s", 160, "max size of output") flag.UintVar(&size, "s", 160, "max size of output")
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
@ -63,7 +63,16 @@ func main() {
flag.Var(&langs, "language", "add symbol resolution for language") flag.Var(&langs, "language", "add symbol resolution for language")
flag.Parse() flag.Parse()
logg.Infof("start command", "build", build, "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size) if connStr != "" {
connStr = config.DbConn
}
connData, err := storage.ToConnData(connStr)
if err != nil {
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
os.Exit(1)
}
logg.Infof("start command", "build", build, "conn", connData, "resourcedir", resourceDir, "outputsize", size)
ctx := context.Background() ctx := context.Background()
ctx = context.WithValue(ctx, "Database", database) ctx = context.WithValue(ctx, "Database", database)
@ -88,14 +97,13 @@ func main() {
cfg.EngineDebug = true cfg.EngineDebug = true
} }
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
rs, err := menuStorageService.GetResource(ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
} }
err = menuStorageService.EnsureDbDir() rs, err := menuStorageService.GetResource(ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
@ -141,7 +149,7 @@ func main() {
rp := &at.ATRequestParser{} rp := &at.ATRequestParser{}
bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl) bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
sh := httpserver.NewATSessionHandler(bsh) sh := at.NewATSessionHandler(bsh)
mux := http.NewServeMux() mux := http.NewServeMux()
mux.Handle(initializers.GetEnv("AT_ENDPOINT", "/"), sh) mux.Handle(initializers.GetEnv("AT_ENDPOINT", "/"), sh)

View File

@ -50,8 +50,8 @@ func (p *asyncRequestParser) GetInput(r any) ([]byte, error) {
func main() { func main() {
config.LoadConfig() config.LoadConfig()
var connStr string
var sessionId string var sessionId string
var dbDir string
var resourceDir string var resourceDir string
var size uint var size uint
var database string var database string
@ -59,13 +59,13 @@ func main() {
var engineDebug bool var engineDebug bool
var host string var host string
var port uint var port uint
var err error
var gettextDir string var gettextDir string
var langs args.LangVar var langs args.LangVar
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") 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(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
flag.StringVar(&database, "db", "gdbm", "database to be used") flag.StringVar(&connStr, "c", "", "connection string")
flag.StringVar(&dbSchema, "schema", "public", "database schema to be used")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output") flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.UintVar(&size, "s", 160, "max size of output") flag.UintVar(&size, "s", 160, "max size of output")
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
@ -74,7 +74,16 @@ func main() {
flag.Var(&langs, "language", "add symbol resolution for language") flag.Var(&langs, "language", "add symbol resolution for language")
flag.Parse() flag.Parse()
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId) if connStr != "" {
connStr = config.DbConn
}
connData, err := storage.ToConnData(connStr)
if err != nil {
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
os.Exit(1)
}
logg.Infof("start command", "conn", connData, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId)
ctx := context.Background() ctx := context.Background()
ctx = context.WithValue(ctx, "Database", database) ctx = context.WithValue(ctx, "Database", database)
@ -100,18 +109,18 @@ func main() {
cfg.EngineDebug = true cfg.EngineDebug = true
} }
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
rs, err := menuStorageService.GetResource(ctx) rs, err := menuStorageService.GetResource(ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
} }
err = menuStorageService.EnsureDbDir()
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
userdataStore, err := menuStorageService.GetUserdataDb(ctx) userdataStore, err := menuStorageService.GetUserdataDb(ctx)
if err != nil { if err != nil {

View File

@ -40,7 +40,7 @@ func init() {
func main() { func main() {
config.LoadConfig() config.LoadConfig()
var dbDir string var connStr string
var resourceDir string var resourceDir string
var size uint var size uint
var database string var database string
@ -48,12 +48,12 @@ func main() {
var engineDebug bool var engineDebug bool
var host string var host string
var port uint var port uint
var err error
var gettextDir string var gettextDir string
var langs args.LangVar 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(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
flag.StringVar(&database, "db", "gdbm", "database to be used") flag.StringVar(&connStr, "c", "", "connection string")
flag.StringVar(&dbSchema, "schema", "public", "database schema to be used")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output") flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.UintVar(&size, "s", 160, "max size of output") flag.UintVar(&size, "s", 160, "max size of output")
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
@ -62,7 +62,16 @@ func main() {
flag.Var(&langs, "language", "add symbol resolution for language") flag.Var(&langs, "language", "add symbol resolution for language")
flag.Parse() flag.Parse()
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size) if connStr != "" {
connStr = config.DbConn
}
connData, err := storage.ToConnData(connStr)
if err != nil {
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
os.Exit(1)
}
logg.Infof("start command", "conn", connData, "resourcedir", resourceDir, "outputsize", size)
ctx := context.Background() ctx := context.Background()
ctx = context.WithValue(ctx, "Database", database) ctx = context.WithValue(ctx, "Database", database)
@ -88,19 +97,14 @@ func main() {
cfg.EngineDebug = true cfg.EngineDebug = true
} }
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
rs, err := menuStorageService.GetResource(ctx) rs, err := menuStorageService.GetResource(ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
} }
err = menuStorageService.EnsureDbDir()
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
userdataStore, err := menuStorageService.GetUserdataDb(ctx) userdataStore, err := menuStorageService.GetUserdataDb(ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())

View File

@ -35,25 +35,36 @@ func init() {
func main() { func main() {
config.LoadConfig() config.LoadConfig()
var dbDir string var connStr string
var size uint var size uint
var sessionId string var sessionId string
var database string var database string
var dbSchema string var dbSchema string
var engineDebug bool var engineDebug bool
var resourceDir string
var err error
var gettextDir string var gettextDir string
var langs args.LangVar var langs args.LangVar
flag.StringVar(&resourceDir, "resourcedir", scriptDir, "resource dir")
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
flag.StringVar(&database, "db", "gdbm", "database to be used") flag.StringVar(&connStr, "c", "", "connection string")
flag.StringVar(&dbSchema, "schema", "public", "database schema to be used")
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output") flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.UintVar(&size, "s", 160, "max size of output") flag.UintVar(&size, "s", 160, "max size of output")
flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory") flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory")
flag.Var(&langs, "language", "add symbol resolution for language") flag.Var(&langs, "language", "add symbol resolution for language")
flag.Parse() flag.Parse()
logg.Infof("start command", "dbdir", dbDir, "outputsize", size) 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 { if len(langs.Langs()) == 0 {
langs.Set(config.DefaultLanguage) langs.Set(config.DefaultLanguage)
@ -81,18 +92,12 @@ func main() {
MenuSeparator: menuSeparator, MenuSeparator: menuSeparator,
} }
resourceDir := scriptDir menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
if gettextDir != "" { if gettextDir != "" {
menuStorageService = menuStorageService.WithGettext(gettextDir, langs.Langs()) 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) rs, err := menuStorageService.GetResource(ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())

View File

@ -14,7 +14,10 @@ import (
"git.defalsify.org/vise.git/engine" "git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/logging"
"git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/internal/ssh" "git.grassecon.net/urdt/ussd/internal/ssh"
"git.grassecon.net/urdt/ussd/internal/storage"
) )
var ( var (
@ -26,25 +29,49 @@ var (
build = "dev" build = "dev"
) )
func init() {
initializers.LoadEnvVariables()
}
func main() { func main() {
var dbDir string config.LoadConfig()
var connStr string
var authConnStr string
var resourceDir string var resourceDir string
var size uint var size uint
var engineDebug bool var engineDebug bool
var stateDebug bool var stateDebug bool
var host string var host string
var port uint var port uint
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") flag.StringVar(&connStr, "c", "", "connection string")
flag.StringVar(&authConnStr, "authdb", "", "auth connection string")
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
flag.BoolVar(&engineDebug, "engine-debug", false, "use engine debug output") flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.BoolVar(&stateDebug, "state-debug", false, "use engine debug output")
flag.UintVar(&size, "s", 160, "max size of output") flag.UintVar(&size, "s", 160, "max size of output")
flag.StringVar(&host, "h", "127.0.0.1", "http host") flag.StringVar(&host, "h", "127.0.0.1", "socket host")
flag.UintVar(&port, "p", 7122, "http port") flag.UintVar(&port, "p", 7122, "socket port")
flag.Parse() 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) sshKeyFile := flag.Arg(0)
_, err := os.Stat(sshKeyFile) _, err = os.Stat(sshKeyFile)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "cannot open ssh server private key file: %v\n", err) fmt.Fprintf(os.Stderr, "cannot open ssh server private key file: %v\n", err)
os.Exit(1) os.Exit(1)
@ -57,7 +84,7 @@ func main() {
logg.WarnCtxf(ctx, "!!!!! Do not expose to internet and only use with tunnel!") logg.WarnCtxf(ctx, "!!!!! Do not expose to internet and only use with tunnel!")
logg.WarnCtxf(ctx, "!!!!! (See ssh -L <...>)") logg.WarnCtxf(ctx, "!!!!! (See ssh -L <...>)")
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "keyfile", sshKeyFile, "host", host, "port", port) logg.Infof("start command", "conn", connData, "authconn", authConnData, "resourcedir", resourceDir, "outputsize", size, "keyfile", sshKeyFile, "host", host, "port", port)
pfp := path.Join(scriptDir, "pp.csv") pfp := path.Join(scriptDir, "pp.csv")
@ -73,7 +100,7 @@ func main() {
cfg.EngineDebug = true cfg.EngineDebug = true
} }
authKeyStore, err := ssh.NewSshKeyStore(ctx, dbDir) authKeyStore, err := ssh.NewSshKeyStore(ctx, authConnData.String())
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "keystore file open error: %v", err) fmt.Fprintf(os.Stderr, "keystore file open error: %v", err)
os.Exit(1) os.Exit(1)
@ -92,10 +119,10 @@ func main() {
signal.Notify(cterm, os.Interrupt, syscall.SIGTERM) signal.Notify(cterm, os.Interrupt, syscall.SIGTERM)
runner := &ssh.SshRunner{ runner := &ssh.SshRunner{
Cfg: cfg, Cfg: cfg,
Debug: engineDebug, Debug: engineDebug,
FlagFile: pfp, FlagFile: pfp,
DbDir: dbDir, Conn: connData,
ResourceDir: resourceDir, ResourceDir: resourceDir,
SrvKeyFile: sshKeyFile, SrvKeyFile: sshKeyFile,
Host: host, Host: host,

View File

@ -7,7 +7,7 @@ import (
"git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/logging"
) )
// DataType is a subprefix value used in association with vise/db.DATATYPE_USERDATA. // 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. // All keys are used only within the context of a single account. Unless otherwise specified, the user context is the session id.
// //
@ -55,6 +55,8 @@ const (
DATA_ACTIVE_DECIMAL DATA_ACTIVE_DECIMAL
// EVM address of the currently active voucher // EVM address of the currently active voucher
DATA_ACTIVE_ADDRESS DATA_ACTIVE_ADDRESS
//Holds count of the number of incorrect PIN attempts
DATA_INCORRECT_PIN_ATTEMPTS
) )
const ( const (

View File

@ -6,9 +6,13 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
// Define the regex pattern as a constant
const ( const (
// Define the regex pattern as a constant
pinPattern = `^\d{4}$` pinPattern = `^\d{4}$`
//Allowed incorrect PIN attempts
AllowedPINAttempts = uint8(3)
) )
// checks whether the given input is a 4 digit number // checks whether the given input is a 4 digit number

View File

@ -23,17 +23,17 @@ type StorageServices interface {
GetPersister(ctx context.Context) (*persist.Persister, error) GetPersister(ctx context.Context) (*persist.Persister, error)
GetUserdataDb(ctx context.Context) (db.Db, error) GetUserdataDb(ctx context.Context) (db.Db, error)
GetResource(ctx context.Context) (resource.Resource, error) GetResource(ctx context.Context) (resource.Resource, error)
EnsureDbDir() error
} }
type StorageService struct { type StorageService struct {
svc *storage.MenuStorageService svc *storage.MenuStorageService
} }
func NewStorageService(dbDir string) *StorageService { func NewStorageService(conn storage.ConnData) (*StorageService, error) {
return &StorageService{ svc := &StorageService{
svc: storage.NewMenuStorageService(dbDir, ""), svc: storage.NewMenuStorageService(conn, ""),
} }
return svc, nil
} }
func(ss *StorageService) GetPersister(ctx context.Context) (*persist.Persister, error) { func(ss *StorageService) GetPersister(ctx context.Context) (*persist.Persister, error) {
@ -47,7 +47,3 @@ func(ss *StorageService) GetUserdataDb(ctx context.Context) (db.Db, error) {
func(ss *StorageService) GetResource(ctx context.Context) (resource.Resource, error) { func(ss *StorageService) GetResource(ctx context.Context) (resource.Resource, error) {
return nil, errors.New("not implemented") return nil, errors.New("not implemented")
} }
func(ss *StorageService) EnsureDbDir() error {
return ss.svc.EnsureDbDir()
}

View File

@ -40,6 +40,7 @@ var (
VoucherTransfersURL string VoucherTransfersURL string
VoucherDataURL string VoucherDataURL string
CheckAliasURL string CheckAliasURL string
DbConn string
DefaultLanguage string DefaultLanguage string
Languages []string Languages []string
) )
@ -69,14 +70,20 @@ func setBase() error {
dataURLBase = initializers.GetEnv("DATA_URL_BASE", "http://localhost:5006") dataURLBase = initializers.GetEnv("DATA_URL_BASE", "http://localhost:5006")
BearerToken = initializers.GetEnv("BEARER_TOKEN", "") BearerToken = initializers.GetEnv("BEARER_TOKEN", "")
_, err = url.JoinPath(custodialURLBase, "/foo") _, err = url.Parse(custodialURLBase)
if err != nil { if err != nil {
return err return err
} }
_, err = url.JoinPath(dataURLBase, "/bar") _, err = url.Parse(dataURLBase)
if err != nil { if err != nil {
return err return err
} }
return nil
}
func setConn() error {
DbConn = initializers.GetEnv("DB_CONN", "")
return nil return nil
} }
@ -86,6 +93,10 @@ func LoadConfig() error {
if err != nil { if err != nil {
return err return err
} }
err = setConn()
if err != nil {
return err
}
err = setLanguage() err = setLanguage()
if err != nil { if err != nil {
return err return err

View File

@ -37,23 +37,34 @@ func formatItem(k []byte, v []byte) (string, error) {
func main() { func main() {
config.LoadConfig() config.LoadConfig()
var dbDir string var connStr string
var sessionId string var sessionId string
var database string var database string
var engineDebug bool var engineDebug bool
var err error
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
flag.StringVar(&database, "db", "gdbm", "database to be used") flag.StringVar(&connStr, "c", ".state", "connection string")
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output") flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.Parse() flag.Parse()
if connStr != "" {
connStr = config.DbConn
}
connData, err := storage.ToConnData(config.DbConn)
if err != nil {
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
os.Exit(1)
}
logg.Infof("start command", "conn", connData)
ctx := context.Background() ctx := context.Background()
ctx = context.WithValue(ctx, "SessionId", sessionId) ctx = context.WithValue(ctx, "SessionId", sessionId)
ctx = context.WithValue(ctx, "Database", database) ctx = context.WithValue(ctx, "Database", database)
resourceDir := scriptDir resourceDir := scriptDir
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
store, err := menuStorageService.GetUserdataDb(ctx) store, err := menuStorageService.GetUserdataDb(ctx)
if err != nil { if err != nil {

View File

@ -29,24 +29,35 @@ func init() {
func main() { func main() {
config.LoadConfig() config.LoadConfig()
var dbDir string var connStr string
var sessionId string var sessionId string
var database string var database string
var engineDebug bool var engineDebug bool
var err error
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
flag.StringVar(&database, "db", "gdbm", "database to be used") flag.StringVar(&connStr, "c", "", "connection string")
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output") flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.Parse() flag.Parse()
if connStr != "" {
connStr = config.DbConn
}
connData, err := storage.ToConnData(config.DbConn)
if err != nil {
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
os.Exit(1)
}
logg.Infof("start command", "conn", connData)
ctx := context.Background() ctx := context.Background()
ctx = context.WithValue(ctx, "SessionId", sessionId) ctx = context.WithValue(ctx, "SessionId", sessionId)
ctx = context.WithValue(ctx, "Database", database) ctx = context.WithValue(ctx, "Database", database)
resourceDir := scriptDir resourceDir := scriptDir
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
store, err := menuStorageService.GetUserdataDb(ctx) store, err := menuStorageService.GetUserdataDb(ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())

View File

@ -128,6 +128,7 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceIn
ls.DbRs.AddLocalFunc("view_statement", ussdHandlers.ViewTransactionStatement) ls.DbRs.AddLocalFunc("view_statement", ussdHandlers.ViewTransactionStatement)
ls.DbRs.AddLocalFunc("update_all_profile_items", ussdHandlers.UpdateAllProfileItems) ls.DbRs.AddLocalFunc("update_all_profile_items", ussdHandlers.UpdateAllProfileItems)
ls.DbRs.AddLocalFunc("set_back", ussdHandlers.SetBack) ls.DbRs.AddLocalFunc("set_back", ussdHandlers.SetBack)
ls.DbRs.AddLocalFunc("show_blocked_account", ussdHandlers.ShowBlockedAccount)
return ussdHandlers, nil return ussdHandlers, nil
} }

View File

@ -734,11 +734,23 @@ func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (res
if h.st.MatchFlag(flag_account_authorized, false) { if h.st.MatchFlag(flag_account_authorized, false) {
res.FlagReset = append(res.FlagReset, flag_incorrect_pin) res.FlagReset = append(res.FlagReset, flag_incorrect_pin)
res.FlagSet = append(res.FlagSet, flag_allow_update, flag_account_authorized) res.FlagSet = append(res.FlagSet, flag_allow_update, flag_account_authorized)
err := h.resetIncorrectPINAttempts(ctx, sessionId)
if err != nil {
return res, err
}
} else { } else {
res.FlagSet = append(res.FlagSet, flag_allow_update) res.FlagSet = append(res.FlagSet, flag_allow_update)
res.FlagReset = append(res.FlagReset, flag_account_authorized) res.FlagReset = append(res.FlagReset, flag_account_authorized)
err := h.resetIncorrectPINAttempts(ctx, sessionId)
if err != nil {
return res, err
}
} }
} else { } else {
err := h.incrementIncorrectPINAttempts(ctx, sessionId)
if err != nil {
return res, err
}
res.FlagSet = append(res.FlagSet, flag_incorrect_pin) res.FlagSet = append(res.FlagSet, flag_incorrect_pin)
res.FlagReset = append(res.FlagReset, flag_account_authorized) res.FlagReset = append(res.FlagReset, flag_account_authorized)
return res, nil return res, nil
@ -752,8 +764,34 @@ func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (res
// ResetIncorrectPin resets the incorrect pin flag after a new PIN attempt. // ResetIncorrectPin resets the incorrect pin flag after a new PIN attempt.
func (h *Handlers) ResetIncorrectPin(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) ResetIncorrectPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
store := h.userdataStore
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin") flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
flag_account_blocked, _ := h.flagManager.GetFlag("flag_account_blocked")
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
res.FlagReset = append(res.FlagReset, flag_incorrect_pin) res.FlagReset = append(res.FlagReset, flag_incorrect_pin)
currentWrongPinAttempts, err := store.ReadEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS)
if err != nil {
if !db.IsNotFound(err) {
return res, err
}
}
pinAttemptsValue, _ := strconv.ParseUint(string(currentWrongPinAttempts), 0, 64)
remainingPINAttempts := common.AllowedPINAttempts - uint8(pinAttemptsValue)
if remainingPINAttempts == 0 {
res.FlagSet = append(res.FlagSet, flag_account_blocked)
return res, nil
}
if remainingPINAttempts < common.AllowedPINAttempts {
res.Content = strconv.Itoa(int(remainingPINAttempts))
}
return res, nil return res, nil
} }
@ -840,6 +878,16 @@ func (h *Handlers) QuitWithHelp(ctx context.Context, sym string, input []byte) (
return res, nil return res, nil
} }
// ShowBlockedAccount displays a message after an account has been blocked and how to reach support.
func (h *Handlers) ShowBlockedAccount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
res.Content = l.Get("Your account has been locked. For help on how to unblock your account, contact support at: 0757628885")
return res, nil
}
// VerifyYob verifies the length of the given input. // VerifyYob verifies the length of the given input.
func (h *Handlers) VerifyYob(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) VerifyYob(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
@ -2075,3 +2123,53 @@ func (h *Handlers) UpdateAllProfileItems(ctx context.Context, sym string, input
} }
return res, nil return res, nil
} }
// incrementIncorrectPINAttempts keeps track of the number of incorrect PIN attempts
func (h *Handlers) incrementIncorrectPINAttempts(ctx context.Context, sessionId string) error {
var pinAttemptsCount uint8
store := h.userdataStore
currentWrongPinAttempts, err := store.ReadEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS)
if err != nil {
if db.IsNotFound(err) {
//First time Wrong PIN attempt: initialize with a count of 1
pinAttemptsCount = 1
err = store.WriteEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS, []byte(strconv.Itoa(int(pinAttemptsCount))))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write incorrect PIN attempts ", "key", common.DATA_INCORRECT_PIN_ATTEMPTS, "value", currentWrongPinAttempts, "error", err)
return err
}
return nil
}
}
pinAttemptsValue, _ := strconv.ParseUint(string(currentWrongPinAttempts), 0, 64)
pinAttemptsCount = uint8(pinAttemptsValue) + 1
err = store.WriteEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS, []byte(strconv.Itoa(int(pinAttemptsCount))))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write incorrect PIN attempts ", "key", common.DATA_INCORRECT_PIN_ATTEMPTS, "value", pinAttemptsCount, "error", err)
return err
}
return nil
}
// resetIncorrectPINAttempts resets the number of incorrect PIN attempts after a correct PIN entry
func (h *Handlers) resetIncorrectPINAttempts(ctx context.Context, sessionId string) error {
store := h.userdataStore
currentWrongPinAttempts, err := store.ReadEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS)
if err != nil {
if db.IsNotFound(err) {
return nil
}
return err
}
currentWrongPinAttemptsCount, _ := strconv.ParseUint(string(currentWrongPinAttempts), 0, 64)
if currentWrongPinAttemptsCount <= uint64(common.AllowedPINAttempts) {
err = store.WriteEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS, []byte(string("0")))
if err != nil {
logg.ErrorCtxf(ctx, "failed to reset incorrect PIN attempts ", "key", common.DATA_INCORRECT_PIN_ATTEMPTS, "value", common.AllowedPINAttempts, "error", err)
return err
}
}
return nil
}

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"log" "log"
"path" "path"
"strconv"
"strings" "strings"
"testing" "testing"
@ -907,37 +908,79 @@ func TestResetAccountAuthorized(t *testing.T) {
} }
func TestIncorrectPinReset(t *testing.T) { func TestIncorrectPinReset(t *testing.T) {
sessionId := "session123"
ctx, store := InitializeTestStore(t)
fm, err := NewFlagManager(flagsPath) fm, err := NewFlagManager(flagsPath)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
flag_incorrect_pin, _ := fm.parser.GetFlag("flag_incorrect_pin") 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 // Define test cases
tests := []struct { tests := []struct {
name string name string
input []byte input []byte
attempts uint8
expectedResult resource.Result expectedResult resource.Result
}{ }{
{ {
name: "Test incorrect pin reset", name: "Test when incorrect PIN attempts is 2",
input: []byte(""), input: []byte(""),
expectedResult: resource.Result{ expectedResult: resource.Result{
FlagReset: []uint32{flag_incorrect_pin}, 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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 // Create the Handlers instance with the mock flag manager
h := &Handlers{ h := &Handlers{
flagManager: fm.parser, flagManager: fm.parser,
userdataStore: store,
} }
// Call the method // Call the method
res, err := h.ResetIncorrectPin(context.Background(), "reset_incorrect_pin", tt.input) res, err := h.ResetIncorrectPin(ctx, "reset_incorrect_pin", tt.input)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -2190,3 +2233,55 @@ func TestGetVoucherDetails(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expectedResult, res) 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))
}

View File

@ -41,6 +41,7 @@ func NewAuther(ctx context.Context, keyStore *SshKeyStore) *auther {
} }
func(a *auther) Check(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) { 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) va, err := a.keyStore.Get(a.Ctx, pubKey)
if err != nil { if err != nil {
return nil, err return nil, err
@ -71,6 +72,20 @@ func(a *auther) Get(k []byte) (string, error) {
return v, nil 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 { func(s *SshRunner) serve(ctx context.Context, sessionId string, ch ssh.NewChannel, en engine.Engine) error {
if ch == nil { if ch == nil {
return errors.New("nil channel") return errors.New("nil channel")
@ -128,32 +143,13 @@ func(s *SshRunner) serve(ctx context.Context, sessionId string, ch ssh.NewChanne
return nil return nil
} }
type SshRunner struct {
Ctx context.Context
Cfg engine.Config
FlagFile string
DbDir string
ResourceDir string
Debug bool
SrvKeyFile string
Host string
Port uint
wg sync.WaitGroup
lst net.Listener
}
func(s *SshRunner) Stop() error { func(s *SshRunner) Stop() error {
return s.lst.Close() return s.lst.Close()
} }
func(s *SshRunner) GetEngine(sessionId string) (engine.Engine, func(), error) { func(s *SshRunner) GetEngine(sessionId string) (engine.Engine, func(), error) {
ctx := s.Ctx ctx := s.Ctx
menuStorageService := storage.NewMenuStorageService(s.DbDir, s.ResourceDir) menuStorageService := storage.NewMenuStorageService(s.Conn, s.ResourceDir)
err := menuStorageService.EnsureDbDir()
if err != nil {
return nil, nil, err
}
rs, err := menuStorageService.GetResource(ctx) rs, err := menuStorageService.GetResource(ctx)
if err != nil { if err != nil {
@ -208,6 +204,7 @@ func(s *SshRunner) GetEngine(sessionId string) (engine.Engine, func(), error) {
// adapted example from crypto/ssh package, NewServerConn doc // adapted example from crypto/ssh package, NewServerConn doc
func(s *SshRunner) Run(ctx context.Context, keyStore *SshKeyStore) { func(s *SshRunner) Run(ctx context.Context, keyStore *SshKeyStore) {
s.Ctx = ctx
running := true running := true
// TODO: waitgroup should probably not be global // TODO: waitgroup should probably not be global

69
internal/storage/parse.go Normal file
View File

@ -0,0 +1,69 @@
package storage
import (
"fmt"
"net/url"
"path"
)
const (
DBTYPE_MEM = iota
DBTYPE_GDBM
DBTYPE_POSTGRES
)
type ConnData struct {
typ int
str string
}
func (cd *ConnData) DbType() int {
return cd.typ
}
func (cd *ConnData) String() string {
return cd.str
}
func probePostgres(s string) (string, bool) {
v, err := url.Parse(s)
if err != nil {
return "", false
}
if v.Scheme != "postgres" {
return "", false
}
return s, true
}
func probeGdbm(s 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, ok := probePostgres(connStr)
if ok {
o.typ = DBTYPE_POSTGRES
o.str = v
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)
}

View File

@ -0,0 +1,28 @@
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")
}
}

View File

@ -13,7 +13,6 @@ import (
"git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/persist" "git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/initializers"
gdbmstorage "git.grassecon.net/urdt/ussd/internal/storage/db/gdbm" gdbmstorage "git.grassecon.net/urdt/ussd/internal/storage/db/gdbm"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
) )
@ -26,11 +25,10 @@ type StorageService interface {
GetPersister(ctx context.Context) (*persist.Persister, error) GetPersister(ctx context.Context) (*persist.Persister, error)
GetUserdataDb(ctx context.Context) db.Db GetUserdataDb(ctx context.Context) db.Db
GetResource(ctx context.Context) (resource.Resource, error) GetResource(ctx context.Context) (resource.Resource, error)
EnsureDbDir() error
} }
type MenuStorageService struct { type MenuStorageService struct {
dbDir string conn ConnData
resourceDir string resourceDir string
poResource resource.Resource poResource resource.Resource
resourceStore db.Db resourceStore db.Db
@ -38,29 +36,53 @@ type MenuStorageService struct {
userDataStore db.Db userDataStore db.Db
} }
func BuildConnStr() string { func NewMenuStorageService(conn ConnData, resourceDir string) *MenuStorageService {
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{ return &MenuStorageService{
dbDir: dbDir, conn: conn,
resourceDir: resourceDir, resourceDir: resourceDir,
} }
} }
func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.Db, section string) (db.Db, error) {
var newDb db.Db
var err error
if existingDb != nil {
return existingDb, nil
}
connStr := ms.conn.String()
dbTyp := ms.conn.DbType()
if dbTyp == DBTYPE_POSTGRES {
// // Ensure the schema exists
// err = ensureSchemaExists(ctx, connStr, schema)
// if err != nil {
// return nil, fmt.Errorf("failed to ensure schema exists: %w", err)
// }
//
// newDb = postgres.NewPgDb().WithSchema(schema)
newDb = postgres.NewPgDb()
} else if dbTyp == DBTYPE_GDBM {
err = ms.ensureDbDir()
if err != nil {
return nil, err
}
connStr = path.Join(connStr, section)
newDb = gdbmstorage.NewThreadGdbmDb()
} else {
return nil, fmt.Errorf("unsupported connection string: '%s'\n", ms.conn.String())
}
logg.DebugCtxf(ctx, "connecting to db", "conn", connStr)
err = newDb.Connect(ctx, connStr)
if err != nil {
return nil, err
}
return newDb, nil
}
// WithGettext triggers use of gettext for translation of templates and menus. // 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 // The first language in `lns` will be used as default language, to resolve node keys to
@ -83,48 +105,6 @@ func (ms *MenuStorageService) WithGettext(path string, lns []lang.Language) *Men
return ms return ms
} }
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")
}
schema, ok := ctx.Value("Schema").(string)
if !ok {
return nil, fmt.Errorf("failed to select the schema")
}
if existingDb != nil {
return existingDb, nil
}
var newDb db.Db
var err error
if database == "postgres" {
connStr := BuildConnStr()
// Ensure the schema exists
err = ensureSchemaExists(ctx, connStr, schema)
if err != nil {
return nil, fmt.Errorf("failed to ensure schema exists: %w", err)
}
newDb = postgres.NewPgDb().WithSchema(schema)
err = newDb.Connect(ctx, connStr)
} else {
newDb = gdbmstorage.NewThreadGdbmDb()
storeFile := path.Join(ms.dbDir, fileName)
err = newDb.Connect(ctx, storeFile)
}
if err != nil {
return nil, err
}
return newDb, nil
}
// ensureSchemaExists creates a new schema if it does not exist // ensureSchemaExists creates a new schema if it does not exist
func ensureSchemaExists(ctx context.Context, connStr, schema string) error { func ensureSchemaExists(ctx context.Context, connStr, schema string) error {
conn, err := pgxpool.New(ctx, connStr) conn, err := pgxpool.New(ctx, connStr)
@ -196,8 +176,8 @@ func (ms *MenuStorageService) GetStateStore(ctx context.Context) (db.Db, error)
return ms.stateStore, nil return ms.stateStore, nil
} }
func (ms *MenuStorageService) EnsureDbDir() error { func (ms *MenuStorageService) ensureDbDir() error {
err := os.MkdirAll(ms.dbDir, 0700) err := os.MkdirAll(ms.conn.String(), 0700)
if err != nil { if err != nil {
return fmt.Errorf("state dir create exited with error: %v\n", err) return fmt.Errorf("state dir create exited with error: %v\n", err)
} }

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"os" "os"
"path" "path"
"path/filepath"
"time" "time"
"git.defalsify.org/vise.git/engine" "git.defalsify.org/vise.git/engine"
@ -16,11 +17,9 @@ import (
"git.grassecon.net/urdt/ussd/internal/testutil/testservice" "git.grassecon.net/urdt/ussd/internal/testutil/testservice"
"git.grassecon.net/urdt/ussd/internal/testutil/testtag" "git.grassecon.net/urdt/ussd/internal/testutil/testtag"
"git.grassecon.net/urdt/ussd/remote" "git.grassecon.net/urdt/ussd/remote"
testdataloader "github.com/peteole/testdata-loader"
) )
var ( var (
baseDir = testdataloader.GetBasePath()
logg = logging.NewVanilla() logg = logging.NewVanilla()
scriptDir = path.Join(baseDir, "services", "registration") scriptDir = path.Join(baseDir, "services", "registration")
selectedDatabase = "" selectedDatabase = ""
@ -28,7 +27,7 @@ var (
) )
func init() { func init() {
initializers.LoadEnvVariables(baseDir) initializers.LoadEnvVariables()
} }
// SetDatabase updates the database used by TestEngine // SetDatabase updates the database used by TestEngine
@ -40,9 +39,6 @@ func SetDatabase(dbType string, dbSchema string) {
func TestEngine(sessionId string) (engine.Engine, func(), chan bool) { func TestEngine(sessionId string) (engine.Engine, func(), chan bool) {
ctx := context.Background() ctx := context.Background()
ctx = context.WithValue(ctx, "SessionId", sessionId) ctx = context.WithValue(ctx, "SessionId", sessionId)
ctx = context.WithValue(ctx, "Database", selectedDatabase)
ctx = context.WithValue(ctx, "Schema", selectedDbSchema)
pfp := path.Join(scriptDir, "pp.csv") pfp := path.Join(scriptDir, "pp.csv")
var eventChannel = make(chan bool) var eventChannel = make(chan bool)
@ -54,37 +50,40 @@ func TestEngine(sessionId string) (engine.Engine, func(), chan bool) {
FlagCount: uint32(128), FlagCount: uint32(128),
} }
dbDir := ".test_state" connStr, err := filepath.Abs(".test_state/state.gdbm")
resourceDir := scriptDir
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
err := menuStorageService.EnsureDbDir()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, "connstr err: %v", err)
os.Exit(1) os.Exit(1)
} }
conn, err := storage.ToConnData(connStr)
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) rs, err := menuStorageService.GetResource(ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, "resource error: %v", err)
os.Exit(1) os.Exit(1)
} }
pe, err := menuStorageService.GetPersister(ctx) pe, err := menuStorageService.GetPersister(ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, "persister error: %v", err)
os.Exit(1) os.Exit(1)
} }
userDataStore, err := menuStorageService.GetUserdataDb(ctx) userDataStore, err := menuStorageService.GetUserdataDb(ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, "userdb error: %v", err)
os.Exit(1) os.Exit(1)
} }
dbResource, ok := rs.(*resource.DbResource) dbResource, ok := rs.(*resource.DbResource)
if !ok { if !ok {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, "dbresource cast error")
os.Exit(1) os.Exit(1)
} }

View File

@ -0,0 +1,15 @@
package testutil
import (
"testing"
)
func TestCreateEngine(t *testing.T) {
o, clean, eventC := TestEngine("foo")
defer clean()
defer func() {
<-eventC
close(eventC)
}()
_ = o
}

View File

@ -54,7 +54,7 @@
}, },
{ {
"input": "1235", "input": "1235",
"expectedContent": "Incorrect PIN\n1:Retry\n9:Quit" "expectedContent": "Incorrect PIN. You have: 2 remaining attempt(s).\n1:Retry\n9:Quit"
}, },
{ {
"input": "1", "input": "1",
@ -95,7 +95,7 @@
}, },
{ {
"input": "1235", "input": "1235",
"expectedContent": "Incorrect PIN\n1:Retry\n9:Quit" "expectedContent": "Incorrect PIN. You have: 2 remaining attempt(s).\n1:Retry\n9:Quit"
}, },
{ {
"input": "1", "input": "1",
@ -107,8 +107,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back" "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
}, },
{ {
"input": "0", "input": "0",
@ -141,7 +140,7 @@
}, },
{ {
"input": "1235", "input": "1235",
"expectedContent": "Incorrect PIN\n1:Retry\n9:Quit" "expectedContent": "Incorrect PIN. You have: 2 remaining attempt(s).\n1:Retry\n9:Quit"
}, },
{ {
"input": "1", "input": "1",
@ -153,8 +152,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back" "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
}, },
{ {
"input": "0", "input": "0",
@ -195,7 +193,7 @@
}, },
{ {
"input": "1", "input": "1",
"expectedContent": "Enter your year of birth\n0:Back" "expectedContent": "Enter your year of birth\n0:Back"
}, },
{ {
"input": "1940", "input": "1940",
@ -258,7 +256,6 @@
"input": "0", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
} }
] ]
}, },
{ {
@ -443,10 +440,4 @@
] ]
} }
] ]
} }

View File

@ -8,6 +8,7 @@ import (
"log" "log"
"math/rand" "math/rand"
"os" "os"
"path/filepath"
"regexp" "regexp"
"testing" "testing"
@ -20,7 +21,6 @@ import (
var ( var (
testData = driver.ReadData() testData = driver.ReadData()
testStore = ".test_state"
sessionID string sessionID string
src = rand.NewSource(42) src = rand.NewSource(42)
g = rand.New(src) g = rand.New(src)
@ -30,6 +30,11 @@ var groupTestFile = flag.String("test-file", "group_test.json", "The test file t
var database = flag.String("db", "gdbm", "Specify the database (gdbm or postgres)") var database = flag.String("db", "gdbm", "Specify the database (gdbm or postgres)")
var dbSchema = flag.String("schema", "test", "Specify the database schema (default test)") var dbSchema = flag.String("schema", "test", "Specify the database schema (default test)")
func testStore() string {
v, _ := filepath.Abs(".test_state/state.gdbm")
return v
}
func GenerateSessionId() string { func GenerateSessionId() string {
uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g)) uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g))
v, err := uu.NewV4() v, err := uu.NewV4()
@ -89,8 +94,8 @@ func TestMain(m *testing.M) {
sessionID = GenerateSessionId() sessionID = GenerateSessionId()
defer func() { defer func() {
if err := os.RemoveAll(testStore); err != nil { if err := os.RemoveAll(testStore()); err != nil {
log.Fatalf("Failed to delete state store %s: %v", testStore, err) log.Fatalf("Failed to delete state store %s: %v", testStore(), err)
} }
}() }()

View File

@ -0,0 +1,2 @@
LOAD show_blocked_account 0
HALT

View File

@ -1 +1 @@
Incorrect PIN Incorrect PIN. You have: {{.reset_incorrect}} remaining attempt(s).

View File

@ -1,5 +1,7 @@
LOAD reset_incorrect 0 LOAD reset_incorrect 0
RELOAD reset_incorrect RELOAD reset_incorrect
MAP reset_incorrect
CATCH blocked_account flag_account_blocked 1
MOUT retry 1 MOUT retry 1
MOUT quit 9 MOUT quit 9
HALT HALT

View File

@ -1 +1 @@
PIN ulioeka sio sahihi PIN ulioeka sio sahihi, una majaribio: {{.reset_incorrect}} yaliyobaki

View File

@ -10,6 +10,9 @@ msgstr "Asante kwa kutumia huduma ya Sarafu. Kwaheri!"
msgid "For more help, please call: 0757628885" msgid "For more help, please call: 0757628885"
msgstr "Kwa usaidizi zaidi, piga: 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 "Balance: %s\n" msgid "Balance: %s\n"
msgstr "Salio: %s\n" msgstr "Salio: %s\n"

View File

@ -28,3 +28,5 @@ flag,flag_gender_set,34,this is set when the gender of the profile is set
flag,flag_location_set,35,this is set when the location of the profile is set flag,flag_location_set,35,this is set when the location of the profile is set
flag,flag_offerings_set,36,this is set when the offerings of the profile is set flag,flag_offerings_set,36,this is set when the offerings of the profile is set
flag,flag_back_set,37,this is set when it is a back navigation flag,flag_back_set,37,this is set when it is a back navigation
flag,flag_account_blocked,38,this is set when an account has been blocked after the allowed incorrect PIN attempts have been exceeded

1 flag flag_language_set 8 checks whether the user has set their prefered language
28 flag flag_location_set 35 this is set when the location of the profile is set
29 flag flag_offerings_set 36 this is set when the offerings of the profile is set
30 flag flag_back_set 37 this is set when it is a back navigation
31 flag flag_account_blocked 38 this is set when an account has been blocked after the allowed incorrect PIN attempts have been exceeded
32

View File

@ -1,3 +1,4 @@
CATCH blocked_account flag_account_blocked 1
CATCH select_language flag_language_set 0 CATCH select_language flag_language_set 0
CATCH terms flag_account_created 0 CATCH terms flag_account_created 0
LOAD check_account_status 0 LOAD check_account_status 0