Test Cases for Rego Polices

618 Views Asked by At

Sorry for my novice question. I have written a rego rule to check for ASV Names, and now I am looking to write a test case for the same. I have looked at sample test cases but has no success in writing for my policy(pasted below). Was wondering how could I get a positive and a failure case for the rule below.

asv_list = {"ASVONE","ASVXYZ"} 
check_asv := { resources[i]: Reason |
    resources:=[resource | data[j] ;
        list := {x| x:=asv_list[_]}
        not(list[data[j].ASV])
        resource:=data[j].Name]
        
    Reason := sprintf("THE ASV - %v being used is not a valid ASV", [data[j].ASV])
}

data = {resource |
    doc = input[i];
        key_ids := [k | doc[k]; startswith(k, "tag."); k != "tag.#"; endswith(k, ".key")]
    resource := {
        doc[k] : doc[replace(k, ".key", ".value")] | key_ids[_] == k
    }
}
1

There are 1 best solutions below

0
On BEST ANSWER

If you're trying to test check_asv your positive and negative test cases will look similar:

  • Define input value
  • Define expected value
  • Assert that check_asv is equal to expected value when evaluated with test input

For example:

test_check_asv_positive {
  expected := {"r1": "..."} 
  fake_input := [{"Name": "foo", ...}]
  check_asv == expected with input as fake_input
}

Before you start writing tests for your rule though, I think you should clarify the logic you're trying to express because there are a few red flags that jump out. I've tried to breakdown the issues I see in the policy below. At the end, I've written out some example test cases.

Format the file to make it easier to read

First of all, just format the Rego so that it's easier to read. I pasted your example into a file, added a package (to make it a valid .rego file) and then ran opa fmt file.rego:

asv_list = {"ASVONE", "ASVXYZ"}

check_asv := {resources[i]: Reason |
        resources := [resource |
                data[j]
                list := {x | x := asv_list[_]}
                not list[data[j].ASV]
                resource := data[j].Name
        ]

        Reason := sprintf("THE ASV - %v being used is not a valid ASV", [data[j].ASV])
}

data = {resource |
        doc = input[i]
        key_ids := [k |
                doc[k]
                startswith(k, "tag.")
                k != "tag.#"
                endswith(k, ".key")
        ]

        resource := {doc[k]: doc[replace(k, ".key", ".value")] |
                key_ids[_] == k
        }
}

This a little bit easier to read.

Do not name rules data

I recommend NOT naming that rule data. data in Rego is a special global variable that refers to state cached inside the policy engine. Any data from outside OPA or generated by rules inside of OPA is accessible under data. Defining a rule named data is allowed but it's confusing. Rename data to something meaningful in the domain. E.g., if these were virtual machine resources, you could name them vm_resources. In this case, I don't have much to go on, so I'm going to rename it to input_docs since doc was used as a variable name:

check_asv := {resources[i]: Reason |
        resources := [resource |
                input_docs[j]
                list := {x | x := asv_list[_]}
                not list[input_docs[j].ASV]
                resource := input_docs[j].Name
        ]

        Reason := sprintf("THE ASV - %v being used is not a valid ASV", [input_docs[j].ASV])
}

input_docs = {resource |
        doc = input[i]
        key_ids := [k |
                doc[k]
                startswith(k, "tag.")
                k != "tag.#"
                endswith(k, ".key")
        ]

        resource := {doc[k]: doc[replace(k, ".key", ".value")] |
                key_ids[_] == k
        }
}

Simplify the input_docs helper rule (which was called data in the original)

At first glance, this rule is doing quite a bit of work, but in reality, it's not. The first line (doc = input[i]) just iterates over each element in input. The rest of the rule is based on each element in input. The second expression, key_ids := [..., computes an array of object keys from the element. The third expression, resources := {doc[k]: ..., constructs a new object by mapping the element.

The first expression can't be simplified much, however, it's better to use := instead of = and since i is not referenced anywhere else we can just use _. The first expression becomes:

doc := input[_]

