diff --git a/.gitignore b/.gitignore index 2b8ea3f8..344d1c05 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,4 @@ target /spring-boot-migrator.log /.run/RsapiApplication.run.xml /.kotlin/sessions/ +/db-migration/jooq-src/main/ diff --git a/adapter/build.gradle.kts b/adapter/build.gradle.kts index 8d68360a..7502be27 100644 --- a/adapter/build.gradle.kts +++ b/adapter/build.gradle.kts @@ -1,12 +1,12 @@ plugins { alias(libs.plugins.kotlin.spring) - alias(libs.plugins.kotlin.kapt) id("java-library") id("java-test-fixtures") } dependencies { implementation(project("::openapi")) + implementation(project("::db-migration")) implementation(project("::core")) implementation("org.springframework.boot:spring-boot-starter-mail") implementation("org.springframework.boot:spring-boot-starter-security") @@ -14,14 +14,13 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-thymeleaf") implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("org.springframework.boot:spring-boot-starter-jooq") implementation(libs.swagger.annotations) implementation("jakarta.validation:jakarta.validation-api") implementation(libs.spring.security.oauth2.authorization.server) implementation("org.flywaydb:flyway-core") implementation("org.flywaydb:flyway-database-postgresql") - implementation(libs.jdbi3.spring5) - implementation(libs.jdbi3.kotlin) - implementation(libs.jdbi3.kotlin.sqlobject) + implementation(libs.jooq.core) implementation(libs.lazysodium.java) implementation(libs.jna) implementation("commons-codec:commons-codec") @@ -30,8 +29,6 @@ dependencies { implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation(libs.bootstrap) - kapt("org.springframework.boot:spring-boot-configuration-processor") - runtimeOnly("org.postgresql:postgresql") runtimeOnly("org.webjars:webjars-locator-lite") diff --git a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/CountryAdapter.kt b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/CountryAdapter.kt new file mode 100644 index 00000000..02365c8f --- /dev/null +++ b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/CountryAdapter.kt @@ -0,0 +1,61 @@ +package org.railwaystations.rsapi.adapter.db + +import org.jooq.DSLContext +import org.jooq.Record2 +import org.jooq.Result +import org.jooq.SelectJoinStep +import org.jooq.impl.DSL.multiset +import org.jooq.impl.DSL.noCondition +import org.jooq.impl.DSL.selectFrom +import org.railwaystations.rsapi.adapter.db.jooq.tables.records.CountryRecord +import org.railwaystations.rsapi.adapter.db.jooq.tables.records.ProviderappRecord +import org.railwaystations.rsapi.adapter.db.jooq.tables.references.CountryTable +import org.railwaystations.rsapi.adapter.db.jooq.tables.references.ProviderappTable +import org.railwaystations.rsapi.core.model.Country +import org.railwaystations.rsapi.core.model.License +import org.railwaystations.rsapi.core.model.ProviderApp +import org.railwaystations.rsapi.core.ports.outbound.CountryPort +import org.springframework.stereotype.Component + +@Component +class CountryAdapter(private val dsl: DSLContext) : CountryPort { + + override fun findById(id: String) = + selectCountriesWithProviderApps() + .where(CountryTable.id.eq(id)) + .fetchOne { record -> record.component1().toCountry(record.component2()) } + + private fun selectCountriesWithProviderApps(): SelectJoinStep>> = + dsl.select( + CountryTable, + multiset( + selectFrom(ProviderappTable) + .where(ProviderappTable.countrycode.eq(CountryTable.id)) + ) + ).from(CountryTable) + + private fun CountryRecord.toCountry(providerAppRecords: Result) = Country( + code = id, + name = name, + _email = email, + timetableUrlTemplate = timetableurltemplate, + overrideLicense = overridelicense?.let { License.valueOf(it) }, + active = active == true, + providerApps = providerAppRecords.map { it.toProviderApp() } + ) + + private fun ProviderappRecord.toProviderApp() = ProviderApp( + type = type, + name = name, + url = url + ) + + override fun list(onlyActive: Boolean): Set { + val condition = if (onlyActive) CountryTable.active.eq(true) else noCondition() + return selectCountriesWithProviderApps() + .where(condition) + .fetch { record -> record.component1().toCountry(record.component2()) } + .toSet() + } + +} diff --git a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/CountryDao.kt b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/CountryDao.kt deleted file mode 100644 index a49ca530..00000000 --- a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/CountryDao.kt +++ /dev/null @@ -1,95 +0,0 @@ -package org.railwaystations.rsapi.adapter.db - -import org.jdbi.v3.core.mapper.RowMapper -import org.jdbi.v3.core.result.LinkedHashMapRowReducer -import org.jdbi.v3.core.result.RowView -import org.jdbi.v3.core.statement.StatementContext -import org.jdbi.v3.sqlobject.config.RegisterRowMapper -import org.jdbi.v3.sqlobject.customizer.Bind -import org.jdbi.v3.sqlobject.statement.SqlQuery -import org.jdbi.v3.sqlobject.statement.UseRowReducer -import org.railwaystations.rsapi.core.model.Country -import org.railwaystations.rsapi.core.model.License -import org.railwaystations.rsapi.core.model.ProviderApp -import org.railwaystations.rsapi.core.ports.outbound.CountryPort -import java.sql.ResultSet -import java.sql.SQLException -import java.util.* - -interface CountryDao : CountryPort { - @SqlQuery( - """ - SELECT c.id c_id, c.name c_name, c.email c_email, c.timetableUrlTemplate c_timetableUrlTemplate, - c.overrideLicense c_overrideLicense, c.active c_active, p.type p_type, p.name p_name, p.url p_url - FROM countries c - LEFT JOIN providerApps p ON c.id = p.countryCode - WHERE c.id = :id - - """ - ) - @UseRowReducer(CountryProviderAppReducer::class) - @RegisterRowMapper(CountryMapper::class) - @RegisterRowMapper( - ProviderAppMapper::class - ) - override fun findById(@Bind("id") id: String): Country? - - @SqlQuery( - """ - SELECT c.id c_id, c.name c_name, c.email c_email, c.timetableUrlTemplate c_timetableUrlTemplate, - c.overrideLicense c_overrideLicense, c.active c_active, p.type p_type, p.name p_name, p.url p_url - FROM countries c - LEFT JOIN providerApps p ON c.id = p.countryCode - WHERE :onlyActive = false OR c.active = true - - """ - ) - @UseRowReducer(CountryProviderAppReducer::class) - @RegisterRowMapper(CountryMapper::class) - @RegisterRowMapper( - ProviderAppMapper::class - ) - override fun list(@Bind("onlyActive") onlyActive: Boolean): Set - - class CountryMapper : RowMapper { - @Throws(SQLException::class) - override fun map(rs: ResultSet, ctx: StatementContext): Country { - val overrideLicense = rs.getString("c_overrideLicense") - return Country( - code = rs.getString("c_id"), - name = rs.getString("c_name"), - email = rs.getString("c_email"), - timetableUrlTemplate = rs.getString("c_timetableUrlTemplate"), - overrideLicense = overrideLicense?.let { License.valueOf(it) }, - active = rs.getBoolean("c_active"), - ) - } - } - - class ProviderAppMapper : RowMapper { - @Throws(SQLException::class) - override fun map(rs: ResultSet, ctx: StatementContext): ProviderApp { - return ProviderApp( - type = rs.getString("p_type"), - name = rs.getString("p_name"), - url = rs.getString("p_url") - ) - } - } - - class CountryProviderAppReducer : LinkedHashMapRowReducer { - override fun accumulate(container: MutableMap, rowView: RowView) { - val country = container.computeIfAbsent( - rowView.getColumn("c_id", String::class.java) - ) { _: String? -> - rowView.getRow( - Country::class.java - ) - } - - if (rowView.getColumn("p_type", String::class.java) != null) { - country?.providerApps?.add(rowView.getRow(ProviderApp::class.java)) - } - } - } -} diff --git a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/InboxAdapter.kt b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/InboxAdapter.kt new file mode 100644 index 00000000..2ca8d480 --- /dev/null +++ b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/InboxAdapter.kt @@ -0,0 +1,274 @@ +package org.railwaystations.rsapi.adapter.db + +import org.jooq.DSLContext +import org.jooq.Record7 +import org.jooq.SelectOnConditionStep +import org.jooq.impl.DSL.power +import org.jooq.impl.DSL.sqrt +import org.jooq.impl.DSL.value +import org.railwaystations.rsapi.adapter.db.jooq.tables.records.InboxRecord +import org.railwaystations.rsapi.adapter.db.jooq.tables.records.StationRecord +import org.railwaystations.rsapi.adapter.db.jooq.tables.references.InboxTable +import org.railwaystations.rsapi.adapter.db.jooq.tables.references.PhotoTable +import org.railwaystations.rsapi.adapter.db.jooq.tables.references.StationTable +import org.railwaystations.rsapi.adapter.db.jooq.tables.references.UserTable +import org.railwaystations.rsapi.core.model.Coordinates +import org.railwaystations.rsapi.core.model.InboxEntry +import org.railwaystations.rsapi.core.model.ProblemReportType +import org.railwaystations.rsapi.core.model.PublicInboxEntry +import org.railwaystations.rsapi.core.ports.outbound.InboxPort +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional +import java.math.BigDecimal +import java.time.Instant + +@Component +class InboxAdapter(private val dsl: DSLContext) : InboxPort { + + override fun findById(id: Long): InboxEntry? { + return selectInboxEntries() + .where(InboxTable.id.eq(id)) + .fetchOne()?.toInboxEntry() + } + + private fun selectInboxEntries(): SelectOnConditionStep> = + dsl.select( + InboxTable, + StationTable.title, + StationTable.lat, + StationTable.lon, + UserTable.name, + UserTable.email, + PhotoTable.urlpath + ) + .from(InboxTable) + .join(UserTable).on(UserTable.id.eq(InboxTable.photographerid)) + .leftJoin(StationTable) + .on(StationTable.countrycode.eq(InboxTable.countrycode).and(StationTable.id.eq(InboxTable.stationid))) + .leftJoin(PhotoTable).on( + PhotoTable.countrycode.eq(InboxTable.countrycode) + .and( + PhotoTable.stationid.eq(InboxTable.stationid) + .and( + PhotoTable.id.eq(InboxTable.photoid) + .or(PhotoTable.primary.eq(true).and(InboxTable.photoid.isNull)) + ) + ) + ) + + private fun Record7.toInboxEntry() = + InboxEntry( + id = value1().id!!, + countryCode = value1().countrycode, + stationId = value1().stationid, + photoId = value1().photoid, + title = value2(), + newTitle = value1().title, + coordinates = createCoordinates(value3(), value4()), + newCoordinates = createCoordinates(value1().lat, value1().lon), + photographerId = value1().photographerid, + photographerNickname = value5(), + photographerEmail = value6(), + extension = value1().extension, + comment = value1().comment, + rejectReason = value1().rejectreason, + createdAt = value1().createdat, + done = value1().done, + existingPhotoUrlPath = value7(), + crc32 = value1().crc32, + problemReportType = value1().problemreporttype?.let { ProblemReportType.valueOf(it) }, + active = value1().active, + notified = value1().notified, + posted = value1().posted + ) + + private fun createCoordinates(lat: Double?, lon: Double?): Coordinates? { + if (lat == null || lon == null) return null + + return Coordinates(lat, lon) + } + + override fun findPendingInboxEntries() = + selectInboxEntries() + .where(InboxTable.done.eq(false)) + .orderBy(InboxTable.id) + .fetch().map { it.toInboxEntry() } + + override fun findOldestImportedPhotoNotYetPosted() = + selectInboxEntries() + .where( + InboxTable.done.eq(true) + .and(InboxTable.rejectreason.isNull) + .and(InboxTable.extension.isNotNull) + .and(InboxTable.posted.eq(false)) + ) + .orderBy(InboxTable.createdat) + .limit(1) + .fetchOne()?.toInboxEntry() + + override fun findPublicInboxEntries(): List { + val result = dsl.select(InboxTable, StationTable) + .from(InboxTable) + .leftJoin(StationTable) + .on(StationTable.countrycode.eq(InboxTable.countrycode).and(StationTable.id.eq(InboxTable.stationid))) + .where(InboxTable.done.eq(false).and(InboxTable.problemreporttype.isNull)) + .fetch() + + return result.map { it.value1().toPublicInboxEntry(it.value2()) } + } + + private fun InboxRecord.toPublicInboxEntry(stationRecord: StationRecord?) = + PublicInboxEntry( + countryCode = countrycode, + stationId = stationid, + title = stationRecord?.title ?: title!!, + coordinates = stationRecord?.let { Coordinates(it.lat, it.lon) } ?: Coordinates(lat ?: 0.0, lon ?: 0.0) + ) + + @Transactional + override fun insert(inboxEntry: InboxEntry): Long { + val inboxRecord = InboxRecord( + id = null, + photographerid = inboxEntry.photographerId, + countrycode = inboxEntry.countryCode, + stationid = inboxEntry.stationId, + title = inboxEntry.title, + lat = inboxEntry.lat, + lon = inboxEntry.lon, + extension = inboxEntry.extension, + comment = inboxEntry.comment, + rejectreason = inboxEntry.rejectReason, + done = inboxEntry.done, + problemreporttype = inboxEntry.problemReportType?.name, + active = inboxEntry.active, + crc32 = inboxEntry.crc32, + notified = inboxEntry.notified, + createdat = inboxEntry.createdAt, + photoid = inboxEntry.photoId, + posted = inboxEntry.posted, + ) + dsl.attach(inboxRecord) + inboxRecord.store() + return inboxRecord.id!! + } + + @Transactional + override fun reject(id: Long, rejectReason: String) { + dsl.update(InboxTable) + .set(InboxTable.rejectreason, rejectReason) + .set(InboxTable.done, true) + .where(InboxTable.id.eq(id)) + .execute() + } + + @Transactional + override fun done(id: Long) { + dsl.update(InboxTable) + .set(InboxTable.done, true) + .where(InboxTable.id.eq(id)) + .execute() + } + + override fun countPendingInboxEntriesForStation(id: Long?, countryCode: String, stationId: String): Int = + dsl.selectCount() + .from(InboxTable) + .where( + InboxTable.done.eq(false) + .and(InboxTable.countrycode.eq(countryCode)) + .and(InboxTable.stationid.eq(stationId)) + .and(id?.let { InboxTable.id.ne(it) }) + ) + .fetchOne(0, Int::class.java) ?: 0 + + override fun countPendingInboxEntries(): Int = + dsl.selectCount() + .from(InboxTable) + .where(InboxTable.done.eq(false)) + .fetchOne(0, Int::class.java) ?: 0 + + /** + * Count nearby pending uploads using simple pythagoras (only valid for a few km) + */ + override fun countPendingInboxEntriesForNearbyCoordinates(id: Long?, coordinates: Coordinates): Int = + dsl.selectCount() + .from(InboxTable) + .where( + InboxTable.done.eq(false) + .and(InboxTable.id.ne(id).or(value(id).isNull)) + .and( + sqrt( + power(value(71.5).mul(InboxTable.lon.minus(coordinates.lon)), 2) + .plus(power(value(111.3).mul(InboxTable.lat.minus(coordinates.lat)), 2)) + ).lt(BigDecimal(0.5)) + ) + ) + .fetchOne(0, Int::class.java) ?: 0 + + @Transactional + override fun updateCrc32(id: Long, crc32: Long) { + dsl.update(InboxTable) + .set(InboxTable.crc32, crc32) + .where(InboxTable.id.eq(id)) + .execute() + } + + override fun findInboxEntriesToNotify() = + selectInboxEntries() + .where(InboxTable.done.eq(true).and(InboxTable.notified.eq(false))) + .orderBy(InboxTable.id) + .fetch().map { it.toInboxEntry() } + + @Transactional + override fun updateNotified(ids: List) { + dsl.update(InboxTable) + .set(InboxTable.notified, true) + .where(InboxTable.id.`in`(ids)) + .execute() + } + + @Transactional + override fun updatePosted(id: Long) { + dsl.update(InboxTable) + .set(InboxTable.posted, true) + .where(InboxTable.id.eq(id)) + .execute() + } + + @Transactional + override fun updatePhotoId(id: Long, photoId: Long) { + dsl.update(InboxTable) + .set(InboxTable.photoid, photoId) + .where(InboxTable.id.eq(id)) + .execute() + } + + @Transactional + override fun updateMissingStationImported(id: Long, countryCode: String, stationId: String, title: String) { + dsl.update(InboxTable) + .set(InboxTable.done, true) + .set(InboxTable.countrycode, countryCode) + .set(InboxTable.stationid, stationId) + .set(InboxTable.title, title) + .where(InboxTable.id.eq(id)) + .execute() + } + + override fun findByUser(photographerId: Long, includeCompletedEntries: Boolean) = + selectInboxEntries() + .where( + InboxTable.photographerid.eq(photographerId) + .and(InboxTable.done.eq(false).or(value(includeCompletedEntries).eq(true))) + ) + .orderBy(InboxTable.id.desc()) + .fetch().map { it.toInboxEntry() } + + override fun findPendingByStation(countryCode: String, stationId: String) = + selectInboxEntries() + .where( + InboxTable.countrycode.eq(countryCode) + .and(InboxTable.stationid.eq(stationId)) + .and(InboxTable.done.eq(false)) + ) + .fetch().map { it.toInboxEntry() } + +} diff --git a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/InboxDao.kt b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/InboxDao.kt deleted file mode 100644 index c1192ac8..00000000 --- a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/InboxDao.kt +++ /dev/null @@ -1,220 +0,0 @@ -package org.railwaystations.rsapi.adapter.db - -import org.jdbi.v3.core.mapper.RowMapper -import org.jdbi.v3.core.statement.StatementContext -import org.jdbi.v3.sqlobject.config.RegisterRowMapper -import org.jdbi.v3.sqlobject.customizer.Bind -import org.jdbi.v3.sqlobject.customizer.BindBean -import org.jdbi.v3.sqlobject.customizer.BindList -import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys -import org.jdbi.v3.sqlobject.statement.SqlQuery -import org.jdbi.v3.sqlobject.statement.SqlUpdate -import org.railwaystations.rsapi.core.model.Coordinates -import org.railwaystations.rsapi.core.model.InboxEntry -import org.railwaystations.rsapi.core.model.ProblemReportType -import org.railwaystations.rsapi.core.model.PublicInboxEntry -import org.railwaystations.rsapi.core.ports.outbound.InboxPort -import java.sql.ResultSet -import java.sql.SQLException - -private const val JOIN_QUERY: String = """ - SELECT i.id, i.countryCode, i.stationId, i.photoId, i.title i_title, s.title s_title, i.lat i_lat, i.lon i_lon, s.lat s_lat, s.lon s_lon, - i.photographerId, u.name photographerNickname, u.email photographerEmail, i.extension, i.comment, i.rejectReason, i.createdAt, - i.done, i.problemReportType, i.active, i.crc32, i.notified, p.urlPath, i.posted, - ( - SELECT COUNT(*) - FROM inbox i2 - WHERE i2.countryCode IS NOT NULL AND i2.countryCode = i.countryCode - AND i2.stationId IS NOT NULL AND i2.stationId = i.stationId AND i2.done = false AND i2.id != i.id - ) AS conflict - FROM inbox i - LEFT JOIN stations s ON s.countryCode = i.countryCode AND s.id = i.stationId - LEFT JOIN users u ON u.id = i.photographerId - LEFT JOIN photos p ON p.countryCode = i.countryCode AND p.stationId = i.stationId AND ( ( p.primary = true AND i.photoId IS NULL) OR ( p.id = i.photoId ) ) - - """ -private const val COUNTRY_CODE: String = "countryCode" -private const val STATION_ID: String = "stationId" -private const val ID: String = "id" -private const val PHOTOGRAPHER_ID: String = "photographerId" -private const val COORDS: String = "coords" - -interface InboxDao : InboxPort { - @SqlQuery("$JOIN_QUERY WHERE i.id = :id") - @RegisterRowMapper( - InboxEntryMapper::class - ) - override fun findById(@Bind(ID) id: Long): InboxEntry? - - @SqlQuery("$JOIN_QUERY WHERE i.done = false ORDER BY id") - @RegisterRowMapper( - InboxEntryMapper::class - ) - override fun findPendingInboxEntries(): List - - @SqlQuery("$JOIN_QUERY WHERE i.done = true AND i.rejectReason IS NULL AND i.extension IS NOT NULL AND i.posted = false ORDER BY createdAt DESC LIMIT 1") - @RegisterRowMapper( - InboxEntryMapper::class - ) - override fun findOldestImportedPhotoNotYetPosted(): InboxEntry? - - @SqlQuery( - """ - SELECT i.countryCode, i.stationId, i.title i_title, s.title s_title, i.lat i_lat, i.lon i_lon, s.lat s_lat, s.lon s_lon - FROM inbox i - LEFT JOIN stations s ON s.countryCode = i.countryCode AND s.id = i.stationId - WHERE i.done = false AND (i.problemReportType IS NULL OR i.problemReportType = '') - - """ - ) - @RegisterRowMapper(PublicInboxEntryMapper::class) - override fun findPublicInboxEntries(): List - - @SqlUpdate( - """ - INSERT INTO inbox (countryCode, stationId, photoId, title, lat, lon, photographerId, extension, comment, done, createdAt, problemReportType, active) - VALUES (:countryCode, :stationId, :photoId, :title, :lat, :lon, :photographerId, :extension, :comment, :done, :createdAt, :problemReportType, :active) - - """ - ) - @GetGeneratedKeys(ID) - override fun insert(@BindBean inboxEntry: InboxEntry): Long - - @SqlUpdate("UPDATE inbox SET rejectReason = :rejectReason, done = true WHERE id = :id") - override fun reject(@Bind(ID) id: Long, @Bind("rejectReason") rejectReason: String) - - @SqlUpdate("UPDATE inbox SET done = true WHERE id = :id") - override fun done(@Bind(ID) id: Long) - - @SqlQuery("SELECT COUNT(*) FROM inbox WHERE countryCode = :countryCode AND stationId = :stationId AND done = false AND (:id IS NULL OR id <> :id)") - override fun countPendingInboxEntriesForStation( - @Bind(ID) id: Long?, @Bind(COUNTRY_CODE) countryCode: String, @Bind(STATION_ID) stationId: String - ): Int - - @SqlQuery("SELECT COUNT(*) FROM inbox WHERE done = false") - override fun countPendingInboxEntries(): Long - - /** - * Count nearby pending uploads using simple pythagoras (only valid for a few km) - */ - @SqlQuery("SELECT COUNT(*) FROM inbox WHERE SQRT(POWER(71.5 * (lon - :coords.lon),2) + POWER(111.3 * (lat - :coords.lat),2)) < 0.5 AND done = false AND (:id IS NULL OR id <> :id)") - override fun countPendingInboxEntriesForNearbyCoordinates( - @Bind(ID) id: Long?, - @BindBean(COORDS) coordinates: Coordinates - ): Int - - @SqlUpdate("UPDATE inbox SET crc32 = :crc32 WHERE id = :id") - override fun updateCrc32(@Bind(ID) id: Long, @Bind("crc32") crc32: Long) - - @SqlQuery("$JOIN_QUERY WHERE i.done = true AND i.notified = false") - @RegisterRowMapper(InboxEntryMapper::class) - override fun findInboxEntriesToNotify(): List - - @SqlUpdate("UPDATE inbox SET notified = true WHERE id IN ()") - override fun updateNotified(@BindList("ids") ids: List) - - @SqlUpdate("UPDATE inbox SET posted = true WHERE id = :id") - override fun updatePosted(@Bind("id") id: Long) - - @SqlUpdate("UPDATE inbox SET photoId = :photoId WHERE id = :id") - override fun updatePhotoId(@Bind("id") id: Long, @Bind("photoId") photoId: Long) - - @SqlUpdate("UPDATE inbox SET done = true, stationId = :stationId, countryCode = :countryCode, title = :title WHERE id = :id") - override fun updateMissingStationImported( - @Bind("id") id: Long, - @Bind("countryCode") countryCode: String, - @Bind("stationId") stationId: String, - @Bind("title") title: String - ) - - @SqlQuery("$JOIN_QUERY WHERE i.photographerId = :photographerId AND (i.done = false OR :showCompletedEntries = true) ORDER BY i.id DESC") - @RegisterRowMapper(InboxEntryMapper::class) - override fun findByUser( - @Bind(PHOTOGRAPHER_ID) photographerId: Int, - @Bind("showCompletedEntries") showCompletedEntries: Boolean - ): List - - @SqlQuery("$JOIN_QUERY WHERE i.countryCode = :countryCode AND i.stationId = :stationId AND i.done = false") - @RegisterRowMapper(InboxEntryMapper::class) - override fun findPendingByStation( - @Bind("countryCode") countryCode: String, - @Bind("stationId") stationId: String - ): List - - class InboxEntryMapper : RowMapper { - @Throws(SQLException::class) - override fun map(rs: ResultSet, ctx: StatementContext): InboxEntry { - val id = rs.getLong(ID) - val done = rs.getBoolean("done") - val problemReportType = rs.getString("problemReportType") - val extension = rs.getString("extension") - var active: Boolean? = rs.getBoolean("active") - if (rs.wasNull()) { - active = null - } - var crc32: Long? = rs.getLong("crc32") - if (rs.wasNull()) { - crc32 = null - } - var photoId: Long? = rs.getLong("photoId") - if (rs.wasNull()) { - photoId = null - } - return InboxEntry( - id = id, - countryCode = rs.getString(COUNTRY_CODE), - stationId = rs.getString(STATION_ID), - photoId = photoId, - title = rs.getString("s_title"), - newTitle = rs.getString("i_title"), - coordinates = getCoordinates(rs, "s_"), - newCoordinates = getCoordinates(rs, "i_"), - photographerId = rs.getInt(PHOTOGRAPHER_ID), - photographerNickname = rs.getString("photographerNickname"), - photographerEmail = rs.getString("photographerEmail"), - extension = extension, - comment = rs.getString("comment"), - rejectReason = rs.getString("rejectReason"), - createdAt = rs.getTimestamp("createdAt").toInstant(), - done = done, - existingPhotoUrlPath = rs.getString("urlPath"), - crc32 = crc32, - conflict = rs.getInt("conflict") > 0, - problemReportType = if (problemReportType != null) ProblemReportType.valueOf(problemReportType) else null, - processed = false, - inboxUrl = null, - ds100 = null, - active = active, - createStation = null, - notified = rs.getBoolean("notified"), - posted = rs.getBoolean("posted") - ) - } - } - - class PublicInboxEntryMapper : RowMapper { - override fun map(rs: ResultSet, ctx: StatementContext): PublicInboxEntry { - var title = rs.getString("s_title") - var coordinates = getCoordinates(rs, "s_") - val stationId = rs.getString(STATION_ID) - if (stationId == null) { - title = rs.getString("i_title") - coordinates = getCoordinates(rs, "i_") - } - return PublicInboxEntry( - countryCode = rs.getString(COUNTRY_CODE), - stationId = stationId, - title = title!!, - coordinates = coordinates - ) - } - } - -} - -/** - * Get the uploaded coordinates, if not present or not valid gets the station coordinates - */ -fun getCoordinates(rs: ResultSet, columnPrefix: String): Coordinates { - return Coordinates(rs.getDouble(columnPrefix + "lat"), rs.getDouble(columnPrefix + "lon")) -} diff --git a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/JdbiConfiguration.kt b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/JdbiConfiguration.kt deleted file mode 100644 index 6dd13c75..00000000 --- a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/JdbiConfiguration.kt +++ /dev/null @@ -1,50 +0,0 @@ -package org.railwaystations.rsapi.adapter.db - -import org.jdbi.v3.core.Jdbi -import org.jdbi.v3.core.kotlin.KotlinPlugin -import org.jdbi.v3.sqlobject.SqlObjectPlugin -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy -import javax.sql.DataSource - -@Configuration -class JdbiConfiguration { - @Bean - fun jdbi(ds: DataSource): Jdbi { - val proxy = TransactionAwareDataSourceProxy(ds) - return Jdbi.create(proxy) - .installPlugin(KotlinPlugin()) - .installPlugin(SqlObjectPlugin()) - } - - @Bean - fun countryDao(jdbi: Jdbi): CountryDao { - return jdbi.onDemand(CountryDao::class.java) - } - - @Bean - fun inboxDao(jdbi: Jdbi): InboxDao { - return jdbi.onDemand(InboxDao::class.java) - } - - @Bean - fun photoDao(jdbi: Jdbi): PhotoDao { - return jdbi.onDemand(PhotoDao::class.java) - } - - @Bean - fun stationDao(jdbi: Jdbi): StationDao { - return jdbi.onDemand(StationDao::class.java) - } - - @Bean - fun userDao(jdbi: Jdbi): UserDao { - return jdbi.onDemand(UserDao::class.java) - } - - @Bean - fun oAuth2AuthorizationDao(jdbi: Jdbi): OAuth2AuthorizationDao { - return jdbi.onDemand(OAuth2AuthorizationDao::class.java) - } -} diff --git a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/JooqCustomizerConfiguration.kt b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/JooqCustomizerConfiguration.kt new file mode 100644 index 00000000..a692d302 --- /dev/null +++ b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/JooqCustomizerConfiguration.kt @@ -0,0 +1,16 @@ +package org.railwaystations.rsapi.adapter.db + +import org.springframework.boot.autoconfigure.jooq.DefaultConfigurationCustomizer +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class JooqCustomizerConfiguration { + + @Bean + fun configurationCustomizer() = + DefaultConfigurationCustomizer { + it.settings().withExecuteWithOptimisticLocking(true) + } + +} diff --git a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/OAuth2AuthorizationAdapter.kt b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/OAuth2AuthorizationAdapter.kt new file mode 100644 index 00000000..9c6aa8d7 --- /dev/null +++ b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/OAuth2AuthorizationAdapter.kt @@ -0,0 +1,40 @@ +package org.railwaystations.rsapi.adapter.db + +import org.jooq.DSLContext +import org.railwaystations.rsapi.adapter.db.jooq.tables.references.Oauth2AuthorizationTable +import org.railwaystations.rsapi.core.ports.outbound.OAuth2AuthorizationPort +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional +import java.time.Instant + +@Component +class OAuth2AuthorizationAdapter(private val dsl: DSLContext) : OAuth2AuthorizationPort { + + @Transactional + override fun deleteExpiredTokens(now: Instant): Int { + return dsl.deleteFrom(Oauth2AuthorizationTable) + .where( + Oauth2AuthorizationTable.authorizationCodeValue.isNull.or( + Oauth2AuthorizationTable.authorizationCodeExpiresAt.lt(now) + ) + ) + .and( + Oauth2AuthorizationTable.accessTokenValue.isNull.or( + (Oauth2AuthorizationTable.accessTokenExpiresAt.lt(now)) + ) + ) + .and( + Oauth2AuthorizationTable.refreshTokenValue.isNull.or( + Oauth2AuthorizationTable.refreshTokenExpiresAt.lt(now) + ) + ) + .execute() + } + + @Transactional + override fun deleteAllByUser(principalName: String) { + dsl.deleteFrom(Oauth2AuthorizationTable) + .where(Oauth2AuthorizationTable.principalName.eq(principalName)) + .execute() + } +} diff --git a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/OAuth2AuthorizationDao.kt b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/OAuth2AuthorizationDao.kt deleted file mode 100644 index 6636cfe0..00000000 --- a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/OAuth2AuthorizationDao.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.railwaystations.rsapi.adapter.db - -import org.jdbi.v3.sqlobject.customizer.Bind -import org.jdbi.v3.sqlobject.statement.SqlUpdate -import org.railwaystations.rsapi.core.ports.outbound.OAuth2AuthorizationPort -import java.time.Instant - -interface OAuth2AuthorizationDao : OAuth2AuthorizationPort { - @SqlUpdate( - """ - DELETE FROM oauth2_authorization - WHERE (authorization_code_value IS NULL OR authorization_code_expires_at < :now) - AND - (access_token_value IS NULL OR access_token_expires_at < :now) - AND - (refresh_token_value IS NULL OR refresh_token_expires_at < :now) - - """ - ) - override fun deleteExpiredTokens(@Bind("now") now: Instant?): Int - - @SqlUpdate( - """ - DELETE FROM oauth2_authorization - WHERE principal_name = :principalName - - """ - ) - override fun deleteAllByUser(@Bind("principalName") principalName: String?) -} diff --git a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/PhotoAdapter.kt b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/PhotoAdapter.kt new file mode 100644 index 00000000..97706512 --- /dev/null +++ b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/PhotoAdapter.kt @@ -0,0 +1,90 @@ +package org.railwaystations.rsapi.adapter.db + +import org.jooq.DSLContext +import org.railwaystations.rsapi.adapter.db.jooq.tables.records.PhotoRecord +import org.railwaystations.rsapi.adapter.db.jooq.tables.references.PhotoTable +import org.railwaystations.rsapi.core.model.Photo +import org.railwaystations.rsapi.core.model.Station +import org.railwaystations.rsapi.core.ports.outbound.PhotoPort +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional + +@Component +class PhotoAdapter(private val dsl: DSLContext) : PhotoPort { + + @Transactional + override fun insert(photo: Photo): Long { + val photosRecord = PhotoRecord( + id = null, + countrycode = photo.stationKey.country, + stationid = photo.stationKey.id, + primary = photo.primary, + outdated = photo.outdated, + urlpath = photo.urlPath, + license = photo.license.name, + photographerid = photo.photographer.id, + createdat = photo.createdAt, + ) + dsl.attach(photosRecord) + photosRecord.store() + return photosRecord.id!! + } + + @Transactional + override fun update(photo: Photo) { + dsl.update(PhotoTable) + .set(PhotoTable.primary, photo.primary) + .set(PhotoTable.urlpath, photo.urlPath) + .set(PhotoTable.license, photo.license.name) + .set(PhotoTable.photographerid, photo.photographer.id) + .set(PhotoTable.createdat, photo.createdAt) + .set(PhotoTable.outdated, photo.outdated) + .where(PhotoTable.id.eq(photo.id)) + .execute() + } + + @Transactional + override fun delete(id: Long) { + dsl.deleteFrom(PhotoTable) + .where(PhotoTable.id.eq(id)) + .execute() + } + + @Transactional + override fun updatePhotoOutdated(id: Long) { + dsl.update(PhotoTable) + .set(PhotoTable.outdated, true) + .where(PhotoTable.id.eq(id)) + .execute() + } + + @Transactional + override fun setAllPhotosForStationSecondary(key: Station.Key) { + dsl.update(PhotoTable) + .set(PhotoTable.primary, false) + .where(PhotoTable.countrycode.eq(key.country).and(PhotoTable.stationid.eq(key.id))) + .execute() + + } + + @Transactional + override fun setPrimary(id: Long) { + dsl.update(PhotoTable) + .set(PhotoTable.primary, true) + .where(PhotoTable.id.eq(id)) + .execute() + } + + override fun countPhotos(): Long { + return dsl.fetchCount(PhotoTable).toLong() + } + + override fun findNthPhotoId(n: Long): Long { + return dsl.select(PhotoTable.id) + .from(PhotoTable) + .offset(n) + .limit(1) + .fetchOne(PhotoTable.id)!! + } + +} diff --git a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/PhotoDao.kt b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/PhotoDao.kt deleted file mode 100644 index e777482b..00000000 --- a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/PhotoDao.kt +++ /dev/null @@ -1,37 +0,0 @@ -package org.railwaystations.rsapi.adapter.db - -import org.jdbi.v3.sqlobject.customizer.Bind -import org.jdbi.v3.sqlobject.customizer.BindBean -import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys -import org.jdbi.v3.sqlobject.statement.SqlQuery -import org.jdbi.v3.sqlobject.statement.SqlUpdate -import org.railwaystations.rsapi.core.model.Photo -import org.railwaystations.rsapi.core.model.Station -import org.railwaystations.rsapi.core.ports.outbound.PhotoPort - -interface PhotoDao : PhotoPort { - @SqlUpdate("INSERT INTO photos (countryCode, stationId, \"primary\", urlPath, license, photographerId, createdAt) VALUES (:stationKey.country, :stationKey.id, :primary, :urlPath, :license, :photographer.id, :createdAt)") - @GetGeneratedKeys("id") - override fun insert(@BindBean photo: Photo?): Long - - @SqlUpdate("UPDATE photos SET \"primary\" = :primary, urlPath = :urlPath, license = :license, photographerId = :photographer.id, createdAt = :createdAt, outdated = :outdated WHERE id = :id") - override fun update(@BindBean photo: Photo?) - - @SqlUpdate("DELETE FROM photos WHERE id = :id") - override fun delete(@Bind("id") id: Long) - - @SqlUpdate("UPDATE photos SET outdated = true WHERE id = :id") - override fun updatePhotoOutdated(@Bind("id") id: Long) - - @SqlUpdate("UPDATE photos SET \"primary\" = false WHERE countryCode = :country and stationId = :id") - override fun setAllPhotosForStationSecondary(@BindBean key: Station.Key?) - - @SqlUpdate("UPDATE photos SET \"primary\" = true WHERE id = :id") - override fun setPrimary(@Bind("id") id: Long) - - @SqlQuery("SELECT count(*) FROM photos") - override fun countPhotos(): Long - - @SqlQuery("SELECT id FROM photos LIMIT 1 OFFSET :n") - override fun findNthPhotoId(@Bind("n") n: Long): Long -} diff --git a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/StationAdapter.kt b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/StationAdapter.kt new file mode 100644 index 00000000..3817c228 --- /dev/null +++ b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/StationAdapter.kt @@ -0,0 +1,264 @@ +package org.railwaystations.rsapi.adapter.db + +import org.jooq.DSLContext +import org.jooq.Record12 +import org.jooq.Record4 +import org.jooq.SelectJoinStep +import org.jooq.SelectOnConditionStep +import org.jooq.impl.DSL.cast +import org.jooq.impl.DSL.count +import org.jooq.impl.DSL.countDistinct +import org.jooq.impl.DSL.max +import org.jooq.impl.DSL.or +import org.jooq.impl.DSL.power +import org.jooq.impl.DSL.sqrt +import org.jooq.impl.DSL.substring +import org.jooq.impl.DSL.value +import org.railwaystations.rsapi.adapter.db.jooq.tables.records.StationRecord +import org.railwaystations.rsapi.adapter.db.jooq.tables.references.PhotoTable +import org.railwaystations.rsapi.adapter.db.jooq.tables.references.StationTable +import org.railwaystations.rsapi.adapter.db.jooq.tables.references.UserTable +import org.railwaystations.rsapi.core.model.ANONYM +import org.railwaystations.rsapi.core.model.Coordinates +import org.railwaystations.rsapi.core.model.Photo +import org.railwaystations.rsapi.core.model.Station +import org.railwaystations.rsapi.core.model.Statistic +import org.railwaystations.rsapi.core.model.User +import org.railwaystations.rsapi.core.model.nameToLicense +import org.railwaystations.rsapi.core.ports.outbound.StationPort +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional +import java.math.BigDecimal +import java.time.Instant +import java.util.* + +@Component +class StationAdapter(private val dsl: DSLContext) : StationPort { + + override fun findByCountryCodes(countryCodes: Set, hasPhoto: Boolean?, active: Boolean?) = + selectStationWithPrimaryPhoto() + .where(StationTable.countrycode.`in`(countryCodes)) + .and(value(active).isNull.or(StationTable.active.eq(active))) + .and( + value(hasPhoto).isNull + .or(PhotoTable.urlpath.isNull.and(value(hasPhoto).isTrue)) + .or(PhotoTable.urlpath.isNotNull.and(value(hasPhoto).isFalse)) + ) + .map { it.toPhotoStation() } + .toSet() + + private fun selectStationWithPrimaryPhoto(): SelectOnConditionStep> = + selectPhotoStation() + .leftJoin(PhotoTable).on( + PhotoTable.countrycode.eq(StationTable.countrycode) + .and(PhotoTable.stationid.eq(StationTable.id).and(PhotoTable.primary.eq(true))) + ) + .leftJoin(UserTable).on(UserTable.id.eq(PhotoTable.photographerid)) + + private fun Record12.toPhotoStation(): Station { + val key = Station.Key(value1().countrycode, value1().id) + val photo = value2()?.let { + Photo( + id = it, + stationKey = key, + primary = value3()!!, + urlPath = value4()!!, + photographer = User( + id = value8()!!, + name = value9()!!, + url = value10()!!, + license = value11()!!.nameToLicense(), + email = null, + ownPhotos = true, + anonymous = value12() == true, + key = null, + admin = false, + emailVerification = null, + newPassword = null, + sendNotifications = true, + locale = Locale.ENGLISH + ), + createdAt = value6()!!, + license = value5()!!.nameToLicense(), + outdated = value7() == true + ) + } + return Station( + key = key, + title = value1().title, + coordinates = Coordinates(value1().lat, value1().lon), + ds100 = value1().ds100, + photos = photo?.let { listOf(it) } ?: emptyList(), + active = value1().active + ) + } + + override fun findByKey(key: Station.Key) = + selectPhotoStation() + .leftJoin(PhotoTable).on( + PhotoTable.countrycode.eq(StationTable.countrycode) + .and(PhotoTable.stationid.eq(StationTable.id)) + ) + .leftJoin(UserTable).on(UserTable.id.eq(PhotoTable.photographerid)) + .where(StationTable.countrycode.eq(key.country).and(StationTable.id.eq(key.id))) + .map { it.toPhotoStation() } + .groupBy { it.key } + .map { it.merge() } + .toSet() + .firstOrNull() + + private fun Map.Entry>.merge() = value.first().copy(photos = value.flatMap { it.photos }) + + override fun findByPhotographer(photographer: String, countryCode: String?) = + selectPhotoStation() + .join(PhotoTable).on( + PhotoTable.countrycode.eq(StationTable.countrycode) + .and(PhotoTable.stationid.eq(StationTable.id)) + ) + .join(UserTable).on(UserTable.id.eq(PhotoTable.photographerid)) + .where(value(countryCode).isNull.or(StationTable.countrycode.eq(countryCode))) + .and( + or( + UserTable.name.eq(photographer).and(UserTable.anonymous.eq(false)), + UserTable.anonymous.eq(true).and(value(photographer).eq(ANONYM)) + ) + ) + .map { it.toPhotoStation() } + .groupBy { it.key } + .map { it.merge() } + .toSet() + + override fun findRecentImports(since: Instant) = + selectPhotoStation() + .join(PhotoTable).on( + PhotoTable.countrycode.eq(StationTable.countrycode) + .and(PhotoTable.stationid.eq(StationTable.id)) + ) + .join(UserTable).on(UserTable.id.eq(PhotoTable.photographerid)) + .where(PhotoTable.createdat.gt(since)) + .map { it.toPhotoStation() } + .toSet() + + private fun selectPhotoStation(): SelectJoinStep> = + dsl.select( + StationTable, + PhotoTable.id, + PhotoTable.primary, + PhotoTable.urlpath, + PhotoTable.license, + PhotoTable.createdat, + PhotoTable.outdated, + UserTable.id, + UserTable.name, + UserTable.url, + UserTable.license, + UserTable.anonymous + ) + .from(StationTable) + + override fun getStatistic(countryCode: String?) = + dsl.select( + value(countryCode), + count(StationTable.asterisk()), + count(PhotoTable.urlpath), + countDistinct(PhotoTable.photographerid) + ) + .from(StationTable) + .leftJoin(PhotoTable).on( + PhotoTable.countrycode.eq(StationTable.countrycode) + .and(PhotoTable.stationid.eq(StationTable.id).and(PhotoTable.primary.eq(true))) + ) + .where(StationTable.countrycode.eq(countryCode).or(value(countryCode).isNull)) + .fetchSingle().toStatistic() + + private fun Record4.toStatistic() = Statistic( + countryCode = value1(), + total = value2() ?: 0, + withPhoto = value3() ?: 0, + photographers = value4() ?: 0 + ) + + override fun getPhotographerMap(countryCode: String?): Map = + dsl.select(UserTable.name, count()) + .from(StationTable) + .join(PhotoTable) + .on(PhotoTable.countrycode.eq(StationTable.countrycode).and(PhotoTable.stationid.eq(StationTable.id))) + .join(UserTable).on(UserTable.id.eq(PhotoTable.photographerid)) + .where(value(countryCode).isNull.or(StationTable.countrycode.eq(countryCode))) + .groupBy(UserTable.name) + .orderBy(count()) + .fetch { it.value1()!! to it.value2() }.toMap() + + @Transactional + override fun insert(station: Station) { + dsl.executeInsert( + StationRecord( + id = station.key.id, + countrycode = station.key.country, + title = station.title, + lat = station.coordinates.lat, + lon = station.coordinates.lon, + ds100 = station.ds100, + active = station.active, + ) + ) + } + + @Transactional + override fun delete(key: Station.Key) { + dsl.deleteFrom(StationTable) + .where(StationTable.countrycode.eq(key.country).and(StationTable.id.eq(key.id))) + .execute() + } + + @Transactional + override fun updateActive(key: Station.Key, active: Boolean) { + dsl.update(StationTable) + .set(StationTable.active, active) + .where(StationTable.countrycode.eq(key.country).and(StationTable.id.eq(key.id))) + .execute() + } + + /** + * Count nearby stations using simple pythagoras (only valid for a few km) + */ + override fun countNearbyCoordinates(coordinates: Coordinates) = + dsl.selectCount() + .from(StationTable) + .where( + sqrt( + power(value(71.5).mul(StationTable.lon.minus(coordinates.lon)), 2) + .plus(power(value(111.3).mul(StationTable.lat.minus(coordinates.lat)), 2)) + ).lt(BigDecimal(0.5)) + ) + .fetchOne(0, Int::class.java) ?: 0 + + override fun maxZ() = + dsl.select(max(cast(substring(StationTable.id, 2), Integer::class.java))) + .from(StationTable) + .where(StationTable.id.like("Z%")) + .fetchOne(0, Int::class.java) ?: 0 + + @Transactional + override fun changeStationTitle(key: Station.Key, newTitle: String) { + dsl.update(StationTable) + .set(StationTable.title, newTitle) + .where(StationTable.countrycode.eq(key.country).and(StationTable.id.eq(key.id))) + .execute() + } + + @Transactional + override fun updateLocation(key: Station.Key, coordinates: Coordinates) { + dsl.update(StationTable) + .set(StationTable.lat, coordinates.lat) + .set(StationTable.lon, coordinates.lon) + .where(StationTable.countrycode.eq(key.country).and(StationTable.id.eq(key.id))) + .execute() + } + + override fun findByPhotoId(photoId: Long) = + selectStationWithPrimaryPhoto() + .where(PhotoTable.id.eq(photoId)) + .fetchSingle().toPhotoStation() + +} diff --git a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/StationDao.kt b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/StationDao.kt deleted file mode 100644 index 558e3797..00000000 --- a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/StationDao.kt +++ /dev/null @@ -1,316 +0,0 @@ -package org.railwaystations.rsapi.adapter.db - -import org.jdbi.v3.core.mapper.RowMapper -import org.jdbi.v3.core.result.RowReducer -import org.jdbi.v3.core.result.RowView -import org.jdbi.v3.core.statement.StatementContext -import org.jdbi.v3.sqlobject.SingleValue -import org.jdbi.v3.sqlobject.config.KeyColumn -import org.jdbi.v3.sqlobject.config.RegisterRowMapper -import org.jdbi.v3.sqlobject.config.ValueColumn -import org.jdbi.v3.sqlobject.customizer.Bind -import org.jdbi.v3.sqlobject.customizer.BindBean -import org.jdbi.v3.sqlobject.customizer.BindList -import org.jdbi.v3.sqlobject.statement.SqlQuery -import org.jdbi.v3.sqlobject.statement.SqlUpdate -import org.jdbi.v3.sqlobject.statement.UseRowReducer -import org.railwaystations.rsapi.core.model.Coordinates -import org.railwaystations.rsapi.core.model.License -import org.railwaystations.rsapi.core.model.Photo -import org.railwaystations.rsapi.core.model.Station -import org.railwaystations.rsapi.core.model.Statistic -import org.railwaystations.rsapi.core.model.User -import org.railwaystations.rsapi.core.model.nameToLicense -import org.railwaystations.rsapi.core.ports.outbound.StationPort -import java.sql.ResultSet -import java.sql.SQLException -import java.time.Instant -import java.util.* -import java.util.stream.Stream - -private const val JOIN_QUERY: String = """ - SELECT s.countryCode, s.id, s.DS100, s.title, s.lat, s.lon, s.active, - p.id AS photoId, p.primary, p.urlPath, p.license, p.createdAt, p.outdated, u.id AS photographerId, - u.name, u.url AS photographerUrl, u.license AS photographerLicense, u.anonymous - FROM countries c - LEFT JOIN stations s ON c.id = s.countryCode - LEFT JOIN photos p ON p.countryCode = s.countryCode AND p.stationId = s.id AND p.primary = true - LEFT JOIN users u ON u.id = p.photographerId - - """ - -interface StationDao : StationPort { - @SqlQuery("$JOIN_QUERY WHERE s.countryCode IN () AND (:active IS NULL OR s.active = :active) AND (:hasPhoto IS NULL OR (p.urlPath IS NULL AND :hasPhoto = false) OR (p.urlPath IS NOT NULL AND :hasPhoto = true))") - @RegisterRowMapper( - StationMapper::class - ) - override fun findByCountryCodes( - @BindList("countryCodes") countryCodes: Set, - @Bind("hasPhoto") hasPhoto: Boolean?, - @Bind("active") active: Boolean? - ): Set - - @SqlQuery( - """ - SELECT s.countryCode, s.id, s.DS100, s.title, s.lat, s.lon, s.active, - p.id AS photoId, p.primary, p.urlPath, p.license, p.createdAt, p.outdated, u.id AS photographerId, - u.name, u.url AS photographerUrl, u.license AS photographerLicense, u.anonymous - FROM stations s - LEFT JOIN photos p ON p.countryCode = s.countryCode AND p.stationId = s.id - LEFT JOIN users u ON u.id = p.photographerId - WHERE s.countryCode = :countryCode AND s.id = :id - - """ - ) - @UseRowReducer(SingleStationReducer::class) - @RegisterRowMapper(SingleStationMapper::class) - @RegisterRowMapper( - PhotoMapper::class - ) - override fun findByKey(@Bind("countryCode") countryCode: String, @Bind("id") id: String): Station? - - @SqlQuery( - """ - SELECT s.countryCode, s.id, s.DS100, s.title, s.lat, s.lon, s.active, - p.id AS photoId, p.primary, p.urlPath, p.license, p.createdAt, p.outdated, u.id AS photographerId, - u.name, u.url AS photographerUrl, u.license AS photographerLicense, u.anonymous - FROM stations s - LEFT JOIN photos p ON p.countryCode = s.countryCode AND p.stationId = s.id - LEFT JOIN users u ON u.id = p.photographerId - WHERE (:countryCode IS NULL OR s.countryCode = :countryCode) AND ((u.name = :photographer AND u.anonymous = false) OR (u.anonymous = true AND :photographer = 'Anonym')) - - """ - ) - @UseRowReducer(SingleStationReducer::class) - @RegisterRowMapper(SingleStationMapper::class) - @RegisterRowMapper( - PhotoMapper::class - ) - override fun findByPhotographer( - @Bind("photographer") photographer: String, - @Bind("countryCode") countryCode: String? - ): Set - - @SqlQuery( - """ - SELECT s.countryCode, s.id, s.DS100, s.title, s.lat, s.lon, s.active, - p.id AS photoId, p.primary, p.urlPath, p.license, p.createdAt, p.outdated, u.id AS photographerId, - u.name, u.url AS photographerUrl, u.license AS photographerLicense, u.anonymous - FROM stations s - LEFT JOIN photos p ON p.countryCode = s.countryCode AND p.stationId = s.id - LEFT JOIN users u ON u.id = p.photographerId - WHERE createdAt > :since - - """ - ) - @UseRowReducer(SingleStationReducer::class) - @RegisterRowMapper(SingleStationMapper::class) - @RegisterRowMapper( - PhotoMapper::class - ) - override fun findRecentImports(@Bind("since") since: Instant): Set - - class SingleStationReducer : RowReducer>>, Station> { - - override fun accumulate( - container: MutableMap>>, - rowView: RowView - ) { - val stationAndPhotos = container.computeIfAbsent( - Station.Key( - country = rowView.getColumn("countryCode", String::class.java), - id = rowView.getColumn("id", String::class.java) - ) - ) { _: Station.Key? -> - rowView.getRow(Station::class.java) to mutableListOf() - } - - if (rowView.getColumn("photoId", Integer::class.java) != null) { - stationAndPhotos.second.add(rowView.getRow(Photo::class.java)) - } - } - - override fun container(): MutableMap>> { - return mutableMapOf() - } - - override fun stream(container: MutableMap>>): Stream { - return container.values.map { - it.first.copy( - photos = it.second - ) - }.stream() - } - - - } - - class SingleStationMapper : RowMapper { - @Throws(SQLException::class) - override fun map(rs: ResultSet, ctx: StatementContext): Station { - val key = Station.Key(rs.getString("countryCode"), rs.getString("id")) - return Station( - key = key, - title = rs.getString("title"), - coordinates = Coordinates(rs.getDouble("lat"), rs.getDouble("lon")), - ds100 = rs.getString("DS100"), - photos = mutableListOf(), - active = rs.getBoolean("active") - ) - } - } - - class PhotoMapper : RowMapper { - @Throws(SQLException::class) - override fun map(rs: ResultSet, ctx: StatementContext): Photo { - val key = Station.Key(rs.getString("countryCode"), rs.getString("id")) - val photoUrlPath = rs.getString("urlPath") - return Photo( - id = rs.getLong("photoId"), - stationKey = key, - primary = rs.getBoolean("primary"), - urlPath = photoUrlPath, - photographer = User( - id = rs.getInt("photographerId"), - name = rs.getString("name"), - url = rs.getString("photographerUrl"), - license = rs.getString("photographerLicense").nameToLicense(), - email = null, - ownPhotos = true, - anonymous = rs.getBoolean("anonymous"), - key = null, - admin = false, - emailVerification = null, - newPassword = null, - sendNotifications = true, - locale = Locale.ENGLISH - ), - createdAt = rs.getTimestamp("createdAt").toInstant(), - license = License.valueOf(rs.getString("license")), - outdated = rs.getBoolean("outdated") - ) - } - } - - - @SqlQuery( - """ - SELECT :countryCode countryCode, COUNT(*) stations, COUNT(p.urlPath) photos, COUNT(distinct p.photographerId) photographers - FROM stations s - LEFT JOIN photos p ON p.countryCode = s.countryCode AND p.stationId = s.id AND p.primary = true - WHERE s.countryCode = :countryCode OR :countryCode IS NULL - """ - ) - @RegisterRowMapper(StatisticMapper::class) - @SingleValue - override fun getStatistic(@Bind("countryCode") countryCode: String?): Statistic - - @SqlQuery( - """ - SELECT u.name photographer, COUNT(*) photocount - FROM stations s - JOIN photos p ON p.countryCode = s.countryCode AND p.stationId = s.id AND p.primary = true - JOIN users u ON u.id = p.photographerId - WHERE s.countryCode = :countryCode OR :countryCode IS NULL - GROUP BY u.name - ORDER BY COUNT(*) DESC - """ - ) - @KeyColumn("photographer") - @ValueColumn("photocount") - override fun getPhotographerMap(@Bind("countryCode") countryCode: String?): Map - - @SqlUpdate("INSERT INTO stations (countryCode, id, title, lat, lon, ds100, active) VALUES (:key.country, :key.id, :title, :coordinates?.lat, :coordinates?.lon, :ds100, :active)") - override fun insert(@BindBean station: Station) - - @SqlUpdate("DELETE FROM stations WHERE countryCode = :country AND id = :id") - override fun delete(@BindBean key: Station.Key) - - @SqlUpdate("UPDATE stations SET active = :active WHERE countryCode = :key.country AND id = :key.id") - override fun updateActive(@BindBean("key") key: Station.Key, @Bind("active") active: Boolean) - - /** - * Count nearby stations using simple pythagoras (only valid for a few km) - */ - @SqlQuery("SELECT COUNT(*) FROM stations WHERE SQRT(POWER(71.5 * (lon - :lon),2) + POWER(111.3 * (lat - :lat),2)) < 0.5") - override fun countNearbyCoordinates(@BindBean coordinates: Coordinates): Int - - @get:SqlQuery("SELECT MAX(CAST(substring(id,2) AS INT)) FROM stations WHERE id LIKE 'Z%'") - override val maxZ: Int - - @SqlUpdate("UPDATE stations SET title = :new_title WHERE countryCode = :key.country AND id = :key.id") - override fun changeStationTitle(@BindBean("key") key: Station.Key, @Bind("new_title") newTitle: String) - - @SqlUpdate("UPDATE stations SET lat = :coords.lat, lon = :coords.lon WHERE countryCode = :key.country AND id = :key.id") - override fun updateLocation(@BindBean("key") key: Station.Key, @BindBean("coords") coordinates: Coordinates) - - @SqlQuery( - """ - SELECT s.countryCode, s.id, s.DS100, s.title, s.lat, s.lon, s.active, - p.id AS photoId, p.primary, p.urlPath, p.license, p.createdAt, p.outdated, u.id AS photographerId, - u.name, u.url AS photographerUrl, u.license AS photographerLicense, u.anonymous - FROM stations s - JOIN photos p ON p.countryCode = s.countryCode AND p.stationId = s.id - JOIN users u ON u.id = p.photographerId - WHERE p.id = :photoId - """ - ) - @UseRowReducer(SingleStationReducer::class) - @RegisterRowMapper(SingleStationMapper::class) - @RegisterRowMapper( - PhotoMapper::class - ) - override fun findByPhotoId(@Bind("photoId") photoId: Long): Station - - class StationMapper : RowMapper { - @Throws(SQLException::class) - override fun map(rs: ResultSet, ctx: StatementContext): Station { - val key = Station.Key(rs.getString("countryCode"), rs.getString("id")) - val photos = buildList { - val photoUrlPath = rs.getString("urlPath") - if (photoUrlPath != null) { - add( - Photo( - id = rs.getLong("photoId"), - stationKey = key, - primary = rs.getBoolean("primary"), - urlPath = photoUrlPath, - photographer = User( - id = rs.getInt("photographerId"), - name = rs.getString("name"), - url = rs.getString("photographerUrl"), - license = rs.getString("photographerLicense").nameToLicense(), - ownPhotos = true, - anonymous = rs.getBoolean("anonymous"), - ), - createdAt = rs.getTimestamp("createdAt").toInstant(), - license = License.valueOf(rs.getString("license")), - outdated = rs.getBoolean("outdated") - ) - ) - } - } - - return Station( - key = key, - title = rs.getString("title"), - coordinates = Coordinates(rs.getDouble("lat"), rs.getDouble("lon")), - ds100 = rs.getString("DS100"), - photos = photos, - active = rs.getBoolean("active") - ) - } - } - - class StatisticMapper : RowMapper { - override fun map(rs: ResultSet, ctx: StatementContext): Statistic { - return Statistic( - countryCode = rs.getString("countryCode"), - total = rs.getLong("stations"), - withPhoto = rs.getLong("photos"), - photographers = rs.getLong("photographers") - ) - } - } - -} diff --git a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/UserAdapter.kt b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/UserAdapter.kt new file mode 100644 index 00000000..4b537074 --- /dev/null +++ b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/UserAdapter.kt @@ -0,0 +1,147 @@ +package org.railwaystations.rsapi.adapter.db + +import org.jooq.DSLContext +import org.railwaystations.rsapi.adapter.db.jooq.tables.records.BlockedUsernameRecord +import org.railwaystations.rsapi.adapter.db.jooq.tables.records.UserRecord +import org.railwaystations.rsapi.adapter.db.jooq.tables.references.BlockedUsernameTable +import org.railwaystations.rsapi.adapter.db.jooq.tables.references.UserTable +import org.railwaystations.rsapi.core.model.License +import org.railwaystations.rsapi.core.model.User +import org.railwaystations.rsapi.core.model.nameToLicense +import org.railwaystations.rsapi.core.ports.outbound.UserPort +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional +import java.time.Instant +import java.util.* + +@Component +class UserAdapter(private val dsl: DSLContext) : UserPort { + + override fun findByName(name: String): User? { + return dsl.selectFrom(UserTable) + .where(UserTable.name.eq(name)) + .fetchOne()?.toUser() + } + + private fun UserRecord.toUser() = + User( + id = id!!, + name = name, + url = url, + license = license.nameToLicense(), + email = email, + ownPhotos = ownphotos, + anonymous = anonymous, + key = key, + admin = admin, + emailVerification = emailverification, + newPassword = null, + sendNotifications = sendnotifications, + locale = Locale.forLanguageTag(locale), + ) + + override fun findById(id: Long): User? { + return dsl.selectFrom(UserTable) + .where(UserTable.id.eq(id)) + .fetchOne()?.toUser() + } + + override fun findByEmail(email: String): User? { + return dsl.selectFrom(UserTable) + .where(UserTable.email.eq(email)) + .fetchOne()?.toUser() + + } + + @Transactional + override fun updateCredentials(id: Long, key: String) { + dsl.update(UserTable) + .set(UserTable.key, key) + .where(UserTable.id.eq(id)) + .execute() + } + + @Transactional + override fun insert(user: User, key: String?, emailVerification: String?): Long { + val userRecord = UserRecord( + id = null, + name = user.name, + email = user.email, + url = user.url, + ownphotos = user.ownPhotos, + anonymous = user.anonymous, + license = user.license.name, + key = key, + admin = user.admin, + emailverification = emailVerification, + sendnotifications = user.sendNotifications, + locale = user.localeLanguageTag, + ) + dsl.attach(userRecord) + userRecord.store() + return userRecord.id!! + } + + @Transactional + override fun update(id: Long, user: User) { + dsl.update(UserTable) + .set(UserTable.name, user.name) + .set(UserTable.url, user.url) + .set(UserTable.email, user.email) + .set(UserTable.ownphotos, user.ownPhotos) + .set(UserTable.anonymous, user.anonymous) + .set(UserTable.sendnotifications, user.sendNotifications) + .set(UserTable.locale, user.localeLanguageTag) + .where(UserTable.id.eq(id)) + .execute() + } + + override fun findByEmailVerification(emailVerification: String): User? { + return dsl.selectFrom(UserTable) + .where(UserTable.emailverification.eq(emailVerification)) + .fetchOne()?.toUser() + } + + @Transactional + override fun updateEmailVerification(id: Long, emailVerification: String?) { + dsl.update(UserTable) + .set(UserTable.emailverification, emailVerification) + .where(UserTable.id.eq(id)) + .execute() + } + + @Transactional + override fun anonymizeUser(id: Long) { + dsl.update(UserTable) + .set(UserTable.name, "deleteduser$id") + .setNull(UserTable.url) + .set(UserTable.anonymous, true) + .setNull(UserTable.email) + .setNull(UserTable.emailverification) + .set(UserTable.ownphotos, false) + .set(UserTable.license, License.UNKNOWN.name) + .setNull(UserTable.key) + .set(UserTable.sendnotifications, false) + .set(UserTable.admin, false) + .where(UserTable.id.eq(id)) + .execute() + } + + @Transactional + override fun addUsernameToBlocklist(name: String) { + dsl.executeInsert(BlockedUsernameRecord(null, name, Instant.now())) + } + + override fun countBlockedUsername(name: String): Int { + return dsl.fetchCount(BlockedUsernameTable.where(BlockedUsernameTable.name.eq(name))) + } + + @Transactional + override fun updateLocale(id: Long, localeLanguageTag: String) { + dsl.update(UserTable) + .set(UserTable.locale, localeLanguageTag) + .where(UserTable.id.eq(id)) + .execute() + } + +} diff --git a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/UserDao.kt b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/UserDao.kt deleted file mode 100644 index 7a430687..00000000 --- a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/db/UserDao.kt +++ /dev/null @@ -1,126 +0,0 @@ -package org.railwaystations.rsapi.adapter.db - -import org.jdbi.v3.core.mapper.RowMapper -import org.jdbi.v3.core.statement.StatementContext -import org.jdbi.v3.sqlobject.config.RegisterRowMapper -import org.jdbi.v3.sqlobject.customizer.Bind -import org.jdbi.v3.sqlobject.customizer.BindBean -import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys -import org.jdbi.v3.sqlobject.statement.SqlQuery -import org.jdbi.v3.sqlobject.statement.SqlUpdate -import org.railwaystations.rsapi.core.model.User -import org.railwaystations.rsapi.core.model.nameToLicense -import org.railwaystations.rsapi.core.ports.outbound.UserPort -import java.sql.ResultSet -import java.util.* - -interface UserDao : UserPort { - @SqlQuery("SELECT * FROM users") - @RegisterRowMapper(UserMapper::class) - override fun list(): List - - @SqlQuery("SELECT * FROM users WHERE name = :name") - @RegisterRowMapper( - UserMapper::class - ) - override fun findByName(@Bind("name") name: String): User? - - @SqlQuery("SELECT * FROM users WHERE id = :id") - @RegisterRowMapper(UserMapper::class) - override fun findById(@Bind("id") id: Int): User? - - @SqlQuery("SELECT * FROM users WHERE email = :email") - @RegisterRowMapper(UserMapper::class) - override fun findByEmail(@Bind("email") email: String): User? - - @SqlUpdate("UPDATE users SET \"key\" = :key WHERE id = :id") - override fun updateCredentials(@Bind("id") id: Int, @Bind("key") key: String) - - @SqlUpdate( - """ - INSERT INTO users (id, name, url, license, email, ownPhotos, anonymous, \"key\", emailVerification, sendNotifications, locale) - VALUES (:id, :name, :url, :license, :email, :ownPhotos, :anonymous, :key, :emailVerification, :sendNotifications, :localeLanguageTag) - - """ - ) - @GetGeneratedKeys("id") - override fun insert( - @BindBean user: User, - @Bind("key") key: String?, - @Bind("emailVerification") emailVerification: String? - ): Int - - @SqlUpdate( - """ - UPDATE users SET name = :name, url = :url, license = :license, email = :email, ownPhotos = :ownPhotos, - anonymous = :anonymous, sendNotifications = :sendNotifications, locale = :localeLanguageTag - WHERE id = :id - - """ - ) - override fun update(@Bind("id") id: Int, @BindBean user: User) - - @SqlQuery("SELECT * FROM users WHERE emailVerification = :emailVerification") - @RegisterRowMapper( - UserMapper::class - ) - override fun findByEmailVerification(@Bind("emailVerification") emailVerification: String): User? - - @SqlUpdate("UPDATE users SET emailVerification = :emailVerification WHERE id = :id") - override fun updateEmailVerification(@Bind("id") id: Int, @Bind("emailVerification") emailVerification: String?) - - @SqlUpdate( - """ - UPDATE users - SET name = CONCAT('deleteduser', id), - url = NULL, - anonymous = true, - email = NULL, - emailVerification = NULL, - ownPhotos = false, - license = NULL, - \"key\" = NULL, - sendNotifications = false, - admin = false - WHERE id = :id - - """ - ) - override fun anonymizeUser(@Bind("id") id: Int) - - @SqlUpdate( - """ - INSERT INTO blocked_usernames (name) - VALUES (:name) - - """ - ) - override fun addUsernameToBlocklist(@Bind("name") name: String) - - @SqlQuery("SELECT COUNT(*) FROM blocked_usernames WHERE name = :name") - override fun countBlockedUsername(@Bind("name") name: String): Int - - @SqlUpdate("UPDATE users SET locale = :localeLanguageTag WHERE id = :id") - override fun updateLocale(@Bind("id") id: Int, @Bind("localeLanguageTag") locallocaleLanguageTage: String) - - class UserMapper : RowMapper { - override fun map(rs: ResultSet, ctx: StatementContext): User { - val locale = rs.getString("locale") - return User( - id = rs.getInt("id"), - name = rs.getString("name"), - url = rs.getString("url"), - license = rs.getString("license").nameToLicense(), - email = rs.getString("email"), - ownPhotos = rs.getBoolean("ownPhotos"), - anonymous = rs.getBoolean("anonymous"), - key = rs.getString("key"), - admin = rs.getBoolean("admin"), - emailVerification = rs.getString("emailVerification"), - newPassword = null, - sendNotifications = rs.getBoolean("sendNotifications"), - locale = if (locale != null) Locale.forLanguageTag(locale) else Locale.ENGLISH - ) - } - } -} diff --git a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/task/CleanupExpiredTokensTask.kt b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/task/CleanupExpiredTokensTask.kt index 309bbfd6..8f74ef59 100644 --- a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/task/CleanupExpiredTokensTask.kt +++ b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/task/CleanupExpiredTokensTask.kt @@ -1,19 +1,19 @@ package org.railwaystations.rsapi.adapter.task -import org.railwaystations.rsapi.adapter.db.OAuth2AuthorizationDao +import org.railwaystations.rsapi.adapter.db.OAuth2AuthorizationAdapter import org.railwaystations.rsapi.core.utils.Logger import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Component import java.time.Instant @Component -class CleanupExpiredTokensTask(private val oAuth2AuthorizationDao: OAuth2AuthorizationDao) { +class CleanupExpiredTokensTask(private val oAuth2AuthorizationAdapter: OAuth2AuthorizationAdapter) { private val log by Logger() @Scheduled(fixedDelay = 3600000) fun cleanupExpiredTokens() { - val numberOfDeletedRows = oAuth2AuthorizationDao.deleteExpiredTokens(Instant.now()) + val numberOfDeletedRows = oAuth2AuthorizationAdapter.deleteExpiredTokens(Instant.now()) log.info("Deleted {} expired tokens", numberOfDeletedRows) } } diff --git a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/task/WebDavSyncTask.kt b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/task/WebDavSyncTask.kt index a05d885a..403cb4db 100644 --- a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/task/WebDavSyncTask.kt +++ b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/task/WebDavSyncTask.kt @@ -5,7 +5,7 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.dataformat.xml.XmlMapper import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper import com.fasterxml.jackson.module.kotlin.KotlinModule -import org.railwaystations.rsapi.adapter.db.InboxDao +import org.railwaystations.rsapi.adapter.db.InboxAdapter import org.railwaystations.rsapi.core.model.InboxEntry import org.railwaystations.rsapi.core.ports.outbound.PhotoStoragePort import org.railwaystations.rsapi.core.utils.Logger @@ -30,7 +30,7 @@ import java.time.temporal.ChronoUnit class WebDavSyncTask( private val config: WebDavSyncConfig, private val photoStoragePort: PhotoStoragePort, - private val inboxDao: InboxDao, + private val inboxAdapter: InboxAdapter, ) { private val log by Logger() @@ -56,7 +56,7 @@ class WebDavSyncTask( @Scheduled(fixedRate = 60000) fun syncWebDav() { log.info("Starting WebDavSync") - val pendingInboxEntries: List = inboxDao.findPendingInboxEntries() + val pendingInboxEntries: List = inboxAdapter.findPendingInboxEntries() if (pendingInboxEntries.isEmpty()) { return // nothing to do } diff --git a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/web/auth/RSUserDetailsService.kt b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/web/auth/RSUserDetailsService.kt index 87228628..701057af 100644 --- a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/web/auth/RSUserDetailsService.kt +++ b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/web/auth/RSUserDetailsService.kt @@ -1,6 +1,6 @@ package org.railwaystations.rsapi.adapter.web.auth -import org.railwaystations.rsapi.adapter.db.UserDao +import org.railwaystations.rsapi.adapter.db.UserAdapter import org.railwaystations.rsapi.core.model.EMAIL_VERIFIED import org.railwaystations.rsapi.core.model.User import org.railwaystations.rsapi.core.model.normalizeEmail @@ -10,12 +10,12 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.stereotype.Service @Service -class RSUserDetailsService(private val userDao: UserDao) : UserDetailsService { +class RSUserDetailsService(private val userAdapter: UserAdapter) : UserDetailsService { @Throws(UsernameNotFoundException::class) override fun loadUserByUsername(username: String): AuthUser { - val user = userDao.findByEmail(normalizeEmail(username)) - ?: userDao.findByName(username) + val user = userAdapter.findByEmail(normalizeEmail(username)) + ?: userAdapter.findByName(username) if (user == null) { throw UsernameNotFoundException("User '$username' not found") @@ -27,7 +27,7 @@ class RSUserDetailsService(private val userDao: UserDao) : UserDetailsService { fun updateEmailVerification(user: User?) { if (user!!.isEmailVerifiedWithNextLogin) { - userDao.updateEmailVerification(user.id, EMAIL_VERIFIED) + userAdapter.updateEmailVerification(user.id, EMAIL_VERIFIED) } } } \ No newline at end of file diff --git a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/web/controller/DeprecatedApiController.kt b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/web/controller/DeprecatedApiController.kt index 6e54ab70..bae18f66 100644 --- a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/web/controller/DeprecatedApiController.kt +++ b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/web/controller/DeprecatedApiController.kt @@ -58,7 +58,7 @@ class DeprecatedApiController( @PathVariable("country") country: @Size(min = 2, max = 2) String, @PathVariable("id") id: String ): ResponseEntity { - val station = findPhotoStationsUseCase.findByCountryAndId(country, id) + val station = findPhotoStationsUseCase.findByKey(Station.Key(country, id)) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) return ResponseEntity.ok() .headers(createDeprecationHeader()) diff --git a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/web/controller/InboxController.kt b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/web/controller/InboxController.kt index d184f8e0..0790b36e 100644 --- a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/web/controller/InboxController.kt +++ b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/web/controller/InboxController.kt @@ -50,7 +50,7 @@ class InboxController( @PreAuthorize("hasRole('ADMIN')") override fun getAdminInboxCount(): ResponseEntity { - return ResponseEntity.ok(InboxCountResponseDto(manageInboxUseCase.countPendingInboxEntries())) + return ResponseEntity.ok(InboxCountResponseDto(manageInboxUseCase.countPendingInboxEntries().toLong())) } @PreAuthorize("hasRole('ADMIN')") @@ -244,7 +244,7 @@ private fun InboxEntry.toDto() = id = id, photographerNickname = photographerNickname!!, comment = comment ?: "", - createdAt = createdAt!!.toEpochMilli(), + createdAt = createdAt.toEpochMilli(), done = done, hasPhoto = hasPhoto, countryCode = countryCode, diff --git a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/web/controller/PhotoStationsController.kt b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/web/controller/PhotoStationsController.kt index a9c7fddd..db4d6506 100644 --- a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/web/controller/PhotoStationsController.kt +++ b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/web/controller/PhotoStationsController.kt @@ -23,12 +23,9 @@ class PhotoStationsController( @Value("\${photoBaseUrl}") private val photoBaseUrl: String, ) : PhotoStationsApi { - override fun getPhotoStationById( - country: String, - id: String - ): ResponseEntity { + override fun getPhotoStationById(country: String, id: String): ResponseEntity { val stations = setOf( - findPhotoStationsUseCase.findByCountryAndId(country, id) + findPhotoStationsUseCase.findByKey(Station.Key(country, id)) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) ) return ResponseEntity.ok(stations.toDto(photoBaseUrl)) diff --git a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/web/controller/StatisticController.kt b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/web/controller/StatisticController.kt index 77431405..9a50d924 100644 --- a/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/web/controller/StatisticController.kt +++ b/adapter/src/main/kotlin/org/railwaystations/rsapi/adapter/web/controller/StatisticController.kt @@ -17,9 +17,9 @@ class StatisticController(private val getStatisticUseCase: GetStatisticUseCase) } private fun Statistic.toDto() = StatisticDto( - total = total, - withPhoto = withPhoto, - withoutPhoto = withoutPhoto, - photographers = photographers, + total = total.toLong(), + withPhoto = withPhoto.toLong(), + withoutPhoto = withoutPhoto.toLong(), + photographers = photographers.toLong(), countryCode = countryCode ) diff --git a/adapter/src/test/kotlin/org/railwaystations/rsapi/TestApplication.kt b/adapter/src/test/kotlin/org/railwaystations/rsapi/TestApplication.kt new file mode 100644 index 00000000..33cb1cce --- /dev/null +++ b/adapter/src/test/kotlin/org/railwaystations/rsapi/TestApplication.kt @@ -0,0 +1,7 @@ +package org.railwaystations.rsapi + +import org.springframework.boot.autoconfigure.SpringBootApplication + +@SpringBootApplication +class TestApplication { +} \ No newline at end of file diff --git a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/db/CountryAdapterTest.kt b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/db/CountryAdapterTest.kt new file mode 100644 index 00000000..cc93111b --- /dev/null +++ b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/db/CountryAdapterTest.kt @@ -0,0 +1,47 @@ +package org.railwaystations.rsapi.adapter.db + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.railwaystations.rsapi.core.model.CountryTestFixtures +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase +import org.springframework.boot.test.autoconfigure.jooq.JooqTest +import org.springframework.context.annotation.Import + +@JooqTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Import( + JooqCustomizerConfiguration::class, + CountryAdapter::class, +) +class CountryAdapterTest : AbstractPostgreSqlTest() { + + @Autowired + private lateinit var sut: CountryAdapter + + @Test + fun findById() { + val de = sut.findById("de") + + assertThat(de).isEqualTo(CountryTestFixtures.countryDe) + } + + @Test + fun listOnlyActive() { + val activeCountries = sut.list(true) + + assertThat(activeCountries).hasSize(2) + assertThat(activeCountries.map { it.code }).containsExactlyInAnyOrder("de", "ch") + assertThat(activeCountries.single { it.code == "de" }).isEqualTo(CountryTestFixtures.countryDe) + } + + @Test + fun listAll() { + val activeCountries = sut.list(false) + + assertThat(activeCountries).hasSize(4) + assertThat(activeCountries.map { it.code }).containsExactlyInAnyOrder("de", "ch", "it", "se") + assertThat(activeCountries.single { it.code == "de" }).isEqualTo(CountryTestFixtures.countryDe) + } + +} diff --git a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/db/InboxAdapterTest.kt b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/db/InboxAdapterTest.kt new file mode 100644 index 00000000..7f8d0b96 --- /dev/null +++ b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/db/InboxAdapterTest.kt @@ -0,0 +1,276 @@ +package org.railwaystations.rsapi.adapter.db + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.railwaystations.rsapi.core.model.Coordinates +import org.railwaystations.rsapi.core.model.InboxEntryTestFixtures.photoUploadDe8000 +import org.railwaystations.rsapi.core.model.InboxEntryTestFixtures.photoUploadMissingStation +import org.railwaystations.rsapi.core.model.InboxEntryTestFixtures.problemReportDe8000 +import org.railwaystations.rsapi.core.model.PublicInboxEntry +import org.railwaystations.rsapi.core.model.StationTestFixtures.stationDe8000 +import org.railwaystations.rsapi.core.model.StationTestFixtures.stationDe8001 +import org.railwaystations.rsapi.core.model.UserTestFixtures.user10 +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase +import org.springframework.boot.test.autoconfigure.jooq.JooqTest +import org.springframework.context.annotation.Import +import java.time.Instant +import java.time.temporal.ChronoUnit + +@JooqTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Import( + JooqCustomizerConfiguration::class, + InboxAdapter::class, +) +class InboxAdapterTest : AbstractPostgreSqlTest() { + + @Autowired + private lateinit var sut: InboxAdapter + + @Test + fun insertAndFindById() { + val inboxEntry = photoUploadDe8000 + val id = sut.insert(inboxEntry) + + val inboxDbEntry = sut.findById(id) + + assertThat(inboxDbEntry).isEqualTo( + inboxEntry.copy( + id = id, + title = stationDe8000.title, + coordinates = stationDe8000.coordinates, + photographerNickname = user10.name, + photographerEmail = user10.email, + ) + ) + } + + @Test + fun findPendingInboxEntries() { + val idPending = sut.insert(photoUploadDe8000.copy(done = false)) + sut.insert(photoUploadDe8000.copy(done = true)) + + val pendingEntries = sut.findPendingInboxEntries() + + assertThat(pendingEntries.map { it.id }).containsExactly(idPending) + } + + @Test + fun findOldestImportedPhotoNotYetPosted() { + val now = Instant.now() + sut.insert(photoUploadDe8000.copy(done = true, posted = true, createdAt = now.minus(10, ChronoUnit.HOURS))) + sut.insert(photoUploadDe8000.copy(done = true, posted = false, createdAt = now.minus(5, ChronoUnit.HOURS))) + val idNotPostedOlder = + sut.insert(photoUploadDe8000.copy(done = true, posted = false, createdAt = now.minus(8, ChronoUnit.HOURS))) + + val oldestNotYetPosted = sut.findOldestImportedPhotoNotYetPosted() + + assertThat(oldestNotYetPosted!!.id).isEqualTo(idNotPostedOlder) + } + + @Test + fun findPublicInboxEntries() { + sut.insert(photoUploadDe8000) + sut.insert(photoUploadDe8000.copy(done = true, stationId = stationDe8001.key.id)) + sut.insert(problemReportDe8000) + + val publicInboxEntries = sut.findPublicInboxEntries() + + assertThat(publicInboxEntries).containsExactly( + PublicInboxEntry( + countryCode = photoUploadDe8000.countryCode, + stationId = photoUploadDe8000.stationId, + title = stationDe8000.title, + coordinates = stationDe8000.coordinates, + ) + ) + } + + @Test + fun reject() { + val id = sut.insert(photoUploadDe8000.copy(rejectReason = null, done = false)) + + sut.reject(id, "reject reason") + + val actual = sut.findById(id) + assertThat(actual!!.done).isTrue + assertThat(actual.rejectReason).isEqualTo("reject reason") + } + + @Test + fun done() { + val id = sut.insert(photoUploadDe8000.copy(rejectReason = null, done = false)) + + sut.done(id) + + val actual = sut.findById(id) + assertThat(actual!!.done).isTrue + assertThat(actual.rejectReason).isNull() + } + + @Test + fun countPendingInboxEntriesForStationWithoutInboxId() { + sut.insert(photoUploadDe8000) + sut.insert(photoUploadDe8000.copy(stationId = stationDe8001.key.id)) + sut.insert(photoUploadDe8000.copy(done = true)) + sut.insert(problemReportDe8000) + sut.insert(problemReportDe8000.copy(stationId = stationDe8001.key.id)) + + val count = sut.countPendingInboxEntriesForStation(null, stationDe8000.key.country, stationDe8000.key.id) + + assertThat(count).isEqualTo(2) + } + + @Test + fun countPendingInboxEntriesForStationWithInboxId() { + val id1 = sut.insert(photoUploadDe8000) + sut.insert(photoUploadDe8000.copy(stationId = stationDe8001.key.id)) + sut.insert(photoUploadDe8000.copy(done = true)) + sut.insert(problemReportDe8000) + sut.insert(problemReportDe8000.copy(stationId = stationDe8001.key.id)) + + val count = sut.countPendingInboxEntriesForStation(id1, stationDe8000.key.country, stationDe8000.key.id) + + assertThat(count).isEqualTo(1) + } + + @Test + fun countPendingInboxEntries() { + sut.insert(photoUploadDe8000) + sut.insert(photoUploadDe8000.copy(stationId = stationDe8001.key.id)) + sut.insert(photoUploadDe8000.copy(done = true)) + sut.insert(problemReportDe8000) + sut.insert(problemReportDe8000.copy(stationId = stationDe8001.key.id)) + + val count = sut.countPendingInboxEntries() + + assertThat(count).isEqualTo(4) + } + + @Test + fun countPendingInboxEntriesForNearbyCoordinatesWithoutInboxId() { + sut.insert(photoUploadDe8000.copy(coordinates = Coordinates(50.10123, 9.50123))) + sut.insert(photoUploadDe8000.copy(coordinates = Coordinates(50.09856, 9.49956))) + sut.insert(photoUploadDe8000.copy(coordinates = Coordinates(60.1, 19.3))) + + val count = sut.countPendingInboxEntriesForNearbyCoordinates(null, Coordinates(50.10, 9.50)) + + assertThat(count).isEqualTo(2) + } + + @Test + fun countPendingInboxEntriesForNearbyCoordinatesWithInboxId() { + val id1 = sut.insert(photoUploadDe8000.copy(coordinates = Coordinates(50.10123, 9.50123))) + sut.insert(photoUploadDe8000.copy(coordinates = Coordinates(50.09856, 9.49956))) + sut.insert(photoUploadDe8000.copy(coordinates = Coordinates(60.1, 19.3))) + + val count = sut.countPendingInboxEntriesForNearbyCoordinates(id1, Coordinates(50.10, 9.50)) + + assertThat(count).isEqualTo(1) + } + + @Test + fun updateCrc32() { + val id = sut.insert(photoUploadDe8000.copy(crc32 = 1234)) + + sut.updateCrc32(id, 45678) + + val actual = sut.findById(id) + assertThat(actual!!.crc32).isEqualTo(45678) + } + + @Test + fun findInboxEntriesToNotify() { + val id1 = sut.insert(photoUploadDe8000.copy(done = true, notified = false)) + sut.insert(photoUploadDe8000.copy(done = true, notified = true)) + sut.insert(photoUploadDe8000.copy(done = false, notified = false)) + + val findsToNotify = sut.findInboxEntriesToNotify() + + assertThat(findsToNotify.size).isEqualTo(1) + assertThat(findsToNotify.first().id).isEqualTo(id1) + } + + @Test + fun updateNotified() { + val id = sut.insert(photoUploadDe8000.copy(notified = false)) + + sut.updateNotified(listOf(id)) + + val actual = sut.findById(id) + assertThat(actual!!.notified).isTrue() + } + + @Test + fun updatePosted() { + val id = sut.insert(photoUploadDe8000.copy(posted = false)) + + sut.updatePosted(id) + + val actual = sut.findById(id) + assertThat(actual!!.posted).isTrue() + } + + @Test + fun updatePhotoId() { + val id = sut.insert(photoUploadDe8000.copy(photoId = null)) + + sut.updatePhotoId(id, 1234) + + val actual = sut.findById(id) + assertThat(actual!!.photoId).isEqualTo(1234) + } + + @Test + fun updateMissingStationImported() { + val id = sut.insert(photoUploadMissingStation) + + sut.updateMissingStationImported( + id = id, + countryCode = stationDe8000.key.country, + stationId = stationDe8000.key.id, + title = stationDe8000.title, + ) + + val updatedEntry = sut.findById(id) + assertThat(updatedEntry!!.countryCode).isEqualTo(stationDe8000.key.country) + assertThat(updatedEntry.stationId).isEqualTo(stationDe8000.key.id) + assertThat(updatedEntry.title).isEqualTo(stationDe8000.title) + assertThat(updatedEntry.done).isTrue() + } + + @Test + fun findByUserIncludeCompletedEntries() { + val id1 = sut.insert(photoUploadDe8000.copy(photographerId = 1, done = false)) + val id2 = sut.insert(photoUploadDe8000.copy(photographerId = 1, done = true)) + sut.insert(photoUploadDe8000.copy(photographerId = 2, done = true)) + + val findsByUser = sut.findByUser(photographerId = 1, includeCompletedEntries = true) + + assertThat(findsByUser.map { it.id }).containsExactlyInAnyOrder(id1, id2) + } + + @Test + fun findByUserOnlyPendingEntries() { + val id1 = sut.insert(photoUploadDe8000.copy(photographerId = 1, done = false)) + sut.insert(photoUploadDe8000.copy(photographerId = 1, done = true)) + sut.insert(photoUploadDe8000.copy(photographerId = 2, done = true)) + + val findsByUser = sut.findByUser(photographerId = 1, includeCompletedEntries = false) + + assertThat(findsByUser.map { it.id }).containsExactlyInAnyOrder(id1) + } + + @Test + fun findPendingByStation() { + val id1 = sut.insert(photoUploadDe8000) + sut.insert(photoUploadDe8000.copy(stationId = stationDe8001.key.id)) + + val findsByStation = sut.findPendingByStation(stationDe8000.key.country, stationDe8000.key.id) + + assertThat(findsByStation.size).isEqualTo(1) + assertThat(findsByStation.first().id).isEqualTo(id1) + } + +} diff --git a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/db/OAuth2AuthorizationAdapterTest.kt b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/db/OAuth2AuthorizationAdapterTest.kt new file mode 100644 index 00000000..66133751 --- /dev/null +++ b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/db/OAuth2AuthorizationAdapterTest.kt @@ -0,0 +1,117 @@ +package org.railwaystations.rsapi.adapter.db + +import org.assertj.core.api.Assertions.assertThat +import org.jooq.DSLContext +import org.junit.jupiter.api.Test +import org.railwaystations.rsapi.adapter.db.jooq.tables.records.Oauth2AuthorizationRecord +import org.railwaystations.rsapi.adapter.db.jooq.tables.references.Oauth2AuthorizationTable +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase +import org.springframework.boot.test.autoconfigure.jooq.JooqTest +import org.springframework.context.annotation.Import +import java.time.Instant +import java.time.temporal.ChronoUnit + +@JooqTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Import( + JooqCustomizerConfiguration::class, + OAuth2AuthorizationAdapter::class, +) +class OAuth2AuthorizationAdapterTest : AbstractPostgreSqlTest() { + + @Autowired + private lateinit var dsl: DSLContext + + @Autowired + private lateinit var sut: OAuth2AuthorizationAdapter + + @Test + fun deleteExpiredTokens() { + val past = Instant.now().minus(1, ChronoUnit.MINUTES) + val future = Instant.now().plus(1, ChronoUnit.MINUTES) + insertRecord(id = "1") + insertRecord(id = "2", authorizationCodeValue = "authorizationCodeValue", authorizationCodeExpiresAt = past) + insertRecord(id = "3", authorizationCodeValue = "authorizationCodeValue", authorizationCodeExpiresAt = future) + insertRecord(id = "4", accessTokenValue = "accessTokenValue", accessTokenExpiresAt = past) + insertRecord(id = "5", accessTokenValue = "accessTokenValue", accessTokenExpiresAt = future) + insertRecord(id = "6", refreshTokenValue = "refreshTokenValue", refreshTokenExpiresAt = past) + insertRecord(id = "7", refreshTokenValue = "refreshTokenValue", refreshTokenExpiresAt = future) + insertRecord( + id = "8", + authorizationCodeValue = "authorizationCodeValue", + authorizationCodeExpiresAt = past, + accessTokenValue = "accessTokenValue", + accessTokenExpiresAt = past, + refreshTokenValue = "refreshTokenValue", + refreshTokenExpiresAt = future, + ) + + val deleteCount = sut.deleteExpiredTokens(Instant.now()) + + assertThat(deleteCount).isEqualTo(4) + val remainingIds = dsl.selectFrom(Oauth2AuthorizationTable).fetch().map { it.id } + assertThat(remainingIds).containsExactlyInAnyOrder("3", "5", "7", "8") + } + + private fun insertRecord( + id: String, + authorizationCodeValue: String? = null, + authorizationCodeExpiresAt: Instant? = null, + accessTokenValue: String? = null, + accessTokenExpiresAt: Instant? = null, + refreshTokenValue: String? = null, + refreshTokenExpiresAt: Instant? = null, + principalName: String = "principalName", + ) { + dsl.executeInsert( + Oauth2AuthorizationRecord( + id = id, + registeredClientId = "registeredClientId", + principalName = principalName, + authorizationGrantType = "authorizationGrantType", + authorizedScopes = null, + attributes = null, + state = null, + authorizationCodeValue = authorizationCodeValue, + authorizationCodeIssuedAt = null, + authorizationCodeExpiresAt = authorizationCodeExpiresAt, + authorizationCodeMetadata = null, + accessTokenValue = accessTokenValue, + accessTokenIssuedAt = null, + accessTokenExpiresAt = accessTokenExpiresAt, + accessTokenMetadata = null, + accessTokenType = null, + accessTokenScopes = null, + oidcIdTokenValue = null, + oidcIdTokenIssuedAt = null, + oidcIdTokenExpiresAt = null, + oidcIdTokenMetadata = null, + refreshTokenValue = refreshTokenValue, + refreshTokenIssuedAt = null, + refreshTokenExpiresAt = refreshTokenExpiresAt, + refreshTokenMetadata = null, + userCodeValue = null, + userCodeIssuedAt = null, + userCodeExpiresAt = null, + userCodeMetadata = null, + deviceCodeValue = null, + deviceCodeIssuedAt = null, + deviceCodeExpiresAt = null, + deviceCodeMetadata = null, + ) + ) + } + + @Test + fun deleteAllByUser() { + insertRecord(id = "1", principalName = "Name 1") + insertRecord(id = "2", principalName = "Name 2") + + sut.deleteAllByUser("Name 1") + + val remainingIds = dsl.selectFrom(Oauth2AuthorizationTable).fetch().map { it.id } + assertThat(remainingIds).containsExactlyInAnyOrder("2") + } + +} \ No newline at end of file diff --git a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/db/PhotoAdapterTest.kt b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/db/PhotoAdapterTest.kt new file mode 100644 index 00000000..af1d67c7 --- /dev/null +++ b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/db/PhotoAdapterTest.kt @@ -0,0 +1,142 @@ +package org.railwaystations.rsapi.adapter.db + +import org.assertj.core.api.Assertions.assertThat +import org.jooq.DSLContext +import org.junit.jupiter.api.Test +import org.railwaystations.rsapi.adapter.db.jooq.tables.references.PhotoTable +import org.railwaystations.rsapi.core.model.License +import org.railwaystations.rsapi.core.model.Photo +import org.railwaystations.rsapi.core.model.PhotoTestFixtures +import org.railwaystations.rsapi.core.model.Station +import org.railwaystations.rsapi.core.model.UserTestFixtures +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase +import org.springframework.boot.test.autoconfigure.jooq.JooqTest +import org.springframework.context.annotation.Import +import java.time.Instant + +@JooqTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Import( + JooqCustomizerConfiguration::class, + PhotoAdapter::class, +) +class PhotoAdapterTest : AbstractPostgreSqlTest() { + + @Autowired + private lateinit var dsl: DSLContext + + @Autowired + private lateinit var sut: PhotoAdapter + + @Test + fun insert() { + val photoCountBefore = sut.countPhotos() + val photo = + PhotoTestFixtures.createPhoto(Station.Key("de", "5081"), UserTestFixtures.someUser) + + val photoId = sut.insert(photo) + + assertThat(sut.countPhotos()).isEqualTo(photoCountBefore + 1) + val photoRecord = findPhotoRecordById(photoId) + with(photoRecord!!) { + assertThat(countrycode).isEqualTo(photo.stationKey.country) + assertThat(stationid).isEqualTo(photo.stationKey.id) + assertThat(primary).isEqualTo(photo.primary) + assertThat(outdated).isEqualTo(photo.outdated) + assertThat(urlpath).isEqualTo(photo.urlPath) + assertThat(license).isEqualTo(photo.license.name) + assertThat(photographerid).isEqualTo(photo.photographer.id) + assertThat(createdat.epochSecond).isEqualTo(photo.createdAt.epochSecond) + } + } + + private fun findPhotoRecordById(photoId: Long) = + dsl.selectFrom(PhotoTable).where(PhotoTable.id.eq(photoId)).fetchOne() + + @Test + fun update() { + val photoRecord = findPhotoRecordById(1) + val updatedPhoto = Photo( + id = photoRecord!!.id!!, + stationKey = Station.Key(photoRecord.countrycode, photoRecord.stationid), + primary = false, + urlPath = "newUrlPath", + photographer = UserTestFixtures.someUser, + createdAt = Instant.now(), + license = License.CC_BY_NC_SA_30_DE, + outdated = true, + ) + + sut.update(updatedPhoto) + + val updatedPhotoRecord = findPhotoRecordById(photoRecord.id!!) + with(updatedPhotoRecord!!) { + assertThat(primary).isEqualTo(updatedPhoto.primary) + assertThat(outdated).isEqualTo(updatedPhoto.outdated) + assertThat(urlpath).isEqualTo(updatedPhoto.urlPath) + assertThat(license).isEqualTo(updatedPhoto.license.name) + assertThat(photographerid).isEqualTo(updatedPhoto.photographer.id) + assertThat(createdat.epochSecond).isEqualTo(updatedPhoto.createdAt.epochSecond) + } + } + + @Test + fun delete() { + val photoRecord = findPhotoRecordById(1) + + sut.delete(photoRecord!!.id!!) + + assertThat(findPhotoRecordById(1)).isNull() + } + + @Test + fun updatePhotoOutdated() { + val photoRecord = findPhotoRecordById(1) + assertThat(photoRecord!!.outdated).isFalse + + sut.updatePhotoOutdated(photoRecord.id!!) + + assertThat(findPhotoRecordById(1)!!.outdated).isTrue() + } + + @Test + fun setAllPhotosForStationSecondary() { + val key = Station.Key("ch", "8503007") + val primaryCount = dsl.selectCount().from(PhotoTable).where( + PhotoTable.countrycode.eq(key.country).and(PhotoTable.stationid.eq(key.id).and(PhotoTable.primary.eq(true))) + ).fetchSingle().component1() + assertThat(primaryCount).isNotEqualTo(0) + + sut.setAllPhotosForStationSecondary(key) + + val photos = dsl.selectFrom(PhotoTable) + .where(PhotoTable.countrycode.eq(key.country).and(PhotoTable.stationid.eq(key.id))).fetch() + assertThat(photos.all { it.primary == false }).isTrue + } + + @Test + fun setPrimary() { + val photoRecord = findPhotoRecordById(130) + assertThat(photoRecord!!.primary).isFalse + + sut.setPrimary(photoRecord.id!!) + + assertThat(findPhotoRecordById(130)!!.primary).isTrue() + } + + @Test + fun countPhotos() { + val photoCount = sut.countPhotos() + + assertThat(photoCount).isEqualTo(94) + } + + @Test + fun findNthPhotoId() { + val photoId = sut.findNthPhotoId(10) + + assertThat(photoId).isEqualTo(11) + } + +} \ No newline at end of file diff --git a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/db/StationAdapterTest.kt b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/db/StationAdapterTest.kt new file mode 100644 index 00000000..3e6fe4de --- /dev/null +++ b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/db/StationAdapterTest.kt @@ -0,0 +1,208 @@ +package org.railwaystations.rsapi.adapter.db + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.railwaystations.rsapi.core.model.Coordinates +import org.railwaystations.rsapi.core.model.Station +import org.railwaystations.rsapi.core.model.StationTestFixtures.createStation +import org.railwaystations.rsapi.core.model.StationTestFixtures.keyCh8503001 +import org.railwaystations.rsapi.core.model.StationTestFixtures.keyDe8000 +import org.railwaystations.rsapi.core.model.StationTestFixtures.stationCh8503001 +import org.railwaystations.rsapi.core.model.StationTestFixtures.stationDe8000 +import org.railwaystations.rsapi.core.model.Statistic +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase +import org.springframework.boot.test.autoconfigure.jooq.JooqTest +import org.springframework.context.annotation.Import +import java.time.Instant + +@JooqTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Import( + JooqCustomizerConfiguration::class, + StationAdapter::class, +) +class StationAdapterTest : AbstractPostgreSqlTest() { + + @Autowired + private lateinit var sut: StationAdapter + + @ParameterizedTest + @CsvSource( + ",,730", + "true,,646", + "false,,84", + ",true,729", + ",false,1", + "true,true,645", + "false,false,0", + "false,true,84", + "true,false,1", + ) + fun findByCountryCodesDe(hasPhoto: Boolean?, active: Boolean?, expectedCount: Int) { + val stations = sut.findByCountryCodes(countryCodes = setOf("de"), hasPhoto = hasPhoto, active = active) + + assertThat(stations).hasSize(expectedCount) + } + + @Test + fun findByCountryCodesDeCh() { + val stations = sut.findByCountryCodes(countryCodes = setOf("de", "ch")) + + assertThat(stations).hasSize(955) + assertThat(stations.first { it.key == keyDe8000 }).isEqualTo(stationDe8000) + } + + @Test + fun findByKey() { + val station = sut.findByKey(keyDe8000) + + assertThat(station).isEqualTo(stationDe8000) + } + + @ParameterizedTest + @CsvSource( + "@user10,,16", + "@user10,de,15", + "@user10,ch,1", + "Anonym,,10", + "Anonym,de,9", + "Anonym,ch,1", + ) + fun findByPhotographer(photographer: String, countryCode: String? = null, expectedCount: Int) { + val stations = sut.findByPhotographer(photographer = photographer, countryCode = countryCode) + + assertThat(stations).hasSize(expectedCount) + } + + @Test + fun findByPhotographerUser2() { + val stations = sut.findByPhotographer(photographer = "@user2") + + assertThat(stations).hasSize(6) + assertThat(stations.first { it.key == keyCh8503001 }).isEqualTo(stationCh8503001) + } + + @ParameterizedTest + @CsvSource( + ",956,91,6", + "ch,225,7,2", + "de,730,84,4", + ) + fun getStatistic(countryCode: String? = null, total: Int, withPhoto: Int, photographers: Int) { + val statistic = sut.getStatistic(countryCode) + + assertThat(statistic).isEqualTo( + Statistic( + countryCode = countryCode, + total = total, + withPhoto = withPhoto, + photographers = photographers, + ) + ) + } + + @Test + fun getPhotographerMapAll() { + val photographers = sut.getPhotographerMap() + + assertThat(photographers).isEqualTo( + mapOf( + "@user0" to 10, "@user10" to 17, "@user2" to 6, "@user27" to 31, "@user4" to 1, "@user8" to 29 + ) + ) + } + + @Test + fun getPhotographerMapAllDe() { + val photographers = sut.getPhotographerMap("de") + + assertThat(photographers).isEqualTo( + mapOf( + "@user0" to 9, "@user10" to 16, "@user27" to 31, "@user8" to 29 + ) + ) + } + + @Test + fun insert() { + val newStation = createStation(key = Station.Key("de", "0815"), coordinates = Coordinates(12.3, 45.6)) + + sut.insert(newStation) + + assertThat(sut.findByKey(newStation.key)).isEqualTo(newStation) + } + + @Test + fun delete() { + assertThat(sut.findByKey(keyDe8000)).isNotNull + + sut.delete(keyDe8000) + + assertThat(sut.findByKey(keyDe8000)).isNull() + } + + @Test + fun updateActive() { + assertThat(sut.findByKey(keyDe8000)!!.active).isTrue + + sut.updateActive(keyDe8000, false) + + assertThat(sut.findByKey(keyDe8000)!!.active).isFalse + } + + @Test + fun countNearbyCoordinates() { + val count = sut.countNearbyCoordinates(Coordinates(48.45937, 7.95547)) + + assertThat(count).isEqualTo(1) + } + + @Test + fun maxZ() { + val maxZ = sut.maxZ() + + assertThat(maxZ).isEqualTo(1191) + } + + @Test + fun changeStationTitle() { + val newTitle = "New Title" + assertThat(sut.findByKey(keyDe8000)!!.title).isEqualTo(stationDe8000.title) + + sut.changeStationTitle(keyDe8000, newTitle) + + assertThat(sut.findByKey(keyDe8000)!!.title).isEqualTo(newTitle) + } + + @Test + fun updateLocation() { + val newCoordinates = Coordinates(50.1, 9.2) + assertThat(sut.findByKey(keyDe8000)!!.coordinates).isEqualTo(stationDe8000.coordinates) + + sut.updateLocation(keyDe8000, newCoordinates) + + assertThat(sut.findByKey(keyDe8000)!!.coordinates).isEqualTo(newCoordinates) + } + + @Test + fun findByPhotoId() { + val station = sut.findByPhotoId(10) + + assertThat(station.key).isEqualTo(Station.Key("de", "6892")) + } + + @Test + fun findRecentImports() { + val stations = sut.findRecentImports(Instant.parse("2022-08-01T00:00:00Z")) + + assertThat(stations.map { it.key }).containsExactlyInAnyOrder( + Station.Key("de", "6932"), + Station.Key("ch", "8500013"), + Station.Key("ch", "8503087"), + ) + } + +} diff --git a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/db/UserAdapterTest.kt b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/db/UserAdapterTest.kt new file mode 100644 index 00000000..3832f8ba --- /dev/null +++ b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/db/UserAdapterTest.kt @@ -0,0 +1,138 @@ +package org.railwaystations.rsapi.adapter.db + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.railwaystations.rsapi.core.model.License +import org.railwaystations.rsapi.core.model.UserTestFixtures +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase +import org.springframework.boot.test.autoconfigure.jooq.JooqTest +import org.springframework.context.annotation.Import +import java.util.* + +@JooqTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Import( + JooqCustomizerConfiguration::class, + UserAdapter::class, +) +class UserAdapterTest : AbstractPostgreSqlTest() { + + @Autowired + private lateinit var sut: UserAdapter + + @Test + fun findByName() { + val user = sut.findByName(UserTestFixtures.user10.name) + + assertThat(user).isEqualTo(UserTestFixtures.user10) + } + + @Test + fun findById() { + val user = sut.findById(UserTestFixtures.user10.id) + + assertThat(user).isEqualTo(UserTestFixtures.user10) + } + + @Test + fun findByEmail() { + val user = sut.findByEmail(UserTestFixtures.user10.email!!) + + assertThat(user).isEqualTo(UserTestFixtures.user10) + } + + @Test + fun updateCredentials() { + val user = sut.findById(10) + assertThat(user!!.key).isNull() + + sut.updateCredentials(10, "newKey") + + assertThat(sut.findById(10)!!.key).isEqualTo("newKey") + } + + @Test + fun insert() { + val id = sut.insert(UserTestFixtures.userJimKnopf, "key", "emailVerification") + val dbUser = sut.findById(id) + assertThat(dbUser).isEqualTo( + UserTestFixtures.userJimKnopf.copy( + id = id, + key = "key", + emailVerification = "emailVerification" + ) + ) + } + + @Test + fun update() { + val originalUser = sut.findById(8) + val updateUser = originalUser!!.copy( + name = "updatedName", + url = "updatedUrl", + email = "updatedEmail", + ownPhotos = !originalUser.ownPhotos, + anonymous = !originalUser.anonymous, + sendNotifications = !originalUser.sendNotifications, + locale = Locale.CANADA, + ) + + sut.update(updateUser.id, updateUser) + + val updatedUser = sut.findById(updateUser.id) + assertThat(updatedUser).isEqualTo(updateUser) + } + + @Test + fun updateEmailVerificationAndFindByEmailVerification() { + val user = sut.findById(22) + assertThat(user!!.emailVerification).isNull() + val newEmailVerification = UUID.randomUUID().toString() + + sut.updateEmailVerification(22, newEmailVerification) + + assertThat(sut.findByEmailVerification(newEmailVerification)!!.id).isEqualTo(22L) + } + + @Test + fun anonymizeUser() { + sut.anonymizeUser(15) + + val anonymizedUser = sut.findById(15) + with(anonymizedUser!!) { + assertThat(id).isEqualTo(15) + assertThat(name).isEqualTo("deleteduser15") + assertThat(url).isNull() + assertThat(anonymous).isTrue() + assertThat(email).isNull() + assertThat(emailVerification).isNull() + assertThat(ownPhotos).isFalse() + assertThat(license).isEqualTo(License.UNKNOWN) + assertThat(key).isNull() + assertThat(sendNotifications).isFalse() + assertThat(admin).isFalse() + } + } + + @Test + fun addUsernameToBlocklistAndCountBlockedUsername() { + val name = "blockedUser" + assertThat(sut.countBlockedUsername(name)).isEqualTo(0) + + sut.addUsernameToBlocklist(name) + + assertThat(sut.countBlockedUsername(name)).isEqualTo(1) + } + + @Test + fun updateLocale() { + val user = sut.findById(22) + assertThat(user!!.locale).isEqualTo(Locale.ENGLISH) + + sut.updateLocale(22, Locale.GERMANY.toLanguageTag()) + + assertThat(sut.findById(22)!!.locale).isEqualTo(Locale.GERMANY) + } + +} \ No newline at end of file diff --git a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/photostorage/PhotoFileStorageTest.kt b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/photostorage/PhotoFileStorageTest.kt index 998cbb10..49a14fc0 100644 --- a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/photostorage/PhotoFileStorageTest.kt +++ b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/photostorage/PhotoFileStorageTest.kt @@ -7,8 +7,8 @@ import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource import org.railwaystations.rsapi.core.model.InboxEntry import org.railwaystations.rsapi.core.model.Station -import org.railwaystations.rsapi.core.model.StationTestFixtures.stationDe0815 -import org.railwaystations.rsapi.core.model.StationTestFixtures.stationDe4711 +import org.railwaystations.rsapi.core.model.StationTestFixtures.stationDe8000 +import org.railwaystations.rsapi.core.model.StationTestFixtures.stationDe8001 import java.nio.file.Files import java.nio.file.Path import java.nio.file.attribute.FileTime @@ -66,15 +66,15 @@ internal class PhotoFileStorageTest { @Test fun importPhoto() { - val inboxEntry = createInboxEntryWithId(1, stationDe4711.key) + val inboxEntry = createInboxEntryWithId(1, stationDe8000.key) val filename = inboxEntry.filename!! createFile(workDir.inboxDir, filename) createFile(workDir.inboxProcessedDir, filename) - val urlPath = storage.importPhoto(inboxEntry, stationDe4711) + val urlPath = storage.importPhoto(inboxEntry, stationDe8000) - assertThat(urlPath).isEqualTo("/de/4711_1.jpg") - assertThat(workDir.photosDir.resolve("de").resolve("4711_1.jpg")).exists() + assertThat(urlPath).isEqualTo("/de/8000_1.jpg") + assertThat(workDir.photosDir.resolve("de").resolve("8000_1.jpg")).exists() assertThat(workDir.inboxDoneDir.resolve(filename)).exists() assertThat(workDir.inboxDir.resolve(filename)).doesNotExist() assertThat(workDir.inboxProcessedDir.resolve(filename)).doesNotExist() @@ -82,16 +82,16 @@ internal class PhotoFileStorageTest { @Test fun importSecondPhoto() { - val inboxEntry = createInboxEntryWithId(2, stationDe0815.key) + val inboxEntry = createInboxEntryWithId(2, stationDe8001.key) val filename = inboxEntry.filename!! createFile(workDir.inboxDir, filename) createFile(workDir.inboxProcessedDir, filename) - createFile(workDir.photosDir.resolve("de"), "0815_1.jpg") + createFile(workDir.photosDir.resolve("de"), "8001_1.jpg") - val urlPath = storage.importPhoto(inboxEntry, stationDe0815) + val urlPath = storage.importPhoto(inboxEntry, stationDe8001) - assertThat(urlPath).isEqualTo("/de/0815_2.jpg") - assertThat(workDir.photosDir.resolve("de").resolve("0815_2.jpg")).exists() + assertThat(urlPath).isEqualTo("/de/8001_2.jpg") + assertThat(workDir.photosDir.resolve("de").resolve("8001_2.jpg")).exists() assertThat(workDir.inboxDoneDir.resolve(filename)).exists() assertThat(workDir.inboxDir.resolve(filename)).doesNotExist() assertThat(workDir.inboxProcessedDir.resolve(filename)).doesNotExist() diff --git a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/task/WebDavSyncTaskTest.kt b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/task/WebDavSyncTaskTest.kt index 5914bc60..ace61e99 100644 --- a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/task/WebDavSyncTaskTest.kt +++ b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/task/WebDavSyncTaskTest.kt @@ -29,7 +29,7 @@ import io.mockk.mockk import org.assertj.core.api.Assertions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import org.railwaystations.rsapi.adapter.db.InboxDao +import org.railwaystations.rsapi.adapter.db.InboxAdapter import org.railwaystations.rsapi.core.model.InboxEntry import org.railwaystations.rsapi.core.ports.outbound.PhotoStoragePort import java.nio.charset.StandardCharsets @@ -49,14 +49,14 @@ const val PROPFIND_METHOD: String = "PROPFIND" internal class WebDavSyncTaskTest { private lateinit var tempdir: Path private lateinit var photoStoragePort: PhotoStoragePort - private lateinit var inboxDao: InboxDao + private lateinit var inboxAdapter: InboxAdapter @BeforeEach fun setup() { tempdir = Files.createTempDirectory("rsapi") photoStoragePort = mockk() - inboxDao = mockk() - every { inboxDao.findPendingInboxEntries() } returns listOf(createInboxEntry()) + inboxAdapter = mockk() + every { inboxAdapter.findPendingInboxEntries() } returns listOf(createInboxEntry()) } @Test @@ -253,7 +253,7 @@ internal class WebDavSyncTaskTest { toProcessUrl = wmRuntimeInfo.httpBaseUrl + TO_PROCESS_PATH, processedUrl = wmRuntimeInfo.httpBaseUrl + PROCESSED_PATH, user = USERNAME, password = PASSWORD ) - return WebDavSyncTask(config, photoStoragePort, inboxDao) + return WebDavSyncTask(config, photoStoragePort, inboxAdapter) } private fun createInboxEntry(): InboxEntry { diff --git a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/CountriesControllerTest.kt b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/CountriesControllerTest.kt index 55ea9c48..35b2678a 100644 --- a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/CountriesControllerTest.kt +++ b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/CountriesControllerTest.kt @@ -6,7 +6,7 @@ import io.mockk.every import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Fail import org.junit.jupiter.api.Test -import org.railwaystations.rsapi.adapter.db.CountryDao +import org.railwaystations.rsapi.adapter.db.CountryAdapter import org.railwaystations.rsapi.adapter.web.ErrorHandlingControllerAdvice import org.railwaystations.rsapi.adapter.web.OpenApiValidatorUtil.validOpenApiResponse import org.railwaystations.rsapi.adapter.web.model.CountryDto @@ -35,11 +35,11 @@ internal class CountriesControllerTest { private lateinit var objectMapper: ObjectMapper @MockkBean - private lateinit var countryDao: CountryDao + private lateinit var countryAdapter: CountryAdapter @Test fun listCountries() { - every { countryDao.list(true) } returns createCountryList(setOf("xy", "ab")) + every { countryAdapter.list(true) } returns createCountryList(setOf("xy", "ab")) val contentAsString = mvc.perform(get("/countries")) .andExpect(status().isOk()) diff --git a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/DeprecatedApiControllerTest.kt b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/DeprecatedApiControllerTest.kt index 4bbba8dc..b822862c 100644 --- a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/DeprecatedApiControllerTest.kt +++ b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/DeprecatedApiControllerTest.kt @@ -31,7 +31,9 @@ import org.springframework.test.web.servlet.ResultActions import org.springframework.test.web.servlet.ResultMatcher import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post -import org.springframework.test.web.servlet.result.MockMvcResultMatchers.* +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.header +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import java.util.function.Consumer private const val USER_AGENT = "UserAgent" @@ -83,7 +85,7 @@ internal class DeprecatedApiControllerTest { @Test fun countryStationId() { every { - findPhotoStationsUseCase.findByCountryAndId(stationDe5WithPhoto.key.country, stationDe5WithPhoto.key.id) + findPhotoStationsUseCase.findByKey(stationDe5WithPhoto.key) } returns stationDe5WithPhoto val request = mvc.perform(get("/de/stations/5")) @@ -93,7 +95,7 @@ internal class DeprecatedApiControllerTest { @Test fun getCountryStationIdNotFound() { - every { findPhotoStationsUseCase.findByCountryAndId("de", "00") } returns null + every { findPhotoStationsUseCase.findByKey(Station.Key("de", "00")) } returns null mvc.perform(get("/de/stations/00")) .andExpect(status().isNotFound()) @@ -198,7 +200,7 @@ internal class DeprecatedApiControllerTest { @Test fun registerNewUser() { - val user = UserTestFixtures.createUserJimKnopf() + val user = UserTestFixtures.userJimKnopf val givenUserProfile = """ { "nickname": "%s", "email": "%s", "link": "%s", "license": "CC0", "anonymous": %b, "sendNotifications": %b, "photoOwner": %b } """.format( @@ -218,7 +220,7 @@ internal class DeprecatedApiControllerTest { @Test fun registerNewUserWithPassword() { - val user = UserTestFixtures.createUserJimKnopf().copy( + val user = UserTestFixtures.userJimKnopf.copy( newPassword = "verySecretPassword" ) val givenUserProfileWithPassword = """ diff --git a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/InboxControllerIntegrationTest.kt b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/InboxControllerIntegrationTest.kt index a665ada0..8660b0b5 100644 --- a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/InboxControllerIntegrationTest.kt +++ b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/InboxControllerIntegrationTest.kt @@ -5,7 +5,11 @@ import io.mockk.every import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import org.railwaystations.rsapi.adapter.db.* +import org.railwaystations.rsapi.adapter.db.CountryAdapter +import org.railwaystations.rsapi.adapter.db.InboxAdapter +import org.railwaystations.rsapi.adapter.db.PhotoAdapter +import org.railwaystations.rsapi.adapter.db.StationAdapter +import org.railwaystations.rsapi.adapter.db.UserAdapter import org.railwaystations.rsapi.adapter.monitoring.FakeMonitor import org.railwaystations.rsapi.adapter.photostorage.PhotoFileStorage import org.railwaystations.rsapi.adapter.web.ErrorHandlingControllerAdvice @@ -16,13 +20,18 @@ import org.railwaystations.rsapi.adapter.web.auth.RSAuthenticationProvider import org.railwaystations.rsapi.adapter.web.auth.RSUserDetailsService import org.railwaystations.rsapi.adapter.web.auth.WebSecurityConfig import org.railwaystations.rsapi.app.ClockTestConfiguration -import org.railwaystations.rsapi.core.model.* +import org.railwaystations.rsapi.core.model.Coordinates +import org.railwaystations.rsapi.core.model.EMAIL_VERIFIED +import org.railwaystations.rsapi.core.model.InboxEntry import org.railwaystations.rsapi.core.model.InboxEntryTestFixtures.createInboxEntry import org.railwaystations.rsapi.core.model.PhotoTestFixtures.createPhoto +import org.railwaystations.rsapi.core.model.ProblemReportType +import org.railwaystations.rsapi.core.model.Station import org.railwaystations.rsapi.core.model.StationTestFixtures.createStation +import org.railwaystations.rsapi.core.model.UserTestFixtures import org.railwaystations.rsapi.core.model.UserTestFixtures.USER_AGENT -import org.railwaystations.rsapi.core.model.UserTestFixtures.createUserJimKnopf -import org.railwaystations.rsapi.core.model.UserTestFixtures.createUserNickname +import org.railwaystations.rsapi.core.model.UserTestFixtures.userJimKnopf +import org.railwaystations.rsapi.core.model.UserTestFixtures.userNickname import org.railwaystations.rsapi.core.ports.inbound.ManageProfileUseCase import org.railwaystations.rsapi.core.services.InboxService import org.springframework.beans.factory.annotation.Autowired @@ -66,19 +75,19 @@ internal class InboxControllerIntegrationTest { private lateinit var clock: Clock @MockkBean - private lateinit var inboxDao: InboxDao + private lateinit var inboxAdapter: InboxAdapter @MockkBean - private lateinit var stationDao: StationDao + private lateinit var stationAdapter: StationAdapter @MockkBean - private lateinit var userDao: UserDao + private lateinit var userAdapter: UserAdapter @MockkBean - private lateinit var countryDao: CountryDao + private lateinit var countryAdapter: CountryAdapter @MockkBean - private lateinit var photoDao: PhotoDao + private lateinit var photoAdapter: PhotoAdapter @MockkBean private lateinit var manageProfileUseCase: ManageProfileUseCase @@ -88,30 +97,30 @@ internal class InboxControllerIntegrationTest { @BeforeEach fun setUp() { - val userNickname = createUserNickname() - every { userDao.findByEmail(userNickname.email!!) } returns userNickname - val userSomeuser = UserTestFixtures.createSomeUser() - every { userDao.findByEmail(userSomeuser.email!!) } returns userSomeuser + val userNickname = userNickname + every { userAdapter.findByEmail(userNickname.email!!) } returns userNickname + val userSomeuser = UserTestFixtures.someUser + every { userAdapter.findByEmail(userSomeuser.email!!) } returns userSomeuser val key0815 = Station.Key("ch", "0815") - val station0815 = createStation(key0815, Coordinates(40.1, 7.0), createPhoto(key0815, createUserJimKnopf())) - every { stationDao.findByKey(key0815.country, key0815.id) } returns station0815 + val station0815 = createStation(key0815, Coordinates(40.1, 7.0), createPhoto(key0815, userJimKnopf)) + every { stationAdapter.findByKey(key0815) } returns station0815 val key1234 = Station.Key("de", "1234") - val station1234 = createStation(key1234, Coordinates(40.1, 7.0), createPhoto(key1234, createUserJimKnopf())) - every { stationDao.findByKey(key1234.country, key1234.id) } returns station1234 + val station1234 = createStation(key1234, Coordinates(40.1, 7.0), createPhoto(key1234, userJimKnopf)) + every { stationAdapter.findByKey(key1234) } returns station1234 monitor.reset() } @Test fun userInbox() { - val user = createUserNickname() + val user = userNickname - every { inboxDao.findById(1) } returns createInboxEntry(user, 1, "de", "4711", null, false) - every { inboxDao.findById(2) } returns createInboxEntry(user, 2, "de", "1234", null, true) - every { inboxDao.findById(3) } returns createInboxEntry(user, 3, "de", "5678", "rejected", true) - every { inboxDao.findById(4) } returns createInboxEntry(user, 4, "ch", "0815", null, false) + every { inboxAdapter.findById(1) } returns createInboxEntry(user, 1, "de", "4711", null, false) + every { inboxAdapter.findById(2) } returns createInboxEntry(user, 2, "de", "1234", null, true) + every { inboxAdapter.findById(3) } returns createInboxEntry(user, 3, "de", "5678", "rejected", true) + every { inboxAdapter.findById(4) } returns createInboxEntry(user, 4, "ch", "0815", null, false) val inboxStateQueries = """ [ @@ -150,7 +159,7 @@ internal class InboxControllerIntegrationTest { .with( user( AuthUser( - createUserNickname().copy( + userNickname.copy( emailVerification = emailVerification ), listOf() ) @@ -164,7 +173,7 @@ internal class InboxControllerIntegrationTest { @Test fun postProblemReportOther() { every { - inboxDao.insert( + inboxAdapter.insert( InboxEntry( countryCode = "de", stationId = "1234", @@ -198,7 +207,7 @@ internal class InboxControllerIntegrationTest { @Test fun postProblemReportWrongLocation() { every { - inboxDao.insert( + inboxAdapter.insert( InboxEntry( countryCode = "de", stationId = "1234", @@ -233,7 +242,7 @@ internal class InboxControllerIntegrationTest { @Test fun postProblemReportWrongName() { every { - inboxDao.insert( + inboxAdapter.insert( InboxEntry( countryCode = "de", stationId = "1234", @@ -280,10 +289,9 @@ internal class InboxControllerIntegrationTest { @Test fun adminInbox() { - val user = createUserNickname() - every { inboxDao.findPendingInboxEntries() } returns listOf( - createInboxEntry(user, 1, "de", "4711", null, false), + every { inboxAdapter.findPendingInboxEntries() } returns listOf( + createInboxEntry(userNickname, 1, "de", "4711", null, false), ) mvc.perform( @@ -291,7 +299,7 @@ internal class InboxControllerIntegrationTest { .header(HttpHeaders.USER_AGENT, USER_AGENT) .header(HttpHeaders.AUTHORIZATION, "not_used_but_required") .contentType("application/json") - .with(user(AuthUser(user, listOf()))) + .with(user(AuthUser(userNickname, listOf()))) .with(csrf()) ) .andExpect(validOpenApiResponse()) diff --git a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/InboxControllerTest.kt b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/InboxControllerTest.kt index 35fb29a1..560b7944 100644 --- a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/InboxControllerTest.kt +++ b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/InboxControllerTest.kt @@ -12,8 +12,17 @@ import org.railwaystations.rsapi.adapter.web.ErrorHandlingControllerAdvice import org.railwaystations.rsapi.adapter.web.OpenApiValidatorUtil.validOpenApiResponse import org.railwaystations.rsapi.adapter.web.RequestUtil import org.railwaystations.rsapi.adapter.web.auth.AuthUser -import org.railwaystations.rsapi.core.model.* +import org.railwaystations.rsapi.core.model.Coordinates +import org.railwaystations.rsapi.core.model.InboxEntry +import org.railwaystations.rsapi.core.model.InboxResponse +import org.railwaystations.rsapi.core.model.InboxStateQuery import org.railwaystations.rsapi.core.model.InboxStateQuery.InboxState +import org.railwaystations.rsapi.core.model.ProblemReport +import org.railwaystations.rsapi.core.model.ProblemReportType +import org.railwaystations.rsapi.core.model.PublicInboxEntry +import org.railwaystations.rsapi.core.model.ROLE_ADMIN +import org.railwaystations.rsapi.core.model.ROLE_USER +import org.railwaystations.rsapi.core.model.UserTestFixtures import org.railwaystations.rsapi.core.ports.inbound.ManageInboxUseCase import org.railwaystations.rsapi.core.ports.inbound.ManageProfileUseCase import org.springframework.beans.factory.annotation.Autowired @@ -28,7 +37,9 @@ import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequ import org.springframework.test.context.ActiveProfiles import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.ResultActions -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import org.springframework.web.servlet.LocaleResolver import java.time.Instant @@ -55,7 +66,7 @@ internal class InboxControllerTest { @MockkBean private lateinit var localeResolver: LocaleResolver - private val userNickname = UserTestFixtures.createUserNickname() + private val userNickname = UserTestFixtures.userNickname @BeforeEach fun setUp() { diff --git a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/LoginControllerTest.kt b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/LoginControllerTest.kt index b3cdc478..ccbd5da5 100644 --- a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/LoginControllerTest.kt +++ b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/LoginControllerTest.kt @@ -231,7 +231,7 @@ internal class LoginControllerTest { } private fun createValidUser(): User { - return UserTestFixtures.createUserNickname().copy( + return UserTestFixtures.userNickname.copy( newPassword = "very_secret", locale = Locale.GERMAN, ) diff --git a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/PhotoStationsControllerTest.kt b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/PhotoStationsControllerTest.kt index 7acebdc2..29870a17 100644 --- a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/PhotoStationsControllerTest.kt +++ b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/PhotoStationsControllerTest.kt @@ -7,7 +7,6 @@ import org.railwaystations.rsapi.adapter.web.ErrorHandlingControllerAdvice import org.railwaystations.rsapi.adapter.web.OpenApiValidatorUtil.validOpenApiResponse import org.railwaystations.rsapi.core.model.Coordinates import org.railwaystations.rsapi.core.model.License -import org.railwaystations.rsapi.core.model.Photo import org.railwaystations.rsapi.core.model.PhotoTestFixtures import org.railwaystations.rsapi.core.model.Station import org.railwaystations.rsapi.core.model.StationTestFixtures.createStation @@ -36,9 +35,9 @@ internal class PhotoStationsControllerTest { @Test fun getPhotoStationsByCountry() { - val stationXY1 = createStationXY1() - val stationXY5 = createStationXY5().copy( - photos = listOf(createPhotoXY5()) + val stationXY1 = stationXY1 + val stationXY5 = stationXY5.copy( + photos = listOf(photoXY5) ) every { photoStationsService.findByCountry(setOf("xy"), null, null) } returns setOf(stationXY1, stationXY5) @@ -75,7 +74,7 @@ internal class PhotoStationsControllerTest { ) .andExpect( jsonPath("$.stations[?(@.id == '5')].photos[0].createdAt") - .value(CREATED_AT.toEpochMilli()) + .value(createdAt.toEpochMilli()) ) .andExpect( jsonPath("$.stations[?(@.id == '5')].photos[0].outdated").doesNotExist() @@ -93,7 +92,7 @@ internal class PhotoStationsControllerTest { @Test fun getPhotoStationsByCountryWithIsActiveFilter() { - every { photoStationsService.findByCountry(setOf("xy"), null, false) } returns setOf(createStationXY1()) + every { photoStationsService.findByCountry(setOf("xy"), null, false) } returns setOf(stationXY1) mvc.perform(get("/photoStationsByCountry/xy?isActive=false")) .andExpect(status().isOk()) @@ -114,8 +113,8 @@ internal class PhotoStationsControllerTest { @Test fun getPhotoStationsByCountryWithHasPhotoFilter() { - val stationXY5 = createStationXY5().copy( - photos = listOf(createPhotoXY5()) + val stationXY5 = stationXY5.copy( + photos = listOf(photoXY5) ) every { photoStationsService.findByCountry(setOf("xy"), true, null) } returns setOf(stationXY5) @@ -145,7 +144,7 @@ internal class PhotoStationsControllerTest { .andExpect(jsonPath("$.stations[0].photos[0].path").value("/xy/5.jpg")) .andExpect(jsonPath("$.stations[0].photos[0].license").value("CC0_10")) .andExpect( - jsonPath("$.stations[0].photos[0].createdAt").value(CREATED_AT.toEpochMilli()) + jsonPath("$.stations[0].photos[0].createdAt").value(createdAt.toEpochMilli()) ) .andExpect(jsonPath("$.stations[0].photos[0].outdated").doesNotExist()) .andExpect(jsonPath("$.stations[0].photos[1]").doesNotExist()) @@ -167,7 +166,7 @@ internal class PhotoStationsControllerTest { @Test fun getPhotoStationByIdOfInactiveStationWithoutPhotos() { - every { photoStationsService.findByCountryAndId("xy", "1") } returns createStationXY1() + every { photoStationsService.findByKey(stationXY1.key) } returns stationXY1 mvc.perform(get("/photoStationById/xy/1")) .andExpect(status().isOk()) @@ -188,10 +187,10 @@ internal class PhotoStationsControllerTest { @Test fun getPhotoStationByIdOfActiveStationWithTwoPhotos() { - val stationAB3 = createStationAB3().copy( - photos = listOf(createPhotoAB3_1(), createPhotoAB3_2()) + val stationAB3 = stationAB3.copy( + photos = listOf(photoAB3_1, photoAB3_2) ) - every { photoStationsService.findByCountryAndId("ab", "3") } returns stationAB3 + every { photoStationsService.findByKey(stationAB3.key) } returns stationAB3 mvc.perform(get("/photoStationById/ab/3")) .andExpect(status().isOk()) @@ -225,7 +224,7 @@ internal class PhotoStationsControllerTest { .andExpect(jsonPath("$.stations[0].photos[0].path").value("/ab/3_2.jpg")) .andExpect(jsonPath("$.stations[0].photos[0].license").value("CC0_10")) .andExpect( - jsonPath("$.stations[0].photos[0].createdAt").value(CREATED_AT.toEpochMilli()) + jsonPath("$.stations[0].photos[0].createdAt").value(createdAt.toEpochMilli()) ) .andExpect(jsonPath("$.stations[0].photos[0].outdated").value(true)) .andExpect(jsonPath("$.stations[0].photos[1].id").value(1L)) @@ -233,7 +232,7 @@ internal class PhotoStationsControllerTest { .andExpect(jsonPath("$.stations[0].photos[1].path").value("/ab/3_1.jpg")) .andExpect(jsonPath("$.stations[0].photos[1].license").value("CC_BY_NC_40_INT")) .andExpect( - jsonPath("$.stations[0].photos[1].createdAt").value(CREATED_AT.toEpochMilli()) + jsonPath("$.stations[0].photos[1].createdAt").value(createdAt.toEpochMilli()) ) .andExpect(jsonPath("$.stations[0].photos[1].outdated").doesNotExist()) .andExpect(jsonPath("$.stations[0].photos[2]").doesNotExist()) @@ -242,7 +241,7 @@ internal class PhotoStationsControllerTest { @Test fun getPhotoStationByIdWithNonExistingId() { - every { photoStationsService.findByCountryAndId(any(), any()) } returns null + every { photoStationsService.findByKey(any()) } returns null mvc.perform(get("/photoStationById/ab/not_existing_id")) .andExpect(status().isNotFound()) .andExpect(validOpenApiResponse()) @@ -250,8 +249,8 @@ internal class PhotoStationsControllerTest { @Test fun getPhotoStationsByPhotographerWithCountryFilter() { - val stationXY5 = createStationXY5().copy( - photos = listOf(createPhotoXY5()) + val stationXY5 = stationXY5.copy( + photos = listOf(photoXY5) ) every { photoStationsService.findByPhotographer("Jim Knopf", "xy") } returns setOf(stationXY5) @@ -281,7 +280,7 @@ internal class PhotoStationsControllerTest { .andExpect(jsonPath("$.stations[0].photos[0].path").value("/xy/5.jpg")) .andExpect(jsonPath("$.stations[0].photos[0].license").value("CC0_10")) .andExpect( - jsonPath("$.stations[0].photos[0].createdAt").value(CREATED_AT.toEpochMilli()) + jsonPath("$.stations[0].photos[0].createdAt").value(createdAt.toEpochMilli()) ) .andExpect(jsonPath("$.stations[0].photos[0].outdated").doesNotExist()) .andExpect(jsonPath("$.stations[0].photos[1]").doesNotExist()) @@ -290,11 +289,11 @@ internal class PhotoStationsControllerTest { @Test fun getPhotoStationsByPhotographer() { - val stationAB3 = createStationAB3().copy( - photos = listOf(createPhotoAB3_2()) + val stationAB3 = stationAB3.copy( + photos = listOf(photoAB3_2) ) - val stationXY5 = createStationXY5().copy( - photos = listOf(createPhotoXY5()) + val stationXY5 = stationXY5.copy( + photos = listOf(photoXY5) ) every { photoStationsService.findByPhotographer("Jim Knopf", null) } returns setOf(stationAB3, stationXY5) @@ -332,7 +331,7 @@ internal class PhotoStationsControllerTest { ) .andExpect( jsonPath("$.stations[?(@.id == '5')].photos[0].createdAt") - .value(CREATED_AT.toEpochMilli()) + .value(createdAt.toEpochMilli()) ) .andExpect( jsonPath("$.stations[?(@.id == '5')].photos[0].outdated").doesNotExist() @@ -358,7 +357,7 @@ internal class PhotoStationsControllerTest { ) .andExpect( jsonPath("$.stations[?(@.id == '3')].photos[0].createdAt") - .value(CREATED_AT.toEpochMilli()) + .value(createdAt.toEpochMilli()) ) .andExpect(jsonPath("$.stations[?(@.id == '3')].photos[0].outdated").value(true)) .andExpect(jsonPath("$.stations[?(@.id == '3')].photos[1]").doesNotExist()) @@ -367,8 +366,8 @@ internal class PhotoStationsControllerTest { @Test fun getPhotoStationsByRecentPhotoImportsWithDefaultSinceHours() { - val stationXY5 = createStationXY5().copy( - photos = listOf(createPhotoXY5()) + val stationXY5 = stationXY5.copy( + photos = listOf(photoXY5) ) every { photoStationsService.findRecentImports(10) } returns setOf(stationXY5) @@ -385,8 +384,8 @@ internal class PhotoStationsControllerTest { @Test fun getPhotoStationsByRecentPhotoImportsWithSinceHours() { - val stationAB3 = createStationAB3().copy( - photos = listOf(createPhotoAB3_1()) + val stationAB3 = stationAB3.copy( + photos = listOf(photoAB3_1) ) every { photoStationsService.findRecentImports(100) } returns setOf(stationAB3) @@ -401,69 +400,57 @@ internal class PhotoStationsControllerTest { .andExpect(jsonPath("$.stations[0].photos[0].id").value(1L)) } - private fun createPhotoAB3_2(): Photo { - return PhotoTestFixtures.createPhoto(KEY_AB_3, PHOTOGRAPHER_JIM_KNOPF).copy( - id = 2L, - primary = true, - createdAt = CREATED_AT, - license = License.CC0_10, - outdated = true, - urlPath = "/${KEY_AB_3.country}/${KEY_AB_3.id}_2.jpg", - ) - } - - private fun createPhotoAB3_1(): Photo { - return PhotoTestFixtures.createPhoto(KEY_AB_3, PHOTOGRAPHER_PETER_PAN).copy( - id = 1L, - createdAt = CREATED_AT, - license = License.CC_BY_NC_40_INT, - urlPath = "/${KEY_AB_3.country}/${KEY_AB_3.id}_1.jpg", - ) - } - - private fun createStationAB3(): Station { - return createStation(KEY_AB_3, Coordinates(40.0, 6.0), null).copy( - title = "Nimmerland", - ds100 = "ABC", - ) - } - - private fun createPhotoXY5(): Photo { - return PhotoTestFixtures.createPhoto(KEY_XY_5, PHOTOGRAPHER_JIM_KNOPF).copy( - createdAt = CREATED_AT, - ) - } - - private fun createStationXY5(): Station { - return createStation(KEY_XY_5, Coordinates(50.0, 9.0), null).copy( - title = "Lummerland", - ds100 = "XYZ", - ) - } - - private fun createStationXY1(): Station { - return createStation(KEY_XY_1, Coordinates(50.1, 9.1), null).copy( - title = "Lummerland Ost", - ds100 = "XYY", - active = false, - ) - } } -private val KEY_XY_1: Station.Key = Station.Key("xy", "1") -private val KEY_XY_5: Station.Key = Station.Key("xy", "5") -private val PHOTOGRAPHER_JIM_KNOPF: User = UserTestFixtures.createSomeUser().copy( +private val keyXY1: Station.Key = Station.Key("xy", "1") +private val keyXY5: Station.Key = Station.Key("xy", "5") +private val photographerJimKnopf: User = UserTestFixtures.someUser.copy( name = "Jim Knopf", url = "photographerUrlJim", license = License.CC0_10, ) -private val KEY_AB_3: Station.Key = Station.Key("ab", "3") -private val CREATED_AT: Instant = Instant.now() -private val PHOTOGRAPHER_PETER_PAN: User = - UserTestFixtures.createSomeUser().copy( +private val keyAB3: Station.Key = Station.Key("ab", "3") +private val createdAt: Instant = Instant.now() +private val photographerPeterPan: User = + UserTestFixtures.someUser.copy( name = "Peter Pan", url = "photographerUrlPeter", license = License.CC_BY_NC_SA_30_DE, ) +private val photoAB3_2 = PhotoTestFixtures.createPhoto(keyAB3, photographerJimKnopf).copy( + id = 2L, + primary = true, + createdAt = createdAt, + license = License.CC0_10, + outdated = true, + urlPath = "/${keyAB3.country}/${keyAB3.id}_2.jpg", +) + +private val photoAB3_1 = PhotoTestFixtures.createPhoto(keyAB3, photographerPeterPan).copy( + id = 1L, + createdAt = createdAt, + license = License.CC_BY_NC_40_INT, + urlPath = "/${keyAB3.country}/${keyAB3.id}_1.jpg", +) + +private val stationAB3 = createStation(keyAB3, Coordinates(40.0, 6.0), null).copy( + title = "Nimmerland", + ds100 = "ABC", +) + +private val photoXY5 = PhotoTestFixtures.createPhoto(keyXY5, photographerJimKnopf).copy( + createdAt = createdAt, +) + +private val stationXY5 = createStation(keyXY5, Coordinates(50.0, 9.0), null).copy( + title = "Lummerland", + ds100 = "XYZ", +) + +private val stationXY1 = createStation(keyXY1, Coordinates(50.1, 9.1), null).copy( + title = "Lummerland Ost", + ds100 = "XYY", + active = false, +) diff --git a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/PhotoUploadControllerTest.kt b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/PhotoUploadControllerTest.kt index bc750ceb..93be4b85 100644 --- a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/PhotoUploadControllerTest.kt +++ b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/PhotoUploadControllerTest.kt @@ -10,7 +10,11 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.CsvSource -import org.railwaystations.rsapi.adapter.db.* +import org.railwaystations.rsapi.adapter.db.CountryAdapter +import org.railwaystations.rsapi.adapter.db.InboxAdapter +import org.railwaystations.rsapi.adapter.db.PhotoAdapter +import org.railwaystations.rsapi.adapter.db.StationAdapter +import org.railwaystations.rsapi.adapter.db.UserAdapter import org.railwaystations.rsapi.adapter.monitoring.FakeMonitor import org.railwaystations.rsapi.adapter.photostorage.PhotoFileStorage import org.railwaystations.rsapi.adapter.photostorage.WorkDir @@ -22,12 +26,16 @@ import org.railwaystations.rsapi.adapter.web.auth.RSAuthenticationProvider import org.railwaystations.rsapi.adapter.web.auth.RSUserDetailsService import org.railwaystations.rsapi.adapter.web.auth.WebSecurityConfig import org.railwaystations.rsapi.app.ClockTestConfiguration -import org.railwaystations.rsapi.core.model.* +import org.railwaystations.rsapi.core.model.Coordinates +import org.railwaystations.rsapi.core.model.EMAIL_VERIFIED +import org.railwaystations.rsapi.core.model.InboxEntry +import org.railwaystations.rsapi.core.model.Photo import org.railwaystations.rsapi.core.model.PhotoTestFixtures.createPhoto +import org.railwaystations.rsapi.core.model.Station import org.railwaystations.rsapi.core.model.StationTestFixtures.createStation -import org.railwaystations.rsapi.core.model.UserTestFixtures.createSomeUser -import org.railwaystations.rsapi.core.model.UserTestFixtures.createUserJimKnopf -import org.railwaystations.rsapi.core.model.UserTestFixtures.createUserNickname +import org.railwaystations.rsapi.core.model.UserTestFixtures.someUser +import org.railwaystations.rsapi.core.model.UserTestFixtures.userJimKnopf +import org.railwaystations.rsapi.core.model.UserTestFixtures.userNickname import org.railwaystations.rsapi.core.ports.inbound.ManageProfileUseCase import org.railwaystations.rsapi.core.services.InboxService import org.springframework.beans.factory.annotation.Autowired @@ -79,42 +87,42 @@ internal class PhotoUploadControllerTest { private lateinit var workDir: WorkDir @MockkBean(relaxed = true) - private lateinit var inboxDao: InboxDao + private lateinit var inboxAdapter: InboxAdapter @MockkBean - private lateinit var stationDao: StationDao + private lateinit var stationAdapter: StationAdapter @MockkBean(relaxed = true) private lateinit var authenticator: RSAuthenticationProvider @MockkBean(relaxed = true) - private lateinit var userDao: UserDao + private lateinit var userAdapter: UserAdapter @MockkBean - private lateinit var countryDao: CountryDao + private lateinit var countryAdapter: CountryAdapter @MockkBean(relaxed = true) - private lateinit var photoDao: PhotoDao + private lateinit var photoAdapter: PhotoAdapter @MockkBean(relaxed = true) private lateinit var manageProfileUseCase: ManageProfileUseCase @BeforeEach fun setUp() { - val userNickname = createUserNickname() - every { userDao.findByEmail(userNickname.email!!) } returns userNickname - val userSomeuser = createSomeUser() - every { userDao.findByEmail(userSomeuser.email!!) } returns userSomeuser + val userNickname = userNickname + every { userAdapter.findByEmail(userNickname.email!!) } returns userNickname + val userSomeuser = someUser + every { userAdapter.findByEmail(userSomeuser.email!!) } returns userSomeuser val key4711 = Station.Key("de", "4711") val station4711 = createStationWithDs100(key4711, Coordinates(50.0, 9.0), "XYZ", null) val key1234 = Station.Key("de", "1234") val station1234 = - createStationWithDs100(key1234, Coordinates(40.1, 7.0), "LAL", createPhoto(key1234, createUserJimKnopf())) + createStationWithDs100(key1234, Coordinates(40.1, 7.0), "LAL", createPhoto(key1234, userJimKnopf)) - every { stationDao.findByKey(key4711.country, key4711.id) } returns station4711 - every { stationDao.findByKey(key1234.country, key1234.id) } returns station1234 - every { stationDao.countNearbyCoordinates(any()) } returns 0 + every { stationAdapter.findByKey(key4711) } returns station4711 + every { stationAdapter.findByKey(key1234) } returns station1234 + every { stationAdapter.countNearbyCoordinates(any()) } returns 0 monitor.reset() } @@ -133,7 +141,7 @@ internal class PhotoUploadControllerTest { private fun whenPostImage( nickname: String, - userId: Int, + userId: Long, email: String, stationId: String?, country: String?, @@ -163,7 +171,7 @@ internal class PhotoUploadControllerTest { private fun whenPostPhotoUpload( nickname: String, - userId: Int, + userId: Long, email: String, stationId: String?, country: String?, @@ -204,7 +212,7 @@ internal class PhotoUploadControllerTest { .with( user( AuthUser( - createUserNickname().copy( + userNickname.copy( name = nickname, id = userId, email = email, @@ -225,24 +233,24 @@ internal class PhotoUploadControllerTest { UsernamePasswordAuthenticationToken("someuser@example.com", "secretUploadToken") ) } returns UsernamePasswordAuthenticationToken("", "", listOf()) - every { inboxDao.insert(any()) } returns 1L + every { inboxAdapter.insert(any()) } returns 1L val response = whenPostImageMultipartFormdata("someuser@example.com", "some_verification_token") assertThat(response).contains("UNAUTHORIZED") assertThat(response).contains("Profile incomplete, not allowed to upload photos") - verify(exactly = 0) { inboxDao.insert(any()) } + verify(exactly = 0) { inboxAdapter.insert(any()) } assertThat(monitor.getMessages().size).isEqualTo(0) } @Test fun postPhotoForExistingStationViaMultipartFormdata() { val uploadCaptor = slot() - every { inboxDao.insert(any()) } returns 1L + every { inboxAdapter.insert(any()) } returns 1L val response = whenPostImageMultipartFormdata("nickname@example.com", EMAIL_VERIFIED) assertThat(response).contains("REVIEW") assertFileWithContentExistsInInbox("image-content", "1.jpg") - verify { inboxDao.insert(capture(uploadCaptor)) } + verify { inboxAdapter.insert(capture(uploadCaptor)) } assertUpload(uploadCaptor.captured, "de", "4711", null, null) assertThat(monitor.getMessages()[0]).isEqualTo( @@ -273,7 +281,7 @@ internal class PhotoUploadControllerTest { .with( user( AuthUser( - createUserNickname().copy( + userNickname.copy( email = email, emailVerification = emailVerified, ), listOf() @@ -294,11 +302,11 @@ internal class PhotoUploadControllerTest { @Test fun repostMissingStationWithoutPhotoViaMultipartFormdata() { val uploadCaptor = slot() - every { inboxDao.insert(any()) } returns 1L + every { inboxAdapter.insert(any()) } returns 1L val response = mvc.perform( multipart("/photoUploadMultipartFormdata") .file(MockMultipartFile("file", null, "application/octet-stream", null as ByteArray?)) - .with(user(AuthUser(createUserNickname(), listOf()))) + .with(user(AuthUser(userNickname, listOf()))) .param("stationTitle", "Missing Station") .param("latitude", "10") .param("longitude", "20") @@ -313,7 +321,7 @@ internal class PhotoUploadControllerTest { .andReturn().response.contentAsString assertThat(response).contains("REVIEW") - verify { inboxDao.insert(capture(uploadCaptor)) } + verify { inboxAdapter.insert(capture(uploadCaptor)) } assertUpload(uploadCaptor.captured, "de", null, "Missing Station", Coordinates(10.0, 20.0)) assertThat(monitor.getMessages()[0]).isEqualTo( @@ -329,7 +337,7 @@ internal class PhotoUploadControllerTest { @Test fun uploadPhoto() { val uploadCaptor = slot() - every { inboxDao.insert(any()) } returns 1L + every { inboxAdapter.insert(any()) } returns 1L whenPostImage("@nick name", 42, "nickname@example.com", "4711", "de", null, null, null, "Some Comment") .andExpect(status().isAccepted()) @@ -339,7 +347,7 @@ internal class PhotoUploadControllerTest { assertFileWithContentExistsInInbox("image-content", "1.jpg") - verify { inboxDao.insert(capture(uploadCaptor)) } + verify { inboxAdapter.insert(capture(uploadCaptor)) } assertUpload(uploadCaptor.captured, "de", "4711", null, null) assertThat(monitor.getMessages()[0]).isEqualTo( """ @@ -375,9 +383,8 @@ internal class PhotoUploadControllerTest { @Test fun postMissingStation() { - every { inboxDao.insert(any()) } returns 4L + every { inboxAdapter.insert(any()) } returns 4L val uploadCaptor = slot() - val userNickname = createUserNickname() whenPostImage( nickname = userNickname.name, @@ -396,7 +403,7 @@ internal class PhotoUploadControllerTest { .andExpect(jsonPath("$.filename").value("4.jpg")) assertFileWithContentExistsInInbox(IMAGE_CONTENT, "4.jpg") - verify { inboxDao.insert(capture(uploadCaptor)) } + verify { inboxAdapter.insert(capture(uploadCaptor)) } assertUpload(uploadCaptor.captured, null, null, "Missing Station", Coordinates(50.9876, 9.1234)) assertThat(monitor.getMessages()[0]).isEqualTo( @@ -412,9 +419,8 @@ internal class PhotoUploadControllerTest { @Test fun postMissingStationWithoutPhoto() { - every { inboxDao.insert(any()) } returns 4L + every { inboxAdapter.insert(any()) } returns 4L val uploadCaptor = slot() - val userNickname = createUserNickname() whenPostPhotoUpload( nickname = userNickname.name, @@ -435,7 +441,7 @@ internal class PhotoUploadControllerTest { .andExpect(jsonPath("$.id").value(4)) .andExpect(jsonPath("$.filename").doesNotExist()) - verify { inboxDao.insert(capture(uploadCaptor)) } + verify { inboxAdapter.insert(capture(uploadCaptor)) } assertUpload(uploadCaptor.captured, "de", null, "Missing Station", Coordinates(50.9876, 9.1234)) assertThat(monitor.getMessages()[0]).isEqualTo( @@ -453,7 +459,6 @@ internal class PhotoUploadControllerTest { "-91d, 9.1234d", "91d, 9.1234d", "50.9876d, -181d", "50.9876d, 181d" ) fun postMissingStationLatLonOutOfRange(latitude: Double?, longitude: Double?) { - val userNickname = createUserNickname() whenPostImage( nickname = userNickname.name, userId = userNickname.id, @@ -473,7 +478,7 @@ internal class PhotoUploadControllerTest { @Test fun postPhotoWithSomeUserWithTokenSalt() { - every { inboxDao.insert(any()) } returns 3L + every { inboxAdapter.insert(any()) } returns 3L whenPostImage("@someuser", 11, "someuser@example.com", "4711", "de", null, null, null, null) .andExpect(status().isAccepted()) .andExpect(jsonPath("$.state").value("REVIEW")) @@ -494,8 +499,8 @@ internal class PhotoUploadControllerTest { @Test fun postDuplicateInbox() { - every { inboxDao.insert(any()) } returns 2L - every { inboxDao.countPendingInboxEntriesForStation(null, "de", "4711") } returns 1 + every { inboxAdapter.insert(any()) } returns 2L + every { inboxAdapter.countPendingInboxEntriesForStation(null, "de", "4711") } returns 1 whenPostImage("@nick name", 42, "nickname@example.com", "4711", "de", null, null, null, null) .andExpect(status().isAccepted()) @@ -527,7 +532,7 @@ internal class PhotoUploadControllerTest { @Test fun postDuplicate() { - every { inboxDao.insert(any()) } returns 5L + every { inboxAdapter.insert(any()) } returns 5L whenPostImage("@nick name", 42, "nickname@example.com", "1234", "de", null, null, null, null) .andExpect(status().isAccepted()) .andExpect(jsonPath("$.state").value("REVIEW")) @@ -557,7 +562,7 @@ internal class PhotoUploadControllerTest { @Test fun postInvalidCountry() { - every { stationDao.findByKey(eq("xy"), any()) } returns null + every { stationAdapter.findByKey(any()) } returns null whenPostImage("nickname", 42, "nickname@example.com", "4711", "xy", null, null, null, null) .andExpect(status().isBadRequest()) diff --git a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/PhotographersControllerTest.kt b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/PhotographersControllerTest.kt index 5c2a9908..230df207 100644 --- a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/PhotographersControllerTest.kt +++ b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/PhotographersControllerTest.kt @@ -6,8 +6,8 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource -import org.railwaystations.rsapi.adapter.db.CountryDao -import org.railwaystations.rsapi.adapter.db.StationDao +import org.railwaystations.rsapi.adapter.db.CountryAdapter +import org.railwaystations.rsapi.adapter.db.StationAdapter import org.railwaystations.rsapi.adapter.web.ErrorHandlingControllerAdvice import org.railwaystations.rsapi.adapter.web.OpenApiValidatorUtil.validOpenApiResponse import org.railwaystations.rsapi.core.model.CountryTestFixtures @@ -30,14 +30,14 @@ internal class PhotographersControllerTest { private lateinit var mvc: MockMvc @MockkBean - private lateinit var stationDao: StationDao + private lateinit var stationAdapter: StationAdapter @MockkBean - private lateinit var countryDao: CountryDao + private lateinit var countryAdapter: CountryAdapter @BeforeEach fun setup() { - every { countryDao.findById("de") } returns CountryTestFixtures.createCountry("de") + every { countryAdapter.findById("de") } returns CountryTestFixtures.createCountry("de") } @ParameterizedTest @@ -50,7 +50,7 @@ internal class PhotographersControllerTest { @Test fun getPhotographersOfCountryDe() { - every { stationDao.getPhotographerMap("de") } returns createPhotographersResponse() + every { stationAdapter.getPhotographerMap("de") } returns createPhotographersResponse() mvc.perform(get("/photographers?country=de")) .andExpect(status().isOk()) @@ -63,7 +63,7 @@ internal class PhotographersControllerTest { @Test fun getPhotographersOfAllCountries() { - every { stationDao.getPhotographerMap(null) } returns createPhotographersResponse() + every { stationAdapter.getPhotographerMap(null) } returns createPhotographersResponse() mvc.perform(get("/photographers")) .andExpect(status().isOk()) @@ -74,7 +74,8 @@ internal class PhotographersControllerTest { .andExpect(validOpenApiResponse()) } - private fun createPhotographersResponse(): Map { - return mapOf("@user27" to 31L, "@user8" to 29L, "@user10" to 15L, "@user0" to 9L) + private fun createPhotographersResponse(): Map { + return mapOf("@user27" to 31, "@user8" to 29, "@user10" to 15, "@user0" to 9) } + } \ No newline at end of file diff --git a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/ProfileControllerTest.kt b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/ProfileControllerTest.kt index 6cdd12da..2b16e1a7 100644 --- a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/ProfileControllerTest.kt +++ b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/ProfileControllerTest.kt @@ -5,7 +5,7 @@ import io.mockk.every import io.mockk.verify import net.javacrumbs.jsonunit.spring.JsonUnitResultMatchers.json import org.junit.jupiter.api.Test -import org.railwaystations.rsapi.adapter.db.UserDao +import org.railwaystations.rsapi.adapter.db.UserAdapter import org.railwaystations.rsapi.adapter.web.ErrorHandlingControllerAdvice import org.railwaystations.rsapi.adapter.web.OpenApiValidatorUtil.validOpenApiResponse import org.railwaystations.rsapi.adapter.web.RequestUtil @@ -31,7 +31,9 @@ import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequ import org.springframework.test.context.ActiveProfiles import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.ResultActions -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post import org.springframework.test.web.servlet.request.RequestPostProcessor import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status @@ -59,7 +61,7 @@ class ProfileControllerTest { private lateinit var profileService: ProfileService @MockkBean - private lateinit var userDao: UserDao + private lateinit var userAdapter: UserAdapter @Test fun getMyProfile() { @@ -96,13 +98,13 @@ class ProfileControllerTest { private fun givenExistingUser(): User { val key = "246172676F6E32696424763D3139246D3D36353533362C743D322C703D3124426D4F637165757646794E44754132726B566A6A3177246A7568362F6E6C2F49437A4B475570446E6B674171754A304F7A486A62694F587442542F2B62584D49476300000000000000000000000000000000000000000000000000000000000000" - val user = UserTestFixtures.createUserNickname().copy( + val user = UserTestFixtures.userNickname.copy( id = EXISTING_USER_ID, email = USER_EMAIL, key = key ) - every { userDao.findByEmail(user.email!!) } returns user - every { userDao.findByName(user.name) } returns user + every { userAdapter.findByEmail(user.email!!) } returns user + every { userAdapter.findByName(user.name) } returns user return user } @@ -224,7 +226,7 @@ class ProfileControllerTest { @Test fun verifyEmailSuccess() { val token = "verification" - val user = UserTestFixtures.createUserNickname().copy( + val user = UserTestFixtures.userNickname.copy( id = EXISTING_USER_ID, emailVerification = token, ) diff --git a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/StatisticControllerTest.kt b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/StatisticControllerTest.kt index 1abaca1d..441578db 100644 --- a/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/StatisticControllerTest.kt +++ b/adapter/src/test/kotlin/org/railwaystations/rsapi/adapter/web/controller/StatisticControllerTest.kt @@ -4,8 +4,8 @@ import com.ninjasquad.springmockk.MockkBean import io.mockk.every import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import org.railwaystations.rsapi.adapter.db.CountryDao -import org.railwaystations.rsapi.adapter.db.StationDao +import org.railwaystations.rsapi.adapter.db.CountryAdapter +import org.railwaystations.rsapi.adapter.db.StationAdapter import org.railwaystations.rsapi.adapter.web.ErrorHandlingControllerAdvice import org.railwaystations.rsapi.adapter.web.OpenApiValidatorUtil.validOpenApiResponse import org.railwaystations.rsapi.core.model.CountryTestFixtures @@ -28,14 +28,14 @@ internal class StatisticControllerTest { private lateinit var mvc: MockMvc @MockkBean - private lateinit var stationDao: StationDao + private lateinit var stationAdapter: StationAdapter @MockkBean - private lateinit var countryDao: CountryDao + private lateinit var countryAdapter: CountryAdapter @BeforeEach fun setup() { - every { countryDao.findById("de") } returns CountryTestFixtures.createCountry("de") + every { countryAdapter.findById("de") } returns CountryTestFixtures.createCountry("de") } @Test @@ -47,7 +47,7 @@ internal class StatisticControllerTest { @Test fun getOverallStatistic() { - every { stationDao.getStatistic(null) } returns Statistic(null, 954, 91, 6) + every { stationAdapter.getStatistic(null) } returns Statistic(null, 954, 91, 6) mvc.perform(get("/stats")) .andExpect(status().isOk()) @@ -61,7 +61,7 @@ internal class StatisticControllerTest { @Test fun getStatisticForCountryDe() { - every { stationDao.getStatistic("de") } returns Statistic("de", 729, 84, 4) + every { stationAdapter.getStatistic("de") } returns Statistic("de", 729, 84, 4) mvc.perform(get("/stats?country=de")) .andExpect(status().isOk()) diff --git a/adapter/src/test/resources/application.yaml b/adapter/src/test/resources/application.yaml index 3cb77aa3..05472be2 100644 --- a/adapter/src/test/resources/application.yaml +++ b/adapter/src/test/resources/application.yaml @@ -10,6 +10,8 @@ spring: max-request-size: 20MB jackson: default-property-inclusion: non_null + flyway: + schemas: rsapi # application workDir: /var/rsapi diff --git a/adapter/src/testFixtures/kotlin/org/railwaystations/rsapi/adapter/db/AbstractPostgreSqlTest.kt b/adapter/src/testFixtures/kotlin/org/railwaystations/rsapi/adapter/db/AbstractPostgreSqlTest.kt index cfaccc7e..82f5038a 100644 --- a/adapter/src/testFixtures/kotlin/org/railwaystations/rsapi/adapter/db/AbstractPostgreSqlTest.kt +++ b/adapter/src/testFixtures/kotlin/org/railwaystations/rsapi/adapter/db/AbstractPostgreSqlTest.kt @@ -11,6 +11,7 @@ abstract class AbstractPostgreSqlTest { PostgreSQLContainer(DockerImageName.parse("postgres:17")) init { + postgresql.withUrlParam("currentSchema", "rsapi") postgresql.start() } diff --git a/service/src/test/resources/db/migration/V9999_01__countries.sql b/adapter/src/testFixtures/resources/db/migration/V9999_01__countries.sql similarity index 86% rename from service/src/test/resources/db/migration/V9999_01__countries.sql rename to adapter/src/testFixtures/resources/db/migration/V9999_01__countries.sql index 6e00e34a..dfea9b43 100644 --- a/service/src/test/resources/db/migration/V9999_01__countries.sql +++ b/adapter/src/testFixtures/resources/db/migration/V9999_01__countries.sql @@ -1,4 +1,4 @@ -INSERT INTO countries (id,name,timetableUrlTemplate,email,overrideLicense,active) VALUES +INSERT INTO country (id,name,timetableUrlTemplate,email,overrideLicense,active) VALUES ('de','Deutschland','https://mobile.bahn.de/bin/mobil/bhftafel.exe/dox?bt=dep&max=10&rt=1&use_realtime_filter=1&start=yes&input={title}','info@railway-stations.org',NULL,true), ('ch','Schweiz','http://fahrplan.sbb.ch/bin/stboard.exe/dn?input={title}&REQTrain_name=&boardType=dep&time=now&maxJourneys=20&selectDate=today&productsFilter=1111111111&start=yes','fotos@schweizer-bahnhoefe.ch',NULL,true), ('it','Repubblica Italiana',NULL,'info@railway-stations.org',NULL,false), diff --git a/service/src/test/resources/db/migration/V9999_02__providerApps.sql b/adapter/src/testFixtures/resources/db/migration/V9999_02__providerApps.sql similarity index 93% rename from service/src/test/resources/db/migration/V9999_02__providerApps.sql rename to adapter/src/testFixtures/resources/db/migration/V9999_02__providerApps.sql index 9a1c2d2c..466d284d 100644 --- a/service/src/test/resources/db/migration/V9999_02__providerApps.sql +++ b/adapter/src/testFixtures/resources/db/migration/V9999_02__providerApps.sql @@ -1,4 +1,4 @@ -INSERT INTO providerApps (countryCode,type,name,url) VALUES +INSERT INTO providerApp (countryCode,type,name,url) VALUES ('ch','android','SBB Mobile','https://play.google.com/store/apps/details?id=ch.sbb.mobile.android.b2c'), ('ch','ios','SBB Mobile','https://apps.apple.com/app/sbb-mobile/id294855237'), ('de','android','DB Navigator','https://play.google.com/store/apps/details?id=de.hafas.android.db'), diff --git a/service/src/test/resources/db/migration/V9999_03__users.sql b/adapter/src/testFixtures/resources/db/migration/V9999_03__users.sql similarity index 64% rename from service/src/test/resources/db/migration/V9999_03__users.sql rename to adapter/src/testFixtures/resources/db/migration/V9999_03__users.sql index 6948ba2a..13299377 100644 --- a/service/src/test/resources/db/migration/V9999_03__users.sql +++ b/adapter/src/testFixtures/resources/db/migration/V9999_03__users.sql @@ -1,33 +1,33 @@ -INSERT INTO users (id,name,email,url,ownPhotos,anonymous,license,"key",admin,emailVerification,sendNotifications,locale) VALUES - (1,'@user0',NULL,'',true,true,'CC0_10',NULL,false,NULL,true,NULL), - (2,'@user1',NULL,'',true,false,'CC0_10',NULL,false,NULL,true,NULL), - (3,'@user2',NULL,'http://www.example.com/user2',true,false,'CC0_10',NULL,false,NULL,true,NULL), - (4,'@user3',NULL,'https://www.example.com/user3',true,false,'CC0_10',NULL,false,NULL,true,NULL), - (5,'@user4',NULL,'http://www.example.com/user4',true,false,'CC0_10',NULL,false,NULL,true,NULL), - (6,'@user5',NULL,'',true,false,'CC0_10',NULL,false,NULL,true,NULL), - (7,'@user6',NULL,'https://www.example.com/user6',true,false,'CC0_10',NULL,false,NULL,true,NULL), - (8,'@user7',NULL,'https://www.example.com/use7',true,false,'CC0_10',NULL,false,NULL,true,NULL), - (9,'@user8',NULL,'https://www.example.com/user8',true,false,'CC0_10',NULL,true,NULL,true,NULL), - (10,'@user9',NULL,'https://www.example.com/user9',true,false,'CC0_10',NULL,false,NULL,true,NULL); -INSERT INTO users (id,name,email,url,ownPhotos,anonymous,license,"key",admin,emailVerification,sendNotifications,locale) VALUES - (11,'@user10','user10@example.com','https://www.example.com/user10',true,false,'CC0_10','246172676F6E32696424763D3139246D3D36353533362C743D322C703D312432634C6B4C6949415958584E52742B6C7A3062614541242B706C7463365A4371386D534551772B4139374B304E37544B386A7072582F774141614B525933456D783400000000000000000000000000000000000000000000000000000000000000',true,'VERIFIED',true,NULL), - (12,'@user11',NULL,'https://www.example.com/user11',true,false,'CC0_10',NULL,false,NULL,true,NULL), - (13,'@user12',NULL,'https://www.example.com/user12',true,false,'CC0_10',NULL,false,NULL,true,NULL), - (14,'@user13',NULL,'',true,false,'CC0_10',NULL,false,NULL,true,NULL), - (15,'@user14','user14@example.com','https://www.example.com/user14',true,false,'CC0_10','246172676F6E32696424763D3139246D3D36353533362C743D322C703D3124674B6B4C4644564D566E75436B4A64526F547838437724396E4E5A763970415750532F41456549456F59702F7A377635796B3270787357395057705053444633723400000000000000000000000000000000000000000000000000000000000000',true,'VERIFIED',true,NULL), - (16,'@user15',NULL,'',true,false,'CC0_10',NULL,false,NULL,true,NULL), - (17,'@user16',NULL,'https://www.example.com/user16',true,false,'CC0_10',NULL,false,NULL,true,NULL), - (18,'@user17',NULL,'https://www.example.com/user17',true,false,'CC0_10',NULL,false,NULL,true,NULL), - (19,'@user18',NULL,'https://www.example.com/user18',true,false,'CC0_10',NULL,false,NULL,true,NULL), - (20,'@user19',NULL,'https://www.example.com/user19',true,false,'CC0_10',NULL,false,NULL,true,NULL); -INSERT INTO users (id,name,email,url,ownPhotos,anonymous,license,"key",admin,emailVerification,sendNotifications,locale) VALUES - (21,'@user20',NULL,'https://www.example.com/user20',true,false,'CC0_10',NULL,false,NULL,true,NULL), - (22,'@user21','user21@example.com','https://www.example.com/user21',true,false,'CC0_10','246172676F6E32696424763D3139246D3D36353533362C743D322C703D312432634C6B4C6949415958584E52742B6C7A3062614541242B706C7463365A4371386D534551772B4139374B304E37544B386A7072582F774141614B525933456D783400000000000000000000000000000000000000000000000000000000000000',false,NULL,true,NULL), - (23,'@user22',NULL,'https://www.example.com/user22',true,false,'CC BY-NC-SA 3.0 DE',NULL,false,NULL,true,NULL), - (24,'@user23',NULL,'https://www.example.com/user23',true,false,'CC0_10',NULL,false,NULL,true,NULL), - (25,'@user24',NULL,'https://www.example.com/user24',true,false,'CC0_10',NULL,false,NULL,true,NULL), - (26,'@user25',NULL,'',true,false,'CC0_10',NULL,false,NULL,true,NULL), - (27,'@user26',NULL,'https://www.example.com/user26',true,false,'CC0_10',NULL,false,NULL,true,NULL), - (28,'@user27',NULL,'https://www.example.com/user27',true,false,'CC0_10','246172676F6E32696424763D3139246D3D36353533362C743D322C703D3124426D4F637165757646794E44754132726B566A6A3177246A7568362F6E6C2F49437A4B475570446E6B674171754A304F7A486A62694F587442542F2B62584D49476300000000000000000000000000000000000000000000000000000000000000',false,NULL,true,NULL); +INSERT INTO "user" (id,name,email,url,ownPhotos,anonymous,license,"key",admin,emailVerification,sendNotifications,locale) VALUES + (1,'@user0',NULL,'',true,true,'CC0_10',NULL,false,NULL,true,'en'), + (2,'@user1',NULL,'',true,false,'CC0_10',NULL,false,NULL,true,'en'), + (3,'@user2',NULL,'http://www.example.com/user2',true,false,'CC0_10',NULL,false,NULL,true,'en'), + (4,'@user3',NULL,'https://www.example.com/user3',true,false,'CC0_10',NULL,false,NULL,true,'en'), + (5,'@user4',NULL,'http://www.example.com/user4',true,false,'CC0_10',NULL,false,NULL,true,'en'), + (6,'@user5',NULL,'',true,false,'CC0_10',NULL,false,NULL,true,'en'), + (7,'@user6',NULL,'https://www.example.com/user6',true,false,'CC0_10',NULL,false,NULL,true,'en'), + (8,'@user7',NULL,'https://www.example.com/use7',true,false,'CC0_10',NULL,false,NULL,true,'en'), + (9,'@user8',NULL,'https://www.example.com/user8',true,false,'CC0_10',NULL,true,NULL,true,'en'), + (10,'@user9',NULL,'https://www.example.com/user9',true,false,'CC0_10',NULL,false,NULL,true,'en'); +INSERT INTO "user" (id,name,email,url,ownPhotos,anonymous,license,"key",admin,emailVerification,sendNotifications,locale) VALUES + (11,'@user10','user10@example.com','https://www.example.com/user10',true,false,'CC0_10','246172676F6E32696424763D3139246D3D36353533362C743D322C703D312432634C6B4C6949415958584E52742B6C7A3062614541242B706C7463365A4371386D534551772B4139374B304E37544B386A7072582F774141614B525933456D783400000000000000000000000000000000000000000000000000000000000000',true,'VERIFIED',true,'en'), + (12,'@user11',NULL,'https://www.example.com/user11',true,false,'CC0_10',NULL,false,NULL,true,'en'), + (13,'@user12',NULL,'https://www.example.com/user12',true,false,'CC0_10',NULL,false,NULL,true,'en'), + (14,'@user13',NULL,'',true,false,'CC0_10',NULL,false,NULL,true,'en'), + (15,'@user14','user14@example.com','https://www.example.com/user14',true,false,'CC0_10','246172676F6E32696424763D3139246D3D36353533362C743D322C703D3124674B6B4C4644564D566E75436B4A64526F547838437724396E4E5A763970415750532F41456549456F59702F7A377635796B3270787357395057705053444633723400000000000000000000000000000000000000000000000000000000000000',true,'VERIFIED',true,'en'), + (16,'@user15',NULL,'',true,false,'CC0_10',NULL,false,NULL,true,'en'), + (17,'@user16',NULL,'https://www.example.com/user16',true,false,'CC0_10',NULL,false,NULL,true,'en'), + (18,'@user17',NULL,'https://www.example.com/user17',true,false,'CC0_10',NULL,false,NULL,true,'en'), + (19,'@user18',NULL,'https://www.example.com/user18',true,false,'CC0_10',NULL,false,NULL,true,'en'), + (20,'@user19',NULL,'https://www.example.com/user19',true,false,'CC0_10',NULL,false,NULL,true,'en'); +INSERT INTO "user" (id,name,email,url,ownPhotos,anonymous,license,"key",admin,emailVerification,sendNotifications,locale) VALUES + (21,'@user20',NULL,'https://www.example.com/user20',true,false,'CC0_10',NULL,false,NULL,true,'en'), + (22,'@user21','user21@example.com','https://www.example.com/user21',true,false,'CC0_10','246172676F6E32696424763D3139246D3D36353533362C743D322C703D312432634C6B4C6949415958584E52742B6C7A3062614541242B706C7463365A4371386D534551772B4139374B304E37544B386A7072582F774141614B525933456D783400000000000000000000000000000000000000000000000000000000000000',false,NULL,true,'en'), + (23,'@user22',NULL,'https://www.example.com/user22',true,false,'CC BY-NC-SA 3.0 DE',NULL,false,NULL,true,'en'), + (24,'@user23',NULL,'https://www.example.com/user23',true,false,'CC0_10',NULL,false,NULL,true,'en'), + (25,'@user24',NULL,'https://www.example.com/user24',true,false,'CC0_10',NULL,false,NULL,true,'en'), + (26,'@user25',NULL,'',true,false,'CC0_10',NULL,false,NULL,true,'en'), + (27,'@user26',NULL,'https://www.example.com/user26',true,false,'CC0_10',NULL,false,NULL,true,'en'), + (28,'@user27',NULL,'https://www.example.com/user27',true,false,'CC0_10','246172676F6E32696424763D3139246D3D36353533362C743D322C703D3124426D4F637165757646794E44754132726B566A6A3177246A7568362F6E6C2F49437A4B475570446E6B674171754A304F7A486A62694F587442542F2B62584D49476300000000000000000000000000000000000000000000000000000000000000',false,NULL,true,'en'); SELECT setval('users_seq', 28); diff --git a/service/src/test/resources/db/migration/V9999_04__oauth2_registered_client.sql b/adapter/src/testFixtures/resources/db/migration/V9999_04__oauth2_registered_client.sql similarity index 100% rename from service/src/test/resources/db/migration/V9999_04__oauth2_registered_client.sql rename to adapter/src/testFixtures/resources/db/migration/V9999_04__oauth2_registered_client.sql diff --git a/service/src/test/resources/db/migration/V9999_05__stations.sql b/adapter/src/testFixtures/resources/db/migration/V9999_05__stations.sql similarity index 91% rename from service/src/test/resources/db/migration/V9999_05__stations.sql rename to adapter/src/testFixtures/resources/db/migration/V9999_05__stations.sql index 47b8917e..8b3f19f3 100644 --- a/service/src/test/resources/db/migration/V9999_05__stations.sql +++ b/adapter/src/testFixtures/resources/db/migration/V9999_05__stations.sql @@ -1,4 +1,4 @@ -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('ch','8500013',8500013,NULL,'BSRB','Basel SBB RB',47.5363531811,7.64637599135,true), ('ch','8500018',8500018,NULL,'BSBH','Birsfelden Hafen',47.5436592309,7.65456008393,true), ('ch','8500020',8500020,NULL,'MU','Muttenz',47.5335803903,7.64787914262,true), @@ -9,7 +9,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('ch','8500103',8500103,NULL,'SOR','Sorvilier',47.2392372997,7.30576524542,true), ('ch','8500126',8500126,NULL,'POR','Porrentruy',47.420909698,7.08007451034,true), ('ch','8500159',8500159,NULL,'GN','Grenchen Nord',47.1918033634,7.38946302189,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('ch','8500164',8500164,NULL,'TRCH','Tramelan-Chalet',47.2234556821,7.09244154101,true), ('ch','8500200',8500200,NULL,'PIE','Pieterlen',47.1714342219,7.34019009864,true), ('ch','8500210',8500210,NULL,'WAA','Wangen an der Aare',47.2319515056,7.6562236042,true), @@ -20,7 +20,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('ch','8500307',8500307,NULL,'EFG','Effingen',47.476556836,8.10066965497,true), ('ch','8500313',8500313,NULL,'RFAU','Rheinfelden Augarten',47.5458662882,7.76802232018,true), ('ch','8501014',8501014,NULL,'MIES','Mies',46.2998757136,6.1695702204,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('ch','8501015',8501015,NULL,'TAN','Tannay',46.3076341414,6.18112481111,true), ('ch','8501031',8501031,NULL,'GLA','Gland',46.419975355,6.26913801937,true), ('ch','8501051',8501051,NULL,'GECT','Genève CT',46.1830988518,6.1256902838,true), @@ -31,7 +31,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('ch','8501105',8501105,NULL,'BRT','Bretonnières',46.7150454119,6.47473141459,true), ('ch','8501107',8501107,NULL,'AX','Arnex',46.6980662731,6.51890326658,true), ('ch','8501108',8501108,NULL,'LSA','La Sarraz',46.6587767613,6.51736524915,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('ch','8501124',8501124,NULL,'CU','Cully',46.488550014,6.72714757327,true), ('ch','8501127',8501127,NULL,'STSA','St-Saphorin',46.4725855624,6.7970306112,true), ('ch','8501146',8501146,NULL,'DAIL','Daillens',46.6322769799,6.5424870787,true), @@ -42,7 +42,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('ch','8501198',8501198,NULL,'PALZ','Château-d''Oex La Palaz',46.4762626898,7.14169746113,true), ('ch','8501200',8501200,NULL,'VV','Vevey',46.4629974303,6.84345000982,true), ('ch','8501374',8501374,NULL,'FON','Fontanivent',46.4527214847,6.90234172937,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('ch','8501388',8501388,NULL,'COMS','Les Combes',46.4774712498,7.17183521402,true), ('ch','8501397',8501397,NULL,'ROU','Rougemont',46.4886174199,7.20896362395,true), ('ch','8501400',8501400,NULL,'AIG','Aigle',46.3168441199,6.9636800936,true), @@ -53,7 +53,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('ch','8501498',8501498,NULL,'BOUQ','Bouquetins',46.3163944087,7.07235034848,true), ('ch','8501502',8501502,NULL,'SAX','Saxon',46.149376272,7.17343442478,true), ('ch','8501507',8501507,NULL,'SL','St-Léonard',46.2514824986,7.41957042997,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('ch','8501561',8501561,NULL,'VEMC','Vernayaz MC',46.1301269722,7.04493371044,true), ('ch','8501576',8501576,NULL,'SEMB','Sembrancher',46.0771013189,7.14629384096,true), ('ch','8501604',8501604,NULL,'RAR','Raron',46.3062732767,7.80150766492,true), @@ -64,7 +64,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('ch','8502113',8502113,NULL,'AA','Aarau',47.3913553369,8.05125354274,true), ('ch','8502161',8502161,NULL,'TEU','Teufenthal AG',47.3294078788,8.11446902971,true), ('ch','8502166',8502166,NULL,'LEIM','Leimbach AG',47.2727123979,8.16785753252,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('ch','8502167',8502167,NULL,'REIN','Reinach AG Nord',47.2572622736,8.1814533182,true), ('ch','8502168',8502168,NULL,'REI','Reinach AG',47.2500828777,8.1844579633,true), ('ch','8502180',8502180,NULL,'MUHN','Muhen Nord',47.3435959852,8.05262416228,true), @@ -75,7 +75,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('ch','8502239',8502239,NULL,'DOTU','Dottikon Umspannanlage',47.3629190442,8.24916930628,true), ('ch','8502277',8502277,NULL,'RUD','Rudolfstetten',47.3695000211,8.38118075797,true), ('ch','8503000',8503000,NULL,'ZUE','Zürich HB',47.3781765756,8.54019221036,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('ch','8503001',8503001,NULL,'ZAS','Zürich Altstetten',47.3914808361,8.4889402654,true), ('ch','8503006',8503006,NULL,'ZOER','Zürich Oerlikon',47.4115288802,8.54411523121,true), ('ch','8503007',8503007,NULL,'ZSEB','Zürich Seebach',47.4187469105,8.54463614173,true), @@ -86,7 +86,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('ch','8503087',8503087,NULL,'ZSAA','Zürich Saalsporthalle',47.3574066029,8.52214389055,true), ('ch','8503094',8503094,NULL,'ZLE','Zürich Leimbach',47.3346006314,8.51960324631,true), ('ch','8503098',8503098,NULL,'SIW','Sihlwald',47.2682765216,8.55749769881,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('ch','8503100',8503100,NULL,'ZK','Zollikon',47.3373317337,8.569717612,true), ('ch','8503109',8503109,NULL,'FB','Feldbach',47.2391975436,8.78348281363,true), ('ch','8503110',8503110,NULL,'RW','Rapperswil',47.2248877672,8.81673057998,true), @@ -97,7 +97,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('ch','8503205',8503205,NULL,'AU','Au ZH',47.2468165268,8.6437080619,true), ('ch','8503230',8503230,NULL,'GL','Glarus',47.0399200317,9.0715516467,true), ('ch','8503233',8503233,NULL,'SCHD','Schwanden',46.9968482066,9.07744743355,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('ch','8503235',8503235,NULL,'LGB','Leuggelbach',46.9757415966,9.04692465402,true), ('ch','8503281',8503281,NULL,'SAT','Sattel-Aegeri',47.0798546452,8.62723255539,true), ('ch','8503295',8503295,NULL,'WILN','Wilen bei Wollerau',47.1975794035,8.73431410161,true), @@ -108,7 +108,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('ch','8503409',8503409,NULL,'ZZ','Bad Zurzach',47.5883101087,8.29556465398,true), ('ch','8503426',8503426,NULL,'LAN','Langwiesen',47.6834480371,8.66229616245,true), ('ch','8503461',8503461,NULL,'THA','Thayngen',47.7455202803,8.70323014905,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('ch','8503462',8503462,NULL,'HERB','Herblingen',47.7172096623,8.66428930544,true), ('ch','8503464',8503464,NULL,'BERB','Beringen Bad Bf',47.6948880279,8.5746740103,true), ('ch','8503466',8503466,NULL,'WIHA','Wilchingen-Hallau',47.6794939072,8.4640170693,true), @@ -119,7 +119,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('ch','8504028',8504028,NULL,'ROS','Rosé',46.7826234886,7.06196897192,true), ('ch','8504038',8504038,NULL,'BOMO','Bossy Moulins (embr)',46.823297431,6.97112719515,true), ('ch','8504125',8504125,NULL,'DOM','Domdidier',46.8685046705,7.01138032832,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('ch','8504134',8504134,NULL,'PAY','Payerne',46.8196387484,6.93987910663,true), ('ch','8504139',8504139,NULL,'BF','Belfaux CFF',46.8169170976,7.10622789234,true), ('ch','8504181',8504181,NULL,'GIV','Givisiez',46.8167103996,7.12533398287,true), @@ -130,7 +130,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('ch','8504296',8504296,NULL,'SIXF','Six-Fontaines',46.7740634606,6.49885385763,true), ('ch','8504298',8504298,NULL,'STCR','Ste-Croix',46.819667331,6.50031077908,true), ('ch','8504310',8504310,NULL,'STI','St-Imier',47.151698154,7.00121493749,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('ch','8504319',8504319,NULL,'LOCF','Le Locle-Frontière',47.0502257746,6.71695602606,true), ('ch','8504382',8504382,NULL,'REYM','Le Reymond',47.0870449944,6.84407585566,true), ('ch','8504386',8504386,NULL,'COEU','Les Coeudres',47.0228289999,6.78017609399,true), @@ -141,7 +141,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('ch','8504472',8504472,NULL,'SUTZ','Sutz',47.101870975,7.21941610659,true), ('ch','8504486',8504486,NULL,'GMM','Gümmenen',46.9421730828,7.23440009719,true), ('ch','8504487',8504487,NULL,'ROSS','Rosshäusern',46.9343342218,7.29685984085,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('ch','8504489',8504489,NULL,'BNBZ','Bern Bümpliz Nord',46.9458272511,7.38974518517,true), ('ch','8504490',8504490,NULL,'ZBR','Zihlbrücke',47.0092706005,7.03057399743,true), ('ch','8505009',8505009,NULL,'LZGB','Luzern Cargo',47.0476016286,8.31311665368,true), @@ -152,7 +152,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('ch','8505206',8505206,NULL,'GIO','Giornico',46.4026513909,8.87212788076,true), ('ch','8505213',8505213,NULL,'BEL','Bellinzona',46.195436816,9.02950898687,true), ('ch','8505301',8505301,NULL,'LGP','Lugano-Paradiso',45.9889500461,8.94627572854,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('ch','8505392',8505392,NULL,'MAGP','Magliaso Paese',45.9834116357,8.89057611812,true), ('ch','8505400',8505400,NULL,'LO','Locarno',46.1724273965,8.80135894865,true), ('ch','8505405',8505405,NULL,'QUA','Quartino',46.1513026375,8.88731923749,true), @@ -163,7 +163,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('ch','8506016',8506016,NULL,'OWT','Oberwinterthur',47.5079184546,8.76037093131,true), ('ch','8506121',8506121,NULL,'RH','Romanshorn',47.5655269844,9.37936259732,true), ('ch','8506125',8506125,NULL,'ALN','Altnau',47.6213657537,9.26589677408,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('ch','8506127',8506127,NULL,'BOT','Bottighofen',47.6407078714,9.21308178053,true), ('ch','8506168',8506168,NULL,'TAEN','Tägerschen',47.5093655501,9.02478054512,true), ('ch','8506173',8506173,NULL,'WEBM','Weberei Matzingen',47.5272231149,8.92219880826,true), @@ -174,7 +174,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('ch','8506203',8506203,NULL,'BUET','Bütschwil',47.3595914091,9.07513534617,true), ('ch','8506204',8506204,NULL,'LUET','Lütisburg',47.3838273619,9.07067425759,true), ('ch','8506210',8506210,NULL,'GSS','Gossau SG',47.4118296551,9.25303263655,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('ch','8506217',8506217,NULL,'SLG','Sulgen',47.5387993681,9.18367857671,true), ('ch','8506223',8506223,NULL,'BAZZ','Bazenheid ZAB (Agl)',47.4187928897,9.07275345589,true), ('ch','8506282',8506282,NULL,'ZUEM','Zürchersmühle',47.3303299425,9.29257922862,true), @@ -185,7 +185,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('ch','8506322',8506322,NULL,'RSST','Rorschach Stadt',47.4754476463,9.48971870021,true), ('ch','8506336',8506336,NULL,'SGCT','St. Gallen CT',47.4183047522,9.36382812989,true), ('ch','8506350',8506350,NULL,'WALZ','Walzenhausen',47.45174123,9.60126132858,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('ch','8506353',8506353,NULL,'WITO','Wienacht-Tobel',47.4662112913,9.54063003948,true), ('ch','8506365',8506365,NULL,'SGAR','Schützengarten',47.4128999701,9.43733940966,true), ('ch','8506368',8506368,NULL,'GFEL','Gfeld',47.404993276,9.45587433744,true), @@ -196,7 +196,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('ch','8507068',8507068,NULL,'BOLI','Bolligen',46.9695437407,7.49741070619,true), ('ch','8507271',8507271,NULL,'SCR','Schönried',46.5045418942,7.29110122969,true), ('ch','8507274',8507274,NULL,'BLB','Blankenburg',46.5411031578,7.38532055303,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('ch','8507288',8507288,NULL,'EIF','Eifeld',46.6839200004,7.64227217644,true), ('ch','8507299',8507299,NULL,'LAT','Lattigen bei Spiez',46.6911822815,7.65058766913,true), ('ch','8507471',8507471,NULL,'EB','Eggerberg',46.3067196577,7.88100325138,true), @@ -207,7 +207,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('ch','8508060',8508060,NULL,'GRRI','Grafenried',47.0801208449,7.51160644661,true), ('ch','8508063',8508063,NULL,'SCN','Schalunen',47.1101643118,7.524163279,true), ('ch','8508064',8508064,NULL,'BKI','Bätterkinden',47.1300169994,7.53439602439,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('ch','8508066',8508066,NULL,'LOLU','Lohn-Lüterkofen',47.1629889082,7.52695040748,true), ('ch','8508088',8508088,NULL,'GLF','Gerlafingen',47.1697445507,7.56518564422,true), ('ch','8508155',8508155,NULL,'BAN','Bannwil',47.2443819331,7.74035841864,true), @@ -218,7 +218,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('ch','8508395',8508395,NULL,'INNP','Innertkirchen Grimseltor',46.7055781543,8.22958535272,true), ('ch','8509054',8509054,NULL,'ZIZ','Zizers',46.9348427166,9.5595893726,true), ('ch','8509063',8509063,NULL,'FID','Fideris',46.922367674,9.73375699397,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('ch','8509156',8509156,NULL,'LITZ','Litzirüti',46.797951013,9.70326831526,true), ('ch','8509173',8509173,NULL,'RUEU','Rueun',46.7746975042,9.14561460428,true), ('ch','8509181',8509181,NULL,'DE','Domat/Ems',46.8331408741,9.45316475001,true), @@ -229,7 +229,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('ch','8515444',8515444,NULL,'ALRH','Altenrhein',47.4827491242,9.54749533526,true), ('ch','8516270',8516270,NULL,'ARBS','Arbon Seemoosriet',47.5215190263,9.42429272006,true), ('ch','8516740',8516740,NULL,'SUHM','Suhr Migros',47.3798308077,8.0895768139,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('ch','8517102',8517102,NULL,'BUZE','Buchs SG Zement',47.1684499467,9.47987147734,true), ('ch','8517280',8517280,NULL,'SHNO','Schaffhausen Nord',47.7153565256,8.65747606498,true), ('ch','8517419',8517419,NULL,'GETG','Gettnau Güteranlage',47.1403681479,7.98434083792,true), @@ -240,7 +240,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('ch','8530066',8530066,NULL,'GMOU','Grand-Moulin',46.252615433,7.02323187517,true), ('ch','8530225',8530225,NULL,'STD','Le Stand',47.0051287464,6.74558607693,true), ('ch','8530261',8530261,NULL,'BCHX','Bois-de-Chexbres',46.4732731469,6.90153894513,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('ch','8530303',8530303,NULL,'FUM','Les Fumeaux',46.1204927795,7.05276514532,true), ('ch','8530813',8530813,NULL,'ZUEK','Zürich Kreuzplatz',47.3648743419,8.55408358421,true), ('ch','8578241',8578241,NULL,'','Oberdorf BL, Winkelweg',47.3965629232,7.74920561685,true), @@ -251,7 +251,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','5005',NULL,5005,'NPOE','Pösing',49.2291838706692,12.5426085607721,true), ('de','5006',NULL,5006,'MPH','Possenhofen',47.9618491160948,11.3063493057301,true), ('de','5007',NULL,5007,'UPS','Pößneck ob Bf',50.6903914886634,11.5879400522495,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','5008',NULL,5008,'UPSU','Pößneck unt Bf',50.7004761356672,11.606850028038,true), ('de','5009',NULL,5009,'NPOH','Postbauer-Heng',49.308423292103,11.3588598387155,true), ('de','5010',NULL,5010,'BPDC','Potsdam Charlottenhof',52.3928053362819,13.0366194248199,true), @@ -262,7 +262,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','5015',NULL,5015,'BWP','Potsdam Park Sanssouci',52.3945673444395,13.0136484596376,true), ('de','5016',NULL,5016,'BPOF','Berlin Potsdamer Platz',52.5095071060745,13.3760999864148,true), ('de','5017',NULL,5017,'EPRA','Praest',51.8218302895556,6.34564518928528,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','5018',NULL,5018,'LPT','Pratau',51.8427759058211,12.6349636128074,true), ('de','5020',NULL,5020,'APZ','Preetz',54.2337692725559,10.2759397029877,true), ('de','5021',NULL,5021,'LPZH','Premnitz Nord',52.5359707914784,12.3335149727385,true), @@ -273,7 +273,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','5027',NULL,5027,'NPS','Pressath',49.7640087316021,11.9391006231308,true), ('de','5028',NULL,5028,'NPR','Pressig-Rothenkirchen',50.3488562421698,11.3108110427856,true), ('de','5029',NULL,5029,'NPRE','Pretzfeld',49.7571941768927,11.1699137091637,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','503',NULL,503,'BBFE','Bergfelde (b Berlin)',52.6701486971792,13.3204239606857,true), ('de','5030',NULL,5030,'LPRZ','Pretzier (Altm)',52.836364406677,11.2647414207459,true), ('de','5032',NULL,5032,'EPRN','Preußen',51.5881547964165,7.53963888179527,true), @@ -284,7 +284,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','5038',NULL,5038,'BPOT','Priort',52.5164493745414,12.9714417457581,true), ('de','5039',NULL,5039,'APD','Prisdorf',53.6752711201783,9.76017773151398,true), ('de','504',NULL,504,'EBGA','Berghausen',51.0330166352177,8.35004112937233,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','5041',NULL,5041,'UPZ','Prittitz',51.1583751907754,11.9373321533203,true), ('de','5042',NULL,5042,'LPI','Pritzerbe',52.499891544188,12.4585268558082,true), ('de','5043',NULL,5043,'WPRZ','Pritzier',53.3649663026512,11.0833214898163,true), @@ -295,7 +295,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','5048',NULL,5048,'WPRA','Prora',54.4442255768543,13.5642957687378,true), ('de','5049',NULL,5049,'WPRO','Prora Ost',54.4290916931849,13.5752824544907,true), ('de','5050',NULL,5050,'DPRO','Prösen',51.4348856226978,13.4881905819241,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','5051',NULL,5051,'DPRW','Prösen West',51.4247413689035,13.4699249267578,true), ('de','5052',NULL,5052,'BPW','Prösen Ost',51.4258394143447,13.5008546885322,true), ('de','5054',NULL,5054,'MPM','Puchheim',48.1720496112781,11.3530662885079,true), @@ -306,7 +306,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','506',NULL,506,'RBGP','Berghausen (Pfalz)',49.2952971525565,8.40612433695276,true), ('de','5060',NULL,5060,'NPU','Puschendorf',49.5207649603291,10.8282029628754,true), ('de','5063',NULL,5063,'APU','Puttgarden',54.4997217960304,11.2259479330367,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','5064',NULL,5064,'DPT','Putzkau',51.1029378459713,14.2355673164962,true), ('de','5065',NULL,5065,'KQ','Quadrath-Ichendorf',50.9377052909814,6.68670600460419,true), ('de','5066',NULL,5066,'HQ','Quakenbrück',52.6739460819204,7.94778227806091,true), @@ -317,7 +317,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','5073',NULL,5073,'SQS','Quierschied',49.3246045292761,7.05925226211549,true), ('de','5074',NULL,5074,'SQI','Quint',49.825887828829,6.70456767082214,true), ('de','5080',NULL,5080,'LRK','Rackwitz (B Leipzig)',51.4406359006132,12.3749213084512,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','5081',NULL,5081,'ARH','Radbruch',53.3179968675149,10.2891737222671,true), ('de','5082',NULL,5082,'BRAD','Raddusch',51.8159720865066,14.0325299722958,true), ('de','5083',NULL,5083,'DRAG','Radeberg',51.1116300921215,13.9132332801819,true), @@ -328,7 +328,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','5088',NULL,5088,'DRBZ','Radebeul-Zitzschewig',51.1145198189389,13.6067430737992,true), ('de','5090',NULL,5090,'WRDN','Wustrau-Radensleben',52.8573379147889,12.8970626864895,true), ('de','5091',NULL,5091,'MRDD','Radersdorf',48.5144953582438,11.1622059345245,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','5093',NULL,5093,'LRA','Radis',51.7546939099066,12.5041803717613,true), ('de','5094',NULL,5094,'NRA','Radldorf (Niederbay)',48.8746571068069,12.4442794919014,true), ('de','5095',NULL,5095,'RRZ','Radolfzell',47.7357599191892,8.96934247077305,true), @@ -339,7 +339,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','6716',NULL,6716,'TWS','Westerstetten',48.5198477154827,9.9494041999181,true), ('de','6717',NULL,6717,'DWES','Westewitz-Hochweitzschen',51.1375598068236,13.0279734730721,true), ('de','6718',NULL,6718,'TWX','Westhausen',48.881509090406,10.1834100340573,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','6719',NULL,6719,'MWHS','Westheim (Schwab)',48.3860809462235,10.8185503021934,true), ('de','672',NULL,672,'MBI','Bischofswiesen',47.6517723877162,12.9602623383683,true), ('de','6720',NULL,6720,'EWSM','Westheim (Westf)',51.4943099312753,8.9094078540802,true), @@ -350,7 +350,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','6728',NULL,6728,'EWET','Wetter (Ruhr)',51.3865048782342,7.38576435668716,true), ('de','6729',NULL,6729,'LWZE','Wetterzeube',51.0084411468424,12.0128566026687,true), ('de','6730',NULL,6730,'FWR','Wetzlar',50.5656217066498,8.50371390581131,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','6731',NULL,6731,'EWIC','Wickede (Ruhr)',51.4932545278315,7.86997675895691,true), ('de','6732',NULL,6732,'NWK','Wicklesgreuth',49.3106519902296,10.7112944126129,true), ('de','6733',NULL,6733,'KWIC','Wickrath',51.1327800600184,6.41156095170545,true), @@ -361,7 +361,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','6741',NULL,6741,'HWRN','Wieren',52.8925859123245,10.6557161224132,true), ('de','6743',NULL,6743,'NWU','Wiesau (Oberpf)',49.910735423068,12.1912431716919,true), ('de','6744',NULL,6744,'FW','Wiesbaden Hbf',50.0699318332051,8.24410915374756,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','6745',NULL,6745,'FWO','Wiesbaden Ost',50.0412773071902,8.25656175613405,true), ('de','6746',NULL,6746,'FWB','Wiesbaden-Biebrich',50.0487432307069,8.23656284356419,true), ('de','6747',NULL,6747,'FWE','Wiesbaden-Erbenheim',50.0547092050314,8.29561219044732,true), @@ -372,7 +372,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','6753',NULL,6753,'DWSB','Wiesenburg (Sachs)',50.6525579045044,12.5668179988861,true), ('de','6754',NULL,6754,'FWIF','Wiesenfeld',51.007575080185,8.74935647998038,true), ('de','6755',NULL,6755,'NWDC','Wiesenfeld (b Coburg)',50.3015878738585,10.9117458368603,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','6756',NULL,6756,'RWI','Wiesental',49.2256754407423,8.50241482257843,true), ('de','6757',NULL,6757,'NWIE','Wiesenthau',49.7134874351084,11.1269391775131,true), ('de','6759',NULL,6759,'RWS','Wiesloch-Walldorf',49.2911328258961,8.66423961441606,true), @@ -383,7 +383,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','6765',NULL,6765,'TWIB','Wildberg (Württ)',48.6252621023387,8.74296426773072,true), ('de','6766',NULL,6766,'HWIH','Wildeshausen',52.8979084919646,8.43149900436402,true), ('de','6768',NULL,6768,'KMAH','Marienheide',51.0811962370328,7.53213467439676,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','6769',NULL,6769,'TWL','Wilferdingen-Singen',48.9537302545833,8.57280437148478,true), ('de','6770',NULL,6770,'RWSW','Wilgartswiesen',49.2094368443987,7.88383483886719,true), ('de','6771',NULL,6771,'BWIG','Wilhelmshagen',52.4384574054115,13.7223192172892,true), @@ -394,7 +394,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','6776',NULL,6776,'DWL','Wilischthal',50.7247131575353,13.056596150991,true), ('de','6777',NULL,6777,'DWI','Wilkau-Haßlau',50.6753763834715,12.5136440992355,true), ('de','6778',NULL,6778,'FWLN','Willingen',51.2933286579108,8.60366463661194,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','6779',NULL,6779,'FWLM','Willmenrod',50.5394465601194,7.98551645460009,true), ('de','678',NULL,678,'SED','Bitburg-Erdorf',49.9987245211634,6.57111148576478,true), ('de','6780',NULL,6780,'NWIG','Willmering',49.2466108327209,12.6701688766479,true), @@ -405,7 +405,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','6786',NULL,6786,'AWIR','Wilster',53.9262495549818,9.3836921453476,true), ('de','6787',NULL,6787,'DWT','Wilthen',51.0929165800785,14.3919411920151,true), ('de','6788',NULL,6788,'SWIG','Wiltingen (Saar)',49.6625913111747,6.59045308828355,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','679',NULL,679,'TBIT','Bittelbronn',48.4443445061693,8.58904312054316,true), ('de','6790',NULL,6790,'SWIN','Wincheringen',49.6084493124215,6.40649020671844,true), ('de','6791',NULL,6791,'FWIN','Nidderau-Windecken',50.2249700277551,8.87596696615219,true), @@ -416,7 +416,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','6797',NULL,6797,'UWIR','Wingerode',51.3901125851962,10.2394384145736,true), ('de','6798',NULL,6798,'AWG','Wingst',53.7457711874136,9.09780979156493,true), ('de','6799',NULL,6799,'NWKH','Winkelhaid',49.3928938936631,11.3010066747666,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','680',NULL,680,'LBT','Bitterfeld',51.6225406382936,12.3167542089899,true), ('de','6801',NULL,6801,'TWI','Winnenden',48.8795605406284,9.39219484458099,true), ('de','6802',NULL,6802,'SWI','Winningen (Mosel)',50.3157265466022,7.52330710490547,true), @@ -427,7 +427,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','6807',NULL,6807,'EWTB','Winterberg (Westf)',51.1990818095532,8.53240307776891,true), ('de','6808',NULL,6808,'NWN','Winterhausen',49.7080588548588,10.0121944706614,true), ('de','6809',NULL,6809,'AWIM','Wintermoor',53.1860582765147,9.82858300209045,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','681',NULL,681,'NBLA','Blaibach (Oberpf)',49.1640554441482,12.8078126907348,true), ('de','6812',NULL,6812,'UWIP','Wipperdorf',51.4530957498171,10.660468850817,true), ('de','6815',NULL,6815,'LWIP','Wippra',51.5764217543438,11.281417285,true), @@ -438,7 +438,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','682',NULL,682,'MBLH','Blaichach (Allgäu)',47.5436380251428,10.2588549256325,true), ('de','6820',NULL,6820,'HWIS','Wissingen',52.2583756051301,8.20486128330231,true), ('de','6821',NULL,6821,'EWIK','Wittbräucke',51.4201669341258,7.44590789079665,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','6822',NULL,6822,'EWIT','Witten Hbf',51.4355582943146,7.32932817935943,true), ('de','6823',NULL,6823,'EWIA','Witten-Annen Nord',51.4478824938629,7.37562417984009,true), ('de','6824',NULL,6824,'BWIN','Wittenau (Wilhelmsruher Damm)',52.5966027530015,13.3350741863251,true), @@ -449,7 +449,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','683',NULL,683,'WBL','Blankenberg (Meckl)',53.7721776478778,11.7156637281699,true), ('de','6830',NULL,6830,'DWIO','Wittgensdorf ob Bf',50.8829728818259,12.8413305075272,true), ('de','6832',NULL,6832,'TWIT','Wittighausen',49.6183357346227,9.84622478485107,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','6833',NULL,6833,'HWTT','Wittingen',52.7274300161698,10.7232570648193,true), ('de','6834',NULL,6834,'SWIH','Wittlich Hbf',49.9729835768243,6.94311261177063,true), ('de','6836',NULL,6836,'HWTM','Wittmund',53.5797693657504,7.78902411460876,true), @@ -460,7 +460,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','6840',NULL,6840,'BMN','Messe Nord / ICC (Witzleben)',52.5076877898203,13.2835257053375,true), ('de','6841',NULL,6841,'DWIT','Witzschdorf',50.7751939176774,13.0884611606598,true), ('de','6842',NULL,6842,'AWRT','Witzwort',54.3858012040046,8.99831082139696,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','6843',NULL,6843,'FWX','Darmstadt-Wixhausen',49.9309105521228,8.64799976348876,true), ('de','6844',NULL,6844,'UWOF','Woffleben',51.5558991150512,10.7213830947876,true), ('de','6845',NULL,6845,'AWLF','Wohltorf',53.5206625173867,10.2780176975109,true), @@ -471,7 +471,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','6850',NULL,6850,'HWOH','Wolfenbüttel',52.1592701111772,10.5322046717208,true), ('de','6851',NULL,6851,'LWOR','Wolferode',51.51223270545,11.5086767695686,true), ('de','6853',NULL,6853,'FWON','Felsberg-Wolfershausen',51.1866152565289,9.44953858852387,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','6854',NULL,6854,'UWLH','Wölfershausen',50.491615309112,10.4366217314571,true), ('de','6855',NULL,6855,'FWOE','Wölfersheim-Södel',50.3972391195768,8.81631658962477,true), ('de','6856',NULL,6856,'FWFG','Wolfgang (Kr Hanau)',50.1235544820385,8.95834470281795,true), @@ -482,7 +482,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','6862',NULL,6862,'NWM','Wolfsmünster',50.0944311645525,9.73513472611736,true), ('de','6863',NULL,6863,'RKLN','Brigachtal Klengen',48.0074240975216,8.46577547549199,true), ('de','6864',NULL,6864,'SWFS','Wolfstein',49.5841055915374,7.61130684715967,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','6867',NULL,6867,'USTA','Steinach Süd',50.4230759383273,11.1664947867393,true), ('de','6869',NULL,6869,'DWO','Wolkenstein',50.6527447636373,13.0650425434112,true), ('de','6870',NULL,6870,'UWK','Wolkramshausen',51.4398568834312,10.7455426454544,true), @@ -493,7 +493,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','6877',NULL,6877,'BWOL','Woltersdorf (b Luckenwalde)',52.1172043255482,13.1951612234116,true), ('de','6879',NULL,6879,'HWWI','Woltwiesche',52.2006870721189,10.2784844613094,true), ('de','688',NULL,688,'BBF','Blankenfelde (Kr Teltow-Fläming)',52.3372991936045,13.4160125255585,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','6887',NULL,6887,'FWOR','Worms Hbf',49.6348928161858,8.35645053100887,true), ('de','6888',NULL,6888,'MWNS','Wörnitzstein',48.7320602773959,10.7143705699086,true), ('de','6889',NULL,6889,'FWRR','Wörrstadt',49.8460546176444,8.11394691467285,true), @@ -504,7 +504,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','6893',NULL,6893,'RWRT','Wörth (Rhein)',49.0457072202884,8.27310562133789,true), ('de','6895',NULL,6895,'AWRE','Wremen',53.646979535667,8.52136019492307,true), ('de','6896',NULL,6896,'BWRZ','Wriezen',52.7157003659627,14.1403461888779,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','6897',NULL,6897,'AWST','Wrist',53.9320941564461,9.74784206040957,true), ('de','6899',NULL,6899,'BWHH','Wuhlheide',52.4685471744522,13.5543394088745,true), ('de','690',NULL,690,'KBLH','Blankenheim (Wald)',50.4411762669806,6.59239053726197,true), @@ -515,7 +515,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','6908',NULL,6908,'UWF','Wünschendorf (Elster)',50.7974543402449,12.0976209640503,true), ('de','6909',NULL,6909,'UWFN','Wünschendorf (Elster) Nord',50.8173747066877,12.0779523253441,true), ('de','6910',NULL,6910,'BWUE','Wünsdorf Waldstadt',52.1655265106846,13.4680938720703,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','6912',NULL,6912,'NHOB','Wunsiedel-Holenbrunn',50.0511594777841,12.0337843894958,true), ('de','6913',NULL,6913,'HWUN','Wunstorf',52.4222888625512,9.45113217945455,true), ('de','6914',NULL,6914,'KW','Wuppertal Hbf',51.2543117840239,7.14955216104334,true), @@ -526,7 +526,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','692',NULL,692,'RBAN','Blankenloch',49.0669167643834,8.46479773521423,true), ('de','6923',NULL,6923,'KWLP','Wuppertal-Langerfeld',51.2777804803123,7.24212795495987,true), ('de','6927',NULL,6927,'RDOG','Donaueschingen Grüningen',47.9832177543091,8.47162635095658,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','6928',NULL,6928,'KWO','Wuppertal-Oberbarmen',51.2739009665495,7.22138059766669,true), ('de','693',NULL,693,'WBLS','Blankensee (Meckl)',53.4082229147222,13.2630801200867,true), ('de','6932',NULL,6932,'KWRO','Wuppertal-Ronsdorf',51.2263901893817,7.21557766199112,true), @@ -537,7 +537,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','6939',NULL,6939,'EWGD','Würgendorf',50.760083442592,8.14176589250564,true), ('de','694',NULL,694,'UBN','Blankenstein (Saale)',50.4012133874771,11.6980774879275,true), ('de','6940',NULL,6940,'EWGT','Würgendorf Ort',50.7569523933259,8.12694668769837,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','6941',NULL,6941,'NWLZ','Wurlitz',50.2531271523174,11.9894126057625,true), ('de','6943',NULL,6943,'SWB','Würzbach (Saar)',49.2444498855526,7.19252831771457,true), ('de','6944',NULL,6944,'UWCH','Wurzbach (Thür)',50.4664957550057,11.5433894390085,true), @@ -548,7 +548,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','6950',NULL,6950,'LWZ','Wurzen',51.3645311785261,12.7387132331333,true), ('de','6951',NULL,6951,'LWZW','Bennewitz',51.3559288056075,12.7085816860199,true), ('de','6952',NULL,6952,'DWD','Wüstenbrand',50.8055009556565,12.7565009224004,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','6954',NULL,6954,'WWSF','Wüstenfelde',54.2302032745035,13.1394425918611,true), ('de','6955',NULL,6955,'NWBZ','Wüstenselbitz',50.2184972802642,11.7095296382468,true), ('de','6957',NULL,6957,'BWUS','Wustermark',52.5519522171824,12.9385086028807,true), @@ -559,7 +559,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','6963',NULL,6963,'UWU','Wutha',50.9570275285132,10.3967871913945,true), ('de','6965',NULL,6965,'RWY','Wyhlen',47.5463117157444,7.69122111046383,true), ('de','6966',NULL,6966,'KX','Xanten',51.6596778645775,6.44533294041279,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','6967',NULL,6967,'BYOR','Yorckstraße',52.4908879943577,13.3721423149109,true), ('de','6968',NULL,6968,'DZT','Zabeltitz',51.3486129934275,13.515704870224,true), ('de','697',NULL,697,'TBLF','Blaufelden',49.2968680840031,9.9672567844391,true), @@ -570,7 +570,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','6979',NULL,6979,'BZS','Zeesen',52.2649006334566,13.6350997655016,true), ('de','698',NULL,698,'TBLS','Blaustein',48.4146696520766,9.9192504449324,true), ('de','6980',NULL,6980,'WZE','Zehdenick (Mark)',52.9791614827097,13.3174089022782,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','6981',NULL,6981,'WZEN','Zehdenick-Neuhof',53.0057036581011,13.3457672777254,true), ('de','6983',NULL,6983,'NZ','Zeil',50.0051056643502,10.5986489054899,true), ('de','6985',NULL,6985,'DZNH','Zeithain',51.3301155648065,13.3445379137993,true), @@ -581,7 +581,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','6990',NULL,6990,'FZR','Zell-Romrod',50.7274692016412,9.20002788305283,true), ('de','6992',NULL,6992,'UZL','Zella-Mehlis',50.6466773950674,10.6740946726583,true), ('de','6993',NULL,6993,'UZLW','Zella-Mehlis West',50.6585422776352,10.6495925015019,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','6994',NULL,6994,'LZD','Zellendorf',51.8941848027717,13.0668745814824,true), ('de','6995',NULL,6995,'FZH','Mainhausen Zellhausen',50.0161510141997,8.96998303835509,true), ('de','6997',NULL,6997,'FZE','Zennern',51.1121599576777,9.31724309921265,true), @@ -592,7 +592,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','7002',NULL,7002,'LZE','Zerbst',51.9539542303416,12.0889715540227,true), ('de','7005',NULL,7005,'BZF','Zernsdorf',52.2994209144664,13.6933565139771,true), ('de','7006',NULL,7006,'WZIN','Zerrenthin',53.4994960885989,14.0976476669312,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','7009',NULL,7009,'UZU','Zeulenroda unt Bf',50.6594447402911,12.0113414525986,true), ('de','701',NULL,701,'FBLH','Bleichenbach (Oberhess)',50.320361825909,9.05150055885315,true), ('de','7010',NULL,7010,'BZN','Zeuthen',52.3484283935155,13.6276164650917,true), @@ -603,7 +603,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','7021',NULL,7021,'KZI','Zieverich',50.9494363986625,6.62653898039175,true), ('de','7022',NULL,7022,'NZF','Zillendorf',49.3454387694782,12.6732505461587,true), ('de','7023',NULL,7023,'BZIL','Ziltendorf',52.1993248756856,14.6162622482324,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','7024',NULL,7024,'TZI','Zimmern',49.595305244734,9.7906592344907,true), ('de','7025',NULL,7025,'RZIM','Zimmern bei Seckach',49.430656612978,9.37126903085534,true), ('de','7026',NULL,7026,'FZM','Zimmersrode',51.0080000330331,9.22581732273102,true), @@ -614,7 +614,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','7037',NULL,7037,'DZL','Zoblitz',51.1236501259237,14.7509684130154,true), ('de','7038',NULL,7038,'DZB','Zöblitz-Pobershau',50.6600375844609,13.2137775421143,true), ('de','7040',NULL,7040,'MZHP','Zollhaus-Petersthal',47.665648274103,10.4184126853943,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','7042',NULL,7042,'LZO','Zoo',51.5036842320307,11.9702842405865,true), ('de','7045',NULL,7045,'MZO','Zorneding',48.0895163074821,11.8325525111166,true), ('de','7046',NULL,7046,'LZGL','Zörnigall',51.8829271927615,12.7314208601048,true), @@ -625,7 +625,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','7054',NULL,7054,'LHNZ','Zscherbener Straße',51.4731056963049,11.9210882449443,true), ('de','7055',NULL,7055,'DZP','Zschopau',50.7432334851788,13.0659171938896,true), ('de','7056',NULL,7056,'LZS','Zschortau',51.4833891127463,12.3614226361267,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','7058',NULL,7058,'KZU','Euskirchen Zuckerfabrik',50.6571367708711,6.81324005126953,true), ('de','7062',NULL,7062,'RZHN','Zusenhofen',48.5448904470563,8.01786368340799,true), ('de','7063',NULL,7063,'WZS','Züssow',53.972840685084,13.5493698881027,true), @@ -636,7 +636,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','707',NULL,707,'SBKL','Blieskastel-Lautzkirchen',49.2491863980313,7.25801151852276,true), ('de','7070',NULL,7070,'DZWP','Zwickau-Pölbitz',50.7371277853822,12.479683606389,true), ('de','7071',NULL,7071,'DZWS','Zwickau-Schedewitz',50.7019080388157,12.4949154281876,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','7072',NULL,7072,'NZWL','Zwiesel (Bay)',49.0203716696716,13.2260785834094,true), ('de','7073',NULL,7073,'NZWU','Zwieselau',49.0065325041363,13.2770955562592,true), ('de','7074',NULL,7074,'RZW','Zwingenberg (Baden)',49.4157831030959,9.04302633122394,true), @@ -647,7 +647,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','7079',NULL,7079,'DZA','Zwotental',50.362193034516,12.3639141461429,true), ('de','708',NULL,708,'MBLM','Blindheim',48.6437867350354,10.6153959035873,true), ('de','7081',NULL,7081,'RDOA','Donaueschingen Allmendshofen',47.940554402497,8.50174427032471,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','7083',NULL,7083,'RWD','Weidenthal',49.4175655335154,7.99447209007223,true), ('de','709',NULL,709,'LBD','Blönsdorf',51.9554536133164,12.8958034515381,true), ('de','71',NULL,71,'BARO','Alt Rosenthal',52.5502971604976,14.2782211303711,true), @@ -658,7 +658,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','712',NULL,712,'LBLG','Blumenberg',52.0334589952065,11.4558577537537,true), ('de','7121',NULL,7121,'DWIS','Wiesa (Erzgeb)',50.6104996710811,13.0234909057617,true), ('de','713',NULL,713,'WBLK','Blumenhagen',53.5283310944805,13.8717412948608,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','714',NULL,714,'EBKP','Blumenkamp',51.6952444005791,6.61307282746706,true), ('de','7144',NULL,7144,'BLAK','Lankwitz',52.4387336799763,13.3419929941495,true), ('de','7146',NULL,7146,'BSDE','Südende',52.4482548378637,13.3536887168884,true), @@ -669,7 +669,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','7152',NULL,7152,'TRNF','Rottweil Neufra',48.1267990755887,8.67034792900083,true), ('de','7153',NULL,7153,'RVLE','Villingen-Schwenningen Eisstadion',48.0527477397657,8.52858290076255,true), ('de','7154',NULL,7154,'RVLH','Villingen-Schwenningen Hammerstatt',48.0601436757818,8.54693586731014,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','7155',NULL,7155,'RVLZ','Zollhaus Villingen-Schwenningen',48.0409267082552,8.50594580173492,true), ('de','7156',NULL,7156,'RNBG','Neuenburg (Baden)',47.8114100771463,7.56292339582205,true), ('de','7157',NULL,7157,'SFIM','Friedrichsthal Mitte',49.3269348124969,7.0940226316452,true), @@ -680,7 +680,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','7161',NULL,7161,'FDAL','Darmstadt-Lichtwiese',49.8608716030797,8.68758916854858,true), ('de','7162',NULL,7162,'RSZM','Sinzheim',48.7597617512172,8.15836429595947,true), ('de','7166',NULL,7166,'FDZ','Dietzenbach Bahnhof',50.0080125581858,8.78558959023267,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','7167',NULL,7167,'FHEU','Heusenstamm',50.0600463798685,8.80215484987605,true), ('de','7168',NULL,7168,'FMHD','Mühlheim-Dietesheim',50.1204645934781,8.85854959487915,true), ('de','7169',NULL,7169,'FOKL','Offenbach-Kaiserlei',50.1051734241434,8.73825073242187,true), @@ -691,7 +691,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','7178',NULL,7178,'RSMM','Sinsheim Museum/Arena',49.2416597175338,8.89927248160045,true), ('de','7179',NULL,7179,'HLGM','Langenhagen-Mitte',52.4401221667493,9.72523183581725,true), ('de','7180',NULL,7180,'HLGF','Hannover-Flughafen',52.4585547160933,9.69878911972046,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','7181',NULL,7181,'HKWA','Hannover Karl-Wiechert-Allee',52.3744122959614,9.80884203987674,true), ('de','7183',NULL,7183,'HLEG','Hannover - Ledeburg',52.4084559136188,9.6907344407792,true), ('de','7184',NULL,7184,'RBZB','Bad Bergzabern',49.1032074332002,8.00331115722656,true), @@ -702,7 +702,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','7200',NULL,7200,'RSAI','Schaidt (Pf)',49.0612487674533,8.07982627083273,true), ('de','7201',NULL,7201,'RAM','Albsheim (Eis)',49.580946104255,8.1790941953659,true), ('de','7202',NULL,7202,'RBOK','Bockenheim-Kindenheim',49.6068514005025,8.18796873092651,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','7203',NULL,7203,'RRN','Ramsen',49.5351552705906,8.02229869339499,true), ('de','7205',NULL,7205,'KASZ','Aachen Schanz',50.7699944112422,6.07366383075714,true), ('de','7213',NULL,7213,'FDZM','Dietzenbach-Mitte',50.0175514810848,8.78916334570125,true), @@ -713,7 +713,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','7250',NULL,7250,'AY','Husby',54.7641902350377,9.57629139008728,true), ('de','7252',NULL,7252,'AKE','Kiel-Elmschenhagen',54.2870803737293,10.1804548501969,true), ('de','7255',NULL,7255,'AKHD','Kremperheide',53.8812062932582,9.48022842407227,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','7257',NULL,7257,'AKRH','Kronshagen',54.3310620233927,10.0832926979188,true), ('de','7267',NULL,7267,'AME','Melsdorf',54.3127955415507,10.0307048651484,true), ('de','7277',NULL,7277,'AKRU','Kiel-Russee',54.3100176694926,10.0702548027039,true), @@ -724,7 +724,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','7434',NULL,7434,'TBIF','Bitzfeld',49.1872188329265,9.45327937602998,true), ('de','7435',NULL,7435,'TFF','Friedrichshafen-Fischbach',47.6725836916973,9.41716411574347,true), ('de','7436',NULL,7436,'TSEP','Scheppach',49.1616747816076,9.43425536155701,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','7459',NULL,7459,'TSUS','Sülzbach Schule',49.144888798788,9.35138187199755,true), ('de','7461',NULL,7461,'RSFL','Steinfeld (Pfalz)',49.0447218053078,8.03944398880048,true), ('de','7463',NULL,7463,'FAR','Bad Arolsen',51.381716907211,8.99789750576019,true), @@ -735,7 +735,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','7494',NULL,7494,'FMGH','Mengeringhausen',51.3605774837256,8.98986233349206,true), ('de','7508',NULL,7508,'FSLM','Vöhl Schmittlotheim',51.156722739148,8.89996636421123,true), ('de','7511',NULL,7511,'FTW','Twiste',51.3362907418293,8.96385512977304,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','7512',NULL,7512,'FVI','Frankenberg (Eder) Viermünden',51.1008710717107,8.83308313405811,true), ('de','7527',NULL,7527,'MKAI','Kainzenbad',47.4830232419896,11.1163052916527,true), ('de','7531',NULL,7531,'FAZS','Alzey Süd',49.7380397499913,8.11876371502876,true), @@ -746,7 +746,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','7562',NULL,7562,'TMAG','Magstadt',48.7362420808569,8.96536231040955,true), ('de','7590',NULL,7590,'HWBD','Willebadessen',51.6215964347361,9.01196122169494,true), ('de','7623',NULL,7623,'TTPS','Heilbronn Trappensee',49.1403044989393,9.25268411636353,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','7626',NULL,7626,'HDVD','Dörverden',52.8469426290407,9.2464542388916,true), ('de','7638',NULL,7638,'TWRW','Weinsberg West',49.1512223657819,9.27389323711395,true), ('de','7651',NULL,7651,'KKBL','Köln-Blumenberg',51.0374456882592,6.88181750914631,true), @@ -757,7 +757,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','7712',NULL,7712,'THBH','Horb-Heiligenfeld',48.4677238190907,8.660169839859,true), ('de','7713',NULL,7713,'TELI','Weinsberg/Ellhofen Gewerbegebiet',49.1487394076972,9.30544859170913,true), ('de','7715',NULL,7715,'FHSU','Hohensülzen',49.6213398897155,8.2140456604975,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','7719',NULL,7719,'BARF','Alt-Reinickendorf',52.5777360152643,13.3512822819632,true), ('de','7720',NULL,7720,'BLIH','Berlin-Lichterfelde Ost',52.4297709311585,13.3286495475162,true), ('de','7721',NULL,7721,'BTG','Berlin-Tegel',52.5881668467864,13.2897055149078,true), @@ -768,7 +768,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','7728',NULL,7728,'BHST','Heerstraße',52.5082122466258,13.2590619411349,true), ('de','7729',NULL,7729,'BHLS','Heiligensee',52.6247338684935,13.2289731502533,true), ('de','7730',NULL,7730,'BLIS','Lichterfelde Süd',52.4096790880467,13.3083421941151,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','7731',NULL,7731,'BOLS','Olympiastadion',52.511383594499,13.2427172581876,true), ('de','7732',NULL,7732,'BPIC','Pichelsberg',52.5102039315973,13.2275805993058,true), ('de','7734',NULL,7734,'BSZF','Schulzendorf (b Tegel)',52.6132831284011,13.2455434584517,true), @@ -779,7 +779,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','7748',NULL,7748,'BFPN','Frankfurt (Oder)-Neuberesinchen',52.3224621352589,14.5463342594461,true), ('de','7751',NULL,7751,'RBRP','Bruchsal Sportzentrum',49.1355454558817,8.58961701393127,true), ('de','7755',NULL,7755,'BGGS','Yorckstraße (Großgörschenstraße)',52.4923390161326,13.3679881602971,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','7756',NULL,7756,'BWED','Wedding',52.5427810682522,13.3670210838318,true), ('de','7759',NULL,7759,'BJLB','Julius-Leber-Brücke',52.4863967192342,13.3610395038159,true), ('de','7760',NULL,7760,'BWH','Westhafen',52.5362502007629,13.3441695570946,true), @@ -790,7 +790,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','7766',NULL,7766,'TRBG','Rosenberg (Baden)',49.45744774745,9.47121233587532,true), ('de','7769',NULL,7769,'KMCP','Meckenheim Industriepark',50.6437838036061,7.02719181776046,true), ('de','7772',NULL,7772,'EHWS','Halle (Westf)-Gerry-Weber-Stadion',52.0616021798886,8.34377533922634,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','7774',NULL,7774,'EOLT','Oldentrup',52.010340213258,8.59070985444956,true), ('de','7848',NULL,7848,'LNW','Leipzig Messe',51.3963139191042,12.3890590667725,true), ('de','7851',NULL,7851,'NZIK','Zirndorf Kneippallee',49.4502865985787,10.9448476938101,true), @@ -801,7 +801,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','7872',NULL,7872,'SNO','Nohfelden',49.5859288714237,7.13955044746398,true), ('de','7875',NULL,7875,'RNFL','Neidenfels',49.3864239342015,8.04525837892654,true), ('de','7884',NULL,7884,'HWUE','Wüsting',53.1181323427885,8.33724975585938,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','7889',NULL,7889,'TPW','Pforzheim-Weißenstein',48.8652238489259,8.67632320403572,true), ('de','7890',NULL,7890,'RRZB','Radolfzell Haselbrunn',47.7446717091979,8.97523409128188,true), ('de','7891',NULL,7891,'TAEW','Albstadt Ebingen West',48.21136195245,8.99706602096557,true), @@ -812,7 +812,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','7910',NULL,7910,'BBRF','Bernau-Friedenstal',52.6683627648285,13.5645961761474,true), ('de','7947',NULL,7947,'TNAI','Nagold-Iselshausen',48.5315264577321,8.72593402862549,true), ('de','7956',NULL,7956,'RHGB','Hagenbach',49.0163786228012,8.25323402881623,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','7957',NULL,7957,'FWED','Dreieich-Weibelfeld',50.007254802887,8.70073840385532,true), ('de','7958',NULL,7958,'HWEH','Wehrden (Weser)',51.7100389780955,9.3815803527832,true), ('de','7960',NULL,7960,'TALD','Aldingen',48.0957883208843,8.71304154396057,true), @@ -823,7 +823,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','7965',NULL,7965,'LVTT','Vatteröder Teich',51.5981452419974,11.4121136069298,true), ('de','7966',NULL,7966,'EHOH','Attendorn Hohen-Hagen',51.0901525152748,7.85530507564545,true), ('de','7968',NULL,7968,'FGOG','Gießen Oswaldsgarten',50.5880642383409,8.6689281463623,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','7969',NULL,7969,'FOUS','Oberursel-Stierstadt',50.1851558705383,8.58476484044989,true), ('de','7971',NULL,7971,'LMHR','Magdeburg-Herrenkrug',52.1452170453073,11.6770210415238,true), ('de','7973',NULL,7973,'LTMW','Tangermünde West',52.5477136484075,11.9604328340665,true), @@ -834,7 +834,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','7980',NULL,7980,'FFME','Frankfurt am Main Messe',50.1119110244626,8.64344750271469,true), ('de','7981',NULL,7981,'KBPS','Boppard Süd',50.2301067649472,7.59577954552837,true), ('de','7982',NULL,7982,'FFLF','Frankfurt am Main Flughafen Fernbahnhof',50.0529456722016,8.56986387454731,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','7983',NULL,7983,'WTIW','Tessin West',54.0343613709732,12.4422866106033,true), ('de','7984',NULL,7984,'WRTF','Rostock Thierfelder Straße',54.0779764903219,12.1002833250502,true), ('de','7985',NULL,7985,'WKAR','Kalsow',53.925776197122,11.5787294932774,true), @@ -845,7 +845,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','7992',NULL,7992,'RFMU','Freiburg Messe/Universität',48.0128560880564,7.83278986574714,true), ('de','7993',NULL,7993,'TTUN','Tuttlingen Nord',47.9925391652491,8.82372756799062,true), ('de','7994',NULL,7994,'TBOX','Boxberg-Wölchingen',49.4854286111985,9.6395813480137,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','7995',NULL,7995,'NHEN','Heroldsberg Nord',49.5449549136114,11.1467456817627,true), ('de','7996',NULL,7996,'MAGM','Augsburg Messe',48.3386990679735,10.8861011266708,true), ('de','7997',NULL,7997,'UWOL','Wolfsgefärth',50.8166750471278,12.0736208328834,true), @@ -856,7 +856,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','8002',NULL,8002,'RUEM','Überlingen',47.7690751219607,9.16285783052445,true), ('de','8003',NULL,8003,'TWHF','Wackershofen-Freilandmuseum',49.1372558626989,9.69714760780335,true), ('de','8004',NULL,8004,'RDUN','Durmersheim Nord',48.937754110544,8.28053018557891,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','8005',NULL,8005,'RSIS','Singen-Landesgartenschau',47.760917561559,8.82707762541941,true), ('de','8006',NULL,8006,'TWVH','Warthausen',48.1298389702487,9.80161249637604,true), ('de','8007',NULL,8007,'TSIH','Sigmaringendorf',48.0689101002142,9.26074147224425,true), @@ -867,7 +867,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','8012',NULL,8012,'FZBR','Zierenberg-Rosental',51.3675885834164,9.29152071475983,true), ('de','8013',NULL,8013,'WNRW','Neuruppin West',52.9263599545071,12.7940940856934,true), ('de','8014',NULL,8014,'WSRB','Stralsund-Grünhufe',54.3042414199415,13.0439937114716,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','8015',NULL,8015,'LFLU','Flughafen Leipzig/Halle',51.4232784916033,12.2236919403076,true), ('de','8016',NULL,8016,'DKTF','Dresden Flughafen',51.1245855890878,13.7659728527069,true), ('de','8017',NULL,8017,'DSCF','Schöneck (Vogtl) Ferienpark',50.3897244229187,12.3471693017266,true), @@ -878,7 +878,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','8023',NULL,8023,'FLGF','Langen-Flugsicherung',50.0053945715418,8.65849256515503,true), ('de','8024',NULL,8024,'HLTT','Lutten',52.7680356195963,8.34378608466067,true), ('de','8025',NULL,8025,'RMSU','Malsch Süd',48.8875567723244,8.3160831297145,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','8026',NULL,8026,'RMU B','Muggensturm-Badesee',48.8799961344862,8.28678826491035,true), ('de','8027',NULL,8027,'TNA M','Nagold Stadtmitte',48.5512760693146,8.72852723116011,true), ('de','8030',NULL,8030,'TGRW','Grüntal/Wittlensweiler',48.4753795717776,8.4602133384565,true), @@ -889,7 +889,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','8036',NULL,8036,'TMHR','Möhringen Rathaus',47.9571309582826,8.76284301280975,true), ('de','8037',NULL,8037,'TSPM','Spaichingen Mitte',48.0784438083992,8.7305247121387,true), ('de','8038',NULL,8038,'TTUS','Tuttlingen Schulen',47.9873873666813,8.80108204327132,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','8039',NULL,8039,'TTUG','Tuttlingen Gänsäcker',47.9646452770826,8.78119826316833,true), ('de','8040',NULL,8040,'TWLH','Weilheim',48.0247953892195,8.77594274357831,true), ('de','8041',NULL,8041,'TWUM','Wurmlingen Mitte',48.0014556827084,8.77803862094879,true), @@ -900,7 +900,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','8046',NULL,8046,'RSCW','Schopfheim West',47.6455551945598,7.801733314991,true), ('de','8047',NULL,8047,'RKOF','Konstanz Fürstenberg',47.6774896487691,9.16279532692649,true), ('de','8048',NULL,8048,'RLAC','Lauchringen West',47.6327464113642,8.30829313823156,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','8050',NULL,8050,'RHBF','HD-Weststadt/Südstadt',49.3983787811584,8.68898391723633,true), ('de','8051',NULL,8051,'RHBO','Heidelberg-Orthopädie',49.4100143723822,8.77491333858756,true), ('de','8052',NULL,8052,'RMOW','Mosbach West',49.3466477921908,9.13394488918949,true), @@ -911,7 +911,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','8057',NULL,8057,'NCN','Coburg Nord',50.2714671096946,10.9648861885071,true), ('de','8058',NULL,8058,'FFZH','Frankfurt-Zeilsheim',50.0901740477464,8.50640487566125,true), ('de','8059',NULL,8059,'RHFM','Hüfingen Mitte',47.9293495870561,8.4902587193563,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','8060',NULL,8060,'TDLM','Deißlingen Mitte',48.1083569905767,8.61155948218175,true), ('de','8063',NULL,8063,'ANSW','Neumünster Stadtwald',54.0834049542114,9.96285080909729,true), ('de','8064',NULL,8064,'HBSK','Bad Salzdetfurth-Solebad',52.0700352917617,10.0185499650187,true), @@ -922,7 +922,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','8086',NULL,8086,'HOTM','Osterode am Harz Mitte',51.7260455718577,10.245482902373,true), ('de','8087',NULL,8087,'HOTL','Osterode am Harz Leege',51.7169889776672,10.2730356760386,true), ('de','8091',NULL,8091,'DHFR','Dresden Freiberger Straße',51.048089232313,13.7198343873024,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','8094',NULL,8094,'LSEL','Leipzig-Olbrichtstraße',51.3698715113397,12.3535072803497,true), ('de','8095',NULL,8095,'LSDW','Schkeuditz West',51.4030030086157,12.1952310204506,true), ('de','8096',NULL,8096,'LSVS','Leipzig-Slevogtstraße',51.3729557950398,12.3444163799286,true), @@ -933,7 +933,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','8109',NULL,8109,'TMIN','Maichingen Nord',48.7274514656348,8.97942048679922,true), ('de','811',NULL,811,'EBWE','Brackwede',51.9972054795561,8.4983994279589,true), ('de','8110',NULL,8110,'TMAI','Maichingen',48.7216357886643,8.96961539983749,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','8111',NULL,8111,'TRXS','Renningen Süd',48.7626555558764,8.93197417259216,true), ('de','8112',NULL,8112,'AFIB','Fischbek',53.4747588082275,9.81944752805815,true), ('de','8113',NULL,8113,'DSRB','Plauen (Vogtl)-Straßberg',50.4801155948601,12.0921671390534,true), @@ -944,7 +944,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','813',NULL,813,'WBRH','Brahlstorf',53.3643522251632,10.9520179544532,true), ('de','8130',NULL,8130,'SRAM','Miesenbach',49.456309142121,7.54879653453827,true), ('de','8131',NULL,8131,'KKWW','Köln-Weiden West',50.9408544904191,6.81496471166611,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','8133',NULL,8133,'FMBB','Melsungen-Bartenwetzerbrücke',51.1322050674238,9.54872846603394,true), ('de','8134',NULL,8134,'FVOE','Vellmar-Osterberg/EKZ',51.3542760744887,9.46073643391645,true), ('de','8135',NULL,8135,'FKKD','Kassel-Kirchditmold',51.3245715539511,9.45210868849244,true), @@ -955,7 +955,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','8141',NULL,8141,'WGMW','Graal-Müritz-Koppelweg',54.2458450378084,12.2369205951691,true), ('de','8142',NULL,8142,'NAHF','Aschaffenburg Hochschule',49.9739373866439,9.1601912838867,true), ('de','8147',NULL,8147,'TRCB','Reichenberg',49.7310948064864,9.91288793196372,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','815',NULL,815,'HBKE','Brake (Unterweser)',53.3286111863404,8.48113059997559,true), ('de','8152',NULL,8152,'NALW','Altenstadt (Waldnaab)',49.7155031009043,12.1648582831934,true), ('de','8154',NULL,8154,'MMHG','München-Hirschgarten',48.1435657432128,11.5194522569245,true), @@ -966,7 +966,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','8162',NULL,8162,'WUMH','Ueckermünde Stadthafen',53.7362213753434,14.0522309374137,true), ('de','8163',NULL,8163,'RESN','Eschelbronn',49.3238726146127,8.86785507202148,true), ('de','8164',NULL,8164,'RNST','Neidenstein',49.3202222026194,8.89024257659912,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','8165',NULL,8165,'RWB','Waibstadt',49.3008793629413,8.91936779022216,true), ('de','8166',NULL,8166,'RNHF','Neckarbischofsheim Nord',49.3069936029972,8.94261360168457,true), ('de','8167',NULL,8167,'RHM','Helmstadt (Baden)',49.3199417129369,8.97483282259426,true), @@ -977,7 +977,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','8194',NULL,8194,'NHAP','Happurg',49.5000379543495,11.4683939860417,true), ('de','8195',NULL,8195,'NNAM','Neustadt (Aisch) Mitte',49.5742440504185,10.6094026816027,true), ('de','8197',NULL,8197,'FSLN','Schwalbach Nord',50.1597845007794,8.53458987593968,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','8198',NULL,8198,'SSTB','Stambach',49.2391553493315,7.45663676572882,true), ('de','8199',NULL,8199,'RND','Neustadt Süd',49.3411675989078,8.15694987773896,true), ('de','8200',NULL,8200,'SHAM','Hauenstein Mitte',49.199164571602,7.84956205974925,true), @@ -988,7 +988,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','8205',NULL,8205,'RWRG','Wörth Zügelstraße',49.0529733870338,8.26200306415558,true), ('de','8206',NULL,8206,'RGES','Germersheim Süd/Nolte',49.2083694864028,8.37379403114317,true), ('de','8209',NULL,8209,'MEFM','Eggenfelden Mitte',48.4061402313299,12.767624258995,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','8210',NULL,8210,'RFTS','Frankenthal-Süd',49.5226036499836,8.34952354431152,true), ('de','8212',NULL,8212,'NKGM','Kleingemünden',50.0629841743865,9.68775987625122,true), ('de','8216',NULL,8216,'DAUS','Aue (Sachs) Erzgebirgsstadion',50.5966272129463,12.7094542980194,true), @@ -999,7 +999,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','8221',NULL,8221,'DFLP','Flöha-Plaue',50.8443871864845,13.0849820762796,true), ('de','8222',NULL,8222,'MJUL','Julbach',48.250963969071,12.9583257436752,true), ('de','8223',NULL,8223,'ENL','Bad Laasphe-Niederlaasphe',50.9243148551329,8.44199895858765,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','8224',NULL,8224,'DNZW','Niederzwönitz',50.6462024148463,12.8218433260918,true), ('de','8225',NULL,8225,'DOLW','Olbernhau West',50.6602208772145,13.3211572766304,true), ('de','8226',NULL,8226,'DSTM','Pockau Strobelmühle',50.6785140203176,13.2150042057037,true), @@ -1010,7 +1010,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','8240',NULL,8240,'BSTH','Ludwigsfelde-Struveshof',52.3171366034566,13.2320022583008,true), ('de','8242',NULL,8242,'RANS','Annweiler-Sarnstall',49.2089311849686,7.93712956564766,true), ('de','8243',NULL,8243,'BAHH','Blumberg-Rehhahn',52.5967765377545,13.5920780897141,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','8244',NULL,8244,'RGEM','Germersheim Mitte/Rhein',49.2201093906166,8.37923347949982,true), ('de','8245',NULL,8245,'MFLH','Freilassing-Hofham',47.8318192865327,12.9678084509713,true), ('de','8246',NULL,8246,'FLSR','Lahntal-Sarnau',50.871143453964,8.75600874423981,true), @@ -1021,7 +1021,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','8252',NULL,8252,'FDHM','Dienheim',49.8413417297459,8.35704935224432,true), ('de','8253',NULL,8253,'NGFA','Grafling-Arzting',48.889852620279,12.9732611775398,true), ('de','8258',NULL,8258,'KKSM','Koblenz Stadtmitte',50.3582093168618,7.5901151158196,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','8259',NULL,8259,'MROS','Rosenheim Hochschule',47.8663720768848,12.1042249149196,true), ('de','8263',NULL,8263,'FATC','Ahnatal-Casselbreite',51.362856659307,9.43399644815002,true), ('de','8264',NULL,8264,'FPU','Pfungstadt',49.8062573292585,8.60725045204163,true), @@ -1032,7 +1032,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','8271',NULL,8271,'TN M','Neckarsulm Mitte',49.1932382145579,9.22285519178633,true), ('de','8273',NULL,8273,'TBWT','Bad Wimpfen im Tal',49.2298190657313,9.18709688292349,true), ('de','8274',NULL,8274,'RBPK','Bad Rappenau Kurpark',49.240349008878,9.11352038383484,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','8275',NULL,8275,'LLN','Leipzig Nord',51.3642828806862,12.4010360240936,true), ('de','8277',NULL,8277,'DMEA','Meißen Altstadt',51.1590604020501,13.4730684757233,true), ('de','8278',NULL,8278,'MGRL','Graben (Lechfeld) Gewerbepark',48.1991027220609,10.852295702154,true), @@ -1043,7 +1043,7 @@ INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) ('de','8285',NULL,8285,'URDN','Reinsdorf (b Nebra)',51.298060566517,11.6012835502625,true), ('de','8292',NULL,8292,'DPM','Plauen (Vogtl) Mitte',50.4928552358645,12.1476531028747,true), ('de','8295',NULL,8295,'ABRB','Bredenbek',54.3150597971543,9.86709904212218,true); -INSERT INTO stations (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES +INSERT INTO station (countryCode,id,uicibnr,dbibnr,DS100,title,lat,lon,active) VALUES ('de','8297',NULL,8297,'LMAN','Markkleeberg Nord',51.2878952112556,12.3720957363238,true), ('de','8300',NULL,8300,'ALD','Lübeck Dänischburg IKEA',53.9110165320888,10.7340721289316,true), ('de','8302',NULL,8302,'FFRG','Frankenberg (Eder) Goßberg',51.0650264497401,8.79926741123199,true), diff --git a/service/src/test/resources/db/migration/V9999_06__photos.sql b/adapter/src/testFixtures/resources/db/migration/V9999_06__photos.sql similarity index 87% rename from service/src/test/resources/db/migration/V9999_06__photos.sql rename to adapter/src/testFixtures/resources/db/migration/V9999_06__photos.sql index 853c607e..1ab17bfb 100644 --- a/service/src/test/resources/db/migration/V9999_06__photos.sql +++ b/adapter/src/testFixtures/resources/db/migration/V9999_06__photos.sql @@ -1,4 +1,4 @@ -INSERT INTO photos (id,countryCode,stationId,"primary",outdated,urlPath,license,photographerId,createdAt) VALUES +INSERT INTO photo (id,countryCode,stationId,"primary",outdated,urlPath,license,photographerId,createdAt) VALUES (1,'ch','8503001',true,false,'/ch/8503001%20-%20Z%C3%BCrich%20Altstetten.jpg','CC0_10',3,'2018-04-06 19:52:47'), (2,'ch','8503006',true,false,'/ch/8503006_0.jpg','CC0_10',5,'2018-04-06 19:52:47'), (3,'ch','8503007',true,false,'/ch/8503007.jpg','CC0_10',3,'2018-04-06 19:52:47'), @@ -9,7 +9,7 @@ INSERT INTO photos (id,countryCode,stationId,"primary",outdated,urlPath,license, (8,'de','5004',true,false,'/de/5004_1.jpg','CC0_10',11,'2018-04-06 19:52:47'), (9,'de','5090',true,false,'/de/5090_1.jpg','CC0_10',9,'2018-04-06 19:52:47'), (10,'de','6892',true,false,'/de/6892_1.jpg','CC0_10',28,'2018-04-06 19:52:47'); -INSERT INTO photos (id,countryCode,stationId,"primary",outdated,urlPath,license,photographerId,createdAt) VALUES +INSERT INTO photo (id,countryCode,stationId,"primary",outdated,urlPath,license,photographerId,createdAt) VALUES (11,'de','6893',true,false,'/de/6893.jpg','CC0_10',1,'2018-04-06 19:52:47'), (12,'de','6896',true,false,'/de/6896_1.jpg','CC0_10',1,'2018-04-06 19:52:47'), (13,'de','6897',true,false,'/de/6897_1.jpg','CC0_10',1,'2018-04-06 19:52:47'), @@ -20,7 +20,7 @@ INSERT INTO photos (id,countryCode,stationId,"primary",outdated,urlPath,license, (18,'de','6913',true,false,'/de/6913.jpg','CC_BY_SA_40',11,'2018-04-06 19:52:47'), (19,'de','6914',true,false,'/de/6914.jpg','CC_BY_SA_40',11,'2018-04-06 19:52:47'), (20,'de','6915',true,false,'/de/6915.jpg','CC0_10',11,'2018-04-06 19:52:47'); -INSERT INTO photos (id,countryCode,stationId,"primary",outdated,urlPath,license,photographerId,createdAt) VALUES +INSERT INTO photo (id,countryCode,stationId,"primary",outdated,urlPath,license,photographerId,createdAt) VALUES (21,'de','6916',true,false,'/de/6916.jpg','CC0_10',11,'2018-04-06 19:52:47'), (22,'de','6923',true,false,'/de/6923.jpg','CC0_10',11,'2018-04-06 19:52:47'), (23,'de','6928',true,false,'/de/6928.jpg','CC0_10',11,'2018-04-06 19:52:47'), @@ -31,7 +31,7 @@ INSERT INTO photos (id,countryCode,stationId,"primary",outdated,urlPath,license, (28,'de','6937',true,false,'/de/6937_1.jpg','CC0_10',11,'2018-04-06 19:52:47'), (29,'de','6939',true,false,'/de/6939_1.jpg','CC0_10',11,'2018-04-06 19:52:47'), (30,'de','6940',true,false,'/de/6940.jpg','CC0_10',28,'2018-04-06 19:52:47'); -INSERT INTO photos (id,countryCode,stationId,"primary",outdated,urlPath,license,photographerId,createdAt) VALUES +INSERT INTO photo (id,countryCode,stationId,"primary",outdated,urlPath,license,photographerId,createdAt) VALUES (31,'de','6943',true,false,'/de/6943.jpg','CC0_10',9,'2018-04-06 19:52:47'), (32,'de','6945',true,false,'/de/6945.jpg','CC_BY_SA_40',11,'2018-04-06 19:52:47'), (33,'de','6946',true,false,'/de/6946_0.jpg','CC0_10',9,'2018-04-06 19:52:47'), @@ -42,7 +42,7 @@ INSERT INTO photos (id,countryCode,stationId,"primary",outdated,urlPath,license, (38,'de','6957',true,false,'/de/6957_1.jpg','CC0_10',9,'2018-04-06 19:52:47'), (39,'de','6960',true,false,'/de/6960.jpg','CC_BY_SA_40',28,'2018-04-06 19:52:47'), (40,'de','6966',true,false,'/de/6966.jpg','CC0_10',11,'2018-04-06 19:52:47'); -INSERT INTO photos (id,countryCode,stationId,"primary",outdated,urlPath,license,photographerId,createdAt) VALUES +INSERT INTO photo (id,countryCode,stationId,"primary",outdated,urlPath,license,photographerId,createdAt) VALUES (41,'de','6967',true,false,'/de/6967_1.jpg','CC0_10',9,'2018-04-06 19:52:47'), (42,'de','6970',true,false,'/de/6970_1.jpg','CC0_10',1,'2018-04-06 19:52:47'), (43,'de','6971',true,false,'/de/6971_1.jpg','CC0_10',28,'2018-04-06 19:52:47'), @@ -53,7 +53,7 @@ INSERT INTO photos (id,countryCode,stationId,"primary",outdated,urlPath,license, (48,'de','6981',true,false,'/de/6981_1.jpg','CC0_10',9,'2018-04-06 19:52:47'), (49,'de','6983',true,false,'/de/6983.jpg','CC0_10',9,'2018-04-06 19:52:47'), (50,'de','6987',true,false,'/de/6987.jpg','CC_BY_SA_40',28,'2018-04-06 19:52:47'); -INSERT INTO photos (id,countryCode,stationId,"primary",outdated,urlPath,license,photographerId,createdAt) VALUES +INSERT INTO photo (id,countryCode,stationId,"primary",outdated,urlPath,license,photographerId,createdAt) VALUES (51,'de','6988',true,false,'/de/6988.jpg','CC0_10',9,'2018-04-06 19:52:47'), (52,'de','6990',true,false,'/de/6990.jpg','CC0_10',9,'2018-04-06 19:52:47'), (53,'de','6994',true,false,'/de/6994_1.jpg','CC0_10',1,'2018-04-06 19:52:47'), @@ -64,7 +64,7 @@ INSERT INTO photos (id,countryCode,stationId,"primary",outdated,urlPath,license, (58,'de','7006',true,false,'/de/7006_1.jpg','CC0_10',9,'2018-04-06 19:52:47'), (59,'de','7010',true,false,'/de/7010_1.jpg','CC0_10',9,'2018-04-06 19:52:47'), (60,'de','7016',true,false,'/de/7016.jpg','CC_BY_SA_40',28,'2018-04-06 19:52:47'); -INSERT INTO photos (id,countryCode,stationId,"primary",outdated,urlPath,license,photographerId,createdAt) VALUES +INSERT INTO photo (id,countryCode,stationId,"primary",outdated,urlPath,license,photographerId,createdAt) VALUES (61,'de','7021',true,false,'/de/7021.jpg','CC0_10',9,'2018-04-06 19:52:47'), (62,'de','7022',true,false,'/de/7022.jpg','CC0_10',9,'2018-04-06 19:52:47'), (63,'de','7023',true,false,'/de/7023.jpg','CC0_10',9,'2018-04-06 19:52:47'), @@ -75,7 +75,7 @@ INSERT INTO photos (id,countryCode,stationId,"primary",outdated,urlPath,license, (68,'de','7036',true,false,'/de/7036_1.jpg','CC0_10',28,'2018-04-06 19:52:47'), (69,'de','7042',true,false,'/de/7042_0.jpg','CC0_10',28,'2018-04-06 19:52:47'), (70,'de','7045',true,false,'/de/7045.jpg','CC0_10',28,'2018-04-06 19:52:47'); -INSERT INTO photos (id,countryCode,stationId,"primary",outdated,urlPath,license,photographerId,createdAt) VALUES +INSERT INTO photo (id,countryCode,stationId,"primary",outdated,urlPath,license,photographerId,createdAt) VALUES (71,'de','7046',true,false,'/de/7046_1.jpg','CC0_10',28,'2018-04-06 19:52:47'), (72,'de','7048',true,false,'/de/7048_1.jpg','CC0_10',1,'2018-04-06 19:52:47'), (73,'de','7049',true,false,'/de/7049_1.jpg','CC0_10',28,'2018-04-06 19:52:47'), @@ -86,7 +86,7 @@ INSERT INTO photos (id,countryCode,stationId,"primary",outdated,urlPath,license, (78,'de','7063',true,false,'/de/7063.jpg','CC0_10',28,'2018-04-06 19:52:47'), (79,'de','7065',true,false,'/de/7065.jpg','CC0_10',28,'2018-04-06 19:52:47'), (80,'de','7066',true,false,'/de/7066_1.jpg','CC0_10',28,'2018-04-06 19:52:47'); -INSERT INTO photos (id,countryCode,stationId,"primary",outdated,urlPath,license,photographerId,createdAt) VALUES +INSERT INTO photo (id,countryCode,stationId,"primary",outdated,urlPath,license,photographerId,createdAt) VALUES (81,'de','7068',true,false,'/de/7068.jpg','CC0_10',28,'2018-04-06 19:52:47'), (82,'de','7070',true,false,'/de/7070.jpg','CC0_10',28,'2018-04-06 19:52:47'), (83,'de','7071',true,false,'/de/7071.jpg','CC0_10',28,'2018-04-06 19:52:47'), @@ -97,7 +97,7 @@ INSERT INTO photos (id,countryCode,stationId,"primary",outdated,urlPath,license, (88,'de','7851',true,false,'/de/7851.jpg','CC0_10',9,'2018-04-06 19:52:47'), (89,'de','7978',true,false,'/de/7978.jpg','CC_BY_SA_40',28,'2018-04-06 19:52:47'), (90,'de','8171',true,false,'/de/8171.jpg','CC0_10',9,'2018-04-06 19:52:47'); -INSERT INTO photos (id,countryCode,stationId,"primary",outdated,urlPath,license,photographerId,createdAt) VALUES +INSERT INTO photo (id,countryCode,stationId,"primary",outdated,urlPath,license,photographerId,createdAt) VALUES (91,'de','8205',true,false,'/de/8205.jpg','CC0_10',28,'2018-04-06 19:52:47'), (128,'de','6932',false,true,'/de/6932_2.jpg','CC0_10',11,'2022-08-01 14:45:23'), (129,'ch','8500013',false,true,'/ch/8500013.jpg','CC0_10',1,'2022-09-03 11:43:11'), diff --git a/core/src/main/kotlin/org/railwaystations/rsapi/core/model/Country.kt b/core/src/main/kotlin/org/railwaystations/rsapi/core/model/Country.kt index 7fe38da5..91bf78a1 100644 --- a/core/src/main/kotlin/org/railwaystations/rsapi/core/model/Country.kt +++ b/core/src/main/kotlin/org/railwaystations/rsapi/core/model/Country.kt @@ -3,12 +3,15 @@ package org.railwaystations.rsapi.core.model data class Country( val code: String, val name: String, - val email: String = "info@railway-stations.org", + private val _email: String? = null, val timetableUrlTemplate: String? = null, val overrideLicense: License? = null, val active: Boolean = false, - val providerApps: MutableList = mutableListOf(), + val providerApps: List = listOf(), ) { + val email: String + get() = _email ?: "info@railway-stations.org" + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false diff --git a/core/src/main/kotlin/org/railwaystations/rsapi/core/model/InboxEntry.kt b/core/src/main/kotlin/org/railwaystations/rsapi/core/model/InboxEntry.kt index 983e7051..e13eeb4f 100644 --- a/core/src/main/kotlin/org/railwaystations/rsapi/core/model/InboxEntry.kt +++ b/core/src/main/kotlin/org/railwaystations/rsapi/core/model/InboxEntry.kt @@ -11,13 +11,13 @@ data class InboxEntry( val newTitle: String? = null, val coordinates: Coordinates? = null, val newCoordinates: Coordinates? = null, - val photographerId: Int = 0, + val photographerId: Long = 0, val photographerNickname: String? = null, val photographerEmail: String? = null, val extension: String? = null, val comment: String? = null, val rejectReason: String? = null, - val createdAt: Instant? = null, + val createdAt: Instant = Instant.now(), val done: Boolean = false, val existingPhotoUrlPath: String? = null, val crc32: Long? = null, diff --git a/core/src/main/kotlin/org/railwaystations/rsapi/core/model/Statistic.kt b/core/src/main/kotlin/org/railwaystations/rsapi/core/model/Statistic.kt index 6ee13ad0..85a1fc36 100644 --- a/core/src/main/kotlin/org/railwaystations/rsapi/core/model/Statistic.kt +++ b/core/src/main/kotlin/org/railwaystations/rsapi/core/model/Statistic.kt @@ -2,11 +2,11 @@ package org.railwaystations.rsapi.core.model data class Statistic( val countryCode: String?, - val total: Long, - val withPhoto: Long, - val photographers: Long + val total: Int, + val withPhoto: Int, + val photographers: Int ) { - val withoutPhoto: Long + val withoutPhoto: Int get() = total - withPhoto - + } diff --git a/core/src/main/kotlin/org/railwaystations/rsapi/core/model/User.kt b/core/src/main/kotlin/org/railwaystations/rsapi/core/model/User.kt index 539cccd1..c1b17927 100644 --- a/core/src/main/kotlin/org/railwaystations/rsapi/core/model/User.kt +++ b/core/src/main/kotlin/org/railwaystations/rsapi/core/model/User.kt @@ -12,7 +12,7 @@ const val ROLE_USER: String = "ROLE_USER" const val ANONYM: String = "Anonym" data class User( - val id: Int = 0, + val id: Long = 0, val name: String, val url: String? = null, val license: License = License.UNKNOWN, diff --git a/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/inbound/FindPhotoStationsUseCase.kt b/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/inbound/FindPhotoStationsUseCase.kt index 3a86094f..ce87c072 100644 --- a/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/inbound/FindPhotoStationsUseCase.kt +++ b/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/inbound/FindPhotoStationsUseCase.kt @@ -12,7 +12,7 @@ interface FindPhotoStationsUseCase { fun findByCountry(countries: Set, hasPhoto: Boolean?, active: Boolean?): Set - fun findByCountryAndId(country: String?, stationId: String?): Station? + fun findByKey(key: Station.Key): Station? fun findRecentImports(sinceHours: Long): Set diff --git a/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/inbound/LoadPhotographersUseCase.kt b/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/inbound/LoadPhotographersUseCase.kt index 4b3582c7..199c0f33 100644 --- a/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/inbound/LoadPhotographersUseCase.kt +++ b/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/inbound/LoadPhotographersUseCase.kt @@ -1,5 +1,5 @@ package org.railwaystations.rsapi.core.ports.inbound interface LoadPhotographersUseCase { - fun getPhotographersPhotocountMap(country: String?): Map + fun getPhotographersPhotocountMap(country: String?): Map } \ No newline at end of file diff --git a/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/inbound/ManageInboxUseCase.kt b/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/inbound/ManageInboxUseCase.kt index b56ba069..4e555a8b 100644 --- a/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/inbound/ManageInboxUseCase.kt +++ b/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/inbound/ManageInboxUseCase.kt @@ -20,7 +20,7 @@ interface ManageInboxUseCase { fun listAdminInbox(user: User): List - fun countPendingInboxEntries(): Long + fun countPendingInboxEntries(): Int fun uploadPhoto( clientInfo: String?, body: InputStream?, stationId: String?, diff --git a/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/outbound/InboxPort.kt b/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/outbound/InboxPort.kt index 85e167fe..c709fd71 100644 --- a/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/outbound/InboxPort.kt +++ b/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/outbound/InboxPort.kt @@ -13,7 +13,7 @@ interface InboxPort { fun reject(id: Long, rejectReason: String) fun done(id: Long) fun countPendingInboxEntriesForStation(id: Long?, countryCode: String, stationId: String): Int - fun countPendingInboxEntries(): Long + fun countPendingInboxEntries(): Int fun countPendingInboxEntriesForNearbyCoordinates(id: Long?, coordinates: Coordinates): Int fun updateCrc32(id: Long, crc32: Long) fun findInboxEntriesToNotify(): List @@ -21,6 +21,6 @@ interface InboxPort { fun updatePosted(id: Long) fun updatePhotoId(id: Long, photoId: Long) fun updateMissingStationImported(id: Long, countryCode: String, stationId: String, title: String) - fun findByUser(photographerId: Int, showCompletedEntries: Boolean): List + fun findByUser(photographerId: Long, showCompletedEntries: Boolean): List fun findPendingByStation(countryCode: String, stationId: String): List } \ No newline at end of file diff --git a/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/outbound/OAuth2AuthorizationPort.kt b/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/outbound/OAuth2AuthorizationPort.kt index 042debbc..9dda814f 100644 --- a/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/outbound/OAuth2AuthorizationPort.kt +++ b/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/outbound/OAuth2AuthorizationPort.kt @@ -3,6 +3,6 @@ package org.railwaystations.rsapi.core.ports.outbound import java.time.Instant interface OAuth2AuthorizationPort { - fun deleteExpiredTokens(now: Instant?): Int - fun deleteAllByUser(principalName: String?) + fun deleteExpiredTokens(now: Instant): Int + fun deleteAllByUser(principalName: String) } \ No newline at end of file diff --git a/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/outbound/PhotoPort.kt b/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/outbound/PhotoPort.kt index f987441d..b00734b0 100644 --- a/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/outbound/PhotoPort.kt +++ b/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/outbound/PhotoPort.kt @@ -4,11 +4,11 @@ import org.railwaystations.rsapi.core.model.Photo import org.railwaystations.rsapi.core.model.Station interface PhotoPort { - fun insert(photo: Photo?): Long - fun update(photo: Photo?) + fun insert(photo: Photo): Long + fun update(photo: Photo) fun delete(id: Long) fun updatePhotoOutdated(id: Long) - fun setAllPhotosForStationSecondary(key: Station.Key?) + fun setAllPhotosForStationSecondary(key: Station.Key) fun setPrimary(id: Long) fun countPhotos(): Long fun findNthPhotoId(n: Long): Long diff --git a/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/outbound/StationPort.kt b/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/outbound/StationPort.kt index dd082778..441c40a4 100644 --- a/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/outbound/StationPort.kt +++ b/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/outbound/StationPort.kt @@ -6,17 +6,17 @@ import org.railwaystations.rsapi.core.model.Statistic import java.time.Instant interface StationPort { - fun findByCountryCodes(countryCodes: Set, hasPhoto: Boolean?, active: Boolean?): Set - fun findByKey(countryCode: String, id: String): Station? - fun findByPhotographer(photographer: String, countryCode: String?): Set + fun findByCountryCodes(countryCodes: Set, hasPhoto: Boolean? = null, active: Boolean? = null): Set + fun findByKey(key: Station.Key): Station? + fun findByPhotographer(photographer: String, countryCode: String? = null): Set fun findRecentImports(since: Instant): Set - fun getStatistic(countryCode: String?): Statistic - fun getPhotographerMap(countryCode: String?): Map + fun getStatistic(countryCode: String? = null): Statistic + fun getPhotographerMap(countryCode: String? = null): Map fun insert(station: Station) fun delete(key: Station.Key) fun updateActive(key: Station.Key, active: Boolean) fun countNearbyCoordinates(coordinates: Coordinates): Int - val maxZ: Int + fun maxZ(): Int fun changeStationTitle(key: Station.Key, newTitle: String) fun updateLocation(key: Station.Key, coordinates: Coordinates) fun findByPhotoId(photoId: Long): Station diff --git a/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/outbound/UserPort.kt b/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/outbound/UserPort.kt index 1ede1127..98240134 100644 --- a/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/outbound/UserPort.kt +++ b/core/src/main/kotlin/org/railwaystations/rsapi/core/ports/outbound/UserPort.kt @@ -3,17 +3,16 @@ package org.railwaystations.rsapi.core.ports.outbound import org.railwaystations.rsapi.core.model.User interface UserPort { - fun list(): List fun findByName(name: String): User? - fun findById(id: Int): User? + fun findById(id: Long): User? fun findByEmail(email: String): User? - fun updateCredentials(id: Int, key: String) - fun insert(user: User, key: String?, emailVerification: String?): Int - fun update(id: Int, user: User) + fun updateCredentials(id: Long, key: String) + fun insert(user: User, key: String?, emailVerification: String?): Long + fun update(id: Long, user: User) fun findByEmailVerification(emailVerification: String): User? - fun updateEmailVerification(id: Int, emailVerification: String?) - fun anonymizeUser(id: Int) + fun updateEmailVerification(id: Long, emailVerification: String?) + fun anonymizeUser(id: Long) fun addUsernameToBlocklist(name: String) fun countBlockedUsername(name: String): Int - fun updateLocale(id: Int, locallocaleLanguageTage: String) + fun updateLocale(id: Long, localeLanguageTag: String) } \ No newline at end of file diff --git a/core/src/main/kotlin/org/railwaystations/rsapi/core/services/InboxService.kt b/core/src/main/kotlin/org/railwaystations/rsapi/core/services/InboxService.kt index 43bf6c0a..f7ef2e33 100644 --- a/core/src/main/kotlin/org/railwaystations/rsapi/core/services/InboxService.kt +++ b/core/src/main/kotlin/org/railwaystations/rsapi/core/services/InboxService.kt @@ -1,10 +1,27 @@ package org.railwaystations.rsapi.core.services -import org.railwaystations.rsapi.core.model.* +import org.railwaystations.rsapi.core.model.Coordinates +import org.railwaystations.rsapi.core.model.Country +import org.railwaystations.rsapi.core.model.InboxCommand +import org.railwaystations.rsapi.core.model.InboxEntry +import org.railwaystations.rsapi.core.model.InboxResponse +import org.railwaystations.rsapi.core.model.InboxStateQuery import org.railwaystations.rsapi.core.model.InboxStateQuery.InboxState +import org.railwaystations.rsapi.core.model.Photo +import org.railwaystations.rsapi.core.model.ProblemReport +import org.railwaystations.rsapi.core.model.PublicInboxEntry +import org.railwaystations.rsapi.core.model.Station +import org.railwaystations.rsapi.core.model.User +import org.railwaystations.rsapi.core.model.createInboxFilename import org.railwaystations.rsapi.core.ports.inbound.ManageInboxUseCase -import org.railwaystations.rsapi.core.ports.outbound.* +import org.railwaystations.rsapi.core.ports.outbound.CountryPort +import org.railwaystations.rsapi.core.ports.outbound.InboxPort +import org.railwaystations.rsapi.core.ports.outbound.MonitorPort +import org.railwaystations.rsapi.core.ports.outbound.PhotoPort +import org.railwaystations.rsapi.core.ports.outbound.PhotoStoragePort import org.railwaystations.rsapi.core.ports.outbound.PhotoStoragePort.PhotoTooLargeException +import org.railwaystations.rsapi.core.ports.outbound.StationPort +import org.railwaystations.rsapi.core.ports.outbound.UserPort import org.railwaystations.rsapi.core.utils.ImageUtil.mimeToExtension import org.railwaystations.rsapi.core.utils.Logger import org.springframework.beans.factory.annotation.Value @@ -141,12 +158,14 @@ class InboxService( InboxState.REVIEW } - override fun listAdminInbox(user: User): List = - inboxPort.findPendingInboxEntries().map(::updateInboxEntry) + override fun listAdminInbox(user: User): List { + val pendingInboxEntries = inboxPort.findPendingInboxEntries() + return pendingInboxEntries.map { updateInboxEntry(it, pendingInboxEntries) } + } - private fun updateInboxEntry(inboxEntry: InboxEntry): InboxEntry { + private fun updateInboxEntry(inboxEntry: InboxEntry, pendingInboxEntries: List): InboxEntry { val filename = inboxEntry.filename - val processed = filename?.let { photoStoragePort.isProcessed(it) } ?: false + val processed = filename?.let { photoStoragePort.isProcessed(it) } == true val inboxUrl = if (filename != null) { getInboxUrl(inboxEntry.filename, inboxEntry.done, inboxEntry.rejectReason, processed) } else if (inboxEntry.hasPhoto) { @@ -157,7 +176,7 @@ class InboxService( val conflict = if (inboxEntry.stationId == null && !inboxEntry.newCoordinates!!.hasZeroCoords) { hasConflict(inboxEntry.id, inboxEntry.newCoordinates) } else { - inboxEntry.conflict + pendingInboxEntries.hasOtherEntryForSameStation(inboxEntry) } return inboxEntry.copy( @@ -167,6 +186,9 @@ class InboxService( ) } + private fun List.hasOtherEntryForSameStation(inboxEntry: InboxEntry) = + any { it.id != inboxEntry.id && it.countryCode == inboxEntry.countryCode && it.stationId == inboxEntry.stationId } + private fun getInboxUrl(filename: String?, done: Boolean, rejectReason: String?, processed: Boolean): String? { if (filename == null) { return null @@ -209,7 +231,7 @@ class InboxService( inboxPort.done(inboxEntry.id) } - override fun countPendingInboxEntries(): Long { + override fun countPendingInboxEntries(): Int { return inboxPort.countPendingInboxEntries() } @@ -413,7 +435,7 @@ class InboxService( requireNotNull(command.active) { "No Active flag provided" } val newStation = Station( - key = Station.Key(country.code, "Z${stationPort.maxZ + 1}"), + key = Station.Key(country.code, "Z${stationPort.maxZ() + 1}"), title = command.title, coordinates = command.coordinates, ds100 = command.ds100, @@ -594,17 +616,15 @@ class InboxService( if (coordinates == null || coordinates.hasZeroCoords) { return false } - return inboxPort.countPendingInboxEntriesForNearbyCoordinates( - id, - coordinates - ) > 0 || stationPort.countNearbyCoordinates(coordinates) > 0 + return inboxPort.countPendingInboxEntriesForNearbyCoordinates(id, coordinates) > 0 + || stationPort.countNearbyCoordinates(coordinates) > 0 } private fun findStationByCountryAndId(countryCode: String?, stationId: String?): Station? { if (countryCode == null || stationId == null) { return null } - return stationPort.findByKey(countryCode, stationId) + return stationPort.findByKey(Station.Key(countryCode, stationId)) } } diff --git a/core/src/main/kotlin/org/railwaystations/rsapi/core/services/PhotoStationsService.kt b/core/src/main/kotlin/org/railwaystations/rsapi/core/services/PhotoStationsService.kt index 92663b45..4e758452 100644 --- a/core/src/main/kotlin/org/railwaystations/rsapi/core/services/PhotoStationsService.kt +++ b/core/src/main/kotlin/org/railwaystations/rsapi/core/services/PhotoStationsService.kt @@ -9,13 +9,8 @@ import java.time.temporal.ChronoUnit @Service class PhotoStationsService(private val stationPort: StationPort) : FindPhotoStationsUseCase { - override fun findByCountryAndId(country: String?, stationId: String?): Station? { - if (stationId.isNullOrBlank() || country.isNullOrBlank()) { - return null - } - - val key = Station.Key(country, stationId) - return stationPort.findByKey(key.country, key.id) + override fun findByKey(key: Station.Key): Station? { + return stationPort.findByKey(key) } override fun findRecentImports(sinceHours: Long): Set { diff --git a/core/src/main/kotlin/org/railwaystations/rsapi/core/services/PhotographersService.kt b/core/src/main/kotlin/org/railwaystations/rsapi/core/services/PhotographersService.kt index 324a0cc2..77efa976 100644 --- a/core/src/main/kotlin/org/railwaystations/rsapi/core/services/PhotographersService.kt +++ b/core/src/main/kotlin/org/railwaystations/rsapi/core/services/PhotographersService.kt @@ -11,7 +11,7 @@ class PhotographersService( private val countryPort: CountryPort, ) : LoadPhotographersUseCase { - override fun getPhotographersPhotocountMap(country: String?): Map { + override fun getPhotographersPhotocountMap(country: String?): Map { require(!(country != null && countryPort.findById(country) == null)) { "Country $country does not exist" } return stationPort.getPhotographerMap(country) } diff --git a/core/src/main/kotlin/org/railwaystations/rsapi/core/services/SocialMediaService.kt b/core/src/main/kotlin/org/railwaystations/rsapi/core/services/SocialMediaService.kt index ee043863..a0bb81b9 100644 --- a/core/src/main/kotlin/org/railwaystations/rsapi/core/services/SocialMediaService.kt +++ b/core/src/main/kotlin/org/railwaystations/rsapi/core/services/SocialMediaService.kt @@ -1,11 +1,15 @@ package org.railwaystations.rsapi.core.services -import org.railwaystations.rsapi.core.model.* +import org.railwaystations.rsapi.core.model.ANONYM import org.railwaystations.rsapi.core.ports.inbound.SocialMediaUseCase -import org.railwaystations.rsapi.core.ports.outbound.* +import org.railwaystations.rsapi.core.ports.outbound.InboxPort +import org.railwaystations.rsapi.core.ports.outbound.MastodonPort +import org.railwaystations.rsapi.core.ports.outbound.PhotoPort +import org.railwaystations.rsapi.core.ports.outbound.StationPort +import org.railwaystations.rsapi.core.ports.outbound.UserPort import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service -import java.util.Random +import java.util.* @Service class SocialMediaService( diff --git a/core/src/test/kotlin/org/railwaystations/rsapi/core/services/InboxServiceTest.kt b/core/src/test/kotlin/org/railwaystations/rsapi/core/services/InboxServiceTest.kt index 459a165c..9154cd42 100644 --- a/core/src/test/kotlin/org/railwaystations/rsapi/core/services/InboxServiceTest.kt +++ b/core/src/test/kotlin/org/railwaystations/rsapi/core/services/InboxServiceTest.kt @@ -1,6 +1,10 @@ package org.railwaystations.rsapi.core.services -import io.mockk.* +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.BeforeEach @@ -10,21 +14,34 @@ import org.junit.jupiter.api.TestInstance import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource -import org.railwaystations.rsapi.core.model.* +import org.railwaystations.rsapi.core.model.Coordinates +import org.railwaystations.rsapi.core.model.Country +import org.railwaystations.rsapi.core.model.CountryTestFixtures +import org.railwaystations.rsapi.core.model.EMAIL_VERIFIED +import org.railwaystations.rsapi.core.model.InboxCommand +import org.railwaystations.rsapi.core.model.InboxEntry import org.railwaystations.rsapi.core.model.InboxResponse.InboxResponseState +import org.railwaystations.rsapi.core.model.License +import org.railwaystations.rsapi.core.model.Photo +import org.railwaystations.rsapi.core.model.ProblemReport +import org.railwaystations.rsapi.core.model.ProblemReportType +import org.railwaystations.rsapi.core.model.Station +import org.railwaystations.rsapi.core.model.User import org.railwaystations.rsapi.core.ports.inbound.ManageInboxUseCase -import org.railwaystations.rsapi.core.ports.outbound.* +import org.railwaystations.rsapi.core.ports.outbound.CountryPort +import org.railwaystations.rsapi.core.ports.outbound.InboxPort +import org.railwaystations.rsapi.core.ports.outbound.MastodonPort +import org.railwaystations.rsapi.core.ports.outbound.MonitorPort +import org.railwaystations.rsapi.core.ports.outbound.PhotoPort +import org.railwaystations.rsapi.core.ports.outbound.PhotoStoragePort +import org.railwaystations.rsapi.core.ports.outbound.StationPort +import org.railwaystations.rsapi.core.ports.outbound.UserPort import java.io.IOException import java.time.Clock import java.time.Instant import java.time.ZoneId -private val DE: Country = Country( - code = "de", - name = "Germany", - email = "email@example.com", -) -private val STATION_KEY_DE_1: Station.Key = Station.Key(DE.code, "1") +private val STATION_KEY_DE_1: Station.Key = Station.Key(CountryTestFixtures.countryDe.code, "1") private const val INBOX_ENTRY1_ID: Long = 1 private val PHOTOGRAPHER: User = User( id = 1, @@ -79,7 +96,7 @@ internal class InboxServiceTest { ) every { countryPort.findById(any()) } returns null - every { countryPort.findById(DE.code) } returns DE + every { countryPort.findById(CountryTestFixtures.countryDe.code) } returns CountryTestFixtures.countryDe every { inboxPort.findById(any()) } returns null every { inboxPort.done(any()) } returns Unit every { inboxPort.updatePhotoId(any(), any()) } returns Unit @@ -90,7 +107,7 @@ internal class InboxServiceTest { every { monitorPort.sendMessage(any()) } returns Unit every { photoPort.setAllPhotosForStationSecondary(any()) } returns Unit every { photoPort.update(any()) } returns Unit - every { stationPort.findByKey(any(), any()) } returns null + every { stationPort.findByKey(any()) } returns null every { stationPort.countNearbyCoordinates(any()) } returns 0 every { stationPort.insert(any()) } returns Unit every { stationPort.updateLocation(any(), any()) } returns Unit @@ -119,7 +136,7 @@ internal class InboxServiceTest { val inboxEntry = createInboxEntry1() every { inboxPort.findById(INBOX_ENTRY1_ID) } returns inboxEntry val station = createStationDe1() - every { stationPort.findByKey(STATION_KEY_DE_1.country, STATION_KEY_DE_1.id) } returns station + every { stationPort.findByKey(STATION_KEY_DE_1) } returns station every { photoPort.insert(capture(photoCaptor)) } returns IMPORTED_PHOTO_ID every { photoStoragePort.importPhoto(inboxEntry, station) } returns IMPORTED_PHOTO_URL_PATH @@ -260,7 +277,7 @@ internal class InboxServiceTest { inboxEntry.stationId!! ) } returns 1 - every { stationPort.findByKey(STATION_KEY_DE_1.country, STATION_KEY_DE_1.id) } returns createStationDe1() + every { stationPort.findByKey(STATION_KEY_DE_1) } returns createStationDe1() assertThatThrownBy { inboxService.importPhoto(command) }.isInstanceOf( IllegalArgumentException::class.java @@ -281,7 +298,7 @@ internal class InboxServiceTest { inboxEntry.stationId!! ) } returns 1 - every { stationPort.findByKey(STATION_KEY_DE_1.country, STATION_KEY_DE_1.id) } returns createStationDe1() + every { stationPort.findByKey(STATION_KEY_DE_1) } returns createStationDe1() assertThatThrownBy { inboxService.importPhoto(command) }.isInstanceOf( IllegalArgumentException::class.java @@ -321,11 +338,11 @@ internal class InboxServiceTest { ) ) ) - every { stationPort.findByKey(STATION_KEY_DE_1.country, STATION_KEY_DE_1.id) } returns stationDe1 + every { stationPort.findByKey(STATION_KEY_DE_1) } returns stationDe1 } private fun whenStation1Exists() { - every { stationPort.findByKey(STATION_KEY_DE_1.country, STATION_KEY_DE_1.id) } returns createStationDe1() + every { stationPort.findByKey(STATION_KEY_DE_1) } returns createStationDe1() } private fun createNewStationByCommand(command: InboxCommand, stationId: String): Station { @@ -339,7 +356,7 @@ internal class InboxServiceTest { } private fun createNewStationCommand1(): InboxCommand = createInboxCommand1().copy( - countryCode = DE.code, + countryCode = CountryTestFixtures.countryDe.code, stationId = NEW_STATION_ID, title = NEW_STATION_TITLE, coordinates = NEW_COORDINATES, @@ -373,8 +390,8 @@ internal class InboxServiceTest { stationId = null ) every { inboxPort.findById(INBOX_ENTRY1_ID) } returns inboxEntry - every { stationPort.findByKey(STATION_KEY_DE_1.country, command.stationId!!) } returns null - every { stationPort.maxZ } returns 1024 + every { stationPort.findByKey(Station.Key(STATION_KEY_DE_1.country, command.stationId!!)) } returns null + every { stationPort.maxZ() } returns 1024 val newStation = createNewStationByCommand(command, "Z1025") every { photoPort.insert(capture(photoCaptor)) } returns IMPORTED_PHOTO_ID every { photoStoragePort.importPhoto(inboxEntry, newStation) } returns IMPORTED_PHOTO_URL_PATH @@ -404,8 +421,8 @@ internal class InboxServiceTest { extension = null, ) every { inboxPort.findById(INBOX_ENTRY1_ID) } returns inboxEntry - every { stationPort.findByKey(STATION_KEY_DE_1.country, command.stationId!!) } returns null - every { stationPort.maxZ } returns 4711 + every { stationPort.findByKey(Station.Key(STATION_KEY_DE_1.country, command.stationId!!)) } returns null + every { stationPort.maxZ() } returns 4711 val newStation = createNewStationByCommand(command, "Z4712") inboxService.importMissingStation(command) @@ -549,7 +566,7 @@ internal class InboxServiceTest { inboxEntry.stationId!! ) } returns 1 - every { stationPort.findByKey(STATION_KEY_DE_1.country, STATION_KEY_DE_1.id) } returns createStationDe1() + every { stationPort.findByKey(STATION_KEY_DE_1) } returns createStationDe1() assertThatThrownBy { inboxService.importMissingStation(command) }.isInstanceOf( IllegalArgumentException::class.java @@ -620,7 +637,7 @@ internal class InboxServiceTest { @Test fun reportWrongPhotoWithWrongPhotoId() { - val problemReport = createWrongPhotoProblemReport(0L) + val problemReport = createWrongPhotoProblemReport(0) whenStation1ExistsWithPhoto() val inboxResponse = inboxService.reportProblem(problemReport, createValidUser(), null) @@ -789,10 +806,7 @@ internal class InboxServiceTest { ) every { inboxPort.findById(inboxEntry1.id) } returns inboxEntry1 every { - stationPort.findByKey( - inboxEntry1.countryCode!!, - inboxEntry1.stationId!! - ) + stationPort.findByKey(Station.Key(inboxEntry1.countryCode!!, inboxEntry1.stationId!!)) } returns createStationDe1() val inboxEntry2 = InboxEntry( id = 2, @@ -840,23 +854,21 @@ internal class InboxServiceTest { } } - fun createCountryWithOverrideLicense(overrideLicense: License?): Country { - return Country( + fun createCountryWithOverrideLicense(overrideLicense: License?) = + Country( code = "xx", name = "XX", - email = "email@example.com", + _email = "email@example.com", timetableUrlTemplate = null, overrideLicense = overrideLicense, ) - } - fun createValidUser(): User { - return User( + fun createValidUser() = + User( name = "name", license = License.CC0_10, email = "email@example.com", ownPhotos = true, emailVerification = EMAIL_VERIFIED, ) - } } \ No newline at end of file diff --git a/core/src/test/kotlin/org/railwaystations/rsapi/core/services/SocialMediaServiceTest.kt b/core/src/test/kotlin/org/railwaystations/rsapi/core/services/SocialMediaServiceTest.kt index 9a0072a8..f4451fc6 100644 --- a/core/src/test/kotlin/org/railwaystations/rsapi/core/services/SocialMediaServiceTest.kt +++ b/core/src/test/kotlin/org/railwaystations/rsapi/core/services/SocialMediaServiceTest.kt @@ -1,10 +1,17 @@ package org.railwaystations.rsapi.core.services -import io.mockk.* +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify import org.junit.jupiter.api.Test -import org.railwaystations.rsapi.core.model.* +import org.railwaystations.rsapi.core.model.InboxEntry import org.railwaystations.rsapi.core.model.StationTestFixtures.stationDe5WithPhoto -import org.railwaystations.rsapi.core.ports.outbound.* +import org.railwaystations.rsapi.core.model.UserTestFixtures +import org.railwaystations.rsapi.core.ports.outbound.InboxPort +import org.railwaystations.rsapi.core.ports.outbound.MastodonPort +import org.railwaystations.rsapi.core.ports.outbound.PhotoPort +import org.railwaystations.rsapi.core.ports.outbound.StationPort +import org.railwaystations.rsapi.core.ports.outbound.UserPort internal class SocialMediaServiceTest { @@ -24,7 +31,6 @@ internal class SocialMediaServiceTest { @Test fun postNewPhotoToMastodon() { - val user = UserTestFixtures.createSomeUser() val inboxEntry = InboxEntry( countryCode = "de", stationId = "1234", @@ -32,7 +38,7 @@ internal class SocialMediaServiceTest { title = "title", comment = "comment", ) - every { userPort.findById(0) } returns user + every { userPort.findById(0) } returns UserTestFixtures.someUser every { inboxPort.findOldestImportedPhotoNotYetPosted() } returns inboxEntry every { inboxPort.updatePosted(any()) } returns Unit diff --git a/core/src/testFixtures/kotlin/org/railwaystations/rsapi/core/model/CountryTestFixtures.kt b/core/src/testFixtures/kotlin/org/railwaystations/rsapi/core/model/CountryTestFixtures.kt index 5764ac78..ad3fd4d7 100644 --- a/core/src/testFixtures/kotlin/org/railwaystations/rsapi/core/model/CountryTestFixtures.kt +++ b/core/src/testFixtures/kotlin/org/railwaystations/rsapi/core/model/CountryTestFixtures.kt @@ -9,17 +9,46 @@ object CountryTestFixtures { val country = Country( code = code, name = "name-$code", - email = "email-$code", + _email = "email-$code", timetableUrlTemplate = "timetable-$code", overrideLicense = License.CC_BY_NC_40_INT, + providerApps = listOf( + createProviderApp("android", code), + createProviderApp("ios", code), + createProviderApp("web", code), + ) ) - country.providerApps.add(createProviderApp("android", code)) - country.providerApps.add(createProviderApp("ios", code)) - country.providerApps.add(createProviderApp("web", code)) return country } private fun createProviderApp(type: String, code: String): ProviderApp { return ProviderApp(type, "Provider-$code", type + "App-" + code) } + + val countryDe = Country( + code = "de", + name = "Deutschland", + _email = "info@railway-stations.org", + timetableUrlTemplate = "https://mobile.bahn.de/bin/mobil/bhftafel.exe/dox?bt=dep&max=10&rt=1&use_realtime_filter=1&start=yes&input={title}", + overrideLicense = null, + active = true, + providerApps = listOf( + ProviderApp( + type = "android", + name = "DB Navigator", + url = "https://play.google.com/store/apps/details?id=de.hafas.android.db" + ), + ProviderApp( + type = "android", + name = "FlixTrain", + url = "https://play.google.com/store/apps/details?id=de.meinfernbus" + ), + ProviderApp( + type = "ios", + name = "DB Navigator", + url = "https://apps.apple.com/app/db-navigator/id343555245" + ) + ) + ) + } \ No newline at end of file diff --git a/core/src/testFixtures/kotlin/org/railwaystations/rsapi/core/model/InboxEntryTestFixtures.kt b/core/src/testFixtures/kotlin/org/railwaystations/rsapi/core/model/InboxEntryTestFixtures.kt index 0fd20e8a..4f9d84dd 100644 --- a/core/src/testFixtures/kotlin/org/railwaystations/rsapi/core/model/InboxEntryTestFixtures.kt +++ b/core/src/testFixtures/kotlin/org/railwaystations/rsapi/core/model/InboxEntryTestFixtures.kt @@ -1,15 +1,53 @@ package org.railwaystations.rsapi.core.model -import java.time.Instant +import org.railwaystations.rsapi.core.model.StationTestFixtures.stationDe8000 +import org.railwaystations.rsapi.core.model.UserTestFixtures.user10 +import java.time.Instant.now +import java.time.temporal.ChronoUnit object InboxEntryTestFixtures { + + val photoUploadDe8000 = InboxEntry( + id = 0L, + countryCode = stationDe8000.key.country, + stationId = stationDe8000.key.id, + photographerId = user10.id, + extension = "jpg", + crc32 = System.currentTimeMillis(), + createdAt = now().truncatedTo(ChronoUnit.SECONDS), + comment = "Some important comment.", + ) + + val problemReportDe8000 = InboxEntry( + id = 0L, + countryCode = stationDe8000.key.country, + stationId = stationDe8000.key.id, + photographerId = user10.id, + existingPhotoUrlPath = "de/4711.jpg", + photoId = 4711, + createdAt = now(), + comment = "Photo outdated", + problemReportType = ProblemReportType.PHOTO_OUTDATED, + ) + + val photoUploadMissingStation = InboxEntry( + id = 0L, + title = "Missing Station", + coordinates = Coordinates(50.1, 9.7), + photographerId = user10.id, + extension = "jpg", + crc32 = System.currentTimeMillis(), + createdAt = now().truncatedTo(ChronoUnit.SECONDS), + comment = "Some missing station.", + ) + fun createInboxEntry( user: User, - id: Int, + id: Int = 0, countryCode: String, stationId: String, - rejectReason: String?, - done: Boolean + rejectReason: String? = null, + done: Boolean = false ): InboxEntry { return InboxEntry( id = id.toLong(), @@ -21,7 +59,7 @@ object InboxEntryTestFixtures { photographerNickname = user.name, extension = "jpg", rejectReason = rejectReason, - createdAt = Instant.now(), + createdAt = now(), done = done, ) } diff --git a/core/src/testFixtures/kotlin/org/railwaystations/rsapi/core/model/StationTestFixtures.kt b/core/src/testFixtures/kotlin/org/railwaystations/rsapi/core/model/StationTestFixtures.kt index b38edaff..335b4b93 100644 --- a/core/src/testFixtures/kotlin/org/railwaystations/rsapi/core/model/StationTestFixtures.kt +++ b/core/src/testFixtures/kotlin/org/railwaystations/rsapi/core/model/StationTestFixtures.kt @@ -1,30 +1,54 @@ package org.railwaystations.rsapi.core.model import org.railwaystations.rsapi.core.model.PhotoTestFixtures.createPhoto +import org.railwaystations.rsapi.core.model.UserTestFixtures.user2 +import java.time.Instant object StationTestFixtures { - val keyDe4711 = Station.Key("de", "4711") - val keyDe0815 = Station.Key("de", "0815") + val keyDe8000 = Station.Key("de", "8000") + val keyDe8001 = Station.Key("de", "8001") val keyDe5 = Station.Key("de", "5") - val stationDe4711 = createStationWithKey(keyDe4711) - val stationDe0815 = createStationWithKey(keyDe0815) + val stationDe8000 = + createStationWithKey(keyDe8000, "Offenburg Kreisschulzentrum", Coordinates(48.459376429721, 7.95547485351562)) + val stationDe8001 = + createStationWithKey(keyDe8001, "Bretten-Rechberg", Coordinates(49.0330697651661, 8.70151340961456)) - private fun createStationWithKey(key: Station.Key): Station = Station( + private fun createStationWithKey( + key: Station.Key, + title: String = "${key.country} ${key.id}", + coordinates: Coordinates = Coordinates(50.0, 9.0) + ): Station = Station( key = key, - title = "${key.country} ${key.id}", + title = title, + coordinates = coordinates ) - val stationDe5WithPhoto = createStationWithKey(keyDe5).copy( - title = "Lummerland", - coordinates = Coordinates(50.0, 9.0), + val stationDe5WithPhoto = createStationWithKey(keyDe5, "Lummerland", Coordinates(50.0, 9.0)).copy( ds100 = "XYZ", - photos = listOf(createPhoto(keyDe5, UserTestFixtures.createUserJimKnopf())) + photos = listOf(createPhoto(keyDe5, UserTestFixtures.userJimKnopf)) ) - fun createStation(key: Station.Key, coordinates: Coordinates, photo: Photo?): Station = - createStationWithKey(key).copy( - coordinates = coordinates, + val keyCh8503001 = Station.Key("ch", "8503001") + + val stationCh8503001 = createStationWithKey( + key = keyCh8503001, + title = "Zürich Altstetten", + coordinates = Coordinates(lat = 47.3914808361, lon = 8.4889402654), + ).copy( + ds100 = "ZAS", + photos = listOf( + createPhoto(keyCh8503001, user2).copy( + id = 1, + urlPath = "/ch/8503001%20-%20Z%C3%BCrich%20Altstetten.jpg", + createdAt = Instant.parse("2018-04-06T17:52:47Z"), + license = License.CC0_10, + ) + ) + ) + + fun createStation(key: Station.Key, coordinates: Coordinates, photo: Photo? = null): Station = + createStationWithKey(key = key, coordinates = coordinates).copy( ds100 = "LAL", photos = if (photo != null) listOf(photo) else listOf() ) diff --git a/core/src/testFixtures/kotlin/org/railwaystations/rsapi/core/model/UserTestFixtures.kt b/core/src/testFixtures/kotlin/org/railwaystations/rsapi/core/model/UserTestFixtures.kt index aa30fa20..c383428a 100644 --- a/core/src/testFixtures/kotlin/org/railwaystations/rsapi/core/model/UserTestFixtures.kt +++ b/core/src/testFixtures/kotlin/org/railwaystations/rsapi/core/model/UserTestFixtures.kt @@ -1,43 +1,65 @@ package org.railwaystations.rsapi.core.model +import java.util.* + object UserTestFixtures { - const val USER_AGENT: String = "UserAgent" - const val USER_NAME: String = "existing" - const val USER_EMAIL: String = "existing@example.com" - const val EXISTING_USER_ID: Int = 42 - - fun createUserJimKnopf(): User { - return User( - id = 0, - name = "Jim Knopf", - url = "photographerUrl", - email = "jim.knopf@example.com", - license = License.CC0_10, - ownPhotos = true, - ) - } - - fun createUserNickname(): User { - return User( - id = 42, - name = "nickname", - license = License.CC0_10, - email = "nickname@example.com", - ownPhotos = true, - anonymous = true, - emailVerification = EMAIL_VERIFIED, - ) - } - - fun createSomeUser(): User { - return User( - id = 1, - name = "someuser", - license = License.CC0_10, - email = "someuser@example.com", - ownPhotos = true, - emailVerification = EMAIL_VERIFIED, - ) - } + const val USER_AGENT = "UserAgent" + const val USER_NAME = "existing" + const val USER_EMAIL = "existing@example.com" + const val EXISTING_USER_ID = 42L + + val user2 = User( + id = 3, + name = "@user2", + url = "http://www.example.com/user2", + license = License.CC0_10, + ownPhotos = true, + sendNotifications = true, + locale = Locale.ENGLISH, + ) + + val user10 = User( + id = 11, + name = "@user10", + email = "user10@example.com", + url = "https://www.example.com/user10", + ownPhotos = true, + anonymous = false, + license = License.CC0_10, + key = "246172676F6E32696424763D3139246D3D36353533362C743D322C703D312432634C6B4C6949415958584E52742B6C7A3062614541242B706C7463365A4371386D534551772B4139374B304E37544B386A7072582F774141614B525933456D783400000000000000000000000000000000000000000000000000000000000000", + admin = true, + emailVerification = "VERIFIED", + sendNotifications = true, + locale = Locale.ENGLISH + ) + + val userJimKnopf = User( + id = 0, + name = "Jim Knopf", + url = "photographerUrl", + email = "jim.knopf@example.com", + license = License.CC0_10, + ownPhotos = true, + ) + + val someUser = User( + id = 1, + name = "someuser", + license = License.CC0_10, + email = "someuser@example.com", + ownPhotos = true, + emailVerification = EMAIL_VERIFIED, + ) + + val userNickname = User( + id = 42, + name = "nickname", + license = License.CC0_10, + email = "nickname@example.com", + ownPhotos = true, + anonymous = true, + emailVerification = EMAIL_VERIFIED, + ) + } \ No newline at end of file diff --git a/db-migration/build.gradle.kts b/db-migration/build.gradle.kts new file mode 100644 index 00000000..06b17527 --- /dev/null +++ b/db-migration/build.gradle.kts @@ -0,0 +1,171 @@ +import nu.studer.gradle.jooq.JooqGenerate +import org.flywaydb.core.Flyway +import org.flywaydb.core.api.configuration.FluentConfiguration +import org.jooq.meta.jaxb.ForcedType +import org.jooq.meta.jaxb.Logging +import org.jooq.meta.jaxb.MatcherRule +import org.jooq.meta.jaxb.MatcherTransformType +import org.jooq.meta.jaxb.Matchers +import org.jooq.meta.jaxb.MatchersFieldType +import org.jooq.meta.jaxb.MatchersTableType +import org.jooq.meta.jaxb.Strategy +import org.testcontainers.containers.JdbcDatabaseContainer +import org.testcontainers.containers.PostgreSQLContainer +import org.testcontainers.utility.DockerImageName + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath(libs.testcontainers.postgresql) + classpath(libs.flyway.postgresql) + classpath(libs.postgresql) + } +} + +sourceSets { + main { + kotlin.srcDirs("jooq-src/main") + resources.srcDirs("flyway") + } +} + +jooq { + version.set(dependencyManagement.importedProperties["jooq.version"]) + edition.set(nu.studer.gradle.jooq.JooqEdition.OSS) + configurations { + create("main") { + jooqConfiguration.apply { + logging = Logging.WARN + generator.apply { + name = "org.jooq.codegen.KotlinGenerator" + database.apply { + name = "org.jooq.meta.postgres.PostgresDatabase" + inputSchema = "rsapi" + recordVersionFields = "version" + forcedTypes.apply { + add( + ForcedType().apply { + name = "INSTANT" + includeExpression = ".*" + includeTypes = "TIMESTAMP.*" + }, + ) + } + includes = ".*" + excludes = "flyway_schema_history" + isIncludeExcludeColumns = true + } + generate.apply { + isDeprecated = false + isRecords = true + isImmutablePojos = true + isKotlinNotNullInterfaceAttributes = true + isKotlinNotNullRecordAttributes = true + isKotlinNotNullPojoAttributes = true + isFluentSetters = true + } + target.apply { + packageName = "org.railwaystations.rsapi.adapter.db.jooq" + directory = "jooq-src/main" + } + + val camelCaseWithSuffix = MatcherRule() + .withExpression("$0_Table") + .withTransform( + MatcherTransformType.PASCAL + ) + + strategy = Strategy().withMatchers( + Matchers() + .withFields( + MatchersFieldType() + .withFieldIdentifier( + MatcherRule() + .withTransform(MatcherTransformType.CAMEL) + .withExpression("$0") + ), + ) + .withTables( + MatchersTableType() + .withTableIdentifier( + camelCaseWithSuffix, + ) + .withTableClass( + camelCaseWithSuffix, + ), + ), + ) + } + } + } + } +} + +plugins { + alias(libs.plugins.jooq.plugin) +} + +dependencies { + jooqGenerator(libs.postgresql) + implementation(libs.jooq.codegen) + implementation("org.springframework.boot:spring-boot-starter-jooq") + jooqGenerator("org.slf4j:slf4j-simple:2.0.9") +} + +tasks.named("generateJooq") { + val migrationFilesLocation = "flyway/db/migration" + inputs.files(fileTree(migrationFilesLocation)) + .withPropertyName("migrations") + .withPathSensitivity(PathSensitivity.ABSOLUTE) + + allInputsDeclared.set(true) + outputs.cacheIf { + true + } + + var container: JdbcDatabaseContainer<*>? = null + + val absoluteMigrationsPath = + inputs.files.first { it.path.contains("flyway/db/migration") }.parentFile.parent + + doFirst { + val newContainer = startContainer("postgres:17") + flywayMigrate(newContainer, absoluteMigrationsPath) + modifyJooqConfiguration(this as JooqGenerate, newContainer) + container = newContainer + } + + doLast { + container?.stop() + } +} + +fun startContainer(imageName: String): JdbcDatabaseContainer<*> { + val container = PostgreSQLContainer(DockerImageName.parse(imageName)) + container.start() + return container +} + +fun flywayMigrate(container: JdbcDatabaseContainer<*>, migrationsLocation: String) { + val withPrefix = "filesystem:$migrationsLocation" + + val configuration: FluentConfiguration = Flyway.configure() + .dataSource(container.jdbcUrl, container.username, container.password) + .defaultSchema("rsapi") + .locations(withPrefix) + val flyway: Flyway = configuration.load() + flyway.migrate() +} + +fun modifyJooqConfiguration(jooqGenerate: JooqGenerate, container: JdbcDatabaseContainer<*>) { + val jooqConfigurationField = JooqGenerate::class.java.getDeclaredField("jooqConfiguration") + jooqConfigurationField.isAccessible = true + val jooqConfiguration = jooqConfigurationField[jooqGenerate] as org.jooq.meta.jaxb.Configuration + jooqConfiguration.jdbc.apply { + url = container.jdbcUrl + user = container.username + password = container.password + } +} \ No newline at end of file diff --git a/adapter/src/main/resources/db/migration/V1__initial_schema.sql b/db-migration/flyway/db/migration/V1__initial_schema.sql similarity index 100% rename from adapter/src/main/resources/db/migration/V1__initial_schema.sql rename to db-migration/flyway/db/migration/V1__initial_schema.sql diff --git a/db-migration/flyway/db/migration/V2__sequences_to_bigint.sql b/db-migration/flyway/db/migration/V2__sequences_to_bigint.sql new file mode 100644 index 00000000..dbe694be --- /dev/null +++ b/db-migration/flyway/db/migration/V2__sequences_to_bigint.sql @@ -0,0 +1,10 @@ +ALTER TABLE users ALTER COLUMN id TYPE bigint; + +ALTER TABLE blocked_usernames ALTER COLUMN id TYPE bigint; + +ALTER TABLE inbox ALTER COLUMN id TYPE bigint; +ALTER TABLE inbox ALTER COLUMN photoId TYPE bigint; +ALTER TABLE inbox ALTER COLUMN photographerId TYPE bigint; + +ALTER TABLE photos ALTER COLUMN id TYPE bigint; +ALTER TABLE photos ALTER COLUMN photographerId TYPE bigint; diff --git a/db-migration/flyway/db/migration/V3__fix_timestamps.sql b/db-migration/flyway/db/migration/V3__fix_timestamps.sql new file mode 100644 index 00000000..af2cf489 --- /dev/null +++ b/db-migration/flyway/db/migration/V3__fix_timestamps.sql @@ -0,0 +1,3 @@ +ALTER TABLE inbox ALTER createdAt TYPE timestamptz USING createdAt AT TIME ZONE 'UTC'; +ALTER TABLE blocked_usernames ALTER created_at TYPE timestamptz USING created_at AT TIME ZONE 'UTC'; +ALTER TABLE photos ALTER createdAt TYPE timestamptz USING createdAt::timestamp at time zone 'UTC'; diff --git a/db-migration/flyway/db/migration/V4__singular_table_names.sql b/db-migration/flyway/db/migration/V4__singular_table_names.sql new file mode 100644 index 00000000..6576197f --- /dev/null +++ b/db-migration/flyway/db/migration/V4__singular_table_names.sql @@ -0,0 +1,6 @@ +ALTER TABLE blocked_usernames RENAME TO blocked_username; +ALTER TABLE countries RENAME TO country; +ALTER TABLE users RENAME TO "user"; +ALTER TABLE photos RENAME TO photo; +ALTER TABLE providerapps RENAME TO providerapp; +ALTER TABLE stations RENAME TO station; diff --git a/db-migration/flyway/db/migration/V5__fix_inbox_table.sql b/db-migration/flyway/db/migration/V5__fix_inbox_table.sql new file mode 100644 index 00000000..fc83d51c --- /dev/null +++ b/db-migration/flyway/db/migration/V5__fix_inbox_table.sql @@ -0,0 +1,10 @@ +ALTER TABLE inbox ALTER COLUMN crc32 TYPE BIGINT USING crc32::bigint; + +ALTER TABLE inbox ALTER COLUMN done SET NOT NULL; +ALTER TABLE inbox ALTER COLUMN done DROP DEFAULT; +ALTER TABLE inbox ALTER COLUMN notified SET NOT NULL; +ALTER TABLE inbox ALTER COLUMN notified DROP DEFAULT; +ALTER TABLE inbox ALTER COLUMN posted SET NOT NULL; +ALTER TABLE inbox ALTER COLUMN posted DROP DEFAULT; +ALTER TABLE inbox ALTER COLUMN createdat SET NOT NULL; +ALTER TABLE inbox ALTER COLUMN createdat DROP DEFAULT; diff --git a/db-migration/flyway/db/migration/V6__fix_station_table.sql b/db-migration/flyway/db/migration/V6__fix_station_table.sql new file mode 100644 index 00000000..f74efe58 --- /dev/null +++ b/db-migration/flyway/db/migration/V6__fix_station_table.sql @@ -0,0 +1,2 @@ +ALTER TABLE station ALTER COLUMN active SET NOT NULL; +ALTER TABLE station ALTER COLUMN active DROP DEFAULT; diff --git a/db-migration/flyway/db/migration/V7__not_null.sql b/db-migration/flyway/db/migration/V7__not_null.sql new file mode 100644 index 00000000..70fc55ac --- /dev/null +++ b/db-migration/flyway/db/migration/V7__not_null.sql @@ -0,0 +1,29 @@ +ALTER TABLE country ALTER COLUMN active SET NOT NULL; +ALTER TABLE country ALTER COLUMN active DROP DEFAULT; + +ALTER TABLE blocked_username ALTER COLUMN name SET NOT NULL; +ALTER TABLE blocked_username ALTER COLUMN name DROP DEFAULT; + +ALTER TABLE blocked_username ALTER COLUMN created_at SET NOT NULL; +ALTER TABLE blocked_username ALTER COLUMN created_at DROP DEFAULT; + +ALTER TABLE "user" ALTER COLUMN sendnotifications SET NOT NULL; +ALTER TABLE "user" ALTER COLUMN sendnotifications DROP DEFAULT; + +UPDATE "user" SET admin = false WHERE admin IS NULL; +ALTER TABLE "user" ALTER COLUMN admin SET NOT NULL; +ALTER TABLE "user" ALTER COLUMN admin DROP DEFAULT; + +UPDATE "user" SET locale = 'en' WHERE locale IS NULL; +ALTER TABLE "user" ALTER COLUMN locale SET NOT NULL; +ALTER TABLE "user" ALTER COLUMN locale DROP DEFAULT; + +ALTER TABLE "user" ALTER COLUMN ownphotos SET NOT NULL; +ALTER TABLE "user" ALTER COLUMN ownphotos DROP DEFAULT; + +ALTER TABLE "user" ALTER COLUMN anonymous SET NOT NULL; +ALTER TABLE "user" ALTER COLUMN anonymous DROP DEFAULT; + +UPDATE "user" SET license = 'UNKNOWN' WHERE license IS NULL; +ALTER TABLE "user" ALTER COLUMN license SET NOT NULL; +ALTER TABLE "user" ALTER COLUMN license DROP DEFAULT; diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..d6d78138 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +org.gradle.jvmargs=-Xmx1g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 40a06fd0..4afa773e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,6 @@ springDependency = "1.1.6" springOauth2AuthorizationServer = "1.4.0" openapiGenerator = "7.10.0" kotlin = "2.1.0" -jdbi = "3.47.0" swaggerRequestValidator = "2.44.1" lazysodium = "5.1.4" jna = "5.15.0" @@ -16,11 +15,13 @@ wiremock = "3.0.1" mockk = "1.13.13" springMockk = "4.0.2" greenmail = "2.1.2" +flyway = "10.15.0" +testcontainers = "1.19.8" +jooq = "3.19.10" +jooqPlugin = "9.0" +postgresql = "42.7.4" [libraries] -jdbi3-spring5 = { group = "org.jdbi", name = "jdbi3-spring5", version.ref = "jdbi" } -jdbi3-kotlin = { group = "org.jdbi", name = "jdbi3-kotlin", version.ref = "jdbi" } -jdbi3-kotlin-sqlobject = { group = "org.jdbi", name = "jdbi3-kotlin-sqlobject", version.ref = "jdbi" } swagger-request-validator-core = { group = "com.atlassian.oai", name = "swagger-request-validator-core", version.ref = "swaggerRequestValidator" } swagger-request-validator-spring-webmvc = { group = "com.atlassian.oai", name = "swagger-request-validator-spring-webmvc", version.ref = "swaggerRequestValidator" } swagger-request-validator-mockmvc = { group = "com.atlassian.oai", name = "swagger-request-validator-mockmvc", version.ref = "swaggerRequestValidator" } @@ -35,6 +36,11 @@ wiremock-jre8-standalone = { group = "com.github.tomakehurst", name = "wiremock- mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } springmockk = { group = "com.ninja-squad", name = "springmockk", version.ref = "springMockk" } greenmail = { group = "com.icegreen", name = "greenmail", version.ref = "greenmail" } +flyway-postgresql = { module = "org.flywaydb:flyway-database-postgresql", version.ref = "flyway" } +jooq-codegen = { module = "org.jooq:jooq-codegen", version.ref = "jooq" } +jooq-core = { module = "org.jooq:jooq", version.ref = "jooq" } +postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" } +testcontainers-postgresql = { module = "org.testcontainers:postgresql", version.ref = "testcontainers" } [plugins] spring-boot = { id = "org.springframework.boot", version.ref = "springBoot" } @@ -42,4 +48,6 @@ spring-dependency-management = { id = "io.spring.dependency-management", version openapi-generator = { id = "org.openapi.generator", version.ref = "openapiGenerator" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" } -kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } \ No newline at end of file +kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } +flyway = { id = "org.flywaydb.flyway", version.ref = "flyway" } +jooq-plugin = { id = "nu.studer.jooq", version.ref = "jooqPlugin" } diff --git a/service/build.gradle.kts b/service/build.gradle.kts index f409fc74..9976985c 100644 --- a/service/build.gradle.kts +++ b/service/build.gradle.kts @@ -17,9 +17,11 @@ tasks { dependencies { implementation(project("::core")) implementation(project("::openapi")) + implementation(project("::db-migration")) implementation(project("::adapter")) implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("org.springframework.boot:spring-boot-starter-jdbc") + implementation("org.springframework.boot:spring-boot-starter-jooq") implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-validation") implementation(libs.spring.security.oauth2.authorization.server) @@ -27,6 +29,7 @@ dependencies { implementation("org.flywaydb:flyway-database-postgresql") implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation(libs.jooq.core) kapt("org.springframework.boot:spring-boot-configuration-processor") diff --git a/service/src/main/resources/application.yaml b/service/src/main/resources/application.yaml index 8637b9e0..7b86fa9e 100644 --- a/service/src/main/resources/application.yaml +++ b/service/src/main/resources/application.yaml @@ -6,10 +6,11 @@ spring: driver-class-name: org.postgresql.Driver username: rsapi password: rsapi - url: jdbc:postgresql://localhost:5432/rsapi + url: jdbc:postgresql://localhost:5432/rsapi?currentSchema=rsapi flyway: baseline-on-migrate: true baseline-version: 1 + schemas: rsapi servlet: multipart: max-file-size: 20MB diff --git a/service/src/test/kotlin/org/railwaystations/rsapi/ProfileIntegrationTest.kt b/service/src/test/kotlin/org/railwaystations/rsapi/ProfileIntegrationTest.kt index f99b680d..74c136db 100644 --- a/service/src/test/kotlin/org/railwaystations/rsapi/ProfileIntegrationTest.kt +++ b/service/src/test/kotlin/org/railwaystations/rsapi/ProfileIntegrationTest.kt @@ -8,7 +8,7 @@ import jakarta.validation.constraints.NotNull import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.railwaystations.rsapi.adapter.db.AbstractPostgreSqlTest -import org.railwaystations.rsapi.adapter.db.UserDao +import org.railwaystations.rsapi.adapter.db.UserAdapter import org.railwaystations.rsapi.core.model.License import org.railwaystations.rsapi.core.model.User import org.railwaystations.rsapi.core.ports.outbound.MailerPort @@ -19,7 +19,12 @@ import org.springframework.boot.test.web.client.exchange import org.springframework.boot.test.web.client.getForEntity import org.springframework.boot.test.web.client.postForEntity import org.springframework.boot.test.web.server.LocalServerPort -import org.springframework.http.* +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity import org.springframework.http.client.SimpleClientHttpRequestFactory import org.springframework.security.oauth2.core.OAuth2AccessToken import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse @@ -34,7 +39,6 @@ import java.security.MessageDigest import java.security.SecureRandom import java.util.* import java.util.regex.Pattern -import kotlin.Throws @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, @@ -52,7 +56,7 @@ internal class ProfileIntegrationTest : AbstractPostgreSqlTest() { private lateinit var restTemplate: TestRestTemplate @Autowired - private lateinit var userDao: UserDao + private lateinit var userAdapter: UserAdapter @MockkBean(relaxed = true) private lateinit var mailerPort: MailerPort @@ -482,7 +486,7 @@ internal class ProfileIntegrationTest : AbstractPostgreSqlTest() { // then assertThat(responseDelete.statusCode).isEqualTo(HttpStatus.NO_CONTENT) - val user = userDao.findById(22) + val user = userAdapter.findById(22) assertThat(user).usingRecursiveComparison().isEqualTo( User( id = 22, diff --git a/service/src/test/kotlin/org/railwaystations/rsapi/RsapiIntegrationTest.kt b/service/src/test/kotlin/org/railwaystations/rsapi/RsapiIntegrationTest.kt index 4772a694..36b0597b 100644 --- a/service/src/test/kotlin/org/railwaystations/rsapi/RsapiIntegrationTest.kt +++ b/service/src/test/kotlin/org/railwaystations/rsapi/RsapiIntegrationTest.kt @@ -15,10 +15,14 @@ import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.CsvSource import org.junit.jupiter.params.provider.ValueSource import org.railwaystations.rsapi.adapter.db.AbstractPostgreSqlTest -import org.railwaystations.rsapi.adapter.db.PhotoDao +import org.railwaystations.rsapi.adapter.db.PhotoAdapter import org.railwaystations.rsapi.adapter.photostorage.WorkDir import org.railwaystations.rsapi.adapter.web.controller.DeprecatedApiController.StationDto -import org.railwaystations.rsapi.adapter.web.model.* +import org.railwaystations.rsapi.adapter.web.model.PhotoDto +import org.railwaystations.rsapi.adapter.web.model.PhotoLicenseDto +import org.railwaystations.rsapi.adapter.web.model.PhotoStationDto +import org.railwaystations.rsapi.adapter.web.model.PhotoStationsDto +import org.railwaystations.rsapi.adapter.web.model.PhotographerDto import org.railwaystations.rsapi.core.model.License import org.railwaystations.rsapi.core.model.Photo import org.railwaystations.rsapi.core.model.Station @@ -34,7 +38,11 @@ import org.springframework.boot.test.web.server.LocalServerPort import org.springframework.context.annotation.Bean import org.springframework.core.io.Resource import org.springframework.core.io.support.EncodedResource -import org.springframework.http.* +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity import org.springframework.test.context.ActiveProfiles import org.springframework.web.servlet.config.annotation.InterceptorRegistry import org.springframework.web.servlet.config.annotation.WebMvcConfigurer @@ -48,7 +56,6 @@ import java.time.Instant import java.time.temporal.ChronoUnit import java.util.* import javax.imageio.ImageIO -import kotlin.Throws private val IMAGE: ByteArray = Base64.getDecoder() .decode("/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////wgALCAABAAEBAREA/8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQABPxA=") @@ -72,7 +79,7 @@ internal class RsapiIntegrationTest : AbstractPostgreSqlTest() { private lateinit var workDir: WorkDir @Autowired - private lateinit var photoDao: PhotoDao + private lateinit var photoAdapter: PhotoAdapter @Test fun stationsAllCountriesIsDefaultingToDE() { @@ -334,7 +341,7 @@ internal class RsapiIntegrationTest : AbstractPostgreSqlTest() { } private fun insertPhotoForDe7051(urlPath: String, createdAt: Instant) { - photoDao.insert( + photoAdapter.insert( Photo( stationKey = Station.Key("de", "7051"), primary = true, @@ -536,8 +543,7 @@ internal class RsapiIntegrationTest : AbstractPostgreSqlTest() { val userInboxProcessedJson = loadUser10UserInboxAsJson()[0] assertThat(userInboxProcessedJson["id"].asInt()).isEqualTo(uploadId) assertThat(userInboxProcessedJson["state"].asText()).isEqualTo("REVIEW") - assertThat(userInboxProcessedJson["inboxUrl"].asText().endsWith("/inbox/processed/$filename")) - .isTrue() + assertThat(userInboxProcessedJson["inboxUrl"].asText().endsWith("/inbox/processed/$filename")).isTrue() // send import command sendInboxCommand( diff --git a/settings.gradle b/settings.gradle index 98e81d45..a63faecb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,4 +7,4 @@ pluginManagement { rootProject.name = 'rsapi' -include(":openapi", ":adapter", ":service", ":core") +include(":db-migration", ":openapi", ":adapter", ":service", ":core")