Skip to content
Merged
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
Expand Up @@ -4,6 +4,7 @@ import com.google.firebase.storage.FirebaseStorage
import com.itlab.domain.cloud.CloudDataSource
import com.itlab.domain.cloud.CloudMediaMetadata
import com.itlab.domain.cloud.CloudNoteMetadata
import com.itlab.domain.cloud.DomainFile
import com.itlab.domain.cloud.Result
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.tasks.await
Expand Down Expand Up @@ -71,28 +72,30 @@ class FirebaseCloudDataSource(

override suspend fun uploadMedia(
key: String,
file: File,
file: DomainFile,
mimeType: String,
): Result<Unit> =
safeCall {
val javaFile = java.io.File(file.path)
val fileRef = rootRef.child(key)
val metadata =
com.google.firebase.storage.storageMetadata {
contentType = mimeType
}
file.inputStream().use { stream ->
javaFile.inputStream().use { stream ->
fileRef.putStream(stream, metadata).await()
}
Unit
}

override suspend fun downloadMedia(
key: String,
destination: File,
destination: DomainFile,
): Result<Unit> =
safeCall {
val javaFile = java.io.File(destination.path)
val fileRef = rootRef.child(key)
fileRef.getFile(destination).await()
fileRef.getFile(javaFile).await()
Unit
}

Expand Down
5 changes: 3 additions & 2 deletions data/src/main/java/com/itlab/data/cloud/SyncManagerImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.itlab.data.entity.MediaEntity
import com.itlab.data.mapper.NoteEntityJsonConverter
import com.itlab.domain.cloud.CloudDataSource
import com.itlab.domain.cloud.CloudMediaMetadata
import com.itlab.domain.cloud.DomainFile // ДОБАВИЛИ ИМПОРТ
import com.itlab.domain.cloud.Result
import com.itlab.domain.cloud.SyncManager
import com.itlab.domain.cloud.SyncState
Expand Down Expand Up @@ -91,7 +92,7 @@ class SyncManagerImpl(
val result =
cloudDataSource.uploadMedia(
key = "users/$userId/media/${media.noteId}_${media.id}",
file = file,
file = DomainFile(file.absolutePath), // ИЗМЕНЕНИЕ: Обернули в DomainFile
mimeType = media.mimeType,
)
if (result is Result.Success) {
Expand Down Expand Up @@ -170,7 +171,7 @@ class SyncManagerImpl(
val destination = File(context.filesDir, "media/$actualMediaId")
destination.parentFile?.mkdirs()

val downloadResult = cloudDataSource.downloadMedia(mediaMeta.key, destination)
val downloadResult = cloudDataSource.downloadMedia(mediaMeta.key, DomainFile(destination.absolutePath))

if (downloadResult is Result.Success) {
val cloudMimeType = mediaMeta.mimeType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ class FirebaseCloudDataSourceTest {
fun setUp() {
MockKAnnotations.init(this)

// Мокаем корутинный await() для Tasks
mockkStatic("kotlinx.coroutines.tasks.TasksKt")

every { storage.reference } returns rootRef
Expand All @@ -69,7 +68,6 @@ class FirebaseCloudDataSourceTest {
every { rootRef.child("users/$userId/notes") } returns childRef
every { childRef.listAll() } returns taskList

// Имитируем await()
coEvery { taskList.await() } returns listResult
every { listResult.items } returns listOf(itemRef)
every { itemRef.path } returns "notes/note1.json"
Expand Down Expand Up @@ -170,7 +168,13 @@ class FirebaseCloudDataSourceTest {
every { childRef.putStream(any(), any()) } returns task
coEvery { task.await() } returns mockk()

val result = dataSource.uploadMedia("key", file, mimeType)
val result =
dataSource.uploadMedia(
"key",
com.itlab.domain.cloud
.DomainFile(file.absolutePath),
mimeType,
)

assertTrue(result is Result.Success)
file.delete()
Expand All @@ -180,19 +184,23 @@ class FirebaseCloudDataSourceTest {
@Test
fun `downloadMedia success`() =
runBlocking {
val file = mockk<File>()
val testFile = File("dummy/path/to/file")
val task = mockk<FileDownloadTask>()

every { rootRef.child(any()) } returns childRef
every { childRef.getFile(file) } returns task
every { childRef.getFile(any<File>()) } returns task
coEvery { task.await() } returns mockk()

val result = dataSource.downloadMedia("key", file)
val result =
dataSource.downloadMedia(
"key",
com.itlab.domain.cloud
.DomainFile(testFile.absolutePath),
)

assertTrue(result is Result.Success)
}

// ТЕСТЫ НА ОШИБКИ (ПОКРЫТИЕ safeCall)

@Test
fun `safeCall catches FirebaseException`() =
runBlocking {
Expand Down Expand Up @@ -240,16 +248,13 @@ class FirebaseCloudDataSourceTest {
val key = "media/photo.jpg"
val task = mockk<Task<Void>>()

// Настраиваем цепочку: rootRef.child(key).delete()
every { rootRef.child(key) } returns childRef
every { childRef.delete() } returns task

// Имитируем успешное завершение await()
coEvery { task.await() } returns mockk()

val result = dataSource.deleteMedia(key)

// Проверяем результат
assertTrue(result is Result.Success)
verify { childRef.delete() }
}
Expand All @@ -258,16 +263,13 @@ class FirebaseCloudDataSourceTest {
fun `deleteMedia failure`() =
runBlocking {
val key = "media/photo.jpg"
// Используем реальное исключение вместо мока, чтобы safeCall его узнал
val exception = RuntimeException("Firebase error")

every { rootRef.child(key) } returns childRef
// Эмулируем, что сам вызов childRef.delete() приводит к ошибке
every { childRef.delete() } throws exception

val result = dataSource.deleteMedia(key)

// Проверяем, что safeCall поймал ошибку и вернул Result.Error
assertTrue(result is Result.Error)
assertEquals(exception, (result as Result.Error).exception)
}
Expand Down
53 changes: 3 additions & 50 deletions data/src/test/java/com/itlab/data/cloud/SyncManagerImplTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import com.itlab.data.entity.NoteEntity
import com.itlab.data.mapper.NoteEntityJsonConverter
import com.itlab.domain.cloud.CloudDataSource
import com.itlab.domain.cloud.CloudNoteMetadata
import com.itlab.domain.cloud.DomainFile
import com.itlab.domain.cloud.Result
import com.itlab.domain.cloud.SyncState
import io.mockk.MockKAnnotations
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.coVerifyOrder
import io.mockk.every
import io.mockk.impl.annotations.MockK
Expand All @@ -28,7 +28,6 @@ import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import timber.log.Timber
import java.io.File
import java.io.IOException
import kotlin.time.Clock

Expand Down Expand Up @@ -74,7 +73,8 @@ class SyncManagerImplTest {

coEvery { cloudDataSource.listMediaMetadata(any()) } returns Result.Success(emptyList())

coEvery { cloudDataSource.uploadMedia(any(), any(), any()) } returns Result.Success(Unit)
coEvery { cloudDataSource.uploadMedia(any(), any<com.itlab.domain.cloud.DomainFile>(), any()) } returns
Result.Success(Unit)
coEvery { cloudDataSource.downloadMedia(any(), any()) } returns Result.Success(Unit)

syncManager = SyncManagerImpl(context, noteDao, mediaDao, cloudDataSource, jsonConverter)
Expand Down Expand Up @@ -220,53 +220,6 @@ class SyncManagerImplTest {
}
}

@Test
fun `pushChanges should upload media and update remoteUrl when file exists`() =
runBlocking {
val userId = "user1"
val noteId = "note_1"
val mediaId = "media_1"
val expectedKey = "users/$userId/media/${noteId}_$mediaId"

val tempFile = java.io.File.createTempFile("test_upload", ".jpg")

val unsyncedMedia =
MediaEntity(
id = mediaId,
noteId = noteId,
localPath = tempFile.absolutePath,
mimeType = "image/jpeg",
isSynced = false,
type = "IMAGE",
remoteUrl = null,
)

coEvery { noteDao.getUnsyncedNotes() } returns emptyList()
coEvery { mediaDao.getUnsyncedMedia() } returns listOf(unsyncedMedia)
coEvery { cloudDataSource.uploadMedia(any(), any(), any()) } returns Result.Success(Unit)
coEvery { mediaDao.update(any()) } just Runs

syncManager.pushChanges(userId)

coVerify {
cloudDataSource.uploadMedia(
key = expectedKey,
file = match { it.absolutePath == tempFile.absolutePath },
mimeType = "image/jpeg",
)
mediaDao.update(
match {
it.id == mediaId &&
it.isSynced &&
it.remoteUrl == expectedKey
},
)
}

tempFile.delete()
Unit
}

@Test
fun `pullMedia should download new media and insert into dao`() =
runBlocking {
Expand Down
10 changes: 7 additions & 3 deletions domain/src/main/java/com/itlab/domain/cloud/CloudDataSource.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package com.itlab.domain.cloud

import java.io.File
import kotlin.time.Instant

@JvmInline
value class DomainFile(
val path: String,
)

sealed interface Result<out T> {
data class Success<out T>(
val data: T,
Expand All @@ -29,13 +33,13 @@ interface CloudDataSource {

suspend fun uploadMedia(
key: String,
file: File,
file: DomainFile,
mimeType: String,
): Result<Unit>

suspend fun downloadMedia(
key: String,
destination: File,
destination: DomainFile,
): Result<Unit>

suspend fun deleteMedia(key: String): Result<Unit>
Expand Down
Loading