package ect import ( "testing" "time" ) func TestValidateDAG_Root(t *testing.T) { store := NewMemoryLedger() payload := &Payload{ Jti: "jti-001", Wid: "wf-1", Par: []string{}, PolDecision: PolDecisionApproved, } err := ValidateDAG(payload, store, DefaultDAGConfig()) if err != nil { t.Fatal(err) } } func TestValidateDAG_DuplicateJti(t *testing.T) { store := NewMemoryLedger() _, _ = store.Append("dummy-jws", &Payload{Jti: "jti-001", Wid: "wf-1", Par: []string{}, PolDecision: PolDecisionApproved}) payload := &Payload{Jti: "jti-001", Wid: "wf-1", Par: []string{}, PolDecision: PolDecisionApproved} err := ValidateDAG(payload, store, DefaultDAGConfig()) if err == nil { t.Fatal("expected error for duplicate jti") } } func TestValidateDAG_ParentExists(t *testing.T) { store := NewMemoryLedger() _, _ = store.Append("jws1", &Payload{Jti: "jti-001", Wid: "wf-1", Par: []string{}, PolDecision: PolDecisionApproved, Iat: time.Now().Unix() - 60}) payload := &Payload{ Jti: "jti-002", Wid: "wf-1", Par: []string{"jti-001"}, PolDecision: PolDecisionApproved, Iat: time.Now().Unix(), } err := ValidateDAG(payload, store, DefaultDAGConfig()) if err != nil { t.Fatal(err) } } func TestValidateDAG_ParentNotFound(t *testing.T) { store := NewMemoryLedger() payload := &Payload{ Jti: "jti-002", Par: []string{"jti-missing"}, PolDecision: PolDecisionApproved, Iat: time.Now().Unix(), } err := ValidateDAG(payload, store, DefaultDAGConfig()) if err == nil { t.Fatal("expected error when parent not found") } } func TestValidateDAG_DepthLimit(t *testing.T) { store := NewMemoryLedger() now := time.Now().Unix() // Chain: jti-1 -> jti-2 -> jti-3 -> ...; validate with maxAncestorLimit=2 so we exceed it _, _ = store.Append("jws1", &Payload{Jti: "jti-1", Wid: "wf", Par: []string{}, PolDecision: PolDecisionApproved, Iat: now - 100}) _, _ = store.Append("jws2", &Payload{Jti: "jti-2", Wid: "wf", Par: []string{"jti-1"}, PolDecision: PolDecisionApproved, Iat: now - 50}) cfg := DAGConfig{ClockSkewTolerance: DefaultClockSkewTolerance, MaxAncestorLimit: 2} payload := &Payload{Jti: "jti-3", Wid: "wf", Par: []string{"jti-2"}, PolDecision: PolDecisionApproved, Iat: now} err := ValidateDAG(payload, store, cfg) if err == nil { t.Fatal("expected error when ancestor limit exceeded") } } func TestValidateDAG_StoreNil(t *testing.T) { payload := &Payload{Jti: "j1", Par: []string{}, PolDecision: PolDecisionApproved, Iat: time.Now().Unix()} err := ValidateDAG(payload, nil, DefaultDAGConfig()) if err == nil { t.Fatal("expected error when store is nil") } } func TestValidateDAG_TemporalOrdering(t *testing.T) { store := NewMemoryLedger() now := time.Now().Unix() _, _ = store.Append("jws1", &Payload{Jti: "jti-1", Wid: "wf", Par: []string{}, PolDecision: PolDecisionApproved, Iat: now}) // child has iat before parent + skew: parent.iat (now) >= child.iat (now+100) + 30 => invalid payload := &Payload{Jti: "jti-2", Wid: "wf", Par: []string{"jti-1"}, PolDecision: PolDecisionApproved, Iat: now + 100} err := ValidateDAG(payload, store, DAGConfig{ClockSkewTolerance: 30, MaxAncestorLimit: 10000}) if err != nil { t.Fatal(err) } // parent.iat >= child.iat + skew: parent at now+50, child at now+10, skew 30 => 50 >= 40 => invalid _, _ = store.Append("jws2", &Payload{Jti: "jti-1b", Wid: "wf", Par: []string{}, PolDecision: PolDecisionApproved, Iat: now + 50}) payload2 := &Payload{Jti: "jti-2b", Wid: "wf", Par: []string{"jti-1b"}, PolDecision: PolDecisionApproved, Iat: now + 10} err = ValidateDAG(payload2, store, DAGConfig{ClockSkewTolerance: 30, MaxAncestorLimit: 10000}) if err == nil { t.Fatal("expected error when parent not earlier than child") } } func TestValidateDAG_DirectCycle(t *testing.T) { // par contains own jti (direct self-reference) -> parent not found store := NewMemoryLedger() now := time.Now().Unix() payload := &Payload{Jti: "jti-self", Wid: "wf", Par: []string{"jti-self"}, PolDecision: PolDecisionApproved, Iat: now} err := ValidateDAG(payload, store, DefaultDAGConfig()) if err == nil { t.Fatal("expected error for direct cycle (par contains self)") } } func TestValidateDAG_hasCycle_visitedContinue(t *testing.T) { // par has duplicate parent ID so we hit "if _, ok := visited[parentID]; ok { continue }" store := NewMemoryLedger() now := time.Now().Unix() _, _ = store.Append("jws1", &Payload{Jti: "jti-a", Wid: "wf", Par: []string{}, PolDecision: PolDecisionApproved, Iat: now - 10}) payload := &Payload{Jti: "jti-b", Wid: "wf", Par: []string{"jti-a", "jti-a"}, PolDecision: PolDecisionApproved, Iat: now} err := ValidateDAG(payload, store, DefaultDAGConfig()) if err != nil { t.Fatal(err) } } func TestValidateDAG_ParentPolicyRejected_RequiresCompensation(t *testing.T) { store := NewMemoryLedger() now := time.Now().Unix() _, _ = store.Append("jws1", &Payload{Jti: "jti-rej", Wid: "wf", Par: []string{}, Pol: "p", PolDecision: PolDecisionRejected, Iat: now - 60}) payload := &Payload{Jti: "jti-child", Wid: "wf", Par: []string{"jti-rej"}, PolDecision: PolDecisionApproved, Iat: now} err := ValidateDAG(payload, store, DefaultDAGConfig()) if err == nil { t.Fatal("expected error when parent rejected and no compensation") } payload.Ext = map[string]interface{}{"compensation_required": true} err = ValidateDAG(payload, store, DefaultDAGConfig()) if err != nil { t.Fatal(err) } }