e4ef25c5 by Ean Schuessler

Convert McpTestSuite to Spock with @Shared ExecutionContext and update test infrastructure

1 parent 077ceb78
......@@ -39,9 +39,6 @@ dependencies {
// Test dependencies
testImplementation project(':framework')
testImplementation project(':framework').configurations.testImplementation.allDependencies
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
testImplementation 'org.junit.platform:junit-platform-suite:1.8.2'
}
// by default the Java plugin runs test on build, change to not do that (only run test if explicit task)
......@@ -57,7 +54,7 @@ test {
dependsOn cleanTest
systemProperty 'moqui.runtime', moquiDir.absolutePath + '/runtime'
systemProperty 'moqui.conf', 'MoquiConf.xml'
systemProperty 'moqui.conf', 'conf/MoquiDevConf.xml'
systemProperty 'moqui.init.static', 'true'
maxHeapSize = "512M"
......@@ -80,4 +77,4 @@ task copyDependencies { doLast {
into file(projectDir.absolutePath + '/lib') }
} }
copyDependencies.dependsOn cleanLib
jar.dependsOn copyDependencies
\ No newline at end of file
jar.dependsOn copyDependencies
......
......@@ -33,6 +33,5 @@
</webapp>
</webapp-list>
<depends-on name="SimpleScreens" version="2.2.0"/>
</component>
......
......@@ -45,25 +45,6 @@
<!-- MCP Test Screen -->
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="component://moqui-mcp-2/screen/McpTestScreen.xml" artifactTypeEnumId="AT_XML_SCREEN"/>
<!-- Common Screen Access Patterns -->
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="apps/order/*" artifactTypeEnumId="AT_XML_SCREEN"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="apps/party/*" artifactTypeEnumId="AT_XML_SCREEN"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="apps/invoice/*" artifactTypeEnumId="AT_XML_SCREEN"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="apps/product/*" artifactTypeEnumId="AT_XML_SCREEN"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="apps/ledger/*" artifactTypeEnumId="AT_XML_SCREEN"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="apps/marketing/*" artifactTypeEnumId="AT_XML_SCREEN"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="apps/sales/*" artifactTypeEnumId="AT_XML_SCREEN"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="apps/manufacturing/*" artifactTypeEnumId="AT_XML_SCREEN"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="apps/warehouse/*" artifactTypeEnumId="AT_XML_SCREEN"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="apps/humanresource/*" artifactTypeEnumId="AT_XML_SCREEN"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="apps/project/*" artifactTypeEnumId="AT_XML_SCREEN"/>
<!-- Specific Business Screens for Testing -->
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="component://mantle/screen/product/ProductList.xml" artifactTypeEnumId="AT_XML_SCREEN"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="component://mantle/screen/product/ProductDetail.xml" artifactTypeEnumId="AT_XML_SCREEN"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="component://mantle/screen/order/OrderList.xml" artifactTypeEnumId="AT_XML_SCREEN"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="component://mantle/screen/party/PartyList.xml" artifactTypeEnumId="AT_XML_SCREEN"/>
<!-- Essential Business Services -->
<moqui.security.ArtifactGroupMember artifactGroupId="McpBusinessServices" artifactName="mantle.order.OrderServices.create#Order" artifactTypeEnumId="AT_SERVICE"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpBusinessServices" artifactName="mantle.party.PartyServices.find#Party" artifactTypeEnumId="AT_SERVICE"/>
......@@ -113,9 +94,9 @@
<!-- MCP Artifact Authz -->
<moqui.security.ArtifactAuthz userGroupId="McpUser" artifactGroupId="McpServices" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/>
<moqui.security.ArtifactAuthz userGroupId="McpUser" artifactGroupId="McpRestPaths" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/>
<moqui.security.ArtifactAuthz userGroupId="McpUser" artifactGroupId="McpScreens" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_VIEW"/>
<!--
<moqui.security.ArtifactAuthz userGroupId="McpUser" artifactGroupId="McpScreenTransitions" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/>
<moqui.security.ArtifactAuthz userGroupId="McpUser" artifactGroupId="McpScreens" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_VIEW"/>
<moqui.security.ArtifactAuthz userGroupId="McpUser" artifactGroupId="McpScreenTools" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/>
-->
......@@ -129,7 +110,7 @@
<moqui.security.ArtifactAuthz userGroupId="ADMIN" artifactGroupId="McpScreens" authzTypeEnumId="AUTHZT_ALWAYS" authzActionEnumId="AUTHZA_ALL"/>
<moqui.security.ArtifactAuthz userGroupId="ADMIN" artifactGroupId="McpScreenTools" authzTypeEnumId="AUTHZT_ALWAYS" authzActionEnumId="AUTHZA_ALL"/>
<!-- Explicit permission for screen execution service -->
<moqui.security.ArtifactAuthz userGroupId="ADMIN" artifactGroupId="McpServices" artifactName="McpServices.execute#ScreenAsMcpTool" authzTypeEnumId="AUTHZT_ALWAYS" authzActionEnumId="AUTHZA_ALL"/>
<!-- <moqui.security.ArtifactAuthz userGroupId="ADMIN" artifactGroupId="McpServices" artifactName="McpServices.execute#ScreenAsMcpTool" authzTypeEnumId="AUTHZT_ALWAYS" authzActionEnumId="AUTHZA_ALL"/> -->
<!-- MCP Business Group Authz -->
<moqui.security.ArtifactAuthz userGroupId="MCP_BUSINESS" artifactGroupId="McpServices" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/>
......@@ -155,4 +136,4 @@
<moqui.security.UserGroupMember userGroupId="MCP_BUSINESS" userId="ORG_ZIZI_JD" fromDate="2025-01-01 00:00:00.000"/>
<moqui.security.UserGroupMember userGroupId="MCP_BUSINESS" userId="ORG_ZIZI_BD" fromDate="2025-01-01 00:00:00.000"/>
-->
</entity-facade-xml>
\ No newline at end of file
</entity-facade-xml>
......
......@@ -74,13 +74,13 @@ class CustomScreenTestImpl implements McpScreenTest {
* instead of the framework's version that has null contextPath issues
*/
protected WebFacade createWebFacade(ExecutionContextFactoryImpl ecfi, Map<String, Object> parameters,
Map<String, Object> sessionAttributes, String requestMethod) {
Map<String, Object> sessionAttributes, String requestMethod, String screenPath) {
if (logger.isDebugEnabled()) {
logger.debug("CustomScreenTestImpl.createWebFacade() called with parameters: ${parameters?.keySet()}, sessionAttributes: ${sessionAttributes?.keySet()}, requestMethod: ${requestMethod}")
logger.debug("CustomScreenTestImpl.createWebFacade() called with parameters: ${parameters?.keySet()}, sessionAttributes: ${sessionAttributes?.keySet()}, requestMethod: ${requestMethod}, screenPath: ${screenPath}")
}
// Use our MCP component's WebFacadeStub which properly handles null contextPath
return new org.moqui.mcp.WebFacadeStub(ecfi, parameters, sessionAttributes, requestMethod)
return new org.moqui.mcp.WebFacadeStub(ecfi, parameters, sessionAttributes, requestMethod, screenPath)
}
@Override
......@@ -222,8 +222,22 @@ class CustomScreenTestImpl implements McpScreenTest {
@Override void run() {
try {
ExecutionContextImpl threadEci = ecfi.getEci()
if (loginSubject != null) threadEci.userFacade.internalLoginSubject(loginSubject)
else if (username != null && !username.isEmpty()) threadEci.userFacade.internalLoginUser(username)
if (loginSubject != null) {
logger.info("CustomScreenTestRender: Login subject for ${username}")
threadEci.userFacade.internalLoginSubject(loginSubject)
} else if (username != null && !username.isEmpty()) {
logger.info("CustomScreenTestRender: Login user ${username}")
threadEci.userFacade.internalLoginUser(username)
} else {
logger.warn("CustomScreenTestRender: No user to login!")
}
if (threadEci.userFacade.userId) {
logger.info("CustomScreenTestRender: Logged in as ${threadEci.userFacade.username} (${threadEci.userFacade.userId})")
} else {
logger.warn("CustomScreenTestRender: Failed to login, userId is null")
}
if (authzDisabled) threadEci.artifactExecutionFacade.disableAuthz()
// as this is used for server-side transition calls don't do tarpit checks
threadEci.artifactExecutionFacade.disableTarpit()
......@@ -253,7 +267,7 @@ class CustomScreenTestImpl implements McpScreenTest {
ContextStack cs = eci.getContext()
cs.push()
// create the WebFacadeStub using our custom method
org.moqui.mcp.WebFacadeStub wfs = (org.moqui.mcp.WebFacadeStub) csti.createWebFacade(csti.ecfi, stri.parameters, csti.sessionAttributes, stri.requestMethod)
org.moqui.mcp.WebFacadeStub wfs = (org.moqui.mcp.WebFacadeStub) csti.createWebFacade(csti.ecfi, stri.parameters, csti.sessionAttributes, stri.requestMethod, stri.screenPath)
// set stub on eci, will also put parameters in the context
eci.setWebFacade(wfs)
// make the ScreenRender
......@@ -277,15 +291,18 @@ class CustomScreenTestImpl implements McpScreenTest {
// do the render
try {
logger.info("Starting render for ${stri.screenPath} with root ${csti.rootScreenLocation}")
screenRender.render(wfs.getRequest(), wfs.getResponse())
// get the response text from the WebFacadeStub
stri.outputString = wfs.getResponseText()
stri.jsonObj = wfs.getResponseJsonObj()
logger.info("Render finished. Output length: ${stri.outputString?.length()}, JSON: ${stri.jsonObj != null}")
} catch (Throwable t) {
String errMsg = "Exception in render of ${stri.screenPath}: ${t.toString()}"
logger.warn(errMsg, t)
stri.errorMessages.add(errMsg)
csti.errorCount++
if (stri.outputString == null) stri.outputString = "RENDER_EXCEPTION: " + errMsg
}
// calc renderTime
stri.renderTime = System.currentTimeMillis() - startTime
......@@ -339,4 +356,4 @@ class CustomScreenTestImpl implements McpScreenTest {
return outputString.matches(regex)
}
}
}
\ No newline at end of file
}
......
......@@ -275,7 +275,25 @@ class WebFacadeStub implements WebFacade {
}
// Helper methods for ScreenTestImpl
String getResponseText() { return responseText }
String getResponseText() {
if (responseText != null) {
logger.info("getResponseText: returning responseText (length: ${responseText.length()})")
return responseText
}
if (httpServletResponse instanceof MockHttpServletResponse) {
// Flush the writer to ensure all content is captured
try {
httpServletResponse.getWriter().flush()
} catch (IOException e) {
logger.warn("Error flushing response writer: ${e.message}")
}
def content = ((MockHttpServletResponse) httpServletResponse).getResponseContent()
logger.info("getResponseText: returning content from mock response (length: ${content?.length() ?: 0})")
return content
}
logger.warn("getResponseText: httpServletResponse is not MockHttpServletResponse: ${httpServletResponse?.getClass()?.getName()}")
return null
}
Object getResponseJsonObj() { return responseJsonObj }
// Mock HTTP classes
......@@ -564,4 +582,4 @@ class WebFacadeStub implements WebFacade {
@Override String getResponseCharacterEncoding() { return "UTF-8" }
@Override void setResponseCharacterEncoding(String encoding) {}
}
}
\ No newline at end of file
}
......
......@@ -13,48 +13,53 @@
*/
package org.moqui.mcp.test
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.MethodOrderer
import org.junit.jupiter.api.Order
import org.junit.jupiter.api.TestMethodOrder
import org.moqui.Moqui
import org.moqui.context.ExecutionContext
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Stepwise
@DisplayName("MCP Test Suite")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class McpTestSuite {
@Stepwise
class McpTestSuite extends Specification {
static SimpleMcpClient client
static boolean criticalTestFailed = false
@Shared
ExecutionContext ec
@Shared
SimpleMcpClient client
@Shared
boolean criticalTestFailed = false
@BeforeAll
static void setupMoqui() {
def setupSpec() {
// Initialize Moqui framework for testing
System.setProperty('moqui.runtime', '../runtime')
System.setProperty('moqui.conf', 'MoquiConf.xml')
System.setProperty('moqui.init.static', 'true')
// Note: moqui.runtime is set by build.gradle
// Clear moqui.conf to ensure we use the runtime's MoquiDevConf.xml instead of component's minimal conf
//System.clearProperty('moqui.conf')
// System.setProperty('moqui.init.static', 'true')
ec = Moqui.getExecutionContext()
// Initialize MCP client
client = new SimpleMcpClient()
}
@AfterAll
static void cleanup() {
def cleanupSpec() {
if (client) {
client.closeSession()
}
if (ec) {
ec.destroy()
}
}
@Test
@Order(1)
@DisplayName("Test Internal Service Direct Call")
void testInternalServiceDirectCall() {
def "Test Internal Service Direct Call"() {
println "🔧 Testing Internal Service Direct Call"
println "📂 Runtime Path: ${System.getProperty('moqui.runtime')}"
println "📂 Conf Path: ${System.getProperty('moqui.conf')}"
// Try to get ExecutionContext to verify if we are running in-container
def ec = Moqui.getExecutionContext()
if (ec == null) {
println "⚠️ No ExecutionContext available - skipping internal service test (running in external client mode)"
return
......@@ -62,7 +67,17 @@ class McpTestSuite {
println "✅ ExecutionContext available, testing service directly"
// Login as mcp-user to ensure we have a valid user context for the screen render
try {
ec.user.internalLoginUser("mcp-user")
println "✅ Logged in as mcp-user"
} catch (Throwable t) {
println "❌ Failed to login as mcp-user: ${t.message}"
t.printStackTrace()
// Continue to see if service call works (it might fail auth but shouldn't crash)
}
when:
// Call the service directly
def result = ec.service.sync().name("McpServices.execute#ScreenAsMcpTool")
.parameters([
......@@ -73,13 +88,14 @@ class McpTestSuite {
.call()
println "✅ Service returned result: ${result}"
then:
// Verify result structure
assert result != null
assert result.result != null
assert result.result.type == "text"
assert result.result.screenPath == "component://moqui-mcp-2/screen/McpTestScreen.xml"
assert !result.result.isError
result != null
result.result != null
result.result.type == "text"
result.result.screenPath == "component://moqui-mcp-2/screen/McpTestScreen.xml"
!result.result.isError
// Verify content
def text = result.result.text
......@@ -88,67 +104,56 @@ class McpTestSuite {
println "🎉 SUCCESS: Found test message in direct render output"
} else {
println "⚠️ Test message not found in output (or output empty)"
// Note: We don't fail the critical test on empty output yet as it might be an environment quirk,
// but if we wanted to enforce it, we would throw AssertionError here.
}
} catch (Exception e) {
println "❌ Service call failed: ${e.message}"
e.printStackTrace()
criticalTestFailed = true
throw e
} catch (AssertionError e) {
println "❌ Service assertion failed: ${e.message}"
criticalTestFailed = true
throw e
}
cleanup:
ec.user.logoutUser()
}
@Test
@Order(2)
@DisplayName("Test MCP Server Connectivity")
void testMcpServerConnectivity() {
def "Test MCP Server Connectivity"() {
if (criticalTestFailed) return
println "🔌 Testing MCP Server Connectivity"
expect:
// Test session initialization first
assert client.initializeSession() : "MCP session should initialize successfully"
client.initializeSession()
println "✅ Session initialized successfully"
// Test server ping
assert client.ping() : "MCP server should respond to ping"
client.ping()
println "✅ Server ping successful"
// Test tool listing
def tools = client.listTools()
assert tools != null : "Tools list should not be null"
assert tools.size() > 0 : "Should have at least one tool available"
tools != null
tools.size() > 0
println "✅ Found ${tools.size()} available tools"
}
@Test
@Order(3)
@DisplayName("Test PopCommerce Product Search")
void testPopCommerceProductSearch() {
def "Test PopCommerce Product Search"() {
if (criticalTestFailed) return
println "🛍️ Testing PopCommerce Product Search"
when:
// Use SimpleScreens search screen directly (PopCommerce/SimpleScreens reuses this)
// Pass "Blue" as queryString to find blue products
def result = client.callScreen("component://SimpleScreens/screen/SimpleScreens/Catalog/Search.xml", [queryString: "Blue"])
assert result != null : "Screen call result should not be null"
assert result instanceof Map : "Screen result should be a map"
then:
result != null
result instanceof Map
// Fail test if screen returns error
assert !result.containsKey('error') : "Screen call should not return error: ${result.error}"
assert !result.isError : "Screen result should not have isError set to true"
!result.containsKey('error')
!result.isError
println "✅ PopCommerce search screen accessed successfully"
// Check if we got content - fail test if no content
def content = result.result?.content
assert content != null && content instanceof List && content.size() > 0 : "Screen should return content with blue products"
content != null && content instanceof List && content.size() > 0
println "✅ Screen returned content with ${content.size()} items"
def blueProductsFound = false
......@@ -194,21 +199,21 @@ class McpTestSuite {
}
// Fail test if no blue products were found
assert blueProductsFound : "Should find at least one blue product (Demo with Variants Blue) in search results"
blueProductsFound
}
@Test
@Order(4)
@DisplayName("Test Customer Lookup")
void testCustomerLookup() {
def "Test Customer Lookup"() {
if (criticalTestFailed) return
println "👤 Testing Customer Lookup"
when:
// Use actual available screen - PartyList from mantle component
def result = client.callScreen("component://mantle/screen/party/PartyList.xml", [:])
assert result != null : "Screen call result should not be null"
assert result instanceof Map : "Screen result should be a map"
then:
result != null
result instanceof Map
if (result.containsKey('error')) {
println "⚠️ Screen call returned error: ${result.error}"
......@@ -233,18 +238,18 @@ class McpTestSuite {
}
}
@Test
@Order(5)
@DisplayName("Test Complete Order Workflow")
void testCompleteOrderWorkflow() {
def "Test Complete Order Workflow"() {
if (criticalTestFailed) return
println "🛒 Testing Complete Order Workflow"
when:
// Use actual available screen - OrderList from mantle component
def result = client.callScreen("component://mantle/screen/order/OrderList.xml", [:])
assert result != null : "Screen call result should not be null"
assert result instanceof Map : "Screen result should be a map"
then:
result != null
result instanceof Map
if (result.containsKey('error')) {
println "⚠️ Screen call returned error: ${result.error}"
......@@ -269,20 +274,20 @@ class McpTestSuite {
}
}
@Test
@Order(6)
@DisplayName("Test MCP Screen Infrastructure")
void testMcpScreenInfrastructure() {
def "Test MCP Screen Infrastructure"() {
if (criticalTestFailed) return
println "🖥️ Testing MCP Screen Infrastructure"
when:
// Test calling the MCP test screen with a custom message
def result = client.callScreen("component://moqui-mcp-2/screen/McpTestScreen.xml", [
message: "MCP Test Successful!"
])
assert result != null : "Screen call result should not be null"
assert result instanceof Map : "Screen result should be a map"
then:
result != null
result instanceof Map
if (result.containsKey('error')) {
println "⚠️ Screen call returned error: ${result.error}"
......@@ -323,5 +328,4 @@ class McpTestSuite {
}
}
}
}
......
# MCP Test Configuration
# Test user credentials
test.user=john.sales
test.password=opencode
test.password=moqui
test.mcp.url=http://localhost:8080/mcp
# Test data
......