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 specified.givenFunction("time.dayOfWeek", Val.of("SATURDAY"))
– a single value only returned when the parameters of the function call match some expectations
.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
.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
.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 value.givenFunctionOnce("time.secondOf", Val.of(4)) .givenFunctionOnce("time.secondOf", Val.of(5))
– or a sequence of values
.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
.givenAttribute("time.now", timestamp0, timestamp1, timestamp2)
– to return a sequence of
Val
in an interval ofDuration
. UsingwithVirtualTime
activates the virtual time feature of Project Reactor.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
.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
.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
.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 methods.when(AuthorizationSubscription.of("willi", "read", "something"))
-
pass a JSON-String to be parsed to an
AuthorizationSubscription
via the framework.when("{\"subject\":\"willi\", \"action\":\"read\", \"resource\":\"something\", \"environment\":{}}")
-
pass a
JsonNode
object of the Jackson-Framework (Reference)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
.expectPermit() .expectDeny() .expectIndeterminate() .expectNotApplicable()
-
pass a
AuthorizationDecision
object to be checked for equalityObjectNode 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
.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
.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.
.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.