Skip to content

Conversation

@praem90
Copy link
Contributor

@praem90 praem90 commented Sep 14, 2025

📑 Description

This PR adds two Artisan commands

  1. Up
  2. Down
go run . artisan down
go run . artisan up

The down commands creates a temporary file in the framework storage folder
The up commands removes that file if exists

There is an another middleware CheckForMaintenance, that checks for the file.
It returns the request with 503 status code if the file exists else allow the request to passthrough.

Closes goravel/goravel#546

✅ Checks

  • [ x ] Added test cases for my code

@praem90 praem90 requested a review from a team as a code owner September 14, 2025 15:52
@codecov
Copy link

codecov bot commented Sep 14, 2025

Codecov Report

❌ Patch coverage is 68.00000% with 40 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.25%. Comparing base (bedddf4) to head (65fd56a).

Files with missing lines Patch % Lines
http/middleware/check_for_maintenance.go 35.29% 21 Missing and 1 partial ⚠️
foundation/console/down_command.go 80.55% 7 Missing and 7 partials ⚠️
foundation/application.go 0.00% 2 Missing ⚠️
foundation/console/up_command.go 88.23% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1198      +/-   ##
==========================================
- Coverage   70.27%   70.25%   -0.02%     
==========================================
  Files         281      284       +3     
  Lines       17121    17246     +125     
==========================================
+ Hits        12031    12116      +85     
- Misses       4579     4610      +31     
- Partials      511      520       +9     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@almas-x
Copy link
Contributor

almas-x commented Sep 14, 2025

This is a fantastic PR with a very useful addition to the framework! 🎉

I have a suggestion: how about allowing an optional reason flag for the down command? This could record the reason or announcement for the maintenance, and store it in the framework/down file. The middleware could then read this reason and return it, making it available for APIsor views to display. This would enhance the user experience by providing more context during maintenance mode.


