diff --git a/contracts/testing/assertable_json.go b/contracts/testing/assertable_json.go index f6e3be5e1..426745393 100644 --- a/contracts/testing/assertable_json.go +++ b/contracts/testing/assertable_json.go @@ -1,16 +1,16 @@ package testing type AssertableJSON interface { - Json() map[string]any Count(key string, value int) AssertableJSON + First(key string, callback func(AssertableJSON)) AssertableJSON + Each(key string, callback func(AssertableJSON)) AssertableJSON Has(key string) AssertableJSON HasAll(keys []string) AssertableJSON HasAny(keys []string) AssertableJSON + HasWithScope(key string, length int, callback func(AssertableJSON)) AssertableJSON + Json() map[string]any Missing(key string) AssertableJSON MissingAll(keys []string) AssertableJSON Where(key string, value any) AssertableJSON WhereNot(key string, value any) AssertableJSON - Each(key string, callback func(AssertableJSON)) AssertableJSON - First(key string, callback func(AssertableJSON)) AssertableJSON - HasWithScope(key string, length int, callback func(AssertableJSON)) AssertableJSON } diff --git a/contracts/testing/test_request.go b/contracts/testing/test_request.go index 9d8927855..56c71b6fe 100644 --- a/contracts/testing/test_request.go +++ b/contracts/testing/test_request.go @@ -16,6 +16,7 @@ type TestRequest interface { WithToken(token string, ttype ...string) TestRequest WithBasicAuth(username, password string) TestRequest WithoutToken() TestRequest + WithSession(attributes map[string]any) TestRequest Get(uri string) (TestResponse, error) Post(uri string, body io.Reader) (TestResponse, error) Put(uri string, body io.Reader) (TestResponse, error) diff --git a/contracts/testing/test_response.go b/contracts/testing/test_response.go index fcf0fb1e7..ea83ce68d 100644 --- a/contracts/testing/test_response.go +++ b/contracts/testing/test_response.go @@ -1,10 +1,17 @@ package testing +import "net/http" + type TestResponse interface { - IsSuccessful() bool - IsServerError() bool Content() (string, error) + Cookie(name string) *http.Cookie + Cookies() []*http.Cookie + Headers() http.Header + IsServerError() bool + IsSuccessful() bool Json() (map[string]any, error) + Session() (map[string]any, error) + AssertStatus(status int) TestResponse AssertOk() TestResponse AssertCreated() TestResponse diff --git a/errors/list.go b/errors/list.go index af3d73411..f1b7065db 100644 --- a/errors/list.go +++ b/errors/list.go @@ -13,6 +13,7 @@ var ( StorageFacadeNotSet = New("storage facade is not initialized") InvalidHttpContext = New("invalid http context") RouteFacadeNotSet = New("route facade is not initialized") + SessionFacadeNotSet = New("session facade is not initialized") AuthEmptySecret = New("authentication secret is missing or required") AuthInvalidClaims = New("authentication token contains invalid claims") diff --git a/mocks/testing/TestRequest.go b/mocks/testing/TestRequest.go index 06cfc88e9..adbedaa92 100644 --- a/mocks/testing/TestRequest.go +++ b/mocks/testing/TestRequest.go @@ -772,6 +772,54 @@ func (_c *TestRequest_WithHeaders_Call) RunAndReturn(run func(map[string]string) return _c } +// WithSession provides a mock function with given fields: attributes +func (_m *TestRequest) WithSession(attributes map[string]any) testing.TestRequest { + ret := _m.Called(attributes) + + if len(ret) == 0 { + panic("no return value specified for WithSession") + } + + var r0 testing.TestRequest + if rf, ok := ret.Get(0).(func(map[string]any) testing.TestRequest); ok { + r0 = rf(attributes) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(testing.TestRequest) + } + } + + return r0 +} + +// TestRequest_WithSession_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithSession' +type TestRequest_WithSession_Call struct { + *mock.Call +} + +// WithSession is a helper method to define mock.On call +// - attributes map[string]any +func (_e *TestRequest_Expecter) WithSession(attributes interface{}) *TestRequest_WithSession_Call { + return &TestRequest_WithSession_Call{Call: _e.mock.On("WithSession", attributes)} +} + +func (_c *TestRequest_WithSession_Call) Run(run func(attributes map[string]any)) *TestRequest_WithSession_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(map[string]any)) + }) + return _c +} + +func (_c *TestRequest_WithSession_Call) Return(_a0 testing.TestRequest) *TestRequest_WithSession_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *TestRequest_WithSession_Call) RunAndReturn(run func(map[string]any) testing.TestRequest) *TestRequest_WithSession_Call { + _c.Call.Return(run) + return _c +} + // WithToken provides a mock function with given fields: token, ttype func (_m *TestRequest) WithToken(token string, ttype ...string) testing.TestRequest { _va := make([]interface{}, len(ttype)) diff --git a/mocks/testing/TestResponse.go b/mocks/testing/TestResponse.go index fbc9016df..cb9f1a03c 100644 --- a/mocks/testing/TestResponse.go +++ b/mocks/testing/TestResponse.go @@ -3,6 +3,8 @@ package testing import ( + http "net/http" + testing "github.com/goravel/framework/contracts/testing" mock "github.com/stretchr/testify/mock" ) @@ -2030,6 +2032,148 @@ func (_c *TestResponse_Content_Call) RunAndReturn(run func() (string, error)) *T return _c } +// Cookie provides a mock function with given fields: name +func (_m *TestResponse) Cookie(name string) *http.Cookie { + ret := _m.Called(name) + + if len(ret) == 0 { + panic("no return value specified for Cookie") + } + + var r0 *http.Cookie + if rf, ok := ret.Get(0).(func(string) *http.Cookie); ok { + r0 = rf(name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*http.Cookie) + } + } + + return r0 +} + +// TestResponse_Cookie_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Cookie' +type TestResponse_Cookie_Call struct { + *mock.Call +} + +// Cookie is a helper method to define mock.On call +// - name string +func (_e *TestResponse_Expecter) Cookie(name interface{}) *TestResponse_Cookie_Call { + return &TestResponse_Cookie_Call{Call: _e.mock.On("Cookie", name)} +} + +func (_c *TestResponse_Cookie_Call) Run(run func(name string)) *TestResponse_Cookie_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *TestResponse_Cookie_Call) Return(_a0 *http.Cookie) *TestResponse_Cookie_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *TestResponse_Cookie_Call) RunAndReturn(run func(string) *http.Cookie) *TestResponse_Cookie_Call { + _c.Call.Return(run) + return _c +} + +// Cookies provides a mock function with given fields: +func (_m *TestResponse) Cookies() []*http.Cookie { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Cookies") + } + + var r0 []*http.Cookie + if rf, ok := ret.Get(0).(func() []*http.Cookie); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*http.Cookie) + } + } + + return r0 +} + +// TestResponse_Cookies_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Cookies' +type TestResponse_Cookies_Call struct { + *mock.Call +} + +// Cookies is a helper method to define mock.On call +func (_e *TestResponse_Expecter) Cookies() *TestResponse_Cookies_Call { + return &TestResponse_Cookies_Call{Call: _e.mock.On("Cookies")} +} + +func (_c *TestResponse_Cookies_Call) Run(run func()) *TestResponse_Cookies_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *TestResponse_Cookies_Call) Return(_a0 []*http.Cookie) *TestResponse_Cookies_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *TestResponse_Cookies_Call) RunAndReturn(run func() []*http.Cookie) *TestResponse_Cookies_Call { + _c.Call.Return(run) + return _c +} + +// Headers provides a mock function with given fields: +func (_m *TestResponse) Headers() http.Header { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Headers") + } + + var r0 http.Header + if rf, ok := ret.Get(0).(func() http.Header); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(http.Header) + } + } + + return r0 +} + +// TestResponse_Headers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Headers' +type TestResponse_Headers_Call struct { + *mock.Call +} + +// Headers is a helper method to define mock.On call +func (_e *TestResponse_Expecter) Headers() *TestResponse_Headers_Call { + return &TestResponse_Headers_Call{Call: _e.mock.On("Headers")} +} + +func (_c *TestResponse_Headers_Call) Run(run func()) *TestResponse_Headers_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *TestResponse_Headers_Call) Return(_a0 http.Header) *TestResponse_Headers_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *TestResponse_Headers_Call) RunAndReturn(run func() http.Header) *TestResponse_Headers_Call { + _c.Call.Return(run) + return _c +} + // IsServerError provides a mock function with given fields: func (_m *TestResponse) IsServerError() bool { ret := _m.Called() @@ -2177,6 +2321,63 @@ func (_c *TestResponse_Json_Call) RunAndReturn(run func() (map[string]any, error return _c } +// Session provides a mock function with given fields: +func (_m *TestResponse) Session() (map[string]any, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Session") + } + + var r0 map[string]any + var r1 error + if rf, ok := ret.Get(0).(func() (map[string]any, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() map[string]any); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]any) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// TestResponse_Session_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Session' +type TestResponse_Session_Call struct { + *mock.Call +} + +// Session is a helper method to define mock.On call +func (_e *TestResponse_Expecter) Session() *TestResponse_Session_Call { + return &TestResponse_Session_Call{Call: _e.mock.On("Session")} +} + +func (_c *TestResponse_Session_Call) Run(run func()) *TestResponse_Session_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *TestResponse_Session_Call) Return(_a0 map[string]any, _a1 error) *TestResponse_Session_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *TestResponse_Session_Call) RunAndReturn(run func() (map[string]any, error)) *TestResponse_Session_Call { + _c.Call.Return(run) + return _c +} + // NewTestResponse creates a new instance of TestResponse. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewTestResponse(t interface { diff --git a/testing/service_provider.go b/testing/service_provider.go index 5d04fa310..d7f9f439c 100644 --- a/testing/service_provider.go +++ b/testing/service_provider.go @@ -4,6 +4,7 @@ import ( contractsconsole "github.com/goravel/framework/contracts/console" "github.com/goravel/framework/contracts/foundation" contractsroute "github.com/goravel/framework/contracts/route" + contractsession "github.com/goravel/framework/contracts/session" "github.com/goravel/framework/errors" "github.com/goravel/framework/support/color" ) @@ -12,6 +13,7 @@ const Binding = "goravel.testing" var artisanFacade contractsconsole.Artisan var routeFacade contractsroute.Route +var sessionFacade contractsession.Manager type ServiceProvider struct { } @@ -32,4 +34,9 @@ func (receiver *ServiceProvider) Boot(app foundation.Application) { if routeFacade == nil { color.Errorln(errors.RouteFacadeNotSet.SetModule(errors.ModuleTesting)) } + + sessionFacade = app.MakeSession() + if sessionFacade == nil { + color.Errorln(errors.SessionFacadeNotSet.SetModule(errors.ModuleTesting)) + } } diff --git a/testing/test_request.go b/testing/test_request.go index c05008f77..b45752118 100644 --- a/testing/test_request.go +++ b/testing/test_request.go @@ -16,18 +16,20 @@ import ( ) type TestRequest struct { - t *testing.T - ctx context.Context - defaultHeaders map[string]string - defaultCookies map[string]string + t *testing.T + ctx context.Context + defaultHeaders map[string]string + defaultCookies map[string]string + sessionAttributes map[string]any } func NewTestRequest(t *testing.T) contractstesting.TestRequest { return &TestRequest{ - t: t, - ctx: context.Background(), - defaultHeaders: make(map[string]string), - defaultCookies: make(map[string]string), + t: t, + ctx: context.Background(), + defaultHeaders: make(map[string]string), + defaultCookies: make(map[string]string), + sessionAttributes: make(map[string]any), } } @@ -83,6 +85,11 @@ func (r *TestRequest) WithoutToken() contractstesting.TestRequest { return r.WithoutHeader("Authorization") } +func (r *TestRequest) WithSession(attributes map[string]any) contractstesting.TestRequest { + r.sessionAttributes = collect.Merge(r.sessionAttributes, attributes) + return r +} + func (r *TestRequest) Get(uri string) (contractstesting.TestResponse, error) { return r.call(http.MethodGet, uri, nil) } @@ -112,6 +119,11 @@ func (r *TestRequest) Options(uri string) (contractstesting.TestResponse, error) } func (r *TestRequest) call(method string, uri string, body io.Reader) (contractstesting.TestResponse, error) { + err := r.setSession() + if err != nil { + return nil, err + } + req := httptest.NewRequest(method, uri, body).WithContext(r.ctx) for key, value := range r.defaultHeaders { @@ -134,3 +146,39 @@ func (r *TestRequest) call(method string, uri string, body io.Reader) (contracts return NewTestResponse(r.t, response), nil } + +func (r *TestRequest) setSession() error { + if len(r.sessionAttributes) == 0 { + return nil + } + + if sessionFacade == nil { + return errors.SessionFacadeNotSet + } + + // Retrieve session driver + driver, err := sessionFacade.Driver() + if err != nil { + return err + } + + // Build session + session, err := sessionFacade.BuildSession(driver) + if err != nil { + return err + } + + for key, value := range r.sessionAttributes { + session.Put(key, value) + } + + r.WithCookie(session.GetName(), session.GetID()) + + if err = session.Save(); err != nil { + return err + } + + // Release session + sessionFacade.ReleaseSession(session) + return nil +} diff --git a/testing/test_request_test.go b/testing/test_request_test.go new file mode 100644 index 000000000..4cf040e7f --- /dev/null +++ b/testing/test_request_test.go @@ -0,0 +1,134 @@ +package testing + +import ( + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/goravel/framework/errors" + mockssession "github.com/goravel/framework/mocks/session" +) + +type TestRequestSuite struct { + suite.Suite + mockSessionManager *mockssession.Manager +} + +func TestTestRequestSuite(t *testing.T) { + suite.Run(t, new(TestRequestSuite)) +} + +// SetupTest will run before each test in the suite. +func (s *TestRequestSuite) SetupTest() { + s.mockSessionManager = mockssession.NewManager(s.T()) + sessionFacade = s.mockSessionManager +} + +func (s *TestRequestSuite) TearDownTest() { + sessionFacade = nil +} + +func (s *TestRequestSuite) TestSetSessionErrors() { + type testCase struct { + name string + mockBehavior func(mockDriver *mockssession.Driver, mockSession *mockssession.Session) + expectedError string + sessionAttributes map[string]any + } + + cases := []testCase{ + { + name: "DriverError", + mockBehavior: func(mockDriver *mockssession.Driver, mockSession *mockssession.Session) { + s.mockSessionManager.On("Driver").Return(nil, errors.New("driver retrieval error")).Once() + }, + expectedError: "driver retrieval error", + sessionAttributes: map[string]any{"user_id": 123}, + }, + { + name: "BuildSessionError", + mockBehavior: func(mockDriver *mockssession.Driver, mockSession *mockssession.Session) { + s.mockSessionManager.On("Driver").Return(mockDriver, nil).Once() + s.mockSessionManager.On("BuildSession", mockDriver).Return(nil, errors.New("build session error")).Once() + }, + expectedError: "build session error", + sessionAttributes: map[string]any{"user_id": 123}, + }, + { + name: "SaveError", + mockBehavior: func(mockDriver *mockssession.Driver, mockSession *mockssession.Session) { + s.mockSessionManager.On("Driver").Return(mockDriver, nil).Once() + s.mockSessionManager.On("BuildSession", mockDriver).Return(mockSession, nil).Once() + + mockSession.On("Put", "user_id", 123).Return(mockSession).Once() + + mockSession.On("GetName").Return("session_name").Once() + mockSession.On("GetID").Return("session_id").Once() + + mockSession.On("Save").Return(errors.New("session save error")).Once() + }, + expectedError: "session save error", + sessionAttributes: map[string]any{"user_id": 123}, + }, + } + + for _, tc := range cases { + s.Run(tc.name, func() { + mockDriver := mockssession.NewDriver(s.T()) + mockSession := mockssession.NewSession(s.T()) + + tc.mockBehavior(mockDriver, mockSession) + + request := NewTestRequest(s.T()).WithSession(tc.sessionAttributes) + + err := request.(*TestRequest).setSession() + + if tc.expectedError == "" { + s.NoError(err) + } else { + s.EqualError(err, tc.expectedError) + } + }) + } +} + +func (s *TestRequestSuite) TestSetSessionUsingWithSession() { + mockDriver := mockssession.NewDriver(s.T()) + mockSession := mockssession.NewSession(s.T()) + + sessionAttributes := map[string]any{ + "user_id": 123, + "user_role": "admin", + } + + s.mockSessionManager.On("Driver").Return(mockDriver, nil).Once() + + s.mockSessionManager.On("BuildSession", mockDriver).Return(mockSession, nil).Once() + + for key, value := range sessionAttributes { + mockSession.On("Put", key, value).Return(mockSession).Once() + } + + mockSession.On("GetName").Return("session_name").Once() + mockSession.On("GetID").Return("session_id").Once() + + mockSession.On("Save").Return(nil).Once() + s.mockSessionManager.On("ReleaseSession", mockSession).Once() + + request := NewTestRequest(s.T()).WithSession(sessionAttributes) + + err := request.(*TestRequest).setSession() + + s.NoError(err) +} + +func (s *TestRequestSuite) TestSetSessionUsingWithoutSession() { + request := NewTestRequest(s.T()) + + err := request.(*TestRequest).setSession() + + s.NoError(err) + + s.mockSessionManager.AssertNotCalled(s.T(), "Driver") + s.mockSessionManager.AssertNotCalled(s.T(), "BuildSession", mockssession.NewDriver(s.T())) +} diff --git a/testing/test_response.go b/testing/test_response.go index 1374685b3..c9b1daeae 100644 --- a/testing/test_response.go +++ b/testing/test_response.go @@ -13,14 +13,16 @@ import ( "github.com/stretchr/testify/assert" contractstesting "github.com/goravel/framework/contracts/testing" + "github.com/goravel/framework/errors" "github.com/goravel/framework/support/carbon" ) type TestResponseImpl struct { - t *testing.T - mu sync.Mutex - response *http.Response - content string + t *testing.T + mu sync.Mutex + response *http.Response + content string + sessionAttributes map[string]any } func NewTestResponse(t *testing.T, response *http.Response) contractstesting.TestResponse { @@ -41,6 +43,45 @@ func (r *TestResponseImpl) Json() (map[string]any, error) { return testAble.Json(), nil } +func (r *TestResponseImpl) Headers() http.Header { + return r.response.Header +} + +func (r *TestResponseImpl) Cookies() []*http.Cookie { + return r.response.Cookies() +} + +func (r *TestResponseImpl) Cookie(name string) *http.Cookie { + return r.getCookie(name) +} + +func (r *TestResponseImpl) Session() (map[string]any, error) { + if r.sessionAttributes != nil { + return r.sessionAttributes, nil + } + + if sessionFacade == nil { + return nil, errors.SessionFacadeNotSet + } + + // Retrieve session driver + driver, err := sessionFacade.Driver() + if err != nil { + return nil, err + } + + // Build session + session, err := sessionFacade.BuildSession(driver) + if err != nil { + return nil, err + } + + r.sessionAttributes = session.All() + sessionFacade.ReleaseSession(session) + + return r.sessionAttributes, nil +} + func (r *TestResponseImpl) IsSuccessful() bool { statusCode := r.getStatusCode() return statusCode >= 200 && statusCode < 300 diff --git a/testing/test_response_test.go b/testing/test_response_test.go index 2e1be1c41..0ed78e25a 100644 --- a/testing/test_response_test.go +++ b/testing/test_response_test.go @@ -8,7 +8,11 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + contractstesting "github.com/goravel/framework/contracts/testing" + "github.com/goravel/framework/errors" + mockssession "github.com/goravel/framework/mocks/session" ) func TestAssertOk(t *testing.T) { @@ -315,6 +319,71 @@ func TestAssertSeeInOrderWithEscape(t *testing.T) { r.AssertSeeInOrder([]string{"Hello,", "