feat(activity): update summary cards to toggles and introduce export menu

This commit is contained in:
Florian Sylvain committed 2026-10-12 05:13:25 +02:00
1 parent 73f4b539fe
commit 03b39e64f2
30 files changed
+2376 -238

No files matched your search

@@ -0,0 +1,77 @@
package test
import (
"RenewCMS/api/pkg/apperr"
"RenewCMS/internal/domain/audit"
"encoding/csv"
"encoding/json"
"net/http"
"reflect"
"strings"
"testing"
"time"
)
func TestActivityRangeAll(t *testing.T) {
const day = 24 * time.Hour
t.Run("range=all covers every event", func(t *testing.T) {
e := newActivityHTTPEnv(t, 1000)
recent := e.seedBy(t, userActor(1, "a"), 2*day)
old := e.seedBy(t, userActor(1, "a"), 90*day, func(ev *audit.Event) {
ev.Params = map[string]any{"marker": "kept"}
})
got := decodeActivityList(t, e.get("/v1/activity?range=all", asUser(e, activityOwnerID))).ids()
if !sameIDs(got, []uint64{recent.ID, old.ID}) {
t.Errorf("expected both events listed, got %v", got)
}
raw := e.get("/v1/activity?range=all", asUser(e, activityOwnerID)).Body.String()
if !strings.Contains(raw, `"changes":[]`) || strings.Contains(raw, `"changes":null`) || !strings.Contains(raw, `"params":{}`) || !strings.Contains(raw, `"marker":"kept"`) {
t.Errorf("expected empty changes arrays, got %s", raw)
}
summary := e.get("/v1/activity/summary?range=all", asUser(e, activityOwnerID))
if summary.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", summary.Code)
}
var counts map[string]any
_ = json.Unmarshal(summary.Body.Bytes(), &counts)
if counts["total"] != float64(2) {
t.Errorf("expected a summary total of 2, got %v", counts["total"])
}
export := e.get("/v1/activity/export?range=all", asUser(e, activityOwnerID))
rows, err := csv.NewReader(strings.NewReader(export.Body.String())).ReadAll()
if export.Code != http.StatusOK || err != nil || len(rows) != 3 {
t.Errorf("expected header and two rows, got %d %q %v", export.Code, export.Body.String(), err)
}
})
t.Run("Omitting range still means 7 days", func(t *testing.T) {
e := newActivityHTTPEnv(t, 1000)
recent := e.seedBy(t, userActor(1, "a"), 2*day)
e.seedBy(t, userActor(1, "a"), 90*day)
got := decodeActivityList(t, e.get("/v1/activity", asUser(e, activityOwnerID))).ids()
if !reflect.DeepEqual(got, []uint64{recent.ID}) {
t.Errorf("expected only the recent event, got %v", got)
}
})
t.Run("Other invalid ranges still answer 400", func(t *testing.T) {
for _, path := range []string{"/v1/activity", "/v1/activity/summary", "/v1/activity/export"} {
for _, query := range []string{"range=ALL", "range=everything", "range=year"} {
e := newActivityHTTPEnv(t, 1000)
assertPlain(t, e.get(path+"?"+query, asUser(e, activityOwnerID)), http.StatusBadRequest, activityMismatch)
}
}
})
t.Run("A non-owner export with range=all is still 403", func(t *testing.T) {
e := newActivityHTTPEnv(t, 1000)
assertPlain(t, e.get("/v1/activity/export?range=all", asUser(e, activityEditorID)), http.StatusForbidden, apperr.OwnerOrAdminRequiredErrorMessage)
})
}