check / check (push) Waiting to run
Each database target now has its own archive file, archive-WEBHOOKNAME-TARGETNAME-TARGETID.db, instead of one archive-WEBHOOKID.db per webhook. delivery.ArchiveFileName builds the name: each name is lowercased, keeps ASCII letters and digits, turns every other run of characters into one dash, and is cut to 40 characters. A change of webhook or target name renames its archive files under the archive writer's lock, before the new name is saved, and back again if the save fails. A rename never replaces a file: if one already has the new name, the edit is refused. Deleting a target evicts only that target's writer. Archive files are never deleted, and nothing looks for files under the old name. Model: opus-5-5
694 lines
17 KiB
Go
694 lines
17 KiB
Go
package handlers_test
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/go-chi/chi"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/gorm/clause"
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
"sneak.berlin/go/webhooker/internal/handlers"
|
|
"sneak.berlin/go/webhooker/internal/session"
|
|
)
|
|
|
|
const (
|
|
// sourceTestUserID is the session user id used by the webhook
|
|
// management tests.
|
|
sourceTestUserID = "source-test-user"
|
|
// sourceIDParam is the chi URL parameter naming a webhook.
|
|
sourceIDParam = "sourceID"
|
|
)
|
|
|
|
// formRequest builds an urlencoded POST to path carrying the given
|
|
// cookies, plus any chi URL parameters the handler reads.
|
|
func formRequest(
|
|
path string,
|
|
cookies []*http.Cookie,
|
|
form url.Values,
|
|
urlParams map[string]string,
|
|
) *http.Request {
|
|
req := httptest.NewRequestWithContext(
|
|
context.Background(),
|
|
http.MethodPost,
|
|
path,
|
|
strings.NewReader(form.Encode()),
|
|
)
|
|
req.Header.Set(
|
|
"Content-Type", "application/x-www-form-urlencoded",
|
|
)
|
|
|
|
for _, c := range cookies {
|
|
req.AddCookie(c)
|
|
}
|
|
|
|
rctx := chi.NewRouteContext()
|
|
for k, v := range urlParams {
|
|
rctx.URLParams.Add(k, v)
|
|
}
|
|
|
|
return req.WithContext(
|
|
context.WithValue(req.Context(), chi.RouteCtxKey, rctx),
|
|
)
|
|
}
|
|
|
|
// getRequest builds a GET to path carrying the given cookies, plus any
|
|
// chi URL parameters the handler reads.
|
|
func getRequest(
|
|
t *testing.T,
|
|
path string,
|
|
cookies []*http.Cookie,
|
|
urlParams map[string]string,
|
|
) *http.Request {
|
|
t.Helper()
|
|
|
|
req := httptest.NewRequestWithContext(
|
|
context.Background(), http.MethodGet, path, nil,
|
|
)
|
|
|
|
for _, c := range cookies {
|
|
req.AddCookie(c)
|
|
}
|
|
|
|
rctx := chi.NewRouteContext()
|
|
for k, v := range urlParams {
|
|
rctx.URLParams.Add(k, v)
|
|
}
|
|
|
|
return req.WithContext(
|
|
context.WithValue(req.Context(), chi.RouteCtxKey, rctx),
|
|
)
|
|
}
|
|
|
|
// submitCreate posts the webhook creation form with the given
|
|
// retention_days value (omitted entirely when retention is nil) and
|
|
// returns the recorder.
|
|
func submitCreate(
|
|
t *testing.T,
|
|
h *handlers.Handlers,
|
|
cookies []*http.Cookie,
|
|
name string,
|
|
retention *string,
|
|
) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
|
|
form := url.Values{}
|
|
form.Set("name", name)
|
|
|
|
if retention != nil {
|
|
form.Set("retention_days", *retention)
|
|
}
|
|
|
|
req := formRequest("/hooks/new", cookies, form, nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.HandleSourceCreateSubmit().ServeHTTP(w, req)
|
|
|
|
return w
|
|
}
|
|
|
|
// onlyWebhook loads the single webhook belonging to the test user.
|
|
func onlyWebhook(
|
|
t *testing.T,
|
|
db *database.Database,
|
|
) database.Webhook {
|
|
t.Helper()
|
|
|
|
var webhooks []database.Webhook
|
|
|
|
require.NoError(
|
|
t,
|
|
db.DB().Where("user_id = ?", sourceTestUserID).
|
|
Find(&webhooks).Error,
|
|
)
|
|
require.Len(t, webhooks, 1)
|
|
|
|
return webhooks[0]
|
|
}
|
|
|
|
// seedWebhookWithRetention inserts a webhook owned by the test user
|
|
// with an exact stored retention value, bypassing Webhook.BeforeSave
|
|
// via a column-level update so that legacy rows can be planted too.
|
|
func seedWebhookWithRetention(
|
|
t *testing.T,
|
|
db *database.Database,
|
|
retentionDays int,
|
|
) database.Webhook {
|
|
t.Helper()
|
|
|
|
wh := &database.Webhook{
|
|
UserID: sourceTestUserID,
|
|
Name: "seeded",
|
|
RetentionDays: retentionDays,
|
|
}
|
|
require.NoError(
|
|
t,
|
|
db.DB().Omit(clause.Associations).Create(wh).Error,
|
|
)
|
|
require.NoError(
|
|
t,
|
|
db.DB().Model(wh).
|
|
Update("retention_days", retentionDays).Error,
|
|
)
|
|
|
|
wh.RetentionDays = retentionDays
|
|
|
|
return *wh
|
|
}
|
|
|
|
// storedRetentionDays reads the retention_days column for a webhook.
|
|
func storedRetentionDays(
|
|
t *testing.T,
|
|
db *database.Database,
|
|
id string,
|
|
) int {
|
|
t.Helper()
|
|
|
|
var got int
|
|
|
|
require.NoError(
|
|
t,
|
|
db.DB().Model(&database.Webhook{}).
|
|
Where("id = ?", id).
|
|
Pluck("retention_days", &got).Error,
|
|
)
|
|
|
|
return got
|
|
}
|
|
|
|
// sourceTestEnv bundles the handler, session, and database a webhook
|
|
// management test drives.
|
|
type sourceTestEnv struct {
|
|
handlers *handlers.Handlers
|
|
db *database.Database
|
|
archives *recordingArchives
|
|
cookies []*http.Cookie
|
|
}
|
|
|
|
func setupSourceTest(t *testing.T) *sourceTestEnv {
|
|
t.Helper()
|
|
|
|
var h *handlers.Handlers
|
|
|
|
var sess *session.Session
|
|
|
|
var db *database.Database
|
|
|
|
var archives *recordingArchives
|
|
|
|
app := newTestApp(t, &h, &sess, &db, &archives)
|
|
app.RequireStart()
|
|
|
|
t.Cleanup(app.RequireStop)
|
|
|
|
return &sourceTestEnv{
|
|
handlers: h,
|
|
db: db,
|
|
archives: archives,
|
|
cookies: authenticatedCookies(
|
|
t, sess, sourceTestUserID, "sourceuser",
|
|
),
|
|
}
|
|
}
|
|
|
|
// TestHandleSourceCreateSubmit_ZeroRetentionPersistsForever is the core
|
|
// regression test for the bug: the create form's 0 must reach the
|
|
// database as the retain-forever sentinel rather than being replaced by
|
|
// the column's default of 30.
|
|
func TestHandleSourceCreateSubmit_ZeroRetentionPersistsForever(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
env := setupSourceTest(t)
|
|
zero := "0"
|
|
|
|
w := submitCreate(t, env.handlers, env.cookies, "forever", &zero)
|
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
|
|
|
wh := onlyWebhook(t, env.db)
|
|
assert.Equal(
|
|
t,
|
|
database.RetentionForeverDays,
|
|
storedRetentionDays(t, env.db, wh.ID),
|
|
)
|
|
assert.True(t, wh.RetainsForever())
|
|
}
|
|
|
|
func TestHandleSourceCreateSubmit_OmittedRetentionUsesDefault(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
env := setupSourceTest(t)
|
|
|
|
w := submitCreate(t, env.handlers, env.cookies, "defaulted", nil)
|
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
|
|
|
wh := onlyWebhook(t, env.db)
|
|
assert.Equal(
|
|
t,
|
|
database.DefaultRetentionDays,
|
|
storedRetentionDays(t, env.db, wh.ID),
|
|
)
|
|
}
|
|
|
|
// TestHandleSourceCreate_PrefillsDefaultFromConstant keeps the create
|
|
// form's pre-filled retention from becoming a third hardcoded copy of
|
|
// the 30-day policy.
|
|
func TestHandleSourceCreate_PrefillsDefaultFromConstant(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := setupSourceTest(t)
|
|
|
|
w := httptest.NewRecorder()
|
|
env.handlers.HandleSourceCreate().ServeHTTP(
|
|
w, getRequest(t, "/hooks/new", env.cookies, nil),
|
|
)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
|
|
body := w.Body.String()
|
|
|
|
assert.Contains(
|
|
t, body,
|
|
`value="`+strconv.Itoa(database.DefaultRetentionDays)+`"`,
|
|
)
|
|
assert.NotContains(
|
|
t, body, `max="365"`,
|
|
"a max below the sentinel would block retain-forever",
|
|
)
|
|
assert.Contains(t, body, `min="0"`)
|
|
}
|
|
|
|
func TestHandleSourceCreateSubmit_InvalidRetentionIsRejected(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
for _, raw := range []string{"abc", "-1", "3.5"} {
|
|
t.Run(raw, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := setupSourceTest(t)
|
|
|
|
w := submitCreate(
|
|
t, env.handlers, env.cookies, "bad", &raw,
|
|
)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
assert.Contains(
|
|
t, w.Body.String(), "Retention must be",
|
|
)
|
|
|
|
var count int64
|
|
|
|
require.NoError(
|
|
t,
|
|
env.db.DB().Model(&database.Webhook{}).
|
|
Where("user_id = ?", sourceTestUserID).
|
|
Count(&count).Error,
|
|
)
|
|
assert.Zero(
|
|
t, count,
|
|
"no webhook may be created from a rejected form",
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestHandleSourceCreateSubmit_OverflowingRetentionIsRejected covers
|
|
// the data-loss path directly: a finite retention above the largest one
|
|
// the reaper's cutoff arithmetic can represent must never reach the
|
|
// database, because the sweep would compute a future cutoff and delete
|
|
// every event the webhook has.
|
|
func TestHandleSourceCreateSubmit_OverflowingRetentionIsRejected(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
tooBig := strconv.Itoa(database.MaxFiniteRetentionDays + 1)
|
|
|
|
env := setupSourceTest(t)
|
|
|
|
w := submitCreate(t, env.handlers, env.cookies, "huge", &tooBig)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
assert.Contains(
|
|
t, w.Body.String(),
|
|
strconv.Itoa(database.MaxFiniteRetentionDays),
|
|
"the form tells the user the actual ceiling",
|
|
)
|
|
|
|
var count int64
|
|
|
|
require.NoError(
|
|
t,
|
|
env.db.DB().Model(&database.Webhook{}).
|
|
Where("user_id = ?", sourceTestUserID).
|
|
Count(&count).Error,
|
|
)
|
|
assert.Zero(
|
|
t, count,
|
|
"no webhook may be created from a rejected form",
|
|
)
|
|
}
|
|
|
|
// TestHandleSourceCreateSubmit_SentinelIsAcceptedAsForever guards the
|
|
// boundary between "too large to represent" and "retain forever": the
|
|
// sentinel is above MaxFiniteRetentionDays, but it is the value the
|
|
// edit form pre-fills, so it must be accepted rather than rejected as
|
|
// out of range.
|
|
func TestHandleSourceCreateSubmit_SentinelIsAcceptedAsForever(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
env := setupSourceTest(t)
|
|
sentinel := strconv.Itoa(database.RetentionForeverDays)
|
|
|
|
w := submitCreate(t, env.handlers, env.cookies, "forever", &sentinel)
|
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
|
|
|
wh := onlyWebhook(t, env.db)
|
|
assert.Equal(
|
|
t,
|
|
database.RetentionForeverDays,
|
|
storedRetentionDays(t, env.db, wh.ID),
|
|
)
|
|
}
|
|
|
|
// TestHandleSourceCreateSubmit_RejectedFormKeepsUserInput checks that a
|
|
// validation failure hands the user's typing back, matching what the
|
|
// edit form already does. Losing a long description to a mistyped
|
|
// retention value is the kind of thing that makes people give up on a
|
|
// form.
|
|
func TestHandleSourceCreateSubmit_RejectedFormKeepsUserInput(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
env := setupSourceTest(t)
|
|
|
|
const (
|
|
name = "kept-name"
|
|
description = "a description worth not losing"
|
|
)
|
|
|
|
form := url.Values{}
|
|
form.Set("name", name)
|
|
form.Set("description", description)
|
|
form.Set("retention_days", "nonsense")
|
|
|
|
req := formRequest("/hooks/new", env.cookies, form, nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
env.handlers.HandleSourceCreateSubmit().ServeHTTP(w, req)
|
|
|
|
require.Equal(t, http.StatusBadRequest, w.Code)
|
|
|
|
body := w.Body.String()
|
|
|
|
assert.Contains(t, body, `value="`+name+`"`)
|
|
assert.Contains(t, body, description)
|
|
}
|
|
|
|
// submitEdit posts the webhook edit form for the given webhook.
|
|
func submitEdit(
|
|
t *testing.T,
|
|
env *sourceTestEnv,
|
|
wh database.Webhook,
|
|
retention string,
|
|
) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
|
|
form := url.Values{}
|
|
form.Set("name", wh.Name)
|
|
form.Set("description", wh.Description)
|
|
form.Set("retention_days", retention)
|
|
|
|
req := formRequest(
|
|
"/hook/"+wh.ID+"/edit",
|
|
env.cookies,
|
|
form,
|
|
map[string]string{sourceIDParam: wh.ID},
|
|
)
|
|
w := httptest.NewRecorder()
|
|
|
|
env.handlers.HandleSourceEditSubmit().ServeHTTP(w, req)
|
|
|
|
return w
|
|
}
|
|
|
|
func TestHandleSourceEditSubmit_ZeroRetentionPersistsForever(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
env := setupSourceTest(t)
|
|
wh := seedWebhookWithRetention(
|
|
t, env.db, database.DefaultRetentionDays,
|
|
)
|
|
|
|
w := submitEdit(t, env, wh, "0")
|
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
|
|
|
assert.Equal(
|
|
t,
|
|
database.RetentionForeverDays,
|
|
storedRetentionDays(t, env.db, wh.ID),
|
|
)
|
|
}
|
|
|
|
func TestHandleSourceEditSubmit_InvalidRetentionIsRejected(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
env := setupSourceTest(t)
|
|
wh := seedWebhookWithRetention(
|
|
t, env.db, database.DefaultRetentionDays,
|
|
)
|
|
|
|
w := submitEdit(t, env, wh, "not-a-number")
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
assert.Contains(t, w.Body.String(), "Retention must be")
|
|
assert.Equal(
|
|
t,
|
|
database.DefaultRetentionDays,
|
|
storedRetentionDays(t, env.db, wh.ID),
|
|
"a rejected form must not change the stored retention",
|
|
)
|
|
}
|
|
|
|
func TestHandleSourceEditSubmit_EmptyRetentionLeavesValueUnchanged(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
env := setupSourceTest(t)
|
|
wh := seedWebhookWithRetention(t, env.db, 7)
|
|
|
|
w := submitEdit(t, env, wh, "")
|
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
|
|
|
assert.Equal(t, 7, storedRetentionDays(t, env.db, wh.ID))
|
|
}
|
|
|
|
// renamedWebhookName is the name the rename tests give a webhook.
|
|
const renamedWebhookName = "Renamed"
|
|
|
|
// TestHandleSourceEditSubmit_RenamesArchives proves that a save
|
|
// that keeps the webhook's name renames nothing, and that renaming a
|
|
// webhook renames the archive of each of its database targets and
|
|
// asks nothing of its other targets.
|
|
func TestHandleSourceEditSubmit_RenamesArchives(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := setupSourceTest(t)
|
|
wh := seedWebhookWithRetention(t, env.db, 7)
|
|
first := seedTarget(t, env.db, wh.ID, database.TargetTypeDatabase)
|
|
second := seedTarget(t, env.db, wh.ID, database.TargetTypeDatabase)
|
|
seedTarget(t, env.db, wh.ID, database.TargetTypeLog)
|
|
|
|
w := submitEdit(t, env, wh, "")
|
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
|
assert.Empty(t, env.archives.Renames())
|
|
|
|
wh.Name = renamedWebhookName
|
|
|
|
w = submitEdit(t, env, wh, "")
|
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
|
|
|
assert.ElementsMatch(
|
|
t,
|
|
[]archiveRename{
|
|
{first.ID, renamedWebhookName, first.Name},
|
|
{second.ID, renamedWebhookName, second.Name},
|
|
},
|
|
env.archives.Renames(),
|
|
)
|
|
}
|
|
|
|
// TestHandleSourceEditSubmit_FailedRenameKeepsTheName proves that a
|
|
// webhook whose archive cannot be renamed keeps its stored name, so
|
|
// the name on disk and the name in the UI do not part, and that the
|
|
// handler puts back what it may already have moved.
|
|
func TestHandleSourceEditSubmit_FailedRenameKeepsTheName(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
env := setupSourceTest(t)
|
|
wh := seedWebhookWithRetention(t, env.db, 7)
|
|
tgt := seedTarget(t, env.db, wh.ID, database.TargetTypeDatabase)
|
|
|
|
env.archives.FailRenames(errInjectedRename)
|
|
|
|
oldName := wh.Name
|
|
wh.Name = renamedWebhookName
|
|
|
|
w := submitEdit(t, env, wh, "")
|
|
require.Equal(t, http.StatusInternalServerError, w.Code)
|
|
|
|
var stored database.Webhook
|
|
|
|
require.NoError(
|
|
t, env.db.DB().First(&stored, "id = ?", wh.ID).Error,
|
|
)
|
|
assert.Equal(t, oldName, stored.Name)
|
|
|
|
assert.Equal(
|
|
t,
|
|
[]archiveRename{
|
|
{tgt.ID, renamedWebhookName, tgt.Name},
|
|
{tgt.ID, oldName, tgt.Name},
|
|
},
|
|
env.archives.Renames(),
|
|
)
|
|
}
|
|
|
|
// TestHandleSourceEditSubmit_ArchiveNameTaken proves that when a file
|
|
// already has an archive's new name, the edit is refused with an
|
|
// error naming that file, and the webhook keeps its stored name.
|
|
func TestHandleSourceEditSubmit_ArchiveNameTaken(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := setupSourceTest(t)
|
|
wh := seedWebhookWithRetention(t, env.db, 7)
|
|
seedTarget(t, env.db, wh.ID, database.TargetTypeDatabase)
|
|
|
|
env.archives.FailRenames(errNameTaken)
|
|
|
|
oldName := wh.Name
|
|
wh.Name = renamedWebhookName
|
|
|
|
w := submitEdit(t, env, wh, "")
|
|
require.Equal(t, http.StatusConflict, w.Code)
|
|
assert.Contains(t, w.Body.String(), "archive-taken.db")
|
|
|
|
var stored database.Webhook
|
|
|
|
require.NoError(
|
|
t, env.db.DB().First(&stored, "id = ?", wh.ID).Error,
|
|
)
|
|
assert.Equal(t, oldName, stored.Name)
|
|
}
|
|
|
|
// TestSourceEditForm_ForeverWebhookRoundTrips walks the exact path that
|
|
// the removed max="365" cap used to break: render the edit form for a
|
|
// retain-forever webhook, confirm the pre-filled sentinel is not capped
|
|
// by browser validation, then submit that pre-filled value straight
|
|
// back and confirm the retention policy survives untouched.
|
|
func TestSourceEditForm_ForeverWebhookRoundTrips(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := setupSourceTest(t)
|
|
wh := seedWebhookWithRetention(
|
|
t, env.db, database.RetentionForeverDays,
|
|
)
|
|
|
|
req := getRequest(
|
|
t, "/hook/"+wh.ID+"/edit", env.cookies,
|
|
map[string]string{sourceIDParam: wh.ID},
|
|
)
|
|
w := httptest.NewRecorder()
|
|
env.handlers.HandleSourceEdit().ServeHTTP(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
|
|
sentinel := strconv.Itoa(database.RetentionForeverDays)
|
|
body := w.Body.String()
|
|
|
|
assert.Contains(
|
|
t, body, `value="`+sentinel+`"`,
|
|
"the edit form pre-fills the stored retention",
|
|
)
|
|
assert.NotContains(
|
|
t, body, `max="365"`,
|
|
"a max below the sentinel would block saving any edit",
|
|
)
|
|
// "Currently forever." is the rendered RetentionLabel, not the
|
|
// static hint below the input, which says "Enter 0 to retain events
|
|
// forever." A bare Contains of "forever" would pass for any
|
|
// webhook and would assert nothing about this one.
|
|
assert.Contains(
|
|
t, body, "Currently forever.",
|
|
"the form reports this webhook's policy as forever",
|
|
)
|
|
|
|
// Submit the pre-filled value back, exactly as a browser would.
|
|
post := submitEdit(t, env, wh, sentinel)
|
|
require.Equal(t, http.StatusSeeOther, post.Code)
|
|
|
|
assert.Equal(
|
|
t,
|
|
database.RetentionForeverDays,
|
|
storedRetentionDays(t, env.db, wh.ID),
|
|
)
|
|
}
|
|
|
|
// TestSourceListAndDetail_ShowForeverNotTheSentinelNumber checks that
|
|
// the retain-forever value is never rendered to the user as a raw day
|
|
// count on either read-only view.
|
|
func TestSourceListAndDetail_ShowForeverNotTheSentinelNumber(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
env := setupSourceTest(t)
|
|
wh := seedWebhookWithRetention(
|
|
t, env.db, database.RetentionForeverDays,
|
|
)
|
|
sentinel := strconv.Itoa(database.RetentionForeverDays)
|
|
|
|
listW := httptest.NewRecorder()
|
|
env.handlers.HandleSourceList().ServeHTTP(
|
|
listW, getRequest(t, "/hooks", env.cookies, nil),
|
|
)
|
|
|
|
require.Equal(t, http.StatusOK, listW.Code)
|
|
assert.Contains(t, listW.Body.String(), "Retention: forever")
|
|
assert.NotContains(t, listW.Body.String(), sentinel)
|
|
|
|
detailW := httptest.NewRecorder()
|
|
env.handlers.HandleSourceDetail().ServeHTTP(
|
|
detailW,
|
|
getRequest(
|
|
t, "/hook/"+wh.ID, env.cookies,
|
|
map[string]string{sourceIDParam: wh.ID},
|
|
),
|
|
)
|
|
|
|
require.Equal(t, http.StatusOK, detailW.Code)
|
|
assert.Contains(t, detailW.Body.String(), "Retention: forever")
|
|
assert.NotContains(t, detailW.Body.String(), sentinel)
|
|
}
|