diff --git a/errors/list.go b/errors/list.go index 6ddf89653..2e3addc71 100644 --- a/errors/list.go +++ b/errors/list.go @@ -264,4 +264,6 @@ var ( ValidationEmptyRules = New("rules can't be empty") ValidationFilterRegisterFailed = New("filter register failed: %v") ValidationRuleRegisterFailed = New("rule register failed: %v") + + ViewTemplateNotExist = New("view template %s does not exist") ) diff --git a/foundation/application.go b/foundation/application.go index a54e16c07..513741bbe 100644 --- a/foundation/application.go +++ b/foundation/application.go @@ -116,6 +116,8 @@ func (r *Application) BootServiceProviders() { console.NewPackageInstallCommand(binding.Bindings, r.GetJson()), console.NewPackageUninstallCommand(binding.Bindings, r.GetJson()), console.NewVendorPublishCommand(r.publishes, r.publishGroups), + console.NewUpCommand(r), + console.NewDownCommand(r), }) r.bootArtisan() } diff --git a/foundation/console/down_command.go b/foundation/console/down_command.go new file mode 100644 index 000000000..766be4507 --- /dev/null +++ b/foundation/console/down_command.go @@ -0,0 +1,142 @@ +package console + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/goravel/framework/contracts/console" + "github.com/goravel/framework/contracts/console/command" + "github.com/goravel/framework/contracts/foundation" + "github.com/goravel/framework/contracts/hash" + "github.com/goravel/framework/contracts/view" + "github.com/goravel/framework/errors" + "github.com/goravel/framework/support/file" + "github.com/goravel/framework/support/path" + "github.com/goravel/framework/support/str" +) + +type DownCommand struct { + app foundation.Application + view view.View + hash hash.Hash +} + +type MaintenanceOptions struct { + Reason string `json:"reason,omitempty"` + Redirect string `json:"redirect,omitempty"` + Render string `json:"render,omitempty"` + Secret string `json:"secret,omitempty"` + Status int `json:"status"` +} + +func NewDownCommand(app foundation.Application) *DownCommand { + return &DownCommand{app, app.MakeView(), app.MakeHash()} +} + +// Signature The name and signature of the console command. +func (r *DownCommand) Signature() string { + return "down" +} + +// Description The console command description. +func (r *DownCommand) Description() string { + return "Put the application into maintenance mode" +} + +// Extend The console command extend. +func (r *DownCommand) Extend() command.Extend { + return command.Extend{ + Flags: []command.Flag{ + &command.StringFlag{ + Name: "reason", + Usage: "The reason for maintenance to show in the response", + Value: "The application is under maintenance", + }, + &command.StringFlag{ + Name: "redirect", + Usage: "The path that the user should be redirected to", + }, + &command.StringFlag{ + Name: "render", + Usage: "The view should be prerendered for display during maintenance mode", + }, + &command.StringFlag{ + Name: "secret", + Usage: "The secret phrase that may be used to bypass the maintenance mode", + }, + &command.BoolFlag{ + Name: "with-secret", + Usage: "Generate a random secret phrase that may be used to bypass the maintenance mode", + }, + &command.IntFlag{ + Name: "status", + Usage: "The status code that should be used when returning the maintenance mode response", + Value: http.StatusServiceUnavailable, + }, + }, + } +} + +// Handle Execute the console command. +func (r *DownCommand) Handle(ctx console.Context) error { + path := path.Storage("framework/maintenance") + + options := MaintenanceOptions{} + + options.Status = ctx.OptionInt("status") + + options.Redirect = ctx.Option("redirect") + + if render := ctx.Option("render"); render != "" { + if r.view.Exists(render) { + options.Render = render + } else { + ctx.Error(errors.ViewTemplateNotExist.Args(render).Error()) + return nil + } + } + + if options.Redirect == "" && options.Render == "" { + options.Reason = ctx.Option("reason") + } + + if secret := ctx.Option("secret"); secret != "" { + hash, err := r.hash.Make(secret) + if err != nil { + ctx.Error(err.Error()) + return nil + } else { + options.Secret = hash + } + } + + if withSecret := ctx.OptionBool("with-secret"); withSecret { + secret := str.Random(40) + hash, err := r.app.MakeHash().Make(secret) + + if err != nil { + ctx.Error(err.Error()) + return nil + } else { + options.Secret = hash + ctx.Info(fmt.Sprintf("Using secret: %s", secret)) + } + } + + jsonBytes, err := json.Marshal(options) + + if err != nil { + ctx.Error(err.Error()) + return nil + } + + if err := file.PutContent(path, string(jsonBytes)); err != nil { + ctx.Error(err.Error()) + return nil + } + + ctx.Success("The application is in maintenance mode now") + + return nil +} diff --git a/foundation/console/down_command_test.go b/foundation/console/down_command_test.go new file mode 100644 index 000000000..0344fc874 --- /dev/null +++ b/foundation/console/down_command_test.go @@ -0,0 +1,261 @@ +package console + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + mock "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + + "github.com/goravel/framework/contracts/console/command" + mocksconsole "github.com/goravel/framework/mocks/console" + mocksfoundation "github.com/goravel/framework/mocks/foundation" + mockshash "github.com/goravel/framework/mocks/hash" + mocksview "github.com/goravel/framework/mocks/view" + "github.com/goravel/framework/support/file" +) + +type DownCommandTestSuite struct { + suite.Suite +} + +func TestDownCommandTestSuite(t *testing.T) { + suite.Run(t, new(DownCommandTestSuite)) +} + +func (s *DownCommandTestSuite) SetupSuite() { +} + +func (s *DownCommandTestSuite) TearDownSuite() { +} + +func (s *DownCommandTestSuite) TestSignature() { + expected := "down" + s.Require().Equal(expected, NewDownCommand(mocksfoundation.NewApplication(s.T())).Signature()) +} + +func (s *DownCommandTestSuite) TestDescription() { + expected := "Put the application into maintenance mode" + s.Require().Equal(expected, NewDownCommand(mocksfoundation.NewApplication(s.T())).Description()) +} + +func (s *DownCommandTestSuite) TestExtend() { + cmd := NewDownCommand(mocksfoundation.NewApplication(s.T())) + got := cmd.Extend() + + s.Equal(6, len(got.Flags)) +} + +func (s *DownCommandTestSuite) TestHandle() { + app := mocksfoundation.NewApplication(s.T()) + tmpfile := filepath.Join(s.T().TempDir(), "/maintenance") + + app.EXPECT().StoragePath("framework/maintenance").Return(tmpfile) + + cmd := NewDownCommand(app) + + mockContext := mocksconsole.NewContext(s.T()) + flag := cmd.Extend().Flags[0].(*command.StringFlag) + mockContext.EXPECT().OptionInt("status").Return(503) + mockContext.EXPECT().Option("render").Return("") + mockContext.EXPECT().Option("redirect").Return("") + mockContext.EXPECT().Option("secret").Return("") + mockContext.EXPECT().OptionBool("with-secret").Return(false) + mockContext.EXPECT().Option("reason").Return(flag.Value) + mockContext.EXPECT().Info("The application is in maintenance mode now") + + err := cmd.Handle(mockContext) + + assert.Nil(s.T(), err) + assert.True(s.T(), file.Exists(tmpfile)) + + content, err := file.GetContent(tmpfile) + + assert.Nil(s.T(), err) + + var maintenanceOptions *MaintenanceOptions + err = json.Unmarshal([]byte(content), &maintenanceOptions) + assert.Nil(s.T(), err) + + assert.Equal(s.T(), flag.Value, maintenanceOptions.Reason) + assert.Equal(s.T(), 503, maintenanceOptions.Status) +} + +func (s *DownCommandTestSuite) TestHandleWithReason() { + app := mocksfoundation.NewApplication(s.T()) + tmpfile := filepath.Join(s.T().TempDir(), "/down_with_reason") + + app.EXPECT().StoragePath("framework/maintenance").Return(tmpfile) + + mockContext := mocksconsole.NewContext(s.T()) + mockContext.EXPECT().OptionInt("status").Return(505) + mockContext.EXPECT().Option("render").Return("") + mockContext.EXPECT().Option("redirect").Return("") + mockContext.EXPECT().Option("secret").Return("") + mockContext.EXPECT().OptionBool("with-secret").Return(false) + mockContext.EXPECT().Option("reason").Return("Under maintenance") + mockContext.EXPECT().Info("The application is in maintenance mode now") + + cmd := NewDownCommand(app) + err := cmd.Handle(mockContext) + + assert.Nil(s.T(), err) + assert.True(s.T(), file.Exists(tmpfile)) + + content, err := file.GetContent(tmpfile) + + assert.Nil(s.T(), err) + var maintenanceOptions *MaintenanceOptions + err = json.Unmarshal([]byte(content), &maintenanceOptions) + assert.Nil(s.T(), err) + + assert.Equal(s.T(), "Under maintenance", maintenanceOptions.Reason) + assert.Equal(s.T(), 505, maintenanceOptions.Status) +} + +func (s *DownCommandTestSuite) TestHandleWithRedirect() { + app := mocksfoundation.NewApplication(s.T()) + tmpfile := filepath.Join(s.T().TempDir(), "/down_with_reason") + + app.EXPECT().StoragePath("framework/maintenance").Return(tmpfile) + + mockContext := mocksconsole.NewContext(s.T()) + mockContext.EXPECT().OptionInt("status").Return(503) + mockContext.EXPECT().Option("render").Return("") + mockContext.EXPECT().Option("redirect").Return("/maintenance") + mockContext.EXPECT().Option("secret").Return("") + mockContext.EXPECT().OptionBool("with-secret").Return(false) + mockContext.EXPECT().Info("The application is in maintenance mode now") + + cmd := NewDownCommand(app) + err := cmd.Handle(mockContext) + + assert.Nil(s.T(), err) + assert.True(s.T(), file.Exists(tmpfile)) + + content, err := file.GetContent(tmpfile) + + assert.Nil(s.T(), err) + var maintenanceOptions *MaintenanceOptions + err = json.Unmarshal([]byte(content), &maintenanceOptions) + assert.Nil(s.T(), err) + + assert.Equal(s.T(), "/maintenance", maintenanceOptions.Redirect) + assert.Equal(s.T(), 503, maintenanceOptions.Status) +} + +func (s *DownCommandTestSuite) TestHandleWithRender() { + app := mocksfoundation.NewApplication(s.T()) + tmpfile := filepath.Join(s.T().TempDir(), "/down_with_reason") + + app.EXPECT().StoragePath("framework/maintenance").Return(tmpfile) + + views := mocksview.NewView(s.T()) + views.EXPECT().Exists("errors/503.tmpl").Return(true) + app.EXPECT().MakeView().Return(views) + + mockContext := mocksconsole.NewContext(s.T()) + mockContext.EXPECT().OptionInt("status").Return(503) + mockContext.EXPECT().Option("render").Return("errors/503.tmpl") + mockContext.EXPECT().Option("redirect").Return("") + mockContext.EXPECT().Option("secret").Return("") + mockContext.EXPECT().OptionBool("with-secret").Return(false) + mockContext.EXPECT().Info("The application is in maintenance mode now") + + cmd := NewDownCommand(app) + err := cmd.Handle(mockContext) + + assert.Nil(s.T(), err) + assert.True(s.T(), file.Exists(tmpfile)) + + content, err := file.GetContent(tmpfile) + + assert.Nil(s.T(), err) + var maintenanceOptions *MaintenanceOptions + err = json.Unmarshal([]byte(content), &maintenanceOptions) + assert.Nil(s.T(), err) + + assert.Equal(s.T(), "errors/503.tmpl", maintenanceOptions.Render) + assert.Equal(s.T(), 503, maintenanceOptions.Status) +} + +func (s *DownCommandTestSuite) TestHandleSecret() { + app := mocksfoundation.NewApplication(s.T()) + tmpfile := filepath.Join(s.T().TempDir(), "/down_with_reason") + + app.EXPECT().StoragePath("framework/maintenance").Return(tmpfile) + + hash := mockshash.NewHash(s.T()) + hash.EXPECT().Make("secretpassword").Return("hashedsecretpassword", nil) + app.EXPECT().MakeHash().Return(hash) + + mockContext := mocksconsole.NewContext(s.T()) + mockContext.EXPECT().OptionInt("status").Return(503) + mockContext.EXPECT().Option("reason").Return("Under maintenance") + mockContext.EXPECT().Option("render").Return("") + mockContext.EXPECT().Option("redirect").Return("") + mockContext.EXPECT().Option("secret").Return("secretpassword") + mockContext.EXPECT().OptionBool("with-secret").Return(false) + mockContext.EXPECT().Info("The application is in maintenance mode now") + + cmd := NewDownCommand(app) + err := cmd.Handle(mockContext) + + assert.Nil(s.T(), err) + assert.True(s.T(), file.Exists(tmpfile)) + + content, err := file.GetContent(tmpfile) + + assert.Nil(s.T(), err) + var maintenanceOptions *MaintenanceOptions + err = json.Unmarshal([]byte(content), &maintenanceOptions) + assert.Nil(s.T(), err) + + assert.Equal(s.T(), "Under maintenance", maintenanceOptions.Reason) + assert.Equal(s.T(), "hashedsecretpassword", maintenanceOptions.Secret) + assert.Equal(s.T(), 503, maintenanceOptions.Status) +} + +func (s *DownCommandTestSuite) TestHandleWithSecret() { + app := mocksfoundation.NewApplication(s.T()) + tmpfile := filepath.Join(s.T().TempDir(), "/down_with_reason") + + app.EXPECT().StoragePath("framework/maintenance").Return(tmpfile) + + hash := mockshash.NewHash(s.T()) + hash.EXPECT().Make(mock.Anything).Return("randomhashedsecretpassword", nil) + app.EXPECT().MakeHash().Return(hash) + + mockContext := mocksconsole.NewContext(s.T()) + mockContext.EXPECT().OptionInt("status").Return(503) + mockContext.EXPECT().Option("reason").Return("Under maintenance") + mockContext.EXPECT().Option("render").Return("") + mockContext.EXPECT().Option("redirect").Return("") + mockContext.EXPECT().Option("secret").Return("") + mockContext.EXPECT().OptionBool("with-secret").Return(true) + mockContext.EXPECT().Info(mock.MatchedBy(func(msg string) bool { + return strings.HasPrefix(msg, "Using secret: ") + })) + mockContext.EXPECT().Info("The application is in maintenance mode now") + + cmd := NewDownCommand(app) + err := cmd.Handle(mockContext) + + assert.Nil(s.T(), err) + assert.True(s.T(), file.Exists(tmpfile)) + + content, err := os.ReadFile(tmpfile) + + assert.Nil(s.T(), err) + var maintenanceOptions *MaintenanceOptions + err = json.Unmarshal(content, &maintenanceOptions) + assert.Nil(s.T(), err) + + assert.Equal(s.T(), "Under maintenance", maintenanceOptions.Reason) + assert.Equal(s.T(), "randomhashedsecretpassword", maintenanceOptions.Secret) + assert.Equal(s.T(), 503, maintenanceOptions.Status) +} diff --git a/foundation/console/up_command.go b/foundation/console/up_command.go new file mode 100644 index 000000000..c77aeb969 --- /dev/null +++ b/foundation/console/up_command.go @@ -0,0 +1,49 @@ +package console + +import ( + "github.com/goravel/framework/contracts/console" + "github.com/goravel/framework/contracts/console/command" + "github.com/goravel/framework/contracts/foundation" + "github.com/goravel/framework/support/file" +) + +type UpCommand struct { + app foundation.Application +} + +func NewUpCommand(app foundation.Application) *UpCommand { + return &UpCommand{app} +} + +// Signature The name and signature of the console command. +func (r *UpCommand) Signature() string { + return "up" +} + +// Description The console command description. +func (r *UpCommand) Description() string { + return "Bring the application out of maintenance mode" +} + +// Extend The console command extend. +func (r *UpCommand) Extend() command.Extend { + return command.Extend{} +} + +// Handle Execute the console command. +func (r *UpCommand) Handle(ctx console.Context) error { + path := r.app.StoragePath("framework/maintenance") + if ok := file.Exists(path); ok { + if err := file.Remove(path); err != nil { + return err + } + + ctx.Success("The application is up and live now") + + return nil + } + + ctx.Error("The application is not in maintenance mode") + + return nil +} diff --git a/foundation/console/up_command_test.go b/foundation/console/up_command_test.go new file mode 100644 index 000000000..c3afc0729 --- /dev/null +++ b/foundation/console/up_command_test.go @@ -0,0 +1,79 @@ +package console + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + + mocksconsole "github.com/goravel/framework/mocks/console" + mocksfoundation "github.com/goravel/framework/mocks/foundation" + "github.com/goravel/framework/support/file" +) + +type UpCommandTestSuite struct { + suite.Suite +} + +func TestUpCommandTestSuite(t *testing.T) { + suite.Run(t, new(UpCommandTestSuite)) +} + +func (s *UpCommandTestSuite) SetupSuite() { +} + +func (s *UpCommandTestSuite) TearDownSuite() { +} + +func (s *UpCommandTestSuite) TestSignature() { + expected := "up" + s.Require().Equal(expected, NewUpCommand(mocksfoundation.NewApplication(s.T())).Signature()) +} + +func (s *UpCommandTestSuite) TestDescription() { + expected := "Bring the application out of maintenance mode" + s.Require().Equal(expected, NewUpCommand(mocksfoundation.NewApplication(s.T())).Description()) +} + +func (s *UpCommandTestSuite) TestExtend() { + cmd := NewUpCommand(mocksfoundation.NewApplication(s.T())) + got := cmd.Extend() + + s.Empty(got) +} + +func (s *UpCommandTestSuite) TestHandle() { + app := mocksfoundation.NewApplication(s.T()) + tmpfile := filepath.Join(s.T().TempDir(), "maintenance_to_test_up") + + fd, err := os.Create(tmpfile) + assert.Nil(s.T(), err) + + err = fd.Close() + assert.Nil(s.T(), err) + + app.EXPECT().StoragePath("framework/maintenance").Return(tmpfile) + + mockContext := mocksconsole.NewContext(s.T()) + mockContext.EXPECT().Info("The application is up and live now") + + cmd := NewUpCommand(app) + err = cmd.Handle(mockContext) + assert.Nil(s.T(), err) + + assert.False(s.T(), file.Exists(tmpfile)) +} + +func (s *UpCommandTestSuite) TestHandleWhenNotDown() { + app := mocksfoundation.NewApplication(s.T()) + app.EXPECT().StoragePath("framework/maintenance").Return(filepath.Join(s.T().TempDir(), "/maintenance_to_when_not_down")) + + mockContext := mocksconsole.NewContext(s.T()) + mockContext.EXPECT().Error("The application is not in maintenance mode") + + cmd := NewUpCommand(app) + err := cmd.Handle(mockContext) + assert.Nil(s.T(), err) +} diff --git a/http/middleware/check_for_maintenance_mode.go b/http/middleware/check_for_maintenance_mode.go new file mode 100644 index 000000000..ae58a534f --- /dev/null +++ b/http/middleware/check_for_maintenance_mode.go @@ -0,0 +1,73 @@ +package middleware + +import ( + "encoding/json" + "os" + + "github.com/goravel/framework/contracts/http" + "github.com/goravel/framework/facades" + "github.com/goravel/framework/foundation/console" + "github.com/goravel/framework/support/file" + "github.com/goravel/framework/support/path" +) + +func CheckForMaintenanceMode() http.Middleware { + return func(ctx http.Context) { + filepath := path.Storage("framework/maintenance") + if !file.Exists(filepath) { + ctx.Request().Next() + return + } + + content, err := os.ReadFile(filepath) + + if err != nil { + if err = ctx.Response().String(http.StatusServiceUnavailable, err.Error()).Abort(); err != nil { + panic(err) + } + return + } + + var maintenanceOptions *console.MaintenanceOptions + err = json.Unmarshal(content, &maintenanceOptions) + + if err != nil { + if err = ctx.Response().String(http.StatusServiceUnavailable, err.Error()).Abort(); err != nil { + panic(err) + } + return + } + + secret := ctx.Request().Query("secret", "") + if secret != "" && maintenanceOptions.Secret != "" { + if facades.Hash().Check(secret, maintenanceOptions.Secret) { + ctx.Request().Next() + return + } + } + + if maintenanceOptions.Redirect != "" { + if ctx.Request().Path() == maintenanceOptions.Redirect { + ctx.Request().Next() + return + } + + if err = ctx.Response().Redirect(http.StatusTemporaryRedirect, maintenanceOptions.Redirect).Abort(); err != nil { + return + } + return + } + + if maintenanceOptions.Render != "" { + ctx.Request().Abort(maintenanceOptions.Status) + if err = ctx.Response().View().Make(maintenanceOptions.Render, map[string]string{}).Render(); err != nil { + return + } + return + } + + if err = ctx.Response().String(maintenanceOptions.Status, maintenanceOptions.Reason).Abort(); err != nil { + panic(err) + } + } +} diff --git a/http/middleware/check_for_maintenance_mode_test.go b/http/middleware/check_for_maintenance_mode_test.go new file mode 100644 index 000000000..12c25100a --- /dev/null +++ b/http/middleware/check_for_maintenance_mode_test.go @@ -0,0 +1,42 @@ +package middleware + +import ( + nethttp "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/goravel/framework/contracts/http" + "github.com/goravel/framework/support/file" + "github.com/goravel/framework/support/path" +) + +func testHttpCheckForMaintenanceMiddleware(next nethttp.Handler) nethttp.Handler { + return nethttp.HandlerFunc(func(w nethttp.ResponseWriter, r *nethttp.Request) { + CheckForMaintenanceMode()(NewTestContext(r.Context(), next, w, r)) + }) +} + +func TestMaintenanceMode(t *testing.T) { + server := httptest.NewServer(testHttpCheckForMaintenanceMiddleware(nethttp.HandlerFunc(func(w nethttp.ResponseWriter, r *nethttp.Request) { + }))) + defer server.Close() + + client := &nethttp.Client{} + + err := file.Create(path.Storage("framework/maintenance"), "") + require.NoError(t, err) + + resp, err := client.Get(server.URL) + require.NoError(t, err) + assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode) + + err = file.Remove(path.Storage("framework/maintenance")) + require.NoError(t, err) + + resp, err = client.Get(server.URL) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} diff --git a/http/middleware/verify_csrf_token_test.go b/http/middleware/verify_csrf_token_test.go index 553117eb8..68cb80080 100644 --- a/http/middleware/verify_csrf_token_test.go +++ b/http/middleware/verify_csrf_token_test.go @@ -1,12 +1,20 @@ package middleware import ( + "context" + "encoding/json" + "io" + "net/http" + "net/url" "testing" + "time" + + "github.com/stretchr/testify/assert" contractshttp "github.com/goravel/framework/contracts/http" + contractsession "github.com/goravel/framework/contracts/session" mockhttp "github.com/goravel/framework/mocks/http" mocksession "github.com/goravel/framework/mocks/session" - "github.com/stretchr/testify/assert" ) func TestTokenMatch(t *testing.T) { @@ -160,3 +168,190 @@ func TestParseExceptPaths(t *testing.T) { }) } } + +type TestContext struct { + ctx context.Context + next http.Handler + request *http.Request + writer http.ResponseWriter +} + +func NewTestContext(ctx context.Context, next http.Handler, w http.ResponseWriter, r *http.Request) *TestContext { + return &TestContext{ + ctx: ctx, + next: next, + request: r, + writer: w, + } +} + +func (r *TestContext) Deadline() (deadline time.Time, ok bool) { + panic("do not need to implement it") +} + +func (r *TestContext) Done() <-chan struct{} { + panic("do not need to implement it") +} + +func (r *TestContext) Err() error { + panic("do not need to implement it") +} + +func (r *TestContext) Value(key any) any { + return r.ctx.Value(key) +} + +func (r *TestContext) Context() context.Context { + return r.ctx +} + +func (r *TestContext) WithContext(context.Context) { + panic("do not need to implement it") +} + +func (r *TestContext) WithValue(key any, value any) { + r.ctx = context.WithValue(r.ctx, key, value) +} + +func (r *TestContext) Request() contractshttp.ContextRequest { + return NewTestRequest(r) +} + +func (r *TestContext) Response() contractshttp.ContextResponse { + return NewTestResponse(r) +} + +type TestRequest struct { + contractshttp.ContextRequest + ctx *TestContext +} + +func NewTestRequest(ctx *TestContext) *TestRequest { + return &TestRequest{ + ctx: ctx, + } +} + +func (r *TestRequest) Path() string { + if r.ctx != nil && r.ctx.request != nil { + return r.ctx.request.URL.Path + } + return "" +} + +func (r *TestRequest) Ip() string { + return "127.0.0.1" +} + +func (r *TestRequest) Cookie(key string, defaultValue ...string) string { + cookie, err := r.ctx.request.Cookie(key) + if err != nil { + if len(defaultValue) > 0 { + return defaultValue[0] + } + + return "" + } + + val, _ := url.QueryUnescape(cookie.Value) + return val +} + +func (r *TestRequest) HasSession() bool { + if r.ctx == nil { + return false + } + session := r.Session() + return session != nil +} + +func (r *TestRequest) Session() contractsession.Session { + s, ok := r.ctx.Value("session").(contractsession.Session) + if !ok { + return nil + } + return s +} + +func (r *TestRequest) SetSession(session contractsession.Session) contractshttp.ContextRequest { + r.ctx.WithValue("session", session) + r.ctx.request = r.ctx.request.WithContext(r.ctx.Context()) + return r +} + +func (r *TestRequest) Abort(code ...int) { + r.ctx.writer.WriteHeader(code[0]) +} + +func (r *TestRequest) Next() { + if r.ctx != nil && r.ctx.next != nil { + r.ctx.next.ServeHTTP(r.ctx.writer, r.ctx.request.WithContext(r.ctx.Context())) + } +} + +func (r *TestRequest) Method() string { + return r.ctx.request.Method +} + +func (r *TestRequest) Header(key string, defaultValue ...string) string { + headerValue := r.ctx.request.Header.Get(key) + if len(headerValue) > 0 { + return headerValue + } else if len(defaultValue) > 0 { + return defaultValue[0] + } + return "" +} + +func (r *TestRequest) Input(key string, defualtVaue ...string) string { + if body, err := io.ReadAll(r.ctx.request.Body); err != nil { + if len(defualtVaue) > 0 { + return defualtVaue[0] + } + return "" + } else { + data := map[string]any{} + if err := json.Unmarshal(body, &data); err != nil { + return "" + } + if data[key] == nil { + return "" + } + return data[key].(string) + } + +} + +type TestResponse struct { + contractshttp.ContextResponse + ctx *TestContext +} + +func NewTestResponse(ctx *TestContext) *TestResponse { + return &TestResponse{ + ctx: ctx, + } +} + +func (r *TestResponse) Cookie(cookie contractshttp.Cookie) contractshttp.ContextResponse { + path := cookie.Path + if path == "" { + path = "/" + } + http.SetCookie(r.ctx.writer, &http.Cookie{ + Name: cookie.Name, + Value: url.QueryEscape(cookie.Value), + MaxAge: cookie.MaxAge, + Path: path, + Domain: cookie.Domain, + Secure: cookie.Secure, + HttpOnly: cookie.HttpOnly, + }) + + return r +} + +func (r *TestResponse) Header(key string, value string) contractshttp.ContextResponse { + r.ctx.writer.Header().Set(key, value) + return r +}