test: enhance resource reading tests (#314)
- Implement comprehensive resource content validation for text/plain and application/octet-stream MIME types - Validate resource count expectations (10 resources) - Add elicitation capability testing with ElicitRequest/ElicitResult support - Clean up unused imports (ObjectMapper, ImageFromDockerfile) Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
parent
aacc631c96
commit
b701a366c0
6 changed files with 342 additions and 127 deletions
|
|
@ -1,13 +1,12 @@
|
||||||
package io.modelcontextprotocol.client;
|
package io.modelcontextprotocol.client;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport;
|
import io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport;
|
||||||
import io.modelcontextprotocol.spec.McpClientTransport;
|
import io.modelcontextprotocol.spec.McpClientTransport;
|
||||||
import org.junit.jupiter.api.Timeout;
|
import org.junit.jupiter.api.Timeout;
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
|
||||||
import org.testcontainers.containers.GenericContainer;
|
import org.testcontainers.containers.GenericContainer;
|
||||||
import org.testcontainers.containers.wait.strategy.Wait;
|
import org.testcontainers.containers.wait.strategy.Wait;
|
||||||
import org.testcontainers.images.builder.ImageFromDockerfile;
|
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
|
|
||||||
@Timeout(15)
|
@Timeout(15)
|
||||||
public class WebClientStreamableHttpAsyncClientTests extends AbstractMcpAsyncClientTests {
|
public class WebClientStreamableHttpAsyncClientTests extends AbstractMcpAsyncClientTests {
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,13 @@ package io.modelcontextprotocol.client;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
|
||||||
import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport;
|
|
||||||
import io.modelcontextprotocol.spec.McpClientTransport;
|
|
||||||
import org.junit.jupiter.api.Timeout;
|
import org.junit.jupiter.api.Timeout;
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
import org.testcontainers.containers.GenericContainer;
|
import org.testcontainers.containers.GenericContainer;
|
||||||
import org.testcontainers.containers.wait.strategy.Wait;
|
import org.testcontainers.containers.wait.strategy.Wait;
|
||||||
|
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport;
|
||||||
|
import io.modelcontextprotocol.spec.McpClientTransport;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for the {@link McpAsyncClient} with {@link WebFluxSseClientTransport}.
|
* Tests for the {@link McpAsyncClient} with {@link WebFluxSseClientTransport}.
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,14 @@
|
||||||
|
|
||||||
package io.modelcontextprotocol.client;
|
package io.modelcontextprotocol.client;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.assertj.core.api.Assertions.fail;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
@ -13,9 +20,16 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
|
|
||||||
import io.modelcontextprotocol.spec.McpClientTransport;
|
import io.modelcontextprotocol.spec.McpClientTransport;
|
||||||
import io.modelcontextprotocol.spec.McpError;
|
import io.modelcontextprotocol.spec.McpError;
|
||||||
import io.modelcontextprotocol.spec.McpSchema;
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.BlobResourceContents;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
|
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
|
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
|
||||||
|
|
@ -24,28 +38,19 @@ import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
|
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
|
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Prompt;
|
import io.modelcontextprotocol.spec.McpSchema.Prompt;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Resource;
|
import io.modelcontextprotocol.spec.McpSchema.Resource;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.ResourceContents;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Root;
|
import io.modelcontextprotocol.spec.McpSchema.Root;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.SubscribeRequest;
|
import io.modelcontextprotocol.spec.McpSchema.SubscribeRequest;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Tool;
|
import io.modelcontextprotocol.spec.McpSchema.Tool;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.UnsubscribeRequest;
|
import io.modelcontextprotocol.spec.McpSchema.UnsubscribeRequest;
|
||||||
import io.modelcontextprotocol.spec.McpTransport;
|
import io.modelcontextprotocol.spec.McpTransport;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Disabled;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
|
||||||
import org.junit.jupiter.params.provider.ValueSource;
|
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
|
||||||
import static org.assertj.core.api.Assertions.fail;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test suite for the {@link McpAsyncClient} that can be used with different
|
* Test suite for the {@link McpAsyncClient} that can be used with different
|
||||||
* {@link McpTransport} implementations.
|
* {@link McpTransport} implementations.
|
||||||
|
|
@ -406,18 +411,59 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Disabled
|
|
||||||
void testReadResource() {
|
void testReadResource() {
|
||||||
withClient(createMcpTransport(), mcpAsyncClient -> {
|
withClient(createMcpTransport(), client -> {
|
||||||
StepVerifier.create(mcpAsyncClient.listResources()).consumeNextWith(resources -> {
|
Flux<McpSchema.ReadResourceResult> resources = client.initialize()
|
||||||
if (!resources.resources().isEmpty()) {
|
.then(client.listResources(null))
|
||||||
Resource firstResource = resources.resources().get(0);
|
.flatMapMany(r -> Flux.fromIterable(r.resources()))
|
||||||
StepVerifier.create(mcpAsyncClient.readResource(firstResource)).consumeNextWith(result -> {
|
.flatMap(r -> client.readResource(r));
|
||||||
assertThat(result).isNotNull();
|
|
||||||
assertThat(result.contents()).isNotNull();
|
StepVerifier.create(resources).recordWith(ArrayList::new).consumeRecordedWith(readResourceResults -> {
|
||||||
}).verifyComplete();
|
|
||||||
|
for (ReadResourceResult result : readResourceResults) {
|
||||||
|
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.contents()).isNotNull().isNotEmpty();
|
||||||
|
|
||||||
|
// Validate each content item
|
||||||
|
for (ResourceContents content : result.contents()) {
|
||||||
|
assertThat(content).isNotNull();
|
||||||
|
assertThat(content.uri()).isNotNull().isNotEmpty();
|
||||||
|
assertThat(content.mimeType()).isNotNull().isNotEmpty();
|
||||||
|
|
||||||
|
// Validate content based on its type with more comprehensive
|
||||||
|
// checks
|
||||||
|
switch (content.mimeType()) {
|
||||||
|
case "text/plain" -> {
|
||||||
|
TextResourceContents textContent = assertInstanceOf(TextResourceContents.class,
|
||||||
|
content);
|
||||||
|
assertThat(textContent.text()).isNotNull().isNotEmpty();
|
||||||
|
assertThat(textContent.uri()).isNotEmpty();
|
||||||
|
}
|
||||||
|
case "application/octet-stream" -> {
|
||||||
|
BlobResourceContents blobContent = assertInstanceOf(BlobResourceContents.class,
|
||||||
|
content);
|
||||||
|
assertThat(blobContent.blob()).isNotNull().isNotEmpty();
|
||||||
|
assertThat(blobContent.uri()).isNotNull().isNotEmpty();
|
||||||
|
// Validate base64 encoding format
|
||||||
|
assertThat(blobContent.blob()).matches("^[A-Za-z0-9+/]*={0,2}$");
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
|
||||||
|
// Still validate basic properties
|
||||||
|
if (content instanceof TextResourceContents textContent) {
|
||||||
|
assertThat(textContent.text()).isNotNull();
|
||||||
|
}
|
||||||
|
else if (content instanceof BlobResourceContents blobContent) {
|
||||||
|
assertThat(blobContent.blob()).isNotNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}).verifyComplete();
|
})
|
||||||
|
.expectNextCount(10) // Expect 10 elements
|
||||||
|
.verifyComplete();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,12 @@
|
||||||
|
|
||||||
package io.modelcontextprotocol.client;
|
package io.modelcontextprotocol.client;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.assertj.core.api.Assertions.fail;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
@ -13,8 +19,17 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import io.modelcontextprotocol.spec.McpClientTransport;
|
import io.modelcontextprotocol.spec.McpClientTransport;
|
||||||
import io.modelcontextprotocol.spec.McpSchema;
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.BlobResourceContents;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
|
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
|
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
||||||
|
|
@ -23,25 +38,17 @@ import io.modelcontextprotocol.spec.McpSchema.ListResourcesResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
|
import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
|
import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Resource;
|
import io.modelcontextprotocol.spec.McpSchema.Resource;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.ResourceContents;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Root;
|
import io.modelcontextprotocol.spec.McpSchema.Root;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.SubscribeRequest;
|
import io.modelcontextprotocol.spec.McpSchema.SubscribeRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.TextContent;
|
import io.modelcontextprotocol.spec.McpSchema.TextContent;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Tool;
|
import io.modelcontextprotocol.spec.McpSchema.Tool;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.UnsubscribeRequest;
|
import io.modelcontextprotocol.spec.McpSchema.UnsubscribeRequest;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
|
||||||
import org.junit.jupiter.params.provider.ValueSource;
|
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.core.scheduler.Schedulers;
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
|
||||||
import static org.assertj.core.api.Assertions.fail;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for MCP Client Session functionality.
|
* Unit tests for MCP Client Session functionality.
|
||||||
*
|
*
|
||||||
|
|
@ -50,6 +57,8 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractMcpSyncClientTests {
|
public abstract class AbstractMcpSyncClientTests {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(AbstractMcpSyncClientTests.class);
|
||||||
|
|
||||||
private static final String TEST_MESSAGE = "Hello MCP Spring AI!";
|
private static final String TEST_MESSAGE = "Hello MCP Spring AI!";
|
||||||
|
|
||||||
abstract protected McpClientTransport createMcpTransport();
|
abstract protected McpClientTransport createMcpTransport();
|
||||||
|
|
@ -124,9 +133,9 @@ public abstract class AbstractMcpSyncClientTests {
|
||||||
|
|
||||||
<T> void verifyCallSucceedsWithImplicitInitialization(Function<McpSyncClient, T> blockingOperation, String action) {
|
<T> void verifyCallSucceedsWithImplicitInitialization(Function<McpSyncClient, T> blockingOperation, String action) {
|
||||||
withClient(createMcpTransport(), mcpSyncClient -> {
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
StepVerifier.create(Mono.fromSupplier(() -> blockingOperation.apply(mcpSyncClient)))
|
StepVerifier.create(Mono.fromSupplier(() -> blockingOperation.apply(mcpSyncClient))
|
||||||
.expectNextCount(1)
|
// Offload the blocking call to the real scheduler
|
||||||
.verifyComplete();
|
.subscribeOn(Schedulers.boundedElastic())).expectNextCount(1).verifyComplete();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -186,6 +195,49 @@ public abstract class AbstractMcpSyncClientTests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPingWithoutInitialization() {
|
||||||
|
verifyCallSucceedsWithImplicitInitialization(client -> client.ping(), "pinging the server");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPing() {
|
||||||
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
|
mcpSyncClient.initialize();
|
||||||
|
assertThatCode(() -> mcpSyncClient.ping()).doesNotThrowAnyException();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCallToolWithoutInitialization() {
|
||||||
|
CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", TEST_MESSAGE));
|
||||||
|
verifyCallSucceedsWithImplicitInitialization(client -> client.callTool(callToolRequest), "calling tools");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCallTool() {
|
||||||
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
|
mcpSyncClient.initialize();
|
||||||
|
CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", TEST_MESSAGE));
|
||||||
|
|
||||||
|
CallToolResult callToolResult = mcpSyncClient.callTool(callToolRequest);
|
||||||
|
|
||||||
|
assertThat(callToolResult).isNotNull().satisfies(result -> {
|
||||||
|
assertThat(result.content()).isNotNull();
|
||||||
|
assertThat(result.isError()).isNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCallToolWithInvalidTool() {
|
||||||
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
|
CallToolRequest invalidRequest = new CallToolRequest("nonexistent_tool", Map.of("message", TEST_MESSAGE));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> mcpSyncClient.callTool(invalidRequest)).isInstanceOf(Exception.class);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@ValueSource(strings = { "success", "error", "debug" })
|
@ValueSource(strings = { "success", "error", "debug" })
|
||||||
void testCallToolWithMessageAnnotations(String messageType) {
|
void testCallToolWithMessageAnnotations(String messageType) {
|
||||||
|
|
@ -240,49 +292,6 @@ public abstract class AbstractMcpSyncClientTests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void testPingWithoutInitialization() {
|
|
||||||
verifyCallSucceedsWithImplicitInitialization(client -> client.ping(), "pinging the server");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testPing() {
|
|
||||||
withClient(createMcpTransport(), mcpSyncClient -> {
|
|
||||||
mcpSyncClient.initialize();
|
|
||||||
assertThatCode(() -> mcpSyncClient.ping()).doesNotThrowAnyException();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testCallToolWithoutInitialization() {
|
|
||||||
CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", TEST_MESSAGE));
|
|
||||||
verifyCallSucceedsWithImplicitInitialization(client -> client.callTool(callToolRequest), "calling tools");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testCallTool() {
|
|
||||||
withClient(createMcpTransport(), mcpSyncClient -> {
|
|
||||||
mcpSyncClient.initialize();
|
|
||||||
CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", TEST_MESSAGE));
|
|
||||||
|
|
||||||
CallToolResult callToolResult = mcpSyncClient.callTool(callToolRequest);
|
|
||||||
|
|
||||||
assertThat(callToolResult).isNotNull().satisfies(result -> {
|
|
||||||
assertThat(result.content()).isNotNull();
|
|
||||||
assertThat(result.isError()).isNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testCallToolWithInvalidTool() {
|
|
||||||
withClient(createMcpTransport(), mcpSyncClient -> {
|
|
||||||
CallToolRequest invalidRequest = new CallToolRequest("nonexistent_tool", Map.of("message", TEST_MESSAGE));
|
|
||||||
|
|
||||||
assertThatThrownBy(() -> mcpSyncClient.callTool(invalidRequest)).isInstanceOf(Exception.class);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRootsListChangedWithoutInitialization() {
|
void testRootsListChangedWithoutInitialization() {
|
||||||
verifyNotificationSucceedsWithImplicitInitialization(client -> client.rootsListChangedNotification(),
|
verifyNotificationSucceedsWithImplicitInitialization(client -> client.rootsListChangedNotification(),
|
||||||
|
|
@ -388,16 +397,70 @@ public abstract class AbstractMcpSyncClientTests {
|
||||||
@Test
|
@Test
|
||||||
void testReadResource() {
|
void testReadResource() {
|
||||||
withClient(createMcpTransport(), mcpSyncClient -> {
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
|
|
||||||
|
int readResourceCount = 0;
|
||||||
|
|
||||||
mcpSyncClient.initialize();
|
mcpSyncClient.initialize();
|
||||||
ListResourcesResult resources = mcpSyncClient.listResources(null);
|
ListResourcesResult resources = mcpSyncClient.listResources(null);
|
||||||
|
|
||||||
if (!resources.resources().isEmpty()) {
|
assertThat(resources).isNotNull();
|
||||||
Resource firstResource = resources.resources().get(0);
|
assertThat(resources.resources()).isNotNull();
|
||||||
ReadResourceResult result = mcpSyncClient.readResource(firstResource);
|
|
||||||
|
assertThat(resources.resources()).isNotNull().isNotEmpty();
|
||||||
|
|
||||||
|
// Test reading each resource individually for better error isolation
|
||||||
|
for (Resource resource : resources.resources()) {
|
||||||
|
ReadResourceResult result = mcpSyncClient.readResource(resource);
|
||||||
|
|
||||||
assertThat(result).isNotNull();
|
assertThat(result).isNotNull();
|
||||||
assertThat(result.contents()).isNotNull();
|
assertThat(result.contents()).isNotNull().isNotEmpty();
|
||||||
|
|
||||||
|
readResourceCount++;
|
||||||
|
|
||||||
|
// Validate each content item
|
||||||
|
for (ResourceContents content : result.contents()) {
|
||||||
|
assertThat(content).isNotNull();
|
||||||
|
assertThat(content.uri()).isNotNull().isNotEmpty();
|
||||||
|
assertThat(content.mimeType()).isNotNull().isNotEmpty();
|
||||||
|
|
||||||
|
// Validate content based on its type with more comprehensive
|
||||||
|
// checks
|
||||||
|
switch (content.mimeType()) {
|
||||||
|
case "text/plain" -> {
|
||||||
|
TextResourceContents textContent = assertInstanceOf(TextResourceContents.class, content);
|
||||||
|
assertThat(textContent.text()).isNotNull().isNotEmpty();
|
||||||
|
// Verify URI consistency
|
||||||
|
assertThat(textContent.uri()).isEqualTo(resource.uri());
|
||||||
|
}
|
||||||
|
case "application/octet-stream" -> {
|
||||||
|
BlobResourceContents blobContent = assertInstanceOf(BlobResourceContents.class, content);
|
||||||
|
assertThat(blobContent.blob()).isNotNull().isNotEmpty();
|
||||||
|
// Verify URI consistency
|
||||||
|
assertThat(blobContent.uri()).isEqualTo(resource.uri());
|
||||||
|
// Validate base64 encoding format
|
||||||
|
assertThat(blobContent.blob()).matches("^[A-Za-z0-9+/]*={0,2}$");
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
// More flexible handling of additional MIME types
|
||||||
|
// Log the unexpected type for debugging but don't fail
|
||||||
|
// the test
|
||||||
|
logger.warn("Warning: Encountered unexpected MIME type: {} for resource: {}",
|
||||||
|
content.mimeType(), resource.uri());
|
||||||
|
|
||||||
|
// Still validate basic properties
|
||||||
|
if (content instanceof TextResourceContents textContent) {
|
||||||
|
assertThat(textContent.text()).isNotNull();
|
||||||
|
}
|
||||||
|
else if (content instanceof BlobResourceContents blobContent) {
|
||||||
|
assertThat(blobContent.blob()).isNotNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Assert that we read exactly 10 resources
|
||||||
|
assertThat(readResourceCount).isEqualTo(10);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,14 @@
|
||||||
|
|
||||||
package io.modelcontextprotocol.client;
|
package io.modelcontextprotocol.client;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.assertj.core.api.Assertions.fail;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
@ -13,9 +20,16 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
|
|
||||||
import io.modelcontextprotocol.spec.McpClientTransport;
|
import io.modelcontextprotocol.spec.McpClientTransport;
|
||||||
import io.modelcontextprotocol.spec.McpError;
|
import io.modelcontextprotocol.spec.McpError;
|
||||||
import io.modelcontextprotocol.spec.McpSchema;
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.BlobResourceContents;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
|
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
|
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
|
||||||
|
|
@ -24,28 +38,19 @@ import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
|
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
|
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Prompt;
|
import io.modelcontextprotocol.spec.McpSchema.Prompt;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Resource;
|
import io.modelcontextprotocol.spec.McpSchema.Resource;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.ResourceContents;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Root;
|
import io.modelcontextprotocol.spec.McpSchema.Root;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.SubscribeRequest;
|
import io.modelcontextprotocol.spec.McpSchema.SubscribeRequest;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Tool;
|
import io.modelcontextprotocol.spec.McpSchema.Tool;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.UnsubscribeRequest;
|
import io.modelcontextprotocol.spec.McpSchema.UnsubscribeRequest;
|
||||||
import io.modelcontextprotocol.spec.McpTransport;
|
import io.modelcontextprotocol.spec.McpTransport;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Disabled;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
|
||||||
import org.junit.jupiter.params.provider.ValueSource;
|
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
|
||||||
import static org.assertj.core.api.Assertions.fail;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test suite for the {@link McpAsyncClient} that can be used with different
|
* Test suite for the {@link McpAsyncClient} that can be used with different
|
||||||
* {@link McpTransport} implementations.
|
* {@link McpTransport} implementations.
|
||||||
|
|
@ -407,18 +412,59 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Disabled
|
|
||||||
void testReadResource() {
|
void testReadResource() {
|
||||||
withClient(createMcpTransport(), mcpAsyncClient -> {
|
withClient(createMcpTransport(), client -> {
|
||||||
StepVerifier.create(mcpAsyncClient.listResources()).consumeNextWith(resources -> {
|
Flux<McpSchema.ReadResourceResult> resources = client.initialize()
|
||||||
if (!resources.resources().isEmpty()) {
|
.then(client.listResources(null))
|
||||||
Resource firstResource = resources.resources().get(0);
|
.flatMapMany(r -> Flux.fromIterable(r.resources()))
|
||||||
StepVerifier.create(mcpAsyncClient.readResource(firstResource)).consumeNextWith(result -> {
|
.flatMap(r -> client.readResource(r));
|
||||||
assertThat(result).isNotNull();
|
|
||||||
assertThat(result.contents()).isNotNull();
|
StepVerifier.create(resources).recordWith(ArrayList::new).consumeRecordedWith(readResourceResults -> {
|
||||||
}).verifyComplete();
|
|
||||||
|
for (ReadResourceResult result : readResourceResults) {
|
||||||
|
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.contents()).isNotNull().isNotEmpty();
|
||||||
|
|
||||||
|
// Validate each content item
|
||||||
|
for (ResourceContents content : result.contents()) {
|
||||||
|
assertThat(content).isNotNull();
|
||||||
|
assertThat(content.uri()).isNotNull().isNotEmpty();
|
||||||
|
assertThat(content.mimeType()).isNotNull().isNotEmpty();
|
||||||
|
|
||||||
|
// Validate content based on its type with more comprehensive
|
||||||
|
// checks
|
||||||
|
switch (content.mimeType()) {
|
||||||
|
case "text/plain" -> {
|
||||||
|
TextResourceContents textContent = assertInstanceOf(TextResourceContents.class,
|
||||||
|
content);
|
||||||
|
assertThat(textContent.text()).isNotNull().isNotEmpty();
|
||||||
|
assertThat(textContent.uri()).isNotEmpty();
|
||||||
|
}
|
||||||
|
case "application/octet-stream" -> {
|
||||||
|
BlobResourceContents blobContent = assertInstanceOf(BlobResourceContents.class,
|
||||||
|
content);
|
||||||
|
assertThat(blobContent.blob()).isNotNull().isNotEmpty();
|
||||||
|
assertThat(blobContent.uri()).isNotNull().isNotEmpty();
|
||||||
|
// Validate base64 encoding format
|
||||||
|
assertThat(blobContent.blob()).matches("^[A-Za-z0-9+/]*={0,2}$");
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
|
||||||
|
// Still validate basic properties
|
||||||
|
if (content instanceof TextResourceContents textContent) {
|
||||||
|
assertThat(textContent.text()).isNotNull();
|
||||||
|
}
|
||||||
|
else if (content instanceof BlobResourceContents blobContent) {
|
||||||
|
assertThat(blobContent.blob()).isNotNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}).verifyComplete();
|
})
|
||||||
|
.expectNextCount(10) // Expect 10 elements
|
||||||
|
.verifyComplete();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,12 @@
|
||||||
|
|
||||||
package io.modelcontextprotocol.client;
|
package io.modelcontextprotocol.client;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.assertj.core.api.Assertions.fail;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
@ -13,8 +19,17 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import io.modelcontextprotocol.spec.McpClientTransport;
|
import io.modelcontextprotocol.spec.McpClientTransport;
|
||||||
import io.modelcontextprotocol.spec.McpSchema;
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.BlobResourceContents;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
|
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
|
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
||||||
|
|
@ -23,27 +38,17 @@ import io.modelcontextprotocol.spec.McpSchema.ListResourcesResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
|
import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
|
import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Resource;
|
import io.modelcontextprotocol.spec.McpSchema.Resource;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.ResourceContents;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Root;
|
import io.modelcontextprotocol.spec.McpSchema.Root;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.SubscribeRequest;
|
import io.modelcontextprotocol.spec.McpSchema.SubscribeRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.TextContent;
|
import io.modelcontextprotocol.spec.McpSchema.TextContent;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Tool;
|
import io.modelcontextprotocol.spec.McpSchema.Tool;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.UnsubscribeRequest;
|
import io.modelcontextprotocol.spec.McpSchema.UnsubscribeRequest;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
|
||||||
import org.junit.jupiter.params.provider.ValueSource;
|
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.core.scheduler.Scheduler;
|
|
||||||
import reactor.core.scheduler.Schedulers;
|
import reactor.core.scheduler.Schedulers;
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
|
||||||
import static org.assertj.core.api.Assertions.fail;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for MCP Client Session functionality.
|
* Unit tests for MCP Client Session functionality.
|
||||||
*
|
*
|
||||||
|
|
@ -53,6 +58,8 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||||
// KEEP IN SYNC with the class in mcp-test module
|
// KEEP IN SYNC with the class in mcp-test module
|
||||||
public abstract class AbstractMcpSyncClientTests {
|
public abstract class AbstractMcpSyncClientTests {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(AbstractMcpSyncClientTests.class);
|
||||||
|
|
||||||
private static final String TEST_MESSAGE = "Hello MCP Spring AI!";
|
private static final String TEST_MESSAGE = "Hello MCP Spring AI!";
|
||||||
|
|
||||||
abstract protected McpClientTransport createMcpTransport();
|
abstract protected McpClientTransport createMcpTransport();
|
||||||
|
|
@ -391,16 +398,70 @@ public abstract class AbstractMcpSyncClientTests {
|
||||||
@Test
|
@Test
|
||||||
void testReadResource() {
|
void testReadResource() {
|
||||||
withClient(createMcpTransport(), mcpSyncClient -> {
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
|
|
||||||
|
int readResourceCount = 0;
|
||||||
|
|
||||||
mcpSyncClient.initialize();
|
mcpSyncClient.initialize();
|
||||||
ListResourcesResult resources = mcpSyncClient.listResources(null);
|
ListResourcesResult resources = mcpSyncClient.listResources(null);
|
||||||
|
|
||||||
if (!resources.resources().isEmpty()) {
|
assertThat(resources).isNotNull();
|
||||||
Resource firstResource = resources.resources().get(0);
|
assertThat(resources.resources()).isNotNull();
|
||||||
ReadResourceResult result = mcpSyncClient.readResource(firstResource);
|
|
||||||
|
assertThat(resources.resources()).isNotNull().isNotEmpty();
|
||||||
|
|
||||||
|
// Test reading each resource individually for better error isolation
|
||||||
|
for (Resource resource : resources.resources()) {
|
||||||
|
ReadResourceResult result = mcpSyncClient.readResource(resource);
|
||||||
|
|
||||||
assertThat(result).isNotNull();
|
assertThat(result).isNotNull();
|
||||||
assertThat(result.contents()).isNotNull();
|
assertThat(result.contents()).isNotNull().isNotEmpty();
|
||||||
|
|
||||||
|
readResourceCount++;
|
||||||
|
|
||||||
|
// Validate each content item
|
||||||
|
for (ResourceContents content : result.contents()) {
|
||||||
|
assertThat(content).isNotNull();
|
||||||
|
assertThat(content.uri()).isNotNull().isNotEmpty();
|
||||||
|
assertThat(content.mimeType()).isNotNull().isNotEmpty();
|
||||||
|
|
||||||
|
// Validate content based on its type with more comprehensive
|
||||||
|
// checks
|
||||||
|
switch (content.mimeType()) {
|
||||||
|
case "text/plain" -> {
|
||||||
|
TextResourceContents textContent = assertInstanceOf(TextResourceContents.class, content);
|
||||||
|
assertThat(textContent.text()).isNotNull().isNotEmpty();
|
||||||
|
// Verify URI consistency
|
||||||
|
assertThat(textContent.uri()).isEqualTo(resource.uri());
|
||||||
|
}
|
||||||
|
case "application/octet-stream" -> {
|
||||||
|
BlobResourceContents blobContent = assertInstanceOf(BlobResourceContents.class, content);
|
||||||
|
assertThat(blobContent.blob()).isNotNull().isNotEmpty();
|
||||||
|
// Verify URI consistency
|
||||||
|
assertThat(blobContent.uri()).isEqualTo(resource.uri());
|
||||||
|
// Validate base64 encoding format
|
||||||
|
assertThat(blobContent.blob()).matches("^[A-Za-z0-9+/]*={0,2}$");
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
// More flexible handling of additional MIME types
|
||||||
|
// Log the unexpected type for debugging but don't fail
|
||||||
|
// the test
|
||||||
|
logger.warn("Warning: Encountered unexpected MIME type: {} for resource: {}",
|
||||||
|
content.mimeType(), resource.uri());
|
||||||
|
|
||||||
|
// Still validate basic properties
|
||||||
|
if (content instanceof TextResourceContents textContent) {
|
||||||
|
assertThat(textContent.text()).isNotNull();
|
||||||
|
}
|
||||||
|
else if (content instanceof BlobResourceContents blobContent) {
|
||||||
|
assertThat(blobContent.blob()).isNotNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Assert that we read exactly 10 resources
|
||||||
|
assertThat(readResourceCount).isEqualTo(10);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue