Skip to content

Commit 5d8fe1d

Browse files
authored
Merge pull request #91 from swisscom/bugfix/46506-directory-check-on-windows
#46506 fix handling of directory check on Windows system
2 parents 4a6cb68 + 8fea153 commit 5d8fe1d

9 files changed

Lines changed: 613 additions & 75 deletions

File tree

cdr-client-common/src/main/kotlin/com/swisscom/health/des/cdr/client/common/DTOs.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ class DTOs {
111111
enum class ValidationMessageKey {
112112
NOT_A_DIRECTORY,
113113
DIRECTORY_NOT_FOUND,
114+
ILLEGAL_VALUE,
114115
NOT_READ_WRITABLE,
115116
LOCAL_DIR_OVERLAPS_WITH_SOURCE_DIRS,
116117
LOCAL_DIR_OVERLAPS_WITH_TARGET_DIRS,

cdr-client-service/src/main/kotlin/com/swisscom/health/des/cdr/client/handler/ConfigValidationService.kt

Lines changed: 289 additions & 71 deletions
Large diffs are not rendered by default.

cdr-client-service/src/main/kotlin/com/swisscom/health/des/cdr/client/http/WebOperations.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ import org.springframework.web.bind.annotation.RequestBody
4545
import org.springframework.web.bind.annotation.RequestParam
4646
import org.springframework.web.bind.annotation.RestController
4747
import java.net.URI
48-
import java.nio.file.Path
4948
import java.time.Instant
5049

5150
private val logger = KotlinLogging.logger {}
@@ -140,14 +139,16 @@ internal class WebOperations(
140139
* query parameter and their results are combined.
141140
*
142141
* @param config the CDR Client configuration to use for validating single use of directories
143-
* @param directory the directory to validate
142+
* @param directory the directory to validate (as a String to handle invalid paths gracefully)
144143
* @param validations the list of validation types to perform on the directory
145144
* @return a [DTOs.ValidationResult] indicating the result of the validation
146145
*/
147146
@PutMapping("api/validate-directory")
148147
internal suspend fun validateDirectory(
149148
@RequestBody config: DTOs.CdrClientConfig,
150-
@RequestParam(name = "dir") directory: Path,
149+
// don't use java.nio.file.Path here to allow the endpoint to handle invalid paths (e.g., with trailing spaces on Windows) gracefully
150+
// and return validation errors instead of throwing exceptions
151+
@RequestParam(name = "dir") directory: String,
151152
@RequestParam(name = "validation") validations: List<DomainObjects.ValidationType>,
152153
): ResponseEntity<ValidationResult> = runCatching {
153154
logger.debug { "validating dir: '$directory', validations: '$validations'" }

cdr-client-service/src/test/kotlin/com/swisscom/health/des/cdr/client/handler/ConfigValidationServiceTest.kt

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@file:Suppress("LargeClass")
2+
13
package com.swisscom.health.des.cdr.client.handler
24

35
import com.swisscom.health.des.cdr.client.common.DTOs
@@ -574,6 +576,35 @@ internal class ConfigValidationServiceTest {
574576
}
575577
}
576578

579+
@OptIn(ExperimentalPathApi::class)
580+
@Test
581+
fun `validateDirectoriesAreAbsolute should fail when sourceArchiveEnabled is true but archive folder is null`() {
582+
val connector = Connector(
583+
connectorId = ConnectorId("test-connector-archive-null"),
584+
sourceFolder = sourceFolder0,
585+
targetFolder = targetFolder0,
586+
contentType = "application/xml",
587+
mode = CdrClientConfig.Mode.TEST,
588+
docTypeFolders = emptyMap(),
589+
sourceErrorFolder = null,
590+
sourceArchiveFolder = null,
591+
sourceArchiveEnabled = true
592+
)
593+
594+
val config = createCdrClientConfig(listOf(connector))
595+
val service = ConfigValidationService(config)
596+
val result = service.validateDirectoriesAreAbsolute(config.toDto())
597+
598+
assertInstanceOf<DTOs.ValidationResult.Failure>(result)
599+
assertEquals(1, result.validationDetails.size)
600+
601+
val validationDetail = result.validationDetails.first()
602+
assertInstanceOf<DTOs.ValidationDetail.ConfigItemDetail>(validationDetail)
603+
val configItemDetail = validationDetail
604+
assertEquals(DomainObjects.ConfigurationItem.ARCHIVE_DIRECTORY, configItemDetail.configItem)
605+
assertEquals(DTOs.ValidationMessageKey.NOT_A_DIRECTORY, configItemDetail.messageKey)
606+
}
607+
577608
private fun createCdrClientConfig(customers: List<Connector>, defaultLocalFolder: Path = localFolder0): CdrClientConfig =
578609
CdrClientConfig(
579610
fileSynchronizationEnabled = FileSynchronization.ENABLED,
@@ -730,4 +761,97 @@ internal class ConfigValidationServiceTest {
730761
Files.deleteIfExists(tempDir)
731762
}
732763
}
764+
765+
@Test
766+
fun `test validation handles path with trailing space gracefully`() {
767+
val tempSourceDir = Files.createTempDirectory("testSourceDir")
768+
val tempTargetDir = Files.createTempDirectory("testTargetDir")
769+
try {
770+
// Create connector with valid paths
771+
val connector = Connector(
772+
connectorId = ConnectorId("test-connector"),
773+
sourceFolder = tempSourceDir,
774+
targetFolder = tempTargetDir,
775+
contentType = "application/xml",
776+
mode = CdrClientConfig.Mode.TEST,
777+
docTypeFolders = emptyMap(),
778+
sourceErrorFolder = null,
779+
sourceArchiveFolder = null,
780+
sourceArchiveEnabled = false
781+
)
782+
783+
val config = createCdrClientConfig(listOf(connector))
784+
val service = ConfigValidationService(config)
785+
786+
// Now create a DTO with paths that have trailing spaces
787+
val configDto = config.toDto()
788+
val connectorWithTrailingSpace = configDto.customer.first().copy(
789+
sourceFolder = "$tempSourceDir ",
790+
targetFolder = "$tempTargetDir "
791+
)
792+
val configDtoWithTrailingSpace = configDto.copy(
793+
customer = listOf(connectorWithTrailingSpace)
794+
)
795+
796+
val result = service.validateAllConfigurationItems(configDtoWithTrailingSpace)
797+
798+
// The paths with trailing spaces should FAIL validation
799+
// On Windows: InvalidPathException -> null path -> directory not found
800+
// On Unix: Path created with trailing space that doesn't exist -> directory not found
801+
assertInstanceOf<DTOs.ValidationResult.Failure>(result)
802+
val failure = result
803+
assertTrue(failure.validationDetails.isNotEmpty())
804+
// Should have at least one path detail with directory not found
805+
assertTrue(failure.validationDetails.any {
806+
it is DTOs.ValidationDetail.PathDetail &&
807+
it.messageKey == DTOs.ValidationMessageKey.DIRECTORY_NOT_FOUND
808+
})
809+
} finally {
810+
Files.deleteIfExists(tempSourceDir)
811+
Files.deleteIfExists(tempTargetDir)
812+
}
813+
}
814+
815+
@Test
816+
fun `test validation handles UNC paths without NullPointerException`() {
817+
val tempSourceDir = Files.createTempDirectory("testSourceDir")
818+
val tempTargetDir = Files.createTempDirectory("testTargetDir")
819+
try {
820+
// Create connector with a UNC path (this simulates Windows network share paths)
821+
val connector = Connector(
822+
connectorId = ConnectorId("test-connector"),
823+
sourceFolder = tempSourceDir,
824+
targetFolder = tempTargetDir,
825+
contentType = "application/xml",
826+
mode = CdrClientConfig.Mode.TEST,
827+
docTypeFolders = emptyMap(),
828+
sourceErrorFolder = null,
829+
sourceArchiveFolder = null,
830+
sourceArchiveEnabled = false
831+
)
832+
833+
val config = createCdrClientConfig(listOf(connector))
834+
val service = ConfigValidationService(config)
835+
836+
// Create a DTO with a UNC path (Windows network share)
837+
val configDto = config.toDto()
838+
val connectorWithUncPath = configDto.customer.first().copy(
839+
sourceFolder = """\\cdrintstpublic.file.core.windows.net\test-file-share"""
840+
)
841+
val configDtoWithUncPath = configDto.copy(
842+
customer = listOf(connectorWithUncPath)
843+
)
844+
845+
// This should not throw NullPointerException when checking fileName
846+
// It should return a validation error (directory not found)
847+
val result = service.validateAllConfigurationItems(configDtoWithUncPath)
848+
849+
// The UNC path likely won't exist, so we expect a failure
850+
// The important thing is that it doesn't crash with NullPointerException
851+
assertInstanceOf<DTOs.ValidationResult.Failure>(result)
852+
} finally {
853+
Files.deleteIfExists(tempSourceDir)
854+
Files.deleteIfExists(tempTargetDir)
855+
}
856+
}
733857
}

cdr-client-ui/build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ kotlin {
1818

1919
sourceSets {
2020
val desktopMain by getting
21+
val desktopTest by getting
2122

2223
commonMain.dependencies {
2324
implementation(compose.runtime)
@@ -51,6 +52,12 @@ kotlin {
5152
implementation(libs.conveyor.control)
5253
implementation(libs.compose.native.tray)
5354
}
55+
56+
desktopTest.dependencies {
57+
implementation(kotlin("test"))
58+
implementation(libs.mockk)
59+
implementation(libs.kotlinx.coroutines.test)
60+
}
5461
}
5562
}
5663

cdr-client-ui/src/commonMain/composeResources/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,5 @@
102102
<string name="error_folder_needs_absolute_path_error">Directories need to be defined with an absolute path (e.g. C:\home\myuser\archive)</string>
103103
<string name="error_proxy_url_must_start_with_http_or_https">Proxy URL must start with http:// or https://</string>
104104
<string name="error_proxy_url_invalid_format">Invalid proxy URL format</string>
105+
<string name="error_illegal_value">Value is not valid/can't be used here</string>
105106
</resources>

cdr-client-ui/src/commonMain/kotlin/com/swisscom/health/des/cdr/client/ui/CdrConfigViewRemoteValidations.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@ import java.nio.file.Path
1212

1313
private val logger = KotlinLogging.logger {}
1414

15+
/**
16+
* Safely compares two path strings, handling invalid paths (e.g., with trailing spaces on Windows).
17+
* Returns true if the paths are equal, false otherwise or if either path is invalid.
18+
*/
19+
private fun pathsEqual(path1: String, path2: String): Boolean = runCatching {
20+
Path.of(path1) == Path.of(path2)
21+
}.getOrElse {
22+
// If either path is invalid (e.g., trailing spaces on Windows), fall back to string comparison
23+
path1 == path2
24+
}
25+
1526
private typealias ValidationErrorHandler = suspend (Map<String, Any>, DomainObjects.ConfigurationItem) -> DTOs.ValidationResult
1627
private typealias ValidationSuccessHandler<T> = suspend (T, DomainObjects.ConfigurationItem) -> DTOs.ValidationResult
1728

@@ -99,7 +110,7 @@ internal class CdrConfigViewRemoteValidations(
99110
// check if any path validation error is matching the path we are validating; if yes, return the failure
100111
if (validationDetails
101112
.any { validationDetail: DTOs.ValidationDetail ->
102-
validationDetail is DTOs.ValidationDetail.PathDetail && path != null && Path.of(validationDetail.path) == Path.of(path)
113+
validationDetail is DTOs.ValidationDetail.PathDetail && path != null && pathsEqual(validationDetail.path, path)
103114
}
104115
)
105116
this

cdr-client-ui/src/commonMain/kotlin/com/swisscom/health/des/cdr/client/ui/ComposeUtils.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import com.swisscom.health.des.cdr.client.ui.cdr_client_ui.generated.resources.e
6767
import com.swisscom.health.des.cdr.client.ui.cdr_client_ui.generated.resources.error_proxy_url_must_start_with_http_or_https
6868
import com.swisscom.health.des.cdr.client.ui.cdr_client_ui.generated.resources.error_test_timeout_too_long
6969
import com.swisscom.health.des.cdr.client.ui.cdr_client_ui.generated.resources.error_value_is_mandatory
70+
import com.swisscom.health.des.cdr.client.ui.cdr_client_ui.generated.resources.error_illegal_value
7071
import io.github.oshai.kotlinlogging.KotlinLogging
7172
import org.jetbrains.compose.resources.StringResource
7273
import org.jetbrains.compose.resources.painterResource
@@ -115,6 +116,7 @@ internal val DTOs.ValidationMessageKey.stringResource: StringResource
115116
DTOs.ValidationMessageKey.DIRECTORY_NEEDS_ABSOLUTE_PATH -> Res.string.error_folder_needs_absolute_path_error
116117
DTOs.ValidationMessageKey.PROXY_URL_MUST_START_WITH_HTTP_OR_HTTPS -> Res.string.error_proxy_url_must_start_with_http_or_https
117118
DTOs.ValidationMessageKey.PROXY_URL_INVALID_FORMAT -> Res.string.error_proxy_url_invalid_format
119+
DTOs.ValidationMessageKey.ILLEGAL_VALUE -> Res.string.error_illegal_value
118120
}
119121

120122
@Composable

0 commit comments

Comments
 (0)