diff --git a/app/gzac/src/main/resources/config/application.yml b/app/gzac/src/main/resources/config/application.yml index b6076a8013..c74157633b 100644 --- a/app/gzac/src/main/resources/config/application.yml +++ b/app/gzac/src/main/resources/config/application.yml @@ -95,6 +95,10 @@ spring: messages: basename: i18n/messages main.allow-bean-definition-overriding: true + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB jersey: application-path: /api/camunda-rest autoconfigure: @@ -219,8 +223,9 @@ valtimo: acceptedMimeTypes: - text/plain - application/pdf - - image/jpeg - application/xml + - image/jpeg + - image/png changelog: pbac: clear-tables: true diff --git a/zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/DocumentenApiPlugin.kt b/zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/DocumentenApiPlugin.kt index 4d87ae8340..0d4db7723c 100644 --- a/zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/DocumentenApiPlugin.kt +++ b/zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/DocumentenApiPlugin.kt @@ -20,8 +20,11 @@ import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.convertValue import com.ritense.documentenapi.DocumentenApiPlugin.Companion.PLUGIN_KEY +import com.ritense.documentenapi.client.BestandsdelenRequest import com.ritense.documentenapi.client.CreateDocumentRequest +import com.ritense.documentenapi.client.CreateDocumentResult import com.ritense.documentenapi.client.DocumentInformatieObject +import com.ritense.documentenapi.client.DocumentLock import com.ritense.documentenapi.client.DocumentStatusType import com.ritense.documentenapi.client.DocumentenApiClient import com.ritense.documentenapi.client.PatchDocumentRequest @@ -135,7 +138,7 @@ class DocumentenApiPlugin( val contentAsInputStream = storageService.getResourceContentAsInputStream(resourceId) val metadata = storageService.getResourceMetadata(resourceId) - storeDocument( + val documentCreateResult = storeDocument( execution = execution, metadata = metadata, titel = null, @@ -148,6 +151,29 @@ class DocumentenApiPlugin( informatieobjecttype = null, storedDocumentUrl = DOCUMENT_URL_PROCESS_VAR, ) + + } + + @PluginAction( + key = "store-uploaded-document-in-parts", + title = "Store uploaded document in parts", + description = "Store an uploaded document in the Documenten API in parts using the bestandsdelen api", + activityTypes = [ActivityTypeWithEventName.SERVICE_TASK_START] + ) + fun storeUploadedDocumentInParts( + execution: DelegateExecution + ) { + val resourceId = execution.getVariable(RESOURCE_ID_PROCESS_VAR) as String? + ?: throw IllegalStateException("Failed to store document. No process variable '$RESOURCE_ID_PROCESS_VAR' found.") + val contentAsInputStream = storageService.getResourceContentAsInputStream(resourceId) + val metadata = storageService.getResourceMetadata(resourceId) + + storeDocumentInParts( + execution = execution, + metadata = metadata, + bestandsnaam = metadata["filename"].toString(), + inhoudAsInputStream = contentAsInputStream, + ) } @PluginAction( @@ -245,7 +271,7 @@ class DocumentenApiPlugin( beschrijving: String?, informatieobjecttype: String?, storedDocumentUrl: String, - ) { + ): CreateDocumentResult { val vertrouwelijkheidaanduidingEnum = Vertrouwelijkheid.fromKey( vertrouwelijkheidaanduiding ?: getMetadataField( metadata, @@ -295,6 +321,59 @@ class DocumentenApiPlugin( "Failed to set the $DOWNLOAD_URL_PROCESS_VAR variable in the DelegateExecution", e ) } + + return documentCreateResult + } + + /** + * Using the bestandsdelen api a document can be uploaded in chunks. This upload method entails several api calls + * to store a document: + * - First the document metadata is uploaded without the 'inhoud' parameter. The response of this method will + * contain a 'lock' parameter that must be used in the next call + * - Using the provided lock the contents of the file is uploaded to the bestandsdelen api + * - When the complete file is uploaded the unlock api must be called. This will unlock the document enabling it + * for download. + */ + private fun storeDocumentInParts( + execution: DelegateExecution, + metadata: Map, + bestandsnaam: String, + inhoudAsInputStream: InputStream, + ) { + val documentCreateResult = storeDocument( + execution = execution, + metadata = metadata, + titel = null, + vertrouwelijkheidaanduiding = null, + status = null, + taal = null, + bestandsnaam = bestandsnaam, + inhoudAsInputStream = InputStream.nullInputStream(), + beschrijving = null, + informatieobjecttype = null, + storedDocumentUrl = "" + ) + + val bestandsdelenRequest = BestandsdelenRequest( + inhoud = inhoudAsInputStream, + lock = documentCreateResult.getLockFromBestandsdelen() + ) + + client.storeDocumentInParts( + authenticationPluginConfiguration, + url, + bestandsdelenRequest, + documentCreateResult, + bestandsnaam + ) + + val documentLock = DocumentLock(documentCreateResult.getLockFromBestandsdelen()) + client.unlockInformatieObject( + authenticationPluginConfiguration, + URI.create(documentCreateResult.url), + documentLock + ) + } private fun getDocumentenApiPluginByInformatieobjectUrl(informatieobjectUrl: URI): PluginConfiguration { diff --git a/zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/client/Bestandsdeel.kt b/zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/client/Bestandsdeel.kt new file mode 100644 index 0000000000..ef14e49246 --- /dev/null +++ b/zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/client/Bestandsdeel.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2015-2024 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.documentenapi.client + +data class Bestandsdeel( + val url: String, + val volgnummer: Int, + val omvang: Int, + val voltooid: Boolean, + val lock: String +) \ No newline at end of file diff --git a/zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/client/BestandsdelenRequest.kt b/zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/client/BestandsdelenRequest.kt new file mode 100644 index 0000000000..dd27f3932e --- /dev/null +++ b/zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/client/BestandsdelenRequest.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2015-2024 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.documentenapi.client + +import java.io.InputStream + +data class BestandsdelenRequest( + val inhoud: InputStream, + val lock: String +) \ No newline at end of file diff --git a/zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/client/BestandsdelenResult.kt b/zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/client/BestandsdelenResult.kt new file mode 100644 index 0000000000..058f83e851 --- /dev/null +++ b/zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/client/BestandsdelenResult.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2015-2024 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. + */ + +data class BestandsdelenResult( + var url: String?, + var lock: String?, + val voltooid: Boolean?, + val volgnummer: Int?, + val statusCode: Int?, + val errorMessage: String? +) \ No newline at end of file diff --git a/zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/client/CreateDocumentResult.kt b/zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/client/CreateDocumentResult.kt index 58fcc2260e..801a0a5b00 100644 --- a/zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/client/CreateDocumentResult.kt +++ b/zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/client/CreateDocumentResult.kt @@ -24,4 +24,17 @@ class CreateDocumentResult( val bestandsnaam: String, val bestandsomvang: Long, val beginRegistratie: LocalDateTime, -) \ No newline at end of file + val bestandsdelen: List +) { + fun getLockFromBestandsdelen(): String { + if (bestandsdelen.isEmpty()) { + return "" + } + + return bestandsdelen[0].lock + } + + fun getDocumentUUIDFromUrl(): String { + return url.substring(url.lastIndexOf("/") + 1) + } +} \ No newline at end of file diff --git a/zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/client/DocumentenApiClient.kt b/zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/client/DocumentenApiClient.kt index c460da7f9b..45a7fa1bdd 100644 --- a/zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/client/DocumentenApiClient.kt +++ b/zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/client/DocumentenApiClient.kt @@ -16,9 +16,11 @@ package com.ritense.documentenapi.client +import BestandsdelenResult import com.fasterxml.jackson.databind.ObjectMapper import com.ritense.documentenapi.DocumentenApiAuthentication import com.ritense.documentenapi.domain.DocumentenApiColumnKey +import com.ritense.documentenapi.domain.FileUploadPart import com.ritense.documentenapi.event.DocumentDeleted import com.ritense.documentenapi.event.DocumentInformatieObjectDownloaded import com.ritense.documentenapi.event.DocumentInformatieObjectViewed @@ -30,6 +32,7 @@ import com.ritense.outbox.OutboxService import com.ritense.zgw.ClientTools import com.ritense.zgw.ClientTools.Companion.optionalQueryParam import com.ritense.zgw.Page +import mu.KotlinLogging import org.springframework.core.io.Resource import org.springframework.data.domain.PageImpl import org.springframework.data.domain.Pageable @@ -72,6 +75,42 @@ class DocumentenApiClient( return result } + fun storeDocumentInParts( + authentication: DocumentenApiAuthentication, + baseUrl: URI, + request: BestandsdelenRequest, + createDocumentResult: CreateDocumentResult, + bestandsnaam: String + ) { + // Inside the CreateDocumentResult there is an array of bestandsdelen. + // Each bestandsdeel needs to be sent separately + // So the documenten api determines the amount (and size) of chunks, not this application. + logger.info { "Starting upload of file $bestandsnaam in ${createDocumentResult.bestandsdelen.size} chunks" } + + createDocumentResult.bestandsdelen.forEach { bestandsdeel -> + logger.debug { "Sending chunk #${bestandsdeel.volgnummer} for a size of ${bestandsdeel.omvang} bytes" } + + val body = FileUploadPart(bestandsdeel, request, bestandsnaam) + .createBody() + + restClient(authentication) + .put() + .uri { + ClientTools.baseUrlToBuilder(it, baseUrl) + .path("bestandsdelen/{uuid}") + .build(bestandsdeel.url.substring(bestandsdeel.url.lastIndexOf("/") + 1)) + } + .contentType(MediaType.MULTIPART_FORM_DATA) + .body(body) + .retrieve() + .body()!! + } + + check(request.inhoud.read() == -1) { + "Failed to upload the full file. The file is larger than the sum of the omvang of all bestandsdelen." + } + } + fun getInformatieObject( authentication: DocumentenApiAuthentication, baseUrl: URI, @@ -96,6 +135,7 @@ class DocumentenApiClient( objectMapper.valueToTree(result) ) } + return result } @@ -107,12 +147,8 @@ class DocumentenApiClient( ): org.springframework.data.domain.Page { // because the documenten api only supports a fixed page size, we will try to calculate the page we need to request // the only page sizes that are supported are those that can fit n times in the itemsPerPage - if (ITEMS_PER_PAGE % pageable.pageSize != 0) { - throw IllegalArgumentException("Page size is not supported") - } - if (documentSearchRequest.zaakUrl == null) { - throw IllegalArgumentException("Zaak URL is required") - } + require(ITEMS_PER_PAGE % pageable.pageSize == 0) { "Page size is not supported" } + requireNotNull(documentSearchRequest.zaakUrl) { "Zaak URL is required" } val pageToRequest = ((pageable.pageSize * pageable.pageNumber) / ITEMS_PER_PAGE) + 1 val result = @@ -297,5 +333,6 @@ class DocumentenApiClient( companion object { const val ITEMS_PER_PAGE = 100 + val logger = KotlinLogging.logger {} } } diff --git a/zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/domain/FileUploadPart.kt b/zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/domain/FileUploadPart.kt new file mode 100644 index 0000000000..ab779c17f2 --- /dev/null +++ b/zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/domain/FileUploadPart.kt @@ -0,0 +1,42 @@ +package com.ritense.documentenapi.domain + +import com.ritense.documentenapi.client.Bestandsdeel +import com.ritense.documentenapi.client.BestandsdelenRequest +import org.springframework.core.io.ByteArrayResource +import org.springframework.util.LinkedMultiValueMap +import org.springframework.util.MultiValueMap + +data class FileUploadPart( + val bestandsdeel: Bestandsdeel, + val bestandsdelenRequest: BestandsdelenRequest, + val bestandsnaam: String, +) { + + fun createBody(): MultiValueMap { + val chunk = ByteArray(bestandsdeel.omvang) + val bytesRead = bestandsdelenRequest.inhoud.read(chunk) + + require(bytesRead == chunk.size) { + "Failed to read all the bytes to upload. " + + "Expected ${chunk.size} bytes, but only read $bytesRead bytes. " + + "Check bestandsdeel: $bestandsdeel." + } + + return createMultiValueMap(createFileResource(chunk), bestandsdelenRequest.lock) + } + + private fun createFileResource(chunk: ByteArray): ByteArrayResource { + return object : ByteArrayResource(chunk) { + override fun getFilename(): String { + return bestandsnaam + } + } + } + + private fun createMultiValueMap(fileResource: ByteArrayResource, lock: Any): MultiValueMap { + return LinkedMultiValueMap().apply { + add("inhoud", fileResource) + add("lock", lock) + } + } +} diff --git a/zgw/documenten-api/src/test/kotlin/com/ritense/documentenapi/DocumentenApiPluginIT.kt b/zgw/documenten-api/src/test/kotlin/com/ritense/documentenapi/DocumentenApiPluginIT.kt index 6373123617..ac568c2319 100644 --- a/zgw/documenten-api/src/test/kotlin/com/ritense/documentenapi/DocumentenApiPluginIT.kt +++ b/zgw/documenten-api/src/test/kotlin/com/ritense/documentenapi/DocumentenApiPluginIT.kt @@ -32,6 +32,11 @@ import com.ritense.processlink.domain.ActivityTypeWithEventName import com.ritense.resource.domain.MetadataType import com.ritense.resource.service.TemporaryResourceStorageService import jakarta.transaction.Transactional +import java.time.LocalDate +import java.util.Optional +import java.util.UUID +import kotlin.test.assertEquals +import kotlin.test.assertNotNull import okhttp3.mockwebserver.Dispatcher import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer @@ -52,11 +57,6 @@ import org.springframework.web.reactive.function.client.ClientRequest import org.springframework.web.reactive.function.client.ClientResponse import org.springframework.web.reactive.function.client.ExchangeFunction import reactor.core.publisher.Mono -import java.time.LocalDate -import java.util.Optional -import java.util.UUID -import kotlin.test.assertEquals -import kotlin.test.assertNotNull @Transactional internal class DocumentenApiPluginIT @Autowired constructor( @@ -323,7 +323,8 @@ internal class DocumentenApiPluginIT @Autowired constructor( "datum": "2019-08-24" }, "informatieobjecttype": "http://example.com", - "locked": true + "locked": true, + "bestandsdelen": [] } """.trimIndent() return mockResponse(body) diff --git a/zgw/documenten-api/src/test/kotlin/com/ritense/documentenapi/DocumentenApiPluginTest.kt b/zgw/documenten-api/src/test/kotlin/com/ritense/documentenapi/DocumentenApiPluginTest.kt index 633a209fcf..dc3a1d0532 100644 --- a/zgw/documenten-api/src/test/kotlin/com/ritense/documentenapi/DocumentenApiPluginTest.kt +++ b/zgw/documenten-api/src/test/kotlin/com/ritense/documentenapi/DocumentenApiPluginTest.kt @@ -55,6 +55,7 @@ import kotlin.test.assertNull internal class DocumentenApiPluginTest { lateinit var pluginService: PluginService + lateinit var client: DocumentenApiClient @BeforeEach fun setUp() { @@ -64,7 +65,7 @@ internal class DocumentenApiPluginTest { key = "key", description = "description", fullyQualifiedClassName = "className", - title= "title" + title = "title" ) val pluginConfiguration = PluginConfiguration( id = PluginConfigurationId(UUID.randomUUID()), @@ -72,13 +73,14 @@ internal class DocumentenApiPluginTest { title = "title" ) whenever(pluginService.findPluginConfiguration(any(), any())).thenReturn(pluginConfiguration) + + client = mock() } @Test fun `should call client to store file`() { - val client: DocumentenApiClient = mock() val storageService: TemporaryResourceStorageService = mock() - val applicationEventPublisher: ApplicationEventPublisher= mock() + val applicationEventPublisher: ApplicationEventPublisher = mock() val authenticationMock = mock() val objectMapper = MapperSingleton.get() val documentenApiVersionService: DocumentenApiVersionService = mock() @@ -92,7 +94,8 @@ internal class DocumentenApiPluginTest { "returnedAuthor", "returnedFileName", 1L, - LocalDateTime.of(2020, 1, 1, 1, 1, 1) + LocalDateTime.of(2020, 1, 1, 1, 1, 1), + listOf() ) whenever(executionMock.getVariable("localDocumentVariableName")) @@ -117,12 +120,14 @@ internal class DocumentenApiPluginTest { whenever(pluginConfiguration.id).thenReturn(pluginConfigurationId) whenever(pluginConfigurationId.id).thenReturn(UUID.randomUUID()) - val pluginAnnotation:Plugin = mock() + val pluginAnnotation: Plugin = mock() whenever(pluginAnnotation.key).thenReturn("documentenApiPluginKey") - whenever(pluginService.findPluginConfiguration( - eq(DocumentenApiPlugin::class.java), - any() - )).thenReturn(pluginConfiguration) + whenever( + pluginService.findPluginConfiguration( + eq(DocumentenApiPlugin::class.java), + any() + ) + ).thenReturn(pluginConfiguration) plugin.storeTemporaryDocument( executionMock, @@ -166,9 +171,8 @@ internal class DocumentenApiPluginTest { @Test fun `should call client to store file after document upload`() { - val client: DocumentenApiClient = mock() val storageService: TemporaryResourceStorageService = mock() - val applicationEventPublisher: ApplicationEventPublisher= mock() + val applicationEventPublisher: ApplicationEventPublisher = mock() val authenticationMock = mock() val documentenApiVersionService: DocumentenApiVersionService = mock() val pluginConfiguration: PluginConfiguration = mock() @@ -181,7 +185,8 @@ internal class DocumentenApiPluginTest { "returnedAuthor", "returnedFileName", 1L, - LocalDateTime.now() + LocalDateTime.now(), + listOf() ) whenever(executionMock.getVariable(RESOURCE_ID_PROCESS_VAR)) @@ -189,28 +194,34 @@ internal class DocumentenApiPluginTest { whenever(storageService.getResourceContentAsInputStream("localDocumentLocation")) .thenReturn(inputStream) whenever(storageService.getResourceMetadata("localDocumentLocation")) - .thenReturn(mapOf("title" to "title", - "confidentialityLevel" to "zaakvertrouwelijk", - "status" to "in_bewerking", - "author" to "author", - "language" to "taal", - "filename" to "test.ext", - "description" to "description", - "receiptDate" to "2022-09-15", - "sendDate" to "2022-09-16", - "description" to "description", - "informatieobjecttype" to "type")) + .thenReturn( + mapOf( + "title" to "title", + "confidentialityLevel" to "zaakvertrouwelijk", + "status" to "in_bewerking", + "author" to "author", + "language" to "taal", + "filename" to "test.ext", + "description" to "description", + "receiptDate" to "2022-09-15", + "sendDate" to "2022-09-16", + "description" to "description", + "informatieobjecttype" to "type" + ) + ) whenever(client.storeDocument(any(), any(), any())).thenReturn(result) whenever(pluginConfiguration.id).thenReturn(pluginConfigurationId) whenever(pluginConfigurationId.id).thenReturn(UUID.randomUUID()) - val pluginAnnotation:Plugin = mock() + val pluginAnnotation: Plugin = mock() whenever(pluginAnnotation.key).thenReturn("documentenApiPluginKey") - whenever(pluginService.findPluginConfiguration( - eq(DocumentenApiPlugin::class.java), - any() - )).thenReturn(pluginConfiguration) + whenever( + pluginService.findPluginConfiguration( + eq(DocumentenApiPlugin::class.java), + any() + ) + ).thenReturn(pluginConfiguration) val plugin = DocumentenApiPlugin( client, @@ -250,9 +261,8 @@ internal class DocumentenApiPluginTest { @Test fun `should call client to store file after document upload with minimal properties`() { - val client: DocumentenApiClient = mock() val storageService: TemporaryResourceStorageService = mock() - val applicationEventPublisher: ApplicationEventPublisher= mock() + val applicationEventPublisher: ApplicationEventPublisher = mock() val authenticationMock = mock() val documentenApiVersionService: DocumentenApiVersionService = mock() val pluginConfiguration: PluginConfiguration = mock() @@ -265,7 +275,8 @@ internal class DocumentenApiPluginTest { "returnedAuthor", "returnedFileName", 1L, - LocalDateTime.now() + LocalDateTime.now(), + listOf() ) whenever(executionMock.getVariable(RESOURCE_ID_PROCESS_VAR)) @@ -273,11 +284,15 @@ internal class DocumentenApiPluginTest { whenever(storageService.getResourceContentAsInputStream("localDocumentLocation")) .thenReturn(inputStream) whenever(storageService.getResourceMetadata("localDocumentLocation")) - .thenReturn(mapOf("title" to "title", - "status" to "in_bewerking", - "language" to "taal", - "filename" to "test.ext", - "informatieobjecttype" to "type")) + .thenReturn( + mapOf( + "title" to "title", + "status" to "in_bewerking", + "language" to "taal", + "filename" to "test.ext", + "informatieobjecttype" to "type" + ) + ) whenever(client.storeDocument(any(), any(), any())).thenReturn(result) val plugin = DocumentenApiPlugin( @@ -296,12 +311,14 @@ internal class DocumentenApiPluginTest { whenever(pluginConfiguration.id).thenReturn(pluginConfigurationId) whenever(pluginConfigurationId.id).thenReturn(UUID.randomUUID()) - val pluginAnnotation:Plugin = mock() + val pluginAnnotation: Plugin = mock() whenever(pluginAnnotation.key).thenReturn("documentenApiPluginKey") - whenever(pluginService.findPluginConfiguration( - eq(DocumentenApiPlugin::class.java), - any() - )).thenReturn(pluginConfiguration) + whenever( + pluginService.findPluginConfiguration( + eq(DocumentenApiPlugin::class.java), + any() + ) + ).thenReturn(pluginConfiguration) plugin.storeUploadedDocument(executionMock) @@ -328,9 +345,8 @@ internal class DocumentenApiPluginTest { @Test fun `should call client to get document`() { - val client: DocumentenApiClient = mock() val storageService: TemporaryResourceStorageService = mock() - val applicationEventPublisher: ApplicationEventPublisher= mock() + val applicationEventPublisher: ApplicationEventPublisher = mock() val authenticationMock = mock() val documentenApiVersionService: DocumentenApiVersionService = mock() diff --git a/zgw/documenten-api/src/test/kotlin/com/ritense/documentenapi/client/CreateDocumentResultTest.kt b/zgw/documenten-api/src/test/kotlin/com/ritense/documentenapi/client/CreateDocumentResultTest.kt new file mode 100644 index 0000000000..fbd9561cac --- /dev/null +++ b/zgw/documenten-api/src/test/kotlin/com/ritense/documentenapi/client/CreateDocumentResultTest.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2015-2024 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.documentenapi.client + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.time.LocalDateTime + +internal class CreateDocumentResultTest { + + @Test + fun `should get uuid from url`() { + val result = CreateDocumentResult( + "https://www.example.com/847789d3-a8b3-469a-ae01-a49a6bd21783", + "", + "", + 0L, + LocalDateTime.now(), + listOf() + ) + + assertEquals("847789d3-a8b3-469a-ae01-a49a6bd21783", result.getDocumentUUIDFromUrl()) + } + + @Test + fun `should get lock from bestanddelen`() { + val bestandsdeel = Bestandsdeel( + "https://www.example.com/", + 0, + 0, + true, + "847789d3-a8b3-469a-ae01-a49a6bd21783" + ) + val result = CreateDocumentResult( + "", + "", + "", + 0L, + LocalDateTime.now(), + listOf(bestandsdeel) + ) + + assertEquals("847789d3-a8b3-469a-ae01-a49a6bd21783", result.getLockFromBestandsdelen()) + } + + @Test + fun `should fail gracefully for lock when there are no bestandsdelen`() { + val result = CreateDocumentResult( + "", + "", + "", + 0L, + LocalDateTime.now(), + listOf() + ) + + assertEquals("", result.getLockFromBestandsdelen()) + } + +} \ No newline at end of file diff --git a/zgw/documenten-api/src/test/kotlin/com/ritense/documentenapi/client/DocumentenApiClientTest.kt b/zgw/documenten-api/src/test/kotlin/com/ritense/documentenapi/client/DocumentenApiClientTest.kt index 0c476ea5f8..8f73152639 100644 --- a/zgw/documenten-api/src/test/kotlin/com/ritense/documentenapi/client/DocumentenApiClientTest.kt +++ b/zgw/documenten-api/src/test/kotlin/com/ritense/documentenapi/client/DocumentenApiClientTest.kt @@ -35,9 +35,9 @@ import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.RecordedRequest import okio.Buffer -import org.assertj.core.api.Assertions import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -59,9 +59,11 @@ import org.springframework.web.reactive.function.client.ClientRequest import org.springframework.web.reactive.function.client.ClientResponse import org.springframework.web.reactive.function.client.ExchangeFunction import reactor.core.publisher.Mono +import java.io.InputStream import java.net.URI import java.time.LocalDate import java.time.LocalDateTime +import java.util.UUID import java.util.function.Supplier import kotlin.test.assertEquals import kotlin.test.assertIs @@ -132,7 +134,8 @@ DocumentenApiClientTest { "datum": "2019-08-24" }, "informatieobjecttype": "http://example.com", - "locked": true + "locked": true, + "bestandsdelen": [] } """.trimIndent() @@ -162,6 +165,62 @@ DocumentenApiClientTest { assertEquals("http://example.com", result.url) } + @Test + fun `should make put call for bestanddelen`() { + val restClientBuilder = RestClient.builder() + val client = DocumentenApiClient(restClientBuilder, outboxService, objectMapper, mock()) + + val request = BestandsdelenRequest( + inhoud = InputStream.nullInputStream(), + lock = UUID.randomUUID().toString() + ) + + val putResponseBody = """ + { + "url": "https://example.com/54ff8243-83f9-4fa3-a32e-29970db52ced", + "volgnummer": 1, + "omvang": 1234, + "voltooid": true, + "lock": "de9c883a-cdfc-493b-9c38-5824e334a1b1" + } + """.trimIndent() + + mockDocumentenApi.enqueue(mockResponse(putResponseBody)) + + val bestandsdelen = listOf( + Bestandsdeel( + "https://www.example.com", + 1, + 0, + false, + "de9c883a-cdfc-493b-9c38-5824e334a1b1" + ) + ) + + val createResult = CreateDocumentResult( + "url", + "auteur", + "bestandsnaam.jpg", + 0L, + LocalDateTime.now(), + bestandsdelen + ) + + client.storeDocumentInParts( + TestAuthentication(), + mockDocumentenApi.url("/").toUri(), + request, + createResult, + "bestand.jpg" + ) + + val recordedRequest = mockDocumentenApi.takeRequest() + assertNotNull(recordedRequest) + + assertEquals("Bearer test", recordedRequest.getHeader("Authorization")) + assertEquals("PUT", recordedRequest.method) + } + @Test fun `should send outbox message on saving document`() { val restClientBuilder = RestClient.builder() @@ -200,7 +259,8 @@ DocumentenApiClientTest { "datum": "2019-08-24" }, "informatieobjecttype": "http://example.com", - "locked": true + "locked": true, + "bestandsdelen": [] } """.trimIndent() @@ -233,9 +293,9 @@ DocumentenApiClientTest { val firstEventValue = eventCapture.firstValue.get() val mappedFirstEventResult: CreateDocumentResult = objectMapper.readValue(firstEventValue.result.toString()) - Assertions.assertThat(firstEventValue).isInstanceOf(DocumentStored::class.java) - Assertions.assertThat(firstEventValue.resultId.toString()).isEqualTo(documentURL) - Assertions.assertThat(mappedFirstEventResult.auteur).isEqualTo(result.auteur) + assertThat(firstEventValue).isInstanceOf(DocumentStored::class.java) + assertThat(firstEventValue.resultId.toString()).isEqualTo(documentURL) + assertThat(mappedFirstEventResult.auteur).isEqualTo(result.auteur) } @Test @@ -309,7 +369,8 @@ DocumentenApiClientTest { "datum": "2019-08-20" }, "informatieobjecttype": "http://example.com", - "locked": true + "locked": true, + "bestandsdelen": [] } """.trimIndent() @@ -381,7 +442,8 @@ DocumentenApiClientTest { "datum": "2019-08-20" }, "informatieobjecttype": "http://example.com", - "locked": true + "locked": true, + "bestandsdelen": [] } """.trimIndent() @@ -434,7 +496,7 @@ DocumentenApiClientTest { val buffer = Buffer() //buffer.writeUtf8("test") - buffer.write(byteArrayOf(72,73,32,84,79,77)) + buffer.write(byteArrayOf(72, 73, 32, 84, 79, 77)) mockDocumentenApi.enqueue(mockInputStreamResponse(buffer)) @@ -895,7 +957,7 @@ DocumentenApiClientTest { zaakUrl = URI("http://example.com/zaak/123"), ) val exception = assertThrows { - val documentSearchResult = doDocumentSearchRequest(pageable, documentSearchRequest, true) + doDocumentSearchRequest(pageable, documentSearchRequest, true) } assertEquals("Page size is not supported", exception.message) @@ -908,7 +970,7 @@ DocumentenApiClientTest { val documentSearchRequest = DocumentSearchRequest() val exception = assertThrows { - val documentSearchResult = doDocumentSearchRequest(pageable, documentSearchRequest, true) + doDocumentSearchRequest(pageable, documentSearchRequest, true) } assertEquals("Zaak URL is required", exception.message) @@ -988,7 +1050,7 @@ DocumentenApiClientTest { zaakUrl = URI("http://example.com/zaak/123"), ) assertThrows { - val documentSearchResult = doDocumentSearchRequest(pageable, documentSearchRequest, true) + doDocumentSearchRequest(pageable, documentSearchRequest, true) } } diff --git a/zgw/documenten-api/src/test/kotlin/com/ritense/documentenapi/domain/FileUploadPartTest.kt b/zgw/documenten-api/src/test/kotlin/com/ritense/documentenapi/domain/FileUploadPartTest.kt new file mode 100644 index 0000000000..9e7293dd81 --- /dev/null +++ b/zgw/documenten-api/src/test/kotlin/com/ritense/documentenapi/domain/FileUploadPartTest.kt @@ -0,0 +1,63 @@ +package com.ritense.documentenapi.domain + +import com.ritense.documentenapi.client.Bestandsdeel +import com.ritense.documentenapi.client.BestandsdelenRequest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.core.io.ByteArrayResource +import org.springframework.util.MultiValueMap +import java.io.ByteArrayInputStream +import java.io.InputStream + +class FileUploadPartTest { + + @Test + fun `createBody should return a valid MultiValueMap when all bytes are read successfully`() { + // Arrange + val bestandsdeel = Bestandsdeel( + url = "https://example.com/file", + omvang = 10, + volgnummer = 1, + voltooid = false, + lock = "test-lock" + ) + val inputStream: InputStream = ByteArrayInputStream(ByteArray(10) { 1 }) + val bestandsdelenRequest = BestandsdelenRequest(inputStream, lock = "test-lock") + val fileUploadPart = FileUploadPart(bestandsdeel, bestandsdelenRequest, "testfile.txt") + + // Act + val result: MultiValueMap = fileUploadPart.createBody() + + // Assert + assertNotNull(result) + assertEquals(2, result.size) + assertEquals("test-lock", result["lock"]?.firstOrNull()) + assertEquals("testfile.txt", (result["inhoud"]?.firstOrNull() as ByteArrayResource).filename) + } + + @Test + fun `createBody should throw an exception when not all bytes are read`() { + // Arrange + val bestandsdeel = Bestandsdeel( + url = "https://example.com/file", + omvang = 10, + volgnummer = 1, + voltooid = false, + lock = "test-lock" + ) + val inputStream: InputStream = ByteArrayInputStream(ByteArray(5) { 1 }) + val bestandsdelenRequest = BestandsdelenRequest(inputStream, lock = "test-lock") + val fileUploadPart = FileUploadPart(bestandsdeel, bestandsdelenRequest, "testfile.txt") + + // Act & Assert + val exception = assertThrows { + fileUploadPart.createBody() + } + assertEquals( + "Failed to read all the bytes to upload. Expected 10 bytes, but only read 5 bytes. Check bestandsdeel: $bestandsdeel.", + exception.message + ) + } +} diff --git a/zgw/documenten-api/src/test/kotlin/com/ritense/documentenapi/service/DocumentenApiServiceTest.kt b/zgw/documenten-api/src/test/kotlin/com/ritense/documentenapi/service/DocumentenApiServiceTest.kt index 9ecd6e9a52..20b4d03848 100644 --- a/zgw/documenten-api/src/test/kotlin/com/ritense/documentenapi/service/DocumentenApiServiceTest.kt +++ b/zgw/documenten-api/src/test/kotlin/com/ritense/documentenapi/service/DocumentenApiServiceTest.kt @@ -17,7 +17,6 @@ package com.ritense.documentenapi.service import com.ritense.authorization.AuthorizationService -import com.ritense.catalogiapi.domain.Informatieobjecttype import com.ritense.catalogiapi.service.CatalogiService import com.ritense.document.domain.Document import com.ritense.document.domain.impl.JsonSchemaDocumentDefinitionId @@ -28,22 +27,15 @@ import com.ritense.documentenapi.client.DocumentInformatieObject import com.ritense.documentenapi.domain.DocumentenApiVersion import com.ritense.documentenapi.repository.DocumentenApiColumnRepository import com.ritense.documentenapi.web.rest.dto.DocumentSearchRequest -import com.ritense.documentenapi.web.rest.dto.DocumentenApiDocumentDto import com.ritense.plugin.domain.PluginConfiguration import com.ritense.plugin.domain.PluginConfigurationId import com.ritense.plugin.domain.PluginDefinition -import com.ritense.plugin.domain.PluginProcessLink import com.ritense.plugin.service.PluginService -import com.ritense.processdocument.domain.impl.DocumentDefinitionProcessLink -import com.ritense.processdocument.domain.impl.DocumentDefinitionProcessLinkId -import com.ritense.valtimo.camunda.domain.CamundaProcessDefinition import com.ritense.zgw.Rsin import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertInstanceOf import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import org.springframework.data.domain.PageImpl