mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-04-27 13:26:21 +02:00
C4: Remove localhost:9100 fallback from 27 dashboard files (use relative URLs) C5: JWT token_type differentiation (access vs refresh) - middleware rejects refresh as Bearer M4: Server-side registration gate via SOC_REGISTRATION_OPEN env var M5: HTML tag stripping on name/org_name fields (XSS prevention) Domain migration: - users.go: admin@syntrex.pro - zerotrust.go: SPIFFE trust domain - sbom.go: namespace URL - .env.production.example: all URLs updated - identity_test.go: test email
395 lines
11 KiB
Go
395 lines
11 KiB
Go
package identity
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
// === Agent Identity Tests ===
|
|
|
|
func TestAgentIdentityValidation(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
agent AgentIdentity
|
|
wantErr error
|
|
}{
|
|
{
|
|
"valid autonomous",
|
|
AgentIdentity{AgentID: "a1", AgentName: "Test", CreatedBy: "admin", AgentType: AgentAutonomous},
|
|
nil,
|
|
},
|
|
{
|
|
"valid supervised",
|
|
AgentIdentity{AgentID: "a2", AgentName: "Test", CreatedBy: "admin", AgentType: AgentSupervised},
|
|
nil,
|
|
},
|
|
{
|
|
"valid external",
|
|
AgentIdentity{AgentID: "a3", AgentName: "Test", CreatedBy: "admin", AgentType: AgentExternal},
|
|
nil,
|
|
},
|
|
{
|
|
"missing agent_id",
|
|
AgentIdentity{AgentName: "Test", CreatedBy: "admin", AgentType: AgentAutonomous},
|
|
ErrMissingAgentID,
|
|
},
|
|
{
|
|
"missing agent_name",
|
|
AgentIdentity{AgentID: "a1", CreatedBy: "admin", AgentType: AgentAutonomous},
|
|
ErrMissingAgentName,
|
|
},
|
|
{
|
|
"missing created_by",
|
|
AgentIdentity{AgentID: "a1", AgentName: "Test", AgentType: AgentAutonomous},
|
|
ErrMissingCreatedBy,
|
|
},
|
|
{
|
|
"invalid agent_type",
|
|
AgentIdentity{AgentID: "a1", AgentName: "Test", CreatedBy: "admin", AgentType: "INVALID"},
|
|
ErrInvalidAgentType,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := tt.agent.Validate()
|
|
if err != tt.wantErr {
|
|
t.Errorf("Validate() = %v, want %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHasPermissionFailSafeClosed(t *testing.T) {
|
|
agent := AgentIdentity{
|
|
Capabilities: []ToolPermission{
|
|
{ToolName: "web_search", Permissions: []Permission{PermRead}},
|
|
{ToolName: "memory_store", Permissions: []Permission{PermRead, PermWrite}},
|
|
},
|
|
}
|
|
|
|
// Allowed
|
|
if !agent.HasPermission("web_search", PermRead) {
|
|
t.Error("should allow READ on web_search")
|
|
}
|
|
if !agent.HasPermission("memory_store", PermWrite) {
|
|
t.Error("should allow WRITE on memory_store")
|
|
}
|
|
|
|
// Deny: wrong permission on known tool
|
|
if agent.HasPermission("web_search", PermWrite) {
|
|
t.Error("should deny WRITE on web_search (insufficient_permissions)")
|
|
}
|
|
|
|
// Deny: unknown tool (fail-safe closed — SDD-003 M3)
|
|
if agent.HasPermission("unknown_tool", PermRead) {
|
|
t.Error("should deny READ on unknown_tool (fail-safe closed)")
|
|
}
|
|
}
|
|
|
|
func TestHasTool(t *testing.T) {
|
|
agent := AgentIdentity{
|
|
Capabilities: []ToolPermission{
|
|
{ToolName: "web_search", Permissions: []Permission{PermRead}},
|
|
},
|
|
}
|
|
if !agent.HasTool("web_search") {
|
|
t.Error("should have web_search")
|
|
}
|
|
if agent.HasTool("unknown") {
|
|
t.Error("should not have unknown")
|
|
}
|
|
}
|
|
|
|
func TestToolNames(t *testing.T) {
|
|
agent := AgentIdentity{
|
|
Capabilities: []ToolPermission{
|
|
{ToolName: "a", Permissions: []Permission{PermRead}},
|
|
{ToolName: "b", Permissions: []Permission{PermWrite}},
|
|
},
|
|
}
|
|
names := agent.ToolNames()
|
|
if len(names) != 2 {
|
|
t.Fatalf("expected 2 tool names, got %d", len(names))
|
|
}
|
|
}
|
|
|
|
// === Store Tests ===
|
|
|
|
func TestStoreRegisterAndGet(t *testing.T) {
|
|
s := NewStore()
|
|
agent := &AgentIdentity{
|
|
AgentID: "agent-01",
|
|
AgentName: "Task Manager",
|
|
CreatedBy: "admin@syntrex.pro",
|
|
AgentType: AgentSupervised,
|
|
}
|
|
if err := s.Register(agent); err != nil {
|
|
t.Fatalf("Register failed: %v", err)
|
|
}
|
|
|
|
got, err := s.Get("agent-01")
|
|
if err != nil {
|
|
t.Fatalf("Get failed: %v", err)
|
|
}
|
|
if got.AgentName != "Task Manager" {
|
|
t.Errorf("got name %q, want %q", got.AgentName, "Task Manager")
|
|
}
|
|
}
|
|
|
|
func TestStoreNotFound(t *testing.T) {
|
|
s := NewStore()
|
|
_, err := s.Get("nonexistent")
|
|
if err != ErrAgentNotFound {
|
|
t.Errorf("expected ErrAgentNotFound, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStoreDuplicateReject(t *testing.T) {
|
|
s := NewStore()
|
|
agent := &AgentIdentity{
|
|
AgentID: "dup-01", AgentName: "A", CreatedBy: "admin", AgentType: AgentAutonomous,
|
|
}
|
|
_ = s.Register(agent)
|
|
err := s.Register(agent)
|
|
if err != ErrAgentExists {
|
|
t.Errorf("expected ErrAgentExists, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStoreRemove(t *testing.T) {
|
|
s := NewStore()
|
|
_ = s.Register(&AgentIdentity{
|
|
AgentID: "rm-01", AgentName: "A", CreatedBy: "admin", AgentType: AgentAutonomous,
|
|
})
|
|
if err := s.Remove("rm-01"); err != nil {
|
|
t.Fatalf("Remove failed: %v", err)
|
|
}
|
|
if s.Count() != 0 {
|
|
t.Error("expected 0 agents after removal")
|
|
}
|
|
}
|
|
|
|
func TestStoreList(t *testing.T) {
|
|
s := NewStore()
|
|
_ = s.Register(&AgentIdentity{AgentID: "l1", AgentName: "A", CreatedBy: "admin", AgentType: AgentAutonomous})
|
|
_ = s.Register(&AgentIdentity{AgentID: "l2", AgentName: "B", CreatedBy: "admin", AgentType: AgentSupervised})
|
|
if len(s.List()) != 2 {
|
|
t.Errorf("expected 2 agents, got %d", len(s.List()))
|
|
}
|
|
}
|
|
|
|
// === Capability Check Tests ===
|
|
|
|
func TestCapabilityAllowed(t *testing.T) {
|
|
s := NewStore()
|
|
_ = s.Register(&AgentIdentity{
|
|
AgentID: "cap-01", AgentName: "A", CreatedBy: "admin", AgentType: AgentAutonomous,
|
|
Capabilities: []ToolPermission{
|
|
{ToolName: "web_search", Permissions: []Permission{PermRead}},
|
|
},
|
|
})
|
|
checker := NewCapabilityChecker(s)
|
|
d := checker.Check("cap-01", "web_search", PermRead)
|
|
if !d.Allowed {
|
|
t.Errorf("expected allowed, got denied: %s", d.Reason)
|
|
}
|
|
}
|
|
|
|
func TestCapabilityDeniedUnknownAgent(t *testing.T) {
|
|
s := NewStore()
|
|
checker := NewCapabilityChecker(s)
|
|
d := checker.Check("ghost", "web_search", PermRead)
|
|
if d.Allowed {
|
|
t.Error("should deny unknown agent")
|
|
}
|
|
if d.Reason != "agent_not_found" {
|
|
t.Errorf("expected reason agent_not_found, got %s", d.Reason)
|
|
}
|
|
}
|
|
|
|
func TestCapabilityDeniedUnknownTool(t *testing.T) {
|
|
s := NewStore()
|
|
_ = s.Register(&AgentIdentity{
|
|
AgentID: "cap-02", AgentName: "A", CreatedBy: "admin", AgentType: AgentAutonomous,
|
|
Capabilities: []ToolPermission{
|
|
{ToolName: "web_search", Permissions: []Permission{PermRead}},
|
|
},
|
|
})
|
|
checker := NewCapabilityChecker(s)
|
|
d := checker.Check("cap-02", "unknown_tool", PermRead)
|
|
if d.Allowed {
|
|
t.Error("should deny unknown tool (fail-safe closed)")
|
|
}
|
|
if d.Reason != "unknown_tool_for_agent" {
|
|
t.Errorf("expected reason unknown_tool_for_agent, got %s", d.Reason)
|
|
}
|
|
}
|
|
|
|
func TestCapabilityDeniedInsufficientPerms(t *testing.T) {
|
|
s := NewStore()
|
|
_ = s.Register(&AgentIdentity{
|
|
AgentID: "cap-03", AgentName: "A", CreatedBy: "admin", AgentType: AgentAutonomous,
|
|
Capabilities: []ToolPermission{
|
|
{ToolName: "web_search", Permissions: []Permission{PermRead}},
|
|
},
|
|
})
|
|
checker := NewCapabilityChecker(s)
|
|
d := checker.Check("cap-03", "web_search", PermWrite)
|
|
if d.Allowed {
|
|
t.Error("should deny WRITE on READ-only tool")
|
|
}
|
|
if d.Reason != "insufficient_permissions" {
|
|
t.Errorf("expected reason insufficient_permissions, got %s", d.Reason)
|
|
}
|
|
}
|
|
|
|
func TestExternalAgentCannotExecute(t *testing.T) {
|
|
s := NewStore()
|
|
_ = s.Register(&AgentIdentity{
|
|
AgentID: "ext-01", AgentName: "External", CreatedBy: "admin", AgentType: AgentExternal,
|
|
Capabilities: []ToolPermission{
|
|
{ToolName: "web_search", Permissions: []Permission{PermRead, PermExecute}},
|
|
},
|
|
})
|
|
checker := NewCapabilityChecker(s)
|
|
d := checker.CheckExternal("ext-01", "web_search", PermExecute)
|
|
if d.Allowed {
|
|
t.Error("external agents should never get EXECUTE permission")
|
|
}
|
|
}
|
|
|
|
// === Namespaced Memory Tests ===
|
|
|
|
func TestNamespacedMemoryIsolation(t *testing.T) {
|
|
m := NewNamespacedMemory()
|
|
|
|
// Agent A stores a value
|
|
m.Store("agent-a", "secret", "classified-data")
|
|
|
|
// Agent A can read it
|
|
val, ok := m.Get("agent-a", "secret")
|
|
if !ok || val.(string) != "classified-data" {
|
|
t.Error("agent-a should be able to read its own data")
|
|
}
|
|
|
|
// Agent B CANNOT read Agent A's data
|
|
_, ok = m.Get("agent-b", "secret")
|
|
if ok {
|
|
t.Error("agent-b should NOT be able to read agent-a's data")
|
|
}
|
|
}
|
|
|
|
func TestNamespacedMemoryKeys(t *testing.T) {
|
|
m := NewNamespacedMemory()
|
|
m.Store("agent-a", "key1", "v1")
|
|
m.Store("agent-a", "key2", "v2")
|
|
m.Store("agent-b", "key3", "v3")
|
|
|
|
keysA := m.Keys("agent-a")
|
|
if len(keysA) != 2 {
|
|
t.Errorf("agent-a should have 2 keys, got %d", len(keysA))
|
|
}
|
|
|
|
keysB := m.Keys("agent-b")
|
|
if len(keysB) != 1 {
|
|
t.Errorf("agent-b should have 1 key, got %d", len(keysB))
|
|
}
|
|
}
|
|
|
|
func TestNamespacedMemoryCount(t *testing.T) {
|
|
m := NewNamespacedMemory()
|
|
m.Store("a", "k1", "v1")
|
|
m.Store("a", "k2", "v2")
|
|
m.Store("b", "k1", "v1")
|
|
|
|
if m.Count("a") != 2 {
|
|
t.Errorf("agent a should have 2 entries, got %d", m.Count("a"))
|
|
}
|
|
if m.Count("b") != 1 {
|
|
t.Errorf("agent b should have 1 entry, got %d", m.Count("b"))
|
|
}
|
|
}
|
|
|
|
func TestNamespacedMemoryDelete(t *testing.T) {
|
|
m := NewNamespacedMemory()
|
|
m.Store("a", "key", "val")
|
|
m.Delete("a", "key")
|
|
_, ok := m.Get("a", "key")
|
|
if ok {
|
|
t.Error("key should be deleted")
|
|
}
|
|
}
|
|
|
|
// === Context Pinning Tests ===
|
|
|
|
func TestSecurityEventsPinned(t *testing.T) {
|
|
messages := []Message{
|
|
{Role: "user", Content: "hello", TokenCount: 100},
|
|
{Role: "security", Content: "injection detected", TokenCount: 50, IsPinned: true, EventType: "injection_detected"},
|
|
{Role: "user", Content: "more chat", TokenCount: 100},
|
|
{Role: "security", Content: "permission denied", TokenCount: 50, IsPinned: true, EventType: "permission_denied"},
|
|
{Role: "user", Content: "latest chat", TokenCount: 100},
|
|
}
|
|
|
|
// Total = 400 tokens, budget = 200
|
|
trimmed := TrimContext(messages, 200)
|
|
|
|
// Both security events MUST survive
|
|
secCount := 0
|
|
for _, m := range trimmed {
|
|
if m.IsPinned {
|
|
secCount++
|
|
}
|
|
}
|
|
if secCount != 2 {
|
|
t.Errorf("expected 2 pinned security events to survive, got %d", secCount)
|
|
}
|
|
}
|
|
|
|
func TestNonSecurityEventsTrimmed(t *testing.T) {
|
|
messages := []Message{
|
|
{Role: "user", Content: "old msg 1", TokenCount: 100},
|
|
{Role: "user", Content: "old msg 2", TokenCount: 100},
|
|
{Role: "user", Content: "old msg 3", TokenCount: 100},
|
|
{Role: "security", Content: "pinned event", TokenCount: 50, IsPinned: true},
|
|
{Role: "user", Content: "newest msg", TokenCount: 100},
|
|
}
|
|
|
|
// Total = 450, budget = 200
|
|
// Pinned = 50, remaining budget = 150 → keep newest msg (100), not enough for old msgs
|
|
trimmed := TrimContext(messages, 200)
|
|
|
|
totalTokens := 0
|
|
for _, m := range trimmed {
|
|
totalTokens += m.TokenCount
|
|
}
|
|
if totalTokens > 200 {
|
|
t.Errorf("trimmed context exceeds budget: %d > 200", totalTokens)
|
|
}
|
|
}
|
|
|
|
func TestPinnedByEventType(t *testing.T) {
|
|
if !IsPinnedEvent("injection_detected") {
|
|
t.Error("injection_detected should be pinned")
|
|
}
|
|
if !IsPinnedEvent("credential_access_blocked") {
|
|
t.Error("credential_access_blocked should be pinned")
|
|
}
|
|
if !IsPinnedEvent("genai_credential_access") {
|
|
t.Error("genai_credential_access should be pinned")
|
|
}
|
|
if IsPinnedEvent("normal_chat") {
|
|
t.Error("normal_chat should NOT be pinned")
|
|
}
|
|
}
|
|
|
|
func TestTrimContextWithinBudget(t *testing.T) {
|
|
messages := []Message{
|
|
{Role: "user", Content: "hello", TokenCount: 50},
|
|
{Role: "assistant", Content: "hi", TokenCount: 50},
|
|
}
|
|
// Within budget — no trimming
|
|
trimmed := TrimContext(messages, 1000)
|
|
if len(trimmed) != 2 {
|
|
t.Errorf("expected 2 messages (within budget), got %d", len(trimmed))
|
|
}
|
|
}
|