mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
test(webchat): HTTP e2e coverage for attachment upload/stream/download
Adds WebChatAttachmentE2ETest — second HTTP-level test in the webchat suite. Boots RANDOM_PORT, drives Spring's multipart parser for real, and verifies the cross-endpoint wiring that turns an upload into an agent-addressable attachment. Coverage (7 tests): - upload + /stream round-trip: persisted user message's content_parts carries the file part with a server path that points into the conversation's upload dir; bytes on disk match what was uploaded - unknown attachmentId → silently dropped (no error, text-only parts) - foreign visitor cannot reference another visitor's fileId (consume() is conversation-scoped) - upload without visitorToken → HTTP 401 - upload with disallowed extension → HTTP 400 - GET /files streams back the uploaded bytes - GET /files without visitorToken → HTTP 401 AgentService is @MockBean'd so /stream returns instantly; what we assert is the persisted user-message shape (DB row content_parts), not the agent's actual file consumption (which would need a real agent + tool runtime — out of scope for the wire-format focus). Worth noting: RHttpStatusAdvice maps R.fail(401/400) to the matching HTTP status, so 4xx assertions are on the HTTP status, not the body. Stack: feat/webchat-stream-e2e-test → feat/webchat-attachment-e2e Follow-up to PR #363.
This commit is contained in:
parent
59713a7215
commit
a0598fb0b8
@ -0,0 +1,418 @@
|
||||
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.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import reactor.core.publisher.Flux;
|
||||
import vip.mate.MateClawApplication;
|
||||
import vip.mate.agent.AgentService;
|
||||
import vip.mate.agent.model.AgentEntity;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
|
||||
/**
|
||||
* End-to-end HTTP coverage of the WebChat attachment flow (epic #355 follow-up):
|
||||
* <ol>
|
||||
* <li>{@code POST /upload} stores bytes under the server-derived conversation dir
|
||||
* and returns an opaque {@code fileId}; the bytes are addressable by the
|
||||
* agent through the conversation's upload path;</li>
|
||||
* <li>{@code POST /stream} with {@code attachmentIds=[fileId]} resolves that id
|
||||
* back to a server path and persists a user message whose {@code content_parts}
|
||||
* carry the path so the agent's file tools can read it on the next turn.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>Boots a real servlet container (RANDOM_PORT), drives the actual multipart
|
||||
* parser, and asserts on the persisted {@code mate_message.content_parts} JSON —
|
||||
* the in-memory {@link WebChatFileServiceTest} already covers the service's
|
||||
* validation, so this layer's job is the cross-endpoint wiring + auth contract.
|
||||
*
|
||||
* <p>{@link AgentService} is mocked (same pattern as {@link WebChatStreamE2ETest})
|
||||
* so /stream returns immediately without invoking a real agent — what we are
|
||||
* asserting is the persisted user-message shape, not the agent's actual file
|
||||
* consumption (which would need a real agent + tool runtime).
|
||||
*
|
||||
* @author MateClaw Team
|
||||
*/
|
||||
@SpringBootTest(
|
||||
classes = MateClawApplication.class,
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
|
||||
)
|
||||
@TestPropertySource(properties = {
|
||||
"spring.datasource.url=jdbc:h2:mem:webchat_att_e2e_${random.uuid};MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE;DB_CLOSE_DELAY=-1",
|
||||
"spring.ai.dashscope.api-key=test-key",
|
||||
"mateclaw.jwt.secret=webchat-it-secret-0123456789",
|
||||
"mateclaw.feature-flag.refresh-ms=999999"
|
||||
})
|
||||
class WebChatAttachmentE2ETest {
|
||||
|
||||
private static final String SECRET = "webchat-it-secret-0123456789";
|
||||
private static final String API_KEY = "testkey1atte2e01"; // key8 = "testkey1"
|
||||
private static final long CHANNEL_ID = 9_148_101L;
|
||||
private static final long AGENT_ID = 9_148_1011L;
|
||||
private static final Duration HTTP_TIMEOUT = Duration.ofSeconds(15);
|
||||
|
||||
/**
|
||||
* Wipe the upload dir for our test conversation ids. The path is deterministic
|
||||
* from the API key prefix, so we can clean it precisely instead of nuking
|
||||
* the whole {@code data/chat-uploads} tree.
|
||||
*/
|
||||
private static final Path UPLOAD_ROOT = Paths.get("data", "chat-uploads");
|
||||
|
||||
@LocalServerPort private int port;
|
||||
@Autowired private JdbcTemplate jdbc;
|
||||
|
||||
@MockBean private AgentService agentService;
|
||||
|
||||
private HttpClient http;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
http = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
|
||||
|
||||
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-att-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 + "\"}");
|
||||
|
||||
// chatStructuredStream: instant completion so /stream returns fast.
|
||||
AgentEntity agent = new AgentEntity();
|
||||
agent.setId(AGENT_ID);
|
||||
agent.setWorkspaceId(1L);
|
||||
org.mockito.Mockito.when(agentService.getAgent(AGENT_ID)).thenReturn(agent);
|
||||
org.mockito.Mockito.when(agentService.chatStructuredStream(
|
||||
eq(AGENT_ID), anyString(), anyString(), anyString(), isNull(), any()))
|
||||
.thenReturn(Flux.just(new AgentService.StreamDelta("ack", null)));
|
||||
}
|
||||
|
||||
// ==================== multipart helpers ====================
|
||||
|
||||
/** Build a multipart/form-data body with one file part plus arbitrary form fields. */
|
||||
private static byte[] multipart(String boundary, Map<String, String> fields,
|
||||
String fileField, String fileName, String contentType, byte[] fileBytes) {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
String crlf = "\r\n";
|
||||
String dash = "--";
|
||||
try {
|
||||
for (var e : fields.entrySet()) {
|
||||
write(bos, dash + boundary + crlf);
|
||||
write(bos, "Content-Disposition: form-data; name=\"" + e.getKey() + "\"" + crlf);
|
||||
write(bos, crlf);
|
||||
write(bos, e.getValue() + crlf);
|
||||
}
|
||||
write(bos, dash + boundary + crlf);
|
||||
write(bos, "Content-Disposition: form-data; name=\"" + fileField + "\"; filename=\""
|
||||
+ fileName + "\"" + crlf);
|
||||
write(bos, "Content-Type: " + contentType + crlf);
|
||||
write(bos, crlf);
|
||||
bos.write(fileBytes);
|
||||
write(bos, crlf);
|
||||
write(bos, dash + boundary + dash + crlf);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
return bos.toByteArray();
|
||||
}
|
||||
|
||||
private static void write(ByteArrayOutputStream bos, String s) throws IOException {
|
||||
bos.write(s.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
// ==================== HTTP helpers ====================
|
||||
|
||||
private URI uploadUri() {
|
||||
return URI.create("http://localhost:" + port + "/api/v1/channels/webchat/upload");
|
||||
}
|
||||
|
||||
private URI streamUri() {
|
||||
return URI.create("http://localhost:" + port + "/api/v1/channels/webchat/stream");
|
||||
}
|
||||
|
||||
private URI filesUri(String storedName, String visitorId, String sessionId) {
|
||||
StringBuilder sb = new StringBuilder("http://localhost:" + port + "/api/v1/channels/webchat/files");
|
||||
sb.append("?storedName=").append(java.net.URLEncoder.encode(storedName, StandardCharsets.UTF_8));
|
||||
sb.append("&visitorId=").append(java.net.URLEncoder.encode(visitorId, StandardCharsets.UTF_8));
|
||||
if (sessionId != null) {
|
||||
sb.append("&sessionId=").append(java.net.URLEncoder.encode(sessionId, StandardCharsets.UTF_8));
|
||||
}
|
||||
return URI.create(sb.toString());
|
||||
}
|
||||
|
||||
private String tokenFor(String visitorId) {
|
||||
return WebChatController.computeVisitorToken(SECRET, CHANNEL_ID, visitorId);
|
||||
}
|
||||
|
||||
/** Upload, return the parsed fileId (or fail loudly if the response isn't 200). */
|
||||
private String upload(String visitorId, String sessionId, String fileName,
|
||||
String contentType, byte[] bytes) throws Exception {
|
||||
String boundary = "----mcboundary" + System.nanoTime();
|
||||
String token = tokenFor(visitorId);
|
||||
Map<String, String> fields = new java.util.LinkedHashMap<>();
|
||||
fields.put("visitorId", visitorId);
|
||||
if (sessionId != null) fields.put("sessionId", sessionId);
|
||||
byte[] body = multipart(boundary, fields, "file", fileName, contentType, bytes);
|
||||
|
||||
HttpRequest req = HttpRequest.newBuilder()
|
||||
.uri(uploadUri())
|
||||
.timeout(HTTP_TIMEOUT)
|
||||
.header("X-MC-Key", API_KEY)
|
||||
.header("X-MC-Visitor-Token", token)
|
||||
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
|
||||
.POST(HttpRequest.BodyPublishers.ofByteArray(body))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
assertThat(resp.statusCode()).as("upload response: %s", resp.body()).isEqualTo(200);
|
||||
// Response shape: {"code":200,...,"data":{"fileId":"...","fileName":"...",...}}
|
||||
String fileId = extractStringField(resp.body(), "fileId");
|
||||
assertThat(fileId).isNotBlank();
|
||||
return fileId;
|
||||
}
|
||||
|
||||
/** POST /stream with the given message + attachmentIds; drain SSE until done. */
|
||||
private void stream(String visitorId, String sessionId, String message, String attachmentJson) throws Exception {
|
||||
String attachmentField = attachmentJson == null ? "" : ",\"attachmentIds\":" + attachmentJson;
|
||||
String sessionField = sessionId == null ? "" : ",\"sessionId\":\"" + sessionId + "\"";
|
||||
String body = "{\"message\":\"" + message + "\",\"visitorId\":\"" + visitorId + "\""
|
||||
+ sessionField + attachmentField + "}";
|
||||
HttpRequest req = HttpRequest.newBuilder()
|
||||
.uri(streamUri())
|
||||
.timeout(HTTP_TIMEOUT)
|
||||
.header("X-MC-Key", API_KEY)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "text/event-stream")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
|
||||
.build();
|
||||
HttpResponse<java.io.InputStream> resp = http.send(req, HttpResponse.BodyHandlers.ofInputStream());
|
||||
// Drain to done so the request completes within the test.
|
||||
try (var is = resp.body();
|
||||
var reader = new java.io.BufferedReader(new java.io.InputStreamReader(is, StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.startsWith("event:done")) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Pull the persisted user message's raw content_parts JSON for this conversation. */
|
||||
private String lastUserContentParts(String conversationId) {
|
||||
return jdbc.queryForObject(
|
||||
"SELECT content_parts FROM mate_message WHERE conversation_id = ? AND role = 'user' " +
|
||||
"ORDER BY create_time DESC, id DESC LIMIT 1",
|
||||
String.class, conversationId);
|
||||
}
|
||||
|
||||
/** Naive JSON string-field extractor — avoids pulling in Jackson in the test body. */
|
||||
private static String extractStringField(String json, String fieldName) {
|
||||
String key = "\"" + fieldName + "\":\"";
|
||||
int i = json.indexOf(key);
|
||||
if (i < 0) return null;
|
||||
int start = i + key.length();
|
||||
int end = json.indexOf('"', start);
|
||||
return end < 0 ? null : json.substring(start, end);
|
||||
}
|
||||
|
||||
// ==================== cleanup ====================
|
||||
|
||||
@org.junit.jupiter.api.AfterEach
|
||||
void cleanUploadDirs() throws IOException {
|
||||
// Only the convs under our key8 prefix are ours; safe to wipe.
|
||||
Path root = UPLOAD_ROOT.resolve("webchat:testkey1");
|
||||
if (Files.exists(root)) {
|
||||
try (Stream<Path> walk = Files.walk(root)) {
|
||||
walk.sorted(Comparator.reverseOrder()).forEach(p -> {
|
||||
try { Files.deleteIfExists(p); } catch (IOException ignored) { }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== tests ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("upload + /stream round-trip: user message content_parts carries the file path")
|
||||
void uploadThenStreamAttachesPath() throws Exception {
|
||||
String visitorId = "vAtt-roundtrip";
|
||||
String sessionId = "s-att-1";
|
||||
String fileBody = "hello attachment e2e";
|
||||
String fileId = upload(visitorId, sessionId, "note.txt", "text/plain", fileBody.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
stream(visitorId, sessionId, "please read the attached", "[\"" + fileId + "\"]");
|
||||
|
||||
String cid = WebChatController.deriveConversationId(API_KEY, visitorId, sessionId);
|
||||
String parts = lastUserContentParts(cid);
|
||||
assertThat(parts).isNotNull();
|
||||
// Text part is present.
|
||||
assertThat(parts).contains("\"type\":\"text\"");
|
||||
assertThat(parts).contains("please read the attached");
|
||||
// File part is present with the server path resolved.
|
||||
assertThat(parts).contains("\"type\":\"file\"");
|
||||
assertThat(parts).contains("\"fileName\":\"note.txt\"");
|
||||
assertThat(parts).contains("\"contentType\":\"text/plain\"");
|
||||
assertThat(parts).contains("\"path\":\"");
|
||||
// Path points into the conversation's upload dir on disk.
|
||||
String path = extractStringField(parts, "path");
|
||||
assertThat(path).contains(cid);
|
||||
assertThat(Files.isRegularFile(Path.of(path))).isTrue();
|
||||
// The bytes on disk match what we uploaded.
|
||||
assertThat(Files.readString(Path.of(path))).isEqualTo(fileBody);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("unknown attachmentId is silently dropped — no error, user message has text only")
|
||||
void unknownAttachmentIdDropped() throws Exception {
|
||||
String visitorId = "vAtt-unknown";
|
||||
stream(visitorId, null, "hello", "[\"totally-bogus-file-id\"]");
|
||||
|
||||
String cid = WebChatController.deriveConversationId(API_KEY, visitorId, null);
|
||||
String parts = lastUserContentParts(cid);
|
||||
assertThat(parts).isNotNull();
|
||||
assertThat(parts).contains("\"type\":\"text\"");
|
||||
// No file part at all.
|
||||
assertThat(parts).doesNotContain("\"type\":\"file\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("foreign visitor cannot reference another visitor's attachmentId")
|
||||
void foreignAttachmentIdDropped() throws Exception {
|
||||
// visitor A uploads legitimately.
|
||||
String visitorA = "vAtt-alice";
|
||||
String visitorB = "vAtt-bob";
|
||||
String aliceFileId = upload(visitorA, null, "alice.txt", "text/plain",
|
||||
"alice's secret".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// visitor B references alice's fileId — should be silently dropped.
|
||||
stream(visitorB, null, "trying to grab alice's file", "[\"" + aliceFileId + "\"]");
|
||||
|
||||
// B's conversation's user message has no file part.
|
||||
String bobCid = WebChatController.deriveConversationId(API_KEY, visitorB, null);
|
||||
String bobParts = lastUserContentParts(bobCid);
|
||||
assertThat(bobParts).doesNotContain("\"type\":\"file\"");
|
||||
// Alice's conversation is untouched — no user message there at all.
|
||||
String aliceCid = WebChatController.deriveConversationId(API_KEY, visitorA, null);
|
||||
Integer aliceMsgCount = jdbc.queryForObject(
|
||||
"SELECT COUNT(*) FROM mate_message WHERE conversation_id = ? AND role = 'user'",
|
||||
Integer.class, aliceCid);
|
||||
assertThat(aliceMsgCount).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("upload without visitorToken → HTTP 401 (RHttpStatusAdvice maps R.code to status)")
|
||||
void uploadRequiresVisitorToken() throws Exception {
|
||||
String boundary = "----mcboundary" + System.nanoTime();
|
||||
byte[] body = multipart(boundary, Map.of("visitorId", "vAtt-notoken"),
|
||||
"file", "x.txt", "text/plain", "x".getBytes(StandardCharsets.UTF_8));
|
||||
HttpRequest req = HttpRequest.newBuilder()
|
||||
.uri(uploadUri())
|
||||
.timeout(HTTP_TIMEOUT)
|
||||
.header("X-MC-Key", API_KEY)
|
||||
// No X-MC-Visitor-Token
|
||||
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
|
||||
.POST(HttpRequest.BodyPublishers.ofByteArray(body))
|
||||
.build();
|
||||
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
assertThat(resp.statusCode()).isEqualTo(401);
|
||||
assertThat(resp.body()).contains("\"code\":401");
|
||||
assertThat(resp.body()).contains("Invalid or missing visitor token");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("upload with disallowed extension → HTTP 400")
|
||||
void uploadRejectsDisallowedExtension() throws Exception {
|
||||
String boundary = "----mcboundary" + System.nanoTime();
|
||||
byte[] body = multipart(boundary, Map.of("visitorId", "vAtt-badext"),
|
||||
"file", "evil.exe", "application/octet-stream", new byte[]{1, 2, 3});
|
||||
HttpRequest req = HttpRequest.newBuilder()
|
||||
.uri(uploadUri())
|
||||
.timeout(HTTP_TIMEOUT)
|
||||
.header("X-MC-Key", API_KEY)
|
||||
.header("X-MC-Visitor-Token", tokenFor("vAtt-badext"))
|
||||
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
|
||||
.POST(HttpRequest.BodyPublishers.ofByteArray(body))
|
||||
.build();
|
||||
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
assertThat(resp.statusCode()).isEqualTo(400);
|
||||
assertThat(resp.body()).contains("File type not allowed");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /files streams back the uploaded bytes")
|
||||
void downloadReturnsUploadedBytes() throws Exception {
|
||||
String visitorId = "vAtt-download";
|
||||
String sessionId = "s-dl-1";
|
||||
byte[] payload = "download me".getBytes(StandardCharsets.UTF_8);
|
||||
String fileId = upload(visitorId, sessionId, "payload.txt", "text/plain", payload);
|
||||
|
||||
// /stream must consume the staged file first; download's ownsConversation
|
||||
// guard also needs the conversation row to exist. sessionId MUST be
|
||||
// threaded through /files too — the controller recomputes conversationId
|
||||
// from (apiKey, visitorId, sessionId) so a missing sid maps to a
|
||||
// different namespace than where the upload lives.
|
||||
stream(visitorId, sessionId, "first message", "[\"" + fileId + "\"]");
|
||||
|
||||
HttpRequest req = HttpRequest.newBuilder()
|
||||
.uri(filesUri(fileId, visitorId, sessionId))
|
||||
.timeout(HTTP_TIMEOUT)
|
||||
.header("X-MC-Key", API_KEY)
|
||||
.header("X-MC-Visitor-Token", tokenFor(visitorId))
|
||||
.GET()
|
||||
.build();
|
||||
HttpResponse<byte[]> resp = http.send(req, HttpResponse.BodyHandlers.ofByteArray());
|
||||
assertThat(resp.statusCode()).isEqualTo(200);
|
||||
assertThat(resp.body()).isEqualTo(payload);
|
||||
assertThat(resp.headers().firstValue("Content-Type").orElse(""))
|
||||
.contains("text/plain");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /files without visitorToken → 401")
|
||||
void downloadRequiresVisitorToken() throws Exception {
|
||||
String visitorId = "vAtt-dl-notoken";
|
||||
String sessionId = "s-dl-2";
|
||||
String fileId = upload(visitorId, sessionId, "x.txt", "text/plain",
|
||||
"x".getBytes(StandardCharsets.UTF_8));
|
||||
stream(visitorId, sessionId, "first", "[\"" + fileId + "\"]");
|
||||
|
||||
HttpRequest req = HttpRequest.newBuilder()
|
||||
.uri(filesUri(fileId, visitorId, sessionId))
|
||||
.timeout(HTTP_TIMEOUT)
|
||||
.header("X-MC-Key", API_KEY)
|
||||
// No X-MC-Visitor-Token
|
||||
.GET()
|
||||
.build();
|
||||
HttpResponse<Void> resp = http.send(req, HttpResponse.BodyHandlers.discarding());
|
||||
assertThat(resp.statusCode()).isEqualTo(401);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user