CustomScreenTestImpl.groovy 16.1 KB
/*
 * This software is in the public domain under CC0 1.0 Universal plus a
 * Grant of Patent License.
 *
 * To the extent possible under law, author(s) have dedicated all
 * copyright and related and neighboring rights to this software to the
 * public domain worldwide. This software is distributed without any
 * warranty.
 *
 * You should have received a copy of the CC0 Public Domain Dedication
 * along with this software (see the LICENSE.md file). If not, see
 * <http://creativecommons.org/publicdomain/zero/1.0/>.
 */
package org.moqui.mcp

import groovy.transform.CompileStatic
import org.apache.shiro.subject.Subject
import org.moqui.BaseArtifactException
import org.moqui.util.ContextStack
import org.moqui.impl.context.ExecutionContextFactoryImpl
import org.moqui.impl.context.ExecutionContextImpl
import org.moqui.impl.screen.ScreenDefinition
import org.moqui.impl.screen.ScreenFacadeImpl
import org.moqui.impl.screen.ScreenTestImpl
import org.moqui.impl.screen.ScreenUrlInfo
import org.moqui.screen.ScreenRender
import org.moqui.screen.ScreenTest
import org.moqui.util.MNode
import org.slf4j.Logger
import org.slf4j.LoggerFactory

import java.util.concurrent.Future

@CompileStatic
class CustomScreenTestImpl implements ScreenTest {
    protected final static Logger logger = LoggerFactory.getLogger(CustomScreenTestImpl.class)

    protected final ExecutionContextFactoryImpl ecfi
    protected final ScreenFacadeImpl sfi
    // see FtlTemplateRenderer.MoquiTemplateExceptionHandler, others
    final List<String> errorStrings = ["[Template Error", "FTL stack trace", "Could not find subscreen or transition"]

    protected String rootScreenLocation = null
    protected ScreenDefinition rootScreenDef = null
    protected String baseScreenPath = null
    protected List<String> baseScreenPathList = null
    protected ScreenDefinition baseScreenDef = null

    protected String outputType = null
    protected String characterEncoding = null
    protected String macroTemplateLocation = null
    protected String baseLinkUrl = null
    protected String servletContextPath = null
    protected String webappName = null
    protected boolean skipJsonSerialize = false
    protected static final String hostname = "localhost"

    long renderCount = 0, errorCount = 0, totalChars = 0, startTime = System.currentTimeMillis()

    final Map<String, Object> sessionAttributes = [:]

    CustomScreenTestImpl(ExecutionContextFactoryImpl ecfi) {
        this.ecfi = ecfi
        sfi = ecfi.screenFacade

        // init default webapp, root screen
        webappName('webroot')
    }

    @Override
    ScreenTest rootScreen(String screenLocation) {
        rootScreenLocation = screenLocation
        rootScreenDef = sfi.getScreenDefinition(rootScreenLocation)
        if (rootScreenDef == null) throw new IllegalArgumentException("Root screen not found: ${rootScreenLocation}")
        baseScreenDef = rootScreenDef
        return this
    }
    @Override
    ScreenTest baseScreenPath(String screenPath) {
        if (!rootScreenLocation) throw new BaseArtifactException("No rootScreen specified")
        baseScreenPath = screenPath
        if (baseScreenPath.endsWith("/")) baseScreenPath = baseScreenPath.substring(0, baseScreenPath.length() - 1)
        if (baseScreenPath) {
            baseScreenPathList = ScreenUrlInfo.parseSubScreenPath(rootScreenDef, rootScreenDef, [], baseScreenPath, null, sfi)
            if (baseScreenPathList == null) throw new BaseArtifactException("Error in baseScreenPath, could find not base screen path ${baseScreenPath} under ${rootScreenDef.location}")
            for (String screenName in baseScreenPathList) {
                ScreenDefinition.SubscreensItem ssi = baseScreenDef.getSubscreensItem(screenName)
                if (ssi == null) throw new BaseArtifactException("Error in baseScreenPath, could not find ${screenName} under ${baseScreenDef.location}")
                baseScreenDef = sfi.getScreenDefinition(ssi.location)
                if (baseScreenDef == null) throw new BaseArtifactException("Error in baseScreenPath, could not find screen ${screenName} at ${ssi.location}")
            }
        }
        return this
    }
    @Override ScreenTest renderMode(String outputType) { this.outputType = outputType; return this }
    @Override ScreenTest encoding(String characterEncoding) { this.characterEncoding = characterEncoding; return this }
    @Override ScreenTest macroTemplate(String macroTemplateLocation) { this.macroTemplateLocation = macroTemplateLocation; return this }
    @Override ScreenTest baseLinkUrl(String baseLinkUrl) { this.baseLinkUrl = baseLinkUrl; return this }
    @Override ScreenTest servletContextPath(String scp) { this.servletContextPath = scp; return this }
    @Override ScreenTest skipJsonSerialize(boolean skip) { this.skipJsonSerialize = skip; return this }

