diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/deployment/DeploymentEndpoint.kt b/src/main/kotlin/kr/co/vividnext/sodalive/deployment/DeploymentEndpoint.kt new file mode 100644 index 00000000..f82441d8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/deployment/DeploymentEndpoint.kt @@ -0,0 +1,21 @@ +package kr.co.vividnext.sodalive.deployment + +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.actuate.endpoint.annotation.Endpoint +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation +import org.springframework.stereotype.Component + +@Component +@Endpoint(id = "deployment") +class DeploymentEndpoint( + @Value("\${deployment.artifact-version}") + private val artifactVersion: String, + @Value("\${deployment.config-commit}") + private val configCommit: String +) { + @ReadOperation + fun deployment(): Map = mapOf( + "application_artifact_version" to artifactVersion, + "config_commit" to configCommit + ) +} diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/deployment/DeploymentEndpointInjectedValueIntegrationTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/deployment/DeploymentEndpointInjectedValueIntegrationTest.kt new file mode 100644 index 00000000..07433b5f --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/deployment/DeploymentEndpointInjectedValueIntegrationTest.kt @@ -0,0 +1,50 @@ +package kr.co.vividnext.sodalive.deployment + +import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +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.LocalManagementPort +import org.springframework.http.HttpStatus +import org.springframework.test.annotation.DirtiesContext +import org.springframework.test.context.ContextConfiguration + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = [ + "management.server.port=0", + "deployment.artifact-version=1.2.3-20260914", + "deployment.config-commit=1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d", + "spring.datasource.url=jdbc:h2:mem:deployment-endpoint-injected-integration;" + + "MODE=MySQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=VALUE;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE" + ] +) +@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class]) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +class DeploymentEndpointInjectedValueIntegrationTest @Autowired constructor( + private val restTemplate: TestRestTemplate +) { + @LocalManagementPort + private var managementPort: Int = 0 + + @Test + @DisplayName("배포 식별자가 주입되면 /actuator/deployment는 주입된 값을 반환한다") + fun shouldReturnInjectedDeploymentIdentity() { + val response = restTemplate.getForEntity( + "http://localhost:$managementPort/actuator/deployment", + Map::class.java + ) + + assertEquals(HttpStatus.OK, response.statusCode) + assertEquals( + mapOf( + "application_artifact_version" to "1.2.3-20260914", + "config_commit" to "1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d" + ), + response.body + ) + } +} diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/deployment/DeploymentEndpointIntegrationTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/deployment/DeploymentEndpointIntegrationTest.kt new file mode 100644 index 00000000..690c5fb1 --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/deployment/DeploymentEndpointIntegrationTest.kt @@ -0,0 +1,85 @@ +package kr.co.vividnext.sodalive.deployment + +import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +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.LocalManagementPort +import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.http.HttpStatus +import org.springframework.test.annotation.DirtiesContext +import org.springframework.test.context.ContextConfiguration + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = [ + "management.server.port=0", + "spring.datasource.url=jdbc:h2:mem:deployment-endpoint-integration;" + + "MODE=MySQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=VALUE;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE" + ] +) +@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class]) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +class DeploymentEndpointIntegrationTest @Autowired constructor( + private val restTemplate: TestRestTemplate +) { + @LocalServerPort + private var serverPort: Int = 0 + + @LocalManagementPort + private var managementPort: Int = 0 + + @Test + @DisplayName("애플리케이션 포트의 /readyz는 인증 없이 200을 반환한다") + fun shouldReturnOkWhenReadyzIsCalledOnServerPort() { + val response = restTemplate.getForEntity("http://localhost:$serverPort/readyz", String::class.java) + + assertEquals(HttpStatus.OK, response.statusCode) + } + + @Test + @DisplayName("management 포트의 /actuator/health/readiness는 인증 없이 200을 반환한다") + fun shouldReturnOkWhenReadinessProbeIsCalledOnManagementPort() { + val response = restTemplate.getForEntity( + "http://localhost:$managementPort/actuator/health/readiness", + String::class.java + ) + + assertEquals(HttpStatus.OK, response.statusCode) + } + + @Test + @DisplayName("환경변수가 없으면 /actuator/deployment는 기본 배포 식별자를 snake_case로 반환한다") + fun shouldReturnDefaultDeploymentIdentityWhenEnvironmentVariablesAreNotProvided() { + val response = restTemplate.getForEntity( + "http://localhost:$managementPort/actuator/deployment", + Map::class.java + ) + + assertEquals(HttpStatus.OK, response.statusCode) + assertEquals( + mapOf( + "application_artifact_version" to "local", + "config_commit" to "0000000000000000000000000000000000000000" + ), + response.body + ) + } + + @Test + @DisplayName("애플리케이션 포트에서는 /actuator/deployment에 접근할 수 없다") + fun shouldNotExposeDeploymentEndpointOnServerPort() { + val response = restTemplate.getForEntity( + "http://localhost:$serverPort/actuator/deployment", + String::class.java + ) + + assertNotEquals(HttpStatus.OK, response.statusCode) + assertFalse(response.body?.contains("application_artifact_version") ?: false) + } +}