package com.xly.service; import com.fasterxml.jackson.databind.ObjectMapper; import com.xly.agent.AgentIdentity; import com.xly.config.RedisChatMemoryStore; import org.junit.jupiter.api.Test; import org.springframework.data.redis.core.HashOperations; import org.springframework.data.redis.core.StringRedisTemplate; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * 会话归属的**安全不变量**(审计 Critical #5):会话 id 绑定服务端身份, * 伪造他人的 conversationId 只会落到自己命名空间,且 owns() 拒绝越权访问。 */ class ConversationScopeTest { private ConversationService service(StringRedisTemplate redis) { return new ConversationService(redis, mock(RedisChatMemoryStore.class), new ObjectMapper(), mock(LedgerService.class), mock(StateService.class), new EventProjectionService(new ObjectMapper())); } private static AgentIdentity user(String id) { return new AgentIdentity("tok", id, "brand", null); } @Test void foreignConversationIdIsRemappedIntoOwnNamespace() { ConversationService s = service(mock(StringRedisTemplate.class)); String remapped = s.scopedId(user("u1"), "u2:c-123"); assertTrue(remapped.startsWith("u1:"), "别人的会话 id 必须被重挂到自己名下"); assertFalse(remapped.equals("u2:c-123"), "绝不能直接命中对方的会话键"); assertEquals("u1:c-123", s.scopedId(user("u1"), "u1:c-123"), "自己的 id 原样保留"); assertEquals("u1:default", s.scopedId(user("u1"), null)); } @Test void ownsRejectsOtherUsersConversation() { StringRedisTemplate redis = mock(StringRedisTemplate.class); @SuppressWarnings("unchecked") HashOperations hash = mock(HashOperations.class); when(redis.opsForHash()).thenReturn(hash); when(hash.hasKey(anyString(), anyString())).thenReturn(true); ConversationService s = service(redis); assertTrue(s.owns("u1", "u1:c-1")); assertFalse(s.owns("u1", "u2:c-1"), "前缀不属于本人 → 拒绝"); assertFalse(s.owns("u1", null)); assertFalse(s.owns(null, "u1:c-1")); } }