diff --git a/common/openapi.yml b/common/openapi.yml index 7ba72a6..e7910ca 100644 --- a/common/openapi.yml +++ b/common/openapi.yml @@ -119,6 +119,8 @@ paths: $ref: '#/components/responses/ServerError' get: summary: Retrieve a Policy Attribute Object + parameters: + - $ref: '#/components/parameters/IncludeDeleted' operationId: getPao tags: [Tps] responses: @@ -489,6 +491,15 @@ components: schema: $ref: '#/components/schemas/TpsDepth' + IncludeDeleted: + name: includeDeleted + in: query + description: | + Include deleted policy attribute objects in the response. Defaults to false. + required: false + schema: + type: boolean + responses: # Error Responses BadRequest: diff --git a/service/src/main/java/bio/terra/policy/app/controller/TpsApiController.java b/service/src/main/java/bio/terra/policy/app/controller/TpsApiController.java index 4b2ee99..5944a12 100644 --- a/service/src/main/java/bio/terra/policy/app/controller/TpsApiController.java +++ b/service/src/main/java/bio/terra/policy/app/controller/TpsApiController.java @@ -107,8 +107,11 @@ private ApiTpsPolicyExplanation convertExplanation(ExplainGraphNode node) { } @Override - public ResponseEntity getPao(UUID objectId) { - Pao pao = paoService.getPao(objectId); + public ResponseEntity getPao(UUID objectId, Boolean includeDeleted) { + if (includeDeleted == null) { + includeDeleted = false; + } + Pao pao = paoService.getPao(objectId, includeDeleted); ApiTpsPaoGetResult result = ConversionUtils.paoToApi(pao); MetricsUtils.incrementPaoGet(); return new ResponseEntity<>(result, HttpStatus.OK); diff --git a/service/src/main/java/bio/terra/policy/db/PaoDao.java b/service/src/main/java/bio/terra/policy/db/PaoDao.java index 8a049b6..6c387e6 100644 --- a/service/src/main/java/bio/terra/policy/db/PaoDao.java +++ b/service/src/main/java/bio/terra/policy/db/PaoDao.java @@ -16,7 +16,6 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -103,11 +102,6 @@ public void createPao( effectiveSetId); } - @WriteTransaction - public void deletePaos(Collection paos) { - paos.forEach((DbPao pao) -> removeDbPao(pao)); - } - /** * Set the 'deleted' field on the PAO to true. * @@ -122,8 +116,8 @@ public void markPaoDeleted(UUID objectId) { } @ReadTransaction - public Pao getPao(UUID objectId) { - DbPao dbPao = getDbPao(objectId); + public Pao getPao(UUID objectId, boolean includeDeleted) { + DbPao dbPao = getDbPao(objectId, includeDeleted); Map attributeSetMap = getAttributeSets(List.of(dbPao.attributeSetId(), dbPao.effectiveSetId())); return Pao.fromDb(dbPao, attributeSetMap); @@ -236,7 +230,7 @@ private void updatePao(GraphNode change) { PolicyInputs effectiveAttributes = change.getEffectivePolicyAttributes(); // Get the dbPao and the attribute sets from the db for comparison - DbPao dbPao = getDbPao(pao.getObjectId()); + DbPao dbPao = getDbPao(pao.getObjectId(), true); Map attributeSetMap = getAttributeSets(List.of(dbPao.attributeSetId(), dbPao.effectiveSetId())); PolicyInputs dbAttributes = attributeSetMap.get(dbPao.attributeSetId()); @@ -395,13 +389,17 @@ private void deleteAttributeSet(String setId) { tpsJdbcTemplate.update(sql, params); } - public DbPao getDbPao(UUID objectId) { - final String sql = + public DbPao getDbPao(UUID objectId, boolean includeDeleted) { + String sql = """ SELECT object_id, component, object_type, attribute_set_id, effective_set_id, sources, deleted, created, last_updated FROM policy_object WHERE object_id = :object_id """; + if (!includeDeleted) { + sql += " AND (deleted is null or not deleted)"; + } + MapSqlParameterSource params = new MapSqlParameterSource().addValue("object_id", objectId.toString()); diff --git a/service/src/main/java/bio/terra/policy/service/pao/PaoService.java b/service/src/main/java/bio/terra/policy/service/pao/PaoService.java index a898258..8284f7d 100644 --- a/service/src/main/java/bio/terra/policy/service/pao/PaoService.java +++ b/service/src/main/java/bio/terra/policy/service/pao/PaoService.java @@ -8,9 +8,7 @@ import bio.terra.policy.common.exception.InvalidInputException; import bio.terra.policy.common.model.PolicyInput; import bio.terra.policy.common.model.PolicyInputs; -import bio.terra.policy.db.DbPao; import bio.terra.policy.db.PaoDao; -import bio.terra.policy.service.pao.graph.DeleteWalker; import bio.terra.policy.service.pao.graph.ExplainWalker; import bio.terra.policy.service.pao.graph.Walker; import bio.terra.policy.service.pao.graph.model.ExplainGraph; @@ -70,9 +68,6 @@ public void createPao( public void deletePao(UUID objectId) { logger.info("Delete PAO id {}", objectId); paoDao.markPaoDeleted(objectId); - DeleteWalker walker = new DeleteWalker(paoDao, objectId); - Set toRemove = walker.findRemovablePaos(); - paoDao.deletePaos(toRemove); } /** @@ -87,9 +82,14 @@ public ExplainGraph explainPao(UUID objectId, int depth) { return walker.getExplainGraph(); } - public Pao getPao(UUID objectId) { + public Pao getPao(UUID objectId, boolean includeDeleted) { logger.info("Get PAO id {}", objectId); - return paoDao.getPao(objectId); + + return paoDao.getPao(objectId, includeDeleted); + } + + public Pao getPao(UUID objectId) { + return getPao(objectId, false); } @ReadTransaction @@ -114,7 +114,7 @@ public PolicyUpdateResult linkSourcePao( logger.info( "LinkSourcePao: dependent {} source {} mode {}", objectId, sourceObjectId, updateMode); - Pao targetPao = paoDao.getPao(objectId); + Pao targetPao = paoDao.getPao(objectId, false); boolean newSource = targetPao.getSourceObjectIds().add(sourceObjectId); // We didn't actually change the source list, so we are done @@ -164,8 +164,8 @@ public PolicyUpdateResult mergeFromPao( "Merge from PAO id {} to {} mode {}", sourceObjectId, destinationObjectId, updateMode); // Step 0: get the paos. This will throw if they are not present - Pao sourcePao = paoDao.getPao(sourceObjectId); - Pao destinationPao = paoDao.getPao(destinationObjectId); + Pao sourcePao = paoDao.getPao(sourceObjectId, false); + Pao destinationPao = paoDao.getPao(destinationObjectId, false); // If the source and destination are the same PAO, there is nothing to do if (sourceObjectId.equals(destinationObjectId)) { @@ -211,7 +211,7 @@ public PolicyUpdateResult replacePao( replacementAttributes, updateMode); validatePolicyInputs(replacementAttributes); - Pao targetPao = paoDao.getPao(targetPaoId); + Pao targetPao = paoDao.getPao(targetPaoId, false); return updateAttributesWorker(replacementAttributes, targetPao, updateMode); } @@ -236,7 +236,7 @@ public PolicyUpdateResult updatePao( removeAttributes, updateMode); - Pao targetPao = paoDao.getPao(targetPaoId); + Pao targetPao = paoDao.getPao(targetPaoId, false); PolicyInputs attributesToUpdate = new PolicyInputs(targetPao.getAttributes()); // We do the removes first, so we don't remove newly added things diff --git a/service/src/main/java/bio/terra/policy/service/pao/graph/DeleteWalker.java b/service/src/main/java/bio/terra/policy/service/pao/graph/DeleteWalker.java deleted file mode 100644 index 345ddb3..0000000 --- a/service/src/main/java/bio/terra/policy/service/pao/graph/DeleteWalker.java +++ /dev/null @@ -1,113 +0,0 @@ -package bio.terra.policy.service.pao.graph; - -import bio.terra.policy.db.DbPao; -import bio.terra.policy.db.PaoDao; -import bio.terra.policy.service.pao.graph.model.DeleteGraphNode; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.Queue; -import java.util.Set; -import java.util.UUID; - -public class DeleteWalker { - private HashMap subgraphMap; - private PaoDao paoDao; - private UUID rootObjectId; - - public DeleteWalker(PaoDao paoDao, UUID rootObjectId) { - subgraphMap = new HashMap<>(); - this.paoDao = paoDao; - this.rootObjectId = rootObjectId; - } - - public Set findRemovablePaos() { - final Set dependents = paoDao.getDependentIds(rootObjectId); - final Set result = new HashSet<>(); - - if (dependents.isEmpty()) { - DbPao dbPao = paoDao.getDbPao(rootObjectId); - - // First Pass: Build a map of all PAOs in our subgraph and note if they are flagged for - // deletion. - walkDeleteSubgraph(dbPao); - - // Second Pass: iterate through our graph, for each PAO: - // Recursively check its dependents and verify it's still removable. - // If all dependents are flagged for deletion and exist in our known subgraph, - // then the PAO is still removable. - HashMap> dependentMap = new HashMap<>(); - walkDeleteDependents(dependentMap, dbPao); - - // Finally - remove PAOs that are still removable - subgraphMap.forEach( - (UUID id, DeleteGraphNode node) -> { - if (node.getRemovable()) { - result.add(node.getPao()); - } - }); - } - - return result; - } - - /** - * Recursively build out a map of which PAOs are in the subgraph and note which PAOs have been - * flagged for deletion. - * - * @param pao the PAO currently being evaluated. On first call, this would be the root of the - * subgraph. - */ - private void walkDeleteSubgraph(DbPao pao) { - subgraphMap.put(pao.objectId(), new DeleteGraphNode(pao, pao.deleted())); - - for (var source : pao.sources()) { - walkDeleteSubgraph(paoDao.getDbPao(UUID.fromString(source))); - } - } - - /** - * Recursive call to check all dependents of a given PAO and update the PAOs removability. - * - * @param dependentMap a map of PAO id to all of that PAOs dependents. This serves as a cache and - * is filled in during recursive calls. - * @param pao the PAO currently being evaluated. On first call, this would be the root of the - * subgraph. - */ - private void walkDeleteDependents(HashMap> dependentMap, DbPao pao) { - HashMap dependents = new HashMap<>(); - - if (pao == null) return; - - if (!dependentMap.containsKey(pao.objectId())) { - // use a BFS to build a dependency list - Queue queue = new LinkedList<>(); - queue.addAll(paoDao.getDependentIds(pao.objectId())); - - while (!queue.isEmpty()) { - UUID depId = queue.poll(); - if (!dependents.containsKey(depId)) { - dependents.put(depId, paoDao.getDbPao(depId)); - } - queue.addAll(paoDao.getDependentIds(depId)); - } - - dependentMap.put(pao.objectId(), new HashSet<>(dependents.values())); - } - - for (var dependent : dependents.values()) { - if (!subgraphMap.containsKey(dependent.objectId()) || !dependent.deleted()) { - // if any dependent is not part of the subgraph or is not flagged for removal - // then the current PAO cannot be removed. - subgraphMap.get(pao.objectId()).setRemovable(false); - break; - } - } - - for (var source : pao.sources()) { - // recursive step to continue checking all the current PAOs sources - // use the subgraphMap so lookup PAOs so that we don't keep querying the db - walkDeleteDependents(dependentMap, subgraphMap.get(UUID.fromString(source)).getPao()); - } - } -} diff --git a/service/src/main/java/bio/terra/policy/service/pao/graph/ExplainWalker.java b/service/src/main/java/bio/terra/policy/service/pao/graph/ExplainWalker.java index f03b841..92e546c 100644 --- a/service/src/main/java/bio/terra/policy/service/pao/graph/ExplainWalker.java +++ b/service/src/main/java/bio/terra/policy/service/pao/graph/ExplainWalker.java @@ -170,7 +170,7 @@ public ExplainGraph getExplainGraph() { private Pao getPao(UUID objectId) { Pao pao = paoMap.get(objectId); if (pao == null) { - pao = paoDao.getPao(objectId); + pao = paoDao.getPao(objectId, true); paoMap.put(objectId, pao); } return pao; diff --git a/service/src/test/java/bio/terra/policy/service/pao/PaoDeleteTest.java b/service/src/test/java/bio/terra/policy/service/pao/PaoDeleteTest.java index 93748b2..9c1c06d 100644 --- a/service/src/test/java/bio/terra/policy/service/pao/PaoDeleteTest.java +++ b/service/src/test/java/bio/terra/policy/service/pao/PaoDeleteTest.java @@ -14,7 +14,6 @@ import bio.terra.policy.service.pao.model.PaoObjectType; import bio.terra.policy.service.pao.model.PaoUpdateMode; import bio.terra.policy.testutils.TestUnitBase; -import java.util.ArrayList; import java.util.Collections; import java.util.UUID; import org.junit.jupiter.api.Test; @@ -31,28 +30,28 @@ public class PaoDeleteTest extends TestUnitBase { @Autowired private PaoService paoService; - /** - * A standalone PAO with no sources and no dependents should be removed from the DB. - * - * @throws Exception - */ + /** A standalone PAO with no sources and no dependents should be marked as deleted in the DB. */ @Test - void deletePaoRemovesFromDb() throws Exception { + void deleteStandalonePaoMarksDeleted() { var objectId = UUID.randomUUID(); createDefaultPao(objectId); // Retrieve and validate - Pao pao = paoService.getPao(objectId); + Pao pao = paoService.getPao(objectId, true); assertEquals(objectId, pao.getObjectId()); + assertFalse(pao.getDeleted()); // Delete removes the PAO from the DB paoService.deletePao(objectId); - assertThrows(PolicyObjectNotFoundException.class, () -> paoService.getPao(objectId)); + final var deletedPao = paoService.getPao(objectId, true); + assertNotNull(deletedPao); + assertTrue(deletedPao.getDeleted()); + assertThrows(PolicyObjectNotFoundException.class, () -> paoService.getPao(objectId, false)); } /** - * If the PAO being deleted is referenced as a source by another PAO, then it doesn't get removed - * from the graph but gets marked as deleted. + * If the PAO being deleted is referenced as a source by another PAO, then it gets marked as + * deleted. * *
    *     {dependentPao}
