check / check (push) Canceled after 0s
The SQLite archive files the database target writes are now managed by internal/archive: file names, rotation, the writer of each target, pruning, renames, the gzipped JSON export, the idle sweep, and the lock renames run under, which was Handlers.renameMu. The database target stays in internal/delivery and writes through archive.Writers; the engine still closes the writers when it stops, so the stop order is unchanged. The handlers take the lock, rename and evict through archive.Archives. Exported names drop their Archive prefix in the new package. The tests moved with the code; the two that check a delivery's recorded attempt stay in delivery. 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/archive"
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
)
|
|
|
|
// 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 := 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()
|
|
}
|