gomcp/internal/domain/identity/identity_test.go
DmitrL-dev 62ecc1c7a3 sec: fix C4/C5/M4/M5 + domain migration to syntrex.pro
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
2026-03-24 11:49:33 +10:00

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))
}
}