ConversationScopeTest.java 2.45 KB
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<String, Object, Object> 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"));
    }
}