check / check (push) Successful in 3m31s
The database target type is called an archive on its badge, in the add target form's type list and on its edit page, and its settings read "Archive expiry" and "Archive rotation" everywhere, the new webhook page included. The retry field is labelled "Delivery attempts", with its help text and error messages to match, on both target forms and in the target list, where a stored 0 shows as one attempt. The target list's other labels take the same capitalisation. The navbar says "Sign out" and the sign-in page "Sign in", and the README follows. The resubmit notice says "webhook". The stored values (`database`, `max_retries`) and their meaning 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()
|
|
}
|