diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index 4185ae2bb..6dc7273e8 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -38,7 +38,7 @@ complexity: LongMethod: # Seed and fixture builders are long because the data is long — straight # line, no branches. Production code keeps detekt's 60-line ceiling. - excludes: ['**/test/**', '**/integrationTest/**', '**/testFixtures/**', '**/db/migration/**'] + excludes: ['**/test/**', '**/integrationTest/**', '**/testFixtures/**', '**/db/migration/**', '**/ShippedBoards.kt', '**/ShippedEsports.kt'] TooManyFunctions: excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/integrationTest/**', '**/testFixtures/**', '**/db/migration/**'] # A Spring service that owns one aggregate lands at twelve to fourteen @@ -110,7 +110,9 @@ style: # sits in: `@Order(50)` says what 50 is, and hoisting it to a constant puts # the ordering one indirection away from the thing being ordered. ignoreAnnotation: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/integrationTest/**', '**/testFixtures/**', '**/db/migration/**', '**/*.kts'] + # The two seed loaders set JDBC parameters by position: the number is the + # position, and a constant naming it would name nothing. + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/integrationTest/**', '**/testFixtures/**', '**/db/migration/**', '**/ShippedBoards.kt', '**/ShippedEsports.kt', '**/*.kts'] MaxLineLength: excludes: ['**/db/migration/**'] # ktlint is the other gate landing on this job and its ktlint_official style diff --git a/services/api/src/integrationTest/kotlin/net/blueshell/api/board/domain/ShippedBoardArtIT.kt b/services/api/src/integrationTest/kotlin/net/blueshell/api/board/domain/ShippedBoardArtIT.kt index defb3cec4..678271dc7 100644 --- a/services/api/src/integrationTest/kotlin/net/blueshell/api/board/domain/ShippedBoardArtIT.kt +++ b/services/api/src/integrationTest/kotlin/net/blueshell/api/board/domain/ShippedBoardArtIT.kt @@ -1,6 +1,5 @@ package net.blueshell.api.board.domain -import db.migration.R__Boards_seed import net.blueshell.api.board.persistence.BoardMemberRepository import net.blueshell.api.board.persistence.BoardRepository import net.blueshell.api.file.api.FileService @@ -10,7 +9,6 @@ import net.blueshell.api.shared.enums.FileType import net.blueshell.api.shared.enums.Role import net.blueshell.api.testsupport.UserTestSupport import org.assertj.core.api.Assertions.assertThat -import org.flywaydb.core.api.migration.Context import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired @@ -19,7 +17,6 @@ import org.springframework.boot.test.context.SpringBootTest import java.io.ByteArrayInputStream import java.nio.file.Files import java.nio.file.Paths -import java.sql.Connection import javax.sql.DataSource /** @@ -49,15 +46,7 @@ class ShippedBoardArtIT : UserTestSupport() { @BeforeEach fun loadTheRecords() { - dataSource.connection.use { connection -> - R__Boards_seed().migrate( - object : Context { - override fun getConfiguration() = null - - override fun getConnection(): Connection = connection - }, - ) - } + ShippedBoards(dataSource, transactionTemplate).apply() } /** One board's photograph, read where its widths can still be read. */ diff --git a/services/api/src/integrationTest/kotlin/net/blueshell/api/board/persistence/BoardSeedLoadIT.kt b/services/api/src/integrationTest/kotlin/net/blueshell/api/board/persistence/BoardSeedLoadIT.kt index 16faac95e..b5e0999b0 100644 --- a/services/api/src/integrationTest/kotlin/net/blueshell/api/board/persistence/BoardSeedLoadIT.kt +++ b/services/api/src/integrationTest/kotlin/net/blueshell/api/board/persistence/BoardSeedLoadIT.kt @@ -1,17 +1,15 @@ package net.blueshell.api.board.persistence -import db.migration.R__Boards_seed import net.blueshell.api.board.domain.BoardSeed +import net.blueshell.api.board.domain.ShippedBoards import net.blueshell.api.shared.enums.Role import net.blueshell.api.testsupport.UserTestSupport import net.blueshell.api.user.persistence.User import org.assertj.core.api.Assertions.assertThat -import org.flywaydb.core.api.migration.Context import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.jdbc.core.JdbcTemplate -import java.sql.Connection import java.time.LocalDate import javax.sql.DataSource @@ -420,15 +418,7 @@ class BoardSeedLoadIT : UserTestSupport() { )!! private fun runLoader() { - dataSource.connection.use { connection -> - R__Boards_seed().migrate( - object : Context { - override fun getConfiguration() = null - - override fun getConnection(): Connection = connection - }, - ) - } + ShippedBoards(dataSource, transactionTemplate).apply() } private companion object { diff --git a/services/api/src/integrationTest/kotlin/net/blueshell/api/esports/domain/ShippedArtIT.kt b/services/api/src/integrationTest/kotlin/net/blueshell/api/esports/domain/ShippedArtIT.kt index 83ed4866c..fb08e1c16 100644 --- a/services/api/src/integrationTest/kotlin/net/blueshell/api/esports/domain/ShippedArtIT.kt +++ b/services/api/src/integrationTest/kotlin/net/blueshell/api/esports/domain/ShippedArtIT.kt @@ -1,6 +1,5 @@ package net.blueshell.api.esports.domain -import db.migration.R__Esports_seed import net.blueshell.api.esports.persistence.GameRepository import net.blueshell.api.esports.persistence.TeamRepository import net.blueshell.api.esports.persistence.TeamSeason @@ -14,7 +13,6 @@ import net.blueshell.api.testsupport.EsportsSeedFixture import net.blueshell.api.testsupport.UserTestSupport import net.blueshell.api.user.api.UserService import org.assertj.core.api.Assertions.assertThat -import org.flywaydb.core.api.migration.Context import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -24,7 +22,6 @@ import org.springframework.boot.test.context.SpringBootTest import java.io.ByteArrayInputStream import java.nio.file.Files import java.nio.file.Paths -import java.sql.Connection import javax.sql.DataSource /** @@ -65,15 +62,7 @@ class ShippedArtIT : UserTestSupport() { @BeforeEach fun loadTheRecords() { - dataSource.connection.use { connection -> - R__Esports_seed(EsportsSeedFixture.files).migrate( - object : Context { - override fun getConfiguration() = null - - override fun getConnection(): Connection = connection - }, - ) - } + ShippedEsports(dataSource, transactionTemplate, EsportsSeedFixture.files).apply() } /** diff --git a/services/api/src/integrationTest/kotlin/net/blueshell/api/esports/persistence/EsportsSeedLoadIT.kt b/services/api/src/integrationTest/kotlin/net/blueshell/api/esports/persistence/EsportsSeedLoadIT.kt index 7821ea682..ada8ab162 100644 --- a/services/api/src/integrationTest/kotlin/net/blueshell/api/esports/persistence/EsportsSeedLoadIT.kt +++ b/services/api/src/integrationTest/kotlin/net/blueshell/api/esports/persistence/EsportsSeedLoadIT.kt @@ -1,16 +1,14 @@ package net.blueshell.api.esports.persistence -import db.migration.R__Esports_seed import net.blueshell.api.testsupport.EsportsSeedFixture +import net.blueshell.api.esports.domain.ShippedEsports import net.blueshell.api.testsupport.UserTestSupport import org.assertj.core.api.Assertions.assertThat -import org.flywaydb.core.api.migration.Context import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.jdbc.core.JdbcTemplate -import java.sql.Connection import javax.sql.DataSource /** @@ -221,15 +219,7 @@ class EsportsSeedLoadIT : UserTestSupport() { } private fun runLoader() { - dataSource.connection.use { connection -> - R__Esports_seed(EsportsSeedFixture.files).migrate( - object : Context { - override fun getConfiguration() = null - - override fun getConnection(): Connection = connection - }, - ) - } + ShippedEsports(dataSource, transactionTemplate, EsportsSeedFixture.files).apply() } @AfterEach diff --git a/services/api/src/integrationTest/kotlin/net/blueshell/api/esports/persistence/RecoveredAttributionIT.kt b/services/api/src/integrationTest/kotlin/net/blueshell/api/esports/persistence/RecoveredAttributionIT.kt index c2b8622e4..808696c70 100644 --- a/services/api/src/integrationTest/kotlin/net/blueshell/api/esports/persistence/RecoveredAttributionIT.kt +++ b/services/api/src/integrationTest/kotlin/net/blueshell/api/esports/persistence/RecoveredAttributionIT.kt @@ -1,18 +1,16 @@ package net.blueshell.api.esports.persistence -import db.migration.R__Esports_seed import net.blueshell.api.shared.enums.Role +import net.blueshell.api.esports.domain.ShippedEsports import net.blueshell.api.testsupport.EsportsSeedFixture import net.blueshell.api.testsupport.UserTestSupport import net.blueshell.api.user.persistence.User import org.assertj.core.api.Assertions.assertThat -import org.flywaydb.core.api.migration.Context import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.jdbc.core.JdbcTemplate -import java.sql.Connection import javax.sql.DataSource /** @@ -120,15 +118,7 @@ class RecoveredAttributionIT : UserTestSupport() { } private fun runLoader() { - dataSource.connection.use { connection -> - R__Esports_seed(EsportsSeedFixture.files).migrate( - object : Context { - override fun getConfiguration() = null - - override fun getConnection(): Connection = connection - }, - ) - } + ShippedEsports(dataSource, transactionTemplate, EsportsSeedFixture.files).apply() } @AfterEach diff --git a/services/api/src/main/kotlin/net/blueshell/api/board/domain/ShippedBoardArt.kt b/services/api/src/main/kotlin/net/blueshell/api/board/domain/ShippedBoardArt.kt index 45a3156d3..717ce707d 100644 --- a/services/api/src/main/kotlin/net/blueshell/api/board/domain/ShippedBoardArt.kt +++ b/services/api/src/main/kotlin/net/blueshell/api/board/domain/ShippedBoardArt.kt @@ -7,9 +7,11 @@ import net.blueshell.api.file.persistence.File import net.blueshell.api.shared.enums.FileType import net.blueshell.api.user.api.UserService import net.blueshell.api.user.persistence.User +import net.blueshell.api.shared.seed.SeedOrder import org.slf4j.LoggerFactory import org.springframework.boot.context.event.ApplicationReadyEvent import org.springframework.context.event.EventListener +import org.springframework.core.annotation.Order import org.springframework.stereotype.Component import org.springframework.transaction.support.TransactionTemplate @@ -238,6 +240,7 @@ class ShippedBoardArt( class ShippedBoardArtOnStartup( private val art: ShippedBoardArt, ) { + @Order(SeedOrder.ART) @EventListener(ApplicationReadyEvent::class) fun onReady() { try { diff --git a/services/api/src/main/kotlin/db/migration/R__Boards_seed.kt b/services/api/src/main/kotlin/net/blueshell/api/board/domain/ShippedBoards.kt similarity index 82% rename from services/api/src/main/kotlin/db/migration/R__Boards_seed.kt rename to services/api/src/main/kotlin/net/blueshell/api/board/domain/ShippedBoards.kt index f431b99f8..8e8df9415 100644 --- a/services/api/src/main/kotlin/db/migration/R__Boards_seed.kt +++ b/services/api/src/main/kotlin/net/blueshell/api/board/domain/ShippedBoards.kt @@ -1,33 +1,47 @@ -package db.migration +package net.blueshell.api.board.domain -import net.blueshell.api.board.domain.BoardSeed import net.blueshell.api.shared.seed.SeedCsv -import org.flywaydb.core.api.migration.BaseJavaMigration -import org.flywaydb.core.api.migration.Context +import net.blueshell.api.shared.seed.SeedOrder import org.slf4j.LoggerFactory +import org.springframework.boot.context.event.ApplicationReadyEvent +import org.springframework.context.event.EventListener +import org.springframework.core.annotation.Order +import org.springframework.jdbc.datasource.DataSourceUtils +import org.springframework.stereotype.Component +import org.springframework.transaction.support.TransactionTemplate import java.sql.Connection import java.sql.Date import java.sql.Types +import javax.sql.DataSource /** - * Loads the boards and their members from the seed files under `db/seed/boards`. - * - * Repeatable and keyed on the files' contents, so correcting a row is an edit and a deploy. - * Deletion outranks the files: a soft-deleted board or member stays deleted while its row is - * still listed, and removing the row is how it leaves for good. The recorded name is the key, - * so the files can correct a role, a nickname, a blurb or a photograph but not a name. - * A member is attached to the account matching their name once and never re-matched, so - * detaching somebody stays detached. `photo` and `portrait` are not written here: storing a - * picture needs the volume and converter a migration runner lacks, so `ShippedBoardArt` fills - * them once the application is up. + * Loads the boards from `db/seed/boards`, upserting on the recorded name so a second run is a no-op. + * Deletion outranks the files, an attached account is never re-matched, and the art follows separately. */ -@Suppress("unused", "ClassNaming") -class R__Boards_seed : BaseJavaMigration() { - /** Hashes the files' contents: Flyway re-runs a repeatable migration when its checksum moves. */ - override fun getChecksum(): Int = SEED_FILES.fold(11) { acc, name -> 31 * acc + read(name).hashCode() } +@Component +class ShippedBoards( + private val dataSource: DataSource, + private val transactions: TransactionTemplate, + private val seed: SeedCsv = BoardSeed.files, +) { + data class Applied( + val boards: Int, + val members: Int, + val leftDeleted: Int, + ) - override fun migrate(context: Context) { - val connection = context.connection + // One transaction, as the migration had. DataSourceUtils returns the connection it is bound to. + fun apply(): Applied = + transactions.execute { + val connection = DataSourceUtils.getConnection(dataSource) + try { + load(connection) + } finally { + DataSourceUtils.releaseConnection(connection, dataSource) + } + }!! + + private fun load(connection: Connection): Applied { val boards = parse(read("boards.csv")) val members = parse(read("members.csv")) @@ -56,6 +70,11 @@ class R__Boards_seed : BaseJavaMigration() { written, outcomes.size - written, ) + return Applied( + boards = boardIds.values.count { it != null }, + members = written, + leftDeleted = outcomes.size - written, + ) } /** What became of one member the file lists. */ @@ -260,11 +279,10 @@ class R__Boards_seed : BaseJavaMigration() { statement.executeQuery().use { rows -> if (rows.next()) rows.getLong(1) else null } } - private fun read(name: String): String = BoardSeed.files.read(name) + private fun read(name: String): String = seed.read(name) companion object { - private val log = LoggerFactory.getLogger(R__Boards_seed::class.java) - private val SEED_FILES = listOf("boards.csv", "members.csv") + private val log = LoggerFactory.getLogger(ShippedBoards::class.java) /** The sentinel a live row carries, as every soft-deleted table here uses it. */ private const val ACTIVE = "deleted_at = '9999-12-31 23:59:59'" @@ -278,3 +296,25 @@ class R__Boards_seed : BaseJavaMigration() { fun parse(content: String): List> = SeedCsv.parse(content) } } + +/** + * A separate bean so the transaction is opened by the proxy, and a failure never blocks start-up. + */ +@Component +class ShippedBoardsOnStartup( + private val boards: ShippedBoards, +) { + @Order(SeedOrder.RECORDS) + @EventListener(ApplicationReadyEvent::class) + fun onReady() { + try { + boards.apply() + } catch (e: Exception) { + log.warn("[boards-seed] could not load the boards that ship: {}", e.message) + } + } + + private companion object { + val log = LoggerFactory.getLogger(ShippedBoardsOnStartup::class.java) + } +} diff --git a/services/api/src/main/kotlin/net/blueshell/api/esports/domain/ShippedArt.kt b/services/api/src/main/kotlin/net/blueshell/api/esports/domain/ShippedArt.kt index 4ca217fab..a836b83ff 100644 --- a/services/api/src/main/kotlin/net/blueshell/api/esports/domain/ShippedArt.kt +++ b/services/api/src/main/kotlin/net/blueshell/api/esports/domain/ShippedArt.kt @@ -7,11 +7,13 @@ import net.blueshell.api.file.api.FileService import net.blueshell.api.file.persistence.File import net.blueshell.api.shared.enums.FileType import net.blueshell.api.shared.seed.SeedCsv +import net.blueshell.api.shared.seed.SeedOrder import net.blueshell.api.user.api.UserService import net.blueshell.api.user.persistence.User import org.slf4j.LoggerFactory import org.springframework.boot.context.event.ApplicationReadyEvent import org.springframework.context.event.EventListener +import org.springframework.core.annotation.Order import org.springframework.stereotype.Component import org.springframework.transaction.support.TransactionTemplate @@ -240,6 +242,7 @@ class ShippedArt( class ShippedArtOnStartup( private val art: ShippedArt, ) { + @Order(SeedOrder.ART) @EventListener(ApplicationReadyEvent::class) fun onReady() { try { diff --git a/services/api/src/main/kotlin/db/migration/R__Esports_seed.kt b/services/api/src/main/kotlin/net/blueshell/api/esports/domain/ShippedEsports.kt similarity index 87% rename from services/api/src/main/kotlin/db/migration/R__Esports_seed.kt rename to services/api/src/main/kotlin/net/blueshell/api/esports/domain/ShippedEsports.kt index 19f6e787a..53f9954bb 100644 --- a/services/api/src/main/kotlin/db/migration/R__Esports_seed.kt +++ b/services/api/src/main/kotlin/net/blueshell/api/esports/domain/ShippedEsports.kt @@ -1,38 +1,51 @@ -package db.migration +package net.blueshell.api.esports.domain -import net.blueshell.api.esports.domain.EsportsSeed import net.blueshell.api.shared.seed.SeedCsv -import org.flywaydb.core.api.migration.BaseJavaMigration -import org.flywaydb.core.api.migration.Context +import net.blueshell.api.shared.seed.SeedOrder import org.slf4j.LoggerFactory +import org.springframework.boot.context.event.ApplicationReadyEvent +import org.springframework.context.event.EventListener +import org.springframework.core.annotation.Order +import org.springframework.jdbc.datasource.DataSourceUtils +import org.springframework.stereotype.Component +import org.springframework.transaction.support.TransactionTemplate import java.sql.Connection import java.sql.Date import java.sql.Statement import java.sql.Types +import javax.sql.DataSource /** - * Loads the recovered esports history from the seed files under `db/seed/esports`, which are its - * only record: one row per game, season, team and roster entry. - * - * Repeatable and keyed on the files' contents, so correcting a row is an edit and a deploy. - * Deletion outranks the files: a soft-deleted season, team or entry stays deleted while its row - * is still listed, and removing the row is how it leaves for good. A place is matched to the - * member whose name it carries once, when the place is created, and never re-matched, so - * detaching somebody stays detached. + * Loads the esports history from `db/seed/esports`, which is its only record. Upserts on the + * recorded name, deletion outranks the files, and an attached account is never re-matched. */ -@Suppress("unused", "ClassNaming") -class R__Esports_seed( - /** Flyway builds this with no arguments, so the shipped seed is the default. Tests pass their own. */ +@Component +class ShippedEsports( + private val dataSource: DataSource, + private val transactions: TransactionTemplate, + // Defaulted so tests can pass their own, as the migration allowed. private val seed: SeedCsv = EsportsSeed.files, -) : BaseJavaMigration() { - /** - * The files are the migration. Flyway re-runs a repeatable migration when its checksum - * moves, so hashing their contents is what makes an edit take effect. - */ - override fun getChecksum(): Int = SEED_FILES.fold(7) { acc, name -> 31 * acc + read(name).hashCode() } +) { + data class Applied( + val games: Int, + val seasons: Int, + val teams: Int, + val entries: Int, + val leftDeleted: Int, + ) + + // One transaction, as the migration had. DataSourceUtils returns the connection it is bound to. + fun apply(): Applied = + transactions.execute { + val connection = DataSourceUtils.getConnection(dataSource) + try { + load(connection) + } finally { + DataSourceUtils.releaseConnection(connection, dataSource) + } + }!! - override fun migrate(context: Context) { - val connection = context.connection + private fun load(connection: Connection): Applied { // The games come first: a team names one, and the database now enforces that it exists. val games = parse(read("games.csv")) games.forEach { row -> upsertGame(connection, row) } @@ -74,6 +87,13 @@ class R__Esports_seed( written, skipped, ) + return Applied( + games = games.size, + seasons = seasonIds.size, + teams = teamIds.size, + entries = written, + leftDeleted = skipped, + ) } /** @@ -401,18 +421,33 @@ class R__Esports_seed( private fun read(name: String): String = seed.read(name) companion object { - private val log = LoggerFactory.getLogger(R__Esports_seed::class.java) - private val SEED_FILES = listOf("games.csv", "seasons.csv", "teams.csv", "roster.csv") + private val log = LoggerFactory.getLogger(ShippedEsports::class.java) /** The sentinel a live row carries, as every soft-deleted table here uses it. */ private const val ACTIVE = "deleted_at = '9999-12-31 23:59:59'" - /** - * The rows of one seed file. - * - * Delegates to [SeedCsv], which the start-up step that puts the art on these records - * reads the same files with. Kept here as the name the migration's own tests call. - */ fun parse(content: String): List> = SeedCsv.parse(content) } } + +/** + * A separate bean so the transaction is opened by the proxy, and a failure never blocks start-up. + */ +@Component +class ShippedEsportsOnStartup( + private val esports: ShippedEsports, +) { + @Order(SeedOrder.RECORDS) + @EventListener(ApplicationReadyEvent::class) + fun onReady() { + try { + esports.apply() + } catch (e: Exception) { + log.warn("[esports-seed] could not load the esports history that ships: {}", e.message) + } + } + + private companion object { + val log = LoggerFactory.getLogger(ShippedEsportsOnStartup::class.java) + } +} diff --git a/services/api/src/main/kotlin/net/blueshell/api/shared/seed/SeedOrder.kt b/services/api/src/main/kotlin/net/blueshell/api/shared/seed/SeedOrder.kt new file mode 100644 index 000000000..618ebaa08 --- /dev/null +++ b/services/api/src/main/kotlin/net/blueshell/api/shared/seed/SeedOrder.kt @@ -0,0 +1,9 @@ +package net.blueshell.api.shared.seed + +/** + * Records before art: both are ApplicationReadyEvent listeners now, so nothing else orders them. + */ +object SeedOrder { + const val RECORDS = 100 + const val ART = 200 +} diff --git a/services/api/src/main/resources/application.yaml b/services/api/src/main/resources/application.yaml index 8a0da2b16..f5565825c 100644 --- a/services/api/src/main/resources/application.yaml +++ b/services/api/src/main/resources/application.yaml @@ -71,6 +71,9 @@ spring: enabled: true locations: classpath:db/migration validate-migration-naming: true + # The two seed repeatables are start-up steps now, but every database still has their history + # rows, and validate refuses an applied migration it cannot resolve. Versioned stays an error. + ignore-migration-patterns: repeatable:missing modulith: # The modules are nested under domain/, platform/ and infrastructure/ today, so the diff --git a/services/api/src/test/kotlin/db/migration/BoardSeedParsingTest.kt b/services/api/src/test/kotlin/net/blueshell/api/board/domain/BoardSeedParsingTest.kt similarity index 98% rename from services/api/src/test/kotlin/db/migration/BoardSeedParsingTest.kt rename to services/api/src/test/kotlin/net/blueshell/api/board/domain/BoardSeedParsingTest.kt index ba72f7925..b22102037 100644 --- a/services/api/src/test/kotlin/db/migration/BoardSeedParsingTest.kt +++ b/services/api/src/test/kotlin/net/blueshell/api/board/domain/BoardSeedParsingTest.kt @@ -1,6 +1,4 @@ -package db.migration - -import net.blueshell.api.board.domain.BoardSeed +package net.blueshell.api.board.domain import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import java.time.LocalDate diff --git a/services/api/src/test/kotlin/db/migration/EsportsSeedParsingTest.kt b/services/api/src/test/kotlin/net/blueshell/api/esports/domain/EsportsSeedParsingTest.kt similarity index 69% rename from services/api/src/test/kotlin/db/migration/EsportsSeedParsingTest.kt rename to services/api/src/test/kotlin/net/blueshell/api/esports/domain/EsportsSeedParsingTest.kt index 607ab9849..2b248504c 100644 --- a/services/api/src/test/kotlin/db/migration/EsportsSeedParsingTest.kt +++ b/services/api/src/test/kotlin/net/blueshell/api/esports/domain/EsportsSeedParsingTest.kt @@ -1,4 +1,4 @@ -package db.migration +package net.blueshell.api.esports.domain import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy @@ -7,7 +7,7 @@ import org.junit.jupiter.api.Test class EsportsSeedParsingTest { @Test fun `reads a row against the header rather than by position`() { - val rows = R__Esports_seed.parse("name,start_date,end_date\nAutumn 2020,2020-09-01,2021-01-31\n") + val rows = ShippedEsports.parse("name,start_date,end_date\nAutumn 2020,2020-09-01,2021-01-31\n") assertThat(rows).singleElement().isEqualTo( mapOf("name" to "Autumn 2020", "start_date" to "2020-09-01", "end_date" to "2021-01-31"), @@ -17,33 +17,33 @@ class EsportsSeedParsingTest { @Test fun `a quoted field keeps the comma inside it`() { // A team is free to have a comma in its name; it is still one field. - val rows = R__Esports_seed.parse("game,name\nVALORANT,\"BS Ohm, Sweet Ohm\"\n") + val rows = ShippedEsports.parse("game,name\nVALORANT,\"BS Ohm, Sweet Ohm\"\n") assertThat(rows.single()["name"]).isEqualTo("BS Ohm, Sweet Ohm") } @Test fun `a doubled quote inside a quoted field is one quote`() { - val rows = R__Esports_seed.parse("handle\n\"the \"\"wall\"\"\"\n") + val rows = ShippedEsports.parse("handle\n\"the \"\"wall\"\"\"\n") assertThat(rows.single()["handle"]).isEqualTo("the \"wall\"") } @Test fun `an empty field is empty rather than absent`() { - val rows = R__Esports_seed.parse("game,name,image\nSMASH,BS Smashers,\n") + val rows = ShippedEsports.parse("game,name,image\nSMASH,BS Smashers,\n") assertThat(rows.single()).containsEntry("image", "") } @Test fun `a file with only a header holds no records`() { - assertThat(R__Esports_seed.parse("name,start_date,end_date\n")).isEmpty() + assertThat(ShippedEsports.parse("name,start_date,end_date\n")).isEmpty() } @Test fun `a blank line is not a record`() { - val rows = R__Esports_seed.parse("name\nAutumn 2020\n\n") + val rows = ShippedEsports.parse("name\nAutumn 2020\n\n") assertThat(rows).hasSize(1) } @@ -51,7 +51,7 @@ class EsportsSeedParsingTest { @Test fun `a row that does not fit the header is refused rather than silently shifted`() { // A missing comma would otherwise put a season's end date into its start. - assertThatThrownBy { R__Esports_seed.parse("name,start_date,end_date\nAutumn,2020-09-01\n") } + assertThatThrownBy { ShippedEsports.parse("name,start_date,end_date\nAutumn,2020-09-01\n") } .isInstanceOf(IllegalArgumentException::class.java) .hasMessageContaining("header has 3") } diff --git a/services/api/src/test/kotlin/net/blueshell/api/shared/seed/SeedStartupOrderTest.kt b/services/api/src/test/kotlin/net/blueshell/api/shared/seed/SeedStartupOrderTest.kt new file mode 100644 index 000000000..0b2ca46a4 --- /dev/null +++ b/services/api/src/test/kotlin/net/blueshell/api/shared/seed/SeedStartupOrderTest.kt @@ -0,0 +1,43 @@ +package net.blueshell.api.shared.seed + +import net.blueshell.api.board.domain.ShippedBoardArtOnStartup +import net.blueshell.api.board.domain.ShippedBoardsOnStartup +import net.blueshell.api.esports.domain.ShippedArtOnStartup +import net.blueshell.api.esports.domain.ShippedEsportsOnStartup +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.core.annotation.AnnotatedElementUtils +import org.springframework.core.annotation.Order +import kotlin.reflect.jvm.javaMethod + +/** + * Spring resolves an @EventListener's order from the method, never the declaring class, so an + * @Order sitting on the class is ignored and the art can run before the records it needs. + */ +class SeedStartupOrderTest { + private fun orderOf(type: Class<*>): Int? { + val method = type.declaredMethods.single { it.name == "onReady" } + return AnnotatedElementUtils.findMergedAnnotation(method, Order::class.java)?.value + } + + @Test + fun `every seed listener carries its order on the method`() { + listOf( + ShippedBoardsOnStartup::class.java, + ShippedEsportsOnStartup::class.java, + ShippedBoardArtOnStartup::class.java, + ShippedArtOnStartup::class.java, + ).forEach { type -> + assertThat(orderOf(type)) + .describedAs("%s.onReady must carry @Order; on the class it is ignored", type.simpleName) + .isNotNull() + } + } + + @Test + fun `records run before the art that hangs on them`() { + assertThat(SeedOrder.RECORDS).isLessThan(SeedOrder.ART) + assertThat(orderOf(ShippedBoardsOnStartup::class.java)).isLessThan(orderOf(ShippedBoardArtOnStartup::class.java)) + assertThat(orderOf(ShippedEsportsOnStartup::class.java)).isLessThan(orderOf(ShippedArtOnStartup::class.java)) + } +}