Author SHA1 Message Date
sneak 0371d43223 Refresh vendored REPO_POLICIES.md from prompts@origin/main
Check / check (pull_request) Successful in 3m8s
2026-07-07 01:57:04 +02:00
sneak eaa164e83d Adopt scripts-to-rule-them-all: script/ entrypoints, Makefile shims 2026-07-07 01:56:42 +02:00
8 changed files with 103 additions and 168 deletions
+10 -20
View File
@@ -10,26 +10,18 @@
# Status # Status
1.0+. Tagged 1.0.0 on 2026-02-26. Policy violation: main currently 1.0+. Tagged 1.0.0 on 2026-02-26; 8 commits on main since. Policy
fails make check under golangci-lint >= 2.12 (25 lint issues remaining: violation: main currently fails make check (91 lint issues), so the tree
1 gosec G703, 24 goconst), so the tree is out of compliance until is out of compliance until fixed.
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 # Next Step
Fix the gosec G703 path traversal finding in the deploy log download Fix the 47 noctx lint findings (HTTP requests without context) in one
handler (issue #177): canonicalize and containment-check the log path commit and confirm the count drops under make check. This is the largest
before http.ServeFile, with a traversal-rejection test. of the three lint classes blocking a green main.
# Completed Steps # 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, - 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints,
Makefile shims, README Entrypoints section Makefile shims, README Entrypoints section
- 2026-03-11: Monolithic env var editing with bulk save (#158). - 2026-03-11: Monolithic env var editing with bulk save (#158).
@@ -53,13 +45,11 @@ before http.ServeFile, with a traversal-rejection test.
# Future Steps # Future Steps
- Get main green (compliance, ordered): - Get main green (compliance, ordered):
- Fix 1 gosec G703 finding (Next Step, #177). - Fix 47 noctx findings (Next Step).
- Fix 24 goconst findings (#178). - Fix 23 gosec findings.
- Bump Dockerfile golangci-lint pin to v2.12.x (#179). - Fix 21 goconst findings.
- Run make check clean on main and keep it green; main must always - Run make check clean on main and keep it green; main must always
pass. pass.
- Confirm .gitea/workflows/check.yml gates merges on make check so main - Confirm .gitea/workflows/check.yml gates merges on make check so main
cannot regress (#180). cannot regress.
- Deploy to fsn1app1 and verify end-to-end (#181), then tag 1.1.0
(#182).
- Resume feature work only after main is green. - Resume feature work only after main is green.
+6 -28
View File
@@ -47,12 +47,7 @@ func setupAPITest(t *testing.T) (*testContext, []*http.Cookie) {
r := apiRouter(tc) r := apiRouter(tc)
loginBody := `{"username":"admin","password":"password123"}` loginBody := `{"username":"admin","password":"password123"}`
req := httptest.NewRequestWithContext( req := httptest.NewRequest(http.MethodPost, "/api/v1/login", strings.NewReader(loginBody))
t.Context(),
http.MethodPost,
"/api/v1/login",
strings.NewReader(loginBody),
)
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
@@ -75,7 +70,7 @@ func apiGet(
) *httptest.ResponseRecorder { ) *httptest.ResponseRecorder {
t.Helper() t.Helper()
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, path, nil) req := httptest.NewRequest(http.MethodGet, path, nil)
for _, c := range cookies { for _, c := range cookies {
req.AddCookie(c) req.AddCookie(c)
@@ -100,12 +95,7 @@ func TestAPILoginSuccess(t *testing.T) {
r := apiRouter(tc) r := apiRouter(tc)
body := `{"username":"admin","password":"password123"}` body := `{"username":"admin","password":"password123"}`
req := httptest.NewRequestWithContext( req := httptest.NewRequest(http.MethodPost, "/api/v1/login", strings.NewReader(body))
t.Context(),
http.MethodPost,
"/api/v1/login",
strings.NewReader(body),
)
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
@@ -132,12 +122,7 @@ func TestAPILoginInvalidCredentials(t *testing.T) {
r := apiRouter(tc) r := apiRouter(tc)
body := `{"username":"admin","password":"wrong"}` body := `{"username":"admin","password":"wrong"}`
req := httptest.NewRequestWithContext( req := httptest.NewRequest(http.MethodPost, "/api/v1/login", strings.NewReader(body))
t.Context(),
http.MethodPost,
"/api/v1/login",
strings.NewReader(body),
)
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
@@ -154,12 +139,7 @@ func TestAPILoginMissingFields(t *testing.T) {
r := apiRouter(tc) r := apiRouter(tc)
body := `{"username":"","password":""}` body := `{"username":"","password":""}`
req := httptest.NewRequestWithContext( req := httptest.NewRequest(http.MethodPost, "/api/v1/login", strings.NewReader(body))
t.Context(),
http.MethodPost,
"/api/v1/login",
strings.NewReader(body),
)
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
@@ -175,9 +155,7 @@ func TestAPIRejectsUnauthenticated(t *testing.T) {
r := apiRouter(tc) r := apiRouter(tc)
req := httptest.NewRequestWithContext( req := httptest.NewRequest(http.MethodGet, "/api/v1/apps", nil)
t.Context(), http.MethodGet, "/api/v1/apps", nil,
)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
r.ServeHTTP(rr, req) r.ServeHTTP(rr, req)
+39 -46
View File
@@ -7,7 +7,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
@@ -15,7 +14,6 @@ import (
"time" "time"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/oklog/ulid/v2"
"sneak.berlin/go/upaas/internal/models" "sneak.berlin/go/upaas/internal/models"
"sneak.berlin/go/upaas/internal/service/app" "sneak.berlin/go/upaas/internal/service/app"
@@ -29,27 +27,6 @@ const (
deploymentsHistoryLimit = 50 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. // HandleAppNew returns the new app form handler.
func (h *Handlers) HandleAppNew() http.HandlerFunc { func (h *Handlers) HandleAppNew() http.HandlerFunc {
tmpl := templates.GetParsed() tmpl := templates.GetParsed()
@@ -142,7 +119,7 @@ func (h *Handlers) HandleAppCreate() http.HandlerFunc { //nolint:funlen // valid
return return
} }
redirectToApp(writer, request, createdApp.ID, "") http.Redirect(writer, request, "/apps/"+createdApp.ID, http.StatusSeeOther)
} }
} }
@@ -308,7 +285,8 @@ func (h *Handlers) HandleAppUpdate() http.HandlerFunc { //nolint:funlen // valid
return return
} }
redirectToApp(writer, request, application.ID, "?success=updated") redirectURL := "/apps/" + application.ID + "?success=updated"
http.Redirect(writer, request, redirectURL, http.StatusSeeOther)
} }
} }
@@ -393,7 +371,12 @@ func (h *Handlers) HandleAppDeploy() http.HandlerFunc {
} }
}(deployCtx, application) }(deployCtx, application)
redirectToApp(writer, request, application.ID, "/deployments") http.Redirect(
writer,
request,
"/apps/"+application.ID+"/deployments",
http.StatusSeeOther,
)
} }
} }
@@ -414,7 +397,12 @@ func (h *Handlers) HandleCancelDeploy() http.HandlerFunc {
h.log.Info("deployment cancelled by user", "app", application.Name) h.log.Info("deployment cancelled by user", "app", application.Name)
} }
redirectToApp(writer, request, application.ID, "") http.Redirect(
writer,
request,
"/apps/"+application.ID,
http.StatusSeeOther,
)
} }
} }
@@ -433,12 +421,12 @@ func (h *Handlers) HandleAppRollback() http.HandlerFunc {
rollbackErr := h.deploy.Rollback(request.Context(), application) rollbackErr := h.deploy.Rollback(request.Context(), application)
if rollbackErr != nil { if rollbackErr != nil {
h.log.Error("rollback failed", "error", rollbackErr, "app", application.Name) h.log.Error("rollback failed", "error", rollbackErr, "app", application.Name)
redirectToApp(writer, request, application.ID, "") http.Redirect(writer, request, "/apps/"+application.ID, http.StatusSeeOther)
return return
} }
redirectToApp(writer, request, application.ID, "?success=rolledback") http.Redirect(writer, request, "/apps/"+application.ID+"?success=rolledback", http.StatusSeeOther)
} }
} }
@@ -811,7 +799,7 @@ func (h *Handlers) handleContainerAction(
containerInfo, containerErr := h.docker.FindContainerByAppID(ctx, appID) containerInfo, containerErr := h.docker.FindContainerByAppID(ctx, appID)
if containerErr != nil || containerInfo == nil { if containerErr != nil || containerInfo == nil {
redirectToApp(writer, request, appID, "") http.Redirect(writer, request, "/apps/"+appID, http.StatusSeeOther)
return return
} }
@@ -844,7 +832,7 @@ func (h *Handlers) handleContainerAction(
"action", action, "app", application.Name, "container", containerID) "action", action, "app", application.Name, "container", containerID)
} }
redirectToApp(writer, request, appID, "") http.Redirect(writer, request, "/apps/"+appID, http.StatusSeeOther)
} }
// HandleAppRestart handles restarting an app's container. // HandleAppRestart handles restarting an app's container.
@@ -898,7 +886,7 @@ func (h *Handlers) addKeyValueToApp(
value := request.FormValue("value") value := request.FormValue("value")
if key == "" || value == "" { if key == "" || value == "" {
redirectToApp(writer, request, application.ID, "") http.Redirect(writer, request, "/apps/"+application.ID, http.StatusSeeOther)
return return
} }
@@ -908,7 +896,7 @@ func (h *Handlers) addKeyValueToApp(
h.log.Error("failed to add key-value pair", "error", saveErr) h.log.Error("failed to add key-value pair", "error", saveErr)
} }
redirectToApp(writer, request, application.ID, "") http.Redirect(writer, request, "/apps/"+application.ID, http.StatusSeeOther)
} }
// envPairJSON represents a key-value pair in the JSON request body. // envPairJSON represents a key-value pair in the JSON request body.
@@ -1043,7 +1031,7 @@ func (h *Handlers) HandleLabelDelete() http.HandlerFunc {
h.log.Error("failed to delete label", "error", deleteErr) h.log.Error("failed to delete label", "error", deleteErr)
} }
redirectToApp(writer, request, appID, "") http.Redirect(writer, request, "/apps/"+appID, http.StatusSeeOther)
} }
} }
@@ -1071,7 +1059,12 @@ func (h *Handlers) HandleVolumeAdd() http.HandlerFunc {
readOnly := request.FormValue("readonly") == "1" readOnly := request.FormValue("readonly") == "1"
if hostPath == "" || containerPath == "" { if hostPath == "" || containerPath == "" {
redirectToApp(writer, request, application.ID, "") http.Redirect(
writer,
request,
"/apps/"+application.ID,
http.StatusSeeOther,
)
return return
} }
@@ -1079,7 +1072,7 @@ func (h *Handlers) HandleVolumeAdd() http.HandlerFunc {
pathErr := validateVolumePaths(hostPath, containerPath) pathErr := validateVolumePaths(hostPath, containerPath)
if pathErr != nil { if pathErr != nil {
h.log.Error("invalid volume path", "error", pathErr) h.log.Error("invalid volume path", "error", pathErr)
redirectToApp(writer, request, application.ID, "") http.Redirect(writer, request, "/apps/"+application.ID, http.StatusSeeOther)
return return
} }
@@ -1095,7 +1088,7 @@ func (h *Handlers) HandleVolumeAdd() http.HandlerFunc {
h.log.Error("failed to add volume", "error", saveErr) h.log.Error("failed to add volume", "error", saveErr)
} }
redirectToApp(writer, request, application.ID, "") http.Redirect(writer, request, "/apps/"+application.ID, http.StatusSeeOther)
} }
} }
@@ -1124,7 +1117,7 @@ func (h *Handlers) HandleVolumeDelete() http.HandlerFunc {
h.log.Error("failed to delete volume", "error", deleteErr) h.log.Error("failed to delete volume", "error", deleteErr)
} }
redirectToApp(writer, request, appID, "") http.Redirect(writer, request, "/apps/"+appID, http.StatusSeeOther)
} }
} }
@@ -1152,7 +1145,7 @@ func (h *Handlers) HandlePortAdd() http.HandlerFunc {
request.FormValue("container_port"), request.FormValue("container_port"),
) )
if !valid { if !valid {
redirectToApp(writer, request, application.ID, "") http.Redirect(writer, request, "/apps/"+application.ID, http.StatusSeeOther)
return return
} }
@@ -1173,7 +1166,7 @@ func (h *Handlers) HandlePortAdd() http.HandlerFunc {
h.log.Error("failed to save port", "error", saveErr) h.log.Error("failed to save port", "error", saveErr)
} }
redirectToApp(writer, request, application.ID, "") http.Redirect(writer, request, "/apps/"+application.ID, http.StatusSeeOther)
} }
} }
@@ -1219,7 +1212,7 @@ func (h *Handlers) HandlePortDelete() http.HandlerFunc {
h.log.Error("failed to delete port", "error", deleteErr) h.log.Error("failed to delete port", "error", deleteErr)
} }
redirectToApp(writer, request, appID, "") http.Redirect(writer, request, "/apps/"+appID, http.StatusSeeOther)
} }
} }
@@ -1281,7 +1274,7 @@ func (h *Handlers) HandleLabelEdit() http.HandlerFunc {
value := request.FormValue("value") value := request.FormValue("value")
if key == "" || value == "" { if key == "" || value == "" {
redirectToApp(writer, request, appID, "") http.Redirect(writer, request, "/apps/"+appID, http.StatusSeeOther)
return return
} }
@@ -1294,7 +1287,7 @@ func (h *Handlers) HandleLabelEdit() http.HandlerFunc {
h.log.Error("failed to update label", "error", saveErr) h.log.Error("failed to update label", "error", saveErr)
} }
redirectToApp(writer, request, appID, "") http.Redirect(writer, request, "/apps/"+appID, http.StatusSeeOther)
} }
} }
@@ -1330,7 +1323,7 @@ func (h *Handlers) HandleVolumeEdit() http.HandlerFunc {
readOnly := request.FormValue("readonly") == "1" readOnly := request.FormValue("readonly") == "1"
if hostPath == "" || containerPath == "" { if hostPath == "" || containerPath == "" {
redirectToApp(writer, request, appID, "") http.Redirect(writer, request, "/apps/"+appID, http.StatusSeeOther)
return return
} }
@@ -1338,7 +1331,7 @@ func (h *Handlers) HandleVolumeEdit() http.HandlerFunc {
pathErr := validateVolumePaths(hostPath, containerPath) pathErr := validateVolumePaths(hostPath, containerPath)
if pathErr != nil { if pathErr != nil {
h.log.Error("invalid volume path", "error", pathErr) h.log.Error("invalid volume path", "error", pathErr)
redirectToApp(writer, request, appID, "") http.Redirect(writer, request, "/apps/"+appID, http.StatusSeeOther)
return return
} }
@@ -1352,7 +1345,7 @@ func (h *Handlers) HandleVolumeEdit() http.HandlerFunc {
h.log.Error("failed to update volume", "error", saveErr) h.log.Error("failed to update volume", "error", saveErr)
} }
redirectToApp(writer, request, appID, "") http.Redirect(writer, request, "/apps/"+appID, http.StatusSeeOther)
} }
} }
+33 -59
View File
@@ -193,8 +193,7 @@ func TestHandleHealthCheck(t *testing.T) {
testCtx := setupTestHandlers(t) testCtx := setupTestHandlers(t)
request := httptest.NewRequestWithContext( request := httptest.NewRequest(
t.Context(),
http.MethodGet, http.MethodGet,
"/.well-known/healthcheck.json", "/.well-known/healthcheck.json",
nil, nil,
@@ -219,7 +218,7 @@ func TestHandleSetupGET(t *testing.T) {
testCtx := setupTestHandlers(t) testCtx := setupTestHandlers(t)
request := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/setup", nil) request := httptest.NewRequest(http.MethodGet, "/setup", nil)
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleSetupGET() handler := testCtx.handlers.HandleSetupGET()
@@ -231,18 +230,14 @@ func TestHandleSetupGET(t *testing.T) {
} }
func createSetupFormRequest( func createSetupFormRequest(
t *testing.T,
username, password, confirm string, username, password, confirm string,
) *http.Request { ) *http.Request {
t.Helper()
form := url.Values{} form := url.Values{}
form.Set("username", username) form.Set("username", username)
form.Set("password", password) form.Set("password", password)
form.Set("password_confirm", confirm) form.Set("password_confirm", confirm)
request := httptest.NewRequestWithContext( request := httptest.NewRequest(
t.Context(),
http.MethodPost, http.MethodPost,
"/setup", "/setup",
strings.NewReader(form.Encode()), strings.NewReader(form.Encode()),
@@ -257,7 +252,7 @@ func TestHandleSetupPOSTCreatesUserAndRedirects(t *testing.T) {
testCtx := setupTestHandlers(t) testCtx := setupTestHandlers(t)
request := createSetupFormRequest(t, "admin", "password123", "password123") request := createSetupFormRequest("admin", "password123", "password123")
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleSetupPOST() handler := testCtx.handlers.HandleSetupPOST()
@@ -272,7 +267,7 @@ func TestHandleSetupPOSTRejectsEmptyUsername(t *testing.T) {
testCtx := setupTestHandlers(t) testCtx := setupTestHandlers(t)
request := createSetupFormRequest(t, "", "password123", "password123") request := createSetupFormRequest("", "password123", "password123")
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleSetupPOST() handler := testCtx.handlers.HandleSetupPOST()
@@ -287,7 +282,7 @@ func TestHandleSetupPOSTRejectsShortPassword(t *testing.T) {
testCtx := setupTestHandlers(t) testCtx := setupTestHandlers(t)
request := createSetupFormRequest(t, "admin", "short", "short") request := createSetupFormRequest("admin", "short", "short")
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleSetupPOST() handler := testCtx.handlers.HandleSetupPOST()
@@ -302,7 +297,7 @@ func TestHandleSetupPOSTRejectsMismatchedPasswords(t *testing.T) {
testCtx := setupTestHandlers(t) testCtx := setupTestHandlers(t)
request := createSetupFormRequest(t, "admin", "password123", "different123") request := createSetupFormRequest("admin", "password123", "different123")
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleSetupPOST() handler := testCtx.handlers.HandleSetupPOST()
@@ -320,7 +315,7 @@ func TestHandleLoginGET(t *testing.T) {
testCtx := setupTestHandlers(t) testCtx := setupTestHandlers(t)
request := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/login", nil) request := httptest.NewRequest(http.MethodGet, "/login", nil)
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleLoginGET() handler := testCtx.handlers.HandleLoginGET()
@@ -331,15 +326,12 @@ func TestHandleLoginGET(t *testing.T) {
}) })
} }
func createLoginFormRequest(t *testing.T, username, password string) *http.Request { func createLoginFormRequest(username, password string) *http.Request {
t.Helper()
form := url.Values{} form := url.Values{}
form.Set("username", username) form.Set("username", username)
form.Set("password", password) form.Set("password", password)
request := httptest.NewRequestWithContext( request := httptest.NewRequest(
t.Context(),
http.MethodPost, http.MethodPost,
"/login", "/login",
strings.NewReader(form.Encode()), strings.NewReader(form.Encode()),
@@ -362,7 +354,7 @@ func TestHandleLoginPOSTAuthenticatesValidCredentials(t *testing.T) {
) )
require.NoError(t, createErr) require.NoError(t, createErr)
request := createLoginFormRequest(t, "testuser", "testpass123") request := createLoginFormRequest("testuser", "testpass123")
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleLoginPOST() handler := testCtx.handlers.HandleLoginPOST()
@@ -385,7 +377,7 @@ func TestHandleLoginPOSTRejectsInvalidCredentials(t *testing.T) {
) )
require.NoError(t, createErr) require.NoError(t, createErr)
request := createLoginFormRequest(t, "testuser", "wrongpassword") request := createLoginFormRequest("testuser", "wrongpassword")
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleLoginPOST() handler := testCtx.handlers.HandleLoginPOST()
@@ -403,7 +395,7 @@ func TestHandleDashboard(t *testing.T) {
testCtx := setupTestHandlers(t) testCtx := setupTestHandlers(t)
request := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil) request := httptest.NewRequest(http.MethodGet, "/", nil)
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleDashboard() handler := testCtx.handlers.HandleDashboard()
@@ -421,7 +413,7 @@ func TestHandleDashboard(t *testing.T) {
// Create an app so the template iterates over AppStats and hits .CSRFField // Create an app so the template iterates over AppStats and hits .CSRFField
createTestApp(t, testCtx, "csrf-test-app") createTestApp(t, testCtx, "csrf-test-app")
request := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil) request := httptest.NewRequest(http.MethodGet, "/", nil)
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleDashboard() handler := testCtx.handlers.HandleDashboard()
@@ -441,9 +433,7 @@ func TestHandleAppNew(t *testing.T) {
testCtx := setupTestHandlers(t) testCtx := setupTestHandlers(t)
request := httptest.NewRequestWithContext( request := httptest.NewRequest(http.MethodGet, "/apps/new", nil)
t.Context(), http.MethodGet, "/apps/new", nil,
)
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleAppNew() handler := testCtx.handlers.HandleAppNew()
@@ -511,8 +501,7 @@ func TestHandleWebhookRejectsOversizedBody(t *testing.T) {
// Create a body larger than 1MB - it should be silently truncated // Create a body larger than 1MB - it should be silently truncated
// and the webhook should still process (or fail gracefully on parse) // and the webhook should still process (or fail gracefully on parse)
largePayload := strings.Repeat("x", 2*1024*1024) // 2MB largePayload := strings.Repeat("x", 2*1024*1024) // 2MB
request := httptest.NewRequestWithContext( request := httptest.NewRequest(
t.Context(),
http.MethodPost, http.MethodPost,
"/webhook/"+createdApp.WebhookSecret, "/webhook/"+createdApp.WebhookSecret,
strings.NewReader(largePayload), strings.NewReader(largePayload),
@@ -555,8 +544,7 @@ func testOwnershipVerification(t *testing.T, cfg ownedResourceTestConfig) {
resourceID := cfg.createFn(t, testCtx, app1) resourceID := cfg.createFn(t, testCtx, app1)
request := httptest.NewRequestWithContext( request := httptest.NewRequest(
t.Context(),
http.MethodPost, http.MethodPost,
cfg.deletePath(app2.ID, resourceID), cfg.deletePath(app2.ID, resourceID),
nil, nil,
@@ -595,8 +583,7 @@ func TestHandleEnvVarSaveBulk(t *testing.T) {
r := chi.NewRouter() r := chi.NewRouter()
r.Post("/apps/{id}/env", testCtx.handlers.HandleEnvVarSave()) r.Post("/apps/{id}/env", testCtx.handlers.HandleEnvVarSave())
request := httptest.NewRequestWithContext( request := httptest.NewRequest(
t.Context(),
http.MethodPost, http.MethodPost,
"/apps/"+createdApp.ID+"/env", "/apps/"+createdApp.ID+"/env",
strings.NewReader(body), strings.NewReader(body),
@@ -638,8 +625,7 @@ func TestHandleEnvVarSaveAppNotFound(t *testing.T) {
r := chi.NewRouter() r := chi.NewRouter()
r.Post("/apps/{id}/env", testCtx.handlers.HandleEnvVarSave()) r.Post("/apps/{id}/env", testCtx.handlers.HandleEnvVarSave())
request := httptest.NewRequestWithContext( request := httptest.NewRequest(
t.Context(),
http.MethodPost, http.MethodPost,
"/apps/nonexistent-id/env", "/apps/nonexistent-id/env",
strings.NewReader(body), strings.NewReader(body),
@@ -665,8 +651,7 @@ func TestHandleEnvVarSaveEmptyKeyRejected(t *testing.T) {
r := chi.NewRouter() r := chi.NewRouter()
r.Post("/apps/{id}/env", testCtx.handlers.HandleEnvVarSave()) r.Post("/apps/{id}/env", testCtx.handlers.HandleEnvVarSave())
request := httptest.NewRequestWithContext( request := httptest.NewRequest(
t.Context(),
http.MethodPost, http.MethodPost,
"/apps/"+createdApp.ID+"/env", "/apps/"+createdApp.ID+"/env",
strings.NewReader(body), strings.NewReader(body),
@@ -693,8 +678,7 @@ func TestHandleEnvVarSaveDuplicateKeyRejected(t *testing.T) {
r := chi.NewRouter() r := chi.NewRouter()
r.Post("/apps/{id}/env", testCtx.handlers.HandleEnvVarSave()) r.Post("/apps/{id}/env", testCtx.handlers.HandleEnvVarSave())
request := httptest.NewRequestWithContext( request := httptest.NewRequest(
t.Context(),
http.MethodPost, http.MethodPost,
"/apps/"+createdApp.ID+"/env", "/apps/"+createdApp.ID+"/env",
strings.NewReader(body), strings.NewReader(body),
@@ -732,8 +716,7 @@ func TestHandleEnvVarSaveCrossAppIsolation(t *testing.T) {
r := chi.NewRouter() r := chi.NewRouter()
r.Post("/apps/{id}/env", testCtx.handlers.HandleEnvVarSave()) r.Post("/apps/{id}/env", testCtx.handlers.HandleEnvVarSave())
request := httptest.NewRequestWithContext( request := httptest.NewRequest(
t.Context(),
http.MethodPost, http.MethodPost,
"/apps/"+appA.ID+"/env", "/apps/"+appA.ID+"/env",
strings.NewReader(body), strings.NewReader(body),
@@ -796,8 +779,7 @@ func TestHandleEnvVarSaveBodySizeLimit(t *testing.T) {
r := chi.NewRouter() r := chi.NewRouter()
r.Post("/apps/{id}/env", testCtx.handlers.HandleEnvVarSave()) r.Post("/apps/{id}/env", testCtx.handlers.HandleEnvVarSave())
request := httptest.NewRequestWithContext( request := httptest.NewRequest(
t.Context(),
http.MethodPost, http.MethodPost,
"/apps/"+createdApp.ID+"/env", "/apps/"+createdApp.ID+"/env",
strings.NewReader(sb.String()), strings.NewReader(sb.String()),
@@ -866,8 +848,7 @@ func TestDeleteVolumeOwnershipVerification(t *testing.T) {
require.NoError(t, volume.Save(context.Background())) require.NoError(t, volume.Save(context.Background()))
// Try to delete app1's volume using app2's URL path // Try to delete app1's volume using app2's URL path
request := httptest.NewRequestWithContext( request := httptest.NewRequest(
t.Context(),
http.MethodPost, http.MethodPost,
"/apps/"+app2.ID+"/volumes/"+strconv.FormatInt(volume.ID, 10)+"/delete", "/apps/"+app2.ID+"/volumes/"+strconv.FormatInt(volume.ID, 10)+"/delete",
nil, nil,
@@ -908,8 +889,7 @@ func TestDeletePortOwnershipVerification(t *testing.T) {
require.NoError(t, port.Save(context.Background())) require.NoError(t, port.Save(context.Background()))
// Try to delete app1's port using app2's URL path // Try to delete app1's port using app2's URL path
request := httptest.NewRequestWithContext( request := httptest.NewRequest(
t.Context(),
http.MethodPost, http.MethodPost,
"/apps/"+app2.ID+"/ports/"+strconv.FormatInt(port.ID, 10)+"/delete", "/apps/"+app2.ID+"/ports/"+strconv.FormatInt(port.ID, 10)+"/delete",
nil, nil,
@@ -950,8 +930,7 @@ func TestHandleEnvVarSaveEmptyClears(t *testing.T) {
r := chi.NewRouter() r := chi.NewRouter()
r.Post("/apps/{id}/env", testCtx.handlers.HandleEnvVarSave()) r.Post("/apps/{id}/env", testCtx.handlers.HandleEnvVarSave())
request := httptest.NewRequestWithContext( request := httptest.NewRequest(
t.Context(),
http.MethodPost, http.MethodPost,
"/apps/"+createdApp.ID+"/env", "/apps/"+createdApp.ID+"/env",
strings.NewReader("[]"), strings.NewReader("[]"),
@@ -1000,8 +979,7 @@ func TestHandleVolumeAddValidatesPaths(t *testing.T) {
form.Set("host_path", tt.hostPath) form.Set("host_path", tt.hostPath)
form.Set("container_path", tt.containerPath) form.Set("container_path", tt.containerPath)
request := httptest.NewRequestWithContext( request := httptest.NewRequest(
t.Context(),
http.MethodPost, http.MethodPost,
"/apps/"+createdApp.ID+"/volumes", "/apps/"+createdApp.ID+"/volumes",
strings.NewReader(form.Encode()), strings.NewReader(form.Encode()),
@@ -1060,7 +1038,7 @@ func TestSetupRequiredExemptsHealthAndStaticAndAPI(t *testing.T) {
t.Run(path, func(t *testing.T) { t.Run(path, func(t *testing.T) {
t.Parallel() t.Parallel()
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, path, nil) req := httptest.NewRequest(http.MethodGet, path, nil)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
wrapped.ServeHTTP(rr, req) wrapped.ServeHTTP(rr, req)
@@ -1073,7 +1051,7 @@ func TestSetupRequiredExemptsHealthAndStaticAndAPI(t *testing.T) {
t.Run("non-exempt redirects", func(t *testing.T) { t.Run("non-exempt redirects", func(t *testing.T) {
t.Parallel() t.Parallel()
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil) req := httptest.NewRequest(http.MethodGet, "/", nil)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
wrapped.ServeHTTP(rr, req) wrapped.ServeHTTP(rr, req)
@@ -1089,8 +1067,7 @@ func TestHandleCancelDeployRedirects(t *testing.T) {
createdApp := createTestApp(t, testCtx, "cancel-deploy-app") createdApp := createTestApp(t, testCtx, "cancel-deploy-app")
request := httptest.NewRequestWithContext( request := httptest.NewRequest(
t.Context(),
http.MethodPost, http.MethodPost,
"/apps/"+createdApp.ID+"/deployments/cancel", "/apps/"+createdApp.ID+"/deployments/cancel",
nil, nil,
@@ -1110,8 +1087,7 @@ func TestHandleCancelDeployReturns404ForUnknownApp(t *testing.T) {
testCtx := setupTestHandlers(t) testCtx := setupTestHandlers(t)
request := httptest.NewRequestWithContext( request := httptest.NewRequest(
t.Context(),
http.MethodPost, http.MethodPost,
"/apps/nonexistent/deployments/cancel", "/apps/nonexistent/deployments/cancel",
nil, nil,
@@ -1132,8 +1108,7 @@ func TestHandleWebhookReturns404ForUnknownSecret(t *testing.T) {
webhookURL := "/webhook/unknown-secret" webhookURL := "/webhook/unknown-secret"
payload := `{"ref": "refs/heads/main"}` payload := `{"ref": "refs/heads/main"}`
request := httptest.NewRequestWithContext( request := httptest.NewRequest(
t.Context(),
http.MethodPost, http.MethodPost,
webhookURL, webhookURL,
strings.NewReader(payload), strings.NewReader(payload),
@@ -1168,8 +1143,7 @@ func TestHandleWebhookProcessesValidWebhook(t *testing.T) {
payload := `{"ref": "refs/heads/main", "after": "abc123"}` payload := `{"ref": "refs/heads/main", "after": "abc123"}`
webhookURL := "/webhook/" + createdApp.WebhookSecret webhookURL := "/webhook/" + createdApp.WebhookSecret
request := httptest.NewRequestWithContext( request := httptest.NewRequest(
t.Context(),
http.MethodPost, http.MethodPost,
webhookURL, webhookURL,
strings.NewReader(payload), strings.NewReader(payload),
+3 -3
View File
@@ -16,7 +16,7 @@ func TestRenderTemplateBuffersOutput(t *testing.T) {
testCtx := setupTestHandlers(t) testCtx := setupTestHandlers(t)
// The setup page is simple and has no DB dependencies // The setup page is simple and has no DB dependencies
request := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/setup", nil) request := httptest.NewRequest(http.MethodGet, "/setup", nil)
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleSetupGET() handler := testCtx.handlers.HandleSetupGET()
@@ -39,7 +39,7 @@ func TestDashboardRenderTemplateBuffersOutput(t *testing.T) {
testCtx := setupTestHandlers(t) testCtx := setupTestHandlers(t)
request := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil) request := httptest.NewRequest(http.MethodGet, "/", nil)
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleDashboard() handler := testCtx.handlers.HandleDashboard()
@@ -59,7 +59,7 @@ func TestLoginRenderTemplateBuffersOutput(t *testing.T) {
testCtx := setupTestHandlers(t) testCtx := setupTestHandlers(t)
request := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/login", nil) request := httptest.NewRequest(http.MethodGet, "/login", nil)
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleLoginGET() handler := testCtx.handlers.HandleLoginGET()
+3 -3
View File
@@ -32,7 +32,7 @@ func TestCORS_NoOriginsConfigured_NoCORSHeaders(t *testing.T) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
})) }))
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil) req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Origin", "https://evil.com") req.Header.Set("Origin", "https://evil.com")
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
@@ -50,7 +50,7 @@ func TestCORS_OriginsConfigured_AllowsMatchingOrigin(t *testing.T) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
})) }))
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil) req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Origin", "https://app.example.com") req.Header.Set("Origin", "https://app.example.com")
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
@@ -70,7 +70,7 @@ func TestCORS_OriginsConfigured_RejectsNonMatchingOrigin(t *testing.T) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
})) }))
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil) req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Origin", "https://evil.com") req.Header.Set("Origin", "https://evil.com")
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
+7 -7
View File
@@ -36,7 +36,7 @@ func TestLoginRateLimitAllowsUpToBurst(t *testing.T) {
// First 5 requests should succeed (burst) // First 5 requests should succeed (burst)
for i := range 5 { for i := range 5 {
req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/login", nil) req := httptest.NewRequest(http.MethodPost, "/login", nil)
req.RemoteAddr = "192.168.1.1:12345" req.RemoteAddr = "192.168.1.1:12345"
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req) handler.ServeHTTP(rec, req)
@@ -44,7 +44,7 @@ func TestLoginRateLimitAllowsUpToBurst(t *testing.T) {
} }
// 6th request should be rate limited // 6th request should be rate limited
req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/login", nil) req := httptest.NewRequest(http.MethodPost, "/login", nil)
req.RemoteAddr = "192.168.1.1:12345" req.RemoteAddr = "192.168.1.1:12345"
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req) handler.ServeHTTP(rec, req)
@@ -63,21 +63,21 @@ func TestLoginRateLimitIsolatesIPs(t *testing.T) {
// Exhaust IP1's budget // Exhaust IP1's budget
for range 5 { for range 5 {
req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/login", nil) req := httptest.NewRequest(http.MethodPost, "/login", nil)
req.RemoteAddr = "10.0.0.1:1234" req.RemoteAddr = "10.0.0.1:1234"
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req) handler.ServeHTTP(rec, req)
} }
// IP1 should be blocked // IP1 should be blocked
req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/login", nil) req := httptest.NewRequest(http.MethodPost, "/login", nil)
req.RemoteAddr = "10.0.0.1:1234" req.RemoteAddr = "10.0.0.1:1234"
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req) handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusTooManyRequests, rec.Code) assert.Equal(t, http.StatusTooManyRequests, rec.Code)
// IP2 should still work // IP2 should still work
req2 := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/login", nil) req2 := httptest.NewRequest(http.MethodPost, "/login", nil)
req2.RemoteAddr = "10.0.0.2:1234" req2.RemoteAddr = "10.0.0.2:1234"
rec2 := httptest.NewRecorder() rec2 := httptest.NewRecorder()
handler.ServeHTTP(rec2, req2) handler.ServeHTTP(rec2, req2)
@@ -96,13 +96,13 @@ func TestLoginRateLimitReturns429Body(t *testing.T) {
// Exhaust burst // Exhaust burst
for range 5 { for range 5 {
req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/login", nil) req := httptest.NewRequest(http.MethodPost, "/login", nil)
req.RemoteAddr = "172.16.0.1:5555" req.RemoteAddr = "172.16.0.1:5555"
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req) handler.ServeHTTP(rec, req)
} }
req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/login", nil) req := httptest.NewRequest(http.MethodPost, "/login", nil)
req.RemoteAddr = "172.16.0.1:5555" req.RemoteAddr = "172.16.0.1:5555"
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req) handler.ServeHTTP(rec, req)
+2 -2
View File
@@ -121,7 +121,7 @@ func getSessionCookie(t *testing.T, svc *auth.Service) *http.Cookie {
require.NoError(t, err) require.NoError(t, err)
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
request := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil) request := httptest.NewRequest(http.MethodGet, "/", nil)
err = svc.CreateSession(recorder, request, user) err = svc.CreateSession(recorder, request, user)
require.NoError(t, err) require.NoError(t, err)
@@ -380,7 +380,7 @@ func TestDestroySessionMaxAge(testingT *testing.T) {
defer cleanup() defer cleanup()
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
request := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil) request := httptest.NewRequest(http.MethodGet, "/", nil)
err := svc.DestroySession(recorder, request) err := svc.DestroySession(recorder, request)
require.NoError(t, err) require.NoError(t, err)