check / check (push) Waiting to run
The new-webhook page gains an optional HTTP target URL, which creates an http target named HTTP, and an archive checkbox whose pruning choice (never, 1h, 12h, 24h, 30d, 90d, 365d) creates a database target named Archive with that expiry. Both are validated by the add target form's own validation, and the webhook, its entrypoint and its targets are created in one transaction or not at all. A refused form comes back with the reason and every value entered, retention included. The targets can be renamed on the webhook page like any other. 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)
|
|
}
|