@@ -63,7 +62,7 @@ void deletePaoRemovesFromDb() throws Exception {
    * Result should be both PAOs are still in the graph, sourcePao is marked as deleted.
    */
   @Test
-  void deletePaoMarksDeleted() {
+  void deleteSourcePaoMarksDeleted() {
     var sourcePaoId = UUID.randomUUID();
     var dependentPaoId = UUID.randomUUID();
     createDefaultPao(sourcePaoId);
@@ -75,214 +74,19 @@ void deletePaoMarksDeleted() {
 
     // Since sourcePao has a dependent, it should be flagged as 'deleted' but should not be removed
     // from the db.
-    final var sourcePao = paoService.getPao(sourcePaoId);
+    final var sourcePao = paoService.getPao(sourcePaoId, true);
     assertNotNull(sourcePao);
     assertEquals(sourcePaoId, sourcePao.getObjectId());
     assertTrue(sourcePao.getDeleted());
+    assertThrows(PolicyObjectNotFoundException.class, () -> paoService.getPao(sourcePaoId, false));
 
     // Dependent should still link back to the source and not be deleted.
-    final var dependentPao = paoService.getPao(dependentPaoId);
+    final var dependentPao = paoService.getPao(dependentPaoId, true);
     assertNotNull(dependentPao);
     assertTrue(dependentPao.getSourceObjectIds().contains(sourcePaoId));
     assertFalse(dependentPao.getDeleted());
   }
 
-  /**
-   * In this case, the deleted PAO has two sources, one of which was previously flagged as deleted.
-   *
-   * 
-   *     {targetPao} <-- delete second
-   *      /         \
-   * {sourcePao1}  {sourcePao2*} <-- delete first
-   *                  \
-   *                {sourcePao3}
-   * 
- * - * The result should be that targetPao and sourcePao2 should be removed from the db. The other - * PAOs should remain. - */ - @Test - void deletePaoRemovesPreviouslyFlaggedSource() throws Exception { - var targetObjectId = UUID.randomUUID(); - var source1ObjectId = UUID.randomUUID(); - var source2ObjectId = UUID.randomUUID(); - var source3ObjectId = UUID.randomUUID(); - createDefaultPao(targetObjectId); - createDefaultPao(source1ObjectId); - createDefaultPao(source2ObjectId); - createDefaultPao(source3ObjectId); - - paoService.linkSourcePao(targetObjectId, source1ObjectId, PaoUpdateMode.FAIL_ON_CONFLICT); - paoService.linkSourcePao(targetObjectId, source2ObjectId, PaoUpdateMode.FAIL_ON_CONFLICT); - paoService.linkSourcePao(targetObjectId, source3ObjectId, PaoUpdateMode.FAIL_ON_CONFLICT); - - // Call delete on sourcePao2 - paoService.deletePao(source2ObjectId); - var pao = paoService.getPao(source2ObjectId); - assertNotNull(pao); - assertTrue(pao.getDeleted()); - - // Call delete on targetPao - paoService.deletePao(targetObjectId); - - // targetPao and sourcePao2 should be deleted - assertThrows(PolicyObjectNotFoundException.class, () -> paoService.getPao(targetObjectId)); - assertThrows(PolicyObjectNotFoundException.class, () -> paoService.getPao(source2ObjectId)); - - // sourcePao1 and sourcePao3 should not be - assertNotNull(paoService.getPao(source1ObjectId)); - assertNotNull(paoService.getPao(source3ObjectId)); - } - - /** - * Recursive test: deleting a Pao should visit all source Paos recursively and apply the delete - * logic on them. - * - *

For setup, we'll flag PAOs indicated with a ! as deleted before calling the final delete. - * - *

-   *                    {targetPao#} <-- delete here
-   *                    /       \
-   *               {pao2#!}     {pao3}
-   *                /   \
-   *            {pao4#!} \    {pao6}
-   *             /        \   /
-   *          {pao8#!}   {pao5*!}
-   *           /           |
-   *        {pao9}      {pao7}
-   * 
- * - * Expected result: pao5 (*) should be flagged as deleted but not removed from the db. paos 2, 4, - * 8 and target (#) should be removed from the db. - */ - @Test - void deleteRecursive() throws Exception { - var targetObjectId = UUID.randomUUID(); - var pao2Id = UUID.randomUUID(); - var pao3Id = UUID.randomUUID(); - var pao4Id = UUID.randomUUID(); - var pao5Id = UUID.randomUUID(); - var pao6Id = UUID.randomUUID(); - var pao7Id = UUID.randomUUID(); - var pao8Id = UUID.randomUUID(); - var pao9Id = UUID.randomUUID(); - - createDefaultPao(targetObjectId); - createDefaultPao(pao2Id); - createDefaultPao(pao3Id); - createDefaultPao(pao4Id); - createDefaultPao(pao5Id); - createDefaultPao(pao6Id); - createDefaultPao(pao7Id); - createDefaultPao(pao8Id); - createDefaultPao(pao9Id); - - paoService.linkSourcePao(targetObjectId, pao2Id, PaoUpdateMode.FAIL_ON_CONFLICT); - paoService.linkSourcePao(targetObjectId, pao3Id, PaoUpdateMode.FAIL_ON_CONFLICT); - paoService.linkSourcePao(pao2Id, pao4Id, PaoUpdateMode.FAIL_ON_CONFLICT); - paoService.linkSourcePao(pao2Id, pao5Id, PaoUpdateMode.FAIL_ON_CONFLICT); - paoService.linkSourcePao(pao6Id, pao5Id, PaoUpdateMode.FAIL_ON_CONFLICT); - paoService.linkSourcePao(pao5Id, pao7Id, PaoUpdateMode.FAIL_ON_CONFLICT); - paoService.linkSourcePao(pao4Id, pao8Id, PaoUpdateMode.FAIL_ON_CONFLICT); - paoService.linkSourcePao(pao8Id, pao9Id, PaoUpdateMode.FAIL_ON_CONFLICT); - - // Mark PAOs 2, 4, 5, and 8 as deleted - ArrayList toMarkDeleted = new ArrayList<>(); - toMarkDeleted.add(pao2Id); - toMarkDeleted.add(pao4Id); - toMarkDeleted.add(pao5Id); - toMarkDeleted.add(pao8Id); - - for (UUID paoId : toMarkDeleted) { - paoService.deletePao(paoId); - var pao = paoService.getPao(paoId); - assertTrue(pao.getDeleted()); - } - - // call final delete to remove targetPao - paoService.deletePao(targetObjectId); - - // These PAOs should have been removed from the DB - assertThrows(PolicyObjectNotFoundException.class, () -> paoService.getPao(targetObjectId)); - assertThrows(PolicyObjectNotFoundException.class, () -> paoService.getPao(pao2Id)); - assertThrows(PolicyObjectNotFoundException.class, () -> paoService.getPao(pao4Id)); - assertThrows(PolicyObjectNotFoundException.class, () -> paoService.getPao(pao8Id)); - - // This PAO should be marked as deleted but not removed from the db - final var pao5 = paoService.getPao(pao5Id); - assertNotNull(pao5); - assertTrue(pao5.getDeleted()); - - // These PAOs should still exist - assertNotNull(paoService.getPao(pao3Id)); - assertNotNull(paoService.getPao(pao6Id)); - assertNotNull(paoService.getPao(pao7Id)); - assertNotNull(paoService.getPao(pao9Id)); - } - - /** - * A case for testing recursion where sources of sources might link back to an undeleted node. - * Here, all PAOs marked with a * were previously flagged as deleted. - * - *
-   *   delete->paoA#  paoB
-   *             \  /  |
-   *             paoC* |
-   *              |    |
-   *            paoD*  |
-   *               \   |
-   *                paoE*
-   * 
- * - * The result should be that only PaoA is removed from the db. All others have a dependent B. - */ - @Test - void deleteWithSkipLevelDependents() throws Exception { - var paoAId = UUID.randomUUID(); - var paoBId = UUID.randomUUID(); - var paoCId = UUID.randomUUID(); - var paoDId = UUID.randomUUID(); - var paoEId = UUID.randomUUID(); - - createDefaultPao(paoAId); - createDefaultPao(paoBId); - createDefaultPao(paoCId); - createDefaultPao(paoDId); - createDefaultPao(paoEId); - - paoService.linkSourcePao(paoAId, paoCId, PaoUpdateMode.FAIL_ON_CONFLICT); - paoService.linkSourcePao(paoAId, paoDId, PaoUpdateMode.FAIL_ON_CONFLICT); - paoService.linkSourcePao(paoBId, paoCId, PaoUpdateMode.FAIL_ON_CONFLICT); - paoService.linkSourcePao(paoBId, paoEId, PaoUpdateMode.FAIL_ON_CONFLICT); - paoService.linkSourcePao(paoCId, paoDId, PaoUpdateMode.FAIL_ON_CONFLICT); - paoService.linkSourcePao(paoDId, paoEId, PaoUpdateMode.FAIL_ON_CONFLICT); - - // Mark C,D,E as deleted - paoService.deletePao(paoCId); - paoService.deletePao(paoDId); - paoService.deletePao(paoEId); - - // call final delete to remove A - paoService.deletePao(paoAId); - - // These PAOs should have been removed from the DB - assertThrows(PolicyObjectNotFoundException.class, () -> paoService.getPao(paoAId)); - - // These PAOs should be marked as deleted but not removed from the db - var paos = new ArrayList(); - paos.add(paoService.getPao(paoCId)); - paos.add(paoService.getPao(paoDId)); - paos.add(paoService.getPao(paoEId)); - - for (var pao : paos) { - assertNotNull(pao); - assertTrue(pao.getDeleted()); - } - - // These PAOs should still exist - assertNotNull(paoService.getPao(paoBId)); - } - private void createDefaultPao(UUID objectId) { var groupPolicy = PolicyInput.createFromMap( diff --git a/service/src/test/java/bio/terra/policy/service/pao/PaoUpdateTest.java b/service/src/test/java/bio/terra/policy/service/pao/PaoUpdateTest.java index 264b809..8aa5713 100644 --- a/service/src/test/java/bio/terra/policy/service/pao/PaoUpdateTest.java +++ b/service/src/test/java/bio/terra/policy/service/pao/PaoUpdateTest.java @@ -16,6 +16,7 @@ import bio.terra.policy.common.exception.DirectConflictException; import bio.terra.policy.common.exception.IllegalCycleException; import bio.terra.policy.common.exception.InternalTpsErrorException; +import bio.terra.policy.common.exception.PolicyObjectNotFoundException; import bio.terra.policy.common.model.PolicyInput; import bio.terra.policy.common.model.PolicyInputs; import bio.terra.policy.common.model.PolicyName; @@ -476,6 +477,33 @@ void updateOneSourceTest_sourcePolicyPropagatesCorrectly() throws Exception { assertTrue(checkD.getEffectiveAttributes().getInputs().isEmpty()); } + @Test + void updateOneSourceTest_sourcePolicyPropagatesThroughDeletedPaoCorrectly() throws Exception { + // We build this graph A --> B --> C + // Then we delete B and update A + // We expect the update to A to propagate to C + PolicyInput europeRegion = PaoTestUtil.makeRegionPolicyInput(PaoTestUtil.REGION_NAME_EUROPE); + PolicyInputs newPolicy = PaoTestUtil.makePolicyInputs(europeRegion); + PolicyInputs emptyPolicy = PaoTestUtil.makePolicyInputs(); + + UUID paoAid = PaoTestUtil.makePao(paoService); + UUID paoBid = PaoTestUtil.makePao(paoService); + UUID paoCid = PaoTestUtil.makePao(paoService); + + // Hook up the graph and then delete B + paoService.linkSourcePao(paoBid, paoAid, PaoUpdateMode.FAIL_ON_CONFLICT); + paoService.linkSourcePao(paoCid, paoBid, PaoUpdateMode.FAIL_ON_CONFLICT); + paoService.deletePao(paoBid); + + PolicyUpdateResult result = + paoService.updatePao(paoAid, newPolicy, emptyPolicy, PaoUpdateMode.FAIL_ON_CONFLICT); + assertTrue(result.updateApplied()); + assertEquals(0, result.conflicts().size()); + PaoTestUtil.checkForPolicies(paoService.getPao(paoAid), europeRegion); + assertThrows(PolicyObjectNotFoundException.class, () -> paoService.getPao(paoBid)); + PaoTestUtil.checkForPolicies(paoService.getPao(paoCid), europeRegion); + } + @Test void updatePropagateConflictTest_sourcesConflict() throws Exception { // Two sources and a dependent