Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright (c) 2014-2026 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getstream.chat.android.client.internal.offline.repository.database.converter

import androidx.test.ext.junit.runners.AndroidJUnit4
import io.getstream.chat.android.client.internal.offline.repository.database.converter.internal.ReminderInfoConverter
import io.getstream.chat.android.client.internal.offline.repository.domain.message.internal.ReminderInfoEntity
import io.getstream.chat.android.randomDate
import io.getstream.chat.android.randomDateOrNull
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
import org.junit.runner.RunWith
import java.util.Date

/**
* Runs under Robolectric because the dates are serialised by the API 26+ formatter, which is stricter about the year
* than the [java.text.SimpleDateFormat] fallback used below it.
*/
@RunWith(AndroidJUnit4::class)
internal class ReminderInfoConverterTest {

private val converter = ReminderInfoConverter()

@Test
fun `a reminder built from the date fixtures survives the round trip`() {
repeat(ROUNDS) {
val reminder = ReminderInfoEntity(
remindAt = randomDateOrNull(),
createdAt = randomDate(),
updatedAt = randomDate(),
)

converter.stringToReminderInfo(converter.reminderInfoToString(reminder)) shouldBeEqualTo reminder
}
}

@Test
fun `a reminder at the last serializable instant survives the round trip`() {
// Guards the upper bound the date fixtures generate up to: past it the year no longer fits the wire format,
// and a reminder comes back holding a different date, or fails to be read back at all.
val lastSerializable = Date(LAST_SERIALIZABLE_DATE_MILLIS)
val reminder = ReminderInfoEntity(
remindAt = lastSerializable,
createdAt = lastSerializable,
updatedAt = lastSerializable,
)

converter.stringToReminderInfo(converter.reminderInfoToString(reminder)) shouldBeEqualTo reminder
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

private companion object {
// Enough draws that a fixture generating unserializable dates again fails here, rather than
// intermittently somewhere downstream.
const val ROUNDS = 5_000
const val LAST_SERIALIZABLE_DATE_MILLIS = 253_402_300_799_999L // 9999-12-31T23:59:59.999Z
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2014-2026 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getstream.chat.android

import org.amshove.kluent.shouldBeNull
import org.junit.jupiter.api.Test
import java.util.Date

/**
* The two properties the rest of the suite leans on. Both have broken before, and because the generator is random,
* breaking either shows up as an unrelated test failing once in a few hundred CI runs rather than as a failure here.
*/
internal class RandomDateTest {

@Test
fun `a generated date is always in the future`() {
// Production code that keeps only future dates, live locations for one, drops a fixture date that is already
// in the past, leaving the test asserting on an empty result.
val now = Date()

val past = List(ROUNDS) { randomDate() }.filter { it.before(now) }

past.firstOrNull().shouldBeNull()
}

@Test
fun `a generated date is always serializable`() {
// The ISO-8601 formatter writes a four digit year, so a later date is written as a different date, or as one
// that cannot be read back at all.
val lastSerializable = Date(LAST_SERIALIZABLE_DATE_MILLIS)

val unserializable = List(ROUNDS) { randomDate() }.filter { it.after(lastSerializable) }

unserializable.firstOrNull().shouldBeNull()
}

@Test
fun `a date generated after another one is serializable too`() {
val lastSerializable = Date(LAST_SERIALIZABLE_DATE_MILLIS)

val unserializable = List(ROUNDS) { randomDateAfter(randomDate()) }.filter { it.after(lastSerializable) }

unserializable.firstOrNull().shouldBeNull()
}

private companion object {
const val ROUNDS = 100_000
const val LAST_SERIALIZABLE_DATE_MILLIS = 253_402_300_799_999L // 9999-12-31T23:59:59.999Z
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -773,10 +773,23 @@ public fun randomFiles(
creationFunction: (Int) -> File = { randomFile() },
): List<File> = (1..size).map(creationFunction)

/**
* The window generated dates are drawn from.
*
* The upper bound is the last instant the SDK can serialise: the ISO-8601 formatter writes a four digit year, so
* anything past year 9999 is written as a different date, or as one that cannot be read back at all.
*
* The lower bound keeps generated dates in the future, which is what callers already relied on. Drawing from the whole
* positive `Long` range put every date millions of years ahead, so production code that keeps only future dates, live
* locations for one, never saw a generated date fall behind the clock. Use [randomDateBefore] for a date in the past.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small thing: "Use randomDateBefore for a date in the past" only works if you pass Date(). randomDateBefore(randomDate()) gives a year 3000 or later date now, and randomDateBefore(Date()) only goes back about 25 days, since it subtracts at most Int.MAX_VALUE - 1 ms. Maybe write it as randomDateBefore(Date())? Not blocking.

*/
private const val MIN_GENERATED_DATE_MILLIS: Long = 32_503_680_000_000L // 3000-01-01T00:00:00.000Z
private const val MAX_GENERATED_DATE_MILLIS: Long = 253_402_300_799_999L // 9999-12-31T23:59:59.999Z

public fun randomDateOrNull(): Date? = randomDate().takeIf { randomBoolean() }
public fun randomDate(): Date = Date(positiveRandomLong())
public fun randomDate(): Date = Date(randomLongBetween(MIN_GENERATED_DATE_MILLIS, MAX_GENERATED_DATE_MILLIS))
public fun randomDateBefore(date: Date): Date = Date(date.time - positiveRandomInt())
public fun randomDateAfter(date: Date): Date = Date(randomLongBetween(date.time))
public fun randomDateAfter(date: Date): Date = Date(randomLongBetween(date.time, MAX_GENERATED_DATE_MILLIS))

public fun createDate(
year: Int = positiveRandomInt(),
Expand Down
Loading