refactor(build, dependencies): introduce SQLDelight WebWorker setup, update Spring Boot dependencies, and align versions

Configured `sqlite.worker.js` for OPFS-backed SQLite WASM operations in the frontend build pipeline. Added new Spring Boot dependency bundles including secure service configurations. Integrated updated database utilities with enhanced error handling. Removed outdated circuit breaker tests and replaced them with modern unit and integration test setups.
This commit is contained in:
Stefan Mogeritsch 2026-01-12 17:05:44 +01:00
parent 2f8529156a
commit 32e43b8fb0
23 changed files with 402 additions and 526 deletions

View File

@ -22,6 +22,18 @@ dependencies {
implementation(platform(projects.platform.platformBom)) implementation(platform(projects.platform.platformBom))
// Stellt gemeinsame Abhängigkeiten bereit. // Stellt gemeinsame Abhängigkeiten bereit.
implementation(projects.platform.platformDependencies) implementation(projects.platform.platformDependencies)
// Spring Boot / Spring Framework APIs used directly in this module
// (e.g. ConditionalOnMissingBean)
implementation("org.springframework.boot:spring-boot-autoconfigure")
implementation("org.springframework.boot:spring-boot")
// Spring Kafka (ReactiveKafkaProducerTemplate, etc.)
implementation(libs.spring.kafka)
// Jakarta annotations used by Spring / configuration classes
implementation(libs.jakarta.annotation.api)
// Baut auf der zentralen Kafka-Konfiguration auf und erbt deren Abhängigkeiten. // Baut auf der zentralen Kafka-Konfiguration auf und erbt deren Abhängigkeiten.
implementation(projects.backend.infrastructure.messaging.messagingConfig) implementation(projects.backend.infrastructure.messaging.messagingConfig)
// Fügt die reaktive Kafka-Implementierung hinzu (Project Reactor). // Fügt die reaktive Kafka-Implementierung hinzu (Project Reactor).

View File

@ -1,6 +1,18 @@
plugins { plugins {
alias(libs.plugins.kotlinJvm) alias(libs.plugins.kotlinJvm)
alias(libs.plugins.kotlinJpa) alias(libs.plugins.kotlinJpa)
alias(libs.plugins.spring.boot) // Spring Boot Plugin hinzufügen
alias(libs.plugins.spring.dependencyManagement) // Dependency Management für Spring
}
// Library module: do not create an executable Spring Boot jar here.
tasks.bootJar {
enabled = false
}
// Ensure a regular jar is produced instead.
tasks.jar {
enabled = true
} }
dependencies { dependencies {
@ -8,6 +20,9 @@ dependencies {
implementation(projects.core.coreDomain) implementation(projects.core.coreDomain)
implementation(projects.platform.platformDependencies) implementation(projects.platform.platformDependencies)
// Spring Boot Database dependencies
implementation(libs.bundles.database.complete)
// Exposed // Exposed
implementation(libs.exposed.core) implementation(libs.exposed.core)
implementation(libs.exposed.jdbc) implementation(libs.exposed.jdbc)

View File

@ -3,9 +3,16 @@ package at.mocode.backend.infrastructure.persistence
import at.mocode.core.domain.model.ErrorCodes import at.mocode.core.domain.model.ErrorCodes
import at.mocode.core.domain.model.ErrorDto import at.mocode.core.domain.model.ErrorDto
import at.mocode.core.domain.model.PagedResponse import at.mocode.core.domain.model.PagedResponse
import org.jetbrains.exposed.sql.* import at.mocode.core.utils.Result
import org.jetbrains.exposed.sql.statements.BatchInsertStatement import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.core.Column
import org.jetbrains.exposed.v1.core.statements.BatchInsertStatement
import org.jetbrains.exposed.v1.jdbc.Database
import org.jetbrains.exposed.v1.jdbc.JdbcTransaction
import org.jetbrains.exposed.v1.jdbc.Query
import org.jetbrains.exposed.v1.jdbc.batchInsert
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import java.sql.SQLException import java.sql.SQLException
import java.sql.SQLTimeoutException import java.sql.SQLTimeoutException
@ -18,7 +25,7 @@ import java.sql.SQLTimeoutException
inline fun <T> transactionResult( inline fun <T> transactionResult(
database: Database? = null, database: Database? = null,
crossinline block: Transaction.() -> T crossinline block: JdbcTransaction.() -> T
): Result<T> { ): Result<T> {
return try { return try {
val result = transaction(database) { block() } val result = transaction(database) { block() }
@ -60,12 +67,12 @@ inline fun <T> transactionResult(
inline fun <T> writeTransaction( inline fun <T> writeTransaction(
database: Database? = null, database: Database? = null,
crossinline block: Transaction.() -> T crossinline block: JdbcTransaction.() -> T
): Result<T> = transactionResult(database, block) ): Result<T> = transactionResult(database, block)
inline fun <T> readTransaction( inline fun <T> readTransaction(
database: Database? = null, database: Database? = null,
crossinline block: Transaction.() -> T crossinline block: JdbcTransaction.() -> T
): Result<T> = transactionResult(database, block) ): Result<T> = transactionResult(database, block)
fun Query.paginate(page: Int, size: Int): Query { fun Query.paginate(page: Int, size: Int): Query {
@ -123,18 +130,17 @@ fun <T> Query.toPagedResponse(
object DatabaseUtils { object DatabaseUtils {
fun tableExists(tableName: String, database: Database? = null): Boolean { fun tableExists(tableName: String, database: Database? = null): Boolean {
return try { return transactionResult(database) {
transaction(database) {
// Postgres-spezifischer, robuster Ansatz über to_regclass // Postgres-spezifischer, robuster Ansatz über to_regclass
val valid = tableName.trim() val valid = tableName.trim()
if (!valid.matches(Regex("^[A-Za-z_][A-Za-z0-9_]*$"))) return@transaction false if (!valid.matches(Regex("^[A-Za-z_][A-Za-z0-9_]*$"))) return@transactionResult false
exec("SELECT to_regclass('$valid')") { rs -> exec("SELECT to_regclass('$valid')") { rs ->
if (rs.next()) rs.getString(1) else null if (rs.next()) rs.getString(1) else null
} != null } != null
} }.fold(
} catch (e: Exception) { onSuccess = { it },
false onFailure = { false }
} )
} }
@JvmName("createIndexIfNotExistsArray") @JvmName("createIndexIfNotExistsArray")

View File

@ -16,16 +16,18 @@ dependencies {
implementation(projects.backend.services.entries.entriesApi) implementation(projects.backend.services.entries.entriesApi)
implementation(projects.backend.infrastructure.monitoring.monitoringClient) implementation(projects.backend.infrastructure.monitoring.monitoringClient)
implementation(libs.bundles.spring.boot.service.complete) // Standard dependencies for a secure microservice (centralized bundle)
implementation(libs.bundles.spring.boot.secure.service)
// Common service extras
implementation(libs.spring.boot.starter.validation)
implementation(libs.spring.boot.starter.json)
implementation(libs.postgresql.driver) implementation(libs.postgresql.driver)
implementation(libs.spring.boot.starter.web)
// KORREKTUR: Jackson Bundle aufgelöst, da Accessor fehlschlägt // KORREKTUR: Jackson Bundle aufgelöst, da Accessor fehlschlägt
implementation(libs.jackson.module.kotlin) implementation(libs.jackson.module.kotlin)
implementation(libs.jackson.datatype.jsr310) implementation(libs.jackson.datatype.jsr310)
implementation(libs.kotlin.reflect) implementation(libs.kotlin.reflect)
implementation(libs.spring.cloud.starter.consul.discovery)
implementation(libs.caffeine) implementation(libs.caffeine)
implementation(libs.spring.web) implementation(libs.spring.web)
@ -39,5 +41,4 @@ dependencies {
testImplementation(projects.platform.platformTesting) testImplementation(projects.platform.platformTesting)
testImplementation(libs.bundles.testing.jvm) testImplementation(libs.bundles.testing.jvm)
testImplementation(libs.spring.boot.starter.test) testImplementation(libs.spring.boot.starter.test)
testImplementation(libs.spring.boot.starter.web)
} }

View File

@ -15,17 +15,19 @@ kotlin {
dependencies { dependencies {
// === Project Dependencies === // === Project Dependencies ===
implementation(projects.contracts.pingApi) implementation(projects.contracts.pingApi)
// Our central BOM for consistent versions
implementation(platform(projects.platform.platformBom))
implementation(projects.platform.platformDependencies) implementation(projects.platform.platformDependencies)
// NEU: Zugriff auf die verschobenen DatabaseUtils // NEU: Zugriff auf die verschobenen DatabaseUtils
implementation(projects.backend.infrastructure.persistence) implementation(projects.backend.infrastructure.persistence)
// === Spring Boot & Cloud === // === Spring Boot & Cloud ===
implementation(libs.bundles.spring.boot.service.complete) // Standard dependencies for a secure microservice
// WICHTIG: Da wir JPA (blockierend) nutzen, brauchen wir Spring MVC (nicht WebFlux) implementation(libs.bundles.spring.boot.secure.service)
implementation(libs.spring.boot.starter.web) // Common service extras
implementation(libs.spring.boot.starter.validation)
// Service Discovery implementation(libs.spring.boot.starter.json)
implementation(libs.spring.cloud.starter.consul.discovery)
// === Database & Persistence === // === Database & Persistence ===
implementation(libs.bundles.database.complete) implementation(libs.bundles.database.complete)
@ -37,8 +39,13 @@ dependencies {
// === Testing === // === Testing ===
testImplementation(libs.bundles.testing.jvm) testImplementation(libs.bundles.testing.jvm)
testImplementation(libs.spring.boot.starter.test)
testImplementation(libs.spring.security.test)
} }
tasks.test { tasks.test {
useJUnitPlatform() useJUnitPlatform()
testLogging {
showStandardStreams = true
}
} }

View File

@ -17,7 +17,8 @@ import kotlin.random.Random
* Nutzt den Application Port (PingUseCase). * Nutzt den Application Port (PingUseCase).
*/ */
@RestController @RestController
@CrossOrigin(allowedHeaders = ["*"], allowCredentials = "true") // Spring requires using `originPatterns` (not wildcard `origins`) when credentials are enabled.
@CrossOrigin(allowedHeaders = ["*"], allowCredentials = "true", originPatterns = ["*"])
class PingController( class PingController(
private val pingUseCase: PingUseCase private val pingUseCase: PingUseCase
) : PingApi { ) : PingApi {

View File

@ -1,246 +1,59 @@
package at.mocode.ping.service package at.mocode.ping.service
import io.github.resilience4j.circuitbreaker.CircuitBreaker import at.mocode.ping.application.PingUseCase
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry import at.mocode.ping.domain.Ping
import at.mocode.ping.infrastructure.web.PingController
import io.mockk.every
import io.mockk.mockk
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.boot.test.web.client.TestRestTemplate import org.springframework.boot.test.context.TestConfiguration
import org.springframework.boot.test.web.server.LocalServerPort import org.springframework.context.annotation.Bean
import org.springframework.http.HttpStatus import org.springframework.context.annotation.Import
import org.springframework.test.context.ActiveProfiles import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import java.time.Instant
/** /**
* Integration tests for PingController * Lightweight Spring MVC integration test (no full application context / datasource).
* Tests REST endpoints with circuit breaker functionality using TestRestTemplate
*/ */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @WebMvcTest(
@ActiveProfiles("test") controllers = [PingController::class],
excludeAutoConfiguration = [
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration::class,
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration::class
]
)
@Import(PingControllerIntegrationTest.TestConfig::class)
class PingControllerIntegrationTest { class PingControllerIntegrationTest {
@LocalServerPort
private var port: Int = 0
@Autowired @Autowired
private lateinit var restTemplate: TestRestTemplate private lateinit var mockMvc: MockMvc
@Autowired @TestConfiguration
private lateinit var circuitBreakerRegistry: CircuitBreakerRegistry class TestConfig {
@Bean
private val logger = LoggerFactory.getLogger(PingControllerIntegrationTest::class.java) fun pingUseCase(): PingUseCase = mockk(relaxed = true)
private lateinit var circuitBreaker: CircuitBreaker
@BeforeEach
fun setUp() {
circuitBreaker = circuitBreakerRegistry.circuitBreaker(PingServiceCircuitBreaker.PING_CIRCUIT_BREAKER)
// Reset circuit breaker state before each test
circuitBreaker.reset()
}
private fun getUrl(endpoint: String) = "http://localhost:$port$endpoint"
@Test
fun `should return basic ping response from standard endpoint`() {
// When
val response = restTemplate.getForEntity(getUrl("/ping/simple"), Map::class.java)
// Then
assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
assertThat(response.body).isNotNull
assertThat(response.body!!["status"]).isEqualTo("pong")
logger.info("Standard ping endpoint response: {}", response.body)
} }
@Test @Test
fun `should return enhanced ping response when circuit breaker is closed`() { fun `should start MVC slice and serve endpoints`() {
// Given // Just verify the MVC wiring starts and endpoints respond 200
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.CLOSED) mockMvc.perform(get("/ping/health")).andExpect(status().isOk)
// When // For endpoints that require the use-case, the relaxed mock is sufficient,
val response = restTemplate.getForEntity(getUrl("/ping/enhanced"), Map::class.java) // but we still provide deterministic ping data.
val useCase = TestConfig().pingUseCase()
// Then every { useCase.executePing(any()) } returns Ping(
assertThat(response.statusCode).isEqualTo(HttpStatus.OK) message = "Simple Ping",
assertThat(response.body).isNotNull timestamp = Instant.parse("2023-10-01T10:00:00Z")
val body = response.body!!
assertThat(body["status"]).isEqualTo("pong")
assertThat(body["service"]).isEqualTo("ping-service")
assertThat(body["circuitBreakerState"]).isEqualTo("CLOSED")
assertThat(body["timestamp"]).isNotNull()
logger.info("Enhanced ping response: {}", body)
}
@Test
fun `should return enhanced ping response without simulation`() {
// When
val response = restTemplate.getForEntity(getUrl("/ping/enhanced?simulate=false"), Map::class.java)
// Then
assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
assertThat(response.body).isNotNull
val body = response.body!!
assertThat(body["status"]).isEqualTo("pong")
assertThat(body["service"]).isEqualTo("ping-service")
assertThat(body["circuitBreakerState"]).isEqualTo("CLOSED")
logger.info("Enhanced ping without simulation: {}", body)
}
@Test
fun `should handle failure simulation in enhanced ping endpoint`() {
// Multiple calls to potentially trigger failures due to random simulation
repeat(3) { i ->
val response = restTemplate.getForEntity(getUrl("/ping/enhanced?simulate=true"), Map::class.java)
assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
assertThat(response.body).isNotNull
val body = response.body!!
logger.info("Attempt {}: Response status = {}, Circuit breaker state = {}",
i + 1, body["status"], circuitBreaker.state)
// Response should be either success or fallback
assertThat(body["status"]).isIn("pong", "fallback")
}
}
@Test
fun `should return health check response when circuit breaker is closed`() {
// Given
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.CLOSED)
// When
val response = restTemplate.getForEntity(getUrl("/ping/health"), Map::class.java)
// Then
assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
assertThat(response.body).isNotNull
val body = response.body!!
assertThat(body["status"]).isEqualTo("pong")
assertThat(body["service"]).isEqualTo("ping-service")
assertThat(body["healthy"]).isEqualTo(true)
assertThat(body["timestamp"]).isNotNull()
logger.info("Health check response: {}", body)
}
@Test
fun `should return fallback health check when circuit breaker is open`() {
// Given - manually open circuit breaker
circuitBreaker.transitionToOpenState()
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.OPEN)
// When
val response = restTemplate.getForEntity(getUrl("/ping/health"), Map::class.java)
// Then
assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
assertThat(response.body).isNotNull
val body = response.body!!
assertThat(body["status"]).isEqualTo("down")
assertThat(body["service"]).isEqualTo("ping-service")
assertThat(body["healthy"]).isEqualTo(false)
logger.info("Fallback health check response: {}", body)
}
@Test
fun `should handle multiple rapid requests correctly`() {
// Execute multiple rapid requests
val results = mutableListOf<Map<String, Any>>()
repeat(5) { i ->
val response = restTemplate.getForEntity(getUrl("/ping/enhanced"), Map::class.java)
assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
assertThat(response.body).isNotNull
@Suppress("UNCHECKED_CAST")
val body = response.body as Map<String, Any>
results.add(body)
logger.info("Rapid request {}: status = {}", i + 1, body["status"])
}
// All should be successful since we're not simulating failures
results.forEach { response ->
assertThat(response["status"]).isEqualTo("pong")
assertThat(response["service"]).isEqualTo("ping-service")
}
}
@Test
fun `should maintain circuit breaker state across requests`() {
// Given - manually open circuit breaker
circuitBreaker.transitionToOpenState()
// When - make multiple requests
repeat(3) { i ->
val response = restTemplate.getForEntity(getUrl("/ping/enhanced"), Map::class.java)
assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
assertThat(response.body).isNotNull
val body = response.body!!
// All should return fallback responses while circuit breaker is open
assertThat(body["status"]).isEqualTo("fallback")
assertThat(body["circuitBreakerState"]).isEqualTo("OPEN")
logger.info("Request {} with OPEN circuit breaker: {}", i + 1, body["status"])
}
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.OPEN)
}
@Test
fun `should test all existing endpoints return valid responses`() {
val endpoints = listOf(
"/ping/simple",
"/ping/enhanced",
"/ping/health"
) )
endpoints.forEach { endpoint -> // Note: we don't assert the full JSON here (covered by PingControllerTest)
val response = restTemplate.getForEntity(getUrl(endpoint), Map::class.java) val result = mockMvc.perform(get("/ping/simple")).andReturn()
assertThat(result.response.status).isEqualTo(200)
assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
assertThat(response.body).isNotNull()
assertThat(response.body!!).isNotEmpty()
logger.info("Endpoint {} returned valid response: {}", endpoint, response.body)
}
}
@Test
fun `should track circuit breaker metrics after calls`() {
// Given
val initialMetrics = circuitBreaker.metrics
logger.info("Initial metrics - Calls: {}, Failures: {}",
initialMetrics.numberOfBufferedCalls, initialMetrics.numberOfFailedCalls)
// When - execute some calls
repeat(3) {
restTemplate.getForEntity(getUrl("/ping/enhanced"), Map::class.java)
}
// Then
val newMetrics = circuitBreaker.metrics
assertThat(newMetrics.numberOfBufferedCalls).isGreaterThanOrEqualTo(3)
logger.info("Updated metrics - Calls: {}, Failure rate: {}%, Successful: {}, Failed: {}",
newMetrics.numberOfBufferedCalls,
newMetrics.failureRate,
newMetrics.numberOfSuccessfulCalls,
newMetrics.numberOfFailedCalls)
} }
} }

View File

@ -1,20 +1,28 @@
package at.mocode.ping.service package at.mocode.ping.service
import at.mocode.ping.api.EnhancedPingResponse import at.mocode.ping.domain.Ping
import at.mocode.ping.api.HealthResponse import at.mocode.ping.infrastructure.web.PingController
import at.mocode.ping.application.PingUseCase
import com.fasterxml.jackson.databind.ObjectMapper
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.boot.test.context.TestConfiguration import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Import import org.springframework.context.annotation.Import
import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.MvcResult
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.* import org.springframework.test.web.servlet.result.MockMvcResultMatchers.request
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import java.time.Instant
/** /**
* Unit tests for PingController * Unit tests for PingController
@ -28,109 +36,102 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
] ]
) )
@Import(PingControllerTest.TestConfig::class) @Import(PingControllerTest.TestConfig::class)
@AutoConfigureMockMvc
class PingControllerTest { class PingControllerTest {
@Autowired @Autowired
private lateinit var mockMvc: MockMvc private lateinit var mockMvc: MockMvc
@Autowired @Autowired
private lateinit var pingService: PingServiceCircuitBreaker private lateinit var pingUseCase: PingUseCase
@Autowired
private lateinit var objectMapper: ObjectMapper
@TestConfiguration @TestConfiguration
class TestConfig { class TestConfig {
@Bean @Bean
fun pingServiceCircuitBreaker(): PingServiceCircuitBreaker = mockk(relaxed = true) fun pingUseCase(): PingUseCase = mockk(relaxed = true)
} }
@BeforeEach @BeforeEach
fun setUp() { fun setUp() {
// Reset mocks before each test // Reset mocks before each test
io.mockk.clearMocks(pingService) io.mockk.clearMocks(pingUseCase)
} }
@Test @Test
fun `should return simple ping response`() { fun `should return simple ping response`() {
// Given
every { pingUseCase.executePing(any()) } returns Ping(
message = "Simple Ping",
timestamp = Instant.parse("2023-10-01T10:00:00Z")
)
// When & Then // When & Then
mockMvc.perform(get("/ping/simple")) val mvcResult: MvcResult = mockMvc.perform(get("/ping/simple"))
.andExpect(request().asyncStarted())
.andReturn()
val result = mockMvc.perform(asyncDispatch(mvcResult))
.andExpect(status().isOk) .andExpect(status().isOk)
.andReturn()
// In some environments the JSONPath matcher fails to parse the response body.
// We still validate the serialized output contains the expected fields.
val body = result.response.contentAsString
System.out.println("[DEBUG_LOG] /ping/simple response status=${result.response.status} contentType=${result.response.contentType} body=$body")
val json = objectMapper.readTree(body)
assertThat(json.has("status")).isTrue
assertThat(json["status"].asText()).isEqualTo("pong")
assertThat(json["service"].asText()).isEqualTo("ping-service")
verify { pingUseCase.executePing("Simple Ping") }
} }
@Test @Test
fun `should return enhanced ping response without simulation`() { fun `should return enhanced ping response`() {
// Given // Given
val expectedResponse = EnhancedPingResponse( every { pingUseCase.executePing(any()) } returns Ping(
status = "pong", message = "Enhanced Ping",
timestamp = "2023-10-01T10:00:00Z", timestamp = Instant.parse("2023-10-01T10:00:00Z")
service = "ping-service",
circuitBreakerState = "CLOSED",
responseTime = 10L
) )
every { pingService.ping(false) } returns expectedResponse
// When & Then // When & Then
mockMvc.perform(get("/ping/enhanced")) val mvcResult: MvcResult = mockMvc.perform(get("/ping/enhanced"))
.andExpect(request().asyncStarted())
.andReturn()
val result = mockMvc.perform(asyncDispatch(mvcResult))
.andExpect(status().isOk) .andExpect(status().isOk)
.andReturn()
// Verify val body = result.response.contentAsString
verify { pingService.ping(false) } System.out.println("[DEBUG_LOG] /ping/enhanced response status=${result.response.status} contentType=${result.response.contentType} body=$body")
} val json = objectMapper.readTree(body)
assertThat(json.has("status")).isTrue
assertThat(json["status"].asText()).isEqualTo("pong")
assertThat(json["service"].asText()).isEqualTo("ping-service")
@Test verify { pingUseCase.executePing("Enhanced Ping") }
fun `should return enhanced ping response with simulation enabled`() {
// Given
val expectedResponse = EnhancedPingResponse(
status = "fallback",
timestamp = "2023-10-01T10:00:00Z",
service = "ping-service-fallback",
circuitBreakerState = "OPEN",
responseTime = 5L
)
every { pingService.ping(true) } returns expectedResponse
// When & Then
mockMvc.perform(get("/ping/enhanced?simulate=true"))
.andExpect(status().isOk)
// Verify
verify { pingService.ping(true) }
} }
@Test @Test
fun `should return health check response`() { fun `should return health check response`() {
// Given
val expectedResponse = HealthResponse(
status = "pong",
timestamp = "2023-10-01T10:00:00Z",
service = "ping-service",
healthy = true
)
every { pingService.healthCheck() } returns expectedResponse
// When & Then // When & Then
mockMvc.perform(get("/ping/health")) val mvcResult: MvcResult = mockMvc.perform(get("/ping/health"))
.andExpect(request().asyncStarted())
.andReturn()
val result = mockMvc.perform(asyncDispatch(mvcResult))
.andExpect(status().isOk) .andExpect(status().isOk)
.andReturn()
// Verify val body = result.response.contentAsString
verify { pingService.healthCheck() } System.out.println("[DEBUG_LOG] /ping/health response status=${result.response.status} contentType=${result.response.contentType} body=$body")
} val json = objectMapper.readTree(body)
assertThat(json.has("status")).isTrue
@Test assertThat(json["status"].asText()).isEqualTo("up")
fun `should handle missing simulate parameter with default false`() { assertThat(json["service"].asText()).isEqualTo("ping-service")
// Given
val expectedResponse = EnhancedPingResponse(
status = "pong",
timestamp = "2023-10-01T10:00:00Z",
service = "ping-service",
circuitBreakerState = "CLOSED",
responseTime = 8L
)
every { pingService.ping(false) } returns expectedResponse
// When & Then
mockMvc.perform(get("/ping/enhanced"))
.andExpect(status().isOk)
// Verify default parameter is used
verify { pingService.ping(false) }
} }
} }

View File

@ -1,216 +1,56 @@
package at.mocode.ping.service package at.mocode.ping.service
import io.github.resilience4j.circuitbreaker.CircuitBreaker import at.mocode.ping.application.PingService
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry import at.mocode.ping.domain.Ping
import at.mocode.ping.domain.PingRepository
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.slf4j.LoggerFactory import kotlin.uuid.ExperimentalUuidApi
import org.springframework.beans.factory.annotation.Autowired import kotlin.uuid.Uuid
import org.springframework.boot.test.context.SpringBootTest
import kotlin.math.ceil
/** /**
* Comprehensive test suite for PingServiceCircuitBreaker * Unit tests for the actual application service (`PingService`).
* Updated to assert DTOs instead of Maps. *
* The previous `PingServiceCircuitBreakerTest` referenced an outdated component.
*/ */
@SpringBootTest @OptIn(ExperimentalUuidApi::class)
class PingServiceCircuitBreakerTest { class PingServiceCircuitBreakerTest {
@Autowired private val repository: PingRepository = mockk()
private lateinit var pingServiceCircuitBreaker: PingServiceCircuitBreaker private val service = PingService(repository)
@Autowired @Test
private lateinit var circuitBreakerRegistry: CircuitBreakerRegistry fun `executePing should persist and return ping`() {
every { repository.save(any()) } answers { firstArg() }
private val logger = LoggerFactory.getLogger(PingServiceCircuitBreakerTest::class.java) val result = service.executePing("Hello")
private lateinit var circuitBreaker: CircuitBreaker assertThat(result.message).isEqualTo("Hello")
verify { repository.save(any()) }
@BeforeEach
fun setUp() {
circuitBreaker = circuitBreakerRegistry.circuitBreaker(PingServiceCircuitBreaker.PING_CIRCUIT_BREAKER)
// Reset circuit breaker state before each test
circuitBreaker.reset()
} }
@Test @Test
fun `should return successful ping response when circuit breaker is closed`() { fun `getPingHistory should delegate to repository`() {
// Given every { repository.findAll() } returns emptyList()
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.CLOSED)
// When val result = service.getPingHistory()
val result = pingServiceCircuitBreaker.ping(simulateFailure = false)
// Then assertThat(result).isEmpty()
assertThat(result.status).isEqualTo("pong") verify { repository.findAll() }
assertThat(result.service).isEqualTo("ping-service")
assertThat(result.circuitBreakerState).isEqualTo("CLOSED")
assertThat(result.timestamp).isNotBlank()
assertThat(result.responseTime).isGreaterThanOrEqualTo(0)
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.CLOSED)
} }
@Test @Test
fun `should handle single failure without opening circuit breaker`() { fun `getPing should delegate to repository`() {
// Given val id = Uuid.generateV7()
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.CLOSED) val ping = Ping(id = id, message = "Hi")
every { repository.findById(id) } returns ping
// When - try until we hit a simulated failure (60% chance) val result = service.getPing(id)
var result = pingServiceCircuitBreaker.ping(simulateFailure = true)
var attempts = 1
while (result.status == "pong" && attempts < 10) {
result = pingServiceCircuitBreaker.ping(simulateFailure = true)
attempts++
}
// Then - should get fallback response eventually assertThat(result).isEqualTo(ping)
assertThat(result.status).isEqualTo("fallback") verify { repository.findById(id) }
assertThat(result.service).isEqualTo("ping-service-fallback")
assertThat(result.circuitBreakerState).isEqualTo("OPEN")
logger.info("Circuit breaker state after single failure (after {} attempts): {}", attempts, circuitBreaker.state)
}
@Test
fun `should open circuit breaker after multiple failures`() {
// Given
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.CLOSED)
// When - trigger multiple failures to reach minimum-number-of-calls (4) and failure threshold (60%)
var failureCount = 0
var totalCalls = 0
val maxAttempts = 20 // Prevent infinite loop
while (circuitBreaker.state == CircuitBreaker.State.CLOSED && totalCalls < maxAttempts) {
val result = pingServiceCircuitBreaker.ping(simulateFailure = true)
totalCalls++
if (result.status == "fallback") failureCount++
logger.info(
"Attempt {}: Circuit breaker state = {}, Response status = {}, Failures so far = {}",
totalCalls, circuitBreaker.state, result.status, failureCount
)
}
// Then - circuit breaker should be open after sufficient failures
logger.info(
"Final circuit breaker state: {} after {} total calls with {} failures",
circuitBreaker.state, totalCalls, failureCount
)
if (totalCalls >= 4 && failureCount >= ceil(totalCalls * 0.6)) {
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.OPEN)
}
}
@Test
fun `should return fallback response when circuit breaker is manually opened`() {
// Given
circuitBreaker.transitionToOpenState()
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.OPEN)
// When
val result = pingServiceCircuitBreaker.ping(simulateFailure = false)
// Then
assertThat(result.status).isEqualTo("fallback")
assertThat(result.service).isEqualTo("ping-service-fallback")
assertThat(result.circuitBreakerState).isEqualTo("OPEN")
}
@Test
fun `should return successful health check when circuit breaker is closed`() {
// Given
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.CLOSED)
// When
val result = pingServiceCircuitBreaker.healthCheck()
// Then
assertThat(result.healthy).isTrue()
assertThat(result.status).isEqualTo("pong")
assertThat(result.timestamp).isNotBlank()
}
@Test
fun `should return fallback health check when circuit breaker is open`() {
// Given
circuitBreaker.transitionToOpenState()
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.OPEN)
// When
val result = pingServiceCircuitBreaker.healthCheck()
// Then
assertThat(result.healthy).isFalse()
assertThat(result.status).isEqualTo("down")
}
@Test
fun `should test circuit breaker state transitions`() {
// Given
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.CLOSED)
// When - manually transition to open state
circuitBreaker.transitionToOpenState()
// Then
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.OPEN)
// When - manually transition to half-open state
circuitBreaker.transitionToHalfOpenState()
// Then
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.HALF_OPEN)
// When - successful call should close circuit breaker
val result = pingServiceCircuitBreaker.ping(simulateFailure = false)
// Then
assertThat(result.status).isEqualTo("pong")
logger.info("Circuit breaker state after successful call in HALF_OPEN: {}", circuitBreaker.state)
}
@Test
fun `should track circuit breaker metrics`() {
// Given
val metrics = circuitBreaker.metrics
val initialNumberOfCalls = metrics.numberOfBufferedCalls
// When - execute some successful calls
repeat(3) {
pingServiceCircuitBreaker.ping(simulateFailure = false)
}
// Then
val newMetrics = circuitBreaker.metrics
assertThat(newMetrics.numberOfBufferedCalls).isGreaterThan(initialNumberOfCalls)
logger.info(
"Circuit breaker metrics - Calls: {}, Failure rate: {}%, Successful calls: {}, Failed calls: {}",
newMetrics.numberOfBufferedCalls,
newMetrics.failureRate,
newMetrics.numberOfSuccessfulCalls,
newMetrics.numberOfFailedCalls
)
}
@Test
fun `should handle concurrent calls correctly`() {
// Given
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.CLOSED)
// When - execute concurrent calls
val threads = (1..10).map { index ->
Thread {
val result = pingServiceCircuitBreaker.ping(simulateFailure = false)
logger.info("Concurrent call {}: status = {}", index, result.status)
}
}
threads.forEach { it.start() }
threads.forEach { it.join() }
// Then - verify circuit breaker recorded calls
val metrics = circuitBreaker.metrics
assertThat(metrics.numberOfBufferedCalls).isGreaterThanOrEqualTo(10)
assertThat(metrics.numberOfSuccessfulCalls).isGreaterThanOrEqualTo(10)
} }
} }

View File

@ -41,6 +41,11 @@ kotlin {
jsMain.dependencies { jsMain.dependencies {
implementation(libs.sqldelight.driver.web) implementation(libs.sqldelight.driver.web)
// NPM deps used by `sqlite.worker.js` (OPFS-backed SQLite WASM worker)
implementation(npm("@cashapp/sqldelight-sqljs-worker", "2.2.1"))
// Use a published build tag from the official package.
implementation(npm("@sqlite.org/sqlite-wasm", "3.51.1-build2"))
} }
/* /*

View File

@ -3,6 +3,6 @@ package at.mocode.frontend.core.localdb
import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.db.SqlDriver
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
expect class DatabaseDriverFactory { expect class DatabaseDriverFactory() {
suspend fun createDriver(): SqlDriver suspend fun createDriver(): SqlDriver
} }

View File

@ -0,0 +1,26 @@
package at.mocode.frontend.core.localdb
import org.koin.dsl.module
/**
* Thin wrapper around SQLDelight `AppDatabase` creation.
*
* The platform-specific part is the `DatabaseDriverFactory` (expect/actual),
* which provides the appropriate SQLDelight driver (JVM sqlite driver, JS WebWorkerDriver, ...).
*/
class DatabaseProvider(
private val driverFactory: DatabaseDriverFactory
) {
suspend fun createDatabase(): AppDatabase {
val driver = driverFactory.createDriver()
return AppDatabase(driver)
}
}
/**
* Koin module to provide the SQLDelight database for all frontend targets.
*/
val localDbModule = module {
single<DatabaseDriverFactory> { DatabaseDriverFactory() }
single<DatabaseProvider> { DatabaseProvider(get()) }
}

View File

@ -1,6 +1,51 @@
import { runWorker } from '@cashapp/sqldelight-sqljs-worker';
import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
// Minimal worker protocol compatible with SQLDelight's `web-worker-driver`.
// Mirrors the message format used by SQLDelight's `sqljs.worker.js` implementation.
function runWorker({ driver }) {
let db = null;
const open = (name) => {
db = driver.open(name);
};
// Open once with the default database name expected by SQLDelight.
open('app.db');
self.onmessage = (event) => {
const data = event.data;
try {
switch (data && data.action) {
case 'exec': {
if (!data.sql) throw new Error('exec: Missing query string');
// sqlite-wasm oo1 DB supports `.exec(...)`.
// We intentionally return only `values` which is sufficient for SQLDelight.
const rows = [];
db.exec({
sql: data.sql,
bind: data.params ?? [],
rowMode: 'array',
callback: (row) => rows.push(row)
});
return postMessage({ id: data.id, results: { values: rows } });
}
case 'begin_transaction':
db.exec('BEGIN TRANSACTION;');
return postMessage({ id: data.id, results: [] });
case 'end_transaction':
db.exec('END TRANSACTION;');
return postMessage({ id: data.id, results: [] });
case 'rollback_transaction':
db.exec('ROLLBACK TRANSACTION;');
return postMessage({ id: data.id, results: [] });
default:
throw new Error(`Unsupported action: ${data && data.action}`);
}
} catch (err) {
return postMessage({ id: data && data.id, error: err?.message ?? String(err) });
}
};
}
sqlite3InitModule({ sqlite3InitModule({
print: console.log, print: console.log,
printErr: console.error, printErr: console.error,

View File

@ -1,6 +1,49 @@
import { runWorker } from '@cashapp/sqldelight-sqljs-worker';
import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
// Minimal worker protocol compatible with SQLDelight's `web-worker-driver`.
// Mirrors the message format used by SQLDelight's `sqljs.worker.js` implementation.
function runWorker({ driver }) {
let db = null;
const open = (name) => {
db = driver.open(name);
};
// Open once with the default database name expected by SQLDelight.
open('app.db');
self.onmessage = (event) => {
const data = event.data;
try {
switch (data && data.action) {
case 'exec': {
if (!data.sql) throw new Error('exec: Missing query string');
const rows = [];
db.exec({
sql: data.sql,
bind: data.params ?? [],
rowMode: 'array',
callback: (row) => rows.push(row)
});
return postMessage({ id: data.id, results: { values: rows } });
}
case 'begin_transaction':
db.exec('BEGIN TRANSACTION;');
return postMessage({ id: data.id, results: [] });
case 'end_transaction':
db.exec('END TRANSACTION;');
return postMessage({ id: data.id, results: [] });
case 'rollback_transaction':
db.exec('ROLLBACK TRANSACTION;');
return postMessage({ id: data.id, results: [] });
default:
throw new Error(`Unsupported action: ${data && data.action}`);
}
} catch (err) {
return postMessage({ id: data && data.id, error: err?.message ?? String(err) });
}
};
}
sqlite3InitModule({ sqlite3InitModule({
print: console.log, print: console.log,
printErr: console.error, printErr: console.error,

View File

@ -39,6 +39,15 @@ kotlin {
implementation(libs.bundles.kmp.common) implementation(libs.bundles.kmp.common)
implementation(libs.bundles.compose.common) implementation(libs.bundles.compose.common)
// Ktor (used directly in shared/di and shared/network)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.contentNegotiation)
implementation(libs.ktor.client.logging)
implementation(libs.ktor.client.serialization.kotlinx.json)
// Serialization
implementation(libs.kotlinx.serialization.json)
// Compose // Compose
implementation(compose.runtime) implementation(compose.runtime)
implementation(compose.foundation) implementation(compose.foundation)

View File

@ -0,0 +1,3 @@
package at.mocode.shared.network
actual fun currentTimeMillis(): Long = System.currentTimeMillis()

View File

@ -74,6 +74,7 @@ kotlin {
commonMain.dependencies { commonMain.dependencies {
// Shared modules // Shared modules
implementation(projects.frontend.shared) implementation(projects.frontend.shared)
implementation(projects.frontend.core.domain)
implementation(projects.frontend.core.designSystem) implementation(projects.frontend.core.designSystem)
implementation(projects.frontend.core.navigation) implementation(projects.frontend.core.navigation)
implementation(projects.frontend.core.network) implementation(projects.frontend.core.network)
@ -127,6 +128,28 @@ kotlin {
} }
} }
// ---------------------------------------------------------------------------
// SQLDelight WebWorker (OPFS) resource
// ---------------------------------------------------------------------------
// `:frontend:core:local-db` ships `sqlite.worker.js` as a JS resource.
// When bundling the final JS app, webpack resolves `new URL("sqlite.worker.js", import.meta.url)`
// relative to the Kotlin JS package folder (root build dir). We therefore copy the worker into
// that folder before webpack runs.
val copySqliteWorkerJs by tasks.registering(Copy::class) {
val localDb = project(":frontend:core:local-db")
dependsOn(localDb.tasks.named("jsProcessResources"))
from(localDb.layout.buildDirectory.file("processedResources/js/main/sqlite.worker.js"))
// Root build directory where Kotlin JS packages are assembled.
into(rootProject.layout.buildDirectory.dir("js/packages/${rootProject.name}-frontend-shells-meldestelle-portal/kotlin"))
}
tasks.matching { it.name == "jsBrowserProductionWebpack" }.configureEach {
dependsOn(copySqliteWorkerJs)
}
// KMP Compile-Optionen // KMP Compile-Optionen
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> { tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
compilerOptions { compilerOptions {

View File

@ -52,7 +52,8 @@ hikari = "7.0.2"
h2 = "2.4.240" h2 = "2.4.240"
flyway = "11.19.1" flyway = "11.19.1"
redisson = "4.0.0" redisson = "4.0.0"
lettuce = "7.2.1.RELEASE" # Spring Boot 3.5.x manages Lettuce 6.6.x; keep aligned to avoid binary/API mismatches.
lettuce = "6.6.0.RELEASE"
# Observability # Observability
micrometer = "1.16.1" micrometer = "1.16.1"
@ -162,6 +163,7 @@ spring-boot-starter-test = { module = "org.springframework.boot:spring-boot-star
spring-boot-starter-oauth2-client = { module = "org.springframework.boot:spring-boot-starter-oauth2-client" } spring-boot-starter-oauth2-client = { module = "org.springframework.boot:spring-boot-starter-oauth2-client" }
spring-boot-starter-oauth2-resource-server = { module = "org.springframework.boot:spring-boot-starter-oauth2-resource-server" } spring-boot-starter-oauth2-resource-server = { module = "org.springframework.boot:spring-boot-starter-oauth2-resource-server" }
spring-boot-starter-security = { module = "org.springframework.boot:spring-boot-starter-security" } spring-boot-starter-security = { module = "org.springframework.boot:spring-boot-starter-security" }
spring-security-test = { module = "org.springframework.security:spring-security-test" }
spring-boot-starter-webflux = { module = "org.springframework.boot:spring-boot-starter-webflux" } spring-boot-starter-webflux = { module = "org.springframework.boot:spring-boot-starter-webflux" }
spring-boot-starter-json = { module = "org.springframework.boot:spring-boot-starter-json" } spring-boot-starter-json = { module = "org.springframework.boot:spring-boot-starter-json" }
spring-boot-starter-aop = { module = "org.springframework.boot:spring-boot-starter-aop", version.ref = "springBoot" } spring-boot-starter-aop = { module = "org.springframework.boot:spring-boot-starter-aop", version.ref = "springBoot" }
@ -293,6 +295,10 @@ testing-jvm = [
"mockk", "mockk",
"assertj-core" "assertj-core"
] ]
test-spring = [
"spring-boot-starter-test",
"spring-security-test"
]
spring-boot-service-complete = [ spring-boot-service-complete = [
"spring-boot-starter-web", "spring-boot-starter-web",
"spring-boot-starter-validation", "spring-boot-starter-validation",
@ -306,6 +312,15 @@ spring-boot-service-complete = [
"zipkin-reporter-brave", "zipkin-reporter-brave",
"zipkin-sender-okhttp3" "zipkin-sender-okhttp3"
] ]
# Standard dependencies for a "secure" Spring Boot microservice (architecture-approved baseline)
spring-boot-secure-service = [
"spring-boot-starter-web",
"spring-boot-starter-actuator",
"spring-boot-starter-security",
"spring-boot-starter-oauth2-resource-server",
"spring-cloud-starter-consul-discovery"
]
database-complete = [ database-complete = [
"spring-boot-starter-data-jpa", "spring-boot-starter-data-jpa",
"postgresql-driver", "postgresql-driver",

View File

@ -2,6 +2,11 @@
# yarn lockfile v1 # yarn lockfile v1
"@cashapp/sqldelight-sqljs-worker@2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@cashapp/sqldelight-sqljs-worker/-/sqldelight-sqljs-worker-2.2.1.tgz#c71776a9dddfc435d4f1e64317a7039d447ea024"
integrity sha512-cj/llgS1T94t7rz63fI7pbi+jJx+vQofCT58KyMZb9XVRuoxb4taB5wbbBa4e/iljiuN5XIGGPFx+5PvtVh3LQ==
"@colors/colors@1.5.0": "@colors/colors@1.5.0":
version "1.5.0" version "1.5.0"
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
@ -122,6 +127,11 @@
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2" resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2"
integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==
"@sqlite.org/sqlite-wasm@3.51.1-build2":
version "3.51.1-build2"
resolved "https://registry.yarnpkg.com/@sqlite.org/sqlite-wasm/-/sqlite-wasm-3.51.1-build2.tgz#822497fdd05cbee3b1e8209aa46ffe1bafc70dbb"
integrity sha512-lVPTBlFsEijJ3wuoIbMfC9QMZKfL8huHN8D/lijNKoVxPqUDNvDtXse0wafe7USSmyfKAMb1JZ3ISSr/Vgbn5w==
"@types/body-parser@*": "@types/body-parser@*":
version "1.19.6" version "1.19.6"
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474"

View File

@ -11,8 +11,9 @@ pluginManagement {
maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots/") } // Added snapshots for plugins maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots/") } // Added snapshots for plugins
} }
} }
plugins { plugins {
alias(libs.plugins.foojayResolver) id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
} }
dependencyResolutionManagement { dependencyResolutionManagement {