diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1de2d6856a32a75bd1594d495e3061196c1692a0..333c71451eed5cdda49f09da06145f3c4fd1f4dc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,8 +5,8 @@ java = "17" kotlin = "2.0.20" ksp = "2.0.20-1.0.24" -ktor = "3.0.0-beta-2" -kotlinx-serialization = "1.7.1" +ktor = "3.0.0-rc-1" +kotlinx-serialization = "1.7.2" slf4j = "2.0.13" @@ -16,15 +16,15 @@ datetime = "0.6.1" sandwich = "2.0.9" arrow = "1.2.4" # Main Koin version - for core, android deps -koin = "3.6.0-Beta5" -koinTest = "3.6.0-Beta5" +koin = "4.0.0" +koinTest = "4.0.0" # Koin version for Compose multiplatform -koinComposeMultiplatform = "1.2.0-Beta4" +koinComposeMultiplatform = "4.0.0" mosaic = "0.13.0" molecule = "2.0.0" jetbrainsNavigationCompose = "2.8.0-alpha09" jetbrainsLifecycle = "2.8.2" -androidxRoom = "2.7.0-alpha08" +androidxRoom = "2.7.0-alpha09" sqlite = "2.5.0-SNAPSHOT" androidxDatastore = "1.1.1" exposed = "0.53.0" @@ -44,7 +44,7 @@ kotlinx-dataframe = "0.13.1" klaxon = "5.6" kotest = "5.9.1" -composables = "1.11.2" +composables = "1.12.0" bottomsheet = "0.1.5" alertKmp = "0.0.7" filekit = "0.7.0" @@ -141,7 +141,7 @@ build-spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", vers dokka-base = { module = "org.jetbrains.dokka:dokka-base", version.ref = "dokka" } build-androidDocumentationPlugin = { module = "org.jetbrains.dokka:android-documentation-plugin", version.ref = "dokka" } build-gitSemVer = { module = "org.danilopianini:git-sensitive-semantic-versioning-gradle-plugin", version.ref = "gitSemVer"} -build-conventionalCommits = { module = "io.github.andreabrighi:conventional-commit-strategy-for-git-sensitive-semantic-versioning-gradle-plugin", version.ref = "conventionalCommits"} +build-conventionalCommits = { module = "io.github.andreabrighi:conventional-commit-strategy-for-git-sensitive-semantic-versioning-gradle-plugin", version.ref = "conventionalCommits" } # Testing deps dotenv-vault = { module = "com.github.dotenv-org:dotenv-vault-kotlin", version.ref = "dotenv-vault" } diff --git a/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/MainScreen.kt b/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/MainScreen.kt index bc3c2d91b627892f41494ed213a5f6ec1c842e6b..ce6bf22e80b4087b6f984d9883de82011cc17a75 100644 --- a/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/MainScreen.kt +++ b/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/MainScreen.kt @@ -89,7 +89,7 @@ import androidx.navigation.createGraph import androidx.navigation.toRoute import com.composables.composetheme.ComposeTheme import com.composables.composetheme.material3.colorScheme -import edu.ucsc.its.temerity.Temerity +import edu.ucsc.its.temerity.core.Temerity import edu.ucsc.its.temerity.model.Course import edu.ucsc.its.temerity.model.Device import edu.ucsc.its.temerity.model.User diff --git a/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/TemerityApp.kt b/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/TemerityApp.kt index 368ff87c3eca13c2ccb5a9d4f86e8e7cfe3d0e1d..cc8b5a292679fb55cda52df09969c905ad9c57fb 100644 --- a/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/TemerityApp.kt +++ b/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/TemerityApp.kt @@ -36,7 +36,6 @@ import org.koin.core.annotation.KoinExperimentalAPI private val koin = initKoin().koin -@OptIn(KoinExperimentalAPI::class) @Composable internal fun TemerityApp( appStateViewModel: AppViewModel = koinViewModel(), diff --git a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/data/repository/PlatformRepository.kt b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/data/repository/PlatformRepository.kt index 384a666f2175e5be9eba5f79dbb9688e4303e60b..e2b02413e6f0fe875ae500bc8a5fcc8286a00db3 100644 --- a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/data/repository/PlatformRepository.kt +++ b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/data/repository/PlatformRepository.kt @@ -35,7 +35,7 @@ package edu.ucsc.its.temerity.shared.data.repository import androidx.compose.runtime.mutableLongStateOf import co.touchlab.kermit.Logger -import edu.ucsc.its.temerity.Temerity +import edu.ucsc.its.temerity.core.Temerity import edu.ucsc.its.temerity.model.Device import edu.ucsc.its.temerity.model.Group import edu.ucsc.its.temerity.model.User diff --git a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/di/CommonModule.kt b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/di/CommonModule.kt index 6a6a6573b8ad7196c2b709d87408ef09eece370c..b451b1c891624aedbaf57ee15cf4c51c43649b5b 100644 --- a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/di/CommonModule.kt +++ b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/di/CommonModule.kt @@ -19,7 +19,7 @@ package edu.ucsc.its.temerity.shared.di import ca.gosyer.appdirs.AppDirs import co.touchlab.kermit.Logger import co.touchlab.kermit.koin.kermitLoggerModule -import edu.ucsc.its.temerity.core.temerityInstance +import edu.ucsc.its.temerity.core.Temerity import edu.ucsc.its.temerity.shared.AppSettings import edu.ucsc.its.temerity.shared.data.repository.PlatformRepository import edu.ucsc.its.temerity.shared.data.storageModule @@ -27,8 +27,8 @@ import edu.ucsc.its.temerity.shared.viewmodel.AppViewModel import edu.ucsc.its.temerity.shared.viewmodel.LoginViewModel import edu.ucsc.its.temerity.shared.viewmodel.RootViewModel import edu.ucsc.its.temerity.shared.viewmodel.UserEditViewModel -import org.koin.compose.viewmodel.dsl.viewModelOf import org.koin.core.context.startKoin +import org.koin.core.module.dsl.viewModelOf import org.koin.core.qualifier.named import org.koin.dsl.KoinAppDeclaration import org.koin.dsl.koinApplication @@ -46,10 +46,10 @@ internal fun commonModule(isDebuggingEnabled: Boolean = false) = module { viewModelOf(::RootViewModel) viewModelOf(::UserEditViewModel) single { (serviceEndpoint: String, serviceToken: String) -> - temerityInstance { - debugEnabled(isDebuggingEnabled) - serviceEndpoint(serviceEndpoint) + Temerity { + serviceUrl(serviceEndpoint) serviceToken(serviceToken) + debugEnabled(isDebuggingEnabled) } } single { PlatformRepository() } diff --git a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/AppViewModel.kt b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/AppViewModel.kt index 0d1ed546722a9499b9e7056e53ed05679e209267..ea92f9ccc8433cb3fde90bb19e5ddab108128ed3 100644 --- a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/AppViewModel.kt +++ b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/AppViewModel.kt @@ -22,7 +22,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.viewModelScope -import edu.ucsc.its.temerity.Temerity +import edu.ucsc.its.temerity.core.Temerity import edu.ucsc.its.temerity.shared.viewmodel.AppStateEvent.ServiceEndpointUpdated import edu.ucsc.its.temerity.shared.viewmodel.AppStateEvent.ServiceTokenUpdated import edu.ucsc.its.temerity.shared.viewmodel.AppStateEvent.UserLoggedIn diff --git a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/DevicesScreenViewModel.kt b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/DevicesScreenViewModel.kt index 194902f2460fdd7505a540b753c1b8290a0d5940..291417b8d7839e393c4fdd816a5faa5bfa62cc89 100644 --- a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/DevicesScreenViewModel.kt +++ b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/DevicesScreenViewModel.kt @@ -22,7 +22,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import edu.ucsc.its.temerity.Temerity +import edu.ucsc.its.temerity.core.Temerity import edu.ucsc.its.temerity.model.Device import kotlinx.coroutines.flow.Flow import org.koin.compose.getKoin diff --git a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/RootViewModel.kt b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/RootViewModel.kt index e29f377d368a39623dd4b2fffddf65bcc70c85e0..ef6c5db27f31faedda5bda7793cc1e5235d05455 100644 --- a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/RootViewModel.kt +++ b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/RootViewModel.kt @@ -17,7 +17,7 @@ package edu.ucsc.its.temerity.shared.viewmodel import androidx.compose.runtime.Composable -import edu.ucsc.its.temerity.Temerity +import edu.ucsc.its.temerity.core.Temerity import kotlinx.coroutines.flow.Flow import org.koin.core.component.KoinComponent import org.koin.core.component.get diff --git a/temerity/.env.vault b/temerity/.env.vault index 176df552965dd99fce402512dac1e512d8be3341..25e8b67272661ad9921e0da0b2e78abcf851b6d9 100644 --- a/temerity/.env.vault +++ b/temerity/.env.vault @@ -4,8 +4,8 @@ #/--------------------------------------------------/ # development -DOTENV_VAULT_DEVELOPMENT="U+0XF45FTYpDQEk5bU9LwOWezR5/PooEYuBsMa32Y5SmRgqaGutPs8bAVrPknRGR9hQG3J69xW9fG+WTMq4jHR5eonHLgitwoYNFPNzhyDEIIvL9ihp+kJdCKdxlmJUOOVqUdkckXgQJMy+CeQf5Td09NmQCRRlL1Sct0/X0pA+uq+osz2NObKr+NBJJuNSCofrfHR7bVxtBRLbBsAvTXAwD5lavNjtKf9i7sjALbmWQDPoZS+5WQfzPPv5B9Jp2Fe6GWvTCcjnctyMFMxxAY7I60uxpRxLJY7jdZRxKVCmUThcak7D7cie9nzMZLvLe9ME0y/ABNB4bgE5Rb1HXvl7yOl7C7f+CbcF+ExxVc6bPCSv5szOmYptT2CqY+85hN+LEpFWjK58GzfnupKE8exc5PSiIPjIwoB0tibZDX3oAeUMWmuQSMH/l8pj4PdxEJkIMJ5b9Kn3oGvIdwY9sKVKWfG57xg3XB3Pa5TmpBOv2DvGx2Qin1r2gTFlFxAgKpQGYSmoDREyNir2uKTlPdPSodBsyb5Srg4jpSQ5XuKMgWdDaf+heualPfddz+qP0wcd3zu/NCw6LwrHgiOQQtXkSQ+4WL8MAqRu5Sz7Z1FrF8MEiR19QrCeTFU1i2cACem6JmDrApJVpR+OpIwHVp2SdsfpGwHQGpa6vsdjeW8ZxwkXugOgqICtxtv7P1N80pi38UCDheIU/yabi7Qe2oC22wJ3A0+/BvNNaXC7J9IcdAimzJfkftrwppGqAndxhHzEeaHgC3UHvpx8ouDkWtf3pKVWka5inFXDaEqQ46Em53W9TKDvcBm1LQ97WKQTbBWx3Q7PG1tEUgmss/hzMzxb5/dlBDhL6j1OUPUTN1ctP7CZVC7irhCD4LWFcVBpivZJQJdOwIWpOjIpHtUbzJZQQh3gljcM+dOjKUuhscCvCvRRP2nbKVOMMM4TVfkWEhCUkGVqyu60q4GKrCWoljvAJJPo=" -DOTENV_VAULT_DEVELOPMENT_VERSION=19 +DOTENV_VAULT_DEVELOPMENT="DCHgKIC5D+Tef2A5UpGbSV6legsP/JCF7fUpqRdMwFsKbMst7VDmKDo4oLOvRCCgJl6ADvCm9oI2B79zntB1K5zRFo7TTbBtw9Vova+6X5BpoA7YYsf5N44aYKwsWqkRp6JD0p4VO0arpX1XWNjmK1LG2UPsfUU3gLu2I7plsYtSi4YRhNSh2gHccQCgC6FmQS9CG8KoQQFikW72DYmIMNALDq5xxKG2DgfRYTBER+qPwFAg7IQACIt/N4L9LEsrcnE1xFoPgwlLXUzLHlYpQpvfkg6V7brGobbcvUwf0+YsDHGFf48y9CGAOIlUYk9TgXyxi9T4Fa/bSwm+vLUDK7Mo26FI+8HIyaPgramA8X2+o6k73ULUfF8DAD2JiC5AWe42CSGaZAwCe0XILX/7iS4NTHnmX1Fx/zRAMxDZmPd0BTm9Mw9cXxtk1zeDAo37EUqKpztwMQ7PbMNjLeDzy/5DNAlO6Ke9u2RoUsphpolS6vm4Lnuheo9BlAk3N8inAzdPG8NMPhCfTmy1GmiseQtsUUdP4QtFRJtBSqJ2BBNTkD8bmwPWBdePM/w1jyLQhg4TRSpQtMydCJKt/WqqSzXq7JQJDz+9T3yH+snH8xiac/nQAvxocEw/CmA4kYsXsvYgFR7FgBTBW1ugcldfHx88eFZyPfJ/T9z0+IVovlTSUDOlWgFV8i4DbTUzYEu3mnoVTr2j98z0qzhpYu50MLl34ZtGVzQAuVWGmGvr+xueGQWhBzhKlPnNAmi5WEnnOY+ysnMaP+bIWHLbRmKyvWdRrxX8xySOEgsn7oH0mQZwWZZuYlHHGwT5CSSqe/LBNPTmSmOC6ZCj5Gf2q2dgCri1rzyyd2Ny7sI/22lB5WE1c7ltdxaVGtTHFt7wOrOrzAaklF4TlJD0gonje6ii+V7di/sn5uVbhpxvjbfnhWt82VYFK1F6Zr0xYwIjSehs7YUnlSLgsUzaWt6E/HbBB+3/M/0=" +DOTENV_VAULT_DEVELOPMENT_VERSION=20 # ci DOTENV_VAULT_CI="n69uhs4m6qCfBKFNpDoqnpPfqeXrU+T3VIO+eseGVKWbRjZhRxx3VqAqq1P5Pml/YNJSR3t7N48cOQEhQdEvgwgIceG6DtJClkHZy712jhivwt6CVF7Hi0znBM9G0mub/guVwTUngFBRsQKw3XpDnxrzfo/FUPP09Zhy3D604wDnKuk/M6cUgT4o3jucOXmsjnwZTm/UUz+65eRSB0L5O8Gy7tB1e30eH5WUmx+hnG85v9+QENjt1inU9bZuefwnf7XzSaPKisVZVm4AYxXWnGIVP84Wy2O2h5nytLPdw2Z72mwaGSTRp0miUtUynTtFsm/SeoIOAgbixm7ScF4o/juwfq6IhVUBDfZStjXMx/150U2QZVbseVHKWrIdpPy9bZ7gO2ufMDps+daXWsJsHRqe11UTPhyZ8jA5V0oxDHe1y1cf0Dr9ZUr0g4E=" diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt new file mode 100644 index 0000000000000000000000000000000000000000..c4463bab1f6a9d689fbeaaf03d732683f70bb8c1 --- /dev/null +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt @@ -0,0 +1,72 @@ +/* + * Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu) + * Copyright 2022-2024 The Regents of the University of California. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package edu.ucsc.its.temerity + +import edu.ucsc.its.temerity.core.TemDsl +import io.ktor.client.HttpClient +import io.ktor.client.HttpClientConfig +import io.ktor.client.plugins.logging.LoggingConfig + +/** + * Configuration for the Temerity client. + * Adapted from the tmdb-api project: https://github.com/MoviebaseApp/tmdb-kotlin/raw/refs/heads/main/tmdb-api/src/commonMain/kotlin/app/moviebase/tmdb/TmdbClientConfig.kt + */ +@TemDsl +public class TemClientConfig { + + /** + * Specifies URL of platform instance to make API calls to + */ + public var serviceUrl: String? = null + /** + * Specifies the authorization token to use for API calls + */ + public var serviceToken: String? = null + /** + * Specifies whether debug logging should be enabled + * Enable this for debugging purposes only + */ + public var optDebugEnabled: Boolean = false + + public var expectSuccess: Boolean = true + public var useTimeout: Boolean = false + public var maxRetriesOnException: Int? = null + + /** + * Configures the HttpClient with the provided block. + * @param () -> HttpClient The block to configure the HttpClient. + */ + internal var httpClientConfigBlock: (HttpClientConfig<*>.() -> Unit)? = null + internal var httpClientBuilder: (() -> HttpClient)? = null + internal var httpClientLoggingBlock: (LoggingConfig.() -> Unit)? = null + + public fun logging(block: LoggingConfig.() -> Unit) { + httpClientLoggingBlock = block + } + + public fun httpClient(block: () -> HttpClient) { + httpClientBuilder = block + } + + public companion object { + + internal fun withToken(serviceToken: String): TemClientConfig = TemClientConfig().apply { + this.serviceToken = serviceToken + } + } +} diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Temerity.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityApi.kt similarity index 78% rename from temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Temerity.kt rename to temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityApi.kt index 1750f49d452d92ef4edcdf1c80ee3ad71a16c25d..37a06a5588372b3cea2d518f723343e3d8a4748a 100644 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Temerity.kt +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityApi.kt @@ -17,7 +17,6 @@ */ package edu.ucsc.its.temerity -import edu.ucsc.its.temerity.core.TemerityImpl import edu.ucsc.its.temerity.model.AuditLogEntry import edu.ucsc.its.temerity.model.Course import edu.ucsc.its.temerity.model.Device @@ -34,24 +33,17 @@ import io.ktor.client.statement.HttpResponse import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime -import kotlinx.datetime.format import kotlinx.datetime.format.DateTimeFormat import kotlinx.datetime.format.char import kotlinx.datetime.plus -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder /** * This interface defines top-level library functions for interacting with the enterprise video platform API. - * The functions defined in this interface are implemented by the [TemerityImpl] class. - * @see [TemerityImpl] + * The functions defined in this interface are implemented by the [edu.ucsc.its.temerity.core.Temerity] class. + * @see [edu.ucsc.its.temerity.core.Temerity] */ -public interface Temerity { +public interface TemerityApi { // User API public library functions @@ -150,11 +142,17 @@ public interface Temerity { // Group API public library functions - public suspend fun getGroups(paginated: Boolean = true): List<Group> + public suspend fun getGroups( + // TODO: Make this field instance-wide and depend on API version + paginated: Boolean = true, + ): List<Group> public suspend fun getCourse(courseCode: String): Course - public suspend fun getCourses(paginated: Boolean = true): List<Course> + public suspend fun getCourses( + // TODO: Make this field instance-wide and depend on API version + paginated: Boolean = true, + ): List<Course> /** * Fetches a list of audit log entries from the platform for a given time period and list of event types. @@ -184,6 +182,8 @@ public interface Temerity { endTime: LocalDate, eventType: EventType, sortOrder: AuditLogSortOrder? = null, + // TODO: Make this field instance-wide and depend on API version + paginated: Boolean = true, ): List<AuditLogEntry> /** @@ -222,35 +222,18 @@ public interface Temerity { * @param groupId The ID of the group to fetch the storage usage for. */ public suspend fun getStorageAnalyticsReport(groupId: Long): ByteArray - - /** - * This interface represents the default PlatformClient builder pattern. - * It provides methods to set the service endpoint and token, and to build the PlatformClient. - */ - public interface Builder { - public fun debugEnabled(debugEnabled: Boolean) - - public fun serviceEndpoint(endpoint: String) {} - public fun serviceToken(token: String) {} - public fun build(): Temerity { - TODO() - } - - @Suppress("CanBePrivate") - public fun build(builder: Builder.() -> Unit): Temerity - } } -// Objects used by implementations of Temerity's PlatformClient +// Objects used by Temerity implementations, and possibly library consumers -private val AUDIT_LOG_REQUEST_DATE_FORMAT: DateTimeFormat<LocalDate> = LocalDate.Format { +public val AUDIT_LOG_REQUEST_DATE_FORMAT: DateTimeFormat<LocalDate> = LocalDate.Format { dayOfMonth() char('/') monthNumber() char('/') year() } -private val AUDIT_LOG_TIMESTAMP_FORMAT: DateTimeFormat<LocalDateTime> = LocalDateTime.Format { +public val AUDIT_LOG_TIMESTAMP_FORMAT: DateTimeFormat<LocalDateTime> = LocalDateTime.Format { year() char('-') monthNumber() @@ -263,7 +246,7 @@ private val AUDIT_LOG_TIMESTAMP_FORMAT: DateTimeFormat<LocalDateTime> = LocalDat char(':') second() } -private val SCHEDULED_SESSION_DATE_FORMAT: DateTimeFormat<LocalDate> = LocalDate.Format { +public val SCHEDULED_SESSION_DATE_FORMAT: DateTimeFormat<LocalDate> = LocalDate.Format { dayOfMonth() char('-') monthNumber() @@ -286,40 +269,6 @@ public enum class AuditLogSortOrder { } } -/** - * Serializer for LocalDateTime objects used in the [AuditLogEntry] class. - * This object is used to serialize and deserialize LocalDateTime objects to and from strings in the format used by the YuJa API. - */ -internal object LocalDateTimeSerializer : KSerializer<LocalDateTime> { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: LocalDateTime) { - encoder.encodeString(value.format(AUDIT_LOG_TIMESTAMP_FORMAT)) - } - - override fun deserialize(decoder: Decoder): LocalDateTime { - return LocalDateTime.parse(decoder.decodeString(), AUDIT_LOG_TIMESTAMP_FORMAT) - } -} - -/** - * Extension functions used by Temerity's PlatformClient - */ - -/** - * Formats a [LocalDate] object as a string in the format used by the YuJa API for audit log requests. - * @return A string representation of the date in the format "dd/MM/yyyy". - */ -internal fun LocalDate.applyAuditLogFormat(): String = - format(AUDIT_LOG_REQUEST_DATE_FORMAT) - -/** - * Formats a [LocalDate] object as a string in the format used by the YuJa API for sessions. - * @return A string representation of the date in the format "dd-MM-yyyy". - */ -internal fun LocalDate.applyScheduledSessionDateFormat(): String = - format(SCHEDULED_SESSION_DATE_FORMAT) - /** * Sorts a list of [AuditLogEntry] objects by their creation date. * @param sortOrder The order in which to sort the entries. diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Util.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Util.kt index 71a62b863d47861d0ce42188ba4adabe8d4b7b17..157f53f1a2dd182e0b04c6f8418679fc667dd3c0 100644 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Util.kt +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Util.kt @@ -25,12 +25,15 @@ import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalTime import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime +import kotlinx.datetime.todayIn import kotlin.coroutines.CoroutineContext -internal fun getDateTime() = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) +internal fun currentTz(): TimeZone = TimeZone.currentSystemDefault() -internal fun currentDate(): LocalDate = getDateTime().date +internal fun thisInstant() = Clock.System.now() -internal fun currentTime(): LocalTime = getDateTime().time +internal fun currentDate(): LocalDate = Clock.System.todayIn(currentTz()) + +internal fun currentTime(): LocalTime = thisInstant().toLocalDateTime(currentTz()).time internal fun createJobScope(coroutineContext: CoroutineContext = Dispatchers.Main): CoroutineScope = CoroutineScope(coroutineContext + Job()) diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/api/PlatformApi.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/api/PlatformApi.kt index ae008b259d58eb65584c8d393daac422b827186e..5fdf5b6ddaeface4c7c4a6d2f52c9ddbec21ed38 100644 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/api/PlatformApi.kt +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/api/PlatformApi.kt @@ -154,8 +154,8 @@ internal interface PlatformApi { suspend fun getAuditLogEntries( @Query("startTime") startTime: String?, @Query("endTime") endTime: String?, - @Query("eventType") eventType: String, @Query("offset") entryOffset: Int? = 0, + @Query("eventType") eventType: String, ): HttpStatement @Streaming diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/HttpClientFactory.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/HttpClientFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..ccb9dd70ca94ab87e60180ea4f9603e656bdb06e --- /dev/null +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/HttpClientFactory.kt @@ -0,0 +1,104 @@ +/* + * Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu) + * Copyright 2022-2024 The Regents of the University of California. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package edu.ucsc.its.temerity.core + +import edu.ucsc.its.temerity.TemClientConfig +import edu.ucsc.its.temerity.core.TemerityLibrary.koin +import io.ktor.client.HttpClient +import io.ktor.client.HttpClientConfig +import io.ktor.client.engine.HttpClientEngine +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.defaultRequest +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logging +import io.ktor.client.request.header +import io.ktor.http.URLProtocol +import co.touchlab.kermit.Logger as KermitLogger +import io.ktor.client.plugins.logging.Logger as KtorLogger +/** + * Factory providing web request clients for the Temerity client. + * Adapted from the tmdb-api project: https://github.com/MoviebaseApp/tmdb-kotlin/raw/refs/heads/main/tmdb-api/src/commonMain/kotlin/app/moviebase/tmdb/core/HttpClientFactory.kt + */ +internal object HttpClientFactory { + /** + * Adapted from https://github.com/joreilly/FantasyPremierLeague/ + * This function creates an HttpClient with the provided HttpClientEngine. + * The HttpClientEngine is injected by the Koin platformModule(), which as its name suggests provides an HttpClientEngine implementation that is platform-dependent. + * It configures the client to log all requests and responses if enableNetworkLogs is true. TODO: read this as a build setting + * It also sets a default request header with the provided authToken. + * @param httpClientEngine The HttpClientEngine to use for the HttpClient. + * @param timeoutDuration The duration in milliseconds to wait for a response before timing out. + * @param config A block specifying the following options: + * - Whether to log all requests and responses. + * - Service URL to make requests to. + * - Token to set as a default request header. + * @return The created HttpClient. + */ + internal fun buildHttpClient( + httpClientEngine: HttpClientEngine, + timeoutDuration: Long? = TemerityLibrary.DEFAULT_TIMEOUT, + config: TemClientConfig, + ): HttpClient { + val defaultConfig: HttpClientConfig<*>.() -> Unit = { + // Can't use install(ContentNegotiation){ json() } here because YuJa's API returns a 406 error if the Accept header is set to application/json + // This requires JSON transformations to be done via manually-called library functions + + // Pass the stored authToken as a header in every request + defaultRequest { + header("authToken", config.serviceToken) + url { + protocol = URLProtocol.HTTPS + } + } + + if (config.optDebugEnabled) { + if (config.httpClientLoggingBlock == null) { + install(Logging) { + val kermit = koin.get<KermitLogger>() + logger = object : KtorLogger { + override fun log(message: String) { + kermit.d(message) + } + } + level = LogLevel.ALL + sanitizeHeader { headerKey -> + headerKey == "authToken" + } + } + } else { + config.httpClientLoggingBlock?.let { + Logging(it) + } + } + } + + expectSuccess = config.expectSuccess + + if (config.useTimeout) { + install(HttpTimeout) { + connectTimeoutMillis = timeoutDuration + requestTimeoutMillis = timeoutDuration + socketTimeoutMillis = timeoutDuration + } + } + + config.httpClientConfigBlock?.invoke(this) + } + return config.httpClientBuilder?.invoke()?.config(defaultConfig) ?: HttpClient(httpClientEngine, defaultConfig) + } +} diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/JsonFactory.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/JsonFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..02f61f54d0c020a81d018f20a3fb4642f3659449 --- /dev/null +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/JsonFactory.kt @@ -0,0 +1,31 @@ +/* + * Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu) + * Copyright 2022-2024 The Regents of the University of California. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package edu.ucsc.its.temerity.core + +import kotlinx.serialization.json.Json + +/** + * Factory for creating a Ktor [Json] object. + * Adapted from the tmdb-api project: https://github.com/MoviebaseApp/tmdb-kotlin/raw/refs/heads/main/tmdb-api/src/commonMain/kotlin/app/moviebase/tmdb/core/JsonFactory.kt + */ +internal object JsonFactory { + + fun buildJson(): Json = Json { + coerceInputValues = true + } +} diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Library.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Library.kt index 760709e708e3ef954f1f05b5fd273a82dee24d08..0a709b19376256999d53fc9622e91fff23a46f5b 100644 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Library.kt +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Library.kt @@ -17,7 +17,7 @@ */ package edu.ucsc.its.temerity.core -import edu.ucsc.its.temerity.Temerity +import edu.ucsc.its.temerity.TemClientConfig import edu.ucsc.its.temerity.di.libModule import edu.ucsc.its.temerity.di.loggerModule import edu.ucsc.its.temerity.platformModule @@ -45,12 +45,10 @@ public object TemerityLibrary { /** * Create a Temerity instance using Kotlin-DSL. + * Adapted from the tmdb-api project: https://github.com/MoviebaseApp/tmdb-kotlin/raw/refs/heads/main/tmdb-api/src/commonMain/kotlin/app/moviebase/tmdb/Tmdb4.kt */ -public fun temerityInstance(builder: Temerity.Builder.() -> Unit): Temerity = - TemerityLibrary.koin.get<Temerity.Builder>().build(builder) - -/** - * Creates a PlatformClient.Builder instance using Kotlin-DSL. - */ -public fun temerityBuilder(builder: Temerity.Builder.() -> Unit): Temerity.Builder = - TemerityLibrary.koin.get<Temerity.Builder>().apply(builder) +@TemDsl +public fun Temerity(block: TemClientConfig.() -> Unit): Temerity { + val config = TemClientConfig().apply(block) + return Temerity(config) +} diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/TemDsl.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/TemDsl.kt new file mode 100644 index 0000000000000000000000000000000000000000..1e31cc55353942773def5c3cf3e8a5e6c18855dc --- /dev/null +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/TemDsl.kt @@ -0,0 +1,25 @@ +/* + * Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu) + * Copyright 2022-2024 The Regents of the University of California. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package edu.ucsc.its.temerity.core + +/** + * DSL marker for the Temerity library. + */ +@DslMarker +@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPEALIAS, AnnotationTarget.TYPE, AnnotationTarget.FUNCTION) +internal annotation class TemDsl diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/TemerityImpl.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt similarity index 77% rename from temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/TemerityImpl.kt rename to temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt index dda90584d67c4771fae74fe10bb7a7f7fcfb358a..eefc882f2e7ac0990ae654d8eb65b52b178d0637 100644 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/TemerityImpl.kt +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt @@ -24,12 +24,14 @@ import com.skydoves.sandwich.ktor.executeApiResponse import com.skydoves.sandwich.ktor.statusCode import com.skydoves.sandwich.onSuccess import edu.ucsc.its.temerity.AuditLogSortOrder -import edu.ucsc.its.temerity.Temerity +import edu.ucsc.its.temerity.TemClientConfig +import edu.ucsc.its.temerity.TemerityApi import edu.ucsc.its.temerity.api.PlatformApi -import edu.ucsc.its.temerity.applyAuditLogFormat -import edu.ucsc.its.temerity.applyScheduledSessionDateFormat +import edu.ucsc.its.temerity.core.JsonFactory.buildJson +import edu.ucsc.its.temerity.core.TemerityLibrary.koin import edu.ucsc.its.temerity.createJobScope -import edu.ucsc.its.temerity.di.TemerityKoinComponent +import edu.ucsc.its.temerity.extensions.applyAuditLogFormat +import edu.ucsc.its.temerity.extensions.applyScheduledSessionDateFormat import edu.ucsc.its.temerity.model.AuditLogEntry import edu.ucsc.its.temerity.model.Course import edu.ucsc.its.temerity.model.Device @@ -44,6 +46,7 @@ import edu.ucsc.its.temerity.model.UserRecordingSession import edu.ucsc.its.temerity.model.UserUpdate import edu.ucsc.its.temerity.sortByCreationDate import io.ktor.client.statement.HttpResponse +import io.ktor.http.parametersOf import io.ktor.util.cio.toByteArray import io.ktor.utils.io.ByteReadChannel import kotlinx.coroutines.Dispatchers @@ -60,84 +63,40 @@ import org.koin.core.context.GlobalContext.get import org.koin.core.parameter.parametersOf import co.touchlab.kermit.Logger as KermitLogger -// Based on sample code available from https://github.com/santimattius/kmp-networking/blob/main/README.md -// Publicly instantiable only via factory method -public class TemerityImpl private constructor( - private val debugEnabled: Boolean, - private var platformApi: PlatformApi, -) : Temerity { - - /** - * Exception class used to break out of a loop when reading paginated API responses. - */ - private class BreakException(val page: Int) : RuntimeException() { - fun lastPage(): Int = page - } - - private val json = Json { - coerceInputValues = true - } +/* + * Adapted from https://github.com/santimattius/kmp-networking/blob/main/README.md + */ +public class Temerity internal constructor( + private val config: TemClientConfig, +) : TemerityApi { /** - * Builder class for Temerity + * Constructor for Temerity */ - internal class Builder : TemerityKoinComponent(), Temerity.Builder { + internal constructor(temApiToken: String) : this(TemClientConfig.withToken(temApiToken)) - private var optDebugEnabled: Boolean = false + private val json: Json = buildJson() + private val platformApi: PlatformApi - /** - * Specifies whether debug logging should be enabled - * @param debugEnabled - * Enable this for debugging purposes only - */ - override fun debugEnabled(debugEnabled: Boolean) { - optDebugEnabled = debugEnabled + init { + check(!config.serviceToken.isNullOrBlank()) { + "Service API auth token must be provided. Set it using TemClientConfig.serviceToken(token)" } - private var serviceEndpoint: String = "" - - /** - * Specifies URL of platform instance to make API calls to - * @param endpoint The URL of the platform instance to make API calls to - */ - override fun serviceEndpoint(endpoint: String) { - serviceEndpoint = endpoint + check(!config.serviceUrl.isNullOrBlank()) { + "Service url must be provided. Set it using TemClientConfig.serviceUrl(url)" } - private var serviceToken: String = "" - - /** - * Specifies the authorization token to use for API calls - * @param token The authorization token to use for API calls - */ - override fun serviceToken(token: String) { - serviceToken = token + platformApi = koin.get<PlatformApi> { + parametersOf(config) } + } - /** - * Apply changes to builder and get the PlatformClient instance without needing to call [build] afterward. - */ - - @Suppress("CanBePrivate") - override fun build(builder: Temerity.Builder.() -> Unit): Temerity = this.apply(builder).build() - - /** - * Creates a Temerity instance with specified service endpoint and token. - */ - override fun build(): Temerity { - require(serviceEndpoint.isNotBlank()) { - "Must pass platform service endpoint as parameter" - } - require(serviceToken.isNotBlank()) { - "Must pass platform authorization token as parameter" - } - val koin = getKoin() - val platformApi: PlatformApi = koin.get { parametersOf(serviceEndpoint, optDebugEnabled, serviceToken) } - return TemerityImpl( - debugEnabled = optDebugEnabled, - platformApi, - ) - } + /** + * Exception class used to break out of a loop when reading paginated API responses. + */ + internal class BreakException(val page: Int = -1) : RuntimeException() { + fun lastPage(): Int = page } // Inlining advantageous here since copied parameter (ApiResponse<String>) is small and code fragment is used constantly (to validate every platform API request) @@ -149,14 +108,13 @@ public class TemerityImpl private constructor( // If the API returns a 204 No Content status code, we should throw a BreakException to stop the page iteration loop // As of Cashew release, this is only used in getGroups() to stop the loop when no more groups are returned // May be used in the future for other paginated endpoints (such as getAuditLogEntries) - StatusCode.NoContent -> { - throw BreakException(page - 1) - } + StatusCode.NoContent -> throw BreakException(page - 1) else -> Unit } } val apiResponsePayload = response.getOrThrow() + if (apiResponsePayload == "[]") throw BreakException(page - 1) // Handle deserialization exceptions. Queue up decode: val deserializedJson = runCatching { json.decodeFromString<T>(apiResponsePayload) @@ -166,6 +124,7 @@ public class TemerityImpl private constructor( when (exception) { is SerializationException -> { // TODO: Implement platform API version checking + get().get<KermitLogger>().d { "Returned JSON object: ${exception.message}" } error("Encountered error decoding response from platform API. You likely need to choose a Temerity release that supports the API version implemented by your instance. \nPlatform API response data : $apiResponsePayload") } @@ -231,7 +190,7 @@ public class TemerityImpl private constructor( } catch (e: Exception) { when (e) { is BreakException -> { - if (debugEnabled) { + if (config.optDebugEnabled) { get().get<KermitLogger>().d("Caught BreakException() from decodeResponseCatching() notifying we're done reading groups from API: ${e.page} pages read") } return returnedGroups @@ -267,7 +226,7 @@ public class TemerityImpl private constructor( } catch (e: Exception) { when (e) { is BreakException -> { - if (debugEnabled) { + if (config.optDebugEnabled) { get().get<KermitLogger>().d("Caught BreakException() from decodeResponseCatching() notifying we're done reading groups from API: $e") } return returnedCourses @@ -320,25 +279,56 @@ public class TemerityImpl private constructor( endTime: LocalDate, eventType: EventType, sortOrder: AuditLogSortOrder?, + paginated: Boolean, ): List<AuditLogEntry> { - val returnedEntries = ArrayList<AuditLogEntry>() - var windowStart = endTime.minus(1, DateTimeUnit.Companion.MONTH) - var windowEnd = endTime - while (windowStart < windowEnd) { - val endpointRequest = platformApi.getAuditLogEntries( - startTime = windowStart.applyAuditLogFormat(), - endTime = windowEnd.applyAuditLogFormat(), - eventType = eventType.value, - ) - val logEventEntries: List<AuditLogEntry> = decodeResponseCatching(endpointRequest.executeApiResponse<String>()) - returnedEntries.addAll(logEventEntries) - windowEnd = windowStart - val backStep = windowStart.minus(1, DateTimeUnit.Companion.MONTH) - windowStart = if (backStep < startTime) startTime else backStep + require(startTime <= endTime) { + "End date must be same or later than start date" + } + when (paginated) { + true -> { + val returnedEntries = ArrayList<AuditLogEntry>() + var windowStart = if (endTime.minus(1, DateTimeUnit.Companion.MONTH) < startTime) { + startTime + } else { + endTime.minus(1, DateTimeUnit.Companion.MONTH) + } + var windowEnd = endTime + while (windowStart < windowEnd) { + try { + for (x in (0..Int.MAX_VALUE)) { + val endpointRequest = platformApi.getAuditLogEntries( + startTime = windowStart.applyAuditLogFormat(), + endTime = windowEnd.applyAuditLogFormat(), + entryOffset = x * 75, + eventType = eventType.value, + ) + val logEventEntries: List<AuditLogEntry> = decodeResponseCatching(endpointRequest.executeApiResponse<String>(), page = x) + returnedEntries.addAll(logEventEntries) + } + } catch (e: Exception) { + when (e) { + is BreakException -> { + if (config.optDebugEnabled) { + get().get<KermitLogger>().d("Caught BreakException() from decodeResponseCatching() notifying we're done reading audit log entries from API for specific window: ${e.page} pages read") + } + break + } + + else -> throw e + } + } + windowEnd = windowStart + val backStep = windowStart.minus(1, DateTimeUnit.Companion.MONTH) + windowStart = if (backStep < startTime) startTime else backStep + } + // Apply passed sortOrder, or default to NEW_FIRST TODO: Read from user settings + returnedEntries.applyOrDefault(sortOrder ?: AuditLogSortOrder.NEW_FIRST) + return returnedEntries + } + false -> { + TODO("Implement non-paginated audit log entry retrieval") + } } - // Apply passed sortOrder, or default to NEW_FIRST TODO: Read from user settings - returnedEntries.applyOrDefault(sortOrder ?: AuditLogSortOrder.NEW_FIRST) - return returnedEntries } private fun ArrayList<AuditLogEntry>.applyOrDefault(sortOrder: AuditLogSortOrder?): ArrayList<AuditLogEntry> { diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/Koin.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/Koin.kt index 8a75898605643259cab57fb0b938847dbbed2c33..92400c898f7ac651fe51ceb7556436fece7e64e2 100644 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/Koin.kt +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/Koin.kt @@ -23,20 +23,12 @@ import co.touchlab.kermit.loggerConfigInit import co.touchlab.kermit.platformLogWriter import com.skydoves.sandwich.ktorfit.ApiResponseConverterFactory import de.jensklingenberg.ktorfit.ktorfit -import edu.ucsc.its.temerity.Temerity +import edu.ucsc.its.temerity.TemClientConfig import edu.ucsc.its.temerity.api.PlatformApi import edu.ucsc.its.temerity.api.createPlatformApi -import edu.ucsc.its.temerity.core.TemerityImpl -import edu.ucsc.its.temerity.core.TemerityLibrary +import edu.ucsc.its.temerity.core.HttpClientFactory.buildHttpClient import edu.ucsc.its.temerity.core.TemerityLibrary.koin import io.ktor.client.HttpClient -import io.ktor.client.engine.HttpClientEngine -import io.ktor.client.plugins.HttpTimeout -import io.ktor.client.plugins.defaultRequest -import io.ktor.client.plugins.logging.LogLevel -import io.ktor.client.plugins.logging.Logger -import io.ktor.client.plugins.logging.Logging -import io.ktor.client.request.header import org.koin.core.Koin import org.koin.core.component.KoinComponent import org.koin.core.parameter.parametersOf @@ -51,38 +43,27 @@ internal fun loggerModule() = module { /** * This function provides the Koin module for the Temerity library. - * It includes the platform module and provides factories for [HttpClient], [PlatformApi], [Temerity.Builder] and [Temerity]. - * Factories for the last two default to using [TemerityImpl] as the implementation. + * It includes the platform module and provides factories for [HttpClient] and [PlatformApi]. */ internal val libModule = module { - factory { (enableNetworkLogs: Boolean, authToken: String) -> - createHttpClient( + factory { (config: TemClientConfig) -> + buildHttpClient( httpClientEngine = get(), - enableNetworkLogs = enableNetworkLogs, - authToken = authToken, + config = config, ) } - factory { (serviceEndpoint: String, enableNetworkLogs: Boolean, authToken: String) -> - val client: HttpClient = get { parametersOf(enableNetworkLogs, authToken) } + factory { (config: TemClientConfig) -> + val client: HttpClient = get { parametersOf(config) } val ktorfit = ktorfit { - baseUrl(serviceEndpoint) + config.serviceUrl?.let { baseUrl(it) } httpClient(client) converterFactories(ApiResponseConverterFactory.create()) } ktorfit.createPlatformApi() } - factory<Temerity.Builder> { - TemerityImpl.Builder() - } - factory<Temerity> { (serviceEndpoint: String, authToken: String) -> - TemerityImpl.Builder().apply { - serviceEndpoint(serviceEndpoint) - serviceToken(authToken) - }.build() - } } -private fun createLogger(tag: String?): co.touchlab.kermit.Logger = KermitLogger( +private fun createLogger(tag: String?): KermitLogger = KermitLogger( config = loggerConfigInit( platformLogWriter(NoTagFormatter), minSeverity = Severity.Debug, @@ -90,47 +71,6 @@ private fun createLogger(tag: String?): co.touchlab.kermit.Logger = KermitLogger tag = tag ?: "TemerityLib", ) -/** - * Adapted from https://github.com/joreilly/FantasyPremierLeague/ - * This function creates an HttpClient with the provided HttpClientEngine. - * The HttpClientEngine is injected by the Koin platformModule(), which as its name suggests provides an HttpClientEngine implementation that is platform-dependent. - * It configures the client to log all requests and responses if enableNetworkLogs is true. TODO: read this as a build setting - * It also sets a default request header with the provided authToken. - * @param httpClientEngine The HttpClientEngine to use for the HttpClient. - * @param timeoutDuration The duration in milliseconds to wait for a response before timing out. - * @param enableNetworkLogs Whether to log all requests and responses. - * @param authToken The authToken to set as a default request header. - * @return The created HttpClient. - */ -private fun createHttpClient(httpClientEngine: HttpClientEngine, timeoutDuration: Long? = TemerityLibrary.DEFAULT_TIMEOUT, enableNetworkLogs: Boolean, authToken: String) = HttpClient(httpClientEngine) { - // Can't use install(ContentNegotiation){ json() } here because YuJa's API returns a 406 error if the Accept header is set to application/json - // This requires JSON transformations to be done via manually-called library functions - if (enableNetworkLogs) { - install(Logging) { - val kermit = koin.get<KermitLogger>() - logger = object : Logger { - override fun log(message: String) { - kermit.d(message) - } - } - level = LogLevel.ALL - sanitizeHeader { headerKey -> - headerKey == "authToken" - } - } - } - - install(HttpTimeout) { - connectTimeoutMillis = timeoutDuration - requestTimeoutMillis = timeoutDuration - socketTimeoutMillis = timeoutDuration - } - // Pass the stored authToken as a header in every request - defaultRequest { - header("authToken", authToken) - } -} - internal abstract class TemerityKoinComponent : KoinComponent { override fun getKoin(): Koin = koin } diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/DateTimeExtensions.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/DateTimeExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..bfb164a31af64f1e313fb4c280d0e6161b6ff0ba --- /dev/null +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/DateTimeExtensions.kt @@ -0,0 +1,37 @@ +/* + * Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu) + * Copyright 2022-2024 The Regents of the University of California. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package edu.ucsc.its.temerity.extensions + +import edu.ucsc.its.temerity.AUDIT_LOG_REQUEST_DATE_FORMAT +import edu.ucsc.its.temerity.SCHEDULED_SESSION_DATE_FORMAT +import kotlinx.datetime.LocalDate +import kotlinx.datetime.format + +/** + * Formats a [LocalDate] object as a string in the format used by the YuJa API for audit log requests. + * @return A string representation of the date in the format "dd/MM/yyyy". + */ +internal fun LocalDate.applyAuditLogFormat(): String = + format(AUDIT_LOG_REQUEST_DATE_FORMAT) + +/** + * Formats a [LocalDate] object as a string in the format used by the YuJa API for sessions. + * @return A string representation of the date in the format "dd-MM-yyyy". + */ +internal fun LocalDate.applyScheduledSessionDateFormat(): String = + format(SCHEDULED_SESSION_DATE_FORMAT) diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/LocalDateTimeSerializer.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/LocalDateTimeSerializer.kt new file mode 100644 index 0000000000000000000000000000000000000000..c4892dd3e20d5b832b5dd03686092d8895e9484e --- /dev/null +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/LocalDateTimeSerializer.kt @@ -0,0 +1,45 @@ +/* + * Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu) + * Copyright 2022-2024 The Regents of the University of California. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package edu.ucsc.its.temerity.extensions + +import edu.ucsc.its.temerity.AUDIT_LOG_TIMESTAMP_FORMAT +import edu.ucsc.its.temerity.model.AuditLogEntry +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.format +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +/** + * Serializer for LocalDateTime objects used in the [AuditLogEntry] class. + * This object is used to serialize and deserialize LocalDateTime objects to and from strings in the format used by the YuJa API. + */ +internal object LocalDateTimeSerializer : KSerializer<LocalDateTime> { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: LocalDateTime) { + encoder.encodeString(value.format(AUDIT_LOG_TIMESTAMP_FORMAT)) + } + + override fun deserialize(decoder: Decoder): LocalDateTime { + return LocalDateTime.parse(decoder.decodeString(), AUDIT_LOG_TIMESTAMP_FORMAT) + } +} diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/model/AuditLogs.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/model/AuditLogs.kt index dc4d6eea6c8cf32b135aceb9865694639edf3f02..538b45508ef65fd405ae3a10fbc0583fec87b7a5 100644 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/model/AuditLogs.kt +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/model/AuditLogs.kt @@ -17,7 +17,7 @@ */ package edu.ucsc.its.temerity.model -import edu.ucsc.its.temerity.LocalDateTimeSerializer +import edu.ucsc.its.temerity.extensions.LocalDateTimeSerializer import kotlinx.datetime.LocalDateTime import kotlinx.serialization.Serializable diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevDeviceApiTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevDeviceApiTests.kt index eda775670db0f1c0711f8593335c11fbbe71c249..e00b4dd812aded46bf37e71867ea2a58e801e81a 100644 --- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevDeviceApiTests.kt +++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevDeviceApiTests.kt @@ -17,8 +17,7 @@ */ package edu.ucsc.its.temerity.test -import edu.ucsc.its.temerity.Temerity -import edu.ucsc.its.temerity.core.temerityInstance +import edu.ucsc.its.temerity.core.Temerity import io.kotest.core.spec.style.FunSpec import kotlinx.coroutines.runBlocking import org.dotenv.vault.dotenvVault @@ -26,19 +25,19 @@ import org.dotenv.vault.dotenvVault class DevDeviceApiTests : FunSpec({ val dotenv = dotenvVault() - lateinit var classUnderTest: Temerity + lateinit var temerityTest: Temerity beforeTest { - classUnderTest = temerityInstance { - debugEnabled(true) - serviceEndpoint(dotenv["YUJADEV_API_URL"]) - serviceToken(dotenv["YUJADEV_TOKEN"]) + temerityTest = Temerity { + serviceUrl = dotenv["YUJADEV_API_URL"] + serviceToken = dotenv["YUJADEV_TOKEN"] + optDebugEnabled = true } } test("4.2.1 - PlatformClient can fetch a list of devices") { runBlocking { - val returnedDevices = classUnderTest.getDevices() + val returnedDevices = temerityTest.getDevices() println("Returned devices:") returnedDevices.forEach { println(it) } assert(returnedDevices.isNotEmpty()) diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevGroupApiTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevGroupApiTests.kt index 6658a22727339d7c920deb811b98740949aef378..0ee6f67c2229ce5f64d3601d723d9978b8964e4e 100644 --- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevGroupApiTests.kt +++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevGroupApiTests.kt @@ -17,8 +17,7 @@ */ package edu.ucsc.its.temerity.test -import edu.ucsc.its.temerity.Temerity -import edu.ucsc.its.temerity.core.temerityInstance +import edu.ucsc.its.temerity.core.Temerity import io.kotest.core.spec.style.FunSpec import kotlinx.coroutines.runBlocking import org.dotenv.vault.dotenvVault @@ -27,19 +26,19 @@ class DevGroupApiTests : FunSpec({ val dotenv = dotenvVault() - lateinit var classUnderTest: Temerity + lateinit var testTemerity: Temerity beforeTest { - classUnderTest = temerityInstance { - debugEnabled(true) - serviceEndpoint(dotenv["YUJADEV_API_URL"]) - serviceToken(dotenv["YUJADEV_TOKEN"]) + testTemerity = Temerity { + serviceUrl = dotenv["YUJADEV_API_URL"] + serviceToken = dotenv["YUJADEV_TOKEN"] + optDebugEnabled = true } } test("2.2.1 - PlatformClient can fetch a list of groups") { runBlocking { - val returnedGroups = classUnderTest.getGroups() + val returnedGroups = testTemerity.getGroups() println("Returned groups:") returnedGroups.forEach { println(it) } assert(returnedGroups.isNotEmpty()) diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUserApiTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUserApiTests.kt index b596f30dad95fab7125b30306b5eea1150e5fee7..40bd76b2787de078b7753e0404d325aa1fbcd2c7 100644 --- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUserApiTests.kt +++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUserApiTests.kt @@ -20,8 +20,7 @@ package edu.ucsc.its.temerity.test import co.touchlab.kermit.Logger import com.skydoves.sandwich.StatusCode import com.skydoves.sandwich.ktor.getStatusCode -import edu.ucsc.its.temerity.Temerity -import edu.ucsc.its.temerity.core.temerityInstance +import edu.ucsc.its.temerity.core.Temerity import edu.ucsc.its.temerity.model.NewUser import edu.ucsc.its.temerity.model.UserUpdate import io.kotest.core.spec.style.FunSpec @@ -34,19 +33,18 @@ class DevUserApiTests : FunSpec({ val dotenv = dotenvVault() - lateinit var classUnderTest: Temerity - + lateinit var testTemerity: Temerity beforeTest { - classUnderTest = temerityInstance { - debugEnabled(true) - serviceEndpoint(dotenv["YUJADEV_API_URL"]) - serviceToken(dotenv["YUJADEV_TOKEN"]) + testTemerity = Temerity { + serviceUrl = dotenv["YUJADEV_API_URL"] + serviceToken = dotenv["YUJADEV_TOKEN"] + optDebugEnabled = true } } test("1.2.1 - PlatformClient can fetch a list of users") { runBlocking { - val returnedUsers = classUnderTest.getUsers() + val returnedUsers = testTemerity.getUsers() println("Returned users:") returnedUsers.forEach { Logger.d(it.toString()) } assert(returnedUsers.isNotEmpty()) @@ -55,10 +53,10 @@ class DevUserApiTests : FunSpec({ test("1.2.2 - PlatformClient can fetch a single user") { runBlocking { - val users = classUnderTest.getUsers() + val users = testTemerity.getUsers() val userRef = users.find { user -> dotenv["YUJADEV_TEST_USER"] == user.loginId } requireNotNull(userRef) - val user = classUnderTest.getUser(userRef.userId) + val user = testTemerity.getUser(userRef.userId) Logger.d("Returned user with details:\n$user") assert(user.toString().isNotEmpty()) } @@ -74,14 +72,14 @@ class DevUserApiTests : FunSpec({ loginPassword = "asdadfadf", userType = "Student", ) - val response = classUnderTest.createUser(newUser = newUserRef) + val response = testTemerity.createUser(newUser = newUserRef) Logger.d("response: $response") response.getStatusCode().shouldBe(StatusCode.OK) } } test("1.2.4. - PlatformClient can update user details") { - val testUser = classUnderTest.getUsers().first { it.loginId == dotenv["YUJADEV_TEST_USER_ID"] } + val testUser = testTemerity.getUsers().first { it.loginId == dotenv["YUJADEV_TEST_USER_ID"] } Logger.d("Previous platform value for test user last name: ${testUser.lastName}") runBlocking { val updateObject = UserUpdate() @@ -89,14 +87,14 @@ class DevUserApiTests : FunSpec({ val newLastName = testUser.lastName.toLong().inc().toString() updateObject.lastName = newLastName Logger.d("Attempting to update user last name with value: ${testUser.lastName}") - val response = classUnderTest.updateUser(testUser.userId, updateObject) + val response = testTemerity.updateUser(testUser.userId, updateObject) Logger.d("Received response: $response") } } test("1.2.5 - PlatformClient can delete a user") { runBlocking { - val response = runBlocking { classUnderTest.deleteUser(dotenv["YUJADEV_TEST_USER_TO_DELETE"].toLong()) } + val response = runBlocking { testTemerity.deleteUser(dotenv["YUJADEV_TEST_USER_TO_DELETE"].toLong()) } Logger.d("response: $response") response.status.shouldBe(HttpStatusCode.OK) } @@ -104,10 +102,10 @@ class DevUserApiTests : FunSpec({ test("1.2.6 - PlatformClient can fetch a list of groups that a user is enrolled in") { runBlocking { - val users = classUnderTest.getUsers() + val users = testTemerity.getUsers() val userRef = users.find { user -> dotenv["YUJADEV_TEST_USER"] == user.loginId } requireNotNull(userRef) - val userGroups = classUnderTest.getUserGroups(userRef.userId) + val userGroups = testTemerity.getUserGroups(userRef.userId) Logger.d("Returned groups for the user ${userRef.loginId}:") userGroups.forEach { Logger.d(it.toString()) } assert(userGroups.isNotEmpty()) @@ -116,10 +114,10 @@ class DevUserApiTests : FunSpec({ test("1.2.7 - PlatformClient can fetch a list of groups that a user is content owner of") { runBlocking { - val users = classUnderTest.getUsers() + val users = testTemerity.getUsers() val userRef = users.find { user -> dotenv["YUJADEV_TEST_USER"] == user.loginId } requireNotNull(userRef) - val userGroups = classUnderTest.getUserGroupsOwned(userRef.userId) + val userGroups = testTemerity.getUserGroupsOwned(userRef.userId) Logger.d("Returned owned groups for the user ${userRef.loginId}:") userGroups.forEach { Logger.d(it.toString()) } assert(userGroups.isNotEmpty()) @@ -136,7 +134,7 @@ class DevUserApiTests : FunSpec({ test("Extra - PlatformClient can retrieve a list of candidate test students to be deleted from the platform") { runBlocking { - val users = classUnderTest.getUsers() + val users = testTemerity.getUsers() val testStudents = users.filter { it.loginId.contains("tstudent") } testStudents.forEach { Logger.d("Test student found: $it\n") diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdReportTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdReportTests.kt index 4859148f7c0125d0b20fb6825114b712a41d1389..bd3fade34ca3d098653796807c5099f9c0919adb 100644 --- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdReportTests.kt +++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdReportTests.kt @@ -17,8 +17,7 @@ */ package edu.ucsc.its.temerity.test import edu.ucsc.its.temerity.AuditLogSortOrder.NEW_FIRST -import edu.ucsc.its.temerity.Temerity -import edu.ucsc.its.temerity.core.temerityInstance +import edu.ucsc.its.temerity.core.Temerity import edu.ucsc.its.temerity.currentDate import edu.ucsc.its.temerity.di.loggerModule import edu.ucsc.its.temerity.model.EventType.AUTOMATED_SESSION_FAILED_TO_START @@ -68,10 +67,10 @@ class ProdReportTests : FunSpec(), KoinTest { val today = Clock.System.todayIn(TimeZone.currentSystemDefault()) beforeTest { - testTemerity = temerityInstance { - debugEnabled(true) - serviceEndpoint(dotenv["YUJAPROD_API_URL"]) - serviceToken(dotenv["YUJAPROD_TOKEN"]) + testTemerity = Temerity { + serviceUrl = dotenv["YUJAPROD_API_URL"] + serviceToken = dotenv["YUJAPROD_TOKEN"] + optDebugEnabled = true } } @@ -218,11 +217,11 @@ class ProdReportTests : FunSpec(), KoinTest { } } - test("Get all audit log entries of type \"New Login\" from the past day") { + test("Get all audit log entries of type \"New Login\" from the past 4 days") { runBlocking { val testEventType = NEW_LOG_IN val currentDate = Clock.System.todayIn(TimeZone.currentSystemDefault()) - val pastDate = currentDate.minus(1, DateTimeUnit.DAY) + val pastDate = currentDate.minus(4, DateTimeUnit.DAY) val returnedAuditLogEntries = testTemerity.getAuditLogEntries( startTime = pastDate, @@ -230,7 +229,7 @@ class ProdReportTests : FunSpec(), KoinTest { eventType = testEventType, sortOrder = NEW_FIRST, ) - returnedAuditLogEntries.forEach { println(it) } + returnedAuditLogEntries.forEach { kermit.d(it.toString()) } val df = returnedAuditLogEntries.toDataFrame() df.writeCSV("${testEventType.name}-Event-Report-${Uuid.random()}-$pastDate-$currentDate-generated-${currentDate()}.csv") diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/TemerityDevTest.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/TemerityDevTest.kt index 7990e05f862ae7e87fe034934faec61498018de0..4d28a1363340bd1222ec662da237847ab2700384 100644 --- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/TemerityDevTest.kt +++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/TemerityDevTest.kt @@ -21,8 +21,7 @@ import co.touchlab.kermit.Logger import com.skydoves.sandwich.StatusCode import com.skydoves.sandwich.ktor.getStatusCode import edu.ucsc.its.temerity.AuditLogSortOrder.NEW_FIRST -import edu.ucsc.its.temerity.Temerity -import edu.ucsc.its.temerity.core.temerityInstance +import edu.ucsc.its.temerity.core.Temerity import edu.ucsc.its.temerity.currentDate import edu.ucsc.its.temerity.model.EventType.NEW_LOG_IN import edu.ucsc.its.temerity.model.NewUser @@ -49,13 +48,13 @@ class TemerityDevTest : FunSpec({ val dotenv = dotenvVault() - lateinit var classUnderTest: Temerity + lateinit var testTemerity: Temerity beforeTest { - classUnderTest = temerityInstance { - debugEnabled(true) - serviceEndpoint(dotenv["YUJADEV_API_URL"]) - serviceToken(dotenv["YUJADEV_TOKEN"]) + testTemerity = Temerity { + serviceUrl = dotenv["YUJADEV_API_URL"] + serviceToken = dotenv["YUJADEV_TOKEN"] + optDebugEnabled = true } } @@ -64,7 +63,7 @@ class TemerityDevTest : FunSpec({ val currentDate = Clock.System.todayIn(TimeZone.currentSystemDefault()) val pastDate = currentDate.minus(1, DateTimeUnit.MONTH) - val returnedAuditLogEntries = classUnderTest.getAuditLogEntries( + val returnedAuditLogEntries = testTemerity.getAuditLogEntries( startTime = pastDate, endTime = currentDate, eventType = NEW_LOG_IN, @@ -79,7 +78,7 @@ class TemerityDevTest : FunSpec({ runBlocking { val currentDate = Clock.System.todayIn(TimeZone.currentSystemDefault()) val pastDate = currentDate.minus(1, DateTimeUnit.MONTH) - val logs = classUnderTest.getAuditLogEntries(pastDate, currentDate, sortOrder = NEW_FIRST) + val logs = testTemerity.getAuditLogEntries(pastDate, currentDate, sortOrder = NEW_FIRST) println("Returned events from the past month:") logs.forEach { println(it) } assert(logs.isNotEmpty()) @@ -89,7 +88,7 @@ class TemerityDevTest : FunSpec({ runBlocking { val currentDate = Clock.System.todayIn(TimeZone.currentSystemDefault()) val pastDate = currentDate.minus(1, DateTimeUnit.DAY) - val logs = classUnderTest.getAuditLogEntries(pastDate, currentDate, sortOrder = NEW_FIRST) + val logs = testTemerity.getAuditLogEntries(pastDate, currentDate, sortOrder = NEW_FIRST) Logger.d("Returned events from the past day:") logs.forEach { Logger.d(it.toString()) } assert(logs.isNotEmpty()) @@ -99,7 +98,7 @@ class TemerityDevTest : FunSpec({ runBlocking { val pastDate = Clock.System.todayIn(TimeZone.currentSystemDefault()).minus(1, DateTimeUnit.DAY) val futureDate = pastDate.plus(1, DateTimeUnit.MONTH) - val sessions = classUnderTest.getUserSessions("dotenv[YUJADEV_TEST_USER]", pastDate, futureDate) + val sessions = testTemerity.getUserSessions("dotenv[YUJADEV_TEST_USER]", pastDate, futureDate) Logger.d("Returned sessions for the next week:") sessions.forEach { Logger.d(it.toString()) } assert(sessions.isNotEmpty()) @@ -116,7 +115,7 @@ class TemerityDevTest : FunSpec({ loginPassword = "asdadfadf", userType = "Student", ) - val response = classUnderTest.createUser(newUser = newUserRef) + val response = testTemerity.createUser(newUser = newUserRef) Logger.d("response: $response") response.getStatusCode().shouldBe(StatusCode.OK) } @@ -127,8 +126,8 @@ class TemerityDevTest : FunSpec({ val pastDate = Clock.System.todayIn(TimeZone.currentSystemDefault()).minus(1, DateTimeUnit.DAY) val futureDate = pastDate.plus(1, DateTimeUnit.MONTH) val testDeviceId = dotenv["YUJADEV_TEST_DEVICE_ID"].toLongOrNull() - val sessions = classUnderTest.getDeviceSchedule(testDeviceId!!, pastDate, futureDate) - val deviceName = classUnderTest.getDevice(testDeviceId).stationName + val sessions = testTemerity.getDeviceSchedule(testDeviceId!!, pastDate, futureDate) + val deviceName = testTemerity.getDevice(testDeviceId).stationName println("Returned sessions for the device code 6489 with name $deviceName:") sessions.forEach { println(it) } assert(sessions.isNotEmpty()) @@ -148,7 +147,7 @@ class TemerityDevTest : FunSpec({ val currentDate = Clock.System.todayIn(TimeZone.currentSystemDefault()) val pastDate = currentDate.minus(1, DateTimeUnit.DAY) - val returnedAuditLogEntries = classUnderTest.getAuditLogEntries( + val returnedAuditLogEntries = testTemerity.getAuditLogEntries( startTime = pastDate, endTime = currentDate, eventType = testEventType, @@ -166,7 +165,7 @@ class TemerityDevTest : FunSpec({ test("PlatformClient can get the analytics storage report for a specific group") { val testGroup = dotenv["YUJADEV_TEST_GROUP"].toLong() runBlocking { - val reportBytes = classUnderTest.getStorageAnalyticsReport(testGroup) + val reportBytes = testTemerity.getStorageAnalyticsReport(testGroup) val outputFile = File("storage-report-group-$testGroup-${currentDate()}-${Uuid.random()}.csv") outputFile.outputStream().use { fileOut -> fileOut.write(reportBytes) @@ -186,7 +185,7 @@ class TemerityDevTest : FunSpec({ loginPassword = "password", userType = "Student", ) - val response = classUnderTest.createUser(newUser = newUser) + val response = testTemerity.createUser(newUser = newUser) Logger.d(response.toString()) } }