From bfcca43580c0cb0c78825694f7c471d416880654 Mon Sep 17 00:00:00 2001 From: JaniruTEC <52893617+JaniruTEC@users.noreply.github.com> Date: Wed, 20 Mar 2024 00:29:35 +0100 Subject: [PATCH] Added tests for "insert" *This commit is related to issue #529 [1]* [1] https://github.com/cryptomator/android/issues/529 --- .../MappingSupportSQLiteDatabaseTest.kt | 241 +++++++++++++++++- 1 file changed, 238 insertions(+), 3 deletions(-) diff --git a/data/src/test/java/org/cryptomator/data/db/sqlmapping/MappingSupportSQLiteDatabaseTest.kt b/data/src/test/java/org/cryptomator/data/db/sqlmapping/MappingSupportSQLiteDatabaseTest.kt index 148a8fb61..91f18a229 100644 --- a/data/src/test/java/org/cryptomator/data/db/sqlmapping/MappingSupportSQLiteDatabaseTest.kt +++ b/data/src/test/java/org/cryptomator/data/db/sqlmapping/MappingSupportSQLiteDatabaseTest.kt @@ -8,24 +8,37 @@ import org.mockito.kotlin.argThat as reifiedArgThat import android.content.ContentValues import android.database.Cursor import android.database.MatrixCursor +import android.database.SQLException +import android.database.sqlite.SQLiteDatabase import android.os.CancellationSignal import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteProgram import androidx.sqlite.db.SupportSQLiteQuery +import androidx.sqlite.db.SupportSQLiteStatement +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource import org.mockito.ArgumentMatcher +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyLong import org.mockito.Mockito.anyString import org.mockito.Mockito.mock import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoInteractions import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.internal.verification.VerificationModeFactory.times +import org.mockito.invocation.InvocationOnMock import org.mockito.kotlin.anyArray import org.mockito.kotlin.eq +import org.mockito.kotlin.inOrder import org.mockito.kotlin.isNull +import org.mockito.stubbing.OngoingStubbing import java.util.stream.Stream import kotlin.streams.asStream @@ -149,7 +162,103 @@ class MappingSupportSQLiteDatabaseTest { verifyNoMoreInteractions(delegateMock) } - /* TODO insert */ + @ParameterizedTest + @MethodSource("org.cryptomator.data.db.sqlmapping.MappingSupportSQLiteDatabaseTestKt#sourceForTestInsert") + fun testInsert(arguments: CallDataTwo) { + val (idCompiledStatement: SupportSQLiteStatement, idBindings: Map) = mockSupportSQLiteStatement() + val (commentCompiledStatement: SupportSQLiteStatement, commentBindings: Map) = mockSupportSQLiteStatement() + + whenCalled(delegateMock.compileStatement(arguments.idExpected)).thenReturn(idCompiledStatement) + whenCalled(delegateMock.compileStatement(arguments.commentExpected)).thenReturn(commentCompiledStatement) + + val order = inOrder(delegateMock, idCompiledStatement, commentCompiledStatement) + identityMapping.insert("id_test", 1, arguments.idCall) + + order.verify(delegateMock).compileStatement(arguments.idExpected) + order.verify(idCompiledStatement, times(arguments.idCall.argCount())).bindString(anyInt(), anyString()) + order.verify(idCompiledStatement, times(arguments.idCall.nullCount())).bindNull(anyInt()) + order.verify(idCompiledStatement, times(arguments.idCall.argCount())).bindLong(anyInt(), anyLong()) + order.verify(idCompiledStatement).executeInsert() + order.verify(idCompiledStatement).close() + verifyNoMoreInteractions(idCompiledStatement) + + order.verifyNoMoreInteractions() + commentMapping.insert("comment_test", 2, arguments.commentCall) + + order.verify(delegateMock).compileStatement(arguments.commentExpected) + /* */ verifyNoMoreInteractions(delegateMock) + order.verify(commentCompiledStatement, times(arguments.commentCall.argCount())).bindString(anyInt(), anyString()) + order.verify(commentCompiledStatement, times(arguments.commentCall.nullCount())).bindNull(anyInt()) + order.verify(commentCompiledStatement, times(arguments.commentCall.argCount())).bindLong(anyInt(), anyLong()) + order.verify(commentCompiledStatement).executeInsert() + order.verify(commentCompiledStatement).close() + verifyNoMoreInteractions(commentCompiledStatement) + + order.verifyNoMoreInteractions() + + assertEquals(arguments.idCall.toBindingsMap(), idBindings) + assertEquals(arguments.commentCall.toBindingsMap(), commentBindings) + } + + @Test + fun testInsertEmptyValues() { + val emptyContentValues = mockContentValues() + + assertThrows { identityMapping.insert("id_test", 1, emptyContentValues) } + assertThrows { commentMapping.insert("comment_test", 2, emptyContentValues) } + + verifyNoInteractions(delegateMock) + } + + @ParameterizedTest + @MethodSource("org.cryptomator.data.db.sqlmapping.MappingSupportSQLiteDatabaseTestKt#sourceForTestInsertConflictAlgorithms") + fun testInsertConflictAlgorithms(arguments: Triple) { + val (conflictAlgorithm: Int, idStatement: String, commentStatement: String) = arguments + + val idCompiledStatement = mock(SupportSQLiteStatement::class.java) + val commentCompiledStatement = mock(SupportSQLiteStatement::class.java) + + whenCalled(delegateMock.compileStatement(idStatement)).thenReturn(idCompiledStatement) + whenCalled(delegateMock.compileStatement(commentStatement)).thenReturn(commentCompiledStatement) + + val order = inOrder(delegateMock, idCompiledStatement, commentCompiledStatement) + + val idContentValues = mockContentValues("col1" to "val1") //Inlining this declaration causes problems for some reason + assertDoesNotThrow { identityMapping.insert("id_test", conflictAlgorithm, idContentValues) } + + order.verify(delegateMock).compileStatement(idStatement) + order.verify(idCompiledStatement).bindString(1, "val1") + order.verify(idCompiledStatement).executeInsert() + order.verify(idCompiledStatement).close() + verifyNoMoreInteractions(idCompiledStatement) + + order.verifyNoMoreInteractions() + val commentContentValues = mockContentValues("col2" to "val2") + assertDoesNotThrow { commentMapping.insert("comment_test", conflictAlgorithm, commentContentValues) } + + order.verify(delegateMock).compileStatement(commentStatement) + /* */ verifyNoMoreInteractions(delegateMock) + order.verify(commentCompiledStatement).bindString(1, "val2") + order.verify(commentCompiledStatement).executeInsert() + order.verify(commentCompiledStatement).close() + verifyNoMoreInteractions(commentCompiledStatement) + + order.verifyNoMoreInteractions() + } + + @Test + fun testInsertInvalidConflictAlgorithms() { + val idContentValues = mockContentValues("col1" to "val1") //Inlining this declaration causes problems for some reason + val commentContentValues = mockContentValues("col2" to "val2") + + assertThrows { identityMapping.insert("id_test", -1, idContentValues) } + assertThrows { commentMapping.insert("comment_test", -1, commentContentValues) } + + assertThrows { identityMapping.insert("id_test", 6, idContentValues) } + assertThrows { commentMapping.insert("comment_test", 6, commentContentValues) } + + verifyNoInteractions(delegateMock) + } @ParameterizedTest @MethodSource("org.cryptomator.data.db.sqlmapping.MappingSupportSQLiteDatabaseTestKt#sourceForTestUpdate") @@ -241,6 +350,8 @@ private class PseudoEqualsMatcher( } } +private inline fun OngoingStubbing.thenDo(crossinline action: (invocation: InvocationOnMock) -> Unit): OngoingStubbing = thenAnswer { action(it) } + private class NullHandlingMatcher( private val delegate: ArgumentMatcher, private val matchNull: Boolean @@ -304,10 +415,34 @@ private fun mockCancellationSignal(isCanceled: Boolean): CancellationSignal { return mock } +private fun mockSupportSQLiteStatement(): Pair> { + val bindings: MutableMap = mutableMapOf() + val mock = mock(SupportSQLiteStatement::class.java) + whenCalled(mock.bindString(anyInt(), anyString())).thenDo { + bindings[it.getArgument(0, Integer::class.java).toInt()] = it.getArgument(1, String::class.java) + } + whenCalled(mock.bindLong(anyInt(), anyLong())).thenDo { + bindings[it.getArgument(0, Integer::class.java).toInt()] = it.getArgument(1, java.lang.Long::class.java) + } + whenCalled(mock.bindNull(anyInt())).thenDo { + bindings[it.getArgument(0, Integer::class.java).toInt()] = null + } + return mock to bindings +} + private fun mockContentValues(vararg elements: Pair): ContentValues { - val entries = mapOf(*elements) + return mockContentValues(mapOf(*elements)) +} + +private fun mockContentValues(entries: Map): ContentValues { val mock = mock(ContentValues::class.java) whenCalled(mock.valueSet()).thenReturn(entries.entries) + whenCalled(mock.size()).thenReturn(entries.size) + whenCalled(mock.isEmpty).thenReturn(entries.isEmpty()) + whenCalled(mock.keySet()).thenReturn(entries.keys) + whenCalled(mock.get(anyString())).then { + entries[it.getArgument(0, String::class.java)] + } whenCalled(mock.toString()).thenReturn("Mock${entries}") return mock } @@ -319,6 +454,13 @@ data class CallData( val commentExpected: T ) +data class CallDataTwo( + val idCall: C, + val commentCall: C, + val idExpected: E, + val commentExpected: E +) + fun sourceForTestQueryCancelable(): Stream { val queries = sequenceOf( CallData( @@ -352,6 +494,87 @@ fun sourceForTestQueryCancelable(): Stream { return queries.cartesianProduct(signals).map { it.toList() }.toArgumentsStream() } +fun sourceForTestInsert(): Stream> = sequenceOf( + //The ContentValues in this dataset always have the following order and counts: + //String [0,2], null[0,1], Int[0,1] + //This makes the ordered verification a lot easier + CallDataTwo( + mockContentValues("key1" to null), + mockContentValues("key2" to null), + "INSERT OR ROLLBACK INTO id_test(key1) VALUES (?)", + "INSERT OR ABORT INTO comment_test(key2) VALUES (?) -- Comment!" + ), + CallDataTwo( + mockContentValues("key1" to "value1"), + mockContentValues("key2" to "value2"), + "INSERT OR ROLLBACK INTO id_test(key1) VALUES (?)", + "INSERT OR ABORT INTO comment_test(key2) VALUES (?) -- Comment!" + ), + CallDataTwo( + mockContentValues("key1-1" to "value1-1", "key1-2" to "value1-2"), + mockContentValues("key2-1" to "value2-1", "key2-2" to "value2-2"), + "INSERT OR ROLLBACK INTO id_test(key1-1,key1-2) VALUES (?,?)", + "INSERT OR ABORT INTO comment_test(key2-1,key2-2) VALUES (?,?) -- Comment!" + ), + CallDataTwo( + mockContentValues("key1" to "value1", "intKey1" to 10101), + mockContentValues("key2" to "value2", "intKey2" to 20202), + "INSERT OR ROLLBACK INTO id_test(key1,intKey1) VALUES (?,?)", + "INSERT OR ABORT INTO comment_test(key2,intKey2) VALUES (?,?) -- Comment!" + ), + CallDataTwo( + mockContentValues("key1" to "value1", "nullKey1" to null), + mockContentValues("key2" to "value2", "nullKey2" to null), + "INSERT OR ROLLBACK INTO id_test(key1,nullKey1) VALUES (?,?)", + "INSERT OR ABORT INTO comment_test(key2,nullKey2) VALUES (?,?) -- Comment!" + ), + CallDataTwo( + mockContentValues("key1" to "value1", "nullKey1" to null, "intKey1" to 10101), + mockContentValues("key2" to "value2"), + "INSERT OR ROLLBACK INTO id_test(key1,nullKey1,intKey1) VALUES (?,?,?)", + "INSERT OR ABORT INTO comment_test(key2) VALUES (?) -- Comment!" + ), + CallDataTwo( + mockContentValues("key1" to "value1"), + mockContentValues("key2" to "value2", "nullKey2" to null, "intKey2" to 20202), + "INSERT OR ROLLBACK INTO id_test(key1) VALUES (?)", + "INSERT OR ABORT INTO comment_test(key2,nullKey2,intKey2) VALUES (?,?,?) -- Comment!" + ) +).asStream() + +fun sourceForTestInsertConflictAlgorithms(): Stream> = sequenceOf( + Triple( + SQLiteDatabase.CONFLICT_NONE, + "INSERT INTO id_test(col1) VALUES (?)", + "INSERT INTO comment_test(col2) VALUES (?) -- Comment!" + ), + Triple( + SQLiteDatabase.CONFLICT_ROLLBACK, + "INSERT OR ROLLBACK INTO id_test(col1) VALUES (?)", + "INSERT OR ROLLBACK INTO comment_test(col2) VALUES (?) -- Comment!" + ), + Triple( + SQLiteDatabase.CONFLICT_ABORT, + "INSERT OR ABORT INTO id_test(col1) VALUES (?)", + "INSERT OR ABORT INTO comment_test(col2) VALUES (?) -- Comment!" + ), + Triple( + SQLiteDatabase.CONFLICT_FAIL, + "INSERT OR FAIL INTO id_test(col1) VALUES (?)", + "INSERT OR FAIL INTO comment_test(col2) VALUES (?) -- Comment!" + ), + Triple( + SQLiteDatabase.CONFLICT_IGNORE, + "INSERT OR IGNORE INTO id_test(col1) VALUES (?)", + "INSERT OR IGNORE INTO comment_test(col2) VALUES (?) -- Comment!" + ), + Triple( + SQLiteDatabase.CONFLICT_REPLACE, + "INSERT OR REPLACE INTO id_test(col1) VALUES (?)", + "INSERT OR REPLACE INTO comment_test(col2) VALUES (?) -- Comment!" + ), +).asStream() + fun sourceForTestUpdate(): Stream { val contentValues = sequenceOf( CallData( @@ -411,4 +634,16 @@ fun Sequence>.cartesianProduct(other: Iterable): Sequenc fun Sequence>.toArgumentsStream(): Stream = map { Arguments { it.toTypedArray() } -}.asStream() \ No newline at end of file +}.asStream() + + +private fun ContentValues.nullCount(): Int = valueSet().count { it.value == null } + +private inline fun ContentValues.argCount(): Int = valueSet().asSequence().map { it.value }.filterIsInstance().count() + +private fun ContentValues.toBindingsMap(): Map { + return valueSet().map { it.value } // + .map { if (it is Int) it.toLong() else it } // Required because java.lang.Integer.valueOf(x) != java.lang.Long.valueOf(x) + .mapIndexed { index, value -> index + 1 to value } // + .toMap() +} \ No newline at end of file