mirror of
https://github.com/hapifhir/hapi-fhir.git
synced 2025-03-09 14:33:32 +00:00
Add ChainedDelegateConsentService with pluggable vote strategy
Introduce some plumbing utils to combine consent votes.
This commit is contained in:
parent
6a365f8722
commit
239bf8d441
@ -0,0 +1,4 @@
|
||||
---
|
||||
type: add
|
||||
issue: 6366
|
||||
title: "Add plumbing for combining IConsentServices with different vote tally strategies"
|
@ -21,7 +21,7 @@ package ca.uhn.fhir.rest.server.interceptor.consent;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public enum ConsentOperationStatusEnum {
|
||||
public enum ConsentOperationStatusEnum implements IConsentVote {
|
||||
|
||||
/**
|
||||
* The requested operation cannot proceed, and an operation outcome suitable for
|
||||
@ -59,53 +59,52 @@ public enum ConsentOperationStatusEnum {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this vote abstain from the verdict?
|
||||
* I.e. this == PROCEED
|
||||
* @return true if this vote can be ignored
|
||||
*/
|
||||
boolean isAbstain() {
|
||||
return this == PROCEED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this vote participate from the verdict?
|
||||
* I.e. this != PROCEED
|
||||
* @return false if this vote can be ignored
|
||||
*/
|
||||
boolean isActiveVote() {
|
||||
return this != PROCEED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConsentOperationStatusEnum getStatus() {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate verdicts in order, taking the first "decision" (i.e. first non-PROCEED) verdict.
|
||||
*
|
||||
* @return the first decisive verdict, or PROCEED when empty or all PROCEED.
|
||||
*/
|
||||
public static ConsentOperationStatusEnum serialReduce(Stream<ConsentOperationStatusEnum> theVoteStream) {
|
||||
return IConsentVote.serialReduce(PROCEED, theVoteStream);
|
||||
}
|
||||
|
||||
public static ConsentOperationStatusEnum parallelReduce(Stream<ConsentOperationStatusEnum> theVoteStream) {
|
||||
return IConsentVote.parallelReduce(PROCEED, theVoteStream);
|
||||
}
|
||||
|
||||
/** @deprecated for rename */
|
||||
@Deprecated(forRemoval = true)
|
||||
public static ConsentOperationStatusEnum serialEvaluate(Stream<ConsentOperationStatusEnum> theVoteStream) {
|
||||
return theVoteStream.filter(verdict -> PROCEED != verdict).findFirst().orElse(PROCEED);
|
||||
return serialReduce(theVoteStream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate verdicts in order, taking the first "decision" (i.e. first non-PROCEED) verdict.
|
||||
*
|
||||
* @param theNextVerdict the next verdict to consider
|
||||
* @return the combined verdict
|
||||
*/
|
||||
public ConsentOperationStatusEnum serialReduce(ConsentOperationStatusEnum theNextVerdict) {
|
||||
if (this != PROCEED) {
|
||||
return this;
|
||||
} else {
|
||||
return theNextVerdict;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate all verdicts together, allowing any to veto (i.e. REJECT) the operation.
|
||||
* <ul>
|
||||
* <li>If any vote is REJECT, then the result is REJECT.
|
||||
* <li>If no vote is REJECT, and any vote is AUTHORIZED, then the result is AUTHORIZED.
|
||||
* <li>If no vote is REJECT or AUTHORIZED, the result is PROCEED.
|
||||
* </ul>
|
||||
*
|
||||
* @return REJECT if any reject, AUTHORIZED if no REJECT and some AUTHORIZED, PROCEED if empty or all PROCEED
|
||||
*/
|
||||
/** @deprecated for rename */
|
||||
@Deprecated(forRemoval = true)
|
||||
public static ConsentOperationStatusEnum parallelEvaluate(Stream<ConsentOperationStatusEnum> theVoteStream) {
|
||||
return theVoteStream.reduce(PROCEED, ConsentOperationStatusEnum::parallelReduce);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate two verdicts together, allowing either to veto (i.e. REJECT) the operation.
|
||||
*
|
||||
* @return REJECT if either reject, AUTHORIZED if no REJECT and some AUTHORIZED, PROCEED otherwise
|
||||
*/
|
||||
public ConsentOperationStatusEnum parallelReduce(ConsentOperationStatusEnum theNextVerdict) {
|
||||
if (theNextVerdict.getPrecedence() > this.getPrecedence()) {
|
||||
return theNextVerdict;
|
||||
} else {
|
||||
return this;
|
||||
}
|
||||
return parallelReduce(theVoteStream);
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,9 @@ import org.apache.commons.lang3.Validate;
|
||||
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
public class ConsentOutcome {
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class ConsentOutcome implements IConsentVote {
|
||||
|
||||
/**
|
||||
* Convenience constant containing <code>new ConsentOutcome(ConsentOperationStatusEnum.PROCEED)</code>
|
||||
@ -67,6 +69,29 @@ public class ConsentOutcome {
|
||||
myResource = theResource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate all verdicts together, allowing any to veto (i.e. REJECT) the operation.
|
||||
* <ul>
|
||||
* <li>If any vote is REJECT, then the result is a REJECT vote.
|
||||
* <li>If no vote is REJECT, and any vote is AUTHORIZED, then the result is one of the AUTHORIZED votes.
|
||||
* <li>If no vote is REJECT or AUTHORIZED, the result is a PROCEED vote.
|
||||
* </ul>
|
||||
*
|
||||
* @return REJECT if any reject, AUTHORIZED if no REJECT and some AUTHORIZED, PROCEED if empty or all PROCEED
|
||||
*/
|
||||
public static ConsentOutcome parallelReduce(Stream<ConsentOutcome> theOutcomes) {
|
||||
return IConsentVote.parallelReduce(ConsentOutcome.PROCEED, theOutcomes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate verdicts in order, taking the first "decision" (i.e. first non-PROCEED) verdict.
|
||||
*
|
||||
* @return the first decisive verdict, or theSeed when empty or all PROCEED.
|
||||
*/
|
||||
public static ConsentOutcome serialReduce(Stream<ConsentOutcome> theStream) {
|
||||
return IConsentVote.serialReduce(ConsentOutcome.PROCEED, theStream);
|
||||
}
|
||||
|
||||
public ConsentOperationStatusEnum getStatus() {
|
||||
return myStatus;
|
||||
}
|
||||
|
@ -0,0 +1,48 @@
|
||||
package ca.uhn.fhir.rest.server.interceptor.consent;
|
||||
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import jakarta.annotation.Nonnull;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
/**
|
||||
* Consent Service that returns a fixed verdict.
|
||||
*/
|
||||
public class ConstantConsentService implements IConsentService {
|
||||
@Nonnull
|
||||
final ConsentOutcome myResult;
|
||||
|
||||
public static ConstantConsentService constantService(ConsentOperationStatusEnum theResult) {
|
||||
return new ConstantConsentService(new ConsentOutcome(theResult));
|
||||
}
|
||||
|
||||
public ConstantConsentService(@Nonnull ConsentOutcome theResult) {
|
||||
myResult = theResult;
|
||||
}
|
||||
|
||||
private @Nonnull ConsentOutcome getOutcome() {
|
||||
return myResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConsentOutcome startOperation(RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
|
||||
return getOutcome();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldProcessCanSeeResource(
|
||||
RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
|
||||
return myResult.getStatus().isActiveVote();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConsentOutcome canSeeResource(
|
||||
RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
|
||||
return getOutcome();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConsentOutcome willSeeResource(
|
||||
RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
|
||||
return getOutcome();
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
package ca.uhn.fhir.rest.server.interceptor.consent;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Something that produces a vote, along with static
|
||||
* tools for combining votes.
|
||||
*/
|
||||
public interface IConsentVote {
|
||||
/**
|
||||
* Get the vote
|
||||
* @return the vote
|
||||
*/
|
||||
ConsentOperationStatusEnum getStatus();
|
||||
|
||||
/**
|
||||
* Evaluate all verdicts together, allowing any to veto (i.e. REJECT) the operation.
|
||||
* <ul>
|
||||
* <li>If any vote is REJECT, then the result is REJECT.
|
||||
* <li>If no vote is REJECT, and any vote is AUTHORIZED, then the result is AUTHORIZED.
|
||||
* <li>If no vote is REJECT or AUTHORIZED, the result is PROCEED.
|
||||
* </ul>
|
||||
*
|
||||
* @return REJECT if any reject, AUTHORIZED if no REJECT and some AUTHORIZED, PROCEED if empty or all PROCEED
|
||||
*/
|
||||
static <T extends IConsentVote> T parallelReduce(T theSeed, Stream<T> theVoteStream) {
|
||||
return theVoteStream.reduce(theSeed, IConsentVote::parallelReduce);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate two votes together, allowing either to veto (i.e. REJECT) the operation.
|
||||
*
|
||||
* @return REJECT if either reject, AUTHORIZED if no REJECT and some AUTHORIZED, PROCEED otherwise
|
||||
*/
|
||||
static <T extends IConsentVote> T parallelReduce(T theAccumulator, T theNextVoter) {
|
||||
if (theNextVoter.getStatus().getPrecedence()
|
||||
< theAccumulator.getStatus().getPrecedence()) {
|
||||
return theAccumulator;
|
||||
} else {
|
||||
return theNextVoter;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate verdicts in order, taking the first "decision" (i.e. first non-PROCEED) verdict.
|
||||
*
|
||||
* @return the first decisive verdict, or theSeed when empty or all PROCEED.
|
||||
*/
|
||||
static <T extends IConsentVote> T serialReduce(T theSeed, Stream<T> theVoterStream) {
|
||||
return theVoterStream.filter(IConsentVote::isActiveVote).findFirst().orElse(theSeed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate verdicts in order, taking the first "decision" (i.e. first non-PROCEED) verdict.
|
||||
*
|
||||
* @param theAccumulator the verdict so fat
|
||||
* @param theNextVoter the next verdict to consider
|
||||
* @return the combined verdict
|
||||
*/
|
||||
static <T extends IConsentVote> T serialReduce(T theAccumulator, T theNextVoter) {
|
||||
if (theAccumulator.getStatus().isAbstain()) {
|
||||
return theNextVoter;
|
||||
} else {
|
||||
return theAccumulator;
|
||||
}
|
||||
}
|
||||
|
||||
private static <T extends IConsentVote> boolean isActiveVote(T nextVoter) {
|
||||
return nextVoter.getStatus().isActiveVote();
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
package ca.uhn.fhir.rest.server.interceptor.consent;
|
||||
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import jakarta.annotation.Nonnull;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* IConsentService combiner over several delegates with pluggable combination strategy
|
||||
*/
|
||||
public class MultiDelegateConsentService implements IConsentService {
|
||||
private final Collection<IConsentService> myDelegates;
|
||||
private final Function<Stream<ConsentOutcome>, ConsentOutcome> myVoteCombiner;
|
||||
|
||||
/**
|
||||
* Combine several consent services allowing any to veto.
|
||||
*/
|
||||
public static @Nonnull MultiDelegateConsentService withParallelVoting(
|
||||
@Nonnull List<IConsentService> theDelegateConsentServices) {
|
||||
return new MultiDelegateConsentService(ConsentOutcome::parallelReduce, theDelegateConsentServices);
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine several consent services with first non-PROCEED vote win.
|
||||
*/
|
||||
public static @Nonnull MultiDelegateConsentService withSerialVoting(
|
||||
@Nonnull List<IConsentService> theDelegateConsentServices) {
|
||||
return new MultiDelegateConsentService(ConsentOutcome::serialReduce, theDelegateConsentServices);
|
||||
}
|
||||
|
||||
private MultiDelegateConsentService(
|
||||
Function<Stream<ConsentOutcome>, ConsentOutcome> theVoteCombiner,
|
||||
Collection<IConsentService> theDelegates) {
|
||||
myVoteCombiner = theVoteCombiner;
|
||||
myDelegates = theDelegates;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConsentOutcome startOperation(RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
|
||||
return myVoteCombiner.apply(myDelegates.stream()
|
||||
.map(nextDelegate -> nextDelegate.startOperation(theRequestDetails, theContextServices)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if any of the delegates return true.
|
||||
*/
|
||||
@Override
|
||||
public boolean shouldProcessCanSeeResource(
|
||||
RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
|
||||
return myDelegates.stream()
|
||||
.map(nextDelegate -> nextDelegate.shouldProcessCanSeeResource(theRequestDetails, theContextServices))
|
||||
.filter(nextShould -> nextShould)
|
||||
.findFirst()
|
||||
.orElse(Boolean.FALSE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConsentOutcome canSeeResource(
|
||||
RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
|
||||
return myVoteCombiner.apply(myDelegates.stream()
|
||||
.map(nextDelegate -> nextDelegate.canSeeResource(theRequestDetails, theResource, theContextServices)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConsentOutcome willSeeResource(
|
||||
RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
|
||||
return myVoteCombiner.apply(myDelegates.stream()
|
||||
.map(nextDelegate -> nextDelegate.willSeeResource(theRequestDetails, theResource, theContextServices)));
|
||||
}
|
||||
}
|
@ -1,130 +1,14 @@
|
||||
package ca.uhn.fhir.rest.server.interceptor.consent;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static ca.uhn.fhir.rest.server.interceptor.consent.ConsentOperationStatusEnum.AUTHORIZED;
|
||||
import static ca.uhn.fhir.rest.server.interceptor.consent.ConsentOperationStatusEnum.PROCEED;
|
||||
import static ca.uhn.fhir.rest.server.interceptor.consent.ConsentOperationStatusEnum.REJECT;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ConsentOperationStatusEnumTest {
|
||||
|
||||
/**
|
||||
* With "serial" evaluation, the first non-PROCEED verdict wins.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@CsvSource(textBlock = """
|
||||
REJECT REJECT REJECT , REJECT
|
||||
REJECT REJECT PROCEED , REJECT
|
||||
REJECT REJECT AUTHORIZED, REJECT
|
||||
REJECT PROCEED REJECT , REJECT
|
||||
REJECT PROCEED PROCEED , REJECT
|
||||
REJECT PROCEED AUTHORIZED, REJECT
|
||||
REJECT AUTHORIZED REJECT , REJECT
|
||||
REJECT AUTHORIZED PROCEED , REJECT
|
||||
REJECT AUTHORIZED AUTHORIZED, REJECT
|
||||
PROCEED REJECT REJECT , REJECT
|
||||
PROCEED REJECT PROCEED , REJECT
|
||||
PROCEED REJECT AUTHORIZED, REJECT
|
||||
PROCEED PROCEED REJECT , REJECT
|
||||
PROCEED PROCEED PROCEED , PROCEED
|
||||
PROCEED PROCEED AUTHORIZED, AUTHORIZED
|
||||
PROCEED AUTHORIZED REJECT , AUTHORIZED
|
||||
PROCEED AUTHORIZED PROCEED , AUTHORIZED
|
||||
PROCEED AUTHORIZED AUTHORIZED, AUTHORIZED
|
||||
AUTHORIZED REJECT REJECT , AUTHORIZED
|
||||
AUTHORIZED REJECT PROCEED , AUTHORIZED
|
||||
AUTHORIZED REJECT AUTHORIZED, AUTHORIZED
|
||||
AUTHORIZED PROCEED REJECT , AUTHORIZED
|
||||
AUTHORIZED PROCEED PROCEED , AUTHORIZED
|
||||
AUTHORIZED PROCEED AUTHORIZED, AUTHORIZED
|
||||
AUTHORIZED AUTHORIZED REJECT , AUTHORIZED
|
||||
AUTHORIZED AUTHORIZED PROCEED , AUTHORIZED
|
||||
AUTHORIZED AUTHORIZED AUTHORIZED, AUTHORIZED
|
||||
""")
|
||||
void testSerialEvaluation_choosesFirstVerdict(String theInput, ConsentOperationStatusEnum theExpectedResult) {
|
||||
// given
|
||||
Stream<ConsentOperationStatusEnum> consentOperationStatusEnumStream = Arrays.stream(theInput.split(" +"))
|
||||
.map(String::trim)
|
||||
.map(ConsentOperationStatusEnum::valueOf);
|
||||
|
||||
// when
|
||||
ConsentOperationStatusEnum result = ConsentOperationStatusEnum.serialEvaluate(consentOperationStatusEnumStream);
|
||||
|
||||
assertEquals(theExpectedResult, result);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(textBlock = """
|
||||
REJECT , REJECT , REJECT
|
||||
REJECT , PROCEED , REJECT
|
||||
REJECT , AUTHORIZED, REJECT
|
||||
AUTHORIZED, REJECT , AUTHORIZED
|
||||
AUTHORIZED, PROCEED , AUTHORIZED
|
||||
AUTHORIZED, AUTHORIZED, AUTHORIZED
|
||||
PROCEED , REJECT , REJECT
|
||||
PROCEED , PROCEED , PROCEED
|
||||
PROCEED , AUTHORIZED, AUTHORIZED
|
||||
""")
|
||||
void testSerialReduction_choosesFirstVerdict(ConsentOperationStatusEnum theFirst, ConsentOperationStatusEnum theSecond, ConsentOperationStatusEnum theExpectedResult) {
|
||||
|
||||
// when
|
||||
ConsentOperationStatusEnum result = theFirst.serialReduce(theSecond);
|
||||
|
||||
assertEquals(theExpectedResult, result);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* With "parallel" evaluation, the "strongest" verdict wins.
|
||||
* REJECT > AUTHORIZED > PROCEED.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@CsvSource(textBlock = """
|
||||
REJECT REJECT REJECT , REJECT
|
||||
REJECT REJECT PROCEED , REJECT
|
||||
REJECT REJECT AUTHORIZED, REJECT
|
||||
REJECT PROCEED REJECT , REJECT
|
||||
REJECT PROCEED PROCEED , REJECT
|
||||
REJECT PROCEED AUTHORIZED, REJECT
|
||||
REJECT AUTHORIZED REJECT , REJECT
|
||||
REJECT AUTHORIZED PROCEED , REJECT
|
||||
REJECT AUTHORIZED AUTHORIZED, REJECT
|
||||
PROCEED REJECT REJECT , REJECT
|
||||
PROCEED REJECT PROCEED , REJECT
|
||||
PROCEED REJECT AUTHORIZED, REJECT
|
||||
PROCEED PROCEED REJECT , REJECT
|
||||
PROCEED PROCEED PROCEED , PROCEED
|
||||
PROCEED PROCEED AUTHORIZED, AUTHORIZED
|
||||
PROCEED AUTHORIZED REJECT , REJECT
|
||||
PROCEED AUTHORIZED PROCEED , AUTHORIZED
|
||||
PROCEED AUTHORIZED AUTHORIZED, AUTHORIZED
|
||||
AUTHORIZED REJECT REJECT , REJECT
|
||||
AUTHORIZED REJECT PROCEED , REJECT
|
||||
AUTHORIZED REJECT AUTHORIZED, REJECT
|
||||
AUTHORIZED PROCEED REJECT , REJECT
|
||||
AUTHORIZED PROCEED PROCEED , AUTHORIZED
|
||||
AUTHORIZED PROCEED AUTHORIZED, AUTHORIZED
|
||||
AUTHORIZED AUTHORIZED REJECT , REJECT
|
||||
AUTHORIZED AUTHORIZED PROCEED , AUTHORIZED
|
||||
AUTHORIZED AUTHORIZED AUTHORIZED, AUTHORIZED
|
||||
""")
|
||||
void testParallelReduction_strongestVerdictWins(String theInput, ConsentOperationStatusEnum theExpectedResult) {
|
||||
// given
|
||||
Stream<ConsentOperationStatusEnum> consentOperationStatusEnumStream = Arrays.stream(theInput.split(" +"))
|
||||
.map(String::trim)
|
||||
.map(ConsentOperationStatusEnum::valueOf);
|
||||
|
||||
// when
|
||||
ConsentOperationStatusEnum result = ConsentOperationStatusEnum.parallelEvaluate(consentOperationStatusEnumStream);
|
||||
|
||||
assertEquals(theExpectedResult, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStrengthOrder() {
|
||||
|
@ -0,0 +1,70 @@
|
||||
package ca.uhn.fhir.rest.server.interceptor.consent;
|
||||
|
||||
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
|
||||
import static ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices.NULL_IMPL;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class ConstantConsentServiceTest {
|
||||
SystemRequestDetails mySrd = new SystemRequestDetails();
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(ConsentOperationStatusEnum.class)
|
||||
void testStartOperation(ConsentOperationStatusEnum theStatus) {
|
||||
// given
|
||||
var svc = ConstantConsentService.constantService(theStatus);
|
||||
|
||||
// when
|
||||
var outcome = svc.startOperation(mySrd,NULL_IMPL);
|
||||
|
||||
// then
|
||||
assertEquals(outcome.getStatus(), theStatus);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* There's no point calling canSee if we return PROCEED.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@EnumSource(ConsentOperationStatusEnum.class)
|
||||
void testShouldProcessCanSeeResource(ConsentOperationStatusEnum theStatus) {
|
||||
// given
|
||||
var svc = ConstantConsentService.constantService(theStatus);
|
||||
|
||||
// when
|
||||
boolean outcome = svc.shouldProcessCanSeeResource(mySrd,NULL_IMPL);
|
||||
|
||||
// then there's no point calling canSee if we return PROCEED.
|
||||
boolean isNotAbstain = theStatus != ConsentOperationStatusEnum.PROCEED;
|
||||
assertEquals(outcome, isNotAbstain);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(ConsentOperationStatusEnum.class)
|
||||
void testCanSeeResource(ConsentOperationStatusEnum theStatus) {
|
||||
// given
|
||||
var svc = ConstantConsentService.constantService(theStatus);
|
||||
|
||||
// when
|
||||
var outcome = svc.canSeeResource(mySrd, null, NULL_IMPL);
|
||||
|
||||
// then
|
||||
assertEquals(outcome.getStatus(), theStatus);
|
||||
}
|
||||
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(ConsentOperationStatusEnum.class)
|
||||
void testWillSeeResource(ConsentOperationStatusEnum theStatus) {
|
||||
// given
|
||||
var svc = ConstantConsentService.constantService(theStatus);
|
||||
|
||||
// when
|
||||
var outcome = svc.willSeeResource(mySrd, null, NULL_IMPL);
|
||||
|
||||
// then
|
||||
assertEquals(outcome.getStatus(), theStatus);
|
||||
}
|
||||
}
|
@ -0,0 +1,156 @@
|
||||
package ca.uhn.fhir.rest.server.interceptor.consent;
|
||||
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
import jakarta.annotation.Nonnull;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class IConsentVoteTest {
|
||||
|
||||
/** col1: stream of votes, col2: expected verdict */
|
||||
public static final String SERIAL_STREAM_EXPECTATION = """
|
||||
REJECT REJECT REJECT , REJECT
|
||||
REJECT REJECT PROCEED , REJECT
|
||||
REJECT REJECT AUTHORIZED, REJECT
|
||||
REJECT PROCEED REJECT , REJECT
|
||||
REJECT PROCEED PROCEED , REJECT
|
||||
REJECT PROCEED AUTHORIZED, REJECT
|
||||
REJECT AUTHORIZED REJECT , REJECT
|
||||
REJECT AUTHORIZED PROCEED , REJECT
|
||||
REJECT AUTHORIZED AUTHORIZED, REJECT
|
||||
PROCEED REJECT REJECT , REJECT
|
||||
PROCEED REJECT PROCEED , REJECT
|
||||
PROCEED REJECT AUTHORIZED, REJECT
|
||||
PROCEED PROCEED REJECT , REJECT
|
||||
PROCEED PROCEED PROCEED , PROCEED
|
||||
PROCEED PROCEED AUTHORIZED, AUTHORIZED
|
||||
PROCEED AUTHORIZED REJECT , AUTHORIZED
|
||||
PROCEED AUTHORIZED PROCEED , AUTHORIZED
|
||||
PROCEED AUTHORIZED AUTHORIZED, AUTHORIZED
|
||||
AUTHORIZED REJECT REJECT , AUTHORIZED
|
||||
AUTHORIZED REJECT PROCEED , AUTHORIZED
|
||||
AUTHORIZED REJECT AUTHORIZED, AUTHORIZED
|
||||
AUTHORIZED PROCEED REJECT , AUTHORIZED
|
||||
AUTHORIZED PROCEED PROCEED , AUTHORIZED
|
||||
AUTHORIZED PROCEED AUTHORIZED, AUTHORIZED
|
||||
AUTHORIZED AUTHORIZED REJECT , AUTHORIZED
|
||||
AUTHORIZED AUTHORIZED PROCEED , AUTHORIZED
|
||||
AUTHORIZED AUTHORIZED AUTHORIZED, AUTHORIZED
|
||||
""";
|
||||
|
||||
/** col1: stream of votes, col2: expected verdict */
|
||||
public static final String PARALLEL_STREAM_EXPECTATION = """
|
||||
REJECT REJECT REJECT , REJECT
|
||||
REJECT REJECT PROCEED , REJECT
|
||||
REJECT REJECT AUTHORIZED, REJECT
|
||||
REJECT PROCEED REJECT , REJECT
|
||||
REJECT PROCEED PROCEED , REJECT
|
||||
REJECT PROCEED AUTHORIZED, REJECT
|
||||
REJECT AUTHORIZED REJECT , REJECT
|
||||
REJECT AUTHORIZED PROCEED , REJECT
|
||||
REJECT AUTHORIZED AUTHORIZED, REJECT
|
||||
PROCEED REJECT REJECT , REJECT
|
||||
PROCEED REJECT PROCEED , REJECT
|
||||
PROCEED REJECT AUTHORIZED, REJECT
|
||||
PROCEED PROCEED REJECT , REJECT
|
||||
PROCEED PROCEED PROCEED , PROCEED
|
||||
PROCEED PROCEED AUTHORIZED, AUTHORIZED
|
||||
PROCEED AUTHORIZED REJECT , REJECT
|
||||
PROCEED AUTHORIZED PROCEED , AUTHORIZED
|
||||
PROCEED AUTHORIZED AUTHORIZED, AUTHORIZED
|
||||
AUTHORIZED REJECT REJECT , REJECT
|
||||
AUTHORIZED REJECT PROCEED , REJECT
|
||||
AUTHORIZED REJECT AUTHORIZED, REJECT
|
||||
AUTHORIZED PROCEED REJECT , REJECT
|
||||
AUTHORIZED PROCEED PROCEED , AUTHORIZED
|
||||
AUTHORIZED PROCEED AUTHORIZED, AUTHORIZED
|
||||
AUTHORIZED AUTHORIZED REJECT , REJECT
|
||||
AUTHORIZED AUTHORIZED PROCEED , AUTHORIZED
|
||||
AUTHORIZED AUTHORIZED AUTHORIZED, AUTHORIZED
|
||||
""";
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(textBlock = """
|
||||
REJECT , REJECT , REJECT
|
||||
REJECT , PROCEED , REJECT
|
||||
REJECT , AUTHORIZED, REJECT
|
||||
AUTHORIZED, REJECT , AUTHORIZED
|
||||
AUTHORIZED, PROCEED , AUTHORIZED
|
||||
AUTHORIZED, AUTHORIZED, AUTHORIZED
|
||||
PROCEED , REJECT , REJECT
|
||||
PROCEED , PROCEED , PROCEED
|
||||
PROCEED , AUTHORIZED, AUTHORIZED
|
||||
""")
|
||||
void testSerialReduction_choosesFirstVerdict(ConsentOperationStatusEnum theFirst, ConsentOperationStatusEnum theSecond, ConsentOperationStatusEnum theExpectedResult) {
|
||||
|
||||
// when
|
||||
ConsentOperationStatusEnum result = IConsentVote.serialReduce(theFirst, theSecond);
|
||||
|
||||
assertEquals(theExpectedResult, result);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* With "serial" evaluation, the first non-PROCEED verdict wins.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@CsvSource(textBlock = SERIAL_STREAM_EXPECTATION)
|
||||
void testSerialStreamReduction_choosesFirstVerdict(String theInput, ConsentOperationStatusEnum theExpectedResult) {
|
||||
// given
|
||||
Stream<ConsentOperationStatusEnum> consentOperationStatusEnumStream = splitEnumsToStream(theInput);
|
||||
|
||||
// when
|
||||
ConsentOperationStatusEnum result = ConsentOperationStatusEnum.serialReduce(consentOperationStatusEnumStream);
|
||||
|
||||
assertEquals(theExpectedResult, result);
|
||||
}
|
||||
|
||||
static @Nonnull Stream<ConsentOperationStatusEnum> splitEnumsToStream(String theInput) {
|
||||
return Arrays.stream(theInput.split(" +"))
|
||||
.map(String::trim)
|
||||
.map(ConsentOperationStatusEnum::valueOf);
|
||||
}
|
||||
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(textBlock = """
|
||||
REJECT , REJECT , REJECT
|
||||
REJECT , PROCEED , REJECT
|
||||
REJECT , AUTHORIZED, REJECT
|
||||
AUTHORIZED, REJECT , REJECT
|
||||
AUTHORIZED, PROCEED , AUTHORIZED
|
||||
AUTHORIZED, AUTHORIZED, AUTHORIZED
|
||||
PROCEED , REJECT , REJECT
|
||||
PROCEED , PROCEED , PROCEED
|
||||
PROCEED , AUTHORIZED, AUTHORIZED
|
||||
""")
|
||||
void testParallelReduction_choosesStrongestVerdict(ConsentOperationStatusEnum theFirst, ConsentOperationStatusEnum theSecond, ConsentOperationStatusEnum theExpectedResult) {
|
||||
|
||||
// when
|
||||
ConsentOperationStatusEnum result = IConsentVote.parallelReduce(theFirst, theSecond);
|
||||
|
||||
assertEquals(theExpectedResult, result);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* With "parallel" evaluation, the "strongest" verdict wins.
|
||||
* REJECT > AUTHORIZED > PROCEED.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@CsvSource(textBlock = PARALLEL_STREAM_EXPECTATION)
|
||||
void testParallelStreamReduction_strongestVerdictWins(String theInput, ConsentOperationStatusEnum theExpectedResult) {
|
||||
// given
|
||||
Stream<ConsentOperationStatusEnum> consentOperationStatusEnumStream = splitEnumsToStream(theInput);
|
||||
|
||||
// when
|
||||
ConsentOperationStatusEnum result = ConsentOperationStatusEnum.parallelReduce(consentOperationStatusEnumStream);
|
||||
|
||||
assertEquals(theExpectedResult, result);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,166 @@
|
||||
package ca.uhn.fhir.rest.server.interceptor.consent;
|
||||
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
import jakarta.annotation.Nonnull;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static ca.uhn.fhir.rest.server.interceptor.consent.MultiDelegateConsentService.withParallelVoting;
|
||||
import static ca.uhn.fhir.rest.server.interceptor.consent.MultiDelegateConsentService.withSerialVoting;
|
||||
import static ca.uhn.fhir.rest.server.interceptor.consent.IConsentVoteTest.PARALLEL_STREAM_EXPECTATION;
|
||||
import static ca.uhn.fhir.rest.server.interceptor.consent.IConsentVoteTest.SERIAL_STREAM_EXPECTATION;
|
||||
import static ca.uhn.fhir.rest.server.interceptor.consent.IConsentVoteTest.splitEnumsToStream;
|
||||
import static org.apache.commons.lang3.StringUtils.defaultString;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class MultiDelegateConsentServiceTest {
|
||||
SystemRequestDetails mySrd = new SystemRequestDetails();
|
||||
|
||||
/**
|
||||
* "parallel" means any voter can veto.
|
||||
*/
|
||||
@Nested
|
||||
class ParallelEvaluation {
|
||||
MultiDelegateConsentService myService;
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(textBlock = PARALLEL_STREAM_EXPECTATION)
|
||||
void testStartOperation(String theInput, ConsentOperationStatusEnum theExpectedResult) {
|
||||
|
||||
var services = splitEnumsToStream(theInput).map(result -> (IConsentService)ConstantConsentService.constantService(result)).toList();
|
||||
myService = withParallelVoting(services);
|
||||
|
||||
var verdict = myService.startOperation(mySrd, IConsentContextServices.NULL_IMPL);
|
||||
|
||||
assertEquals(theExpectedResult.getStatus(), verdict.getStatus());
|
||||
}
|
||||
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(textBlock = """
|
||||
, false
|
||||
true , true
|
||||
false , false
|
||||
false true , true
|
||||
true false , true
|
||||
""")
|
||||
void testCanSeeResource(String theInput, boolean theExpectedResult) {
|
||||
|
||||
List<IConsentService> consentServices = Arrays.stream(defaultString(theInput).split(" +"))
|
||||
.map(String::trim)
|
||||
.map(Boolean::valueOf)
|
||||
.map(MultiDelegateConsentServiceTest::buildConsentShouldProcessCanSee)
|
||||
.toList();
|
||||
myService = withParallelVoting(consentServices);
|
||||
|
||||
var result = myService.shouldProcessCanSeeResource(mySrd, IConsentContextServices.NULL_IMPL);
|
||||
|
||||
assertEquals(theExpectedResult, result);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(textBlock = PARALLEL_STREAM_EXPECTATION)
|
||||
void testCanSeeResource(String theInput, ConsentOperationStatusEnum theExpectedResult) {
|
||||
|
||||
var services = splitEnumsToStream(theInput).map(result -> (IConsentService)ConstantConsentService.constantService(result)).toList();
|
||||
myService = withParallelVoting(services);
|
||||
|
||||
var verdict = myService.canSeeResource(mySrd, null, IConsentContextServices.NULL_IMPL);
|
||||
|
||||
assertEquals(theExpectedResult.getStatus(), verdict.getStatus());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(textBlock = PARALLEL_STREAM_EXPECTATION)
|
||||
void testWillSeeResource(String theInput, ConsentOperationStatusEnum theExpectedResult) {
|
||||
|
||||
var services = splitEnumsToStream(theInput).map(result -> (IConsentService)ConstantConsentService.constantService(result)).toList();
|
||||
myService = withParallelVoting(services);
|
||||
|
||||
var verdict = myService.willSeeResource(mySrd, null, IConsentContextServices.NULL_IMPL);
|
||||
|
||||
assertEquals(theExpectedResult.getStatus(), verdict.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* "serial" means first comited vote wins
|
||||
*/
|
||||
@Nested
|
||||
class SerialEvaluation {
|
||||
MultiDelegateConsentService myService;
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(textBlock = SERIAL_STREAM_EXPECTATION)
|
||||
void testStartOperation(String theInput, ConsentOperationStatusEnum theExpectedResult) {
|
||||
|
||||
var services = splitEnumsToStream(theInput).map(result -> (IConsentService)ConstantConsentService.constantService(result)).toList();
|
||||
myService = withSerialVoting(services);
|
||||
|
||||
var verdict = myService.startOperation(mySrd, IConsentContextServices.NULL_IMPL);
|
||||
|
||||
assertEquals(theExpectedResult.getStatus(), verdict.getStatus());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(textBlock = """
|
||||
, false
|
||||
true , true
|
||||
false , false
|
||||
false true , true
|
||||
true false , true
|
||||
""")
|
||||
void testCanSeeResource(String theInput, boolean theExpectedResult) {
|
||||
|
||||
List<IConsentService> consentServices = Arrays.stream(defaultString(theInput).split(" +"))
|
||||
.map(String::trim)
|
||||
.map(Boolean::valueOf)
|
||||
.map(MultiDelegateConsentServiceTest::buildConsentShouldProcessCanSee)
|
||||
.toList();
|
||||
myService = withSerialVoting(consentServices);
|
||||
|
||||
var result = myService.shouldProcessCanSeeResource(mySrd, IConsentContextServices.NULL_IMPL);
|
||||
|
||||
assertEquals(theExpectedResult, result);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(textBlock = SERIAL_STREAM_EXPECTATION)
|
||||
void testCanSeeResource(String theInput, ConsentOperationStatusEnum theExpectedResult) {
|
||||
|
||||
var services = splitEnumsToStream(theInput).map(result -> (IConsentService)ConstantConsentService.constantService(result)).toList();
|
||||
myService = withSerialVoting(services);
|
||||
|
||||
var verdict = myService.canSeeResource(mySrd, null, IConsentContextServices.NULL_IMPL);
|
||||
|
||||
assertEquals(theExpectedResult.getStatus(), verdict.getStatus());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(textBlock = SERIAL_STREAM_EXPECTATION)
|
||||
void testWillSeeResource(String theInput, ConsentOperationStatusEnum theExpectedResult) {
|
||||
|
||||
var services = splitEnumsToStream(theInput).map(result -> (IConsentService)ConstantConsentService.constantService(result)).toList();
|
||||
myService = withSerialVoting(services);
|
||||
|
||||
var verdict = myService.willSeeResource(mySrd, null, IConsentContextServices.NULL_IMPL);
|
||||
|
||||
assertEquals(theExpectedResult.getStatus(), verdict.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
private static @Nonnull IConsentService buildConsentShouldProcessCanSee(boolean result) {
|
||||
return new IConsentService() {
|
||||
@Override
|
||||
public boolean shouldProcessCanSeeResource(RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
|
||||
return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user