fix: resolve all 22 gosec G710 open-redirect findings (closes #176) #186

Closed
clawbot wants to merge 2 commits from fix-gosec-g710 into main
8 changed files with 168 additions and 103 deletions

30
TODO.md
View File

@@ -10,18 +10,26 @@
# Status
1.0+. Tagged 1.0.0 on 2026-02-26; 8 commits on main since. Policy
violation: main currently fails make check (91 lint issues), so the tree
is out of compliance until fixed.
1.0+. Tagged 1.0.0 on 2026-02-26. Policy violation: main currently
fails make check under golangci-lint >= 2.12 (25 lint issues remaining:
1 gosec G703, 24 goconst), so the tree is out of compliance until
fixed. CI (Dockerfile lint stage, pinned golangci-lint v2.10.1) is
green; the pin bump is tracked in issue #179. The road to release
1.1.0 is tracked in Gitea issues #175-#185 (milestone 1.1.0).
# Next Step
Fix the 47 noctx lint findings (HTTP requests without context) in one
commit and confirm the count drops under make check. This is the largest
of the three lint classes blocking a green main.
Fix the gosec G703 path traversal finding in the deploy log download
handler (issue #177): canonicalize and containment-check the log path
before http.ServeFile, with a traversal-rejection test.
# Completed Steps
- 2026-08-07: Fixed all 22 gosec G710 open-redirect findings: app
redirects go through a redirectToApp helper that ULID-validates the
app ID (#176).
- 2026-08-07: Fixed all 47 noctx lint findings: tests now use
httptest.NewRequestWithContext with t.Context() (#175).
- 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints,
Makefile shims, README Entrypoints section
- 2026-03-11: Monolithic env var editing with bulk save (#158).
@@ -45,11 +53,13 @@ of the three lint classes blocking a green main.
# Future Steps
- Get main green (compliance, ordered):
- Fix 47 noctx findings (Next Step).
- Fix 23 gosec findings.
- Fix 21 goconst findings.
- Fix 1 gosec G703 finding (Next Step, #177).
- Fix 24 goconst findings (#178).
- Bump Dockerfile golangci-lint pin to v2.12.x (#179).
- Run make check clean on main and keep it green; main must always
pass.
- Confirm .gitea/workflows/check.yml gates merges on make check so main
cannot regress.
cannot regress (#180).
- Deploy to fsn1app1 and verify end-to-end (#181), then tag 1.1.0
(#182).
- Resume feature work only after main is green.

View File

@@ -47,7 +47,12 @@ func setupAPITest(t *testing.T) (*testContext, []*http.Cookie) {
r := apiRouter(tc)
loginBody := `{"username":"admin","password":"password123"}`
req := httptest.NewRequest(http.MethodPost, "/api/v1/login", strings.NewReader(loginBody))
req := httptest.NewRequestWithContext(
t.Context(),
http.MethodPost,
"/api/v1/login",
strings.NewReader(loginBody),
)
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
@@ -70,7 +75,7 @@ func apiGet(
) *httptest.ResponseRecorder {
t.Helper()
req := httptest.NewRequest(http.MethodGet, path, nil)
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, path, nil)
for _, c := range cookies {
req.AddCookie(c)
@@ -95,7 +100,12 @@ func TestAPILoginSuccess(t *testing.T) {
r := apiRouter(tc)
body := `{"username":"admin","password":"password123"}`
req := httptest.NewRequest(http.MethodPost, "/api/v1/login", strings.NewReader(body))
req := httptest.NewRequestWithContext(
t.Context(),
http.MethodPost,
"/api/v1/login",
strings.NewReader(body),
)
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
@@ -122,7 +132,12 @@ func TestAPILoginInvalidCredentials(t *testing.T) {
r := apiRouter(tc)
body := `{"username":"admin","password":"wrong"}`
req := httptest.NewRequest(http.MethodPost, "/api/v1/login", strings.NewReader(body))
req := httptest.NewRequestWithContext(
t.Context(),
http.MethodPost,
"/api/v1/login",
strings.NewReader(body),
)
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
@@ -139,7 +154,12 @@ func TestAPILoginMissingFields(t *testing.T) {
r := apiRouter(tc)
body := `{"username":"","password":""}`
req := httptest.NewRequest(http.MethodPost, "/api/v1/login", strings.NewReader(body))
req := httptest.NewRequestWithContext(
t.Context(),
http.MethodPost,
"/api/v1/login",
strings.NewReader(body),
)
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
@@ -155,7 +175,9 @@ func TestAPIRejectsUnauthenticated(t *testing.T) {
r := apiRouter(tc)
req := httptest.NewRequest(http.MethodGet, "/api/v1/apps", nil)
req := httptest.NewRequestWithContext(
t.Context(), http.MethodGet, "/api/v1/apps", nil,
)
rr := httptest.NewRecorder()
r.ServeHTTP(rr, req)

View File

@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
@@ -14,6 +15,7 @@ import (
"time"
"github.com/go-chi/chi/v5"
"github.com/oklog/ulid/v2"
"sneak.berlin/go/upaas/internal/models"
"sneak.berlin/go/upaas/internal/service/app"
@@ -27,6 +29,27 @@ const (
deploymentsHistoryLimit = 50
)
// redirectToApp issues a See Other redirect to the detail page of the
// given app, plus an optional suffix (a sub-path like "/deployments"
// or a query string like "?success=updated"). App IDs are ULIDs: the
// ID is parsed and re-serialized so the redirect target never
// contains unvalidated request input; an invalid ID yields 404.
func redirectToApp(
writer http.ResponseWriter,
request *http.Request,
appID, suffix string,
) {
id, parseErr := ulid.ParseStrict(appID)
if parseErr != nil {
http.NotFound(writer, request)
return
}
target := "/apps/" + url.PathEscape(id.String()) + suffix
http.Redirect(writer, request, target, http.StatusSeeOther)
}
// HandleAppNew returns the new app form handler.
func (h *Handlers) HandleAppNew() http.HandlerFunc {
tmpl := templates.GetParsed()
@@ -119,7 +142,7 @@ func (h *Handlers) HandleAppCreate() http.HandlerFunc { //nolint:funlen // valid
return
}
http.Redirect(writer, request, "/apps/"+createdApp.ID, http.StatusSeeOther)
redirectToApp(writer, request, createdApp.ID, "")
}
}
@@ -285,8 +308,7 @@ func (h *Handlers) HandleAppUpdate() http.HandlerFunc { //nolint:funlen // valid
return
}
redirectURL := "/apps/" + application.ID + "?success=updated"
http.Redirect(writer, request, redirectURL, http.StatusSeeOther)
redirectToApp(writer, request, application.ID, "?success=updated")
}
}
@@ -371,12 +393,7 @@ func (h *Handlers) HandleAppDeploy() http.HandlerFunc {
}
}(deployCtx, application)
http.Redirect(
writer,
request,
"/apps/"+application.ID+"/deployments",
http.StatusSeeOther,
)
redirectToApp(writer, request, application.ID, "/deployments")
}
}
@@ -397,12 +414,7 @@ func (h *Handlers) HandleCancelDeploy() http.HandlerFunc {
h.log.Info("deployment cancelled by user", "app", application.Name)
}
http.Redirect(
writer,
request,
"/apps/"+application.ID,
http.StatusSeeOther,
)
redirectToApp(writer, request, application.ID, "")
}
}
@@ -421,12 +433,12 @@ func (h *Handlers) HandleAppRollback() http.HandlerFunc {
rollbackErr := h.deploy.Rollback(request.Context(), application)
if rollbackErr != nil {
h.log.Error("rollback failed", "error", rollbackErr, "app", application.Name)
http.Redirect(writer, request, "/apps/"+application.ID, http.StatusSeeOther)
redirectToApp(writer, request, application.ID, "")
return
}
http.Redirect(writer, request, "/apps/"+application.ID+"?success=rolledback", http.StatusSeeOther)
redirectToApp(writer, request, application.ID, "?success=rolledback")
}
}
@@ -799,7 +811,7 @@ func (h *Handlers) handleContainerAction(
containerInfo, containerErr := h.docker.FindContainerByAppID(ctx, appID)
if containerErr != nil || containerInfo == nil {
http.Redirect(writer, request, "/apps/"+appID, http.StatusSeeOther)
redirectToApp(writer, request, appID, "")
return
}
@@ -832,7 +844,7 @@ func (h *Handlers) handleContainerAction(
"action", action, "app", application.Name, "container", containerID)
}
http.Redirect(writer, request, "/apps/"+appID, http.StatusSeeOther)
redirectToApp(writer, request, appID, "")
}
// HandleAppRestart handles restarting an app's container.
@@ -886,7 +898,7 @@ func (h *Handlers) addKeyValueToApp(
value := request.FormValue("value")
if key == "" || value == "" {
http.Redirect(writer, request, "/apps/"+application.ID, http.StatusSeeOther)
redirectToApp(writer, request, application.ID, "")
return
}
@@ -896,7 +908,7 @@ func (h *Handlers) addKeyValueToApp(
h.log.Error("failed to add key-value pair", "error", saveErr)
}
http.Redirect(writer, request, "/apps/"+application.ID, http.StatusSeeOther)
redirectToApp(writer, request, application.ID, "")
}
// envPairJSON represents a key-value pair in the JSON request body.
@@ -1031,7 +1043,7 @@ func (h *Handlers) HandleLabelDelete() http.HandlerFunc {
h.log.Error("failed to delete label", "error", deleteErr)
}
http.Redirect(writer, request, "/apps/"+appID, http.StatusSeeOther)
redirectToApp(writer, request, appID, "")
}
}
@@ -1059,12 +1071,7 @@ func (h *Handlers) HandleVolumeAdd() http.HandlerFunc {
readOnly := request.FormValue("readonly") == "1"
if hostPath == "" || containerPath == "" {
http.Redirect(
writer,
request,
"/apps/"+application.ID,
http.StatusSeeOther,
)
redirectToApp(writer, request, application.ID, "")
return
}
@@ -1072,7 +1079,7 @@ func (h *Handlers) HandleVolumeAdd() http.HandlerFunc {
pathErr := validateVolumePaths(hostPath, containerPath)
if pathErr != nil {
h.log.Error("invalid volume path", "error", pathErr)
http.Redirect(writer, request, "/apps/"+application.ID, http.StatusSeeOther)
redirectToApp(writer, request, application.ID, "")
return
}
@@ -1088,7 +1095,7 @@ func (h *Handlers) HandleVolumeAdd() http.HandlerFunc {
h.log.Error("failed to add volume", "error", saveErr)
}
http.Redirect(writer, request, "/apps/"+application.ID, http.StatusSeeOther)
redirectToApp(writer, request, application.ID, "")
}
}
@@ -1117,7 +1124,7 @@ func (h *Handlers) HandleVolumeDelete() http.HandlerFunc {
h.log.Error("failed to delete volume", "error", deleteErr)
}
http.Redirect(writer, request, "/apps/"+appID, http.StatusSeeOther)
redirectToApp(writer, request, appID, "")
}
}
@@ -1145,7 +1152,7 @@ func (h *Handlers) HandlePortAdd() http.HandlerFunc {
request.FormValue("container_port"),
)
if !valid {
http.Redirect(writer, request, "/apps/"+application.ID, http.StatusSeeOther)
redirectToApp(writer, request, application.ID, "")
return
}
@@ -1166,7 +1173,7 @@ func (h *Handlers) HandlePortAdd() http.HandlerFunc {
h.log.Error("failed to save port", "error", saveErr)
}
http.Redirect(writer, request, "/apps/"+application.ID, http.StatusSeeOther)
redirectToApp(writer, request, application.ID, "")
}
}
@@ -1212,7 +1219,7 @@ func (h *Handlers) HandlePortDelete() http.HandlerFunc {
h.log.Error("failed to delete port", "error", deleteErr)
}
http.Redirect(writer, request, "/apps/"+appID, http.StatusSeeOther)
redirectToApp(writer, request, appID, "")
}
}
@@ -1274,7 +1281,7 @@ func (h *Handlers) HandleLabelEdit() http.HandlerFunc {
value := request.FormValue("value")
if key == "" || value == "" {
http.Redirect(writer, request, "/apps/"+appID, http.StatusSeeOther)
redirectToApp(writer, request, appID, "")
return
}
@@ -1287,7 +1294,7 @@ func (h *Handlers) HandleLabelEdit() http.HandlerFunc {
h.log.Error("failed to update label", "error", saveErr)
}
http.Redirect(writer, request, "/apps/"+appID, http.StatusSeeOther)
redirectToApp(writer, request, appID, "")
}
}
@@ -1323,7 +1330,7 @@ func (h *Handlers) HandleVolumeEdit() http.HandlerFunc {
readOnly := request.FormValue("readonly") == "1"
if hostPath == "" || containerPath == "" {
http.Redirect(writer, request, "/apps/"+appID, http.StatusSeeOther)
redirectToApp(writer, request, appID, "")
return
}
@@ -1331,7 +1338,7 @@ func (h *Handlers) HandleVolumeEdit() http.HandlerFunc {
pathErr := validateVolumePaths(hostPath, containerPath)
if pathErr != nil {
h.log.Error("invalid volume path", "error", pathErr)
http.Redirect(writer, request, "/apps/"+appID, http.StatusSeeOther)
redirectToApp(writer, request, appID, "")
return
}
@@ -1345,7 +1352,7 @@ func (h *Handlers) HandleVolumeEdit() http.HandlerFunc {
h.log.Error("failed to update volume", "error", saveErr)
}
http.Redirect(writer, request, "/apps/"+appID, http.StatusSeeOther)
redirectToApp(writer, request, appID, "")
}
}

