959 lines
25 KiB
Go
959 lines
25 KiB
Go
package policy
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/ipfs/go-cid"
|
|
"github.com/ipld/go-ipld-prime"
|
|
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
|
"github.com/ipld/go-ipld-prime/datamodel"
|
|
"github.com/ipld/go-ipld-prime/fluent/qp"
|
|
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
|
"github.com/ipld/go-ipld-prime/node/basicnode"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
|
)
|
|
|
|
func TestMatch(t *testing.T) {
|
|
t.Run("equality", func(t *testing.T) {
|
|
t.Run("string", func(t *testing.T) {
|
|
nd := literal.String("test")
|
|
|
|
pol := MustConstruct(Equal(".", literal.String("test")))
|
|
ok, leaf := pol.Match(nd)
|
|
require.True(t, ok)
|
|
require.Nil(t, leaf)
|
|
|
|
pol = MustConstruct(Equal(".", literal.String("test2")))
|
|
ok, leaf = pol.Match(nd)
|
|
require.False(t, ok)
|
|
require.Equal(t, pol[0], leaf)
|
|
|
|
pol = MustConstruct(Equal(".", literal.Int(138)))
|
|
ok, leaf = pol.Match(nd)
|
|
require.False(t, ok)
|
|
require.Equal(t, pol[0], leaf)
|
|
})
|
|
|
|
t.Run("int", func(t *testing.T) {
|
|
nd := literal.Int(138)
|
|
|
|
pol := MustConstruct(Equal(".", literal.Int(138)))
|
|
ok, leaf := pol.Match(nd)
|
|
require.True(t, ok)
|
|
require.Nil(t, leaf)
|
|
|
|
pol = MustConstruct(Equal(".", literal.Int(1138)))
|
|
ok, leaf = pol.Match(nd)
|
|
require.False(t, ok)
|
|
require.Equal(t, pol[0], leaf)
|
|
|
|
pol = MustConstruct(Equal(".", literal.String("138")))
|
|
ok, leaf = pol.Match(nd)
|
|
require.False(t, ok)
|
|
require.Equal(t, pol[0], leaf)
|
|
})
|
|
|
|
t.Run("float", func(t *testing.T) {
|
|
nd := literal.Float(1.138)
|
|
|
|
pol := MustConstruct(Equal(".", literal.Float(1.138)))
|
|
ok, leaf := pol.Match(nd)
|
|
require.True(t, ok)
|
|
require.Nil(t, leaf)
|
|
|
|
pol = MustConstruct(Equal(".", literal.Float(11.38)))
|
|
ok, leaf = pol.Match(nd)
|
|
require.False(t, ok)
|
|
require.Equal(t, pol[0], leaf)
|
|
|
|
pol = MustConstruct(Equal(".", literal.String("138")))
|
|
ok, leaf = pol.Match(nd)
|
|
require.False(t, ok)
|
|
require.Equal(t, pol[0], leaf)
|
|
})
|
|
|
|
t.Run("IPLD Link", func(t *testing.T) {
|
|
l0 := cidlink.Link{Cid: cid.MustParse("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")}
|
|
l1 := cidlink.Link{Cid: cid.MustParse("bafkreifau35r7vi37tvbvfy3hdwvgb4tlflqf7zcdzeujqcjk3rsphiwte")}
|
|
|
|
nd := literal.Link(l0)
|
|
|
|
pol := MustConstruct(Equal(".", literal.Link(l0)))
|
|
ok, leaf := pol.Match(nd)
|
|
require.True(t, ok)
|
|
require.Nil(t, leaf)
|
|
|
|
pol = MustConstruct(Equal(".", literal.Link(l1)))
|
|
ok, leaf = pol.Match(nd)
|
|
require.False(t, ok)
|
|
require.Equal(t, pol[0], leaf)
|
|
|
|
pol = MustConstruct(Equal(".", literal.String("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")))
|
|
ok, leaf = pol.Match(nd)
|
|
require.False(t, ok)
|
|
require.Equal(t, pol[0], leaf)
|
|
})
|
|
|
|
t.Run("string in map", func(t *testing.T) {
|
|
nd, _ := literal.Map(map[string]any{
|
|
"foo": "bar",
|
|
})
|
|
|
|
pol := MustConstruct(Equal(".foo", literal.String("bar")))
|
|
ok, leaf := pol.Match(nd)
|
|
require.True(t, ok)
|
|
require.Nil(t, leaf)
|
|
|
|
pol = MustConstruct(Equal(".[\"foo\"]", literal.String("bar")))
|
|
ok, leaf = pol.Match(nd)
|
|
require.True(t, ok)
|
|
require.Nil(t, leaf)
|
|
|
|
pol = MustConstruct(Equal(".foo", literal.String("baz")))
|
|
ok, leaf = pol.Match(nd)
|
|
require.False(t, ok)
|
|
require.Equal(t, pol[0], leaf)
|
|
|
|
pol = MustConstruct(Equal(".foobar", literal.String("bar")))
|
|
ok, leaf = pol.Match(nd)
|
|
require.False(t, ok)
|
|
require.Equal(t, pol[0], leaf)
|
|
})
|
|
|
|
t.Run("string in list", func(t *testing.T) {
|
|
nd, _ := literal.List([]any{"foo"})
|
|
|
|
pol := MustConstruct(Equal(".[0]", literal.String("foo")))
|
|
ok, leaf := pol.Match(nd)
|
|
require.True(t, ok)
|
|
require.Nil(t, leaf)
|
|
|
|
pol = MustConstruct(Equal(".[1]", literal.String("foo")))
|
|
ok, leaf = pol.Match(nd)
|
|
require.False(t, ok)
|
|
require.Equal(t, pol[0], leaf)
|
|
})
|
|
})
|
|
|
|
t.Run("inequality", func(t *testing.T) {
|
|
t.Run("gt int", func(t *testing.T) {
|
|
nd := literal.Int(138)
|
|
|
|
pol := MustConstruct(GreaterThan(".", literal.Int(1)))
|
|
ok, leaf := pol.Match(nd)
|
|
require.True(t, ok)
|
|
require.Nil(t, leaf)
|
|
|
|
pol = MustConstruct(GreaterThan(".", literal.Int(138)))
|
|
ok, leaf = pol.Match(nd)
|
|
require.False(t, ok)
|
|
require.Equal(t, pol[0], leaf)
|
|
|
|
pol = MustConstruct(GreaterThan(".", literal.Int(140)))
|
|
ok, leaf = pol.Match(nd)
|
|
require.False(t, ok)
|
|
require.Equal(t, pol[0], leaf)
|
|
})
|
|
|
|
t.Run("gte int", func(t *testing.T) {
|
|
nd := literal.Int(138)
|
|
|
|
pol := MustConstruct(GreaterThanOrEqual(".", literal.Int(1)))
|
|
ok, leaf := pol.Match(nd)
|
|
require.True(t, ok)
|
|
require.Nil(t, leaf)
|
|
|
|
pol = MustConstruct(GreaterThanOrEqual(".", literal.Int(138)))
|
|
ok, leaf = pol.Match(nd)
|
|
require.True(t, ok)
|
|
require.Nil(t, leaf)
|
|
|
|
pol = MustConstruct(GreaterThanOrEqual(".", literal.Int(140)))
|
|
ok, leaf = pol.Match(nd)
|
|
require.False(t, ok)
|
|
require.Equal(t, pol[0], leaf)
|
|
})
|
|
|
|
t.Run("gt float", func(t *testing.T) {
|
|
nd := literal.Float(1.38)
|
|
|
|
pol := MustConstruct(GreaterThan(".", literal.Float(1)))
|
|
ok, leaf := pol.Match(nd)
|
|
require.True(t, ok)
|
|
require.Nil(t, leaf)
|
|
|
|
pol = MustConstruct(GreaterThan(".", literal.Float(2)))
|
|
ok, leaf = pol.Match(nd)
|
|
require.False(t, ok)
|
|
require.Equal(t, pol[0], leaf)
|
|
})
|
|
|
|
t.Run("gte float", func(t *testing.T) {
|
|
nd := literal.Float(1.38)
|
|
|
|
pol := MustConstruct(GreaterThanOrEqual(".", literal.Float(1)))
|
|
ok, leaf := pol.Match(nd)
|
|
require.True(t, ok)
|
|
require.Nil(t, leaf)
|
|
|
|
pol = MustConstruct(GreaterThanOrEqual(".", literal.Float(1.38)))
|
|
ok, leaf = pol.Match(nd)
|
|
require.True(t, ok)
|
|
require.Nil(t, leaf)
|
|
|
|
pol = MustConstruct(GreaterThanOrEqual(".", literal.Float(2)))
|
|
ok, leaf = pol.Match(nd)
|
|
require.False(t, ok)
|
|
require.Equal(t, pol[0], leaf)
|
|
})
|
|
|
|
t.Run("lt int", func(t *testing.T) {
|
|
nd := literal.Int(138)
|
|
|
|
pol := MustConstruct(LessThan(".", literal.Int(1138)))
|
|
ok, leaf := pol.Match(nd)
|
|
require.True(t, ok)
|
|
require.Nil(t, leaf)
|
|
|
|
pol = MustConstruct(LessThan(".", literal.Int(138)))
|
|
ok, leaf = pol.Match(nd)
|
|
require.False(t, ok)
|
|
require.Equal(t, pol[0], leaf)
|
|
|
|
pol = MustConstruct(LessThan(".", literal.Int(100)))
|
|
ok, leaf = pol.Match(nd)
|
|
require.False(t, ok)
|
|
require.Equal(t, pol[0], leaf)
|
|
})
|
|
|
|
t.Run("lte int", func(t *testing.T) {
|
|
nd := literal.Int(138)
|
|
|
|
pol := MustConstruct(LessThanOrEqual(".", literal.Int(1138)))
|
|
ok, leaf := pol.Match(nd)
|
|
require.True(t, ok)
|
|
require.Nil(t, leaf)
|
|
|
|
pol = MustConstruct(LessThanOrEqual(".", literal.Int(138)))
|
|
ok, leaf = pol.Match(nd)
|
|
require.True(t, ok)
|
|
require.Nil(t, leaf)
|
|
|
|
pol = MustConstruct(LessThanOrEqual(".", literal.Int(100)))
|
|
ok, leaf = pol.Match(nd)
|
|
require.False(t, ok)
|
|
require.Equal(t, pol[0], leaf)
|
|
})
|
|
})
|
|
|
|
t.Run("negation", func(t *testing.T) {
|
|
nd := literal.Bool(false)
|
|
|
|
pol := MustConstruct(Not(Equal(".", literal.Bool(true))))
|
|
ok, leaf := pol.Match(nd)
|
|
require.True(t, ok)
|
|
require.Nil(t, leaf)
|
|
|
|
pol = MustConstruct(Not(Equal(".", literal.Bool(false))))
|
|
ok, leaf = pol.Match(nd)
|
|
require.False(t, ok)
|
|
require.Equal(t, pol[0], leaf)
|
|
})
|
|
|
|
t.Run("conjunction", func(t *testing.T) {
|
|
nd := literal.Int(138)
|
|
|
|
pol := MustConstruct(
|
|
And(
|
|
GreaterThan(".", literal.Int(1)),
|
|
LessThan(".", literal.Int(1138)),
|
|
),
|
|
)
|
|
ok, leaf := pol.Match(nd)
|
|
require.True(t, ok)
|
|
require.Nil(t, leaf)
|
|
|
|
pol = MustConstruct(
|
|
And(
|
|
GreaterThan(".", literal.Int(1)),
|
|
Equal(".", literal.Int(1138)),
|
|
),
|
|
)
|
|
ok, leaf = pol.Match(nd)
|
|
require.False(t, ok)
|
|
require.Equal(t, MustConstruct(Equal(".", literal.Int(1138)))[0], leaf)
|
|
|
|
pol = MustConstruct(And())
|
|
ok, leaf = pol.Match(nd)
|
|
require.True(t, ok)
|
|
require.Nil(t, leaf)
|
|
})
|
|
|
|
t.Run("disjunction", func(t *testing.T) {
|
|
nd := literal.Int(138)
|
|
|
|
pol := MustConstruct(
|
|
Or(
|
|
GreaterThan(".", literal.Int(138)),
|
|
LessThan(".", literal.Int(1138)),
|
|
),
|
|
)
|
|
ok, leaf := pol.Match(nd)
|
|
require.True(t, ok)
|
|
require.Nil(t, leaf)
|
|
|
|
pol = MustConstruct(
|
|
Or(
|
|
GreaterThan(".", literal.Int(138)),
|
|
Equal(".", literal.Int(1138)),
|
|
),
|
|
)
|
|
ok, leaf = pol.Match(nd)
|
|
require.False(t, ok)
|
|
require.Equal(t, pol[0], leaf)
|
|
|
|
pol = MustConstruct(Or())
|
|
ok, leaf = pol.Match(nd)
|
|
require.True(t, ok)
|
|
require.Nil(t, leaf)
|
|
})
|
|
|
|
t.Run("wildcard", func(t *testing.T) {
|
|
pattern := `Alice\*, Bob*, Carol.`
|
|
|
|
for _, s := range []string{
|
|
"Alice*, Bob, Carol.",
|
|
"Alice*, Bob, Dan, Erin, Carol.",
|
|
"Alice*, Bob , Carol.",
|
|
"Alice*, Bob*, Carol.",
|
|
} {
|
|
func(s string) {
|
|
t.Run(fmt.Sprintf("pass %s", s), func(t *testing.T) {
|
|
nd := literal.String(s)
|
|
|
|
pol := MustConstruct(Like(".", pattern))
|
|
ok, leaf := pol.Match(nd)
|
|
require.True(t, ok)
|
|
require.Nil(t, leaf)
|
|
})
|
|
}(s)
|
|
}
|
|
|
|
for _, s := range []string{
|
|
"Alice*, Bob, Carol",
|
|
"Alice*, Bob*, Carol!",
|
|
"Alice Cooper, Bob, Carol.",
|
|
"Alice, Bob, Carol.",
|
|
" Alice*, Bob, Carol. ",
|
|
} {
|
|
func(s string) {
|
|
t.Run(fmt.Sprintf("fail %s", s), func(t *testing.T) {
|
|
nd := literal.String(s)
|
|
|
|
pol := MustConstruct(Like(".", pattern))
|
|
ok, leaf := pol.Match(nd)
|
|
require.False(t, ok)
|
|
require.Equal(t, pol[0], leaf)
|
|
})
|
|
}(s)
|
|
}
|
|
})
|
|
|
|
t.Run("quantification", func(t *testing.T) {
|
|
t.Run("all", func(t *testing.T) {
|
|
nd, _ := literal.List([]any{
|
|
map[string]int{"value": 5},
|
|
map[string]int{"value": 10},
|
|
map[string]int{"value": 20},
|
|
map[string]int{"value": 50},
|
|
map[string]int{"value": 100},
|
|
})
|
|
|
|
pol := MustConstruct(All(".[]", GreaterThan(".value", literal.Int(2))))
|
|
ok, leaf := pol.Match(nd)
|
|
require.True(t, ok)
|
|
require.Nil(t, leaf)
|
|
|
|
pol = MustConstruct(All(".[]", GreaterThan(".value", literal.Int(20))))
|
|
ok, leaf = pol.Match(nd)
|
|
require.False(t, ok)
|
|
require.Equal(t, MustConstruct(GreaterThan(".value", literal.Int(20)))[0], leaf)
|
|
})
|
|
|
|
t.Run("any", func(t *testing.T) {
|
|
nd, _ := literal.List([]any{
|
|
map[string]int{"value": 5},
|
|
map[string]int{"value": 10},
|
|
map[string]int{"value": 20},
|
|
map[string]int{"value": 50},
|
|
map[string]int{"value": 100},
|
|
})
|
|
|
|
pol := MustConstruct(Any(".[]", GreaterThan(".value", literal.Int(60))))
|
|
ok, leaf := pol.Match(nd)
|
|
require.True(t, ok)
|
|
require.Nil(t, leaf)
|
|
|
|
pol = MustConstruct(Any(".[]", GreaterThan(".value", literal.Int(100))))
|
|
ok, leaf = pol.Match(nd)
|
|
require.False(t, ok)
|
|
require.Equal(t, pol[0], leaf)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestPolicyExamples(t *testing.T) {
|
|
makeNode := func(data string) ipld.Node {
|
|
nd, err := ipld.Decode([]byte(data), dagjson.Decode)
|
|
require.NoError(t, err)
|
|
return nd
|
|
}
|
|
|
|
evaluate := func(statement string, data ipld.Node) bool {
|
|
// we need to wrap statement with [] to make them a policy
|
|
policy := fmt.Sprintf("[%s]", statement)
|
|
|
|
pol, err := FromDagJson(policy)
|
|
require.NoError(t, err)
|
|
res, _ := pol.Match(data)
|
|
return res
|
|
}
|
|
|
|
t.Run("And", func(t *testing.T) {
|
|
data := makeNode(`{ "name": "Katie", "age": 35, "nationalities": ["Canadian", "South African"] }`)
|
|
|
|
require.True(t, evaluate(`["and", []]`, data))
|
|
require.True(t, evaluate(`
|
|
["and", [
|
|
["==", ".name", "Katie"],
|
|
[">=", ".age", 21]
|
|
]]`, data))
|
|
require.False(t, evaluate(`
|
|
["and", [
|
|
["==", ".name", "Katie"],
|
|
[">=", ".age", 21],
|
|
["==", ".nationalities", ["American"]]
|
|
]]`, data))
|
|
})
|
|
|
|
t.Run("Or", func(t *testing.T) {
|
|
data := makeNode(`{ "name": "Katie", "age": 35, "nationalities": ["Canadian", "South African"] }`)
|
|
|
|
require.True(t, evaluate(`["or", []]`, data))
|
|
require.True(t, evaluate(`
|
|
["or", [
|
|
["==", ".name", "Katie"],
|
|
[">", ".age", 45]
|
|
]]
|
|
`, data))
|
|
|
|
})
|
|
|
|
t.Run("Not", func(t *testing.T) {
|
|
data := makeNode(`{ "name": "Katie", "nationalities": ["Canadian", "South African"] }`)
|
|
|
|
require.True(t, evaluate(`
|
|
["not",
|
|
["and", [
|
|
["==", ".name", "Katie"],
|
|
["==", ".nationalities", ["American"]]
|
|
]]
|
|
]
|
|
`, data))
|
|
})
|
|
|
|
t.Run("All", func(t *testing.T) {
|
|
data := makeNode(`{"a": [{"b": 1}, {"b": 2}, {"z": [7, 8, 9]}]}`)
|
|
|
|
require.False(t, evaluate(`["all", ".a", [">", ".b", 0]]`, data))
|
|
})
|
|
|
|
t.Run("Any", func(t *testing.T) {
|
|
data := makeNode(`{"a": [{"b": 1}, {"b": 2}, {"z": [7, 8, 9]}]}`)
|
|
|
|
require.True(t, evaluate(`["any", ".a", ["==", ".b", 2]]`, data))
|
|
})
|
|
}
|
|
|
|
func FuzzMatch(f *testing.F) {
|
|
// Policy + Data examples
|
|
f.Add([]byte(`[["==", ".status", "draft"]]`), []byte(`{"status": "draft"}`))
|
|
f.Add([]byte(`[["all", ".reviewer", ["like", ".email", "*@example.com"]]]`), []byte(`{"reviewer": [{"email": "alice@example.com"}, {"email": "bob@example.com"}]}`))
|
|
f.Add([]byte(`[["any", ".tags", ["or", [["==", ".", "news"], ["==", ".", "press"]]]]]`), []byte(`{"tags": ["news", "press"]}`))
|
|
f.Add([]byte(`[["==", ".name", "Alice"]]`), []byte(`{"name": "Alice"}`))
|
|
f.Add([]byte(`[[">", ".age", 30]]`), []byte(`{"age": 31}`))
|
|
f.Add([]byte(`[["<=", ".height", 180]]`), []byte(`{"height": 170}`))
|
|
f.Add([]byte(`[["not", ["==", ".status", "inactive"]]]`), []byte(`{"status": "active"}`))
|
|
f.Add([]byte(`[["and", [["==", ".role", "admin"], [">=", ".experience", 5]]]]`), []byte(`{"role": "admin", "experience": 6}`))
|
|
f.Add([]byte(`[["or", [["==", ".department", "HR"], ["==", ".department", "Finance"]]]]`), []byte(`{"department": "HR"}`))
|
|
f.Add([]byte(`[["like", ".email", "*@company.com"]]`), []byte(`{"email": "user@company.com"}`))
|
|
f.Add([]byte(`[["all", ".projects", [">", ".budget", 10000]]]`), []byte(`{"projects": [{"budget": 15000}, {"budget": 8000}]}`))
|
|
f.Add([]byte(`[["any", ".skills", ["==", ".", "Go"]]]`), []byte(`{"skills": ["Go", "Python", "JavaScript"]}`))
|
|
f.Add(
|
|
[]byte(`[["and", [
|
|
["==", ".name", "Bob"],
|
|
["or", [[">", ".age", 25],["==", ".status", "active"]]],
|
|
["all", ".tasks", ["==", ".completed", true]]
|
|
]]]`),
|
|
[]byte(`{
|
|
"name": "Bob",
|
|
"age": 26,
|
|
"status": "active",
|
|
"tasks": [{"completed": true}, {"completed": true}, {"completed": false}]
|
|
}`),
|
|
)
|
|
|
|
f.Fuzz(func(t *testing.T, policyBytes []byte, dataBytes []byte) {
|
|
policyNode, err := ipld.Decode(policyBytes, dagjson.Decode)
|
|
if err != nil {
|
|
t.Skip()
|
|
}
|
|
|
|
dataNode, err := ipld.Decode(dataBytes, dagjson.Decode)
|
|
if err != nil {
|
|
t.Skip()
|
|
}
|
|
|
|
// policy node -> policy object
|
|
policy, err := FromIPLD(policyNode)
|
|
if err != nil {
|
|
t.Skip()
|
|
}
|
|
|
|
_, _ = policy.Match(dataNode)
|
|
})
|
|
}
|
|
|
|
func TestOptionalSelectors(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
policy Policy
|
|
data map[string]any
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "missing optional field returns true",
|
|
policy: MustConstruct(Equal(".field?", literal.String("value"))),
|
|
data: map[string]any{},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "present optional field with matching value returns true",
|
|
policy: MustConstruct(Equal(".field?", literal.String("value"))),
|
|
data: map[string]any{"field": "value"},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "present optional field with non-matching value returns false",
|
|
policy: MustConstruct(Equal(".field?", literal.String("value"))),
|
|
data: map[string]any{"field": "other"},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "missing non-optional field returns false",
|
|
policy: MustConstruct(Equal(".field", literal.String("value"))),
|
|
data: map[string]any{},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "nested missing non-optional field returns false",
|
|
policy: MustConstruct(Equal(".outer?.inner", literal.String("value"))),
|
|
data: map[string]any{"outer": map[string]any{}},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "completely missing nested optional path returns true",
|
|
policy: MustConstruct(Equal(".outer?.inner?", literal.String("value"))),
|
|
data: map[string]any{},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "partially present nested optional path with missing end returns true",
|
|
policy: MustConstruct(Equal(".outer?.inner?", literal.String("value"))),
|
|
data: map[string]any{"outer": map[string]any{}},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "optional array index returns true when array is empty",
|
|
policy: MustConstruct(Equal(".array[0]?", literal.String("value"))),
|
|
data: map[string]any{"array": []any{}},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "non-optional array index returns false when array is empty",
|
|
policy: MustConstruct(Equal(".array[0]", literal.String("value"))),
|
|
data: map[string]any{"array": []any{}},
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
nb := basicnode.Prototype.Map.NewBuilder()
|
|
n, err := literal.Map(tt.data)
|
|
require.NoError(t, err)
|
|
err = nb.AssignNode(n)
|
|
require.NoError(t, err)
|
|
|
|
result, _ := tt.policy.Match(nb.Build())
|
|
require.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
// The unique behaviour of PartialMatch is that it should return true for missing non-optional data (unlike Match).
|
|
func TestPartialMatch(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
policy Policy
|
|
data map[string]any
|
|
expectedMatch bool
|
|
expectedStmt Statement
|
|
}{
|
|
{
|
|
name: "returns true for missing non-optional field",
|
|
policy: MustConstruct(
|
|
Equal(".field", literal.String("value")),
|
|
),
|
|
data: map[string]any{},
|
|
expectedMatch: true,
|
|
expectedStmt: nil,
|
|
},
|
|
{
|
|
name: "returns true when present data matches",
|
|
policy: MustConstruct(
|
|
Equal(".foo", literal.String("correct")),
|
|
Equal(".missing", literal.String("whatever")),
|
|
),
|
|
data: map[string]any{
|
|
"foo": "correct",
|
|
},
|
|
expectedMatch: true,
|
|
expectedStmt: nil,
|
|
},
|
|
{
|
|
name: "returns false with failing statement for present but non-matching value",
|
|
policy: MustConstruct(
|
|
Equal(".foo", literal.String("value1")),
|
|
Equal(".bar", literal.String("value2")),
|
|
),
|
|
data: map[string]any{
|
|
"foo": "wrong",
|
|
"bar": "value2",
|
|
},
|
|
expectedMatch: false,
|
|
expectedStmt: MustConstruct(
|
|
Equal(".foo", literal.String("value1")),
|
|
)[0],
|
|
},
|
|
{
|
|
name: "continues past missing data until finding actual mismatch",
|
|
policy: MustConstruct(
|
|
Equal(".missing", literal.String("value")),
|
|
Equal(".present", literal.String("wrong")),
|
|
),
|
|
data: map[string]any{
|
|
"present": "actual",
|
|
},
|
|
expectedMatch: false,
|
|
expectedStmt: MustConstruct(
|
|
Equal(".present", literal.String("wrong")),
|
|
)[0],
|
|
},
|
|
|
|
// Optional fields
|
|
{
|
|
name: "returns false when optional field present but wrong",
|
|
policy: MustConstruct(
|
|
Equal(".field?", literal.String("value")),
|
|
),
|
|
data: map[string]any{
|
|
"field": "wrong",
|
|
},
|
|
expectedMatch: false,
|
|
expectedStmt: MustConstruct(
|
|
Equal(".field?", literal.String("value")),
|
|
)[0],
|
|
},
|
|
|
|
// Like pattern matching
|
|
{
|
|
name: "returns true for matching like pattern",
|
|
policy: MustConstruct(
|
|
Like(".pattern", "test*"),
|
|
),
|
|
data: map[string]any{
|
|
"pattern": "testing123",
|
|
},
|
|
expectedMatch: true,
|
|
expectedStmt: nil,
|
|
},
|
|
{
|
|
name: "returns false for non-matching like pattern",
|
|
policy: MustConstruct(
|
|
Like(".pattern", "test*"),
|
|
),
|
|
data: map[string]any{
|
|
"pattern": "wrong123",
|
|
},
|
|
expectedMatch: false,
|
|
expectedStmt: MustConstruct(
|
|
Like(".pattern", "test*"),
|
|
)[0],
|
|
},
|
|
|
|
// Array quantifiers
|
|
{
|
|
name: "all matches when every element satisfies condition",
|
|
policy: MustConstruct(
|
|
All(".numbers", Equal(".", literal.Int(1))),
|
|
),
|
|
data: map[string]interface{}{
|
|
"numbers": []interface{}{1, 1, 1},
|
|
},
|
|
expectedMatch: true,
|
|
expectedStmt: nil,
|
|
},
|
|
{
|
|
name: "all fails when any element doesn't satisfy",
|
|
policy: MustConstruct(
|
|
All(".numbers", Equal(".", literal.Int(1))),
|
|
),
|
|
data: map[string]interface{}{
|
|
"numbers": []interface{}{1, 2, 1},
|
|
},
|
|
expectedMatch: false,
|
|
expectedStmt: MustConstruct(
|
|
Equal(".", literal.Int(1)),
|
|
)[0],
|
|
},
|
|
{
|
|
name: "any succeeds when one element matches",
|
|
policy: MustConstruct(
|
|
Any(".numbers", Equal(".", literal.Int(2))),
|
|
),
|
|
data: map[string]interface{}{
|
|
"numbers": []interface{}{1, 2, 3},
|
|
},
|
|
expectedMatch: true,
|
|
expectedStmt: nil,
|
|
},
|
|
{
|
|
name: "any fails when no elements match",
|
|
policy: MustConstruct(
|
|
Any(".numbers", Equal(".", literal.Int(4))),
|
|
),
|
|
data: map[string]interface{}{
|
|
"numbers": []interface{}{1, 2, 3},
|
|
},
|
|
expectedMatch: false,
|
|
expectedStmt: MustConstruct(
|
|
Any(".numbers", Equal(".", literal.Int(4))),
|
|
)[0],
|
|
},
|
|
|
|
// Complex nested case
|
|
{
|
|
name: "complex nested policy",
|
|
policy: MustConstruct(
|
|
And(
|
|
Equal(".required", literal.String("present")),
|
|
Equal(".optional?", literal.String("value")),
|
|
Any(".items",
|
|
And(
|
|
Equal(".name", literal.String("test")),
|
|
Like(".id", "ID*"),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
data: map[string]any{
|
|
"required": "present",
|
|
"items": []any{
|
|
map[string]any{
|
|
"name": "wrong",
|
|
"id": "ID123",
|
|
},
|
|
map[string]any{
|
|
"name": "test",
|
|
"id": "ID456",
|
|
},
|
|
},
|
|
},
|
|
expectedMatch: true,
|
|
expectedStmt: nil,
|
|
},
|
|
|
|
// missing optional values for all the operators
|
|
{
|
|
name: "returns true for missing optional equal",
|
|
policy: MustConstruct(
|
|
Equal(".field?", literal.String("value")),
|
|
),
|
|
data: map[string]any{},
|
|
expectedMatch: true,
|
|
expectedStmt: nil,
|
|
},
|
|
{
|
|
name: "returns true for missing optional like pattern",
|
|
policy: MustConstruct(
|
|
Like(".pattern?", "test*"),
|
|
),
|
|
data: map[string]any{},
|
|
expectedMatch: true,
|
|
expectedStmt: nil,
|
|
},
|
|
{
|
|
name: "returns true for missing optional greater than",
|
|
policy: MustConstruct(
|
|
GreaterThan(".number?", literal.Int(5)),
|
|
),
|
|
data: map[string]any{},
|
|
expectedMatch: true,
|
|
expectedStmt: nil,
|
|
},
|
|
{
|
|
name: "returns true for missing optional less than",
|
|
policy: MustConstruct(
|
|
LessThan(".number?", literal.Int(5)),
|
|
),
|
|
data: map[string]any{},
|
|
expectedMatch: true,
|
|
expectedStmt: nil,
|
|
},
|
|
{
|
|
name: "returns true for missing optional array with all",
|
|
policy: MustConstruct(
|
|
All(".numbers?", Equal(".", literal.Int(1))),
|
|
),
|
|
data: map[string]any{},
|
|
expectedMatch: true,
|
|
expectedStmt: nil,
|
|
},
|
|
{
|
|
name: "returns true for missing optional array with any",
|
|
policy: MustConstruct(
|
|
Any(".numbers?", Equal(".", literal.Int(1))),
|
|
),
|
|
data: map[string]any{},
|
|
expectedMatch: true,
|
|
expectedStmt: nil,
|
|
},
|
|
{
|
|
name: "returns true for complex nested optional paths",
|
|
policy: MustConstruct(
|
|
And(
|
|
Equal(".required", literal.String("present")),
|
|
Any(".optional_array?",
|
|
And(
|
|
Equal(".name?", literal.String("test")),
|
|
Like(".id?", "ID*"),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
data: map[string]any{
|
|
"required": "present",
|
|
},
|
|
expectedMatch: true,
|
|
expectedStmt: nil,
|
|
},
|
|
{
|
|
name: "returns true for partially present nested optional paths",
|
|
policy: MustConstruct(
|
|
And(
|
|
Equal(".required", literal.String("present")),
|
|
Any(".items",
|
|
And(
|
|
Equal(".name", literal.String("test")),
|
|
Like(".optional_id?", "ID*"),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
data: map[string]any{
|
|
"required": "present",
|
|
"items": []any{
|
|
map[string]any{
|
|
"name": "test",
|
|
// optional_id is missing
|
|
},
|
|
},
|
|
},
|
|
expectedMatch: true,
|
|
expectedStmt: nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
node, err := literal.Map(tt.data)
|
|
require.NoError(t, err)
|
|
|
|
match, stmt := tt.policy.PartialMatch(node)
|
|
require.Equal(t, tt.expectedMatch, match)
|
|
if tt.expectedStmt == nil {
|
|
require.Nil(t, stmt)
|
|
} else {
|
|
require.Equal(t, tt.expectedStmt, stmt)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestInvocationValidation applies the example policy to the second
|
|
// example arguments as defined in the [Validation] section of the
|
|
// invocation specification.
|
|
//
|
|
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
|
|
func TestInvocationValidationSpecExamples(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
pol := MustConstruct(
|
|
Equal(".from", literal.String("alice@example.com")),
|
|
Any(".to", Like(".", "*@example.com")),
|
|
)
|
|
|
|
t.Run("with passing args", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
argsNode, err := qp.BuildMap(basicnode.Prototype.Any, 2, func(ma datamodel.MapAssembler) {
|
|
qp.MapEntry(ma, "from", qp.String("alice@example.com"))
|
|
qp.MapEntry(ma, "to", qp.List(2, func(la datamodel.ListAssembler) {
|
|
qp.ListEntry(la, qp.String("bob@example.com"))
|
|
qp.ListEntry(la, qp.String("carol@not.example.com"))
|
|
}))
|
|
qp.MapEntry(ma, "title", qp.String("Coffee"))
|
|
qp.MapEntry(ma, "body", qp.String("Still on for coffee"))
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
exec, stmt := pol.Match(argsNode)
|
|
assert.True(t, exec)
|
|
assert.Nil(t, stmt)
|
|
})
|
|
|
|
t.Run("fails on recipients (second statement)", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
argsNode, err := qp.BuildMap(basicnode.Prototype.Any, 2, func(ma datamodel.MapAssembler) {
|
|
qp.MapEntry(ma, "from", qp.String("alice@example.com"))
|
|
qp.MapEntry(ma, "to", qp.List(2, func(la datamodel.ListAssembler) {
|
|
qp.ListEntry(la, qp.String("bob@null.com"))
|
|
qp.ListEntry(la, qp.String("carol@elsewhere.example.com"))
|
|
}))
|
|
qp.MapEntry(ma, "title", qp.String("Coffee"))
|
|
qp.MapEntry(ma, "body", qp.String("Still on for coffee"))
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
exec, stmt := pol.Match(argsNode)
|
|
assert.False(t, exec)
|
|
assert.NotNil(t, stmt)
|
|
})
|
|
}
|