The second expression computes an array of keys but the filtering is a bit redundant: k cannot be equal to tag.# AND end with .key so the != expression can be removed:

key_ids := [k |
  some k
  doc[k]
  startswith(k, "tag.")
  endswith(k, ".key")
]

At this point we could stop however it's worth noticing that the second and third expression in the rule are both just iterating over the doc object. There's no reason for two separate expressions here. To simplify this rule we can combine them:

input_docs = {resource |        
        doc := input[_]
        resource := {v: x |
                some k
                v := doc[k]
                startswith(k, "tag.")
                endswith(k, ".key")
                x := doc[replace(k, ".key", ".value")]
        }
}

Simplify the check_asv rule

Now that we have a simplified version of the input_docs rule that generates a set of mapped resources we can focus on the check_asv rule. The check_asv rule can be simplified quite a bit:

  1. Multiple iterators in the rule body (i and j) when there should be only one.
  2. Construction of invalid resource list is overly complex.

The rule can be simplified down to the following:

check_asv := {name: reason |
        r := input_docs[_]
        not asv_list[r.ASV]
        name := r.Name
        reason := sprintf("THE ASV - %v being used is not a valid ASV", [r.ASV])
}
  • No need to iterate over input_docs twice (in fact this would be incorrect.)
  • No need for a nested comprehension.
  • No need constructing a set from asv_list, it's already a set.

Putting it all together

At this point the policy looks like this:

asv_list = {"ASVONE", "ASVXYZ"}

check_asv := {name: reason |
        r := input_docs[_]
        not asv_list[r.ASV]
        name := r.Name
        reason := sprintf("THE ASV - %v being used is not a valid ASV", [r.ASV])
}

input_docs = {resource |        
        doc := input[_]
        resource := {v: x |
                some k
                v := doc[k]
                startswith(k, "tag.")
                endswith(k, ".key")
                x := doc[replace(k, ".key", ".value")]
        }
}

To test this policy we can easily write a few test cases:

test_check_asv_positive {
    exp := {
        "dog": "THE ASV - ASVBAD being used is not a valid ASV"
    }
    
    inp := [
       {
           "tag.foo.key": "Name",
           "tag.foo.value": "dog",
           "tag.bar.key": "ASV",
           "tag.bar.value": "ASVBAD"
       }
   ]
    
    check_asv == exp with input as inp
}

test_check_asv_positive_multiple_resources {
    exp := {
        "dog": "THE ASV - ASVBAD being used is not a valid ASV",
        "horse": "THE ASV - ASVBAD2 being used is not a valid ASV"
    }
    
    inp := [
       {
           "tag.foo.key": "Name",
           "tag.foo.value": "dog",
           "tag.bar.key": "ASV",
           "tag.bar.value": "ASVBAD"
       },
       {
           "tag.foo.key": "Name",
           "tag.foo.value": "horse",
           "tag.bar.key": "ASV",
           "tag.bar.value": "ASVBAD2"
       }
   ]
    
    check_asv == exp with input as inp
}


test_check_asv_positive_multiple_resources_mixed {
    exp := {
        "horse": "THE ASV - ASVBAD2 being used is not a valid ASV"
    }
    
    inp := [
       {
           "tag.foo.key": "Name",
           "tag.foo.value": "cat",
           "tag.bar.key": "ASV",
           "tag.bar.value": "ASVONE"
       },
       {
           "tag.foo.key": "Name",
           "tag.foo.value": "horse",
           "tag.bar.key": "ASV",
           "tag.bar.value": "ASVBAD2"
       }
   ]
    
    check_asv == exp with input as inp
}


test_check_asv_negative {
    exp := {}
    
    inp := [
       {
           "tag.foo.key": "Name",
           "tag.foo.value": "cat",
           "tag.bar.key": "ASV",
           "tag.bar.value": "ASVONE"
       }
   ]
    
    check_asv == exp with input as inp
}

Here's a link to the full policy in the playground: https://play.openpolicyagent.org/p/6w7aC9xWYH