--- /dev/null
+package net.pterodactylus.sone.database.migration
+
+import jakarta.inject.Inject
+import java.sql.Connection
+import net.pterodactylus.sone.database.h2.jooq.tables.references.OPTIONS
+import net.pterodactylus.util.config.Configuration
+import net.pterodactylus.util.config.ConfigurationException
+import org.flywaydb.core.api.MigrationVersion
+import org.flywaydb.core.api.migration.BaseJavaMigration
+import org.flywaydb.core.api.migration.Context
+import org.flywaydb.core.api.migration.JavaMigration
+import org.jooq.DSLContext
+import org.jooq.Migration
+import org.jooq.SQLDialect
+import org.jooq.impl.DSL
+
+/**
+ * This migration will read
+ */
+@Suppress("unused", "ClassName")
+class V202504051058_MigratePreferences @Inject constructor(private val configuration: Configuration, private val sqlDialect: SQLDialect) : JavaMigration {
+
+ override fun getVersion(): MigrationVersion = MigrationVersion.fromVersion("202504051058")
+
+ override fun getDescription() = "MigratePreferences"
+
+ override fun getChecksum() = 0
+
+ override fun canExecuteInTransaction() = true
+
+ override fun migrate(context: Context) {
+ DSL.using(context.connection, sqlDialect).transaction { configuration ->
+ copyIntegerValueFromPreferencesToDatabase(configuration.dsl(), "Option/InsertionDelay", "InsertionDelay")
+ copyIntegerValueFromPreferencesToDatabase(configuration.dsl(), "Option/PostsPerPage", "PostsPerPage")
+ copyIntegerValueFromPreferencesToDatabase(configuration.dsl(), "Option/ImagesPerPage", "ImagesPerPage")
+ copyIntegerValueFromPreferencesToDatabase(configuration.dsl(), "Option/CharactersPerPost", "CharactersPerPost")
+ copyIntegerValueFromPreferencesToDatabase(configuration.dsl(), "Option/PostCutOffLength", "PostCutOffLength")
+ copyBooleanValueFromPreferencesToDatabase(configuration.dsl(), "Option/RequireFullAccess", "RequireFullAccess")
+ copyBooleanValueFromPreferencesToDatabase(configuration.dsl(), "Option/ActivateFcpInterface", "FcpInterfaceActive")
+ copyIntegerValueFromPreferencesToDatabase(configuration.dsl(), "Option/FcpFullAccessRequired", "FcpFullAccessRequired")
+ copyBooleanValueFromPreferencesToDatabase(configuration.dsl(), "Option/StrictFiltering", "StrictFiltering")
+ }
+ }
+
+ private fun copyIntegerValueFromPreferencesToDatabase(dslContext: DSLContext, preferencesName: String, databaseOptionName: String) {
+ val preferenceValue = try {
+ configuration.getIntValue(preferencesName).value
+ } catch (e: ConfigurationException) {
+ null
+ }
+ dslContext.insertInto(OPTIONS).columns(OPTIONS.KEY, OPTIONS.VALUE).values(databaseOptionName, preferenceValue)
+ .onDuplicateKeyUpdate().set(OPTIONS.VALUE, preferenceValue)
+ .execute()
+ }
+
+ private fun copyBooleanValueFromPreferencesToDatabase(dslContext: DSLContext, preferencesName: String, databaseOptionName: String) {
+ val preferenceValue = try {
+ configuration.getBooleanValue(preferencesName).value
+ } catch (e: ConfigurationException) {
+ null
+ }
+ dslContext.insertInto(OPTIONS).columns(OPTIONS.KEY, OPTIONS.VALUE).values(databaseOptionName, preferenceValue?.asInt)
+ .onDuplicateKeyUpdate().set(OPTIONS.VALUE, preferenceValue?.asInt)
+ .execute()
+ }
+
+}
+
+private val Boolean.asInt: Int get() = if (this) 1 else 0
--- /dev/null
+package net.pterodactylus.sone.database.migration
+
+import com.google.inject.Guice.createInjector
+import kotlin.test.Test
+import net.pterodactylus.sone.database.h2.jooq.tables.references.OPTIONS
+import net.pterodactylus.sone.test.getInstance
+import net.pterodactylus.sone.test.getMigrationSql
+import net.pterodactylus.sone.test.isProvidedBy
+import net.pterodactylus.sone.test.jOOQ
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.randomInMemoryDataSource
+import net.pterodactylus.util.config.Configuration
+import net.pterodactylus.util.config.MapConfigurationBackend
+import org.flywaydb.core.api.MigrationVersion
+import org.flywaydb.core.api.migration.Context
+import org.hamcrest.Matcher
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.nullValue
+import org.jooq.SQLDialect
+import org.junit.Before
+
+@Suppress("ClassName")
+class V202504051058_MigratePreferencesTest {
+
+ @Test
+ fun `migration can be created by guice`() {
+ createInjector(
+ Configuration::class.isProvidedBy(configuration),
+ SQLDialect::class.isProvidedBy(SQLDialect.H2)
+ ).getInstance<V202504051058_MigratePreferences>()
+ }
+
+ @Test
+ fun `migration returns correct version number`() {
+ assertThat(migration.version, equalTo(MigrationVersion.fromVersion("202504051058")))
+ }
+
+ @Test
+ fun `migration transfers null for insertion delay as null`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/InsertionDelay", null, "InsertionDelay", nullValue())
+ }
+
+ @Test
+ fun `migration transfers insertion delay`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/InsertionDelay", "1", "InsertionDelay", equalTo(1))
+ }
+
+ @Test
+ fun `migration transfers invalid insertion delay as null`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/InsertionDelay", "invalid", "InsertionDelay", nullValue())
+ }
+
+ @Test
+ fun `migration transfers null for posts per page as null`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/PostsPerPage", null, "PostsPerPage", nullValue())
+ }
+
+ @Test
+ fun `migration transfers posts per page`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/PostsPerPage", "2", "PostsPerPage", equalTo(2))
+ }
+
+ @Test
+ fun `migration transfers invalid posts per page as null`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/PostsPerPage", "invalid", "PostsPerPage", nullValue())
+ }
+
+ @Test
+ fun `migration transfers null for images per page as null`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/ImagesPerPage", null, "ImagesPerPage", nullValue())
+ }
+
+ @Test
+ fun `migration transfers images per page`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/ImagesPerPage", "3", "ImagesPerPage", equalTo(3))
+ }
+
+ @Test
+ fun `migration transfers invalid images per page as null`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/ImagesPerPage", "invalid", "ImagesPerPage", nullValue())
+ }
+
+ @Test
+ fun `migration transfers null for characters per post as null`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/CharactersPerPost", null, "CharactersPerPost", nullValue())
+ }
+
+ @Test
+ fun `migration transfers characters per post`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/CharactersPerPost", "4", "CharactersPerPost", equalTo(4))
+ }
+
+ @Test
+ fun `migration transfers invalid characters per post as null`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/CharactersPerPost", "invalid", "CharactersPerPost", nullValue())
+ }
+
+ @Test
+ fun `migration transfers null for post cut-off length as null`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/PostCutOffLength", null, "PostCutOffLength", nullValue())
+ }
+
+ @Test
+ fun `migration transfers post cut-off length`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/PostCutOffLength", "5", "PostCutOffLength", equalTo(5))
+ }
+
+ @Test
+ fun `migration transfers invalid post cut-off length as null`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/PostCutOffLength", "invalid", "PostCutOffLength", nullValue())
+ }
+
+ @Test
+ fun `migration transfers null for require full access as null`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/RequireFullAccess", null, "RequireFullAccess", nullValue())
+ }
+
+ @Test
+ fun `migration transfers require full access`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/RequireFullAccess", "true", "RequireFullAccess", equalTo(1))
+ }
+
+ @Test
+ fun `migration transfers invalid require full access as false`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/RequireFullAccess", "invalid", "RequireFullAccess", equalTo(0))
+ }
+
+ @Test
+ fun `migration transfers null for fcp interface active as null`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/ActivateFcpInterface", null, "FcpInterfaceActive", nullValue())
+ }
+
+ @Test
+ fun `migration transfers fcp interface active`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/ActivateFcpInterface", "true", "FcpInterfaceActive", equalTo(1))
+ }
+
+ @Test
+ fun `migration transfers invalid fcp interface active as false`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/ActivateFcpInterface", "invalid", "FcpInterfaceActive", equalTo(0))
+ }
+
+ @Test
+ fun `migration transfers null for fcp full access required as null`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/FcpFullAccessRequired", null, "FcpFullAccessRequired", nullValue())
+ }
+
+ @Test
+ fun `migration transfers fcp full access required`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/FcpFullAccessRequired", "2", "FcpFullAccessRequired", equalTo(2))
+ }
+
+ @Test
+ fun `migration transfers invalid fcp full access required as false`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/FcpFullAccessRequired", "invalid", "FcpFullAccessRequired", nullValue())
+ }
+
+ @Test
+ fun `migration transfers null for strict filtering as null`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/StrictFiltering", null, "StrictFiltering", nullValue())
+ }
+
+ @Test
+ fun `migration transfers strict filtering`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/StrictFiltering", "true", "StrictFiltering", equalTo(1))
+ }
+
+ @Test
+ fun `migration transfers invalid strict filtering as false`() {
+ storeValueInConfigurationAndVerifyTransferredDatabaseValue("Option/StrictFiltering", "invalid", "StrictFiltering", equalTo(0))
+ }
+
+ private fun storeValueInConfigurationAndVerifyTransferredDatabaseValue(nameInConfiguration: String, stringValue: String?, optionName: String, intMatcher: Matcher<in Int?>?) {
+ configurationMap[nameInConfiguration] = stringValue
+ migration.migrate(context)
+ val migratedValue = jOOQ.select(OPTIONS.VALUE).from(OPTIONS).where(OPTIONS.KEY.eq(optionName)).fetchOne(OPTIONS.VALUE)
+ assertThat(migratedValue, intMatcher)
+ }
+
+ @Before
+ fun runAllRequiredMigrations() =
+ jOOQ.transaction { transaction ->
+ getMigrationSql("V202501202137__create_sone_schema", "V202501221927__create_options_table")
+ .forEach { sql -> transaction.dsl().execute(sql) }
+ }
+
+ private val dataSource = randomInMemoryDataSource
+ private val jOOQ = jOOQ(dataSource)
+ private val context: Context = object : Context {
+ override fun getConfiguration() = mock<org.flywaydb.core.api.configuration.Configuration>()
+ override fun getConnection() = dataSource.connection
+ }
+ private val configurationMap = mutableMapOf<String, String?>()
+ private val configuration by lazy { Configuration(MapConfigurationBackend(configurationMap)) }
+ private val migration by lazy { V202504051058_MigratePreferences(configuration, SQLDialect.H2) }
+
+}
--- /dev/null
+package net.pterodactylus.sone.main
+
+import com.google.inject.Guice.createInjector
+import javax.sql.DataSource
+import kotlin.test.Test
+import net.pterodactylus.sone.database.h2.JdbcPreferences
+import net.pterodactylus.sone.test.getInstance
+import net.pterodactylus.sone.test.isProvidedByMock
+import net.pterodactylus.sone.test.verifySingletonInstance
+import net.pterodactylus.sone.test.withNameIsProvidedBy
+import net.pterodactylus.util.config.Configuration
+import org.flywaydb.core.Flyway
+import org.h2.jdbcx.JdbcDataSource
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.containsInAnyOrder
+import org.hamcrest.Matchers.containsString
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.instanceOf
+import org.jooq.SQLDialect
+import org.junit.Rule
+import org.junit.rules.TemporaryFolder
+
+class DatabaseModuleTest {
+
+ @Test
+ fun `module provides SQL dialect for jOOQ`() {
+ assertThat(createInjector().getInstance<SQLDialect>(), equalTo(SQLDialect.H2))
+ }
+
+ @Test
+ fun `module provides data source`() {
+ createInjector("/node/user-dir").getInstance<DataSource>().let { dataSource ->
+ assertThat(dataSource, instanceOf(JdbcDataSource::class.java))
+ assertThat((dataSource as JdbcDataSource).getUrl(), containsString("/node/user-dir"))
+ }
+ }
+
+ @Test
+ fun `provided data source is a singleton`() {
+ createInjector().verifySingletonInstance<DataSource>()
+ }
+
+ @Test
+ fun `module provides flyway`() {
+ createInjector().getInstance<Flyway>()
+ }
+
+ @Test
+ fun `flyway has migrations loaded from classpath`() {
+ val flyway = createInjector().getInstance<Flyway>()
+ assertThat(flyway.info().all().map { it.version.version }, containsInAnyOrder(*migrations))
+ }
+
+ @Test
+ fun `provided flyway is a singleton`() {
+ createInjector().verifySingletonInstance<Flyway>()
+ }
+
+ @Test
+ fun `module provides jdbc preferences`() {
+ createInjector().getInstance<JdbcPreferences>()
+ }
+
+ @Test
+ fun `module provides jdbc preferences as singleton`() {
+ createInjector().verifySingletonInstance<JdbcPreferences>()
+ }
+
+ private fun createInjector(databasePath: String = tempFolder.newFolder().path) =
+ createInjector(DatabaseModule(), Configuration::class.isProvidedByMock(), String::class.withNameIsProvidedBy(databasePath, "NodeUserDir"))
+
+ @Rule
+ @JvmField
+ val tempFolder = TemporaryFolder()
+
+}
+
+private val migrations = arrayOf(
+ "202501202137",
+ "202501221927",
+ "202504051058",
+)