Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01B38FGKKam5SCGgwgduVAh3
110 lines
5.0 KiB
Java
110 lines
5.0 KiB
Java
package com.ankurm.redis;
|
|
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
|
import static org.assertj.core.api.Assertions.catchThrowable;
|
|
|
|
import com.ankurm.redis.model.User;
|
|
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
import org.springframework.beans.factory.annotation.Qualifier;
|
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
|
import org.springframework.data.redis.core.RedisTemplate;
|
|
import org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer;
|
|
import org.springframework.data.redis.serializer.SerializationException;
|
|
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
|
|
|
/**
|
|
* The "after" picture: String keys and Jackson 3 values. Writes docs/output/06-jackson-serializers.txt
|
|
* and 07-jackson-untrusted-class.txt.
|
|
*/
|
|
@RedisTest
|
|
class JacksonSerializerTest {
|
|
|
|
@Autowired
|
|
@Qualifier("userRedisTemplate")
|
|
RedisTemplate<String, User> userTemplate;
|
|
|
|
@Autowired
|
|
@Qualifier("jsonRedisTemplate")
|
|
RedisTemplate<String, Object> jsonTemplate;
|
|
|
|
@Autowired
|
|
RedisConnectionFactory factory;
|
|
|
|
@BeforeEach
|
|
void clean() {
|
|
LocalRedis.flush();
|
|
}
|
|
|
|
@Test
|
|
void typedAndGenericSerializers() {
|
|
try (Transcript t = new Transcript("06-jackson-serializers.txt",
|
|
"Jackson 3 serializers: JacksonJsonRedisSerializer and GenericJacksonJsonRedisSerializer")) {
|
|
|
|
t.section("JacksonJsonRedisSerializer<User>: one type, plain JSON");
|
|
t.line("value serializer: %s", userTemplate.getValueSerializer().getClass().getName());
|
|
userTemplate.opsForValue().set("user:1", new User(1, "Ankur"));
|
|
t.line("userTemplate.opsForValue().set(\"user:1\", new User(1, \"Ankur\"))");
|
|
t.cli("KEYS", "*");
|
|
t.cli("GET", "user:1");
|
|
User typed = userTemplate.opsForValue().get("user:1");
|
|
t.line("read back: %s (%s)", typed, typed.getClass().getSimpleName());
|
|
assertThat(typed).isEqualTo(new User(1, "Ankur"));
|
|
|
|
t.section("GenericJacksonJsonRedisSerializer with an allow-list: any type, plus @class");
|
|
t.line("value serializer: %s", jsonTemplate.getValueSerializer().getClass().getName());
|
|
jsonTemplate.opsForValue().set("user:2", new User(2, "Ankur"));
|
|
t.line("jsonTemplate.opsForValue().set(\"user:2\", new User(2, \"Ankur\"))");
|
|
t.cli("GET", "user:2");
|
|
Object generic = jsonTemplate.opsForValue().get("user:2");
|
|
t.line("read back: %s (%s)", generic, generic.getClass().getSimpleName());
|
|
assertThat(generic).isEqualTo(new User(2, "Ankur"));
|
|
|
|
t.section("GenericJacksonJsonRedisSerializer with no typing configured");
|
|
RedisTemplate<String, Object> plain = new RedisTemplate<>();
|
|
plain.setConnectionFactory(factory);
|
|
plain.setKeySerializer(StringRedisSerializer.UTF_8);
|
|
plain.setValueSerializer(GenericJacksonJsonRedisSerializer.builder().build());
|
|
plain.afterPropertiesSet();
|
|
plain.opsForValue().set("user:3", new User(3, "Ankur"));
|
|
t.line("plain.opsForValue().set(\"user:3\", new User(3, \"Ankur\"))");
|
|
t.cli("GET", "user:3");
|
|
Object untyped = plain.opsForValue().get("user:3");
|
|
t.line("read back: %s (%s)", untyped, untyped.getClass().getSimpleName());
|
|
|
|
t.section("Two different shapes of the same JSON");
|
|
t.cli("GET", "user:1");
|
|
t.cli("GET", "user:2");
|
|
}
|
|
}
|
|
|
|
@Test
|
|
void whoeverWritesToRedisChoosesTheClass() {
|
|
try (Transcript t = new Transcript("07-jackson-untrusted-class.txt",
|
|
"The @class property is input: the allow-list decides what it may name")) {
|
|
|
|
t.section("A class outside the allow-list");
|
|
t.cli("SET", "user:evil", "{\"@class\":\"com.ankurm.other.Outsider\",\"name\":\"x\"}");
|
|
t.line("jsonTemplate.opsForValue().get(\"user:evil\")");
|
|
Throwable denied = catchThrowable(() -> jsonTemplate.opsForValue().get("user:evil"));
|
|
assertThat(denied).isInstanceOf(SerializationException.class);
|
|
t.line("%s", denied.getClass().getName());
|
|
t.line(" message: %s", firstLine(denied.getMessage()));
|
|
|
|
t.section("A class inside the allow-list that no longer exists");
|
|
t.cli("SET", "user:gone", "{\"@class\":\"com.ankurm.redis.model.Gone\",\"id\":9}");
|
|
t.line("jsonTemplate.opsForValue().get(\"user:gone\")");
|
|
Throwable gone = catchThrowable(() -> jsonTemplate.opsForValue().get("user:gone"));
|
|
assertThat(gone).isInstanceOf(SerializationException.class);
|
|
t.line("%s", gone.getClass().getName());
|
|
t.line(" message: %s", firstLine(gone.getMessage()));
|
|
}
|
|
}
|
|
|
|
private static String firstLine(String message) {
|
|
return message == null ? "null" : message.lines().findFirst().orElse("");
|
|
}
|
|
}
|