Skip to content

Commit 610f36d

Browse files
committed
add a reproducer for #11799. The issue is due to the code only accepted the property name bound to setActiveKeys
1 parent c97bfac commit 610f36d

File tree

1 file changed

+151
-0
lines changed

1 file changed

+151
-0
lines changed
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package io.micronaut.reproduce
2+
3+
import com.fasterxml.jackson.databind.JsonNode
4+
import com.fasterxml.jackson.databind.ObjectMapper
5+
import io.micronaut.context.ApplicationContext
6+
import io.micronaut.http.HttpRequest
7+
import io.micronaut.http.HttpResponse
8+
import io.micronaut.http.client.HttpClient
9+
import io.micronaut.runtime.server.EmbeddedServer
10+
import org.junit.jupiter.api.AfterEach
11+
import org.junit.jupiter.api.Assertions.*
12+
import org.junit.jupiter.api.Test
13+
14+
class EnvEndpointKeysConfigTest {
15+
16+
private val mapper = ObjectMapper()
17+
18+
private var server: EmbeddedServer? = null
19+
private var client: HttpClient? = null
20+
21+
@AfterEach
22+
fun cleanup() {
23+
try { client?.close() } catch (e: Exception) { /* ignore */ }
24+
try { server?.close() } catch (e: Exception) { /* ignore */ }
25+
}
26+
27+
@Test
28+
fun `default env endpoint contains packages and propertySources`() {
29+
val props = HashMap<String, Any?>()
30+
props["endpoints.env.enabled"] = true
31+
// ensure the endpoint is exposed over HTTP for newer Micronaut versions
32+
props["management.endpoints.web.exposure.include"] = "env"
33+
// try to avoid security blocking the endpoint in test environments
34+
props["micronaut.security.enabled"] = false
35+
props["micronaut.server.port"] = 0
36+
37+
server = ApplicationContext.run(EmbeddedServer::class.java, props)
38+
client = HttpClient.create(server!!.url)
39+
40+
val node = retrieveEnvJsonNode()
41+
42+
assertTrue(node.has("activeEnvironments"), "activeEnvironments should be present by default. Body: $node")
43+
assertTrue(node.has("packages"), "packages should be present by default. Body: $node")
44+
assertTrue(node.has("propertySources"), "propertySources should be present by default. Body: $node")
45+
}
46+
47+
@Test
48+
fun `env endpoint respects endpoints env keys when configured to only activeEnvironments (list binding)`() {
49+
val props = HashMap<String, Any?>()
50+
props["endpoints.env.enabled"] = true
51+
props["management.endpoints.web.exposure.include"] = "env"
52+
props["micronaut.security.enabled"] = false
53+
// bind keys as a list containing only activeEnvironments
54+
props["endpoints.env.keys"] = listOf("activeEnvironments")
55+
props["micronaut.server.port"] = 0
56+
57+
server = ApplicationContext.run(EmbeddedServer::class.java, props)
58+
client = HttpClient.create(server!!.url)
59+
60+
val node = retrieveEnvJsonNode()
61+
62+
// Expect only the configured key to be present
63+
val fieldNames = node.fieldNames().asSequence().toList()
64+
assertEquals(1, fieldNames.size, "Expected exactly one top-level key when endpoints.env.keys is configured. Body: $node")
65+
assertTrue(node.has("activeEnvironments"), "activeEnvironments should be present when configured. Body: $node")
66+
assertFalse(node.has("packages"), "packages should not be present when not configured. Body: $node")
67+
assertFalse(node.has("propertySources"), "propertySources should not be present when not configured. Body: $node")
68+
}
69+
70+
private fun retrieveEnvJsonNode(): JsonNode {
71+
val pathsToTry = arrayOf(
72+
"/env",
73+
"/env/",
74+
"/endpoints/env",
75+
"/endpoints/env/",
76+
"/management/env",
77+
"/management/env/"
78+
)
79+
var lastBody: String? = null
80+
for (path in pathsToTry) {
81+
try {
82+
val resp: HttpResponse<String> = client!!.toBlocking().exchange(HttpRequest.GET<Any>(path).accept("application/json"), String::class.java)
83+
val code = resp.status.code
84+
lastBody = resp.body.orElse(null)
85+
if (code in 200..299) {
86+
val root = mapper.readTree(lastBody ?: "{}")
87+
// some Micronaut versions wrap endpoint output (e.g. {"result":{...}} or {"value":{...}})
88+
findNodeWithActiveEnvironments(root)?.let { return it }
89+
// otherwise if root itself is an object, return it
90+
if (root.isObject) {
91+
return root
92+
}
93+
}
94+
} catch (e: Exception) {
95+
lastBody = e.message
96+
}
97+
}
98+
99+
// fallback: try to locate a known env endpoint bean and serialize it
100+
try {
101+
val ctx = server!!.applicationContext
102+
val candidateClassNames = listOf(
103+
"io.micronaut.management.endpoint.env.EnvironmentEndpoint",
104+
"io.micronaut.management.endpoint.env.EnvEndpoint",
105+
"io.micronaut.management.endpoint.env.EnvironmentController"
106+
)
107+
108+
for (className in candidateClassNames) {
109+
try {
110+
val clazz = Class.forName(className)
111+
val bean = ctx.getBean(clazz)
112+
// try to find a no-arg method that returns the payload
113+
val method = clazz.methods.firstOrNull { m -> m.parameterCount == 0 }
114+
val result = if (method != null) {
115+
method.invoke(bean)
116+
} else bean
117+
val json = mapper.writeValueAsString(result ?: mapOf<String, Any>())
118+
val root = mapper.readTree(json)
119+
findNodeWithActiveEnvironments(root)?.let { return it }
120+
if (root.isObject) return root
121+
} catch (cnf: ClassNotFoundException) {
122+
// try next
123+
}
124+
}
125+
126+
throw AssertionError("Could not locate env endpoint via HTTP or known bean classes. Last HTTP message: $lastBody")
127+
} catch (e: Exception) {
128+
throw AssertionError("Failed to retrieve env endpoint payload. Last HTTP message: $lastBody", e)
129+
}
130+
}
131+
132+
private fun findNodeWithActiveEnvironments(root: JsonNode): JsonNode? {
133+
// If the node itself contains the key, return it
134+
if (root.has("activeEnvironments")) return root
135+
// Common wrappers: value, result, data
136+
val wrapperNames = listOf("value", "result", "data", "environment")
137+
for (name in wrapperNames) {
138+
val child = root.get(name)
139+
if (child != null && child.has("activeEnvironments")) return child
140+
}
141+
// search children shallowly
142+
if (root.isObject) {
143+
val fields = root.fieldNames()
144+
while (fields.hasNext()) {
145+
val f = root.get(fields.next())
146+
if (f != null && f.has("activeEnvironments")) return f
147+
}
148+
}
149+
return null
150+
}
151+
}

0 commit comments

Comments
 (0)