diff --git a/commons/build.gradle.kts b/commons/build.gradle.kts index 56ec463bd..37b51fa5c 100644 --- a/commons/build.gradle.kts +++ b/commons/build.gradle.kts @@ -104,6 +104,9 @@ kotlin { // Secure key storage via OS keychain (macOS/Windows/Linux) implementation(libs.java.keyring) + + // EXIF stripping for image uploads (used by service/upload/MediaCompressor). + implementation(libs.commons.imaging) } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopBlossomAuth.kt b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/BlossomAuth.kt similarity index 95% rename from desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopBlossomAuth.kt rename to commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/BlossomAuth.kt index b93214497..9f02b737e 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopBlossomAuth.kt +++ b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/BlossomAuth.kt @@ -18,14 +18,14 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.desktop.service.upload +package com.vitorpamplona.amethyst.commons.service.upload import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent import java.util.Base64 -object DesktopBlossomAuth { +object BlossomAuth { suspend fun createUploadAuth( hash: HexKey, size: Long, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopBlossomClient.kt b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/BlossomClient.kt similarity index 82% rename from desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopBlossomClient.kt rename to commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/BlossomClient.kt index 8bc6b99fe..2901a94f1 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopBlossomClient.kt +++ b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/BlossomClient.kt @@ -18,9 +18,8 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.desktop.service.upload +package com.vitorpamplona.amethyst.commons.service.upload -import com.vitorpamplona.amethyst.desktop.network.DesktopHttpClient import com.vitorpamplona.quartz.nip01Core.core.JsonMapper import com.vitorpamplona.quartz.nipB7Blossom.BlossomUploadResult import kotlinx.coroutines.Dispatchers @@ -34,11 +33,16 @@ import okio.BufferedSink import okio.source import java.io.File -class DesktopBlossomClient( - private val clientOverride: OkHttpClient? = null, +/** + * Blossom HTTP client for JVM consumers (desktop + CLI). Owns no global + * state — pass a configured [OkHttpClient] (e.g. desktop's Tor-aware + * `DesktopHttpClient.currentClient()`) for proxying / connection pooling. + * The default constructor uses a fresh OkHttpClient — fine for one-shot + * uses such as the CLI. + */ +class BlossomClient( + private val okHttpClient: OkHttpClient = OkHttpClient(), ) { - private val okHttpClient: OkHttpClient get() = clientOverride ?: DesktopHttpClient.currentClient() - suspend fun upload( file: File, contentType: String, @@ -72,7 +76,8 @@ class DesktopBlossomClient( val reason = it.headers["X-Reason"] ?: it.code.toString() throw RuntimeException("Upload failed ($serverBaseUrl): $reason") } - JsonMapper.fromJson(it.body.string()) + val body = it.body ?: throw RuntimeException("Upload to $serverBaseUrl returned no body") + JsonMapper.fromJson(body.string()) } } @@ -103,7 +108,8 @@ class DesktopBlossomClient( val reason = it.headers["X-Reason"] ?: it.code.toString() throw RuntimeException("Upload failed ($serverBaseUrl): $reason") } - JsonMapper.fromJson(it.body.string()) + val body = it.body ?: throw RuntimeException("Upload to $serverBaseUrl returned no body") + JsonMapper.fromJson(body.string()) } } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopMediaCompressor.kt b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/MediaCompressor.kt similarity index 95% rename from desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopMediaCompressor.kt rename to commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/MediaCompressor.kt index c197441d7..bd4621eb4 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopMediaCompressor.kt +++ b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/MediaCompressor.kt @@ -18,14 +18,14 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.desktop.service.upload +package com.vitorpamplona.amethyst.commons.service.upload import org.apache.commons.imaging.Imaging import org.apache.commons.imaging.formats.jpeg.exif.ExifRewriter import java.io.ByteArrayOutputStream import java.io.File -object DesktopMediaCompressor { +object MediaCompressor { fun stripExif(file: File): File { if (!file.name.lowercase().let { it.endsWith(".jpg") || it.endsWith(".jpeg") }) { return file diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopMediaMetadata.kt b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/MediaMetadata.kt similarity index 92% rename from desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopMediaMetadata.kt rename to commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/MediaMetadata.kt index 7981cb3a8..e1d60f046 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopMediaMetadata.kt +++ b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/MediaMetadata.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.desktop.service.upload +package com.vitorpamplona.amethyst.commons.service.upload import com.vitorpamplona.amethyst.commons.blurhash.toBlurhash import com.vitorpamplona.amethyst.commons.blurhash.toPlatformImage @@ -38,7 +38,12 @@ data class MediaMetadata( val thumbhash: String? = null, ) -object DesktopMediaMetadata { +/** + * Reads media metadata (sha256, size, mime-type, dimensions, blurhash, thumbhash) + * from a JVM [File]. Named to avoid colliding with the [MediaMetadata] data class + * in the same package. + */ +object MediaMetadataReader { fun compute(file: File): MediaMetadata { val bytes = file.readBytes() val hash = sha256(bytes).toHexKey() diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopUploadOrchestrator.kt b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/UploadOrchestrator.kt similarity index 90% rename from desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopUploadOrchestrator.kt rename to commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/UploadOrchestrator.kt index e22f00abc..54af97d0d 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopUploadOrchestrator.kt +++ b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/UploadOrchestrator.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.desktop.service.upload +package com.vitorpamplona.amethyst.commons.service.upload import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner @@ -39,8 +39,8 @@ data class EncryptedUploadResult( val encryptedSize: Int, ) -class DesktopUploadOrchestrator( - private val client: DesktopBlossomClient = DesktopBlossomClient(), +class UploadOrchestrator( + private val client: BlossomClient = BlossomClient(), ) { suspend fun upload( file: File, @@ -52,17 +52,17 @@ class DesktopUploadOrchestrator( // 1. Strip EXIF if requested (JPEG only) val processedFile = if (stripExif) { - DesktopMediaCompressor.stripExif(file) + MediaCompressor.stripExif(file) } else { file } // 2. Compute metadata (hash, dimensions, blurhash) - val metadata = DesktopMediaMetadata.compute(processedFile) + val metadata = MediaMetadataReader.compute(processedFile) // 3. Create auth header val authHeader = - DesktopBlossomAuth.createUploadAuth( + BlossomAuth.createUploadAuth( hash = metadata.sha256, size = metadata.size, alt = alt ?: "Uploading ${file.name}", @@ -98,7 +98,7 @@ class DesktopUploadOrchestrator( signer: NostrSigner, ): EncryptedUploadResult { // 1. Compute pre-encryption metadata (dimensions, blurhash, mime, originalHash) - val metadata = DesktopMediaMetadata.compute(file) + val metadata = MediaMetadataReader.compute(file) // 2. Read file bytes and encrypt val plaintext = file.readBytes() @@ -110,7 +110,7 @@ class DesktopUploadOrchestrator( // 4. Create Blossom auth with encrypted hash and size val authHeader = - DesktopBlossomAuth.createUploadAuth( + BlossomAuth.createUploadAuth( hash = encryptedHash, size = encryptedSize.toLong(), alt = "Encrypted upload", diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopUploadTracker.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopUploadTracker.kt index e79244ecf..3350957ed 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopUploadTracker.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopUploadTracker.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.desktop.service.upload +import com.vitorpamplona.amethyst.commons.service.upload.UploadResult import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt index bea6b8dd3..96a804554 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt @@ -51,12 +51,12 @@ import androidx.compose.ui.draganddrop.DragAndDropEvent import androidx.compose.ui.draganddrop.DragAndDropTarget import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog +import com.vitorpamplona.amethyst.commons.service.upload.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.upload.UploadResult import com.vitorpamplona.amethyst.desktop.DesktopPreferences import com.vitorpamplona.amethyst.desktop.account.AccountState import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager -import com.vitorpamplona.amethyst.desktop.service.upload.DesktopUploadOrchestrator import com.vitorpamplona.amethyst.desktop.service.upload.DesktopUploadTracker -import com.vitorpamplona.amethyst.desktop.service.upload.UploadResult import com.vitorpamplona.amethyst.desktop.ui.compose.ComposeRelayPicker import com.vitorpamplona.amethyst.desktop.ui.compose.RelayPickerState import com.vitorpamplona.amethyst.desktop.ui.media.ClipboardPasteHandler @@ -103,7 +103,7 @@ fun ComposeNoteDialog( val attachedFiles = remember { mutableStateListOf() } val uploadTracker = remember { DesktopUploadTracker() } val uploadState by uploadTracker.state.collectAsState() - val orchestrator = remember { DesktopUploadOrchestrator() } + val orchestrator = remember { UploadOrchestrator() } var selectedServer by remember { mutableStateOf(DesktopPreferences.preferredBlossomServer) } var postAsPicture by remember { mutableStateOf(false) } @@ -482,11 +482,11 @@ private fun buildPictureMetas(results: List): List + meta.height?.let { h -> + com.vitorpamplona.quartz.nip94FileMetadata.tags + .DimensionTag(w, h) + } }, hash = meta.sha256, size = meta.size.toInt(), diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/chats/ChatPane.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/chats/ChatPane.kt index 041626af1..07206b66a 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/chats/ChatPane.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/chats/ChatPane.kt @@ -86,6 +86,7 @@ import androidx.compose.ui.window.PopupProperties import com.vitorpamplona.amethyst.commons.model.IAccount import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider +import com.vitorpamplona.amethyst.commons.service.upload.UploadOrchestrator import com.vitorpamplona.amethyst.commons.ui.chat.ChatMessageCompose import com.vitorpamplona.amethyst.commons.ui.chat.ChatroomHeader import com.vitorpamplona.amethyst.commons.ui.chat.DmBroadcastBanner @@ -97,7 +98,6 @@ import com.vitorpamplona.amethyst.commons.util.toTimeAgo import com.vitorpamplona.amethyst.commons.viewmodels.ChatNewMessageState import com.vitorpamplona.amethyst.commons.viewmodels.ChatroomFeedViewModel import com.vitorpamplona.amethyst.desktop.DesktopPreferences -import com.vitorpamplona.amethyst.desktop.service.upload.DesktopUploadOrchestrator import com.vitorpamplona.amethyst.desktop.ui.media.DesktopFilePicker import com.vitorpamplona.amethyst.desktop.ui.media.MediaAttachmentRow import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle @@ -801,7 +801,7 @@ private suspend fun sendEncryptedFiles( account: IAccount, cacheProvider: ICacheProvider, ) { - val orchestrator = DesktopUploadOrchestrator() + val orchestrator = UploadOrchestrator() val server = DesktopPreferences.preferredBlossomServer val recipients = roomKey.users.mapNotNull { cacheProvider.getUserIfExists(it) }.map { it.toPTag() } @@ -819,10 +819,8 @@ private suspend fun sendEncryptedFiles( hash = result.encryptedHash, size = result.encryptedSize, dimension = - if (result.metadata.width != null && result.metadata.height != null) { - DimensionTag(result.metadata.width, result.metadata.height) - } else { - null + result.metadata.width?.let { w -> + result.metadata.height?.let { h -> DimensionTag(w, h) } }, blurhash = result.metadata.blurhash, thumbhash = result.metadata.thumbhash, diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopBlossomClientTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/BlossomClientTest.kt similarity index 95% rename from desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopBlossomClientTest.kt rename to desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/BlossomClientTest.kt index 75b94e6a8..514a8ee7f 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopBlossomClientTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/BlossomClientTest.kt @@ -37,7 +37,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertTrue -class DesktopBlossomClientTest { +class BlossomClientTest { private fun mockOkHttp( responseCode: Int, body: String = "", @@ -67,7 +67,7 @@ class DesktopBlossomClientTest { runTest { val json = """{"url":"https://blossom.example.com/abc123.png","sha256":"abc123","size":1024}""" - val client = DesktopBlossomClient(mockOkHttp(200, json)) + val client = BlossomClient(mockOkHttp(200, json)) val file = File.createTempFile("test_", ".png") file.deleteOnExit() @@ -94,7 +94,7 @@ class DesktopBlossomClientTest { fun uploadFailureThrowsException() = runTest { val headers = Headers.headersOf("X-Reason", "File too large") - val client = DesktopBlossomClient(mockOkHttp(413, "", headers)) + val client = BlossomClient(mockOkHttp(413, "", headers)) val file = File.createTempFile("test_", ".png") file.deleteOnExit() @@ -119,7 +119,7 @@ class DesktopBlossomClientTest { @Test fun uploadFailureUsesStatusCodeWhenNoXReason() = runTest { - val client = DesktopBlossomClient(mockOkHttp(500)) + val client = BlossomClient(mockOkHttp(500)) val file = File.createTempFile("test_", ".png") file.deleteOnExit() @@ -159,7 +159,7 @@ class DesktopBlossomClientTest { .body("""{"url":"https://example.com/hash"}""".toResponseBody()) .build() - val client = DesktopBlossomClient(mockClient) + val client = BlossomClient(mockClient) val file = File.createTempFile("test_", ".png") file.deleteOnExit() file.writeBytes(byteArrayOf(1)) @@ -199,7 +199,7 @@ class DesktopBlossomClientTest { .body("""{"url":"https://example.com/hash"}""".toResponseBody()) .build() - val client = DesktopBlossomClient(mockClient) + val client = BlossomClient(mockClient) val file = File.createTempFile("test_", ".png") file.deleteOnExit() file.writeBytes(byteArrayOf(1)) diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopUploadTrackerTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopUploadTrackerTest.kt index 193aa1019..ce8992c68 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopUploadTrackerTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopUploadTrackerTest.kt @@ -20,6 +20,8 @@ */ package com.vitorpamplona.amethyst.desktop.service.upload +import com.vitorpamplona.amethyst.commons.service.upload.MediaMetadata +import com.vitorpamplona.amethyst.commons.service.upload.UploadResult import com.vitorpamplona.quartz.nipB7Blossom.BlossomUploadResult import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopMediaCompressorTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/MediaCompressorTest.kt similarity index 90% rename from desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopMediaCompressorTest.kt rename to desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/MediaCompressorTest.kt index 6cef50f6f..52369ca95 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopMediaCompressorTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/MediaCompressorTest.kt @@ -27,7 +27,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -class DesktopMediaCompressorTest { +class MediaCompressorTest { @Test fun stripExifReturnsSameFileForPng() { val file = File.createTempFile("test_", ".png") @@ -35,7 +35,7 @@ class DesktopMediaCompressorTest { val img = BufferedImage(2, 2, BufferedImage.TYPE_INT_RGB) ImageIO.write(img, "png", file) - val result = DesktopMediaCompressor.stripExif(file) + val result = MediaCompressor.stripExif(file) // Should return the same file object since it's not JPEG assertEquals(file, result) @@ -48,7 +48,7 @@ class DesktopMediaCompressorTest { file.deleteOnExit() file.writeText("not a jpeg") - val result = DesktopMediaCompressor.stripExif(file) + val result = MediaCompressor.stripExif(file) assertEquals(file, result) file.delete() @@ -60,7 +60,7 @@ class DesktopMediaCompressorTest { file.deleteOnExit() file.writeBytes(byteArrayOf(0, 0, 0)) - val result = DesktopMediaCompressor.stripExif(file) + val result = MediaCompressor.stripExif(file) assertEquals(file, result) file.delete() @@ -71,7 +71,7 @@ class DesktopMediaCompressorTest { // Create a minimal JPEG without EXIF val file = createMinimalJpeg() try { - val result = DesktopMediaCompressor.stripExif(file) + val result = MediaCompressor.stripExif(file) // Should return the same file since there's no EXIF to strip assertEquals(file, result) } finally { @@ -87,7 +87,7 @@ class DesktopMediaCompressorTest { val img = BufferedImage(4, 4, BufferedImage.TYPE_INT_RGB) ImageIO.write(img, "jpg", file) - val result = DesktopMediaCompressor.stripExif(file) + val result = MediaCompressor.stripExif(file) // Result should be a valid file regardless assertTrue(result.exists()) @@ -107,7 +107,7 @@ class DesktopMediaCompressorTest { val img = BufferedImage(2, 2, BufferedImage.TYPE_INT_RGB) ImageIO.write(img, "jpg", file) - val result = DesktopMediaCompressor.stripExif(file) + val result = MediaCompressor.stripExif(file) assertTrue(result.exists()) if (result != file) result.delete() diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopMediaMetadataTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/MediaMetadataReaderTest.kt similarity index 68% rename from desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopMediaMetadataTest.kt rename to desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/MediaMetadataReaderTest.kt index 2bb0f4016..53b2ba7a1 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopMediaMetadataTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/MediaMetadataReaderTest.kt @@ -29,60 +29,60 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue -class DesktopMediaMetadataTest { +class MediaMetadataReaderTest { // --- guessMimeType --- @Test fun guessMimeTypeForJpeg() { - assertEquals("image/jpeg", DesktopMediaMetadata.guessMimeType(File("photo.jpg"))) - assertEquals("image/jpeg", DesktopMediaMetadata.guessMimeType(File("photo.jpeg"))) - assertEquals("image/jpeg", DesktopMediaMetadata.guessMimeType(File("photo.JPEG"))) + assertEquals("image/jpeg", MediaMetadataReader.guessMimeType(File("photo.jpg"))) + assertEquals("image/jpeg", MediaMetadataReader.guessMimeType(File("photo.jpeg"))) + assertEquals("image/jpeg", MediaMetadataReader.guessMimeType(File("photo.JPEG"))) } @Test fun guessMimeTypeForPng() { - assertEquals("image/png", DesktopMediaMetadata.guessMimeType(File("image.png"))) + assertEquals("image/png", MediaMetadataReader.guessMimeType(File("image.png"))) } @Test fun guessMimeTypeForGif() { - assertEquals("image/gif", DesktopMediaMetadata.guessMimeType(File("anim.gif"))) + assertEquals("image/gif", MediaMetadataReader.guessMimeType(File("anim.gif"))) } @Test fun guessMimeTypeForWebp() { - assertEquals("image/webp", DesktopMediaMetadata.guessMimeType(File("image.webp"))) + assertEquals("image/webp", MediaMetadataReader.guessMimeType(File("image.webp"))) } @Test fun guessMimeTypeForSvg() { - assertEquals("image/svg+xml", DesktopMediaMetadata.guessMimeType(File("icon.svg"))) + assertEquals("image/svg+xml", MediaMetadataReader.guessMimeType(File("icon.svg"))) } @Test fun guessMimeTypeForAvif() { - assertEquals("image/avif", DesktopMediaMetadata.guessMimeType(File("photo.avif"))) + assertEquals("image/avif", MediaMetadataReader.guessMimeType(File("photo.avif"))) } @Test fun guessMimeTypeForVideoFormats() { - assertEquals("video/mp4", DesktopMediaMetadata.guessMimeType(File("clip.mp4"))) - assertEquals("video/webm", DesktopMediaMetadata.guessMimeType(File("clip.webm"))) - assertEquals("video/quicktime", DesktopMediaMetadata.guessMimeType(File("clip.mov"))) + assertEquals("video/mp4", MediaMetadataReader.guessMimeType(File("clip.mp4"))) + assertEquals("video/webm", MediaMetadataReader.guessMimeType(File("clip.webm"))) + assertEquals("video/quicktime", MediaMetadataReader.guessMimeType(File("clip.mov"))) } @Test fun guessMimeTypeForAudioFormats() { - assertEquals("audio/mpeg", DesktopMediaMetadata.guessMimeType(File("song.mp3"))) - assertEquals("audio/ogg", DesktopMediaMetadata.guessMimeType(File("track.ogg"))) - assertEquals("audio/wav", DesktopMediaMetadata.guessMimeType(File("sound.wav"))) - assertEquals("audio/flac", DesktopMediaMetadata.guessMimeType(File("lossless.flac"))) + assertEquals("audio/mpeg", MediaMetadataReader.guessMimeType(File("song.mp3"))) + assertEquals("audio/ogg", MediaMetadataReader.guessMimeType(File("track.ogg"))) + assertEquals("audio/wav", MediaMetadataReader.guessMimeType(File("sound.wav"))) + assertEquals("audio/flac", MediaMetadataReader.guessMimeType(File("lossless.flac"))) } @Test fun guessMimeTypeForUnknownExtension() { - assertEquals("application/octet-stream", DesktopMediaMetadata.guessMimeType(File("data.xyz"))) - assertEquals("application/octet-stream", DesktopMediaMetadata.guessMimeType(File("noext"))) + assertEquals("application/octet-stream", MediaMetadataReader.guessMimeType(File("data.xyz"))) + assertEquals("application/octet-stream", MediaMetadataReader.guessMimeType(File("noext"))) } // --- compute --- @@ -91,7 +91,7 @@ class DesktopMediaMetadataTest { fun computeForPngImage() { val file = createTempPng(width = 10, height = 5) try { - val meta = DesktopMediaMetadata.compute(file) + val meta = MediaMetadataReader.compute(file) assertEquals("image/png", meta.mimeType) assertTrue(meta.size > 0) @@ -110,7 +110,7 @@ class DesktopMediaMetadataTest { file.deleteOnExit() file.writeText("hello world") try { - val meta = DesktopMediaMetadata.compute(file) + val meta = MediaMetadataReader.compute(file) assertEquals("application/octet-stream", meta.mimeType) assertEquals(11L, meta.size) @@ -129,8 +129,8 @@ class DesktopMediaMetadataTest { file.deleteOnExit() file.writeBytes(byteArrayOf(1, 2, 3, 4, 5)) try { - val meta1 = DesktopMediaMetadata.compute(file) - val meta2 = DesktopMediaMetadata.compute(file) + val meta1 = MediaMetadataReader.compute(file) + val meta2 = MediaMetadataReader.compute(file) assertEquals(meta1.sha256, meta2.sha256) } finally { file.delete() @@ -143,7 +143,7 @@ class DesktopMediaMetadataTest { file.deleteOnExit() file.writeBytes(byteArrayOf(0, 0, 0)) try { - val meta = DesktopMediaMetadata.compute(file) + val meta = MediaMetadataReader.compute(file) assertEquals("video/mp4", meta.mimeType) assertNull(meta.width) assertNull(meta.height) diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopUploadOrchestratorTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/UploadOrchestratorTest.kt similarity index 91% rename from desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopUploadOrchestratorTest.kt rename to desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/UploadOrchestratorTest.kt index b2ba3f3ba..dd0c9d3fe 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/DesktopUploadOrchestratorTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/service/upload/UploadOrchestratorTest.kt @@ -36,24 +36,24 @@ import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue -class DesktopUploadOrchestratorTest { +class UploadOrchestratorTest { @BeforeTest fun setup() { - mockkObject(DesktopBlossomAuth) + mockkObject(BlossomAuth) coEvery { - DesktopBlossomAuth.createUploadAuth(any(), any(), any(), any()) + BlossomAuth.createUploadAuth(any(), any(), any(), any()) } returns "Nostr fakeAuthToken" } @AfterTest fun teardown() { - unmockkObject(DesktopBlossomAuth) + unmockkObject(BlossomAuth) } @Test fun uploadCallsClientWithCorrectParameters() = runTest { - val mockClient = mockk() + val mockClient = mockk() val fileSlot = slot() val contentTypeSlot = slot() val urlSlot = slot() @@ -72,7 +72,7 @@ class DesktopUploadOrchestratorTest { size = 100, ) - val orchestrator = DesktopUploadOrchestrator(mockClient) + val orchestrator = UploadOrchestrator(mockClient) val file = File.createTempFile("test_", ".png") file.deleteOnExit() @@ -112,7 +112,7 @@ class DesktopUploadOrchestratorTest { @Test fun uploadPassesSameFileWhenNoStripExif() = runTest { - val mockClient = mockk() + val mockClient = mockk() val fileSlot = slot() coEvery { @@ -124,7 +124,7 @@ class DesktopUploadOrchestratorTest { ) } returns BlossomUploadResult(url = "https://example.com/hash") - val orchestrator = DesktopUploadOrchestrator(mockClient) + val orchestrator = UploadOrchestrator(mockClient) val file = File.createTempFile("test_", ".txt") file.deleteOnExit() @@ -150,13 +150,13 @@ class DesktopUploadOrchestratorTest { @Test fun uploadComputesMetadata() = runTest { - val mockClient = mockk() + val mockClient = mockk() coEvery { mockClient.upload(any(), any(), any(), any()) } returns BlossomUploadResult(url = "https://example.com/hash") - val orchestrator = DesktopUploadOrchestrator(mockClient) + val orchestrator = UploadOrchestrator(mockClient) val file = File.createTempFile("test_", ".txt") file.deleteOnExit() @@ -185,7 +185,7 @@ class DesktopUploadOrchestratorTest { @Test fun uploadPassesAuthHeaderToClient() = runTest { - val mockClient = mockk() + val mockClient = mockk() val authSlot = slot() coEvery { @@ -197,7 +197,7 @@ class DesktopUploadOrchestratorTest { ) } returns BlossomUploadResult(url = "https://example.com/hash") - val orchestrator = DesktopUploadOrchestrator(mockClient) + val orchestrator = UploadOrchestrator(mockClient) val file = File.createTempFile("test_", ".txt") file.deleteOnExit()