mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-25 14:22:57 +00:00
* feat(api): unblock trace follow and provider reconnect * test(cron): stabilize run log execution test * feat(http): add CLI P6 backend endpoints
136 lines
4.5 KiB
Go
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)
|
|
}
|
|
}
|