    @Override
    ScreenTest webappName(String wan) {
        webappName = wan

        // set a default root screen based on config for "localhost"
        MNode webappNode = ecfi.getWebappNode(webappName)
        for (MNode rootScreenNode in webappNode.children("root-screen")) {
            if (hostname.matches(rootScreenNode.attribute('host'))) {
                String rsLoc = rootScreenNode.attribute('location')
                rootScreen(rsLoc)
                break
            }
        }

        return this
    }

    @Override
    List<String> getNoRequiredParameterPaths(Set<String> screensToSkip) {
        if (!rootScreenLocation) throw new IllegalStateException("No rootScreen specified")

        List<String> noReqParmLocations = baseScreenDef.nestedNoReqParmLocations("", screensToSkip)
        // logger.info("======= rootScreenLocation=${rootScreenLocation}\nbaseScreenPath=${baseScreenPath}\nbaseScreenDef: ${baseScreenDef.location}\nnoReqParmLocations: ${noReqParmLocations}")
        return noReqParmLocations
    }

    @Override
    ScreenTestRender render(String screenPath, Map<String, Object> parameters, String requestMethod) {
        if (!rootScreenLocation) throw new IllegalArgumentException("No rootScreenLocation specified")
        return new CustomScreenTestRenderImpl(this, screenPath, parameters, requestMethod).render()
    }
    @Override
    void renderAll(List<String> screenPathList, Map<String, Object> parameters, String requestMethod) {
        // NOTE: using single thread for now, doesn't actually make a lot of difference in overall test run time
        int threads = 1
        if (threads == 1) {
            for (String screenPath in screenPathList) {
                ScreenTestRender str = render(screenPath, parameters, requestMethod)
                logger.info("Rendered ${screenPath} in ${str.getRenderTime()}ms, ${str.output?.length()} characters")
            }
        } else {
            ExecutionContextImpl eci = ecfi.getEci()
            ArrayList<Future> threadList = new ArrayList<Future>(threads)
            int screenPathListSize = screenPathList.size()
            for (int si = 0; si < screenPathListSize; si++) {
                String screenPath = (String) screenPathList.get(si)
                threadList.add(eci.runAsync({
                    ScreenTestRender str = render(screenPath, parameters, requestMethod)
                    logger.info("Rendered ${screenPath} in ${str.getRenderTime()}ms, ${str.output?.length()} characters")
                }))
                if (threadList.size() == threads || (si + 1) == screenPathListSize) {
                    for (int i = 0; i < threadList.size(); i++) { ((Future) threadList.get(i)).get() }
                    threadList.clear()
                }
            }
        }
    }

    long getRenderCount() { return renderCount }
    long getErrorCount() { return errorCount }
    long getRenderTotalChars() { return totalChars }
    long getStartTime() { return startTime }

    @CompileStatic
    static class CustomScreenTestRenderImpl implements ScreenTestRender {
        protected final CustomScreenTestImpl sti
        String screenPath = (String) null
        Map<String, Object> parameters = [:]
        String requestMethod = (String) null

        ScreenRender screenRender = (ScreenRender) null
        String outputString = (String) null
        Object jsonObj = null
        long renderTime = 0
        Map postRenderContext = (Map) null
        protected List<String> errorMessages = []

        CustomScreenTestRenderImpl(CustomScreenTestImpl sti, String screenPath, Map<String, Object> parameters, String requestMethod) {
            this.sti = sti
            this.screenPath = screenPath
            if (parameters != null) this.parameters.putAll(parameters)
            this.requestMethod = requestMethod
        }


