iop/packages/go/audit/audit_test.go
toki e051f7ca5b feat(audit): 정책 이력감시 Go 패키지를 추가한다
- packages/go/audit: AuditEntry 구조체 및 검증 로직 구현
- agent-task: m-policy-history-audit 작업 아카이브 추가
- agent-roadmap: 정책 이력감시 마일스톤 상태 갱신
- README: 프로젝트 구조 문서 업데이트
2026-06-03 13:05:26 +09:00

253 lines
7.7 KiB
Go

package audit
import (
"testing"
"time"
)
func TestEventValidateRequiresTypeSourceTimestamp(t *testing.T) {
t.Parallel()
// Empty type should return an error
e := Event{Source: SourceEdge, Timestamp: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)}
err := e.Validate()
if err == nil {
t.Fatal("expected error for missing event type")
}
if got := err.Error(); got != "audit event type is required" {
t.Fatalf("expected 'audit event type is required', got %q", got)
}
// Empty source should return an error
e = Event{Type: TypeExecutionRequested, Timestamp: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)}
err = e.Validate()
if err == nil {
t.Fatal("expected error for missing source")
}
if got := err.Error(); got != "audit event source is required" {
t.Fatalf("expected 'audit event source is required', got %q", got)
}
// Zero timestamp should return an error
e = Event{Type: TypeExecutionRequested, Source: SourceNode}
err = e.Validate()
if err == nil {
t.Fatal("expected error for zero timestamp")
}
if got := err.Error(); got != "audit event timestamp is required" {
t.Fatalf("expected 'audit event timestamp is required', got %q", got)
}
// All required fields present should pass
e = Event{
Type: TypeExecutionCompleted,
Source: SourceControlPlane,
Timestamp: time.Date(2026, 6, 1, 12, 0, 0, 0, time.UTC),
}
if err := e.Validate(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestEventValidateAcceptsStructuredExecutionAuditEvent(t *testing.T) {
t.Parallel()
ts := time.Date(2026, 6, 1, 15, 30, 0, 0, time.UTC)
e := Event{
EventID: "evt-001",
Type: TypeExecutionPolicyEvaluated,
Source: SourceEdge,
ActorID: "user-42",
RequestID: "req-100",
RunID: "run-200",
SessionID: "sess-300",
EdgeID: "edge-1",
NodeID: "node-5",
Adapter: "k8s",
Target: "deploy/web",
CredentialRef: MetadataCredentialReference,
PolicyDecision: DecisionAllow,
PolicyRule: "rule-default-allow",
Timestamp: ts,
Status: "passed",
Metadata: map[string]string{
"extra": "value",
},
}
if err := e.Validate(); err != nil {
t.Fatalf("unexpected validation error: %v", err)
}
}
func TestEventValidateAcceptsTerminalAuditEvent(t *testing.T) {
t.Parallel()
ts := time.Date(2026, 6, 2, 10, 0, 0, 0, time.UTC)
e := Event{
Type: TypeTerminalSessionClosed,
Source: SourceNode,
EventID: "evt-term-01",
NodeID: "node-abc",
Timestamp: ts,
}
if err := e.Validate(); err != nil {
t.Fatalf("unexpected validation error: %v", err)
}
}
func TestEventValidateAcceptsBootstrapAuditEvent(t *testing.T) {
t.Parallel()
ts := time.Date(2026, 6, 3, 8, 0, 0, 0, time.UTC)
e := Event{
Type: TypeBootstrapEnrollmentRequested,
Source: SourceControlPlane,
EventID: "evt-boot-01",
Timestamp: ts,
}
if err := e.Validate(); err != nil {
t.Fatalf("unexpected validation error: %v", err)
}
}
func TestCopyMetadataDefensivelyCopiesMap(t *testing.T) {
t.Parallel()
// nil metadata should return nil
e := Event{
Type: TypeExecutionRequested,
Source: SourceEdge,
Timestamp: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC),
}
if got := e.CopyMetadata(); got != nil {
t.Fatalf("expected nil for nil metadata, got %v", got)
}
// Non-nil metadata should be defensively copied
original := map[string]string{
"foo": "bar",
"key": "val",
}
e.Metadata = original
copied := e.CopyMetadata()
if len(copied) != len(original) {
t.Fatalf("expected copied map length %d, got %d", len(original), len(copied))
}
if copied["foo"] != "bar" {
t.Fatalf("expected copied['foo'] == 'bar', got %q", copied["foo"])
}
if copied["key"] != "val" {
t.Fatalf("expected copied['key'] == 'val', got %q", copied["key"])
}
// Modifying the copy must not affect the original
copied["foo"] = "changed"
if original["foo"] != "bar" {
t.Fatal("original metadata was mutated by changing the copy")
}
// Modifying the original must not affect the copy
original["key"] = "changed"
if copied["key"] != "val" {
t.Fatal("copied metadata was mutated by changing the original")
}
}
func TestAuditPolicyDefaultsAvoidRawTerminalTranscript(t *testing.T) {
t.Parallel()
// MetadataCredentialReference should not contain raw secret text
if MetadataCredentialReference == "" {
t.Fatal("MetadataCredentialReference should not be empty")
}
if MetadataRedactionMarker == "" {
t.Fatal("MetadataRedactionMarker should not be empty")
}
// Neither should contain raw terminal transcript or secret content
if MetadataCredentialReference != "credential_ref" {
t.Fatalf("expected 'credential_ref', got %q", MetadataCredentialReference)
}
if MetadataRedactionMarker != "redaction_marker" {
t.Fatalf("expected 'redaction_marker', got %q", MetadataRedactionMarker)
}
}
func TestRequiredEventTypesMatchMilestoneDraft(t *testing.T) {
t.Parallel()
expected := []struct {
name string
got EventType
want string
}{
{"TypeExecutionRequested", TypeExecutionRequested, "execution.requested"},
{"TypeExecutionPolicyEvaluated", TypeExecutionPolicyEvaluated, "execution.policy_evaluated"},
{"TypeExecutionDispatched", TypeExecutionDispatched, "execution.dispatched"},
{"TypeExecutionStarted", TypeExecutionStarted, "execution.started"},
{"TypeExecutionCompleted", TypeExecutionCompleted, "execution.completed"},
{"TypeExecutionFailed", TypeExecutionFailed, "execution.failed"},
{"TypeExecutionCanceled", TypeExecutionCanceled, "execution.canceled"},
{"TypeTerminalSessionRequested", TypeTerminalSessionRequested, "terminal.session_requested"},
{"TypeTerminalSessionStarted", TypeTerminalSessionStarted, "terminal.session_started"},
{"TypeTerminalCommandSubmitted", TypeTerminalCommandSubmitted, "terminal.command_submitted"},
{"TypeTerminalCommandCompleted", TypeTerminalCommandCompleted, "terminal.command_completed"},
{"TypeTerminalSessionClosed", TypeTerminalSessionClosed, "terminal.session_closed"},
{"TypeBootstrapEnrollmentRequested", TypeBootstrapEnrollmentRequested, "bootstrap.enrollment_requested"},
{"TypeBootstrapCredentialIssued", TypeBootstrapCredentialIssued, "bootstrap.credential_issued"},
{"TypeBootstrapNodeRegistered", TypeBootstrapNodeRegistered, "bootstrap.node_registered"},
{"TypeBootstrapEnrollmentFailed", TypeBootstrapEnrollmentFailed, "bootstrap.enrollment_failed"},
{"TypeBootstrapRevoked", TypeBootstrapRevoked, "bootstrap.revoked"},
}
if len(expected) != 17 {
t.Fatalf("expected 17 event type constants, got %d", len(expected))
}
seen := map[string]string{}
for _, tt := range expected {
if string(tt.got) != tt.want {
t.Fatalf("%s = %q, want %q", tt.name, tt.got, tt.want)
}
if previous, ok := seen[tt.want]; ok {
t.Fatalf("duplicate event type value %q for %s and %s", tt.want, previous, tt.name)
}
seen[tt.want] = tt.name
}
}
func TestSourceConstants(t *testing.T) {
t.Parallel()
expectedSources := []Source{SourceControlPlane, SourceEdge, SourceNode}
for _, s := range expectedSources {
if string(s) == "" {
t.Fatalf("source constant resolved to empty string: %v", s)
}
}
}
func TestPolicyDecisionConstants(t *testing.T) {
t.Parallel()
expectedDecisions := []PolicyDecision{DecisionAllow, DecisionDeny, DecisionWarn}
for _, d := range expectedDecisions {
if string(d) == "" {
t.Fatalf("policy decision constant resolved to empty string: %v", d)
}
}
}
func TestRetentionDefaults(t *testing.T) {
t.Parallel()
if DefaultEdgeLocalRetentionDays != 30 {
t.Fatalf("expected DefaultEdgeLocalRetentionDays == 30, got %d", DefaultEdgeLocalRetentionDays)
}
if DefaultControlPlaneMetadataRetentionDays != 90 {
t.Fatalf("expected DefaultControlPlaneMetadataRetentionDays == 90, got %d", DefaultControlPlaneMetadataRetentionDays)
}
}