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