Fix storage related compile problems
This commit is contained in:
commit
4735da9887
28
go.mod
Normal file
28
go.mod
Normal file
@ -0,0 +1,28 @@
|
||||
module git.grassecon.net/grassrootseconomics/visedriver-africastalking
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d
|
||||
git.grassecon.net/urdt/ussd v0.8.0-beta.5.0.20250104211339-51b6fc0dde43
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alecthomas/participle/v2 v2.0.0 // indirect
|
||||
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
|
||||
github.com/grassrootseconomics/eth-custodial v1.3.0-beta // indirect
|
||||
github.com/grassrootseconomics/ussd-data-service v1.2.0-beta // indirect
|
||||
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 // 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/joho/godotenv v1.5.1 // indirect
|
||||
github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // 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/leonelquinteros/gotext.v1 v1.3.1 // indirect
|
||||
)
|
121
internal/africastalking/parse.go
Normal file
121
internal/africastalking/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/errors"
|
||||
)
|
||||
|
||||
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 "", errors.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, errors.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
|
||||
|
||||
}
|
98
internal/africastalking/server.go
Normal file
98
internal/africastalking/server.go
Normal file
@ -0,0 +1,98 @@
|
||||
package at
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.grassecon.net/urdt/ussd/request"
|
||||
"git.grassecon.net/urdt/ussd/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla().WithDomain("atserver")
|
||||
)
|
||||
|
||||
type ATSessionHandler struct {
|
||||
*request.SessionHandler
|
||||
}
|
||||
|
||||
func NewATSessionHandler(h request.RequestHandler) *ATSessionHandler {
|
||||
return &ATSessionHandler{
|
||||
SessionHandler: request.ToSessionHandler(h),
|
||||
}
|
||||
}
|
||||
|
||||
func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
var code int
|
||||
var err error
|
||||
|
||||
rqs := request.RequestSession{
|
||||
Ctx: req.Context(),
|
||||
Writer: w,
|
||||
}
|
||||
|
||||
rp := ash.GetRequestParser()
|
||||
cfg := ash.GetConfig()
|
||||
cfg.SessionId, err = rp.GetSessionId(req)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
||||
ash.WriteError(w, 400, err)
|
||||
return
|
||||
}
|
||||
rqs.Config = cfg
|
||||
rqs.Input, err = rp.GetInput(req)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
||||
ash.WriteError(w, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
rqs, err = ash.Process(rqs)
|
||||
switch err {
|
||||
case nil: // set code to 200 if no err
|
||||
code = 200
|
||||
case errors.ErrStorage, errors.ErrEngineInit, errors.ErrEngineExec, errors.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 request.RequestSession) (request.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
|
||||
}
|
234
internal/africastalking/server_test.go
Normal file
234
internal/africastalking/server_test.go
Normal file
@ -0,0 +1,234 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user