From 048d5e4a96e8752060aa2e4cfe33c8244116eacb Mon Sep 17 00:00:00 2001
From: Lukas Lechner <lukas.lechner@sap.com>
Date: Mon, 12 Apr 2021 10:34:37 +0200
Subject: [PATCH] Adapt auto-checkout default values (EXPOSUREAPP-6243) (#2791)

* Prepare calculation of defaultAutoCheckoutLength

* Not allow defaultCheckInLength < 15min and > 23h45min

* Add more test cases

* Make some tests green

* Make ALL tests green

* Disable Confirm-CheckIn Button when automatic checkout is set to 00:00

* Suppress detekt issue

* Add test for enabling and disabling the confirm button in ConfirmCheckInViewModel.kt

* Add and adapt some comments

* Extract default auto checkout duration logic to extension function

* Add test and comments for roundToNearest15Minutes

* Rename AutoCheckoutHelper.kt -> DefaultAutoCheckoutLength.kt

Co-authored-by: harambasicluka <64483219+harambasicluka@users.noreply.github.com>
---
 .../qrcode/DefaultAutoCheckoutLength.kt       |  74 +++++++
 .../confirm/ConfirmCheckInFragment.kt         |   1 +
 .../confirm/ConfirmCheckInViewModel.kt        |  32 ++-
 .../confirm/ConfirmCheckInViewModelTest.kt    |  37 ++--
 .../checkins/qrcode/AutoCheckoutHelperTest.kt |  46 +++++
 .../events/DefaultAutoCheckoutLengthTest.kt   | 188 ++++++++++++++++++
 6 files changed, 348 insertions(+), 30 deletions(-)
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultAutoCheckoutLength.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/AutoCheckoutHelperTest.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/DefaultAutoCheckoutLengthTest.kt

diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultAutoCheckoutLength.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultAutoCheckoutLength.kt
new file mode 100644
index 000000000..0f5c0d6df
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultAutoCheckoutLength.kt
@@ -0,0 +1,74 @@
+package de.rki.coronawarnapp.eventregistration.checkins.qrcode
+
+import androidx.annotation.VisibleForTesting
+import org.joda.time.Duration
+import org.joda.time.Instant
+import java.util.concurrent.TimeUnit
+import kotlin.math.roundToLong
+
+/**
+ * Evaluates the default auto-checkout length depending on the current time
+ */
+@Suppress("ReturnCount")
+fun TraceLocation.getDefaultAutoCheckoutLengthInMinutes(now: Instant): Int {
+
+    // min valid value is 00:15h
+    val minDefaultAutoCheckOutLengthInMinutes = 15
+
+    // max valid value is 23:45h
+    val maxDefaultAutoCheckOutLengthInMinutes = (TimeUnit.HOURS.toMinutes(23) + 45).toInt()
+
+    // for permanent traceLocations, a defaultCheckInLength is always available
+    if (defaultCheckInLengthInMinutes != null) {
+
+        if (defaultCheckInLengthInMinutes < 15) {
+            return minDefaultAutoCheckOutLengthInMinutes
+        }
+
+        if (defaultCheckInLengthInMinutes > maxDefaultAutoCheckOutLengthInMinutes) {
+            return maxDefaultAutoCheckOutLengthInMinutes
+        }
+
+        return roundToNearest15Minutes(defaultCheckInLengthInMinutes)
+    }
+    // for temporary traceLocations, the defaultCheckInLength could be empty
+    else {
+
+        // For QR-codes generated by CWA, endDate can never be null here. However, a QR code that was created or
+        // modified by a third party could have an endDate that is null.
+        if (endDate == null) {
+            return minDefaultAutoCheckOutLengthInMinutes
+        }
+
+        if (now.isAfter(endDate)) {
+            return minDefaultAutoCheckOutLengthInMinutes
+        }
+
+        val minutesUntilEndDate = Duration(now, endDate).standardMinutes.toInt()
+
+        if (minutesUntilEndDate < minDefaultAutoCheckOutLengthInMinutes) {
+            return minDefaultAutoCheckOutLengthInMinutes
+        }
+
+        if (minutesUntilEndDate > maxDefaultAutoCheckOutLengthInMinutes) {
+            return maxDefaultAutoCheckOutLengthInMinutes
+        }
+
+        return roundToNearest15Minutes(minutesUntilEndDate)
+    }
+}
+
+/**
+ * Rounds to the nearest 15 minute interval.
+ * for more details see AutoCheckoutHelperTest
+ */
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+fun roundToNearest15Minutes(minutes: Int): Int {
+    val roundingStepInMinutes = 15
+    return Duration
+        .standardMinutes(
+            (minutes.toFloat() / roundingStepInMinutes)
+                .roundToLong() * roundingStepInMinutes
+        )
+        .standardMinutes.toInt()
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInFragment.kt
index c2c41c171..5b9767a72 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInFragment.kt
@@ -91,6 +91,7 @@ class ConfirmCheckInFragment : Fragment(R.layout.fragment_confirm_check_in), Aut
                     uiState.eventInFutureDateText,
                     uiState.eventInFutureTimeText
                 )
+                confirmCheckinConfirmButton.isEnabled = uiState.confirmButtonEnabled
             }
         }
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInViewModel.kt
index 4fa1bc62e..a8654db5b 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInViewModel.kt
@@ -8,6 +8,7 @@ import de.rki.coronawarnapp.eventregistration.checkins.CheckIn
 import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository
 import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation
 import de.rki.coronawarnapp.eventregistration.checkins.qrcode.VerifiedTraceLocation
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.getDefaultAutoCheckoutLengthInMinutes
 import de.rki.coronawarnapp.ui.durationpicker.toContactDiaryFormat
 import de.rki.coronawarnapp.ui.durationpicker.toReadableDuration
 import de.rki.coronawarnapp.ui.eventregistration.organizer.category.adapter.category.mapTraceLocationToTitleRes
