Files
webhooker/internal/handlers/archive_rotation_test.go
T
clawbot 8406c77949
check / check (push) Waiting to run
Store a target's settings as columns (closes #543)
A target's settings move from the per-type JSON string in
targets.config to columns: url, headers, timeout_seconds,
forward_query, archive_expiry and archive_rotation. A column a type
does not use stays empty. headers holds the header map as JSON text
through GORM's JSON serializer, and archive_expiry holds a
time.Duration, 0 meaning never. The forms check each setting once,
with the same refusals and messages, and the edit form is filled from
the columns, so the per-type config structs, their parsers and the
"could not be read" branches are gone. A delivery task carries the
six settings in place of the JSON string; tests check that each copy
into a task, and back out of one, keeps all six.

Model: opus-5-5
2026-10-15 23:02:39 +02:00

244 lines
7.4 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/archive"
"sneak.berlin/go/webhooker/internal/database"
)
// The archive rotations these tests store: none keeps one file.
const (
rotationNone = "none"
rotationDaily = "daily"
rotationHourly = "hourly"
)
// 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", rotationDaily, rotationHourly,
} {
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.Equal(t, 720*time.Hour, targets[0].ArchiveExpiry)
assert.Equal(t, rotation, targets[0].ArchiveRotation)
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())
stored := storedTarget(t, env, targets[0].ID)
assert.Equal(t, 720*time.Hour, stored.ArchiveExpiry)
assert.Equal(t, rotationHourly, stored.ArchiveRotation)
})
}
}
// TestArchiveRotationEditStartsOnNone checks that a database target
// added with no rotation is stored with none and shown with none, and
// that its edit form starts on none.
func TestArchiveRotationEditStartsOnNone(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))
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.Equal(t, rotationNone, targets[0].ArchiveRotation)
page := targetEditPage(t, env, webhook.ID, targets[0].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,
database.Target{ArchiveRotation: rotationDaily},
)
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.Equal(
t, rotationDaily, storedTarget(t, env, target.ID).ArchiveRotation,
)
}
// 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.Equal(t, 720*time.Hour, targets[0].ArchiveExpiry)
assert.Equal(t, rotationDaily, targets[0].ArchiveRotation)
}
// 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,
database.Target{ArchiveRotation: rotationDaily},
)
path := archive.Path(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()
}