        ScreenTestRender render() {
            // render in separate thread with an independent ExecutionContext so it doesn't muck up the current one
            ExecutionContextFactoryImpl ecfi = sti.ecfi
            ExecutionContextImpl localEci = ecfi.getEci()
            String username = localEci.userFacade.getUsername()
            Subject loginSubject = localEci.userFacade.getCurrentSubject()
            boolean authzDisabled = localEci.artifactExecutionFacade.getAuthzDisabled()
            CustomScreenTestRenderImpl stri = this
            Throwable threadThrown = null

            Thread newThread = new Thread("CustomScreenTestRender") {
                @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 (authzDisabled) threadEci.artifactExecutionFacade.disableAuthz()
                        // as this is used for server-side transition calls don't do tarpit checks
                        threadEci.artifactExecutionFacade.disableTarpit()
                        
                        // Ensure user is properly authenticated in the thread context
                        // This is critical for screen authentication checks
                        if (username != null && !username.isEmpty()) {
                            threadEci.userFacade.internalLoginUser(username)
                        }
                        
                        renderInternal(threadEci, stri)
                        threadEci.destroy()
                    } catch (Throwable t) {
                        threadThrown = t
                    }
                }
            }
            newThread.start()
            newThread.join()
            if (threadThrown != null) throw threadThrown
            return this
        }
        private static void renderInternal(ExecutionContextImpl eci, CustomScreenTestRenderImpl stri) {
            CustomScreenTestImpl sti = stri.sti
            long startTime = System.currentTimeMillis()

            // parse the screenPath - if empty or null, use empty list to render the root screen
            ArrayList<String> screenPathList = []
            if (stri.screenPath != null && !stri.screenPath.trim().isEmpty()) {
                screenPathList = ScreenUrlInfo.parseSubScreenPath(sti.rootScreenDef, sti.baseScreenDef,
                        sti.baseScreenPathList, stri.screenPath, stri.parameters, sti.sfi)
                if (screenPathList == null) throw new BaseArtifactException("Could not find screen path ${stri.screenPath} under base screen ${sti.baseScreenDef.location}")
            }

            // push context
            ContextStack cs = eci.getContext()
            cs.push()
            
            // Ensure user context is properly set in session attributes for WebFacadeStub
            def sessionAttributes = new HashMap(sti.sessionAttributes)
            sessionAttributes.putAll([
                userId: eci.userFacade.getUserId(),
                username: eci.userFacade.getUsername(),
                userAccountId: eci.userFacade.getUserId()
            ])
            
            // create our custom WebFacadeStub instead of framework's, passing screen path for proper path handling
            WebFacadeStub wfs = new WebFacadeStub(sti.ecfi, stri.parameters, sessionAttributes, stri.requestMethod, stri.screenPath)
            // set stub on eci, will also put parameters in context
            eci.setWebFacade(wfs)
            // make the ScreenRender
            ScreenRender screenRender = sti.sfi.makeRender()
            stri.screenRender = screenRender
            // pass through various settings
            if (sti.rootScreenLocation != null && sti.rootScreenLocation.length() > 0) screenRender.rootScreen(sti.rootScreenLocation)
            if (sti.outputType != null && sti.outputType.length() > 0) screenRender.renderMode(sti.outputType)
            if (sti.characterEncoding != null && sti.characterEncoding.length() > 0) screenRender.encoding(sti.characterEncoding)
            if (sti.macroTemplateLocation != null && sti.macroTemplateLocation.length() > 0) screenRender.macroTemplate(sti.macroTemplateLocation)
            if (sti.baseLinkUrl != null && sti.baseLinkUrl.length() > 0) screenRender.baseLinkUrl(sti.baseLinkUrl)
            if (sti.servletContextPath != null && sti.servletContextPath.length() > 0) screenRender.servletContextPath(sti.servletContextPath)
            screenRender.webappName(sti.webappName)
            if (sti.skipJsonSerialize) wfs.skipJsonSerialize = true

            // set the screenPath
            screenRender.screenPath(screenPathList)

            // do the render
            try {
                screenRender.render(wfs.httpServletRequest, wfs.httpServletResponse)
                // get the response text from our WebFacadeStub
                stri.outputString = wfs.getResponseText()
                stri.jsonObj = wfs.getResponseJsonObj()
                System.out.println("STRI: " + stri.outputString + " : " + stri.jsonObj)
            } catch (Throwable t) {
                String errMsg = "Exception in render of ${stri.screenPath}: ${t.toString()}"
                logger.warn(errMsg, t)
                stri.errorMessages.add(errMsg)
                sti.errorCount++
            }
            // calc renderTime
            stri.renderTime = System.currentTimeMillis() - startTime

            // pop the context stack, get rid of var space
            stri.postRenderContext = cs.pop()

            // check, pass through, error messages
            if (eci.message.hasError()) {
                stri.errorMessages.addAll(eci.message.getErrors())
                eci.message.clearErrors()
                StringBuilder sb = new StringBuilder("Error messages from ${stri.screenPath}: ")
                for (String errorMessage in stri.errorMessages) sb.append("\n").append(errorMessage)
                logger.warn(sb.toString())
                sti.errorCount += stri.errorMessages.size()
            }

            // check for error strings in output
            if (stri.outputString != null) for (String errorStr in sti.errorStrings) if (stri.outputString.contains(errorStr)) {
                String errMsg = "Found error [${errorStr}] in output from ${stri.screenPath}"
                stri.errorMessages.add(errMsg)
                sti.errorCount++
                logger.warn(errMsg)
            }

            // update stats
            sti.renderCount++
            if (stri.outputString != null) sti.totalChars += stri.outputString.length()
        }

        @Override ScreenRender getScreenRender() { return screenRender }
        @Override String getOutput() { return outputString }
        @Override Object getJsonObject() { return jsonObj }
        @Override long getRenderTime() { return renderTime }
        @Override Map getPostRenderContext() { return postRenderContext }
        @Override List<String> getErrorMessages() { return errorMessages }

        @Override
        boolean assertContains(String text) {
            if (!outputString) return false
            return outputString.contains(text)
        }
        @Override
        boolean assertNotContains(String text) {
            if (!outputString) return true
            return !outputString.contains(text)
        }
        @Override
        boolean assertRegex(String regex) {
            if (!outputString) return false
            return outputString.matches(regex)
        }
    }
}