package http import ( "bytes" "context" "errors" "io" "net/http" "net/http/httptest" "net/url" "strings" "testing" "git.defalsify.org/vise.git/engine" "git.grassecon.net/urdt/ussd/internal/handlers" "git.grassecon.net/urdt/ussd/internal/mocks/httpmocks" ) // invalidRequestType is a custom type to test invalid request scenarios type invalidRequestType struct{} // errorReader is a helper type that always returns an error when Read is called type errorReader struct{} func (e *errorReader) Read(p []byte) (n int, err error) { return 0, errors.New("read error") } func TestNewATSessionHandler(t *testing.T) { mockHandler := &httpmocks.MockRequestHandler{} ash := NewATSessionHandler(mockHandler) if ash == nil { t.Fatal("NewATSessionHandler returned nil") } if ash.SessionHandler == nil { t.Fatal("SessionHandler is nil") } } func TestATSessionHandler_ServeHTTP(t *testing.T) { tests := []struct { name string setupMocks func(*httpmocks.MockRequestHandler, *httpmocks.MockRequestParser, *httpmocks.MockEngine) formData url.Values expectedStatus int expectedBody string }{ { name: "Successful request", setupMocks: func(mh *httpmocks.MockRequestHandler, mrp *httpmocks.MockRequestParser, me *httpmocks.MockEngine) { mrp.GetSessionIdFunc = func(rq any) (string, error) { req := rq.(*http.Request) return req.FormValue("phoneNumber"), nil } mrp.GetInputFunc = func(rq any) ([]byte, error) { req := rq.(*http.Request) text := req.FormValue("text") parts := strings.Split(text, "*") return []byte(parts[len(parts)-1]), nil } mh.ProcessFunc = func(rqs handlers.RequestSession) (handlers.RequestSession, error) { rqs.Continue = true rqs.Engine = me return rqs, nil } mh.GetConfigFunc = func() engine.Config { return engine.Config{} } mh.GetRequestParserFunc = func() handlers.RequestParser { return mrp } mh.OutputFunc = func(rs handlers.RequestSession) (handlers.RequestSession, error) { return rs, nil } mh.ResetFunc = func(rs handlers.RequestSession) (handlers.RequestSession, error) { return rs, nil } me.WriteResultFunc = 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{ WriteResultFunc: 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{ WriteResultFunc: func(context.Context, io.Writer) (int, error) { return 0, nil }, }, Writer: &httpmocks.MockWriter{}, }, expectedPrefix: "END ", expectedError: false, }, { name: "WriteResult error", input: handlers.RequestSession{ Continue: true, Engine: &httpmocks.MockEngine{ WriteResultFunc: func(context.Context, io.Writer) (int, error) { return 0, errors.New("write error") }, }, Writer: &httpmocks.MockWriter{}, }, expectedPrefix: "CON ", expectedError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ash := &ATSessionHandler{} _, err := ash.Output(tt.input) if tt.expectedError && err == nil { t.Error("Expected an error, but got nil") } if !tt.expectedError && err != nil { t.Errorf("Unexpected error: %v", err) } mw := tt.input.Writer.(*httpmocks.MockWriter) if !mw.WriteStringCalled { t.Error("WriteString was not called") } if mw.WrittenString != tt.expectedPrefix { t.Errorf("Expected prefix %q, got %q", tt.expectedPrefix, mw.WrittenString) } }) } } func TestSessionHandler_ServeHTTP(t *testing.T) { tests := []struct { name string sessionID string input []byte parserErr error processErr error outputErr error resetErr error expectedStatus int }{ { name: "Success", sessionID: "123", input: []byte("test input"), expectedStatus: http.StatusOK, }, { name: "Missing Session ID", sessionID: "", parserErr: handlers.ErrSessionMissing, expectedStatus: http.StatusBadRequest, }, { name: "Process Error", sessionID: "123", input: []byte("test input"), processErr: handlers.ErrStorage, expectedStatus: http.StatusInternalServerError, }, { name: "Output Error", sessionID: "123", input: []byte("test input"), outputErr: errors.New("output error"), expectedStatus: http.StatusOK, }, { name: "Reset Error", sessionID: "123", input: []byte("test input"), resetErr: errors.New("reset error"), expectedStatus: http.StatusOK, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockRequestParser := &httpmocks.MockRequestParser{ GetSessionIdFunc: func(any) (string, error) { return tt.sessionID, tt.parserErr }, GetInputFunc: func(any) ([]byte, error) { return tt.input, nil }, } mockRequestHandler := &httpmocks.MockRequestHandler{ ProcessFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) { return rs, tt.processErr }, OutputFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) { return rs, tt.outputErr }, ResetFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) { return rs, tt.resetErr }, GetRequestParserFunc: func() handlers.RequestParser { return mockRequestParser }, GetConfigFunc: func() engine.Config { return engine.Config{} }, } sessionHandler := ToSessionHandler(mockRequestHandler) req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.input)) req.Header.Set("X-Vise-Session", tt.sessionID) rr := httptest.NewRecorder() sessionHandler.ServeHTTP(rr, req) if status := rr.Code; status != tt.expectedStatus { t.Errorf("handler returned wrong status code: got %v want %v", status, tt.expectedStatus) } }) } } func TestSessionHandler_writeError(t *testing.T) { handler := &SessionHandler{} mockWriter := &httpmocks.MockWriter{} err := errors.New("test error") handler.writeError(mockWriter, http.StatusBadRequest, err) if mockWriter.WrittenString != "" { t.Errorf("Expected empty body, got %s", mockWriter.WrittenString) } } func TestDefaultRequestParser_GetSessionId(t *testing.T) { tests := []struct { name string request any expectedID string expectedError error }{ { name: "Valid Session ID", request: func() *http.Request { req := httptest.NewRequest(http.MethodPost, "/", nil) req.Header.Set("X-Vise-Session", "123456") return req }(), expectedID: "123456", expectedError: nil, }, { name: "Missing Session ID", request: httptest.NewRequest(http.MethodPost, "/", nil), expectedID: "", expectedError: handlers.ErrSessionMissing, }, { name: "Invalid Request Type", request: invalidRequestType{}, expectedID: "", expectedError: handlers.ErrInvalidRequest, }, } parser := &DefaultRequestParser{} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { id, err := parser.GetSessionId(tt.request) if id != tt.expectedID { t.Errorf("Expected session ID %s, got %s", tt.expectedID, id) } if err != tt.expectedError { t.Errorf("Expected error %v, got %v", tt.expectedError, err) } }) } } func TestDefaultRequestParser_GetInput(t *testing.T) { tests := []struct { name string request any expectedInput []byte expectedError error }{ { name: "Valid Input", request: func() *http.Request { return httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString("test input")) }(), expectedInput: []byte("test input"), expectedError: nil, }, { name: "Empty Input", request: httptest.NewRequest(http.MethodPost, "/", nil), expectedInput: []byte{}, expectedError: nil, }, { name: "Invalid Request Type", request: invalidRequestType{}, expectedInput: nil, expectedError: handlers.ErrInvalidRequest, }, { name: "Read Error", request: func() *http.Request { return httptest.NewRequest(http.MethodPost, "/", &errorReader{}) }(), expectedInput: nil, expectedError: errors.New("read error"), }, } parser := &DefaultRequestParser{} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { input, err := parser.GetInput(tt.request) if !bytes.Equal(input, tt.expectedInput) { t.Errorf("Expected input %s, got %s", tt.expectedInput, input) } if err != tt.expectedError && (err == nil || err.Error() != tt.expectedError.Error()) { t.Errorf("Expected error %v, got %v", tt.expectedError, err) } }) } }