diff --git a/backend/infrastructure/messaging/messaging-client/build.gradle.kts b/backend/infrastructure/messaging/messaging-client/build.gradle.kts index a9f53660..60f96e45 100644 --- a/backend/infrastructure/messaging/messaging-client/build.gradle.kts +++ b/backend/infrastructure/messaging/messaging-client/build.gradle.kts @@ -22,6 +22,18 @@ dependencies { implementation(platform(projects.platform.platformBom)) // Stellt gemeinsame Abhängigkeiten bereit. 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. implementation(projects.backend.infrastructure.messaging.messagingConfig) // Fügt die reaktive Kafka-Implementierung hinzu (Project Reactor). diff --git a/backend/infrastructure/persistence/build.gradle.kts b/backend/infrastructure/persistence/build.gradle.kts index 16d0b831..d690cc3e 100644 --- a/backend/infrastructure/persistence/build.gradle.kts +++ b/backend/infrastructure/persistence/build.gradle.kts @@ -1,6 +1,18 @@ plugins { alias(libs.plugins.kotlinJvm) 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 { @@ -8,6 +20,9 @@ dependencies { implementation(projects.core.coreDomain) implementation(projects.platform.platformDependencies) + // Spring Boot Database dependencies + implementation(libs.bundles.database.complete) + // Exposed implementation(libs.exposed.core) implementation(libs.exposed.jdbc) diff --git a/backend/infrastructure/persistence/src/main/kotlin/at/mocode/backend/infrastructure/persistence/DatabaseUtils.kt b/backend/infrastructure/persistence/src/main/kotlin/at/mocode/backend/infrastructure/persistence/DatabaseUtils.kt index 5cd7491f..18d2904c 100644 --- a/backend/infrastructure/persistence/src/main/kotlin/at/mocode/backend/infrastructure/persistence/DatabaseUtils.kt +++ b/backend/infrastructure/persistence/src/main/kotlin/at/mocode/backend/infrastructure/persistence/DatabaseUtils.kt @@ -3,9 +3,16 @@ package at.mocode.backend.infrastructure.persistence import at.mocode.core.domain.model.ErrorCodes import at.mocode.core.domain.model.ErrorDto import at.mocode.core.domain.model.PagedResponse -import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.statements.BatchInsertStatement -import org.jetbrains.exposed.sql.transactions.transaction +import at.mocode.core.utils.Result +import org.jetbrains.exposed.v1.core.ResultRow +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.SQLTimeoutException @@ -18,7 +25,7 @@ import java.sql.SQLTimeoutException inline fun transactionResult( database: Database? = null, - crossinline block: Transaction.() -> T + crossinline block: JdbcTransaction.() -> T ): Result { return try { val result = transaction(database) { block() } @@ -60,12 +67,12 @@ inline fun transactionResult( inline fun writeTransaction( database: Database? = null, - crossinline block: Transaction.() -> T + crossinline block: JdbcTransaction.() -> T ): Result = transactionResult(database, block) inline fun readTransaction( database: Database? = null, - crossinline block: Transaction.() -> T + crossinline block: JdbcTransaction.() -> T ): Result = transactionResult(database, block) fun Query.paginate(page: Int, size: Int): Query { @@ -123,18 +130,17 @@ fun Query.toPagedResponse( object DatabaseUtils { fun tableExists(tableName: String, database: Database? = null): Boolean { - return try { - transaction(database) { - // Postgres-spezifischer, robuster Ansatz über to_regclass - val valid = tableName.trim() - if (!valid.matches(Regex("^[A-Za-z_][A-Za-z0-9_]*$"))) return@transaction false - exec("SELECT to_regclass('$valid')") { rs -> - if (rs.next()) rs.getString(1) else null - } != null - } - } catch (e: Exception) { - false - } + return transactionResult(database) { + // Postgres-spezifischer, robuster Ansatz über to_regclass + val valid = tableName.trim() + if (!valid.matches(Regex("^[A-Za-z_][A-Za-z0-9_]*$"))) return@transactionResult false + exec("SELECT to_regclass('$valid')") { rs -> + if (rs.next()) rs.getString(1) else null + } != null + }.fold( + onSuccess = { it }, + onFailure = { false } + ) } @JvmName("createIndexIfNotExistsArray") diff --git a/backend/services/entries/entries-service/build.gradle.kts b/backend/services/entries/entries-service/build.gradle.kts index 94a0aedb..eba30cf2 100644 --- a/backend/services/entries/entries-service/build.gradle.kts +++ b/backend/services/entries/entries-service/build.gradle.kts @@ -16,16 +16,18 @@ dependencies { implementation(projects.backend.services.entries.entriesApi) 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.spring.boot.starter.web) // KORREKTUR: Jackson Bundle aufgelöst, da Accessor fehlschlägt implementation(libs.jackson.module.kotlin) implementation(libs.jackson.datatype.jsr310) implementation(libs.kotlin.reflect) - implementation(libs.spring.cloud.starter.consul.discovery) implementation(libs.caffeine) implementation(libs.spring.web) @@ -39,5 +41,4 @@ dependencies { testImplementation(projects.platform.platformTesting) testImplementation(libs.bundles.testing.jvm) testImplementation(libs.spring.boot.starter.test) - testImplementation(libs.spring.boot.starter.web) } diff --git a/backend/services/ping/ping-api/build.gradle.kts b/backend/services/ping/ping-api/build.gradle.kts deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/services/ping/ping-api/src/commonMain/kotlin/at/mocode/ping/api/PingApi.kt b/backend/services/ping/ping-api/src/commonMain/kotlin/at/mocode/ping/api/PingApi.kt deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/services/ping/ping-api/src/commonMain/kotlin/at/mocode/ping/api/PingData.kt b/backend/services/ping/ping-api/src/commonMain/kotlin/at/mocode/ping/api/PingData.kt deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/services/ping/ping-service/build.gradle.kts b/backend/services/ping/ping-service/build.gradle.kts index 3b1bd5fa..51947758 100644 --- a/backend/services/ping/ping-service/build.gradle.kts +++ b/backend/services/ping/ping-service/build.gradle.kts @@ -15,17 +15,19 @@ kotlin { dependencies { // === Project Dependencies === implementation(projects.contracts.pingApi) + + // Our central BOM for consistent versions + implementation(platform(projects.platform.platformBom)) implementation(projects.platform.platformDependencies) // NEU: Zugriff auf die verschobenen DatabaseUtils implementation(projects.backend.infrastructure.persistence) // === Spring Boot & Cloud === - implementation(libs.bundles.spring.boot.service.complete) - // WICHTIG: Da wir JPA (blockierend) nutzen, brauchen wir Spring MVC (nicht WebFlux) - implementation(libs.spring.boot.starter.web) - - // Service Discovery - implementation(libs.spring.cloud.starter.consul.discovery) + // Standard dependencies for a secure microservice + implementation(libs.bundles.spring.boot.secure.service) + // Common service extras + implementation(libs.spring.boot.starter.validation) + implementation(libs.spring.boot.starter.json) // === Database & Persistence === implementation(libs.bundles.database.complete) @@ -37,8 +39,13 @@ dependencies { // === Testing === testImplementation(libs.bundles.testing.jvm) + testImplementation(libs.spring.boot.starter.test) + testImplementation(libs.spring.security.test) } tasks.test { useJUnitPlatform() + testLogging { + showStandardStreams = true + } } diff --git a/backend/services/ping/ping-service/src/main/kotlin/at/mocode/ping/infrastructure/web/PingController.kt b/backend/services/ping/ping-service/src/main/kotlin/at/mocode/ping/infrastructure/web/PingController.kt index 261b1e32..84f85fec 100644 --- a/backend/services/ping/ping-service/src/main/kotlin/at/mocode/ping/infrastructure/web/PingController.kt +++ b/backend/services/ping/ping-service/src/main/kotlin/at/mocode/ping/infrastructure/web/PingController.kt @@ -17,7 +17,8 @@ import kotlin.random.Random * Nutzt den Application Port (PingUseCase). */ @RestController -@CrossOrigin(allowedHeaders = ["*"], allowCredentials = "true") +// Spring requires using `originPatterns` (not wildcard `origins`) when credentials are enabled. +@CrossOrigin(allowedHeaders = ["*"], allowCredentials = "true", originPatterns = ["*"]) class PingController( private val pingUseCase: PingUseCase ) : PingApi { diff --git a/backend/services/ping/ping-service/src/test/kotlin/at/mocode/ping/service/PingControllerIntegrationTest.kt b/backend/services/ping/ping-service/src/test/kotlin/at/mocode/ping/service/PingControllerIntegrationTest.kt index 9af036fa..4121983c 100644 --- a/backend/services/ping/ping-service/src/test/kotlin/at/mocode/ping/service/PingControllerIntegrationTest.kt +++ b/backend/services/ping/ping-service/src/test/kotlin/at/mocode/ping/service/PingControllerIntegrationTest.kt @@ -1,246 +1,59 @@ package at.mocode.ping.service -import io.github.resilience4j.circuitbreaker.CircuitBreaker -import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry +import at.mocode.ping.application.PingUseCase +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.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.test.web.client.TestRestTemplate -import org.springframework.boot.test.web.server.LocalServerPort -import org.springframework.http.HttpStatus -import org.springframework.test.context.ActiveProfiles +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Import +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 - * Tests REST endpoints with circuit breaker functionality using TestRestTemplate + * Lightweight Spring MVC integration test (no full application context / datasource). */ -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@ActiveProfiles("test") +@WebMvcTest( + 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 { - @LocalServerPort - private var port: Int = 0 - @Autowired - private lateinit var restTemplate: TestRestTemplate + private lateinit var mockMvc: MockMvc - @Autowired - private lateinit var circuitBreakerRegistry: CircuitBreakerRegistry - - private val logger = LoggerFactory.getLogger(PingControllerIntegrationTest::class.java) - - 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) + @TestConfiguration + class TestConfig { + @Bean + fun pingUseCase(): PingUseCase = mockk(relaxed = true) } @Test - fun `should return enhanced ping response when circuit breaker is closed`() { - // Given - assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.CLOSED) + fun `should start MVC slice and serve endpoints`() { + // Just verify the MVC wiring starts and endpoints respond 200 + mockMvc.perform(get("/ping/health")).andExpect(status().isOk) - // When - val response = restTemplate.getForEntity(getUrl("/ping/enhanced"), 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") - 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>() - - 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 - 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" + // For endpoints that require the use-case, the relaxed mock is sufficient, + // but we still provide deterministic ping data. + val useCase = TestConfig().pingUseCase() + every { useCase.executePing(any()) } returns Ping( + message = "Simple Ping", + timestamp = Instant.parse("2023-10-01T10:00:00Z") ) - endpoints.forEach { endpoint -> - val response = restTemplate.getForEntity(getUrl(endpoint), Map::class.java) - - 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) + // Note: we don't assert the full JSON here (covered by PingControllerTest) + val result = mockMvc.perform(get("/ping/simple")).andReturn() + assertThat(result.response.status).isEqualTo(200) } } diff --git a/backend/services/ping/ping-service/src/test/kotlin/at/mocode/ping/service/PingControllerTest.kt b/backend/services/ping/ping-service/src/test/kotlin/at/mocode/ping/service/PingControllerTest.kt index f502368a..0984015f 100644 --- a/backend/services/ping/ping-service/src/test/kotlin/at/mocode/ping/service/PingControllerTest.kt +++ b/backend/services/ping/ping-service/src/test/kotlin/at/mocode/ping/service/PingControllerTest.kt @@ -1,20 +1,28 @@ package at.mocode.ping.service -import at.mocode.ping.api.EnhancedPingResponse -import at.mocode.ping.api.HealthResponse +import at.mocode.ping.domain.Ping +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.mockk import io.mockk.verify +import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach 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.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Import 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.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 @@ -28,109 +36,102 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.* ] ) @Import(PingControllerTest.TestConfig::class) +@AutoConfigureMockMvc class PingControllerTest { @Autowired private lateinit var mockMvc: MockMvc @Autowired - private lateinit var pingService: PingServiceCircuitBreaker + private lateinit var pingUseCase: PingUseCase + + @Autowired + private lateinit var objectMapper: ObjectMapper @TestConfiguration class TestConfig { @Bean - fun pingServiceCircuitBreaker(): PingServiceCircuitBreaker = mockk(relaxed = true) + fun pingUseCase(): PingUseCase = mockk(relaxed = true) } @BeforeEach fun setUp() { // Reset mocks before each test - io.mockk.clearMocks(pingService) + io.mockk.clearMocks(pingUseCase) } @Test 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 - 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) + .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 - fun `should return enhanced ping response without simulation`() { + fun `should return enhanced ping response`() { // Given - val expectedResponse = EnhancedPingResponse( - status = "pong", - timestamp = "2023-10-01T10:00:00Z", - service = "ping-service", - circuitBreakerState = "CLOSED", - responseTime = 10L + every { pingUseCase.executePing(any()) } returns Ping( + message = "Enhanced Ping", + timestamp = Instant.parse("2023-10-01T10:00:00Z") ) - every { pingService.ping(false) } returns expectedResponse // 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) + .andReturn() - // Verify - verify { pingService.ping(false) } - } + val body = result.response.contentAsString + 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 - 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) } + verify { pingUseCase.executePing("Enhanced Ping") } } @Test 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 - 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) + .andReturn() - // Verify - verify { pingService.healthCheck() } - } - - @Test - fun `should handle missing simulate parameter with default false`() { - // 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) } + val body = result.response.contentAsString + 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 + assertThat(json["status"].asText()).isEqualTo("up") + assertThat(json["service"].asText()).isEqualTo("ping-service") } } diff --git a/backend/services/ping/ping-service/src/test/kotlin/at/mocode/ping/service/PingServiceCircuitBreakerTest.kt b/backend/services/ping/ping-service/src/test/kotlin/at/mocode/ping/service/PingServiceCircuitBreakerTest.kt index 04a57125..b38fb516 100644 --- a/backend/services/ping/ping-service/src/test/kotlin/at/mocode/ping/service/PingServiceCircuitBreakerTest.kt +++ b/backend/services/ping/ping-service/src/test/kotlin/at/mocode/ping/service/PingServiceCircuitBreakerTest.kt @@ -1,216 +1,56 @@ package at.mocode.ping.service -import io.github.resilience4j.circuitbreaker.CircuitBreaker -import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry +import at.mocode.ping.application.PingService +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.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import kotlin.math.ceil +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid /** - * Comprehensive test suite for PingServiceCircuitBreaker - * Updated to assert DTOs instead of Maps. + * Unit tests for the actual application service (`PingService`). + * + * The previous `PingServiceCircuitBreakerTest` referenced an outdated component. */ -@SpringBootTest +@OptIn(ExperimentalUuidApi::class) class PingServiceCircuitBreakerTest { - @Autowired - private lateinit var pingServiceCircuitBreaker: PingServiceCircuitBreaker + private val repository: PingRepository = mockk() + private val service = PingService(repository) - @Autowired - private lateinit var circuitBreakerRegistry: CircuitBreakerRegistry + @Test + 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 + fun `getPingHistory should delegate to repository`() { + every { repository.findAll() } returns emptyList() - @Test - fun `should return successful ping response when circuit breaker is closed`() { - // Given - assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.CLOSED) + val result = service.getPingHistory() - // When - val result = pingServiceCircuitBreaker.ping(simulateFailure = false) + assertThat(result).isEmpty() + verify { repository.findAll() } + } - // Then - assertThat(result.status).isEqualTo("pong") - 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 + fun `getPing should delegate to repository`() { + val id = Uuid.generateV7() + val ping = Ping(id = id, message = "Hi") + every { repository.findById(id) } returns ping - @Test - fun `should handle single failure without opening circuit breaker`() { - // Given - assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.CLOSED) + val result = service.getPing(id) - // When - try until we hit a simulated failure (60% chance) - 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.status).isEqualTo("fallback") - 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) - } + assertThat(result).isEqualTo(ping) + verify { repository.findById(id) } + } } diff --git a/frontend/core/local-db/build.gradle.kts b/frontend/core/local-db/build.gradle.kts index 0f641ef7..0b6acf45 100644 --- a/frontend/core/local-db/build.gradle.kts +++ b/frontend/core/local-db/build.gradle.kts @@ -41,6 +41,11 @@ kotlin { jsMain.dependencies { 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")) } /* diff --git a/frontend/core/local-db/src/commonMain/kotlin/at/mocode/frontend/core/localdb/DatabaseDriverFactory.kt b/frontend/core/local-db/src/commonMain/kotlin/at/mocode/frontend/core/localdb/DatabaseDriverFactory.kt index e9ac88d0..472ce4b6 100644 --- a/frontend/core/local-db/src/commonMain/kotlin/at/mocode/frontend/core/localdb/DatabaseDriverFactory.kt +++ b/frontend/core/local-db/src/commonMain/kotlin/at/mocode/frontend/core/localdb/DatabaseDriverFactory.kt @@ -3,6 +3,6 @@ package at.mocode.frontend.core.localdb import app.cash.sqldelight.db.SqlDriver @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") -expect class DatabaseDriverFactory { +expect class DatabaseDriverFactory() { suspend fun createDriver(): SqlDriver } diff --git a/frontend/core/local-db/src/commonMain/kotlin/at/mocode/frontend/core/localdb/LocalDbModule.kt b/frontend/core/local-db/src/commonMain/kotlin/at/mocode/frontend/core/localdb/LocalDbModule.kt new file mode 100644 index 00000000..6dd1c31f --- /dev/null +++ b/frontend/core/local-db/src/commonMain/kotlin/at/mocode/frontend/core/localdb/LocalDbModule.kt @@ -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() } + single { DatabaseProvider(get()) } +} diff --git a/frontend/core/local-db/src/jsMain/resources/sqlite.worker.js b/frontend/core/local-db/src/jsMain/resources/sqlite.worker.js index d5d8dfe4..22e644d3 100644 --- a/frontend/core/local-db/src/jsMain/resources/sqlite.worker.js +++ b/frontend/core/local-db/src/jsMain/resources/sqlite.worker.js @@ -1,6 +1,51 @@ -import { runWorker } from '@cashapp/sqldelight-sqljs-worker'; 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({ print: console.log, printErr: console.error, diff --git a/frontend/core/local-db/src/wasmJsMain/resources/sqlite.worker.js b/frontend/core/local-db/src/wasmJsMain/resources/sqlite.worker.js index d5d8dfe4..464046e7 100644 --- a/frontend/core/local-db/src/wasmJsMain/resources/sqlite.worker.js +++ b/frontend/core/local-db/src/wasmJsMain/resources/sqlite.worker.js @@ -1,6 +1,49 @@ -import { runWorker } from '@cashapp/sqldelight-sqljs-worker'; 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({ print: console.log, printErr: console.error, diff --git a/frontend/shared/build.gradle.kts b/frontend/shared/build.gradle.kts index c472af90..27bdfafe 100644 --- a/frontend/shared/build.gradle.kts +++ b/frontend/shared/build.gradle.kts @@ -39,6 +39,15 @@ kotlin { implementation(libs.bundles.kmp.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 implementation(compose.runtime) implementation(compose.foundation) diff --git a/frontend/shared/src/desktopMain/kotlin/at/mocode/shared/network/TimeDesktop.kt b/frontend/shared/src/desktopMain/kotlin/at/mocode/shared/network/TimeDesktop.kt new file mode 100644 index 00000000..4558d9aa --- /dev/null +++ b/frontend/shared/src/desktopMain/kotlin/at/mocode/shared/network/TimeDesktop.kt @@ -0,0 +1,3 @@ +package at.mocode.shared.network + +actual fun currentTimeMillis(): Long = System.currentTimeMillis() diff --git a/frontend/shells/meldestelle-portal/build.gradle.kts b/frontend/shells/meldestelle-portal/build.gradle.kts index d4ed6b95..53be6e5b 100644 --- a/frontend/shells/meldestelle-portal/build.gradle.kts +++ b/frontend/shells/meldestelle-portal/build.gradle.kts @@ -74,6 +74,7 @@ kotlin { commonMain.dependencies { // Shared modules implementation(projects.frontend.shared) + implementation(projects.frontend.core.domain) implementation(projects.frontend.core.designSystem) implementation(projects.frontend.core.navigation) 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 tasks.withType { compilerOptions { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index faee7f9d..a7152054 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -52,7 +52,8 @@ hikari = "7.0.2" h2 = "2.4.240" flyway = "11.19.1" 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 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-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-security-test = { module = "org.springframework.security:spring-security-test" } 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-aop = { module = "org.springframework.boot:spring-boot-starter-aop", version.ref = "springBoot" } @@ -293,6 +295,10 @@ testing-jvm = [ "mockk", "assertj-core" ] +test-spring = [ + "spring-boot-starter-test", + "spring-security-test" +] spring-boot-service-complete = [ "spring-boot-starter-web", "spring-boot-starter-validation", @@ -306,6 +312,15 @@ spring-boot-service-complete = [ "zipkin-reporter-brave", "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 = [ "spring-boot-starter-data-jpa", "postgresql-driver", diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index bc541f09..be7f2866 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -2,6 +2,11 @@ # 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": version "1.5.0" 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" 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@*": version "1.19.6" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474" diff --git a/settings.gradle.kts b/settings.gradle.kts index 5dd14de6..ed1132cb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,8 +11,9 @@ pluginManagement { maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots/") } // Added snapshots for plugins } } + plugins { - alias(libs.plugins.foojayResolver) + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } dependencyResolutionManagement {