Compare commits
352 Commits
account-st
...
lash/purif
Author | SHA1 | Date | |
---|---|---|---|
|
c93a07832d | ||
|
46bf21b7b8 | ||
|
dff662663d | ||
|
977d14c529 | ||
|
fd6e5caf53 | ||
|
840c22ca89 | ||
|
f939a20543 | ||
|
8387644019 | ||
|
c535938dbc | ||
|
b60d2648ea | ||
|
85ede15613 | ||
b11f11b5fa | |||
3d35a5de78 | |||
a19ace85f8 | |||
|
a7a8a482ab | ||
|
d8e7c443b5 | ||
|
5216a9383e | ||
|
6cd639fd19 | ||
|
a7ca280964 | ||
8f5ed0cd4f | |||
|
f1664a43a8 | ||
c29abfe21e | |||
9a6d8e5158 | |||
|
9ca5091692 | ||
db431c750e | |||
2b9c6d641e | |||
|
b9712098ef | ||
|
bcf1965a6c | ||
3747f87a7c | |||
1f0568df32 | |||
|
24c513d4f0 | ||
b3fd6f5c1a | |||
73eb765408 | |||
f660f6c19a | |||
3fccfaab61 | |||
|
b50a51df9b | ||
|
df8c9aab0c | ||
|
ddefdd7fb3 | ||
5734011f96 | |||
|
379d98ccd5 | ||
f40e11c267 | |||
b698f08136 | |||
4d7589ad95 | |||
efdb52bccd | |||
2ff9fed3c5 | |||
477b4cf8f6 | |||
ed6651697a | |||
c359d99075 | |||
8d477356f3 | |||
7f3294a8a2 | |||
4b5f08e25e | |||
ea9cab930e | |||
a37f6e6da3 | |||
f59c3a53ef | |||
81c3378ea6 | |||
46a6d2bc6e | |||
|
721f80d0f2 | ||
f49e54a562 | |||
|
5081b6d4ce | ||
4d72ae0313 | |||
4fe64a7747 | |||
3004698d5b | |||
50c7ff1046 | |||
07b061a68b | |||
6339f0c2e5 | |||
|
1fa830f286 | ||
64fba91670 | |||
c15958a1ad | |||
ee442daefa | |||
656052dc74 | |||
6c5873da6f | |||
11d30583a4 | |||
f83f539046 | |||
562bd4fa24 | |||
90df0eefc3 | |||
b37f2a0a11 | |||
68e4c9af03 | |||
c12e867ac3 | |||
79de0a9092 | |||
3ee15497a5 | |||
|
e09e324a50 | ||
|
599815c343 | ||
|
462c0d7677 | ||
80b96e9bf6 | |||
b5561decd1 | |||
|
02823fd64e | ||
|
cd575c2edb | ||
f3d4f35718 | |||
52787bdb4d | |||
|
f8d8f265f1 | ||
|
9371b52f3e | ||
|
563000ec15 | ||
824d39908b | |||
|
52fd1eced2 | ||
a312ea5b84 | |||
|
5c7a535288 | ||
4836162f40 | |||
|
cc2f7b41df | ||
|
d39740a09a | ||
|
2024cc96e2 | ||
|
d2d878d5d7 | ||
c995143543 | |||
44570e20ef | |||
362eb209ef | |||
c69d3896f1 | |||
974af6b2a7 | |||
|
bb4037e73f | ||
|
739fd90dfd | ||
|
6789c4f550 | ||
|
437f73827d | ||
|
f0a4a0df61 | ||
|
bd604219b8 | ||
|
51b6fc0dde | ||
|
cc9760125a | ||
|
3a9f3fa373 | ||
|
89c21847b9 | ||
|
450dfa02cc | ||
|
f61e65f4fe | ||
|
a4d6cef9c0 | ||
|
2992f7ae8e | ||
|
dc61d05584 | ||
|
83857026d3 | ||
|
349051b5ef | ||
47b5ff0435 | |||
|
e92e498726 | ||
|
25867cf05e | ||
|
c3cbe1cd92 | ||
|
418080d093 | ||
|
2e30739ec9 | ||
d5a2680500 | |||
|
dc1674ec55 | ||
|
d950b10b50 | ||
|
bcb3ab905e | ||
|
3ed9caf16d | ||
|
86464c31d2 | ||
5ee10d8e14 | |||
62f3681b9e | |||
3ce1435591 | |||
f65c458daa | |||
|
67007fcd48 | ||
|
f1b258fa6d | ||
|
daec816a3e | ||
|
ac0c43cb43 | ||
|
9013cc3618 | ||
|
056d056613 | ||
|
e581ec4771 | ||
|
e16b7445e8 | ||
|
1b12f0ba5f | ||
d2fce05461 | |||
68ac237449 | |||
162e6c1934 | |||
8bd025f2b2 | |||
9d6e25e184 | |||
c26f5683f6 | |||
91dc9ce82f | |||
0fe48a30fa | |||
|
c1e0617bb3 | ||
|
6723884103 | ||
|
b888af446d | ||
|
43b2c3b78d | ||
|
d67853f6d9 | ||
58edfa01a2 | |||
3830c12a57 | |||
f1fd690a7b | |||
|
06230dc557 | ||
491b7424a9 | |||
29ce4b83bd | |||
ca8df5989a | |||
82b4365d16 | |||
98db85511b | |||
99a4d3ff42 | |||
d95c7abea4 | |||
fd1ac85a1b | |||
c899c098f6 | |||
5ca6a74274 | |||
48d63fb43f | |||
|
6ee2c88fe2 | ||
e666c58644 | |||
e980586910 | |||
ffd5be1f1f | |||
ed1aeecf7d | |||
3b69f3d38d | |||
|
cd58f5ae33 | ||
7a535f796a | |||
7c4c73125e | |||
|
c7dbe1d88f | ||
4ea52bf3fb | |||
be2ea3a2f0 | |||
8217ea8fdc | |||
3c73fc7188 | |||
1311a0cab9 | |||
|
3bcd48e5a7 | ||
|
0e12c0ee4e | ||
3caee98cdb | |||
db7c9bf56d | |||
0a332ec501 | |||
90367fe53e | |||
50c006546c | |||
e8c171a82e | |||
58a60f2c81 | |||
0820e1b9f2 | |||
46edf2b819 | |||
11eb61ba35 | |||
813b92af78 | |||
5579991d66 | |||
f4f4fdd3ac | |||
be215d3f75 | |||
235af3519d | |||
1292851226 | |||
dfd0a0994b | |||
97fcdda12f | |||
055c2db790 | |||
ecfdab47a8 | |||
fda68231ea | |||
d08afff443 | |||
17ba6a06ba | |||
dbd59a4023 | |||
5534706189 | |||
5428626c3f | |||
9b33117cb1 | |||
70b2fa4ac2 | |||
fd6ff86579 | |||
549782f230 | |||
8cf4848b45 | |||
9f6c0a1111 | |||
|
1ab49647f6 | ||
|
8d4d8a48e0 | ||
7aea2af9a1 | |||
5cd791aae7 | |||
df5e5f1a4b | |||
64c1fe5276 | |||
f38ea59569 | |||
6cc285d1e8 | |||
0d7f7aaca1 | |||
f8ea2daa73 | |||
|
c820e89cb7 | ||
e05f8e7291 | |||
2383e8ead3 | |||
1a4ee0d3e1 | |||
6f3b30e2fe | |||
b1e4b63c6a | |||
3129e8210e | |||
5d8de80a18 | |||
|
604c16ec90 | ||
|
a3e821fb16 | ||
|
890f50704f | ||
|
3416fdf50c | ||
43892f0d8c | |||
8e6b1e6f52 | |||
|
ff943a125c | ||
9cbbdff993 | |||
316358765d | |||
14737b5f12 | |||
caff27b43d | |||
589a94216b | |||
a659fb06fa | |||
f733fe5636 | |||
18423fcd9c | |||
72a3681767 | |||
160ccbb220 | |||
22f96363ba | |||
3e7f90733e | |||
321f038c7c | |||
7a9de79aae | |||
862830e9de | |||
bc0e536d3d | |||
82884a75a3 | |||
93c44861e0 | |||
4ecfc9de38 | |||
a84c3e0852 | |||
8efed966a0 | |||
e7c4b5bca7 | |||
ed632248c5 | |||
5c202741d6 | |||
c5ebdbf85b | |||
c4282a870e | |||
91cd6077ce | |||
c0ed6fa9c8 | |||
22c9c3e0f0 | |||
a709d24520 | |||
e56138e416 | |||
|
d516584d90 | ||
b420a9bba0 | |||
a20ab79355 | |||
e0ec15b272 | |||
9e998f9a29 | |||
1c7c0af712 | |||
d40a4a171f | |||
ba430a5849 | |||
0f21b01813 | |||
10586baf0d | |||
e979742424 | |||
ff3f049226 | |||
13b45c49da | |||
a72fb08dc8 | |||
944fa89b3c | |||
48e1b02e0e | |||
3e0bbe5ffe | |||
fd586d09c3 | |||
|
c2a4efde2b | ||
b615c27cf6 | |||
22e870b3e5 | |||
da462346f9 | |||
|
406bd84875 | ||
|
c9deca1180 | ||
419cd185fc | |||
7976e237ca | |||
19ec8f0817 | |||
ef3a3d6717 | |||
c2019267d1 | |||
0091fbcabb | |||
|
6d4f3109f8 | ||
|
35cf3a1cd1 | ||
|
1a782c1db9 | ||
|
3d2ca606ca | ||
aa7497573e | |||
54c1fe51ef | |||
7a86b2ad3b | |||
6b23c284e5 | |||
aab6660edd | |||
c46f41e25f | |||
00c0445eed | |||
c8c6b05b8a | |||
08ff1056d7 | |||
0f4a7e900f | |||
2d6c434bde | |||
f5d2644031 | |||
09b4fa2860 | |||
a748c1b6b2 | |||
a17cf78d29 | |||
9847433e0a | |||
7ce50398d1 | |||
e30bc177e9 | |||
b9ff467c0c | |||
1174500e3f | |||
07df450b3c | |||
b8d938d3aa | |||
d1e9340ea9 | |||
8925e26c4c | |||
9b89462797 | |||
7880294c6f | |||
451b15fb6b | |||
d20700ca74 | |||
9cf1cbe425 | |||
584d02db29 | |||
5937c6bf5c | |||
|
10b3083647 | ||
|
bb1a846cb3 | ||
|
967e53d83b | ||
|
d246cdee51 | ||
|
d518a76536 | ||
|
6f65c33be4 |
@ -1,5 +1,6 @@
|
||||
/**
|
||||
!/cmd/africastalking
|
||||
!/cmd/ssh
|
||||
!/common
|
||||
!/config
|
||||
!/initializers
|
||||
|
14
.env.example
14
.env.example
@ -6,15 +6,15 @@ HOST=127.0.0.1
|
||||
AT_ENDPOINT=/ussd/africastalking
|
||||
|
||||
#PostgreSQL
|
||||
DB_HOST=localhost
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=strongpass
|
||||
DB_NAME=urdt_ussd
|
||||
DB_PORT=5432
|
||||
DB_SSLMODE=disable
|
||||
DB_TIMEZONE=Africa/Nairobi
|
||||
DB_CONN=postgres://postgres:strongpass@localhost:5432/urdt_ussd
|
||||
#DB_TIMEZONE=Africa/Nairobi
|
||||
#DB_SCHEMA=vise
|
||||
|
||||
#External API Calls
|
||||
CUSTODIAL_URL_BASE=http://localhost:5003
|
||||
BEARER_TOKEN=eyJeSIsInRcCI6IkpXVCJ.yJwdWJsaWNLZXkiOiIwrrrrrr
|
||||
DATA_URL_BASE=http://localhost:5006
|
||||
|
||||
#Language
|
||||
DEFAULT_LANGUAGE=eng
|
||||
LANGUAGES=eng, swa
|
||||
|
@ -19,6 +19,7 @@ WORKDIR /build
|
||||
RUN echo "Building on $BUILDPLATFORM, building for $TARGETPLATFORM"
|
||||
RUN go mod download
|
||||
RUN go build -tags logtrace -o ussd-africastalking -ldflags="-X main.build=${BUILD} -s -w" cmd/africastalking/main.go
|
||||
RUN go build -tags logtrace -o ussd-ssh -ldflags="-X main.build=${BUILD} -s -w" cmd/ssh/main.go
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
@ -30,6 +31,7 @@ RUN apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /service
|
||||
|
||||
COPY --from=build /build/ussd-africastalking .
|
||||
COPY --from=build /build/ussd-ssh .
|
||||
COPY --from=build /build/LICENSE .
|
||||
COPY --from=build /build/README.md .
|
||||
COPY --from=build /build/services ./services
|
||||
@ -37,5 +39,6 @@ COPY --from=build /build/.env.example .
|
||||
RUN mv .env.example .env
|
||||
|
||||
EXPOSE 7123
|
||||
EXPOSE 7122
|
||||
|
||||
CMD ["./ussd-africastalking"]
|
@ -6,7 +6,7 @@ import (
|
||||
"os"
|
||||
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.grassecon.net/urdt/ussd/internal/utils"
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/utils"
|
||||
)
|
||||
|
||||
var (
|
@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/devtools/admin/commands"
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/cmd/admin/commands"
|
||||
)
|
||||
|
||||
func main() {
|
@ -1,215 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/config"
|
||||
"git.grassecon.net/urdt/ussd/initializers"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
httpserver "git.grassecon.net/urdt/ussd/internal/http"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
"git.grassecon.net/urdt/ussd/remote"
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla()
|
||||
scriptDir = path.Join("services", "registration")
|
||||
|
||||
build = "dev"
|
||||
)
|
||||
|
||||
func init() {
|
||||
initializers.LoadEnvVariables()
|
||||
}
|
||||
|
||||
type atRequestParser struct{}
|
||||
|
||||
func (arp *atRequestParser) GetSessionId(rq any) (string, error) {
|
||||
rqv, ok := rq.(*http.Request)
|
||||
if !ok {
|
||||
log.Println("got an invalid request:", rq)
|
||||
return "", handlers.ErrInvalidRequest
|
||||
}
|
||||
|
||||
// Capture body (if any) for logging
|
||||
body, err := io.ReadAll(rqv.Body)
|
||||
if err != nil {
|
||||
log.Println("failed to read request body:", err)
|
||||
return "", fmt.Errorf("failed to read request body: %v", err)
|
||||
}
|
||||
// Reset the body for further reading
|
||||
rqv.Body = io.NopCloser(bytes.NewReader(body))
|
||||
|
||||
// Log the body as JSON
|
||||
bodyLog := map[string]string{"body": string(body)}
|
||||
logBytes, err := json.Marshal(bodyLog)
|
||||
if err != nil {
|
||||
log.Println("failed to marshal request body:", err)
|
||||
} else {
|
||||
log.Println("Received request:", string(logBytes))
|
||||
}
|
||||
|
||||
if err := rqv.ParseForm(); err != nil {
|
||||
log.Println("failed to parse form data: %v", err)
|
||||
return "", fmt.Errorf("failed to parse form data: %v", err)
|
||||
}
|
||||
|
||||
phoneNumber := rqv.FormValue("phoneNumber")
|
||||
if phoneNumber == "" {
|
||||
return "", fmt.Errorf("no phone number found")
|
||||
}
|
||||
|
||||
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() {
|
||||
config.LoadConfig()
|
||||
|
||||
var dbDir string
|
||||
var resourceDir string
|
||||
var size uint
|
||||
var database string
|
||||
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.StringVar(&database, "db", "gdbm", "database to be used")
|
||||
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||
flag.UintVar(&size, "s", 160, "max size of output")
|
||||
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
|
||||
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
|
||||
flag.Parse()
|
||||
|
||||
logg.Infof("start command", "build", build, "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "Database", database)
|
||||
pfp := path.Join(scriptDir, "pp.csv")
|
||||
|
||||
cfg := engine.Config{
|
||||
Root: "root",
|
||||
OutputSize: uint32(size),
|
||||
FlagCount: uint32(128),
|
||||
}
|
||||
|
||||
if engineDebug {
|
||||
cfg.EngineDebug = true
|
||||
}
|
||||
|
||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
||||
rs, err := menuStorageService.GetResource(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = menuStorageService.EnsureDbDir()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
userdataStore, err := menuStorageService.GetUserdataDb(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
defer userdataStore.Close()
|
||||
|
||||
dbResource, ok := rs.(*resource.DbResource)
|
||||
if !ok {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
lhs.SetDataStore(&userdataStore)
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
accountService := remote.AccountService{}
|
||||
hl, err := lhs.GetHandler(&accountService)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
stateStore, err := menuStorageService.GetStateStore(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
defer stateStore.Close()
|
||||
|
||||
rp := &atRequestParser{}
|
||||
bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
|
||||
sh := httpserver.NewATSessionHandler(bsh)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle(initializers.GetEnv("AT_ENDPOINT", "/"), sh)
|
||||
|
||||
s := &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))),
|
||||
Handler: mux,
|
||||
}
|
||||
s.RegisterOnShutdown(sh.Shutdown)
|
||||
|
||||
cint := make(chan os.Signal)
|
||||
cterm := make(chan os.Signal)
|
||||
signal.Notify(cint, os.Interrupt, syscall.SIGINT)
|
||||
signal.Notify(cterm, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
select {
|
||||
case _ = <-cint:
|
||||
case _ = <-cterm:
|
||||
}
|
||||
s.Shutdown(ctx)
|
||||
}()
|
||||
err = s.ListenAndServe()
|
||||
if err != nil {
|
||||
logg.Infof("Server closed with error", "err", err)
|
||||
}
|
||||
}
|
@ -1,174 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"syscall"
|
||||
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/config"
|
||||
"git.grassecon.net/urdt/ussd/initializers"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
"git.grassecon.net/urdt/ussd/remote"
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla()
|
||||
scriptDir = path.Join("services", "registration")
|
||||
)
|
||||
|
||||
func init() {
|
||||
initializers.LoadEnvVariables()
|
||||
}
|
||||
|
||||
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() {
|
||||
config.LoadConfig()
|
||||
|
||||
var sessionId string
|
||||
var dbDir string
|
||||
var resourceDir string
|
||||
var size uint
|
||||
var database string
|
||||
var engineDebug bool
|
||||
var host string
|
||||
var port uint
|
||||
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
|
||||
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
||||
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
|
||||
flag.StringVar(&database, "db", "gdbm", "database to be used")
|
||||
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||
flag.UintVar(&size, "s", 160, "max size of output")
|
||||
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
|
||||
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
|
||||
flag.Parse()
|
||||
|
||||
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "Database", database)
|
||||
pfp := path.Join(scriptDir, "pp.csv")
|
||||
|
||||
cfg := engine.Config{
|
||||
Root: "root",
|
||||
OutputSize: uint32(size),
|
||||
FlagCount: uint32(128),
|
||||
}
|
||||
|
||||
if engineDebug {
|
||||
cfg.EngineDebug = true
|
||||
}
|
||||
|
||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
||||
rs, err := menuStorageService.GetResource(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = menuStorageService.EnsureDbDir()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
userdataStore, err := menuStorageService.GetUserdataDb(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
defer userdataStore.Close()
|
||||
|
||||
dbResource, ok := rs.(*resource.DbResource)
|
||||
if !ok {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs)
|
||||
lhs.SetDataStore(&userdataStore)
|
||||
accountService := remote.AccountService{}
|
||||
|
||||
hl, err := lhs.GetHandler(&accountService)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
stateStore, err := menuStorageService.GetStateStore(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
defer stateStore.Close()
|
||||
|
||||
rp := &asyncRequestParser{
|
||||
sessionId: sessionId,
|
||||
}
|
||||
sh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
|
||||
cfg.SessionId = sessionId
|
||||
rqs := handlers.RequestSession{
|
||||
Ctx: ctx,
|
||||
Writer: os.Stdout,
|
||||
Config: cfg,
|
||||
}
|
||||
|
||||
cint := make(chan os.Signal)
|
||||
cterm := make(chan os.Signal)
|
||||
signal.Notify(cint, os.Interrupt, syscall.SIGINT)
|
||||
signal.Notify(cterm, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
select {
|
||||
case _ = <-cint:
|
||||
case _ = <-cterm:
|
||||
}
|
||||
sh.Shutdown()
|
||||
}()
|
||||
|
||||
for true {
|
||||
rqs, err = sh.Process(rqs)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "error in process: %v", "err", err)
|
||||
fmt.Errorf("error in process: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
rqs, err = sh.Output(rqs)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "error in output: %v", "err", err)
|
||||
fmt.Errorf("error in output: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
rqs, err = sh.Reset(rqs)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "error in reset: %v", "err", err)
|
||||
fmt.Errorf("error in reset: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("")
|
||||
_, err = fmt.Scanln(&rqs.Input)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "error in input", "err", err)
|
||||
fmt.Errorf("error in input: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
141
cmd/http/main.go
141
cmd/http/main.go
@ -1,141 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/config"
|
||||
"git.grassecon.net/urdt/ussd/initializers"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
httpserver "git.grassecon.net/urdt/ussd/internal/http"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
"git.grassecon.net/urdt/ussd/remote"
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla()
|
||||
scriptDir = path.Join("services", "registration")
|
||||
)
|
||||
|
||||
func init() {
|
||||
initializers.LoadEnvVariables()
|
||||
}
|
||||
|
||||
func main() {
|
||||
config.LoadConfig()
|
||||
|
||||
var dbDir string
|
||||
var resourceDir string
|
||||
var size uint
|
||||
var database string
|
||||
var engineDebug bool
|
||||
var host string
|
||||
var port uint
|
||||
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
||||
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
|
||||
flag.StringVar(&database, "db", "gdbm", "database to be used")
|
||||
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||
flag.UintVar(&size, "s", 160, "max size of output")
|
||||
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
|
||||
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
|
||||
flag.Parse()
|
||||
|
||||
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "Database", database)
|
||||
pfp := path.Join(scriptDir, "pp.csv")
|
||||
|
||||
cfg := engine.Config{
|
||||
Root: "root",
|
||||
OutputSize: uint32(size),
|
||||
FlagCount: uint32(128),
|
||||
}
|
||||
|
||||
if engineDebug {
|
||||
cfg.EngineDebug = true
|
||||
}
|
||||
|
||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
||||
rs, err := menuStorageService.GetResource(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = menuStorageService.EnsureDbDir()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
userdataStore, err := menuStorageService.GetUserdataDb(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
defer userdataStore.Close()
|
||||
|
||||
dbResource, ok := rs.(*resource.DbResource)
|
||||
if !ok {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs)
|
||||
lhs.SetDataStore(&userdataStore)
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
accountService := remote.AccountService{}
|
||||
hl, err := lhs.GetHandler(&accountService)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
stateStore, err := menuStorageService.GetStateStore(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
defer stateStore.Close()
|
||||
|
||||
rp := &httpserver.DefaultRequestParser{}
|
||||
bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
|
||||
sh := httpserver.ToSessionHandler(bsh)
|
||||
s := &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))),
|
||||
Handler: sh,
|
||||
}
|
||||
s.RegisterOnShutdown(sh.Shutdown)
|
||||
|
||||
cint := make(chan os.Signal)
|
||||
cterm := make(chan os.Signal)
|
||||
signal.Notify(cint, os.Interrupt, syscall.SIGINT)
|
||||
signal.Notify(cterm, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
select {
|
||||
case _ = <-cint:
|
||||
case _ = <-cterm:
|
||||
}
|
||||
s.Shutdown(ctx)
|
||||
}()
|
||||
err = s.ListenAndServe()
|
||||
if err != nil {
|
||||
logg.Infof("Server closed with error", "err", err)
|
||||
}
|
||||
}
|
126
cmd/lang/main.go
Normal file
126
cmd/lang/main.go
Normal file
@ -0,0 +1,126 @@
|
||||
// create language files from environment
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/lang"
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/config"
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/initializers"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
changeHeadSrc = `LOAD reset_account_authorized 0
|
||||
LOAD reset_incorrect 0
|
||||
CATCH incorrect_pin flag_incorrect_pin 1
|
||||
CATCH pin_entry flag_account_authorized 0
|
||||
`
|
||||
|
||||
selectSrc = `LOAD set_language 6
|
||||
RELOAD set_language
|
||||
CATCH terms flag_account_created 0
|
||||
MOVE language_changed
|
||||
`
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla()
|
||||
mouts string
|
||||
incmps string
|
||||
)
|
||||
|
||||
func init() {
|
||||
initializers.LoadEnvVariables()
|
||||
}
|
||||
|
||||
func toLanguageLabel(ln lang.Language) string {
|
||||
s := ln.Name
|
||||
v := strings.Split(s, " (")
|
||||
if len(v) > 1 {
|
||||
s = v[0]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func toLanguageKey(ln lang.Language) string {
|
||||
s := toLanguageLabel(ln)
|
||||
return strings.ToLower(s)
|
||||
}
|
||||
|
||||
func main() {
|
||||
var srcDir string
|
||||
|
||||
flag.StringVar(&srcDir, "o", ".", "resource dir write to")
|
||||
flag.Parse()
|
||||
|
||||
logg.Infof("start command", "dir", srcDir)
|
||||
|
||||
err := config.LoadConfig()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "config load error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
logg.Tracef("using languages", "lang", config.Languages)
|
||||
|
||||
for i, v := range(config.Languages) {
|
||||
ln, err := lang.LanguageFromCode(v)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error parsing language: %s\n", v)
|
||||
os.Exit(1)
|
||||
}
|
||||
n := i + 1
|
||||
s := toLanguageKey(ln)
|
||||
mouts += fmt.Sprintf("MOUT %s %v\n", s, n)
|
||||
v = "set_" + ln.Code
|
||||
incmps += fmt.Sprintf("INCMP %s %v\n", v, n)
|
||||
|
||||
p := path.Join(srcDir, v)
|
||||
w, err := os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed open language set template output: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
s = toLanguageLabel(ln)
|
||||
defer w.Close()
|
||||
_, err = w.Write([]byte(s))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
src := mouts + "HALT\n" + incmps
|
||||
src += "INCMP . *\n"
|
||||
|
||||
p := path.Join(srcDir, "select_language.vis")
|
||||
w, err := os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed open select language vis output: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer w.Close()
|
||||
_, err = w.Write([]byte(src))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
src = changeHeadSrc + src
|
||||
p = path.Join(srcDir, "change_language.vis")
|
||||
w, err = os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed open select language vis output: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer w.Close()
|
||||
_, err = w.Write([]byte(src))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
118
cmd/main.go
118
cmd/main.go
@ -1,118 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.grassecon.net/urdt/ussd/config"
|
||||
"git.grassecon.net/urdt/ussd/initializers"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
"git.grassecon.net/urdt/ussd/remote"
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla()
|
||||
scriptDir = path.Join("services", "registration")
|
||||
)
|
||||
|
||||
func init() {
|
||||
initializers.LoadEnvVariables()
|
||||
}
|
||||
|
||||
func main() {
|
||||
config.LoadConfig()
|
||||
|
||||
var dbDir string
|
||||
var size uint
|
||||
var sessionId string
|
||||
var database string
|
||||
var engineDebug bool
|
||||
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
|
||||
flag.StringVar(&database, "db", "gdbm", "database to be used")
|
||||
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
||||
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||
flag.UintVar(&size, "s", 160, "max size of output")
|
||||
flag.Parse()
|
||||
|
||||
logg.Infof("start command", "dbdir", dbDir, "outputsize", size)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||
ctx = context.WithValue(ctx, "Database", database)
|
||||
pfp := path.Join(scriptDir, "pp.csv")
|
||||
|
||||
cfg := engine.Config{
|
||||
Root: "root",
|
||||
SessionId: sessionId,
|
||||
OutputSize: uint32(size),
|
||||
FlagCount: uint32(128),
|
||||
}
|
||||
|
||||
resourceDir := scriptDir
|
||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
||||
|
||||
err := menuStorageService.EnsureDbDir()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
rs, err := menuStorageService.GetResource(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
pe, err := menuStorageService.GetPersister(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
userdatastore, err := menuStorageService.GetUserdataDb(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
dbResource, ok := rs.(*resource.DbResource)
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs)
|
||||
lhs.SetDataStore(&userdatastore)
|
||||
lhs.SetPersister(pe)
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
accountService := remote.AccountService{}
|
||||
hl, err := lhs.GetHandler(&accountService)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
en := lhs.GetEngine()
|
||||
en = en.WithFirst(hl.Init)
|
||||
if engineDebug {
|
||||
en = en.WithDebug(nil)
|
||||
}
|
||||
|
||||
err = engine.Loop(ctx, en, os.Stdin, os.Stdout, nil)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "loop exited with error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
100
cmd/store/dump/main.go
Normal file
100
cmd/store/dump/main.go
Normal file
@ -0,0 +1,100 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/config"
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/initializers"
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/storage"
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/debug"
|
||||
"git.defalsify.org/vise.git/db"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla()
|
||||
scriptDir = path.Join("services", "registration")
|
||||
)
|
||||
|
||||
func init() {
|
||||
initializers.LoadEnvVariables()
|
||||
}
|
||||
|
||||
|
||||
func formatItem(k []byte, v []byte) (string, error) {
|
||||
o, err := debug.FromKey(k)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
s := fmt.Sprintf("%vValue: %v\n\n", o, string(v))
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
config.LoadConfig()
|
||||
|
||||
var connStr string
|
||||
var sessionId string
|
||||
var database string
|
||||
var engineDebug bool
|
||||
var err error
|
||||
|
||||
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
|
||||
flag.StringVar(&connStr, "c", ".state", "connection string")
|
||||
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||
flag.Parse()
|
||||
|
||||
if connStr != "" {
|
||||
connStr = config.DbConn
|
||||
}
|
||||
connData, err := storage.ToConnData(config.DbConn)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logg.Infof("start command", "conn", connData)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||
ctx = context.WithValue(ctx, "Database", database)
|
||||
|
||||
resourceDir := scriptDir
|
||||
menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
|
||||
|
||||
store, err := menuStorageService.GetUserdataDb(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "get userdata db: %v\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
store.SetPrefix(db.DATATYPE_USERDATA)
|
||||
|
||||
d, err := store.Dump(ctx, []byte(sessionId))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "store dump fail: %v\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for true {
|
||||
k, v := d.Next(ctx)
|
||||
if k == nil {
|
||||
break
|
||||
}
|
||||
r, err := formatItem(k, v)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "format db item error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf(r)
|
||||
}
|
||||
|
||||
err = store.Close()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
90
cmd/store/generate/main.go
Normal file
90
cmd/store/generate/main.go
Normal file
@ -0,0 +1,90 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
testdataloader "github.com/peteole/testdata-loader"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/config"
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/storage"
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/initializers"
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/common"
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla()
|
||||
baseDir = testdataloader.GetBasePath()
|
||||
scriptDir = path.Join("services", "registration")
|
||||
)
|
||||
|
||||
func init() {
|
||||
initializers.LoadEnvVariables()
|
||||
}
|
||||
|
||||
func main() {
|
||||
config.LoadConfig()
|
||||
|
||||
var connStr string
|
||||
var sessionId string
|
||||
var database string
|
||||
var engineDebug bool
|
||||
var err error
|
||||
|
||||
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
|
||||
flag.StringVar(&connStr, "c", "", "connection string")
|
||||
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||
flag.Parse()
|
||||
|
||||
if connStr != "" {
|
||||
connStr = config.DbConn
|
||||
}
|
||||
connData, err := storage.ToConnData(config.DbConn)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logg.Infof("start command", "conn", connData)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||
ctx = context.WithValue(ctx, "Database", database)
|
||||
|
||||
resourceDir := scriptDir
|
||||
menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
|
||||
|
||||
store, err := menuStorageService.GetUserdataDb(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
userStore := common.UserDataStore{store}
|
||||
|
||||
h := sha1.New()
|
||||
h.Write([]byte(sessionId))
|
||||
address := h.Sum(nil)
|
||||
addressString := fmt.Sprintf("%x", address)
|
||||
|
||||
err = userStore.WriteEntry(ctx, sessionId, common.DATA_PUBLIC_KEY, []byte(addressString))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = userStore.WriteEntry(ctx, addressString, common.DATA_PUBLIC_KEY_REVERSE, []byte(sessionId))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = store.Close()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
75
common/db.go
75
common/db.go
@ -7,32 +7,88 @@ import (
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
)
|
||||
|
||||
// DataType is a subprefix value used in association with vise/db.DATATYPE_USERDATA.
|
||||
//
|
||||
// All keys are used only within the context of a single account. Unless otherwise specified, the user context is the session id.
|
||||
//
|
||||
// * The first byte is vise/db.DATATYPE_USERDATA
|
||||
// * The last 2 bytes are the DataTyp value, big-endian.
|
||||
// * The intermediate bytes are the id of the user context.
|
||||
//
|
||||
// All values are strings
|
||||
type DataTyp uint16
|
||||
|
||||
const (
|
||||
DATA_ACCOUNT DataTyp = iota
|
||||
DATA_ACCOUNT_CREATED
|
||||
DATA_TRACKING_ID
|
||||
// API Tracking id to follow status of account creation
|
||||
DATA_TRACKING_ID = iota
|
||||
// EVM address returned from API on account creation
|
||||
DATA_PUBLIC_KEY
|
||||
DATA_CUSTODIAL_ID
|
||||
// Currently active PIN used to authenticate ussd state change requests
|
||||
DATA_ACCOUNT_PIN
|
||||
DATA_ACCOUNT_STATUS
|
||||
// The first name of the user
|
||||
DATA_FIRST_NAME
|
||||
// The last name of the user
|
||||
DATA_FAMILY_NAME
|
||||
// The year-of-birth of the user
|
||||
DATA_YOB
|
||||
// The location of the user
|
||||
DATA_LOCATION
|
||||
// The gender of the user
|
||||
DATA_GENDER
|
||||
// The offerings description of the user
|
||||
DATA_OFFERINGS
|
||||
// The ethereum address of the recipient of an ongoing send request
|
||||
DATA_RECIPIENT
|
||||
// The voucher value amount of an ongoing send request
|
||||
DATA_AMOUNT
|
||||
// A general swap field for temporary values
|
||||
DATA_TEMPORARY_VALUE
|
||||
// Currently active voucher symbol of user
|
||||
DATA_ACTIVE_SYM
|
||||
// Voucher balance of user's currently active voucher
|
||||
DATA_ACTIVE_BAL
|
||||
// String boolean indicating whether use of PIN is blocked
|
||||
DATA_BLOCKED_NUMBER
|
||||
// Reverse mapping of a user's evm address to a session id.
|
||||
DATA_PUBLIC_KEY_REVERSE
|
||||
// Decimal count of the currently active voucher
|
||||
DATA_ACTIVE_DECIMAL
|
||||
// EVM address of the currently active voucher
|
||||
DATA_ACTIVE_ADDRESS
|
||||
DATA_TRANSACTIONS
|
||||
//Holds count of the number of incorrect PIN attempts
|
||||
DATA_INCORRECT_PIN_ATTEMPTS
|
||||
//ISO 639 code for the selected language.
|
||||
DATA_SELECTED_LANGUAGE_CODE
|
||||
)
|
||||
|
||||
const (
|
||||
// List of valid voucher symbols in the user context.
|
||||
DATA_VOUCHER_SYMBOLS DataTyp = 256 + iota
|
||||
// List of voucher balances for vouchers valid in the user context.
|
||||
DATA_VOUCHER_BALANCES
|
||||
// List of voucher decimal counts for vouchers valid in the user context.
|
||||
DATA_VOUCHER_DECIMALS
|
||||
// List of voucher EVM addresses for vouchers valid in the user context.
|
||||
DATA_VOUCHER_ADDRESSES
|
||||
// List of senders for valid transactions in the user context.
|
||||
)
|
||||
|
||||
const (
|
||||
DATA_TX_SENDERS = 512 + iota
|
||||
// List of recipients for valid transactions in the user context.
|
||||
DATA_TX_RECIPIENTS
|
||||
// List of voucher values for valid transactions in the user context.
|
||||
DATA_TX_VALUES
|
||||
// List of voucher EVM addresses for valid transactions in the user context.
|
||||
DATA_TX_ADDRESSES
|
||||
// List of valid transaction hashes in the user context.
|
||||
DATA_TX_HASHES
|
||||
// List of transaction dates for valid transactions in the user context.
|
||||
DATA_TX_DATES
|
||||
// List of voucher symbols for valid transactions in the user context.
|
||||
DATA_TX_SYMBOLS
|
||||
// List of voucher decimal counts for valid transactions in the user context.
|
||||
DATA_TX_DECIMALS
|
||||
)
|
||||
|
||||
var (
|
||||
@ -69,3 +125,10 @@ func StringToDataTyp(str string) (DataTyp, error) {
|
||||
return 0, errors.New("invalid DataTyp string")
|
||||
}
|
||||
}
|
||||
|
||||
// ToBytes converts DataTyp or int to a byte slice
|
||||
func ToBytes[T ~uint16 | int](value T) []byte {
|
||||
bytes := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(bytes, uint16(value))
|
||||
return bytes
|
||||
}
|
||||
|
37
common/pin.go
Normal file
37
common/pin.go
Normal file
@ -0,0 +1,37 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
// Define the regex pattern as a constant
|
||||
pinPattern = `^\d{4}$`
|
||||
|
||||
//Allowed incorrect PIN attempts
|
||||
AllowedPINAttempts = uint8(3)
|
||||
|
||||
)
|
||||
|
||||
// checks whether the given input is a 4 digit number
|
||||
func IsValidPIN(pin string) bool {
|
||||
match, _ := regexp.MatchString(pinPattern, pin)
|
||||
return match
|
||||
}
|
||||
|
||||
// HashPIN uses bcrypt with 8 salt rounds to hash the PIN
|
||||
func HashPIN(pin string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(pin), 8)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hash), nil
|
||||
}
|
||||
|
||||
// VerifyPIN compareS the hashed PIN with the plaintext PIN
|
||||
func VerifyPIN(hashedPIN, pin string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hashedPIN), []byte(pin))
|
||||
return err == nil
|
||||
}
|
173
common/pin_test.go
Normal file
173
common/pin_test.go
Normal file
@ -0,0 +1,173 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func TestIsValidPIN(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pin string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "Valid PIN with 4 digits",
|
||||
pin: "1234",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Valid PIN with leading zeros",
|
||||
pin: "0001",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid PIN with less than 4 digits",
|
||||
pin: "123",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid PIN with more than 4 digits",
|
||||
pin: "12345",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid PIN with letters",
|
||||
pin: "abcd",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid PIN with special characters",
|
||||
pin: "12@#",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Empty PIN",
|
||||
pin: "",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actual := IsValidPIN(tt.pin)
|
||||
if actual != tt.expected {
|
||||
t.Errorf("IsValidPIN(%q) = %v; expected %v", tt.pin, actual, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashPIN(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pin string
|
||||
}{
|
||||
{
|
||||
name: "Valid PIN with 4 digits",
|
||||
pin: "1234",
|
||||
},
|
||||
{
|
||||
name: "Valid PIN with leading zeros",
|
||||
pin: "0001",
|
||||
},
|
||||
{
|
||||
name: "Empty PIN",
|
||||
pin: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
hashedPIN, err := HashPIN(tt.pin)
|
||||
if err != nil {
|
||||
t.Errorf("HashPIN(%q) returned an error: %v", tt.pin, err)
|
||||
return
|
||||
}
|
||||
|
||||
if hashedPIN == "" {
|
||||
t.Errorf("HashPIN(%q) returned an empty hash", tt.pin)
|
||||
}
|
||||
|
||||
// Ensure the hash can be verified with bcrypt
|
||||
err = bcrypt.CompareHashAndPassword([]byte(hashedPIN), []byte(tt.pin))
|
||||
if tt.pin != "" && err != nil {
|
||||
t.Errorf("HashPIN(%q) produced a hash that does not match: %v", tt.pin, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyMigratedHashPin(t *testing.T) {
|
||||
tests := []struct {
|
||||
pin string
|
||||
hash string
|
||||
}{
|
||||
{
|
||||
pin: "1234",
|
||||
hash: "$2b$08$dTvIGxCCysJtdvrSnaLStuylPoOS/ZLYYkxvTeR5QmTFY3TSvPQC6",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.pin, func(t *testing.T) {
|
||||
ok := VerifyPIN(tt.hash, tt.pin)
|
||||
if !ok {
|
||||
t.Errorf("VerifyPIN could not verify migrated PIN: %v", tt.pin)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyPIN(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pin string
|
||||
hashedPIN string
|
||||
shouldPass bool
|
||||
}{
|
||||
{
|
||||
name: "Valid PIN verification",
|
||||
pin: "1234",
|
||||
hashedPIN: hashPINHelper("1234"),
|
||||
shouldPass: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid PIN verification with incorrect PIN",
|
||||
pin: "5678",
|
||||
hashedPIN: hashPINHelper("1234"),
|
||||
shouldPass: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid PIN verification with empty PIN",
|
||||
pin: "",
|
||||
hashedPIN: hashPINHelper("1234"),
|
||||
shouldPass: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid PIN verification with invalid hash",
|
||||
pin: "1234",
|
||||
hashedPIN: "invalidhash",
|
||||
shouldPass: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := VerifyPIN(tt.hashedPIN, tt.pin)
|
||||
if result != tt.shouldPass {
|
||||
t.Errorf("VerifyPIN(%q, %q) = %v; expected %v", tt.hashedPIN, tt.pin, result, tt.shouldPass)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to hash a PIN for testing purposes
|
||||
func hashPINHelper(pin string) string {
|
||||
hashedPIN, err := HashPIN(pin)
|
||||
if err != nil {
|
||||
panic("Failed to hash PIN for test setup: " + err.Error())
|
||||
}
|
||||
return hashedPIN
|
||||
}
|
73
common/recipient.go
Normal file
73
common/recipient.go
Normal file
@ -0,0 +1,73 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Define the regex patterns as constants
|
||||
const (
|
||||
phoneRegex = `^(?:\+254|254|0)?((?:7[0-9]{8})|(?:1[01][0-9]{7}))$`
|
||||
addressRegex = `^0x[a-fA-F0-9]{40}$`
|
||||
aliasRegex = `^[a-zA-Z0-9]+$`
|
||||
)
|
||||
|
||||
// IsValidPhoneNumber checks if the given number is a valid phone number
|
||||
func IsValidPhoneNumber(phonenumber string) bool {
|
||||
match, _ := regexp.MatchString(phoneRegex, phonenumber)
|
||||
return match
|
||||
}
|
||||
|
||||
// IsValidAddress checks if the given address is a valid Ethereum address
|
||||
func IsValidAddress(address string) bool {
|
||||
match, _ := regexp.MatchString(addressRegex, address)
|
||||
return match
|
||||
}
|
||||
|
||||
// IsValidAlias checks if the alias is a valid alias format
|
||||
func IsValidAlias(alias string) bool {
|
||||
match, _ := regexp.MatchString(aliasRegex, alias)
|
||||
return match
|
||||
}
|
||||
|
||||
// CheckRecipient validates the recipient format based on the criteria
|
||||
func CheckRecipient(recipient string) (string, error) {
|
||||
if IsValidPhoneNumber(recipient) {
|
||||
return "phone number", nil
|
||||
}
|
||||
|
||||
if IsValidAddress(recipient) {
|
||||
return "address", nil
|
||||
}
|
||||
|
||||
if IsValidAlias(recipient) {
|
||||
return "alias", nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("invalid recipient: must be a phone number, address or alias")
|
||||
}
|
||||
|
||||
// FormatPhoneNumber formats a Kenyan phone number to "+254xxxxxxxx".
|
||||
func FormatPhoneNumber(phone string) (string, error) {
|
||||
if !IsValidPhoneNumber(phone) {
|
||||
return "", errors.New("invalid phone number")
|
||||
}
|
||||
|
||||
// Remove any leading "+" and spaces
|
||||
phone = strings.TrimPrefix(phone, "+")
|
||||
phone = strings.ReplaceAll(phone, " ", "")
|
||||
|
||||
// Replace leading "0" with "254" if present
|
||||
if strings.HasPrefix(phone, "0") {
|
||||
phone = "254" + phone[1:]
|
||||
}
|
||||
|
||||
// Add "+" if not already present
|
||||
if !strings.HasPrefix(phone, "254") {
|
||||
return "", errors.New("unexpected format")
|
||||
}
|
||||
|
||||
return "+" + phone, nil
|
||||
}
|
@ -7,32 +7,49 @@ import (
|
||||
"git.defalsify.org/vise.git/db"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
"git.defalsify.org/vise.git/lang"
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/storage"
|
||||
dbstorage "git.grassecon.net/grassrootseconomics/visedriver/storage/db"
|
||||
)
|
||||
|
||||
var (
|
||||
ToConnData = storage.ToConnData
|
||||
)
|
||||
|
||||
func StoreToDb(store *UserDataStore) db.Db {
|
||||
return store.Db
|
||||
}
|
||||
|
||||
func StoreToPrefixDb(store *UserDataStore, pfx []byte) storage.PrefixDb {
|
||||
return storage.NewSubPrefixDb(store.Db, pfx)
|
||||
func StoreToPrefixDb(store *UserDataStore, pfx []byte) dbstorage.PrefixDb {
|
||||
return dbstorage.NewSubPrefixDb(store.Db, pfx)
|
||||
}
|
||||
|
||||
type StorageServices interface {
|
||||
GetPersister(ctx context.Context) (*persist.Persister, error)
|
||||
GetUserdataDb(ctx context.Context) (db.Db, error)
|
||||
GetResource(ctx context.Context) (resource.Resource, error)
|
||||
EnsureDbDir() error
|
||||
}
|
||||
|
||||
type StorageService struct {
|
||||
svc *storage.MenuStorageService
|
||||
}
|
||||
|
||||
func NewStorageService(dbDir string) *StorageService {
|
||||
return &StorageService{
|
||||
svc: storage.NewMenuStorageService(dbDir, ""),
|
||||
func NewStorageService(conn storage.ConnData) (*StorageService, error) {
|
||||
svc := &StorageService{
|
||||
svc: storage.NewMenuStorageService(conn, ""),
|
||||
}
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
func (ss *StorageService) WithGettext(path string, lns []lang.Language) *StorageService {
|
||||
ss.svc = ss.svc.WithGettext(path, lns)
|
||||
return ss
|
||||
}
|
||||
|
||||
// TODO: simplify enable poresource, conndata instead
|
||||
func(ss *StorageService) SetResourceDir(resourceDir string) error {
|
||||
ss.svc = ss.svc.WithResourceDir(resourceDir)
|
||||
return nil
|
||||
}
|
||||
|
||||
func(ss *StorageService) GetPersister(ctx context.Context) (*persist.Persister, error) {
|
||||
@ -47,6 +64,6 @@ func(ss *StorageService) GetResource(ctx context.Context) (resource.Resource, er
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func(ss *StorageService) EnsureDbDir() error {
|
||||
return ss.svc.EnsureDbDir()
|
||||
func(ss *StorageService) GetStateStore(ctx context.Context) (db.Db, error) {
|
||||
return ss.svc.GetStateStore(ctx)
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
dbstorage "git.grassecon.net/grassrootseconomics/visedriver/storage/db"
|
||||
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||
)
|
||||
|
||||
@ -56,26 +56,26 @@ func ProcessTransfers(transfers []dataserviceapi.Last10TxResponse) TransferMetad
|
||||
|
||||
// GetTransferData retrieves and matches transfer data
|
||||
// returns a formatted string of the full transaction/statement
|
||||
func GetTransferData(ctx context.Context, db storage.PrefixDb, publicKey string, index int) (string, error) {
|
||||
keys := []string{"txfrom", "txto", "txval", "txaddr", "txhash", "txdate", "txsym"}
|
||||
data := make(map[string]string)
|
||||
func GetTransferData(ctx context.Context, db dbstorage.PrefixDb, publicKey string, index int) (string, error) {
|
||||
keys := []DataTyp{DATA_TX_SENDERS, DATA_TX_RECIPIENTS, DATA_TX_VALUES, DATA_TX_ADDRESSES, DATA_TX_HASHES, DATA_TX_DATES, DATA_TX_SYMBOLS}
|
||||
data := make(map[DataTyp]string)
|
||||
|
||||
for _, key := range keys {
|
||||
value, err := db.Get(ctx, []byte(key))
|
||||
value, err := db.Get(ctx, ToBytes(key))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get %s: %v", key, err)
|
||||
return "", fmt.Errorf("failed to get %s: %v", ToBytes(key), err)
|
||||
}
|
||||
data[key] = string(value)
|
||||
}
|
||||
|
||||
// Split the data
|
||||
senders := strings.Split(string(data["txfrom"]), "\n")
|
||||
recipients := strings.Split(string(data["txto"]), "\n")
|
||||
values := strings.Split(string(data["txval"]), "\n")
|
||||
addresses := strings.Split(string(data["txaddr"]), "\n")
|
||||
hashes := strings.Split(string(data["txhash"]), "\n")
|
||||
dates := strings.Split(string(data["txdate"]), "\n")
|
||||
syms := strings.Split(string(data["txsym"]), "\n")
|
||||
senders := strings.Split(string(data[DATA_TX_SENDERS]), "\n")
|
||||
recipients := strings.Split(string(data[DATA_TX_RECIPIENTS]), "\n")
|
||||
values := strings.Split(string(data[DATA_TX_VALUES]), "\n")
|
||||
addresses := strings.Split(string(data[DATA_TX_ADDRESSES]), "\n")
|
||||
hashes := strings.Split(string(data[DATA_TX_HASHES]), "\n")
|
||||
dates := strings.Split(string(data[DATA_TX_DATES]), "\n")
|
||||
syms := strings.Split(string(data[DATA_TX_SYMBOLS]), "\n")
|
||||
|
||||
// Check if index is within range
|
||||
if index < 1 || index > len(senders) {
|
||||
@ -84,18 +84,18 @@ func GetTransferData(ctx context.Context, db storage.PrefixDb, publicKey string,
|
||||
|
||||
// Adjust for 0-based indexing
|
||||
i := index - 1
|
||||
transactionType := "received"
|
||||
party := fmt.Sprintf("from: %s", strings.TrimSpace(senders[i]))
|
||||
transactionType := "Received"
|
||||
party := fmt.Sprintf("From: %s", strings.TrimSpace(senders[i]))
|
||||
if strings.TrimSpace(senders[i]) == publicKey {
|
||||
transactionType = "sent"
|
||||
party = fmt.Sprintf("to: %s", strings.TrimSpace(recipients[i]))
|
||||
transactionType = "Sent"
|
||||
party = fmt.Sprintf("To: %s", strings.TrimSpace(recipients[i]))
|
||||
}
|
||||
|
||||
formattedDate := formatDate(strings.TrimSpace(dates[i]))
|
||||
|
||||
// Build the full transaction detail
|
||||
detail := fmt.Sprintf(
|
||||
"%s %s %s\n%s\ncontract address: %s\ntxhash: %s\ndate: %s",
|
||||
"%s %s %s\n%s\nContract address: %s\nTxhash: %s\nDate: %s",
|
||||
transactionType,
|
||||
strings.TrimSpace(values[i]),
|
||||
strings.TrimSpace(syms[i]),
|
||||
|
@ -20,7 +20,7 @@ type UserDataStore struct {
|
||||
func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error) {
|
||||
store.SetPrefix(db.DATATYPE_USERDATA)
|
||||
store.SetSession(sessionId)
|
||||
k := PackKey(typ, []byte(sessionId))
|
||||
k := ToBytes(typ)
|
||||
return store.Get(ctx, k)
|
||||
}
|
||||
|
||||
@ -29,6 +29,6 @@ func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ
|
||||
func (store *UserDataStore) WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error {
|
||||
store.SetPrefix(db.DATATYPE_USERDATA)
|
||||
store.SetSession(sessionId)
|
||||
k := PackKey(typ, []byte(sessionId))
|
||||
k := ToBytes(typ)
|
||||
return store.Put(ctx, k, value)
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
dbstorage "git.grassecon.net/grassrootseconomics/visedriver/storage/db"
|
||||
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||
)
|
||||
|
||||
@ -63,23 +63,24 @@ func ScaleDownBalance(balance, decimals string) string {
|
||||
}
|
||||
|
||||
// GetVoucherData retrieves and matches voucher data
|
||||
func GetVoucherData(ctx context.Context, db storage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) {
|
||||
keys := []string{"sym", "bal", "deci", "addr"}
|
||||
data := make(map[string]string)
|
||||
func GetVoucherData(ctx context.Context, db dbstorage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) {
|
||||
keys := []DataTyp{DATA_VOUCHER_SYMBOLS, DATA_VOUCHER_BALANCES, DATA_VOUCHER_DECIMALS, DATA_VOUCHER_ADDRESSES}
|
||||
data := make(map[DataTyp]string)
|
||||
|
||||
for _, key := range keys {
|
||||
value, err := db.Get(ctx, []byte(key))
|
||||
value, err := db.Get(ctx, ToBytes(key))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get %s: %v", key, err)
|
||||
return nil, fmt.Errorf("failed to get %s: %v", ToBytes(key), err)
|
||||
}
|
||||
data[key] = string(value)
|
||||
}
|
||||
|
||||
symbol, balance, decimal, address := MatchVoucher(input,
|
||||
data["sym"],
|
||||
data["bal"],
|
||||
data["deci"],
|
||||
data["addr"])
|
||||
data[DATA_VOUCHER_SYMBOLS],
|
||||
data[DATA_VOUCHER_BALANCES],
|
||||
data[DATA_VOUCHER_DECIMALS],
|
||||
data[DATA_VOUCHER_ADDRESSES],
|
||||
)
|
||||
|
||||
if symbol == "" {
|
||||
return nil, nil
|
||||
@ -151,7 +152,7 @@ func GetTemporaryVoucherData(ctx context.Context, store DataStore, sessionId str
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// UpdateVoucherData sets the active voucher data in the DataStore.
|
||||
// UpdateVoucherData updates the active voucher data in the DataStore.
|
||||
func UpdateVoucherData(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error {
|
||||
logg.TraceCtxf(ctx, "dtal", "data", data)
|
||||
// Active voucher data entries
|
||||
|
@ -8,8 +8,9 @@ import (
|
||||
"github.com/alecthomas/assert/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
visedb "git.defalsify.org/vise.git/db"
|
||||
memdb "git.defalsify.org/vise.git/db/mem"
|
||||
dbstorage "git.grassecon.net/grassrootseconomics/visedriver/storage/db"
|
||||
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||
)
|
||||
|
||||
@ -83,19 +84,21 @@ func TestGetVoucherData(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
spdb := storage.NewSubPrefixDb(db, []byte("vouchers"))
|
||||
|
||||
prefix := ToBytes(visedb.DATATYPE_USERDATA)
|
||||
spdb := dbstorage.NewSubPrefixDb(db, prefix)
|
||||
|
||||
// Test voucher data
|
||||
mockData := map[string][]byte{
|
||||
"sym": []byte("1:SRF\n2:MILO"),
|
||||
"bal": []byte("1:100\n2:200"),
|
||||
"deci": []byte("1:6\n2:4"),
|
||||
"addr": []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"),
|
||||
mockData := map[DataTyp][]byte{
|
||||
DATA_VOUCHER_SYMBOLS: []byte("1:SRF\n2:MILO"),
|
||||
DATA_VOUCHER_BALANCES: []byte("1:100\n2:200"),
|
||||
DATA_VOUCHER_DECIMALS: []byte("1:6\n2:4"),
|
||||
DATA_VOUCHER_ADDRESSES: []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"),
|
||||
}
|
||||
|
||||
// Put the data
|
||||
for key, value := range mockData {
|
||||
err = spdb.Put(ctx, []byte(key), []byte(value))
|
||||
err = spdb.Put(ctx, []byte(ToBytes(key)), []byte(value))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -2,8 +2,9 @@ package config
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/initializers"
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/initializers"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -15,6 +16,12 @@ const (
|
||||
voucherHoldingsPathPrefix = "/api/v1/holdings"
|
||||
voucherTransfersPathPrefix = "/api/v1/transfers/last10"
|
||||
voucherDataPathPrefix = "/api/v1/token"
|
||||
AliasPrefix = "api/v1/alias"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultLanguage = "eng"
|
||||
languages []string
|
||||
)
|
||||
|
||||
var (
|
||||
@ -32,8 +39,30 @@ var (
|
||||
VoucherHoldingsURL string
|
||||
VoucherTransfersURL string
|
||||
VoucherDataURL string
|
||||
CheckAliasURL string
|
||||
DbConn string
|
||||
DefaultLanguage string
|
||||
Languages []string
|
||||
)
|
||||
|
||||
func setLanguage() error {
|
||||
defaultLanguage = initializers.GetEnv("DEFAULT_LANGUAGE", defaultLanguage)
|
||||
languages = strings.Split(initializers.GetEnv("LANGUAGES", defaultLanguage), ",")
|
||||
haveDefaultLanguage := false
|
||||
for i, v := range(languages) {
|
||||
languages[i] = strings.ReplaceAll(v, " ", "")
|
||||
if languages[i] == defaultLanguage {
|
||||
haveDefaultLanguage = true
|
||||
}
|
||||
}
|
||||
|
||||
if !haveDefaultLanguage {
|
||||
languages = append([]string{defaultLanguage}, languages...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setBase() error {
|
||||
var err error
|
||||
|
||||
@ -41,14 +70,20 @@ func setBase() error {
|
||||
dataURLBase = initializers.GetEnv("DATA_URL_BASE", "http://localhost:5006")
|
||||
BearerToken = initializers.GetEnv("BEARER_TOKEN", "")
|
||||
|
||||
_, err = url.JoinPath(custodialURLBase, "/foo")
|
||||
_, err = url.Parse(custodialURLBase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = url.JoinPath(dataURLBase, "/bar")
|
||||
_, err = url.Parse(dataURLBase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setConn() error {
|
||||
DbConn = initializers.GetEnv("DB_CONN", "")
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -58,6 +93,14 @@ func LoadConfig() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = setConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = setLanguage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
CreateAccountURL, _ = url.JoinPath(custodialURLBase, createAccountPath)
|
||||
TrackStatusURL, _ = url.JoinPath(custodialURLBase, trackStatusPath)
|
||||
BalanceURL, _ = url.JoinPath(custodialURLBase, balancePathPrefix)
|
||||
@ -66,6 +109,9 @@ func LoadConfig() error {
|
||||
VoucherHoldingsURL, _ = url.JoinPath(dataURLBase, voucherHoldingsPathPrefix)
|
||||
VoucherTransfersURL, _ = url.JoinPath(dataURLBase, voucherTransfersPathPrefix)
|
||||
VoucherDataURL, _ = url.JoinPath(dataURLBase, voucherDataPathPrefix)
|
||||
CheckAliasURL, _ = url.JoinPath(dataURLBase, AliasPrefix)
|
||||
DefaultLanguage = defaultLanguage
|
||||
Languages = languages
|
||||
|
||||
return nil
|
||||
}
|
||||
|
5
debug/cap.go
Normal file
5
debug/cap.go
Normal file
@ -0,0 +1,5 @@
|
||||
package debug
|
||||
|
||||
var (
|
||||
DebugCap uint32
|
||||
)
|
84
debug/db.go
Normal file
84
debug/db.go
Normal file
@ -0,0 +1,84 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"encoding/binary"
|
||||
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/common"
|
||||
"git.defalsify.org/vise.git/db"
|
||||
)
|
||||
|
||||
var (
|
||||
dbTypStr map[common.DataTyp]string = make(map[common.DataTyp]string)
|
||||
)
|
||||
|
||||
type KeyInfo struct {
|
||||
SessionId string
|
||||
Typ uint8
|
||||
SubTyp common.DataTyp
|
||||
Label string
|
||||
Description string
|
||||
}
|
||||
|
||||
func (k KeyInfo) String() string {
|
||||
v := uint16(k.SubTyp)
|
||||
s := subTypToString(k.SubTyp)
|
||||
if s == "" {
|
||||
v = uint16(k.Typ)
|
||||
s = typToString(k.Typ)
|
||||
}
|
||||
return fmt.Sprintf("Session Id: %s\nTyp: %s (%d)\n", k.SessionId, s, v)
|
||||
}
|
||||
|
||||
func ToKeyInfo(k []byte, sessionId string) (KeyInfo, error) {
|
||||
o := KeyInfo{}
|
||||
b := []byte(sessionId)
|
||||
|
||||
if len(k) <= len(b) {
|
||||
return o, fmt.Errorf("storage key missing")
|
||||
}
|
||||
|
||||
o.SessionId = sessionId
|
||||
|
||||
o.Typ = uint8(k[0])
|
||||
k = k[1:]
|
||||
o.SessionId = string(k[:len(b)])
|
||||
k = k[len(b):]
|
||||
|
||||
if o.Typ == db.DATATYPE_USERDATA {
|
||||
if len(k) == 0 {
|
||||
return o, fmt.Errorf("missing subtype key")
|
||||
}
|
||||
v := binary.BigEndian.Uint16(k[:2])
|
||||
o.SubTyp = common.DataTyp(v)
|
||||
o.Label = subTypToString(o.SubTyp)
|
||||
k = k[2:]
|
||||
} else {
|
||||
o.Label = typToString(o.Typ)
|
||||
}
|
||||
|
||||
if len(k) != 0 {
|
||||
return o, fmt.Errorf("excess key information")
|
||||
}
|
||||
|
||||
return o, nil
|
||||
}
|
||||
|
||||
func FromKey(k []byte) (KeyInfo, error) {
|
||||
o := KeyInfo{}
|
||||
|
||||
if len(k) < 4 {
|
||||
return o, fmt.Errorf("insufficient key length")
|
||||
}
|
||||
|
||||
sessionIdBytes := k[1:len(k)-2]
|
||||
return ToKeyInfo(k, string(sessionIdBytes))
|
||||
}
|
||||
|
||||
func subTypToString(v common.DataTyp) string {
|
||||
return dbTypStr[v + db.DATATYPE_USERDATA + 1]
|
||||
}
|
||||
|
||||
func typToString(v uint8) string {
|
||||
return dbTypStr[common.DataTyp(uint16(v))]
|
||||
}
|
44
debug/db_debug.go
Normal file
44
debug/db_debug.go
Normal file
@ -0,0 +1,44 @@
|
||||
// +build debugdb
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"git.defalsify.org/vise.git/db"
|
||||
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/common"
|
||||
)
|
||||
|
||||
func init() {
|
||||
DebugCap |= 1
|
||||
dbTypStr[db.DATATYPE_STATE] = "internal state"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TRACKING_ID] = "tracking id"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_PUBLIC_KEY] = "public key"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT_PIN] = "account pin"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_FIRST_NAME] = "first name"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_FAMILY_NAME] = "family name"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_YOB] = "year of birth"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_LOCATION] = "location"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_GENDER] = "gender"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_OFFERINGS] = "offerings"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_RECIPIENT] = "recipient"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_AMOUNT] = "amount"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TEMPORARY_VALUE] = "temporary value"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_SYM] = "active sym"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_BAL] = "active bal"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_BLOCKED_NUMBER] = "blocked number"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_PUBLIC_KEY_REVERSE] = "public_key_reverse"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_DECIMAL] = "active decimal"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_ADDRESS] = "active address"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_SYMBOLS] = "voucher symbols"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_BALANCES] = "voucher balances"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_DECIMALS] = "voucher decimals"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_ADDRESSES] = "voucher addresses"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_SENDERS] = "tx senders"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_RECIPIENTS] = "tx recipients"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_VALUES] = "tx values"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_ADDRESSES] = "tx addresses"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_HASHES] = "tx hashes"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_DATES] = "tx dates"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_SYMBOLS] = "tx symbols"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_DECIMALS] = "tx decimals"
|
||||
}
|
78
debug/db_test.go
Normal file
78
debug/db_test.go
Normal file
@ -0,0 +1,78 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/common"
|
||||
"git.defalsify.org/vise.git/db"
|
||||
)
|
||||
|
||||
func TestDebugDbSubKeyInfo(t *testing.T) {
|
||||
s := "foo"
|
||||
b := []byte{0x20}
|
||||
b = append(b, []byte(s)...)
|
||||
b = append(b, []byte{0x00, 0x02}...)
|
||||
r, err := ToKeyInfo(b, s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.SessionId != s {
|
||||
t.Fatalf("expected %s, got %s", s, r.SessionId)
|
||||
}
|
||||
if r.Typ != 32 {
|
||||
t.Fatalf("expected 64, got %d", r.Typ)
|
||||
}
|
||||
if r.SubTyp != 2 {
|
||||
t.Fatalf("expected 2, got %d", r.SubTyp)
|
||||
}
|
||||
if DebugCap & 1 > 0 {
|
||||
if r.Label != "tracking id" {
|
||||
t.Fatalf("expected 'tracking id', got '%s'", r.Label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDebugDbKeyInfo(t *testing.T) {
|
||||
s := "bar"
|
||||
b := []byte{0x10}
|
||||
b = append(b, []byte(s)...)
|
||||
r, err := ToKeyInfo(b, s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.SessionId != s {
|
||||
t.Fatalf("expected %s, got %s", s, r.SessionId)
|
||||
}
|
||||
if r.Typ != 16 {
|
||||
t.Fatalf("expected 16, got %d", r.Typ)
|
||||
}
|
||||
if DebugCap & 1 > 0 {
|
||||
if r.Label != "internal state" {
|
||||
t.Fatalf("expected 'internal_state', got '%s'", r.Label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDebugDbKeyInfoRestore(t *testing.T) {
|
||||
s := "bar"
|
||||
b := []byte{db.DATATYPE_USERDATA}
|
||||
b = append(b, []byte(s)...)
|
||||
k := common.ToBytes(common.DATA_ACTIVE_SYM)
|
||||
b = append(b, k...)
|
||||
|
||||
r, err := ToKeyInfo(b, s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.SessionId != s {
|
||||
t.Fatalf("expected %s, got %s", s, r.SessionId)
|
||||
}
|
||||
if r.Typ != 32 {
|
||||
t.Fatalf("expected 32, got %d", r.Typ)
|
||||
}
|
||||
if DebugCap & 1 > 0 {
|
||||
if r.Label != "active sym" {
|
||||
t.Fatalf("expected 'active sym', got '%s'", r.Label)
|
||||
}
|
||||
}
|
||||
}
|
28
doc/data.md
Normal file
28
doc/data.md
Normal file
@ -0,0 +1,28 @@
|
||||
# Internals
|
||||
|
||||
## Version
|
||||
|
||||
This document describes component versions:
|
||||
|
||||
* `urdt-ussd` `v0.5.0-beta`
|
||||
* `go-vise` `v0.2.2`
|
||||
|
||||
|
||||
## User profile data
|
||||
|
||||
All user profile items are stored under keys matching the user's session id, prefixed with the 8-bit value `git.defalsify.org/vise.git/db.DATATYPE_USERDATA` (32), and followed with a 16-big big-endian value subprefix.
|
||||
|
||||
For example, given the sessionId `+254123` and the key `git.grassecon.net/urdt-ussd/common.DATA_PUBLIC_KEY` (2) will be stored under the key:
|
||||
|
||||
```
|
||||
0x322b3235343132330002
|
||||
|
||||
prefix sessionid subprefix
|
||||
32 2b323534313233 0002
|
||||
```
|
||||
|
||||
### Sub-prefixes
|
||||
|
||||
All sub-prefixes are defined as constants in the `git.grassecon.net/urdt-ussd/common` package. The constant names have the prefix `DATA_`
|
||||
|
||||
Please refer to inline godoc documentation for the `git.grassecon.net/urdt-ussd/common` package for details on each data item.
|
13
entry/handlers.go
Normal file
13
entry/handlers.go
Normal file
@ -0,0 +1,13 @@
|
||||
package entry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
)
|
||||
|
||||
type EntryHandler interface {
|
||||
Init(context.Context, string, []byte) (resource.Result, error) // HandlerFunc
|
||||
Exit()
|
||||
SetPersister(*persist.Persister)
|
||||
}
|
15
errors/errors.go
Normal file
15
errors/errors.go
Normal file
@ -0,0 +1,15 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/internal/handlers"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidRequest = handlers.ErrInvalidRequest
|
||||
ErrSessionMissing = handlers.ErrSessionMissing
|
||||
ErrInvalidInput = handlers.ErrInvalidInput
|
||||
ErrStorage = handlers.ErrStorage
|
||||
ErrEngineType = handlers.ErrEngineType
|
||||
ErrEngineInit = handlers.ErrEngineInit
|
||||
ErrEngineExec = handlers.ErrEngineExec
|
||||
)
|
40
go.mod
40
go.mod
@ -1,45 +1,39 @@
|
||||
module git.grassecon.net/urdt/ussd
|
||||
module git.grassecon.net/grassrootseconomics/visedriver
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.23.2
|
||||
|
||||
require (
|
||||
git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b
|
||||
git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d
|
||||
github.com/alecthomas/assert/v2 v2.2.2
|
||||
github.com/gofrs/uuid v4.4.0+incompatible
|
||||
github.com/grassrootseconomics/eth-custodial v1.3.0-beta
|
||||
github.com/peteole/testdata-loader v0.3.0
|
||||
gopkg.in/leonelquinteros/gotext.v1 v1.3.1
|
||||
)
|
||||
|
||||
require github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a // indirect
|
||||
|
||||
require (
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.1 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/grassrootseconomics/ussd-data-service v1.2.0-beta
|
||||
github.com/jackc/pgx/v5 v5.7.1
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||
golang.org/x/crypto v0.27.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
github.com/peteole/testdata-loader v0.3.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/crypto v0.27.0
|
||||
)
|
||||
|
||||
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.2-0.20180830191138-d8f796af33cc // 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/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
gopkg.in/leonelquinteros/gotext.v1 v1.3.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
14
go.sum
14
go.sum
@ -1,9 +1,7 @@
|
||||
git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b h1:dxBplsIlzJHV+5EH+gzB+w08Blt7IJbb2jeRe1OEjLU=
|
||||
git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
|
||||
git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d h1:bPAOVZOX4frSGhfOdcj7kc555f8dc9DmMd2YAyC2AMw=
|
||||
git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
|
||||
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=
|
||||
@ -18,8 +16,8 @@ github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1
|
||||
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/grassrootseconomics/eth-custodial v1.3.0-beta h1:twrMBhl89GqDUL9PlkzQxMP/6OST1BByrNDj+rqXDmU=
|
||||
github.com/grassrootseconomics/eth-custodial v1.3.0-beta/go.mod h1:7uhRcdnJplX4t6GKCEFkbeDhhjlcaGJeJqevbcvGLZo=
|
||||
github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a h1:q/YH7nE2j8epNmFnTu0tU1vwtCxtQ6nH+d7hRVV5krU=
|
||||
github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a/go.mod h1:hdKaKwqiW6/kphK4j/BhmuRlZDLo1+DYo3gYw5O0siw=
|
||||
github.com/grassrootseconomics/ussd-data-service v1.2.0-beta h1:fn1gwbWIwHVEBtUC2zi5OqTlfI/5gU1SMk0fgGixIXk=
|
||||
github.com/grassrootseconomics/ussd-data-service v1.2.0-beta/go.mod h1:omfI0QtUwIdpu9gMcUqLMCG8O1XWjqJGBx1qUMiGWC0=
|
||||
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=
|
||||
@ -62,6 +60,10 @@ golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
|
||||
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
@ -3,24 +3,30 @@ package initializers
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func LoadEnvVariables() {
|
||||
err := godotenv.Load()
|
||||
LoadEnvVariablesPath(".")
|
||||
}
|
||||
|
||||
func LoadEnvVariablesPath(dir string) {
|
||||
fp := path.Join(dir, ".env")
|
||||
err := godotenv.Load(fp)
|
||||
if err != nil {
|
||||
log.Fatal("Error loading .env file")
|
||||
log.Fatal("Error loading .env file", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to get environment variables with a default fallback
|
||||
func GetEnv(key, defaultVal string) string {
|
||||
if value, exists := os.LookupEnv(key); exists {
|
||||
return value
|
||||
if value, exists := os.LookupEnv(key); exists {
|
||||
return value
|
||||
}
|
||||
return defaultVal
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
// Helper to safely convert environment variables to uint
|
||||
|
@ -1,133 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.defalsify.org/vise.git/asm"
|
||||
"git.defalsify.org/vise.git/db"
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers/ussd"
|
||||
"git.grassecon.net/urdt/ussd/internal/utils"
|
||||
"git.grassecon.net/urdt/ussd/remote"
|
||||
)
|
||||
|
||||
type HandlerService interface {
|
||||
GetHandler() (*ussd.Handlers, error)
|
||||
}
|
||||
|
||||
func getParser(fp string, debug bool) (*asm.FlagParser, error) {
|
||||
flagParser := asm.NewFlagParser().WithDebug()
|
||||
_, err := flagParser.Load(fp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return flagParser, nil
|
||||
}
|
||||
|
||||
type LocalHandlerService struct {
|
||||
Parser *asm.FlagParser
|
||||
DbRs *resource.DbResource
|
||||
Pe *persist.Persister
|
||||
UserdataStore *db.Db
|
||||
AdminStore *utils.AdminStore
|
||||
Cfg engine.Config
|
||||
Rs resource.Resource
|
||||
}
|
||||
|
||||
func NewLocalHandlerService(ctx context.Context, fp string, debug bool, dbResource *resource.DbResource, cfg engine.Config, rs resource.Resource) (*LocalHandlerService, error) {
|
||||
parser, err := getParser(fp, debug)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
adminstore, err := utils.NewAdminStore(ctx, "admin_numbers")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &LocalHandlerService{
|
||||
Parser: parser,
|
||||
DbRs: dbResource,
|
||||
AdminStore: adminstore,
|
||||
Cfg: cfg,
|
||||
Rs: rs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ls *LocalHandlerService) SetPersister(Pe *persist.Persister) {
|
||||
ls.Pe = Pe
|
||||
}
|
||||
|
||||
func (ls *LocalHandlerService) SetDataStore(db *db.Db) {
|
||||
ls.UserdataStore = db
|
||||
}
|
||||
|
||||
func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceInterface) (*ussd.Handlers, error) {
|
||||
ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore, ls.AdminStore, accountService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ussdHandlers = ussdHandlers.WithPersister(ls.Pe)
|
||||
ls.DbRs.AddLocalFunc("set_language", ussdHandlers.SetLanguage)
|
||||
ls.DbRs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
|
||||
ls.DbRs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin)
|
||||
ls.DbRs.AddLocalFunc("verify_create_pin", ussdHandlers.VerifyCreatePin)
|
||||
ls.DbRs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier)
|
||||
ls.DbRs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus)
|
||||
ls.DbRs.AddLocalFunc("authorize_account", ussdHandlers.Authorize)
|
||||
ls.DbRs.AddLocalFunc("quit", ussdHandlers.Quit)
|
||||
ls.DbRs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance)
|
||||
ls.DbRs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient)
|
||||
ls.DbRs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset)
|
||||
ls.DbRs.AddLocalFunc("invite_valid_recipient", ussdHandlers.InviteValidRecipient)
|
||||
ls.DbRs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount)
|
||||
ls.DbRs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount)
|
||||
ls.DbRs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount)
|
||||
ls.DbRs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient)
|
||||
ls.DbRs.AddLocalFunc("get_sender", ussdHandlers.GetSender)
|
||||
ls.DbRs.AddLocalFunc("get_amount", ussdHandlers.GetAmount)
|
||||
ls.DbRs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin)
|
||||
ls.DbRs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname)
|
||||
ls.DbRs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname)
|
||||
ls.DbRs.AddLocalFunc("save_gender", ussdHandlers.SaveGender)
|
||||
ls.DbRs.AddLocalFunc("save_location", ussdHandlers.SaveLocation)
|
||||
ls.DbRs.AddLocalFunc("save_yob", ussdHandlers.SaveYob)
|
||||
ls.DbRs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings)
|
||||
ls.DbRs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized)
|
||||
ls.DbRs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate)
|
||||
ls.DbRs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo)
|
||||
ls.DbRs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob)
|
||||
ls.DbRs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob)
|
||||
ls.DbRs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction)
|
||||
ls.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin)
|
||||
ls.DbRs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange)
|
||||
ls.DbRs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp)
|
||||
ls.DbRs.AddLocalFunc("fetch_community_balance", ussdHandlers.FetchCommunityBalance)
|
||||
ls.DbRs.AddLocalFunc("set_default_voucher", ussdHandlers.SetDefaultVoucher)
|
||||
ls.DbRs.AddLocalFunc("check_vouchers", ussdHandlers.CheckVouchers)
|
||||
ls.DbRs.AddLocalFunc("get_vouchers", ussdHandlers.GetVoucherList)
|
||||
ls.DbRs.AddLocalFunc("view_voucher", ussdHandlers.ViewVoucher)
|
||||
ls.DbRs.AddLocalFunc("set_voucher", ussdHandlers.SetVoucher)
|
||||
ls.DbRs.AddLocalFunc("get_voucher_details", ussdHandlers.GetVoucherDetails)
|
||||
ls.DbRs.AddLocalFunc("reset_valid_pin", ussdHandlers.ResetValidPin)
|
||||
ls.DbRs.AddLocalFunc("check_pin_mismatch", ussdHandlers.CheckPinMisMatch)
|
||||
ls.DbRs.AddLocalFunc("validate_blocked_number", ussdHandlers.ValidateBlockedNumber)
|
||||
ls.DbRs.AddLocalFunc("retrieve_blocked_number", ussdHandlers.RetrieveBlockedNumber)
|
||||
ls.DbRs.AddLocalFunc("reset_unregistered_number", ussdHandlers.ResetUnregisteredNumber)
|
||||
ls.DbRs.AddLocalFunc("reset_others_pin", ussdHandlers.ResetOthersPin)
|
||||
ls.DbRs.AddLocalFunc("save_others_temporary_pin", ussdHandlers.SaveOthersTemporaryPin)
|
||||
ls.DbRs.AddLocalFunc("get_current_profile_info", ussdHandlers.GetCurrentProfileInfo)
|
||||
ls.DbRs.AddLocalFunc("check_transactions", ussdHandlers.CheckTransactions)
|
||||
ls.DbRs.AddLocalFunc("get_transactions", ussdHandlers.GetTransactionsList)
|
||||
ls.DbRs.AddLocalFunc("view_statement", ussdHandlers.ViewTransactionStatement)
|
||||
|
||||
return ussdHandlers, nil
|
||||
}
|
||||
|
||||
// TODO: enable setting of sessionId on engine init time
|
||||
func (ls *LocalHandlerService) GetEngine() *engine.DefaultEngine {
|
||||
en := engine.NewEngine(ls.Cfg, ls.Rs)
|
||||
en = en.WithPersister(ls.Pe)
|
||||
return en
|
||||
}
|
@ -6,11 +6,11 @@ import (
|
||||
"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.defalsify.org/vise.git/persist"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/storage"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -20,33 +20,33 @@ var (
|
||||
var (
|
||||
ErrInvalidRequest = errors.New("invalid request for context")
|
||||
ErrSessionMissing = errors.New("missing session")
|
||||
ErrInvalidInput = errors.New("invalid input")
|
||||
ErrStorage = errors.New("storage retrieval fail")
|
||||
ErrEngineType = errors.New("incompatible engine")
|
||||
ErrEngineInit = errors.New("engine init fail")
|
||||
ErrEngineExec = errors.New("engine exec fail")
|
||||
ErrInvalidInput = errors.New("invalid input")
|
||||
ErrStorage = errors.New("storage retrieval fail")
|
||||
ErrEngineType = errors.New("incompatible engine")
|
||||
ErrEngineInit = errors.New("engine init fail")
|
||||
ErrEngineExec = errors.New("engine exec fail")
|
||||
)
|
||||
|
||||
type RequestSession struct {
|
||||
Ctx context.Context
|
||||
Config engine.Config
|
||||
Engine engine.Engine
|
||||
Input []byte
|
||||
Storage *storage.Storage
|
||||
Writer io.Writer
|
||||
Ctx context.Context
|
||||
Config engine.Config
|
||||
Engine engine.Engine
|
||||
Input []byte
|
||||
Storage *storage.Storage
|
||||
Writer io.Writer
|
||||
Continue bool
|
||||
}
|
||||
|
||||
// TODO: seems like can remove this.
|
||||
type RequestParser interface {
|
||||
GetSessionId(rq any) (string, error)
|
||||
GetSessionId(context context.Context, rq any) (string, error)
|
||||
GetInput(rq any) ([]byte, error)
|
||||
}
|
||||
|
||||
type RequestHandler interface {
|
||||
GetConfig() engine.Config
|
||||
GetRequestParser() RequestParser
|
||||
GetEngine(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine
|
||||
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)
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,92 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
)
|
||||
|
||||
type ATSessionHandler struct {
|
||||
*SessionHandler
|
||||
}
|
||||
|
||||
func NewATSessionHandler(h handlers.RequestHandler) *ATSessionHandler {
|
||||
return &ATSessionHandler{
|
||||
SessionHandler: ToSessionHandler(h),
|
||||
}
|
||||
}
|
||||
|
||||
func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
var code int
|
||||
var err error
|
||||
|
||||
rqs := handlers.RequestSession{
|
||||
Ctx: req.Context(),
|
||||
Writer: w,
|
||||
}
|
||||
|
||||
rp := ash.GetRequestParser()
|
||||
cfg := ash.GetConfig()
|
||||
cfg.SessionId, err = rp.GetSessionId(req)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
||||
ash.writeError(w, 400, err)
|
||||
return
|
||||
}
|
||||
rqs.Config = cfg
|
||||
rqs.Input, err = rp.GetInput(req)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
||||
ash.writeError(w, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
rqs, err = ash.Process(rqs)
|
||||
switch err {
|
||||
case nil: // set code to 200 if no err
|
||||
code = 200
|
||||
case handlers.ErrStorage, handlers.ErrEngineInit, handlers.ErrEngineExec, handlers.ErrEngineType:
|
||||
code = 500
|
||||
default:
|
||||
code = 500
|
||||
}
|
||||
|
||||
if code != 200 {
|
||||
ash.writeError(w, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(200)
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
rqs, err = ash.Output(rqs)
|
||||
if err != nil {
|
||||
ash.writeError(w, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
rqs, err = ash.Reset(rqs)
|
||||
if err != nil {
|
||||
ash.writeError(w, 500, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (ash *ATSessionHandler) Output(rqs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
var err error
|
||||
var prefix string
|
||||
|
||||
if rqs.Continue {
|
||||
prefix = "CON "
|
||||
} else {
|
||||
prefix = "END "
|
||||
}
|
||||
|
||||
_, err = io.WriteString(rqs.Writer, prefix)
|
||||
if err != nil {
|
||||
return rqs, err
|
||||
}
|
||||
|
||||
_, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer)
|
||||
return rqs, err
|
||||
}
|
@ -1,449 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
"git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks"
|
||||
)
|
||||
|
||||
// invalidRequestType is a custom type to test invalid request scenarios
|
||||
type invalidRequestType struct{}
|
||||
|
||||
// errorReader is a helper type that always returns an error when Read is called
|
||||
type errorReader struct{}
|
||||
|
||||
func (e *errorReader) Read(p []byte) (n int, err error) {
|
||||
return 0, errors.New("read error")
|
||||
}
|
||||
|
||||
func TestNewATSessionHandler(t *testing.T) {
|
||||
mockHandler := &httpmocks.MockRequestHandler{}
|
||||
ash := NewATSessionHandler(mockHandler)
|
||||
|
||||
if ash == nil {
|
||||
t.Fatal("NewATSessionHandler returned nil")
|
||||
}
|
||||
|
||||
if ash.SessionHandler == nil {
|
||||
t.Fatal("SessionHandler is nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestATSessionHandler_ServeHTTP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setupMocks func(*httpmocks.MockRequestHandler, *httpmocks.MockRequestParser, *httpmocks.MockEngine)
|
||||
formData url.Values
|
||||
expectedStatus int
|
||||
expectedBody string
|
||||
}{
|
||||
{
|
||||
name: "Successful request",
|
||||
setupMocks: func(mh *httpmocks.MockRequestHandler, mrp *httpmocks.MockRequestParser, me *httpmocks.MockEngine) {
|
||||
mrp.GetSessionIdFunc = func(rq any) (string, error) {
|
||||
req := rq.(*http.Request)
|
||||
return req.FormValue("phoneNumber"), nil
|
||||
}
|
||||
mrp.GetInputFunc = func(rq any) ([]byte, error) {
|
||||
req := rq.(*http.Request)
|
||||
text := req.FormValue("text")
|
||||
parts := strings.Split(text, "*")
|
||||
return []byte(parts[len(parts)-1]), nil
|
||||
}
|
||||
mh.ProcessFunc = func(rqs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
rqs.Continue = true
|
||||
rqs.Engine = me
|
||||
return rqs, nil
|
||||
}
|
||||
mh.GetConfigFunc = func() engine.Config { return engine.Config{} }
|
||||
mh.GetRequestParserFunc = func() handlers.RequestParser { return mrp }
|
||||
mh.OutputFunc = func(rs handlers.RequestSession) (handlers.RequestSession, error) { return rs, nil }
|
||||
mh.ResetFunc = func(rs handlers.RequestSession) (handlers.RequestSession, error) { return rs, nil }
|
||||
me.FlushFunc = func(context.Context, io.Writer) (int, error) { return 0, nil }
|
||||
},
|
||||
formData: url.Values{
|
||||
"phoneNumber": []string{"+1234567890"},
|
||||
"text": []string{"1*2*3"},
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: "CON ",
|
||||
},
|
||||
{
|
||||
name: "GetSessionId error",
|
||||
setupMocks: func(mh *httpmocks.MockRequestHandler, mrp *httpmocks.MockRequestParser, me *httpmocks.MockEngine) {
|
||||
mrp.GetSessionIdFunc = func(rq any) (string, error) {
|
||||
return "", errors.New("no phone number found")
|
||||
}
|
||||
mh.GetConfigFunc = func() engine.Config { return engine.Config{} }
|
||||
mh.GetRequestParserFunc = func() handlers.RequestParser { return mrp }
|
||||
},
|
||||
formData: url.Values{
|
||||
"text": []string{"1*2*3"},
|
||||
},
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedBody: "",
|
||||
},
|
||||
{
|
||||
name: "GetInput error",
|
||||
setupMocks: func(mh *httpmocks.MockRequestHandler, mrp *httpmocks.MockRequestParser, me *httpmocks.MockEngine) {
|
||||
mrp.GetSessionIdFunc = func(rq any) (string, error) {
|
||||
req := rq.(*http.Request)
|
||||
return req.FormValue("phoneNumber"), nil
|
||||
}
|
||||
mrp.GetInputFunc = func(rq any) ([]byte, error) {
|
||||
return nil, errors.New("no input found")
|
||||
}
|
||||
mh.GetConfigFunc = func() engine.Config { return engine.Config{} }
|
||||
mh.GetRequestParserFunc = func() handlers.RequestParser { return mrp }
|
||||
},
|
||||
formData: url.Values{
|
||||
"phoneNumber": []string{"+1234567890"},
|
||||
},
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedBody: "",
|
||||
},
|
||||
{
|
||||
name: "Process error",
|
||||
setupMocks: func(mh *httpmocks.MockRequestHandler, mrp *httpmocks.MockRequestParser, me *httpmocks.MockEngine) {
|
||||
mrp.GetSessionIdFunc = func(rq any) (string, error) {
|
||||
req := rq.(*http.Request)
|
||||
return req.FormValue("phoneNumber"), nil
|
||||
}
|
||||
mrp.GetInputFunc = func(rq any) ([]byte, error) {
|
||||
req := rq.(*http.Request)
|
||||
text := req.FormValue("text")
|
||||
parts := strings.Split(text, "*")
|
||||
return []byte(parts[len(parts)-1]), nil
|
||||
}
|
||||
mh.ProcessFunc = func(rqs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return rqs, handlers.ErrStorage
|
||||
}
|
||||
mh.GetConfigFunc = func() engine.Config { return engine.Config{} }
|
||||
mh.GetRequestParserFunc = func() handlers.RequestParser { return mrp }
|
||||
},
|
||||
formData: url.Values{
|
||||
"phoneNumber": []string{"+1234567890"},
|
||||
"text": []string{"1*2*3"},
|
||||
},
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
expectedBody: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockHandler := &httpmocks.MockRequestHandler{}
|
||||
mockRequestParser := &httpmocks.MockRequestParser{}
|
||||
mockEngine := &httpmocks.MockEngine{}
|
||||
tt.setupMocks(mockHandler, mockRequestParser, mockEngine)
|
||||
|
||||
ash := NewATSessionHandler(mockHandler)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tt.formData.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
ash.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != tt.expectedStatus {
|
||||
t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code)
|
||||
}
|
||||
|
||||
if tt.expectedBody != "" && w.Body.String() != tt.expectedBody {
|
||||
t.Errorf("Expected body %q, got %q", tt.expectedBody, w.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestATSessionHandler_Output(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input handlers.RequestSession
|
||||
expectedPrefix string
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "Continue true",
|
||||
input: handlers.RequestSession{
|
||||
Continue: true,
|
||||
Engine: &httpmocks.MockEngine{
|
||||
FlushFunc: func(context.Context, io.Writer) (int, error) {
|
||||
return 0, nil
|
||||
},
|
||||
},
|
||||
Writer: &httpmocks.MockWriter{},
|
||||
},
|
||||
expectedPrefix: "CON ",
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "Continue false",
|
||||
input: handlers.RequestSession{
|
||||
Continue: false,
|
||||
Engine: &httpmocks.MockEngine{
|
||||
FlushFunc: func(context.Context, io.Writer) (int, error) {
|
||||
return 0, nil
|
||||
},
|
||||
},
|
||||
Writer: &httpmocks.MockWriter{},
|
||||
},
|
||||
expectedPrefix: "END ",
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "Flush error",
|
||||
input: handlers.RequestSession{
|
||||
Continue: true,
|
||||
Engine: &httpmocks.MockEngine{
|
||||
FlushFunc: func(context.Context, io.Writer) (int, error) {
|
||||
return 0, errors.New("write error")
|
||||
},
|
||||
},
|
||||
Writer: &httpmocks.MockWriter{},
|
||||
},
|
||||
expectedPrefix: "CON ",
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ash := &ATSessionHandler{}
|
||||
_, err := ash.Output(tt.input)
|
||||
|
||||
if tt.expectedError && err == nil {
|
||||
t.Error("Expected an error, but got nil")
|
||||
}
|
||||
|
||||
if !tt.expectedError && err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
mw := tt.input.Writer.(*httpmocks.MockWriter)
|
||||
if !mw.WriteStringCalled {
|
||||
t.Error("WriteString was not called")
|
||||
}
|
||||
|
||||
if mw.WrittenString != tt.expectedPrefix {
|
||||
t.Errorf("Expected prefix %q, got %q", tt.expectedPrefix, mw.WrittenString)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionHandler_ServeHTTP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
sessionID string
|
||||
input []byte
|
||||
parserErr error
|
||||
processErr error
|
||||
outputErr error
|
||||
resetErr error
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
name: "Success",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Missing Session ID",
|
||||
sessionID: "",
|
||||
parserErr: handlers.ErrSessionMissing,
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "Process Error",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
processErr: handlers.ErrStorage,
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
name: "Output Error",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
outputErr: errors.New("output error"),
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Reset Error",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
resetErr: errors.New("reset error"),
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockRequestParser := &httpmocks.MockRequestParser{
|
||||
GetSessionIdFunc: func(any) (string, error) {
|
||||
return tt.sessionID, tt.parserErr
|
||||
},
|
||||
GetInputFunc: func(any) ([]byte, error) {
|
||||
return tt.input, nil
|
||||
},
|
||||
}
|
||||
|
||||
mockRequestHandler := &httpmocks.MockRequestHandler{
|
||||
ProcessFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return rs, tt.processErr
|
||||
},
|
||||
OutputFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return rs, tt.outputErr
|
||||
},
|
||||
ResetFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return rs, tt.resetErr
|
||||
},
|
||||
GetRequestParserFunc: func() handlers.RequestParser {
|
||||
return mockRequestParser
|
||||
},
|
||||
GetConfigFunc: func() engine.Config {
|
||||
return engine.Config{}
|
||||
},
|
||||
}
|
||||
|
||||
sessionHandler := ToSessionHandler(mockRequestHandler)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.input))
|
||||
req.Header.Set("X-Vise-Session", tt.sessionID)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
sessionHandler.ServeHTTP(rr, req)
|
||||
|
||||
if status := rr.Code; status != tt.expectedStatus {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||
status, tt.expectedStatus)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionHandler_writeError(t *testing.T) {
|
||||
handler := &SessionHandler{}
|
||||
mockWriter := &httpmocks.MockWriter{}
|
||||
err := errors.New("test error")
|
||||
|
||||
handler.writeError(mockWriter, http.StatusBadRequest, err)
|
||||
|
||||
if mockWriter.WrittenString != "" {
|
||||
t.Errorf("Expected empty body, got %s", mockWriter.WrittenString)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRequestParser_GetSessionId(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
request any
|
||||
expectedID string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Valid Session ID",
|
||||
request: func() *http.Request {
|
||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
req.Header.Set("X-Vise-Session", "123456")
|
||||
return req
|
||||
}(),
|
||||
expectedID: "123456",
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Missing Session ID",
|
||||
request: httptest.NewRequest(http.MethodPost, "/", nil),
|
||||
expectedID: "",
|
||||
expectedError: handlers.ErrSessionMissing,
|
||||
},
|
||||
{
|
||||
name: "Invalid Request Type",
|
||||
request: invalidRequestType{},
|
||||
expectedID: "",
|
||||
expectedError: handlers.ErrInvalidRequest,
|
||||
},
|
||||
}
|
||||
|
||||
parser := &DefaultRequestParser{}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
id, err := parser.GetSessionId(tt.request)
|
||||
|
||||
if id != tt.expectedID {
|
||||
t.Errorf("Expected session ID %s, got %s", tt.expectedID, id)
|
||||
}
|
||||
|
||||
if err != tt.expectedError {
|
||||
t.Errorf("Expected error %v, got %v", tt.expectedError, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRequestParser_GetInput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
request any
|
||||
expectedInput []byte
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Valid Input",
|
||||
request: func() *http.Request {
|
||||
return httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString("test input"))
|
||||
}(),
|
||||
expectedInput: []byte("test input"),
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Empty Input",
|
||||
request: httptest.NewRequest(http.MethodPost, "/", nil),
|
||||
expectedInput: []byte{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Invalid Request Type",
|
||||
request: invalidRequestType{},
|
||||
expectedInput: nil,
|
||||
expectedError: handlers.ErrInvalidRequest,
|
||||
},
|
||||
{
|
||||
name: "Read Error",
|
||||
request: func() *http.Request {
|
||||
return httptest.NewRequest(http.MethodPost, "/", &errorReader{})
|
||||
}(),
|
||||
expectedInput: nil,
|
||||
expectedError: errors.New("read error"),
|
||||
},
|
||||
}
|
||||
|
||||
parser := &DefaultRequestParser{}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input, err := parser.GetInput(tt.request)
|
||||
|
||||
if !bytes.Equal(input, tt.expectedInput) {
|
||||
t.Errorf("Expected input %s, got %s", tt.expectedInput, input)
|
||||
}
|
||||
|
||||
if err != tt.expectedError && (err == nil || err.Error() != tt.expectedError.Error()) {
|
||||
t.Errorf("Expected error %v, got %v", tt.expectedError, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
"git.grassecon.net/urdt/ussd/internal/testutil/testservice"
|
||||
"git.grassecon.net/urdt/ussd/internal/testutil/testtag"
|
||||
testdataloader "github.com/peteole/testdata-loader"
|
||||
"git.grassecon.net/urdt/ussd/remote"
|
||||
)
|
||||
|
||||
var (
|
||||
baseDir = testdataloader.GetBasePath()
|
||||
logg = logging.NewVanilla()
|
||||
scriptDir = path.Join(baseDir, "services", "registration")
|
||||
)
|
||||
|
||||
func TestEngine(sessionId string) (engine.Engine, func(), chan bool) {
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||
ctx = context.WithValue(ctx, "Database", "gdbm")
|
||||
pfp := path.Join(scriptDir, "pp.csv")
|
||||
|
||||
var eventChannel = make(chan bool)
|
||||
|
||||
cfg := engine.Config{
|
||||
Root: "root",
|
||||
SessionId: sessionId,
|
||||
OutputSize: uint32(160),
|
||||
FlagCount: uint32(128),
|
||||
}
|
||||
|
||||
dbDir := ".test_state"
|
||||
resourceDir := scriptDir
|
||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
||||
|
||||
err := menuStorageService.EnsureDbDir()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
rs, err := menuStorageService.GetResource(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
pe, err := menuStorageService.GetPersister(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
userDataStore, err := menuStorageService.GetUserdataDb(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
dbResource, ok := rs.(*resource.DbResource)
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs)
|
||||
lhs.SetDataStore(&userDataStore)
|
||||
lhs.SetPersister(pe)
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if testtag.AccountService == nil {
|
||||
testtag.AccountService = &remote.AccountService{}
|
||||
}
|
||||
|
||||
switch testtag.AccountService.(type) {
|
||||
case *testservice.TestAccountService:
|
||||
go func() {
|
||||
eventChannel <- false
|
||||
}()
|
||||
case *remote.AccountService:
|
||||
go func() {
|
||||
time.Sleep(5 * time.Second) // Wait for 5 seconds
|
||||
eventChannel <- true
|
||||
}()
|
||||
default:
|
||||
panic("Unknown account service type")
|
||||
}
|
||||
|
||||
hl, err := lhs.GetHandler(testtag.AccountService)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
en := lhs.GetEngine()
|
||||
en = en.WithFirst(hl.Init)
|
||||
cleanFn := func() {
|
||||
err := en.Finish()
|
||||
if err != nil {
|
||||
logg.Errorf(err.Error())
|
||||
}
|
||||
|
||||
err = menuStorageService.Close()
|
||||
if err != nil {
|
||||
logg.Errorf(err.Error())
|
||||
}
|
||||
logg.Infof("testengine storage closed")
|
||||
}
|
||||
return en, cleanFn, eventChannel
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
package httpmocks
|
||||
|
||||
import (
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
)
|
||||
|
||||
// MockRequestHandler implements handlers.RequestHandler interface for testing
|
||||
type MockRequestHandler struct {
|
||||
ProcessFunc func(handlers.RequestSession) (handlers.RequestSession, error)
|
||||
GetConfigFunc func() engine.Config
|
||||
GetEngineFunc func(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine
|
||||
OutputFunc func(rs handlers.RequestSession) (handlers.RequestSession, error)
|
||||
ResetFunc func(rs handlers.RequestSession) (handlers.RequestSession, error)
|
||||
ShutdownFunc func()
|
||||
GetRequestParserFunc func() handlers.RequestParser
|
||||
}
|
||||
|
||||
func (m *MockRequestHandler) Process(rqs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return m.ProcessFunc(rqs)
|
||||
}
|
||||
|
||||
func (m *MockRequestHandler) GetConfig() engine.Config {
|
||||
return m.GetConfigFunc()
|
||||
}
|
||||
|
||||
func (m *MockRequestHandler) GetEngine(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine {
|
||||
return m.GetEngineFunc(cfg, rs, pe)
|
||||
}
|
||||
|
||||
func (m *MockRequestHandler) Output(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return m.OutputFunc(rs)
|
||||
}
|
||||
|
||||
func (m *MockRequestHandler) Reset(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return m.ResetFunc(rs)
|
||||
}
|
||||
|
||||
func (m *MockRequestHandler) Shutdown() {
|
||||
m.ShutdownFunc()
|
||||
}
|
||||
|
||||
func (m *MockRequestHandler) GetRequestParser() handlers.RequestParser {
|
||||
return m.GetRequestParserFunc()
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/models"
|
||||
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockAccountService implements AccountServiceInterface for testing
|
||||
type MockAccountService struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockAccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) {
|
||||
args := m.Called()
|
||||
return args.Get(0).(*models.AccountResult), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) {
|
||||
args := m.Called(publicKey)
|
||||
return args.Get(0).(*models.BalanceResult), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockAccountService) TrackAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResult, error) {
|
||||
args := m.Called(trackingId)
|
||||
return args.Get(0).(*models.TrackStatusResult), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) {
|
||||
args := m.Called(publicKey)
|
||||
return args.Get(0).([]dataserviceapi.TokenHoldings), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockAccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) {
|
||||
args := m.Called(publicKey)
|
||||
return args.Get(0).([]dataserviceapi.Last10TxResponse), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockAccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) {
|
||||
args := m.Called(address)
|
||||
return args.Get(0).(*models.VoucherDataResult), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockAccountService) TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) {
|
||||
args := m.Called()
|
||||
return args.Get(0).(*models.TokenTransferResponse), args.Error(1)
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
package testservice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/models"
|
||||
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||
)
|
||||
|
||||
type TestAccountService struct {
|
||||
}
|
||||
|
||||
func (tas *TestAccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) {
|
||||
return &models.AccountResult{
|
||||
TrackingId: "075ccc86-f6ef-4d33-97d5-e91cfb37aa0d",
|
||||
PublicKey: "0x623EFAFa8868df4B934dd12a8B26CB3Dd75A7AdD",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (tas *TestAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) {
|
||||
balanceResponse := &models.BalanceResult{
|
||||
Balance: "0.003 CELO",
|
||||
Nonce: json.Number("0"),
|
||||
}
|
||||
return balanceResponse, nil
|
||||
}
|
||||
|
||||
func (tas *TestAccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error) {
|
||||
return &models.TrackStatusResult{
|
||||
Active: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) {
|
||||
return []dataserviceapi.TokenHoldings {
|
||||
dataserviceapi.TokenHoldings {
|
||||
ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee",
|
||||
TokenSymbol: "SRF",
|
||||
TokenDecimals: "6",
|
||||
Balance: "2745987",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (tas *TestAccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) {
|
||||
return []dataserviceapi.Last10TxResponse{}, nil
|
||||
}
|
||||
|
||||
func (m TestAccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) {
|
||||
return &models.VoucherDataResult{}, nil
|
||||
}
|
||||
|
||||
func (tas *TestAccountService) TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) {
|
||||
return &models.TokenTransferResponse{
|
||||
TrackingId: "e034d147-747d-42ea-928d-b5a7cb3426af",
|
||||
}, nil
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
// +build !online
|
||||
|
||||
package testtag
|
||||
|
||||
import (
|
||||
"git.grassecon.net/urdt/ussd/remote"
|
||||
accountservice "git.grassecon.net/urdt/ussd/internal/testutil/testservice"
|
||||
)
|
||||
|
||||
var (
|
||||
AccountService remote.AccountServiceInterface = &accountservice.TestAccountService{}
|
||||
)
|
@ -1,9 +0,0 @@
|
||||
// +build online
|
||||
|
||||
package testtag
|
||||
|
||||
import "git.grassecon.net/urdt/ussd/internal/handlers/server"
|
||||
|
||||
var (
|
||||
AccountService server.AccountServiceInterface
|
||||
)
|
@ -1,460 +0,0 @@
|
||||
{
|
||||
"groups": [
|
||||
{
|
||||
"name": "my_account_change_pin",
|
||||
"steps": [
|
||||
{
|
||||
"input": "",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "3",
|
||||
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "5",
|
||||
"expectedContent": "PIN Management\n1:Change PIN\n2:Reset other's PIN\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "1",
|
||||
"expectedContent": "Enter your old PIN\n\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "1234",
|
||||
"expectedContent": "Enter a new four number PIN:\n\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "1234",
|
||||
"expectedContent": "Confirm your new PIN:\n\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "1234",
|
||||
"expectedContent": "Your PIN change request has been successful\n\n0:Back\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "menu_my_account_language_change",
|
||||
"steps": [
|
||||
{
|
||||
"input": "",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "3",
|
||||
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "2",
|
||||
"expectedContent": "Please enter your PIN:"
|
||||
},
|
||||
{
|
||||
"input": "1235",
|
||||
"expectedContent": "Incorrect pin\n1:Retry\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "1",
|
||||
"expectedContent": "Please enter your PIN:"
|
||||
},
|
||||
{
|
||||
"input": "1234",
|
||||
"expectedContent": "Select language:\n0:english\n1:kiswahili"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "Your language change request was successful.\n0:Back\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "menu_my_account_check_my_balance",
|
||||
"steps": [
|
||||
{
|
||||
"input": "",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "3",
|
||||
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "3",
|
||||
"expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "1",
|
||||
"expectedContent": "Please enter your PIN:"
|
||||
},
|
||||
{
|
||||
"input": "1235",
|
||||
"expectedContent": "Incorrect pin\n1:Retry\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "1",
|
||||
"expectedContent": "Please enter your PIN:"
|
||||
},
|
||||
{
|
||||
"input": "1234",
|
||||
"expectedContent": "Balance: {balance}\n\n0:Back\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
|
||||
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "menu_my_account_check_community_balance",
|
||||
"steps": [
|
||||
{
|
||||
"input": "",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "3",
|
||||
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "3",
|
||||
"expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "2",
|
||||
"expectedContent": "Please enter your PIN:"
|
||||
},
|
||||
{
|
||||
"input": "1235",
|
||||
"expectedContent": "Incorrect pin\n1:Retry\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "1",
|
||||
"expectedContent": "Please enter your PIN:"
|
||||
},
|
||||
{
|
||||
"input": "1234",
|
||||
"expectedContent": "{balance}\n0:Back\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
|
||||
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "menu_my_account_edit_firstname",
|
||||
"steps": [
|
||||
{
|
||||
"input": "",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "3",
|
||||
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "1",
|
||||
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "1",
|
||||
"expectedContent": "Enter your first names:\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "foo",
|
||||
"expectedContent": "Please enter your PIN:"
|
||||
},
|
||||
{
|
||||
"input": "1234",
|
||||
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "menu_my_account_edit_familyname",
|
||||
"steps": [
|
||||
{
|
||||
"input": "",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "3",
|
||||
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "1",
|
||||
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "2",
|
||||
"expectedContent": "Enter family name:\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "bar",
|
||||
"expectedContent": "Please enter your PIN:"
|
||||
},
|
||||
{
|
||||
"input": "1234",
|
||||
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "menu_my_account_edit_gender",
|
||||
"steps": [
|
||||
{
|
||||
"input": "",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "3",
|
||||
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "1",
|
||||
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "3",
|
||||
"expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "1",
|
||||
"expectedContent": "Please enter your PIN:"
|
||||
},
|
||||
{
|
||||
"input": "1234",
|
||||
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "menu_my_account_edit_yob",
|
||||
"steps": [
|
||||
{
|
||||
"input": "",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "3",
|
||||
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "1",
|
||||
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "4",
|
||||
"expectedContent": "Enter your year of birth\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "1945",
|
||||
"expectedContent": "Please enter your PIN:"
|
||||
},
|
||||
{
|
||||
"input": "1234",
|
||||
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "menu_my_account_edit_location",
|
||||
"steps": [
|
||||
{
|
||||
"input": "",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "3",
|
||||
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "1",
|
||||
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "5",
|
||||
"expectedContent": "Enter your location:\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "Kilifi",
|
||||
"expectedContent": "Please enter your PIN:"
|
||||
},
|
||||
{
|
||||
"input": "1234",
|
||||
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "menu_my_account_edit_offerings",
|
||||
"steps": [
|
||||
{
|
||||
"input": "",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "3",
|
||||
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "1",
|
||||
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "6",
|
||||
"expectedContent": "Enter the services or goods you offer: \n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "Bananas",
|
||||
"expectedContent": "Please enter your PIN:"
|
||||
},
|
||||
{
|
||||
"input": "1234",
|
||||
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "menu_my_account_view_profile",
|
||||
"steps": [
|
||||
{
|
||||
"input": "",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "3",
|
||||
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "1",
|
||||
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "7",
|
||||
"expectedContent": "Please enter your PIN:"
|
||||
},
|
||||
{
|
||||
"input": "1234",
|
||||
"expectedContent": "My profile:\nName: foo bar\nGender: male\nAge: 79\nLocation: Kilifi\nYou provide: Bananas\n\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -1,380 +0,0 @@
|
||||
package menutraversaltest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/testutil"
|
||||
"git.grassecon.net/urdt/ussd/internal/testutil/driver"
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
var (
|
||||
testData = driver.ReadData()
|
||||
testStore = ".test_state"
|
||||
groupTestFile = "group_test.json"
|
||||
sessionID string
|
||||
src = rand.NewSource(42)
|
||||
g = rand.New(src)
|
||||
)
|
||||
|
||||
func GenerateSessionId() string {
|
||||
uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g))
|
||||
v, err := uu.NewV4()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v.String()
|
||||
}
|
||||
|
||||
// Extract the public key from the engine response
|
||||
func extractPublicKey(response []byte) string {
|
||||
// Regex pattern to match the public key starting with 0x and 40 characters
|
||||
re := regexp.MustCompile(`0x[a-fA-F0-9]{40}`)
|
||||
match := re.Find(response)
|
||||
if match != nil {
|
||||
return string(match)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Extracts the balance value from the engine response.
|
||||
func extractBalance(response []byte) string {
|
||||
// Regex to match "Balance: <amount> <symbol>" followed by a newline
|
||||
re := regexp.MustCompile(`(?m)^Balance:\s+(\d+(\.\d+)?)\s+([A-Z]+)`)
|
||||
match := re.FindSubmatch(response)
|
||||
if match != nil {
|
||||
return string(match[1]) + " " + string(match[3]) // "<amount> <symbol>"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Extracts the Maximum amount value from the engine response.
|
||||
func extractMaxAmount(response []byte) string {
|
||||
// Regex to match "Maximum amount: <amount>" followed by a newline
|
||||
re := regexp.MustCompile(`(?m)^Maximum amount:\s+(\d+(\.\d+)?)`)
|
||||
match := re.FindSubmatch(response)
|
||||
if match != nil {
|
||||
return string(match[1]) // "<amount>"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Extracts the send amount value from the engine response.
|
||||
func extractSendAmount(response []byte) string {
|
||||
// Regex to match the pattern "will receive X.XX SYM from"
|
||||
re := regexp.MustCompile(`will receive (\d+\.\d{2}\s+[A-Z]+) from`)
|
||||
match := re.FindSubmatch(response)
|
||||
if match != nil {
|
||||
return string(match[1]) // Returns "X.XX SYM"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
sessionID = GenerateSessionId()
|
||||
defer func() {
|
||||
if err := os.RemoveAll(testStore); err != nil {
|
||||
log.Fatalf("Failed to delete state store %s: %v", testStore, err)
|
||||
}
|
||||
}()
|
||||
m.Run()
|
||||
}
|
||||
|
||||
func TestAccountCreationSuccessful(t *testing.T) {
|
||||
en, fn, eventChannel := testutil.TestEngine(sessionID)
|
||||
defer fn()
|
||||
ctx := context.Background()
|
||||
sessions := testData
|
||||
for _, session := range sessions {
|
||||
groups := driver.FilterGroupsByName(session.Groups, "account_creation_successful")
|
||||
for _, group := range groups {
|
||||
for _, step := range group.Steps {
|
||||
cont, err := en.Exec(ctx, []byte(step.Input))
|
||||
if err != nil {
|
||||
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
|
||||
}
|
||||
if !cont {
|
||||
break
|
||||
}
|
||||
w := bytes.NewBuffer(nil)
|
||||
_, err = en.Flush(ctx, w)
|
||||
if err != nil {
|
||||
t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
|
||||
}
|
||||
b := w.Bytes()
|
||||
match, err := step.MatchesExpectedContent(b)
|
||||
if err != nil {
|
||||
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
|
||||
}
|
||||
if !match {
|
||||
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
<-eventChannel
|
||||
|
||||
}
|
||||
|
||||
func TestAccountRegistrationRejectTerms(t *testing.T) {
|
||||
// Generate a new UUID for this edge case test
|
||||
uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g))
|
||||
v, err := uu.NewV4()
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
edgeCaseSessionID := v.String()
|
||||
en, fn, _ := testutil.TestEngine(edgeCaseSessionID)
|
||||
defer fn()
|
||||
ctx := context.Background()
|
||||
sessions := testData
|
||||
for _, session := range sessions {
|
||||
groups := driver.FilterGroupsByName(session.Groups, "account_creation_reject_terms")
|
||||
for _, group := range groups {
|
||||
for _, step := range group.Steps {
|
||||
cont, err := en.Exec(ctx, []byte(step.Input))
|
||||
if err != nil {
|
||||
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
|
||||
return
|
||||
}
|
||||
if !cont {
|
||||
break
|
||||
}
|
||||
w := bytes.NewBuffer(nil)
|
||||
if _, err := en.Flush(ctx, w); err != nil {
|
||||
t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
|
||||
}
|
||||
|
||||
b := w.Bytes()
|
||||
match, err := step.MatchesExpectedContent(b)
|
||||
if err != nil {
|
||||
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
|
||||
}
|
||||
if !match {
|
||||
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMainMenuHelp(t *testing.T) {
|
||||
en, fn, _ := testutil.TestEngine(sessionID)
|
||||
defer fn()
|
||||
ctx := context.Background()
|
||||
sessions := testData
|
||||
for _, session := range sessions {
|
||||
groups := driver.FilterGroupsByName(session.Groups, "main_menu_help")
|
||||
for _, group := range groups {
|
||||
for _, step := range group.Steps {
|
||||
cont, err := en.Exec(ctx, []byte(step.Input))
|
||||
if err != nil {
|
||||
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
|
||||
return
|
||||
}
|
||||
if !cont {
|
||||
break
|
||||
}
|
||||
w := bytes.NewBuffer(nil)
|
||||
if _, err := en.Flush(ctx, w); err != nil {
|
||||
t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
|
||||
}
|
||||
|
||||
b := w.Bytes()
|
||||
balance := extractBalance(b)
|
||||
|
||||
expectedContent := []byte(step.ExpectedContent)
|
||||
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
|
||||
|
||||
step.ExpectedContent = string(expectedContent)
|
||||
match, err := step.MatchesExpectedContent(b)
|
||||
if err != nil {
|
||||
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
|
||||
}
|
||||
if !match {
|
||||
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMainMenuQuit(t *testing.T) {
|
||||
en, fn, _ := testutil.TestEngine(sessionID)
|
||||
defer fn()
|
||||
ctx := context.Background()
|
||||
sessions := testData
|
||||
for _, session := range sessions {
|
||||
groups := driver.FilterGroupsByName(session.Groups, "main_menu_quit")
|
||||
for _, group := range groups {
|
||||
for _, step := range group.Steps {
|
||||
cont, err := en.Exec(ctx, []byte(step.Input))
|
||||
if err != nil {
|
||||
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
|
||||
return
|
||||
}
|
||||
if !cont {
|
||||
break
|
||||
}
|
||||
w := bytes.NewBuffer(nil)
|
||||
if _, err := en.Flush(ctx, w); err != nil {
|
||||
t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
|
||||
}
|
||||
|
||||
b := w.Bytes()
|
||||
balance := extractBalance(b)
|
||||
|
||||
expectedContent := []byte(step.ExpectedContent)
|
||||
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
|
||||
|
||||
step.ExpectedContent = string(expectedContent)
|
||||
match, err := step.MatchesExpectedContent(b)
|
||||
if err != nil {
|
||||
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
|
||||
}
|
||||
if !match {
|
||||
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMyAccount_MyAddress(t *testing.T) {
|
||||
en, fn, _ := testutil.TestEngine(sessionID)
|
||||
defer fn()
|
||||
ctx := context.Background()
|
||||
sessions := testData
|
||||
for _, session := range sessions {
|
||||
groups := driver.FilterGroupsByName(session.Groups, "menu_my_account_my_address")
|
||||
for _, group := range groups {
|
||||
for index, step := range group.Steps {
|
||||
t.Logf("step %v with input %v", index, step.Input)
|
||||
cont, err := en.Exec(ctx, []byte(step.Input))
|
||||
if err != nil {
|
||||
t.Errorf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
|
||||
return
|
||||
}
|
||||
if !cont {
|
||||
break
|
||||
}
|
||||
w := bytes.NewBuffer(nil)
|
||||
if _, err := en.Flush(ctx, w); err != nil {
|
||||
t.Errorf("Test case '%s' failed during Flush: %v", group.Name, err)
|
||||
}
|
||||
b := w.Bytes()
|
||||
|
||||
balance := extractBalance(b)
|
||||
publicKey := extractPublicKey(b)
|
||||
|
||||
expectedContent := []byte(step.ExpectedContent)
|
||||
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
|
||||
expectedContent = bytes.Replace(expectedContent, []byte("{public_key}"), []byte(publicKey), -1)
|
||||
|
||||
step.ExpectedContent = string(expectedContent)
|
||||
match, err := step.MatchesExpectedContent(b)
|
||||
if err != nil {
|
||||
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
|
||||
}
|
||||
if !match {
|
||||
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expectedContent, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMainMenuSend(t *testing.T) {
|
||||
en, fn, _ := testutil.TestEngine(sessionID)
|
||||
defer fn()
|
||||
ctx := context.Background()
|
||||
sessions := testData
|
||||
for _, session := range sessions {
|
||||
groups := driver.FilterGroupsByName(session.Groups, "send_with_invalid_inputs")
|
||||
for _, group := range groups {
|
||||
for _, step := range group.Steps {
|
||||
cont, err := en.Exec(ctx, []byte(step.Input))
|
||||
if err != nil {
|
||||
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
|
||||
return
|
||||
}
|
||||
if !cont {
|
||||
break
|
||||
}
|
||||
w := bytes.NewBuffer(nil)
|
||||
if _, err := en.Flush(ctx, w); err != nil {
|
||||
t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
|
||||
}
|
||||
|
||||
b := w.Bytes()
|
||||
balance := extractBalance(b)
|
||||
max_amount := extractMaxAmount(b)
|
||||
send_amount := extractSendAmount(b)
|
||||
|
||||
expectedContent := []byte(step.ExpectedContent)
|
||||
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
|
||||
expectedContent = bytes.Replace(expectedContent, []byte("{max_amount}"), []byte(max_amount), -1)
|
||||
expectedContent = bytes.Replace(expectedContent, []byte("{send_amount}"), []byte(send_amount), -1)
|
||||
expectedContent = bytes.Replace(expectedContent, []byte("{session_id}"), []byte(sessionID), -1)
|
||||
|
||||
step.ExpectedContent = string(expectedContent)
|
||||
match, err := step.MatchesExpectedContent(b)
|
||||
if err != nil {
|
||||
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
|
||||
}
|
||||
if !match {
|
||||
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroups(t *testing.T) {
|
||||
groups, err := driver.LoadTestGroups(groupTestFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load test groups: %v", err)
|
||||
}
|
||||
en, fn, _ := testutil.TestEngine(sessionID)
|
||||
defer fn()
|
||||
ctx := context.Background()
|
||||
// Create test cases from loaded groups
|
||||
tests := driver.CreateTestCases(groups)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
cont, err := en.Exec(ctx, []byte(tt.Input))
|
||||
if err != nil {
|
||||
t.Errorf("Test case '%s' failed at input '%s': %v", tt.Name, tt.Input, err)
|
||||
return
|
||||
}
|
||||
if !cont {
|
||||
return
|
||||
}
|
||||
w := bytes.NewBuffer(nil)
|
||||
if _, err := en.Flush(ctx, w); err != nil {
|
||||
t.Errorf("Test case '%s' failed during Flush: %v", tt.Name, err)
|
||||
}
|
||||
b := w.Bytes()
|
||||
balance := extractBalance(b)
|
||||
|
||||
expectedContent := []byte(tt.ExpectedContent)
|
||||
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
|
||||
|
||||
tt.ExpectedContent = string(expectedContent)
|
||||
|
||||
match, err := tt.MatchesExpectedContent(b)
|
||||
if err != nil {
|
||||
t.Fatalf("Error compiling regex for step '%s': %v", tt.Input, err)
|
||||
}
|
||||
if !match {
|
||||
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", tt.ExpectedContent, b)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,133 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "session one",
|
||||
"groups": [
|
||||
{
|
||||
"name": "account_creation_successful",
|
||||
"steps": [
|
||||
{
|
||||
"input": "",
|
||||
"expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:english\n1:kiswahili"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "Do you agree to terms and conditions?\n0:yes\n1:no"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "Please enter a new four number PIN for your account:\n0:Exit"
|
||||
},
|
||||
{
|
||||
"input": "1234",
|
||||
"expectedContent": "Enter your four number PIN again:"
|
||||
},
|
||||
{
|
||||
"input": "1111",
|
||||
"expectedContent": "The PIN is not a match. Try again\n1:Retry\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "1",
|
||||
"expectedContent": "Enter your four number PIN again:"
|
||||
},
|
||||
{
|
||||
"input": "1234",
|
||||
"expectedContent": "Your account is being created...Thank you for using Sarafu. Goodbye!"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "account_creation_reject_terms",
|
||||
"steps": [
|
||||
{
|
||||
"input": "",
|
||||
"expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:english\n1:kiswahili"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "Do you agree to terms and conditions?\n0:yes\n1:no"
|
||||
},
|
||||
{
|
||||
"input": "1",
|
||||
"expectedContent": "Thank you for using Sarafu. Goodbye!"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "send_with_invite",
|
||||
"steps": [
|
||||
{
|
||||
"input": "",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "1",
|
||||
"expectedContent": "Enter recipient's phone number:\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "000",
|
||||
"expectedContent": "000 is invalid, please try again:\n1:Retry\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "1",
|
||||
"expectedContent": "Enter recipient's phone number:\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "0712345678",
|
||||
"expectedContent": "0712345678 is not registered, please try again:\n1:Retry\n2:Invite to Sarafu Network\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "2",
|
||||
"expectedContent": "Your invite request for 0712345678 to Sarafu Network failed. Please try again later."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "main_menu_help",
|
||||
"steps": [
|
||||
{
|
||||
"input": "",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "4",
|
||||
"expectedContent": "For more help,please call: 0757628885"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "main_menu_quit",
|
||||
"steps": [
|
||||
{
|
||||
"input": "",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "9",
|
||||
"expectedContent": "Thank you for using Sarafu. Goodbye!"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "menu_my_account_my_address",
|
||||
"steps": [
|
||||
{
|
||||
"input": "",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "3",
|
||||
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "6",
|
||||
"expectedContent": "Address: {public_key}\n0:Back\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "9",
|
||||
"expectedContent": "Thank you for using Sarafu. Goodbye!"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
@ -1,6 +0,0 @@
|
||||
package models
|
||||
|
||||
type AccountResult struct {
|
||||
PublicKey string `json:"publicKey"`
|
||||
TrackingId string `json:"trackingId"`
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
package models
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type BalanceResult struct {
|
||||
Balance string `json:"balance"`
|
||||
Nonce json.Number `json:"nonce"`
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package models
|
||||
|
||||
type TokenTransferResponse struct {
|
||||
TrackingId string `json:"trackingId"`
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Transaction struct {
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Status string `json:"status"`
|
||||
TransferValue json.Number `json:"transferValue"`
|
||||
TxHash string `json:"txHash"`
|
||||
TxType string `json:"txType"`
|
||||
}
|
||||
|
||||
type TrackStatusResult struct {
|
||||
Active bool `json:"active"`
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
package models
|
||||
|
||||
type VoucherDataResult struct {
|
||||
TokenName string `json:"tokenName"`
|
||||
TokenSymbol string `json:"tokenSymbol"`
|
||||
TokenDecimals int `json:"tokenDecimals"`
|
||||
SinkAddress string `json:"sinkAddress"`
|
||||
}
|
@ -1,273 +0,0 @@
|
||||
package remote
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/config"
|
||||
"git.grassecon.net/urdt/ussd/models"
|
||||
"github.com/grassrootseconomics/eth-custodial/pkg/api"
|
||||
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||
)
|
||||
|
||||
type AccountServiceInterface interface {
|
||||
CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error)
|
||||
CreateAccount(ctx context.Context) (*models.AccountResult, error)
|
||||
TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error)
|
||||
FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error)
|
||||
FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error)
|
||||
VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error)
|
||||
TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error)
|
||||
}
|
||||
|
||||
type AccountService struct {
|
||||
}
|
||||
|
||||
// Parameters:
|
||||
// - trackingId: A unique identifier for the account.This should be obtained from a previous call to
|
||||
// CreateAccount or a similar function that returns an AccountResponse. The `trackingId` field in the
|
||||
// AccountResponse struct can be used here to check the account status during a transaction.
|
||||
//
|
||||
// Returns:
|
||||
// - string: The status of the transaction as a string. If there is an error during the request or processing, this will be an empty string.
|
||||
// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
|
||||
// If no error occurs, this will be nil
|
||||
func (as *AccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error) {
|
||||
var r models.TrackStatusResult
|
||||
|
||||
ep, err := url.JoinPath(config.TrackURL, publicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", ep, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = doRequest(ctx, req, &r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
// CheckBalance retrieves the balance for a given public key from the custodial balance API endpoint.
|
||||
// Parameters:
|
||||
// - publicKey: The public key associated with the account whose balance needs to be checked.
|
||||
func (as *AccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) {
|
||||
var balanceResult models.BalanceResult
|
||||
|
||||
ep, err := url.JoinPath(config.BalanceURL, publicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", ep, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = doRequest(ctx, req, &balanceResult)
|
||||
return &balanceResult, err
|
||||
}
|
||||
|
||||
// CreateAccount creates a new account in the custodial system.
|
||||
// Returns:
|
||||
// - *models.AccountResponse: A pointer to an AccountResponse struct containing the details of the created account.
|
||||
// If there is an error during the request or processing, this will be nil.
|
||||
// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
|
||||
// If no error occurs, this will be nil.
|
||||
func (as *AccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) {
|
||||
var r models.AccountResult
|
||||
// Create a new request
|
||||
req, err := http.NewRequest("POST", config.CreateAccountURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = doRequest(ctx, req, &r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
// FetchVouchers retrieves the token holdings for a given public key from the data indexer API endpoint
|
||||
// Parameters:
|
||||
// - publicKey: The public key associated with the account.
|
||||
func (as *AccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) {
|
||||
var r struct {
|
||||
Holdings []dataserviceapi.TokenHoldings `json:"holdings"`
|
||||
}
|
||||
|
||||
ep, err := url.JoinPath(config.VoucherHoldingsURL, publicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", ep, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = doRequest(ctx, req, &r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.Holdings, nil
|
||||
}
|
||||
|
||||
// FetchTransactions retrieves the last 10 transactions for a given public key from the data indexer API endpoint
|
||||
// Parameters:
|
||||
// - publicKey: The public key associated with the account.
|
||||
func (as *AccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) {
|
||||
var r struct {
|
||||
Transfers []dataserviceapi.Last10TxResponse `json:"transfers"`
|
||||
}
|
||||
|
||||
ep, err := url.JoinPath(config.VoucherTransfersURL, publicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", ep, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = doRequest(ctx, req, &r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.Transfers, nil
|
||||
}
|
||||
|
||||
// VoucherData retrieves voucher metadata from the data indexer API endpoint.
|
||||
// Parameters:
|
||||
// - address: The voucher address.
|
||||
func (as *AccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) {
|
||||
var r struct {
|
||||
TokenDetails models.VoucherDataResult `json:"tokenDetails"`
|
||||
}
|
||||
|
||||
ep, err := url.JoinPath(config.VoucherDataURL, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", ep, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = doRequest(ctx, req, &r)
|
||||
return &r.TokenDetails, err
|
||||
}
|
||||
|
||||
// TokenTransfer creates a new token transfer in the custodial system.
|
||||
// Returns:
|
||||
// - *models.TokenTransferResponse: A pointer to an TokenTransferResponse struct containing the trackingId.
|
||||
// If there is an error during the request or processing, this will be nil.
|
||||
// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
|
||||
// If no error occurs, this will be nil.
|
||||
func (as *AccountService) TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) {
|
||||
var r models.TokenTransferResponse
|
||||
|
||||
// Create request payload
|
||||
payload := map[string]string{
|
||||
"amount": amount,
|
||||
"from": from,
|
||||
"to": to,
|
||||
"tokenAddress": tokenAddress,
|
||||
}
|
||||
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create a new request
|
||||
req, err := http.NewRequest("POST", config.TokenTransferURL, bytes.NewBuffer(payloadBytes))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = doRequest(ctx, req, &r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) {
|
||||
var okResponse api.OKResponse
|
||||
var errResponse api.ErrResponse
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+config.BearerToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
logRequestDetails(req)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("Failed to make %s request to endpoint: %s with reason: %s", req.Method, req.URL, err.Error())
|
||||
errResponse.Description = err.Error()
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
log.Printf("Received response for %s: Status Code: %d | Content-Type: %s", req.URL, resp.StatusCode, resp.Header.Get("Content-Type"))
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
err := json.Unmarshal([]byte(body), &errResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errors.New(errResponse.Description)
|
||||
}
|
||||
err = json.Unmarshal([]byte(body), &okResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(okResponse.Result) == 0 {
|
||||
return nil, errors.New("Empty api result")
|
||||
}
|
||||
|
||||
v, err := json.Marshal(okResponse.Result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(v, &rcpt)
|
||||
return &okResponse, err
|
||||
}
|
||||
|
||||
func logRequestDetails(req *http.Request) {
|
||||
var bodyBytes []byte
|
||||
contentType := req.Header.Get("Content-Type")
|
||||
if req.Body != nil {
|
||||
bodyBytes, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
log.Printf("Error reading request body: %s", err)
|
||||
return
|
||||
}
|
||||
req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
} else {
|
||||
bodyBytes = []byte("-")
|
||||
}
|
||||
|
||||
log.Printf("URL: %s | Content-Type: %s | Method: %s| Request Body: %s", req.URL, contentType, req.Method, string(bodyBytes))
|
||||
}
|
42
request/request.go
Normal file
42
request/request.go
Normal file
@ -0,0 +1,42 @@
|
||||
package request
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/storage"
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla().WithDomain("visedriver.request")
|
||||
)
|
||||
|
||||
type RequestSession struct {
|
||||
Ctx context.Context
|
||||
Config engine.Config
|
||||
Engine engine.Engine
|
||||
Input []byte
|
||||
Storage *storage.Storage
|
||||
Writer io.Writer
|
||||
Continue bool
|
||||
}
|
||||
|
||||
// TODO: seems like can remove this.
|
||||
type RequestParser interface {
|
||||
GetSessionId(ctx context.Context, rq any) (string, error)
|
||||
GetInput(rq any) ([]byte, error)
|
||||
}
|
||||
|
||||
type RequestHandler interface {
|
||||
GetConfig() engine.Config
|
||||
GetRequestParser() RequestParser
|
||||
GetEngine(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine
|
||||
Process(rs RequestSession) (RequestSession, error)
|
||||
Output(rs RequestSession) (RequestSession, error)
|
||||
Reset(rs RequestSession) (RequestSession, error)
|
||||
Shutdown()
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
# Variables to match files in the current directory
|
||||
INPUTS = $(wildcard ./*.vis)
|
||||
TXTS = $(wildcard ./*.txt.orig)
|
||||
VISE_PATH := ../../go-vise
|
||||
|
||||
# Rule to build .bin files from .vis files
|
||||
%.vis:
|
||||
go run $(VISE_PATH)/dev/asm/main.go -f pp.csv $(basename $@).vis > $(basename $@).bin
|
||||
@echo "Built $(basename $@).bin from $(basename $@).vis"
|
||||
|
||||
# Rule to copy .orig files to .txt
|
||||
%.txt.orig:
|
||||
cp -v $(basename $@).orig $(basename $@)
|
||||
@echo "Copied $(basename $@).orig to $(basename $@)"
|
||||
|
||||
# 'all' target depends on all .vis and .txt.orig files
|
||||
all: $(INPUTS) $(TXTS)
|
||||
@echo "Running all: $(INPUTS) $(TXTS)"
|
@ -1 +0,0 @@
|
||||
Something went wrong.Please try again
|
@ -1 +0,0 @@
|
||||
HALT
|
@ -1 +0,0 @@
|
||||
Your account is being created...
|
@ -1,4 +0,0 @@
|
||||
RELOAD verify_create_pin
|
||||
CATCH create_pin_mismatch flag_pin_mismatch 1
|
||||
LOAD quit 0
|
||||
HALT
|
@ -1 +0,0 @@
|
||||
Your account creation request failed. Please try again later.
|
@ -1,3 +0,0 @@
|
||||
MOUT quit 9
|
||||
HALT
|
||||
INCMP quit 9
|
@ -1 +0,0 @@
|
||||
Ombi lako la kusajiliwa haliwezi kukamilishwa. Tafadhali jaribu tena baadaye.
|
@ -1 +0,0 @@
|
||||
Akaunti yako inatengenezwa...
|
@ -1 +0,0 @@
|
||||
My Account
|
@ -1 +0,0 @@
|
||||
Akaunti yangu
|
@ -1 +0,0 @@
|
||||
Your account is still being created.
|
@ -1,3 +0,0 @@
|
||||
RELOAD check_account_status
|
||||
CATCH main flag_account_success 1
|
||||
HALT
|
@ -1 +0,0 @@
|
||||
Akaunti yako bado inatengenezwa
|
@ -1 +0,0 @@
|
||||
Address: {{.check_identifier}}
|
@ -1,8 +0,0 @@
|
||||
LOAD check_identifier 0
|
||||
RELOAD check_identifier
|
||||
MAP check_identifier
|
||||
MOUT back 0
|
||||
MOUT quit 9
|
||||
HALT
|
||||
INCMP _ 0
|
||||
INCMP quit 9
|
@ -1 +0,0 @@
|
||||
Anwani:{{.check_identifier}}
|
@ -1,2 +0,0 @@
|
||||
Maximum amount: {{.max_amount}}
|
||||
Enter amount:
|
@ -1,15 +0,0 @@
|
||||
LOAD reset_transaction_amount 0
|
||||
LOAD max_amount 10
|
||||
RELOAD max_amount
|
||||
MAP max_amount
|
||||
MOUT back 0
|
||||
HALT
|
||||
LOAD validate_amount 64
|
||||
RELOAD validate_amount
|
||||
CATCH api_failure flag_api_call_error 1
|
||||
CATCH invalid_amount flag_invalid_amount 1
|
||||
INCMP _ 0
|
||||
LOAD get_recipient 0
|
||||
LOAD get_sender 64
|
||||
LOAD get_amount 32
|
||||
INCMP transaction_pin *
|
@ -1,2 +0,0 @@
|
||||
Kiwango cha juu: {{.max_amount}}
|
||||
Weka kiwango:
|
@ -1 +0,0 @@
|
||||
Failed to connect to the custodial service.Please try again.
|
@ -1,5 +0,0 @@
|
||||
MOUT retry 1
|
||||
MOUT quit 9
|
||||
HALT
|
||||
INCMP _ 1
|
||||
INCMP quit 9
|
@ -1 +0,0 @@
|
||||
Back
|
@ -1 +0,0 @@
|
||||
Rudi
|
@ -1 +0,0 @@
|
||||
Balances:
|
@ -1,9 +0,0 @@
|
||||
LOAD reset_account_authorized 0
|
||||
RELOAD reset_account_authorized
|
||||
MOUT my_balance 1
|
||||
MOUT community_balance 2
|
||||
MOUT back 0
|
||||
HALT
|
||||
INCMP _ 0
|
||||
INCMP my_balance 1
|
||||
INCMP community_balance 2
|
@ -1 +0,0 @@
|
||||
Salio:
|
@ -1 +0,0 @@
|
||||
Select language:
|
@ -1,10 +0,0 @@
|
||||
LOAD reset_account_authorized 0
|
||||
LOAD reset_incorrect 0
|
||||
CATCH incorrect_pin flag_incorrect_pin 1
|
||||
CATCH pin_entry flag_account_authorized 0
|
||||
MOUT english 0
|
||||
MOUT kiswahili 1
|
||||
HALT
|
||||
INCMP set_default 0
|
||||
INCMP set_swa 1
|
||||
INCMP . *
|
@ -1 +0,0 @@
|
||||
Change language
|
@ -1 +0,0 @@
|
||||
Badili lugha
|
@ -1 +0,0 @@
|
||||
Chagua lugha:
|
@ -1 +0,0 @@
|
||||
Change PIN
|
@ -1 +0,0 @@
|
||||
Badili PIN
|
@ -1 +0,0 @@
|
||||
Check balances
|
@ -1 +0,0 @@
|
||||
Angalia salio
|
@ -1 +0,0 @@
|
||||
Please enter your PIN to view statement:
|
@ -1,12 +0,0 @@
|
||||
LOAD check_transactions 0
|
||||
RELOAD check_transactions
|
||||
CATCH no_transfers flag_no_transfers 1
|
||||
LOAD authorize_account 6
|
||||
MOUT back 0
|
||||
MOUT quit 9
|
||||
HALT
|
||||
RELOAD authorize_account
|
||||
CATCH incorrect_pin flag_incorrect_pin 1
|
||||
INCMP _ 0
|
||||
INCMP quit 9
|
||||
INCMP transactions *
|
@ -1 +0,0 @@
|
||||
Check statement
|
@ -1 +0,0 @@
|
||||
Taarifa ya matumizi
|
@ -1 +0,0 @@
|
||||
Tafadhali weka PIN yako kuona taarifa ya matumizi:
|
@ -1 +0,0 @@
|
||||
{{.fetch_community_balance}}
|
@ -1 +0,0 @@
|
||||
{{.fetch_community_balance}}
|
@ -1,11 +0,0 @@
|
||||
LOAD reset_incorrect 6
|
||||
LOAD fetch_community_balance 0
|
||||
CATCH api_failure flag_api_call_error 1
|
||||
MAP fetch_community_balance
|
||||
CATCH incorrect_pin flag_incorrect_pin 1
|
||||
CATCH pin_entry flag_account_authorized 0
|
||||
MOUT back 0
|
||||
MOUT quit 9
|
||||
HALT
|
||||
INCMP _ 0
|
||||
INCMP quit 9
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user