Files
goclaw/internal/http/activity_aggregate_test.go
Duy /zuey/andGitHub 43049d3b3f feat(http): unblock CLI P6 backend APIs (#44)
* feat(api): unblock trace follow and provider reconnect

* test(cron): stabilize run log execution test

* feat(http): add CLI P6 backend endpoints
2026-05-22 20:59:47 +07:00

136 lines
4.5 KiB
Go

package http
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/nextlevelbuilder/goclaw/internal/crypto"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
type fakeActivityStore struct {
opts store.ActivityAggregateOpts
}
func (s *fakeActivityStore) Log(context.Context, *store.ActivityLog) error { return nil }
func (s *fakeActivityStore) List(context.Context, store.ActivityListOpts) ([]store.ActivityLog, error) {
return nil, nil
}
func (s *fakeActivityStore) Count(context.Context, store.ActivityListOpts) (int, error) {
return 0, nil
}
func (s *fakeActivityStore) Aggregate(_ context.Context, opts store.ActivityAggregateOpts) ([]store.ActivityAggregateBucket, int, error) {
s.opts = opts
return []store.ActivityAggregateBucket{{Key: "session.branch", Count: 2, LastSeen: time.Now().UTC()}}, 2, nil
}
func TestActivityAggregateScopesViewerToCaller(t *testing.T) {
token := "activity-read-key"
setupTestCache(t, map[string]*store.APIKeyData{
crypto.HashAPIKey(token): {Scopes: []string{"operator.write"}, OwnerID: "caller"},
})
activity := &fakeActivityStore{}
mux := http.NewServeMux()
NewActivityHandler(activity).RegisterRoutes(mux)
req := httptest.NewRequest(http.MethodGet, "/v1/activity/aggregate?group_by=action&actor_id=other&limit=999", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
}
if activity.opts.ActorID != "caller" {
t.Fatalf("ActorID = %q, want caller", activity.opts.ActorID)
}
if activity.opts.Limit != 200 {
t.Fatalf("Limit = %d, want 200", activity.opts.Limit)
}
}
func TestActivityAggregateRejectsActorIDGroupForViewer(t *testing.T) {
token := "activity-viewer-key"
setupTestCache(t, map[string]*store.APIKeyData{
crypto.HashAPIKey(token): {Scopes: []string{"operator.write"}, OwnerID: "caller"},
})
mux := http.NewServeMux()
NewActivityHandler(&fakeActivityStore{}).RegisterRoutes(mux)
req := httptest.NewRequest(http.MethodGet, "/v1/activity/aggregate?group_by=actor_id", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf("status = %d, want 403; body=%s", rec.Code, rec.Body.String())
}
}
func TestActivityAggregateRejectsUnboundNonAdminKey(t *testing.T) {
token := "activity-unbound-key"
setupTestCache(t, map[string]*store.APIKeyData{
crypto.HashAPIKey(token): {Scopes: []string{"operator.write"}},
})
mux := http.NewServeMux()
NewActivityHandler(&fakeActivityStore{}).RegisterRoutes(mux)
req := httptest.NewRequest(http.MethodGet, "/v1/activity/aggregate?group_by=action", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want 400; body=%s", rec.Code, rec.Body.String())
}
}
func TestActivityAggregateRejectsInvalidRange(t *testing.T) {
token := "activity-admin-key"
setupTestCache(t, map[string]*store.APIKeyData{
crypto.HashAPIKey(token): {Scopes: []string{"operator.admin"}, OwnerID: "admin"},
})
mux := http.NewServeMux()
NewActivityHandler(&fakeActivityStore{}).RegisterRoutes(mux)
req := httptest.NewRequest(http.MethodGet, "/v1/activity/aggregate?group_by=action&from=2026-05-23T00:00:00Z&to=2026-05-22T00:00:00Z", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want 400; body=%s", rec.Code, rec.Body.String())
}
}
func TestActivityAggregateResponseShape(t *testing.T) {
token := "activity-admin-shape-key"
setupTestCache(t, map[string]*store.APIKeyData{
crypto.HashAPIKey(token): {Scopes: []string{"operator.admin"}, OwnerID: "admin"},
})
mux := http.NewServeMux()
NewActivityHandler(&fakeActivityStore{}).RegisterRoutes(mux)
req := httptest.NewRequest(http.MethodGet, "/v1/activity/aggregate?group_by=action", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
var body struct {
Source string `json:"source"`
GroupBy string `json:"group_by"`
Total int `json:"total"`
Buckets []any `json:"buckets"`
}
if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
t.Fatal(err)
}
if body.Source != "activity" || body.GroupBy != "action" || body.Total != 2 || len(body.Buckets) != 1 {
t.Fatalf("body = %+v", body)
}
}