mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 03:55:09 +08:00
feat(webchat): explicit empty-session creation endpoint POST /sessions
Complements the implicit getOrCreate in /stream: lets a caller pre-create an empty thread (message_count = 0) and receive sessionId / conversationId / visitorToken up front, then decide when to send the first message via /stream. Mirrors how downstream CRM/ticketing systems model "create the conversation object first, message later". Auth is the visitor's first touch — only X-MC-Key is required (no X-MC-Visitor-Token, which the visitor can't have yet); the server signs and returns a fresh visitorToken the caller must echo back on subsequent GET/PUT/DELETE. Behavior (issue #351): - Idempotent on sessionId collision → returns the existing thread 200, does NOT clobber title. - Empty-session quota ≤ 5 per (channel, visitor); 409 with a clear message when exceeded. Existing rows are exempt (re-create is idempotent). - Caller-supplied title (1-100 chars) is persisted; absent title leaves the default "新对话" so the first /stream user message still derives it. getOrCreateWebchatConversation now accepts an optional title and only writes it on insert (existing rows untouched). - agentId override mirrors /stream's workspace check. ConversationService.getOrCreateWebchatConversation gains a title-aware overload; the original 5-arg signature delegates with title = null. End-to-end coverage in WebChatCreateSessionTest (@SpringBootTest, H2 with V147 migration): happy path, caller-title survives first user message, default-title still derived, idempotent collision, quota 409, bad API key 401, illegal sessionId/title 400, listed after creation.
This commit is contained in:
parent
bdda9c7357
commit
d84be668fa
@ -288,6 +288,114 @@ public class WebChatController {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Cap on how many empty (message_count = 0) threads one visitor may hold on a
|
||||||
|
* channel at once. Guards against pathologic clients churning placeholder
|
||||||
|
* sessions without ever sending a message. */
|
||||||
|
private static final int MAX_EMPTY_SESSIONS_PER_VISITOR = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显式创建一条访客会话线程(空会话)。
|
||||||
|
* <p>
|
||||||
|
* 与 {@code POST /stream} 的隐式 getOrCreate 互补:本端点先建一条 message_count=0
|
||||||
|
* 的占位线程,调用方拿到 {@code sessionId/conversationId/visitorToken} 之后,再决定
|
||||||
|
* 何时通过 {@code /stream} 发首条消息。鉴权为访客的<b>首次接触</b>:仅校验
|
||||||
|
* {@code X-MC-Key},不要求 {@code X-MC-Visitor-Token},后端会签发并回传 token,
|
||||||
|
* 调用方在后续 GET/PUT/DELETE 上必须回带。
|
||||||
|
* <p>
|
||||||
|
* 行为:
|
||||||
|
* <ul>
|
||||||
|
* <li>幂等:{@code sessionId} 与该 visitor 已有线程冲突 → 直接返回现有线程,
|
||||||
|
* 不报错、不覆盖 title。</li>
|
||||||
|
* <li>配额:单 (渠道, visitor) 未活跃空线程 ≤ {@value MAX_EMPTY_SESSIONS_PER_VISITOR},
|
||||||
|
* 超出返回 409。已存在的线程走幂等路径不受配额限制。</li>
|
||||||
|
* <li>title 非空时写入;为空时落默认 "新对话",首条 user 消息仍会按现有规则截取。</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@Operation(summary = "显式创建访客会话线程(空会话)")
|
||||||
|
@PostMapping("/sessions")
|
||||||
|
public R<Map<String, Object>> createSession(
|
||||||
|
@RequestHeader("X-MC-Key") String apiKey,
|
||||||
|
@RequestBody(required = false) WebChatCreateSessionRequest request) {
|
||||||
|
|
||||||
|
ChannelEntity channel = resolveChannel(apiKey);
|
||||||
|
if (channel == null) {
|
||||||
|
return R.fail(401, "Invalid API Key");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve agent: explicit request.agentId overrides channel's bound agent,
|
||||||
|
// but must belong to channel's workspace (mirrors /stream).
|
||||||
|
final Long agentId;
|
||||||
|
if (request != null && request.getAgentId() != null) {
|
||||||
|
var requested = agentService.getAgent(request.getAgentId());
|
||||||
|
if (requested == null) {
|
||||||
|
return R.fail(400, "Requested agent not found");
|
||||||
|
}
|
||||||
|
if (channel.getWorkspaceId() != null && requested.getWorkspaceId() != null
|
||||||
|
&& !channel.getWorkspaceId().equals(requested.getWorkspaceId())) {
|
||||||
|
return R.fail(400, "Requested agent does not belong to this channel's workspace");
|
||||||
|
}
|
||||||
|
agentId = request.getAgentId();
|
||||||
|
} else {
|
||||||
|
agentId = channel.getAgentId();
|
||||||
|
if (agentId == null) {
|
||||||
|
return R.fail(400, "No agent configured for this WebChat channel");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final String visitorId;
|
||||||
|
final String sessionId;
|
||||||
|
try {
|
||||||
|
visitorId = normalizeVisitorId(request != null ? request.getVisitorId() : null);
|
||||||
|
sessionId = normalizeSessionId(request != null ? request.getSessionId() : null);
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
return R.fail(400, ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
String title = (request != null && request.getTitle() != null) ? request.getTitle().trim() : null;
|
||||||
|
if (title != null && (title.isEmpty() || title.length() > 100)) {
|
||||||
|
return R.fail(400, "title 不合法(1-100 字)");
|
||||||
|
}
|
||||||
|
|
||||||
|
String conversationId = deriveConversationId(apiKey, visitorId, sessionId);
|
||||||
|
String owner = webchatUsername(visitorId);
|
||||||
|
|
||||||
|
// Idempotency: existing thread is returned as-is. Title and every other
|
||||||
|
// field are left untouched — a re-create call must not clobber a previously
|
||||||
|
// set title. Existing rows are exempt from the empty-session quota.
|
||||||
|
ConversationEntity existing = conversationService.findByConversationId(conversationId);
|
||||||
|
if (existing != null && owner.equals(existing.getUsername())) {
|
||||||
|
return R.ok(buildCreateSessionResponse(existing, sessionId, channel.getId(), visitorId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quota: count empty threads this visitor already holds on this channel.
|
||||||
|
// loadVisitorSessions already scopes to (channel prefix ∩ visitor owner).
|
||||||
|
long emptyCount = loadVisitorSessions(apiKey, visitorId).stream()
|
||||||
|
.filter(s -> s.getMessageCount() == null || s.getMessageCount() == 0)
|
||||||
|
.count();
|
||||||
|
if (emptyCount >= MAX_EMPTY_SESSIONS_PER_VISITOR) {
|
||||||
|
return R.fail(409, "未活跃会话数已达上限(" + MAX_EMPTY_SESSIONS_PER_VISITOR
|
||||||
|
+ "),请先发送消息或删除旧会话");
|
||||||
|
}
|
||||||
|
|
||||||
|
ConversationEntity conv = conversationService.getOrCreateWebchatConversation(
|
||||||
|
conversationId, agentId, owner, channel.getWorkspaceId(), sessionId, title);
|
||||||
|
return R.ok(buildCreateSessionResponse(conv, sessionId, channel.getId(), visitorId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> buildCreateSessionResponse(ConversationEntity conv, String sessionId,
|
||||||
|
Long channelId, String visitorId) {
|
||||||
|
String visitorToken = computeVisitorToken(visitorTokenSecret, channelId, visitorId);
|
||||||
|
// LinkedHashMap (not Map.of) because Map.of rejects null and we want a
|
||||||
|
// stable key order for the response payload.
|
||||||
|
Map<String, Object> m = new java.util.LinkedHashMap<>();
|
||||||
|
m.put("sessionId", sessionId != null ? sessionId : "");
|
||||||
|
m.put("conversationId", conv.getConversationId());
|
||||||
|
m.put("visitorToken", visitorToken);
|
||||||
|
m.put("title", conv.getTitle() != null ? conv.getTitle() : "");
|
||||||
|
m.put("createTime", conv.getCreateTime());
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 列出某访客的会话线程
|
* 列出某访客的会话线程
|
||||||
* <p>
|
* <p>
|
||||||
@ -889,4 +997,20 @@ public class WebChatController {
|
|||||||
private LocalDateTime lastActiveTime;
|
private LocalDateTime lastActiveTime;
|
||||||
private Integer messageCount;
|
private Integer messageCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Body for {@code POST /sessions} — explicitly create an empty thread. */
|
||||||
|
@lombok.Data
|
||||||
|
public static class WebChatCreateSessionRequest {
|
||||||
|
/** Optional; server mints a UUID when absent (same convention as /stream). */
|
||||||
|
private String visitorId;
|
||||||
|
/** Optional; server generates one when absent. Whitelisted charset, ≤ 64 chars. */
|
||||||
|
private String sessionId;
|
||||||
|
/** Optional; 1–100 chars when non-blank, otherwise left null so the first
|
||||||
|
* /stream message still derives the title (mirrors PUT /sessions/title rules). */
|
||||||
|
private String title;
|
||||||
|
/** Optional; override the channel's bound agent. Must belong to the channel's
|
||||||
|
* workspace. Only applied on first creation — once the thread exists, a
|
||||||
|
* different agentId is ignored. */
|
||||||
|
private Long agentId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -310,12 +310,40 @@ public class ConversationService {
|
|||||||
public ConversationEntity getOrCreateWebchatConversation(String conversationId, Long agentId,
|
public ConversationEntity getOrCreateWebchatConversation(String conversationId, Long agentId,
|
||||||
String username, Long workspaceId,
|
String username, Long workspaceId,
|
||||||
String sessionId) {
|
String sessionId) {
|
||||||
|
return getOrCreateWebchatConversation(conversationId, agentId, username, workspaceId, sessionId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebChat get-or-create with an optional caller-supplied title.
|
||||||
|
* <p>
|
||||||
|
* When the row is freshly inserted and {@code title} is non-blank, it
|
||||||
|
* overrides the default {@code "新对话"}; otherwise the default is kept and
|
||||||
|
* {@link #saveMessage} will still derive a title from the first user
|
||||||
|
* message. An existing row is never rewritten — neither {@code sessionId}
|
||||||
|
* nor {@code title} are clobbered, so a session created via
|
||||||
|
* {@code POST /sessions} with a caller-supplied title keeps that title
|
||||||
|
* when the first {@code /stream} message later lands.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public ConversationEntity getOrCreateWebchatConversation(String conversationId, Long agentId,
|
||||||
|
String username, Long workspaceId,
|
||||||
|
String sessionId, String title) {
|
||||||
boolean existed = conversationMapper.selectOne(new LambdaQueryWrapper<ConversationEntity>()
|
boolean existed = conversationMapper.selectOne(new LambdaQueryWrapper<ConversationEntity>()
|
||||||
.eq(ConversationEntity::getConversationId, conversationId)) != null;
|
.eq(ConversationEntity::getConversationId, conversationId)) != null;
|
||||||
ConversationEntity conv = getOrCreateConversation(conversationId, agentId, username, workspaceId);
|
ConversationEntity conv = getOrCreateConversation(conversationId, agentId, username, workspaceId);
|
||||||
if (!existed && sessionId != null && !sessionId.isBlank() && conv.getWebchatSessionId() == null) {
|
if (!existed) {
|
||||||
conv.setWebchatSessionId(sessionId);
|
boolean dirty = false;
|
||||||
conversationMapper.updateById(conv);
|
if (sessionId != null && !sessionId.isBlank() && conv.getWebchatSessionId() == null) {
|
||||||
|
conv.setWebchatSessionId(sessionId);
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
if (title != null && !title.isBlank()) {
|
||||||
|
conv.setTitle(title.trim());
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
if (dirty) {
|
||||||
|
conversationMapper.updateById(conv);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return conv;
|
return conv;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,204 @@
|
|||||||
|
package vip.mate.channel.webchat;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.test.context.TestPropertySource;
|
||||||
|
import vip.mate.MateClawApplication;
|
||||||
|
import vip.mate.channel.webchat.WebChatController.WebChatCreateSessionRequest;
|
||||||
|
import vip.mate.common.result.R;
|
||||||
|
import vip.mate.workspace.conversation.ConversationService;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* End-to-end verification of {@code POST /api/v1/channels/webchat/sessions}
|
||||||
|
* (explicit empty-session creation) against a booted context + real H2 with
|
||||||
|
* migrations (incl. V147 {@code webchat_session_id}) applied.
|
||||||
|
* <p>
|
||||||
|
* Covers the four behaviors promised in issue #351:
|
||||||
|
* <ol>
|
||||||
|
* <li>happy path inserts an empty thread and returns sessionId/conversationId/
|
||||||
|
* visitorToken;</li>
|
||||||
|
* <li>a caller-supplied title is persisted and survives the first /stream
|
||||||
|
* user message (saveMessage's "title-derive" guard must not fire);</li>
|
||||||
|
* <li>re-creating with a colliding sessionId is idempotent — 200, no title
|
||||||
|
* clobber;</li>
|
||||||
|
* <li>the empty-session quota (≤ 5) is enforced with a clear 409.</li>
|
||||||
|
* </ol>
|
||||||
|
*/
|
||||||
|
@SpringBootTest(
|
||||||
|
classes = MateClawApplication.class,
|
||||||
|
webEnvironment = SpringBootTest.WebEnvironment.NONE
|
||||||
|
)
|
||||||
|
@TestPropertySource(properties = {
|
||||||
|
"spring.datasource.url=jdbc:h2:mem:webchat_create_sess_${random.uuid};MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE;DB_CLOSE_DELAY=-1",
|
||||||
|
"spring.ai.dashscope.api-key=test-key",
|
||||||
|
"spring.main.web-application-type=none",
|
||||||
|
"mateclaw.jwt.secret=webchat-it-secret-0123456789"
|
||||||
|
})
|
||||||
|
class WebChatCreateSessionTest {
|
||||||
|
|
||||||
|
private static final String SECRET = "webchat-it-secret-0123456789";
|
||||||
|
private static final String API_KEY = "testkey1abcdefgh"; // key8 = "testkey1"
|
||||||
|
private static final long CHANNEL_ID = 9_147_101L;
|
||||||
|
private static final long AGENT_ID = 9_147_1011L;
|
||||||
|
|
||||||
|
@Autowired private WebChatController controller;
|
||||||
|
@Autowired private ConversationService conversationService;
|
||||||
|
@Autowired private JdbcTemplate jdbc;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
jdbc.update("DELETE FROM mate_channel WHERE id = ?", CHANNEL_ID);
|
||||||
|
jdbc.update("DELETE FROM mate_agent WHERE id = ?", AGENT_ID);
|
||||||
|
jdbc.update(
|
||||||
|
"MERGE INTO mate_agent (id, name, agent_type, system_prompt, max_iterations, enabled, " +
|
||||||
|
"workspace_id, create_time, update_time, deleted) " +
|
||||||
|
"KEY(id) VALUES (?, 'wc-test-agent', 'react', '', 10, TRUE, 1, " +
|
||||||
|
"CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 0)",
|
||||||
|
AGENT_ID);
|
||||||
|
jdbc.update("INSERT INTO mate_channel (id, name, channel_type, agent_id, config_json, enabled, " +
|
||||||
|
"workspace_id, create_time, update_time, deleted) " +
|
||||||
|
"VALUES (?, 'wc', 'webchat', ?, ?, TRUE, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 0)",
|
||||||
|
CHANNEL_ID, AGENT_ID, "{\"api_key\":\"" + API_KEY + "\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebChatCreateSessionRequest req(String visitorId, String sessionId, String title) {
|
||||||
|
WebChatCreateSessionRequest r = new WebChatCreateSessionRequest();
|
||||||
|
r.setVisitorId(visitorId);
|
||||||
|
r.setSessionId(sessionId);
|
||||||
|
r.setTitle(title);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("createSession inserts an empty thread and returns all required fields")
|
||||||
|
void createsEmptySession() {
|
||||||
|
R<Map<String, Object>> r = controller.createSession(API_KEY, req("visitorA", "s1", null));
|
||||||
|
assertThat(r.getCode()).isEqualTo(200);
|
||||||
|
Map<String, Object> data = r.getData();
|
||||||
|
assertThat(data.get("sessionId")).isEqualTo("s1");
|
||||||
|
assertThat(data.get("conversationId"))
|
||||||
|
.isEqualTo(WebChatController.deriveConversationId(API_KEY, "visitorA", "s1"));
|
||||||
|
assertThat(data.get("visitorToken"))
|
||||||
|
.isEqualTo(WebChatController.computeVisitorToken(SECRET, CHANNEL_ID, "visitorA"));
|
||||||
|
// No title supplied → default placeholder, will be derived from first user message later.
|
||||||
|
assertThat(data.get("title")).isEqualTo("新对话");
|
||||||
|
assertThat(data.get("createTime")).isNotNull();
|
||||||
|
|
||||||
|
// Row actually persisted with message_count = 0.
|
||||||
|
Integer count = jdbc.queryForObject(
|
||||||
|
"SELECT message_count FROM mate_conversation WHERE conversation_id = ?",
|
||||||
|
Integer.class, data.get("conversationId"));
|
||||||
|
assertThat(count).isZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("caller-supplied title survives the first /stream user message")
|
||||||
|
void titleSurvivesFirstMessage() {
|
||||||
|
String cid = (String) controller
|
||||||
|
.createSession(API_KEY, req("visitorB", "s-title", "Quarterly Report"))
|
||||||
|
.getData().get("conversationId");
|
||||||
|
|
||||||
|
// Simulate /stream saving the first user message.
|
||||||
|
conversationService.saveMessage(cid, "user", "随便说点什么,看看会不会把 title 覆盖掉");
|
||||||
|
|
||||||
|
String persisted = jdbc.queryForObject(
|
||||||
|
"SELECT title FROM mate_conversation WHERE conversation_id = ?",
|
||||||
|
String.class, cid);
|
||||||
|
assertThat(persisted).isEqualTo("Quarterly Report");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("default-title thread still derives its title from the first user message")
|
||||||
|
void defaultTitleIsDerivedFromFirstMessage() {
|
||||||
|
String cid = (String) controller
|
||||||
|
.createSession(API_KEY, req("visitorC", "s-default", null))
|
||||||
|
.getData().get("conversationId");
|
||||||
|
|
||||||
|
conversationService.saveMessage(cid, "user", "今天天气不错");
|
||||||
|
|
||||||
|
String persisted = jdbc.queryForObject(
|
||||||
|
"SELECT title FROM mate_conversation WHERE conversation_id = ?",
|
||||||
|
String.class, cid);
|
||||||
|
assertThat(persisted).isEqualTo("今天天气不错");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("re-create with colliding sessionId is idempotent — no title clobber")
|
||||||
|
void isIdempotentOnCollision() {
|
||||||
|
// First call creates with a caller title.
|
||||||
|
controller.createSession(API_KEY, req("visitorD", "s-collide", "OriginalTitle"));
|
||||||
|
|
||||||
|
// Second call tries to re-create the same sessionId with a different title.
|
||||||
|
R<Map<String, Object>> r = controller
|
||||||
|
.createSession(API_KEY, req("visitorD", "s-collide", "AttemptedOverride"));
|
||||||
|
assertThat(r.getCode()).isEqualTo(200);
|
||||||
|
assertThat(r.getData().get("title")).isEqualTo("OriginalTitle");
|
||||||
|
|
||||||
|
String persisted = jdbc.queryForObject(
|
||||||
|
"SELECT title FROM mate_conversation WHERE conversation_id = ?",
|
||||||
|
String.class, r.getData().get("conversationId"));
|
||||||
|
assertThat(persisted).isEqualTo("OriginalTitle");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("empty-session quota (≤ 5) is enforced with a 409")
|
||||||
|
void enforcesQuota() {
|
||||||
|
// Pre-seed 5 empty threads directly through the service (bypasses the controller
|
||||||
|
// quota so we can verify the controller is the gate, not the service).
|
||||||
|
String owner = WebChatController.webchatUsername("visitorE");
|
||||||
|
for (int i = 1; i <= 5; i++) {
|
||||||
|
conversationService.getOrCreateWebchatConversation(
|
||||||
|
WebChatController.deriveConversationId(API_KEY, "visitorE", "seed" + i),
|
||||||
|
null, owner, 1L, "seed" + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
R<Map<String, Object>> r = controller.createSession(API_KEY, req("visitorE", "s-new", null));
|
||||||
|
assertThat(r.getCode()).isEqualTo(409);
|
||||||
|
assertThat(r.getMsg()).contains("未活跃会话数已达上限");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("bad API Key → 401")
|
||||||
|
void rejectsBadApiKey() {
|
||||||
|
R<Map<String, Object>> r = controller.createSession("bogus-key", req("visitorF", "s1", null));
|
||||||
|
assertThat(r.getCode()).isEqualTo(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("illegal sessionId charset → 400")
|
||||||
|
void rejectsIllegalSessionId() {
|
||||||
|
R<Map<String, Object>> r = controller
|
||||||
|
.createSession(API_KEY, req("visitorG", "has space", null));
|
||||||
|
assertThat(r.getCode()).isEqualTo(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("illegal title length (>100) → 400")
|
||||||
|
void rejectsOverlongTitle() {
|
||||||
|
R<Map<String, Object>> r = controller
|
||||||
|
.createSession(API_KEY, req("visitorH", "s1", "x".repeat(101)));
|
||||||
|
assertThat(r.getCode()).isEqualTo(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("once a session is created, listSessions sees it (with the recovered sessionId)")
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
void createdSessionIsListable() {
|
||||||
|
controller.createSession(API_KEY, req("visitorI", "s-listed", null));
|
||||||
|
|
||||||
|
String token = WebChatController.computeVisitorToken(SECRET, CHANNEL_ID, "visitorI");
|
||||||
|
R<?> r = controller.listSessions(API_KEY, token, "visitorI");
|
||||||
|
assertThat(r.getCode()).isEqualTo(200);
|
||||||
|
assertThat(((java.util.List<WebChatController.WebChatSessionView>) (Object) r.getData()))
|
||||||
|
.extracting(WebChatController.WebChatSessionView::getSessionId)
|
||||||
|
.contains("s-listed");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user