Update SDK, Gradle and packages

Fix timezone issues
Add timezone tests
This commit is contained in:
Geoffroy Bonneville
2026-07-16 13:53:51 -04:00
parent c2c146a28e
commit 897b1892e5
15 changed files with 1335 additions and 147 deletions
@@ -0,0 +1,17 @@
package com.wismna.geoffroy.donext.data.injection
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import java.time.Clock
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object ClockModule {
@Provides
@Singleton
fun provideClock(): Clock = Clock.systemDefaultZone()
}
@@ -3,20 +3,24 @@ package com.wismna.geoffroy.donext.domain.usecase
import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
import kotlinx.coroutines.flow.Flow
import java.time.Clock
import java.time.LocalDate
import java.time.ZoneOffset
import javax.inject.Inject
class GetDueTodayTasksUseCase @Inject constructor(private val repository: TaskRepository) {
class GetDueTodayTasksUseCase @Inject constructor(
private val repository: TaskRepository,
private val clock: Clock
) {
operator fun invoke(): Flow<List<Task>> {
val todayStart = LocalDate.now()
.atStartOfDay(ZoneOffset.UTC)
val today = LocalDate.now(clock)
val todayStart = today
.atStartOfDay(clock.zone)
.toInstant()
.toEpochMilli()
val todayEnd = LocalDate.now()
val todayEnd = today
.plusDays(1)
.atStartOfDay(ZoneOffset.UTC)
.atStartOfDay(clock.zone)
.toInstant()
.toEpochMilli() - 1
return repository.getDueTodayTasks(
@@ -3,17 +3,20 @@ package com.wismna.geoffroy.donext.domain.usecase
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
import kotlinx.coroutines.flow.Flow
import java.time.Clock
import java.time.LocalDate
import java.time.ZoneOffset
import javax.inject.Inject
class GetTaskListsWithOverdueUseCase @Inject constructor(private val taskRepository: TaskRepository) {
class GetTaskListsWithOverdueUseCase @Inject constructor(
private val taskRepository: TaskRepository,
private val clock: Clock
) {
operator fun invoke(): Flow<List<TaskListWithOverdue>> {
return taskRepository.getTaskListsWithOverdue(
LocalDate.now()
.atStartOfDay(ZoneOffset.UTC)
.toInstant()
.toEpochMilli()
LocalDate.now(clock)
.atStartOfDay(clock.zone)
.toInstant()
.toEpochMilli()
)
}
}
@@ -0,0 +1,80 @@
package com.wismna.geoffroy.donext.domain.usecase
import com.google.common.truth.Truth.assertThat
import com.wismna.geoffroy.donext.domain.model.Priority
import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.time.Clock
import java.time.LocalDate
import java.time.ZoneId
class GetDueTodayTasksUseCaseTest {
@Test
fun `computes today's window using the clock's own zone, not UTC`() {
// Paris in summer is UTC+2: local midnight for this date is 22:00 UTC
// the *previous* day. A window anchored to UTC midnight would miss it.
val zone = ZoneId.of("Europe/Paris")
val today = LocalDate.of(2026, 7, 16)
val clock = Clock.fixed(today.atStartOfDay(zone).plusHours(10).toInstant(), zone)
val repository = mockk<TaskRepository>()
every { repository.getDueTodayTasks(any(), any()) } returns flowOf(emptyList())
GetDueTodayTasksUseCase(repository, clock).invoke()
val expectedStart = today.atStartOfDay(zone).toInstant().toEpochMilli()
val expectedEnd = today.plusDays(1).atStartOfDay(zone).toInstant().toEpochMilli() - 1
verify { repository.getDueTodayTasks(expectedStart, expectedEnd) }
}
@Test
fun `window covers exactly local midnight to the millisecond before the next local midnight`() {
val zone = ZoneId.of("America/Montreal")
val today = LocalDate.of(2026, 1, 5)
val clock = Clock.fixed(today.atStartOfDay(zone).plusHours(6).toInstant(), zone)
val repository = mockk<TaskRepository>()
every { repository.getDueTodayTasks(any(), any()) } returns flowOf(emptyList())
GetDueTodayTasksUseCase(repository, clock).invoke()
val expectedStart = today.atStartOfDay(zone).toInstant().toEpochMilli()
val expectedEnd = today.plusDays(1).atStartOfDay(zone).toInstant().toEpochMilli() - 1
assertThat(expectedEnd - expectedStart).isEqualTo(24L * 60 * 60 * 1000 - 1)
verify { repository.getDueTodayTasks(expectedStart, expectedEnd) }
}
@Test
fun `passes through the tasks emitted by the repository`() = runTest {
val zone = ZoneId.systemDefault()
val clock = Clock.fixed(LocalDate.of(2026, 1, 1).atStartOfDay(zone).toInstant(), zone)
val tasks = listOf(
Task(
id = 1L,
taskListId = 1L,
name = "Task",
description = "",
priority = Priority.NORMAL,
isDone = false,
isDeleted = false
)
)
val repository = mockk<TaskRepository>()
every { repository.getDueTodayTasks(any(), any()) } returns flowOf(tasks)
val result = GetDueTodayTasksUseCase(repository, clock).invoke().first()
assertThat(result).isEqualTo(tasks)
}
}
@@ -0,0 +1,50 @@
package com.wismna.geoffroy.donext.domain.usecase
import com.google.common.truth.Truth.assertThat
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.time.Clock
import java.time.LocalDate
import java.time.ZoneId
class GetTaskListsWithOverdueUseCaseTest {
@Test
fun `computes the overdue cutoff at local midnight, not UTC midnight`() {
// Paris in summer is UTC+2: local midnight for this date is 22:00 UTC
// the *previous* day. A cutoff anchored to UTC midnight would flag
// today's tasks as overdue a full day too early.
val zone = ZoneId.of("Europe/Paris")
val today = LocalDate.of(2026, 7, 16)
val clock = Clock.fixed(today.atStartOfDay(zone).plusHours(10).toInstant(), zone)
val repository = mockk<TaskRepository>()
every { repository.getTaskListsWithOverdue(any()) } returns flowOf(emptyList())
GetTaskListsWithOverdueUseCase(repository, clock).invoke()
val expectedCutoff = today.atStartOfDay(zone).toInstant().toEpochMilli()
verify { repository.getTaskListsWithOverdue(expectedCutoff) }
}
@Test
fun `passes through the lists emitted by the repository`() = runTest {
val zone = ZoneId.systemDefault()
val clock = Clock.fixed(LocalDate.of(2026, 1, 1).atStartOfDay(zone).toInstant(), zone)
val lists = listOf(TaskListWithOverdue(id = 1L, name = "Work", overdueCount = 2))
val repository = mockk<TaskRepository>()
every { repository.getTaskListsWithOverdue(any()) } returns flowOf(lists)
val result = GetTaskListsWithOverdueUseCase(repository, clock).invoke().first()
assertThat(result).isEqualTo(lists)
}
}