Files
RenewCMS/test/activity_retention_size_http_test.go

191 lines
7.0 KiB
Go

package test
import (
"RenewCMS/api/pkg/apperr"
"encoding/json"
"net/http"
"reflect"
"sort"
"testing"
)
func sizeSettingsBody(t *testing.T, body []byte) map[string]any {
t.Helper()
var decoded map[string]any
if err := json.Unmarshal(body, &decoded); err != nil {
t.Fatalf("invalid json %q: %v", body, err)
}
return decoded
}
func TestSettingsRetentionSizeHttp(t *testing.T) {
owner := func(e *activityHTTPEnv) activityRequest { return asUser(e, activityOwnerID) }
t.Run("Settings API, GET reports the size limit and the measured size", func(t *testing.T) {
cases := []struct {
days, gb int
bytes int64
next any
}{
{90, 0, 0, "2026-10-07T03:00:00+02:00"},
{90, 5, 3435973837, "2026-10-07T03:00:00+02:00"},
{0, 5, 3435973837, "2026-10-07T03:00:00+02:00"},
{0, 0, 1024, nil},
}
keys := []string{"version", "activity_retention_days", "activity_retention_max_gb", "activity_log_size_bytes", "activity_last_cleanup_at", "activity_last_cleanup_removed", "activity_next_cleanup_at"}
sort.Strings(keys)
for _, c := range cases {
e := newActivityHTTPEnv(t, 1000)
e.now = settingsTestClock()
e.settings.settings.RetentionDays = c.days
e.settings.settings.RetentionMaxGB = c.gb
e.probe.sizeBytes = &c.bytes
rec := e.get("/v1/settings", asUser(e, activityEditorID))
body := sizeSettingsBody(t, rec.Body.Bytes())
var got []string
for key := range body {
got = append(got, key)
}
sort.Strings(got)
if rec.Code != http.StatusOK || !reflect.DeepEqual(got, keys) {
t.Fatalf("unexpected %d %v", rec.Code, got)
}
if body["activity_retention_max_gb"] != float64(c.gb) || body["activity_log_size_bytes"] != float64(c.bytes) || body["activity_next_cleanup_at"] != c.next {
t.Errorf("%+v: unexpected body %v", c, body)
}
}
})
t.Run("Settings API, a fresh install defaults to no size limit", func(t *testing.T) {
e := newActivityHTTPEnv(t, 1000)
body := sizeSettingsBody(t, e.get("/v1/settings", asUser(e, activityEditorID)).Body.Bytes())
if body["activity_retention_days"] != float64(90) || body["activity_retention_max_gb"] != float64(0) || body["activity_log_size_bytes"] != float64(0) {
t.Errorf("unexpected body %v", body)
}
})
t.Run("Settings API, PATCH saves a size limit, an age, or both", func(t *testing.T) {
cases := []struct {
body string
days, gb int
null bool
}{
{`{"activity_retention_max_gb": 5}`, 90, 5, false},
{`{"activity_retention_days": 0, "activity_retention_max_gb": 5}`, 0, 5, false},
{`{"activity_retention_days": 30}`, 30, 0, false},
{`{"activity_retention_days": 0}`, 0, 0, true},
}
for _, c := range cases {
e := newActivityHTTPEnv(t, 1000)
rec := e.send(http.MethodPatch, "/v1/settings", c.body, owner(e))
body := sizeSettingsBody(t, rec.Body.Bytes())
next, present := body["activity_next_cleanup_at"]
if rec.Code != http.StatusOK || body["activity_retention_days"] != float64(c.days) || body["activity_retention_max_gb"] != float64(c.gb) || !present || (next == nil) != c.null {
t.Errorf("%s: unexpected %d %v", c.body, rec.Code, body)
}
if e.settings.settings.RetentionDays != c.days || e.settings.settings.RetentionMaxGB != c.gb {
t.Errorf("%s: the repository holds %+v", c.body, e.settings.settings)
}
}
})
t.Run("Settings API, leaving the size limit while Forever clears the schedule", func(t *testing.T) {
e := newActivityHTTPEnv(t, 1000)
e.settings.settings.RetentionDays = 0
e.settings.settings.RetentionMaxGB = 5
rec := e.send(http.MethodPatch, "/v1/settings", `{"activity_retention_max_gb": 0}`, owner(e))
body := sizeSettingsBody(t, rec.Body.Bytes())
next, present := body["activity_next_cleanup_at"]
if rec.Code != http.StatusOK || !present || next != nil {
t.Errorf("unexpected %d %v", rec.Code, body)
}
})
t.Run("Settings API, PATCH rejects invalid size input", func(t *testing.T) {
bodies := []string{
`{"activity_retention_max_gb": 2}`,
`{"activity_retention_max_gb": -1}`,
`{"activity_retention_max_gb": 100}`,
`{"activity_retention_max_gb": 5.5}`,
`{"activity_retention_max_gb": "5"}`,
`{"activity_retention_max_gb": null}`,
`{"activity_retention_days": 30, "activity_retention_max_gb": 2}`,
`{"activity_retention_days": null}`,
`{"activity_retention_days": 30, "activity_retention_max_gb": null}`,
`{"activity_retention_days": null, "activity_retention_max_gb": 5}`,
`[1]`,
`{}`,
`not json`,
}
for _, body := range bodies {
e := newActivityHTTPEnv(t, 1000)
e.settings.settings.RetentionMaxGB = 5
assertPlain(t, e.send(http.MethodPatch, "/v1/settings", body, owner(e)), http.StatusBadRequest, activityMismatch)
if e.settings.retentionOK != 0 || e.settings.settings.RetentionDays != 90 || e.settings.settings.RetentionMaxGB != 5 || len(e.all(t)) != 0 {
t.Errorf("%s: something was saved", body)
}
}
})
t.Run("Settings API, a PATCH changing nothing is a no-op", func(t *testing.T) {
e := newActivityHTTPEnv(t, 1000)
e.settings.settings.RetentionMaxGB = 5
rec := e.send(http.MethodPatch, "/v1/settings", `{"activity_retention_days": 90, "activity_retention_max_gb": 5}`, owner(e))
if rec.Code != http.StatusOK || e.settings.retentionOK != 0 || len(e.all(t)) != 0 {
t.Errorf("unexpected %d, saves %d, events %d", rec.Code, e.settings.retentionOK, len(e.all(t)))
}
})
t.Run("Settings API, a non-owner cannot set the size limit", func(t *testing.T) {
e := newActivityHTTPEnv(t, 1000)
rec := e.send(http.MethodPatch, "/v1/settings", `{"activity_retention_max_gb": 5}`, asUser(e, activityAdminID))
assertPlain(t, rec, http.StatusForbidden, "Only the owner can do this.")
if e.settings.retentionOK != 0 {
t.Error("the admin changed something")
}
assertPlain(t, e.send(http.MethodPatch, "/v1/settings", `{"activity_retention_max_gb": 5}`), http.StatusUnauthorized, apperr.UnauthorizedErrorMessage)
})
t.Run("Settings API, a failing size measurement answers 500", func(t *testing.T) {
e := newActivityHTTPEnv(t, 1000)
e.probe.sizeErr = errRepositoryFailure
assertPlain(t, e.get("/v1/settings", asUser(e, activityEditorID)), http.StatusInternalServerError, "Internal server error")
})
t.Run("Settings API, the change is audited", func(t *testing.T) {
cases := []struct {
body string
params map[string]any
}{
{`{"activity_retention_days": 30}`, map[string]any{"from": float64(90), "to": float64(30)}},
{`{"activity_retention_max_gb": 5}`, map[string]any{"max_gb_from": float64(0), "max_gb_to": float64(5)}},
{`{"activity_retention_days": 30, "activity_retention_max_gb": 5}`, map[string]any{"from": float64(90), "to": float64(30), "max_gb_from": float64(0), "max_gb_to": float64(5)}},
}
for _, c := range cases {
e := newActivityHTTPEnv(t, 1000)
e.send(http.MethodPatch, "/v1/settings", c.body, owner(e))
events := e.all(t)
if len(events) != 1 || events[0].Action != "system.activity_retention_changed" || !reflect.DeepEqual(events[0].Params, c.params) {
t.Errorf("%s: unexpected events %+v", c.body, events)
}
}
})
}