check / check (push) Successful in 3m21s
The UI gave one thing several names. The `database` type is now Archive in the type list, on its badge and on the edit page, and its settings read "Archive expiry" and "Archive rotation" everywhere. The retry field is "Delivery attempts", with help text, errors and the target list saying it counts every attempt; a stored 0 shows as one attempt. The target list uses one capitalisation. The navbar says "Sign out", the sign-in page "Sign in", and the resubmit notice "webhook" instead of "source". The README follows. Stored values, their meaning, routes and form field names are unchanged. Model: opus-5-5
230 lines
6.8 KiB
Go
230 lines
6.8 KiB
Go
package handlers_test
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
"sneak.berlin/go/webhooker/internal/delivery"
|
|
)
|
|
|
|
// rotationNone is the archive rotation that keeps one file.
|
|
const rotationNone = "none"
|
|
|
|
// rotationShown returns the archive rotations the webhook page's
|
|
// target list shows.
|
|
func rotationShown(
|
|
t *testing.T, env *sourceTestEnv, webhookID string,
|
|
) []string {
|
|
t.Helper()
|
|
|
|
return matched(
|
|
`Archive rotation:</span>\s*<span>([^<]*)</span>`,
|
|
renderedPage(t, env, webhookID),
|
|
)
|
|
}
|
|
|
|
// TestArchiveRotationChoices adds a database target with each archive
|
|
// rotation the forms offer, and checks that it is stored as chosen,
|
|
// shown in the target list, and that the target edit form starts on
|
|
// it. It then edits the target to hourly, keeping its expiry.
|
|
func TestArchiveRotationChoices(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := setupSourceTest(t)
|
|
|
|
for _, rotation := range []string{rotationNone, "monthly", "daily", "hourly"} {
|
|
t.Run(rotation, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
webhook := seedWebhookWithRetention(t, env.db, 30)
|
|
|
|
form := url.Values{}
|
|
form.Set("name", "archive")
|
|
form.Set("type", string(database.TargetTypeDatabase))
|
|
form.Set("expiry", "720h")
|
|
form.Set("rotation", rotation)
|
|
|
|
w := serveTarget(
|
|
env, http.MethodPost, "/hook/"+webhook.ID+"/targets", form,
|
|
)
|
|
require.Equal(t, http.StatusSeeOther, w.Code, w.Body.String())
|
|
|
|
targets := targetsForWebhook(t, env.db, webhook.ID)
|
|
require.Len(t, targets, 1)
|
|
assert.JSONEq(t,
|
|
`{"expiry":"720h","rotation":"`+rotation+`"}`,
|
|
targets[0].Config,
|
|
)
|
|
|
|
assert.Equal(t,
|
|
[]string{rotation}, rotationShown(t, env, webhook.ID))
|
|
|
|
page := targetEditPage(t, env, webhook.ID, targets[0].ID)
|
|
assert.Equal(t, []string{rotation}, selectedIn(page, "rotation"))
|
|
|
|
form = url.Values{}
|
|
form.Set("name", "archive")
|
|
form.Set("expiry", "720h")
|
|
form.Set("rotation", "hourly")
|
|
|
|
w = submitTargetEdit(env, webhook.ID, targets[0].ID, form)
|
|
require.Equal(t, http.StatusSeeOther, w.Code, w.Body.String())
|
|
assert.JSONEq(t,
|
|
`{"expiry":"720h","rotation":"hourly"}`,
|
|
storedTarget(t, env, targets[0].ID).Config,
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestArchiveRotationEditStartsOnNone checks the edit form of a
|
|
// database target with no rotation stored starts on none.
|
|
func TestArchiveRotationEditStartsOnNone(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := setupSourceTest(t)
|
|
webhook := seedWebhookWithRetention(t, env.db, 30)
|
|
target := seedConfiguredTarget(
|
|
t, env.db, webhook.ID, database.TargetTypeDatabase, "",
|
|
)
|
|
|
|
page := targetEditPage(t, env, webhook.ID, target.ID)
|
|
assert.Equal(t, []string{rotationNone}, selectedIn(page, "rotation"))
|
|
assert.Equal(t, []string{rotationNone}, rotationShown(t, env, webhook.ID))
|
|
}
|
|
|
|
// TestArchiveRotationRefused proves a rotation that is not one of the
|
|
// four is refused on the add target form and the target edit form, and
|
|
// that nothing is stored.
|
|
func TestArchiveRotationRefused(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := setupSourceTest(t)
|
|
webhook := seedWebhookWithRetention(t, env.db, 30)
|
|
|
|
form := url.Values{}
|
|
form.Set("name", "archive")
|
|
form.Set("type", string(database.TargetTypeDatabase))
|
|
form.Set("rotation", "weekly")
|
|
|
|
w := serveTarget(
|
|
env, http.MethodPost, "/hook/"+webhook.ID+"/targets", form,
|
|
)
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
assert.Contains(t, w.Body.String(), "Invalid archive rotation")
|
|
assert.Empty(t, targetsForWebhook(t, env.db, webhook.ID))
|
|
|
|
target := seedConfiguredTarget(
|
|
t, env.db, webhook.ID, database.TargetTypeDatabase,
|
|
`{"rotation":"daily"}`,
|
|
)
|
|
|
|
form.Del("type")
|
|
|
|
w = submitTargetEdit(env, webhook.ID, target.ID, form)
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
assert.Contains(t, w.Body.String(), "Invalid archive rotation")
|
|
assert.JSONEq(t,
|
|
`{"rotation":"daily"}`, storedTarget(t, env, target.ID).Config,
|
|
)
|
|
}
|
|
|
|
// TestHandleSourceCreateSubmit_ArchiveRotation proves the new webhook
|
|
// page's archive rotation is stored on the archive target it creates.
|
|
func TestHandleSourceCreateSubmit_ArchiveRotation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := setupSourceTest(t)
|
|
|
|
form := url.Values{}
|
|
form.Set("name", "rotated")
|
|
form.Set("archive", "on")
|
|
form.Set("archive_expiry", "720h")
|
|
form.Set("archive_rotation", "daily")
|
|
|
|
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 = ?", "rotated").First(&webhook).Error)
|
|
|
|
targets := targetsForWebhook(t, env.db, webhook.ID)
|
|
require.Len(t, targets, 1)
|
|
assert.JSONEq(t,
|
|
`{"expiry":"720h","rotation":"daily"}`, targets[0].Config,
|
|
)
|
|
}
|
|
|
|
// TestArchiveFileView_Rotated describes a daily target's archive files
|
|
// at two times. On a day that has a file, the view names that file;
|
|
// on the next, before any event, it names the file the next event
|
|
// will go to, not created yet. Both times the size is of every file
|
|
// together and the last write the latest of them.
|
|
func TestArchiveFileView_Rotated(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := setupSourceTest(t)
|
|
webhook := seedWebhookWithRetention(t, env.db, 30)
|
|
target := seedConfiguredTarget(
|
|
t, env.db, webhook.ID, database.TargetTypeDatabase,
|
|
`{"rotation":"daily"}`,
|
|
)
|
|
|
|
path := delivery.ArchivePath(env.dbMgr, &webhook, target)
|
|
stem := strings.TrimSuffix(path, ".db")
|
|
written := time.Date(2026, 10, 2, 9, 0, 0, 0, time.UTC)
|
|
|
|
for i, day := range []string{"2026-10-01", "2026-10-02"} {
|
|
file := stem + "-" + day + ".db"
|
|
require.NoError(t, os.WriteFile(file, make([]byte, 1000), 0o600))
|
|
|
|
at := written.Add(time.Duration(i-1) * 24 * time.Hour)
|
|
require.NoError(t, os.Chtimes(file, at, at))
|
|
}
|
|
|
|
view := env.handlers.ArchiveFileViewForTest(
|
|
&webhook, target, time.Date(2026, 10, 2, 23, 0, 0, 0, time.UTC),
|
|
)
|
|
assert.Equal(t, filepath.Base(stem)+"-2026-10-02.db", view.Name)
|
|
assert.Empty(t, view.Note)
|
|
assert.Equal(t, 2, view.Files)
|
|
assert.Equal(t, "2.0 kB", view.Size)
|
|
assert.Equal(t, "2026-10-02 09:00:00 UTC", view.WrittenUTC)
|
|
|
|
view = env.handlers.ArchiveFileViewForTest(
|
|
&webhook, target, time.Date(2026, 10, 3, 0, 0, 0, 0, time.UTC),
|
|
)
|
|
assert.Equal(t, filepath.Base(stem)+"-2026-10-03.db", view.Name)
|
|
assert.Equal(t, "not created yet", view.Note)
|
|
assert.Equal(t, 2, view.Files)
|
|
assert.Equal(t, "2.0 kB", view.Size)
|
|
|
|
page := targetList(t, renderedPage(t, env, webhook.ID))
|
|
assert.Contains(t, page, "Archive size: 2.0 kB in 2 files")
|
|
}
|
|
|
|
// renderedPage returns the webhook page.
|
|
func renderedPage(t *testing.T, env *sourceTestEnv, webhookID string) string {
|
|
t.Helper()
|
|
|
|
w := httptest.NewRecorder()
|
|
env.handlers.HandleSourceDetail().ServeHTTP(w, getRequest(
|
|
t, "/hook/"+webhookID, env.cookies,
|
|
map[string]string{sourceIDParam: webhookID},
|
|
))
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
|
|
return w.Body.String()
|
|
}
|