check / check (push) Waiting to run
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
244 lines
7.4 KiB
Go
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()
|
|
}
|