-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: Extract AppEngine Cron source check into middleware (#99)
This de-duplicates the code we have to check the AppEngine Cron request header and pushes it into the HTTP router and out of the app logic. As a convenient side effect, this also ensures that any cron job failure produces an HTTP 500 response (indicating failure to AppEngine) and an ERROR-severity log line (which we're starting to use for alerts).
- Loading branch information
Showing
4 changed files
with
138 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"log/slog" | ||
"net/http" | ||
) | ||
|
||
// JobFunc is the type of function that can run as a cron job. | ||
type JobFunc func(context.Context) error | ||
|
||
// cron wraps a job function to make it an HTTP handler. The handler enforces | ||
// that requests originate from App Engine's Cron scheduler. | ||
func cron(job JobFunc) http.HandlerFunc { | ||
return func(w http.ResponseWriter, r *http.Request) { | ||
// Check that the request is originating from within app engine | ||
// https://cloud.google.com/appengine/docs/standard/go/scheduling-jobs-with-cron-yaml#validating_cron_requests | ||
if r.Header.Get("X-Appengine-Cron") != "true" { | ||
http.NotFound(w, r) | ||
return | ||
} | ||
|
||
err := job(r.Context()) | ||
if err != nil { | ||
slog.Error("Job failed", slog.Any("error", err)) | ||
w.WriteHeader(http.StatusInternalServerError) | ||
return | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
"github.com/recursecenter/pairing-bot/internal/assert" | ||
) | ||
|
||
func Test_cron(t *testing.T) { | ||
t.Run("run job for AppEngine", func(t *testing.T) { | ||
// Arrange a cron job that tells us whether it ran. | ||
ran := false | ||
handler := cron(func(context.Context) error { | ||
ran = true | ||
return nil | ||
}) | ||
|
||
// Prepare an AppEngine-sourced request. | ||
req := httptest.NewRequest(http.MethodGet, "/", nil) | ||
req.Header.Set("X-Appengine-Cron", "true") | ||
|
||
// Run it! | ||
w := httptest.NewRecorder() | ||
handler(w, req) | ||
|
||
resp := w.Result() | ||
defer resp.Body.Close() | ||
|
||
assert.Equal(t, ran, true) | ||
assert.Equal(t, resp.StatusCode, 200) | ||
}) | ||
|
||
t.Run("deny request outside of cron", func(t *testing.T) { | ||
// Arrange a cron job that fails the test if it runs. | ||
handler := cron(func(context.Context) error { | ||
t.Error("handler should not have run") | ||
return nil | ||
}) | ||
|
||
// Prepare a request from outside of AppEngine (no custom header). | ||
req := httptest.NewRequest(http.MethodGet, "/", nil) | ||
|
||
// Run it! | ||
w := httptest.NewRecorder() | ||
handler(w, req) | ||
|
||
resp := w.Result() | ||
defer resp.Body.Close() | ||
|
||
assert.Equal(t, resp.StatusCode, 404) | ||
}) | ||
|
||
t.Run("report job failure", func(t *testing.T) { | ||
// Arrange a cron job that errors. | ||
handler := cron(func(context.Context) error { | ||
return errors.New("test error") | ||
}) | ||
|
||
// Prepare an AppEngine-sourced request. | ||
req := httptest.NewRequest(http.MethodGet, "/", nil) | ||
req.Header.Set("X-Appengine-Cron", "true") | ||
|
||
// Run it! | ||
w := httptest.NewRecorder() | ||
handler(w, req) | ||
|
||
resp := w.Result() | ||
defer resp.Body.Close() | ||
|
||
assert.Equal(t, resp.StatusCode, 500) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters