Skip to content

Commit f178c78

Browse files
committed
Add tests
Signed-off-by: Dmitry Sulman <[email protected]>
1 parent 7e8cc41 commit f178c78

File tree

22 files changed

+389
-90
lines changed

22 files changed

+389
-90
lines changed

buildSrc/src/main/kotlin/conventions.gradle.kts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,10 @@ repositories {
2323
}
2424

2525
kotlin {
26-
compilerOptions {
27-
jvmTarget = JvmTarget.JVM_17
28-
}
29-
jvmToolchain(21)
26+
jvmToolchain(17)
3027
}
3128

3229
java {
33-
targetCompatibility = JavaVersion.VERSION_17
3430
withSourcesJar()
3531
}
3632

gradle/libs.versions.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ jreleaser = "1.18.0"
44
junit = "5.12.2"
55
kotest = "5.9.1"
66
kotlin = "2.1.20"
7+
kotlinxCoroutines = "1.10.2"
78
ktlint = "12.2.0"
89
logbackAccess = "2.0.6"
910
logbackClassic = "1.5.18"
@@ -18,6 +19,8 @@ junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.r
1819
junit-platformLauncher = { group = "org.junit.platform", name = "junit-platform-launcher" }
1920
kotest-assertions-core-jvm = { group = "io.kotest", name = "kotest-assertions-core-jvm", version.ref = "kotest" }
2021
kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
22+
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
23+
kotlinx-coroutines-reactor = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-reactor", version.ref = "kotlinxCoroutines" }
2124
ktlintPlugin = { group = "org.jlleitschuh.gradle.ktlint", name = "org.jlleitschuh.gradle.ktlint.gradle.plugin", version.ref = "ktlint" }
2225
logback-access-common = { group = "ch.qos.logback.access", name = "logback-access-common", version.ref = "logbackAccess" }
2326
logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logbackClassic" }

logback-access-reactor-netty/build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ dependencies {
1111

1212
testImplementation(libs.junit.jupiter)
1313
testImplementation(libs.kotest.assertions.core.jvm)
14-
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
14+
testImplementation(libs.kotlinx.coroutines.core)
15+
testImplementation(libs.kotlinx.coroutines.reactor)
1516
testImplementation(libs.logback.classic)
1617
testImplementation(libs.logback.core)
1718
testImplementation(libs.mockk)

logback-access-reactor-netty/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/AccessEvent.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package io.github.dmitrysulman.logback.access.reactor.netty
33
import ch.qos.logback.access.common.spi.AccessContext
44
import ch.qos.logback.access.common.spi.IAccessEvent
55
import jakarta.servlet.http.Cookie
6+
import jakarta.servlet.http.HttpServletRequest
7+
import jakarta.servlet.http.HttpServletResponse
68
import reactor.netty.http.server.logging.AccessLogArgProvider
79
import java.io.Serializable
810
import java.net.InetSocketAddress
@@ -122,8 +124,7 @@ class AccessEvent(
122124
?: emptyMap()
123125
}
124126

125-
@Transient
126-
private val _serverAdapter = ReactorNettyServerAdapter(argProvider)
127+
private val _serverAdapter by lazy { ReactorNettyServerAdapter(argProvider) }
127128

128129
private fun String.decodeCatching() =
129130
try {
@@ -150,11 +151,12 @@ class AccessEvent(
150151
responseHeaderMap
151152
requestHeaderMap
152153
threadName
154+
serverAdapter.requestTimestamp
153155
}
154156

155-
override fun getRequest() = null
157+
override fun getRequest(): HttpServletRequest? = null
156158

157-
override fun getResponse() = null
159+
override fun getResponse(): HttpServletResponse? = null
158160

159161
override fun getTimeStamp() = _timeStamp
160162

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package io.github.dmitrysulman.logback.access.reactor.netty
2+
3+
import reactor.netty.http.server.HttpServer
4+
5+
/**
6+
* Extension for [HttpServer] providing [HttpServer.accessLog] method alternative.
7+
*
8+
* @param reactorNettyAccessLogFactory The [ReactorNettyAccessLogFactory] instance for access log configuration.
9+
*/
10+
fun HttpServer.enableLogbackAccess(reactorNettyAccessLogFactory: ReactorNettyAccessLogFactory): HttpServer =
11+
accessLog(true, reactorNettyAccessLogFactory)

logback-access-reactor-netty/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/ReactorNettyServerAdapter.kt

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@ package io.github.dmitrysulman.logback.access.reactor.netty
22

33
import ch.qos.logback.access.common.spi.ServerAdapter
44
import reactor.netty.http.server.logging.AccessLogArgProvider
5+
import java.io.Serializable
56

67
class ReactorNettyServerAdapter(
8+
@Transient
79
private val argProvider: AccessLogArgProvider,
8-
) : ServerAdapter {
9-
override fun getRequestTimestamp() = argProvider.accessDateTime()?.toInstant()?.toEpochMilli() ?: 0
10-
11-
override fun getContentLength() = argProvider.contentLength()
12-
13-
override fun getStatusCode() = argProvider.status()?.toString()?.toIntOrNull() ?: -1
14-
15-
override fun buildResponseHeaderMap() =
10+
) : ServerAdapter,
11+
Serializable {
12+
private val _requestTimestamp by lazy { argProvider.accessDateTime()?.toInstant()?.toEpochMilli() ?: 0 }
13+
private val _contentLength by lazy { argProvider.contentLength() }
14+
private val _statusCode by lazy { argProvider.status()?.toString()?.toIntOrNull() ?: -1 }
15+
private val _responseHeaderMap by lazy {
1616
argProvider
1717
.responseHeaderIterator()
1818
?.asSequence()
@@ -22,4 +22,13 @@ class ReactorNettyServerAdapter(
2222
name.toString() to value.toString()
2323
}?.toMap()
2424
?: emptyMap()
25+
}
26+
27+
override fun getRequestTimestamp() = _requestTimestamp
28+
29+
override fun getContentLength() = _contentLength
30+
31+
override fun getStatusCode() = _statusCode
32+
33+
override fun buildResponseHeaderMap() = _responseHeaderMap
2534
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package io.github.dmitrysulman.logback.access.reactor.netty.integration;
2+
3+
import ch.qos.logback.access.common.joran.JoranConfigurator;
4+
import io.github.dmitrysulman.logback.access.reactor.netty.ReactorNettyAccessLogFactory;
5+
import org.junit.jupiter.api.Test;
6+
import reactor.core.publisher.Mono;
7+
import reactor.netty.http.client.HttpClient;
8+
import reactor.netty.http.server.HttpServer;
9+
10+
import java.time.Duration;
11+
12+
import static org.junit.jupiter.api.Assertions.assertEquals;
13+
import static org.junit.jupiter.api.Assertions.assertNotNull;
14+
15+
public class JavaIntegrationTests {
16+
@Test
17+
public void smokeTest() throws InterruptedException {
18+
var accessLogFactory =
19+
new ReactorNettyAccessLogFactory("logback-access-stdout.xml", new JoranConfigurator(), true);
20+
var eventCaptureAppender = (EventCaptureAppender) accessLogFactory.getAccessContext().getAppender("CAPTURE");
21+
var responseBody = "test";
22+
var server = HttpServer
23+
.create()
24+
.accessLog(true, accessLogFactory)
25+
.handle((request, response) -> response.sendByteArray(Mono.just(responseBody.getBytes())))
26+
.bindNow();
27+
var uri = "/test";
28+
var response = HttpClient
29+
.create()
30+
.port(server.port())
31+
.get()
32+
.uri(uri)
33+
.response()
34+
.block(Duration.ofSeconds(10));
35+
36+
assertNotNull(response);
37+
assertEquals(200, response.status().code());
38+
39+
Thread.sleep(100);
40+
assertEquals(1, eventCaptureAppender.getList().size());
41+
assertEquals(uri, eventCaptureAppender.getList().get(0).getRequestURI());
42+
assertEquals(responseBody.length(), eventCaptureAppender.getList().get(0).getContentLength());
43+
44+
server.disposeNow();
45+
}
46+
}

logback-access-reactor-netty/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/AccessEventTests.kt

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
package io.github.dmitrysulman.logback.access.reactor.netty
22

33
import ch.qos.logback.access.common.spi.AccessContext
4+
import io.kotest.matchers.booleans.shouldBeFalse
5+
import io.kotest.matchers.collections.shouldBeEmpty
46
import io.kotest.matchers.collections.shouldContain
7+
import io.kotest.matchers.longs.shouldBeZero
8+
import io.kotest.matchers.maps.shouldBeEmpty
59
import io.kotest.matchers.maps.shouldContainKey
10+
import io.kotest.matchers.nulls.shouldBeNull
11+
import io.kotest.matchers.nulls.shouldNotBeNull
612
import io.kotest.matchers.shouldBe
13+
import io.kotest.matchers.string.shouldBeEmpty
714
import io.mockk.every
815
import io.mockk.mockk
916
import io.mockk.verify
@@ -17,6 +24,9 @@ import java.io.ByteArrayOutputStream
1724
import java.io.ObjectInputStream
1825
import java.io.ObjectOutputStream
1926
import java.net.SocketAddress
27+
import java.time.Instant
28+
import java.time.ZoneId
29+
import java.time.ZonedDateTime
2030
import java.util.Collections
2131
import io.netty.handler.codec.http.cookie.Cookie as NettyCookie
2232

@@ -142,9 +152,9 @@ class AccessEventTests {
142152

143153
val accessEvent = AccessEvent(mockArgProvider, mockContext)
144154

145-
accessEvent.requestURI.isEmpty() shouldBe true
146-
accessEvent.queryString.isEmpty() shouldBe true
147-
accessEvent.requestParameterMap.isEmpty() shouldBe true
155+
accessEvent.requestURI.shouldBeEmpty()
156+
accessEvent.queryString.shouldBeEmpty()
157+
accessEvent.requestParameterMap.shouldBeEmpty()
148158
accessEvent.getRequestParameter(PARAM) shouldBe NA_ARRAY
149159
}
150160

@@ -157,7 +167,7 @@ class AccessEventTests {
157167
val accessEvent = AccessEvent(mockArgProvider, mockContext)
158168

159169
accessEvent.queryString shouldBe "?"
160-
accessEvent.requestParameterMap.isEmpty() shouldBe true
170+
accessEvent.requestParameterMap.shouldBeEmpty()
161171
accessEvent.getRequestParameter(PARAM) shouldBe NA_ARRAY
162172
}
163173

@@ -169,8 +179,8 @@ class AccessEventTests {
169179

170180
val accessEvent = AccessEvent(mockArgProvider, mockContext)
171181

172-
accessEvent.queryString.isEmpty() shouldBe true
173-
accessEvent.requestParameterMap.isEmpty() shouldBe true
182+
accessEvent.queryString.shouldBeEmpty()
183+
accessEvent.requestParameterMap.shouldBeEmpty()
174184
accessEvent.getRequestParameter(PARAM) shouldBe NA_ARRAY
175185
}
176186

@@ -227,7 +237,7 @@ class AccessEventTests {
227237
val accessEvent = AccessEvent(mockArgProvider, mockContext)
228238

229239
accessEvent.queryString shouldBe "?param1="
230-
accessEvent.requestParameterMap.isEmpty() shouldBe true
240+
accessEvent.requestParameterMap.shouldBeEmpty()
231241
accessEvent.getRequestParameter("param1") shouldBe NA_ARRAY
232242
}
233243

@@ -315,10 +325,11 @@ class AccessEventTests {
315325
accessEvent.remoteUser shouldBe NA
316326
accessEvent.getRequestHeader(HEADER) shouldBe NA
317327
accessEvent.getResponseHeader(HEADER) shouldBe NA
318-
accessEvent.requestHeaderMap.isEmpty() shouldBe true
319-
accessEvent.requestHeaderNames.hasMoreElements() shouldBe false
320-
accessEvent.responseHeaderMap.isEmpty() shouldBe true
321-
accessEvent.responseHeaderNameList.isEmpty() shouldBe true
328+
accessEvent.requestHeaderMap.shouldBeEmpty()
329+
accessEvent.requestHeaderNames.hasMoreElements().shouldBeFalse()
330+
accessEvent.responseHeaderMap.shouldBeEmpty()
331+
accessEvent.responseHeaderNameList.shouldBeEmpty()
332+
accessEvent.cookies.shouldBeEmpty()
322333
accessEvent.getCookie(COOKIE) shouldBe NA
323334
}
324335

@@ -348,6 +359,7 @@ class AccessEventTests {
348359
add(DefaultCookie("cookie2", "value21"))
349360
add(DefaultCookie("cookie2", "value22"))
350361
},
362+
"cookie3" to emptySet(),
351363
)
352364

353365
val accessEvent = AccessEvent(mockArgProvider, mockContext)
@@ -357,6 +369,7 @@ class AccessEventTests {
357369
accessEvent.cookies shouldContain Cookie("cookie2", "value21")
358370
accessEvent.getCookie("cookie1") shouldBe "value1"
359371
accessEvent.getCookie("cookie2") shouldBe "value21"
372+
accessEvent.getCookie("cookie3") shouldBe NA
360373
accessEvent.getCookie("not_exist") shouldBe NA
361374
}
362375

@@ -451,7 +464,7 @@ class AccessEventTests {
451464
accessEvent.getRequestParameter("no_param") shouldBe NA_ARRAY
452465
accessEvent.protocol shouldBe HTTP11
453466
accessEvent.requestURL shouldBe "$GET $REQUEST_URI$QUERY_STRING $HTTP11"
454-
accessEvent.statusCode shouldBe OK.toInt()
467+
accessEvent.statusCode shouldBe OK_STATUS_CODE.toInt()
455468
accessEvent.contentLength shouldBe CONTENT_LENGTH
456469
accessEvent.elapsedTime shouldBe DURATION
457470
accessEvent.elapsedSeconds shouldBe DURATION_SECONDS
@@ -460,22 +473,27 @@ class AccessEventTests {
460473
accessEvent.serverName shouldBe IP_ADDRESS
461474
accessEvent.localPort shouldBe PORT
462475
accessEvent.remoteUser shouldBe USERNAME
463-
accessEvent.sequenceNumber shouldBe 0
476+
accessEvent.sequenceNumber.shouldBeZero()
464477
accessEvent.getRequestHeader(HEADER) shouldBe NA
465478
accessEvent.getResponseHeader(HEADER) shouldBe NA
466-
accessEvent.requestHeaderMap.isEmpty() shouldBe true
467-
accessEvent.requestHeaderNames.hasMoreElements() shouldBe false
468-
accessEvent.responseHeaderMap.isEmpty() shouldBe true
469-
accessEvent.responseHeaderNameList.isEmpty() shouldBe true
470-
accessEvent.cookies.isEmpty() shouldBe true
479+
accessEvent.requestHeaderMap.shouldBeEmpty()
480+
accessEvent.requestHeaderNames.hasMoreElements().shouldBeFalse()
481+
accessEvent.responseHeaderMap.shouldBeEmpty()
482+
accessEvent.responseHeaderNameList.shouldBeEmpty()
483+
accessEvent.cookies.shouldBeEmpty()
471484
accessEvent.getCookie(COOKIE) shouldBe NA
472485
accessEvent.threadName shouldBe THREAD
473-
accessEvent.request shouldBe null
474-
accessEvent.response shouldBe null
486+
accessEvent.request.shouldBeNull()
487+
accessEvent.response.shouldBeNull()
475488
accessEvent.sessionID shouldBe NA
476489
accessEvent.getAttribute(ATTRIBUTE) shouldBe NA
477-
accessEvent.requestContent.isEmpty() shouldBe true
478-
accessEvent.responseContent.isEmpty() shouldBe true
490+
accessEvent.requestContent.shouldBeEmpty()
491+
accessEvent.responseContent.shouldBeEmpty()
492+
accessEvent.serverAdapter.shouldNotBeNull()
493+
accessEvent.serverAdapter.requestTimestamp shouldBe TIMESTAMP
494+
accessEvent.serverAdapter.contentLength shouldBe CONTENT_LENGTH
495+
accessEvent.serverAdapter.statusCode shouldBe OK_STATUS_CODE.toInt()
496+
accessEvent.serverAdapter.buildResponseHeaderMap().shouldBeEmpty()
479497
}
480498

481499
private fun mockArgProvider(mockArgProvider: AccessLogArgProvider) {
@@ -488,10 +506,11 @@ class AccessEventTests {
488506
every { mockArgProvider.method() } returns GET
489507
every { mockArgProvider.uri() } returns "$REQUEST_URI$QUERY_STRING"
490508
every { mockArgProvider.protocol() } returns HTTP11
491-
every { mockArgProvider.status() } returns OK
509+
every { mockArgProvider.status() } returns OK_STATUS_CODE
492510
every { mockArgProvider.contentLength() } returns CONTENT_LENGTH
493511
every { mockArgProvider.duration() } returns DURATION
494512
every { mockArgProvider.user() } returns USERNAME
513+
every { mockArgProvider.accessDateTime() } returns ZonedDateTime.ofInstant(Instant.ofEpochMilli(TIMESTAMP), ZoneId.of("UTC"))
495514
every { mockArgProvider.requestHeaderIterator() } returns Collections.emptyIterator()
496515
every { mockArgProvider.responseHeaderIterator() } returns Collections.emptyIterator()
497516
every { mockArgProvider.cookies() } returns emptyMap()
@@ -503,7 +522,7 @@ class AccessEventTests {
503522
private val NA_ARRAY = arrayOf(NA)
504523
private const val GET = "GET"
505524
private const val HTTP11 = "HTTP/1.1"
506-
private const val OK = "200"
525+
private const val OK_STATUS_CODE = "200"
507526
private const val IP_ADDRESS = "192.168.1.1"
508527
private const val USERNAME = "username"
509528
private const val PORT = 1000
@@ -518,5 +537,6 @@ class AccessEventTests {
518537
private const val REQUEST_URI = "/test"
519538
private const val QUERY_STRING = "?param=value"
520539
private const val DURATION_SECONDS = 1L
540+
private const val TIMESTAMP = 1746734856000
521541
}
522542
}

0 commit comments

Comments
 (0)