Первые тесты
This commit is contained in:
@@ -1,42 +1,19 @@
|
||||
package com.github.nullptroma.wallenc.usecases
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.DataPage
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretEntryRecord
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncJournalEntry
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncLock
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IDirectory
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IFile
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IMetaInfo
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageMetaInfo
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgress
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import com.github.nullptroma.wallenc.usecases.fakes.FakeStorage
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
|
||||
class StorageDomainUseCasesTest {
|
||||
|
||||
@Test
|
||||
fun twoFaCrudWorksAndPersists() = runBlocking {
|
||||
fun twoFaCrudWorksAndPersists() = runTest {
|
||||
val storage = FakeStorage()
|
||||
val useCase = ManageTwoFaTokensUseCase()
|
||||
|
||||
@@ -63,7 +40,7 @@ class StorageDomainUseCasesTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun twoFaInvalidJsonFallsBackToEmptyList() = runBlocking {
|
||||
fun twoFaInvalidJsonFallsBackToEmptyList() = runTest {
|
||||
val storage = FakeStorage().apply {
|
||||
setDomainFile(StorageDomainDataFiles.TWO_FA_TOKENS_FILE, "not-json")
|
||||
}
|
||||
@@ -72,7 +49,7 @@ class StorageDomainUseCasesTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun textSecretsCrudWorksWithOptionalLabels() = runBlocking {
|
||||
fun textSecretsCrudWorksWithOptionalLabels() = runTest {
|
||||
val storage = FakeStorage()
|
||||
val useCase = ManageTextSecretsUseCase()
|
||||
|
||||
@@ -104,7 +81,7 @@ class StorageDomainUseCasesTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun textSecretsInvalidJsonFallsBackToEmptyList() = runBlocking {
|
||||
fun textSecretsInvalidJsonFallsBackToEmptyList() = runTest {
|
||||
val storage = FakeStorage().apply {
|
||||
setDomainFile(StorageDomainDataFiles.TEXT_SECRETS_FILE, "{broken")
|
||||
}
|
||||
@@ -112,131 +89,3 @@ class StorageDomainUseCasesTest {
|
||||
assertTrue(useCase.observe(storage).first().isEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeStorage : IStorage {
|
||||
private val accessorImpl = FakeStorageAccessor()
|
||||
|
||||
override val uuid: UUID = UUID.randomUUID()
|
||||
override val isAvailable: StateFlow<Boolean> = MutableStateFlow(true)
|
||||
override val size: StateFlow<Long?> = MutableStateFlow(0L)
|
||||
override val numberOfFiles: StateFlow<Int?> = MutableStateFlow(0)
|
||||
override val isEmpty: Flow<Boolean?> = flowOf(true)
|
||||
override val metaInfo: StateFlow<IStorageMetaInfo> = MutableStateFlow(FakeMetaInfo())
|
||||
override val isVirtualStorage: Boolean = false
|
||||
override val accessor: IStorageAccessor = accessorImpl
|
||||
|
||||
override suspend fun rename(newName: String) = Unit
|
||||
|
||||
override suspend fun setEncInfo(encInfo: StorageEncryptionInfo?) = Unit
|
||||
|
||||
override suspend fun clearAllContent(onProgress: suspend (TaskProgress) -> Unit) = Unit
|
||||
|
||||
fun setDomainFile(path: String, value: String) {
|
||||
accessorImpl.dataFiles[path] = value.encodeToByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeMetaInfo : IStorageMetaInfo {
|
||||
override val encInfo: StorageEncryptionInfo? = null
|
||||
override val name: String = "Fake"
|
||||
override val lastModified: Instant = Instant.now()
|
||||
}
|
||||
|
||||
private class FakeStorageAccessor : IStorageAccessor {
|
||||
val dataFiles: MutableMap<String, ByteArray> = mutableMapOf()
|
||||
private val systemFiles: MutableMap<String, ByteArray> = mutableMapOf()
|
||||
private val _filesUpdates = MutableSharedFlow<DataPage<IFile>>(extraBufferCapacity = 16)
|
||||
|
||||
override val size: StateFlow<Long?> = MutableStateFlow(0L)
|
||||
override val numberOfFiles: StateFlow<Int?> = MutableStateFlow(0)
|
||||
override val isAvailable: StateFlow<Boolean> = MutableStateFlow(true)
|
||||
override val filesUpdates: SharedFlow<DataPage<IFile>> = _filesUpdates
|
||||
override val dirsUpdates: SharedFlow<DataPage<IDirectory>> = MutableSharedFlow()
|
||||
|
||||
override suspend fun getAllFiles(): List<IFile> = emptyList()
|
||||
|
||||
override suspend fun getFiles(path: String): List<IFile> = emptyList()
|
||||
|
||||
override fun getFilesFlow(path: String): Flow<DataPage<IFile>> = emptyFlow()
|
||||
|
||||
override suspend fun getAllDirs(): List<IDirectory> = emptyList()
|
||||
|
||||
override suspend fun getDirs(path: String): List<IDirectory> = emptyList()
|
||||
|
||||
override fun getDirsFlow(path: String): Flow<DataPage<IDirectory>> = emptyFlow()
|
||||
|
||||
override suspend fun getFileInfo(path: String): IFile {
|
||||
error("Not implemented in tests")
|
||||
}
|
||||
|
||||
override suspend fun getDirInfo(path: String): IDirectory {
|
||||
error("Not implemented in tests")
|
||||
}
|
||||
|
||||
override suspend fun setHidden(path: String, hidden: Boolean) = Unit
|
||||
|
||||
override suspend fun touchFile(path: String) = Unit
|
||||
|
||||
override suspend fun touchDir(path: String) = Unit
|
||||
|
||||
override suspend fun delete(path: String) = Unit
|
||||
|
||||
override suspend fun openWrite(path: String): OutputStream {
|
||||
return object : ByteArrayOutputStream() {
|
||||
override fun close() {
|
||||
dataFiles[path] = toByteArray()
|
||||
_filesUpdates.tryEmit(
|
||||
DataPage(
|
||||
listOf(FakeFile(path)),
|
||||
pageLength = 1,
|
||||
pageIndex = 0,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun openRead(path: String): InputStream {
|
||||
val bytes = dataFiles[path] ?: throw IllegalStateException("File not found: $path")
|
||||
return ByteArrayInputStream(bytes)
|
||||
}
|
||||
|
||||
override suspend fun moveToTrash(path: String) = Unit
|
||||
|
||||
override suspend fun openReadSystemFile(name: String): InputStream {
|
||||
val bytes = systemFiles[name] ?: ByteArray(0)
|
||||
return ByteArrayInputStream(bytes)
|
||||
}
|
||||
|
||||
override suspend fun openWriteSystemFile(name: String): OutputStream {
|
||||
return object : ByteArrayOutputStream() {
|
||||
override fun close() {
|
||||
systemFiles[name] = toByteArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun readSyncJournal(): List<StorageSyncJournalEntry> = emptyList()
|
||||
|
||||
override suspend fun appendSyncJournal(entries: List<StorageSyncJournalEntry>) = Unit
|
||||
|
||||
override suspend fun rewriteSyncJournal(entries: List<StorageSyncJournalEntry>) = Unit
|
||||
|
||||
override suspend fun readSyncLock(): StorageSyncLock? = null
|
||||
|
||||
override suspend fun tryAcquireSyncLock(holderId: String, leaseUntil: Instant): Boolean = true
|
||||
|
||||
override suspend fun releaseSyncLock(holderId: String) = Unit
|
||||
|
||||
override suspend fun forceClearSyncLock() = Unit
|
||||
}
|
||||
|
||||
private class FakeFile(path: String) : IFile {
|
||||
override val metaInfo: IMetaInfo = object : IMetaInfo {
|
||||
override val size: Long = 0L
|
||||
override val isDeleted: Boolean = false
|
||||
override val isHidden: Boolean = false
|
||||
override val lastModified: Instant = Instant.now()
|
||||
override val path: String = path
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
package com.github.nullptroma.wallenc.usecases
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncJournalEntry
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncOperation
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncRevision
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.StorageSyncGroup
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.StorageSyncGroupEncryptionKind
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgressLabel
|
||||
import com.github.nullptroma.wallenc.usecases.fakes.FakeStorage
|
||||
import com.github.nullptroma.wallenc.usecases.fakes.FakeStorageSyncGroupStore
|
||||
import com.github.nullptroma.wallenc.usecases.fakes.FakeVaultsManager
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
|
||||
class StorageSyncEngineTest {
|
||||
|
||||
@Test
|
||||
fun syncAllGroupsReportsNoGroupsWhenEmpty() = runBlocking {
|
||||
val labels = mutableListOf<TaskProgressLabel?>()
|
||||
val engine = createEngine(storages = emptyList(), groups = emptyList())
|
||||
engine.syncAllGroups { _, label -> labels.add(label) }
|
||||
assertTrue(labels.any { it is TaskProgressLabel.SyncNoGroups })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun syncGroupCopiesFileFromSourceToTarget() = runBlocking {
|
||||
val source = FakeStorage()
|
||||
val target = FakeStorage()
|
||||
val path = "shared/doc.txt"
|
||||
val payload = "sync-payload".encodeToByteArray()
|
||||
source.putFile(path, payload)
|
||||
|
||||
val entry = StorageSyncJournalEntry(
|
||||
path = path,
|
||||
operation = StorageSyncOperation.UPSERT,
|
||||
revision = StorageSyncRevision(
|
||||
sequence = 1L,
|
||||
actorId = "actor-a",
|
||||
createdAt = Instant.parse("2024-01-01T00:00:00Z"),
|
||||
),
|
||||
size = payload.size.toLong(),
|
||||
originStorageUuid = source.uuid,
|
||||
)
|
||||
source.addSyncJournalEntry(entry)
|
||||
|
||||
val groupId = "group-1"
|
||||
val group = StorageSyncGroup(
|
||||
id = groupId,
|
||||
storageUuids = setOf(source.uuid, target.uuid),
|
||||
encryptionKind = StorageSyncGroupEncryptionKind.NONE,
|
||||
)
|
||||
val labels = mutableListOf<TaskProgressLabel?>()
|
||||
val engine = createEngine(
|
||||
storages = listOf(source, target),
|
||||
groups = listOf(group),
|
||||
)
|
||||
engine.syncGroup(groupId) { _, label -> labels.add(label) }
|
||||
|
||||
assertArrayEquals(payload, target.fileBytes(path))
|
||||
assertTrue(labels.any { it is TaskProgressLabel.SyncGroupCompleted })
|
||||
assertTrue(target.syncJournalEntries().any { it.path == path })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun syncGroupSkippedWhenFewerThanTwoStorages() = runBlocking {
|
||||
val only = FakeStorage()
|
||||
val groupId = "solo"
|
||||
val group = StorageSyncGroup(
|
||||
id = groupId,
|
||||
storageUuids = setOf(only.uuid),
|
||||
encryptionKind = StorageSyncGroupEncryptionKind.NONE,
|
||||
)
|
||||
val labels = mutableListOf<TaskProgressLabel?>()
|
||||
val engine = createEngine(storages = listOf(only), groups = listOf(group))
|
||||
engine.syncGroup(groupId) { _, label -> labels.add(label) }
|
||||
assertTrue(labels.any { it is TaskProgressLabel.SyncGroupSkippedTooFewStorages })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun syncGroupDeleteRemovesFileOnTarget() = runBlocking {
|
||||
val source = FakeStorage()
|
||||
val target = FakeStorage()
|
||||
val path = "obsolete.bin"
|
||||
target.putFile(path, "old".encodeToByteArray())
|
||||
|
||||
val entry = StorageSyncJournalEntry(
|
||||
path = path,
|
||||
operation = StorageSyncOperation.DELETE,
|
||||
revision = StorageSyncRevision(
|
||||
sequence = 2L,
|
||||
actorId = "actor-b",
|
||||
createdAt = Instant.parse("2024-06-01T00:00:00Z"),
|
||||
),
|
||||
)
|
||||
source.addSyncJournalEntry(entry)
|
||||
|
||||
val group = StorageSyncGroup(
|
||||
id = "delete-group",
|
||||
storageUuids = setOf(source.uuid, target.uuid),
|
||||
encryptionKind = StorageSyncGroupEncryptionKind.NONE,
|
||||
)
|
||||
val engine = createEngine(
|
||||
storages = listOf(source, target),
|
||||
groups = listOf(group),
|
||||
)
|
||||
engine.syncGroup(group.id) { _, _ -> }
|
||||
|
||||
assertNull(target.fileBytes(path))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun syncGroupStopsWhenLockCannotBeAcquired() = runBlocking {
|
||||
val first = FakeStorage()
|
||||
val second = FakeStorage().apply { setAcquireLockResult(false) }
|
||||
val group = StorageSyncGroup(
|
||||
id = "lock-fail",
|
||||
storageUuids = setOf(first.uuid, second.uuid),
|
||||
encryptionKind = StorageSyncGroupEncryptionKind.NONE,
|
||||
)
|
||||
first.addSyncJournalEntry(
|
||||
StorageSyncJournalEntry(
|
||||
path = "a.txt",
|
||||
operation = StorageSyncOperation.UPSERT,
|
||||
revision = StorageSyncRevision(1L, "x", Instant.EPOCH),
|
||||
),
|
||||
)
|
||||
val labels = mutableListOf<TaskProgressLabel?>()
|
||||
val engine = createEngine(
|
||||
storages = listOf(first, second),
|
||||
groups = listOf(group),
|
||||
)
|
||||
engine.syncGroup(group.id) { _, label -> labels.add(label) }
|
||||
assertTrue(labels.any { it is TaskProgressLabel.SyncGroupLockFailed })
|
||||
}
|
||||
|
||||
private fun createEngine(
|
||||
storages: List<FakeStorage>,
|
||||
groups: List<StorageSyncGroup>,
|
||||
): StorageSyncEngine {
|
||||
val vaultsManager = FakeVaultsManager(storages)
|
||||
val findStorage = FindStorageUseCase(vaultsManager)
|
||||
return StorageSyncEngine(
|
||||
vaultsManager = vaultsManager,
|
||||
groupStore = FakeStorageSyncGroupStore(groups),
|
||||
findStorageUseCase = findStorage,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.github.nullptroma.wallenc.usecases
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.TwoFaTokenRecord
|
||||
import com.eatthepath.otp.TimeBasedOneTimePasswordGenerator
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class TwoFaTotpTest {
|
||||
|
||||
@Test
|
||||
fun buildTwoFaCodeStateMatchesJavaOtpForKnownSecret() {
|
||||
val secretBase32 = "JBSWY3DPEHPK3PXP"
|
||||
val token = TwoFaTokenRecord(
|
||||
id = "1",
|
||||
issuer = "Test",
|
||||
account = "user",
|
||||
secret = secretBase32,
|
||||
digits = 6,
|
||||
periodSeconds = 30,
|
||||
algorithm = "SHA1",
|
||||
notes = null,
|
||||
)
|
||||
val nowMillis = 1_700_000_000_000L
|
||||
val state = buildTwoFaCodeState(token, nowMillis)
|
||||
assertNotNull(state)
|
||||
val expected = generateReferenceTotp(secretBase32, nowMillis, 6, 30, "HmacSHA1")
|
||||
assertEquals(expected, state!!.code)
|
||||
assertTrue(state.secondsUntilRefresh in 1..30)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun buildTwoFaCodeStateReturnsNullForInvalidSecret() {
|
||||
val token = TwoFaTokenRecord(
|
||||
id = "1",
|
||||
issuer = "Test",
|
||||
account = "user",
|
||||
secret = "!!!not-base32!!!",
|
||||
digits = 6,
|
||||
periodSeconds = 30,
|
||||
algorithm = "SHA1",
|
||||
notes = null,
|
||||
)
|
||||
assertEquals(null, buildTwoFaCodeState(token, System.currentTimeMillis()))
|
||||
}
|
||||
|
||||
private fun generateReferenceTotp(
|
||||
secretBase32: String,
|
||||
nowMillis: Long,
|
||||
digits: Int,
|
||||
periodSeconds: Int,
|
||||
macAlgorithm: String,
|
||||
): String {
|
||||
val key = decodeBase32(secretBase32)
|
||||
val generator = TimeBasedOneTimePasswordGenerator(
|
||||
Duration.ofSeconds(periodSeconds.toLong()),
|
||||
digits,
|
||||
macAlgorithm,
|
||||
)
|
||||
val otp = generator.generateOneTimePassword(
|
||||
SecretKeySpec(key, "RAW"),
|
||||
Instant.ofEpochMilli(nowMillis),
|
||||
)
|
||||
return otp.toString().padStart(digits, '0')
|
||||
}
|
||||
|
||||
private fun decodeBase32(input: String): ByteArray {
|
||||
val alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
|
||||
val clean = input.uppercase().replace(" ", "").replace("=", "")
|
||||
var buffer = 0
|
||||
var bitsLeft = 0
|
||||
val out = ArrayList<Byte>()
|
||||
for (ch in clean) {
|
||||
val value = alphabet.indexOf(ch)
|
||||
buffer = (buffer shl 5) or value
|
||||
bitsLeft += 5
|
||||
if (bitsLeft >= 8) {
|
||||
out.add(((buffer shr (bitsLeft - 8)) and 0xFF).toByte())
|
||||
bitsLeft -= 8
|
||||
}
|
||||
}
|
||||
return out.toByteArray()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.github.nullptroma.wallenc.usecases.fakes
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncJournalEntry
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageMetaInfo
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgress
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
|
||||
class FakeStorage(
|
||||
override val uuid: UUID = UUID.randomUUID(),
|
||||
private val accessorImpl: FakeStorageAccessor = FakeStorageAccessor(),
|
||||
private val meta: FakeMetaInfo = FakeMetaInfo(),
|
||||
) : IStorage {
|
||||
override val isAvailable: StateFlow<Boolean> = MutableStateFlow(true)
|
||||
override val size: StateFlow<Long?> = MutableStateFlow(0L)
|
||||
override val numberOfFiles: StateFlow<Int?> = MutableStateFlow(0)
|
||||
override val isEmpty: Flow<Boolean?> = flowOf(true)
|
||||
override val metaInfo: StateFlow<IStorageMetaInfo> = MutableStateFlow(meta)
|
||||
override val isVirtualStorage: Boolean = false
|
||||
override val accessor: IStorageAccessor = accessorImpl
|
||||
|
||||
override suspend fun rename(newName: String) = Unit
|
||||
|
||||
override suspend fun setEncInfo(encInfo: StorageEncryptionInfo?) = Unit
|
||||
|
||||
override suspend fun clearAllContent(onProgress: suspend (TaskProgress) -> Unit) = Unit
|
||||
|
||||
fun setDomainFile(path: String, value: String) {
|
||||
putFile(path, value.encodeToByteArray())
|
||||
}
|
||||
|
||||
fun putFile(path: String, bytes: ByteArray) {
|
||||
accessorImpl.dataFiles[path] = bytes
|
||||
}
|
||||
|
||||
fun fileBytes(path: String): ByteArray? = accessorImpl.dataFiles[path]
|
||||
|
||||
fun addSyncJournalEntry(entry: StorageSyncJournalEntry) {
|
||||
accessorImpl.syncJournal.add(entry)
|
||||
}
|
||||
|
||||
fun syncJournalEntries(): List<StorageSyncJournalEntry> = accessorImpl.syncJournal.toList()
|
||||
|
||||
fun setAcquireLockResult(result: Boolean) {
|
||||
accessorImpl.acquireLockResult = result
|
||||
}
|
||||
}
|
||||
|
||||
class FakeMetaInfo(
|
||||
override val encInfo: StorageEncryptionInfo? = null,
|
||||
override val name: String = "Fake",
|
||||
override val lastModified: Instant = Instant.now(),
|
||||
) : IStorageMetaInfo
|
||||
@@ -0,0 +1,140 @@
|
||||
package com.github.nullptroma.wallenc.usecases.fakes
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.DataPage
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncJournalEntry
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncLock
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IDirectory
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IFile
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IMetaInfo
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.time.Instant
|
||||
|
||||
class FakeStorageAccessor : IStorageAccessor {
|
||||
val dataFiles: MutableMap<String, ByteArray> = mutableMapOf()
|
||||
private val systemFiles: MutableMap<String, ByteArray> = mutableMapOf()
|
||||
private val _filesUpdates = MutableSharedFlow<DataPage<IFile>>(extraBufferCapacity = 16)
|
||||
|
||||
var syncJournal: MutableList<StorageSyncJournalEntry> = mutableListOf()
|
||||
var syncLock: StorageSyncLock? = null
|
||||
var acquireLockResult: Boolean = true
|
||||
|
||||
override val size: StateFlow<Long?> = MutableStateFlow(0L)
|
||||
override val numberOfFiles: StateFlow<Int?> = MutableStateFlow(0)
|
||||
override val isAvailable: StateFlow<Boolean> = MutableStateFlow(true)
|
||||
override val filesUpdates: SharedFlow<DataPage<IFile>> = _filesUpdates
|
||||
override val dirsUpdates: SharedFlow<DataPage<IDirectory>> = MutableSharedFlow()
|
||||
|
||||
override suspend fun getAllFiles(): List<IFile> = emptyList()
|
||||
|
||||
override suspend fun getFiles(path: String): List<IFile> = emptyList()
|
||||
|
||||
override fun getFilesFlow(path: String): Flow<DataPage<IFile>> = emptyFlow()
|
||||
|
||||
override suspend fun getAllDirs(): List<IDirectory> = emptyList()
|
||||
|
||||
override suspend fun getDirs(path: String): List<IDirectory> = emptyList()
|
||||
|
||||
override fun getDirsFlow(path: String): Flow<DataPage<IDirectory>> = emptyFlow()
|
||||
|
||||
override suspend fun getFileInfo(path: String): IFile {
|
||||
error("Not implemented in tests")
|
||||
}
|
||||
|
||||
override suspend fun getDirInfo(path: String): IDirectory {
|
||||
error("Not implemented in tests")
|
||||
}
|
||||
|
||||
override suspend fun setHidden(path: String, hidden: Boolean) = Unit
|
||||
|
||||
override suspend fun touchFile(path: String) = Unit
|
||||
|
||||
override suspend fun touchDir(path: String) = Unit
|
||||
|
||||
override suspend fun delete(path: String) {
|
||||
dataFiles.remove(path)
|
||||
}
|
||||
|
||||
override suspend fun openWrite(path: String): OutputStream {
|
||||
return object : ByteArrayOutputStream() {
|
||||
override fun close() {
|
||||
dataFiles[path] = toByteArray()
|
||||
_filesUpdates.tryEmit(
|
||||
DataPage(
|
||||
listOf(FakeFile(path)),
|
||||
pageLength = 1,
|
||||
pageIndex = 0,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun openRead(path: String): InputStream {
|
||||
val bytes = dataFiles[path] ?: throw IllegalStateException("File not found: $path")
|
||||
return ByteArrayInputStream(bytes)
|
||||
}
|
||||
|
||||
override suspend fun moveToTrash(path: String) = Unit
|
||||
|
||||
override suspend fun openReadSystemFile(name: String): InputStream {
|
||||
val bytes = systemFiles[name] ?: ByteArray(0)
|
||||
return ByteArrayInputStream(bytes)
|
||||
}
|
||||
|
||||
override suspend fun openWriteSystemFile(name: String): OutputStream {
|
||||
return object : ByteArrayOutputStream() {
|
||||
override fun close() {
|
||||
systemFiles[name] = toByteArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun readSyncJournal(): List<StorageSyncJournalEntry> = syncJournal.toList()
|
||||
|
||||
override suspend fun appendSyncJournal(entries: List<StorageSyncJournalEntry>) {
|
||||
syncJournal.addAll(entries)
|
||||
}
|
||||
|
||||
override suspend fun rewriteSyncJournal(entries: List<StorageSyncJournalEntry>) {
|
||||
syncJournal.clear()
|
||||
syncJournal.addAll(entries)
|
||||
}
|
||||
|
||||
override suspend fun readSyncLock(): StorageSyncLock? = syncLock
|
||||
|
||||
override suspend fun tryAcquireSyncLock(holderId: String, leaseUntil: Instant): Boolean {
|
||||
if (!acquireLockResult) return false
|
||||
syncLock = StorageSyncLock(holderId, leaseUntil, Instant.now())
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun releaseSyncLock(holderId: String) {
|
||||
if (syncLock?.holderId == holderId) {
|
||||
syncLock = null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun forceClearSyncLock() {
|
||||
syncLock = null
|
||||
}
|
||||
}
|
||||
|
||||
class FakeFile(path: String) : IFile {
|
||||
override val metaInfo: IMetaInfo = object : IMetaInfo {
|
||||
override val size: Long = 0L
|
||||
override val isDeleted: Boolean = false
|
||||
override val isHidden: Boolean = false
|
||||
override val lastModified: Instant = Instant.now()
|
||||
override val path: String = path
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.github.nullptroma.wallenc.usecases.fakes
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageSyncGroupStore
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.StorageSyncGroup
|
||||
|
||||
class FakeStorageSyncGroupStore(
|
||||
private var groups: List<StorageSyncGroup> = emptyList(),
|
||||
) : IStorageSyncGroupStore {
|
||||
override suspend fun getGroups(): List<StorageSyncGroup> = groups
|
||||
|
||||
override suspend fun putGroup(group: StorageSyncGroup) {
|
||||
groups = groups.filterNot { it.id == group.id } + group
|
||||
}
|
||||
|
||||
override suspend fun removeGroup(groupId: String) {
|
||||
groups = groups.filterNot { it.id == groupId }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.github.nullptroma.wallenc.usecases.fakes
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IVault
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.util.UUID
|
||||
|
||||
class FakeVaultsManager(
|
||||
storages: List<IStorage>,
|
||||
) : IVaultsManager {
|
||||
override val vaults: StateFlow<List<IVault>> = MutableStateFlow(emptyList())
|
||||
override val allStorages: StateFlow<List<IStorage>> = MutableStateFlow(storages)
|
||||
override val unlockManager: IUnlockManager = FakeUnlockManager()
|
||||
}
|
||||
|
||||
class FakeUnlockManager : IUnlockManager {
|
||||
override val openedStorages: StateFlow<Map<UUID, IStorage>> = MutableStateFlow(emptyMap())
|
||||
|
||||
override fun getOpenedStorageKey(uuid: UUID): EncryptKey? = null
|
||||
|
||||
override suspend fun open(storage: IStorage, key: EncryptKey, rememberPassword: Boolean): IStorage = storage
|
||||
|
||||
override suspend fun close(storage: IStorage) = Unit
|
||||
|
||||
override suspend fun close(uuid: UUID) = Unit
|
||||
}
|
||||
Reference in New Issue
Block a user