check / check (push) Waiting to run
The new-webhook page keeps name, description and retention and adds an HTTP target URL field and an archive checkbox. A filled-in URL creates an http target named HTTP; a checked archive shows a pruning choice (never, 1h, 12h, 24h, 30d, 90d, 365d) and creates a database target named Archive with that expiry. Each target goes through the add target form's validation, and the webhook, its entrypoint and its targets are created in one transaction. A refused form comes back with every value entered, retention included. Model: opus-5-5
208 lines
5.7 KiB
Go
208 lines
5.7 KiB
Go
package handlers_test
|
|
|
|
import (
|
|
"errors"
|
|
"html"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/gorm"
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
)
|
|
|
|
// submitCreateForm posts the new webhook form and returns the
|
|
// recorder.
|
|
func submitCreateForm(
|
|
env *sourceTestEnv, form url.Values,
|
|
) *httptest.ResponseRecorder {
|
|
req := formRequest("/hooks/new", env.cookies, form, nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
env.handlers.HandleSourceCreateSubmit().ServeHTTP(w, req)
|
|
|
|
return w
|
|
}
|
|
|
|
// assertNothingCreated checks that the main database holds no webhook,
|
|
// entrypoint or target.
|
|
func assertNothingCreated(t *testing.T, db *database.Database) {
|
|
t.Helper()
|
|
|
|
for _, model := range []any{
|
|
&database.Webhook{}, &database.Entrypoint{}, &database.Target{},
|
|
} {
|
|
var count int64
|
|
|
|
require.NoError(t, db.DB().Model(model).Count(&count).Error)
|
|
assert.Zerof(t, count, "%T rows were created", model)
|
|
}
|
|
}
|
|
|
|
// TestHandleSourceCreateSubmit_CreatesRequestedTargets submits the new
|
|
// webhook form with the HTTP target URL filled in or empty, and with
|
|
// the archive checkbox off or on with each pruning choice. The webhook
|
|
// gets an HTTP target only for a URL and a database target only for a
|
|
// checked archive. The pruning choice is always submitted, as the
|
|
// browser submits it while it is hidden, and is ignored when archive
|
|
// is off.
|
|
func TestHandleSourceCreateSubmit_CreatesRequestedTargets(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := setupSourceTest(t)
|
|
|
|
// Each value the archive pruning choice submits, after an empty
|
|
// one that stands for the archive checkbox left off.
|
|
expiries := []string{
|
|
"", "never", "1h", "12h", "24h", "720h", "2160h", "8760h",
|
|
}
|
|
|
|
for _, httpURL := range []string{"", editOriginalURL} {
|
|
for _, expiry := range expiries {
|
|
name := "url=" + httpURL + " archive=" + expiry
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
form := url.Values{}
|
|
form.Set("name", name)
|
|
form.Set("http_url", httpURL)
|
|
form.Set("archive_expiry", "720h")
|
|
|
|
if expiry != "" {
|
|
form.Set("archive", "on")
|
|
form.Set("archive_expiry", expiry)
|
|
}
|
|
|
|
w := submitCreateForm(env, form)
|
|
require.Equal(t, http.StatusSeeOther, w.Code, w.Body.String())
|
|
|
|
var webhook database.Webhook
|
|
|
|
require.NoError(t, env.db.DB().
|
|
Where("name = ?", name).First(&webhook).Error)
|
|
|
|
byType := map[database.TargetType]database.Target{}
|
|
for _, target := range targetsForWebhook(t, env.db, webhook.ID) {
|
|
byType[target.Type] = target
|
|
}
|
|
|
|
wantCount := 0
|
|
|
|
if httpURL != "" {
|
|
wantCount++
|
|
|
|
assert.Equal(t, "HTTP", byType[database.TargetTypeHTTP].Name)
|
|
assert.JSONEq(t, `{"url":"`+httpURL+`"}`,
|
|
byType[database.TargetTypeHTTP].Config)
|
|
}
|
|
|
|
if expiry != "" {
|
|
wantCount++
|
|
|
|
assert.Equal(t, "Archive",
|
|
byType[database.TargetTypeDatabase].Name)
|
|
assert.JSONEq(t, `{"expiry":"`+expiry+`"}`,
|
|
byType[database.TargetTypeDatabase].Config)
|
|
}
|
|
|
|
assert.Len(t, byType, wantCount)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestHandleSourceCreateSubmit_RefusedFormKeepsEveryValue refuses the
|
|
// new webhook form for an invalid HTTP target URL and for an invalid
|
|
// retention, each with archive on. Nothing is created, and the form
|
|
// comes back with the reason and every value entered: name,
|
|
// description, retention, URL, the checked archive box and the pruning
|
|
// choice.
|
|
func TestHandleSourceCreateSubmit_RefusedFormKeepsEveryValue(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
const badURL = "Invalid target URL"
|
|
|
|
cases := []struct {
|
|
name string
|
|
retention string
|
|
httpURL string
|
|
reason string
|
|
}{
|
|
{"blocked url", "7", editBlockedURL, badURL},
|
|
{"unsupported scheme", "7", "ftp://93.184.216.34/hook", badURL},
|
|
{"bad retention", "-5", editOriginalURL, "Retention must be"},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := setupSourceTest(t)
|
|
|
|
form := url.Values{}
|
|
form.Set("name", "kept name")
|
|
form.Set("description", "kept description")
|
|
form.Set("retention_days", tc.retention)
|
|
form.Set("http_url", tc.httpURL)
|
|
form.Set("archive", "on")
|
|
form.Set("archive_expiry", "2160h")
|
|
|
|
w := submitCreateForm(env, form)
|
|
require.Equal(t, http.StatusBadRequest, w.Code)
|
|
|
|
page := w.Body.String()
|
|
assert.Contains(t, page, tc.reason)
|
|
assert.Contains(t, page, `value="kept name"`)
|
|
assert.Contains(t, page, `>kept description</textarea>`)
|
|
assert.Contains(t, page, `value="`+tc.retention+`"`)
|
|
assert.Contains(t, page,
|
|
`value="`+html.EscapeString(tc.httpURL)+`"`)
|
|
assert.Contains(t, page, `name="archive" value="on" checked`)
|
|
assert.Contains(t, page, `x-data="collapsible" data-open`)
|
|
assert.Contains(t, page, `<option value="2160h" selected>`)
|
|
|
|
assertNothingCreated(t, env.db)
|
|
})
|
|
}
|
|
}
|
|
|
|
// errInjectedTargetCreate is the failure a test makes the insert of a
|
|
// target report.
|
|
var errInjectedTargetCreate = errors.New("injected target create failure")
|
|
|
|
// TestHandleSourceCreateSubmit_FailedTargetInsertCreatesNothing makes
|
|
// inserting a target fail after the webhook and its entrypoint were
|
|
// inserted, and checks that neither is left behind.
|
|
func TestHandleSourceCreateSubmit_FailedTargetInsertCreatesNothing(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
env := setupSourceTest(t)
|
|
|
|
require.NoError(t, env.db.DB().Callback().Create().
|
|
Before("gorm:create").
|
|
Register("test:fail_target_create", func(tx *gorm.DB) {
|
|
if tx.Statement.Table == "targets" {
|
|
_ = tx.AddError(errInjectedTargetCreate)
|
|
}
|
|
}),
|
|
)
|
|
|
|
form := url.Values{}
|
|
form.Set("name", "rolled back")
|
|
form.Set("archive", "on")
|
|
form.Set("archive_expiry", "never")
|
|
|
|
w := submitCreateForm(env, form)
|
|
require.Equal(t, http.StatusInternalServerError, w.Code)
|
|
|
|
assertNothingCreated(t, env.db)
|
|
}
|