Compare commits

...

65 Commits

Author SHA1 Message Date
lash
c0534ede1b
Strip scheme from connstring in gdbm 2025-01-15 00:32:56 +00:00
lash
24e729d275
Fix untidy go mod 2025-01-15 00:11:11 +00:00
lash
ae6e2a99c5
upgrade vise dep 2025-01-15 00:09:14 +00:00
lash
7ca3974371
Enable fs, mem in storageservice 2025-01-15 00:08:44 +00:00
lash
adbfab3964
Enable conndata fs, mem 2025-01-14 18:57:04 +00:00
lash
5228aef088
Add mem storage service 2025-01-13 21:33:25 +00:00
lash
5bf0a0e858
remove session package 2025-01-13 10:31:42 +00:00
lash
f13dab9a45
Remove docker stuff 2025-01-12 15:54:57 +00:00
lash
3f8e08151a
Add debug string for db type in menuhandler 2025-01-12 15:35:44 +00:00
lash
9e4c65c8b4
Correct storage service interface signature 2025-01-12 12:13:25 +00:00
lash
611c5a8dfc
Rename session handler to request handler 2025-01-12 10:40:23 +00:00
lash
dcf777bf08
Fix missing err in config 2025-01-12 10:24:50 +00:00
lash
ec4ad6e44b
Move out config 2025-01-12 09:37:40 +00:00
lash
5b312f9569
Move out common, util, debug, storage to sarafu-vise 2025-01-12 07:59:03 +00:00
lash
2d89db3a37
Rename http source file 2025-01-11 22:38:38 +00:00
lash
6d8992fe38
Remove remaining devtools cmds 2025-01-11 22:33:03 +00:00
lash
5cffbe5cd8
Remove lang devtool 2025-01-11 22:31:21 +00:00
lash
b28a047777
Remove dialoguss reference 2025-01-11 22:29:08 +00:00
lash
2ea51d88d8
Factor out adminstore 2025-01-11 22:21:09 +00:00
lash
2e0e854c4b
Remove last internal package 2025-01-11 21:47:57 +00:00
lash
c93a07832d
Move out API services related code 2025-01-11 16:31:06 +00:00
lash
46bf21b7b8
Remove commented code 2025-01-11 15:16:14 +00:00
lash
dff662663d
Rehabilitate http server test 2025-01-11 08:37:10 +00:00
lash
977d14c529
Properly isolate http session handler 2025-01-11 08:33:52 +00:00
lash
fd6e5caf53
Remove persister chainer for menu handler 2025-01-11 08:08:52 +00:00
lash
840c22ca89
Factor out session handler, introduce entry handler 2025-01-11 08:01:48 +00:00
lash
f939a20543
Remove vise code 2025-01-11 07:23:15 +00:00
lash
8387644019
Export http package 2025-01-10 20:39:36 +00:00
lash
c535938dbc
Move out remaining local handler dependent code 2025-01-10 20:31:34 +00:00
lash
b60d2648ea
Rehabilitate until localhandler 2025-01-10 14:04:23 +00:00
lash
85ede15613 Merge branch 'master' into lash/purify-max 2025-01-10 13:46:52 +00:00
b11f11b5fa Merge pull request 'refactor: rename files to snake_case' (#268) from konstantinmds/ussd:refactor/148-convert-files-to-snake-case into master
Reviewed-on: urdt/ussd#268
2025-01-10 14:40:58 +01:00
3d35a5de78 rename files to snake case 2025-01-10 13:43:49 +01:00
a19ace85f8 refactor: rename files to snake_case 2025-01-10 13:41:23 +01:00
lash
a7a8a482ab
Rehabilitate go test running 2025-01-10 12:03:37 +00:00
lash
d8e7c443b5
Add gettext in exported storage service 2025-01-10 11:54:59 +00:00
lash
5216a9383e
Add service mock missing 2025-01-10 11:49:47 +00:00
lash
6cd639fd19
Add missing pgxpool module 2025-01-10 11:44:15 +00:00
lash
a7ca280964 Merge branch 'master' into lash/purify-max 2025-01-10 11:40:49 +00:00
8f5ed0cd4f Merge pull request 'postgres-switch-for-tests' (#255) from postgres-switch-for-tests into master
Reviewed-on: urdt/ussd#255
2025-01-10 12:07:07 +01:00
lash
f1664a43a8
Rename module, export storage, testutil 2025-01-10 10:53:27 +00:00
c29abfe21e Merge branch 'master' into postgres-switch-for-tests 2025-01-10 13:43:16 +03:00
9a6d8e5158
Refactored the code to switch between postgres and gdbm, with db cleanup 2025-01-10 13:41:05 +03:00
lash
9ca5091692 Merge branch 'master' into lash/purify-max 2025-01-10 08:56:59 +00:00
db431c750e Merge pull request 'copy-language-code' (#264) from copy-language-code into master
Reviewed-on: urdt/ussd#264
2025-01-10 09:53:17 +01:00
2b9c6d641e
Merge branch 'master' into copy-language-code 2025-01-10 11:06:32 +03:00
lash
b9712098ef
Fix remaining conflict diff 2025-01-09 13:49:50 +00:00
lash
bcf1965a6c Merge branch 'master' into lash/purify-max 2025-01-09 13:48:39 +00:00
3747f87a7c
test language code saving 2025-01-09 15:11:29 +03:00
1f0568df32 Merge pull request 'Implement connstring handling' (#247) from lash/purify-more into master
Reviewed-on: urdt/ussd#247
2025-01-09 13:03:28 +01:00
lash
24c513d4f0 Merge branch 'master' into lash/purify-more 2025-01-09 12:02:17 +00:00
b3fd6f5c1a Merge pull request 'Rename handler/ussd package' (#254) from konstantinmds/ussd:refactor/24-rename-ussd-to-application into master
Reviewed-on: urdt/ussd#254
2025-01-09 13:01:19 +01:00
73eb765408
persist selected language code 2025-01-09 13:14:46 +03:00
f660f6c19a
add key to hold selected langauge code 2025-01-09 13:04:11 +03:00
3fccfaab61
Replace the connStr if it is not set 2025-01-09 13:01:28 +03:00
5734011f96 refactor: rename ussd package to application (#24)
- Rename internal/handlers/ussd directory to application
- Update all imports and references to use new package name
2025-01-08 13:40:00 +01:00
lash
e09e324a50 Merge branch 'lash/purify-more' into lash/purify-max 2025-01-06 09:03:30 +00:00
lash
f8d8f265f1
Merge upstream 2025-01-06 08:40:50 +00:00
lash
9371b52f3e Merge branch 'lash/ssh-fixes' into lash/purify-max 2025-01-06 08:39:40 +00:00
lash
563000ec15 Merge branch 'lash/purify-more' into lash/purify-max 2025-01-06 08:38:05 +00:00
lash
739fd90dfd
Expose httpmocks 2025-01-05 21:04:21 +00:00
lash
6789c4f550
Export handlers 2025-01-05 11:17:58 +00:00
lash
437f73827d
Rehabilitate all tets 2025-01-05 09:54:19 +00:00
lash
f0a4a0df61
Correct package name in errors, add state store getter 2025-01-05 07:19:25 +00:00
lash
bd604219b8
WIP Factor out request, errors 2025-01-04 22:27:46 +00:00
386 changed files with 372 additions and 12001 deletions

View File

@ -1,14 +0,0 @@
/**
!/cmd/africastalking
!/cmd/ssh
!/common
!/config
!/initializers
!/internal
!/models
!/remote
!/services
!/LICENSE
!/README.md
!/go.*
!/.env.example

View File

@ -1,56 +0,0 @@
name: release
on:
push:
tags:
- "v*"
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Check out repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Login to GHCR Docker registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set outputs
run: |
echo "RELEASE_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV \
&& echo "RELEASE_SHORT_COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Build and push image
uses: docker/build-push-action@v2
with:
context: ./
file: ./Dockerfile
platforms: linux/amd64
push: true
build-args: |
BUILD=${{ env.RELEASE_SHORT_COMMIT }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
tags: |
ghcr.io/grassrootseconomics/urdt-ussd:latest
ghcr.io/grassrootseconomics/urdt-ussd:${{ env.RELEASE_TAG }}

View File

@ -1,44 +0,0 @@
FROM golang:1.23.0-bookworm AS build
ENV CGO_ENABLED=1
ARG BUILDPLATFORM
ARG TARGETPLATFORM
ARG BUILD=dev
WORKDIR /build
COPY . .
RUN apt update && apt install libgdbm-dev
RUN git clone https://git.defalsify.org/vise.git go-vise
WORKDIR /build/services/registration
RUN echo "Compiling go-vise files"
RUN make VISE_PATH=/build/go-vise -B
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
ENV DEBIAN_FRONTEND=noninteractive
RUN apt update && apt install libgdbm-dev ca-certificates -y
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
COPY --from=build /build/.env.example .
RUN mv .env.example .env
EXPOSE 7123
EXPOSE 7122
CMD ["./ussd-africastalking"]

View File

@ -1,92 +0,0 @@
# URDT USSD service
This is a USSD service built using the [go-vise](https://github.com/nolash/go-vise) engine.
## Prerequisites
### 1. [go-vise](https://github.com/nolash/go-vise)
Set up `go-vise` by cloning the repository into a separate directory. The main upstream repository is hosted at: `https://git.defalsify.org/vise.git`
```
git clone https://git.defalsify.org/vise.git
```
## Setup
1. Clone the ussd repo in its own directory
```
git clone https://git.grassecon.net/urdt/ussd.git
```
2. Navigate to the project directory.
3. Enter the `services/registration` subfolder:
```
cd services/registration
```
4. make the .bin files from the .vis files
```
make VISE_PATH=/var/path/to/your/go-vise -B
```
5. Return to the project root (`cd ../..`)
6. Run the USSD menu
```
go run cmd/main.go -session-id=0712345678
```
## Running the different binaries
1. ### CLI:
```
go run cmd/main.go -session-id=0712345678
```
2. ### Africastalking:
```
go run cmd/africastalking/main.go
```
3. ### Async:
```
go run cmd/async/main.go
```
4. ### Http:
```
go run cmd/http/main.go
```
## Flags
Below are the supported flags:
1. `-session-id`:
Specifies the session ID. (CLI only).
Default: `075xx2123`.
Example:
```
go run cmd/main.go -session-id=0712345678
```
2. `-d`:
Enables engine debug output.
Default: `false`.
Example:
```
go run cmd/main.go -session-id=0712345678 -d
```
3. `-db`:
Specifies the database type.
Default: `gdbm`.
Example:
```
go run cmd/main.go -session-id=0712345678 -d -db=postgres
```
>Note: If using `-db=postgres`, ensure PostgreSQL is running with the connection details specified in your `.env` file.
## License
[AGPL-3.0](LICENSE).

View File

@ -1,172 +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.defalsify.org/vise.git/lang"
"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/http/at"
"git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/remote"
"git.grassecon.net/urdt/ussd/internal/args"
)
var (
logg = logging.NewVanilla().WithDomain("AfricasTalking").WithContextKey("at-session-id")
scriptDir = path.Join("services", "registration")
build = "dev"
menuSeparator = ": "
)
func init() {
initializers.LoadEnvVariables()
}
func main() {
config.LoadConfig()
var connStr string
var resourceDir string
var size uint
var engineDebug bool
var host string
var port uint
var err error
var gettextDir string
var langs args.LangVar
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
flag.StringVar(&connStr, "c", "", "connection string")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.UintVar(&size, "s", 160, "max size of output")
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory")
flag.Var(&langs, "language", "add symbol resolution for language")
flag.Parse()
if connStr != "" {
connStr = config.DbConn
}
connData, err := storage.ToConnData(connStr)
if err != nil {
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
os.Exit(1)
}
logg.Infof("start command", "build", build, "conn", connData, "resourcedir", resourceDir, "outputsize", size)
ctx := context.Background()
ln, err := lang.LanguageFromCode(config.DefaultLanguage)
if err != nil {
fmt.Fprintf(os.Stderr, "default language set error: %v", err)
os.Exit(1)
}
ctx = context.WithValue(ctx, "Language", ln)
pfp := path.Join(scriptDir, "pp.csv")
cfg := engine.Config{
Root: "root",
OutputSize: uint32(size),
FlagCount: uint32(128),
MenuSeparator: menuSeparator,
}
if engineDebug {
cfg.EngineDebug = true
}
menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
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)
}
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 := &at.ATRequestParser{}
bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
sh := at.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)
}
}

View File

@ -1,198 +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.defalsify.org/vise.git/lang"
"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"
"git.grassecon.net/urdt/ussd/internal/args"
)
var (
logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration")
menuSeparator = ": "
)
func init() {
initializers.LoadEnvVariables()
}
type asyncRequestParser struct {
sessionId string
input []byte
}
func (p *asyncRequestParser) GetSessionId(ctx context.Context, 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 connStr string
var sessionId string
var resourceDir string
var size uint
var engineDebug bool
var host string
var port uint
var err error
var gettextDir string
var langs args.LangVar
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
flag.StringVar(&connStr, "c", "", "connection string")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.UintVar(&size, "s", 160, "max size of output")
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory")
flag.Var(&langs, "language", "add symbol resolution for language")
flag.Parse()
if connStr != "" {
connStr = config.DbConn
}
connData, err := storage.ToConnData(connStr)
if err != nil {
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
os.Exit(1)
}
logg.Infof("start command", "conn", connData, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId)
ctx := context.Background()
ln, err := lang.LanguageFromCode(config.DefaultLanguage)
if err != nil {
fmt.Fprintf(os.Stderr, "default language set error: %v", err)
os.Exit(1)
}
ctx = context.WithValue(ctx, "Language", ln)
pfp := path.Join(scriptDir, "pp.csv")
cfg := engine.Config{
Root: "root",
OutputSize: uint32(size),
FlagCount: uint32(128),
MenuSeparator: menuSeparator,
}
if engineDebug {
cfg.EngineDebug = true
}
menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
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)
}
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)
}
}
}

View File

@ -1,160 +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.defalsify.org/vise.git/lang"
"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"
"git.grassecon.net/urdt/ussd/internal/args"
)
var (
logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration")
menuSeparator = ": "
)
func init() {
initializers.LoadEnvVariables()
}
func main() {
config.LoadConfig()
var connStr string
var resourceDir string
var size uint
var engineDebug bool
var host string
var port uint
var err error
var gettextDir string
var langs args.LangVar
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
flag.StringVar(&connStr, "c", "", "connection string")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.UintVar(&size, "s", 160, "max size of output")
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory")
flag.Var(&langs, "language", "add symbol resolution for language")
flag.Parse()
if connStr != "" {
connStr = config.DbConn
}
connData, err := storage.ToConnData(connStr)
if err != nil {
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
os.Exit(1)
}
logg.Infof("start command", "conn", connData, "resourcedir", resourceDir, "outputsize", size)
ctx := context.Background()
ln, err := lang.LanguageFromCode(config.DefaultLanguage)
if err != nil {
fmt.Fprintf(os.Stderr, "default language set error: %v", err)
os.Exit(1)
}
ctx = context.WithValue(ctx, "Language", ln)
pfp := path.Join(scriptDir, "pp.csv")
cfg := engine.Config{
Root: "root",
OutputSize: uint32(size),
FlagCount: uint32(128),
MenuSeparator: menuSeparator,
}
if engineDebug {
cfg.EngineDebug = true
}
menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
rs, err := menuStorageService.GetResource(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)
}
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)
}
}

View File

@ -1,146 +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.defalsify.org/vise.git/lang"
"git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/internal/args"
"git.grassecon.net/urdt/ussd/remote"
)
var (
logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration")
menuSeparator = ": "
)
func init() {
initializers.LoadEnvVariables()
}
// TODO: external script automatically generate language handler list from select language vise code OR consider dynamic menu generation script possibility
func main() {
config.LoadConfig()
var connStr string
var size uint
var sessionId string
var engineDebug bool
var resourceDir string
var err error
var gettextDir string
var langs args.LangVar
flag.StringVar(&resourceDir, "resourcedir", scriptDir, "resource dir")
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
flag.StringVar(&connStr, "c", "", "connection string")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.UintVar(&size, "s", 160, "max size of output")
flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory")
flag.Var(&langs, "language", "add symbol resolution for language")
flag.Parse()
if connStr != "" {
connStr = config.DbConn
}
connData, err := storage.ToConnData(connStr)
if err != nil {
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
os.Exit(1)
}
logg.Infof("start command", "conn", connData, "outputsize", size)
if len(langs.Langs()) == 0 {
langs.Set(config.DefaultLanguage)
}
ctx := context.Background()
ctx = context.WithValue(ctx, "SessionId", sessionId)
ln, err := lang.LanguageFromCode(config.DefaultLanguage)
if err != nil {
fmt.Fprintf(os.Stderr, "default language set error: %v", err)
os.Exit(1)
}
ctx = context.WithValue(ctx, "Language", ln)
pfp := path.Join(scriptDir, "pp.csv")
cfg := engine.Config{
Root: "root",
SessionId: sessionId,
OutputSize: uint32(size),
FlagCount: uint32(128),
MenuSeparator: menuSeparator,
}
menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
if gettextDir != "" {
menuStorageService = menuStorageService.WithGettext(gettextDir, langs.Langs())
}
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)
}
}

View File

@ -1,34 +0,0 @@
# URDT-USSD SSH server
An SSH server entry point for the vise engine.
## Adding public keys for access
Map your (client) public key to a session identifier (e.g. phone number)
```
go run -v -tags logtrace ./cmd/ssh/sshkey/main.go -i <session_id> [--dbdir <dbpath>] <client_publickey_filepath>
```
## Create a private key for the server
```
ssh-keygen -N "" -f <server_privatekey_filepath>
```
## Run the server
```
go run -v -tags logtrace ./cmd/ssh/main.go -h <host> -p <port> [--dbdir <dbpath>] <server_privatekey_filepath>
```
## Connect to the server
```
ssh [-v] -T -p <port> -i <client_publickey_filepath> <host>
```

View File

@ -1,144 +0,0 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"os/signal"
"path"
"sync"
"syscall"
"git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/logging"
"git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/internal/ssh"
"git.grassecon.net/urdt/ussd/internal/storage"
)
var (
wg sync.WaitGroup
keyStore db.Db
logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration")
build = "dev"
)
func init() {
initializers.LoadEnvVariables()
}
func main() {
config.LoadConfig()
var connStr string
var authConnStr string
var resourceDir string
var size uint
var engineDebug bool
var stateDebug bool
var host string
var port uint
flag.StringVar(&connStr, "c", "", "connection string")
flag.StringVar(&authConnStr, "authdb", "", "auth connection string")
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.UintVar(&size, "s", 160, "max size of output")
flag.StringVar(&host, "h", "127.0.0.1", "socket host")
flag.UintVar(&port, "p", 7122, "socket port")
flag.Parse()
if connStr == "" {
connStr = config.DbConn
}
if authConnStr == "" {
authConnStr = connStr
}
connData, err := storage.ToConnData(connStr)
if err != nil {
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
os.Exit(1)
}
authConnData, err := storage.ToConnData(authConnStr)
if err != nil {
fmt.Fprintf(os.Stderr, "auth connstr err: %v", err)
os.Exit(1)
}
sshKeyFile := flag.Arg(0)
_, err = os.Stat(sshKeyFile)
if err != nil {
fmt.Fprintf(os.Stderr, "cannot open ssh server private key file: %v\n", err)
os.Exit(1)
}
ctx := context.Background()
logg.WarnCtxf(ctx, "!!!!! WARNING WARNING WARNING")
logg.WarnCtxf(ctx, "!!!!! =======================")
logg.WarnCtxf(ctx, "!!!!! This is not a production ready server!")
logg.WarnCtxf(ctx, "!!!!! Do not expose to internet and only use with tunnel!")
logg.WarnCtxf(ctx, "!!!!! (See ssh -L <...>)")
logg.Infof("start command", "conn", connData, "authconn", authConnData, "resourcedir", resourceDir, "outputsize", size, "keyfile", sshKeyFile, "host", host, "port", port)
pfp := path.Join(scriptDir, "pp.csv")
cfg := engine.Config{
Root: "root",
OutputSize: uint32(size),
FlagCount: uint32(16),
}
if stateDebug {
cfg.StateDebug = true
}
if engineDebug {
cfg.EngineDebug = true
}
authKeyStore, err := ssh.NewSshKeyStore(ctx, authConnData.String())
if err != nil {
fmt.Fprintf(os.Stderr, "keystore file open error: %v", err)
os.Exit(1)
}
defer func() {
logg.TraceCtxf(ctx, "shutdown auth key store reached")
err = authKeyStore.Close()
if err != nil {
logg.ErrorCtxf(ctx, "keystore close error", "err", err)
}
}()
cint := make(chan os.Signal)
cterm := make(chan os.Signal)
signal.Notify(cint, os.Interrupt, syscall.SIGINT)
signal.Notify(cterm, os.Interrupt, syscall.SIGTERM)
runner := &ssh.SshRunner{
Cfg: cfg,
Debug: engineDebug,
FlagFile: pfp,
Conn: connData,
ResourceDir: resourceDir,
SrvKeyFile: sshKeyFile,
Host: host,
Port: port,
}
go func() {
select {
case _ = <-cint:
case _ = <-cterm:
}
logg.TraceCtxf(ctx, "shutdown runner reached")
err := runner.Stop()
if err != nil {
logg.ErrorCtxf(ctx, "runner stop error", "err", err)
}
}()
runner.Run(ctx, authKeyStore)
}

View File

@ -1,44 +0,0 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"git.grassecon.net/urdt/ussd/internal/ssh"
)
func main() {
var dbDir string
var sessionId string
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
flag.StringVar(&sessionId, "i", "", "session id")
flag.Parse()
if sessionId == "" {
fmt.Fprintf(os.Stderr, "empty session id\n")
os.Exit(1)
}
ctx := context.Background()
sshKeyFile := flag.Arg(0)
if sshKeyFile == "" {
fmt.Fprintf(os.Stderr, "missing key file argument\n")
os.Exit(1)
}
store, err := ssh.NewSshKeyStore(ctx, dbDir)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
defer store.Close()
err = store.AddFromFile(ctx, sshKeyFile, sessionId)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}

View File

@ -1,132 +0,0 @@
package common
import (
"encoding/binary"
"errors"
"git.defalsify.org/vise.git/logging"
)
// DataType is a subprefix value used in association with vise/db.DATATYPE_USERDATA.
//
// All keys are used only within the context of a single account. Unless otherwise specified, the user context is the session id.
//
// * The first byte is vise/db.DATATYPE_USERDATA
// * The last 2 bytes are the DataTyp value, big-endian.
// * The intermediate bytes are the id of the user context.
//
// All values are strings
type DataTyp uint16
const (
// API Tracking id to follow status of account creation
DATA_TRACKING_ID = iota
// EVM address returned from API on account creation
DATA_PUBLIC_KEY
// Currently active PIN used to authenticate ussd state change requests
DATA_ACCOUNT_PIN
// 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
//Holds count of the number of incorrect PIN attempts
DATA_INCORRECT_PIN_ATTEMPTS
)
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 (
logg = logging.NewVanilla().WithDomain("urdt-common")
)
func typToBytes(typ DataTyp) []byte {
var b [2]byte
binary.BigEndian.PutUint16(b[:], uint16(typ))
return b[:]
}
func PackKey(typ DataTyp, data []byte) []byte {
v := typToBytes(typ)
return append(v, data...)
}
func StringToDataTyp(str string) (DataTyp, error) {
switch str {
case "DATA_FIRST_NAME":
return DATA_FIRST_NAME, nil
case "DATA_FAMILY_NAME":
return DATA_FAMILY_NAME, nil
case "DATA_YOB":
return DATA_YOB, nil
case "DATA_LOCATION":
return DATA_LOCATION, nil
case "DATA_GENDER":
return DATA_GENDER, nil
case "DATA_OFFERINGS":
return DATA_OFFERINGS, nil
default:
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
}

View File

@ -1,31 +0,0 @@
package common
import (
"encoding/hex"
"strings"
)
func NormalizeHex(s string) (string, error) {
if len(s) >= 2 {
if s[:2] == "0x" {
s = s[2:]
}
}
r, err := hex.DecodeString(s)
if err != nil {
return "", err
}
return hex.EncodeToString(r), nil
}
func IsSameHex(left string, right string) bool {
bl, err := NormalizeHex(left)
if err != nil {
return false
}
br, err := NormalizeHex(left)
if err != nil {
return false
}
return strings.Compare(bl, br) == 0
}

View File

@ -1,37 +0,0 @@
package common
import (
"regexp"
"golang.org/x/crypto/bcrypt"
)
const (
// Define the regex pattern as a constant
pinPattern = `^\d{4}$`
//Allowed incorrect PIN attempts
AllowedPINAttempts = uint8(3)
)
// checks whether the given input is a 4 digit number
func IsValidPIN(pin string) bool {
match, _ := regexp.MatchString(pinPattern, pin)
return match
}
// HashPIN uses bcrypt with 8 salt rounds to hash the PIN
func HashPIN(pin string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(pin), 8)
if err != nil {
return "", err
}
return string(hash), nil
}
// VerifyPIN compareS the hashed PIN with the plaintext PIN
func VerifyPIN(hashedPIN, pin string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPIN), []byte(pin))
return err == nil
}

View File

@ -1,173 +0,0 @@
package common
import (
"testing"
"golang.org/x/crypto/bcrypt"
)
func TestIsValidPIN(t *testing.T) {
tests := []struct {
name string
pin string
expected bool
}{
{
name: "Valid PIN with 4 digits",
pin: "1234",
expected: true,
},
{
name: "Valid PIN with leading zeros",
pin: "0001",
expected: true,
},
{
name: "Invalid PIN with less than 4 digits",
pin: "123",
expected: false,
},
{
name: "Invalid PIN with more than 4 digits",
pin: "12345",
expected: false,
},
{
name: "Invalid PIN with letters",
pin: "abcd",
expected: false,
},
{
name: "Invalid PIN with special characters",
pin: "12@#",
expected: false,
},
{
name: "Empty PIN",
pin: "",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := IsValidPIN(tt.pin)
if actual != tt.expected {
t.Errorf("IsValidPIN(%q) = %v; expected %v", tt.pin, actual, tt.expected)
}
})
}
}
func TestHashPIN(t *testing.T) {
tests := []struct {
name string
pin string
}{
{
name: "Valid PIN with 4 digits",
pin: "1234",
},
{
name: "Valid PIN with leading zeros",
pin: "0001",
},
{
name: "Empty PIN",
pin: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hashedPIN, err := HashPIN(tt.pin)
if err != nil {
t.Errorf("HashPIN(%q) returned an error: %v", tt.pin, err)
return
}
if hashedPIN == "" {
t.Errorf("HashPIN(%q) returned an empty hash", tt.pin)
}
// Ensure the hash can be verified with bcrypt
err = bcrypt.CompareHashAndPassword([]byte(hashedPIN), []byte(tt.pin))
if tt.pin != "" && err != nil {
t.Errorf("HashPIN(%q) produced a hash that does not match: %v", tt.pin, err)
}
})
}
}
func TestVerifyMigratedHashPin(t *testing.T) {
tests := []struct {
pin string
hash string
}{
{
pin: "1234",
hash: "$2b$08$dTvIGxCCysJtdvrSnaLStuylPoOS/ZLYYkxvTeR5QmTFY3TSvPQC6",
},
}
for _, tt := range tests {
t.Run(tt.pin, func(t *testing.T) {
ok := VerifyPIN(tt.hash, tt.pin)
if !ok {
t.Errorf("VerifyPIN could not verify migrated PIN: %v", tt.pin)
}
})
}
}
func TestVerifyPIN(t *testing.T) {
tests := []struct {
name string
pin string
hashedPIN string
shouldPass bool
}{
{
name: "Valid PIN verification",
pin: "1234",
hashedPIN: hashPINHelper("1234"),
shouldPass: true,
},
{
name: "Invalid PIN verification with incorrect PIN",
pin: "5678",
hashedPIN: hashPINHelper("1234"),
shouldPass: false,
},
{
name: "Invalid PIN verification with empty PIN",
pin: "",
hashedPIN: hashPINHelper("1234"),
shouldPass: false,
},
{
name: "Invalid PIN verification with invalid hash",
pin: "1234",
hashedPIN: "invalidhash",
shouldPass: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := VerifyPIN(tt.hashedPIN, tt.pin)
if result != tt.shouldPass {
t.Errorf("VerifyPIN(%q, %q) = %v; expected %v", tt.hashedPIN, tt.pin, result, tt.shouldPass)
}
})
}
}
// Helper function to hash a PIN for testing purposes
func hashPINHelper(pin string) string {
hashedPIN, err := HashPIN(pin)
if err != nil {
panic("Failed to hash PIN for test setup: " + err.Error())
}
return hashedPIN
}

View File

@ -1,73 +0,0 @@
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
}

View File

@ -1,49 +0,0 @@
package common
import (
"context"
"errors"
"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"
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
)
func StoreToDb(store *UserDataStore) db.Db {
return store.Db
}
func StoreToPrefixDb(store *UserDataStore, pfx []byte) dbstorage.PrefixDb {
return dbstorage.NewSubPrefixDb(store.Db, pfx)
}
type StorageServices interface {
GetPersister(ctx context.Context) (*persist.Persister, error)
GetUserdataDb(ctx context.Context) (db.Db, error)
GetResource(ctx context.Context) (resource.Resource, error)
}
type StorageService struct {
svc *storage.MenuStorageService
}
func NewStorageService(conn storage.ConnData) (*StorageService, error) {
svc := &StorageService{
svc: storage.NewMenuStorageService(conn, ""),
}
return svc, nil
}
func(ss *StorageService) GetPersister(ctx context.Context) (*persist.Persister, error) {
return ss.svc.GetPersister(ctx)
}
func(ss *StorageService) GetUserdataDb(ctx context.Context) (db.Db, error) {
return ss.svc.GetUserdataDb(ctx)
}
func(ss *StorageService) GetResource(ctx context.Context) (resource.Resource, error) {
return nil, errors.New("not implemented")
}

View File

@ -1,81 +0,0 @@
package common
import (
"context"
"errors"
"math/big"
"reflect"
"strconv"
)
type TransactionData struct {
TemporaryValue string
ActiveSym string
Amount string
PublicKey string
Recipient string
ActiveDecimal string
ActiveAddress string
}
func ParseAndScaleAmount(storedAmount, activeDecimal string) (string, error) {
// Parse token decimal
tokenDecimal, err := strconv.Atoi(activeDecimal)
if err != nil {
return "", err
}
// Parse amount
amount, _, err := big.ParseFloat(storedAmount, 10, 0, big.ToZero)
if err != nil {
return "", err
}
// Scale the amount
multiplier := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(tokenDecimal)), nil))
finalAmount := new(big.Float).Mul(amount, multiplier)
// Convert finalAmount to a string
finalAmountStr := new(big.Int)
finalAmount.Int(finalAmountStr)
return finalAmountStr.String(), nil
}
func ReadTransactionData(ctx context.Context, store DataStore, sessionId string) (TransactionData, error) {
data := TransactionData{}
fieldToKey := map[string]DataTyp{
"TemporaryValue": DATA_TEMPORARY_VALUE,
"ActiveSym": DATA_ACTIVE_SYM,
"Amount": DATA_AMOUNT,
"PublicKey": DATA_PUBLIC_KEY,
"Recipient": DATA_RECIPIENT,
"ActiveDecimal": DATA_ACTIVE_DECIMAL,
"ActiveAddress": DATA_ACTIVE_ADDRESS,
}
v := reflect.ValueOf(&data).Elem()
for fieldName, key := range fieldToKey {
field := v.FieldByName(fieldName)
if !field.IsValid() || !field.CanSet() {
return data, errors.New("invalid struct field: " + fieldName)
}
value, err := readStringEntry(ctx, store, sessionId, key)
if err != nil {
return data, err
}
field.SetString(value)
}
return data, nil
}
func readStringEntry(ctx context.Context, store DataStore, sessionId string, key DataTyp) (string, error) {
entry, err := store.ReadEntry(ctx, sessionId, key)
if err != nil {
return "", err
}
return string(entry), nil
}

View File

@ -1,129 +0,0 @@
package common
import (
"testing"
"github.com/alecthomas/assert/v2"
)
func TestParseAndScaleAmount(t *testing.T) {
tests := []struct {
name string
amount string
decimals string
want string
expectError bool
}{
{
name: "whole number",
amount: "123",
decimals: "2",
want: "12300",
expectError: false,
},
{
name: "decimal number",
amount: "123.45",
decimals: "2",
want: "12345",
expectError: false,
},
{
name: "zero decimals",
amount: "123.45",
decimals: "0",
want: "123",
expectError: false,
},
{
name: "large number",
amount: "1000000.01",
decimals: "6",
want: "1000000010000",
expectError: false,
},
{
name: "invalid amount",
amount: "abc",
decimals: "2",
want: "",
expectError: true,
},
{
name: "invalid decimals",
amount: "123.45",
decimals: "abc",
want: "",
expectError: true,
},
{
name: "zero amount",
amount: "0",
decimals: "2",
want: "0",
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseAndScaleAmount(tt.amount, tt.decimals)
// Check error cases
if tt.expectError {
if err == nil {
t.Errorf("ParseAndScaleAmount(%q, %q) expected error, got nil", tt.amount, tt.decimals)
}
return
}
if err != nil {
t.Errorf("ParseAndScaleAmount(%q, %q) unexpected error: %v", tt.amount, tt.decimals, err)
return
}
if got != tt.want {
t.Errorf("ParseAndScaleAmount(%q, %q) = %v, want %v", tt.amount, tt.decimals, got, tt.want)
}
})
}
}
func TestReadTransactionData(t *testing.T) {
sessionId := "session123"
publicKey := "0X13242618721"
ctx, store := InitializeTestDb(t)
// Test transaction data
transactionData := map[DataTyp]string{
DATA_TEMPORARY_VALUE: "0712345678",
DATA_ACTIVE_SYM: "SRF",
DATA_AMOUNT: "1000000",
DATA_PUBLIC_KEY: publicKey,
DATA_RECIPIENT: "0x41c188d63Qa",
DATA_ACTIVE_DECIMAL: "6",
DATA_ACTIVE_ADDRESS: "0xd4c288865Ce",
}
// Store the data
for key, value := range transactionData {
if err := store.WriteEntry(ctx, sessionId, key, []byte(value)); err != nil {
t.Fatal(err)
}
}
expectedResult := TransactionData{
TemporaryValue: "0712345678",
ActiveSym: "SRF",
Amount: "1000000",
PublicKey: publicKey,
Recipient: "0x41c188d63Qa",
ActiveDecimal: "6",
ActiveAddress: "0xd4c288865Ce",
}
data, err := ReadTransactionData(ctx, store, sessionId)
assert.NoError(t, err)
assert.Equal(t, expectedResult, data)
}

View File

@ -1,119 +0,0 @@
package common
import (
"context"
"fmt"
"strings"
"time"
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
)
// TransferMetadata helps organize data fields
type TransferMetadata struct {
Senders string
Recipients string
TransferValues string
Addresses string
TxHashes string
Dates string
Symbols string
Decimals string
}
// ProcessTransfers converts transfers into formatted strings
func ProcessTransfers(transfers []dataserviceapi.Last10TxResponse) TransferMetadata {
var data TransferMetadata
var senders, recipients, transferValues, addresses, txHashes, dates, symbols, decimals []string
for _, t := range transfers {
senders = append(senders, t.Sender)
recipients = append(recipients, t.Recipient)
// Scale down the amount
scaledBalance := ScaleDownBalance(t.TransferValue, t.TokenDecimals)
transferValues = append(transferValues, scaledBalance)
addresses = append(addresses, t.ContractAddress)
txHashes = append(txHashes, t.TxHash)
dates = append(dates, fmt.Sprintf("%s", t.DateBlock))
symbols = append(symbols, t.TokenSymbol)
decimals = append(decimals, t.TokenDecimals)
}
data.Senders = strings.Join(senders, "\n")
data.Recipients = strings.Join(recipients, "\n")
data.TransferValues = strings.Join(transferValues, "\n")
data.Addresses = strings.Join(addresses, "\n")
data.TxHashes = strings.Join(txHashes, "\n")
data.Dates = strings.Join(dates, "\n")
data.Symbols = strings.Join(symbols, "\n")
data.Decimals = strings.Join(decimals, "\n")
return data
}
// GetTransferData retrieves and matches transfer data
// returns a formatted string of the full transaction/statement
func GetTransferData(ctx context.Context, db dbstorage.PrefixDb, publicKey string, index int) (string, error) {
keys := []DataTyp{DATA_TX_SENDERS, DATA_TX_RECIPIENTS, DATA_TX_VALUES, DATA_TX_ADDRESSES, DATA_TX_HASHES, DATA_TX_DATES, DATA_TX_SYMBOLS}
data := make(map[DataTyp]string)
for _, key := range keys {
value, err := db.Get(ctx, ToBytes(key))
if err != nil {
return "", fmt.Errorf("failed to get %s: %v", ToBytes(key), err)
}
data[key] = string(value)
}
// Split the data
senders := strings.Split(string(data[DATA_TX_SENDERS]), "\n")
recipients := strings.Split(string(data[DATA_TX_RECIPIENTS]), "\n")
values := strings.Split(string(data[DATA_TX_VALUES]), "\n")
addresses := strings.Split(string(data[DATA_TX_ADDRESSES]), "\n")
hashes := strings.Split(string(data[DATA_TX_HASHES]), "\n")
dates := strings.Split(string(data[DATA_TX_DATES]), "\n")
syms := strings.Split(string(data[DATA_TX_SYMBOLS]), "\n")
// Check if index is within range
if index < 1 || index > len(senders) {
return "", fmt.Errorf("transaction not found: index %d out of range", index)
}
// Adjust for 0-based indexing
i := index - 1
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]))
}
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",
transactionType,
strings.TrimSpace(values[i]),
strings.TrimSpace(syms[i]),
party,
strings.TrimSpace(addresses[i]),
strings.TrimSpace(hashes[i]),
formattedDate,
)
return detail, nil
}
// Helper function to format date in desired output
func formatDate(dateStr string) string {
parsedDate, err := time.Parse("2006-01-02 15:04:05 -0700 MST", dateStr)
if err != nil {
fmt.Println("Error parsing date:", err)
return ""
}
return parsedDate.Format("2006-01-02 03:04:05 PM")
}

View File

@ -1,34 +0,0 @@
package common
import (
"context"
"git.defalsify.org/vise.git/db"
)
type DataStore interface {
db.Db
ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error)
WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error
}
type UserDataStore struct {
db.Db
}
// ReadEntry retrieves an entry to the userdata store.
func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error) {
store.SetPrefix(db.DATATYPE_USERDATA)
store.SetSession(sessionId)
k := ToBytes(typ)
return store.Get(ctx, k)
}
// WriteEntry adds an entry to the userdata store.
// BUG: this uses sessionId twice
func (store *UserDataStore) WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error {
store.SetPrefix(db.DATATYPE_USERDATA)
store.SetSession(sessionId)
k := ToBytes(typ)
return store.Put(ctx, k, value)
}

View File

@ -1,174 +0,0 @@
package common
import (
"context"
"fmt"
"math/big"
"strings"
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
)
// VoucherMetadata helps organize data fields
type VoucherMetadata struct {
Symbols string
Balances string
Decimals string
Addresses string
}
// ProcessVouchers converts holdings into formatted strings
func ProcessVouchers(holdings []dataserviceapi.TokenHoldings) VoucherMetadata {
var data VoucherMetadata
var symbols, balances, decimals, addresses []string
for i, h := range holdings {
symbols = append(symbols, fmt.Sprintf("%d:%s", i+1, h.TokenSymbol))
// Scale down the balance
scaledBalance := ScaleDownBalance(h.Balance, h.TokenDecimals)
balances = append(balances, fmt.Sprintf("%d:%s", i+1, scaledBalance))
decimals = append(decimals, fmt.Sprintf("%d:%s", i+1, h.TokenDecimals))
addresses = append(addresses, fmt.Sprintf("%d:%s", i+1, h.ContractAddress))
}
data.Symbols = strings.Join(symbols, "\n")
data.Balances = strings.Join(balances, "\n")
data.Decimals = strings.Join(decimals, "\n")
data.Addresses = strings.Join(addresses, "\n")
return data
}
func ScaleDownBalance(balance, decimals string) string {
// Convert balance and decimals to big.Float
bal := new(big.Float)
bal.SetString(balance)
dec, ok := new(big.Int).SetString(decimals, 10)
if !ok {
dec = big.NewInt(0) // Default to 0 decimals in case of conversion failure
}
divisor := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), dec, nil))
scaledBalance := new(big.Float).Quo(bal, divisor)
// Return the scaled balance without trailing decimals if it's an integer
if scaledBalance.IsInt() {
return scaledBalance.Text('f', 0)
}
return scaledBalance.Text('f', -1)
}
// GetVoucherData retrieves and matches voucher data
func GetVoucherData(ctx context.Context, db dbstorage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) {
keys := []DataTyp{DATA_VOUCHER_SYMBOLS, DATA_VOUCHER_BALANCES, DATA_VOUCHER_DECIMALS, DATA_VOUCHER_ADDRESSES}
data := make(map[DataTyp]string)
for _, key := range keys {
value, err := db.Get(ctx, ToBytes(key))
if err != nil {
return nil, fmt.Errorf("failed to get %s: %v", ToBytes(key), err)
}
data[key] = string(value)
}
symbol, balance, decimal, address := MatchVoucher(input,
data[DATA_VOUCHER_SYMBOLS],
data[DATA_VOUCHER_BALANCES],
data[DATA_VOUCHER_DECIMALS],
data[DATA_VOUCHER_ADDRESSES],
)
if symbol == "" {
return nil, nil
}
return &dataserviceapi.TokenHoldings{
TokenSymbol: string(symbol),
Balance: string(balance),
TokenDecimals: string(decimal),
ContractAddress: string(address),
}, nil
}
// MatchVoucher finds the matching voucher symbol, balance, decimals and contract address based on the input.
func MatchVoucher(input, symbols, balances, decimals, addresses string) (symbol, balance, decimal, address string) {
symList := strings.Split(symbols, "\n")
balList := strings.Split(balances, "\n")
decList := strings.Split(decimals, "\n")
addrList := strings.Split(addresses, "\n")
logg.Tracef("found", "symlist", symList, "syms", symbols, "input", input)
for i, sym := range symList {
parts := strings.SplitN(sym, ":", 2)
if input == parts[0] || strings.EqualFold(input, parts[1]) {
symbol = parts[1]
if i < len(balList) {
balance = strings.SplitN(balList[i], ":", 2)[1]
}
if i < len(decList) {
decimal = strings.SplitN(decList[i], ":", 2)[1]
}
if i < len(addrList) {
address = strings.SplitN(addrList[i], ":", 2)[1]
}
break
}
}
return
}
// StoreTemporaryVoucher saves voucher metadata as temporary entries in the DataStore.
func StoreTemporaryVoucher(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error {
tempData := fmt.Sprintf("%s,%s,%s,%s", data.TokenSymbol, data.Balance, data.TokenDecimals, data.ContractAddress)
if err := store.WriteEntry(ctx, sessionId, DATA_TEMPORARY_VALUE, []byte(tempData)); err != nil {
return err
}
return nil
}
// GetTemporaryVoucherData retrieves temporary voucher metadata from the DataStore.
func GetTemporaryVoucherData(ctx context.Context, store DataStore, sessionId string) (*dataserviceapi.TokenHoldings, error) {
temp_data, err := store.ReadEntry(ctx, sessionId, DATA_TEMPORARY_VALUE)
if err != nil {
return nil, err
}
values := strings.SplitN(string(temp_data), ",", 4)
data := &dataserviceapi.TokenHoldings{}
data.TokenSymbol = values[0]
data.Balance = values[1]
data.TokenDecimals = values[2]
data.ContractAddress = values[3]
return data, nil
}
// 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
activeEntries := map[DataTyp][]byte{
DATA_ACTIVE_SYM: []byte(data.TokenSymbol),
DATA_ACTIVE_BAL: []byte(data.Balance),
DATA_ACTIVE_DECIMAL: []byte(data.TokenDecimals),
DATA_ACTIVE_ADDRESS: []byte(data.ContractAddress),
}
// Write active data
for key, value := range activeEntries {
if err := store.WriteEntry(ctx, sessionId, key, value); err != nil {
return err
}
}
return nil
}

View File

@ -1,200 +0,0 @@
package common
import (
"context"
"fmt"
"testing"
"github.com/alecthomas/assert/v2"
"github.com/stretchr/testify/require"
visedb "git.defalsify.org/vise.git/db"
memdb "git.defalsify.org/vise.git/db/mem"
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
)
// InitializeTestDb sets up and returns an in-memory database and store.
func InitializeTestDb(t *testing.T) (context.Context, *UserDataStore) {
ctx := context.Background()
// Initialize memDb
db := memdb.NewMemDb()
err := db.Connect(ctx, "")
require.NoError(t, err, "Failed to connect to memDb")
// Create UserDataStore with memDb
store := &UserDataStore{Db: db}
t.Cleanup(func() {
db.Close() // Ensure the DB is closed after each test
})
return ctx, store
}
func TestMatchVoucher(t *testing.T) {
symbols := "1:SRF\n2:MILO"
balances := "1:100\n2:200"
decimals := "1:6\n2:4"
addresses := "1:0xd4c288865Ce\n2:0x41c188d63Qa"
// Test for valid voucher
symbol, balance, decimal, address := MatchVoucher("2", symbols, balances, decimals, addresses)
// Assertions for valid voucher
assert.Equal(t, "MILO", symbol)
assert.Equal(t, "200", balance)
assert.Equal(t, "4", decimal)
assert.Equal(t, "0x41c188d63Qa", address)
// Test for non-existent voucher
symbol, balance, decimal, address = MatchVoucher("3", symbols, balances, decimals, addresses)
// Assertions for non-match
assert.Equal(t, "", symbol)
assert.Equal(t, "", balance)
assert.Equal(t, "", decimal)
assert.Equal(t, "", address)
}
func TestProcessVouchers(t *testing.T) {
holdings := []dataserviceapi.TokenHoldings{
{ContractAddress: "0xd4c288865Ce", TokenSymbol: "SRF", TokenDecimals: "6", Balance: "100000000"},
{ContractAddress: "0x41c188d63Qa", TokenSymbol: "MILO", TokenDecimals: "4", Balance: "200000000"},
}
expectedResult := VoucherMetadata{
Symbols: "1:SRF\n2:MILO",
Balances: "1:100\n2:20000",
Decimals: "1:6\n2:4",
Addresses: "1:0xd4c288865Ce\n2:0x41c188d63Qa",
}
result := ProcessVouchers(holdings)
assert.Equal(t, expectedResult, result)
}
func TestGetVoucherData(t *testing.T) {
ctx := context.Background()
db := memdb.NewMemDb()
err := db.Connect(ctx, "")
if err != nil {
t.Fatal(err)
}
prefix := ToBytes(visedb.DATATYPE_USERDATA)
spdb := dbstorage.NewSubPrefixDb(db, prefix)
// Test voucher data
mockData := map[DataTyp][]byte{
DATA_VOUCHER_SYMBOLS: []byte("1:SRF\n2:MILO"),
DATA_VOUCHER_BALANCES: []byte("1:100\n2:200"),
DATA_VOUCHER_DECIMALS: []byte("1:6\n2:4"),
DATA_VOUCHER_ADDRESSES: []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"),
}
// Put the data
for key, value := range mockData {
err = spdb.Put(ctx, []byte(ToBytes(key)), []byte(value))
if err != nil {
t.Fatal(err)
}
}
result, err := GetVoucherData(ctx, spdb, "1")
assert.NoError(t, err)
assert.Equal(t, "SRF", result.TokenSymbol)
assert.Equal(t, "100", result.Balance)
assert.Equal(t, "6", result.TokenDecimals)
assert.Equal(t, "0xd4c288865Ce", result.ContractAddress)
}
func TestStoreTemporaryVoucher(t *testing.T) {
ctx, store := InitializeTestDb(t)
sessionId := "session123"
// Test data
voucherData := &dataserviceapi.TokenHoldings{
TokenSymbol: "SRF",
Balance: "200",
TokenDecimals: "6",
ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9",
}
// Execute the function being tested
err := StoreTemporaryVoucher(ctx, store, sessionId, voucherData)
require.NoError(t, err)
// Verify stored data
expectedData := fmt.Sprintf("%s,%s,%s,%s", "SRF", "200", "6", "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9")
storedValue, err := store.ReadEntry(ctx, sessionId, DATA_TEMPORARY_VALUE)
require.NoError(t, err)
require.Equal(t, expectedData, string(storedValue), "Mismatch for key %v", DATA_TEMPORARY_VALUE)
}
func TestGetTemporaryVoucherData(t *testing.T) {
ctx, store := InitializeTestDb(t)
sessionId := "session123"
// Test voucher data
tempData := &dataserviceapi.TokenHoldings{
TokenSymbol: "SRF",
Balance: "200",
TokenDecimals: "6",
ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9",
}
// Store the data
err := StoreTemporaryVoucher(ctx, store, sessionId, tempData)
require.NoError(t, err)
// Execute the function being tested
data, err := GetTemporaryVoucherData(ctx, store, sessionId)
require.NoError(t, err)
require.Equal(t, tempData, data)
}
func TestUpdateVoucherData(t *testing.T) {
ctx, store := InitializeTestDb(t)
sessionId := "session123"
// New voucher data
newData := &dataserviceapi.TokenHoldings{
TokenSymbol: "SRF",
Balance: "200",
TokenDecimals: "6",
ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9",
}
// Old temporary data
tempData := &dataserviceapi.TokenHoldings{
TokenSymbol: "OLD",
Balance: "100",
TokenDecimals: "8",
ContractAddress: "0xold",
}
require.NoError(t, StoreTemporaryVoucher(ctx, store, sessionId, tempData))
// Execute update
err := UpdateVoucherData(ctx, store, sessionId, newData)
require.NoError(t, err)
// Verify active data was stored correctly
activeEntries := map[DataTyp][]byte{
DATA_ACTIVE_SYM: []byte(newData.TokenSymbol),
DATA_ACTIVE_BAL: []byte(newData.Balance),
DATA_ACTIVE_DECIMAL: []byte(newData.TokenDecimals),
DATA_ACTIVE_ADDRESS: []byte(newData.ContractAddress),
}
for key, expectedValue := range activeEntries {
storedValue, err := store.ReadEntry(ctx, sessionId, key)
require.NoError(t, err)
require.Equal(t, expectedValue, storedValue, "Active data mismatch for key %v", key)
}
}

View File

@ -1,22 +1,9 @@
package config
import (
"net/url"
"strings"
"git.grassecon.net/urdt/ussd/initializers"
)
const (
createAccountPath = "/api/v2/account/create"
trackStatusPath = "/api/track"
balancePathPrefix = "/api/account"
trackPath = "/api/v2/account/status"
tokenTransferPrefix = "/api/v2/token/transfer"
voucherHoldingsPathPrefix = "/api/v1/holdings"
voucherTransfersPathPrefix = "/api/v1/transfers/last10"
voucherDataPathPrefix = "/api/v1/token"
AliasPrefix = "api/v1/alias"
"git.grassecon.net/grassrootseconomics/visedriver/env"
)
var (
@ -25,29 +12,14 @@ var (
)
var (
custodialURLBase string
dataURLBase string
BearerToken string
)
var (
CreateAccountURL string
TrackStatusURL string
BalanceURL string
TrackURL string
TokenTransferURL string
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), ",")
defaultLanguage = env.GetEnv("DEFAULT_LANGUAGE", defaultLanguage)
languages = strings.Split(env.GetEnv("LANGUAGES", defaultLanguage), ",")
haveDefaultLanguage := false
for i, v := range(languages) {
languages[i] = strings.ReplaceAll(v, " ", "")
@ -63,37 +35,16 @@ func setLanguage() error {
return nil
}
func setBase() error {
var err error
custodialURLBase = initializers.GetEnv("CUSTODIAL_URL_BASE", "http://localhost:5003")
dataURLBase = initializers.GetEnv("DATA_URL_BASE", "http://localhost:5006")
BearerToken = initializers.GetEnv("BEARER_TOKEN", "")
_, err = url.Parse(custodialURLBase)
if err != nil {
return err
}
_, err = url.Parse(dataURLBase)
if err != nil {
return err
}
return nil
}
func setConn() error {
DbConn = initializers.GetEnv("DB_CONN", "")
DbConn = env.GetEnv("DB_CONN", "")
return nil
}
// LoadConfig initializes the configuration values after environment variables are loaded.
func LoadConfig() error {
err := setBase()
if err != nil {
return err
}
err = setConn()
err := setConn()
if err != nil {
return err
}
@ -101,15 +52,6 @@ func LoadConfig() error {
if err != nil {
return err
}
CreateAccountURL, _ = url.JoinPath(custodialURLBase, createAccountPath)
TrackStatusURL, _ = url.JoinPath(custodialURLBase, trackStatusPath)
BalanceURL, _ = url.JoinPath(custodialURLBase, balancePathPrefix)
TrackURL, _ = url.JoinPath(custodialURLBase, trackPath)
TokenTransferURL, _ = url.JoinPath(custodialURLBase, tokenTransferPrefix)
VoucherHoldingsURL, _ = url.JoinPath(dataURLBase, voucherHoldingsPathPrefix)
VoucherTransfersURL, _ = url.JoinPath(dataURLBase, voucherTransfersPathPrefix)
VoucherDataURL, _ = url.JoinPath(dataURLBase, voucherDataPathPrefix)
CheckAliasURL, _ = url.JoinPath(dataURLBase, AliasPrefix)
DefaultLanguage = defaultLanguage
Languages = languages

View File

@ -1,5 +0,0 @@
package debug
var (
DebugCap uint32
)

View File

@ -1,84 +0,0 @@
package debug
import (
"fmt"
"encoding/binary"
"git.grassecon.net/urdt/ussd/common"
"git.defalsify.org/vise.git/db"
)
var (
dbTypStr map[common.DataTyp]string = make(map[common.DataTyp]string)
)
type KeyInfo struct {
SessionId string
Typ uint8
SubTyp common.DataTyp
Label string
Description string
}
func (k KeyInfo) String() string {
v := uint16(k.SubTyp)
s := subTypToString(k.SubTyp)
if s == "" {
v = uint16(k.Typ)
s = typToString(k.Typ)
}
return fmt.Sprintf("Session Id: %s\nTyp: %s (%d)\n", k.SessionId, s, v)
}
func ToKeyInfo(k []byte, sessionId string) (KeyInfo, error) {
o := KeyInfo{}
b := []byte(sessionId)
if len(k) <= len(b) {
return o, fmt.Errorf("storage key missing")
}
o.SessionId = sessionId
o.Typ = uint8(k[0])
k = k[1:]
o.SessionId = string(k[:len(b)])
k = k[len(b):]
if o.Typ == db.DATATYPE_USERDATA {
if len(k) == 0 {
return o, fmt.Errorf("missing subtype key")
}
v := binary.BigEndian.Uint16(k[:2])
o.SubTyp = common.DataTyp(v)
o.Label = subTypToString(o.SubTyp)
k = k[2:]
} else {
o.Label = typToString(o.Typ)
}
if len(k) != 0 {
return o, fmt.Errorf("excess key information")
}
return o, nil
}
func FromKey(k []byte) (KeyInfo, error) {
o := KeyInfo{}
if len(k) < 4 {
return o, fmt.Errorf("insufficient key length")
}
sessionIdBytes := k[1:len(k)-2]
return ToKeyInfo(k, string(sessionIdBytes))
}
func subTypToString(v common.DataTyp) string {
return dbTypStr[v + db.DATATYPE_USERDATA + 1]
}
func typToString(v uint8) string {
return dbTypStr[common.DataTyp(uint16(v))]
}

View File

@ -1,44 +0,0 @@
// +build debugdb
package debug
import (
"git.defalsify.org/vise.git/db"
"git.grassecon.net/urdt/ussd/common"
)
func init() {
DebugCap |= 1
dbTypStr[db.DATATYPE_STATE] = "internal state"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TRACKING_ID] = "tracking id"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_PUBLIC_KEY] = "public key"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT_PIN] = "account pin"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_FIRST_NAME] = "first name"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_FAMILY_NAME] = "family name"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_YOB] = "year of birth"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_LOCATION] = "location"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_GENDER] = "gender"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_OFFERINGS] = "offerings"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_RECIPIENT] = "recipient"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_AMOUNT] = "amount"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TEMPORARY_VALUE] = "temporary value"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_SYM] = "active sym"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_BAL] = "active bal"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_BLOCKED_NUMBER] = "blocked number"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_PUBLIC_KEY_REVERSE] = "public_key_reverse"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_DECIMAL] = "active decimal"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_ADDRESS] = "active address"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_SYMBOLS] = "voucher symbols"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_BALANCES] = "voucher balances"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_DECIMALS] = "voucher decimals"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_ADDRESSES] = "voucher addresses"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_SENDERS] = "tx senders"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_RECIPIENTS] = "tx recipients"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_VALUES] = "tx values"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_ADDRESSES] = "tx addresses"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_HASHES] = "tx hashes"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_DATES] = "tx dates"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_SYMBOLS] = "tx symbols"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_DECIMALS] = "tx decimals"
}

View File

@ -1,78 +0,0 @@
package debug
import (
"testing"
"git.grassecon.net/urdt/ussd/common"
"git.defalsify.org/vise.git/db"
)
func TestDebugDbSubKeyInfo(t *testing.T) {
s := "foo"
b := []byte{0x20}
b = append(b, []byte(s)...)
b = append(b, []byte{0x00, 0x02}...)
r, err := ToKeyInfo(b, s)
if err != nil {
t.Fatal(err)
}
if r.SessionId != s {
t.Fatalf("expected %s, got %s", s, r.SessionId)
}
if r.Typ != 32 {
t.Fatalf("expected 64, got %d", r.Typ)
}
if r.SubTyp != 2 {
t.Fatalf("expected 2, got %d", r.SubTyp)
}
if DebugCap & 1 > 0 {
if r.Label != "tracking id" {
t.Fatalf("expected 'tracking id', got '%s'", r.Label)
}
}
}
func TestDebugDbKeyInfo(t *testing.T) {
s := "bar"
b := []byte{0x10}
b = append(b, []byte(s)...)
r, err := ToKeyInfo(b, s)
if err != nil {
t.Fatal(err)
}
if r.SessionId != s {
t.Fatalf("expected %s, got %s", s, r.SessionId)
}
if r.Typ != 16 {
t.Fatalf("expected 16, got %d", r.Typ)
}
if DebugCap & 1 > 0 {
if r.Label != "internal state" {
t.Fatalf("expected 'internal_state', got '%s'", r.Label)
}
}
}
func TestDebugDbKeyInfoRestore(t *testing.T) {
s := "bar"
b := []byte{db.DATATYPE_USERDATA}
b = append(b, []byte(s)...)
k := common.ToBytes(common.DATA_ACTIVE_SYM)
b = append(b, k...)
r, err := ToKeyInfo(b, s)
if err != nil {
t.Fatal(err)
}
if r.SessionId != s {
t.Fatalf("expected %s, got %s", s, r.SessionId)
}
if r.Typ != 32 {
t.Fatalf("expected 32, got %d", r.Typ)
}
if DebugCap & 1 > 0 {
if r.Label != "active sym" {
t.Fatalf("expected 'active sym', got '%s'", r.Label)
}
}
}

View File

@ -1,3 +0,0 @@
url: http://localhost:7123
dial: "*384*96#"
phoneNumber: +254722123456

View File

@ -1,21 +0,0 @@
services:
ussd-pg-store:
image: postgres:17-alpine
restart: unless-stopped
user: postgres
environment:
- POSTGRES_PASSWORD=postgres
- POSTGRES_USER=postgres
volumes:
- ./init_db.sql:/docker-entrypoint-initdb.d/init_db.sql
- ussd-pg:/var/lib/postgresql/data
ports:
- "127.0.0.1:5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 10s
timeout: 5s
retries: 5
volumes:
ussd-pg:
driver: local

View File

@ -1 +0,0 @@
CREATE DATABASE urdt_ussd;

View File

@ -1,7 +0,0 @@
{
"admins": [
{
"phonenumber" : "<replace with any admin number to test with >"
}
]
}

View File

@ -1,47 +0,0 @@
package commands
import (
"context"
"encoding/json"
"os"
"git.defalsify.org/vise.git/logging"
"git.grassecon.net/urdt/ussd/internal/utils"
)
var (
logg = logging.NewVanilla().WithDomain("adminstore")
)
type Admin struct {
PhoneNumber string `json:"phonenumber"`
}
type Config struct {
Admins []Admin `json:"admins"`
}
func Seed(ctx context.Context) error {
var config Config
adminstore, err := utils.NewAdminStore(ctx, "../admin_numbers")
store := adminstore.FsStore
if err != nil {
return err
}
defer store.Close()
data, err := os.ReadFile("admin_numbers.json")
if err != nil {
return err
}
if err := json.Unmarshal(data, &config); err != nil {
return err
}
for _, admin := range config.Admins {
err := store.Put(ctx, []byte(admin.PhoneNumber), []byte("1"))
if err != nil {
logg.Printf(logging.LVL_DEBUG, "Failed to insert admin number", admin.PhoneNumber)
return err
}
}
return nil
}

View File

@ -1,17 +0,0 @@
package main
import (
"context"
"log"
"git.grassecon.net/urdt/ussd/devtools/admin/commands"
)
func main() {
ctx := context.Background()
err := commands.Seed(ctx)
if err != nil {
log.Fatalf("Failed to initialize a list of admins with error %s", err)
}
}

View File

@ -1,126 +0,0 @@
// create language files from environment
package main
import (
"flag"
"fmt"
"os"
"path"
"strings"
"git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/lang"
"git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/initializers"
)
const (
changeHeadSrc = `LOAD reset_account_authorized 0
LOAD reset_incorrect 0
CATCH incorrect_pin flag_incorrect_pin 1
CATCH pin_entry flag_account_authorized 0
`
selectSrc = `LOAD set_language 6
RELOAD set_language
CATCH terms flag_account_created 0
MOVE language_changed
`
)
var (
logg = logging.NewVanilla()
mouts string
incmps string
)
func init() {
initializers.LoadEnvVariables()
}
func toLanguageLabel(ln lang.Language) string {
s := ln.Name
v := strings.Split(s, " (")
if len(v) > 1 {
s = v[0]
}
return s
}
func toLanguageKey(ln lang.Language) string {
s := toLanguageLabel(ln)
return strings.ToLower(s)
}
func main() {
var srcDir string
flag.StringVar(&srcDir, "o", ".", "resource dir write to")
flag.Parse()
logg.Infof("start command", "dir", srcDir)
err := config.LoadConfig()
if err != nil {
fmt.Fprintf(os.Stderr, "config load error: %v", err)
os.Exit(1)
}
logg.Tracef("using languages", "lang", config.Languages)
for i, v := range(config.Languages) {
ln, err := lang.LanguageFromCode(v)
if err != nil {
fmt.Fprintf(os.Stderr, "error parsing language: %s\n", v)
os.Exit(1)
}
n := i + 1
s := toLanguageKey(ln)
mouts += fmt.Sprintf("MOUT %s %v\n", s, n)
v = "set_" + ln.Code
incmps += fmt.Sprintf("INCMP %s %v\n", v, n)
p := path.Join(srcDir, v)
w, err := os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600)
if err != nil {
fmt.Fprintf(os.Stderr, "failed open language set template output: %v\n", err)
os.Exit(1)
}
s = toLanguageLabel(ln)
defer w.Close()
_, err = w.Write([]byte(s))
if err != nil {
fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err)
os.Exit(1)
}
}
src := mouts + "HALT\n" + incmps
src += "INCMP . *\n"
p := path.Join(srcDir, "select_language.vis")
w, err := os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600)
if err != nil {
fmt.Fprintf(os.Stderr, "failed open select language vis output: %v\n", err)
os.Exit(1)
}
defer w.Close()
_, err = w.Write([]byte(src))
if err != nil {
fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err)
os.Exit(1)
}
src = changeHeadSrc + src
p = path.Join(srcDir, "change_language.vis")
w, err = os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600)
if err != nil {
fmt.Fprintf(os.Stderr, "failed open select language vis output: %v\n", err)
os.Exit(1)
}
defer w.Close()
_, err = w.Write([]byte(src))
if err != nil {
fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err)
os.Exit(1)
}
}

View File

@ -1,100 +0,0 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"path"
"git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/debug"
"git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/logging"
)
var (
logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration")
)
func init() {
initializers.LoadEnvVariables()
}
func formatItem(k []byte, v []byte) (string, error) {
o, err := debug.FromKey(k)
if err != nil {
return "", err
}
s := fmt.Sprintf("%vValue: %v\n\n", o, string(v))
return s, nil
}
func main() {
config.LoadConfig()
var connStr string
var sessionId string
var database string
var engineDebug bool
var err error
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
flag.StringVar(&connStr, "c", ".state", "connection string")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.Parse()
if connStr != "" {
connStr = config.DbConn
}
connData, err := storage.ToConnData(config.DbConn)
if err != nil {
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
os.Exit(1)
}
logg.Infof("start command", "conn", connData)
ctx := context.Background()
ctx = context.WithValue(ctx, "SessionId", sessionId)
ctx = context.WithValue(ctx, "Database", database)
resourceDir := scriptDir
menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
store, err := menuStorageService.GetUserdataDb(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "get userdata db: %v\n", err.Error())
os.Exit(1)
}
store.SetPrefix(db.DATATYPE_USERDATA)
d, err := store.Dump(ctx, []byte(sessionId))
if err != nil {
fmt.Fprintf(os.Stderr, "store dump fail: %v\n", err.Error())
os.Exit(1)
}
for true {
k, v := d.Next(ctx)
if k == nil {
break
}
r, err := formatItem(k, v)
if err != nil {
fmt.Fprintf(os.Stderr, "format db item error: %v", err)
os.Exit(1)
}
fmt.Printf(r)
}
err = store.Close()
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
}

View File

@ -1,90 +0,0 @@
package main
import (
"context"
"crypto/sha1"
"flag"
"fmt"
"os"
"path"
"git.defalsify.org/vise.git/logging"
"git.grassecon.net/urdt/ussd/common"
"git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/internal/storage"
testdataloader "github.com/peteole/testdata-loader"
)
var (
logg = logging.NewVanilla()
baseDir = testdataloader.GetBasePath()
scriptDir = path.Join("services", "registration")
)
func init() {
initializers.LoadEnvVariables()
}
func main() {
config.LoadConfig()
var connStr string
var sessionId string
var database string
var engineDebug bool
var err error
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
flag.StringVar(&connStr, "c", "", "connection string")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.Parse()
if connStr != "" {
connStr = config.DbConn
}
connData, err := storage.ToConnData(config.DbConn)
if err != nil {
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
os.Exit(1)
}
logg.Infof("start command", "conn", connData)
ctx := context.Background()
ctx = context.WithValue(ctx, "SessionId", sessionId)
ctx = context.WithValue(ctx, "Database", database)
resourceDir := scriptDir
menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
store, err := menuStorageService.GetUserdataDb(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
userStore := common.UserDataStore{store}
h := sha1.New()
h.Write([]byte(sessionId))
address := h.Sum(nil)
addressString := fmt.Sprintf("%x", address)
err = userStore.WriteEntry(ctx, sessionId, common.DATA_PUBLIC_KEY, []byte(addressString))
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
err = userStore.WriteEntry(ctx, addressString, common.DATA_PUBLIC_KEY_REVERSE, []byte(sessionId))
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
err = store.Close()
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
}

13
entry/handlers.go Normal file
View 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)
}

View File

@ -1,4 +1,4 @@
package initializers
package env
import (
"log"

15
errors/errors.go Normal file
View File

@ -0,0 +1,15 @@
package errors
import (
"errors"
)
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")
)

24
go.mod
View File

@ -1,39 +1,27 @@
module git.grassecon.net/urdt/ussd
module git.grassecon.net/grassrootseconomics/visedriver
go 1.23.0
require (
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/grassrootseconomics/ussd-data-service v1.2.0-beta
git.defalsify.org/vise.git v0.2.3-0.20250114225117-3b5fc85b650b
github.com/jackc/pgx/v5 v5.7.1
github.com/joho/godotenv v1.5.1
github.com/peteole/testdata-loader v0.3.0
github.com/stretchr/testify v1.9.0
golang.org/x/crypto v0.27.0
gopkg.in/leonelquinteros/gotext.v1 v1.3.1
)
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/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/pgx/v5 v5.7.1 // 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 // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/text v0.18.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gopkg.in/leonelquinteros/gotext.v1 v1.3.1 // indirect
)

29
go.sum
View File

@ -1,29 +1,14 @@
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=
git.defalsify.org/vise.git v0.2.3-0.20250114225117-3b5fc85b650b h1:rwWXMtNSn7aqhb4p1oVZkCA1vC7pVdohwW61QQM8fUs=
git.defalsify.org/vise.git v0.2.3-0.20250114225117-3b5fc85b650b/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c h1:H9Nm+I7Cg/YVPpEV1RzU3Wq2pjamPc/UtHDgItcb7lE=
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c/go.mod h1:rGod7o6KPeJ+hyBpHfhi4v7blx9sf+QsHsA7KAsdN6U=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/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 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=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@ -34,10 +19,6 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a h1:0Q3H0YXzMHiciXtRcM+j0jiCe8WKPQHoRgQiRTnfcLY=
github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a/go.mod h1:CdTTBOYzS5E4mWS1N8NWP6AHI19MP0A2B18n3hLzRMk=
github.com/pashagolub/pgxmock/v4 v4.3.0 h1:DqT7fk0OCK6H0GvqtcMsLpv8cIwWqdxWgfZNLeHCb/s=
@ -47,11 +28,7 @@ github.com/peteole/testdata-loader v0.3.0/go.mod h1:Mt0ZbRtb56u8SLJpNP+BnQbENljM
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
@ -65,8 +42,6 @@ golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/leonelquinteros/gotext.v1 v1.3.1 h1:8d9/fdTG0kn/B7NNGV1BsEyvektXFAbkMsTZS2sFSCc=
gopkg.in/leonelquinteros/gotext.v1 v1.3.1/go.mod h1:X1WlGDeAFIYsW6GjgMm4VwUwZ2XjI7Zan2InxSUQWrU=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,34 +0,0 @@
package args
import (
"strings"
"git.defalsify.org/vise.git/lang"
)
type LangVar struct {
v []lang.Language
}
func(lv *LangVar) Set(s string) error {
v, err := lang.LanguageFromCode(s)
if err != nil {
return err
}
lv.v = append(lv.v, v)
return err
}
func(lv *LangVar) String() string {
var s []string
for _, v := range(lv.v) {
s = append(s, v.Code)
}
return strings.Join(s, ",")
}
func(lv *LangVar) Langs() []lang.Language {
return lv.v
}

View File

@ -1,141 +0,0 @@
package handlers
import (
"context"
"strings"
"git.defalsify.org/vise.git/asm"
"git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/internal/handlers/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) {
replaceSeparatorFunc := func(input string) string {
return strings.ReplaceAll(input, ":", ls.Cfg.MenuSeparator)
}
ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore, ls.AdminStore, accountService, replaceSeparatorFunc)
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.CheckBlockedNumPinMisMatch)
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)
ls.DbRs.AddLocalFunc("update_all_profile_items", ussdHandlers.UpdateAllProfileItems)
ls.DbRs.AddLocalFunc("set_back", ussdHandlers.SetBack)
ls.DbRs.AddLocalFunc("show_blocked_account", ussdHandlers.ShowBlockedAccount)
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
}

View File

@ -1,54 +0,0 @@
package handlers
import (
"context"
"errors"
"io"
"git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/internal/storage"
)
var (
logg = logging.NewVanilla().WithDomain("handlers")
)
var (
ErrInvalidRequest = errors.New("invalid request for context")
ErrSessionMissing = errors.New("missing session")
ErrInvalidInput = errors.New("invalid input")
ErrStorage = errors.New("storage retrieval fail")
ErrEngineType = errors.New("incompatible engine")
ErrEngineInit = errors.New("engine init fail")
ErrEngineExec = errors.New("engine exec fail")
)
type RequestSession struct {
Ctx context.Context
Config engine.Config
Engine engine.Engine
Input []byte
Storage *storage.Storage
Writer io.Writer
Continue bool
}
// TODO: seems like can remove this.
type RequestParser interface {
GetSessionId(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
Process(rs RequestSession) (RequestSession, error)
Output(rs RequestSession) (RequestSession, error)
Reset(rs RequestSession) (RequestSession, error)
Shutdown()
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,120 +0,0 @@
package at
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"git.grassecon.net/urdt/ussd/common"
"git.grassecon.net/urdt/ussd/internal/handlers"
)
type ATRequestParser struct {
}
func (arp *ATRequestParser) GetSessionId(ctx context.Context, rq any) (string, error) {
rqv, ok := rq.(*http.Request)
if !ok {
logg.Warnf("got an invalid request", "req", rq)
return "", handlers.ErrInvalidRequest
}
// Capture body (if any) for logging
body, err := io.ReadAll(rqv.Body)
if err != nil {
logg.Warnf("failed to read request body", "err", err)
return "", fmt.Errorf("failed to read request body: %v", err)
}
// Reset the body for further reading
rqv.Body = io.NopCloser(bytes.NewReader(body))
// Log the body as JSON
bodyLog := map[string]string{"body": string(body)}
logBytes, err := json.Marshal(bodyLog)
if err != nil {
logg.Warnf("failed to marshal request body", "err", err)
} else {
decodedStr := string(logBytes)
sessionId, err := extractATSessionId(decodedStr)
if err != nil {
ctx = context.WithValue(ctx, "AT-SessionId", sessionId)
}
logg.DebugCtxf(ctx, "Received request:", decodedStr)
}
if err := rqv.ParseForm(); err != nil {
logg.Warnf("failed to parse form data", "err", err)
return "", fmt.Errorf("failed to parse form data: %v", err)
}
phoneNumber := rqv.FormValue("phoneNumber")
if phoneNumber == "" {
return "", fmt.Errorf("no phone number found")
}
formattedNumber, err := common.FormatPhoneNumber(phoneNumber)
if err != nil {
logg.Warnf("failed to format phone number", "err", err)
return "", fmt.Errorf("failed to format number")
}
return formattedNumber, nil
}
func (arp *ATRequestParser) GetInput(rq any) ([]byte, error) {
rqv, ok := rq.(*http.Request)
if !ok {
return nil, handlers.ErrInvalidRequest
}
if err := rqv.ParseForm(); err != nil {
return nil, fmt.Errorf("failed to parse form data: %v", err)
}
text := rqv.FormValue("text")
parts := strings.Split(text, "*")
if len(parts) == 0 {
return nil, fmt.Errorf("no input found")
}
trimmedInput := strings.TrimSpace(parts[len(parts)-1])
return []byte(trimmedInput), nil
}
func parseQueryParams(query string) map[string]string {
params := make(map[string]string)
queryParams := strings.Split(query, "&")
for _, param := range queryParams {
// Split each key-value pair by '='
parts := strings.SplitN(param, "=", 2)
if len(parts) == 2 {
params[parts[0]] = parts[1]
}
}
return params
}
func extractATSessionId(decodedStr string) (string, error) {
var data map[string]string
err := json.Unmarshal([]byte(decodedStr), &data)
if err != nil {
logg.Errorf("Error unmarshalling JSON: %v", err)
return "", nil
}
decodedBody, err := url.QueryUnescape(data["body"])
if err != nil {
logg.Errorf("Error URL-decoding body: %v", err)
return "", nil
}
params := parseQueryParams(decodedBody)
sessionId := params["sessionId"]
return sessionId, nil
}

View File

@ -1,98 +0,0 @@
package at
import (
"io"
"net/http"
"git.defalsify.org/vise.git/logging"
"git.grassecon.net/urdt/ussd/internal/handlers"
httpserver "git.grassecon.net/urdt/ussd/internal/http"
)
var (
logg = logging.NewVanilla().WithDomain("atserver").WithContextKey("SessionId").WithContextKey("AT-SessionId")
)
type ATSessionHandler struct {
*httpserver.SessionHandler
}
func NewATSessionHandler(h handlers.RequestHandler) *ATSessionHandler {
return &ATSessionHandler{
SessionHandler: httpserver.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.Context(), 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
}

View File

@ -1,234 +0,0 @@
package at
import (
"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"
)
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)
}
})
}
}

View File

@ -1,91 +0,0 @@
package http
import (
"net/http"
"strconv"
"git.defalsify.org/vise.git/logging"
"git.grassecon.net/urdt/ussd/internal/handlers"
)
var (
logg = logging.NewVanilla().WithDomain("httpserver")
)
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(s))
if err != nil {
logg.Errorf("error writing error!!", "err", err, "olderr", s)
w.WriteHeader(500)
}
}
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.Context(), 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
}
}

View File

@ -1,65 +0,0 @@
package ssh
import (
"context"
"fmt"
"os"
"path"
"golang.org/x/crypto/ssh"
"git.defalsify.org/vise.git/db"
"git.grassecon.net/urdt/ussd/internal/storage"
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db/gdbm"
)
type SshKeyStore struct {
store db.Db
}
func NewSshKeyStore(ctx context.Context, dbDir string) (*SshKeyStore, error) {
keyStore := &SshKeyStore{}
keyStoreFile := path.Join(dbDir, "ssh_authorized_keys.gdbm")
keyStore.store = dbstorage.NewThreadGdbmDb()
err := keyStore.store.Connect(ctx, keyStoreFile)
if err != nil {
return nil, err
}
return keyStore, nil
}
func(s *SshKeyStore) AddFromFile(ctx context.Context, fp string, sessionId string) error {
_, err := os.Stat(fp)
if err != nil {
return fmt.Errorf("cannot open ssh server public key file: %v\n", err)
}
publicBytes, err := os.ReadFile(fp)
if err != nil {
return fmt.Errorf("Failed to load public key: %v", err)
}
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(publicBytes)
if err != nil {
return fmt.Errorf("Failed to parse public key: %v", err)
}
k := append([]byte{0x01}, pubKey.Marshal()...)
s.store.SetPrefix(storage.DATATYPE_EXTEND)
logg.Infof("Added key", "sessionId", sessionId, "public key", string(publicBytes))
return s.store.Put(ctx, k, []byte(sessionId))
}
func(s *SshKeyStore) Get(ctx context.Context, pubKey ssh.PublicKey) (string, error) {
s.store.SetLanguage(nil)
s.store.SetPrefix(storage.DATATYPE_EXTEND)
k := append([]byte{0x01}, pubKey.Marshal()...)
v, err := s.store.Get(ctx, k)
if err != nil {
return "", err
}
return string(v), nil
}
func(s *SshKeyStore) Close() error {
return s.store.Close()
}

View File

@ -1,284 +0,0 @@
package ssh
import (
"context"
"encoding/hex"
"encoding/base64"
"errors"
"fmt"
"net"
"os"
"sync"
"golang.org/x/crypto/ssh"
"git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/resource"
"git.defalsify.org/vise.git/state"
"git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/remote"
)
var (
logg = logging.NewVanilla().WithDomain("ssh")
)
type auther struct {
Ctx context.Context
keyStore *SshKeyStore
auth map[string]string
}
func NewAuther(ctx context.Context, keyStore *SshKeyStore) *auther {
return &auther{
Ctx: ctx,
keyStore: keyStore,
auth: make(map[string]string),
}
}
func(a *auther) Check(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
logg.TraceCtxf(a.Ctx, "looking for publickey", "pubkey", fmt.Sprintf("%x", pubKey))
va, err := a.keyStore.Get(a.Ctx, pubKey)
if err != nil {
return nil, err
}
ka := hex.EncodeToString(conn.SessionID())
a.auth[ka] = va
fmt.Fprintf(os.Stderr, "connect: %s -> %s\n", ka, va)
return nil, nil
}
func(a *auther) FromConn(c *ssh.ServerConn) (string, error) {
if c == nil {
return "", errors.New("nil server conn")
}
if c.Conn == nil {
return "", errors.New("nil underlying conn")
}
return a.Get(c.Conn.SessionID())
}
func(a *auther) Get(k []byte) (string, error) {
ka := hex.EncodeToString(k)
v, ok := a.auth[ka]
if !ok {
return "", errors.New("not found")
}
return v, nil
}
type SshRunner struct {
Ctx context.Context
Cfg engine.Config
FlagFile string
Conn storage.ConnData
ResourceDir string
Debug bool
SrvKeyFile string
Host string
Port uint
wg sync.WaitGroup
lst net.Listener
}
func(s *SshRunner) serve(ctx context.Context, sessionId string, ch ssh.NewChannel, en engine.Engine) error {
if ch == nil {
return errors.New("nil channel")
}
if ch.ChannelType() != "session" {
ch.Reject(ssh.UnknownChannelType, "that is not the channel you are looking for")
return errors.New("not a session")
}
channel, requests, err := ch.Accept()
if err != nil {
panic(err)
}
defer channel.Close()
s.wg.Add(1)
go func(reqIn <-chan *ssh.Request) {
defer s.wg.Done()
for req := range reqIn {
req.Reply(req.Type == "shell", nil)
}
_ = requests
}(requests)
cont, err := en.Exec(ctx, []byte{})
if err != nil {
return fmt.Errorf("initial engine exec err: %v", err)
}
var input [state.INPUT_LIMIT]byte
for cont {
c, err := en.Flush(ctx, channel)
if err != nil {
return fmt.Errorf("flush err: %v", err)
}
_, err = channel.Write([]byte{0x0a})
if err != nil {
return fmt.Errorf("newline err: %v", err)
}
c, err = channel.Read(input[:])
if err != nil {
return fmt.Errorf("read input fail: %v", err)
}
logg.TraceCtxf(ctx, "input read", "c", c, "input", input[:c-1])
cont, err = en.Exec(ctx, input[:c-1])
if err != nil {
return fmt.Errorf("engine exec err: %v", err)
}
logg.TraceCtxf(ctx, "exec cont", "cont", cont, "en", en)
_ = c
}
c, err := en.Flush(ctx, channel)
if err != nil {
return fmt.Errorf("last flush err: %v", err)
}
_ = c
return nil
}
func(s *SshRunner) Stop() error {
return s.lst.Close()
}
func(s *SshRunner) GetEngine(sessionId string) (engine.Engine, func(), error) {
ctx := s.Ctx
menuStorageService := storage.NewMenuStorageService(s.Conn, s.ResourceDir)
rs, err := menuStorageService.GetResource(ctx)
if err != nil {
return nil, nil, err
}
pe, err := menuStorageService.GetPersister(ctx)
if err != nil {
return nil, nil, err
}
userdatastore, err := menuStorageService.GetUserdataDb(ctx)
if err != nil {
return nil, nil, err
}
dbResource, ok := rs.(*resource.DbResource)
if !ok {
return nil, nil, err
}
lhs, err := handlers.NewLocalHandlerService(ctx, s.FlagFile, true, dbResource, s.Cfg, rs)
lhs.SetDataStore(&userdatastore)
lhs.SetPersister(pe)
lhs.Cfg.SessionId = sessionId
if err != nil {
return nil, nil, err
}
// TODO: clear up why pointer here and by-value other cmds
accountService := &remote.AccountService{}
hl, err := lhs.GetHandler(accountService)
if err != nil {
return nil, nil, err
}
en := lhs.GetEngine()
en = en.WithFirst(hl.Init)
if s.Debug {
en = en.WithDebug(nil)
}
// TODO: this is getting very hacky!
closer := func() {
err := menuStorageService.Close()
if err != nil {
logg.ErrorCtxf(ctx, "menu storage service cleanup fail", "err", err)
}
}
return en, closer, nil
}
// adapted example from crypto/ssh package, NewServerConn doc
func(s *SshRunner) Run(ctx context.Context, keyStore *SshKeyStore) {
s.Ctx = ctx
running := true
// TODO: waitgroup should probably not be global
defer s.wg.Wait()
auth := NewAuther(ctx, keyStore)
cfg := ssh.ServerConfig{
PublicKeyCallback: auth.Check,
}
privateBytes, err := os.ReadFile(s.SrvKeyFile)
if err != nil {
logg.ErrorCtxf(ctx, "Failed to load private key", "err", err)
}
private, err := ssh.ParsePrivateKey(privateBytes)
if err != nil {
logg.ErrorCtxf(ctx, "Failed to parse private key", "err", err)
}
srvPub := private.PublicKey()
srvPubStr := base64.StdEncoding.EncodeToString(srvPub.Marshal())
logg.InfoCtxf(ctx, "have server key", "type", srvPub.Type(), "public", srvPubStr)
cfg.AddHostKey(private)
s.lst, err = net.Listen("tcp", fmt.Sprintf("%s:%d", s.Host, s.Port))
if err != nil {
panic(err)
}
for running {
conn, err := s.lst.Accept()
if err != nil {
logg.ErrorCtxf(ctx, "ssh accept error", "err", err)
running = false
continue
}
go func(conn net.Conn) {
defer conn.Close()
for true {
srvConn, nC, rC, err := ssh.NewServerConn(conn, &cfg)
if err != nil {
logg.InfoCtxf(ctx, "rejected client", "err", err)
return
}
logg.DebugCtxf(ctx, "ssh client connected", "conn", srvConn)
s.wg.Add(1)
go func() {
ssh.DiscardRequests(rC)
s.wg.Done()
}()
sessionId, err := auth.FromConn(srvConn)
if err != nil {
logg.ErrorCtxf(ctx, "Cannot find authentication")
return
}
en, closer, err := s.GetEngine(sessionId)
if err != nil {
logg.ErrorCtxf(ctx, "engine won't start", "err", err)
return
}
defer func() {
err := en.Finish()
if err != nil {
logg.ErrorCtxf(ctx, "engine won't stop", "err", err)
}
closer()
}()
for ch := range nC {
err = s.serve(ctx, sessionId, ch, en)
logg.ErrorCtxf(ctx, "ssh server finish", "err", err)
}
}
}(conn)
}
}

View File

@ -1,43 +0,0 @@
package storage
import (
"context"
"git.defalsify.org/vise.git/db"
)
// PrefixDb interface abstracts the database operations.
type PrefixDb interface {
Get(ctx context.Context, key []byte) ([]byte, error)
Put(ctx context.Context, key []byte, val []byte) error
}
var _ PrefixDb = (*SubPrefixDb)(nil)
type SubPrefixDb struct {
store db.Db
pfx []byte
}
func NewSubPrefixDb(store db.Db, pfx []byte) *SubPrefixDb {
return &SubPrefixDb{
store: store,
pfx: pfx,
}
}
func (s *SubPrefixDb) toKey(k []byte) []byte {
return append(s.pfx, k...)
}
func (s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) {
s.store.SetPrefix(db.DATATYPE_USERDATA)
key = s.toKey(key)
return s.store.Get(ctx, key)
}
func (s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error {
s.store.SetPrefix(db.DATATYPE_USERDATA)
key = s.toKey(key)
return s.store.Put(ctx, key, val)
}

View File

@ -1,54 +0,0 @@
package storage
import (
"bytes"
"context"
"testing"
memdb "git.defalsify.org/vise.git/db/mem"
)
func TestSubPrefix(t *testing.T) {
ctx := context.Background()
db := memdb.NewMemDb()
err := db.Connect(ctx, "")
if err != nil {
t.Fatal(err)
}
sdba := NewSubPrefixDb(db, []byte("tinkywinky"))
err = sdba.Put(ctx, []byte("foo"), []byte("dipsy"))
if err != nil {
t.Fatal(err)
}
r, err := sdba.Get(ctx, []byte("foo"))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(r, []byte("dipsy")) {
t.Fatalf("expected 'dipsy', got %s", r)
}
sdbb := NewSubPrefixDb(db, []byte("lala"))
r, err = sdbb.Get(ctx, []byte("foo"))
if err == nil {
t.Fatal("expected not found")
}
err = sdbb.Put(ctx, []byte("foo"), []byte("pu"))
if err != nil {
t.Fatal(err)
}
r, err = sdbb.Get(ctx, []byte("foo"))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(r, []byte("pu")) {
t.Fatalf("expected 'pu', got %s", r)
}
r, err = sdba.Get(ctx, []byte("foo"))
if !bytes.Equal(r, []byte("dipsy")) {
t.Fatalf("expected 'dipsy', got %s", r)
}
}

View File

@ -1,140 +0,0 @@
package testutil
import (
"context"
"fmt"
"os"
"path"
"path/filepath"
"time"
"git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/internal/testutil/testservice"
"git.grassecon.net/urdt/ussd/internal/testutil/testtag"
"git.grassecon.net/urdt/ussd/remote"
testdataloader "github.com/peteole/testdata-loader"
)
var (
logg = logging.NewVanilla()
baseDir = testdataloader.GetBasePath()
scriptDir = path.Join(baseDir, "services", "registration")
selectedDatabase = ""
selectedDbSchema = ""
)
func init() {
initializers.LoadEnvVariablesPath(baseDir)
}
// SetDatabase updates the database used by TestEngine
func SetDatabase(dbType string, dbSchema string) {
selectedDatabase = dbType
selectedDbSchema = dbSchema
}
func TestEngine(sessionId string) (engine.Engine, func(), chan bool) {
ctx := context.Background()
ctx = context.WithValue(ctx, "SessionId", sessionId)
pfp := path.Join(scriptDir, "pp.csv")
var eventChannel = make(chan bool)
cfg := engine.Config{
Root: "root",
SessionId: sessionId,
OutputSize: uint32(160),
FlagCount: uint32(128),
}
connStr, err := filepath.Abs(".test_state/state.gdbm")
if err != nil {
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
os.Exit(1)
}
conn, err := storage.ToConnData(connStr)
if err != nil {
fmt.Fprintf(os.Stderr, "connstr parse err: %v", err)
os.Exit(1)
}
resourceDir := scriptDir
menuStorageService := storage.NewMenuStorageService(conn, resourceDir)
rs, err := menuStorageService.GetResource(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "resource error: %v", err)
os.Exit(1)
}
pe, err := menuStorageService.GetPersister(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "persister error: %v", err)
os.Exit(1)
}
userDataStore, err := menuStorageService.GetUserdataDb(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "userdb error: %v", err)
os.Exit(1)
}
dbResource, ok := rs.(*resource.DbResource)
if !ok {
fmt.Fprintf(os.Stderr, "dbresource cast error")
os.Exit(1)
}
lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs)
lhs.SetDataStore(&userDataStore)
lhs.SetPersister(pe)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
if testtag.AccountService == nil {
testtag.AccountService = &remote.AccountService{}
}
switch testtag.AccountService.(type) {
case *testservice.TestAccountService:
go func() {
eventChannel <- false
}()
case *remote.AccountService:
go func() {
time.Sleep(5 * time.Second) // Wait for 5 seconds
eventChannel <- true
}()
default:
panic("Unknown account service type")
}
hl, err := lhs.GetHandler(testtag.AccountService)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
en := lhs.GetEngine()
en = en.WithFirst(hl.Init)
cleanFn := func() {
err := en.Finish()
if err != nil {
logg.Errorf(err.Error())
}
err = menuStorageService.Close()
if err != nil {
logg.Errorf(err.Error())
}
logg.Infof("testengine storage closed")
}
return en, cleanFn, eventChannel
}

View File

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

View File

@ -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()
}

View File

@ -1,54 +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)
}
func (m *MockAccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) {
args := m.Called(alias)
return args.Get(0).(*dataserviceapi.AliasAddress), args.Error(1)
}

View File

@ -1,62 +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
}
func (m TestAccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) {
return &dataserviceapi.AliasAddress{}, nil
}

View File

@ -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{}
)

View File

@ -1,9 +0,0 @@
// +build online
package testtag
import "git.grassecon.net/urdt/ussd/remote"
var (
AccountService remote.AccountServiceInterface
)

View File

@ -1,51 +0,0 @@
package utils
import (
"context"
"git.defalsify.org/vise.git/db"
fsdb "git.defalsify.org/vise.git/db/fs"
"git.defalsify.org/vise.git/logging"
)
var (
logg = logging.NewVanilla().WithDomain("adminstore")
)
type AdminStore struct {
ctx context.Context
FsStore db.Db
}
func NewAdminStore(ctx context.Context, fileName string) (*AdminStore, error) {
fsStore, err := getFsStore(ctx, fileName)
if err != nil {
return nil, err
}
return &AdminStore{ctx: ctx, FsStore: fsStore}, nil
}
func getFsStore(ctx context.Context, connectStr string) (db.Db, error) {
fsStore := fsdb.NewFsDb()
err := fsStore.Connect(ctx, connectStr)
fsStore.SetPrefix(db.DATATYPE_USERDATA)
if err != nil {
return nil, err
}
return fsStore, nil
}
// Checks if the given sessionId is listed as an admin.
func (as *AdminStore) IsAdmin(sessionId string) (bool, error) {
_, err := as.FsStore.Get(as.ctx, []byte(sessionId))
if err != nil {
if db.IsNotFound(err) {
logg.Printf(logging.LVL_INFO, "Returning false because session id was not found")
return false, nil
} else {
return false, err
}
}
return true, nil
}

View File

@ -1,56 +0,0 @@
package utils
import (
"strconv"
"time"
)
// CalculateAge calculates the age based on a given birthdate and the current date in the format dd/mm/yy
// It adjusts for cases where the current date is before the birthday in the current year.
func CalculateAge(birthdate, today time.Time) int {
today = today.In(birthdate.Location())
ty, tm, td := today.Date()
today = time.Date(ty, tm, td, 0, 0, 0, 0, time.UTC)
by, bm, bd := birthdate.Date()
birthdate = time.Date(by, bm, bd, 0, 0, 0, 0, time.UTC)
if today.Before(birthdate) {
return 0
}
age := ty - by
anniversary := birthdate.AddDate(age, 0, 0)
if anniversary.After(today) {
age--
}
return age
}
// CalculateAgeWithYOB calculates the age based on the given year of birth (YOB).
// It subtracts the YOB from the current year to determine the age.
//
// Parameters:
//
// yob: The year of birth as an integer.
//
// Returns:
//
// The calculated age as an integer.
func CalculateAgeWithYOB(yob int) int {
currentYear := time.Now().Year()
return currentYear - yob
}
//IsValidYob checks if the provided yob can be considered valid
func IsValidYOb(yob string) bool {
currentYear := time.Now().Year()
yearOfBirth, err := strconv.ParseInt(yob, 10, 64)
if err != nil {
return false
}
if yearOfBirth >= 1900 && int(yearOfBirth) <= currentYear {
return true
} else {
return false
}
}

View File

@ -1,11 +0,0 @@
package utils
var isoCodes = map[string]bool{
"eng": true, // English
"swa": true, // Swahili
"default": true, // Default language: English
}
func IsValidISO639(code string) bool {
return isoCodes[code]
}

View File

@ -1,17 +0,0 @@
package utils
func ConstructName(firstName, familyName, defaultValue string) string {
name := defaultValue
if familyName != defaultValue {
if firstName != defaultValue {
name = firstName + " " + familyName
} else {
name = familyName
}
} else {
if firstName != defaultValue {
name = firstName
}
}
return name
}

View File

@ -1,443 +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. You have: 2 remaining attempt(s).\n1:Retry\n9:Quit"
},
{
"input": "1",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Select language:\n1:English\n2:Kiswahili"
},
{
"input": "1",
"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. You have: 2 remaining attempt(s).\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. You have: 2 remaining attempt(s).\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_all_account_details_starting_from_firstname",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "1",
"expectedContent": "Enter your first names:\n0:Back"
},
{
"input": "foo",
"expectedContent": "Enter family name:\n0:Back"
},
{
"input": "bar",
"expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back"
},
{
"input": "1",
"expectedContent": "Enter your year of birth\n0:Back"
},
{
"input": "1940",
"expectedContent": "Enter your location:\n0:Back"
},
{
"input": "Kilifi",
"expectedContent": "Enter the services or goods you offer: \n0:Back"
},
{
"input": "Bananas",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
{
"name": "menu_my_account_edit_familyname_when_all_account__details_have_been_set",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "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": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
{
"name": "menu_my_account_edit_gender_when_all_account__details_have_been_set",
"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": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
{
"name": "menu_my_account_edit_yob_when_all_account__details_have_been_set",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "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": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
{
"name": "menu_my_account_edit_location_when_all_account_details_have_been_set",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "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": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
{
"name": "menu_my_account_edit_offerings_when_all_account__details_have_been_set",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "6",
"expectedContent": "Enter the services or goods you offer: \n0:Back"
},
{
"input": "Bananas",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
{
"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: 80\nLocation: Kilifi\nYou provide: Bananas\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}
]
}

View File

@ -1,419 +0,0 @@
package menutraversaltest
import (
"bytes"
"context"
"flag"
"fmt"
"log"
"math/rand"
"os"
"path/filepath"
"regexp"
"testing"
"git.grassecon.net/urdt/ussd/internal/testutil"
"git.grassecon.net/urdt/ussd/internal/testutil/driver"
"github.com/gofrs/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
var (
testData = driver.ReadData()
sessionID string
src = rand.NewSource(42)
g = rand.New(src)
)
var groupTestFile = flag.String("test-file", "group_test.json", "The test file to use for running the group tests")
var database = flag.String("db", "gdbm", "Specify the database (gdbm or postgres)")
var dbSchema = flag.String("schema", "test", "Specify the database schema (default test)")
func testStore() string {
v, _ := filepath.Abs(".test_state/state.gdbm")
return v
}
func GenerateSessionId() string {
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) {
// Parse the flags
flag.Parse()
sessionID = GenerateSessionId()
defer func() {
if err := os.RemoveAll(testStore()); err != nil {
log.Fatalf("Failed to delete state store %s: %v", testStore(), err)
}
}()
// Set the selected database
testutil.SetDatabase(*database, *dbSchema)
// Cleanup the schema table after tests
defer func() {
if *database == "postgres" {
ctx := context.Background()
connStr := "postgres://" //storage.BuildConnStr()
dbConn, err := pgxpool.New(ctx, connStr)
if err != nil {
log.Fatalf("Failed to connect to database for cleanup: %v", err)
}
defer dbConn.Close()
query := fmt.Sprintf("DELETE FROM %s.kv_vise;", *dbSchema)
_, execErr := dbConn.Exec(ctx, query)
if execErr != nil {
log.Printf("Failed to cleanup table %s.kv_vise: %v", *dbSchema, execErr)
} else {
log.Printf("Successfully cleaned up table %s.kv_vise", *dbSchema)
}
}
}()
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_invite")
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.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)
}
})
}
}

View File

@ -1,68 +0,0 @@
{
"groups": [
{
"name": "menu_my_account_edit_all_account_details_starting_from_family_name",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "2",
"expectedContent": "Enter family name:\n0:Back"
},
{
"input": "bar",
"expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back"
},
{
"input": "1",
"expectedContent": "Enter your year of birth\n0:Back"
},
{
"input": "1940",
"expectedContent": "Enter your location:\n0:Back"
},
{
"input": "Kilifi",
"expectedContent": "Enter the services or goods you offer: \n0:Back"
},
{
"input": "Bananas",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}
]
}

View File

@ -1,61 +0,0 @@
{
"groups": [
{
"name": "menu_my_account_edit_all_account_details_starting_from_firstname",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "1",
"expectedContent": "Enter your first names:\n0:Back"
},
{
"input": "foo",
"expectedContent": "Enter family name:\n0:Back"
},
{
"input": "bar",
"expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back"
},
{
"input": "1",
"expectedContent": "Enter your year of birth\n0:Back"
},
{
"input": "1940",
"expectedContent": "Enter your location:\n0:Back"
},
{
"input": "Kilifi",
"expectedContent": "Enter the services or goods you offer: \n0:Back"
},
{
"input": "Bananas",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}
]
}

View File

@ -1,55 +0,0 @@
{
"groups": [
{
"name": "menu_my_account_edit_all_account_details_starting_from_gender",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "3",
"expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back"
},
{
"input": "1",
"expectedContent": "Enter your year of birth\n0:Back"
},
{
"input": "1940",
"expectedContent": "Enter your location:\n0:Back"
},
{
"input": "Kilifi",
"expectedContent": "Enter the services or goods you offer: \n0:Back"
},
{
"input": "Bananas",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}
]
}

View File

@ -1,46 +0,0 @@
{
"groups": [
{
"name": "menu_my_account_edit_all_account_details_starting_from_location",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "5",
"expectedContent": "Enter your location:\n0:Back"
},
{
"input": "Kilifi",
"expectedContent": "Enter the services or goods you offer: \n0:Back"
},
{
"input": "Bananas",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}
]
}

View File

@ -1,42 +0,0 @@
{
"groups": [
{
"name": "menu_my_account_edit_all_account_details_starting_from_offerings",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "6",
"expectedContent": "Enter the services or goods you offer: \n0:Back"
},
{
"input": "Bananas",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}
]
}

View File

@ -1,50 +0,0 @@
{
"groups": [
{
"name": "menu_my_account_edit_all_account_details_starting_from_yob",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "4",
"expectedContent": "Enter your year of birth\n0:Back"
},
{
"input": "1940",
"expectedContent": "Enter your location:\n0:Back"
},
{
"input": "Kilifi",
"expectedContent": "Enter the services or goods you offer: \n0:Back"
},
{
"input": "Bananas",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}
]
}

View File

@ -1,70 +0,0 @@
{
"groups": [
{
"name": "menu_my_account_edit_familyname_when_adjacent_profile_information_set",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "3",
"expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back"
},
{
"input": "1",
"expectedContent": "Enter your year of birth\n0:Back"
},
{
"input": "1940",
"expectedContent": "Enter your location:\n0:Back"
},
{
"input": "Kilifi",
"expectedContent": "Enter the services or goods you offer: \n0:Back"
},
{
"input": "Bananas",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "2",
"expectedContent": "Enter family name:\n0:Back"
},
{
"input": "foo2",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}
]
}

View File

@ -1,133 +0,0 @@
[
{
"name": "session one",
"groups": [
{
"name": "account_creation_successful",
"steps": [
{
"input": "",
"expectedContent": "Welcome to Sarafu Network\nPlease select a language\n1:English\n2:Kiswahili"
},
{
"input": "1",
"expectedContent": "Do you agree to terms and conditions?\nhttps://grassecon.org/pages/terms-and-conditions\n\n1:Yes\n2:No"
},
{
"input": "1",
"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\n1:English\n2:Kiswahili"
},
{
"input": "1",
"expectedContent": "Do you agree to terms and conditions?\nhttps://grassecon.org/pages/terms-and-conditions\n\n1:Yes\n2:No"
},
{
"input": "2",
"expectedContent": "Thank you for using Sarafu. Goodbye!"
}
]
},
{
"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/address/alias:\n0:Back"
},
{
"input": "0@0",
"expectedContent": "0@0 is invalid, please try again:\n1:Retry\n9:Quit"
},
{
"input": "1",
"expectedContent": "Enter recipient's phone number/address/alias:\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!"
}
]
}
]
}
]

View File

@ -1,6 +0,0 @@
package models
type AccountResult struct {
PublicKey string `json:"publicKey"`
TrackingId string `json:"trackingId"`
}

View File

@ -1,8 +0,0 @@
package models
import "encoding/json"
type BalanceResult struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}

View File

@ -1,18 +0,0 @@
package models
type Profile struct {
ProfileItems []string
Max int
}
func (p *Profile) InsertOrShift(index int, value string) {
if index < len(p.ProfileItems) {
p.ProfileItems = append(p.ProfileItems[:index], value)
} else {
for len(p.ProfileItems) < index {
p.ProfileItems = append(p.ProfileItems, "0")
}
p.ProfileItems = append(p.ProfileItems, "0")
p.ProfileItems[index] = value
}
}

View File

@ -1,5 +0,0 @@
package models
type TokenTransferResponse struct {
TrackingId string `json:"trackingId"`
}

View File

@ -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"`
}

View File

@ -1,10 +0,0 @@
package models
type VoucherDataResult struct {
TokenName string `json:"tokenName"`
TokenSymbol string `json:"tokenSymbol"`
TokenDecimals int `json:"tokenDecimals"`
SinkAddress string `json:"sinkAddress"`
TokenCommodity string `json:"tokenCommodity"`
TokenLocation string `json:"tokenLocation"`
}

View File

@ -1,294 +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)
CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, 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
}
// CheckAliasAddress retrieves the address of an alias from the API endpoint.
// Parameters:
// - alias: The alias of the user.
func (as *AccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) {
var r dataserviceapi.AliasAddress
ep, err := url.JoinPath(config.CheckAliasURL, alias)
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, err
}
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))
}

View File

@ -1,60 +1,62 @@
package handlers
package request
import (
"git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/internal/handlers/ussd"
"git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/grassrootseconomics/visedriver/storage"
"git.grassecon.net/grassrootseconomics/visedriver/errors"
"git.grassecon.net/grassrootseconomics/visedriver/entry"
)
type BaseSessionHandler struct {
type BaseRequestHandler struct {
cfgTemplate engine.Config
rp RequestParser
rs resource.Resource
hn *ussd.Handlers
hn entry.EntryHandler
provider storage.StorageProvider
}
func NewBaseSessionHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp RequestParser, hn *ussd.Handlers) *BaseSessionHandler {
return &BaseSessionHandler{
//func NewBaseRequestHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp request.RequestParser, hn *handlers.Handlers) *BaseRequestHandler {
func NewBaseRequestHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp RequestParser, hn entry.EntryHandler) *BaseRequestHandler {
return &BaseRequestHandler{
cfgTemplate: cfg,
rs: rs,
hn: hn,
rp: rp,
provider: storage.NewSimpleStorageProvider(stateDb, userdataDb),
rs: rs,
hn: hn,
rp: rp,
provider: storage.NewSimpleStorageProvider(stateDb, userdataDb),
}
}
func(f* BaseSessionHandler) Shutdown() {
func (f *BaseRequestHandler) Shutdown() {
err := f.provider.Close()
if err != nil {
logg.Errorf("handler shutdown error", "err", err)
}
}
func(f *BaseSessionHandler) GetEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) engine.Engine {
func (f *BaseRequestHandler) GetEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) engine.Engine {
en := engine.NewEngine(cfg, rs)
en = en.WithPersister(pr)
return en
}
func(f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error) {
func(f *BaseRequestHandler) Process(rqs RequestSession) (RequestSession, error) {
var r bool
var err error
var ok bool
logg.InfoCtxf(rqs.Ctx, "new request", "data", rqs)
rqs.Storage, err = f.provider.Get(rqs.Config.SessionId)
if err != nil {
logg.ErrorCtxf(rqs.Ctx, "", "storage get error", err)
return rqs, ErrStorage
return rqs, errors.ErrStorage
}
f.hn = f.hn.WithPersister(rqs.Storage.Persister)
//f.hn = f.hn.WithPersister(rqs.Storage.Persister)
f.hn.SetPersister(rqs.Storage.Persister)
defer func() {
f.hn.Exit()
}()
@ -66,7 +68,7 @@ func(f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error)
if perr != nil {
logg.ErrorCtxf(rqs.Ctx, "", "storage put error", perr)
}
return rqs, ErrEngineType
return rqs, errors.ErrEngineType
}
en = en.WithFirst(f.hn.Init)
if rqs.Config.EngineDebug {
@ -84,25 +86,25 @@ func(f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error)
return rqs, err
}
rqs.Continue = r
rqs.Continue = r
return rqs, nil
}
func(f *BaseSessionHandler) Output(rqs RequestSession) (RequestSession, error) {
func(f *BaseRequestHandler) Output(rqs RequestSession) (RequestSession, error) {
var err error
_, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer)
return rqs, err
}
func(f *BaseSessionHandler) Reset(rqs RequestSession) (RequestSession, error) {
func(f *BaseRequestHandler) Reset(rqs RequestSession) (RequestSession, error) {
defer f.provider.Put(rqs.Config.SessionId, rqs.Storage)
return rqs, rqs.Engine.Finish()
}
func(f *BaseSessionHandler) GetConfig() engine.Config {
func (f *BaseRequestHandler) GetConfig() engine.Config {
return f.cfgTemplate
}
func(f *BaseSessionHandler) GetRequestParser() RequestParser {
func(f *BaseRequestHandler) GetRequestParser() RequestParser {
return f.rp
}

View File

@ -5,7 +5,7 @@ import (
"io/ioutil"
"net/http"
"git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/grassrootseconomics/visedriver/errors"
)
type DefaultRequestParser struct {
@ -14,11 +14,11 @@ type DefaultRequestParser struct {
func (rp *DefaultRequestParser) GetSessionId(ctx context.Context, rq any) (string, error) {
rqv, ok := rq.(*http.Request)
if !ok {
return "", handlers.ErrInvalidRequest
return "", errors.ErrInvalidRequest
}
v := rqv.Header.Get("X-Vise-Session")
if v == "" {
return "", handlers.ErrSessionMissing
return "", errors.ErrSessionMissing
}
return v, nil
}
@ -26,7 +26,7 @@ func (rp *DefaultRequestParser) GetSessionId(ctx context.Context, rq any) (strin
func (rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) {
rqv, ok := rq.(*http.Request)
if !ok {
return nil, handlers.ErrInvalidRequest
return nil, errors.ErrInvalidRequest
}
defer rqv.Body.Close()
v, err := ioutil.ReadAll(rqv.Body)

92
request/http/server.go Normal file
View File

@ -0,0 +1,92 @@
package http
import (
"net/http"
"strconv"
"git.defalsify.org/vise.git/logging"
"git.grassecon.net/grassrootseconomics/visedriver/request"
"git.grassecon.net/grassrootseconomics/visedriver/errors"
)
var (
logg = logging.NewVanilla().WithDomain("visedriver.http.session")
)
// HTTPRequestHandler implements the session handler for HTTP
type HTTPRequestHandler struct {
request.RequestHandler
}
func (f *HTTPRequestHandler) WriteError(w http.ResponseWriter, code int, err error) {
s := err.Error()
w.Header().Set("Content-Length", strconv.Itoa(len(s)))
w.WriteHeader(code)
_, err = w.Write([]byte(s))
if err != nil {
logg.Errorf("error writing error!!", "err", err, "olderr", s)
w.WriteHeader(500)
}
}
func NewHTTPRequestHandler(h request.RequestHandler) *HTTPRequestHandler {
return &HTTPRequestHandler{
RequestHandler: h,
}
}
func (hh *HTTPRequestHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
var code int
var err error
var perr error
rqs := request.RequestSession{
Ctx: req.Context(),
Writer: w,
}
rp := hh.GetRequestParser()
cfg := hh.GetConfig()
cfg.SessionId, err = rp.GetSessionId(req.Context(), req)
if err != nil {
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
hh.WriteError(w, 400, err)
}
rqs.Config = cfg
rqs.Input, err = rp.GetInput(req)
if err != nil {
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
hh.WriteError(w, 400, err)
return
}
rqs, err = hh.Process(rqs)
switch err {
case errors.ErrStorage:
code = 500
case errors.ErrEngineInit:
code = 500
case errors.ErrEngineExec:
code = 500
default:
code = 200
}
if code != 200 {
hh.WriteError(w, 500, err)
return
}
w.WriteHeader(200)
w.Header().Set("Content-Type", "text/plain")
rqs, err = hh.Output(rqs)
rqs, perr = hh.Reset(rqs)
if err != nil {
hh.WriteError(w, 500, err)
return
}
if perr != nil {
hh.WriteError(w, 500, perr)
return
}
}

View File

@ -9,8 +9,9 @@ import (
"testing"
"git.defalsify.org/vise.git/engine"
"git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks"
viseerrors "git.grassecon.net/grassrootseconomics/visedriver/errors"
"git.grassecon.net/grassrootseconomics/visedriver/testutil/mocks/httpmocks"
"git.grassecon.net/grassrootseconomics/visedriver/request"
)
// invalidRequestType is a custom type to test invalid request scenarios
@ -23,7 +24,7 @@ func (e *errorReader) Read(p []byte) (n int, err error) {
return 0, errors.New("read error")
}
func TestSessionHandler_ServeHTTP(t *testing.T) {
func TestRequestHandler_ServeHTTP(t *testing.T) {
tests := []struct {
name string
sessionID string
@ -43,14 +44,14 @@ func TestSessionHandler_ServeHTTP(t *testing.T) {
{
name: "Missing Session ID",
sessionID: "",
parserErr: handlers.ErrSessionMissing,
parserErr: viseerrors.ErrSessionMissing,
expectedStatus: http.StatusBadRequest,
},
{
name: "Process Error",
sessionID: "123",
input: []byte("test input"),
processErr: handlers.ErrStorage,
processErr: viseerrors.ErrStorage,
expectedStatus: http.StatusInternalServerError,
},
{
@ -81,16 +82,16 @@ func TestSessionHandler_ServeHTTP(t *testing.T) {
}
mockRequestHandler := &httpmocks.MockRequestHandler{
ProcessFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
ProcessFunc: func(rs request.RequestSession) (request.RequestSession, error) {
return rs, tt.processErr
},
OutputFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
OutputFunc: func(rs request.RequestSession) (request.RequestSession, error) {
return rs, tt.outputErr
},
ResetFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
ResetFunc: func(rs request.RequestSession) (request.RequestSession, error) {
return rs, tt.resetErr
},
GetRequestParserFunc: func() handlers.RequestParser {
GetRequestParserFunc: func() request.RequestParser {
return mockRequestParser
},
GetConfigFunc: func() engine.Config {
@ -98,7 +99,9 @@ func TestSessionHandler_ServeHTTP(t *testing.T) {
},
}
sessionHandler := ToSessionHandler(mockRequestHandler)
sessionHandler := &HTTPRequestHandler{
RequestHandler: mockRequestHandler,
}
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.input))
req.Header.Set("X-Vise-Session", tt.sessionID)
@ -115,8 +118,8 @@ func TestSessionHandler_ServeHTTP(t *testing.T) {
}
}
func TestSessionHandler_WriteError(t *testing.T) {
handler := &SessionHandler{}
func TestRequestHandler_WriteError(t *testing.T) {
handler := &HTTPRequestHandler{}
mockWriter := &httpmocks.MockWriter{}
err := errors.New("test error")
@ -148,13 +151,13 @@ func TestDefaultRequestParser_GetSessionId(t *testing.T) {
name: "Missing Session ID",
request: httptest.NewRequest(http.MethodPost, "/", nil),
expectedID: "",
expectedError: handlers.ErrSessionMissing,
expectedError: viseerrors.ErrSessionMissing,
},
{
name: "Invalid Request Type",
request: invalidRequestType{},
expectedID: "",
expectedError: handlers.ErrInvalidRequest,
expectedError: viseerrors.ErrInvalidRequest,
},
}
@ -200,7 +203,7 @@ func TestDefaultRequestParser_GetInput(t *testing.T) {
name: "Invalid Request Type",
request: invalidRequestType{},
expectedInput: nil,
expectedError: handlers.ErrInvalidRequest,
expectedError: viseerrors.ErrInvalidRequest,
},
{
name: "Read Error",

42
request/request.go Normal file
View 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()
}

View File

@ -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)"

View File

@ -1 +0,0 @@
Something went wrong.Please try again

View File

@ -1 +0,0 @@
HALT

View File

@ -1 +0,0 @@
Tatizo la kimtambo limetokea,tafadhali jaribu tena baadaye.

View File

@ -1 +0,0 @@
Your account is being created...

View File

@ -1,4 +0,0 @@
RELOAD verify_create_pin
CATCH create_pin_mismatch flag_pin_mismatch 1
LOAD quit 0
HALT

View File

@ -1 +0,0 @@
Your account creation request failed. Please try again later.

View File

@ -1,3 +0,0 @@
MOUT quit 9
HALT
INCMP quit 9

View File

@ -1 +0,0 @@
Ombi lako la kusajiliwa haliwezi kukamilishwa. Tafadhali jaribu tena baadaye.

View File

@ -1 +0,0 @@
Akaunti yako inatengenezwa...

View File

@ -1 +0,0 @@
My Account

View File

@ -1 +0,0 @@
Akaunti yangu

Some files were not shown because too many files have changed in this diff Show More