View File

@@ -193,7 +193,8 @@ func TestHandleHealthCheck(t *testing.T) {
testCtx := setupTestHandlers(t)
request := httptest.NewRequest(
request := httptest.NewRequestWithContext(
t.Context(),
http.MethodGet,
"/.well-known/healthcheck.json",
nil,
@@ -218,7 +219,7 @@ func TestHandleSetupGET(t *testing.T) {
testCtx := setupTestHandlers(t)
request := httptest.NewRequest(http.MethodGet, "/setup", nil)
request := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/setup", nil)
recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleSetupGET()
@@ -230,14 +231,18 @@ func TestHandleSetupGET(t *testing.T) {
}
func createSetupFormRequest(
t *testing.T,
username, password, confirm string,
) *http.Request {
t.Helper()
form := url.Values{}
form.Set("username", username)
form.Set("password", password)
form.Set("password_confirm", confirm)
request := httptest.NewRequest(
request := httptest.NewRequestWithContext(
t.Context(),
http.MethodPost,
"/setup",
strings.NewReader(form.Encode()),
@@ -252,7 +257,7 @@ func TestHandleSetupPOSTCreatesUserAndRedirects(t *testing.T) {
testCtx := setupTestHandlers(t)
request := createSetupFormRequest("admin", "password123", "password123")
request := createSetupFormRequest(t, "admin", "password123", "password123")
recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleSetupPOST()
@@ -267,7 +272,7 @@ func TestHandleSetupPOSTRejectsEmptyUsername(t *testing.T) {
testCtx := setupTestHandlers(t)
request := createSetupFormRequest("", "password123", "password123")
request := createSetupFormRequest(t, "", "password123", "password123")
recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleSetupPOST()
@@ -282,7 +287,7 @@ func TestHandleSetupPOSTRejectsShortPassword(t *testing.T) {
testCtx := setupTestHandlers(t)
request := createSetupFormRequest("admin", "short", "short")
request := createSetupFormRequest(t, "admin", "short", "short")
recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleSetupPOST()
@@ -297,7 +302,7 @@ func TestHandleSetupPOSTRejectsMismatchedPasswords(t *testing.T) {
testCtx := setupTestHandlers(t)
request := createSetupFormRequest("admin", "password123", "different123")
request := createSetupFormRequest(t, "admin", "password123", "different123")
recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleSetupPOST()
@@ -315,7 +320,7 @@ func TestHandleLoginGET(t *testing.T) {
testCtx := setupTestHandlers(t)
request := httptest.NewRequest(http.MethodGet, "/login", nil)
request := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/login", nil)
recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleLoginGET()
@@ -326,12 +331,15 @@ func TestHandleLoginGET(t *testing.T) {
})
}
func createLoginFormRequest(username, password string) *http.Request {
func createLoginFormRequest(t *testing.T, username, password string) *http.Request {
t.Helper()
form := url.Values{}
form.Set("username", username)
form.Set("password", password)
request := httptest.NewRequest(
request := httptest.NewRequestWithContext(
t.Context(),
http.MethodPost,
"/login",
strings.NewReader(form.Encode()),
@@ -354,7 +362,7 @@ func TestHandleLoginPOSTAuthenticatesValidCredentials(t *testing.T) {
)
require.NoError(t, createErr)
request := createLoginFormRequest("testuser", "testpass123")
request := createLoginFormRequest(t, "testuser", "testpass123")
recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleLoginPOST()
@@ -377,7 +385,7 @@ func TestHandleLoginPOSTRejectsInvalidCredentials(t *testing.T) {
)
require.NoError(t, createErr)
request := createLoginFormRequest("testuser", "wrongpassword")
request := createLoginFormRequest(t, "testuser", "wrongpassword")
recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleLoginPOST()
@@ -395,7 +403,7 @@ func TestHandleDashboard(t *testing.T) {
testCtx := setupTestHandlers(t)
request := httptest.NewRequest(http.MethodGet, "/", nil)
request := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleDashboard()
@@ -413,7 +421,7 @@ func TestHandleDashboard(t *testing.T) {
// Create an app so the template iterates over AppStats and hits .CSRFField
createTestApp(t, testCtx, "csrf-test-app")
request := httptest.NewRequest(http.MethodGet, "/", nil)
request := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleDashboard()
@@ -433,7 +441,9 @@ func TestHandleAppNew(t *testing.T) {
testCtx := setupTestHandlers(t)
request := httptest.NewRequest(http.MethodGet, "/apps/new", nil)
request := httptest.NewRequestWithContext(
t.Context(), http.MethodGet, "/apps/new", nil,
)
recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleAppNew()
@@ -501,7 +511,8 @@ func TestHandleWebhookRejectsOversizedBody(t *testing.T) {
// Create a body larger than 1MB - it should be silently truncated
// and the webhook should still process (or fail gracefully on parse)
largePayload := strings.Repeat("x", 2*1024*1024) // 2MB
request := httptest.NewRequest(
request := httptest.NewRequestWithContext(
t.Context(),
http.MethodPost,
"/webhook/"+createdApp.WebhookSecret,
strings.NewReader(largePayload),
@@ -544,7 +555,8 @@ func testOwnershipVerification(t *testing.T, cfg ownedResourceTestConfig) {
resourceID := cfg.createFn(t, testCtx, app1)
request := httptest.NewRequest(
request := httptest.NewRequestWithContext(
t.Context(),
http.MethodPost,
cfg.deletePath(app2.ID, resourceID),
nil,
@@ -583,7 +595,8 @@ func TestHandleEnvVarSaveBulk(t *testing.T) {
r := chi.NewRouter()
r.Post("/apps/{id}/env", testCtx.handlers.HandleEnvVarSave())
request := httptest.NewRequest(
request := httptest.NewRequestWithContext(
t.Context(),
http.MethodPost,
"/apps/"+createdApp.ID+"/env",
strings.NewReader(body),
@@ -625,7 +638,8 @@ func TestHandleEnvVarSaveAppNotFound(t *testing.T) {
r := chi.NewRouter()
r.Post("/apps/{id}/env", testCtx.handlers.HandleEnvVarSave())
request := httptest.NewRequest(
request := httptest.NewRequestWithContext(
t.Context(),
http.MethodPost,
"/apps/nonexistent-id/env",
strings.NewReader(body),
@@ -651,7 +665,8 @@ func TestHandleEnvVarSaveEmptyKeyRejected(t *testing.T) {
r := chi.NewRouter()
r.Post("/apps/{id}/env", testCtx.handlers.HandleEnvVarSave())
request := httptest.NewRequest(
request := httptest.NewRequestWithContext(
t.Context(),
http.MethodPost,
"/apps/"+createdApp.ID+"/env",
strings.NewReader(body),
@@ -678,7 +693,8 @@ func TestHandleEnvVarSaveDuplicateKeyRejected(t *testing.T) {
r := chi.NewRouter()
r.Post("/apps/{id}/env", testCtx.handlers.HandleEnvVarSave())
request := httptest.NewRequest(
request := httptest.NewRequestWithContext(
t.Context(),
http.MethodPost,
"/apps/"+createdApp.ID+"/env",
strings.NewReader(body),
@@ -716,7 +732,8 @@ func TestHandleEnvVarSaveCrossAppIsolation(t *testing.T) {
r := chi.NewRouter()
r.Post("/apps/{id}/env", testCtx.handlers.HandleEnvVarSave())
request := httptest.NewRequest(
request := httptest.NewRequestWithContext(
t.Context(),
http.MethodPost,
"/apps/"+appA.ID+"/env",
strings.NewReader(body),
@@ -779,7 +796,8 @@ func TestHandleEnvVarSaveBodySizeLimit(t *testing.T) {
r := chi.NewRouter()
r.Post("/apps/{id}/env", testCtx.handlers.HandleEnvVarSave())
request := httptest.NewRequest(
request := httptest.NewRequestWithContext(
t.Context(),
http.MethodPost,
"/apps/"+createdApp.ID+"/env",
strings.NewReader(sb.String()),
@@ -848,7 +866,8 @@ func TestDeleteVolumeOwnershipVerification(t *testing.T) {
require.NoError(t, volume.Save(context.Background()))
// Try to delete app1's volume using app2's URL path
request := httptest.NewRequest(
request := httptest.NewRequestWithContext(
t.Context(),
http.MethodPost,
"/apps/"+app2.ID+"/volumes/"+strconv.FormatInt(volume.ID, 10)+"/delete",
nil,
@@ -889,7 +908,8 @@ func TestDeletePortOwnershipVerification(t *testing.T) {
require.NoError(t, port.Save(context.Background()))
// Try to delete app1's port using app2's URL path
request := httptest.NewRequest(
request := httptest.NewRequestWithContext(
t.Context(),
http.MethodPost,
"/apps/"+app2.ID+"/ports/"+strconv.FormatInt(port.ID, 10)+"/delete",
nil,
@@ -930,7 +950,8 @@ func TestHandleEnvVarSaveEmptyClears(t *testing.T) {
r := chi.NewRouter()
r.Post("/apps/{id}/env", testCtx.handlers.HandleEnvVarSave())
request := httptest.NewRequest(
request := httptest.NewRequestWithContext(
t.Context(),
http.MethodPost,
"/apps/"+createdApp.ID+"/env",
strings.NewReader("[]"),
@@ -979,7 +1000,8 @@ func TestHandleVolumeAddValidatesPaths(t *testing.T) {
form.Set("host_path", tt.hostPath)
form.Set("container_path", tt.containerPath)
request := httptest.NewRequest(
request := httptest.NewRequestWithContext(
t.Context(),
http.MethodPost,
"/apps/"+createdApp.ID+"/volumes",
strings.NewReader(form.Encode()),
@@ -1038,7 +1060,7 @@ func TestSetupRequiredExemptsHealthAndStaticAndAPI(t *testing.T) {
t.Run(path, func(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(http.MethodGet, path, nil)
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, path, nil)
rr := httptest.NewRecorder()
wrapped.ServeHTTP(rr, req)
@@ -1051,7 +1073,7 @@ func TestSetupRequiredExemptsHealthAndStaticAndAPI(t *testing.T) {
t.Run("non-exempt redirects", func(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(http.MethodGet, "/", nil)
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
rr := httptest.NewRecorder()
wrapped.ServeHTTP(rr, req)
@@ -1067,7 +1089,8 @@ func TestHandleCancelDeployRedirects(t *testing.T) {
createdApp := createTestApp(t, testCtx, "cancel-deploy-app")
request := httptest.NewRequest(
request := httptest.NewRequestWithContext(
t.Context(),
http.MethodPost,
"/apps/"+createdApp.ID+"/deployments/cancel",
nil,
@@ -1087,7 +1110,8 @@ func TestHandleCancelDeployReturns404ForUnknownApp(t *testing.T) {
testCtx := setupTestHandlers(t)
request := httptest.NewRequest(
request := httptest.NewRequestWithContext(
t.Context(),
http.MethodPost,
"/apps/nonexistent/deployments/cancel",
nil,
@@ -1108,7 +1132,8 @@ func TestHandleWebhookReturns404ForUnknownSecret(t *testing.T) {
webhookURL := "/webhook/unknown-secret"
payload := `{"ref": "refs/heads/main"}`
request := httptest.NewRequest(
request := httptest.NewRequestWithContext(
t.Context(),
http.MethodPost,
webhookURL,
strings.NewReader(payload),
@@ -1143,7 +1168,8 @@ func TestHandleWebhookProcessesValidWebhook(t *testing.T) {
payload := `{"ref": "refs/heads/main", "after": "abc123"}`
webhookURL := "/webhook/" + createdApp.WebhookSecret
request := httptest.NewRequest(
request := httptest.NewRequestWithContext(
t.Context(),
http.MethodPost,
webhookURL,
strings.NewReader(payload),

View File

@@ -16,7 +16,7 @@ func TestRenderTemplateBuffersOutput(t *testing.T) {
testCtx := setupTestHandlers(t)
// The setup page is simple and has no DB dependencies
request := httptest.NewRequest(http.MethodGet, "/setup", nil)
request := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/setup", nil)
recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleSetupGET()
@@ -39,7 +39,7 @@ func TestDashboardRenderTemplateBuffersOutput(t *testing.T) {
testCtx := setupTestHandlers(t)
request := httptest.NewRequest(http.MethodGet, "/", nil)
request := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleDashboard()
@@ -59,7 +59,7 @@ func TestLoginRenderTemplateBuffersOutput(t *testing.T) {
testCtx := setupTestHandlers(t)
request := httptest.NewRequest(http.MethodGet, "/login", nil)
request := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/login", nil)
recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleLoginGET()

View File

@@ -32,7 +32,7 @@ func TestCORS_NoOriginsConfigured_NoCORSHeaders(t *testing.T) {
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodGet, "/", nil)
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
req.Header.Set("Origin", "https://evil.com")
rec := httptest.NewRecorder()
@@ -50,7 +50,7 @@ func TestCORS_OriginsConfigured_AllowsMatchingOrigin(t *testing.T) {
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodGet, "/", nil)
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
req.Header.Set("Origin", "https://app.example.com")
rec := httptest.NewRecorder()
@@ -70,7 +70,7 @@ func TestCORS_OriginsConfigured_RejectsNonMatchingOrigin(t *testing.T) {
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodGet, "/", nil)
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
req.Header.Set("Origin", "https://evil.com")
rec := httptest.NewRecorder()

View File

@@ -36,7 +36,7 @@ func TestLoginRateLimitAllowsUpToBurst(t *testing.T) {
// First 5 requests should succeed (burst)
for i := range 5 {
req := httptest.NewRequest(http.MethodPost, "/login", nil)
req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/login", nil)
req.RemoteAddr = "192.168.1.1:12345"
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
@@ -44,7 +44,7 @@ func TestLoginRateLimitAllowsUpToBurst(t *testing.T) {
}
// 6th request should be rate limited
req := httptest.NewRequest(http.MethodPost, "/login", nil)
req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/login", nil)
req.RemoteAddr = "192.168.1.1:12345"
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
@@ -63,21 +63,21 @@ func TestLoginRateLimitIsolatesIPs(t *testing.T) {
// Exhaust IP1's budget
for range 5 {
req := httptest.NewRequest(http.MethodPost, "/login", nil)
req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/login", nil)
req.RemoteAddr = "10.0.0.1:1234"
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
}
// IP1 should be blocked
req := httptest.NewRequest(http.MethodPost, "/login", nil)
req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/login", nil)
req.RemoteAddr = "10.0.0.1:1234"
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusTooManyRequests, rec.Code)
// IP2 should still work
req2 := httptest.NewRequest(http.MethodPost, "/login", nil)
req2 := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/login", nil)
req2.RemoteAddr = "10.0.0.2:1234"
rec2 := httptest.NewRecorder()
handler.ServeHTTP(rec2, req2)
@@ -96,13 +96,13 @@ func TestLoginRateLimitReturns429Body(t *testing.T) {
// Exhaust burst
for range 5 {
req := httptest.NewRequest(http.MethodPost, "/login", nil)
req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/login", nil)
req.RemoteAddr = "172.16.0.1:5555"
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
}
req := httptest.NewRequest(http.MethodPost, "/login", nil)
req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/login", nil)
req.RemoteAddr = "172.16.0.1:5555"
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)

View File

@@ -121,7 +121,7 @@ func getSessionCookie(t *testing.T, svc *auth.Service) *http.Cookie {
require.NoError(t, err)
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/", nil)
request := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
err = svc.CreateSession(recorder, request, user)
require.NoError(t, err)
@@ -380,7 +380,7 @@ func TestDestroySessionMaxAge(testingT *testing.T) {
defer cleanup()
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/", nil)
request := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
err := svc.DestroySession(recorder, request)
require.NoError(t, err)