diff --git a/android/src/main/kotlin/sncf/connect/tech/eventide/CalendarImplem.kt b/android/src/main/kotlin/sncf/connect/tech/eventide/CalendarImplem.kt index fe5839f..f189a5d 100644 --- a/android/src/main/kotlin/sncf/connect/tech/eventide/CalendarImplem.kt +++ b/android/src/main/kotlin/sncf/connect/tech/eventide/CalendarImplem.kt @@ -41,18 +41,12 @@ class CalendarImplem( put(CalendarContract.Calendars.CALENDAR_COLOR, color) put(CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, CalendarContract.Calendars.CAL_ACCESS_OWNER) put(CalendarContract.Calendars.OWNER_ACCOUNT, resolvedAccount.name) + put(CalendarContract.CALLER_IS_SYNCADAPTER, "true") put(CalendarContract.Calendars.VISIBLE, 1) put(CalendarContract.Calendars.SYNC_EVENTS, 1) } - val uri = calendarContentUri - .buildUpon() - .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true") - .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, resolvedAccount.name) - .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, resolvedAccount.type) - .build() - - val calendarUri = contentResolver.insert(uri, values) + val calendarUri = contentResolver.insert(calendarContentUri, values) if (calendarUri != null) { val calendarId = calendarUri.lastPathSegment if (calendarId != null) { @@ -68,7 +62,7 @@ class CalendarImplem( callback(Result.failure( FlutterError( code = "NOT_FOUND", - message = "Failed to retrieve calendar ID" + message = "Failed to retrieve calendar ID. It might not have been created" ) )) } @@ -196,8 +190,8 @@ class CalendarImplem( callback( Result.failure( FlutterError( - code = "NOT_FOUND", - message = "Failed to delete calendar" + code = "GENERIC_ERROR", + message = "An error occurred during deletion" ) ) ) @@ -251,48 +245,65 @@ class CalendarImplem( if (granted) { CoroutineScope(Dispatchers.IO).launch { try { - // TODO: check if calendar is writable - - val eventValues = ContentValues().apply { - put(CalendarContract.Events.CALENDAR_ID, calendarId) - put(CalendarContract.Events.TITLE, title) - put(CalendarContract.Events.DESCRIPTION, description) - put(CalendarContract.Events.DTSTART, startDate) - put(CalendarContract.Events.DTEND, endDate) - put(CalendarContract.Events.EVENT_TIMEZONE, "UTC") - put(CalendarContract.Events.ALL_DAY, isAllDay) - } + if (isCalendarWritable(calendarId)) { + val eventValues = ContentValues().apply { + put(CalendarContract.Events.CALENDAR_ID, calendarId) + put(CalendarContract.Events.TITLE, title) + put(CalendarContract.Events.DESCRIPTION, description) + put(CalendarContract.Events.DTSTART, startDate) + put(CalendarContract.Events.DTEND, endDate) + put(CalendarContract.Events.EVENT_TIMEZONE, "UTC") + put(CalendarContract.Events.ALL_DAY, isAllDay) + } - val eventUri = contentResolver.insert(eventContentUri, eventValues) - if (eventUri != null) { - val eventId = eventUri.lastPathSegment?.toLong() - if (eventId != null) { - val event = Event( - id = eventId.toString(), - title = title, - startDate = startDate, - endDate = endDate, - calendarId = calendarId, - description = description, - isAllDay = isAllDay - ) - callback(Result.success(event)) + val eventUri = contentResolver.insert(eventContentUri, eventValues) + if (eventUri != null) { + val eventId = eventUri.lastPathSegment?.toLong() + if (eventId != null) { + val event = Event( + id = eventId.toString(), + title = title, + startDate = startDate, + endDate = endDate, + calendarId = calendarId, + description = description, + isAllDay = isAllDay + ) + callback(Result.success(event)) + } else { + callback( + Result.failure( + FlutterError( + code = "NOT_FOUND", + message = "Failed to retrieve event ID" + ) + ) + ) + } } else { - callback(Result.failure( - FlutterError( - code = "NOT_FOUND", - message = "Failed to retrieve event ID" + callback( + Result.failure( + FlutterError( + code = "GENERIC_ERROR", + message = "Failed to create event" + ) ) - )) + ) } } else { - callback(Result.failure( - FlutterError( - code = "GENERIC_ERROR", - message = "Failed to create event" + callback( + Result.failure( + FlutterError( + code = "NOT_EDITABLE", + message = "Calendar is not writable" + ) ) - )) + ) } + + } catch (e: FlutterError) { + callback(Result.failure(e)) + } catch (e: Exception) { callback(Result.failure( FlutterError( @@ -416,21 +427,38 @@ class CalendarImplem( if (granted) { CoroutineScope(Dispatchers.IO).launch { try { - // TODO: check if calendar is writable - val selection = CalendarContract.Events._ID + " = ?" - val selectionArgs = arrayOf(eventId) + val calendarId = getCalendarId(eventId) + if (isCalendarWritable(calendarId)) { + val selection = CalendarContract.Events._ID + " = ?" + val selectionArgs = arrayOf(eventId) - val deleted = contentResolver.delete(eventContentUri, selection, selectionArgs) - if (deleted > 0) { - callback(Result.success(Unit)) + val deleted = contentResolver.delete(eventContentUri, selection, selectionArgs) + if (deleted > 0) { + callback(Result.success(Unit)) + } else { + callback( + Result.failure( + FlutterError( + code = "NOT_FOUND", + message = "Failed to delete event" + ) + ) + ) + } } else { - callback(Result.failure( - FlutterError( - code = "NOT_FOUND", - message = "Failed to delete event" + callback( + Result.failure( + FlutterError( + code = "NOT_EDITABLE", + message = "Calendar is not writable" + ) ) - )) + ) } + + } catch (e: FlutterError) { + callback(Result.failure(e)) + } catch (e: Exception) { callback(Result.failure( FlutterError( @@ -555,6 +583,33 @@ class CalendarImplem( ) } + private fun getCalendarId( + eventId: String, + ): String { + val projection = arrayOf( + CalendarContract.Events.CALENDAR_ID + ) + val selection = CalendarContract.Events._ID + " = ?" + val selectionArgs = arrayOf(eventId) + + val cursor = contentResolver.query(eventContentUri, projection, selection, selectionArgs, null) + cursor?.use { + if (it.moveToNext()) { + return it.getLong(it.getColumnIndexOrThrow(CalendarContract.Events.CALENDAR_ID)).toString() + } else { + throw FlutterError( + code = "NOT_FOUND", + message = "Failed to retrieve event" + ) + } + } + + throw FlutterError( + code = "GENERIC_ERROR", + message = "An error occurred" + ) + } + private fun retrieveEvent( eventId: String, callback: (Result) -> Unit diff --git a/android/src/test/kotlin/sncf/connect/tech/eventide/CalendarImplemTest.kt b/android/src/test/kotlin/sncf/connect/tech/eventide/CalendarImplemTest.kt deleted file mode 100644 index cbdecb3..0000000 --- a/android/src/test/kotlin/sncf/connect/tech/eventide/CalendarImplemTest.kt +++ /dev/null @@ -1,798 +0,0 @@ -package sncf.connect.tech.eventide - -import android.content.ContentResolver -import android.database.Cursor -import android.net.Uri -import android.provider.CalendarContract -import io.mockk.* -import kotlinx.coroutines.delay -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import java.time.Instant -import java.util.concurrent.CountDownLatch - -class CalendarImplemTest { - private lateinit var contentResolver: ContentResolver - private lateinit var permissionHandler: PermissionHandler - private lateinit var calendarImplem: CalendarImplem - private lateinit var calendarContentUri: Uri - private lateinit var eventContentUri: Uri - private lateinit var remindersContentUri: Uri - - @BeforeEach - fun setup() { - calendarContentUri = mockk(relaxed = true) - eventContentUri = mockk(relaxed = true) - remindersContentUri = mockk(relaxed = true) - - contentResolver = mockk(relaxed = true) - permissionHandler = mockk(relaxed = true) - calendarImplem = CalendarImplem( - contentResolver = contentResolver, - permissionHandler = permissionHandler, - calendarContentUri = calendarContentUri, - eventContentUri = eventContentUri, - remindersContentUri = remindersContentUri, - ) - } - - @Test - fun requestCalendarPermission_withGrantedPermission_returnsTrue() = runTest { - every { permissionHandler.requestWritePermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(true) - } - - var result: Result? = null - val latch = CountDownLatch(1) - calendarImplem.requestCalendarPermission { - result = it - latch.countDown() - } - - latch.await() - - assertTrue(result!!.isSuccess) - assertTrue(result!!.getOrNull()!!) - } - - @Test - fun requestCalendarPermission_withDeniedPermission_returnsFalse() = runTest { - every { permissionHandler.requestWritePermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(false) - } - - var result: Result? = null - val latch = CountDownLatch(1) - calendarImplem.requestCalendarPermission { - result = it - latch.countDown() - } - - latch.await() - - assertTrue(result!!.isSuccess) - assertFalse(result!!.getOrNull()!!) - } - - @Test - fun createCalendar_withGrantedPermission_createsCalendarSuccessfully() = runTest { - every { permissionHandler.requestWritePermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(true) - } - val uri = mockk(relaxed = true) - every { contentResolver.insert(any(), any()) } returns uri - every { uri.lastPathSegment } returns "1" - - var result: Result? = null - val latch = CountDownLatch(1) - calendarImplem.createCalendar("Test Calendar", 0x00FF00, Account("1", "Test Account")) { - result = it - latch.countDown() - } - - latch.await() - - assertTrue(result!!.isSuccess) - assertTrue(result!!.getOrNull()!!.title == "Test Calendar") - assertTrue(result!!.getOrNull()!!.color.toInt() == 0x00FF00) - assertTrue(result!!.getOrNull()!!.account.name == "1") - assertTrue(result!!.getOrNull()!!.account.type == "Test Account") - assertEquals("1", result!!.getOrNull()?.id) - } - - @Test - fun createCalendar_withDeniedPermission_failsToCreateCalendar() = runTest { - every { permissionHandler.requestWritePermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(false) - } - - var result: Result? = null - calendarImplem.createCalendar("Test Calendar", 0xFF0000, null) { - result = it - } - - assertTrue(result!!.isFailure) - assertNull(result!!.getOrNull()) - } - - @Test - fun createCalendar_withInvalidUri_failsToCreateCalendar() = runTest { - every { permissionHandler.requestWritePermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(true) - } - every { contentResolver.insert(any(), any()) } returns null - - var result: Result? = null - val latch = CountDownLatch(1) - calendarImplem.createCalendar("Test Calendar", 0xFF0000, null) { - result = it - latch.countDown() - } - - latch.await() - - assertTrue(result!!.isFailure) - assertNull(result!!.getOrNull()) - } - - @Test - fun createCalendar_withNullLastPathSegment_failsToRetrieveCalendarId() = runTest { - every { permissionHandler.requestWritePermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(true) - } - val uri = mockk(relaxed = true) - every { contentResolver.insert(any(), any()) } returns uri - every { uri.lastPathSegment } returns null - - var result: Result? = null - val latch = CountDownLatch(1) - calendarImplem.createCalendar("Test Calendar", 0xFF0000, null) { - result = it - latch.countDown() - } - - latch.await() - - assertTrue(result!!.isFailure) - assertNull(result!!.getOrNull()) - } - - @Test - fun retrieveCalendars_withGrantedPermission_returnsCalendars() = runTest { - every { permissionHandler.requestReadPermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(true) - } - val cursor = mockk(relaxed = true) - every { contentResolver.query(calendarContentUri, any(), any(), any(), any()) } returns cursor - every { cursor.moveToNext() } returnsMany listOf(true, true, false) - every { cursor.getLong(any()) } returnsMany listOf(1L, 0xFF0000, 2L, 0xFF0000) - every { cursor.getString(any()) } returnsMany listOf("Test Calendar", "Test Account", "Test Account Type", "Test Calendar2", "Test Account", "Test Account Type") - every { cursor.getInt(any()) } returnsMany listOf(CalendarContract.Calendars.CAL_ACCESS_OWNER, CalendarContract.Calendars.CAL_ACCESS_OWNER) - - var result: Result>? = null - val latch = CountDownLatch(1) - calendarImplem.retrieveCalendars(false, null) { - result = it - latch.countDown() - } - - latch.await() - - assertTrue(result!!.isSuccess) - assertEquals(2, result!!.getOrNull()?.size) - assertEquals("Test Calendar", result!!.getOrNull()?.get(0)?.title) - assertEquals("Test Calendar2", result!!.getOrNull()?.get(1)?.title) - } - - @Test - fun retrieveCalendars_withGrantedPermission_returnsOnlyWritableCalendars() = runTest { - every { permissionHandler.requestReadPermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(true) - } - val cursor = mockk(relaxed = true) - every { contentResolver.query(calendarContentUri, any(), any(), any(), any()) } returns cursor - every { cursor.moveToNext() } returnsMany listOf(true, true, false) - every { cursor.getLong(any()) } returnsMany listOf(1L, 0xFF0000, 2L, 0xFF0000) - every { cursor.getString(any()) } returnsMany listOf("Test Calendar", "Test Account", "Test Account Type", "Test Calendar", "Test Account", "Test Account Type") - every { cursor.getInt(any()) } returnsMany listOf(CalendarContract.Calendars.CAL_ACCESS_OWNER, CalendarContract.Calendars.CAL_ACCESS_READ) - - var result: Result>? = null - val latch = CountDownLatch(1) - calendarImplem.retrieveCalendars(true, null) { - result = it - latch.countDown() - } - - latch.await() - - assertTrue(result!!.isSuccess) - assertEquals(1, result!!.getOrNull()?.size) - assertEquals("Test Calendar", result!!.getOrNull()?.get(0)?.title) - } - - @Test - fun retrieveCalendars_accountFilter_appliesCorrectSelection() = runTest { - every { permissionHandler.requestReadPermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(true) - } - val cursor = mockk(relaxed = true) - every { contentResolver.query(calendarContentUri, any(), any(), any(), any()) } returns cursor - every { cursor.moveToNext() } returns false - - var result: Result>? = null - val latch = CountDownLatch(1) - calendarImplem.retrieveCalendars(false, Account("testAccount", "testType")) { - result = it - latch.countDown() - } - - latch.await() - - val expectedSelection = "${CalendarContract.Calendars.ACCOUNT_NAME} = ? AND ${CalendarContract.Calendars.ACCOUNT_TYPE} = ?" - val expectedSelectionArgs = arrayOf("testAccount", "testType") - - verify { - contentResolver.query( - calendarContentUri, - any(), - expectedSelection, - expectedSelectionArgs, - any() - ) - } - - assertTrue(result!!.isSuccess) - assertTrue(result!!.getOrNull()?.isEmpty()!!) - } - - @Test - fun retrieveCalendars_noFilter_appliesCorrectSelection() = runTest { - every { permissionHandler.requestReadPermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(true) - } - val cursor = mockk(relaxed = true) - every { contentResolver.query(calendarContentUri, any(), any(), any(), any()) } returns cursor - every { cursor.moveToNext() } returns false - - var result: Result>? = null - val latch = CountDownLatch(1) - calendarImplem.retrieveCalendars(false, null) { - result = it - latch.countDown() - } - - latch.await() - - verify { - contentResolver.query( - calendarContentUri, - any(), - null, - null, - any() - ) - } - - assertTrue(result!!.isSuccess) - assertTrue(result!!.getOrNull()?.isEmpty()!!) - } - - @Test - fun retrieveCalendars_withDeniedPermission_failsToRetrieveCalendars() = runTest { - every { permissionHandler.requestReadPermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(false) - } - - var result: Result>? = null - calendarImplem.retrieveCalendars(false, null) { - result = it - } - - assertTrue(result!!.isFailure) - } - - @Test - fun retrieveCalendars_withEmptyCursor_returnsEmptyList() = runTest { - every { permissionHandler.requestReadPermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(true) - } - val cursor = mockk(relaxed = true) - every { contentResolver.query(calendarContentUri, any(), any(), any(), any()) } returns cursor - every { cursor.moveToNext() } returns false - - var result: Result>? = null - val latch = CountDownLatch(1) - calendarImplem.retrieveCalendars(false, null) { - result = it - latch.countDown() - } - - latch.await() - - assertTrue(result!!.isSuccess) - assertTrue(result!!.getOrNull()?.isEmpty()!!) - } - - @Test - fun deleteCalendar_withGrantedPermission_andWritableCalendar_deletesCalendarSuccessfully() = runTest { - every { permissionHandler.requestWritePermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(true) - } - val cursor = mockk(relaxed = true) - every { contentResolver.query(calendarContentUri, any(), any(), any(), any()) } returns cursor - every { cursor.moveToNext() } returns true - every { cursor.getInt(any()) } returns CalendarContract.Calendars.CAL_ACCESS_OWNER - every { contentResolver.delete(calendarContentUri, any(), any()) } returns 1 - - var result: Result? = null - val latch = CountDownLatch(1) - calendarImplem.deleteCalendar("1") { - result = it - latch.countDown() - } - - latch.await() - - assertTrue(result!!.isSuccess) - } - - @Test - fun deleteCalendar_withDeniedPermission_failsToDeleteCalendar() = runTest { - every { permissionHandler.requestWritePermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(false) - } - - var result: Result? = null - calendarImplem.deleteCalendar("1") { - result = it - } - - assertTrue(result!!.isFailure) - } - - @Test - fun deleteCalendar_withException_failsToDeleteCalendar() = runTest { - every { permissionHandler.requestWritePermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(true) - } - every { contentResolver.delete(calendarContentUri, any(), any()) } throws Exception("Delete failed") - - var result: Result? = null - val latch = CountDownLatch(1) - calendarImplem.deleteCalendar("1") { - result = it - latch.countDown() - } - - latch.await() - - assertTrue(result!!.isFailure) - } - - @Test - fun deleteCalendar_withNoRowsDeleted_failsToDeleteCalendar() = runTest { - every { permissionHandler.requestWritePermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(true) - } - every { contentResolver.delete(calendarContentUri, any(), any()) } returns 0 - - var result: Result? = null - val latch = CountDownLatch(1) - calendarImplem.deleteCalendar("1") { - result = it - latch.countDown() - } - - latch.await() - - assertTrue(result!!.isFailure) - } - - @Test - fun createEvent_withGrantedPermission_createsEventSuccessfully() = runTest { - every { permissionHandler.requestWritePermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(true) - } - val uri = mockk(relaxed = true) - every { contentResolver.insert(any(), any()) } returns uri - every { uri.lastPathSegment } returns "1" - - val startMilli = Instant.now().toEpochMilli() - val endMilli = Instant.now().toEpochMilli() - - var result: Result? = null - val latch = CountDownLatch(1) - calendarImplem.createEvent( - title = "Test Event", - startDate = startMilli, - endDate = endMilli, - calendarId = "1", - description = "Description", - isAllDay = false, - url = null - ) { - result = it - latch.countDown() - } - - latch.await() - - assertTrue(result!!.isSuccess) - assertEquals(Event( - id = "1", - title = "Test Event", - startDate = startMilli, - endDate = endMilli, - calendarId = "1", - description = "Description", - isAllDay = false, - ), result!!.getOrNull()!!) - } - - @Test - fun createEvent_withDeniedPermission_failsToCreateEvent() = runTest { - every { permissionHandler.requestWritePermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(false) - } - - var result: Result? = null - calendarImplem.createEvent( - title = "Test Event", - startDate = Instant.now().toEpochMilli(), - endDate = Instant.now().toEpochMilli(), - calendarId = "1", - description = "Description", - isAllDay = false, - url = null - ) { - result = it - } - - assertTrue(result!!.isFailure) - } - - @Test - fun createEvent_withInvalidUri_failsToCreateEvent() = runTest { - every { permissionHandler.requestWritePermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(true) - } - every { contentResolver.insert(any(), any()) } returns null - - var result: Result? = null - val latch = CountDownLatch(1) - calendarImplem.createEvent( - title = "Test Event", - startDate = Instant.now().toEpochMilli(), - endDate = Instant.now().toEpochMilli(), - calendarId = "1", - description = "Description", - isAllDay = false, - url = null - ) { - result = it - latch.countDown() - } - - latch.await() - - assertTrue(result!!.isFailure) - } - - @Test - fun retrieveEvents_withGrantedPermission_returnsEvents() = runTest { - every { permissionHandler.requestReadPermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(true) - } - val cursor = mockk(relaxed = true) - - CalendarContract.Events.ALL_DAY - every { contentResolver.query(eventContentUri, any(), any(), any(), any()) } returns cursor - every { cursor.moveToNext() } returnsMany listOf(true, false) - every { cursor.getLong(cursor.getColumnIndexOrThrow(CalendarContract.Events._ID)) } returns 1L - every { cursor.getString(any()) } returnsMany listOf("Test Event", "Description") - every { cursor.getLong(any()) } returns 0L - every { cursor.getInt(cursor.getColumnIndexOrThrow(CalendarContract.Events.ALL_DAY)) } returns 0 - - var result: Result>? = null - val latch = CountDownLatch(1) - calendarImplem.retrieveEvents("1", 0L, 0L) { - result = it - latch.countDown() - } - - latch.await() - - assertTrue(result!!.isSuccess) - assertEquals(1, result!!.getOrNull()?.size) - assertEquals("Test Event", result!!.getOrNull()?.get(0)?.title) - } - - @Test - fun retrieveEvents_withDeniedPermission_failsToRetrieveEvents() = runTest { - every { permissionHandler.requestReadPermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(false) - } - - var result: Result>? = null - calendarImplem.retrieveEvents("1", 0L, 0L) { - result = it - } - - assertTrue(result!!.isFailure) - } - - @Test - fun retrieveEvents_withEmptyCursor_returnsEmptyList() = runTest { - every { permissionHandler.requestReadPermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(true) - } - val cursor = mockk(relaxed = true) - every { contentResolver.query(eventContentUri, any(), any(), any(), any()) } returns cursor - every { cursor.moveToNext() } returns false - - var result: Result>? = null - val latch = CountDownLatch(1) - calendarImplem.retrieveEvents("1", 0L, 0L) { - result = it - latch.countDown() - } - - latch.await() - - assertTrue(result!!.isSuccess) - assertTrue(result!!.getOrNull()?.isEmpty()!!) - } - - @Test - fun retrieveEvents_withException_failsToRetrieveEvents() = runTest { - every { permissionHandler.requestReadPermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(true) - } - every { contentResolver.query(eventContentUri, any(), any(), any(), any()) } throws Exception("Query failed") - - var result: Result>? = null - val latch = CountDownLatch(1) - calendarImplem.retrieveEvents("1", 0L, 0L) { - result = it - latch.countDown() - } - - latch.await() - - assertTrue(result!!.isFailure) - } - - @Test - fun deleteEvent_withGrantedPermission_deletesEventSuccessfully() = runTest { - every { permissionHandler.requestWritePermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(true) - } - every { contentResolver.delete(eventContentUri, any(), any()) } returns 1 - - var result: Result? = null - val latch = CountDownLatch(1) - calendarImplem.deleteEvent("1") { - result = it - latch.countDown() - } - - latch.await() - - assertTrue(result!!.isSuccess) - } - - @Test - fun deleteEvent_withDeniedPermission_failsToDeleteEvent() = runTest { - every { permissionHandler.requestWritePermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(false) - } - - var result: Result? = null - calendarImplem.deleteEvent("1") { - result = it - } - - assertTrue(result!!.isFailure) - } - - @Test - fun deleteEvent_withException_failsToDeleteEvent() = runTest { - every { permissionHandler.requestWritePermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(true) - } - every { contentResolver.delete(eventContentUri, any(), any()) } throws Exception("Delete failed") - - var result: Result? = null - val latch = CountDownLatch(1) - calendarImplem.deleteEvent("1") { - result = it - latch.countDown() - } - - latch.await() - - assertTrue(result!!.isFailure) - } - - @Test - fun deleteEvent_withNoRowsDeleted_failsToDeleteEvent() = runTest { - every { permissionHandler.requestWritePermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(true) - } - every { contentResolver.delete(eventContentUri, any(), any()) } returns 0 - - var result: Result? = null - val latch = CountDownLatch(1) - calendarImplem.deleteEvent("1") { - result = it - latch.countDown() - } - - latch.await() - - assertTrue(result!!.isFailure) - } - - @Test - fun createReminder_withGrantedPermission_createsReminderSuccessfully() = runTest { - every { permissionHandler.requestWritePermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(true) - } - val eventId = "1" - val minutes = 10L - every { contentResolver.insert(remindersContentUri, any()) } returns mockk(relaxed = true) - - val eventCursor = mockk(relaxed = true) - every { contentResolver.query(eventContentUri, any(), any(), any(), any()) } returns eventCursor - every { eventCursor.moveToNext() } returnsMany listOf(true, false) - every { eventCursor.getLong(any()) } returns 1L - every { eventCursor.getString(any()) } returns "Test Event" - every { eventCursor.getLong(any()) } returns 0L - - val remindersCursor = mockk(relaxed = true) - every { contentResolver.query(remindersContentUri, any(), any(), any(), any()) } returns remindersCursor - every { remindersCursor.moveToNext() } returnsMany listOf(true, false) - every { remindersCursor.getLong(any()) } returns 10L - - var result: Result? = null - val latch = CountDownLatch(1) - calendarImplem.createReminder(minutes, eventId) { - result = it - latch.countDown() - } - - latch.await() - - assertTrue(result!!.isSuccess) - } - - @Test - fun createReminder_withDeniedPermission_failsToCreateReminder() = runTest { - every { permissionHandler.requestWritePermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(false) - } - val eventId = "1" - val minutes = 10L - - var result: Result? = null - calendarImplem.createReminder(minutes, eventId) { - result = it - } - - assertTrue(result!!.isFailure) - } - - @Test - fun createReminder_withException_failsToCreateReminder() = runTest { - every { permissionHandler.requestWritePermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(true) - } - val eventId = "1" - val minutes = 10L - every { contentResolver.insert(remindersContentUri, any()) } throws Exception("Insert failed") - - var result: Result? = null - val latch = CountDownLatch(1) - calendarImplem.createReminder(minutes, eventId) { - result = it - latch.countDown() - } - - latch.await() - - assertTrue(result!!.isFailure) - } - - @Test - fun deleteReminder_withGrantedPermission_deletesReminderSuccessfully() = runTest { - val eventId = "1" - val minutes = 10L - every { permissionHandler.requestWritePermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(true) - } - every { contentResolver.delete(remindersContentUri, any(), any()) } returns 1 - - val eventCursor = mockk(relaxed = true) - every { contentResolver.query(eventContentUri, any(), any(), any(), any()) } returns eventCursor - every { eventCursor.moveToNext() } returnsMany listOf(true, false) - every { eventCursor.getLong(any()) } returns 1L - every { eventCursor.getString(any()) } returns "Test Event" - every { eventCursor.getLong(any()) } returns 0L - - - val remindersCursor = mockk(relaxed = true) - every { contentResolver.query(remindersContentUri, any(), any(), any(), any()) } returns remindersCursor - every { remindersCursor.moveToNext() } returnsMany listOf(false, false) - - var result: Result? = null - val latch = CountDownLatch(1) - calendarImplem.deleteReminder(minutes, eventId) { - result = it - latch.countDown() - } - - latch.await() - - assertTrue(result!!.isSuccess) - } - - @Test - fun deleteReminder_withDeniedPermission_failsToDeleteReminder() = runTest { - val eventId = "1" - val minutes = 10L - every { permissionHandler.requestWritePermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(false) - } - - var result: Result? = null - calendarImplem.deleteReminder(minutes, eventId) { - result = it - } - - assertTrue(result!!.isFailure) - } - - @Test - fun deleteReminder_withException_failsToDeleteReminder() = runTest { - val eventId = "1" - val minutes = 10L - every { permissionHandler.requestWritePermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(true) - } - every { contentResolver.delete(eventContentUri, any(), any()) } throws Exception("Delete failed") - - var result: Result? = null - val latch = CountDownLatch(1) - calendarImplem.deleteReminder(minutes, eventId) { - result = it - latch.countDown() - } - - latch.await() - - assertTrue(result!!.isFailure) - } - - @Test - fun deleteReminder_withNoRowsDeleted_failsToDeleteReminder() = runTest { - val eventId = "1" - val minutes = 10L - every { permissionHandler.requestWritePermission(any()) } answers { - firstArg<(Boolean) -> Unit>().invoke(true) - } - every { contentResolver.delete(remindersContentUri, any(), any()) } returns 0 - - var result: Result? = null - val latch = CountDownLatch(1) - calendarImplem.deleteReminder(minutes, eventId) { - result = it - latch.countDown() - } - - latch.await() - - assertTrue(result!!.isFailure) - } -} diff --git a/android/src/test/kotlin/sncf/connect/tech/eventide/CalendarTests.kt b/android/src/test/kotlin/sncf/connect/tech/eventide/CalendarTests.kt new file mode 100644 index 0000000..ddcfe39 --- /dev/null +++ b/android/src/test/kotlin/sncf/connect/tech/eventide/CalendarTests.kt @@ -0,0 +1,473 @@ +package sncf.connect.tech.eventide + +import android.content.ContentResolver +import android.database.Cursor +import android.net.Uri +import android.provider.CalendarContract +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.util.concurrent.CountDownLatch + +class CalendarTests { + private lateinit var contentResolver: ContentResolver + private lateinit var permissionHandler: PermissionHandler + private lateinit var calendarImplem: CalendarImplem + private lateinit var calendarContentUri: Uri + private lateinit var eventContentUri: Uri + private lateinit var remindersContentUri: Uri + + @BeforeEach + fun setup() { + contentResolver = mockk(relaxed = true) + permissionHandler = mockk(relaxed = true) + calendarContentUri = mockk(relaxed = true) + eventContentUri = mockk(relaxed = true) + remindersContentUri = mockk(relaxed = true) + + calendarImplem = CalendarImplem( + contentResolver = contentResolver, + permissionHandler = permissionHandler, + calendarContentUri = calendarContentUri, + eventContentUri = eventContentUri, + remindersContentUri = remindersContentUri + ) + } + + private fun mockPermissionGranted() { + every { permissionHandler.requestWritePermission(any()) } answers { + firstArg<(Boolean) -> Unit>().invoke(true) + } + + every { permissionHandler.requestReadPermission(any()) } answers { + firstArg<(Boolean) -> Unit>().invoke(true) + } + } + + private fun mockPermissionDenied() { + every { permissionHandler.requestWritePermission(any()) } answers { + firstArg<(Boolean) -> Unit>().invoke(false) + } + every { permissionHandler.requestReadPermission(any()) } answers { + firstArg<(Boolean) -> Unit>().invoke(false) + } + } + + private fun mockWritableCalendar() { + val cursor = mockk(relaxed = true) + every { contentResolver.query(calendarContentUri, any(), any(), any(), any()) } returns cursor + every { cursor.moveToNext() } returns true + every { cursor.getInt(any()) } returns CalendarContract.Calendars.CAL_ACCESS_CONTRIBUTOR + } + + private fun mockNotWritableCalendar() { + val cursor = mockk(relaxed = true) + every { contentResolver.query(calendarContentUri, any(), any(), any(), any()) } returns cursor + every { cursor.moveToNext() } returns true + every { cursor.getInt(any()) } returns CalendarContract.Calendars.CAL_ACCESS_READ + } + + private fun mockCalendarNotFound() { + val cursor = mockk(relaxed = true) + every { contentResolver.query(calendarContentUri, any(), any(), any(), any()) } returns cursor + every { cursor.moveToNext() } returns false + } + + @Test + fun createCalendar_withGrantedPermission_createsCalendarSuccessfully() = runTest { + mockPermissionGranted() + + val uri = mockk(relaxed = true) + every { contentResolver.insert(calendarContentUri, any()) } returns uri + every { uri.lastPathSegment } returns "1" + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.createCalendar("Test Calendar", 0x00FF00, Account("1", "Test Account")) { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isSuccess) + assertTrue(result!!.getOrNull()!!.title == "Test Calendar") + assertTrue(result!!.getOrNull()!!.color.toInt() == 0x00FF00) + assertTrue(result!!.getOrNull()!!.account.name == "1") + assertTrue(result!!.getOrNull()!!.account.type == "Test Account") + assertEquals("1", result!!.getOrNull()?.id) + } + + @Test + fun createCalendar_withDeniedPermission_returnsAccessRefusedError() = runTest { + mockPermissionDenied() + + var result: Result? = null + calendarImplem.createCalendar("Test Calendar", 0xFF0000, null) { + result = it + } + + assertTrue(result!!.isFailure) + assertEquals("ACCESS_REFUSED", (result!!.exceptionOrNull() as FlutterError).code) + } + + @Test + fun createCalendar_withInvalidUri_returnsGenericError() = runTest { + mockPermissionGranted() + + every { contentResolver.insert(calendarContentUri, any()) } returns null + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.createCalendar("Test Calendar", 0xFF0000, null) { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isFailure) + assertEquals("GENERIC_ERROR", (result!!.exceptionOrNull() as FlutterError).code) + } + + @Test + fun createCalendar_withNullLastPathSegment_returnsNotFoundError() = runTest { + mockPermissionGranted() + + val uri = mockk(relaxed = true) + every { contentResolver.insert(calendarContentUri, any()) } returns uri + every { uri.lastPathSegment } returns null + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.createCalendar("Test Calendar", 0xFF0000, null) { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isFailure) + assertEquals("NOT_FOUND", (result!!.exceptionOrNull() as FlutterError).code) + } + + @Test + fun createCalendar_withException_returnsGenericError() = runTest { + mockPermissionGranted() + + every { contentResolver.insert(calendarContentUri, any()) } throws Exception("Insert failed") + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.createCalendar("Test Calendar", 0xFF0000, null) { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isFailure) + assertEquals("GENERIC_ERROR", (result!!.exceptionOrNull() as FlutterError).code) + } + + @Test + fun retrieveCalendars_withGrantedPermission_returnsCalendars() = runTest { + mockPermissionGranted() + val cursor = mockk(relaxed = true) + every { contentResolver.query(calendarContentUri, any(), any(), any(), any()) } returns cursor + every { cursor.moveToNext() } returnsMany listOf(true, true, false) + every { cursor.getLong(any()) } returnsMany listOf(1L, 0xFF0000, 2L, 0xFF0000) + every { cursor.getString(any()) } returnsMany listOf("Test Calendar", "Test Account", "Test Account Type", "Test Calendar2", "Test Account", "Test Account Type") + every { cursor.getInt(any()) } returnsMany listOf(CalendarContract.Calendars.CAL_ACCESS_OWNER, CalendarContract.Calendars.CAL_ACCESS_OWNER) + + var result: Result>? = null + val latch = CountDownLatch(1) + calendarImplem.retrieveCalendars(false, null) { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isSuccess) + assertEquals(2, result!!.getOrNull()?.size) + assertEquals("Test Calendar", result!!.getOrNull()?.get(0)?.title) + assertEquals("Test Calendar2", result!!.getOrNull()?.get(1)?.title) + } + + @Test + fun retrieveCalendars_withGrantedPermission_returnsOnlyWritableCalendars() = runTest { + mockPermissionGranted() + + val cursor = mockk(relaxed = true) + every { contentResolver.query(calendarContentUri, any(), any(), any(), any()) } returns cursor + every { cursor.moveToNext() } returnsMany listOf(true, true, false) + every { cursor.getLong(any()) } returnsMany listOf(1L, 0xFF0000, 2L, 0xFF0000) + every { cursor.getString(any()) } returnsMany listOf("Test Calendar", "Test Account", "Test Account Type", "Test Calendar", "Test Account", "Test Account Type") + every { cursor.getInt(any()) } returnsMany listOf(CalendarContract.Calendars.CAL_ACCESS_OWNER, CalendarContract.Calendars.CAL_ACCESS_READ) + + var result: Result>? = null + val latch = CountDownLatch(1) + calendarImplem.retrieveCalendars(true, null) { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isSuccess) + assertEquals(1, result!!.getOrNull()?.size) + assertEquals("Test Calendar", result!!.getOrNull()?.get(0)?.title) + } + + @Test + fun retrieveCalendars_accountFilter_appliesCorrectSelection() = runTest { + mockPermissionGranted() + + val cursor = mockk(relaxed = true) + every { contentResolver.query(calendarContentUri, any(), any(), any(), any()) } returns cursor + + var result: Result>? = null + val latch = CountDownLatch(1) + calendarImplem.retrieveCalendars(false, Account("testAccount", "testType")) { + result = it + latch.countDown() + } + + latch.await() + + val expectedSelection = "${CalendarContract.Calendars.ACCOUNT_NAME} = ? AND ${CalendarContract.Calendars.ACCOUNT_TYPE} = ?" + val expectedSelectionArgs = arrayOf("testAccount", "testType") + + verify { + contentResolver.query( + calendarContentUri, + any(), + expectedSelection, + expectedSelectionArgs, + any() + ) + } + + assertTrue(result!!.isSuccess) + assertTrue(result!!.getOrNull()?.isEmpty()!!) + } + + @Test + fun retrieveCalendars_noFilter_appliesCorrectSelection() = runTest { + mockPermissionGranted() + + val cursor = mockk(relaxed = true) + every { contentResolver.query(calendarContentUri, any(), any(), any(), any()) } returns cursor + + var result: Result>? = null + val latch = CountDownLatch(1) + calendarImplem.retrieveCalendars(false, null) { + result = it + latch.countDown() + } + + latch.await() + + verify { + contentResolver.query( + calendarContentUri, + any(), + null, + null, + any() + ) + } + + assertTrue(result!!.isSuccess) + assertTrue(result!!.getOrNull()?.isEmpty()!!) + } + + @Test + fun retrieveCalendars_withDeniedPermission_returnsAccessRefusedError() = runTest { + mockPermissionDenied() + + var result: Result>? = null + calendarImplem.retrieveCalendars(false, null) { + result = it + } + + assertTrue(result!!.isFailure) + assertEquals("ACCESS_REFUSED", (result!!.exceptionOrNull() as FlutterError).code) + } + + @Test + fun retrieveCalendars_withEmptyCursor_returnsEmptyList() = runTest { + mockPermissionGranted() + + val cursor = mockk(relaxed = true) + every { contentResolver.query(calendarContentUri, any(), any(), any(), any()) } returns cursor + every { cursor.moveToNext() } returns false + + var result: Result>? = null + val latch = CountDownLatch(1) + calendarImplem.retrieveCalendars(false, null) { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isSuccess) + assertTrue(result!!.getOrNull()?.isEmpty()!!) + } + + @Test + fun retrieveCalendars_withException_returnsGenericError() = runTest { + mockPermissionGranted() + + every { contentResolver.query(calendarContentUri, any(), any(), any(), any()) } throws Exception("Query failed") + + var result: Result>? = null + val latch = CountDownLatch(1) + calendarImplem.retrieveCalendars(false, null) { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isFailure) + assertEquals("GENERIC_ERROR", (result!!.exceptionOrNull() as FlutterError).code) + } + + @Test + fun deleteCalendar_withGrantedPermission_andWritableCalendar_deletesCalendarSuccessfully() = runTest { + mockPermissionGranted() + mockWritableCalendar() + + every { contentResolver.delete(calendarContentUri, any(), any()) } returns 1 + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.deleteCalendar("1") { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isSuccess) + } + + @Test + fun deleteCalendar_withGrantedPermission_andNotWritableCalendar_deletesCalendarSuccessfully() = runTest { + mockPermissionGranted() + mockNotWritableCalendar() + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.deleteCalendar("1") { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isFailure) + assertEquals("NOT_EDITABLE", (result!!.exceptionOrNull() as FlutterError).code) + } + + @Test + fun deleteCalendar_withDeniedPermission_returnsAccessRefusedError() = runTest { + mockPermissionDenied() + + var result: Result? = null + calendarImplem.deleteCalendar("1") { + result = it + } + + assertTrue(result!!.isFailure) + assertEquals("ACCESS_REFUSED", (result!!.exceptionOrNull() as FlutterError).code) + } + + @Test + fun deleteCalendar_withException_returnsGenericError() = runTest { + mockPermissionGranted() + mockWritableCalendar() + + every { contentResolver.delete(calendarContentUri, any(), any()) } throws Exception("Delete failed") + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.deleteCalendar("1") { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isFailure) + assertEquals("GENERIC_ERROR", (result!!.exceptionOrNull() as FlutterError).code) + } + + @Test + fun deleteCalendar_withNoRowsDeleted_returnsGenericError() = runTest { + mockPermissionGranted() + mockWritableCalendar() + + every { contentResolver.delete(calendarContentUri, any(), any()) } returns 0 + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.deleteCalendar("1") { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isFailure) + assertEquals("GENERIC_ERROR", (result!!.exceptionOrNull() as FlutterError).code) + } + + @Test + fun deleteCalendar_withNotWritableCalendar_returnsNotEditableError() = runTest { + mockPermissionGranted() + mockNotWritableCalendar() + + every { contentResolver.delete(calendarContentUri, any(), any()) } returns 0 + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.deleteCalendar("1") { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isFailure) + assertEquals("NOT_EDITABLE", (result!!.exceptionOrNull() as FlutterError).code) + } + + @Test + fun deleteCalendar_withNotFoundCalendar_returnsNotFoundError() = runTest { + mockPermissionGranted() + mockCalendarNotFound() + + every { contentResolver.delete(calendarContentUri, any(), any()) } returns 0 + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.deleteCalendar("1") { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isFailure) + assertEquals("NOT_FOUND", (result!!.exceptionOrNull() as FlutterError).code) + } +} diff --git a/android/src/test/kotlin/sncf/connect/tech/eventide/EventTests.kt b/android/src/test/kotlin/sncf/connect/tech/eventide/EventTests.kt new file mode 100644 index 0000000..d193ecf --- /dev/null +++ b/android/src/test/kotlin/sncf/connect/tech/eventide/EventTests.kt @@ -0,0 +1,407 @@ +package sncf.connect.tech.eventide + +import android.content.ContentResolver +import android.database.Cursor +import android.net.Uri +import android.provider.CalendarContract +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.time.Instant +import java.util.concurrent.CountDownLatch + +class EventTests { + private lateinit var contentResolver: ContentResolver + private lateinit var permissionHandler: PermissionHandler + private lateinit var calendarImplem: CalendarImplem + private lateinit var calendarContentUri: Uri + private lateinit var eventContentUri: Uri + private lateinit var remindersContentUri: Uri + + @BeforeEach + fun setup() { + contentResolver = mockk(relaxed = true) + permissionHandler = mockk(relaxed = true) + calendarContentUri = mockk(relaxed = true) + eventContentUri = mockk(relaxed = true) + remindersContentUri = mockk(relaxed = true) + + calendarImplem = CalendarImplem( + contentResolver = contentResolver, + permissionHandler = permissionHandler, + calendarContentUri = calendarContentUri, + eventContentUri = eventContentUri, + remindersContentUri = remindersContentUri + ) + } + + private fun mockPermissionGranted() { + every { permissionHandler.requestWritePermission(any()) } answers { + firstArg<(Boolean) -> Unit>().invoke(true) + } + + every { permissionHandler.requestReadPermission(any()) } answers { + firstArg<(Boolean) -> Unit>().invoke(true) + } + } + + private fun mockPermissionDenied() { + every { permissionHandler.requestWritePermission(any()) } answers { + firstArg<(Boolean) -> Unit>().invoke(false) + } + every { permissionHandler.requestReadPermission(any()) } answers { + firstArg<(Boolean) -> Unit>().invoke(false) + } + } + + private fun mockWritableCalendar() { + val cursor = mockk(relaxed = true) + every { contentResolver.query(calendarContentUri, any(), any(), any(), any()) } returns cursor + every { cursor.moveToNext() } returns true + every { cursor.getInt(any()) } returns CalendarContract.Calendars.CAL_ACCESS_CONTRIBUTOR + } + + private fun mockNotWritableCalendar() { + val cursor = mockk(relaxed = true) + every { contentResolver.query(calendarContentUri, any(), any(), any(), any()) } returns cursor + every { cursor.moveToNext() } returns true + every { cursor.getInt(any()) } returns CalendarContract.Calendars.CAL_ACCESS_READ + } + + private fun mockCalendarNotFound() { + val cursor = mockk(relaxed = true) + every { contentResolver.query(calendarContentUri, any(), any(), any(), any()) } returns cursor + every { cursor.moveToNext() } returns false + } + + private fun mockCalendarIdFound() { + val cursor = mockk(relaxed = true) + every { contentResolver.query(eventContentUri, any(), any(), any(), any()) } returns cursor + every { cursor.moveToNext() } returns true + every { cursor.getLong(any()) } returns 1L + } + + private fun mockCalendarIdNotFound() { + val cursor = mockk(relaxed = true) + every { contentResolver.query(eventContentUri, any(), any(), any(), any()) } returns cursor + every { cursor.moveToNext() } returns false + } + + @Test + fun createEvent_withGrantedPermission_andWritableCalendar_createsEventSuccessfully() = runTest { + mockPermissionGranted() + mockWritableCalendar() + + val uri = mockk(relaxed = true) + every { contentResolver.insert(any(), any()) } returns uri + every { uri.lastPathSegment } returns "1" + + val startMilli = Instant.now().toEpochMilli() + val endMilli = Instant.now().toEpochMilli() + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.createEvent( + title = "Test Event", + startDate = startMilli, + endDate = endMilli, + calendarId = "1", + description = "Description", + isAllDay = false, + url = null + ) { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isSuccess) + assertEquals(Event( + id = "1", + title = "Test Event", + startDate = startMilli, + endDate = endMilli, + calendarId = "1", + description = "Description", + isAllDay = false, + ), result!!.getOrNull()!!) + } + + @Test + fun createEvent_withGrantedPermission_andNotWritableCalendar_returnsNotEditableError() = runTest { + mockPermissionGranted() + mockNotWritableCalendar() + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.createEvent( + title = "Test Event", + startDate = Instant.now().toEpochMilli(), + endDate = Instant.now().toEpochMilli(), + calendarId = "1", + description = "Description", + isAllDay = false, + url = null + ) { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isFailure) + assertEquals("NOT_EDITABLE", (result!!.exceptionOrNull() as FlutterError).code) + } + + @Test + fun createEvent_withGrantedPermission_andNotFoundCalendar_returnsNotFoundError() = runTest { + mockPermissionGranted() + mockCalendarNotFound() + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.createEvent( + title = "Test Event", + startDate = Instant.now().toEpochMilli(), + endDate = Instant.now().toEpochMilli(), + calendarId = "1", + description = "Description", + isAllDay = false, + url = null + ) { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isFailure) + assertEquals("NOT_FOUND", (result!!.exceptionOrNull() as FlutterError).code) + } + + @Test + fun createEvent_withDeniedPermission_returnsAccessRefusedError() = runTest { + mockPermissionDenied() + + var result: Result? = null + calendarImplem.createEvent( + title = "Test Event", + startDate = Instant.now().toEpochMilli(), + endDate = Instant.now().toEpochMilli(), + calendarId = "1", + description = "Description", + isAllDay = false, + url = null + ) { + result = it + } + + assertTrue(result!!.isFailure) + assertEquals("ACCESS_REFUSED", (result!!.exceptionOrNull() as FlutterError).code) + } + + @Test + fun createEvent_withInvalidUri_returnsNotFoundError() = runTest { + mockPermissionGranted() + + every { contentResolver.insert(any(), any()) } returns null + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.createEvent( + title = "Test Event", + startDate = Instant.now().toEpochMilli(), + endDate = Instant.now().toEpochMilli(), + calendarId = "1", + description = "Description", + isAllDay = false, + url = null + ) { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isFailure) + assertEquals("NOT_FOUND", (result!!.exceptionOrNull() as FlutterError).code) + } + + @Test + fun retrieveEvents_withGrantedPermission_returnsEvents() = runTest { + mockPermissionGranted() + + val cursor = mockk(relaxed = true) + + CalendarContract.Events.ALL_DAY + every { contentResolver.query(eventContentUri, any(), any(), any(), any()) } returns cursor + every { cursor.moveToNext() } returnsMany listOf(true, false) + every { cursor.getLong(cursor.getColumnIndexOrThrow(CalendarContract.Events._ID)) } returns 1L + every { cursor.getString(any()) } returnsMany listOf("Test Event", "Description") + every { cursor.getLong(any()) } returns 0L + every { cursor.getInt(cursor.getColumnIndexOrThrow(CalendarContract.Events.ALL_DAY)) } returns 0 + + var result: Result>? = null + val latch = CountDownLatch(1) + calendarImplem.retrieveEvents("1", 0L, 0L) { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isSuccess) + assertEquals(1, result!!.getOrNull()?.size) + assertEquals("Test Event", result!!.getOrNull()?.get(0)?.title) + } + + @Test + fun retrieveEvents_withDeniedPermission_returnsAccessRefusedError() = runTest { + mockPermissionDenied() + + var result: Result>? = null + calendarImplem.retrieveEvents("1", 0L, 0L) { + result = it + } + + assertTrue(result!!.isFailure) + assertEquals("ACCESS_REFUSED", (result!!.exceptionOrNull() as FlutterError).code) + } + + @Test + fun retrieveEvents_withEmptyCursor_returnsEmptyList() = runTest { + mockPermissionGranted() + + val cursor = mockk(relaxed = true) + every { contentResolver.query(eventContentUri, any(), any(), any(), any()) } returns cursor + every { cursor.moveToNext() } returns false + + var result: Result>? = null + val latch = CountDownLatch(1) + calendarImplem.retrieveEvents("1", 0L, 0L) { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isSuccess) + assertTrue(result!!.getOrNull()?.isEmpty()!!) + } + + @Test + fun retrieveEvents_withException_returnsGenericError() = runTest { + mockPermissionGranted() + + every { contentResolver.query(eventContentUri, any(), any(), any(), any()) } throws Exception("Query failed") + + var result: Result>? = null + val latch = CountDownLatch(1) + calendarImplem.retrieveEvents("1", 0L, 0L) { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isFailure) + assertEquals("GENERIC_ERROR", (result!!.exceptionOrNull() as FlutterError).code) + } + + @Test + fun deleteEvent_withGrantedPermission_deletesEventSuccessfully() = runTest { + mockPermissionGranted() + mockCalendarIdFound() + mockWritableCalendar() + + every { contentResolver.delete(eventContentUri, any(), any()) } returns 1 + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.deleteEvent("1") { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isSuccess) + } + + @Test + fun deleteEvent_withDeniedPermission_returnsAccessRefusedError() = runTest { + mockPermissionDenied() + + var result: Result? = null + calendarImplem.deleteEvent("1") { + result = it + } + + assertTrue(result!!.isFailure) + } + + @Test + fun deleteEvent_withException_returnsGenericError() = runTest { + mockPermissionGranted() + mockCalendarIdFound() + mockWritableCalendar() + + every { contentResolver.delete(eventContentUri, any(), any()) } throws Exception("Delete failed") + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.deleteEvent("1") { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isFailure) + assertEquals("GENERIC_ERROR", (result!!.exceptionOrNull() as FlutterError).code) + } + + @Test + fun deleteEvent_withNoRowsDeleted_returnsNotFoundError() = runTest { + mockPermissionGranted() + mockCalendarIdFound() + mockWritableCalendar() + + every { contentResolver.delete(eventContentUri, any(), any()) } returns 0 + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.deleteEvent("1") { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isFailure) + assertEquals("NOT_FOUND", (result!!.exceptionOrNull() as FlutterError).code) + } + + @Test + fun deleteEvent_withCalendarIdNotFound_returnsNotFoundError() = runTest { + mockPermissionGranted() + mockCalendarIdNotFound() + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.deleteEvent("1") { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isFailure) + assertEquals("NOT_FOUND", (result!!.exceptionOrNull() as FlutterError).code) + } +} diff --git a/android/src/test/kotlin/sncf/connect/tech/eventide/PermissionRequestTests.kt b/android/src/test/kotlin/sncf/connect/tech/eventide/PermissionRequestTests.kt new file mode 100644 index 0000000..b3eed3a --- /dev/null +++ b/android/src/test/kotlin/sncf/connect/tech/eventide/PermissionRequestTests.kt @@ -0,0 +1,65 @@ +package sncf.connect.tech.eventide + +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.util.concurrent.CountDownLatch + +class PermissionRequestTests { + private lateinit var permissionHandler: PermissionHandler + private lateinit var calendarImplem: CalendarImplem + + @BeforeEach + fun setup() { + permissionHandler = mockk(relaxed = true) + calendarImplem = CalendarImplem( + contentResolver = mockk(relaxed = true), + permissionHandler = permissionHandler, + calendarContentUri = mockk(relaxed = true), + eventContentUri = mockk(relaxed = true), + remindersContentUri = mockk(relaxed = true), + ) + } + + @Test + fun requestCalendarPermission_withGrantedPermission_returnsTrue() = runTest { + every { permissionHandler.requestWritePermission(any()) } answers { + firstArg<(Boolean) -> Unit>().invoke(true) + } + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.requestCalendarPermission { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isSuccess) + assertTrue(result!!.getOrNull()!!) + } + + @Test + fun requestCalendarPermission_withDeniedPermission_returnsFalse() = runTest { + every { permissionHandler.requestWritePermission(any()) } answers { + firstArg<(Boolean) -> Unit>().invoke(false) + } + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.requestCalendarPermission { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isSuccess) + assertFalse(result!!.getOrNull()!!) + } +} diff --git a/android/src/test/kotlin/sncf/connect/tech/eventide/ReminderTests.kt b/android/src/test/kotlin/sncf/connect/tech/eventide/ReminderTests.kt new file mode 100644 index 0000000..d3f0aaa --- /dev/null +++ b/android/src/test/kotlin/sncf/connect/tech/eventide/ReminderTests.kt @@ -0,0 +1,203 @@ +package sncf.connect.tech.eventide + +import android.content.ContentResolver +import android.database.Cursor +import android.net.Uri +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.util.concurrent.CountDownLatch + +class ReminderTests { + private lateinit var contentResolver: ContentResolver + private lateinit var permissionHandler: PermissionHandler + private lateinit var calendarImplem: CalendarImplem + private lateinit var calendarContentUri: Uri + private lateinit var eventContentUri: Uri + private lateinit var remindersContentUri: Uri + + @BeforeEach + fun setup() { + contentResolver = mockk(relaxed = true) + permissionHandler = mockk(relaxed = true) + calendarContentUri = mockk(relaxed = true) + eventContentUri = mockk(relaxed = true) + remindersContentUri = mockk(relaxed = true) + + calendarImplem = CalendarImplem( + contentResolver = contentResolver, + permissionHandler = permissionHandler, + calendarContentUri = calendarContentUri, + eventContentUri = eventContentUri, + remindersContentUri = remindersContentUri, + ) + } + + private fun mockPermissionGranted() { + every { permissionHandler.requestWritePermission(any()) } answers { + firstArg<(Boolean) -> Unit>().invoke(true) + } + } + + private fun mockPermissionDenied() { + every { permissionHandler.requestWritePermission(any()) } answers { + firstArg<(Boolean) -> Unit>().invoke(false) + } + } + + @Test + fun createReminder_withGrantedPermission_createsReminderSuccessfully() = runTest { + mockPermissionGranted() + + val eventId = "1" + val minutes = 10L + every { contentResolver.insert(remindersContentUri, any()) } returns mockk(relaxed = true) + + val eventCursor = mockk(relaxed = true) + every { contentResolver.query(eventContentUri, any(), any(), any(), any()) } returns eventCursor + every { eventCursor.moveToNext() } returnsMany listOf(true, false) + every { eventCursor.getLong(any()) } returns 1L + every { eventCursor.getString(any()) } returns "Test Event" + every { eventCursor.getLong(any()) } returns 0L + + val remindersCursor = mockk(relaxed = true) + every { contentResolver.query(remindersContentUri, any(), any(), any(), any()) } returns remindersCursor + every { remindersCursor.moveToNext() } returnsMany listOf(true, false) + every { remindersCursor.getLong(any()) } returns 10L + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.createReminder(minutes, eventId) { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isSuccess) + } + + @Test + fun createReminder_withDeniedPermission_failsToCreateReminder() = runTest { + mockPermissionDenied() + + val eventId = "1" + val minutes = 10L + + var result: Result? = null + calendarImplem.createReminder(minutes, eventId) { + result = it + } + + assertTrue(result!!.isFailure) + } + + @Test + fun createReminder_withException_failsToCreateReminder() = runTest { + mockPermissionGranted() + + val eventId = "1" + val minutes = 10L + every { contentResolver.insert(remindersContentUri, any()) } throws Exception("Insert failed") + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.createReminder(minutes, eventId) { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isFailure) + } + + @Test + fun deleteReminder_withGrantedPermission_deletesReminderSuccessfully() = runTest { + mockPermissionGranted() + + val eventId = "1" + val minutes = 10L + every { contentResolver.delete(remindersContentUri, any(), any()) } returns 1 + + val eventCursor = mockk(relaxed = true) + every { contentResolver.query(eventContentUri, any(), any(), any(), any()) } returns eventCursor + every { eventCursor.moveToNext() } returnsMany listOf(true, false) + every { eventCursor.getLong(any()) } returns 1L + every { eventCursor.getString(any()) } returns "Test Event" + every { eventCursor.getLong(any()) } returns 0L + + + val remindersCursor = mockk(relaxed = true) + every { contentResolver.query(remindersContentUri, any(), any(), any(), any()) } returns remindersCursor + every { remindersCursor.moveToNext() } returnsMany listOf(false, false) + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.deleteReminder(minutes, eventId) { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isSuccess) + } + + @Test + fun deleteReminder_withDeniedPermission_failsToDeleteReminder() = runTest { + mockPermissionDenied() + + val eventId = "1" + val minutes = 10L + + var result: Result? = null + calendarImplem.deleteReminder(minutes, eventId) { + result = it + } + + assertTrue(result!!.isFailure) + } + + @Test + fun deleteReminder_withException_failsToDeleteReminder() = runTest { + mockPermissionGranted() + + val eventId = "1" + val minutes = 10L + every { contentResolver.delete(eventContentUri, any(), any()) } throws Exception("Delete failed") + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.deleteReminder(minutes, eventId) { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isFailure) + } + + @Test + fun deleteReminder_withNoRowsDeleted_failsToDeleteReminder() = runTest { + mockPermissionGranted() + + val eventId = "1" + val minutes = 10L + every { contentResolver.delete(remindersContentUri, any(), any()) } returns 0 + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.deleteReminder(minutes, eventId) { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isFailure) + } +}