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:\s*([^<]*)`, 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() }