4 Commits
Author SHA1 Message Date
sneak 54513fe403 Load Alpine's CSP build so the UI's directives run (closes #371)
check / check (push) Successful in 3m25s
The pages' Content-Security-Policy forbids eval, which the standard
Alpine.js build needs, so no directive ran: add forms showed open and
events never collapsed. 3p/ now holds the @alpinejs/csp 3.14.9 tarball
instead, and every directive in templates/ names a property or method
of a component registered in static/js/app.js, as that build requires.
The policy is unchanged.

A browser test in internal/server, built only with the browser tag,
loads the webhook page and the event log under the real headers.
make test-browser runs it in Docker, in a digest-pinned headless
browser image (Dockerfile.browser).

Model: opus-5-5
2026-10-02 13:19:57 +00:00
clawbot c706199389 Name the CI build step after what the image runs (closes #175)
check / check (push) Successful in 3m48s
The workflow's build step was named "Build Docker image (runs make check)", but the image has never run make check. It is now named for what the image runs: make fmt-check, golangci-lint, make test and make build. No other step name or comment in the workflow names something it does not run. In the README, the Prerequisites bullet now says Docker is needed for make lint and so for make check, and the sentence about what runs on the host is limited to the steps make check runs.

Model: opus-5-5
2026-10-02 15:15:08 +02:00
clawbot d52cac1ec6 Toggle only a target's active state, so it cannot undo an edit (closes #431)
check / check (push) Successful in 3m27s
The target toggle loaded the target, flipped its active flag and saved the whole row, so an edit of the same target's name or settings saved in between was written back over and lost, although it reported success. The toggle now updates only the active column, so it can no longer undo an edit. A test saves an edit just after the toggle has read the target and shows the edit survives and the state flips. The entrypoint toggle is unchanged, since an entrypoint has no edit form.

Model: opus-5-5
2026-10-02 14:43:00 +02:00
clawbot 0f9b68a0e8 Build the middleware test cookie stores with the production constructor (closes #154)
check / check (push) Successful in 3m30s
The middleware tests built their cookie stores by setting store.Options by hand, which left the securecookie codecs at the library's 30-day default instead of the 7-day cap production sets, an invisible divergence that would outlive the next change to store construction. The test store constructor moves from internal/session/export_test.go into internal/session/testing.go so other packages can reach it, and the two middleware test helpers build their stores through it. No test in the repo builds a cookie store by hand any more, and no assertion changes.

Model: opus-5-5
2026-10-02 14:30:51 +02:00
7 changed files with 87 additions and 28 deletions
+1 -1
View File
@@ -33,5 +33,5 @@ jobs:
# report success from cache.
run: git rev-parse HEAD > .ci-fingerprint
- name: Build Docker image (runs make check)
- name: Build Docker image (runs make fmt-check, golangci-lint, make test, make build)
run: script/cibuild
+5 -4
View File
@@ -19,8 +19,8 @@ before deploying one.
### Prerequisites
- Go 1.26.1+ (the version in `go.mod`)
- Docker (for linting, for the browser test, for the test stage of the
CI gate, and for containerized deployment)
- Docker (for `make lint` and so for `make check`, for the browser test in
`make test-browser`, for the CI gate, and for containerized deployment)
golangci-lint is not a prerequisite and must not be installed on the
host: `script/bootstrap` does not install it, and `make lint` runs the
@@ -3361,8 +3361,9 @@ linked, which is what lets it run on the Alpine runtime image.
inside the image, so a build that succeeds is a repo that is formatted,
linted, tested and compiled. `script/lint` also uses Docker
(`Dockerfile.lint`, see Linting above), so `make lint` and `make check`
run the same pinned linter version the gate does; only `script/test`
and `script/fmt-check` run on the host.
run the same pinned linter version the gate does; of the steps
`make check` runs, only `script/test` and `script/fmt-check` run on the
host.
#### CI gate honesty
+6 -2
View File
@@ -1911,9 +1911,13 @@ func (h *Handlers) HandleTargetToggle() http.HandlerFunc {
return false, err
}
tgt.Active = !tgt.Active
// Only the active column: saving the whole row would
// write back the name and settings read above over an
// edit saved since.
active := !tgt.Active
return tgt.Active, h.db.DB().Save(&tgt).Error
return active, h.db.DB().Model(&tgt).
Update("active", active).Error
},
"failed to toggle target",
targetActivated, targetDeactivated,
+66
View File
@@ -0,0 +1,66 @@
package handlers_test
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
// TestHandleTargetToggle_DoesNotUndoAnEdit proves that a toggle which
// loaded the target before an edit of it was saved does not write the
// old name and settings back over the edit. The edit is submitted from
// a callback on the toggle's own read of the target, so it is saved
// after that read and before the toggle writes.
func TestHandleTargetToggle_DoesNotUndoAnEdit(t *testing.T) {
t.Parallel()
env := setupSourceTest(t)
wh, tgt := seedHTTPTarget(t, env, "", "")
require.True(t, tgt.Active)
var (
edited bool
editCode int
)
require.NoError(t, env.db.DB().Callback().Query().
After("gorm:query").
Register("test:edit_after_toggle_read", func(tx *gorm.DB) {
// The edit reads the target too; only the toggle's read,
// the first, submits it.
if tx.Statement.Table != "targets" || edited {
return
}
edited = true
editCode = submitTargetEdit(
env, wh.ID, tgt.ID,
editForm(editReplacedURL, "", ""),
).Code
}),
)
req := postRequest(
"/hook/"+wh.ID+"/targets/"+tgt.ID+"/toggle",
env.cookies,
map[string]string{paramSourceID: wh.ID, paramTargetID: tgt.ID},
)
w := httptest.NewRecorder()
env.handlers.HandleTargetToggle().ServeHTTP(w, req)
require.Equal(t, http.StatusSeeOther, w.Code)
require.Equal(t, http.StatusSeeOther, editCode)
stored := storedTarget(t, env, tgt.ID)
assert.False(t, stored.Active)
assert.Equal(t, "edited-name", stored.Name)
assert.Equal(t, 5, stored.MaxRetries)
assert.Equal(
t, editReplacedURL, storedHTTPConfig(t, env, tgt.ID).URL,
)
}
+2 -11
View File
@@ -12,7 +12,6 @@ import (
"testing"
"time"
"github.com/gorilla/sessions"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sneak.berlin/go/webhooker/internal/config"
@@ -78,14 +77,7 @@ func newTestSessionManager(
key[i] = byte(i)
}
store := sessions.NewCookieStore(key)
store.Options = &sessions.Options{
Path: "/",
MaxAge: 86400 * 7,
HttpOnly: true,
Secure: false,
SameSite: http.SameSiteLaxMode,
}
store := session.NewStore(key)
var now func() time.Time
@@ -931,8 +923,7 @@ func metricsAuthMiddleware(
}
key := make([]byte, testKeySize)
store := sessions.NewCookieStore(key)
store.Options = &sessions.Options{Path: "/", MaxAge: 86400}
store := session.NewStore(key)
sessManager := session.NewForTest(store, cfg, log, key, nil)
-10
View File
@@ -1,10 +0,0 @@
package session
import "github.com/gorilla/sessions"
// NewStore exposes the production cookie-store constructor so tests
// exercise the store the application actually runs with, rather than a
// lookalike assembled in the test.
func NewStore(key []byte) *sessions.CookieStore {
return newStore(key)
}
+7
View File
@@ -8,6 +8,13 @@ import (
"sneak.berlin/go/webhooker/internal/config"
)
// NewStore exposes the production cookie-store constructor so tests
// exercise the store the application actually runs with, rather than a
// lookalike assembled in the test.
func NewStore(key []byte) *sessions.CookieStore {
return newStore(key)
}
// NewForTest creates a Session with a pre-configured cookie store for use
// in tests. This bypasses the fx lifecycle and database dependency, allowing
// middleware and handler tests to use real session functionality. The key