diff --git a/data/src/main/kotlin/io/timemates/backend/data/authorization/PostgresqlAuthorizationsRepository.kt b/data/src/main/kotlin/io/timemates/backend/data/authorization/PostgresqlAuthorizationsRepository.kt index f9ebb469..d090aa5e 100644 --- a/data/src/main/kotlin/io/timemates/backend/data/authorization/PostgresqlAuthorizationsRepository.kt +++ b/data/src/main/kotlin/io/timemates/backend/data/authorization/PostgresqlAuthorizationsRepository.kt @@ -1,7 +1,6 @@ package io.timemates.backend.data.authorization import com.timemates.backend.time.UnixTime -import io.timemates.backend.validation.createOrThrowInternally import io.timemates.backend.authorization.repositories.AuthorizationsRepository import io.timemates.backend.authorization.types.Authorization import io.timemates.backend.authorization.types.metadata.ClientMetadata @@ -17,6 +16,7 @@ import io.timemates.backend.pagination.Page import io.timemates.backend.pagination.PageToken import io.timemates.backend.pagination.map import io.timemates.backend.users.types.value.UserId +import io.timemates.backend.validation.createOrThrowInternally class PostgresqlAuthorizationsRepository( private val tableAuthorizationsDataSource: TableAuthorizationsDataSource, @@ -68,11 +68,11 @@ class PostgresqlAuthorizationsRepository( return tableAuthorizationsDataSource.removeAuthorization(accessToken.string) } - override suspend fun get(accessToken: AccessHash, afterTime: UnixTime): Authorization? { + override suspend fun get(accessToken: AccessHash, currentTime: UnixTime): Authorization? { cacheAuthorizations.getAuthorization(accessToken.string) ?.let { return mapper.cacheAuthToDomainAuth(it) } - return tableAuthorizationsDataSource.getAuthorization(accessToken.string, afterTime.inMilliseconds) + return tableAuthorizationsDataSource.getAuthorization(accessToken.string, currentTime.inMilliseconds) ?.also { cacheAuthorizations.saveAuthorization(accessToken.string, mapper.dbAuthToCacheAuth(it)) } ?.let(mapper::dbAuthToDomainAuth) } diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/PostgresqlTimersRepository.kt b/data/src/main/kotlin/io/timemates/backend/data/timers/PostgresqlTimersRepository.kt index 45b6d22d..8bff3b88 100644 --- a/data/src/main/kotlin/io/timemates/backend/data/timers/PostgresqlTimersRepository.kt +++ b/data/src/main/kotlin/io/timemates/backend/data/timers/PostgresqlTimersRepository.kt @@ -1,7 +1,6 @@ package io.timemates.backend.data.timers import com.timemates.backend.time.UnixTime -import io.timemates.backend.validation.createOrThrowInternally import io.timemates.backend.common.types.value.Count import io.timemates.backend.data.timers.cache.CacheTimersDataSource import io.timemates.backend.data.timers.db.TableTimerParticipantsDataSource @@ -17,6 +16,7 @@ import io.timemates.backend.timers.types.value.TimerDescription import io.timemates.backend.timers.types.value.TimerId import io.timemates.backend.timers.types.value.TimerName import io.timemates.backend.users.types.value.UserId +import io.timemates.backend.validation.createOrThrowInternally class PostgresqlTimersRepository( private val tableTimers: TableTimersDataSource, @@ -35,6 +35,7 @@ class PostgresqlTimersRepository( name = name.string, description = null, ownerId = ownerId.long, + creationTime = creationTime.inMilliseconds, ) tableTimers.setSettings( diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/db/TableTimersDataSource.kt b/data/src/main/kotlin/io/timemates/backend/data/timers/db/TableTimersDataSource.kt index e1f05a3b..f2699fd1 100644 --- a/data/src/main/kotlin/io/timemates/backend/data/timers/db/TableTimersDataSource.kt +++ b/data/src/main/kotlin/io/timemates/backend/data/timers/db/TableTimersDataSource.kt @@ -9,9 +9,9 @@ import io.timemates.backend.exposed.update import io.timemates.backend.pagination.Ordering import io.timemates.backend.pagination.Page import io.timemates.backend.pagination.PageToken -import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import org.jetbrains.annotations.TestOnly import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.transactions.transaction @@ -31,11 +31,13 @@ class TableTimersDataSource( name: String, description: String? = null, ownerId: Long, + creationTime: Long, ): Long = suspendedTransaction(database) { TimersTable.insert { it[NAME] = name - it[DESCRIPTION] = description ?: "" + it[DESCRIPTION] = description.orEmpty() it[OWNER_ID] = ownerId + it[CREATION_TIME] = creationTime }[TimersTable.ID] } @@ -105,14 +107,17 @@ class TableTimersDataSource( val decodedPageToken: TimersPageToken? = pageToken?.forInternal()?.let { json.decodeFromString(it) } val result = TimersTable.select { - TimersTable.ID greater (decodedPageToken?.nextRetrievedTimerId ?: 0) and + TimersTable.CREATION_TIME less (decodedPageToken?.beforeTime ?: Long.MAX_VALUE) and + (TimersTable.ID less (decodedPageToken?.prevReceivedId ?: Long.MAX_VALUE)) and (TimersTable.OWNER_ID eq userId) - }.orderBy(TimersTable.CREATION_TIME, SortOrder.DESC).map(timersMapper::resultRowToDbTimer) + }.orderBy( + order = arrayOf(TimersTable.CREATION_TIME to SortOrder.DESC, TimersTable.ID to SortOrder.DESC) + ).limit(20).map(timersMapper::resultRowToDbTimer) val lastId = result.lastOrNull()?.id val nextPageToken = if (lastId != null) - PageToken.toGive(json.encodeToString(TimersPageToken(lastId))) - else pageToken + PageToken.toGive(json.encodeToString(TimersPageToken(lastId, result.lastOrNull()!!.creationTime))) + else null return@suspendedTransaction Page( value = result, @@ -126,4 +131,9 @@ class TableTimersDataSource( .singleOrNull() ?.let(timersMapper::resultRowToDbTimer) } + + @TestOnly + suspend fun clear(): Unit = suspendedTransaction(database) { + TimersTable.deleteAll() + } } \ No newline at end of file diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/db/entities/DbTimer.kt b/data/src/main/kotlin/io/timemates/backend/data/timers/db/entities/DbTimer.kt index 5f47e46e..6bee27bb 100644 --- a/data/src/main/kotlin/io/timemates/backend/data/timers/db/entities/DbTimer.kt +++ b/data/src/main/kotlin/io/timemates/backend/data/timers/db/entities/DbTimer.kt @@ -6,8 +6,9 @@ data class DbTimer( val description: String, val ownerId: Long, val settings: Settings, + val creationTime: Long, ) { - class Settings( + data class Settings( val workTime: Long, val restTime: Long, val bigRestTime: Long, diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/db/entities/TimersPageToken.kt b/data/src/main/kotlin/io/timemates/backend/data/timers/db/entities/TimersPageToken.kt index d10957f2..243c69a4 100644 --- a/data/src/main/kotlin/io/timemates/backend/data/timers/db/entities/TimersPageToken.kt +++ b/data/src/main/kotlin/io/timemates/backend/data/timers/db/entities/TimersPageToken.kt @@ -4,5 +4,6 @@ import kotlinx.serialization.Serializable @Serializable data class TimersPageToken( - val nextRetrievedTimerId: Long, + val prevReceivedId: Long, + val beforeTime: Long, ) \ No newline at end of file diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/db/tables/TimersTable.kt b/data/src/main/kotlin/io/timemates/backend/data/timers/db/tables/TimersTable.kt index 336ef1c6..82871359 100644 --- a/data/src/main/kotlin/io/timemates/backend/data/timers/db/tables/TimersTable.kt +++ b/data/src/main/kotlin/io/timemates/backend/data/timers/db/tables/TimersTable.kt @@ -3,6 +3,7 @@ package io.timemates.backend.data.timers.db.tables import io.timemates.backend.data.users.datasource.PostgresqlUsersDataSource import io.timemates.backend.exposed.emptyAsDefault import org.jetbrains.exposed.sql.Table +import kotlin.time.Duration.Companion.minutes internal object TimersTable : Table("timers") { val ID = long("id").autoIncrement() @@ -13,13 +14,13 @@ internal object TimersTable : Table("timers") { val CREATION_TIME = long("creation_time") // Settings - val WORK_TIME = long("work_time") - val REST_TIME = long("rest_time") - val BIG_REST_TIME = long("big_rest_time") - val BIG_REST_TIME_ENABLED = bool("big_rest_time_enabled") - val BIG_REST_PER = integer("big_rest_per") - val IS_EVERYONE_CAN_PAUSE = bool("is_everyone_can_pause") - val IS_CONFIRMATION_REQUIRED = bool("is_confirmation_required") + val WORK_TIME = long("work_time").default(25.minutes.inWholeMilliseconds) + val REST_TIME = long("rest_time").default(5.minutes.inWholeMilliseconds) + val BIG_REST_TIME = long("big_rest_time").default(10.minutes.inWholeMilliseconds) + val BIG_REST_TIME_ENABLED = bool("big_rest_time_enabled").default(false) + val BIG_REST_PER = integer("big_rest_per").default(4) + val IS_EVERYONE_CAN_PAUSE = bool("is_everyone_can_pause").default(false) + val IS_CONFIRMATION_REQUIRED = bool("is_confirmation_required").default(true) override val primaryKey = PrimaryKey(ID) } \ No newline at end of file diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/mappers/TimersMapper.kt b/data/src/main/kotlin/io/timemates/backend/data/timers/mappers/TimersMapper.kt index 969df9f4..c4a88c78 100644 --- a/data/src/main/kotlin/io/timemates/backend/data/timers/mappers/TimersMapper.kt +++ b/data/src/main/kotlin/io/timemates/backend/data/timers/mappers/TimersMapper.kt @@ -3,7 +3,6 @@ package io.timemates.backend.data.timers.mappers import com.timemates.backend.time.TimeProvider -import io.timemates.backend.validation.createOrThrowInternally import io.timemates.backend.common.types.value.Count import io.timemates.backend.data.common.markers.Mapper import io.timemates.backend.data.timers.db.entities.DbTimer @@ -17,6 +16,7 @@ import io.timemates.backend.timers.types.value.TimerDescription import io.timemates.backend.timers.types.value.TimerId import io.timemates.backend.timers.types.value.TimerName import io.timemates.backend.users.types.value.UserId +import io.timemates.backend.validation.createOrThrowInternally import org.jetbrains.exposed.sql.ResultRow import kotlin.time.Duration.Companion.minutes @@ -28,6 +28,7 @@ class TimersMapper(private val sessionMapper: TimerSessionMapper) : Mapper { get(TimersTable.DESCRIPTION), get(TimersTable.OWNER_ID), resultRowToTimerSettings(resultRow), + get(TimersTable.CREATION_TIME), ) } diff --git a/data/src/test/kotlin/io/timemates/backend/data/timers/datasource/TableTimersDataSourceTest.kt b/data/src/test/kotlin/io/timemates/backend/data/timers/datasource/TableTimersDataSourceTest.kt new file mode 100644 index 00000000..349797d3 --- /dev/null +++ b/data/src/test/kotlin/io/timemates/backend/data/timers/datasource/TableTimersDataSourceTest.kt @@ -0,0 +1,185 @@ +package io.timemates.backend.data.timers.datasource + +import io.timemates.backend.data.timers.db.TableTimersDataSource +import io.timemates.backend.data.timers.db.entities.DbTimer +import io.timemates.backend.data.timers.mappers.TimerSessionMapper +import io.timemates.backend.data.timers.mappers.TimersMapper +import io.timemates.backend.data.users.datasource.PostgresqlUsersDataSource +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json +import org.jetbrains.exposed.sql.Database +import org.junit.Before +import org.junit.Test +import org.junit.jupiter.api.BeforeEach +import kotlin.properties.Delegates +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class TableTimersDataSourceTest { + private val databaseUrl = "jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1;" + private val databaseDriver = "org.h2.Driver" + + private val database = Database.connect(databaseUrl, databaseDriver) + private val timers: TableTimersDataSource = TableTimersDataSource(database, TimersMapper(TimerSessionMapper()), Json) + private val users: PostgresqlUsersDataSource = PostgresqlUsersDataSource(database) + + private var ownerId by Delegates.notNull() + + @BeforeEach + fun `clear database`(): Unit = runBlocking { + timers.clear() + } + + @Before + fun `create test user`(): Unit = runBlocking { + ownerId = users.createUser( + email = "test@email.com", + userName = "test name", + shortBio = "Test bio", + creationTime = System.currentTimeMillis(), + ) + } + + @Test + fun `createTimer should return the correct timer ID`(): Unit = runBlocking { + // Arrange + val name = "Test Timer" + val description = "This is a test timer" + val creationTime = System.currentTimeMillis() + + // Act + timers.createTimer(name, description, ownerId, creationTime) + } + + @Test + fun `createTimer should return a different timer ID for each call`(): Unit = runBlocking { + // Arrange + val name = "Test Timer" + val description = "This is a test timer" + val creationTime = System.currentTimeMillis() + + // Act + val timerId1 = timers.createTimer(name, description, ownerId, creationTime) + val timerId2 = timers.createTimer(name, description, ownerId, creationTime) + + // Assert + assertEquals(timerId1 + 1, timerId2) + } + + @Test + fun `createTimer should return a default description if not provided`(): Unit = runBlocking { + // Arrange + val name = "Test Timer" + val creationTime = System.currentTimeMillis() + + // Act + val timerId = timers.createTimer(name, ownerId = ownerId, creationTime = creationTime) + + // Assert + assertEquals("", timers.getTimer(timerId)?.description.orEmpty()) + } + + @Test + fun `editTimer updates name successfully`(): Unit = runBlocking { + val timerId = 1L + val newName = "New Timer Name" + timers.editTimer(timerId, newName = newName) + val updatedTimer = timers.getTimer(timerId) + assertEquals(newName, updatedTimer?.name) + } + + @Test + fun `editTimer updates description`(): Unit = runBlocking { + val timerId = 1L + val newDescription = "New Timer Description" + timers.editTimer(timerId, newDescription = newDescription) + val updatedTimer = timers.getTimer(timerId) + assertEquals(newDescription, updatedTimer?.description) + } + + @Test + fun `editTimer with timer that does not exist`(): Unit = runBlocking { + val timerId = 999L + val newName = "New Timer Name" + timers.editTimer(timerId, newName = newName) + val updatedTimer = timers.getTimer(timerId) + assertNull(updatedTimer) + } + + @Test + fun `setSettings should update the timer settings`(): Unit = runBlocking { + // Arrange + val name = "Test Timer" + val creationTime = System.currentTimeMillis() + + // Act + val timerId = timers.createTimer(name, ownerId = ownerId, creationTime = creationTime) + + val settings = DbTimer.Settings.Patchable( + workTime = 10, + bigRestEnabled = true, + bigRestPer = 2, + bigRestTime = 10, + isEveryoneCanPause = true, + isConfirmationRequired = true, + restTime = 5, + ) + + // Act + runBlocking { + timers.setSettings(timerId, settings) + } + + // Assert + val updatedTimer = runBlocking { timers.getTimer(timerId) } + assertEquals(settings.workTime, updatedTimer?.settings?.workTime) + assertEquals(settings.bigRestEnabled, updatedTimer?.settings?.bigRestEnabled) + assertEquals(settings.bigRestPer, updatedTimer?.settings?.bigRestPer) + assertEquals(settings.bigRestTime, updatedTimer?.settings?.bigRestTime) + assertEquals(settings.isEveryoneCanPause, updatedTimer?.settings?.isEveryoneCanPause) + assertEquals(settings.isConfirmationRequired, updatedTimer?.settings?.isConfirmationRequired) + assertEquals(settings.restTime, updatedTimer?.settings?.restTime) + } + + + @Test + fun `check get timers should return empty list if no timers`(): Unit = runBlocking { + val anotherUser = users.createUser("test2@gmail.com", "Test2", null, System.currentTimeMillis()) + timers.createTimer("Test", null, anotherUser, System.currentTimeMillis()) + + assert(timers.getTimers(ownerId, pageToken = null).value.isEmpty()) + } + + @Test + fun `check get timers should return correct list of timers`(): Unit = runBlocking { + val anotherUser = users.createUser("test2@gmail.com", "Test2", null, System.currentTimeMillis()) + + val ids = buildList { + add(timers.createTimer("Test", null, anotherUser, System.currentTimeMillis())) + add(timers.createTimer("Test 2", null, anotherUser, System.currentTimeMillis())) + add(timers.createTimer("Test 3", null, anotherUser, System.currentTimeMillis())) + }.reversed() + + val expected = ids.map { timers.getTimer(it) } + + assertContentEquals(timers.getTimers(anotherUser, pageToken = null).value, expected) + } + + @Test + fun `check get timers with page token returns correct page`(): Unit = runBlocking { + List(50) { index -> + timers.createTimer("Test ${index + 1}", null, ownerId, creationTime = index.toLong()) + } + + val first = timers.getTimers(ownerId, null) + val second = timers.getTimers(ownerId, first.nextPageToken!!) + val third = timers.getTimers(ownerId, second.nextPageToken!!) + + assertEquals(actual = first.value.first().name, expected = "Test 50") + assertEquals(actual = first.value.last().name, expected = "Test 31") + assertEquals(actual = second.value.first().name, expected = "Test 30") + assertEquals(actual = third.value.first().name, expected = "Test 10") + assertEquals(actual = third.value.last().name, expected = "Test 1") + } +} \ No newline at end of file