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

StepBuilderPatternForSaplTest_English



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 a Val specified in the method parameters for every call. – a single value can be specified

    1
    
      .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 a Val or multiple Val-Objects which are emitted once (in a sequence) when this mocked function is called – a single value

    1
    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 more Val

    1
    
      .givenAttribute("time.now", timestamp0, timestamp1, timestamp2)
    

    – to return a sequence of Val in an interval of Duration. Using withVirtualTime activates the virtual time feature of Project Reactor

    1
    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 methods

    1
    
    .when(AuthorizationSubscription.of("willi", "read", "something"))
    
  • pass a JSON-String to be parsed to an AuthorizationSubscription via the framework

    1
    
    .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 equality

    1
    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.