diff --git a/README.md b/README.md index 566eae6..c04fd84 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,9 @@ dependencies { } ``` -This library comes with a default implementation for both jedis and lettuce. +This library comes with a default implementation +for [Jedis](https://github.com/redis/jedis),[Lettuce](https://lettuce.io/) +and [Spring Data Redis](https://spring.io/projects/spring-data-redis/). ### Emit Cheatsheet @@ -81,19 +83,6 @@ The [example](example) directory contains a working docker-compose setup which c using `docker-compose --compatibility up`. The setup contains one redis instance, one java publisher, three socket.io-servers and three consuming socket.io-clients. -### Usage in Spring Boot and Jedis - -We don't want to rely on the Spring Data Redis abstractions in this project. -Unfortunately, this makes it necessary that you configure the `JedisPool` manually in your Spring Boot application. -Having JedisPool configured, the emitter can be created as follows: - -```kotlin -@Bean -fun emitter(jedisPool: JedisPool): Emitter { - return Emitter(JedisPublisher(jedisPool)) -} -``` - ## :warning: Limitations - The room and namespaces have not been tested yet. diff --git a/build.gradle b/build.gradle index 4e796c2..9381720 100644 --- a/build.gradle +++ b/build.gradle @@ -18,6 +18,9 @@ buildscript { gradleVersionsVersion = "0.47.0" jedisVersion = "4.4.3" lettuceVersion = "6.2.6.RELEASE" + springDataVersion = "2.7.18" + testcontainersVersion= "1.19.3" + testcontainersRedisVersion= "2.0.1" } configurations.classpath { @@ -61,12 +64,18 @@ dependencies { testImplementation "org.amshove.kluent:kluent:$kluentVersion" testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion" testImplementation "org.skyscreamer:jsonassert:$jsonAssertVersion" + testImplementation "org.testcontainers:junit-jupiter:$testcontainersVersion" + testImplementation "com.redis:testcontainers-redis:$testcontainersRedisVersion" + testImplementation "redis.clients:jedis:$jedisVersion" + testImplementation "io.lettuce:lettuce-core:$lettuceVersion" + testImplementation "org.springframework.boot:spring-boot-starter-data-redis:$springDataVersion" testRuntimeOnly "net.bytebuddy:byte-buddy:$byteBuddyVersion" compileOnly "redis.clients:jedis:$jedisVersion" compileOnly "io.lettuce:lettuce-core:$lettuceVersion" + compileOnly "org.springframework.boot:spring-boot-starter-data-redis:$springDataVersion" } java { diff --git a/src/main/kotlin/de/smartsquare/socketio/emitter/LettucePublisher.kt b/src/main/kotlin/de/smartsquare/socketio/emitter/LettucePublisher.kt index 70a29bd..8674d95 100644 --- a/src/main/kotlin/de/smartsquare/socketio/emitter/LettucePublisher.kt +++ b/src/main/kotlin/de/smartsquare/socketio/emitter/LettucePublisher.kt @@ -1,9 +1,9 @@ package de.smartsquare.socketio.emitter -import io.lettuce.core.api.StatefulRedisConnection +import io.lettuce.core.api.sync.RedisCommands -class LettucePublisher(private val connection: StatefulRedisConnection) : RedisPublisher { +class LettucePublisher(private val commands: RedisCommands) : RedisPublisher { override fun publish(channel: String, message: ByteArray) { - connection.use { it.sync().publish(channel, message.decodeToString()) } + commands.publish(channel, message.decodeToString()) } } diff --git a/src/main/kotlin/de/smartsquare/socketio/emitter/SpringDataPublisher.kt b/src/main/kotlin/de/smartsquare/socketio/emitter/SpringDataPublisher.kt new file mode 100644 index 0000000..d755d65 --- /dev/null +++ b/src/main/kotlin/de/smartsquare/socketio/emitter/SpringDataPublisher.kt @@ -0,0 +1,9 @@ +package de.smartsquare.socketio.emitter + +import org.springframework.data.redis.core.RedisTemplate + +class SpringDataPublisher(private val redisTemplate: RedisTemplate) : RedisPublisher { + override fun publish(channel: String, message: ByteArray) { + redisTemplate.execute({ it.publish(channel.toByteArray(), message) }, true) + } +} diff --git a/src/test/kotlin/de/smartsquare/socketio/emitter/JedisPublisherTest.kt b/src/test/kotlin/de/smartsquare/socketio/emitter/JedisPublisherTest.kt new file mode 100644 index 0000000..9bfcced --- /dev/null +++ b/src/test/kotlin/de/smartsquare/socketio/emitter/JedisPublisherTest.kt @@ -0,0 +1,42 @@ +package de.smartsquare.socketio.emitter + +import com.redis.testcontainers.RedisContainer +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldContain +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.testcontainers.junit.jupiter.Container +import org.testcontainers.junit.jupiter.Testcontainers +import redis.clients.jedis.JedisPool + +@Testcontainers +class JedisPublisherTest { + + @Container + private val redis = RedisContainer("redis:6-alpine") + + private lateinit var pool: JedisPool + + @BeforeEach + fun setUp() { + pool = JedisPool(redis.redisURI) + } + + @AfterEach + fun tearDown() { + pool.close() + } + + @Test + fun `publish string message`() { + val publisher = Emitter(JedisPublisher(pool)) + + val (channel, message) = awaitRedisMessage(redis.redisURI, "socket.io#/#") { + publisher.broadcast("topic", "test 123") + } + + channel shouldBeEqualTo "socket.io#/#" + message shouldContain "test 123" + } +} diff --git a/src/test/kotlin/de/smartsquare/socketio/emitter/LettucePublisherTest.kt b/src/test/kotlin/de/smartsquare/socketio/emitter/LettucePublisherTest.kt new file mode 100644 index 0000000..691b12c --- /dev/null +++ b/src/test/kotlin/de/smartsquare/socketio/emitter/LettucePublisherTest.kt @@ -0,0 +1,42 @@ +package de.smartsquare.socketio.emitter + +import com.redis.testcontainers.RedisContainer +import io.lettuce.core.RedisClient +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldContain +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.testcontainers.junit.jupiter.Container +import org.testcontainers.junit.jupiter.Testcontainers + +@Testcontainers +class LettucePublisherTest { + + @Container + private val redis = RedisContainer("redis:6-alpine") + + private lateinit var client: RedisClient + + @BeforeEach + fun setUp() { + client = RedisClient.create(redis.redisURI) + } + + @AfterEach + fun tearDown() { + client.close() + } + + @Test + fun `publish string message`() { + val publisher = Emitter(LettucePublisher(client.connect().sync())) + + val (channel, message) = awaitRedisMessage(redis.redisURI, "socket.io#/#") { + publisher.broadcast("topic", "test 123") + } + + channel shouldBeEqualTo "socket.io#/#" + message shouldContain "test 123" + } +} diff --git a/src/test/kotlin/de/smartsquare/socketio/emitter/SpringDataPublisherTest.kt b/src/test/kotlin/de/smartsquare/socketio/emitter/SpringDataPublisherTest.kt new file mode 100644 index 0000000..258a31d --- /dev/null +++ b/src/test/kotlin/de/smartsquare/socketio/emitter/SpringDataPublisherTest.kt @@ -0,0 +1,51 @@ +package de.smartsquare.socketio.emitter + +import com.redis.testcontainers.RedisContainer +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldContain +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory +import org.springframework.data.redis.core.StringRedisTemplate +import org.testcontainers.junit.jupiter.Container +import org.testcontainers.junit.jupiter.Testcontainers + +@Testcontainers +class SpringDataPublisherTest { + + @Container + private val redis = RedisContainer("redis:6-alpine") + + private lateinit var lettuceConnectionFactory: LettuceConnectionFactory + private lateinit var template: StringRedisTemplate + + @BeforeEach + fun setUp() { + lettuceConnectionFactory = LettuceConnectionFactory(redis.host, redis.firstMappedPort).apply { + afterPropertiesSet() + } + + template = StringRedisTemplate().apply { + connectionFactory = lettuceConnectionFactory + afterPropertiesSet() + } + } + + @AfterEach + fun tearDown() { + lettuceConnectionFactory.destroy() + } + + @Test + fun `publish string message`() { + val publisher = Emitter(SpringDataPublisher(template)) + + val (channel, message) = awaitRedisMessage(redis.redisURI, "socket.io#/#") { + publisher.broadcast("topic", "test 123") + } + + channel shouldBeEqualTo "socket.io#/#" + message shouldContain "test 123" + } +} diff --git a/src/test/kotlin/de/smartsquare/socketio/emitter/TestUtils.kt b/src/test/kotlin/de/smartsquare/socketio/emitter/TestUtils.kt index 0aa2592..cd3388e 100644 --- a/src/test/kotlin/de/smartsquare/socketio/emitter/TestUtils.kt +++ b/src/test/kotlin/de/smartsquare/socketio/emitter/TestUtils.kt @@ -1,5 +1,43 @@ package de.smartsquare.socketio.emitter +import io.lettuce.core.RedisClient +import io.lettuce.core.pubsub.RedisPubSubAdapter +import org.amshove.kluent.shouldBeTrue import org.skyscreamer.jsonassert.JSONAssert +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit infix fun String.shouldBeEqualToJson(expected: String): String = apply { JSONAssert.assertEquals(expected, this, true) } + +/** + * Returns the first message published on the given [channel] of the redis available under the [redisURI]. Waits for at + * most five seconds after executing [body] for the message to arrive. + */ +fun awaitRedisMessage(redisURI: String, channel: String, body: () -> Unit): RedisMessage { + return RedisClient.create(redisURI).use { client -> + var result: RedisMessage? = null + + val countDownLatch = CountDownLatch(1) + + val listener = object : RedisPubSubAdapter() { + override fun message(channel: String, message: String) { + result = RedisMessage(channel, message) + + countDownLatch.countDown() + } + } + + client.connectPubSub().apply { + addListener(listener) + sync().subscribe(channel) + } + + body() + + countDownLatch.await(5, TimeUnit.SECONDS).shouldBeTrue() + + result!! + } +} + +data class RedisMessage(val channel: String, val message: String) diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml new file mode 100644 index 0000000..40ee1ab --- /dev/null +++ b/src/test/resources/logback-test.xml @@ -0,0 +1,17 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n + + + + + + + + + + + + +