Writing test cases
The Step-Builder-Pattern is used for defining the concrete test case. It consists of the following four steps:
- Given-Step: Define mocks for attributes and functions
- When-Step: Specify the
AuthorizationSubscription
- Expect-Step: Define expectations for generated
AuthorizationDecision
- Verify-Step: Verify the generated
AuthorizationDecision
Starting with constructTestCaseWithMocks() or constructTestCase() called on the fixture, the test case definition process is started at the Given-Step or the When-Step.
Given-Step
Mocking of functions:
-
the
givenFunction
methods can be used to mock a function returning aVal
specified in the method parameters for every call. – a single value can be specified1
.givenFunction("time.dayOfWeek", Val.of("SATURDAY"))
– a single value only returned when the parameters of the function call match some expectations
1 2
.givenFunction("corp.subjectConverter", whenFunctionParams(is(Val.of("USER")), is(Val.of("nikolai"))), Val.of("ROLE_ADMIN"))
– or a Lambda-Expression evaluating the parameters of the function call
1 2 3 4 5 6 7 8
.givenFunction("company.complexFunction", (FunctionCall call) -> { //probably one should check for number and type of parameters first Double param0 = call.getArgument(0).get().asDouble(); Double param1 = call.getArgument(1).get().asDouble(); return param0 % param1 == 0 ? Val.of(true) : Val.of(false); })
– and verify the number of calls to this mock
1
.givenFunction("time.dayOfWeek", Val.of("SATURDAY"), times(1))
-
givenFunctionOnce
can specify aVal
or multipleVal
-Objects which are emitted once (in a sequence) when this mocked function is called – a single value1 2
.givenFunctionOnce("time.secondOf", Val.of(4)) .givenFunctionOnce("time.secondOf", Val.of(5))
– or a sequence of values
1
.givenFunctionOnce("time.secondOf", Val.of(3), Val.of(4), Val.of(5))
Mocking of attributes:
-
givenAttribute
methods can mock attributes – to return one or moreVal
1
.givenAttribute("time.now", timestamp0, timestamp1, timestamp2)
– to return a sequence of
Val
in an interval ofDuration
. UsingwithVirtualTime
activates the virtual time feature of Project Reactor1 2
.withVirtualTime() .givenAttribute("time.now", Duration.ofSeconds(10), timestamp0, timestamp1, timestamp2, timestamp3, timestamp4, timestamp5)
The virtual time feature can be used with real time-based PIPs registered the fixture level. Virtual time is “no silver bullet” to cite the Project Reactor Reference Guide. It says further that “[v]irtual time also gets very limited with infinite sequences, which might hog the thread on which both the sequence and its verification run.”
-
to mark an attribute to be mocked and specify return values in a sequence next to expectations
1 2 3 4 5 6 7 8
.givenAttribute("company.pip1") .givenAttribute("company.pip2") .when(AuthorizationSubscription.of("User1", "read", "heartBeatData")) .thenAttribute("company.pip1", Val.of(1)) .thenAttribute("company.pip2", Val.of("foo")) .expectNextPermit() .thenAttribute("company.pip2", Val.of("bar")) .expectNextNotApplicable()
-
to mock an attribute depending on the parent value
1
.givenAttribute("test.upper", whenParentValue(val("willi")), thenReturn(Val.of("WILLI")))
-
to mock an attribute depending on the parent value and every value the arguments are called for
1
.givenAttribute("pip.attributeWithParams", whenAttributeParams(parentValue(val(true)), arguments(val(2), val(2))), thenReturn(Val.of(true)))
Further mock types or overloaded methods are available here.
When-Step
The next defines the AuthorizationSubscription
for the policy evaluation.
-
pass an
AuthorizationSubscription
created by it’s factory methods1
.when(AuthorizationSubscription.of("willi", "read", "something"))
-
pass a JSON-String to be parsed to an
AuthorizationSubscription
via the framework1
.when("{\"subject\":\"willi\", \"action\":\"read\", \"resource\":\"something\", \"environment\":{}}")
-
pass a
JsonNode
object of the Jackson-Framework (Reference)1 2 3 4 5 6 7
JsonNode authzSub = mapper.createObjectNode() .put("subject", "willi") .put("action", "read") .put("resource", "something") .put("environment", "test"); ... .when(authzSub)
Expect-Step
This step defines the expected AuthorizationDecision
.
-
check only the Decision via
1 2 3 4
.expectPermit() .expectDeny() .expectIndeterminate() .expectNotApplicable()
-
pass a
AuthorizationDecision
object to be checked for equality1 2 3 4 5 6 7 8 9 10 11
ObjectNode obligation = mapper.createObjectNode(); obligation.put("type", "logAccess"); obligation.put("message", "Willi has accessed patient data (id=56) as an administrator."); ArrayNode obligations = mapper.createArrayNode(); obligations.add(obligation); AuthorizationDecision decision = new AuthorizationDecision(Decision.PERMIT).withObligations(obligations); ... .expect(decision)
-
use a predicate function to manually define checks
1 2 3 4 5 6 7
.expect((AuthorizationDecision dec) -> { // some complex and custom assertions if(dec.getObligations().isEmpty()) { return true; } return false; })
-
Hamcrest matchers provided by the sapl-hamcrest module can be used to express complex expectations on the decision, obligations, advice or resources of the
AuthorizationDecision
1 2 3 4 5 6 7 8
.expect( allOf( isPermit(), hasObligationContainingKeyValue("type", "logAccess"), isResourceMatching((JsonNode resource) -> resource.get("id").asText().equals("56")) ... ) )
All available
Matcher<AuthorizationDecision>
can be found here
These methods come with additional methods (e.g., expectNextPermit
) to define multiple expectations for testing stream-based policies.
1
.expectNextPermit(3)
More available methods are documented here.
Verify-Step
The verify()
method completes the test definition, and triggers the evaluation of the policy/policies and verifies the expectations.