Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
/*
* Copyright 2015-2026 Ritense BV, the Netherlands.
*
* Licensed under EUPL, Version 1.2 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.ritense.buildingblock.service

import com.fasterxml.jackson.databind.ObjectMapper
import com.ritense.authorization.AuthorizationContext.Companion.runWithoutAuthorization
import com.ritense.buildingblock.BaseIntegrationTest
import com.ritense.buildingblock.repository.BuildingBlockInstanceRepository
import com.ritense.buildingblock.web.rest.dto.CreateCaseDefinitionBuildingBlockLinkDto
import com.ritense.document.domain.impl.request.ModifyDocumentRequest
import com.ritense.document.domain.impl.request.NewDocumentRequest
import com.ritense.processdocument.domain.impl.request.ModifyDocumentAndStartProcessRequest
import com.ritense.processdocument.domain.impl.request.NewDocumentAndStartProcessRequest
import com.ritense.processdocument.service.ProcessDocumentService
import com.ritense.valtimo.contract.buildingblock.BuildingBlockDefinitionId
import com.ritense.valtimo.contract.case_.CaseDefinitionId
import com.ritense.valtimo.service.OperatonProcessService
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.operaton.bpm.engine.RepositoryService
import org.operaton.bpm.engine.RuntimeService
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.transaction.annotation.Transactional
import java.io.ByteArrayInputStream
import java.util.UUID

/**
* Regression tests for GZAC issue 819: a building block version that is linked to a case as an action
* must be started at that exact version, even when a newer draft version of the same building block has
* redeployed the same process definition key under a higher engine version.
*/
@Transactional
class BuildingBlockStartableItemVersionIT @Autowired constructor(
private val processDocumentService: ProcessDocumentService,
private val caseDefinitionBuildingBlockLinkService: CaseDefinitionBuildingBlockLinkService,
private val buildingBlockInstanceRepository: BuildingBlockInstanceRepository,
private val repositoryService: RepositoryService,
private val runtimeService: RuntimeService,
private val operatonProcessService: OperatonProcessService,
private val objectMapper: ObjectMapper,
) : BaseIntegrationTest() {

@Test
fun `should start the linked building block version and not the latest one`() {
val linkedVersionProcessDefinitionId = mainProcessDefinitionIdOf(BUILDING_BLOCK_VERSION)
linkBuildingBlockToCase()
val caseDocumentId = startCase()

deployNewerBuildingBlockVersionOfMainProcess()

// Guard: both versions share one process key, so a lookup by key alone is ambiguous.
assertThat(deployedVersionTagsOfMainProcess())
.contains("BB:$BUILDING_BLOCK_KEY:$BUILDING_BLOCK_VERSION", "BB:$BUILDING_BLOCK_KEY:$DRAFT_VERSION")

val result = runWithoutAuthorization {
processDocumentService.modifyDocumentAndStartProcess(
ModifyDocumentAndStartProcessRequest(
MAIN_PROCESS_KEY,
ModifyDocumentRequest(caseDocumentId.toString(), objectMapper.createObjectNode())
).withProcessDefinitionId(linkedVersionProcessDefinitionId)
)
}

assertThat(result.errors()).isEmpty()
val processInstanceId = result.resultingProcessInstanceId().orElseThrow().toString()
val startedDefinitionId = runtimeService.createProcessInstanceQuery()
.processInstanceId(processInstanceId)
.singleResult()
.processDefinitionId
assertThat(startedDefinitionId).isEqualTo(linkedVersionProcessDefinitionId)

// The listener derives the building block version from the started definition's version tag, so
// starting the wrong version leaves the case without a building block instance altogether.
val instances = buildingBlockInstancesOf(caseDocumentId)
assertThat(instances).hasSize(1)
assertThat(instances.first().definition.id)
.isEqualTo(BuildingBlockDefinitionId.of(BUILDING_BLOCK_KEY, BUILDING_BLOCK_VERSION))
}

@Test
fun `should fail instead of guessing a version when only the process definition key is given`() {
linkBuildingBlockToCase()
val caseDocumentId = startCase()
deployNewerBuildingBlockVersionOfMainProcess()

val result = runWithoutAuthorization {
processDocumentService.modifyDocumentAndStartProcess(
ModifyDocumentAndStartProcessRequest(
MAIN_PROCESS_KEY,
ModifyDocumentRequest(caseDocumentId.toString(), objectMapper.createObjectNode())
)
)
}

assertThat(result.errors()).isNotEmpty()
assertThat(buildingBlockInstancesOf(caseDocumentId)).isEmpty()
}

@Test
fun `should resolve a standalone building block start by its building block blueprint`() {
val linkedVersionProcessDefinitionId = mainProcessDefinitionIdOf(BUILDING_BLOCK_VERSION)
deployNewerBuildingBlockVersionOfMainProcess()

val result = runWithoutAuthorization {
processDocumentService.newDocumentAndStartProcess(
NewDocumentAndStartProcessRequest(
MAIN_PROCESS_KEY,
NewDocumentRequest(
BUILDING_BLOCK_KEY,
null,
null,
BUILDING_BLOCK_KEY,
BUILDING_BLOCK_VERSION,
objectMapper.createObjectNode()
)
)
)
}

assertThat(result.errors()).isEmpty()
val startedDefinitionId = runtimeService.createProcessInstanceQuery()
.processInstanceId(result.resultingProcessInstanceId().orElseThrow().toString())
.singleResult()
.processDefinitionId
assertThat(startedDefinitionId).isEqualTo(linkedVersionProcessDefinitionId)
}

/**
* Resolves the process definition of a building block version by its version tag rather than through
* the `main` link, so the test does not depend on state other integration tests may have committed.
*/
private fun mainProcessDefinitionIdOf(versionTag: String): String {
return repositoryService.createProcessDefinitionQuery()
.processDefinitionKey(MAIN_PROCESS_KEY)
.versionTag("BB:$BUILDING_BLOCK_KEY:$versionTag")
.orderByProcessDefinitionVersion()
.desc()
.list()
.firstOrNull()
?.id
?: throw IllegalStateException("No process definition for building block $BUILDING_BLOCK_KEY:$versionTag")
}

private fun buildingBlockInstancesOf(caseDocumentId: UUID) =
buildingBlockInstanceRepository.findAll().filter { it.caseDocumentId == caseDocumentId }

private fun linkBuildingBlockToCase() {
runWithoutAuthorization {
caseDefinitionBuildingBlockLinkService.createLink(
CaseDefinitionId.of(CASE_DEFINITION_KEY, CASE_DEFINITION_VERSION),
CreateCaseDefinitionBuildingBlockLinkDto(BUILDING_BLOCK_KEY, BUILDING_BLOCK_VERSION)
)
}
}

/**
* Redeploys the building block's main process under a newer building block version tag - the same
* thing creating a draft version does (see BuildingBlockDefinitionEventListener.copyProcessDefinitions),
* but done directly so the test does not depend on the state of the shared `bezwaar` fixture.
*/
private fun deployNewerBuildingBlockVersionOfMainProcess() {
val bpmn = requireNotNull(javaClass.classLoader.getResourceAsStream(MAIN_PROCESS_RESOURCE)) {
"Missing test resource $MAIN_PROCESS_RESOURCE"
}.use { it.readBytes() }

runWithoutAuthorization {
operatonProcessService.deploy(
BuildingBlockDefinitionId.of(BUILDING_BLOCK_KEY, DRAFT_VERSION),
"$MAIN_PROCESS_KEY.bpmn",
ByteArrayInputStream(bpmn),
true,
true
)
}

// Both versions now share one process definition key, which is exactly the ambiguity that made the
// wrong version start.
assertThat(mainProcessDefinitionIdOf(DRAFT_VERSION))
.isNotEqualTo(mainProcessDefinitionIdOf(BUILDING_BLOCK_VERSION))
}

private fun deployedVersionTagsOfMainProcess(): List<String?> {
return repositoryService.createProcessDefinitionQuery()
.processDefinitionKey(MAIN_PROCESS_KEY)
.list()
.map { it.versionTag }
}

private fun startCase(): UUID {
val result = runWithoutAuthorization {
processDocumentService.newDocumentAndStartProcess(
NewDocumentAndStartProcessRequest(
CASE_MAIN_PROCESS_KEY,
NewDocumentRequest(
CASE_DEFINITION_KEY,
CASE_DEFINITION_KEY,
CASE_DEFINITION_VERSION,
objectMapper.createObjectNode()
)
)
)
}
return result.resultingDocument()
.orElseThrow { IllegalStateException("Case document not created: ${result.errors()}") }
.id()
.id
}

private companion object {
const val BUILDING_BLOCK_KEY = "bezwaar"
const val BUILDING_BLOCK_VERSION = "1.0.0"
// Deliberately distinctive so other integration tests in this module cannot have created it.
const val DRAFT_VERSION = "8.1.9"
const val CASE_DEFINITION_KEY = "bb-case"
const val CASE_DEFINITION_VERSION = "1.0.0"
const val CASE_MAIN_PROCESS_KEY = "bb-case-plain-main"
const val MAIN_PROCESS_KEY = "building-block-process"
const val MAIN_PROCESS_RESOURCE =
"config/building-block/bezwaar/1-0-0/bpmn/building-block-process.bpmn"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_bb_case_plain_main" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.39.0" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.22.0">
<bpmn:process id="bb-case-plain-main" name="BB Case Plain Main" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_1</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:userTask id="userTask" name="Wait">
<bpmn:incoming>Flow_1</bpmn:incoming>
<bpmn:outgoing>Flow_2</bpmn:outgoing>
</bpmn:userTask>
<bpmn:endEvent id="EndEvent_1">
<bpmn:incoming>Flow_2</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1" sourceRef="StartEvent_1" targetRef="userTask" />
<bpmn:sequenceFlow id="Flow_2" sourceRef="userTask" targetRef="EndEvent_1" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="bb-case-plain-main">
<bpmndi:BPMNShape id="StartEvent_1_di" bpmnElement="StartEvent_1">
<dc:Bounds x="182" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_1_di" bpmnElement="userTask">
<dc:Bounds x="270" y="80" width="120" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_1_di" bpmnElement="EndEvent_1">
<dc:Bounds x="430" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1_di" bpmnElement="Flow_1">
<di:waypoint x="218" y="120" />
<di:waypoint x="270" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_2_di" bpmnElement="Flow_2">
<di:waypoint x="390" y="120" />
<di:waypoint x="430" y="120" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@

package com.ritense.document.domain;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import com.ritense.document.domain.validation.DocumentContentValidationResult;
import com.ritense.valtimo.contract.BlueprintId;
import com.ritense.valtimo.contract.buildingblock.BuildingBlockDefinitionId;
import com.ritense.valtimo.contract.case_.CaseDefinitionId;
import java.time.temporal.Temporal;
Expand Down Expand Up @@ -46,6 +48,18 @@ interface Id {

@JsonProperty
default BuildingBlockDefinitionId buildingBlockDefinitionId() { return null; }

/**
* The blueprint this document definition belongs to, regardless of its type. A document
* definition is owned by either a case definition or a building block definition, so use
* this whenever the caller does not care which of the two it is - {@link #caseDefinitionId()}
* on its own returns {@code null} for building block documents.
*/
@JsonIgnore
default BlueprintId asBlueprintId() {
CaseDefinitionId caseDefinitionId = caseDefinitionId();
return caseDefinitionId != null ? caseDefinitionId : buildingBlockDefinitionId();
}
}

}
Loading