Compare commits
1
Commits
next
...
f61b608b1c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f61b608b1c |
@@ -1617,10 +1617,11 @@ event routing.
|
|||||||
The new webhook form can also give the webhook its first targets: an
|
The new webhook form can also give the webhook its first targets: an
|
||||||
optional HTTP target URL creates an `http` target named `HTTP`, and the
|
optional HTTP target URL creates an `http` target named `HTTP`, and the
|
||||||
archive checkbox creates a `database` target named `Archive` whose
|
archive checkbox creates a `database` target named `Archive` whose
|
||||||
`expiry` is the pruning chosen beside it (never, 1h, 12h, 24h, 30d, 90d
|
`expiry` is the archive expiry chosen beside it (never, 1h, 12h, 24h,
|
||||||
or 365d) and whose `rotation` is the rotation chosen below that (none,
|
30d, 90d or 365d) and whose `rotation` is the rotation chosen below that
|
||||||
monthly, daily or hourly). Both are validated as on the add target form,
|
(none, monthly, daily or hourly). Both are validated as on the add
|
||||||
and the webhook and its targets are created together or not at all.
|
target form, and the webhook and its targets are created together or
|
||||||
|
not at all.
|
||||||
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
| ---------------- | ------- | ----------- |
|
| ---------------- | ------- | ----------- |
|
||||||
@@ -1707,7 +1708,7 @@ events should be forwarded.
|
|||||||
| `type` | TargetType | One of: `http`, `slack`, `database`, `log` |
|
| `type` | TargetType | One of: `http`, `slack`, `database`, `log` |
|
||||||
| `active` | boolean | Whether deliveries are enabled (default: true) |
|
| `active` | boolean | Whether deliveries are enabled (default: true) |
|
||||||
| `config` | JSON text | Type-specific configuration |
|
| `config` | JSON text | Type-specific configuration |
|
||||||
| `max_retries` | integer | Total delivery attempts for `http` and `slack` targets, not retries on top of the first: 0 is a single fire-and-forget attempt with no retries and no circuit breaker, and a value of N makes N attempts in all, with exponential backoff and a per-target circuit breaker. Ignored by `database` and `log` targets |
|
| `max_retries` | integer | Total delivery attempts for `http` and `slack` targets, not retries on top of the first: 0 is a single fire-and-forget attempt with no retries and no circuit breaker, and a value of N makes N attempts in all, with exponential backoff and a per-target circuit breaker. Ignored by `database` and `log` targets. The web UI labels it Delivery attempts |
|
||||||
|
|
||||||
**Relations:** Belongs to Webhook. Has many Deliveries.
|
**Relations:** Belongs to Webhook. Has many Deliveries.
|
||||||
|
|
||||||
@@ -1736,7 +1737,9 @@ events should be forwarded.
|
|||||||
expiry in plain units, such as "30 days", and the rotation. No external
|
expiry in plain units, such as "30 days", and the rotation. No external
|
||||||
delivery and no retries; an archive write failure fails the delivery.
|
delivery and no retries; an archive write failure fails the delivery.
|
||||||
See the database target section under "Per-Webhook Event Databases"
|
See the database target section under "Per-Webhook Event Databases"
|
||||||
for the full semantics.
|
for the full semantics. The web UI calls this type an archive: its
|
||||||
|
badge, the add target form's type list and the target edit page say
|
||||||
|
so, and its settings are labelled Archive expiry and Archive rotation.
|
||||||
- **`log`** — Write the event to the application log (stdout). Useful
|
- **`log`** — Write the event to the application log (stdout). Useful
|
||||||
for debugging.
|
for debugging.
|
||||||
|
|
||||||
@@ -3081,9 +3084,9 @@ abuse limit later; they are tracked as future work.
|
|||||||
|
|
||||||
| Method | Path | Description |
|
| Method | Path | Description |
|
||||||
| ------ | --------------- | ----------- |
|
| ------ | --------------- | ----------- |
|
||||||
| `GET` | `/pages/login` | Login page (not rate limited). Its `next` parameter names the page to return to after login; anything but a path on this site is replaced with `/` |
|
| `GET` | `/pages/login` | Sign-in page (not rate limited). Its `next` parameter names the page to return to after signing in; anything but a path on this site is replaced with `/` |
|
||||||
| `POST` | `/pages/login` | Login form submission. On success, redirects to the form's `next` when it is a path on this site, otherwise to `/`. Credentials are verified before any limit is consulted, so a correct password is never throttled; 5 FAILED attempts per minute per bucket per submitted username, then `429`. `503` if no verification slot frees up within 5s, or immediately if 16 requests are already queued for one (see [Rate Limiting](#rate-limiting)) |
|
| `POST` | `/pages/login` | Sign-in form submission. On success, redirects to the form's `next` when it is a path on this site, otherwise to `/`. Credentials are verified before any limit is consulted, so a correct password is never throttled; 5 FAILED attempts per minute per bucket per submitted username, then `429`. `503` if no verification slot frees up within 5s, or immediately if 16 requests are already queued for one (see [Rate Limiting](#rate-limiting)) |
|
||||||
| `POST` | `/pages/logout` | Logout (destroys session) |
|
| `POST` | `/pages/logout` | Sign out (destroys session) |
|
||||||
|
|
||||||
#### Authenticated Endpoints
|
#### Authenticated Endpoints
|
||||||
|
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ func maxRetriesField(t *database.Target) ConfigField {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ConfigField{
|
return ConfigField{
|
||||||
Label: "Max Retries",
|
Label: "Delivery attempts",
|
||||||
Value: retries,
|
Value: retries,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,10 +213,10 @@ func databaseConfigFields(configJSON string) []ConfigField {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return []ConfigField{{
|
return []ConfigField{{
|
||||||
Label: "Archive Expiry",
|
Label: "Archive expiry",
|
||||||
Value: value,
|
Value: value,
|
||||||
}, {
|
}, {
|
||||||
Label: "Archive Rotation",
|
Label: "Archive rotation",
|
||||||
Value: rotation,
|
Value: rotation,
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const (
|
|||||||
viewMaskedOrigin = viewExampleOrigin + "/..."
|
viewMaskedOrigin = viewExampleOrigin + "/..."
|
||||||
viewUnavailable = "(unavailable)"
|
viewUnavailable = "(unavailable)"
|
||||||
viewExpiryNever = "never"
|
viewExpiryNever = "never"
|
||||||
viewMaxRetries = "Max Retries"
|
viewMaxRetries = "Delivery attempts"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMaskedWebhookURL(t *testing.T) {
|
func TestMaskedWebhookURL(t *testing.T) {
|
||||||
@@ -329,8 +329,8 @@ func TestNewTargetViews_Database(t *testing.T) {
|
|||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
t,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"Archive Expiry": tc.want,
|
"Archive expiry": tc.want,
|
||||||
"Archive Rotation": rotationNone,
|
"Archive rotation": rotationNone,
|
||||||
},
|
},
|
||||||
fieldMap(view.Config),
|
fieldMap(view.Config),
|
||||||
)
|
)
|
||||||
@@ -365,7 +365,7 @@ func TestNewTargetViews_DatabaseRotation(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t, want, fieldMap(view.Config)["Archive Rotation"],
|
t, want, fieldMap(view.Config)["Archive rotation"],
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func expiryShown(
|
|||||||
require.Equal(t, http.StatusOK, w.Code)
|
require.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
return matched(
|
return matched(
|
||||||
`Archive Expiry:</span>\s*<span>([^<]*)</span>`, w.Body.String(),
|
`Archive expiry:</span>\s*<span>([^<]*)</span>`, w.Body.String(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func rotationShown(
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
return matched(
|
return matched(
|
||||||
`Archive Rotation:</span>\s*<span>([^<]*)</span>`,
|
`Archive rotation:</span>\s*<span>([^<]*)</span>`,
|
||||||
renderedPage(t, env, webhookID),
|
renderedPage(t, env, webhookID),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ func (h *Handlers) rejectLogin(
|
|||||||
)))
|
)))
|
||||||
h.renderLoginError(
|
h.renderLoginError(
|
||||||
w, r,
|
w, r,
|
||||||
"Too many failed login attempts. Please try again later.",
|
"Too many failed sign-in attempts. Please try again later.",
|
||||||
http.StatusTooManyRequests,
|
http.StatusTooManyRequests,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ func noticeFor(r *http.Request) *notice {
|
|||||||
},
|
},
|
||||||
resubmitNoTargets: {
|
resubmitNoTargets: {
|
||||||
Text: "Resubmitted: a new event was created, but this " +
|
Text: "Resubmitted: a new event was created, but this " +
|
||||||
"source has no active targets, so nothing was queued.",
|
"webhook has no active targets, so nothing was queued.",
|
||||||
},
|
},
|
||||||
}[noticeCode(r.URL.Query().Get(noticeParam))]
|
}[noticeCode(r.URL.Query().Get(noticeParam))]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func TestEveryPageRendersItsOwnTitle(t *testing.T) {
|
|||||||
data map[string]any
|
data map[string]any
|
||||||
title string
|
title string
|
||||||
}{
|
}{
|
||||||
{"login.html", map[string]any{}, "Login - Webhooker"},
|
{"login.html", map[string]any{}, "Sign in - Webhooker"},
|
||||||
{"profile.html", map[string]any{}, "Profile - Webhooker"},
|
{"profile.html", map[string]any{}, "Profile - Webhooker"},
|
||||||
{"settings.html", map[string]any{}, "Settings - Webhooker"},
|
{"settings.html", map[string]any{}, "Settings - Webhooker"},
|
||||||
{"sources_list.html", map[string]any{}, "Webhooks - Webhooker"},
|
{"sources_list.html", map[string]any{}, "Webhooks - Webhooker"},
|
||||||
|
|||||||
@@ -233,8 +233,13 @@ func TestHandleSourceDetail_RendersNamedTargetFields(
|
|||||||
assert.Contains(t, body, "1 configured")
|
assert.Contains(t, body, "1 configured")
|
||||||
assert.NotContains(t, body, "sekrit")
|
assert.NotContains(t, body, "sekrit")
|
||||||
|
|
||||||
assert.Contains(t, body, "Archive Expiry")
|
// The database type is called an archive: on its badge, in the
|
||||||
assert.Contains(t, body, "30 days")
|
// add target form's type list and in its settings.
|
||||||
|
list := targetList(t, body)
|
||||||
|
assert.Contains(t, list, "t-database archive Active")
|
||||||
|
assert.Contains(t, list, "Archive expiry: 30 days")
|
||||||
|
assert.Contains(t, list, "Archive rotation: none")
|
||||||
|
assert.Contains(t, body, `<option value="database">Archive</option>`)
|
||||||
|
|
||||||
// An unknown type gets the neutral placeholder, never the
|
// An unknown type gets the neutral placeholder, never the
|
||||||
// stored blob.
|
// stored blob.
|
||||||
|
|||||||
@@ -1749,7 +1749,7 @@ func (h *Handlers) setTargetFromForm(
|
|||||||
// cannot destroy the count a target is delivering with.
|
// cannot destroy the count a target is delivering with.
|
||||||
maxRetries, err := parseMaxRetries(in.MaxRetries, target.MaxRetries)
|
maxRetries, err := parseMaxRetries(in.MaxRetries, target.MaxRetries)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "Invalid max retries: " + retriesErrorMessage(err), nil
|
return "Invalid delivery attempts: " + retriesErrorMessage(err), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
target.Name = in.Name
|
target.Name = in.Name
|
||||||
|
|||||||
@@ -418,6 +418,30 @@ func TestHandleTargetEdit_PrefillsTheStoredValuesUnmasked(
|
|||||||
assert.Contains(t, page, "original-name")
|
assert.Contains(t, page, "original-name")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestHandleTargetEdit_CallsTheDatabaseTypeArchive pins the names the
|
||||||
|
// edit page of a database target gives its type and its settings to
|
||||||
|
// the ones its badge and the add target form use.
|
||||||
|
func TestHandleTargetEdit_CallsTheDatabaseTypeArchive(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := setupSourceTest(t)
|
||||||
|
webhook := seedWebhookWithRetention(t, env.db, 30)
|
||||||
|
target := seedTarget(t, env.db, webhook.ID, database.TargetTypeDatabase)
|
||||||
|
|
||||||
|
w := serveTarget(
|
||||||
|
env, http.MethodGet,
|
||||||
|
"/hook/"+webhook.ID+"/targets/"+target.ID+"/edit",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
require.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
page := w.Body.String()
|
||||||
|
|
||||||
|
assert.Contains(t, page, "Type: archive.")
|
||||||
|
assert.Contains(t, page, `class="label">Archive expiry</label>`)
|
||||||
|
assert.Contains(t, page, `class="label">Archive rotation</label>`)
|
||||||
|
}
|
||||||
|
|
||||||
// TestHandleTargetEditSubmit_Rejects covers every submission that
|
// TestHandleTargetEditSubmit_Rejects covers every submission that
|
||||||
// must not reach storage.
|
// must not reach storage.
|
||||||
//
|
//
|
||||||
@@ -588,7 +612,7 @@ func TestHandleTargetEditSubmit_RefusedFormComesBack(t *testing.T) {
|
|||||||
{
|
{
|
||||||
database.TargetTypeSlack,
|
database.TargetTypeSlack,
|
||||||
"name=edited&url=" + editOriginalURL + "&max_retries=25",
|
"name=edited&url=" + editOriginalURL + "&max_retries=25",
|
||||||
"Invalid max retries",
|
"Invalid delivery attempts",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
database.TargetTypeDatabase,
|
database.TargetTypeDatabase,
|
||||||
|
|||||||
@@ -25,14 +25,14 @@ var (
|
|||||||
// errRetriesInvalid signals a max_retries form value that is not
|
// errRetriesInvalid signals a max_retries form value that is not
|
||||||
// a non-negative whole number.
|
// a non-negative whole number.
|
||||||
errRetriesInvalid = errors.New(
|
errRetriesInvalid = errors.New(
|
||||||
"retries must be a whole number of attempts",
|
"must be a whole number",
|
||||||
)
|
)
|
||||||
|
|
||||||
// errRetriesTooLarge signals a max_retries form value that is a
|
// errRetriesTooLarge signals a max_retries form value that is a
|
||||||
// whole number but above maxTargetRetries. It is distinguished
|
// whole number but above maxTargetRetries. It is distinguished
|
||||||
// from errRetriesInvalid so the message can name the ceiling
|
// from errRetriesInvalid so the message can name the ceiling
|
||||||
// instead of implying the input was not a number.
|
// instead of implying the input was not a number.
|
||||||
errRetriesTooLarge = errors.New("retries out of range")
|
errRetriesTooLarge = errors.New("out of range")
|
||||||
)
|
)
|
||||||
|
|
||||||
// parseMaxRetries interprets a max_retries form value.
|
// parseMaxRetries interprets a max_retries form value.
|
||||||
@@ -82,7 +82,7 @@ func retriesErrorMessage(err error) string {
|
|||||||
if errors.Is(err, errRetriesTooLarge) {
|
if errors.Is(err, errRetriesTooLarge) {
|
||||||
return errRetriesTooLarge.Error() +
|
return errRetriesTooLarge.Error() +
|
||||||
": at most " + strconv.Itoa(maxTargetRetries) +
|
": at most " + strconv.Itoa(maxTargetRetries) +
|
||||||
" retries"
|
" attempts"
|
||||||
}
|
}
|
||||||
|
|
||||||
return errRetriesInvalid.Error() +
|
return errRetriesInvalid.Error() +
|
||||||
|
|||||||
@@ -354,15 +354,14 @@ func TestEntrypointCopyButtonIsProgressiveEnhancement(t *testing.T) {
|
|||||||
// and target_http gives up once the attempt number reaches
|
// and target_http gives up once the attempt number reaches
|
||||||
// max_retries), and 0 is special-cased to a single fire-and-forget
|
// max_retries), and 0 is special-cased to a single fire-and-forget
|
||||||
// attempt with no circuit breaker.
|
// attempt with no circuit breaker.
|
||||||
const maxRetriesHelp = "This is the total number of delivery attempts, " +
|
const maxRetriesHelp = "How many times each delivery is attempted in " +
|
||||||
"not retries on top of the first: a value of 3 makes three attempts " +
|
"all, the first attempt included. 0 means a single attempt with no " +
|
||||||
"in all. 0 means a single attempt with no retries and no circuit " +
|
"retries and no circuit breaker."
|
||||||
"breaker."
|
|
||||||
|
|
||||||
// TestTargetFormMaxRetriesCopyMatchesBehaviour pins the max_retries
|
// TestTargetFormMaxRetriesCopyMatchesBehaviour pins the max_retries
|
||||||
// help text on both the create form (the add-target form on the webhook
|
// label, "Delivery attempts", and help text on both the create form
|
||||||
// detail page) and the edit form, so the copy cannot drift back to
|
// (the add-target form on the webhook detail page) and the edit form,
|
||||||
// calling the number a retry count.
|
// so the copy cannot drift back to calling the number a retry count.
|
||||||
func TestTargetFormMaxRetriesCopyMatchesBehaviour(t *testing.T) {
|
func TestTargetFormMaxRetriesCopyMatchesBehaviour(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -393,6 +392,7 @@ func TestTargetFormMaxRetriesCopyMatchesBehaviour(t *testing.T) {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assert.Contains(t, createBody, "Delivery attempts:</label>")
|
||||||
assert.Contains(
|
assert.Contains(
|
||||||
t, createBody, maxRetriesHelp,
|
t, createBody, maxRetriesHelp,
|
||||||
"the add-target form must explain max_retries as total attempts",
|
"the add-target form must explain max_retries as total attempts",
|
||||||
@@ -419,8 +419,33 @@ func TestTargetFormMaxRetriesCopyMatchesBehaviour(t *testing.T) {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assert.Contains(t, editBody, `class="label">Delivery attempts</label>`)
|
||||||
assert.Contains(
|
assert.Contains(
|
||||||
t, editBody, maxRetriesHelp,
|
t, editBody, maxRetriesHelp,
|
||||||
"the target edit form must explain max_retries as total attempts",
|
"the target edit form must explain max_retries as total attempts",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestCreateFormCallsTheDatabaseTargetAnArchive pins the names the new
|
||||||
|
// webhook page gives the database target its Archive checkbox creates,
|
||||||
|
// and that target's settings, to the ones the target forms use.
|
||||||
|
func TestCreateFormCallsTheDatabaseTargetAnArchive(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var h *handlers.Handlers
|
||||||
|
|
||||||
|
var sess *session.Session
|
||||||
|
|
||||||
|
app := newTestApp(t, &h, &sess)
|
||||||
|
app.RequireStart()
|
||||||
|
|
||||||
|
t.Cleanup(app.RequireStop)
|
||||||
|
|
||||||
|
body := renderPage(t, h, sess, "sources_new.html", map[string]any{
|
||||||
|
dataKeyError: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Contains(t, body, "created with an archive target")
|
||||||
|
assert.Contains(t, body, `class="label">Archive expiry</label>`)
|
||||||
|
assert.Contains(t, body, `class="label">Archive rotation</label>`)
|
||||||
|
}
|
||||||
|
|||||||
@@ -318,31 +318,35 @@ const (
|
|||||||
func checkAddEachTargetType(ctx context.Context, t *testing.T, url string) {
|
func checkAddEachTargetType(ctx context.Context, t *testing.T, url string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
// Each target type, with the fields its add target form submits, in
|
// Each target type, with the badge its targets are listed with and
|
||||||
// page order. Only http and slack have a url field.
|
// the fields its add target form submits, in page order. Only http
|
||||||
|
// and slack have a url field.
|
||||||
targetTypes := []struct {
|
targetTypes := []struct {
|
||||||
name string
|
name string
|
||||||
|
badge string
|
||||||
fields string
|
fields string
|
||||||
values map[string]string
|
values map[string]string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"http", "csrf_token name type url headers timeout max_retries",
|
"http", "http",
|
||||||
|
"csrf_token name type url headers timeout max_retries",
|
||||||
map[string]string{"url": publicTargetURL},
|
map[string]string{"url": publicTargetURL},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slack", "csrf_token name type url max_retries",
|
"slack", "slack", "csrf_token name type url max_retries",
|
||||||
map[string]string{"url": publicTargetURL},
|
map[string]string{"url": publicTargetURL},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"database", "csrf_token name type expiry rotation",
|
"database", "archive", "csrf_token name type expiry rotation",
|
||||||
map[string]string{"expiry": "720h", "rotation": "daily"},
|
map[string]string{"expiry": "720h", "rotation": "daily"},
|
||||||
},
|
},
|
||||||
{"log", "csrf_token name type", nil},
|
{"log", "log", "csrf_token name type", nil},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range targetTypes {
|
for _, tt := range targetTypes {
|
||||||
checkAddTarget(
|
checkAddTarget(
|
||||||
ctx, t, url, tt.name, strings.Fields(tt.fields), tt.values,
|
ctx, t, url, tt.name, tt.badge,
|
||||||
|
strings.Fields(tt.fields), tt.values,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,11 +357,11 @@ func checkAddEachTargetType(ctx context.Context, t *testing.T, url string) {
|
|||||||
// type's own fields in place of the choice, and the form then submits
|
// type's own fields in place of the choice, and the form then submits
|
||||||
// exactly fields, so a field another type uses, such as url, is absent;
|
// exactly fields, so a field another type uses, such as url, is absent;
|
||||||
// Cancel closes it again. It then adds a target of the type, filling in
|
// Cancel closes it again. It then adds a target of the type, filling in
|
||||||
// values, and checks that the section lists it with that type.
|
// values, and checks that the section lists it with badge.
|
||||||
func checkAddTarget(
|
func checkAddTarget(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
url, targetType string,
|
url, targetType, badge string,
|
||||||
fields []string,
|
fields []string,
|
||||||
values map[string]string,
|
values map[string]string,
|
||||||
) {
|
) {
|
||||||
@@ -412,8 +416,8 @@ func checkAddTarget(
|
|||||||
|
|
||||||
click(ctx, t, saveButton)
|
click(ctx, t, saveButton)
|
||||||
assert.Truef(t, shown(ctx, `//span[text()="`+name+
|
assert.Truef(t, shown(ctx, `//span[text()="`+name+
|
||||||
`"]/following-sibling::div/span[text()="`+targetType+`"]`),
|
`"]/following-sibling::div/span[text()="`+badge+`"]`),
|
||||||
"%s: the added target is not listed with its type", targetType)
|
"%s: the added target is not listed as %s", targetType, badge)
|
||||||
}
|
}
|
||||||
|
|
||||||
// chooseTargetType clicks Add, picks targetType and clicks Next, and
|
// chooseTargetType clicks Add, picks targetType and clicks Next, and
|
||||||
@@ -466,10 +470,10 @@ func checkArchiveChoices(ctx context.Context, t *testing.T, url string) {
|
|||||||
assert.Equal(t, "none", startRotation,
|
assert.Equal(t, "none", startRotation,
|
||||||
"the add target form's archive rotation does not start on none")
|
"the add target form's archive rotation does not start on none")
|
||||||
|
|
||||||
assert.True(t, shown(ctx, row+`//span[text()="Archive Expiry:"]`+
|
assert.True(t, shown(ctx, row+`//span[text()="Archive expiry:"]`+
|
||||||
`/following-sibling::span[text()="30 days"]`),
|
`/following-sibling::span[text()="30 days"]`),
|
||||||
"a database target added with 720h is not listed as 30 days")
|
"a database target added with 720h is not listed as 30 days")
|
||||||
assert.True(t, shown(ctx, row+`//span[text()="Archive Rotation:"]`+
|
assert.True(t, shown(ctx, row+`//span[text()="Archive rotation:"]`+
|
||||||
`/following-sibling::span[text()="daily"]`),
|
`/following-sibling::span[text()="daily"]`),
|
||||||
"a database target added with daily is not listed as daily")
|
"a database target added with daily is not listed as daily")
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ func TestEventResubmit_SignedOutRequestsNeverReachTheRateLimit(
|
|||||||
env.requireNotice(
|
env.requireNotice(
|
||||||
t, env.post(path, csrfForm(token), cookies),
|
t, env.post(path, csrfForm(token), cookies),
|
||||||
logsPath, "resubmit-no-targets",
|
logsPath, "resubmit-no-targets",
|
||||||
"this source has no active targets", cookies,
|
"this webhook has no active targets", cookies,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ func TestEventResubmit_RefusedWithoutAValidCSRFToken(t *testing.T) {
|
|||||||
env.requireNotice(
|
env.requireNotice(
|
||||||
t, env.post(path, csrfForm(token), cookies),
|
t, env.post(path, csrfForm(token), cookies),
|
||||||
logsPath, "resubmit-no-targets",
|
logsPath, "resubmit-no-targets",
|
||||||
"this source has no active targets", cookies,
|
"this webhook has no active targets", cookies,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@ func TestEventResubmit_AnotherWebhooksEvent404s(t *testing.T) {
|
|||||||
csrfForm(token), cookies,
|
csrfForm(token), cookies,
|
||||||
),
|
),
|
||||||
intrudersLogs, "resubmit-no-targets",
|
intrudersLogs, "resubmit-no-targets",
|
||||||
"this source has no active targets", cookies,
|
"this webhook has no active targets", cookies,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -914,6 +914,26 @@ func TestPagesLogout_SaysSignedOut(t *testing.T) {
|
|||||||
env.requireNotice(t, w, "/pages/login", "signed-out", "Signed out.", nil)
|
env.requireNotice(t, w, "/pages/login", "signed-out", "Signed out.", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestSignInAndSignOutWording pins one wording for both: the sign-in
|
||||||
|
// page's button reads "Sign in" and the navbar's buttons "Sign out", as
|
||||||
|
// the sign-in page's heading, the error page's link and the notice
|
||||||
|
// after signing out do.
|
||||||
|
func TestSignInAndSignOutWording(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newTestEnv(t)
|
||||||
|
|
||||||
|
assert.Contains(
|
||||||
|
t, env.get("/pages/login", nil).Body.String(), ">Sign in</button>",
|
||||||
|
)
|
||||||
|
|
||||||
|
userID, _ := env.seedUser(t, "reader", "somepassword")
|
||||||
|
page := env.get("/hooks", env.authCookies(t, userID, "reader")).Body.String()
|
||||||
|
|
||||||
|
assert.Equal(t, 2, strings.Count(page, ">Sign out</button>"),
|
||||||
|
"the desktop and the mobile navbar each say Sign out")
|
||||||
|
}
|
||||||
|
|
||||||
// --- /user/{username} group ---
|
// --- /user/{username} group ---
|
||||||
|
|
||||||
// TestPasswordChange_OversizeBody_RejectedAndPasswordUnchanged
|
// TestPasswordChange_OversizeBody_RejectedAndPasswordUnchanged
|
||||||
@@ -1363,7 +1383,7 @@ func TestHook_ResubmitFromEventLog(t *testing.T) {
|
|||||||
)
|
)
|
||||||
env.requireNotice(
|
env.requireNotice(
|
||||||
t, w, logsPath, "resubmit-no-targets",
|
t, w, logsPath, "resubmit-no-targets",
|
||||||
"this source has no active targets", cookies,
|
"this webhook has no active targets", cookies,
|
||||||
)
|
)
|
||||||
|
|
||||||
webhookDB, err := env.dbMgr.GetDB(wh.ID)
|
webhookDB, err := env.dbMgr.GetDB(wh.ID)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{{template "base" .}}
|
{{template "base" .}}
|
||||||
|
|
||||||
{{define "title"}}Login - Webhooker{{end}}
|
{{define "title"}}Sign in - Webhooker{{end}}
|
||||||
|
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<div class="min-h-screen flex items-center justify-center py-12 px-4">
|
<div class="min-h-screen flex items-center justify-center py-12 px-4">
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn-primary w-full py-3">Sign In</button>
|
<button type="submit" class="btn-primary w-full py-3">Sign in</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
{{if .CSRFToken}}
|
{{if .CSRFToken}}
|
||||||
<form method="POST" action="/pages/logout" class="inline">
|
<form method="POST" action="/pages/logout" class="inline">
|
||||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||||
<button type="submit" class="btn-secondary">Logout</button>
|
<button type="submit" class="btn-secondary">Sign out</button>
|
||||||
</form>
|
</form>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
{{if .CSRFToken}}
|
{{if .CSRFToken}}
|
||||||
<form method="POST" action="/pages/logout">
|
<form method="POST" action="/pages/logout">
|
||||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||||
<button type="submit" class="btn-secondary w-full">Logout</button>
|
<button type="submit" class="btn-secondary w-full">Sign out</button>
|
||||||
</form>
|
</form>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -159,7 +159,7 @@
|
|||||||
<select x-ref="type" aria-label="Target type" class="input text-sm p-2 flex-1">
|
<select x-ref="type" aria-label="Target type" class="input text-sm p-2 flex-1">
|
||||||
<option value="http">HTTP</option>
|
<option value="http">HTTP</option>
|
||||||
<option value="slack">Slack</option>
|
<option value="slack">Slack</option>
|
||||||
<option value="database">Database</option>
|
<option value="database">Archive</option>
|
||||||
<option value="log">Log</option>
|
<option value="log">Log</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="button" @click="next" class="btn-primary text-sm">Next</button>
|
<button type="button" @click="next" class="btn-primary text-sm">Next</button>
|
||||||
@@ -182,10 +182,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<label class="text-sm text-gray-700">Max retries:</label>
|
<label class="text-sm text-gray-700">Delivery attempts:</label>
|
||||||
<input type="number" name="max_retries" :value="maxRetries" placeholder="0" min="0" max="20" class="input text-sm w-24">
|
<input type="number" name="max_retries" :value="maxRetries" placeholder="0" min="0" max="20" class="input text-sm w-24">
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-500 mt-1">This is the total number of delivery attempts, not retries on top of the first: a value of 3 makes three attempts in all. 0 means a single attempt with no retries and no circuit breaker.</p>
|
<p class="text-xs text-gray-500 mt-1">How many times each delivery is attempted in all, the first attempt included. 0 means a single attempt with no retries and no circuit breaker.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -198,10 +198,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<label class="text-sm text-gray-700">Max retries:</label>
|
<label class="text-sm text-gray-700">Delivery attempts:</label>
|
||||||
<input type="number" name="max_retries" :value="maxRetries" placeholder="0" min="0" max="20" class="input text-sm w-24">
|
<input type="number" name="max_retries" :value="maxRetries" placeholder="0" min="0" max="20" class="input text-sm w-24">
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-500 mt-1">This is the total number of delivery attempts, not retries on top of the first: a value of 3 makes three attempts in all. 0 means a single attempt with no retries and no circuit breaker.</p>
|
<p class="text-xs text-gray-500 mt-1">How many times each delivery is attempted in all, the first attempt included. 0 means a single attempt with no retries and no circuit breaker.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -251,7 +251,7 @@
|
|||||||
<div class="flex flex-wrap items-center justify-between gap-2 mb-1">
|
<div class="flex flex-wrap items-center justify-between gap-2 mb-1">
|
||||||
<span class="text-sm font-medium text-gray-900">{{.Name}}</span>
|
<span class="text-sm font-medium text-gray-900">{{.Name}}</span>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<span class="badge-info">{{.Type}}</span>
|
<span class="badge-info">{{if eq .Type "database"}}archive{{else}}{{.Type}}{{end}}</span>
|
||||||
{{if .Active}}
|
{{if .Active}}
|
||||||
<span class="badge-success">Active</span>
|
<span class="badge-success">Active</span>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|||||||
@@ -46,9 +46,9 @@
|
|||||||
<input type="checkbox" name="archive" value="on"{{if .Form.Archive}} checked{{end}} autocomplete="off" @change="toggle" class="h-4 w-4">
|
<input type="checkbox" name="archive" value="on"{{if .Form.Archive}} checked{{end}} autocomplete="off" @change="toggle" class="h-4 w-4">
|
||||||
Archive
|
Archive
|
||||||
</label>
|
</label>
|
||||||
<p class="text-xs text-gray-500 mt-1">When checked, the webhook is created with a database target that keeps a copy of every event.</p>
|
<p class="text-xs text-gray-500 mt-1">When checked, the webhook is created with an archive target that keeps a copy of every event.</p>
|
||||||
<div x-show="open" x-cloak class="mt-3">
|
<div x-show="open" x-cloak class="mt-3">
|
||||||
<label for="archive_expiry" class="label">Archive pruning</label>
|
<label for="archive_expiry" class="label">Archive expiry</label>
|
||||||
<select id="archive_expiry" name="archive_expiry" class="input">
|
<select id="archive_expiry" name="archive_expiry" class="input">
|
||||||
{{range .ArchiveExpiryChoices}}
|
{{range .ArchiveExpiryChoices}}
|
||||||
<option value="{{.Value}}"{{if .Selected}} selected{{end}}>{{.Label}}</option>
|
<option value="{{.Value}}"{{if .Selected}} selected{{end}}>{{.Label}}</option>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<a href="/hook/{{.Webhook.ID}}" class="btn-small">← Back to {{.Webhook.Name}}</a>
|
<a href="/hook/{{.Webhook.ID}}" class="btn-small">← Back to {{.Webhook.Name}}</a>
|
||||||
<h1 class="text-2xl font-medium text-gray-900 mt-2">Edit Target</h1>
|
<h1 class="text-2xl font-medium text-gray-900 mt-2">Edit Target</h1>
|
||||||
<p class="text-sm text-gray-500 mt-1">Type: {{.Target.Type}}. A target's type cannot be changed; create a new target to deliver a different way.</p>
|
<p class="text-sm text-gray-500 mt-1">Type: {{if eq .Target.Type "database"}}archive{{else}}{{.Target.Type}}{{end}}. A target's type cannot be changed; create a new target to deliver a different way.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card p-6">
|
<div class="card p-6">
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
|
|
||||||
{{if eq .Target.Type "database"}}
|
{{if eq .Target.Type "database"}}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="expiry" class="label">Archive Expiry</label>
|
<label for="expiry" class="label">Archive expiry</label>
|
||||||
<select id="expiry" name="expiry" class="input">
|
<select id="expiry" name="expiry" class="input">
|
||||||
{{range .ArchiveExpiryChoices}}
|
{{range .ArchiveExpiryChoices}}
|
||||||
<option value="{{.Value}}"{{if .Selected}} selected{{end}}>{{.Label}}</option>
|
<option value="{{.Value}}"{{if .Selected}} selected{{end}}>{{.Label}}</option>
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="rotation" class="label">Archive Rotation</label>
|
<label for="rotation" class="label">Archive rotation</label>
|
||||||
<select id="rotation" name="rotation" class="input">
|
<select id="rotation" name="rotation" class="input">
|
||||||
{{range .ArchiveRotationChoices}}
|
{{range .ArchiveRotationChoices}}
|
||||||
<option value="{{.Value}}"{{if .Selected}} selected{{end}}>{{.Label}}</option>
|
<option value="{{.Value}}"{{if .Selected}} selected{{end}}>{{.Label}}</option>
|
||||||
@@ -81,9 +81,9 @@
|
|||||||
|
|
||||||
{{if or (eq .Target.Type "http") (eq .Target.Type "slack")}}
|
{{if or (eq .Target.Type "http") (eq .Target.Type "slack")}}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="max_retries" class="label">Max retries</label>
|
<label for="max_retries" class="label">Delivery attempts</label>
|
||||||
<input type="number" id="max_retries" name="max_retries" value="{{.TargetForm.MaxRetries}}" min="0" max="20" class="input">
|
<input type="number" id="max_retries" name="max_retries" value="{{.TargetForm.MaxRetries}}" min="0" max="20" class="input">
|
||||||
<p class="text-xs text-gray-500 mt-1">This is the total number of delivery attempts, not retries on top of the first: a value of 3 makes three attempts in all. 0 means a single attempt with no retries and no circuit breaker.</p>
|
<p class="text-xs text-gray-500 mt-1">How many times each delivery is attempted in all, the first attempt included. 0 means a single attempt with no retries and no circuit breaker.</p>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user