Compare commits
23 Commits
v0.8.0-bet
...
lash/gette
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb4037e73f
|
||
|
|
349051b5ef | ||
| 47b5ff0435 | |||
|
|
25867cf05e
|
||
|
|
d5a2680500
|
||
|
|
d950b10b50
|
||
|
|
bcb3ab905e
|
||
|
|
3ed9caf16d
|
||
|
|
86464c31d2 | ||
|
|
67007fcd48
|
||
|
|
f1b258fa6d
|
||
|
|
9013cc3618
|
||
|
|
056d056613
|
||
|
|
e581ec4771 | ||
|
|
e16b7445e8
|
||
|
|
1b12f0ba5f
|
||
|
|
c1e0617bb3
|
||
|
|
6723884103
|
||
|
|
b888af446d
|
||
|
|
43b2c3b78d
|
||
|
|
d67853f6d9 | ||
|
|
06230dc557
|
||
|
|
6ee2c88fe2
|
@@ -18,3 +18,7 @@ DB_TIMEZONE=Africa/Nairobi
|
||||
CUSTODIAL_URL_BASE=http://localhost:5003
|
||||
BEARER_TOKEN=eyJeSIsInRcCI6IkpXVCJ.yJwdWJsaWNLZXkiOiIwrrrrrr
|
||||
DATA_URL_BASE=http://localhost:5006
|
||||
|
||||
#Language
|
||||
DEFAULT_LANGUAGE=eng
|
||||
LANGUAGES=eng, swa
|
||||
|
||||
@@ -1,32 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.defalsify.org/vise.git/lang"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/common"
|
||||
"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/http/at"
|
||||
httpserver "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 (
|
||||
@@ -39,113 +36,6 @@ var (
|
||||
func init() {
|
||||
initializers.LoadEnvVariables()
|
||||
}
|
||||
|
||||
type atRequestParser struct {
|
||||
context context.Context
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
func (arp *atRequestParser) GetSessionId(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 {
|
||||
context.WithValue(arp.context, "at-session-id", sessionId)
|
||||
}
|
||||
logg.Debugf("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")
|
||||
}
|
||||
|
||||
return []byte(parts[len(parts)-1]), nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
config.LoadConfig()
|
||||
|
||||
@@ -156,6 +46,8 @@ func main() {
|
||||
var engineDebug bool
|
||||
var host string
|
||||
var port uint
|
||||
var gettextDir string
|
||||
var langs args.LangVar
|
||||
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
||||
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
|
||||
flag.StringVar(&database, "db", "gdbm", "database to be used")
|
||||
@@ -163,12 +55,21 @@ func main() {
|
||||
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()
|
||||
|
||||
logg.Infof("start command", "build", build, "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "Database", database)
|
||||
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{
|
||||
@@ -233,8 +134,8 @@ func main() {
|
||||
}
|
||||
defer stateStore.Close()
|
||||
|
||||
rp := &atRequestParser{
|
||||
context: ctx,
|
||||
rp := &at.ATRequestParser{
|
||||
Context: ctx,
|
||||
}
|
||||
bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
|
||||
sh := httpserver.NewATSessionHandler(bsh)
|
||||
|
||||
@@ -12,12 +12,14 @@ import (
|
||||
"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 (
|
||||
@@ -54,6 +56,8 @@ func main() {
|
||||
var engineDebug bool
|
||||
var host string
|
||||
var port uint
|
||||
var gettextDir string
|
||||
var langs args.LangVar
|
||||
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
|
||||
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
||||
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
|
||||
@@ -62,12 +66,22 @@ func main() {
|
||||
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()
|
||||
|
||||
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "Database", database)
|
||||
|
||||
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{
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"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"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
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 (
|
||||
@@ -43,6 +45,8 @@ func main() {
|
||||
var engineDebug bool
|
||||
var host string
|
||||
var port uint
|
||||
var gettextDir string
|
||||
var langs args.LangVar
|
||||
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
||||
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
|
||||
flag.StringVar(&database, "db", "gdbm", "database to be used")
|
||||
@@ -50,12 +54,22 @@ func main() {
|
||||
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()
|
||||
|
||||
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "Database", database)
|
||||
|
||||
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{
|
||||
|
||||
24
cmd/main.go
24
cmd/main.go
@@ -10,10 +10,12 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -27,6 +29,7 @@ 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()
|
||||
|
||||
@@ -35,18 +38,34 @@ func main() {
|
||||
var sessionId string
|
||||
var database string
|
||||
var engineDebug bool
|
||||
var gettextDir string
|
||||
var langs args.LangVar
|
||||
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
|
||||
flag.StringVar(&database, "db", "gdbm", "database to be used")
|
||||
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
||||
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||
flag.UintVar(&size, "s", 160, "max size of output")
|
||||
flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory")
|
||||
flag.Var(&langs, "language", "add symbol resolution for language")
|
||||
flag.Parse()
|
||||
|
||||
logg.Infof("start command", "dbdir", dbDir, "outputsize", size)
|
||||
|
||||
if len(langs.Langs()) == 0 {
|
||||
langs.Set(config.DefaultLanguage)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||
ctx = context.WithValue(ctx, "Database", database)
|
||||
|
||||
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{
|
||||
@@ -59,8 +78,11 @@ func main() {
|
||||
|
||||
resourceDir := scriptDir
|
||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
||||
if gettextDir != "" {
|
||||
menuStorageService = menuStorageService.WithGettext(gettextDir, langs.Langs())
|
||||
}
|
||||
|
||||
err := menuStorageService.EnsureDbDir()
|
||||
err = menuStorageService.EnsureDbDir()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
|
||||
@@ -8,14 +8,15 @@ import (
|
||||
"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) storage.PrefixDb {
|
||||
return storage.NewSubPrefixDb(store.Db, pfx)
|
||||
func StoreToPrefixDb(store *UserDataStore, pfx []byte) dbstorage.PrefixDb {
|
||||
return dbstorage.NewSubPrefixDb(store.Db, pfx)
|
||||
}
|
||||
|
||||
type StorageServices interface {
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||
)
|
||||
|
||||
@@ -56,7 +56,7 @@ func ProcessTransfers(transfers []dataserviceapi.Last10TxResponse) TransferMetad
|
||||
|
||||
// GetTransferData retrieves and matches transfer data
|
||||
// returns a formatted string of the full transaction/statement
|
||||
func GetTransferData(ctx context.Context, db storage.PrefixDb, publicKey string, index int) (string, error) {
|
||||
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)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||
)
|
||||
|
||||
@@ -63,7 +63,7 @@ func ScaleDownBalance(balance, decimals string) string {
|
||||
}
|
||||
|
||||
// GetVoucherData retrieves and matches voucher data
|
||||
func GetVoucherData(ctx context.Context, db storage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) {
|
||||
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)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
visedb "git.defalsify.org/vise.git/db"
|
||||
memdb "git.defalsify.org/vise.git/db/mem"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||
)
|
||||
|
||||
@@ -86,7 +86,7 @@ func TestGetVoucherData(t *testing.T) {
|
||||
}
|
||||
|
||||
prefix := ToBytes(visedb.DATATYPE_USERDATA)
|
||||
spdb := storage.NewSubPrefixDb(db, prefix)
|
||||
spdb := dbstorage.NewSubPrefixDb(db, prefix)
|
||||
|
||||
// Test voucher data
|
||||
mockData := map[DataTyp][]byte{
|
||||
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/initializers"
|
||||
)
|
||||
@@ -18,6 +19,11 @@ const (
|
||||
AliasPrefix = "api/v1/alias"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultLanguage = "eng"
|
||||
languages []string
|
||||
)
|
||||
|
||||
var (
|
||||
custodialURLBase string
|
||||
dataURLBase string
|
||||
@@ -34,8 +40,28 @@ var (
|
||||
VoucherTransfersURL string
|
||||
VoucherDataURL string
|
||||
CheckAliasURL string
|
||||
DefaultLanguage string
|
||||
Languages []string
|
||||
)
|
||||
|
||||
func setLanguage() error {
|
||||
defaultLanguage = initializers.GetEnv("DEFAULT_LANGUAGE", defaultLanguage)
|
||||
languages = strings.Split(initializers.GetEnv("LANGUAGES", defaultLanguage), ",")
|
||||
haveDefaultLanguage := false
|
||||
for i, v := range(languages) {
|
||||
languages[i] = strings.ReplaceAll(v, " ", "")
|
||||
if languages[i] == defaultLanguage {
|
||||
haveDefaultLanguage = true
|
||||
}
|
||||
}
|
||||
|
||||
if !haveDefaultLanguage {
|
||||
languages = append([]string{defaultLanguage}, languages...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setBase() error {
|
||||
var err error
|
||||
|
||||
@@ -60,6 +86,10 @@ func LoadConfig() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = setLanguage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
CreateAccountURL, _ = url.JoinPath(custodialURLBase, createAccountPath)
|
||||
TrackStatusURL, _ = url.JoinPath(custodialURLBase, trackStatusPath)
|
||||
BalanceURL, _ = url.JoinPath(custodialURLBase, balancePathPrefix)
|
||||
@@ -69,6 +99,8 @@ func LoadConfig() error {
|
||||
VoucherTransfersURL, _ = url.JoinPath(dataURLBase, voucherTransfersPathPrefix)
|
||||
VoucherDataURL, _ = url.JoinPath(dataURLBase, voucherDataPathPrefix)
|
||||
CheckAliasURL, _ = url.JoinPath(dataURLBase, AliasPrefix)
|
||||
DefaultLanguage = defaultLanguage
|
||||
Languages = languages
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
126
devtools/lang/main.go
Normal file
126
devtools/lang/main.go
Normal file
@@ -0,0 +1,126 @@
|
||||
// create language files from environment
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/lang"
|
||||
"git.grassecon.net/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)
|
||||
}
|
||||
}
|
||||
34
internal/args/lang.go
Normal file
34
internal/args/lang.go
Normal file
@@ -0,0 +1,34 @@
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
"git.grassecon.net/urdt/ussd/remote"
|
||||
"gopkg.in/leonelquinteros/gotext.v1"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||
)
|
||||
|
||||
@@ -64,7 +64,7 @@ type Handlers struct {
|
||||
adminstore *utils.AdminStore
|
||||
flagManager *asm.FlagParser
|
||||
accountService remote.AccountServiceInterface
|
||||
prefixDb storage.PrefixDb
|
||||
prefixDb dbstorage.PrefixDb
|
||||
profile *models.Profile
|
||||
ReplaceSeparatorFunc func(string) string
|
||||
}
|
||||
@@ -80,7 +80,7 @@ func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, adminstore *util
|
||||
|
||||
// Instantiate the SubPrefixDb with "DATATYPE_USERDATA" prefix
|
||||
prefix := common.ToBytes(db.DATATYPE_USERDATA)
|
||||
prefixDb := storage.NewSubPrefixDb(userdataStore, prefix)
|
||||
prefixDb := dbstorage.NewSubPrefixDb(userdataStore, prefix)
|
||||
|
||||
h := &Handlers{
|
||||
userdataStore: userDb,
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.defalsify.org/vise.git/state"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||
"git.grassecon.net/urdt/ussd/internal/testutil/mocks"
|
||||
"git.grassecon.net/urdt/ussd/internal/testutil/testservice"
|
||||
"git.grassecon.net/urdt/ussd/internal/utils"
|
||||
@@ -59,14 +59,14 @@ func InitializeTestStore(t *testing.T) (context.Context, *common.UserDataStore)
|
||||
return ctx, store
|
||||
}
|
||||
|
||||
func InitializeTestSubPrefixDb(t *testing.T, ctx context.Context) *storage.SubPrefixDb {
|
||||
func InitializeTestSubPrefixDb(t *testing.T, ctx context.Context) *dbstorage.SubPrefixDb {
|
||||
db := memdb.NewMemDb()
|
||||
err := db.Connect(ctx, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prefix := common.ToBytes(visedb.DATATYPE_USERDATA)
|
||||
spdb := storage.NewSubPrefixDb(db, prefix)
|
||||
spdb := dbstorage.NewSubPrefixDb(db, prefix)
|
||||
|
||||
return spdb
|
||||
}
|
||||
|
||||
121
internal/http/at/parse.go
Normal file
121
internal/http/at/parse.go
Normal file
@@ -0,0 +1,121 @@
|
||||
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 {
|
||||
Context context.Context
|
||||
}
|
||||
|
||||
func (arp *ATRequestParser) GetSessionId(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 {
|
||||
context.WithValue(arp.Context, "at-session-id", sessionId)
|
||||
}
|
||||
logg.Debugf("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")
|
||||
}
|
||||
|
||||
return []byte(parts[len(parts)-1]), 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
|
||||
|
||||
}
|
||||
@@ -1,19 +1,25 @@
|
||||
package http
|
||||
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")
|
||||
)
|
||||
|
||||
type ATSessionHandler struct {
|
||||
*SessionHandler
|
||||
*httpserver.SessionHandler
|
||||
}
|
||||
|
||||
func NewATSessionHandler(h handlers.RequestHandler) *ATSessionHandler {
|
||||
return &ATSessionHandler{
|
||||
SessionHandler: ToSessionHandler(h),
|
||||
SessionHandler: httpserver.ToSessionHandler(h),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,14 +37,14 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
|
||||
cfg.SessionId, err = rp.GetSessionId(req)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
||||
ash.writeError(w, 400, 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)
|
||||
ash.WriteError(w, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -53,7 +59,7 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
|
||||
}
|
||||
|
||||
if code != 200 {
|
||||
ash.writeError(w, 500, err)
|
||||
ash.WriteError(w, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -61,13 +67,13 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
rqs, err = ash.Output(rqs)
|
||||
if err != nil {
|
||||
ash.writeError(w, 500, err)
|
||||
ash.WriteError(w, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
rqs, err = ash.Reset(rqs)
|
||||
if err != nil {
|
||||
ash.writeError(w, 500, err)
|
||||
ash.WriteError(w, 500, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -89,4 +95,4 @@ func (ash *ATSessionHandler) Output(rqs handlers.RequestSession) (handlers.Reque
|
||||
|
||||
_, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer)
|
||||
return rqs, err
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package http
|
||||
package at
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
@@ -16,16 +15,6 @@ import (
|
||||
"git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks"
|
||||
)
|
||||
|
||||
// invalidRequestType is a custom type to test invalid request scenarios
|
||||
type invalidRequestType struct{}
|
||||
|
||||
// errorReader is a helper type that always returns an error when Read is called
|
||||
type errorReader struct{}
|
||||
|
||||
func (e *errorReader) Read(p []byte) (n int, err error) {
|
||||
return 0, errors.New("read error")
|
||||
}
|
||||
|
||||
func TestNewATSessionHandler(t *testing.T) {
|
||||
mockHandler := &httpmocks.MockRequestHandler{}
|
||||
ash := NewATSessionHandler(mockHandler)
|
||||
@@ -242,208 +231,4 @@ func TestATSessionHandler_Output(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionHandler_ServeHTTP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
sessionID string
|
||||
input []byte
|
||||
parserErr error
|
||||
processErr error
|
||||
outputErr error
|
||||
resetErr error
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
name: "Success",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Missing Session ID",
|
||||
sessionID: "",
|
||||
parserErr: handlers.ErrSessionMissing,
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "Process Error",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
processErr: handlers.ErrStorage,
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
name: "Output Error",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
outputErr: errors.New("output error"),
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Reset Error",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
resetErr: errors.New("reset error"),
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockRequestParser := &httpmocks.MockRequestParser{
|
||||
GetSessionIdFunc: func(any) (string, error) {
|
||||
return tt.sessionID, tt.parserErr
|
||||
},
|
||||
GetInputFunc: func(any) ([]byte, error) {
|
||||
return tt.input, nil
|
||||
},
|
||||
}
|
||||
|
||||
mockRequestHandler := &httpmocks.MockRequestHandler{
|
||||
ProcessFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return rs, tt.processErr
|
||||
},
|
||||
OutputFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return rs, tt.outputErr
|
||||
},
|
||||
ResetFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return rs, tt.resetErr
|
||||
},
|
||||
GetRequestParserFunc: func() handlers.RequestParser {
|
||||
return mockRequestParser
|
||||
},
|
||||
GetConfigFunc: func() engine.Config {
|
||||
return engine.Config{}
|
||||
},
|
||||
}
|
||||
|
||||
sessionHandler := ToSessionHandler(mockRequestHandler)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.input))
|
||||
req.Header.Set("X-Vise-Session", tt.sessionID)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
sessionHandler.ServeHTTP(rr, req)
|
||||
|
||||
if status := rr.Code; status != tt.expectedStatus {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||
status, tt.expectedStatus)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionHandler_writeError(t *testing.T) {
|
||||
handler := &SessionHandler{}
|
||||
mockWriter := &httpmocks.MockWriter{}
|
||||
err := errors.New("test error")
|
||||
|
||||
handler.writeError(mockWriter, http.StatusBadRequest, err)
|
||||
|
||||
if mockWriter.WrittenString != "" {
|
||||
t.Errorf("Expected empty body, got %s", mockWriter.WrittenString)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRequestParser_GetSessionId(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
request any
|
||||
expectedID string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Valid Session ID",
|
||||
request: func() *http.Request {
|
||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
req.Header.Set("X-Vise-Session", "123456")
|
||||
return req
|
||||
}(),
|
||||
expectedID: "123456",
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Missing Session ID",
|
||||
request: httptest.NewRequest(http.MethodPost, "/", nil),
|
||||
expectedID: "",
|
||||
expectedError: handlers.ErrSessionMissing,
|
||||
},
|
||||
{
|
||||
name: "Invalid Request Type",
|
||||
request: invalidRequestType{},
|
||||
expectedID: "",
|
||||
expectedError: handlers.ErrInvalidRequest,
|
||||
},
|
||||
}
|
||||
|
||||
parser := &DefaultRequestParser{}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
id, err := parser.GetSessionId(tt.request)
|
||||
|
||||
if id != tt.expectedID {
|
||||
t.Errorf("Expected session ID %s, got %s", tt.expectedID, id)
|
||||
}
|
||||
|
||||
if err != tt.expectedError {
|
||||
t.Errorf("Expected error %v, got %v", tt.expectedError, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRequestParser_GetInput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
request any
|
||||
expectedInput []byte
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Valid Input",
|
||||
request: func() *http.Request {
|
||||
return httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString("test input"))
|
||||
}(),
|
||||
expectedInput: []byte("test input"),
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Empty Input",
|
||||
request: httptest.NewRequest(http.MethodPost, "/", nil),
|
||||
expectedInput: []byte{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Invalid Request Type",
|
||||
request: invalidRequestType{},
|
||||
expectedInput: nil,
|
||||
expectedError: handlers.ErrInvalidRequest,
|
||||
},
|
||||
{
|
||||
name: "Read Error",
|
||||
request: func() *http.Request {
|
||||
return httptest.NewRequest(http.MethodPost, "/", &errorReader{})
|
||||
}(),
|
||||
expectedInput: nil,
|
||||
expectedError: errors.New("read error"),
|
||||
},
|
||||
}
|
||||
|
||||
parser := &DefaultRequestParser{}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input, err := parser.GetInput(tt.request)
|
||||
|
||||
if !bytes.Equal(input, tt.expectedInput) {
|
||||
t.Errorf("Expected input %s, got %s", tt.expectedInput, input)
|
||||
}
|
||||
|
||||
if err != tt.expectedError && (err == nil || err.Error() != tt.expectedError.Error()) {
|
||||
t.Errorf("Expected error %v, got %v", tt.expectedError, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
38
internal/http/parse.go
Normal file
38
internal/http/parse.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
)
|
||||
|
||||
type DefaultRequestParser struct {
|
||||
}
|
||||
|
||||
func (rp *DefaultRequestParser) GetSessionId(rq any) (string, error) {
|
||||
rqv, ok := rq.(*http.Request)
|
||||
if !ok {
|
||||
return "", handlers.ErrInvalidRequest
|
||||
}
|
||||
v := rqv.Header.Get("X-Vise-Session")
|
||||
if v == "" {
|
||||
return "", handlers.ErrSessionMissing
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) {
|
||||
rqv, ok := rq.(*http.Request)
|
||||
if !ok {
|
||||
return nil, handlers.ErrInvalidRequest
|
||||
}
|
||||
defer rqv.Body.Close()
|
||||
v, err := ioutil.ReadAll(rqv.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@@ -14,34 +13,6 @@ var (
|
||||
logg = logging.NewVanilla().WithDomain("httpserver")
|
||||
)
|
||||
|
||||
type DefaultRequestParser struct {
|
||||
}
|
||||
|
||||
func (rp *DefaultRequestParser) GetSessionId(rq any) (string, error) {
|
||||
rqv, ok := rq.(*http.Request)
|
||||
if !ok {
|
||||
return "", handlers.ErrInvalidRequest
|
||||
}
|
||||
v := rqv.Header.Get("X-Vise-Session")
|
||||
if v == "" {
|
||||
return "", handlers.ErrSessionMissing
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) {
|
||||
rqv, ok := rq.(*http.Request)
|
||||
if !ok {
|
||||
return nil, handlers.ErrInvalidRequest
|
||||
}
|
||||
defer rqv.Body.Close()
|
||||
v, err := ioutil.ReadAll(rqv.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
type SessionHandler struct {
|
||||
handlers.RequestHandler
|
||||
}
|
||||
@@ -52,7 +23,7 @@ func ToSessionHandler(h handlers.RequestHandler) *SessionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
func (f *SessionHandler) writeError(w http.ResponseWriter, code int, err error) {
|
||||
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)
|
||||
@@ -78,13 +49,13 @@ func (f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
cfg.SessionId, err = rp.GetSessionId(req)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
||||
f.writeError(w, 400, 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)
|
||||
f.WriteError(w, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -101,7 +72,7 @@ func (f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
if code != 200 {
|
||||
f.writeError(w, 500, err)
|
||||
f.WriteError(w, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -110,11 +81,11 @@ func (f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
rqs, err = f.Output(rqs)
|
||||
rqs, perr = f.Reset(rqs)
|
||||
if err != nil {
|
||||
f.writeError(w, 500, err)
|
||||
f.WriteError(w, 500, err)
|
||||
return
|
||||
}
|
||||
if perr != nil {
|
||||
f.writeError(w, 500, perr)
|
||||
f.WriteError(w, 500, perr)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
229
internal/http/server_test.go
Normal file
229
internal/http/server_test.go
Normal file
@@ -0,0 +1,229 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
"git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks"
|
||||
)
|
||||
|
||||
// invalidRequestType is a custom type to test invalid request scenarios
|
||||
type invalidRequestType struct{}
|
||||
|
||||
// errorReader is a helper type that always returns an error when Read is called
|
||||
type errorReader struct{}
|
||||
|
||||
func (e *errorReader) Read(p []byte) (n int, err error) {
|
||||
return 0, errors.New("read error")
|
||||
}
|
||||
|
||||
func TestSessionHandler_ServeHTTP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
sessionID string
|
||||
input []byte
|
||||
parserErr error
|
||||
processErr error
|
||||
outputErr error
|
||||
resetErr error
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
name: "Success",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Missing Session ID",
|
||||
sessionID: "",
|
||||
parserErr: handlers.ErrSessionMissing,
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "Process Error",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
processErr: handlers.ErrStorage,
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
name: "Output Error",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
outputErr: errors.New("output error"),
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Reset Error",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
resetErr: errors.New("reset error"),
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockRequestParser := &httpmocks.MockRequestParser{
|
||||
GetSessionIdFunc: func(any) (string, error) {
|
||||
return tt.sessionID, tt.parserErr
|
||||
},
|
||||
GetInputFunc: func(any) ([]byte, error) {
|
||||
return tt.input, nil
|
||||
},
|
||||
}
|
||||
|
||||
mockRequestHandler := &httpmocks.MockRequestHandler{
|
||||
ProcessFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return rs, tt.processErr
|
||||
},
|
||||
OutputFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return rs, tt.outputErr
|
||||
},
|
||||
ResetFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return rs, tt.resetErr
|
||||
},
|
||||
GetRequestParserFunc: func() handlers.RequestParser {
|
||||
return mockRequestParser
|
||||
},
|
||||
GetConfigFunc: func() engine.Config {
|
||||
return engine.Config{}
|
||||
},
|
||||
}
|
||||
|
||||
sessionHandler := ToSessionHandler(mockRequestHandler)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.input))
|
||||
req.Header.Set("X-Vise-Session", tt.sessionID)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
sessionHandler.ServeHTTP(rr, req)
|
||||
|
||||
if status := rr.Code; status != tt.expectedStatus {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||
status, tt.expectedStatus)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionHandler_WriteError(t *testing.T) {
|
||||
handler := &SessionHandler{}
|
||||
mockWriter := &httpmocks.MockWriter{}
|
||||
err := errors.New("test error")
|
||||
|
||||
handler.WriteError(mockWriter, http.StatusBadRequest, err)
|
||||
|
||||
if mockWriter.WrittenString != "" {
|
||||
t.Errorf("Expected empty body, got %s", mockWriter.WrittenString)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRequestParser_GetSessionId(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
request any
|
||||
expectedID string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Valid Session ID",
|
||||
request: func() *http.Request {
|
||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
req.Header.Set("X-Vise-Session", "123456")
|
||||
return req
|
||||
}(),
|
||||
expectedID: "123456",
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Missing Session ID",
|
||||
request: httptest.NewRequest(http.MethodPost, "/", nil),
|
||||
expectedID: "",
|
||||
expectedError: handlers.ErrSessionMissing,
|
||||
},
|
||||
{
|
||||
name: "Invalid Request Type",
|
||||
request: invalidRequestType{},
|
||||
expectedID: "",
|
||||
expectedError: handlers.ErrInvalidRequest,
|
||||
},
|
||||
}
|
||||
|
||||
parser := &DefaultRequestParser{}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
id, err := parser.GetSessionId(tt.request)
|
||||
|
||||
if id != tt.expectedID {
|
||||
t.Errorf("Expected session ID %s, got %s", tt.expectedID, id)
|
||||
}
|
||||
|
||||
if err != tt.expectedError {
|
||||
t.Errorf("Expected error %v, got %v", tt.expectedError, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRequestParser_GetInput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
request any
|
||||
expectedInput []byte
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Valid Input",
|
||||
request: func() *http.Request {
|
||||
return httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString("test input"))
|
||||
}(),
|
||||
expectedInput: []byte("test input"),
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Empty Input",
|
||||
request: httptest.NewRequest(http.MethodPost, "/", nil),
|
||||
expectedInput: []byte{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Invalid Request Type",
|
||||
request: invalidRequestType{},
|
||||
expectedInput: nil,
|
||||
expectedError: handlers.ErrInvalidRequest,
|
||||
},
|
||||
{
|
||||
name: "Read Error",
|
||||
request: func() *http.Request {
|
||||
return httptest.NewRequest(http.MethodPost, "/", &errorReader{})
|
||||
}(),
|
||||
expectedInput: nil,
|
||||
expectedError: errors.New("read error"),
|
||||
},
|
||||
}
|
||||
|
||||
parser := &DefaultRequestParser{}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input, err := parser.GetInput(tt.request)
|
||||
|
||||
if !bytes.Equal(input, tt.expectedInput) {
|
||||
t.Errorf("Expected input %s, got %s", tt.expectedInput, input)
|
||||
}
|
||||
|
||||
if err != tt.expectedError && (err == nil || err.Error() != tt.expectedError.Error()) {
|
||||
t.Errorf("Expected error %v, got %v", tt.expectedError, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,11 @@ import (
|
||||
"git.defalsify.org/vise.git/db"
|
||||
gdbmdb "git.defalsify.org/vise.git/db/gdbm"
|
||||
"git.defalsify.org/vise.git/lang"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla().WithDomain("gdbmstorage")
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -9,10 +9,12 @@ import (
|
||||
"git.defalsify.org/vise.git/db"
|
||||
fsdb "git.defalsify.org/vise.git/db/fs"
|
||||
"git.defalsify.org/vise.git/db/postgres"
|
||||
"git.defalsify.org/vise.git/lang"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.grassecon.net/urdt/ussd/initializers"
|
||||
gdbmstorage "git.grassecon.net/urdt/ussd/internal/storage/db/gdbm"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -29,6 +31,7 @@ type StorageService interface {
|
||||
type MenuStorageService struct {
|
||||
dbDir string
|
||||
resourceDir string
|
||||
poResource resource.Resource
|
||||
resourceStore db.Db
|
||||
stateStore db.Db
|
||||
userDataStore db.Db
|
||||
@@ -57,6 +60,28 @@ func NewMenuStorageService(dbDir string, resourceDir string) *MenuStorageService
|
||||
}
|
||||
}
|
||||
|
||||
// WithGettext triggers use of gettext for translation of templates and menus.
|
||||
//
|
||||
// The first language in `lns` will be used as default language, to resolve node keys to
|
||||
// language strings.
|
||||
//
|
||||
// If `lns` is an empty array, gettext will not be used.
|
||||
func (ms *MenuStorageService) WithGettext(path string, lns []lang.Language) *MenuStorageService {
|
||||
if len(lns) == 0 {
|
||||
logg.Warnf("Gettext requested but no languages supplied")
|
||||
return ms
|
||||
}
|
||||
rs := resource.NewPoResource(lns[0], path)
|
||||
|
||||
for _, ln := range(lns) {
|
||||
rs = rs.WithLanguage(ln)
|
||||
}
|
||||
|
||||
ms.poResource = rs
|
||||
|
||||
return ms
|
||||
}
|
||||
|
||||
func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.Db, fileName string) (db.Db, error) {
|
||||
database, ok := ctx.Value("Database").(string)
|
||||
if !ok {
|
||||
@@ -75,7 +100,7 @@ func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.D
|
||||
connStr := buildConnStr()
|
||||
err = newDb.Connect(ctx, connStr)
|
||||
} else {
|
||||
newDb = NewThreadGdbmDb()
|
||||
newDb = gdbmstorage.NewThreadGdbmDb()
|
||||
storeFile := path.Join(ms.dbDir, fileName)
|
||||
err = newDb.Connect(ctx, storeFile)
|
||||
}
|
||||
@@ -119,6 +144,11 @@ func (ms *MenuStorageService) GetResource(ctx context.Context) (resource.Resourc
|
||||
return nil, err
|
||||
}
|
||||
rfs := resource.NewDbResource(ms.resourceStore)
|
||||
if ms.poResource != nil {
|
||||
logg.InfoCtxf(ctx, "using poresource for menu and template")
|
||||
rfs.WithMenuGetter(ms.poResource.GetMenu)
|
||||
rfs.WithTemplateGetter(ms.poResource.GetTemplate)
|
||||
}
|
||||
return rfs, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user