246 lines
8.0 KiB
Go
246 lines
8.0 KiB
Go
package admin_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/veylant/ia-gateway/internal/admin"
|
|
"github.com/veylant/ia-gateway/internal/middleware"
|
|
"github.com/veylant/ia-gateway/internal/routing"
|
|
)
|
|
|
|
const testTenantID = "tenant-test"
|
|
|
|
// ─── Test fixtures ────────────────────────────────────────────────────────────
|
|
|
|
func setupHandler(t *testing.T) (*admin.Handler, *routing.MemStore, *routing.RuleCache) {
|
|
t.Helper()
|
|
store := routing.NewMemStore()
|
|
cache := routing.NewRuleCache(store, 30*time.Second, zap.NewNop())
|
|
h := admin.New(store, cache, zap.NewNop())
|
|
return h, store, cache
|
|
}
|
|
|
|
// authCtx returns a request context with tenant JWT claims.
|
|
func authCtx(tenantID string) context.Context {
|
|
return middleware.WithClaims(context.Background(), &middleware.UserClaims{
|
|
UserID: "admin-user",
|
|
TenantID: tenantID,
|
|
Roles: []string{"admin"},
|
|
})
|
|
}
|
|
|
|
// newRouter builds a chi.Router with the handler routes mounted.
|
|
func newRouter(h *admin.Handler) chi.Router {
|
|
r := chi.NewRouter()
|
|
h.Routes(r)
|
|
return r
|
|
}
|
|
|
|
// postJSON sends a POST with JSON body.
|
|
func postJSON(t *testing.T, router http.Handler, path string, body interface{}, ctx context.Context) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
b, _ := json.Marshal(body)
|
|
req := httptest.NewRequest(http.MethodPost, path, bytes.NewReader(b))
|
|
req = req.WithContext(ctx)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
return rec
|
|
}
|
|
|
|
func getReq(t *testing.T, router http.Handler, path string, ctx context.Context) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
|
req = req.WithContext(ctx)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
return rec
|
|
}
|
|
|
|
func deleteReq(t *testing.T, router http.Handler, path string, ctx context.Context) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
req := httptest.NewRequest(http.MethodDelete, path, nil)
|
|
req = req.WithContext(ctx)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
return rec
|
|
}
|
|
|
|
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
|
|
func TestAdminHandler_Create_ReturnsCreated(t *testing.T) {
|
|
h, _, _ := setupHandler(t)
|
|
r := newRouter(h)
|
|
|
|
body := map[string]interface{}{
|
|
"name": "finance rule",
|
|
"priority": 10,
|
|
"is_enabled": true,
|
|
"conditions": []map[string]interface{}{
|
|
{"field": "user.department", "operator": "eq", "value": "finance"},
|
|
},
|
|
"action": map[string]interface{}{"provider": "ollama"},
|
|
}
|
|
rec := postJSON(t, r, "/policies", body, authCtx(testTenantID))
|
|
assert.Equal(t, http.StatusCreated, rec.Code)
|
|
|
|
var got routing.RoutingRule
|
|
require.NoError(t, json.NewDecoder(rec.Body).Decode(&got))
|
|
assert.Equal(t, "finance rule", got.Name)
|
|
assert.Equal(t, testTenantID, got.TenantID)
|
|
}
|
|
|
|
func TestAdminHandler_Create_InvalidCondition_Returns400(t *testing.T) {
|
|
h, _, _ := setupHandler(t)
|
|
r := newRouter(h)
|
|
|
|
body := map[string]interface{}{
|
|
"name": "bad rule",
|
|
"conditions": []map[string]interface{}{
|
|
{"field": "user.unknown_field", "operator": "eq", "value": "x"},
|
|
},
|
|
"action": map[string]interface{}{"provider": "openai"},
|
|
}
|
|
rec := postJSON(t, r, "/policies", body, authCtx(testTenantID))
|
|
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
|
}
|
|
|
|
func TestAdminHandler_Create_MissingName_Returns400(t *testing.T) {
|
|
h, _, _ := setupHandler(t)
|
|
r := newRouter(h)
|
|
|
|
body := map[string]interface{}{
|
|
"conditions": []map[string]interface{}{},
|
|
"action": map[string]interface{}{"provider": "openai"},
|
|
}
|
|
rec := postJSON(t, r, "/policies", body, authCtx(testTenantID))
|
|
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
|
}
|
|
|
|
func TestAdminHandler_List_ReturnsTenantRules(t *testing.T) {
|
|
h, store, _ := setupHandler(t)
|
|
r := newRouter(h)
|
|
|
|
// Seed two rules: one for testTenantID, one for another tenant.
|
|
_, _ = store.Create(context.Background(), routing.RoutingRule{
|
|
TenantID: testTenantID, Name: "r1", IsEnabled: true,
|
|
Conditions: []routing.Condition{}, Action: routing.Action{Provider: "openai"},
|
|
})
|
|
_, _ = store.Create(context.Background(), routing.RoutingRule{
|
|
TenantID: "other-tenant", Name: "r2", IsEnabled: true,
|
|
Conditions: []routing.Condition{}, Action: routing.Action{Provider: "openai"},
|
|
})
|
|
|
|
rec := getReq(t, r, "/policies", authCtx(testTenantID))
|
|
assert.Equal(t, http.StatusOK, rec.Code)
|
|
|
|
var resp map[string][]routing.RoutingRule
|
|
require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp))
|
|
// Only the rule for testTenantID should be visible.
|
|
assert.Len(t, resp["data"], 1)
|
|
assert.Equal(t, "r1", resp["data"][0].Name)
|
|
}
|
|
|
|
func TestAdminHandler_Get_ExistingRule(t *testing.T) {
|
|
h, store, _ := setupHandler(t)
|
|
r := newRouter(h)
|
|
|
|
rule, _ := store.Create(context.Background(), routing.RoutingRule{
|
|
TenantID: testTenantID, Name: "my-rule", IsEnabled: true,
|
|
Conditions: []routing.Condition{}, Action: routing.Action{Provider: "openai"},
|
|
})
|
|
|
|
rec := getReq(t, r, "/policies/"+rule.ID, authCtx(testTenantID))
|
|
assert.Equal(t, http.StatusOK, rec.Code)
|
|
|
|
var got routing.RoutingRule
|
|
require.NoError(t, json.NewDecoder(rec.Body).Decode(&got))
|
|
assert.Equal(t, "my-rule", got.Name)
|
|
}
|
|
|
|
func TestAdminHandler_Get_NotFound_Returns404(t *testing.T) {
|
|
h, _, _ := setupHandler(t)
|
|
r := newRouter(h)
|
|
|
|
rec := getReq(t, r, "/policies/nonexistent-id", authCtx(testTenantID))
|
|
assert.Equal(t, http.StatusNotFound, rec.Code)
|
|
}
|
|
|
|
func TestAdminHandler_Delete_RemovesRule(t *testing.T) {
|
|
h, store, _ := setupHandler(t)
|
|
r := newRouter(h)
|
|
|
|
rule, _ := store.Create(context.Background(), routing.RoutingRule{
|
|
TenantID: testTenantID, Name: "to-delete", IsEnabled: true,
|
|
Conditions: []routing.Condition{}, Action: routing.Action{Provider: "openai"},
|
|
})
|
|
|
|
rec := deleteReq(t, r, "/policies/"+rule.ID, authCtx(testTenantID))
|
|
assert.Equal(t, http.StatusNoContent, rec.Code)
|
|
|
|
// Second delete should return 404.
|
|
rec2 := deleteReq(t, r, "/policies/"+rule.ID, authCtx(testTenantID))
|
|
assert.Equal(t, http.StatusNotFound, rec2.Code)
|
|
}
|
|
|
|
func TestAdminHandler_TenantIsolation_CannotDeleteOtherTenantRule(t *testing.T) {
|
|
h, store, _ := setupHandler(t)
|
|
r := newRouter(h)
|
|
|
|
// Rule belongs to another tenant.
|
|
rule, _ := store.Create(context.Background(), routing.RoutingRule{
|
|
TenantID: "other-tenant", Name: "private-rule", IsEnabled: true,
|
|
Conditions: []routing.Condition{}, Action: routing.Action{Provider: "openai"},
|
|
})
|
|
|
|
// testTenantID cannot delete a rule that belongs to other-tenant — returns 404.
|
|
rec := deleteReq(t, r, "/policies/"+rule.ID, authCtx(testTenantID))
|
|
assert.Equal(t, http.StatusNotFound, rec.Code, "cannot delete another tenant's rule")
|
|
}
|
|
|
|
func TestAdminHandler_SeedTemplate_Catchall(t *testing.T) {
|
|
h, _, cache := setupHandler(t)
|
|
r := newRouter(h)
|
|
|
|
// Pre-populate cache to verify it gets invalidated.
|
|
_, _ = cache.Get(context.Background(), testTenantID)
|
|
|
|
rec := postJSON(t, r, "/policies/seed/catchall", nil, authCtx(testTenantID))
|
|
assert.Equal(t, http.StatusCreated, rec.Code)
|
|
|
|
var got routing.RoutingRule
|
|
require.NoError(t, json.NewDecoder(rec.Body).Decode(&got))
|
|
assert.Equal(t, 9999, got.Priority)
|
|
assert.Equal(t, "openai", got.Action.Provider)
|
|
}
|
|
|
|
func TestAdminHandler_SeedTemplate_UnknownTemplate_Returns400(t *testing.T) {
|
|
h, _, _ := setupHandler(t)
|
|
r := newRouter(h)
|
|
|
|
rec := postJSON(t, r, "/policies/seed/unknown", nil, authCtx(testTenantID))
|
|
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
|
}
|
|
|
|
func TestAdminHandler_NoAuth_Returns401(t *testing.T) {
|
|
h, _, _ := setupHandler(t)
|
|
r := newRouter(h)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/policies", nil)
|
|
// No claims in context.
|
|
rec := httptest.NewRecorder()
|
|
r.ServeHTTP(rec, req)
|
|
assert.Equal(t, http.StatusUnauthorized, rec.Code)
|
|
}
|