diff --git a/app/src/androidTest/kotlin/com/syncflow/FullSyncEngineTest.kt b/app/src/androidTest/kotlin/com/syncflow/FullSyncEngineTest.kt index 31b1b8a..2cfdb17 100644 --- a/app/src/androidTest/kotlin/com/syncflow/FullSyncEngineTest.kt +++ b/app/src/androidTest/kotlin/com/syncflow/FullSyncEngineTest.kt @@ -236,7 +236,24 @@ class FullSyncEngineTest { assertEquals("newer local must win", "local-newer", remoteText("$remote/n.txt")) } - // ── 13. Content integrity: binary-ish bytes round-trip exactly ──────────── + // ── 13b. Special & non-ASCII filenames upload (WebDAV URL/header encoding) ─ + @Test fun specialAndNonAsciiNames_upload() = runBlocking { + val (pair, local, remote) = newPair("special", SyncDirection.UPLOAD_ONLY, DeleteBehavior.KEEP) + write(local, "naïve café.txt", "accents") // non-ASCII (broke MOVE Destination header) + write(local, "a&b (1).txt", "ampersand") // & ( ) space + write(local, "日本語.txt", "cjk") // multibyte unicode + write(local, "my photo.txt", "space") + val r = sync(pair) + assertEquals("all special-name files must upload", 4, r.uploaded) + assertEquals(0, r.failedFiles) + val names = remoteNames(remote) + assertTrue("naïve café.txt" in names) + assertTrue("a&b (1).txt" in names) + assertTrue("日本語.txt" in names) + assertTrue("my photo.txt" in names) + } + + // ── 14. Content integrity: binary-ish bytes round-trip exactly ──────────── @Test fun contentIntegrity_roundTrip() = runBlocking { val (pair, local, remote) = newPair("integrity", SyncDirection.TWO_WAY) val payload = (0..5000).joinToString("") { "Ω$it·" } diff --git a/app/src/main/kotlin/com/syncflow/data/providers/webdav/WebDavProvider.kt b/app/src/main/kotlin/com/syncflow/data/providers/webdav/WebDavProvider.kt index 9851d5e..a5f3887 100644 --- a/app/src/main/kotlin/com/syncflow/data/providers/webdav/WebDavProvider.kt +++ b/app/src/main/kotlin/com/syncflow/data/providers/webdav/WebDavProvider.kt @@ -181,7 +181,13 @@ open class WebDavProvider(protected val account: CloudAccount) : CloudProvider { } } - protected fun url(path: String) = "$baseUrl/${path.trimStart('/')}" + // Build a properly percent-encoded URL. addPathSegments encodes each segment (spaces, + // ampersands, and — critically — non-ASCII like "café"), which keeps OkHttp from rejecting + // non-ASCII in the WebDAV MOVE "Destination" header and avoids malformed request URLs. + protected fun url(path: String): String { + val base = baseUrl.toHttpUrlOrNull() ?: return "$baseUrl/${path.trimStart('/')}" + return base.newBuilder().addPathSegments(path.trimStart('/')).build().toString() + } private fun parsePropfind(xml: String, parentPath: String, dropFirst: Boolean = true): List { val results = mutableListOf()