Skip to content

Commit

Permalink
feat(sql): CRUD Tests for SqlStorageService
Browse files Browse the repository at this point in the history
  • Loading branch information
ajordens committed Jul 8, 2019
1 parent e3a725a commit 1ed13dd
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 7 deletions.
18 changes: 12 additions & 6 deletions front50-sql/front50-sql.gradle
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
apply from: "$rootDir/gradle/kotlin.gradle"

dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-common:1.2.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.2.1"

implementation "com.netflix.spinnaker.front50:front50-core:${front50Version}"

implementation "com.netflix.spinnaker.kork:kork-exceptions"
implementation "com.netflix.spinnaker.kork:kork-sql"
implementation "com.netflix.spinnaker.kork:kork-exceptions"

implementation "org.jooq:jooq:3.11.11"
implementation "io.strikt:strikt-core"
implementation "com.netflix.hystrix:hystrix-core"
implementation "io.github.resilience4j:resilience4j-retry"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.2.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-common:1.2.1"

runtime "mysql:mysql-connector-java:8.0.12"
runtimeOnly "mysql:mysql-connector-java"

testImplementation "dev.minutest:minutest"
testImplementation "org.testcontainers:mysql"
testImplementation "org.junit.jupiter:junit-jupiter-api"
testImplementation "com.netflix.spinnaker.kork:kork-sql-test"
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.netflix.spinnaker.front50.model

import com.fasterxml.jackson.databind.ObjectMapper
import com.netflix.spectator.api.Registry
import com.netflix.spinnaker.front50.exception.NotFoundException
import com.netflix.spinnaker.security.AuthenticatedRequest
import org.jooq.DSLContext
import org.jooq.impl.DSL
Expand Down Expand Up @@ -71,7 +72,7 @@ class SqlStorageService(
.from(definitionsByType[objectType]!!.tableName)
.where(field("id", String::class.java).eq(objectKey))
.fetchOne()
}
} ?: throw NotFoundException("Object not found (key: $objectKey)")

return objectMapper.readValue(result.get(field("body", String::class.java)), objectType.clazz as Class<T>)
}
Expand Down Expand Up @@ -137,6 +138,7 @@ class SqlStorageService(
}

override fun listObjectKeys(objectType: ObjectType): Map<String, Long> {
val startTime = System.currentTimeMillis()
val resultSet = jooq.withRetry(sqlRetryProperties.reads) { ctx ->
ctx
.select(
Expand All @@ -155,6 +157,12 @@ class SqlStorageService(
objectKeys.put(resultSet.getString(1), resultSet.getLong(2))
}

log.debug("Took {}ms to fetch {} object keys for {}",
System.currentTimeMillis() - startTime,
objectKeys.size,
objectType
)

return objectKeys
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright 2019 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.spinnaker.front50.model

import com.fasterxml.jackson.databind.ObjectMapper
import com.netflix.spectator.api.NoopRegistry
import com.netflix.spinnaker.front50.exception.NotFoundException
import com.netflix.spinnaker.front50.model.application.Application
import com.netflix.spinnaker.kork.sql.config.SqlRetryProperties
import dev.minutest.junit.JUnit5Minutests
import dev.minutest.rootContext
import org.jooq.SQLDialect
import org.junit.jupiter.api.AfterAll
import strikt.api.expectThat
import strikt.api.expectThrows
import strikt.assertions.isEqualTo
import java.time.Clock

internal object SqlStorageServiceTests : JUnit5Minutests {
private val jooq = initDatabase(
"jdbc:tc:mysql:5.7.22://somehostname:someport/databasename",
SQLDialect.MYSQL_5_7
)

private val sqlStorageService = SqlStorageService(
ObjectMapper(),
NoopRegistry(),
jooq,
Clock.systemDefaultZone(),
SqlRetryProperties()
)

fun tests() = rootContext {
after {
jooq.flushAll()
}

context("Application CRUD") {
test("throws NotFoundException when application does not exist") {
expectThrows<NotFoundException> {
sqlStorageService.loadObject<Application>(ObjectType.APPLICATION, "application001")
}
}

test("creates an application") {
sqlStorageService.storeObject(
ObjectType.APPLICATION,
"application001",
Application().apply {
name = "application001"
description = "my first application!"
updateTs = "100"
}
)

val application = sqlStorageService.loadObject<Application>(ObjectType.APPLICATION, "application001")
expectThat(application.description).isEqualTo("my first application!")
}

test("deletes an application") {
sqlStorageService.deleteObject(ObjectType.APPLICATION, "application001")

expectThrows<NotFoundException> {
sqlStorageService.loadObject<Application>(ObjectType.APPLICATION, "application001")
}
}
}
}

@JvmStatic
@AfterAll
fun shutdown() {
jooq.close()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Copyright 2019 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.spinnaker.front50.model


import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import liquibase.Liquibase
import liquibase.configuration.ConfigurationContainer
import liquibase.configuration.GlobalConfiguration
import liquibase.configuration.LiquibaseConfiguration
import liquibase.database.DatabaseFactory
import liquibase.database.jvm.JdbcConnection
import liquibase.exception.DatabaseException
import liquibase.exception.LiquibaseException
import liquibase.resource.ClassLoaderResourceAccessor
import org.jooq.DSLContext
import org.jooq.SQLDialect
import org.jooq.Schema
import org.jooq.conf.RenderNameStyle.AS_IS
import org.jooq.impl.DSL.currentSchema
import org.jooq.impl.DataSourceConnectionProvider
import org.jooq.impl.DefaultConfiguration
import org.jooq.impl.DefaultDSLContext
import org.slf4j.LoggerFactory
import java.sql.SQLException

internal fun initDatabase(jdbcUrl: String, sqlDialect: SQLDialect): DSLContext {
val dataSource = HikariDataSource(
HikariConfig().also {
it.jdbcUrl = jdbcUrl
it.maximumPoolSize = 5
}
)

val config = DefaultConfiguration().also {
it.set(DataSourceConnectionProvider(dataSource))
it.setSQLDialect(sqlDialect)
it.settings().withRenderNameStyle(AS_IS)
}

try {
Liquibase(
"db/changelog-master.yml",
ClassLoaderResourceAccessor(),
DatabaseFactory
.getInstance()
.findCorrectDatabaseImplementation(JdbcConnection(dataSource.connection))
)
.update("test")
} catch (e: DatabaseException) {
log.error("Caught exception running liquibase: {}", e.message)
throw DatabaseInitializationFailed(e)
} catch (e: SQLException) {
log.error("Caught exception running liquibase: {}", e.message)
throw DatabaseInitializationFailed(e)
} catch (e: LiquibaseException) {
log.error("Caught exception running liquibase: {}", e.message)
throw DatabaseInitializationFailed(e)
}

return DefaultDSLContext(config)
}

internal class DatabaseInitializationFailed(cause: Throwable) : RuntimeException(cause)

internal fun DSLContext.flushAll() {
val schema = select(currentSchema())
.fetch()
.getValue(0, 0)
with(LiquibaseConfiguration.getInstance().getConfiguration<GlobalConfiguration>()) {
meta()
.schemas
.filter { it.name == schema }
.flatMap(Schema::getTables)
.filterNot {
it.name in setOf(
databaseChangeLogTableName,
databaseChangeLogLockTableName
)
}
.forEach {
truncate(it).execute()
}
}
}

private inline fun <reified T : ConfigurationContainer> LiquibaseConfiguration.getConfiguration(): T =
getConfiguration(T::class.java)

private val log by lazy { LoggerFactory.getLogger(::initDatabase.javaClass) }

0 comments on commit 1ed13dd

Please sign in to comment.