func (s *DownCommandTestSuite) TestHandle() {
app := mocksfoundation.NewApplication(s.T())
tmpfile := filepath.Join(os.TempDir(), "/down")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In unit tests, use t.TempDir() instead of os.TempDir().

t.TempDir() is preferred in unit tests because it creates a unique temporary directory for each test and automatically cleans it up after the test finishes. This helps prevent conflicts and side effects between tests, making your tests safer and more reliable.

@hwbrzzl
Copy link
Contributor

hwbrzzl commented Sep 15, 2025

Please fix the windows CI.

@hwbrzzl
Copy link
Contributor

hwbrzzl commented Sep 15, 2025

Thanks, great job 👍 Could add some related screenshots in the PR description?

func CheckForMaintenance() http.Middleware {
return func(ctx http.Context) {
if file.Exists(path.Storage("framework/down")) {
ctx.Request().AbortWithStatus(http.StatusServiceUnavailable)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ctx.Request().AbortWithStatus(http.StatusServiceUnavailable)
ctx.Request().Abort(http.StatusServiceUnavailable)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


// Handle Execute the console command.
func (r *DownCommand) Handle(ctx console.Context) error {
path := r.app.StoragePath("framework/down")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
path := r.app.StoragePath("framework/down")
path := r.app.StoragePath("framework/maintenance")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


// Extend The console command extend.
func (r *DownCommand) Extend() command.Extend {
return command.Extend{}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you implement these options as well?

image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is reason still required in this case? @almas-x

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I can implement those options @hwbrzzl

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compared to Laravel, the current implementation of reason is indeed a bit limited. I think we can refer to Laravel's implementation and support all the options in your screenshot except for the two that are crossed out.

Copy link
Contributor Author

@praem90 praem90 Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. What should be the response would be when no --render option is provided?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning the current logic should be fine.

@praem90 praem90 force-pushed the feat-546-artisan-command-up-and-down branch from 3479a4b to b70c346 Compare September 18, 2025 08:16
@praem90 praem90 force-pushed the feat-546-artisan-command-up-and-down branch from e635bf5 to 07bc616 Compare January 4, 2026 15:58
@hwbrzzl hwbrzzl force-pushed the feat-546-artisan-command-up-and-down branch from a224b58 to 63f223a Compare January 11, 2026 13:42
@hwbrzzl
Copy link
Contributor

hwbrzzl commented Jan 13, 2026

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces up and down artisan commands to manage maintenance mode, along with a middleware to enforce it. The implementation is a good start, but there are several areas for improvement. My review focuses on enhancing error handling within the new commands to provide clearer feedback for scripting and automation, and on improving the test coverage for the new maintenance middleware to ensure all its features are working as expected. I've also identified a few issues in a test helper function that could lead to flaky tests.

Comment on lines +101 to +106
hash, err := r.app.MakeHash().Make(secret)
if err != nil {
ctx.Error("Unable to generate and hash the secret")
} else {
options.Secret = hash
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When hashing the provided secret fails, an error is printed, but the command continues its execution. This can lead to the application entering maintenance mode without the intended secret protection, which could be a security risk if the secret is expected for bypass. The function should return the error to halt execution.

Suggested change
hash, err := r.app.MakeHash().Make(secret)
if err != nil {
ctx.Error("Unable to generate and hash the secret")
} else {
options.Secret = hash
}
hash, err := r.app.MakeHash().Make(secret)
if err != nil {
ctx.Error("Unable to generate and hash the secret")
return err
}
options.Secret = hash

Comment on lines +22 to +42
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)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The test for CheckForMaintenance middleware is incomplete. It only covers the case where an empty maintenance file exists, which causes a JSON unmarshalling error and results in a 503 status. The test suite should be expanded to cover all the features of the maintenance mode:

  • A valid maintenance file with a reason and custom status.
  • The redirect option.
  • The render option.
  • The secret bypass mechanism.

Without these tests, the middleware's core logic is not verified.

Comment on lines +306 to +323
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)
}

}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This implementation of Input has several issues:

  1. Request body consumption: io.ReadAll(r.ctx.request.Body) consumes the request body. Subsequent calls to Input (or any other method that reads the body) within the same request lifecycle will find an empty body, leading to unpredictable test failures. The body should be read once and cached.
  2. Unsafe type assertion: data[key].(string) is an unsafe type assertion that will panic if the value is not a string (e.g., a number). You should use the two-variable form: val, ok := data[key].(string).
  3. Error handling: If json.Unmarshal fails, it returns an empty string, silently swallowing the error. This can make debugging tests difficult.
  4. Content-Type assumption: The method assumes the request body is JSON. It doesn't handle other common content types like application/x-www-form-urlencoded.

A more robust implementation should be considered to avoid these problems.

Comment on lines +114 to +115
ctx.Error("Unable to generate and hash the secret")
return nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When hashing the auto-generated secret fails, an error is printed, but the function returns nil, indicating success. This is misleading and prevents proper error handling by the caller. The function should propagate the underlying error.

Suggested change
ctx.Error("Unable to generate and hash the secret")
return nil
ctx.Error("Unable to generate and hash the secret")
return err

Comment on lines +76 to +82
content, err := file.GetContent(tmpfile)

assert.Nil(s.T(), err)

var maintenanceOptions *MaintenanceOptions
err = json.Unmarshal([]byte(content), &maintenanceOptions)
assert.Nil(s.T(), err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In this test and others (TestHandleWithReason, TestHandleWithRedirect, TestHandleWithRender, TestHandleSecret), file.GetContent is used to read the file, which returns a string. This string is then converted back to a byte slice for json.Unmarshal. It would be more efficient and consistent with TestHandleWithSecret to use os.ReadFile directly, which returns a byte slice.

Suggested change
content, err := file.GetContent(tmpfile)
assert.Nil(s.T(), err)
var maintenanceOptions *MaintenanceOptions
err = json.Unmarshal([]byte(content), &maintenanceOptions)
assert.Nil(s.T(), err)
content, err := os.ReadFile(tmpfile)
assert.Nil(s.T(), err)
var maintenanceOptions *MaintenanceOptions
err = json.Unmarshal(content, &maintenanceOptions)
assert.Nil(s.T(), err)


ctx.Error("The application is not in maintenance mode")

return nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When the application is not in maintenance mode, the command prints an error but returns nil, indicating success. This can be misleading for scripts that rely on exit codes. It would be better to return an error to signal that the command's intended action was not performed. You will also need to import the fmt package.

Suggested change
return nil
return fmt.Errorf("the application is not in maintenance mode")

return
}

// Checking err to suppress the linter
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This comment is a code smell. It implies that the error check is only present to satisfy the linter, not for actual error handling. If the error is not important, it should be explicitly ignored with _ = .... If it is important, it should be handled properly (e.g., logged), and this comment should be removed.

Copy link
Contributor

@hwbrzzl hwbrzzl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, great PR! Just left a few questions.


// Handle Execute the console command.
func (r *DownCommand) Handle(ctx console.Context) error {
path := r.app.StoragePath("framework/maintenance")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use path.Storage() instead.

}

if render := ctx.Option("render"); render != "" {
if r.app.MakeView().Exists(render) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please inject View to the command directly instead of App, the same Hash.

if r.app.MakeView().Exists(render) {
options.Render = render
} else {
ctx.Error("Unable to find the view template")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add the error to errors/list.go, the same other errors.

Comment on lines +83 to +86
if redirect := ctx.Option("redirect"); redirect != "" {
options.Redirect = redirect
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if redirect := ctx.Option("redirect"); redirect != "" {
options.Redirect = redirect
}
options.Redirect = ctx.Option("redirect")

if secret := ctx.Option("secret"); secret != "" {
hash, err := r.app.MakeHash().Make(secret)
if err != nil {
ctx.Error("Unable to generate and hash the secret")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return nil here.

"github.com/goravel/framework/support/path"
)

func CheckForMaintenance() http.Middleware {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func CheckForMaintenance() http.Middleware {
func CheckForMaintenanceMode() http.Middleware {

content, err := os.ReadFile(filepath)

if err != nil {
ctx.Request().Abort(http.StatusServiceUnavailable)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ctx.Request().Abort(http.StatusServiceUnavailable)
ctx.Response().String(http.StatusServiceUnavailable, err.Error()).Abort()

}

if err = ctx.Response().Redirect(http.StatusTemporaryRedirect, maintenanceOptions.Redirect).Abort(); err != nil {
return
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about panic in such cases? The panic will be caught and logged.

})
}

func TestMaintenanceMode(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please cover more cases.


func testHttpCheckForMaintenanceMiddleware(next nethttp.Handler) nethttp.Handler {
return nethttp.HandlerFunc(func(w nethttp.ResponseWriter, r *nethttp.Request) {
CheckForMaintenance()(NewTestContext(r.Context(), next, w, r))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to use a mock context instead of creating a test one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ [Feature] artisan command up and down to set website Maintenance mode

3 participants