Compare commits
18 Commits
menu-api-e
...
wip-code-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7f0ddec9b
|
||
|
|
2fe4ada5d3
|
||
|
|
b0342936e1
|
||
|
|
63d060afe2
|
||
|
|
92d212f891
|
||
|
|
b25288db2c
|
||
|
|
5aed7c647f
|
||
|
|
8765077177
|
||
|
|
4a6e4ebe55
|
||
|
|
525eee93d4
|
||
|
|
0c3ef357df
|
||
|
|
c2d2bd250a
|
||
|
|
cb2254664d
|
||
|
|
f010d097ed
|
||
|
|
c5fcb79e9e
|
||
|
|
cb77e44cbd
|
||
|
|
4d7c584394
|
||
|
|
5ff06e8626
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,5 +4,3 @@ go.work*
|
|||||||
**/*/*.bin
|
**/*/*.bin
|
||||||
**/*/.state/
|
**/*/.state/
|
||||||
cmd/.state/
|
cmd/.state/
|
||||||
id_*
|
|
||||||
*.gdbm
|
|
||||||
|
|||||||
@@ -1,167 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"path"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"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"
|
|
||||||
httpserver "git.grassecon.net/urdt/ussd/internal/http"
|
|
||||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
logg = logging.NewVanilla()
|
|
||||||
scriptDir = path.Join("services", "registration")
|
|
||||||
)
|
|
||||||
|
|
||||||
type atRequestParser struct{}
|
|
||||||
|
|
||||||
func (arp *atRequestParser) GetSessionId(rq any) (string, error) {
|
|
||||||
rqv, ok := rq.(*http.Request)
|
|
||||||
if !ok {
|
|
||||||
return "", handlers.ErrInvalidRequest
|
|
||||||
}
|
|
||||||
if err := rqv.ParseForm(); err != nil {
|
|
||||||
return "", fmt.Errorf("failed to parse form data: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
phoneNumber := rqv.FormValue("phoneNumber")
|
|
||||||
if phoneNumber == "" {
|
|
||||||
return "", fmt.Errorf("no phone number found")
|
|
||||||
}
|
|
||||||
|
|
||||||
return phoneNumber, 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() {
|
|
||||||
var dbDir string
|
|
||||||
var resourceDir string
|
|
||||||
var size uint
|
|
||||||
var engineDebug bool
|
|
||||||
var host string
|
|
||||||
var port uint
|
|
||||||
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
|
||||||
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
|
|
||||||
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
|
||||||
flag.UintVar(&size, "s", 160, "max size of output")
|
|
||||||
flag.StringVar(&host, "h", "127.0.0.1", "http host")
|
|
||||||
flag.UintVar(&port, "p", 7123, "http port")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
pfp := path.Join(scriptDir, "pp.csv")
|
|
||||||
|
|
||||||
cfg := engine.Config{
|
|
||||||
Root: "root",
|
|
||||||
OutputSize: uint32(size),
|
|
||||||
FlagCount: uint32(128),
|
|
||||||
}
|
|
||||||
|
|
||||||
if engineDebug {
|
|
||||||
cfg.EngineDebug = true
|
|
||||||
}
|
|
||||||
|
|
||||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
|
||||||
rs, err := menuStorageService.GetResource(ctx)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = menuStorageService.EnsureDbDir()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
userdataStore, err := menuStorageService.GetUserdataDb(ctx)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer userdataStore.Close()
|
|
||||||
|
|
||||||
dbResource, ok := rs.(*resource.DbResource)
|
|
||||||
if !ok {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
lhs, err := handlers.NewLocalHandlerService(pfp, true, dbResource, cfg, rs)
|
|
||||||
lhs.SetDataStore(&userdataStore)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
hl, err := lhs.GetHandler()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
stateStore, err := menuStorageService.GetStateStore(ctx)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer stateStore.Close()
|
|
||||||
|
|
||||||
rp := &atRequestParser{}
|
|
||||||
bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
|
|
||||||
sh := httpserver.NewATSessionHandler(bsh)
|
|
||||||
s := &http.Server{
|
|
||||||
Addr: fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))),
|
|
||||||
Handler: sh,
|
|
||||||
}
|
|
||||||
s.RegisterOnShutdown(sh.Shutdown)
|
|
||||||
|
|
||||||
cint := make(chan os.Signal)
|
|
||||||
cterm := make(chan os.Signal)
|
|
||||||
signal.Notify(cint, os.Interrupt, syscall.SIGINT)
|
|
||||||
signal.Notify(cterm, os.Interrupt, syscall.SIGTERM)
|
|
||||||
go func() {
|
|
||||||
select {
|
|
||||||
case _ = <-cint:
|
|
||||||
case _ = <-cterm:
|
|
||||||
}
|
|
||||||
s.Shutdown(ctx)
|
|
||||||
}()
|
|
||||||
err = s.ListenAndServe()
|
|
||||||
if err != nil {
|
|
||||||
logg.Infof("Server closed with error", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"path"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"git.defalsify.org/vise.git/engine"
|
|
||||||
"git.defalsify.org/vise.git/logging"
|
|
||||||
"git.defalsify.org/vise.git/resource"
|
|
||||||
|
|
||||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
|
||||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
logg = logging.NewVanilla()
|
|
||||||
scriptDir = path.Join("services", "registration")
|
|
||||||
)
|
|
||||||
|
|
||||||
type asyncRequestParser struct {
|
|
||||||
sessionId string
|
|
||||||
input []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *asyncRequestParser) GetSessionId(r any) (string, error) {
|
|
||||||
return p.sessionId, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *asyncRequestParser) GetInput(r any) ([]byte, error) {
|
|
||||||
return p.input, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var sessionId string
|
|
||||||
var dbDir string
|
|
||||||
var resourceDir string
|
|
||||||
var size uint
|
|
||||||
var engineDebug bool
|
|
||||||
var host string
|
|
||||||
var port uint
|
|
||||||
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.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", "http host")
|
|
||||||
flag.UintVar(&port, "p", 7123, "http port")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
pfp := path.Join(scriptDir, "pp.csv")
|
|
||||||
|
|
||||||
cfg := engine.Config{
|
|
||||||
Root: "root",
|
|
||||||
OutputSize: uint32(size),
|
|
||||||
FlagCount: uint32(128),
|
|
||||||
}
|
|
||||||
|
|
||||||
if engineDebug {
|
|
||||||
cfg.EngineDebug = true
|
|
||||||
}
|
|
||||||
|
|
||||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
|
||||||
rs, err := menuStorageService.GetResource(ctx)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = menuStorageService.EnsureDbDir()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
userdataStore, err := menuStorageService.GetUserdataDb(ctx)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer userdataStore.Close()
|
|
||||||
|
|
||||||
dbResource, ok := rs.(*resource.DbResource)
|
|
||||||
if !ok {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
lhs, err := handlers.NewLocalHandlerService(pfp, true, dbResource, cfg, rs)
|
|
||||||
lhs.SetDataStore(&userdataStore)
|
|
||||||
|
|
||||||
hl, err := lhs.GetHandler()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
stateStore, err := menuStorageService.GetStateStore(ctx)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer stateStore.Close()
|
|
||||||
|
|
||||||
rp := &asyncRequestParser{
|
|
||||||
sessionId: sessionId,
|
|
||||||
}
|
|
||||||
sh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
|
|
||||||
cfg.SessionId = sessionId
|
|
||||||
rqs := handlers.RequestSession{
|
|
||||||
Ctx: ctx,
|
|
||||||
Writer: os.Stdout,
|
|
||||||
Config: cfg,
|
|
||||||
}
|
|
||||||
|
|
||||||
cint := make(chan os.Signal)
|
|
||||||
cterm := make(chan os.Signal)
|
|
||||||
signal.Notify(cint, os.Interrupt, syscall.SIGINT)
|
|
||||||
signal.Notify(cterm, os.Interrupt, syscall.SIGTERM)
|
|
||||||
go func() {
|
|
||||||
select {
|
|
||||||
case _ = <-cint:
|
|
||||||
case _ = <-cterm:
|
|
||||||
}
|
|
||||||
sh.Shutdown()
|
|
||||||
}()
|
|
||||||
|
|
||||||
for true {
|
|
||||||
rqs, err = sh.Process(rqs)
|
|
||||||
if err != nil {
|
|
||||||
logg.ErrorCtxf(ctx, "error in process: %v", "err", err)
|
|
||||||
fmt.Errorf("error in process: %v", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
rqs, err = sh.Output(rqs)
|
|
||||||
if err != nil {
|
|
||||||
logg.ErrorCtxf(ctx, "error in output: %v", "err", err)
|
|
||||||
fmt.Errorf("error in output: %v", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
rqs, err = sh.Reset(rqs)
|
|
||||||
if err != nil {
|
|
||||||
logg.ErrorCtxf(ctx, "error in reset: %v", "err", err)
|
|
||||||
fmt.Errorf("error in reset: %v", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
fmt.Println("")
|
|
||||||
_, err = fmt.Scanln(&rqs.Input)
|
|
||||||
if err != nil {
|
|
||||||
logg.ErrorCtxf(ctx, "error in input", "err", err)
|
|
||||||
fmt.Errorf("error in input: %v", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
137
cmd/http/main.go
137
cmd/http/main.go
@@ -11,64 +11,159 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"git.defalsify.org/vise.git/asm"
|
||||||
|
"git.defalsify.org/vise.git/db"
|
||||||
|
fsdb "git.defalsify.org/vise.git/db/fs"
|
||||||
|
gdbmdb "git.defalsify.org/vise.git/db/gdbm"
|
||||||
"git.defalsify.org/vise.git/engine"
|
"git.defalsify.org/vise.git/engine"
|
||||||
"git.defalsify.org/vise.git/logging"
|
|
||||||
"git.defalsify.org/vise.git/resource"
|
"git.defalsify.org/vise.git/resource"
|
||||||
|
"git.defalsify.org/vise.git/logging"
|
||||||
|
|
||||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
"git.grassecon.net/urdt/ussd/internal/handlers/ussd"
|
||||||
httpserver "git.grassecon.net/urdt/ussd/internal/http"
|
httpserver "git.grassecon.net/urdt/ussd/internal/http"
|
||||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
logg = logging.NewVanilla()
|
logg = logging.NewVanilla()
|
||||||
scriptDir = path.Join("services", "registration")
|
scriptDir = path.Join("services", "registration")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func getFlags(fp string, debug bool) (*asm.FlagParser, error) {
|
||||||
|
flagParser := asm.NewFlagParser().WithDebug()
|
||||||
|
_, err := flagParser.Load(fp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return flagParser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHandler(appFlags *asm.FlagParser, rs *resource.DbResource, userdataStore db.Db) (*ussd.Handlers, error) {
|
||||||
|
|
||||||
|
ussdHandlers, err := ussd.NewHandlers(appFlags, userdataStore)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rs.AddLocalFunc("select_language", ussdHandlers.SetLanguage)
|
||||||
|
rs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
|
||||||
|
rs.AddLocalFunc("save_pin", ussdHandlers.SavePin)
|
||||||
|
rs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin)
|
||||||
|
rs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier)
|
||||||
|
rs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus)
|
||||||
|
rs.AddLocalFunc("authorize_account", ussdHandlers.Authorize)
|
||||||
|
rs.AddLocalFunc("quit", ussdHandlers.Quit)
|
||||||
|
rs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance)
|
||||||
|
rs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient)
|
||||||
|
rs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset)
|
||||||
|
rs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount)
|
||||||
|
rs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount)
|
||||||
|
rs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount)
|
||||||
|
rs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient)
|
||||||
|
rs.AddLocalFunc("get_sender", ussdHandlers.GetSender)
|
||||||
|
rs.AddLocalFunc("get_amount", ussdHandlers.GetAmount)
|
||||||
|
rs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin)
|
||||||
|
rs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname)
|
||||||
|
rs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname)
|
||||||
|
rs.AddLocalFunc("save_gender", ussdHandlers.SaveGender)
|
||||||
|
rs.AddLocalFunc("save_location", ussdHandlers.SaveLocation)
|
||||||
|
rs.AddLocalFunc("save_yob", ussdHandlers.SaveYob)
|
||||||
|
rs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings)
|
||||||
|
rs.AddLocalFunc("quit_with_balance", ussdHandlers.QuitWithBalance)
|
||||||
|
rs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized)
|
||||||
|
rs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate)
|
||||||
|
rs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo)
|
||||||
|
rs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob)
|
||||||
|
rs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob)
|
||||||
|
rs.AddLocalFunc("set_reset_single_edit", ussdHandlers.SetResetSingleEdit)
|
||||||
|
rs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction)
|
||||||
|
|
||||||
|
return ussdHandlers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureDbDir(dbDir string) error {
|
||||||
|
err := os.MkdirAll(dbDir, 0700)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("state dir create exited with error: %v\n", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStateStore(dbDir string, ctx context.Context) (db.Db, error) {
|
||||||
|
store := gdbmdb.NewGdbmDb()
|
||||||
|
storeFile := path.Join(dbDir, "state.gdbm")
|
||||||
|
store.Connect(ctx, storeFile)
|
||||||
|
return store, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUserdataDb(dbDir string, ctx context.Context) db.Db {
|
||||||
|
store := gdbmdb.NewGdbmDb()
|
||||||
|
storeFile := path.Join(dbDir, "userdata.gdbm")
|
||||||
|
store.Connect(ctx, storeFile)
|
||||||
|
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
func getResource(resourceDir string, ctx context.Context) (resource.Resource, error) {
|
||||||
|
store := fsdb.NewFsDb()
|
||||||
|
err := store.Connect(ctx, resourceDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rfs := resource.NewDbResource(store)
|
||||||
|
return rfs, nil
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var dbDir string
|
var dbDir string
|
||||||
var resourceDir string
|
var resourceDir string
|
||||||
var size uint
|
var size uint
|
||||||
var engineDebug bool
|
var engineDebug 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(&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.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
flag.BoolVar(&engineDebug, "engine-debug", false, "use engine debug output")
|
||||||
|
flag.BoolVar(&stateDebug, "state-debug", false, "use engine debug output")
|
||||||
flag.UintVar(&size, "s", 160, "max size of output")
|
flag.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", "http host")
|
||||||
flag.UintVar(&port, "p", 7123, "http port")
|
flag.UintVar(&port, "p", 7123, "http port")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
|
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
pfp := path.Join(scriptDir, "pp.csv")
|
pfp := path.Join(scriptDir, "pp.csv")
|
||||||
|
flagParser, err := getFlags(pfp, true)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
cfg := engine.Config{
|
cfg := engine.Config{
|
||||||
Root: "root",
|
Root: "root",
|
||||||
OutputSize: uint32(size),
|
OutputSize: uint32(size),
|
||||||
FlagCount: uint32(128),
|
FlagCount: uint32(16),
|
||||||
|
}
|
||||||
|
if stateDebug {
|
||||||
|
cfg.StateDebug = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if engineDebug {
|
if engineDebug {
|
||||||
cfg.EngineDebug = true
|
cfg.EngineDebug = true
|
||||||
}
|
}
|
||||||
|
|
||||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
rs, err := getResource(resourceDir, 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()
|
err = ensureDbDir(dbDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
fmt.Fprintf(os.Stderr, err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
userdataStore, err := menuStorageService.GetUserdataDb(ctx)
|
userdataStore := getUserdataDb(dbDir, 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)
|
||||||
@@ -80,21 +175,13 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
lhs, err := handlers.NewLocalHandlerService(pfp, true, dbResource, cfg, rs)
|
hl, err := getHandler(flagParser, dbResource, userdataStore)
|
||||||
lhs.SetDataStore(&userdataStore)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
fmt.Fprintf(os.Stderr, err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
hl, err := lhs.GetHandler()
|
stateStore, err := getStateStore(dbDir, ctx)
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
stateStore, err := menuStorageService.GetStateStore(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)
|
||||||
@@ -102,10 +189,10 @@ func main() {
|
|||||||
defer stateStore.Close()
|
defer stateStore.Close()
|
||||||
|
|
||||||
rp := &httpserver.DefaultRequestParser{}
|
rp := &httpserver.DefaultRequestParser{}
|
||||||
bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
|
//sh := httpserver.NewSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl.Init)
|
||||||
sh := httpserver.ToSessionHandler(bsh)
|
sh := httpserver.NewSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
|
||||||
s := &http.Server{
|
s := &http.Server{
|
||||||
Addr: fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))),
|
Addr: fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))),
|
||||||
Handler: sh,
|
Handler: sh,
|
||||||
}
|
}
|
||||||
s.RegisterOnShutdown(sh.Shutdown)
|
s.RegisterOnShutdown(sh.Shutdown)
|
||||||
|
|||||||
154
cmd/main.go
154
cmd/main.go
@@ -7,11 +7,15 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
|
"git.defalsify.org/vise.git/asm"
|
||||||
|
"git.defalsify.org/vise.git/db"
|
||||||
|
fsdb "git.defalsify.org/vise.git/db/fs"
|
||||||
|
gdbmdb "git.defalsify.org/vise.git/db/gdbm"
|
||||||
"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.defalsify.org/vise.git/persist"
|
||||||
"git.defalsify.org/vise.git/resource"
|
"git.defalsify.org/vise.git/resource"
|
||||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
"git.grassecon.net/urdt/ussd/internal/handlers/ussd"
|
||||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -19,14 +23,111 @@ var (
|
|||||||
scriptDir = path.Join("services", "registration")
|
scriptDir = path.Join("services", "registration")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHandler(appFlags *asm.FlagParser, rs *resource.DbResource, pe *persist.Persister, userdataStore db.Db) (*ussd.Handlers, error) {
|
||||||
|
|
||||||
|
ussdHandlers, err := ussd.NewHandlers(appFlags, userdataStore)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ussdHandlers = ussdHandlers.WithPersister(pe)
|
||||||
|
rs.AddLocalFunc("select_language", ussdHandlers.SetLanguage)
|
||||||
|
rs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
|
||||||
|
rs.AddLocalFunc("save_pin", ussdHandlers.SavePin)
|
||||||
|
rs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin)
|
||||||
|
rs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier)
|
||||||
|
rs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus)
|
||||||
|
rs.AddLocalFunc("authorize_account", ussdHandlers.Authorize)
|
||||||
|
rs.AddLocalFunc("quit", ussdHandlers.Quit)
|
||||||
|
rs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance)
|
||||||
|
rs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient)
|
||||||
|
rs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset)
|
||||||
|
rs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount)
|
||||||
|
rs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount)
|
||||||
|
rs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount)
|
||||||
|
rs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient)
|
||||||
|
rs.AddLocalFunc("get_sender", ussdHandlers.GetSender)
|
||||||
|
rs.AddLocalFunc("get_amount", ussdHandlers.GetAmount)
|
||||||
|
rs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin)
|
||||||
|
rs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname)
|
||||||
|
rs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname)
|
||||||
|
rs.AddLocalFunc("save_gender", ussdHandlers.SaveGender)
|
||||||
|
rs.AddLocalFunc("save_location", ussdHandlers.SaveLocation)
|
||||||
|
rs.AddLocalFunc("save_yob", ussdHandlers.SaveYob)
|
||||||
|
rs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings)
|
||||||
|
rs.AddLocalFunc("quit_with_balance", ussdHandlers.QuitWithBalance)
|
||||||
|
rs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized)
|
||||||
|
rs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate)
|
||||||
|
rs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo)
|
||||||
|
rs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob)
|
||||||
|
rs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob)
|
||||||
|
rs.AddLocalFunc("set_reset_single_edit", ussdHandlers.SetResetSingleEdit)
|
||||||
|
rs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction)
|
||||||
|
rs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp)
|
||||||
|
|
||||||
|
return ussdHandlers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureDbDir(dbDir string) error {
|
||||||
|
err := os.MkdirAll(dbDir, 0700)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("state dir create exited with error: %v\n", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPersister(dbDir string, ctx context.Context) (*persist.Persister, error) {
|
||||||
|
err := ensureDbDir(dbDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
store := gdbmdb.NewGdbmDb()
|
||||||
|
storeFile := path.Join(dbDir, "state.gdbm")
|
||||||
|
store.Connect(ctx, storeFile)
|
||||||
|
pr := persist.NewPersister(store)
|
||||||
|
return pr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUserdataDb(dbDir string, ctx context.Context) db.Db {
|
||||||
|
store := gdbmdb.NewGdbmDb()
|
||||||
|
storeFile := path.Join(dbDir, "userdata.gdbm")
|
||||||
|
store.Connect(ctx, storeFile)
|
||||||
|
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
func getResource(resourceDir string, ctx context.Context) (resource.Resource, error) {
|
||||||
|
store := fsdb.NewFsDb()
|
||||||
|
err := store.Connect(ctx, resourceDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rfs := resource.NewDbResource(store)
|
||||||
|
return rfs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) *engine.DefaultEngine {
|
||||||
|
en := engine.NewEngine(cfg, rs)
|
||||||
|
en = en.WithPersister(pr)
|
||||||
|
return en
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var dbDir string
|
var dbDir string
|
||||||
var size uint
|
var size uint
|
||||||
var sessionId string
|
var sessionId string
|
||||||
var engineDebug bool
|
var debug bool
|
||||||
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(&dbDir, "dbdir", ".state", "database dir to read from")
|
||||||
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
flag.BoolVar(&debug, "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.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
@@ -35,36 +136,32 @@ func main() {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||||
pfp := path.Join(scriptDir, "pp.csv")
|
pfp := path.Join(scriptDir, "pp.csv")
|
||||||
|
flagParser, err := getParser(pfp, true)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
cfg := engine.Config{
|
cfg := engine.Config{
|
||||||
Root: "root",
|
Root: "root",
|
||||||
SessionId: sessionId,
|
SessionId: sessionId,
|
||||||
OutputSize: uint32(size),
|
OutputSize: uint32(size),
|
||||||
FlagCount: uint32(128),
|
FlagCount: uint32(16),
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceDir := scriptDir
|
rs, err := getResource(scriptDir, ctx)
|
||||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
|
||||||
|
|
||||||
err := menuStorageService.EnsureDbDir()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
fmt.Fprintf(os.Stderr, err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
rs, err := menuStorageService.GetResource(ctx)
|
pe, err := getPersister(dbDir, 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
pe, err := menuStorageService.GetPersister(ctx)
|
store := getUserdataDb(dbDir, ctx)
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
userdatastore, err := menuStorageService.GetUserdataDb(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)
|
||||||
@@ -76,28 +173,25 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
lhs, err := handlers.NewLocalHandlerService(pfp, true, dbResource, cfg, rs)
|
hl, err := getHandler(flagParser, dbResource, pe, store)
|
||||||
lhs.SetDataStore(&userdatastore)
|
|
||||||
lhs.SetPersister(pe)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
fmt.Fprintf(os.Stderr, err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
hl, err := lhs.GetHandler()
|
en := getEngine(cfg, rs, pe)
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
en := lhs.GetEngine()
|
|
||||||
en = en.WithFirst(hl.Init)
|
en = en.WithFirst(hl.Init)
|
||||||
if engineDebug {
|
if debug {
|
||||||
en = en.WithDebug(nil)
|
en = en.WithDebug(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = engine.Loop(ctx, en, os.Stdin, os.Stdout, nil)
|
_, err = en.Init(ctx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "engine init exited with error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = engine.Loop(ctx, en, os.Stdin, os.Stdout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "loop exited with error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "loop exited with error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CreateAccountURL = "http://localhost:5003/api/v2/account/create"
|
CreateAccountURL = "https://custodial.sarafu.africa/api/account/create"
|
||||||
BalanceURL = "https://custodial.sarafu.africa/api/account/status/"
|
TrackStatusURL = "https://custodial.sarafu.africa/api/track/"
|
||||||
TrackURL = "http://localhost:5003/api/v2/account/status"
|
BalanceURL = "https://custodial.sarafu.africa/api/account/status/"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
4
go.mod
4
go.mod
@@ -3,12 +3,14 @@ module git.grassecon.net/urdt/ussd
|
|||||||
go 1.22.6
|
go 1.22.6
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb
|
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240911162138-1f2af8672dc7
|
||||||
github.com/alecthomas/assert/v2 v2.2.2
|
github.com/alecthomas/assert/v2 v2.2.2
|
||||||
github.com/peteole/testdata-loader v0.3.0
|
github.com/peteole/testdata-loader v0.3.0
|
||||||
gopkg.in/leonelquinteros/gotext.v1 v1.3.1
|
gopkg.in/leonelquinteros/gotext.v1 v1.3.1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require gopkg.in/dnaeon/go-vcr.v4 v4.0.1 // indirect
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alecthomas/participle/v2 v2.0.0 // indirect
|
github.com/alecthomas/participle/v2 v2.0.0 // indirect
|
||||||
github.com/alecthomas/repr v0.2.0 // indirect
|
github.com/alecthomas/repr v0.2.0 // indirect
|
||||||
|
|||||||
6
go.sum
6
go.sum
@@ -1,5 +1,5 @@
|
|||||||
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb h1:6P4kxihcwMjDKzvUFC6t2zGNb7MDW+l/ACGlSAN1N8Y=
|
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240911162138-1f2af8672dc7 h1:embPZDx0Sgpq6jp9vcZ1GVI0eum3PsPCmAfxAa/1KLI=
|
||||||
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M=
|
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240911162138-1f2af8672dc7/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M=
|
||||||
github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk=
|
github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk=
|
||||||
github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
|
github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
|
||||||
github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g=
|
github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g=
|
||||||
@@ -30,6 +30,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
|||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/dnaeon/go-vcr.v4 v4.0.1 h1:dIFuOqqDZIJ9BTcK+DXmElzypQ6PV9fBQZSIwY+J1yM=
|
||||||
|
gopkg.in/dnaeon/go-vcr.v4 v4.0.1/go.mod h1:65yxh9goQVrudqofKtHA4JNFWd6XZRkWfKN4YpMx7KI=
|
||||||
gopkg.in/leonelquinteros/gotext.v1 v1.3.1 h1:8d9/fdTG0kn/B7NNGV1BsEyvektXFAbkMsTZS2sFSCc=
|
gopkg.in/leonelquinteros/gotext.v1 v1.3.1 h1:8d9/fdTG0kn/B7NNGV1BsEyvektXFAbkMsTZS2sFSCc=
|
||||||
gopkg.in/leonelquinteros/gotext.v1 v1.3.1/go.mod h1:X1WlGDeAFIYsW6GjgMm4VwUwZ2XjI7Zan2InxSUQWrU=
|
gopkg.in/leonelquinteros/gotext.v1 v1.3.1/go.mod h1:X1WlGDeAFIYsW6GjgMm4VwUwZ2XjI7Zan2InxSUQWrU=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.defalsify.org/vise.git/db"
|
|
||||||
"git.defalsify.org/vise.git/engine"
|
|
||||||
"git.defalsify.org/vise.git/persist"
|
|
||||||
"git.defalsify.org/vise.git/resource"
|
|
||||||
|
|
||||||
"git.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 *ussd.Handlers
|
|
||||||
provider storage.StorageProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
en := engine.NewEngine(cfg, rs)
|
|
||||||
en = en.WithPersister(pr)
|
|
||||||
return en
|
|
||||||
}
|
|
||||||
|
|
||||||
func(f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error) {
|
|
||||||
var r bool
|
|
||||||
var err error
|
|
||||||
var ok bool
|
|
||||||
|
|
||||||
logg.InfoCtxf(rqs.Ctx, "new request", "data", rqs)
|
|
||||||
|
|
||||||
rqs.Storage, err = f.provider.Get(rqs.Config.SessionId)
|
|
||||||
if err != nil {
|
|
||||||
logg.ErrorCtxf(rqs.Ctx, "", "storage get error", err)
|
|
||||||
return rqs, ErrStorage
|
|
||||||
}
|
|
||||||
|
|
||||||
f.hn = f.hn.WithPersister(rqs.Storage.Persister)
|
|
||||||
eni := f.GetEngine(rqs.Config, f.rs, rqs.Storage.Persister)
|
|
||||||
en, ok := eni.(*engine.DefaultEngine)
|
|
||||||
if !ok {
|
|
||||||
perr := f.provider.Put(rqs.Config.SessionId, rqs.Storage)
|
|
||||||
rqs.Storage = nil
|
|
||||||
if perr != nil {
|
|
||||||
logg.ErrorCtxf(rqs.Ctx, "", "storage put error", perr)
|
|
||||||
}
|
|
||||||
return rqs, ErrEngineType
|
|
||||||
}
|
|
||||||
en = en.WithFirst(f.hn.Init)
|
|
||||||
if rqs.Config.EngineDebug {
|
|
||||||
en = en.WithDebug(nil)
|
|
||||||
}
|
|
||||||
rqs.Engine = en
|
|
||||||
|
|
||||||
r, err = rqs.Engine.Exec(rqs.Ctx, rqs.Input)
|
|
||||||
if err != nil {
|
|
||||||
perr := f.provider.Put(rqs.Config.SessionId, rqs.Storage)
|
|
||||||
rqs.Storage = nil
|
|
||||||
if perr != nil {
|
|
||||||
logg.ErrorCtxf(rqs.Ctx, "", "storage put error", perr)
|
|
||||||
}
|
|
||||||
return rqs, err
|
|
||||||
}
|
|
||||||
|
|
||||||
rqs.Continue = r
|
|
||||||
return rqs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
defer f.provider.Put(rqs.Config.SessionId, rqs.Storage)
|
|
||||||
return rqs, rqs.Engine.Finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
func(f *BaseSessionHandler) GetConfig() engine.Config {
|
|
||||||
return f.cfgTemplate
|
|
||||||
}
|
|
||||||
|
|
||||||
func(f *BaseSessionHandler) GetRequestParser() RequestParser {
|
|
||||||
return f.rp
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"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"
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
Cfg engine.Config
|
|
||||||
Rs resource.Resource
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLocalHandlerService(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
|
|
||||||
}
|
|
||||||
return &LocalHandlerService{
|
|
||||||
Parser: parser,
|
|
||||||
DbRs: dbResource,
|
|
||||||
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() (*ussd.Handlers, error) {
|
|
||||||
ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore)
|
|
||||||
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_pin", ussdHandlers.SavePin)
|
|
||||||
ls.DbRs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin)
|
|
||||||
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("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("save_temporary_pin", ussdHandlers.SaveTemporaryPin)
|
|
||||||
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_custodial_balances", ussdHandlers.FetchCustodialBalances)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
129
internal/handlers/server/account_service_offline_test.go
Normal file
129
internal/handlers/server/account_service_offline_test.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
//go:build !online
|
||||||
|
// +build !online
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alecthomas/assert/v2"
|
||||||
|
"gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCheckBalanceOffline(t *testing.T) {
|
||||||
|
r, err := recorder.New("custodial/balance")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer r.Stop()
|
||||||
|
|
||||||
|
client := r.GetDefaultClient()
|
||||||
|
|
||||||
|
as := AccountService{
|
||||||
|
Client: client,
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
balance string
|
||||||
|
publicKey string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Test check balance with correct public key",
|
||||||
|
publicKey: "0x216a4A64E1e699F9d65Dd9CbD0058dAB21DeF002",
|
||||||
|
balance: "3.06000000003 CELO",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test check balance with public key that doesn't exist in the custodial system",
|
||||||
|
balance: "",
|
||||||
|
publicKey: "0x216a4A64E1e699F9d65Dd9CbD0058dAB21DeF00",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
balance, err := as.CheckBalance(tt.publicKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get balance with error %s", err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, balance, tt.balance, "Expected balance and actual balance should be equal")
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckAccountStatusOffline(t *testing.T) {
|
||||||
|
r, err := recorder.New("custodial/status")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer r.Stop()
|
||||||
|
|
||||||
|
client := r.GetDefaultClient()
|
||||||
|
|
||||||
|
as := AccountService{
|
||||||
|
Client: client,
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
status string
|
||||||
|
trackingId string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Test check status with tracking id that exists in the custodial system",
|
||||||
|
trackingId: "bb23945b-65cd-4110-ac2e-a5df40572e18",
|
||||||
|
status: "SUCCESS",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test check status with tracking id that doesn't exist in the custodial system",
|
||||||
|
status: "",
|
||||||
|
trackingId: "bb23945b-65cd-4110-ac2e-a5df40572e1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
status, err := as.CheckAccountStatus(tt.trackingId)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to account status with error %s", err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, status, tt.status, "Expected status and actual status should be equal")
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateAccountOffline(t *testing.T) {
|
||||||
|
r, err := recorder.New("custodial/create")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer r.Stop()
|
||||||
|
|
||||||
|
client := r.GetDefaultClient()
|
||||||
|
|
||||||
|
as := AccountService{
|
||||||
|
Client: client,
|
||||||
|
}
|
||||||
|
accountRes, err := as.CreateAccount()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create an account with error %s", err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, accountRes.Ok, true, "account response status is true")
|
||||||
|
|
||||||
|
}
|
||||||
130
internal/handlers/server/account_service_online_test.go
Normal file
130
internal/handlers/server/account_service_online_test.go
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
//go:build online
|
||||||
|
// +build online
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alecthomas/assert/v2"
|
||||||
|
"gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCheckBalance(t *testing.T) {
|
||||||
|
r, err := recorder.New("custodial/balance")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer r.Stop()
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
as := AccountService{
|
||||||
|
Client: client,
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
balance string
|
||||||
|
publicKey string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Test check balance with correct public key",
|
||||||
|
publicKey: "0x216a4A64E1e699F9d65Dd9CbD0058dAB21DeF002",
|
||||||
|
balance: "3.06000000003 CELO",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test check balance with public key that doesn't exist in the custodial system",
|
||||||
|
balance: "",
|
||||||
|
publicKey: "0x216a4A64E1e699F9d65Dd9CbD0058dAB21DeF00",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
balance, err := as.CheckBalance(tt.publicKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get balance with error %s", err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, balance, tt.balance, "Expected balance and actual balance should be equal")
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckAccountStatus(t *testing.T) {
|
||||||
|
r, err := recorder.New("custodial/status")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer r.Stop()
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
as := AccountService{
|
||||||
|
Client: client,
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
status string
|
||||||
|
trackingId string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Test check status with tracking id that exists in the custodial system",
|
||||||
|
trackingId: "bb23945b-65cd-4110-ac2e-a5df40572e18",
|
||||||
|
status: "SUCCESS",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test check status with tracking id that doesn't exist in the custodial system",
|
||||||
|
status: "",
|
||||||
|
trackingId: "bb23945b-65cd-4110-ac2e-a5df40572e1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
status, err := as.CheckAccountStatus(tt.trackingId)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to account status with error %s", err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, status, tt.status, "Expected status and actual status should be equal")
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateAccount(t *testing.T) {
|
||||||
|
r, err := recorder.New("custodial/create")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer r.Stop()
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
as := AccountService{
|
||||||
|
Client: client,
|
||||||
|
}
|
||||||
|
accountRes, err := as.CreateAccount()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create an account with error %s", err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, accountRes.Ok, true, "account response status is true")
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
@@ -10,175 +9,108 @@ import (
|
|||||||
"git.grassecon.net/urdt/ussd/internal/models"
|
"git.grassecon.net/urdt/ussd/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
var apiResponse struct {
|
|
||||||
Ok bool `json:"ok"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AccountServiceInterface interface {
|
type AccountServiceInterface interface {
|
||||||
CheckBalance(publicKey string) (*models.BalanceResponse, error)
|
CheckBalance(publicKey string) (string, error)
|
||||||
CreateAccount() (*OKResponse, *ErrResponse)
|
CreateAccount() (*models.AccountResponse, error)
|
||||||
CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error)
|
CheckAccountStatus(trackingId string) (string, error)
|
||||||
TrackAccountStatus(publicKey string) (*OKResponse, *ErrResponse)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccountService struct {
|
type AccountService struct {
|
||||||
|
Client *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// CheckAccountStatus retrieves the status of an account transaction based on the provided tracking ID.
|
||||||
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// - trackingId: A unique identifier for the account.This should be obtained from a previous call to
|
// - trackingId: A unique identifier for the account.This should be obtained from a previous call to
|
||||||
// CreateAccount or a similar function that returns an AccountResponse. The `trackingId` field in the
|
// CreateAccount or a similar function that returns an AccountResponse. The `trackingId` field in the
|
||||||
// AccountResponse struct can be used here to check the account status during a transaction.
|
// AccountResponse struct can be used here to check the account status during a transaction.
|
||||||
//
|
//
|
||||||
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - string: The status of the transaction as a string. If there is an error during the request or processing, this will be an empty string.
|
// - string: The status of the transaction as a string. If there is an error during the request or processing, this will be an empty string.
|
||||||
// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
|
// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
|
||||||
// If no error occurs, this will be nil
|
// If no error occurs, this will be nil.
|
||||||
func (as *AccountService) CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error) {
|
//
|
||||||
resp, err := http.Get(config.BalanceURL + trackingId)
|
func (as *AccountService) CheckAccountStatus(trackingId string) (string, error) {
|
||||||
|
resp,err := as.Client.Get(config.TrackStatusURL + trackingId)
|
||||||
|
// resp, err := http.Get(config.TrackStatusURL + trackingId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return "", err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var trackResp models.TrackStatusResponse
|
var trackResp models.TrackStatusResponse
|
||||||
err = json.Unmarshal(body, &trackResp)
|
err = json.Unmarshal(body, &trackResp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return "", err
|
||||||
}
|
}
|
||||||
return &trackResp, nil
|
|
||||||
|
|
||||||
|
status := trackResp.Result.Transaction.Status
|
||||||
|
|
||||||
|
return status, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (as *AccountService) TrackAccountStatus(publicKey string) (*OKResponse, *ErrResponse) {
|
|
||||||
var errResponse ErrResponse
|
|
||||||
var okResponse OKResponse
|
|
||||||
var err error
|
|
||||||
// Construct the URL with the path parameter
|
|
||||||
url := fmt.Sprintf("%s/%s", config.TrackURL, publicKey)
|
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
|
||||||
if err != nil {
|
|
||||||
errResponse.Description = err.Error()
|
|
||||||
return nil, &errResponse
|
|
||||||
}
|
|
||||||
// Set headers
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
req.Header.Set("X-GE-KEY", "xd")
|
|
||||||
|
|
||||||
// Send the request
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
errResponse.Description = err.Error()
|
|
||||||
return nil, &errResponse
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// Read the response body
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
errResponse.Description = err.Error()
|
|
||||||
return nil, &errResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Unmarshal into the generic struct
|
|
||||||
err = json.Unmarshal([]byte(body), &apiResponse)
|
|
||||||
if err != nil {
|
|
||||||
errResponse.Description = err.Error()
|
|
||||||
return nil, &errResponse
|
|
||||||
}
|
|
||||||
if apiResponse.Ok {
|
|
||||||
err = json.Unmarshal([]byte(body), &okResponse)
|
|
||||||
if err != nil {
|
|
||||||
errResponse.Description = err.Error()
|
|
||||||
return nil, &errResponse
|
|
||||||
}
|
|
||||||
return &okResponse, nil
|
|
||||||
} else {
|
|
||||||
err := json.Unmarshal([]byte(body), &errResponse)
|
|
||||||
if err != nil {
|
|
||||||
errResponse.Description = err.Error()
|
|
||||||
return nil, &errResponse
|
|
||||||
}
|
|
||||||
return nil, &errResponse
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckBalance retrieves the balance for a given public key from the custodial balance API endpoint.
|
// CheckBalance retrieves the balance for a given public key from the custodial balance API endpoint.
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// - publicKey: The public key associated with the account whose balance needs to be checked.
|
// - publicKey: The public key associated with the account whose balance needs to be checked.
|
||||||
func (as *AccountService) CheckBalance(publicKey string) (*models.BalanceResponse, error) {
|
func (as *AccountService) CheckBalance(publicKey string) (string, error) {
|
||||||
resp, err := http.Get(config.BalanceURL + publicKey)
|
|
||||||
|
//resp, err := http.Get(config.BalanceURL + publicKey)
|
||||||
|
resp, err := as.Client.Get(config.BalanceURL + publicKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return "0.0", err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return "0.0", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var balanceResp models.BalanceResponse
|
var balanceResp models.BalanceResponse
|
||||||
err = json.Unmarshal(body, &balanceResp)
|
err = json.Unmarshal(body, &balanceResp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return "0.0", err
|
||||||
}
|
}
|
||||||
return &balanceResp, nil
|
|
||||||
|
balance := balanceResp.Result.Balance
|
||||||
|
return balance, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateAccount creates a new account in the custodial system.
|
|
||||||
|
//CreateAccount creates a new account in the custodial system.
|
||||||
// Returns:
|
// Returns:
|
||||||
// - *models.AccountResponse: A pointer to an AccountResponse struct containing the details of the created account.
|
// - *models.AccountResponse: A pointer to an AccountResponse struct containing the details of the created account.
|
||||||
// If there is an error during the request or processing, this will be nil.
|
// If there is an error during the request or processing, this will be nil.
|
||||||
// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
|
// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
|
||||||
// If no error occurs, this will be nil.
|
// If no error occurs, this will be nil.
|
||||||
func (as *AccountService) CreateAccount() (*OKResponse, *ErrResponse) {
|
func (as *AccountService) CreateAccount() (*models.AccountResponse, error) {
|
||||||
|
//resp, err := http.Post(config.CreateAccountURL, "application/json", nil)
|
||||||
var errResponse ErrResponse
|
resp, err := as.Client.Post(config.CreateAccountURL, "application/json", nil)
|
||||||
var okResponse OKResponse
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// Create a new request
|
|
||||||
req, err := http.NewRequest("POST", config.CreateAccountURL, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errResponse.Description = err.Error()
|
return nil, err
|
||||||
return nil, &errResponse
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
req.Header.Set("X-GE-KEY", "xd")
|
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
errResponse.Description = err.Error()
|
|
||||||
return nil, &errResponse
|
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errResponse.Description = err.Error()
|
return nil, err
|
||||||
return nil, &errResponse
|
|
||||||
}
|
}
|
||||||
err = json.Unmarshal([]byte(body), &apiResponse)
|
|
||||||
|
var accountResp models.AccountResponse
|
||||||
|
err = json.Unmarshal(body, &accountResp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &errResponse
|
return nil, err
|
||||||
}
|
|
||||||
if apiResponse.Ok {
|
|
||||||
err = json.Unmarshal([]byte(body), &okResponse)
|
|
||||||
if err != nil {
|
|
||||||
errResponse.Description = err.Error()
|
|
||||||
return nil, &errResponse
|
|
||||||
}
|
|
||||||
return &okResponse, nil
|
|
||||||
} else {
|
|
||||||
err := json.Unmarshal([]byte(body), &errResponse)
|
|
||||||
if err != nil {
|
|
||||||
errResponse.Description = err.Error()
|
|
||||||
return nil, &errResponse
|
|
||||||
}
|
|
||||||
return nil, &errResponse
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return &accountResp, nil
|
||||||
}
|
}
|
||||||
|
|||||||
126
internal/handlers/server/accountservice_test.go
Normal file
126
internal/handlers/server/accountservice_test.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alecthomas/assert/v2"
|
||||||
|
"gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCheckBalance(t *testing.T) {
|
||||||
|
r, err := recorder.New("custodial/balance")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer r.Stop()
|
||||||
|
|
||||||
|
client := r.GetDefaultClient()
|
||||||
|
|
||||||
|
as := AccountService{
|
||||||
|
Client: client,
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
balance string
|
||||||
|
publicKey string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Test check balance with correct public key",
|
||||||
|
publicKey: "0x216a4A64E1e699F9d65Dd9CbD0058dAB21DeF002",
|
||||||
|
balance: "3.06000000003 CELO",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test check balance with public key that doesn't exist in the custodial system",
|
||||||
|
balance: "",
|
||||||
|
publicKey: "0x216a4A64E1e699F9d65Dd9CbD0058dAB21DeF00",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
balance, err := as.CheckBalance(tt.publicKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get balance with error %s", err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, balance, tt.balance, "Expected balance and actual balance should be equal")
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckAccountStatus(t *testing.T) {
|
||||||
|
r, err := recorder.New("custodial/status")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer r.Stop()
|
||||||
|
|
||||||
|
client := r.GetDefaultClient()
|
||||||
|
|
||||||
|
as := AccountService{
|
||||||
|
Client: client,
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
status string
|
||||||
|
trackingId string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Test check status with tracking id that exists in the custodial system",
|
||||||
|
trackingId: "bb23945b-65cd-4110-ac2e-a5df40572e18",
|
||||||
|
status: "SUCCESS",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test check status with tracking id that doesn't exist in the custodial system",
|
||||||
|
status: "",
|
||||||
|
trackingId: "bb23945b-65cd-4110-ac2e-a5df40572e1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
status, err := as.CheckAccountStatus(tt.trackingId)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to account status with error %s", err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, status, tt.status, "Expected status and actual status should be equal")
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateAccount(t *testing.T) {
|
||||||
|
r, err := recorder.New("custodial/create")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer r.Stop()
|
||||||
|
|
||||||
|
client := r.GetDefaultClient()
|
||||||
|
|
||||||
|
as := AccountService{
|
||||||
|
Client: client,
|
||||||
|
}
|
||||||
|
accountRes, err := as.CreateAccount()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create an account with error %s", err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, accountRes.Ok, true, "account response status is true")
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
type (
|
|
||||||
OKResponse struct {
|
|
||||||
Ok bool `json:"ok"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Result map[string]any `json:"result"`
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrResponse struct {
|
|
||||||
Ok bool `json:"ok"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
ErrCode string `json:"errorCode"`
|
|
||||||
}
|
|
||||||
|
|
||||||
TransferRequest struct {
|
|
||||||
From string `json:"from" validate:"required,eth_addr_checksum"`
|
|
||||||
To string `json:"to" validate:"required,eth_addr_checksum"`
|
|
||||||
TokenAddress string `json:"tokenAddress" validate:"required,eth_addr_checksum"`
|
|
||||||
Amount string `json:"amount" validate:"required,number,gt=0"`
|
|
||||||
}
|
|
||||||
|
|
||||||
PoolSwapRequest struct {
|
|
||||||
From string `json:"from" validate:"required,eth_addr_checksum"`
|
|
||||||
FromTokenAddress string `json:"fromTokenAddress" validate:"required,eth_addr_checksum"`
|
|
||||||
ToTokenAddress string `json:"toTokenAddress" validate:"required,eth_addr_checksum"`
|
|
||||||
PoolAddress string `json:"poolAddress" validate:"required,eth_addr_checksum"`
|
|
||||||
Amount string `json:"amount" validate:"required,number,gt=0"`
|
|
||||||
}
|
|
||||||
|
|
||||||
PoolDepositRequest struct {
|
|
||||||
From string `json:"from" validate:"required,eth_addr_checksum"`
|
|
||||||
TokenAddress string `json:"tokenAddress" validate:"required,eth_addr_checksum"`
|
|
||||||
PoolAddress string `json:"poolAddress" validate:"required,eth_addr_checksum"`
|
|
||||||
Amount string `json:"amount" validate:"required,number,gt=0"`
|
|
||||||
}
|
|
||||||
|
|
||||||
AccountAddressParam struct {
|
|
||||||
Address string `param:"address" validate:"required,eth_addr_checksum"`
|
|
||||||
}
|
|
||||||
|
|
||||||
TrackingIDParam struct {
|
|
||||||
TrackingID string `param:"trackingId" validate:"required,uuid"`
|
|
||||||
}
|
|
||||||
|
|
||||||
OTXByAccountRequest struct {
|
|
||||||
Address string `param:"address" validate:"required,eth_addr_checksum"`
|
|
||||||
PerPage int `query:"perPage" validate:"required,number,gt=0"`
|
|
||||||
Cursor int `query:"cursor" validate:"number"`
|
|
||||||
Next bool `query:"next"`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ErrCodeInternalServerError = "E01"
|
|
||||||
ErrCodeInvalidJSON = "E02"
|
|
||||||
ErrCodeInvalidAPIKey = "E03"
|
|
||||||
ErrCodeValidationFailed = "E04"
|
|
||||||
ErrCodeAccountNotExists = "E05"
|
|
||||||
)
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"git.defalsify.org/vise.git/engine"
|
|
||||||
"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"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
logg = logging.NewVanilla().WithDomain("handlers")
|
|
||||||
)
|
|
||||||
|
|
||||||
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")
|
|
||||||
)
|
|
||||||
|
|
||||||
type RequestSession struct {
|
|
||||||
Ctx context.Context
|
|
||||||
Config engine.Config
|
|
||||||
Engine engine.Engine
|
|
||||||
Input []byte
|
|
||||||
Storage *storage.Storage
|
|
||||||
Writer io.Writer
|
|
||||||
Continue bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: seems like can remove this.
|
|
||||||
type RequestParser interface {
|
|
||||||
GetSessionId(rq any) (string, error)
|
|
||||||
GetInput(rq any) ([]byte, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type RequestHandler interface {
|
|
||||||
GetConfig() engine.Config
|
|
||||||
GetRequestParser() RequestParser
|
|
||||||
GetEngine(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine
|
|
||||||
Process(rs RequestSession) (RequestSession, error)
|
|
||||||
Output(rs RequestSession) (RequestSession, error)
|
|
||||||
Reset(rs RequestSession) (RequestSession, error)
|
|
||||||
Shutdown()
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -27,10 +28,13 @@ var (
|
|||||||
logg = logging.NewVanilla().WithDomain("ussdmenuhandler")
|
logg = logging.NewVanilla().WithDomain("ussdmenuhandler")
|
||||||
scriptDir = path.Join("services", "registration")
|
scriptDir = path.Join("services", "registration")
|
||||||
translationDir = path.Join(scriptDir, "locale")
|
translationDir = path.Join(scriptDir, "locale")
|
||||||
okResponse *server.OKResponse
|
|
||||||
errResponse *server.ErrResponse
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type FSData struct {
|
||||||
|
Path string
|
||||||
|
St *state.State
|
||||||
|
}
|
||||||
|
|
||||||
// FlagManager handles centralized flag management
|
// FlagManager handles centralized flag management
|
||||||
type FlagManager struct {
|
type FlagManager struct {
|
||||||
parser *asm.FlagParser
|
parser *asm.FlagParser
|
||||||
@@ -70,10 +74,13 @@ func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db) (*Handlers, erro
|
|||||||
userDb := &utils.UserDataStore{
|
userDb := &utils.UserDataStore{
|
||||||
Db: userdataStore,
|
Db: userdataStore,
|
||||||
}
|
}
|
||||||
|
client := &http.Client{}
|
||||||
h := &Handlers{
|
h := &Handlers{
|
||||||
userdataStore: userDb,
|
userdataStore: userDb,
|
||||||
flagManager: appFlags,
|
flagManager: appFlags,
|
||||||
accountService: &server.AccountService{},
|
accountService: &server.AccountService{
|
||||||
|
Client: client,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return h, nil
|
return h, nil
|
||||||
}
|
}
|
||||||
@@ -118,15 +125,18 @@ func (h *Handlers) Init(ctx context.Context, sym string, input []byte) (resource
|
|||||||
// SetLanguage sets the language across the menu
|
// SetLanguage sets the language across the menu
|
||||||
func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
|
var err error
|
||||||
|
|
||||||
symbol, _ := h.st.Where()
|
inputStr := string(input)
|
||||||
code := strings.Split(symbol, "_")[1]
|
switch inputStr {
|
||||||
|
case "0":
|
||||||
if !utils.IsValidISO639(code) {
|
res.FlagSet = []uint32{state.FLAG_LANG}
|
||||||
return res, nil
|
res.Content = "eng"
|
||||||
|
case "1":
|
||||||
|
res.FlagSet = []uint32{state.FLAG_LANG}
|
||||||
|
res.Content = "swa"
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
res.FlagSet = append(res.FlagSet, state.FLAG_LANG)
|
|
||||||
res.Content = code
|
|
||||||
|
|
||||||
languageSetFlag, err := h.flagManager.GetFlag("flag_language_set")
|
languageSetFlag, err := h.flagManager.GetFlag("flag_language_set")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -138,16 +148,11 @@ func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (r
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handlers) createAccountNoExist(ctx context.Context, sessionId string, res *resource.Result) error {
|
func (h *Handlers) createAccountNoExist(ctx context.Context, sessionId string, res *resource.Result) error {
|
||||||
okResponse, errResponse := h.accountService.CreateAccount()
|
accountResp, err := h.accountService.CreateAccount()
|
||||||
if errResponse != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
trackingId := okResponse.Result["trackingId"].(string)
|
|
||||||
publicKey := okResponse.Result["publicKey"].(string)
|
|
||||||
|
|
||||||
data := map[utils.DataTyp]string{
|
data := map[utils.DataTyp]string{
|
||||||
utils.DATA_TRACKING_ID: trackingId,
|
utils.DATA_TRACKING_ID: accountResp.Result.TrackingId,
|
||||||
utils.DATA_PUBLIC_KEY: publicKey,
|
utils.DATA_PUBLIC_KEY: accountResp.Result.PublicKey,
|
||||||
|
utils.DATA_CUSTODIAL_ID: accountResp.Result.CustodialId.String(),
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, value := range data {
|
for key, value := range data {
|
||||||
@@ -159,7 +164,7 @@ func (h *Handlers) createAccountNoExist(ctx context.Context, sessionId string, r
|
|||||||
}
|
}
|
||||||
flag_account_created, _ := h.flagManager.GetFlag("flag_account_created")
|
flag_account_created, _ := h.flagManager.GetFlag("flag_account_created")
|
||||||
res.FlagSet = append(res.FlagSet, flag_account_created)
|
res.FlagSet = append(res.FlagSet, flag_account_created)
|
||||||
return nil
|
return err
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,6 +203,7 @@ func (h *Handlers) SavePin(ctx context.Context, sym string, input []byte) (resou
|
|||||||
}
|
}
|
||||||
|
|
||||||
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
|
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
|
||||||
|
|
||||||
accountPIN := string(input)
|
accountPIN := string(input)
|
||||||
// Validate that the PIN is a 4-digit number
|
// Validate that the PIN is a 4-digit number
|
||||||
if !isValidPIN(accountPIN) {
|
if !isValidPIN(accountPIN) {
|
||||||
@@ -214,71 +220,29 @@ func (h *Handlers) SavePin(ctx context.Context, sym string, input []byte) (resou
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handlers) VerifyNewPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
// SetResetSingleEdit sets and resets flags to allow gradual editing of profile information.
|
||||||
res := resource.Result{}
|
func (h *Handlers) SetResetSingleEdit(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
_, ok := ctx.Value("SessionId").(string)
|
|
||||||
if !ok {
|
|
||||||
return res, fmt.Errorf("missing session")
|
|
||||||
}
|
|
||||||
flag_valid_pin, _ := h.flagManager.GetFlag("flag_valid_pin")
|
|
||||||
pinInput := string(input)
|
|
||||||
// Validate that the PIN is a 4-digit number
|
|
||||||
if isValidPIN(pinInput) {
|
|
||||||
res.FlagSet = append(res.FlagSet, flag_valid_pin)
|
|
||||||
} else {
|
|
||||||
res.FlagReset = append(res.FlagReset, flag_valid_pin)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
var err error
|
|
||||||
|
|
||||||
sessionId, ok := ctx.Value("SessionId").(string)
|
menuOption := string(input)
|
||||||
if !ok {
|
|
||||||
return res, fmt.Errorf("missing session")
|
|
||||||
}
|
|
||||||
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
|
|
||||||
|
|
||||||
accountPIN := string(input)
|
flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update")
|
||||||
|
flag_single_edit, _ := h.flagManager.GetFlag("flag_single_edit")
|
||||||
|
|
||||||
// Validate that the PIN is a 4-digit number
|
switch menuOption {
|
||||||
if !isValidPIN(accountPIN) {
|
case "2":
|
||||||
res.FlagSet = append(res.FlagSet, flag_incorrect_pin)
|
res.FlagReset = append(res.FlagReset, flag_allow_update)
|
||||||
return res, nil
|
res.FlagSet = append(res.FlagSet, flag_single_edit)
|
||||||
|
case "3":
|
||||||
|
res.FlagReset = append(res.FlagReset, flag_allow_update)
|
||||||
|
res.FlagSet = append(res.FlagSet, flag_single_edit)
|
||||||
|
case "4":
|
||||||
|
res.FlagReset = append(res.FlagReset, flag_allow_update)
|
||||||
|
res.FlagSet = append(res.FlagSet, flag_single_edit)
|
||||||
|
default:
|
||||||
|
res.FlagReset = append(res.FlagReset, flag_single_edit)
|
||||||
}
|
}
|
||||||
store := h.userdataStore
|
|
||||||
err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_PIN, []byte(accountPIN))
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handlers) ConfirmPinChange(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
var res resource.Result
|
|
||||||
sessionId, ok := ctx.Value("SessionId").(string)
|
|
||||||
if !ok {
|
|
||||||
return res, fmt.Errorf("missing session")
|
|
||||||
}
|
|
||||||
flag_pin_mismatch, _ := h.flagManager.GetFlag("flag_pin_mismatch")
|
|
||||||
|
|
||||||
store := h.userdataStore
|
|
||||||
temporaryPin, err := store.ReadEntry(ctx, sessionId, utils.DATA_TEMPORARY_PIN)
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
if bytes.Equal(temporaryPin, input) {
|
|
||||||
res.FlagReset = append(res.FlagReset, flag_pin_mismatch)
|
|
||||||
} else {
|
|
||||||
res.FlagSet = append(res.FlagSet, flag_pin_mismatch)
|
|
||||||
}
|
|
||||||
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(temporaryPin))
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,6 +260,8 @@ func (h *Handlers) VerifyPin(ctx context.Context, sym string, input []byte) (res
|
|||||||
if !ok {
|
if !ok {
|
||||||
return res, fmt.Errorf("missing session")
|
return res, fmt.Errorf("missing session")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//AccountPin, _ := utils.ReadEntry(ctx, h.userdataStore, sessionId, utils.DATA_ACCOUNT_PIN)
|
||||||
store := h.userdataStore
|
store := h.userdataStore
|
||||||
AccountPin, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN)
|
AccountPin, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -331,6 +297,7 @@ func (h *Handlers) SaveFirstname(ctx context.Context, sym string, input []byte)
|
|||||||
if !ok {
|
if !ok {
|
||||||
return res, fmt.Errorf("missing session")
|
return res, fmt.Errorf("missing session")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(input) > 0 {
|
if len(input) > 0 {
|
||||||
firstName := string(input)
|
firstName := string(input)
|
||||||
store := h.userdataStore
|
store := h.userdataStore
|
||||||
@@ -359,6 +326,9 @@ func (h *Handlers) SaveFamilyname(ctx context.Context, sym string, input []byte)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
if err != nil {
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return res, fmt.Errorf("a family name cannot be less than one character")
|
return res, fmt.Errorf("a family name cannot be less than one character")
|
||||||
}
|
}
|
||||||
@@ -374,6 +344,7 @@ func (h *Handlers) SaveYob(ctx context.Context, sym string, input []byte) (resou
|
|||||||
if !ok {
|
if !ok {
|
||||||
return res, fmt.Errorf("missing session")
|
return res, fmt.Errorf("missing session")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(input) == 4 {
|
if len(input) == 4 {
|
||||||
yob := string(input)
|
yob := string(input)
|
||||||
store := h.userdataStore
|
store := h.userdataStore
|
||||||
@@ -409,18 +380,28 @@ func (h *Handlers) SaveLocation(ctx context.Context, sym string, input []byte) (
|
|||||||
|
|
||||||
// SaveGender updates the gender in the gdbm with the provided input.
|
// SaveGender updates the gender in the gdbm with the provided input.
|
||||||
func (h *Handlers) SaveGender(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) SaveGender(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
symbol, _ := h.st.Where()
|
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
var err error
|
var err error
|
||||||
sessionId, ok := ctx.Value("SessionId").(string)
|
sessionId, ok := ctx.Value("SessionId").(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return res, fmt.Errorf("missing session")
|
return res, fmt.Errorf("missing session")
|
||||||
}
|
}
|
||||||
gender := strings.Split(symbol, "_")[1]
|
|
||||||
store := h.userdataStore
|
if len(input) > 0 {
|
||||||
err = store.WriteEntry(ctx, sessionId, utils.DATA_GENDER, []byte(gender))
|
gender := string(input)
|
||||||
if err != nil {
|
switch gender {
|
||||||
return res, nil
|
case "1":
|
||||||
|
gender = "Male"
|
||||||
|
case "2":
|
||||||
|
gender = "Female"
|
||||||
|
case "3":
|
||||||
|
gender = "Unspecified"
|
||||||
|
}
|
||||||
|
store := h.userdataStore
|
||||||
|
err = store.WriteEntry(ctx, sessionId, utils.DATA_GENDER, []byte(gender))
|
||||||
|
if err != nil {
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
@@ -434,6 +415,7 @@ func (h *Handlers) SaveOfferings(ctx context.Context, sym string, input []byte)
|
|||||||
if !ok {
|
if !ok {
|
||||||
return res, fmt.Errorf("missing session")
|
return res, fmt.Errorf("missing session")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(input) > 0 {
|
if len(input) > 0 {
|
||||||
offerings := string(input)
|
offerings := string(input)
|
||||||
store := h.userdataStore
|
store := h.userdataStore
|
||||||
@@ -459,6 +441,7 @@ func (h *Handlers) ResetAllowUpdate(ctx context.Context, sym string, input []byt
|
|||||||
// ResetAccountAuthorized resets the account authorization flag after a successful PIN entry.
|
// ResetAccountAuthorized resets the account authorization flag after a successful PIN entry.
|
||||||
func (h *Handlers) ResetAccountAuthorized(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) ResetAccountAuthorized(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
|
|
||||||
flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
|
flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
|
||||||
|
|
||||||
res.FlagReset = append(res.FlagReset, flag_account_authorized)
|
res.FlagReset = append(res.FlagReset, flag_account_authorized)
|
||||||
@@ -468,10 +451,12 @@ func (h *Handlers) ResetAccountAuthorized(ctx context.Context, sym string, input
|
|||||||
// CheckIdentifier retrieves the PublicKey from the JSON data file.
|
// CheckIdentifier retrieves the PublicKey from the JSON data file.
|
||||||
func (h *Handlers) CheckIdentifier(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) CheckIdentifier(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
|
|
||||||
sessionId, ok := ctx.Value("SessionId").(string)
|
sessionId, ok := ctx.Value("SessionId").(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return res, fmt.Errorf("missing session")
|
return res, fmt.Errorf("missing session")
|
||||||
}
|
}
|
||||||
|
|
||||||
store := h.userdataStore
|
store := h.userdataStore
|
||||||
publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
|
publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
|
||||||
|
|
||||||
@@ -485,10 +470,12 @@ func (h *Handlers) CheckIdentifier(ctx context.Context, sym string, input []byte
|
|||||||
func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
sessionId, ok := ctx.Value("SessionId").(string)
|
sessionId, ok := ctx.Value("SessionId").(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return res, fmt.Errorf("missing session")
|
return res, fmt.Errorf("missing session")
|
||||||
}
|
}
|
||||||
|
|
||||||
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
|
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
|
||||||
flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
|
flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
|
||||||
flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update")
|
flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update")
|
||||||
@@ -515,13 +502,16 @@ func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (res
|
|||||||
} else {
|
} else {
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|
||||||
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
|
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
|
||||||
|
|
||||||
res.FlagReset = append(res.FlagReset, flag_incorrect_pin)
|
res.FlagReset = append(res.FlagReset, flag_incorrect_pin)
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
@@ -533,27 +523,28 @@ func (h *Handlers) CheckAccountStatus(ctx context.Context, sym string, input []b
|
|||||||
|
|
||||||
flag_account_success, _ := h.flagManager.GetFlag("flag_account_success")
|
flag_account_success, _ := h.flagManager.GetFlag("flag_account_success")
|
||||||
flag_account_pending, _ := h.flagManager.GetFlag("flag_account_pending")
|
flag_account_pending, _ := h.flagManager.GetFlag("flag_account_pending")
|
||||||
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
|
|
||||||
|
|
||||||
sessionId, ok := ctx.Value("SessionId").(string)
|
sessionId, ok := ctx.Value("SessionId").(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return res, fmt.Errorf("missing session")
|
return res, fmt.Errorf("missing session")
|
||||||
}
|
}
|
||||||
store := h.userdataStore
|
store := h.userdataStore
|
||||||
publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
|
trackingId, err := store.ReadEntry(ctx, sessionId, utils.DATA_TRACKING_ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
okResponse, errResponse = h.accountService.TrackAccountStatus(string(publicKey))
|
|
||||||
if errResponse != nil {
|
status, err := h.accountService.CheckAccountStatus(string(trackingId))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error checking account status:", err)
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
res.FlagReset = append(res.FlagReset, flag_api_error)
|
|
||||||
isActive := okResponse.Result["active"].(bool)
|
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACCOUNT_STATUS, []byte(status))
|
||||||
if !ok {
|
if err != nil {
|
||||||
return res, err
|
return res, nil
|
||||||
}
|
}
|
||||||
if isActive {
|
if status == "SUCCESS" {
|
||||||
res.FlagSet = append(res.FlagSet, flag_account_success)
|
res.FlagSet = append(res.FlagSet, flag_account_success)
|
||||||
res.FlagReset = append(res.FlagReset, flag_account_pending)
|
res.FlagReset = append(res.FlagReset, flag_account_pending)
|
||||||
} else {
|
} else {
|
||||||
@@ -578,21 +569,6 @@ func (h *Handlers) Quit(ctx context.Context, sym string, input []byte) (resource
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// QuitWithHelp displays helpline information then exits the menu
|
|
||||||
func (h *Handlers) QuitWithHelp(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
var res resource.Result
|
|
||||||
|
|
||||||
flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
|
|
||||||
|
|
||||||
code := codeFromCtx(ctx)
|
|
||||||
l := gotext.NewLocale(translationDir, code)
|
|
||||||
l.AddDomain("default")
|
|
||||||
|
|
||||||
res.Content = l.Get("For more help,please call: 0757628885")
|
|
||||||
res.FlagReset = append(res.FlagReset, flag_account_authorized)
|
|
||||||
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
|
||||||
@@ -633,8 +609,6 @@ func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) (
|
|||||||
var res resource.Result
|
var res resource.Result
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
|
|
||||||
|
|
||||||
sessionId, ok := ctx.Value("SessionId").(string)
|
sessionId, ok := ctx.Value("SessionId").(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return res, fmt.Errorf("missing session")
|
return res, fmt.Errorf("missing session")
|
||||||
@@ -646,60 +620,15 @@ func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) (
|
|||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
balanceResponse, err := h.accountService.CheckBalance(string(publicKey))
|
balance, err := h.accountService.CheckBalance(string(publicKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
if !balanceResponse.Ok {
|
|
||||||
res.FlagSet = append(res.FlagSet, flag_api_error)
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
res.FlagReset = append(res.FlagReset, flag_api_error)
|
|
||||||
balance := balanceResponse.Result.Balance
|
|
||||||
res.Content = balance
|
res.Content = balance
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handlers) FetchCustodialBalances(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
var res resource.Result
|
|
||||||
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
|
|
||||||
|
|
||||||
sessionId, ok := ctx.Value("SessionId").(string)
|
|
||||||
if !ok {
|
|
||||||
return res, fmt.Errorf("missing session")
|
|
||||||
}
|
|
||||||
symbol, _ := h.st.Where()
|
|
||||||
balanceType := strings.Split(symbol, "_")[0]
|
|
||||||
|
|
||||||
store := h.userdataStore
|
|
||||||
publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
balanceResponse, err := h.accountService.CheckBalance(string(publicKey))
|
|
||||||
if err != nil {
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
if !balanceResponse.Ok {
|
|
||||||
res.FlagSet = append(res.FlagSet, flag_api_error)
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
res.FlagReset = append(res.FlagReset, flag_api_error)
|
|
||||||
balance := balanceResponse.Result.Balance
|
|
||||||
|
|
||||||
switch balanceType {
|
|
||||||
case "my":
|
|
||||||
res.Content = fmt.Sprintf("Your balance is %s", balance)
|
|
||||||
case "community":
|
|
||||||
res.Content = fmt.Sprintf("Your community balance is %s", balance)
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateRecipient validates that the given input is a valid phone number.
|
// ValidateRecipient validates that the given input is a valid phone number.
|
||||||
func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
@@ -796,11 +725,10 @@ func (h *Handlers) MaxAmount(ctx context.Context, sym string, input []byte) (res
|
|||||||
store := h.userdataStore
|
store := h.userdataStore
|
||||||
publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
|
publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
|
||||||
|
|
||||||
balanceResp, err := h.accountService.CheckBalance(string(publicKey))
|
balance, err := h.accountService.CheckBalance(string(publicKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
balance := balanceResp.Result.Balance
|
|
||||||
|
|
||||||
res.Content = balance
|
res.Content = balance
|
||||||
|
|
||||||
@@ -819,31 +747,26 @@ func (h *Handlers) ValidateAmount(ctx context.Context, sym string, input []byte)
|
|||||||
}
|
}
|
||||||
|
|
||||||
flag_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount")
|
flag_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount")
|
||||||
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
|
|
||||||
|
|
||||||
store := h.userdataStore
|
store := h.userdataStore
|
||||||
publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
|
publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
|
||||||
|
|
||||||
amountStr := string(input)
|
amountStr := string(input)
|
||||||
|
|
||||||
balanceRes, err := h.accountService.CheckBalance(string(publicKey))
|
balanceStr, err := h.accountService.CheckBalance(string(publicKey))
|
||||||
balanceStr := balanceRes.Result.Balance
|
|
||||||
|
|
||||||
if !balanceRes.Ok {
|
|
||||||
res.FlagSet = append(res.FlagSet, flag_api_error)
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
res.Content = balanceStr
|
res.Content = balanceStr
|
||||||
res.FlagReset = append(res.FlagReset, flag_api_error)
|
|
||||||
|
|
||||||
// Parse the balance
|
// Parse the balance
|
||||||
balanceParts := strings.Split(balanceStr, " ")
|
balanceParts := strings.Split(balanceStr, " ")
|
||||||
if len(balanceParts) != 2 {
|
if len(balanceParts) != 2 {
|
||||||
return res, fmt.Errorf("unexpected balance format: %s", balanceStr)
|
return res, fmt.Errorf("unexpected balance format: %s", balanceStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
balanceValue, err := strconv.ParseFloat(balanceParts[0], 64)
|
balanceValue, err := strconv.ParseFloat(balanceParts[0], 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, fmt.Errorf("failed to parse balance: %v", err)
|
return res, fmt.Errorf("failed to parse balance: %v", err)
|
||||||
@@ -853,6 +776,7 @@ func (h *Handlers) ValidateAmount(ctx context.Context, sym string, input []byte)
|
|||||||
re := regexp.MustCompile(`^(\d+(\.\d+)?)\s*(?:CELO)?$`)
|
re := regexp.MustCompile(`^(\d+(\.\d+)?)\s*(?:CELO)?$`)
|
||||||
matches := re.FindStringSubmatch(strings.TrimSpace(amountStr))
|
matches := re.FindStringSubmatch(strings.TrimSpace(amountStr))
|
||||||
if len(matches) < 2 {
|
if len(matches) < 2 {
|
||||||
|
|
||||||
res.FlagSet = append(res.FlagSet, flag_invalid_amount)
|
res.FlagSet = append(res.FlagSet, flag_invalid_amount)
|
||||||
res.Content = amountStr
|
res.Content = amountStr
|
||||||
return res, nil
|
return res, nil
|
||||||
@@ -860,6 +784,7 @@ func (h *Handlers) ValidateAmount(ctx context.Context, sym string, input []byte)
|
|||||||
|
|
||||||
inputAmount, err := strconv.ParseFloat(matches[1], 64)
|
inputAmount, err := strconv.ParseFloat(matches[1], 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
||||||
res.FlagSet = append(res.FlagSet, flag_invalid_amount)
|
res.FlagSet = append(res.FlagSet, flag_invalid_amount)
|
||||||
res.Content = amountStr
|
res.Content = amountStr
|
||||||
return res, nil
|
return res, nil
|
||||||
@@ -872,6 +797,7 @@ func (h *Handlers) ValidateAmount(ctx context.Context, sym string, input []byte)
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.Content = fmt.Sprintf("%.3f", inputAmount) // Format to 3 decimal places
|
res.Content = fmt.Sprintf("%.3f", inputAmount) // Format to 3 decimal places
|
||||||
|
store = h.userdataStore
|
||||||
err = store.WriteEntry(ctx, sessionId, utils.DATA_AMOUNT, []byte(amountStr))
|
err = store.WriteEntry(ctx, sessionId, utils.DATA_AMOUNT, []byte(amountStr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, err
|
return res, err
|
||||||
@@ -929,6 +855,36 @@ func (h *Handlers) GetAmount(ctx context.Context, sym string, input []byte) (res
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QuickWithBalance retrieves the balance for a given public key from the custodial balance API endpoint before
|
||||||
|
// gracefully exiting the session.
|
||||||
|
func (h *Handlers) QuitWithBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
var res resource.Result
|
||||||
|
var err error
|
||||||
|
sessionId, ok := ctx.Value("SessionId").(string)
|
||||||
|
if !ok {
|
||||||
|
return res, fmt.Errorf("missing session")
|
||||||
|
}
|
||||||
|
|
||||||
|
flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
|
||||||
|
|
||||||
|
code := codeFromCtx(ctx)
|
||||||
|
l := gotext.NewLocale(translationDir, code)
|
||||||
|
l.AddDomain("default")
|
||||||
|
|
||||||
|
store := h.userdataStore
|
||||||
|
publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
balance, err := h.accountService.CheckBalance(string(publicKey))
|
||||||
|
if err != nil {
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
res.Content = l.Get("Your account balance is %s", balance)
|
||||||
|
res.FlagReset = append(res.FlagReset, flag_account_authorized)
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
// InitiateTransaction returns a confirmation and resets the transaction data
|
// InitiateTransaction returns a confirmation and resets the transaction data
|
||||||
// on the gdbm store.
|
// on the gdbm store.
|
||||||
func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
@@ -962,23 +918,16 @@ func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input []
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetProfileInfo retrieves and formats the profile information of a user from a Gdbm backed storage.
|
||||||
func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
var defaultValue string
|
|
||||||
sessionId, ok := ctx.Value("SessionId").(string)
|
sessionId, ok := ctx.Value("SessionId").(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return res, fmt.Errorf("missing session")
|
return res, fmt.Errorf("missing session")
|
||||||
}
|
}
|
||||||
language, ok := ctx.Value("Language").(lang.Language)
|
|
||||||
if !ok {
|
// Default value when an entry is not found
|
||||||
return res, fmt.Errorf("value for 'Language' is not of type lang.Language")
|
defaultValue := "Not Provided"
|
||||||
}
|
|
||||||
code := language.Code
|
|
||||||
if code == "swa" {
|
|
||||||
defaultValue = "Haipo"
|
|
||||||
} else {
|
|
||||||
defaultValue = "Not Provided"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to handle nil byte slices and convert them to string
|
// Helper function to handle nil byte slices and convert them to string
|
||||||
getEntryOrDefault := func(entry []byte, err error) string {
|
getEntryOrDefault := func(entry []byte, err error) string {
|
||||||
@@ -1015,23 +964,12 @@ func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte)
|
|||||||
return res, fmt.Errorf("invalid year of birth: %v", err)
|
return res, fmt.Errorf("invalid year of birth: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch language.Code {
|
|
||||||
case "eng":
|
// Format the result
|
||||||
res.Content = fmt.Sprintf(
|
res.Content = fmt.Sprintf(
|
||||||
"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
|
"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
|
||||||
name, gender, age, location, offerings,
|
name, gender, age, location, offerings,
|
||||||
)
|
)
|
||||||
case "swa":
|
|
||||||
res.Content = fmt.Sprintf(
|
|
||||||
"Jina: %s\nJinsia: %s\nUmri: %s\nEneo: %s\nUnauza: %s\n",
|
|
||||||
name, gender, age, location, offerings,
|
|
||||||
)
|
|
||||||
default:
|
|
||||||
res.Content = fmt.Sprintf(
|
|
||||||
"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
|
|
||||||
name, gender, age, location, offerings,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
26
internal/handlers/ussd/mocks/servicemock.go
Normal file
26
internal/handlers/ussd/mocks/servicemock.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/models"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockAccountService implements AccountServiceInterface for testing
|
||||||
|
type MockAccountService struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockAccountService) CreateAccount() (*models.AccountResponse, error) {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Get(0).(*models.AccountResponse), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockAccountService) CheckBalance(publicKey string) (string, error) {
|
||||||
|
args := m.Called(publicKey)
|
||||||
|
return args.String(0), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockAccountService) CheckAccountStatus(trackingId string) (string, error) {
|
||||||
|
args := m.Called(trackingId)
|
||||||
|
return args.String(0), args.Error(1)
|
||||||
|
}
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ATSessionHandler struct {
|
|
||||||
*SessionHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewATSessionHandler(h handlers.RequestHandler) *ATSessionHandler {
|
|
||||||
return &ATSessionHandler{
|
|
||||||
SessionHandler: ToSessionHandler(h),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
||||||
var code int
|
|
||||||
var err error
|
|
||||||
|
|
||||||
rqs := handlers.RequestSession{
|
|
||||||
Ctx: req.Context(),
|
|
||||||
Writer: w,
|
|
||||||
}
|
|
||||||
|
|
||||||
rp := ash.GetRequestParser()
|
|
||||||
cfg := ash.GetConfig()
|
|
||||||
cfg.SessionId, err = rp.GetSessionId(req)
|
|
||||||
if err != nil {
|
|
||||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
|
||||||
ash.writeError(w, 400, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rqs.Config = cfg
|
|
||||||
rqs.Input, err = rp.GetInput(req)
|
|
||||||
if err != nil {
|
|
||||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
|
||||||
ash.writeError(w, 400, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rqs, err = ash.Process(rqs)
|
|
||||||
switch err {
|
|
||||||
case nil: // set code to 200 if no err
|
|
||||||
code = 200
|
|
||||||
case handlers.ErrStorage, handlers.ErrEngineInit, handlers.ErrEngineExec, handlers.ErrEngineType:
|
|
||||||
code = 500
|
|
||||||
default:
|
|
||||||
code = 500
|
|
||||||
}
|
|
||||||
|
|
||||||
if code != 200 {
|
|
||||||
ash.writeError(w, 500, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(200)
|
|
||||||
w.Header().Set("Content-Type", "text/plain")
|
|
||||||
rqs, err = ash.Output(rqs)
|
|
||||||
if err != nil {
|
|
||||||
ash.writeError(w, 500, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rqs, err = ash.Reset(rqs)
|
|
||||||
if err != nil {
|
|
||||||
ash.writeError(w, 500, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ash *ATSessionHandler) Output(rqs handlers.RequestSession) (handlers.RequestSession, error) {
|
|
||||||
var err error
|
|
||||||
var prefix string
|
|
||||||
|
|
||||||
if rqs.Continue {
|
|
||||||
prefix = "CON "
|
|
||||||
} else {
|
|
||||||
prefix = "END "
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = io.WriteString(rqs.Writer, prefix)
|
|
||||||
if err != nil {
|
|
||||||
return rqs, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer)
|
|
||||||
return rqs, err
|
|
||||||
}
|
|
||||||
@@ -1,449 +0,0 @@
|
|||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.defalsify.org/vise.git/engine"
|
|
||||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
|
||||||
"git.grassecon.net/urdt/ussd/internal/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)
|
|
||||||
|
|
||||||
if ash == nil {
|
|
||||||
t.Fatal("NewATSessionHandler returned nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if ash.SessionHandler == nil {
|
|
||||||
t.Fatal("SessionHandler is nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestATSessionHandler_ServeHTTP(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
setupMocks func(*httpmocks.MockRequestHandler, *httpmocks.MockRequestParser, *httpmocks.MockEngine)
|
|
||||||
formData url.Values
|
|
||||||
expectedStatus int
|
|
||||||
expectedBody string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Successful request",
|
|
||||||
setupMocks: func(mh *httpmocks.MockRequestHandler, mrp *httpmocks.MockRequestParser, me *httpmocks.MockEngine) {
|
|
||||||
mrp.GetSessionIdFunc = func(rq any) (string, error) {
|
|
||||||
req := rq.(*http.Request)
|
|
||||||
return req.FormValue("phoneNumber"), nil
|
|
||||||
}
|
|
||||||
mrp.GetInputFunc = func(rq any) ([]byte, error) {
|
|
||||||
req := rq.(*http.Request)
|
|
||||||
text := req.FormValue("text")
|
|
||||||
parts := strings.Split(text, "*")
|
|
||||||
return []byte(parts[len(parts)-1]), nil
|
|
||||||
}
|
|
||||||
mh.ProcessFunc = func(rqs handlers.RequestSession) (handlers.RequestSession, error) {
|
|
||||||
rqs.Continue = true
|
|
||||||
rqs.Engine = me
|
|
||||||
return rqs, nil
|
|
||||||
}
|
|
||||||
mh.GetConfigFunc = func() engine.Config { return engine.Config{} }
|
|
||||||
mh.GetRequestParserFunc = func() handlers.RequestParser { return mrp }
|
|
||||||
mh.OutputFunc = func(rs handlers.RequestSession) (handlers.RequestSession, error) { return rs, nil }
|
|
||||||
mh.ResetFunc = func(rs handlers.RequestSession) (handlers.RequestSession, error) { return rs, nil }
|
|
||||||
me.FlushFunc = func(context.Context, io.Writer) (int, error) { return 0, nil }
|
|
||||||
},
|
|
||||||
formData: url.Values{
|
|
||||||
"phoneNumber": []string{"+1234567890"},
|
|
||||||
"text": []string{"1*2*3"},
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedBody: "CON ",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "GetSessionId error",
|
|
||||||
setupMocks: func(mh *httpmocks.MockRequestHandler, mrp *httpmocks.MockRequestParser, me *httpmocks.MockEngine) {
|
|
||||||
mrp.GetSessionIdFunc = func(rq any) (string, error) {
|
|
||||||
return "", errors.New("no phone number found")
|
|
||||||
}
|
|
||||||
mh.GetConfigFunc = func() engine.Config { return engine.Config{} }
|
|
||||||
mh.GetRequestParserFunc = func() handlers.RequestParser { return mrp }
|
|
||||||
},
|
|
||||||
formData: url.Values{
|
|
||||||
"text": []string{"1*2*3"},
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusBadRequest,
|
|
||||||
expectedBody: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "GetInput error",
|
|
||||||
setupMocks: func(mh *httpmocks.MockRequestHandler, mrp *httpmocks.MockRequestParser, me *httpmocks.MockEngine) {
|
|
||||||
mrp.GetSessionIdFunc = func(rq any) (string, error) {
|
|
||||||
req := rq.(*http.Request)
|
|
||||||
return req.FormValue("phoneNumber"), nil
|
|
||||||
}
|
|
||||||
mrp.GetInputFunc = func(rq any) ([]byte, error) {
|
|
||||||
return nil, errors.New("no input found")
|
|
||||||
}
|
|
||||||
mh.GetConfigFunc = func() engine.Config { return engine.Config{} }
|
|
||||||
mh.GetRequestParserFunc = func() handlers.RequestParser { return mrp }
|
|
||||||
},
|
|
||||||
formData: url.Values{
|
|
||||||
"phoneNumber": []string{"+1234567890"},
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusBadRequest,
|
|
||||||
expectedBody: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Process error",
|
|
||||||
setupMocks: func(mh *httpmocks.MockRequestHandler, mrp *httpmocks.MockRequestParser, me *httpmocks.MockEngine) {
|
|
||||||
mrp.GetSessionIdFunc = func(rq any) (string, error) {
|
|
||||||
req := rq.(*http.Request)
|
|
||||||
return req.FormValue("phoneNumber"), nil
|
|
||||||
}
|
|
||||||
mrp.GetInputFunc = func(rq any) ([]byte, error) {
|
|
||||||
req := rq.(*http.Request)
|
|
||||||
text := req.FormValue("text")
|
|
||||||
parts := strings.Split(text, "*")
|
|
||||||
return []byte(parts[len(parts)-1]), nil
|
|
||||||
}
|
|
||||||
mh.ProcessFunc = func(rqs handlers.RequestSession) (handlers.RequestSession, error) {
|
|
||||||
return rqs, handlers.ErrStorage
|
|
||||||
}
|
|
||||||
mh.GetConfigFunc = func() engine.Config { return engine.Config{} }
|
|
||||||
mh.GetRequestParserFunc = func() handlers.RequestParser { return mrp }
|
|
||||||
},
|
|
||||||
formData: url.Values{
|
|
||||||
"phoneNumber": []string{"+1234567890"},
|
|
||||||
"text": []string{"1*2*3"},
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusInternalServerError,
|
|
||||||
expectedBody: "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
mockHandler := &httpmocks.MockRequestHandler{}
|
|
||||||
mockRequestParser := &httpmocks.MockRequestParser{}
|
|
||||||
mockEngine := &httpmocks.MockEngine{}
|
|
||||||
tt.setupMocks(mockHandler, mockRequestParser, mockEngine)
|
|
||||||
|
|
||||||
ash := NewATSessionHandler(mockHandler)
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tt.formData.Encode()))
|
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
ash.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
if w.Code != tt.expectedStatus {
|
|
||||||
t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tt.expectedBody != "" && w.Body.String() != tt.expectedBody {
|
|
||||||
t.Errorf("Expected body %q, got %q", tt.expectedBody, w.Body.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestATSessionHandler_Output(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input handlers.RequestSession
|
|
||||||
expectedPrefix string
|
|
||||||
expectedError bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Continue true",
|
|
||||||
input: handlers.RequestSession{
|
|
||||||
Continue: true,
|
|
||||||
Engine: &httpmocks.MockEngine{
|
|
||||||
FlushFunc: func(context.Context, io.Writer) (int, error) {
|
|
||||||
return 0, nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Writer: &httpmocks.MockWriter{},
|
|
||||||
},
|
|
||||||
expectedPrefix: "CON ",
|
|
||||||
expectedError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Continue false",
|
|
||||||
input: handlers.RequestSession{
|
|
||||||
Continue: false,
|
|
||||||
Engine: &httpmocks.MockEngine{
|
|
||||||
FlushFunc: func(context.Context, io.Writer) (int, error) {
|
|
||||||
return 0, nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Writer: &httpmocks.MockWriter{},
|
|
||||||
},
|
|
||||||
expectedPrefix: "END ",
|
|
||||||
expectedError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Flush error",
|
|
||||||
input: handlers.RequestSession{
|
|
||||||
Continue: true,
|
|
||||||
Engine: &httpmocks.MockEngine{
|
|
||||||
FlushFunc: func(context.Context, io.Writer) (int, error) {
|
|
||||||
return 0, errors.New("write error")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Writer: &httpmocks.MockWriter{},
|
|
||||||
},
|
|
||||||
expectedPrefix: "CON ",
|
|
||||||
expectedError: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
ash := &ATSessionHandler{}
|
|
||||||
_, err := ash.Output(tt.input)
|
|
||||||
|
|
||||||
if tt.expectedError && err == nil {
|
|
||||||
t.Error("Expected an error, but got nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tt.expectedError && err != nil {
|
|
||||||
t.Errorf("Unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mw := tt.input.Writer.(*httpmocks.MockWriter)
|
|
||||||
if !mw.WriteStringCalled {
|
|
||||||
t.Error("WriteString was not called")
|
|
||||||
}
|
|
||||||
|
|
||||||
if mw.WrittenString != tt.expectedPrefix {
|
|
||||||
t.Errorf("Expected prefix %q, got %q", tt.expectedPrefix, mw.WrittenString)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,42 +1,42 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
|
"git.defalsify.org/vise.git/db"
|
||||||
|
"git.defalsify.org/vise.git/engine"
|
||||||
"git.defalsify.org/vise.git/logging"
|
"git.defalsify.org/vise.git/logging"
|
||||||
|
"git.defalsify.org/vise.git/persist"
|
||||||
|
"git.defalsify.org/vise.git/resource"
|
||||||
|
|
||||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
"git.grassecon.net/urdt/ussd/internal/handlers/ussd"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
logg = logging.NewVanilla().WithDomain("httpserver")
|
logg = logging.NewVanilla().WithDomain("httpserver")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type RequestParser interface {
|
||||||
|
GetSessionId(rq *http.Request) (string, error)
|
||||||
|
GetInput(rq *http.Request) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
type DefaultRequestParser struct {
|
type DefaultRequestParser struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func(rp *DefaultRequestParser) GetSessionId(rq *http.Request) (string, error) {
|
||||||
func(rp *DefaultRequestParser) GetSessionId(rq any) (string, error) {
|
v := rq.Header.Get("X-Vise-Session")
|
||||||
rqv, ok := rq.(*http.Request)
|
|
||||||
if !ok {
|
|
||||||
return "", handlers.ErrInvalidRequest
|
|
||||||
}
|
|
||||||
v := rqv.Header.Get("X-Vise-Session")
|
|
||||||
if v == "" {
|
if v == "" {
|
||||||
return "", handlers.ErrSessionMissing
|
return "", fmt.Errorf("no session found")
|
||||||
}
|
}
|
||||||
return v, nil
|
return v, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func(rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) {
|
func(rp *DefaultRequestParser) GetInput(rq *http.Request) ([]byte, error) {
|
||||||
rqv, ok := rq.(*http.Request)
|
defer rq.Body.Close()
|
||||||
if !ok {
|
v, err := ioutil.ReadAll(rq.Body)
|
||||||
return nil, handlers.ErrInvalidRequest
|
|
||||||
}
|
|
||||||
defer rqv.Body.Close()
|
|
||||||
v, err := ioutil.ReadAll(rqv.Body)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -44,79 +44,107 @@ func(rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SessionHandler struct {
|
type SessionHandler struct {
|
||||||
handlers.RequestHandler
|
cfgTemplate engine.Config
|
||||||
|
rp RequestParser
|
||||||
|
rs resource.Resource
|
||||||
|
//first resource.EntryFunc
|
||||||
|
hn *ussd.Handlers
|
||||||
|
provider StorageProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToSessionHandler(h handlers.RequestHandler) *SessionHandler {
|
//func NewSessionHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp RequestParser, first resource.EntryFunc) *SessionHandler {
|
||||||
|
func NewSessionHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp RequestParser, hn *ussd.Handlers) *SessionHandler {
|
||||||
return &SessionHandler{
|
return &SessionHandler{
|
||||||
RequestHandler: h,
|
cfgTemplate: cfg,
|
||||||
|
rs: rs,
|
||||||
|
//first: first,
|
||||||
|
hn: hn,
|
||||||
|
rp: rp,
|
||||||
|
provider: NewSimpleStorageProvider(stateDb, userdataDb),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func(f *SessionHandler) writeError(w http.ResponseWriter, code int, err error) {
|
func(f *SessionHandler) writeError(w http.ResponseWriter, code int, msg string, err error) {
|
||||||
s := err.Error()
|
w.Header().Set("X-Vise", msg + ": " + err.Error())
|
||||||
w.Header().Set("Content-Length", strconv.Itoa(len(s)))
|
w.Header().Set("Content-Length", "0")
|
||||||
w.WriteHeader(code)
|
w.WriteHeader(code)
|
||||||
_, err = w.Write([]byte{})
|
_, err = w.Write([]byte{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logg.Errorf("error writing error!!", "err", err, "olderr", s)
|
|
||||||
w.WriteHeader(500)
|
w.WriteHeader(500)
|
||||||
|
w.Header().Set("X-Vise", err.Error())
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func(f* SessionHandler) Shutdown() {
|
||||||
|
err := f.provider.Close()
|
||||||
|
if err != nil {
|
||||||
|
logg.Errorf("handler shutdown error", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
var code int
|
var r bool
|
||||||
var err error
|
sessionId, err := f.rp.GetSessionId(req)
|
||||||
var perr error
|
|
||||||
|
|
||||||
rqs := handlers.RequestSession{
|
|
||||||
Ctx: req.Context(),
|
|
||||||
Writer: w,
|
|
||||||
}
|
|
||||||
|
|
||||||
rp := f.GetRequestParser()
|
|
||||||
cfg := f.GetConfig()
|
|
||||||
cfg.SessionId, err = rp.GetSessionId(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
f.writeError(w, 400, "Session missing", err)
|
||||||
f.writeError(w, 400, err)
|
|
||||||
}
|
|
||||||
rqs.Config = cfg
|
|
||||||
rqs.Input, err = rp.GetInput(req)
|
|
||||||
if err != nil {
|
|
||||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
|
||||||
f.writeError(w, 400, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
input, err := f.rp.GetInput(req)
|
||||||
|
if err != nil {
|
||||||
|
f.writeError(w, 400, "Input read fail", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx := req.Context()
|
||||||
|
cfg := f.cfgTemplate
|
||||||
|
cfg.SessionId = sessionId
|
||||||
|
|
||||||
rqs, err = f.Process(rqs)
|
logg.InfoCtxf(ctx, "new request", "session", cfg.SessionId, "input", input)
|
||||||
switch err {
|
|
||||||
case handlers.ErrStorage:
|
storage, err := f.provider.Get(cfg.SessionId)
|
||||||
code = 500
|
if err != nil {
|
||||||
case handlers.ErrEngineInit:
|
f.writeError(w, 500, "Storage retrieval fail", err)
|
||||||
code = 500
|
return
|
||||||
case handlers.ErrEngineExec:
|
}
|
||||||
code = 500
|
f.hn = f.hn.WithPersister(storage.Persister)
|
||||||
default:
|
defer f.provider.Put(cfg.SessionId, storage)
|
||||||
code = 200
|
en := getEngine(cfg, f.rs, storage.Persister)
|
||||||
|
en = en.WithFirst(f.hn.Init)
|
||||||
|
if cfg.EngineDebug {
|
||||||
|
en = en.WithDebug(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
if code != 200 {
|
r, err = en.Init(ctx)
|
||||||
f.writeError(w, 500, err)
|
if err != nil {
|
||||||
|
f.writeError(w, 500, "Engine init fail", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r && len(input) > 0 {
|
||||||
|
r, err = en.Exec(ctx, input)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
f.writeError(w, 500, "Engine exec fail", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
w.Header().Set("Content-Type", "text/plain")
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
rqs, err = f.Output(rqs)
|
_, err = en.WriteResult(ctx, w)
|
||||||
rqs, perr = f.Reset(rqs)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.writeError(w, 500, err)
|
f.writeError(w, 500, "Write result fail", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if perr != nil {
|
err = en.Finish()
|
||||||
f.writeError(w, 500, perr)
|
if err != nil {
|
||||||
|
f.writeError(w, 500, "Engine finish fail", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = r
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) *engine.DefaultEngine {
|
||||||
|
en := engine.NewEngine(cfg, rs)
|
||||||
|
en = en.WithPersister(pr)
|
||||||
|
return en
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package storage
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.defalsify.org/vise.git/db"
|
"git.defalsify.org/vise.git/db"
|
||||||
@@ -11,31 +11,31 @@ type Storage struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type StorageProvider interface {
|
type StorageProvider interface {
|
||||||
Get(sessionId string) (*Storage, error)
|
Get(sessionId string) (Storage, error)
|
||||||
Put(sessionId string, storage *Storage) error
|
Put(sessionId string, storage Storage) error
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
type SimpleStorageProvider struct {
|
type SimpleStorageProvider struct {
|
||||||
*Storage
|
Storage
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSimpleStorageProvider(stateStore db.Db, userdataStore db.Db) StorageProvider {
|
func NewSimpleStorageProvider(stateStore db.Db, userdataStore db.Db) StorageProvider {
|
||||||
pe := persist.NewPersister(stateStore)
|
pe := persist.NewPersister(stateStore)
|
||||||
pe = pe.WithFlush()
|
pe = pe.WithFlush()
|
||||||
return &SimpleStorageProvider{
|
return &SimpleStorageProvider{
|
||||||
Storage: &Storage{
|
Storage: Storage{
|
||||||
Persister: pe,
|
Persister: pe,
|
||||||
UserdataDb: userdataStore,
|
UserdataDb: userdataStore,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *SimpleStorageProvider) Get(sessionId string) (*Storage, error) {
|
func (p *SimpleStorageProvider) Get(sessionId string) (Storage, error) {
|
||||||
return p.Storage, nil
|
return p.Storage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *SimpleStorageProvider) Put(sessionId string, storage *Storage) error {
|
func (p *SimpleStorageProvider) Put(sessionId string, storage Storage) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package httpmocks
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockEngine implements the engine.Engine interface for testing
|
|
||||||
type MockEngine struct {
|
|
||||||
InitFunc func(context.Context) (bool, error)
|
|
||||||
ExecFunc func(context.Context, []byte) (bool, error)
|
|
||||||
FlushFunc func(context.Context, io.Writer) (int, error)
|
|
||||||
FinishFunc func() error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockEngine) Init(ctx context.Context) (bool, error) {
|
|
||||||
return m.InitFunc(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockEngine) Exec(ctx context.Context, input []byte) (bool, error) {
|
|
||||||
return m.ExecFunc(ctx, input)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockEngine) Flush(ctx context.Context, w io.Writer) (int, error) {
|
|
||||||
return m.FlushFunc(ctx, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockEngine) Finish() error {
|
|
||||||
return m.FinishFunc()
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package httpmocks
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.defalsify.org/vise.git/engine"
|
|
||||||
"git.defalsify.org/vise.git/persist"
|
|
||||||
"git.defalsify.org/vise.git/resource"
|
|
||||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockRequestHandler implements handlers.RequestHandler interface for testing
|
|
||||||
type MockRequestHandler struct {
|
|
||||||
ProcessFunc func(handlers.RequestSession) (handlers.RequestSession, error)
|
|
||||||
GetConfigFunc func() engine.Config
|
|
||||||
GetEngineFunc func(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine
|
|
||||||
OutputFunc func(rs handlers.RequestSession) (handlers.RequestSession, error)
|
|
||||||
ResetFunc func(rs handlers.RequestSession) (handlers.RequestSession, error)
|
|
||||||
ShutdownFunc func()
|
|
||||||
GetRequestParserFunc func() handlers.RequestParser
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockRequestHandler) Process(rqs handlers.RequestSession) (handlers.RequestSession, error) {
|
|
||||||
return m.ProcessFunc(rqs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockRequestHandler) GetConfig() engine.Config {
|
|
||||||
return m.GetConfigFunc()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockRequestHandler) GetEngine(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine {
|
|
||||||
return m.GetEngineFunc(cfg, rs, pe)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockRequestHandler) Output(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
|
||||||
return m.OutputFunc(rs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockRequestHandler) Reset(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
|
||||||
return m.ResetFunc(rs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockRequestHandler) Shutdown() {
|
|
||||||
m.ShutdownFunc()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockRequestHandler) GetRequestParser() handlers.RequestParser {
|
|
||||||
return m.GetRequestParserFunc()
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package httpmocks
|
|
||||||
|
|
||||||
// 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(rq any) (string, error) {
|
|
||||||
return m.GetSessionIdFunc(rq)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockRequestParser) GetInput(rq any) ([]byte, error) {
|
|
||||||
return m.GetInputFunc(rq)
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package httpmocks
|
|
||||||
|
|
||||||
import "net/http"
|
|
||||||
|
|
||||||
// MockWriter implements a mock io.Writer for testing
|
|
||||||
type MockWriter struct {
|
|
||||||
WriteStringCalled bool
|
|
||||||
WrittenString string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockWriter) Write(p []byte) (n int, err error) {
|
|
||||||
return len(p), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockWriter) WriteString(s string) (n int, err error) {
|
|
||||||
m.WriteStringCalled = true
|
|
||||||
m.WrittenString = s
|
|
||||||
return len(s), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockWriter) Header() http.Header {
|
|
||||||
return http.Header{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockWriter) WriteHeader(statusCode int) {}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package mocks
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.grassecon.net/urdt/ussd/internal/handlers/server"
|
|
||||||
"git.grassecon.net/urdt/ussd/internal/models"
|
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockAccountService implements AccountServiceInterface for testing
|
|
||||||
type MockAccountService struct {
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockAccountService) CreateAccount() (*server.OKResponse, *server.ErrResponse) {
|
|
||||||
args := m.Called()
|
|
||||||
okResponse, ok := args.Get(0).(*server.OKResponse)
|
|
||||||
errResponse, err := args.Get(1).(*server.ErrResponse)
|
|
||||||
|
|
||||||
if ok {
|
|
||||||
return okResponse, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err {
|
|
||||||
return nil, errResponse
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockAccountService) CheckBalance(publicKey string) (*models.BalanceResponse, error) {
|
|
||||||
args := m.Called(publicKey)
|
|
||||||
return args.Get(0).(*models.BalanceResponse), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockAccountService) CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error) {
|
|
||||||
args := m.Called(trackingId)
|
|
||||||
return args.Get(0).(*models.TrackStatusResponse), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockAccountService) TrackAccountStatus(publicKey string) (*server.OKResponse, *server.ErrResponse) {
|
|
||||||
args := m.Called(publicKey)
|
|
||||||
okResponse, ok := args.Get(0).(*server.OKResponse)
|
|
||||||
errResponse, err := args.Get(1).(*server.ErrResponse)
|
|
||||||
if ok {
|
|
||||||
return okResponse, nil
|
|
||||||
}
|
|
||||||
if err {
|
|
||||||
return nil, errResponse
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
type AccountResponse struct {
|
type AccountResponse struct {
|
||||||
Ok bool `json:"ok"`
|
Ok bool `json:"ok"`
|
||||||
Description string `json:"description"` // Include the description field
|
Result struct {
|
||||||
Result struct {
|
CustodialId json.Number `json:"custodialId"`
|
||||||
PublicKey string `json:"publicKey"`
|
PublicKey string `json:"publicKey"`
|
||||||
TrackingId string `json:"trackingId"`
|
TrackingId string `json:"trackingId"`
|
||||||
} `json:"result"`
|
} `json:"result"`
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
type TrackStatusResponse struct {
|
type TrackStatusResponse struct {
|
||||||
Ok bool `json:"ok"`
|
Ok bool `json:"ok"`
|
||||||
Result struct {
|
Result struct {
|
||||||
@@ -16,4 +17,4 @@ type TrackStatusResponse struct {
|
|||||||
TxType string `json:"txType"`
|
TxType string `json:"txType"`
|
||||||
}
|
}
|
||||||
} `json:"result"`
|
} `json:"result"`
|
||||||
}
|
}
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"git.defalsify.org/vise.git/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
DATATYPE_USERSUB = 64
|
|
||||||
)
|
|
||||||
|
|
||||||
type SubPrefixDb struct {
|
|
||||||
store db.Db
|
|
||||||
pfx []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSubPrefixDb(store db.Db, pfx []byte) *SubPrefixDb {
|
|
||||||
return &SubPrefixDb{
|
|
||||||
store: store,
|
|
||||||
pfx: pfx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func(s *SubPrefixDb) toKey(k []byte) []byte {
|
|
||||||
return append(s.pfx, k...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func(s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) {
|
|
||||||
s.store.SetPrefix(DATATYPE_USERSUB)
|
|
||||||
key = s.toKey(key)
|
|
||||||
v, err := s.store.Get(ctx, key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func(s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error {
|
|
||||||
s.store.SetPrefix(DATATYPE_USERSUB)
|
|
||||||
key = s.toKey(key)
|
|
||||||
return s.store.Put(ctx, key, val)
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
memdb "git.defalsify.org/vise.git/db/mem"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSubPrefix(t *testing.T) {
|
|
||||||
ctx := context.Background()
|
|
||||||
db := memdb.NewMemDb()
|
|
||||||
err := db.Connect(ctx, "")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
sdba := NewSubPrefixDb(db, []byte("tinkywinky"))
|
|
||||||
err = sdba.Put(ctx, []byte("foo"), []byte("dipsy"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := sdba.Get(ctx, []byte("foo"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(r, []byte("dipsy")) {
|
|
||||||
t.Fatalf("expected 'dipsy', got %s", r)
|
|
||||||
}
|
|
||||||
|
|
||||||
sdbb := NewSubPrefixDb(db, []byte("lala"))
|
|
||||||
r, err = sdbb.Get(ctx, []byte("foo"))
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = sdbb.Put(ctx, []byte("foo"), []byte("pu"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
r, err = sdbb.Get(ctx, []byte("foo"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(r, []byte("pu")) {
|
|
||||||
t.Fatalf("expected 'pu', got %s", r)
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err = sdba.Get(ctx, []byte("foo"))
|
|
||||||
if !bytes.Equal(r, []byte("dipsy")) {
|
|
||||||
t.Fatalf("expected 'dipsy', got %s", r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"git.defalsify.org/vise.git/db"
|
|
||||||
"git.defalsify.org/vise.git/lang"
|
|
||||||
gdbmdb "git.defalsify.org/vise.git/db/gdbm"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
dbC map[string]chan db.Db
|
|
||||||
)
|
|
||||||
|
|
||||||
type ThreadGdbmDb struct {
|
|
||||||
db db.Db
|
|
||||||
connStr string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewThreadGdbmDb() *ThreadGdbmDb {
|
|
||||||
if dbC == nil {
|
|
||||||
dbC = make(map[string]chan db.Db)
|
|
||||||
}
|
|
||||||
return &ThreadGdbmDb{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func(tdb *ThreadGdbmDb) Connect(ctx context.Context, connStr string) error {
|
|
||||||
var ok bool
|
|
||||||
_, ok = dbC[connStr]
|
|
||||||
if ok {
|
|
||||||
logg.WarnCtxf(ctx, "already registered thread gdbm, skipping", "connStr", connStr)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
gdb := gdbmdb.NewGdbmDb()
|
|
||||||
err := gdb.Connect(ctx, connStr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dbC[connStr] = make(chan db.Db, 1)
|
|
||||||
dbC[connStr]<- gdb
|
|
||||||
tdb.connStr = connStr
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func(tdb *ThreadGdbmDb) reserve() {
|
|
||||||
if tdb.db == nil {
|
|
||||||
tdb.db = <-dbC[tdb.connStr]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func(tdb *ThreadGdbmDb) release() {
|
|
||||||
if tdb.db == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
dbC[tdb.connStr] <- tdb.db
|
|
||||||
tdb.db = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func(tdb *ThreadGdbmDb) SetPrefix(pfx uint8) {
|
|
||||||
tdb.reserve()
|
|
||||||
tdb.db.SetPrefix(pfx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func(tdb *ThreadGdbmDb) SetSession(sessionId string) {
|
|
||||||
tdb.reserve()
|
|
||||||
tdb.db.SetSession(sessionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
func(tdb *ThreadGdbmDb) SetLanguage(lng *lang.Language) {
|
|
||||||
tdb.reserve()
|
|
||||||
tdb.db.SetLanguage(lng)
|
|
||||||
}
|
|
||||||
|
|
||||||
func(tdb *ThreadGdbmDb) Safe() bool {
|
|
||||||
tdb.reserve()
|
|
||||||
v := tdb.db.Safe()
|
|
||||||
tdb.release()
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func(tdb *ThreadGdbmDb) Prefix() uint8 {
|
|
||||||
tdb.reserve()
|
|
||||||
v := tdb.db.Prefix()
|
|
||||||
tdb.release()
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func(tdb *ThreadGdbmDb) SetLock(typ uint8, locked bool) error {
|
|
||||||
tdb.reserve()
|
|
||||||
err := tdb.db.SetLock(typ, locked)
|
|
||||||
tdb.release()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func(tdb *ThreadGdbmDb) Put(ctx context.Context, key []byte, val []byte) error {
|
|
||||||
tdb.reserve()
|
|
||||||
err := tdb.db.Put(ctx, key, val)
|
|
||||||
tdb.release()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func(tdb *ThreadGdbmDb) Get(ctx context.Context, key []byte) ([]byte, error) {
|
|
||||||
tdb.reserve()
|
|
||||||
v, err := tdb.db.Get(ctx, key)
|
|
||||||
tdb.release()
|
|
||||||
return v, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func(tdb *ThreadGdbmDb) Close() error {
|
|
||||||
tdb.reserve()
|
|
||||||
close(dbC[tdb.connStr])
|
|
||||||
err := tdb.db.Close()
|
|
||||||
tdb.db = nil
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"git.defalsify.org/vise.git/db"
|
|
||||||
fsdb "git.defalsify.org/vise.git/db/fs"
|
|
||||||
"git.defalsify.org/vise.git/persist"
|
|
||||||
"git.defalsify.org/vise.git/resource"
|
|
||||||
"git.defalsify.org/vise.git/logging"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
logg = logging.NewVanilla().WithDomain("storage")
|
|
||||||
)
|
|
||||||
|
|
||||||
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{
|
|
||||||
dbDir string
|
|
||||||
resourceDir string
|
|
||||||
resourceStore db.Db
|
|
||||||
stateStore db.Db
|
|
||||||
userDataStore db.Db
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMenuStorageService(dbDir string, resourceDir string) *MenuStorageService {
|
|
||||||
return &MenuStorageService{
|
|
||||||
dbDir: dbDir,
|
|
||||||
resourceDir: resourceDir,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ms *MenuStorageService) GetPersister(ctx context.Context) (*persist.Persister, error) {
|
|
||||||
ms.stateStore = NewThreadGdbmDb()
|
|
||||||
storeFile := path.Join(ms.dbDir, "state.gdbm")
|
|
||||||
err := ms.stateStore.Connect(ctx, storeFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
pr := persist.NewPersister(ms.stateStore)
|
|
||||||
logg.TraceCtxf(ctx, "menu storage service", "persist", pr, "store", ms.stateStore)
|
|
||||||
return pr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ms *MenuStorageService) GetUserdataDb(ctx context.Context) (db.Db, error) {
|
|
||||||
ms.userDataStore = NewThreadGdbmDb()
|
|
||||||
storeFile := path.Join(ms.dbDir, "userdata.gdbm")
|
|
||||||
err := ms.userDataStore.Connect(ctx, storeFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return ms.userDataStore, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ms *MenuStorageService) GetResource(ctx context.Context) (resource.Resource, error) {
|
|
||||||
ms.resourceStore = fsdb.NewFsDb()
|
|
||||||
err := ms.resourceStore.Connect(ctx, ms.resourceDir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rfs := resource.NewDbResource(ms.resourceStore)
|
|
||||||
return rfs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ms *MenuStorageService) GetStateStore(ctx context.Context) (db.Db, error) {
|
|
||||||
if ms.stateStore != nil {
|
|
||||||
panic("set up store when already exists")
|
|
||||||
}
|
|
||||||
ms.stateStore = NewThreadGdbmDb()
|
|
||||||
storeFile := path.Join(ms.dbDir, "state.gdbm")
|
|
||||||
err := ms.stateStore.Connect(ctx, storeFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return ms.stateStore, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ms *MenuStorageService) Close() error {
|
|
||||||
errA := ms.stateStore.Close()
|
|
||||||
errB := ms.userDataStore.Close()
|
|
||||||
errC := ms.resourceStore.Close()
|
|
||||||
if errA != nil || errB != nil || errC != nil {
|
|
||||||
return fmt.Errorf("%v %v %v", errA, errB, errC)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -22,7 +22,6 @@ const (
|
|||||||
DATA_OFFERINGS
|
DATA_OFFERINGS
|
||||||
DATA_RECIPIENT
|
DATA_RECIPIENT
|
||||||
DATA_AMOUNT
|
DATA_AMOUNT
|
||||||
DATA_TEMPORARY_PIN
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func typToBytes(typ DataTyp) []byte {
|
func typToBytes(typ DataTyp) []byte {
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
var isoCodes = map[string]bool{
|
|
||||||
"eng": true, // English
|
|
||||||
"swa": true, // Swahili
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsValidISO639(code string) bool {
|
|
||||||
return isoCodes[code]
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
# Variables to match files in the current directory
|
# Variables to match files in the current directory
|
||||||
INPUTS = $(wildcard ./*.vis)
|
INPUTS = $(wildcard ./*.vis)
|
||||||
TXTS = $(wildcard ./*.txt.orig)
|
TXTS = $(wildcard ./*.txt.orig)
|
||||||
VISE_PATH := ../../go-vise
|
|
||||||
|
|
||||||
# Rule to build .bin files from .vis files
|
# Rule to build .bin files from .vis files
|
||||||
%.vis:
|
%.vis:
|
||||||
go run $(VISE_PATH)/dev/asm/main.go -f pp.csv $(basename $@).vis > $(basename $@).bin
|
go run ../../go-vise/dev/asm/main.go -f pp.csv $(basename $@).vis > $(basename $@).bin
|
||||||
@echo "Built $(basename $@).bin from $(basename $@).vis"
|
@echo "Built $(basename $@).bin from $(basename $@).vis"
|
||||||
|
|
||||||
# Rule to copy .orig files to .txt
|
# Rule to copy .orig files to .txt
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
Something went wrong.Please try again
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
HALT
|
|
||||||
@@ -5,10 +5,8 @@ MOUT back 0
|
|||||||
HALT
|
HALT
|
||||||
LOAD validate_amount 64
|
LOAD validate_amount 64
|
||||||
RELOAD validate_amount
|
RELOAD validate_amount
|
||||||
CATCH api_failure flag_api_call_error 1
|
|
||||||
CATCH invalid_amount flag_invalid_amount 1
|
CATCH invalid_amount flag_invalid_amount 1
|
||||||
INCMP _ 0
|
INCMP _ 0
|
||||||
LOAD get_recipient 12
|
LOAD get_recipient 12
|
||||||
LOAD get_sender 64
|
LOAD get_sender 64
|
||||||
LOAD get_amount 12
|
|
||||||
INCMP transaction_pin *
|
INCMP transaction_pin *
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
Failed to connect to the custodial service.Please try again.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
MOUT retry 0
|
|
||||||
MOUT quit 9
|
|
||||||
HALT
|
|
||||||
INCMP _ 0
|
|
||||||
INCMP quit 9
|
|
||||||
@@ -1 +1 @@
|
|||||||
Rudi
|
Rudi
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Balances:
|
Balances:
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
LOAD reset_account_authorized 0
|
LOAD reset_account_authorized 0
|
||||||
RELOAD reset_account_authorized
|
|
||||||
MOUT my_balance 1
|
MOUT my_balance 1
|
||||||
MOUT community_balance 2
|
MOUT community_balance 2
|
||||||
MOUT back 0
|
MOUT back 0
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Salio:
|
Salio
|
||||||
@@ -1 +0,0 @@
|
|||||||
Select language:
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
LOAD reset_account_authorized 0
|
|
||||||
LOAD reset_incorrect 0
|
|
||||||
CATCH incorrect_pin flag_incorrect_pin 1
|
|
||||||
CATCH pin_entry flag_account_authorized 0
|
|
||||||
MOUT english 0
|
|
||||||
MOUT kiswahili 1
|
|
||||||
HALT
|
|
||||||
INCMP set_default 0
|
|
||||||
INCMP set_swa 1
|
|
||||||
INCMP . *
|
|
||||||
@@ -1 +1 @@
|
|||||||
Badili lugha
|
Badili lugha
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
Chagua lugha:
|
|
||||||
@@ -1 +1 @@
|
|||||||
Change PIN
|
Change PIN
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Badili PIN
|
Badili PIN
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Check statement
|
Check statement
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
{{.fetch_custodial_balances}}
|
Your community balance is: 0.00SRF
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
LOAD reset_incorrect 6
|
LOAD reset_incorrect 0
|
||||||
LOAD fetch_custodial_balances 0
|
|
||||||
CATCH api_failure flag_api_call_error 1
|
|
||||||
MAP fetch_custodial_balances
|
|
||||||
CATCH incorrect_pin flag_incorrect_pin 1
|
CATCH incorrect_pin flag_incorrect_pin 1
|
||||||
CATCH pin_entry flag_account_authorized 0
|
CATCH pin_entry flag_account_authorized 0
|
||||||
MOUT back 0
|
LOAD quit_with_balance 0
|
||||||
MOUT quit 9
|
|
||||||
HALT
|
HALT
|
||||||
INCMP _ 0
|
|
||||||
INCMP quit 9
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Community balance
|
Community balance
|
||||||
@@ -1 +0,0 @@
|
|||||||
Confirm your new PIN:
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
CATCH invalid_pin flag_valid_pin 0
|
|
||||||
MOUT back 0
|
|
||||||
HALT
|
|
||||||
INCMP _ 0
|
|
||||||
INCMP * pin_reset_success
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Thibitisha PIN yako mpya:
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Edit family name
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Weka jina la familia
|
|
||||||
@@ -1 +1 @@
|
|||||||
Edit name
|
Edit name
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Weka jina
|
Weka jina
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Edit offerings
|
Edit offerings
|
||||||
|
|||||||
@@ -2,19 +2,19 @@ LOAD reset_account_authorized 16
|
|||||||
LOAD reset_allow_update 0
|
LOAD reset_allow_update 0
|
||||||
RELOAD reset_allow_update
|
RELOAD reset_allow_update
|
||||||
MOUT edit_name 1
|
MOUT edit_name 1
|
||||||
MOUT edit_familyname 2
|
MOUT edit_gender 2
|
||||||
MOUT edit_gender 3
|
MOUT edit_yob 3
|
||||||
MOUT edit_yob 4
|
MOUT edit_location 4
|
||||||
MOUT edit_location 5
|
MOUT edit_offerings 5
|
||||||
MOUT edit_offerings 6
|
MOUT view 6
|
||||||
MOUT view 7
|
|
||||||
MOUT back 0
|
MOUT back 0
|
||||||
HALT
|
HALT
|
||||||
INCMP my_account 0
|
INCMP _ 0
|
||||||
|
LOAD set_reset_single_edit 0
|
||||||
|
RELOAD set_reset_single_edit
|
||||||
INCMP enter_name 1
|
INCMP enter_name 1
|
||||||
INCMP enter_familyname 2
|
INCMP select_gender 2
|
||||||
INCMP select_gender 3
|
INCMP enter_yob 3
|
||||||
INCMP enter_yob 4
|
INCMP enter_location 4
|
||||||
INCMP enter_location 5
|
INCMP enter_offerings 5
|
||||||
INCMP enter_offerings 6
|
INCMP view_profile 6
|
||||||
INCMP view_profile 7
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Enter family name:
|
Enter family name:
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
CATCH incorrect_pin flag_incorrect_pin 1
|
LOAD save_firstname 0
|
||||||
CATCH profile_update_success flag_allow_update 1
|
|
||||||
LOAD save_familyname 0
|
|
||||||
RELOAD save_familyname
|
|
||||||
MOUT back 0
|
MOUT back 0
|
||||||
HALT
|
HALT
|
||||||
RELOAD save_familyname
|
|
||||||
INCMP _ 0
|
INCMP _ 0
|
||||||
INCMP pin_entry *
|
INCMP select_gender *
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
Weka jina la familia
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Enter your location:
|
Enter your location:
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
CATCH incorrect_pin flag_incorrect_pin 1
|
CATCH incorrect_date_format flag_incorrect_date_format 1
|
||||||
CATCH profile_update_success flag_allow_update 1
|
LOAD save_yob 0
|
||||||
LOAD save_location 0
|
CATCH update_success flag_allow_update 1
|
||||||
MOUT back 0
|
MOUT back 0
|
||||||
HALT
|
HALT
|
||||||
RELOAD save_location
|
|
||||||
INCMP _ 0
|
INCMP _ 0
|
||||||
INCMP pin_entry *
|
LOAD save_location 0
|
||||||
|
CATCH pin_entry flag_single_edit 1
|
||||||
|
INCMP enter_offerings *
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
CATCH incorrect_pin flag_incorrect_pin 1
|
|
||||||
CATCH profile_update_success flag_allow_update 1
|
|
||||||
LOAD save_firstname 0
|
|
||||||
RELOAD save_firstname
|
|
||||||
MOUT back 0
|
MOUT back 0
|
||||||
HALT
|
HALT
|
||||||
RELOAD save_firstname
|
|
||||||
INCMP _ 0
|
INCMP _ 0
|
||||||
INCMP pin_entry *
|
INCMP enter_familyname *
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Weka majina yako ya kwanza:
|
Weka majina yako ya kwanza:
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
LOAD save_location 0
|
||||||
CATCH incorrect_pin flag_incorrect_pin 1
|
CATCH incorrect_pin flag_incorrect_pin 1
|
||||||
CATCH profile_update_success flag_allow_update 1
|
CATCH update_success flag_allow_update 1
|
||||||
LOAD save_offerings 0
|
|
||||||
MOUT back 0
|
MOUT back 0
|
||||||
HALT
|
HALT
|
||||||
RELOAD save_offerings
|
LOAD save_offerings 0
|
||||||
INCMP _ 0
|
INCMP _ 0
|
||||||
INCMP pin_entry *
|
INCMP pin_entry *
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
CATCH incorrect_pin flag_incorrect_pin 1
|
LOAD save_gender 0
|
||||||
CATCH profile_update_success flag_allow_update 1
|
CATCH update_success flag_allow_update 1
|
||||||
LOAD save_yob 0
|
|
||||||
MOUT back 0
|
MOUT back 0
|
||||||
HALT
|
HALT
|
||||||
LOAD verify_yob 0
|
|
||||||
CATCH incorrect_date_format flag_incorrect_date_format 1
|
|
||||||
RELOAD save_yob
|
|
||||||
INCMP _ 0
|
INCMP _ 0
|
||||||
INCMP pin_entry *
|
LOAD verify_yob 8
|
||||||
|
LOAD save_yob 0
|
||||||
|
CATCH pin_entry flag_single_edit 1
|
||||||
|
INCMP enter_location *
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Female
|
Female
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Guard my PIN
|
Guard my PIN
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Linda PIN yangu
|
Linda PIN yangu
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
The year of birth you entered is invalid.
|
The year of birth you entered is invalid.
|
||||||
Please try again.
|
Please try again.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Incorrect pin
|
Incorrect pin
|
||||||
@@ -1 +0,0 @@
|
|||||||
The PIN you entered is invalid.The PIN must be different from your current PIN.For help call +254757628885
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
MOUT back 0
|
|
||||||
HALT
|
|
||||||
INCMP _ 0
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
PIN mpya na udhibitisho wa pin mpya hazilingani.Tafadhali jaribu tena.Kwa usaidizi piga simu +254757628885.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Your language change request was successful.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
MOUT back 0
|
|
||||||
MOUT quit 9
|
|
||||||
HALT
|
|
||||||
INCMP ^ 0
|
|
||||||
INCMP quit 9
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Ombi lako la kubadilisha lugha limefanikiwa.
|
|
||||||
@@ -6,7 +6,3 @@ msgstr "Ombi lako limetumwa. %s atapokea %s kutoka kwa %s."
|
|||||||
|
|
||||||
msgid "Thank you for using Sarafu. Goodbye!"
|
msgid "Thank you for using Sarafu. Goodbye!"
|
||||||
msgstr "Asante kwa kutumia huduma ya Sarafu. Kwaheri!"
|
msgstr "Asante kwa kutumia huduma ya Sarafu. Kwaheri!"
|
||||||
|
|
||||||
|
|
||||||
msgid "For more help,please call: 0757628885"
|
|
||||||
msgstr "Kwa usaidizi zaidi,piga: 0757628885"
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
LOAD check_balance 64
|
LOAD check_balance 64
|
||||||
RELOAD check_balance
|
RELOAD check_balance
|
||||||
CATCH api_failure flag_api_call_error 1
|
|
||||||
MAP check_balance
|
MAP check_balance
|
||||||
MOUT send 1
|
MOUT send 1
|
||||||
MOUT vouchers 2
|
MOUT vouchers 2
|
||||||
|
|||||||
@@ -7,9 +7,8 @@ MOUT pin_options 5
|
|||||||
MOUT my_address 6
|
MOUT my_address 6
|
||||||
MOUT back 0
|
MOUT back 0
|
||||||
HALT
|
HALT
|
||||||
INCMP main 0
|
INCMP _ 0
|
||||||
INCMP edit_profile 1
|
INCMP edit_profile 1
|
||||||
INCMP change_language 2
|
|
||||||
INCMP balances 3
|
INCMP balances 3
|
||||||
INCMP pin_management 5
|
INCMP pin_management 5
|
||||||
INCMP address 6
|
INCMP address 6
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Anwani yangu
|
Anwani yangu
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{{.fetch_custodial_balances}}
|
Your balance is: 0.00 SRF
|
||||||
@@ -1,11 +1,5 @@
|
|||||||
LOAD reset_incorrect 6
|
LOAD reset_incorrect 0
|
||||||
LOAD fetch_custodial_balances 0
|
|
||||||
CATCH api_failure flag_api_call_error 1
|
|
||||||
MAP fetch_custodial_balances
|
|
||||||
CATCH incorrect_pin flag_incorrect_pin 1
|
CATCH incorrect_pin flag_incorrect_pin 1
|
||||||
CATCH pin_entry flag_account_authorized 0
|
CATCH pin_entry flag_account_authorized 0
|
||||||
MOUT back 0
|
LOAD quit_with_balance 0
|
||||||
MOUT quit 9
|
|
||||||
HALT
|
HALT
|
||||||
INCMP _ 0
|
|
||||||
INCMP quit 9
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Salio lako ni: 0.00 SRF
|
Salio lako ni: 0.00 SRF
|
||||||
|
|||||||
@@ -1,13 +1,3 @@
|
|||||||
LOAD authorize_account 12
|
|
||||||
RELOAD authorize_account
|
|
||||||
CATCH incorrect_pin flag_incorrect_pin 1
|
|
||||||
CATCH old_pin flag_allow_update 0
|
|
||||||
MOUT back 0
|
MOUT back 0
|
||||||
HALT
|
HALT
|
||||||
INCMP _ 0
|
INCMP _ 0
|
||||||
LOAD save_temporary_pin 6
|
|
||||||
LOAD verify_new_pin 0
|
|
||||||
RELOAD save_temporary_pin
|
|
||||||
RELOAD verify_new_pin
|
|
||||||
INCMP * confirm_pin_change
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
Weka PIN mpya ya nne nambari:
|
|
||||||
|
|
||||||
@@ -1 +1 @@
|
|||||||
no
|
no
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
la
|
la
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
LOAD reset_allow_update 0
|
LOAD authorize_account 6
|
||||||
MOUT back 0
|
MOUT back 0
|
||||||
HALT
|
HALT
|
||||||
RELOAD reset_allow_update
|
RELOAD authorize_account
|
||||||
|
CATCH incorrect_pin flag_incorrect_pin 1
|
||||||
|
MOVE new_pin
|
||||||
INCMP _ 0
|
INCMP _ 0
|
||||||
INCMP new_pin *
|
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
Weka PIN yako ya zamani:
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user