@@ -20,7 +21,6 @@ import kotlinx.coroutines.flow.combine
 import org.joda.time.Duration
 import org.joda.time.Instant
 import org.joda.time.format.DateTimeFormat
-import kotlin.math.roundToLong
 
 class ConfirmCheckInViewModel @AssistedInject constructor(
     @Assisted private val verifiedTraceLocation: VerifiedTraceLocation,
@@ -30,13 +30,11 @@ class ConfirmCheckInViewModel @AssistedInject constructor(
     private val traceLocation = MutableStateFlow(verifiedTraceLocation.traceLocation)
     private val createJournalEntry = MutableStateFlow(true)
 
-    private val truncatedDefaultCheckInLength = roundToNearestValidDuration(
-        verifiedTraceLocation.traceLocation.defaultCheckInLengthInMinutes ?: 0
+    private val autoCheckOutLength = MutableStateFlow(
+        Duration.standardMinutes(
+            verifiedTraceLocation.traceLocation.getDefaultAutoCheckoutLengthInMinutes(timeStamper.nowUTC).toLong()
+        )
     )
-    private val checkInLength = MutableStateFlow(truncatedDefaultCheckInLength)
-
-    private fun roundToNearestValidDuration(minutes: Int): Duration =
-        Duration.standardMinutes((minutes.toFloat() / 15).roundToLong() * 15)
 
     val openDatePickerEvent = SingleLiveEvent<String>()
     val events = SingleLiveEvent<ConfirmCheckInNavigation>()
@@ -44,14 +42,15 @@ class ConfirmCheckInViewModel @AssistedInject constructor(
     val uiState = combine(
         traceLocation,
         createJournalEntry,
-        checkInLength
+        autoCheckOutLength
     ) { traceLocation, createEntry, checkInLength ->
         UiState(
             traceLocation = traceLocation,
             createJournalEntry = createEntry,
             checkInEndOffset = checkInLength,
             eventInPastVisible = traceLocation.isAfterEndTime(timeStamper.nowUTC),
-            eventInFutureVisible = traceLocation.isBeforeStartTime(timeStamper.nowUTC)
+            eventInFutureVisible = traceLocation.isBeforeStartTime(timeStamper.nowUTC),
+            confirmButtonEnabled = checkInLength.standardMinutes > 0
         )
     }.asLiveData()
 
@@ -66,7 +65,7 @@ class ConfirmCheckInViewModel @AssistedInject constructor(
                 verifiedTraceLocation.toCheckIn(
                     checkInStart = now,
                     createJournalEntry = createJournalEntry.value,
-                    checkInEnd = now + checkInLength.value
+                    checkInEnd = now + autoCheckOutLength.value
                 )
             )
             events.postValue(ConfirmCheckInNavigation.ConfirmNavigation)
@@ -78,18 +77,16 @@ class ConfirmCheckInViewModel @AssistedInject constructor(
     }
 
     fun dateSelectorClicked() {
-        openDatePickerEvent.value = checkInLength.value.toContactDiaryFormat()
+        openDatePickerEvent.value = autoCheckOutLength.value.toContactDiaryFormat()
     }
 
     fun durationUpdated(duration: Duration) {
-        checkInLength.value = duration
+        autoCheckOutLength.value = duration
     }
 
     private fun VerifiedTraceLocation.toCheckIn(
         checkInStart: Instant,
-        checkInEnd: Instant = checkInStart.plus(
-            Duration.standardMinutes(traceLocation.defaultCheckInLengthInMinutes?.toLong() ?: 3L)
-        ),
+        checkInEnd: Instant,
         completed: Boolean = false,
         createJournalEntry: Boolean = true
     ): CheckIn {
@@ -102,7 +99,7 @@ class ConfirmCheckInViewModel @AssistedInject constructor(
             address = traceLocation.address,
             traceLocationStart = traceLocation.startDate,
             traceLocationEnd = traceLocation.endDate,
-            defaultCheckInLengthInMinutes = truncatedDefaultCheckInLength.standardMinutes.toInt(),
+            defaultCheckInLengthInMinutes = traceLocation.defaultCheckInLengthInMinutes,
             cryptographicSeed = traceLocation.cryptographicSeed,
             cnPublicKey = traceLocation.cnPublicKey,
             checkInStart = checkInStart,
@@ -124,7 +121,8 @@ class ConfirmCheckInViewModel @AssistedInject constructor(
         private val checkInEndOffset: Duration,
         val createJournalEntry: Boolean,
         val eventInPastVisible: Boolean,
-        val eventInFutureVisible: Boolean
+        val eventInFutureVisible: Boolean,
+        val confirmButtonEnabled: Boolean
     ) {
         val description get() = traceLocation.description
         val typeRes get() = mapTraceLocationToTitleRes(traceLocation.type)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/attendee/confirm/ConfirmCheckInViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/attendee/confirm/ConfirmCheckInViewModelTest.kt
index aee24dd76..f5b58f654 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/attendee/confirm/ConfirmCheckInViewModelTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/attendee/confirm/ConfirmCheckInViewModelTest.kt
@@ -14,6 +14,7 @@ import io.mockk.coEvery
 import io.mockk.every
 import io.mockk.impl.annotations.MockK
 import okio.ByteString.Companion.decodeBase64
+import org.joda.time.Duration
 import org.joda.time.Instant
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
@@ -29,8 +30,6 @@ class ConfirmCheckInViewModelTest : BaseTest() {
     @MockK lateinit var checkInRepository: CheckInRepository
     @MockK lateinit var timeStamper: TimeStamper
 
-    private lateinit var viewModel: ConfirmCheckInViewModel
-
     private val traceLocation = TraceLocation(
         id = 1,
         type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER,
@@ -51,23 +50,35 @@ class ConfirmCheckInViewModelTest : BaseTest() {
         coEvery { checkInRepository.addCheckIn(any()) } returns 1L
         every { verifiedTraceLocation.traceLocation } returns traceLocation
         every { timeStamper.nowUTC } returns Instant.parse("2021-03-04T10:30:00Z")
+    }
+
+    private fun createInstance() = ConfirmCheckInViewModel(
+        verifiedTraceLocation = verifiedTraceLocation,
+        checkInRepository = checkInRepository,
+        timeStamper = timeStamper
+    )
 
-        viewModel = ConfirmCheckInViewModel(
-            verifiedTraceLocation = verifiedTraceLocation,
-            checkInRepository = checkInRepository,
-            timeStamper = timeStamper
-        )
+    @Test
+    fun onClose() = with(createInstance()) {
+        onClose()
+        events.getOrAwaitValue() shouldBe ConfirmCheckInNavigation.BackNavigation
+    }
+
+    @Test
+    fun onConfirmEvent() = with(createInstance()) {
+        onConfirmTraceLocation()
+        events.getOrAwaitValue() shouldBe ConfirmCheckInNavigation.ConfirmNavigation
     }
 
     @Test
-    fun onClose() {
-        viewModel.onClose()
-        viewModel.events.getOrAwaitValue() shouldBe ConfirmCheckInNavigation.BackNavigation
+    fun `confirm button should be disabled when autoCheckOutLength is 0`() = with(createInstance()) {
+        durationUpdated(Duration.standardMinutes(0))
+        uiState.getOrAwaitValue().confirmButtonEnabled shouldBe false
     }
 
     @Test
-    fun onConfirmEvent() {
-        viewModel.onConfirmTraceLocation()
-        viewModel.events.getOrAwaitValue() shouldBe ConfirmCheckInNavigation.ConfirmNavigation
+    fun `confirm button should be enabled when autoCheckOutLength is greater than 0`() = with(createInstance()) {
+        durationUpdated(Duration.standardMinutes(15))
+        uiState.getOrAwaitValue().confirmButtonEnabled shouldBe true
     }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/AutoCheckoutHelperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/AutoCheckoutHelperTest.kt
new file mode 100644
index 000000000..c0690025c
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/AutoCheckoutHelperTest.kt
@@ -0,0 +1,46 @@
+package de.rki.coronawarnapp.eventregistration.checkins.qrcode
+
+import io.kotest.matchers.shouldBe
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.MethodSource
+
+internal class AutoCheckoutHelperTest {
+
+    @ParameterizedTest
+    @MethodSource("provideArguments")
+    fun `roundToNearest15Minutes() should round correctly`(testCase: TestCase) = with(testCase) {
+        roundToNearest15Minutes(minutesToRound) shouldBe expectedRoundingResult
+    }
+
+    companion object {
+        @Suppress("unused")
+        @JvmStatic
+        fun provideArguments() = listOf(
+            TestCase(
+                minutesToRound = 0,
+                expectedRoundingResult = 0
+            ),
+            TestCase(
+                minutesToRound = 7,
+                expectedRoundingResult = 0
+            ),
+            TestCase(
+                minutesToRound = 8,
+                expectedRoundingResult = 15
+            ),
+            TestCase(
+                minutesToRound = 22,
+                expectedRoundingResult = 15
+            ),
+            TestCase(
+                minutesToRound = 23,
+                expectedRoundingResult = 30
+            )
+        )
+    }
+
+    data class TestCase(
+        val minutesToRound: Int,
+        val expectedRoundingResult: Int
+    )
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/DefaultAutoCheckoutLengthTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/DefaultAutoCheckoutLengthTest.kt
new file mode 100644
index 000000000..f5d72d1ae
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/DefaultAutoCheckoutLengthTest.kt
@@ -0,0 +1,188 @@
+package de.rki.coronawarnapp.eventregistration.events
+
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.getDefaultAutoCheckoutLengthInMinutes
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
+import io.kotest.matchers.shouldBe
+import okio.ByteString.Companion.encode
+import org.joda.time.Instant
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.MethodSource
+import testhelpers.BaseTest
+import java.util.concurrent.TimeUnit
+
+class DefaultAutoCheckoutLengthTest : BaseTest() {
+
+    @ParameterizedTest
+    @MethodSource("provideArguments")
+    fun `getDefaultAuthCheckoutLengthInMinutes(now) should return correct value`(
+        testCase: DefaultAutoCheckoutLengthTestCase
+    ) = with(testCase) {
+
+        createTraceLocation(this)
+            .getDefaultAutoCheckoutLengthInMinutes(now) shouldBe expectedDefaultAutoCheckoutLength
+    }
+
+    private fun createTraceLocation(testCase: DefaultAutoCheckoutLengthTestCase) = TraceLocation(
+        id = 1,
+        type = TraceLocationOuterClass.TraceLocationType.UNRECOGNIZED,
+        description = "",
+        address = "",
+        startDate = testCase.startDate,
+        endDate = testCase.endDate,
+        defaultCheckInLengthInMinutes = testCase.defaultCheckInLengthInMinutes,
+        cryptographicSeed = "seed byte array".encode(),
+        cnPublicKey = "cnPublicKey"
+    )
+
+    companion object {
+
+        // min valid length = 00:15h
+        private const val MIN_VALID_LENGTH = 15
+
+        // max valid length = 23:45h
+        private val MAX_VALID_LENGTH = (TimeUnit.HOURS.toMinutes(23) + 45).toInt()
+
+        @Suppress("unused")
+        @JvmStatic
+        fun provideArguments() = listOf(
+            DefaultAutoCheckoutLengthTestCase(
+                // now doesn't matter, as defaultCheckInLengthInMinutes is not null
+                now = Instant.parse("1970-01-01T00:00:00.000Z"),
+                defaultCheckInLengthInMinutes = 30,
+                startDate = null,
+                endDate = null,
+                expectedDefaultAutoCheckoutLength = 30,
+            ),
+            DefaultAutoCheckoutLengthTestCase(
+                // now doesn't matter here, as defaultCheckInLengthInMinutes is not null
+                now = Instant.parse("1970-01-01T00:00:00.000Z"),
+                // min valid length = 00:15h
+                defaultCheckInLengthInMinutes = 0,
+                startDate = null,
+                endDate = null,
+                expectedDefaultAutoCheckoutLength = MIN_VALID_LENGTH
+            ),
+            DefaultAutoCheckoutLengthTestCase(
+                // now doesn't matter here, as defaultCheckInLengthInMinutes is not null
+                now = Instant.parse("1970-01-01T00:00:00.000Z"),
+                // TraceLocations with CWA can actually only have 15 minute interval lengths. However, a trace location
+                // created by a third party could create arbitrary lengths.
+                // 22 min should be rounded to 15
+                defaultCheckInLengthInMinutes = 22,
+                startDate = null,
+                endDate = null,
+                expectedDefaultAutoCheckoutLength = 15
+            ),
+            DefaultAutoCheckoutLengthTestCase(
+                // now doesn't matter here, as defaultCheckInLengthInMinutes is not null
+                now = Instant.parse("1970-01-01T00:00:00.000Z"),
+                // TraceLocations with CWA can actually only have 15 minute interval lengths. However, a trace location
+                // created by a third party could create arbitrary lengths.
+                // 23 min should be rounded to 30
+                defaultCheckInLengthInMinutes = 23,
+                startDate = null,
+                endDate = null,
+                expectedDefaultAutoCheckoutLength = 30
+            ),
+            DefaultAutoCheckoutLengthTestCase(
+                // now doesn't matter here, as defaultCheckInLengthInMinutes is not null
+                now = Instant.parse("1970-01-01T00:00:00.000Z"),
+                // max valid length = 23:45h
+                defaultCheckInLengthInMinutes = MAX_VALID_LENGTH,
+                startDate = null,
+                endDate = null,
+                expectedDefaultAutoCheckoutLength = MAX_VALID_LENGTH
+            ),
+            DefaultAutoCheckoutLengthTestCase(
+                // now doesn't matter here, as defaultCheckInLengthInMinutes is not null
+                now = Instant.parse("1970-01-01T00:00:00.000Z"),
+                // max valid length = 23:45h
+                // TraceLocations with CWA can actually only have a max length of 23:45h. However, a trace location
+                // created by a third party could have a bigger length.
+                defaultCheckInLengthInMinutes = MAX_VALID_LENGTH + 1,
+                startDate = null,
+                endDate = null,
+                expectedDefaultAutoCheckoutLength = MAX_VALID_LENGTH
+            ),
+            DefaultAutoCheckoutLengthTestCase(
+                now = Instant.parse("2021-12-23T17:00:00.000Z"),
+                defaultCheckInLengthInMinutes = null,
+                startDate = Instant.parse("2021-12-24T15:00:00.000Z"),
+                endDate = Instant.parse("2021-12-24T17:00:00.000Z"),
+                // use 23:45h if user checks in earlier than 23:45 before the event ends
+                expectedDefaultAutoCheckoutLength = MAX_VALID_LENGTH
+            ),
+            DefaultAutoCheckoutLengthTestCase(
+                now = Instant.parse("2021-12-23T17:30:00.000Z"),
+                defaultCheckInLengthInMinutes = null,
+                startDate = Instant.parse("2021-12-24T15:00:00.000Z"),
+                endDate = Instant.parse("2021-12-24T17:00:00.000Z"),
+                // 23:30h in minutes
+                expectedDefaultAutoCheckoutLength = (TimeUnit.HOURS.toMinutes(23) + 30).toInt()
+            ),
+            DefaultAutoCheckoutLengthTestCase(
+                now = Instant.parse("2021-12-24T16:00:00.000Z"),
+                defaultCheckInLengthInMinutes = null,
+                startDate = Instant.parse("2021-12-24T15:00:00.000Z"),
+                endDate = Instant.parse("2021-12-24T17:00:00.000Z"),
+                expectedDefaultAutoCheckoutLength = 60
+            ),
+            DefaultAutoCheckoutLengthTestCase(
+                now = Instant.parse("2021-12-24T17:01:00.000Z"),
+                defaultCheckInLengthInMinutes = null,
+                startDate = Instant.parse("2021-12-24T15:00:00.000Z"),
+                endDate = Instant.parse("2021-12-24T17:00:00.000Z"),
+                expectedDefaultAutoCheckoutLength = 15
+            ),
+            DefaultAutoCheckoutLengthTestCase(
+                now = Instant.parse("2021-12-24T17:30:00.000Z"),
+                defaultCheckInLengthInMinutes = null,
+                startDate = Instant.parse("2021-12-24T15:00:00.000Z"),
+                endDate = Instant.parse("2021-12-24T17:00:00.000Z"),
+                // We take the min value when the user checks in after the event has already ended
+                expectedDefaultAutoCheckoutLength = 15
+            ),
+            DefaultAutoCheckoutLengthTestCase(
+                now = Instant.parse("2021-12-24T16:31:00.000Z"),
+                defaultCheckInLengthInMinutes = null,
+                startDate = Instant.parse("2021-12-24T15:00:00.000Z"),
+                endDate = Instant.parse("2021-12-24T17:00:00.000Z"),
+                // Event ends in 29min ->  we round to the nearest 15 minutes
+                expectedDefaultAutoCheckoutLength = 30
+            ),
+            DefaultAutoCheckoutLengthTestCase(
+                now = Instant.parse("2021-12-24T16:38:00.000Z"),
+                defaultCheckInLengthInMinutes = null,
+                startDate = Instant.parse("2021-12-24T15:00:00.000Z"),
+                endDate = Instant.parse("2021-12-24T17:00:00.000Z"),
+                // Event ends in 22min ->  we round to the nearest 15 minutes
+                expectedDefaultAutoCheckoutLength = 15
+            ),
+            DefaultAutoCheckoutLengthTestCase(
+                now = Instant.parse("2021-12-24T16:59:00.000Z"),
+                defaultCheckInLengthInMinutes = null,
+                startDate = Instant.parse("2021-12-24T15:00:00.000Z"),
+                endDate = Instant.parse("2021-12-24T17:00:00.000Z"),
+                // Event ends in 1min ->  we are not rounding to 0 but to the min value (15min)
+                expectedDefaultAutoCheckoutLength = 15
+            ),
+            DefaultAutoCheckoutLengthTestCase(
+                now = Instant.parse("2021-12-24T17:00:00.000Z"),
+                defaultCheckInLengthInMinutes = null,
+                startDate = Instant.parse("2021-12-24T15:00:00.000Z"),
+                endDate = Instant.parse("2021-12-24T17:00:00.000Z"),
+                // We check in at event end, return min value (15min)
+                expectedDefaultAutoCheckoutLength = 15
+            )
+        )
+    }
+}
+
+data class DefaultAutoCheckoutLengthTestCase(
+    val now: Instant,
+    val defaultCheckInLengthInMinutes: Int?,
+    val startDate: Instant?,
+    val endDate: Instant?,
+    val expectedDefaultAutoCheckoutLength: Int,
+)
-- 
GitLab