diff --git a/Architecture.md b/Architecture.md index 7979e95..90619b6 100644 --- a/Architecture.md +++ b/Architecture.md @@ -65,6 +65,7 @@ Public extraction and proxy routes: - `/search`, `/suggestions`, `/trending` - `/comments`, `/bullet-comments`, `/channel` - `/proxy`, `/proxy/storyboard`, `/proxy/nicovideo` +- `/health`, `/instance` (public instance metadata from `admin_settings` + Gradle version; `/instance` returns name, tagline, logoUrl, bannerUrl, version, apiVersion, registrationAllowed, guestAllowed, supportedServices, and minClientVersion.android with `Cache-Control: public, max-age=300`) Protected user routes: diff --git a/build.gradle.kts b/build.gradle.kts index f4e4f99..78ad288 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -50,6 +50,24 @@ dependencies { testImplementation("org.testcontainers:testcontainers-junit-jupiter:2.0.5") } +val buildInfoVersion = version.toString().trim().takeUnless { it.isBlank() || it == "unspecified" } ?: "0.0.0-dev" +val generatedBuildInfoDir = layout.buildDirectory.dir("generated/sources/buildInfo/main") +val generateBuildInfo = tasks.register("generateBuildInfo") { + inputs.property("version", buildInfoVersion) + outputs.dir(generatedBuildInfoDir) + doLast { + val output = generatedBuildInfoDir.get().file("dev/typetype/server/BuildInfo.kt").asFile + output.parentFile.mkdirs() + output.writeText(""" + package dev.typetype.server + + object BuildInfo { + const val VERSION: String = "${buildInfoVersion.replace("\\", "\\\\").replace("\"", "\\\"")}" + } + """.trimIndent()) + } +} + tasks.test { useJUnitPlatform { excludeTags("network") @@ -89,8 +107,11 @@ tasks.check { kotlin { jvmToolchain(21) + sourceSets.named("main") { kotlin.srcDir(generatedBuildInfoDir) } } +tasks.named("compileKotlin") { dependsOn(generateBuildInfo) } + tasks.withType().configureEach { duplicatesStrategy = DuplicatesStrategy.EXCLUDE } diff --git a/src/main/kotlin/dev/typetype/server/Application.kt b/src/main/kotlin/dev/typetype/server/Application.kt index 66c876f..942cded 100644 --- a/src/main/kotlin/dev/typetype/server/Application.kt +++ b/src/main/kotlin/dev/typetype/server/Application.kt @@ -33,6 +33,7 @@ import dev.typetype.server.routes.adminRoutes import dev.typetype.server.routes.adminBugReportRoutes import dev.typetype.server.routes.authRoutes import dev.typetype.server.routes.trendingRoutes +import dev.typetype.server.routes.publicMetadataRoutes import dev.typetype.server.routes.watchLaterRoutes import dev.typetype.server.routes.userDataRoutes import dev.typetype.server.services.AuthService @@ -44,6 +45,7 @@ import dev.typetype.server.services.PasswordResetService import dev.typetype.server.services.ProfileService import dev.typetype.server.services.PipePipeBackupImporterService import dev.typetype.server.services.OpenMojiProxyService +import dev.typetype.server.services.InstanceService import dev.typetype.server.services.UserAdminService import io.ktor.server.application.Application import io.ktor.server.netty.EngineMain @@ -71,6 +73,7 @@ fun Application.module() { val avatarService = AvatarService() val gitHubIssueService = GitHubIssueService() val adminSettingsService = AdminSettingsService() + val instanceService = InstanceService(authService, adminSettingsService) val restoreService = PipePipeBackupImporterService() val cacheUrl = System.getenv("DRAGONFLY_URL") ?: "redis://localhost:6379" @@ -84,6 +87,7 @@ fun Application.module() { configurePlugins(authService) routing { + publicMetadataRoutes(instanceService::getInstance) rateLimit(STREAMS_ZONE) { streamRoutes(svc.streamService) manifestRoutes(svc.manifestService, svc.nativeManifestService, svc.hlsManifestService) diff --git a/src/main/kotlin/dev/typetype/server/InstanceDefaults.kt b/src/main/kotlin/dev/typetype/server/InstanceDefaults.kt new file mode 100644 index 0000000..500c5ca --- /dev/null +++ b/src/main/kotlin/dev/typetype/server/InstanceDefaults.kt @@ -0,0 +1,5 @@ +package dev.typetype.server + +const val DEFAULT_INSTANCE_NAME = "TypeType" +const val INSTANCE_API_VERSION = 1 +const val INSTANCE_CACHE_CONTROL = "public, max-age=300" diff --git a/src/main/kotlin/dev/typetype/server/db/DatabaseSessionAuthMigration.kt b/src/main/kotlin/dev/typetype/server/db/DatabaseSessionAuthMigration.kt index bb3c97f..bdba576 100644 --- a/src/main/kotlin/dev/typetype/server/db/DatabaseSessionAuthMigration.kt +++ b/src/main/kotlin/dev/typetype/server/db/DatabaseSessionAuthMigration.kt @@ -1,5 +1,6 @@ package dev.typetype.server.db +import dev.typetype.server.DEFAULT_INSTANCE_NAME import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager object DatabaseSessionAuthMigration { @@ -7,6 +8,11 @@ object DatabaseSessionAuthMigration { exec("ALTER TABLE sessions ADD COLUMN IF NOT EXISTS refresh_token_hash TEXT") exec("ALTER TABLE sessions ADD COLUMN IF NOT EXISTS created_at BIGINT NOT NULL DEFAULT 0") exec("ALTER TABLE sessions ADD COLUMN IF NOT EXISTS revoked_at BIGINT") + exec("ALTER TABLE admin_settings ADD COLUMN IF NOT EXISTS name TEXT DEFAULT '$DEFAULT_INSTANCE_NAME'") + exec("ALTER TABLE admin_settings ADD COLUMN IF NOT EXISTS tagline TEXT") + exec("ALTER TABLE admin_settings ADD COLUMN IF NOT EXISTS logo_url TEXT") + exec("ALTER TABLE admin_settings ADD COLUMN IF NOT EXISTS banner_url TEXT") + exec("ALTER TABLE admin_settings ADD COLUMN IF NOT EXISTS min_android_client_version TEXT") exec("UPDATE sessions SET created_at = CASE WHEN created_at = 0 THEN expires_at - 86400000 ELSE created_at END") exec("CREATE UNIQUE INDEX IF NOT EXISTS sessions_refresh_token_hash_unique ON sessions (refresh_token_hash)") } diff --git a/src/main/kotlin/dev/typetype/server/db/tables/AdminSettingsTable.kt b/src/main/kotlin/dev/typetype/server/db/tables/AdminSettingsTable.kt index 9be1b5c..83e197b 100644 --- a/src/main/kotlin/dev/typetype/server/db/tables/AdminSettingsTable.kt +++ b/src/main/kotlin/dev/typetype/server/db/tables/AdminSettingsTable.kt @@ -1,9 +1,15 @@ package dev.typetype.server.db.tables +import dev.typetype.server.DEFAULT_INSTANCE_NAME import org.jetbrains.exposed.v1.core.Table object AdminSettingsTable : Table("admin_settings") { val id = integer("id").default(1) + val name = text("name").default(DEFAULT_INSTANCE_NAME) + val tagline = text("tagline").nullable() + val logoUrl = text("logo_url").nullable() + val bannerUrl = text("banner_url").nullable() + val minAndroidClientVersion = text("min_android_client_version").nullable() val allowRegistration = bool("allow_registration").default(true) val allowGuest = bool("allow_guest").default(true) val forceEmailVerification = bool("force_email_verification").default(false) diff --git a/src/main/kotlin/dev/typetype/server/models/AdminSettingsItem.kt b/src/main/kotlin/dev/typetype/server/models/AdminSettingsItem.kt index 69398d6..b6ff62a 100644 --- a/src/main/kotlin/dev/typetype/server/models/AdminSettingsItem.kt +++ b/src/main/kotlin/dev/typetype/server/models/AdminSettingsItem.kt @@ -1,9 +1,15 @@ package dev.typetype.server.models +import dev.typetype.server.DEFAULT_INSTANCE_NAME import kotlinx.serialization.Serializable @Serializable data class AdminSettingsItem( + val name: String = DEFAULT_INSTANCE_NAME, + val tagline: String? = null, + val logoUrl: String? = null, + val bannerUrl: String? = null, + val minAndroidClientVersion: String? = null, val allowRegistration: Boolean = true, val allowGuest: Boolean = true, val forceEmailVerification: Boolean = false, diff --git a/src/main/kotlin/dev/typetype/server/models/HealthResponse.kt b/src/main/kotlin/dev/typetype/server/models/HealthResponse.kt new file mode 100644 index 0000000..cd077b1 --- /dev/null +++ b/src/main/kotlin/dev/typetype/server/models/HealthResponse.kt @@ -0,0 +1,8 @@ +package dev.typetype.server.models + +import kotlinx.serialization.Serializable + +@Serializable +data class HealthResponse( + val status: String = "ok", +) diff --git a/src/main/kotlin/dev/typetype/server/models/InstanceMinClientVersion.kt b/src/main/kotlin/dev/typetype/server/models/InstanceMinClientVersion.kt new file mode 100644 index 0000000..6c5443c --- /dev/null +++ b/src/main/kotlin/dev/typetype/server/models/InstanceMinClientVersion.kt @@ -0,0 +1,8 @@ +package dev.typetype.server.models + +import kotlinx.serialization.Serializable + +@Serializable +data class InstanceMinClientVersion( + val android: String? = null, +) diff --git a/src/main/kotlin/dev/typetype/server/models/InstanceResponse.kt b/src/main/kotlin/dev/typetype/server/models/InstanceResponse.kt new file mode 100644 index 0000000..e550690 --- /dev/null +++ b/src/main/kotlin/dev/typetype/server/models/InstanceResponse.kt @@ -0,0 +1,17 @@ +package dev.typetype.server.models + +import kotlinx.serialization.Serializable + +@Serializable +data class InstanceResponse( + val name: String, + val tagline: String? = null, + val version: String, + val apiVersion: Int, + val registrationAllowed: Boolean, + val guestAllowed: Boolean, + val supportedServices: List, + val minClientVersion: InstanceMinClientVersion = InstanceMinClientVersion(), + val logoUrl: String? = null, + val bannerUrl: String? = null, +) diff --git a/src/main/kotlin/dev/typetype/server/routes/PublicMetadataRoutes.kt b/src/main/kotlin/dev/typetype/server/routes/PublicMetadataRoutes.kt new file mode 100644 index 0000000..5613a03 --- /dev/null +++ b/src/main/kotlin/dev/typetype/server/routes/PublicMetadataRoutes.kt @@ -0,0 +1,20 @@ +package dev.typetype.server.routes + +import dev.typetype.server.INSTANCE_CACHE_CONTROL +import dev.typetype.server.models.HealthResponse +import dev.typetype.server.models.InstanceResponse +import io.ktor.http.HttpHeaders +import io.ktor.server.application.call +import io.ktor.server.response.respond +import io.ktor.server.routing.Route +import io.ktor.server.routing.get + +fun Route.publicMetadataRoutes(instanceProvider: suspend () -> InstanceResponse) { + get("/health") { + call.respond(HealthResponse()) + } + get("/instance") { + call.response.headers.append(HttpHeaders.CacheControl, INSTANCE_CACHE_CONTROL) + call.respond(instanceProvider()) + } +} diff --git a/src/main/kotlin/dev/typetype/server/services/AdminSettingsService.kt b/src/main/kotlin/dev/typetype/server/services/AdminSettingsService.kt index 5c07e2d..12a2bb5 100644 --- a/src/main/kotlin/dev/typetype/server/services/AdminSettingsService.kt +++ b/src/main/kotlin/dev/typetype/server/services/AdminSettingsService.kt @@ -2,6 +2,7 @@ package dev.typetype.server.services import dev.typetype.server.db.DatabaseFactory import dev.typetype.server.db.tables.AdminSettingsTable +import dev.typetype.server.DEFAULT_INSTANCE_NAME import dev.typetype.server.models.AdminSettingsItem import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.jdbc.insert @@ -12,34 +13,75 @@ private const val SETTINGS_ROW_ID = 1 class AdminSettingsService { - suspend fun get(): AdminSettingsItem = DatabaseFactory.query { - AdminSettingsTable.selectAll().singleOrNull()?.let { - AdminSettingsItem( - allowRegistration = it[AdminSettingsTable.allowRegistration], - allowGuest = it[AdminSettingsTable.allowGuest], - forceEmailVerification = it[AdminSettingsTable.forceEmailVerification], - ) - } ?: AdminSettingsItem() + suspend fun get(): AdminSettingsItem { + cachedSettings?.let { return it } + val settings = DatabaseFactory.query { + AdminSettingsTable.selectAll().singleOrNull()?.let { + AdminSettingsItem( + name = it[AdminSettingsTable.name], + tagline = it[AdminSettingsTable.tagline], + logoUrl = it[AdminSettingsTable.logoUrl], + bannerUrl = it[AdminSettingsTable.bannerUrl], + minAndroidClientVersion = it[AdminSettingsTable.minAndroidClientVersion], + allowRegistration = it[AdminSettingsTable.allowRegistration], + allowGuest = it[AdminSettingsTable.allowGuest], + forceEmailVerification = it[AdminSettingsTable.forceEmailVerification], + ).normalized() + } ?: AdminSettingsItem() + } + cachedSettings = settings + return settings } suspend fun upsert(item: AdminSettingsItem): AdminSettingsItem { + val settings = item.normalized() DatabaseFactory.query { val exists = AdminSettingsTable.selectAll().count() > 0 if (exists) { AdminSettingsTable.update({ AdminSettingsTable.id eq SETTINGS_ROW_ID }) { - it[allowRegistration] = item.allowRegistration - it[allowGuest] = item.allowGuest - it[forceEmailVerification] = item.forceEmailVerification + it[name] = settings.name + it[tagline] = settings.tagline + it[logoUrl] = settings.logoUrl + it[bannerUrl] = settings.bannerUrl + it[minAndroidClientVersion] = settings.minAndroidClientVersion + it[allowRegistration] = settings.allowRegistration + it[allowGuest] = settings.allowGuest + it[forceEmailVerification] = settings.forceEmailVerification } } else { AdminSettingsTable.insert { it[id] = SETTINGS_ROW_ID - it[allowRegistration] = item.allowRegistration - it[allowGuest] = item.allowGuest - it[forceEmailVerification] = item.forceEmailVerification + it[name] = settings.name + it[tagline] = settings.tagline + it[logoUrl] = settings.logoUrl + it[bannerUrl] = settings.bannerUrl + it[minAndroidClientVersion] = settings.minAndroidClientVersion + it[allowRegistration] = settings.allowRegistration + it[allowGuest] = settings.allowGuest + it[forceEmailVerification] = settings.forceEmailVerification } } } - return item + cachedSettings = settings + return settings + } + + private fun AdminSettingsItem.normalized(): AdminSettingsItem = copy( + name = name.trim().takeIf { it.isNotEmpty() } ?: DEFAULT_INSTANCE_NAME, + tagline = tagline.normalizeOptionalText(), + logoUrl = logoUrl.normalizeOptionalText(), + bannerUrl = bannerUrl.normalizeOptionalText(), + minAndroidClientVersion = minAndroidClientVersion.normalizeOptionalText(), + ) + + private fun String?.normalizeOptionalText(): String? = this?.trim()?.takeIf { it.isNotEmpty() } + + companion object { + @Volatile + private var cachedSettings: AdminSettingsItem? = null + + fun clearCache() { + cachedSettings = null + } } } diff --git a/src/main/kotlin/dev/typetype/server/services/InstanceService.kt b/src/main/kotlin/dev/typetype/server/services/InstanceService.kt new file mode 100644 index 0000000..e1bf476 --- /dev/null +++ b/src/main/kotlin/dev/typetype/server/services/InstanceService.kt @@ -0,0 +1,33 @@ +package dev.typetype.server.services + +import dev.typetype.server.BuildInfo +import dev.typetype.server.DEFAULT_INSTANCE_NAME +import dev.typetype.server.INSTANCE_API_VERSION +import dev.typetype.server.models.InstanceMinClientVersion +import dev.typetype.server.models.InstanceResponse + +class InstanceService( + private val authService: AuthService, + private val adminSettingsService: AdminSettingsService, +) { + + suspend fun getInstance(): InstanceResponse { + val settings = adminSettingsService.get() + return InstanceResponse( + name = settings.name.normalizeName(), + tagline = settings.tagline.normalizeOptionalText(), + version = BuildInfo.VERSION, + apiVersion = INSTANCE_API_VERSION, + registrationAllowed = settings.allowRegistration || !authService.hasUsers(), + guestAllowed = settings.allowGuest, + supportedServices = VALID_SERVICE_IDS.sorted(), + minClientVersion = InstanceMinClientVersion(android = settings.minAndroidClientVersion.normalizeOptionalText()), + logoUrl = settings.logoUrl.normalizeOptionalText(), + bannerUrl = settings.bannerUrl.normalizeOptionalText(), + ) + } + + private fun String.normalizeName(): String = trim().takeIf { it.isNotEmpty() } ?: DEFAULT_INSTANCE_NAME + + private fun String?.normalizeOptionalText(): String? = this?.trim()?.takeIf { it.isNotEmpty() } +} diff --git a/src/test/kotlin/dev/typetype/server/HealthRoutesTest.kt b/src/test/kotlin/dev/typetype/server/HealthRoutesTest.kt new file mode 100644 index 0000000..ee3dd03 --- /dev/null +++ b/src/test/kotlin/dev/typetype/server/HealthRoutesTest.kt @@ -0,0 +1,35 @@ +package dev.typetype.server + +import dev.typetype.server.routes.publicMetadataRoutes +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import io.ktor.http.HttpStatusCode +import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.application.install +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation +import io.ktor.server.routing.routing +import io.ktor.server.testing.testApplication +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Test + +class HealthRoutesTest { + + @Test + fun `health returns ok without calling instance provider`() = testApplication { + var called = false + application { + install(ContentNegotiation) { json() } + routing { + publicMetadataRoutes { + called = true + error("instance provider should not be called") + } + } + } + val response = client.get("/health") + assertEquals(HttpStatusCode.OK, response.status) + assertEquals("""{"status":"ok"}""", response.bodyAsText()) + assertFalse(called) + } +} diff --git a/src/test/kotlin/dev/typetype/server/InstanceRoutesTest.kt b/src/test/kotlin/dev/typetype/server/InstanceRoutesTest.kt new file mode 100644 index 0000000..cca417b --- /dev/null +++ b/src/test/kotlin/dev/typetype/server/InstanceRoutesTest.kt @@ -0,0 +1,112 @@ +package dev.typetype.server + +import dev.typetype.server.models.AdminSettingsItem +import dev.typetype.server.routes.authRoutes +import dev.typetype.server.routes.publicMetadataRoutes +import dev.typetype.server.services.AdminSettingsService +import dev.typetype.server.services.AuthService +import dev.typetype.server.services.InstanceService +import dev.typetype.server.services.PasswordResetService +import dev.typetype.server.services.ProfileService +import io.ktor.client.request.get +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.client.statement.bodyAsText +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.application.install +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation +import io.ktor.server.routing.routing +import io.ktor.server.testing.testApplication +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class InstanceRoutesTest { + private val adminSettings = AdminSettingsService() + private val passwordReset = PasswordResetService() + private val profile = ProfileService() + + companion object { + @BeforeAll + @JvmStatic + fun initDb() = TestDatabase.setup() + } + + @BeforeEach + fun clean() { + TestDatabase.truncateAll() + } + + @Test + fun `instance returns defaults and cache header`() = testApplication { + val auth = AuthService.fixed(TEST_USER_ID, hasUsers = false) + val instanceService = InstanceService(auth, adminSettings) + application { + install(ContentNegotiation) { json() } + routing { publicMetadataRoutes(instanceService::getInstance) } + } + val response = client.get("/instance") + assertEquals(HttpStatusCode.OK, response.status) + assertEquals("public, max-age=300", response.headers[HttpHeaders.CacheControl]) + val root = Json.parseToJsonElement(response.bodyAsText()).jsonObject + assertEquals("TypeType", root["name"]?.jsonPrimitive?.contentOrNull) + assertEquals(BuildInfo.VERSION, root["version"]?.jsonPrimitive?.contentOrNull) + assertEquals(1, root["apiVersion"]?.jsonPrimitive?.int) + assertEquals(true, root["registrationAllowed"]?.jsonPrimitive?.boolean) + assertEquals(true, root["guestAllowed"]?.jsonPrimitive?.boolean) + assertEquals(listOf(0, 3, 4, 5, 6), root["supportedServices"]?.jsonArray?.map { it.jsonPrimitive.int }) + assertEquals(null, root["logoUrl"]?.jsonPrimitive?.contentOrNull) + assertEquals(null, root["bannerUrl"]?.jsonPrimitive?.contentOrNull) + assertEquals(null, root["minClientVersion"]?.jsonObject?.get("android")?.jsonPrimitive?.contentOrNull) + } + + @Test + fun `instance reflects custom settings and blocks registration when users exist`() = testApplication { + adminSettings.upsert( + AdminSettingsItem( + name = "Custom Instance", + tagline = "Privacy-respecting video platform", + logoUrl = "https://cdn.example.com/typetype/logo.png", + bannerUrl = "https://cdn.example.com/typetype/banner.jpg", + minAndroidClientVersion = "0.1.0", + allowRegistration = false, + allowGuest = false, + ) + ) + val auth = AuthService.fixed(TEST_USER_ID, hasUsers = true) + val instanceService = InstanceService(auth, adminSettings) + application { + install(ContentNegotiation) { json() } + routing { + publicMetadataRoutes(instanceService::getInstance) + authRoutes(auth, passwordReset, profile, adminSettings) + } + } + val response = client.get("/instance") + val root = Json.parseToJsonElement(response.bodyAsText()).jsonObject + assertEquals("Custom Instance", root["name"]?.jsonPrimitive?.contentOrNull) + assertEquals("Privacy-respecting video platform", root["tagline"]?.jsonPrimitive?.contentOrNull) + assertEquals("https://cdn.example.com/typetype/logo.png", root["logoUrl"]?.jsonPrimitive?.contentOrNull) + assertEquals("https://cdn.example.com/typetype/banner.jpg", root["bannerUrl"]?.jsonPrimitive?.contentOrNull) + assertEquals("0.1.0", root["minClientVersion"]?.jsonObject?.get("android")?.jsonPrimitive?.contentOrNull) + assertEquals(false, root["registrationAllowed"]?.jsonPrimitive?.boolean) + assertEquals(false, root["guestAllowed"]?.jsonPrimitive?.boolean) + val register = client.post("/auth/register") { + contentType(ContentType.Application.Json) + setBody("""{"email":"new@test.local","password":"secret","name":"New"}""") + } + assertEquals(HttpStatusCode.Forbidden, register.status) + } +} diff --git a/src/test/kotlin/dev/typetype/server/TestDatabase.kt b/src/test/kotlin/dev/typetype/server/TestDatabase.kt index 8ceab87..0bc64b2 100644 --- a/src/test/kotlin/dev/typetype/server/TestDatabase.kt +++ b/src/test/kotlin/dev/typetype/server/TestDatabase.kt @@ -27,6 +27,7 @@ import dev.typetype.server.db.tables.YoutubeTakeoutImportJobsTable import dev.typetype.server.db.tables.YoutubeTakeoutPlaylistKeysTable import dev.typetype.server.db.tables.UsersTable import dev.typetype.server.db.tables.WatchLaterTable +import dev.typetype.server.services.AdminSettingsService import org.jetbrains.exposed.v1.jdbc.deleteAll import org.jetbrains.exposed.v1.jdbc.transactions.transaction import org.testcontainers.containers.ContainerLaunchException @@ -48,6 +49,7 @@ object TestDatabase { } fun setup() { + AdminSettingsService.clearCache() if (initialized) return synchronized(this) { if (initialized) return @@ -112,5 +114,6 @@ object TestDatabase { YoutubeTakeoutPlaylistKeysTable.deleteAll() BugReportsTable.deleteAll() NotificationStatesTable.deleteAll() + AdminSettingsService.clearCache() } }