Compare commits
287 Commits
lash/draft
...
menu-trave
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc85bd7eed
|
||
|
|
5869324c16
|
||
|
|
1d67fb694b
|
||
|
|
3d409a7a59
|
||
|
|
22047ba703
|
||
|
|
8751c4f5bd
|
||
|
|
fc935535f9
|
||
|
|
ef4846b11f
|
||
|
|
827b99e48d
|
||
|
|
e09c1e1e98
|
||
|
|
87c6029199
|
||
|
|
992dd0bd54
|
||
|
|
4cd7c742ef
|
||
|
|
0d18eec39f
|
||
|
|
bb009fa1e2
|
||
|
|
2ffe749c9d
|
||
|
|
0a92444ae8
|
||
|
|
2703e6d1df
|
||
|
|
86ba03b375
|
||
|
|
f91480f3de
|
||
|
|
79a127cea9
|
||
|
|
6178425355
|
||
|
|
09bf266644
|
||
|
|
bf1df95af1
|
||
|
|
9647d29bd7
|
||
|
|
64cc9bc752
|
||
|
|
ddfd9d4e89
|
||
|
|
b7d93c2249
|
||
|
|
a1201e2525
|
||
|
|
d6cd2766df
|
||
|
|
e2316b38a8
|
||
|
|
d94037d499
|
||
|
|
0f2ec2f2bd
|
||
|
|
089691db4c
|
||
|
|
1b8c8a12c0
|
||
|
|
c965d3083d
|
||
|
|
59aa4eae77
|
||
|
|
587cfb5a36
|
||
|
|
d94758c32c
|
||
|
|
0fa4a81826
|
||
|
|
73013f53bb
|
||
|
|
986f3979fb
|
||
|
|
96ec3919b2
|
||
|
|
079529ebac
|
||
|
|
94bf4ffaa6
|
||
|
|
24380e1449
|
||
|
|
7f6405d356
|
||
|
|
2a1e48e5e8
|
||
|
|
6b8f62fbc6
|
||
|
|
948a9d3a93
|
||
|
|
01476d5408
|
||
|
|
ab02a8882a
|
||
|
|
74fa2c3d65
|
||
|
|
204ce3ce56
|
||
|
|
abbf22086f
|
||
|
|
6eec9571c7
|
||
|
|
7c0ae80bbf
|
||
|
|
1ff2c3602f
|
||
|
|
a0ef57ca15
|
||
|
|
dddb7d7309
|
||
|
|
c5b2348f5e
|
||
|
|
48c608ad4d
|
||
|
|
14218ffb88
|
||
|
|
3e74c1d939
|
||
|
|
285002738f
|
||
|
|
f267aa2b41
|
||
|
|
1b2c6933e1
|
||
|
|
94d1271dbb
|
||
|
|
f3f276c549
|
||
|
|
9eb35ec52d
|
||
|
|
be165e5033 | ||
|
|
e7a3de526c
|
||
|
|
cf5b4ec744
|
||
|
|
80ce141b80
|
||
|
|
170d075545
|
||
|
|
4aad23ab30
|
||
| ddeafe015b | |||
|
|
a3ff3be5b1
|
||
|
|
7d1db50294
|
||
|
|
afe98b8695
|
||
|
|
ea52a7cf0a
|
||
|
|
a993026380
|
||
|
|
ece80b31f6
|
||
|
|
4ee241714b
|
||
|
|
d7dc743fa2 | ||
|
|
c220cbb767
|
||
| 0dc322729f | |||
| 726d6dd338 | |||
| a8b202bd79 | |||
|
|
221db4e998
|
||
|
|
0e376e0d9e
|
||
|
|
7aa44caea2
|
||
|
|
188cb573dd
|
||
|
|
6dbd250694
|
||
|
|
cc760e7698
|
||
|
|
8648ea599c
|
||
|
|
77e4c5d43a
|
||
|
|
b15ba367c4
|
||
|
|
4db862bc97
|
||
|
|
6ad67f6adc
|
||
| 0caf82a27e | |||
| a430857309 | |||
|
|
2643c0c9ff
|
||
|
|
b8852e1ab3
|
||
|
|
0dea34daab
|
||
|
|
8057313c78
|
||
|
|
1bd96d0689
|
||
|
|
bbfe46f162
|
||
|
|
eff2cbde8b
|
||
|
|
5c85ecffd1
|
||
|
|
ad1f9233ca | ||
| e9fdd1ddbe | |||
|
|
8a03b05c90
|
||
|
|
c18c40b1b8 | ||
|
|
5a38b40eaf | ||
|
|
6112ca175f | ||
|
|
c7ebca4729
|
||
|
|
fa0070b9a8
|
||
|
|
4a447fbb45
|
||
|
|
cb32cfe9d8
|
||
|
|
cf4e80dcb8
|
||
|
|
c02d70eaa4
|
||
|
|
909d1f3409
|
||
|
|
66cf90f044
|
||
|
|
74d5da3987 | ||
|
|
f215159725
|
||
|
|
a6301163eb
|
||
|
|
a07fce527f | ||
|
|
8a5209584d
|
||
|
|
ddc8f6dad1
|
||
|
|
c2b68231f5
|
||
|
|
3f3dbf414c
|
||
|
|
e4c3e9f015
|
||
|
|
9b71244391 | ||
|
|
ada1f26b68
|
||
|
|
935b777e57
|
||
|
|
7d3ff690f0
|
||
|
|
a455cfb854
|
||
|
|
19372c17f4
|
||
|
|
1cc2f58ab3
|
||
|
|
3756bdc98a | ||
|
|
e07f88b368
|
||
| 472624a831 | |||
|
|
84422684c5
|
||
|
|
28fc7a0462
|
||
|
|
78d349ccc7
|
||
|
|
0813a619b4
|
||
|
|
5ed9d2643b
|
||
|
|
8dcba67fe9
|
||
|
|
5cac17676a
|
||
| 4439cc249a | |||
|
|
e754773bce
|
||
|
|
63c7c65a00
|
||
|
|
9925b3a2e5
|
||
|
|
a1056cf287
|
||
|
|
e6e701a609
|
||
|
|
b66891a646
|
||
| 7197382911 | |||
| 5c5f4cbc8e | |||
|
|
e581a1ad2f
|
||
| 929e812992 | |||
|
|
3fe66466c5
|
||
|
|
f6979868e5
|
||
|
|
e8be64ae28
|
||
|
|
e4d8bfad7b
|
||
|
|
96358959b4
|
||
|
|
bc9dfe4f65
|
||
|
|
ffa00ae15c
|
||
|
|
13294b42d3
|
||
|
|
81159e77d1
|
||
|
|
5bd51b280e
|
||
|
|
71e1ae6e3c
|
||
|
|
10de039da0
|
||
|
|
27aa71e0ee
|
||
|
|
a9a429824c
|
||
| 4098ac8a19 | |||
| 2982f08b41 | |||
|
|
75ab9c66a3
|
||
| 95f02231b3 | |||
|
|
599f7a2857
|
||
|
|
2e7c07e6f4
|
||
|
|
01f7571185
|
||
|
|
065c8e5c1d
|
||
|
|
dc14480519 | ||
|
|
aaf4923f64 | ||
|
|
e9c645bd87 | ||
|
|
1be6da9139 | ||
|
|
fa2930d93a | ||
|
|
a409e292ab | ||
|
|
00c86a2850 | ||
|
|
4daac7e90b | ||
|
|
06e23565df | ||
| b19188165b | |||
|
|
2ed9f083bb | ||
| e323ffa078 | |||
| 68de83af97 | |||
|
|
a3f2b23128
|
||
|
|
09f970db9b
|
||
|
|
04edd90c21
|
||
|
|
a3f410875a
|
||
|
|
3f3e98e637
|
||
|
|
660f8b7aa2
|
||
|
|
8ae3372c36
|
||
| 3a7c3ffc67 | |||
|
|
78c033e61c
|
||
|
|
b5ef483f34
|
||
|
|
006eef0a28
|
||
|
|
e99a788c60
|
||
| b5d33a98f0 | |||
| f4f475bd45 | |||
| 6fe2e7287f | |||
|
|
1e9c9cf6ad
|
||
|
|
3cb0b099e4
|
||
|
|
b53658e038
|
||
|
|
512460fdeb
|
||
|
|
762f90adf6
|
||
|
|
9b4a4eeaf4
|
||
|
|
5e4e6a21a0
|
||
|
|
ffeb28e851
|
||
|
|
9c751aff30
|
||
|
|
b31d3b907a
|
||
|
|
d49f866ca4
|
||
|
|
dde9f552a6
|
||
| 0f9b5551ec | |||
|
|
514e043e38
|
||
|
|
836e5fe8ee
|
||
|
|
660fcaa0b6 | ||
|
|
44015b1c76
|
||
|
|
7438531900
|
||
|
|
681f293d3c
|
||
|
|
8e3ff27bb8
|
||
|
|
dd2468a4d7 | ||
| 63cee42261 | |||
|
|
9f034967b5
|
||
|
|
ad48890a9f
|
||
|
|
c0a3ad7e2b
|
||
|
|
a3dffdf4e9
|
||
|
|
47d39a1c6f
|
||
|
|
f5f1cbaaba
|
||
|
|
134aa96194
|
||
|
|
0cdb23fbea
|
||
|
|
ca655b0cdc
|
||
|
|
da93444d3f
|
||
|
|
c2564a9b8f
|
||
|
|
33c6b35f8f
|
||
|
|
ba72b3bf44
|
||
|
|
e961b6cb6a
|
||
|
|
2c059afa7d
|
||
|
|
9aab7d3a9b
|
||
|
|
a25beb5b80
|
||
|
|
39d27209cd
|
||
|
|
16a56ef29d
|
||
|
|
6d02ea79ec
|
||
|
|
deb4706b1d
|
||
|
|
e14fd5e496
|
||
|
|
01c13ec581
|
||
|
|
dd97531861
|
||
|
|
be33b7458f
|
||
|
|
4c3f63a48f
|
||
|
|
d1d5c897d1
|
||
|
|
04ea11dd6d
|
||
|
|
5722552395
|
||
|
|
421fbe5543
|
||
|
|
de24ca0648
|
||
|
|
175cbd1983
|
||
|
|
8175d6ac12
|
||
|
|
d4bae50ff0
|
||
|
|
d7376a4196
|
||
|
|
eb9f470ac5
|
||
|
|
7a12588744
|
||
|
|
6947b1e4a4
|
||
|
|
2cc85379cc
|
||
|
|
d001c5a7fc
|
||
|
|
de8c7ce34a
|
||
|
|
cad8e86c8b
|
||
|
|
0feb013c78
|
||
|
|
643b4d87a9
|
||
|
|
5abaf28f49
|
||
|
|
17804e7641
|
||
|
|
db3ec1991d
|
||
|
|
62b7adb967
|
||
|
|
6fa5f49bc0
|
||
|
|
2bf7a5c246
|
||
|
|
98c74dffc4
|
||
|
|
771a1e8169
|
||
|
|
c6c66f956a | ||
|
|
1957606bc2
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,3 +4,5 @@ go.work*
|
||||
**/*/*.bin
|
||||
**/*/.state/
|
||||
cmd/.state/
|
||||
id_*
|
||||
*.gdbm
|
||||
|
||||
169
cmd/africastalking/main.go
Normal file
169
cmd/africastalking/main.go
Normal file
@@ -0,0 +1,169 @@
|
||||
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"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers/server"
|
||||
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(16),
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
accountService := server.AccountService{}
|
||||
hl, err := lhs.GetHandler(&accountService)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
stateStore, err := menuStorageService.GetStateStore(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
defer stateStore.Close()
|
||||
|
||||
rp := &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)
|
||||
}
|
||||
}
|
||||
164
cmd/async/main.go
Normal file
164
cmd/async/main.go
Normal file
@@ -0,0 +1,164 @@
|
||||
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/handlers/server"
|
||||
"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(16),
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
accountService := server.AccountService{}
|
||||
|
||||
hl, err := lhs.GetHandler(&accountService)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
stateStore, err := menuStorageService.GetStateStore(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
defer stateStore.Close()
|
||||
|
||||
rp := &asyncRequestParser{
|
||||
sessionId: sessionId,
|
||||
}
|
||||
sh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
|
||||
cfg.SessionId = sessionId
|
||||
rqs := handlers.RequestSession{
|
||||
Ctx: ctx,
|
||||
Writer: os.Stdout,
|
||||
Config: cfg,
|
||||
}
|
||||
|
||||
cint := make(chan os.Signal)
|
||||
cterm := make(chan os.Signal)
|
||||
signal.Notify(cint, os.Interrupt, syscall.SIGINT)
|
||||
signal.Notify(cterm, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
select {
|
||||
case _ = <-cint:
|
||||
case _ = <-cterm:
|
||||
}
|
||||
sh.Shutdown()
|
||||
}()
|
||||
|
||||
for true {
|
||||
rqs, err = sh.Process(rqs)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "error in process: %v", "err", err)
|
||||
fmt.Errorf("error in process: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
rqs, err = sh.Output(rqs)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "error in output: %v", "err", err)
|
||||
fmt.Errorf("error in output: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
rqs, err = sh.Reset(rqs)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "error in reset: %v", "err", err)
|
||||
fmt.Errorf("error in reset: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("")
|
||||
_, err = fmt.Scanln(&rqs.Input)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "error in input", "err", err)
|
||||
fmt.Errorf("error in input: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
130
cmd/http/main.go
Normal file
130
cmd/http/main.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers/server"
|
||||
httpserver "git.grassecon.net/urdt/ussd/internal/http"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla()
|
||||
scriptDir = path.Join("services", "registration")
|
||||
)
|
||||
|
||||
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(16),
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
accountService := server.AccountService{}
|
||||
hl, err := lhs.GetHandler(&accountService)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
stateStore, err := menuStorageService.GetStateStore(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
defer stateStore.Close()
|
||||
|
||||
rp := &httpserver.DefaultRequestParser{}
|
||||
bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
|
||||
sh := httpserver.ToSessionHandler(bsh)
|
||||
s := &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))),
|
||||
Handler: sh,
|
||||
}
|
||||
s.RegisterOnShutdown(sh.Shutdown)
|
||||
|
||||
cint := make(chan os.Signal)
|
||||
cterm := make(chan os.Signal)
|
||||
signal.Notify(cint, os.Interrupt, syscall.SIGINT)
|
||||
signal.Notify(cterm, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
select {
|
||||
case _ = <-cint:
|
||||
case _ = <-cterm:
|
||||
}
|
||||
s.Shutdown(ctx)
|
||||
}()
|
||||
err = s.ListenAndServe()
|
||||
if err != nil {
|
||||
logg.Infof("Server closed with error", "err", err)
|
||||
}
|
||||
}
|
||||
186
cmd/main.go
186
cmd/main.go
@@ -2,167 +2,103 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"git.defalsify.org/vise.git/cache"
|
||||
fsdb "git.defalsify.org/vise.git/db/fs"
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.defalsify.org/vise.git/state"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers/ussd"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers/server"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla()
|
||||
scriptDir = path.Join("services", "registration")
|
||||
store = fsdb.NewFsDb()
|
||||
pr = persist.NewPersister(store)
|
||||
)
|
||||
|
||||
type menuResource struct {
|
||||
*resource.DbResource
|
||||
}
|
||||
|
||||
func newMenuResource(rs *resource.DbResource) resource.Resource {
|
||||
return &menuResource{
|
||||
rs,
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
var dir string
|
||||
var root string
|
||||
var dbDir string
|
||||
var size uint
|
||||
var sessionId string
|
||||
flag.StringVar(&dir, "d", ".", "resource dir to read from")
|
||||
flag.UintVar(&size, "s", 0, "max size of output")
|
||||
flag.StringVar(&root, "root", "root", "entry point symbol")
|
||||
flag.StringVar(&sessionId, "session-id", "default", "session id")
|
||||
var engineDebug bool
|
||||
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
|
||||
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
||||
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||
flag.UintVar(&size, "s", 160, "max size of output")
|
||||
flag.Parse()
|
||||
fmt.Fprintf(os.Stderr, "starting session at symbol '%s' using resource dir: %s\n", root, dir)
|
||||
|
||||
logg.Infof("start command", "dbdir", dbDir, "outputsize", size)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||
pfp := path.Join(scriptDir, "pp.csv")
|
||||
file, err := os.Open(pfp)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to open CSV file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
reader := csv.NewReader(file)
|
||||
|
||||
// Iterate through the CSV records and register the flags
|
||||
for {
|
||||
record, err := reader.Read()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Error reading CSV file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Ensure the record starts with "flag" and has at least 3 columns
|
||||
if len(record) < 3 || record[0] != "flag" {
|
||||
continue
|
||||
}
|
||||
|
||||
flagName := record[1]
|
||||
flagValue, err := strconv.Atoi(record[2])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to convert flag value %s to integer: %v\n", record[2], err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Register the flag
|
||||
state.FlagDebugger.Register(uint32(flagValue), flagName)
|
||||
}
|
||||
|
||||
ca := cache.NewCache()
|
||||
cfg := engine.Config{
|
||||
Root: "root",
|
||||
SessionId: sessionId,
|
||||
FlagCount: uint32(16),
|
||||
Root: "root",
|
||||
SessionId: sessionId,
|
||||
OutputSize: uint32(size),
|
||||
FlagCount: uint32(16),
|
||||
}
|
||||
|
||||
dp := path.Join(scriptDir, ".state")
|
||||
err = os.MkdirAll(dp, 0700)
|
||||
resourceDir := scriptDir
|
||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
||||
|
||||
err := menuStorageService.EnsureDbDir()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "state dir create exited with error: %v\n", err)
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
store := fsdb.NewFsDb()
|
||||
err = store.Connect(ctx, scriptDir)
|
||||
rs, err := menuStorageService.GetResource(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
rfs := resource.NewDbResource(store)
|
||||
pe, err := menuStorageService.GetPersister(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
rs, ok := newMenuResource(rfs).(*menuResource)
|
||||
userdatastore, err := menuStorageService.GetUserdataDb(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
dbResource, ok := rs.(*resource.DbResource)
|
||||
if !ok {
|
||||
os.Exit(1)
|
||||
}
|
||||
en := engine.NewEngine(cfg, rs)
|
||||
en = en.WithMemory(ca)
|
||||
en = en.WithPersister(pr)
|
||||
|
||||
fp := path.Join(dp, sessionId)
|
||||
|
||||
ussdHandlers, err := ussd.NewHandlers(fp, pr.State, sessionId)
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "handler setup failed with error: %v\n", err)
|
||||
}
|
||||
|
||||
rfs.AddLocalFunc("select_language", ussdHandlers.SetLanguage)
|
||||
rfs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
|
||||
rfs.AddLocalFunc("save_pin", ussdHandlers.SavePin)
|
||||
rfs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin)
|
||||
rfs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier)
|
||||
rfs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus)
|
||||
rfs.AddLocalFunc("authorize_account", ussdHandlers.Authorize)
|
||||
rfs.AddLocalFunc("quit", ussdHandlers.Quit)
|
||||
rfs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance)
|
||||
rfs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient)
|
||||
rfs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset)
|
||||
rfs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount)
|
||||
rfs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount)
|
||||
rfs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount)
|
||||
rfs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient)
|
||||
rfs.AddLocalFunc("get_sender", ussdHandlers.GetSender)
|
||||
rfs.AddLocalFunc("get_amount", ussdHandlers.GetAmount)
|
||||
rfs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin)
|
||||
rfs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname)
|
||||
rfs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname)
|
||||
rfs.AddLocalFunc("save_gender", ussdHandlers.SaveGender)
|
||||
rfs.AddLocalFunc("save_location", ussdHandlers.SaveLocation)
|
||||
rfs.AddLocalFunc("save_yob", ussdHandlers.SaveYob)
|
||||
rfs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings)
|
||||
rfs.AddLocalFunc("quit_with_balance", ussdHandlers.QuitWithBalance)
|
||||
rfs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized)
|
||||
rfs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate)
|
||||
rfs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo)
|
||||
rfs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob)
|
||||
rfs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob)
|
||||
rfs.AddLocalFunc("set_reset_single_edit", ussdHandlers.SetResetSingleEdit)
|
||||
rfs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction)
|
||||
|
||||
_, err = en.Init(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "engine init exited with error: %v\n", err)
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = engine.Loop(ctx, en, os.Stdin, os.Stdout)
|
||||
lhs, err := handlers.NewLocalHandlerService(pfp, true, dbResource, cfg, rs)
|
||||
lhs.SetDataStore(&userdatastore)
|
||||
lhs.SetPersister(pe)
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
accountService := server.AccountService{}
|
||||
hl, err := lhs.GetHandler(&accountService)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
en := lhs.GetEngine()
|
||||
en = en.WithFirst(hl.Init)
|
||||
if engineDebug {
|
||||
en = en.WithDebug(nil)
|
||||
}
|
||||
|
||||
err = engine.Loop(ctx, en, os.Stdin, os.Stdout, nil)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "loop exited with error: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
||||
111
driver/testdriver.go
Normal file
111
driver/testdriver.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package driver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type Step struct {
|
||||
Input string `json:"input"`
|
||||
ExpectedContent string `json:"expectedContent"`
|
||||
}
|
||||
|
||||
func (s *Step) MatchesExpectedContent(content []byte) (bool, error) {
|
||||
pattern := `.*\?.*|.*`
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// Check if the content matches the regex pattern
|
||||
if re.Match(content) {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Group represents a group of steps
|
||||
type Group struct {
|
||||
Name string `json:"name"`
|
||||
Steps []Step `json:"steps"`
|
||||
}
|
||||
|
||||
type TestCase struct {
|
||||
Name string
|
||||
Input string
|
||||
ExpectedContent string
|
||||
}
|
||||
|
||||
func (s *TestCase) MatchesExpectedContent(content []byte) (bool, error) {
|
||||
re, err := regexp.Compile(s.ExpectedContent)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// Check if the content matches the regex pattern
|
||||
if re.Match(content) {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// DataGroup represents the overall structure of the JSON.
|
||||
type DataGroup struct {
|
||||
Groups []Group `json:"groups"`
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
Name string `json:"name"`
|
||||
Groups []Group `json:"groups"`
|
||||
}
|
||||
|
||||
func ReadData() []Session {
|
||||
data, err := os.ReadFile("test_setup.json")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read file: %v", err)
|
||||
}
|
||||
// Unmarshal JSON data
|
||||
var sessions []Session
|
||||
err = json.Unmarshal(data, &sessions)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to unmarshal JSON: %v", err)
|
||||
}
|
||||
|
||||
return sessions
|
||||
}
|
||||
|
||||
func FilterGroupsByName(groups []Group, name string) []Group {
|
||||
var filteredGroups []Group
|
||||
for _, group := range groups {
|
||||
if group.Name == name {
|
||||
filteredGroups = append(filteredGroups, group)
|
||||
}
|
||||
}
|
||||
return filteredGroups
|
||||
}
|
||||
|
||||
func LoadTestGroups(filePath string) (DataGroup, error) {
|
||||
var sessionsData DataGroup
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return sessionsData, err
|
||||
}
|
||||
err = json.Unmarshal(data, &sessionsData)
|
||||
return sessionsData, err
|
||||
}
|
||||
|
||||
func CreateTestCases(group DataGroup) []TestCase {
|
||||
var tests []TestCase
|
||||
for _, group := range group.Groups {
|
||||
for _, step := range group.Steps {
|
||||
// Create a test case for each group
|
||||
tests = append(tests, TestCase{
|
||||
Name: group.Name,
|
||||
Input: step.Input,
|
||||
ExpectedContent: step.ExpectedContent,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return tests
|
||||
}
|
||||
1
go-vise
1
go-vise
Submodule go-vise deleted from 1f47a674d9
24
go.mod
24
go.mod
@@ -2,4 +2,26 @@ module git.grassecon.net/urdt/ussd
|
||||
|
||||
go 1.22.6
|
||||
|
||||
require github.com/stretchr/testify v1.9.0 // indirect
|
||||
require (
|
||||
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb
|
||||
github.com/alecthomas/assert/v2 v2.2.2
|
||||
github.com/peteole/testdata-loader v0.3.0
|
||||
gopkg.in/leonelquinteros/gotext.v1 v1.3.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alecthomas/participle/v2 v2.0.0 // indirect
|
||||
github.com/alecthomas/repr v0.2.0 // indirect
|
||||
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible
|
||||
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 // indirect
|
||||
github.com/hexops/gotextdiff v1.0.3 // indirect
|
||||
github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
36
go.sum
36
go.sum
@@ -1,2 +1,38 @@
|
||||
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.20240923162317-c20d557a3dbb/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M=
|
||||
github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk=
|
||||
github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
|
||||
github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g=
|
||||
github.com/alecthomas/participle/v2 v2.0.0/go.mod h1:rAKZdJldHu8084ojcWevWAL8KmEU+AT+Olodb+WoN2Y=
|
||||
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
|
||||
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c h1:H9Nm+I7Cg/YVPpEV1RzU3Wq2pjamPc/UtHDgItcb7lE=
|
||||
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c/go.mod h1:rGod7o6KPeJ+hyBpHfhi4v7blx9sf+QsHsA7KAsdN6U=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
|
||||
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo=
|
||||
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a h1:0Q3H0YXzMHiciXtRcM+j0jiCe8WKPQHoRgQiRTnfcLY=
|
||||
github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a/go.mod h1:CdTTBOYzS5E4mWS1N8NWP6AHI19MP0A2B18n3hLzRMk=
|
||||
github.com/peteole/testdata-loader v0.3.0 h1:8jckE9KcyNHgyv/VPoaljvKZE0Rqr8+dPVYH6rfNr9I=
|
||||
github.com/peteole/testdata-loader v0.3.0/go.mod h1:Mt0ZbRtb56u8SLJpNP+BnQbENljMorYBpqlvt3cS83U=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
105
internal/handlers/base.go
Normal file
105
internal/handlers/base.go
Normal file
@@ -0,0 +1,105 @@
|
||||
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
|
||||
}
|
||||
106
internal/handlers/handlerservice.go
Normal file
106
internal/handlers/handlerservice.go
Normal file
@@ -0,0 +1,106 @@
|
||||
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/server"
|
||||
"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(accountService server.AccountServiceInterface) (*ussd.Handlers, error) {
|
||||
ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore, accountService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ussdHandlers = ussdHandlers.WithPersister(ls.Pe)
|
||||
ls.DbRs.AddLocalFunc("set_language", ussdHandlers.SetLanguage)
|
||||
ls.DbRs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
|
||||
ls.DbRs.AddLocalFunc("save_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("quit_with_balance", ussdHandlers.QuitWithBalance)
|
||||
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)
|
||||
|
||||
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
|
||||
}
|
||||
@@ -18,7 +18,8 @@ type AccountServiceInterface interface {
|
||||
type AccountService struct {
|
||||
}
|
||||
|
||||
|
||||
type MockAccountService struct {
|
||||
}
|
||||
|
||||
// CheckAccountStatus retrieves the status of an account transaction based on the provided tracking ID.
|
||||
//
|
||||
@@ -27,12 +28,10 @@ type AccountService struct {
|
||||
// CreateAccount or a similar function that returns an AccountResponse. The `trackingId` field in the
|
||||
// AccountResponse struct can be used here to check the account status during a transaction.
|
||||
//
|
||||
//
|
||||
// Returns:
|
||||
// - string: The status of the transaction as a string. If there is an error during the request or processing, this will be an empty string.
|
||||
// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
|
||||
// If no error occurs, this will be nil.
|
||||
//
|
||||
func (as *AccountService) CheckAccountStatus(trackingId string) (string, error) {
|
||||
resp, err := http.Get(config.TrackStatusURL + trackingId)
|
||||
if err != nil {
|
||||
@@ -56,7 +55,6 @@ func (as *AccountService) CheckAccountStatus(trackingId string) (string, error)
|
||||
return status, nil
|
||||
}
|
||||
|
||||
|
||||
// CheckBalance retrieves the balance for a given public key from the custodial balance API endpoint.
|
||||
// Parameters:
|
||||
// - publicKey: The public key associated with the account whose balance needs to be checked.
|
||||
@@ -83,8 +81,7 @@ func (as *AccountService) CheckBalance(publicKey string) (string, error) {
|
||||
return balance, nil
|
||||
}
|
||||
|
||||
|
||||
//CreateAccount creates a new account in the custodial system.
|
||||
// CreateAccount creates a new account in the custodial system.
|
||||
// Returns:
|
||||
// - *models.AccountResponse: A pointer to an AccountResponse struct containing the details of the created account.
|
||||
// If there is an error during the request or processing, this will be nil.
|
||||
@@ -110,3 +107,38 @@ func (as *AccountService) CreateAccount() (*models.AccountResponse, error) {
|
||||
|
||||
return &accountResp, nil
|
||||
}
|
||||
|
||||
func (mas *MockAccountService) CreateAccount() (*models.AccountResponse, error) {
|
||||
return &models.AccountResponse{
|
||||
Ok: true,
|
||||
Result: struct {
|
||||
CustodialId json.Number `json:"custodialId"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
TrackingId string `json:"trackingId"`
|
||||
}{
|
||||
CustodialId: json.Number("182"),
|
||||
PublicKey: "0x48ADca309b5085852207FAaf2816eD72B52F527C",
|
||||
TrackingId: "28ebe84d-b925-472c-87ae-bbdfa1fb97be",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (mas *MockAccountService) CheckBalance(publicKey string) (string, error) {
|
||||
|
||||
balanceResponse := &models.BalanceResponse{
|
||||
Ok: true,
|
||||
Result: struct {
|
||||
Balance string `json:"balance"`
|
||||
Nonce json.Number `json:"nonce"`
|
||||
}{
|
||||
Balance: "0.003 CELO",
|
||||
Nonce: json.Number("0"),
|
||||
},
|
||||
}
|
||||
|
||||
return balanceResponse.Result.Balance, nil
|
||||
}
|
||||
|
||||
func (mas *MockAccountService) CheckAccountStatus(trackingId string) (string, error) {
|
||||
return "SUCCESS", nil
|
||||
}
|
||||
|
||||
54
internal/handlers/single.go
Normal file
54
internal/handlers/single.go
Normal file
@@ -0,0 +1,54 @@
|
||||
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()
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,44 +0,0 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"git.grassecon.net/urdt/ussd/internal/models"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type MockAccountFileHandler struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockAccountFileHandler) EnsureFileExists() error {
|
||||
args := m.Called()
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockAccountFileHandler) ReadAccountData() (map[string]string, error) {
|
||||
args := m.Called()
|
||||
return args.Get(0).(map[string]string), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockAccountFileHandler) WriteAccountData(data map[string]string) error {
|
||||
args := m.Called(data)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
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) CheckAccountStatus(TrackingId string) (string, error) {
|
||||
args := m.Called()
|
||||
return args.Get(0).(string), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockAccountService) CheckBalance(PublicKey string) (string, error) {
|
||||
args := m.Called()
|
||||
return args.Get(0).(string), args.Error(1)
|
||||
}
|
||||
92
internal/http/at_session_handler.go
Normal file
92
internal/http/at_session_handler.go
Normal file
@@ -0,0 +1,92 @@
|
||||
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
|
||||
}
|
||||
449
internal/http/http_test.go
Normal file
449
internal/http/http_test.go
Normal file
@@ -0,0 +1,449 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
122
internal/http/server.go
Normal file
122
internal/http/server.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla().WithDomain("httpserver")
|
||||
)
|
||||
|
||||
type DefaultRequestParser struct {
|
||||
}
|
||||
|
||||
|
||||
func(rp *DefaultRequestParser) GetSessionId(rq any) (string, error) {
|
||||
rqv, ok := rq.(*http.Request)
|
||||
if !ok {
|
||||
return "", handlers.ErrInvalidRequest
|
||||
}
|
||||
v := rqv.Header.Get("X-Vise-Session")
|
||||
if v == "" {
|
||||
return "", handlers.ErrSessionMissing
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func(rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) {
|
||||
rqv, ok := rq.(*http.Request)
|
||||
if !ok {
|
||||
return nil, handlers.ErrInvalidRequest
|
||||
}
|
||||
defer rqv.Body.Close()
|
||||
v, err := ioutil.ReadAll(rqv.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
type SessionHandler struct {
|
||||
handlers.RequestHandler
|
||||
}
|
||||
|
||||
func ToSessionHandler(h handlers.RequestHandler) *SessionHandler {
|
||||
return &SessionHandler{
|
||||
RequestHandler: h,
|
||||
}
|
||||
}
|
||||
|
||||
func(f *SessionHandler) writeError(w http.ResponseWriter, code int, err error) {
|
||||
s := err.Error()
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(s)))
|
||||
w.WriteHeader(code)
|
||||
_, err = w.Write([]byte{})
|
||||
if err != nil {
|
||||
logg.Errorf("error writing error!!", "err", err, "olderr", s)
|
||||
w.WriteHeader(500)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
var code int
|
||||
var err error
|
||||
var perr error
|
||||
|
||||
rqs := handlers.RequestSession{
|
||||
Ctx: req.Context(),
|
||||
Writer: w,
|
||||
}
|
||||
|
||||
rp := f.GetRequestParser()
|
||||
cfg := f.GetConfig()
|
||||
cfg.SessionId, err = rp.GetSessionId(req)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
||||
f.writeError(w, 400, err)
|
||||
}
|
||||
rqs.Config = cfg
|
||||
rqs.Input, err = rp.GetInput(req)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
||||
f.writeError(w, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
rqs, err = f.Process(rqs)
|
||||
switch err {
|
||||
case handlers.ErrStorage:
|
||||
code = 500
|
||||
case handlers.ErrEngineInit:
|
||||
code = 500
|
||||
case handlers.ErrEngineExec:
|
||||
code = 500
|
||||
default:
|
||||
code = 200
|
||||
}
|
||||
|
||||
if code != 200 {
|
||||
f.writeError(w, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(200)
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
rqs, err = f.Output(rqs)
|
||||
rqs, perr = f.Reset(rqs)
|
||||
if err != nil {
|
||||
f.writeError(w, 500, err)
|
||||
return
|
||||
}
|
||||
if perr != nil {
|
||||
f.writeError(w, 500, perr)
|
||||
return
|
||||
}
|
||||
}
|
||||
59
internal/mocks/dbmock.go
Normal file
59
internal/mocks/dbmock.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.defalsify.org/vise.git/lang"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type MockDb struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockDb) SetPrefix(prefix uint8) {
|
||||
m.Called(prefix)
|
||||
}
|
||||
|
||||
func (m *MockDb) Prefix() uint8 {
|
||||
args := m.Called()
|
||||
return args.Get(0).(uint8)
|
||||
}
|
||||
|
||||
func (m *MockDb) Safe() bool {
|
||||
args := m.Called()
|
||||
return args.Get(0).(bool)
|
||||
}
|
||||
|
||||
func (m *MockDb) SetLanguage(language *lang.Language) {
|
||||
m.Called(language)
|
||||
}
|
||||
|
||||
func (m *MockDb) SetLock(uint8, bool) error {
|
||||
args := m.Called()
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockDb) Connect(ctx context.Context, connectionStr string) error {
|
||||
args := m.Called(ctx, connectionStr)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockDb) SetSession(sessionId string) {
|
||||
m.Called(sessionId)
|
||||
}
|
||||
|
||||
func (m *MockDb) Put(ctx context.Context, key, value []byte) error {
|
||||
args := m.Called(ctx, key, value)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockDb) Get(ctx context.Context, key []byte) ([]byte, error) {
|
||||
args := m.Called(ctx, key)
|
||||
return nil, args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockDb) Close() error {
|
||||
args := m.Called(nil)
|
||||
return args.Error(0)
|
||||
}
|
||||
30
internal/mocks/httpmocks/enginemock.go
Normal file
30
internal/mocks/httpmocks/enginemock.go
Normal file
@@ -0,0 +1,30 @@
|
||||
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()
|
||||
}
|
||||
47
internal/mocks/httpmocks/requesthandlermock.go
Normal file
47
internal/mocks/httpmocks/requesthandlermock.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package httpmocks
|
||||
|
||||
import (
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
)
|
||||
|
||||
// MockRequestHandler implements handlers.RequestHandler interface for testing
|
||||
type MockRequestHandler struct {
|
||||
ProcessFunc func(handlers.RequestSession) (handlers.RequestSession, error)
|
||||
GetConfigFunc func() engine.Config
|
||||
GetEngineFunc func(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine
|
||||
OutputFunc func(rs handlers.RequestSession) (handlers.RequestSession, error)
|
||||
ResetFunc func(rs handlers.RequestSession) (handlers.RequestSession, error)
|
||||
ShutdownFunc func()
|
||||
GetRequestParserFunc func() handlers.RequestParser
|
||||
}
|
||||
|
||||
func (m *MockRequestHandler) Process(rqs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return m.ProcessFunc(rqs)
|
||||
}
|
||||
|
||||
func (m *MockRequestHandler) GetConfig() engine.Config {
|
||||
return m.GetConfigFunc()
|
||||
}
|
||||
|
||||
func (m *MockRequestHandler) GetEngine(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine {
|
||||
return m.GetEngineFunc(cfg, rs, pe)
|
||||
}
|
||||
|
||||
func (m *MockRequestHandler) Output(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return m.OutputFunc(rs)
|
||||
}
|
||||
|
||||
func (m *MockRequestHandler) Reset(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return m.ResetFunc(rs)
|
||||
}
|
||||
|
||||
func (m *MockRequestHandler) Shutdown() {
|
||||
m.ShutdownFunc()
|
||||
}
|
||||
|
||||
func (m *MockRequestHandler) GetRequestParser() handlers.RequestParser {
|
||||
return m.GetRequestParserFunc()
|
||||
}
|
||||
15
internal/mocks/httpmocks/requestparsermock.go
Normal file
15
internal/mocks/httpmocks/requestparsermock.go
Normal file
@@ -0,0 +1,15 @@
|
||||
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)
|
||||
}
|
||||
25
internal/mocks/httpmocks/writermock.go
Normal file
25
internal/mocks/httpmocks/writermock.go
Normal file
@@ -0,0 +1,25 @@
|
||||
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) {}
|
||||
26
internal/mocks/servicemock.go
Normal file
26
internal/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)
|
||||
}
|
||||
24
internal/mocks/userdbmock.go
Normal file
24
internal/mocks/userdbmock.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.defalsify.org/vise.git/db"
|
||||
"git.grassecon.net/urdt/ussd/internal/utils"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type MockUserDataStore struct {
|
||||
db.Db
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockUserDataStore) ReadEntry(ctx context.Context, sessionId string, typ utils.DataTyp) ([]byte, error) {
|
||||
args := m.Called(ctx, sessionId, typ)
|
||||
return args.Get(0).([]byte), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserDataStore) WriteEntry(ctx context.Context, sessionId string, typ utils.DataTyp, value []byte) error {
|
||||
args := m.Called(ctx, sessionId, typ, value)
|
||||
return args.Error(0)
|
||||
}
|
||||
116
internal/storage/gdbm.go
Normal file
116
internal/storage/gdbm.go
Normal file
@@ -0,0 +1,116 @@
|
||||
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])
|
||||
delete(dbC, tdb.connStr)
|
||||
err := tdb.db.Close()
|
||||
tdb.db = nil
|
||||
return err
|
||||
}
|
||||
48
internal/storage/storage.go
Normal file
48
internal/storage/storage.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"git.defalsify.org/vise.git/db"
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
)
|
||||
|
||||
const (
|
||||
DATATYPE_CUSTOM = 128
|
||||
)
|
||||
|
||||
type Storage struct {
|
||||
Persister *persist.Persister
|
||||
UserdataDb db.Db
|
||||
}
|
||||
|
||||
type StorageProvider interface {
|
||||
Get(sessionId string) (*Storage, error)
|
||||
Put(sessionId string, storage *Storage) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
type SimpleStorageProvider struct {
|
||||
*Storage
|
||||
}
|
||||
|
||||
func NewSimpleStorageProvider(stateStore db.Db, userdataStore db.Db) StorageProvider {
|
||||
pe := persist.NewPersister(stateStore)
|
||||
pe = pe.WithFlush()
|
||||
return &SimpleStorageProvider{
|
||||
Storage: &Storage{
|
||||
Persister: pe,
|
||||
UserdataDb: userdataStore,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *SimpleStorageProvider) Get(sessionId string) (*Storage, error) {
|
||||
return p.Storage, nil
|
||||
}
|
||||
|
||||
func (p *SimpleStorageProvider) Put(sessionId string, storage *Storage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *SimpleStorageProvider) Close() error {
|
||||
return p.Storage.UserdataDb.Close()
|
||||
}
|
||||
103
internal/storage/storageservice.go
Normal file
103
internal/storage/storageservice.go
Normal file
@@ -0,0 +1,103 @@
|
||||
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
|
||||
}
|
||||
119
internal/testutil/TestEngine.go
Normal file
119
internal/testutil/TestEngine.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers/server"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
testdataloader "github.com/peteole/testdata-loader"
|
||||
)
|
||||
|
||||
var (
|
||||
baseDir = testdataloader.GetBasePath()
|
||||
logg = logging.NewVanilla()
|
||||
scriptDir = path.Join(baseDir, "services", "registration")
|
||||
)
|
||||
|
||||
func TestEngine(sessionId string) (engine.Engine, func(), chan bool) {
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||
pfp := path.Join(scriptDir, "pp.csv")
|
||||
|
||||
var eventChannel = make(chan bool)
|
||||
|
||||
cfg := engine.Config{
|
||||
Root: "root",
|
||||
SessionId: sessionId,
|
||||
OutputSize: uint32(160),
|
||||
FlagCount: uint32(16),
|
||||
}
|
||||
|
||||
dbDir := ".test_state"
|
||||
resourceDir := scriptDir
|
||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
||||
|
||||
err := menuStorageService.EnsureDbDir()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
rs, err := menuStorageService.GetResource(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
pe, err := menuStorageService.GetPersister(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
userDataStore, err := menuStorageService.GetUserdataDb(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
dbResource, ok := rs.(*resource.DbResource)
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
lhs, err := handlers.NewLocalHandlerService(pfp, true, dbResource, cfg, rs)
|
||||
lhs.SetDataStore(&userDataStore)
|
||||
lhs.SetPersister(pe)
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
switch AccountService.(type) {
|
||||
case *server.MockAccountService:
|
||||
go func() {
|
||||
eventChannel <- false
|
||||
}()
|
||||
case *server.AccountService:
|
||||
go func() {
|
||||
time.Sleep(5 * time.Second) // Wait for 5 seconds
|
||||
eventChannel <- true
|
||||
}()
|
||||
default:
|
||||
panic("Unknown account service type")
|
||||
}
|
||||
|
||||
hl, err := lhs.GetHandler(AccountService)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
en := lhs.GetEngine()
|
||||
en = en.WithFirst(hl.Init)
|
||||
cleanFn := func() {
|
||||
err := en.Finish()
|
||||
if err != nil {
|
||||
logg.Errorf(err.Error())
|
||||
}
|
||||
|
||||
err = menuStorageService.Close()
|
||||
if err != nil {
|
||||
logg.Errorf(err.Error())
|
||||
}
|
||||
logg.Infof("testengine storage closed")
|
||||
}
|
||||
|
||||
//en = en.WithDebug(nil)
|
||||
return en, cleanFn, eventChannel
|
||||
}
|
||||
13
internal/testutil/tag_offline.go
Normal file
13
internal/testutil/tag_offline.go
Normal file
@@ -0,0 +1,13 @@
|
||||
// +build !online
|
||||
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers/server"
|
||||
)
|
||||
|
||||
var AccountService server.AccountServiceInterface
|
||||
|
||||
func init() {
|
||||
AccountService = &server.MockAccountService{}
|
||||
}
|
||||
12
internal/testutil/tag_online.go
Normal file
12
internal/testutil/tag_online.go
Normal file
@@ -0,0 +1,12 @@
|
||||
//go:build online
|
||||
// +build online
|
||||
|
||||
package testutil
|
||||
|
||||
import "git.grassecon.net/urdt/ussd/internal/handlers/server"
|
||||
|
||||
var AccountService server.AccountServiceInterface
|
||||
|
||||
func init() {
|
||||
AccountService = &server.AccountService{}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
)
|
||||
|
||||
type AccountFileHandler struct {
|
||||
FilePath string
|
||||
}
|
||||
|
||||
func NewAccountFileHandler(path string) *AccountFileHandler {
|
||||
return &AccountFileHandler{FilePath: path}
|
||||
}
|
||||
|
||||
func (afh *AccountFileHandler) ReadAccountData() (map[string]string, error) {
|
||||
jsonData, err := os.ReadFile(afh.FilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var accountData map[string]string
|
||||
err = json.Unmarshal(jsonData, &accountData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return accountData, nil
|
||||
}
|
||||
|
||||
func (afh *AccountFileHandler) WriteAccountData(accountData map[string]string) error {
|
||||
jsonData, err := json.Marshal(accountData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(afh.FilePath, jsonData, 0644)
|
||||
}
|
||||
|
||||
func (afh *AccountFileHandler) EnsureFileExists() error {
|
||||
f, err := os.OpenFile(afh.FilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return f.Close()
|
||||
}
|
||||
37
internal/utils/db.go
Normal file
37
internal/utils/db.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
type DataTyp uint16
|
||||
|
||||
const (
|
||||
DATA_ACCOUNT DataTyp = iota
|
||||
DATA_ACCOUNT_CREATED
|
||||
DATA_TRACKING_ID
|
||||
DATA_PUBLIC_KEY
|
||||
DATA_CUSTODIAL_ID
|
||||
DATA_ACCOUNT_PIN
|
||||
DATA_ACCOUNT_STATUS
|
||||
DATA_FIRST_NAME
|
||||
DATA_FAMILY_NAME
|
||||
DATA_YOB
|
||||
DATA_LOCATION
|
||||
DATA_GENDER
|
||||
DATA_OFFERINGS
|
||||
DATA_RECIPIENT
|
||||
DATA_AMOUNT
|
||||
DATA_TEMPORARY_PIN
|
||||
)
|
||||
|
||||
func typToBytes(typ DataTyp) []byte {
|
||||
var b [2]byte
|
||||
binary.BigEndian.PutUint16(b[:], uint16(typ))
|
||||
return b[:]
|
||||
}
|
||||
|
||||
func PackKey(typ DataTyp, data []byte) []byte {
|
||||
v := typToBytes(typ)
|
||||
return append(v, data...)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package utils
|
||||
|
||||
|
||||
|
||||
|
||||
type AccountFileHandlerInterface interface {
|
||||
EnsureFileExists() error
|
||||
ReadAccountData() (map[string]string, error)
|
||||
WriteAccountData(data map[string]string) error
|
||||
}
|
||||
|
||||
|
||||
|
||||
11
internal/utils/isocode.go
Normal file
11
internal/utils/isocode.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package utils
|
||||
|
||||
var isoCodes = map[string]bool{
|
||||
"eng": true, // English
|
||||
"swa": true, // Swahili
|
||||
|
||||
}
|
||||
|
||||
func IsValidISO639(code string) bool {
|
||||
return isoCodes[code]
|
||||
}
|
||||
32
internal/utils/userStore.go
Normal file
32
internal/utils/userStore.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.defalsify.org/vise.git/db"
|
||||
)
|
||||
|
||||
type DataStore interface {
|
||||
db.Db
|
||||
ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error)
|
||||
WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error
|
||||
}
|
||||
|
||||
type UserDataStore struct {
|
||||
db.Db
|
||||
}
|
||||
|
||||
// ReadEntry retrieves an entry from the store based on the provided parameters.
|
||||
func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error) {
|
||||
store.SetPrefix(db.DATATYPE_USERDATA)
|
||||
store.SetSession(sessionId)
|
||||
k := PackKey(typ, []byte(sessionId))
|
||||
return store.Get(ctx, k)
|
||||
}
|
||||
|
||||
func (store *UserDataStore) WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error {
|
||||
store.SetPrefix(db.DATATYPE_USERDATA)
|
||||
store.SetSession(sessionId)
|
||||
k := PackKey(typ, []byte(sessionId))
|
||||
return store.Put(ctx, k, value)
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
# Variables to match files in the current directory
|
||||
INPUTS = $(wildcard ./*.vis)
|
||||
TXTS = $(wildcard ./*.txt.orig)
|
||||
VISE_PATH := ../../go-vise
|
||||
|
||||
# Rule to build .bin files from .vis files
|
||||
%.vis:
|
||||
go run ../../go-vise/dev/asm -f pp.csv $(basename $@).vis > $(basename $@).bin
|
||||
go run $(VISE_PATH)/dev/asm/main.go -f pp.csv $(basename $@).vis > $(basename $@).bin
|
||||
@echo "Built $(basename $@).bin from $(basename $@).vis"
|
||||
|
||||
# Rule to copy .orig files to .txt
|
||||
|
||||
1
services/registration/_catch
Normal file
1
services/registration/_catch
Normal file
@@ -0,0 +1 @@
|
||||
Something went wrong.Please try again
|
||||
1
services/registration/_catch.vis
Normal file
1
services/registration/_catch.vis
Normal file
@@ -0,0 +1 @@
|
||||
HALT
|
||||
@@ -9,4 +9,5 @@ CATCH invalid_amount flag_invalid_amount 1
|
||||
INCMP _ 0
|
||||
LOAD get_recipient 12
|
||||
LOAD get_sender 64
|
||||
LOAD get_amount 12
|
||||
INCMP transaction_pin *
|
||||
|
||||
@@ -1 +1 @@
|
||||
Rudi
|
||||
Rudi
|
||||
@@ -1 +1 @@
|
||||
Balances:
|
||||
Balances:
|
||||
@@ -1 +1 @@
|
||||
Salio
|
||||
Salio:
|
||||
1
services/registration/change_language
Normal file
1
services/registration/change_language
Normal file
@@ -0,0 +1 @@
|
||||
Select language:
|
||||
10
services/registration/change_language.vis
Normal file
10
services/registration/change_language.vis
Normal file
@@ -0,0 +1,10 @@
|
||||
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
services/registration/change_language_swa
Normal file
1
services/registration/change_language_swa
Normal file
@@ -0,0 +1 @@
|
||||
Chagua lugha:
|
||||
@@ -1 +1 @@
|
||||
Change PIN
|
||||
Change PIN
|
||||
@@ -1 +1 @@
|
||||
Badili PIN
|
||||
Badili PIN
|
||||
@@ -1 +1 @@
|
||||
Check statement
|
||||
Check statement
|
||||
@@ -1,2 +1 @@
|
||||
Your community balance is: 0.00SRF
|
||||
|
||||
Your community balance is: 0.00SRF
|
||||
@@ -1 +1 @@
|
||||
Community balance
|
||||
Community balance
|
||||
1
services/registration/confirm_pin_change
Normal file
1
services/registration/confirm_pin_change
Normal file
@@ -0,0 +1 @@
|
||||
Confirm your new PIN:
|
||||
5
services/registration/confirm_pin_change.vis
Normal file
5
services/registration/confirm_pin_change.vis
Normal file
@@ -0,0 +1,5 @@
|
||||
CATCH invalid_pin flag_valid_pin 0
|
||||
MOUT back 0
|
||||
HALT
|
||||
INCMP _ 0
|
||||
INCMP pin_reset_success *
|
||||
1
services/registration/confirm_pin_change_swa
Normal file
1
services/registration/confirm_pin_change_swa
Normal file
@@ -0,0 +1 @@
|
||||
Thibitisha PIN yako mpya:
|
||||
1
services/registration/edit_familyname_menu
Normal file
1
services/registration/edit_familyname_menu
Normal file
@@ -0,0 +1 @@
|
||||
Edit family name
|
||||
1
services/registration/edit_familyname_menu_swa
Normal file
1
services/registration/edit_familyname_menu_swa
Normal file
@@ -0,0 +1 @@
|
||||
Weka jina la familia
|
||||
@@ -1 +1 @@
|
||||
Edit name
|
||||
Edit name
|
||||
@@ -1 +1 @@
|
||||
Weka jina
|
||||
Weka jina
|
||||
@@ -1 +1 @@
|
||||
Edit offerings
|
||||
Edit offerings
|
||||
@@ -1,20 +1,21 @@
|
||||
LOAD reset_account_authorized 16
|
||||
RELOAD reset_account_authorized
|
||||
LOAD reset_allow_update 0
|
||||
RELOAD reset_allow_update
|
||||
MOUT edit_name 1
|
||||
MOUT edit_gender 2
|
||||
MOUT edit_yob 3
|
||||
MOUT edit_location 4
|
||||
MOUT edit_offerings 5
|
||||
MOUT view 6
|
||||
MOUT edit_familyname 2
|
||||
MOUT edit_gender 3
|
||||
MOUT edit_yob 4
|
||||
MOUT edit_location 5
|
||||
MOUT edit_offerings 6
|
||||
MOUT view 7
|
||||
MOUT back 0
|
||||
HALT
|
||||
INCMP _ 0
|
||||
LOAD set_reset_single_edit 0
|
||||
RELOAD set_reset_single_edit
|
||||
INCMP my_account 0
|
||||
INCMP enter_name 1
|
||||
INCMP select_gender 2
|
||||
INCMP enter_yob 3
|
||||
INCMP enter_location 4
|
||||
INCMP enter_offerings 5
|
||||
INCMP view_profile 6
|
||||
INCMP enter_familyname 2
|
||||
INCMP select_gender 3
|
||||
INCMP enter_yob 4
|
||||
INCMP enter_location 5
|
||||
INCMP enter_offerings 6
|
||||
INCMP view_profile 7
|
||||
@@ -1 +1 @@
|
||||
Enter family name:
|
||||
Enter family name:
|
||||
@@ -1,5 +1,9 @@
|
||||
LOAD save_firstname 0
|
||||
CATCH incorrect_pin flag_incorrect_pin 1
|
||||
CATCH profile_update_success flag_allow_update 1
|
||||
LOAD save_familyname 0
|
||||
RELOAD save_familyname
|
||||
MOUT back 0
|
||||
HALT
|
||||
RELOAD save_familyname
|
||||
INCMP _ 0
|
||||
INCMP select_gender *
|
||||
INCMP pin_entry *
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Weka jina la familia
|
||||
@@ -1 +1 @@
|
||||
Enter your location:
|
||||
Enter your location:
|
||||
@@ -1,9 +1,8 @@
|
||||
CATCH incorrect_date_format flag_incorrect_date_format 1
|
||||
LOAD save_yob 0
|
||||
CATCH update_success flag_allow_update 1
|
||||
CATCH incorrect_pin flag_incorrect_pin 1
|
||||
CATCH profile_update_success flag_allow_update 1
|
||||
LOAD save_location 0
|
||||
MOUT back 0
|
||||
HALT
|
||||
RELOAD save_location
|
||||
INCMP _ 0
|
||||
LOAD save_location 0
|
||||
CATCH pin_entry flag_single_edit 1
|
||||
INCMP enter_offerings *
|
||||
INCMP pin_entry *
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
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
|
||||
HALT
|
||||
RELOAD save_firstname
|
||||
INCMP _ 0
|
||||
INCMP enter_familyname *
|
||||
INCMP pin_entry *
|
||||
|
||||
@@ -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 update_success flag_allow_update 1
|
||||
CATCH profile_update_success flag_allow_update 1
|
||||
LOAD save_offerings 0
|
||||
MOUT back 0
|
||||
HALT
|
||||
LOAD save_offerings 0
|
||||
RELOAD save_offerings
|
||||
INCMP _ 0
|
||||
INCMP pin_entry *
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
LOAD save_gender 0
|
||||
CATCH update_success flag_allow_update 1
|
||||
CATCH incorrect_pin flag_incorrect_pin 1
|
||||
CATCH profile_update_success flag_allow_update 1
|
||||
LOAD save_yob 0
|
||||
MOUT back 0
|
||||
HALT
|
||||
LOAD verify_yob 0
|
||||
CATCH incorrect_date_format flag_incorrect_date_format 1
|
||||
RELOAD save_yob
|
||||
INCMP _ 0
|
||||
LOAD verify_yob 8
|
||||
LOAD save_yob 0
|
||||
CATCH pin_entry flag_single_edit 1
|
||||
INCMP enter_location *
|
||||
INCMP pin_entry *
|
||||
|
||||
@@ -1 +1 @@
|
||||
Female
|
||||
Female
|
||||
@@ -1 +1 @@
|
||||
Guard my PIN
|
||||
Guard my PIN
|
||||
@@ -1 +1 @@
|
||||
Linda PIN yangu
|
||||
Linda PIN yangu
|
||||
2
services/registration/help.vis
Normal file
2
services/registration/help.vis
Normal file
@@ -0,0 +1,2 @@
|
||||
LOAD quit_with_help 0
|
||||
HALT
|
||||
@@ -1,2 +1,2 @@
|
||||
The year of birth you entered is invalid.
|
||||
Please try again.
|
||||
Please try again.
|
||||
@@ -1 +1 @@
|
||||
Incorrect pin
|
||||
Incorrect pin
|
||||
1
services/registration/invalid_pin
Normal file
1
services/registration/invalid_pin
Normal file
@@ -0,0 +1 @@
|
||||
The PIN you entered is invalid.The PIN must be different from your current PIN.For help call +254757628885
|
||||
3
services/registration/invalid_pin.vis
Normal file
3
services/registration/invalid_pin.vis
Normal file
@@ -0,0 +1,3 @@
|
||||
MOUT back 0
|
||||
HALT
|
||||
INCMP _ 0
|
||||
2
services/registration/invalid_pin_swa
Normal file
2
services/registration/invalid_pin_swa
Normal file
@@ -0,0 +1,2 @@
|
||||
PIN mpya na udhibitisho wa PIN mpya hazilingani. Tafadhali jaribu tena.
|
||||
Kwa usaidizi piga simu +254757628885.
|
||||
1
services/registration/language_changed
Normal file
1
services/registration/language_changed
Normal file
@@ -0,0 +1 @@
|
||||
Your language change request was successful.
|
||||
6
services/registration/language_changed.vis
Normal file
6
services/registration/language_changed.vis
Normal file
@@ -0,0 +1,6 @@
|
||||
RELOAD reset_account_authorized
|
||||
MOUT back 0
|
||||
MOUT quit 9
|
||||
HALT
|
||||
INCMP ^ 0
|
||||
INCMP quit 9
|
||||
1
services/registration/language_changed_swa
Normal file
1
services/registration/language_changed_swa
Normal file
@@ -0,0 +1 @@
|
||||
Ombi lako la kubadilisha lugha limefanikiwa.
|
||||
@@ -6,3 +6,7 @@ msgstr "Ombi lako limetumwa. %s atapokea %s kutoka kwa %s."
|
||||
|
||||
msgid "Thank you for using Sarafu. Goodbye!"
|
||||
msgstr "Asante kwa kutumia huduma ya Sarafu. Kwaheri!"
|
||||
|
||||
|
||||
msgid "For more help,please call: 0757628885"
|
||||
msgstr "Kwa usaidizi zaidi,piga: 0757628885"
|
||||
|
||||
@@ -10,6 +10,6 @@ HALT
|
||||
INCMP send 1
|
||||
INCMP quit 2
|
||||
INCMP my_account 3
|
||||
INCMP quit 4
|
||||
INCMP help 4
|
||||
INCMP quit 9
|
||||
INCMP . *
|
||||
|
||||
@@ -7,8 +7,9 @@ MOUT pin_options 5
|
||||
MOUT my_address 6
|
||||
MOUT back 0
|
||||
HALT
|
||||
INCMP _ 0
|
||||
INCMP main 0
|
||||
INCMP edit_profile 1
|
||||
INCMP change_language 2
|
||||
INCMP balances 3
|
||||
INCMP pin_management 5
|
||||
INCMP address 6
|
||||
|
||||
@@ -1 +1 @@
|
||||
Anwani yangu
|
||||
Anwani yangu
|
||||
@@ -1 +1 @@
|
||||
Salio lako ni: 0.00 SRF
|
||||
Salio lako ni: 0.00 SRF
|
||||
1
services/registration/my_vouchers
Normal file
1
services/registration/my_vouchers
Normal file
@@ -0,0 +1 @@
|
||||
My vouchers
|
||||
6
services/registration/my_vouchers.vis
Normal file
6
services/registration/my_vouchers.vis
Normal file
@@ -0,0 +1,6 @@
|
||||
MOUT select_voucher 1
|
||||
MOUT voucher_details 2
|
||||
MOUT back 0
|
||||
HALT
|
||||
INCMP _ 0
|
||||
INCMP select_voucher 1
|
||||
1
services/registration/new_pin
Normal file
1
services/registration/new_pin
Normal file
@@ -0,0 +1 @@
|
||||
Enter a new four number PIN:
|
||||
10
services/registration/new_pin.vis
Normal file
10
services/registration/new_pin.vis
Normal file
@@ -0,0 +1,10 @@
|
||||
CATCH _ flag_allow_update 0
|
||||
MOUT back 0
|
||||
HALT
|
||||
INCMP _ 0
|
||||
LOAD save_temporary_pin 6
|
||||
RELOAD save_temporary_pin
|
||||
LOAD verify_new_pin 8
|
||||
RELOAD verify_new_pin
|
||||
CATCH incorrect_pin flag_incorrect_pin 1
|
||||
INCMP confirm_pin_change *
|
||||
1
services/registration/new_pin_swa
Normal file
1
services/registration/new_pin_swa
Normal file
@@ -0,0 +1 @@
|
||||
Weka PIN mpya ya nne nambari:
|
||||
@@ -1 +1 @@
|
||||
no
|
||||
no
|
||||
@@ -1 +1 @@
|
||||
la
|
||||
la
|
||||
1
services/registration/old_pin
Normal file
1
services/registration/old_pin
Normal file
@@ -0,0 +1 @@
|
||||
Enter your old PIN
|
||||
9
services/registration/old_pin.vis
Normal file
9
services/registration/old_pin.vis
Normal file
@@ -0,0 +1,9 @@
|
||||
LOAD reset_allow_update 0
|
||||
RELOAD reset_allow_update
|
||||
MOUT back 0
|
||||
HALT
|
||||
LOAD authorize_account 12
|
||||
RELOAD authorize_account
|
||||
CATCH incorrect_pin flag_incorrect_pin 1
|
||||
INCMP _ 0
|
||||
INCMP new_pin *
|
||||
1
services/registration/old_pin_swa
Normal file
1
services/registration/old_pin_swa
Normal file
@@ -0,0 +1 @@
|
||||
Weka PIN yako ya zamani:
|
||||
@@ -1 +1 @@
|
||||
Tafadhali weka PIN yako
|
||||
Tafadhali weka PIN yako
|
||||
@@ -1 +1 @@
|
||||
PIN Management
|
||||
PIN Management
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user