Шаблоны запросов Wiremock в автономном режиме: могу ли я использовать файл XML в качестве шаблона ответа и вводить значение с помощью XPATH?

Я знаю, что шаблон запроса поддерживает XPath, поэтому я могу получить значение из запроса, например {{xPath request.body '/outer/inner/text()'}}. У меня уже есть XML-файл в качестве ответа, и я хочу ввести это значение, полученное из запроса, но сохранить другие части этого XML-ответа нетронутыми. Например, я хочу внедрить его в XPATH /svc_result/slia/pos/msid.

И мне нужно использовать его в автономном режиме.

Я вижу еще один вопрос (Wiremock Stand Alone - Как манипулировать ответ с данными запроса), но это было с JSON, у меня есть запрос/ответ XML.

Как это сделать? Спасибо.

Например, у меня есть такое определение отображения:

{
    "request": {
        "method": "POST",
        "bodyPatterns": [
            {
                "matchesXPath": {
                    "expression": "/svc_init/slir/msids/msid[@type='MSISDN']/text()",
                    "equalTo": "200853000105614"
                }
            },
            {
                "matchesXPath": "/svc_init/hdr/client[id and pwd]"
            }
        ]
    },
    "response": {
        "status": 200,
        "bodyFileName": "slia.xml",
        "headers": {
            "Content-Type": "application/xml;charset=UTF-8"
        }
    }
}

И этот запрос:

<?xml version="1.0"?>
<!DOCTYPE svc_init>
<svc_init ver="3.2.0">
    <hdr ver="3.2.0">
        <client>
            <id>dummy</id>
            <pwd>dummy</pwd>
        </client>
    </hdr>
    <slir ver="3.2.0" res_type="SYNC">
        <msids>
            <msid type="MSISDN">200853000105614</msid>
        </msids>
    </slir>
</svc_init>

Я ожидаю этот ответ с заменой xxxxxxxxxxx на <msid> в запросе.

<?xml version="1.0" ?>
<!DOCTYPE svc_result SYSTEM "MLP_SVC_RESULT_320.DTD">
<svc_result ver="3.2.0">
    <slia ver="3.0.0">
        <pos>
            <msid type="MSISDN" enc="ASC">xxxxxxxxxxx</msid>
            <pd>
                <time utc_off="+0800">20111122144915</time>
                <shape>
                    <EllipticalArea srsName="www.epsg.org#4326">
                        <coord>
                            <X>00 01 01N</X>
                            <Y>016 31 53E</Y>
                        </coord>
                        <angle>0</angle>
                        <semiMajor>2091</semiMajor>
                        <semiMinor>2091</semiMinor>
                        <angularUnit>Degrees</angularUnit>
                    </EllipticalArea>
                </shape>
                <lev_conf>90</lev_conf>
            </pd>
            <gsm_net_param>
                <cgi>
                    <mcc>100</mcc>
                    <mnc>01</mnc>
                    <lac>2222</lac>
                    <cellid>10002</cellid>
                </cgi>
                <neid>
                    <vmscid>
                        <vmscno>00004946000</vmscno>
                    </vmscid>
                    <vlrid>
                        <vlrno>99994946000</vlrno>
                    </vlrid>
                </neid>
            </gsm_net_param>
        </pos>
    </slia>
</svc_result>

person WesternGun    schedule 05.07.2021    source источник
comment
Можете ли вы привести несколько примеров того, что у вас есть в качестве файла и что вы хотели бы обслужить?   -  person agoff    schedule 06.07.2021
comment
Отредактируйте согласно запросу.   -  person WesternGun    schedule 07.07.2021


Ответы (2)


Моей первой мыслью было использовать transformerParameters для изменения файла ответов, вставив значение из тела. К сожалению, WireMock не разрешает хелперы перед их вставкой в ​​тело ответа. Поэтому, хотя мы можем ссылаться на это значение MSID с помощью помощника xpath, например

{{xPath request.body '/svc_init/slir/msids/msid/text()'}}

если мы попытаемся вставить это как настраиваемый параметр преобразователя, это не будет разрешено. (Я написал об этом вопрос на github WireMock.)

К сожалению, я думаю, что это оставляет нам необходимость писать собственное расширение, которое будет принимать запрос и находить значение, а затем изменять файл ответов. Дополнительную информацию о создании пользовательских расширений преобразования можно найти здесь.

person agoff    schedule 07.07.2021
comment
Ага. Так же, как я думал. Мы должны изобрести новые колеса. Спасибо за сообщение о проблеме автору. - person WesternGun; 09.07.2021

Наконец-то я создал свой собственный трансформатор:

package com.company.department.app.extensions;

import com.github.tomakehurst.wiremock.common.FileSource;
import com.github.tomakehurst.wiremock.extension.Parameters;
import com.github.tomakehurst.wiremock.extension.ResponseTransformer;
import com.github.tomakehurst.wiremock.http.Request;
import com.github.tomakehurst.wiremock.http.Response;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

public class NLGResponseTransformer extends ResponseTransformer {

    private static final Logger LOG = LoggerFactory.getLogger(NLGResponseTransformer.class);

    private static final String SLIA_FILE = "/stubs/__files/slia.xml";
    private static final String REQ_IMSI_XPATH = "/svc_init/slir/msids/msid";
    private static final String[] RES_IMSI_XPATHS = {
            "/svc_result/slia/pos/msid",
            "/svc_result/slia/company_mlp320_slia/company_netinfo/company_ms_netinfo/msid"
    };
    private static final String[] RES_TIME_XPATHS = {
            // for slia.xml
            "/svc_result/slia/company_mlp320_slia/company_netinfo/company_ms_netinfo/time",
            // for slia_poserror.xml
            "/svc_result/slia/pos/poserror/time"
    };

    private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance();
    private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
    private static final String UTC_OFF = "utc_off";
    private static final String TRANSFORM_FACTORY_ATTRIBUTE_INDENT_NUMBER = "indent-number";
    protected static final String COMPANY_MLP_320_SLIA_EXTENSION_DTD = "company_mlp320_slia_extension.dtd";
    protected static final String MLP_SVC_RESULT_320_DTD = "MLP_SVC_RESULT_320.DTD";

    @Override
    public String getName() {
        return "inject-request-values";
    }

    @Override
    public Response transform(Request request, Response response, FileSource fileSource, Parameters parameters) {
        Document responseDocument = injectValuesFromRequest(request);
        String transformedResponse = transformToString(responseDocument);
        if (transformedResponse == null) {
            return response;
        }
        return Response.Builder.like(response)
                .but()
                .body(transformedResponse)
                .build();
    }

    private Document injectValuesFromRequest(Request request) {
        // NOTE: according to quickscan:
        // "time" element in the MLP is the time MME reports cell_id to GMLC (NLG), NOT the time when MME got the cell_id.
        LocalDateTime now = LocalDateTime.now();
        Document responseTemplate = readDocument(SLIA_FILE);
        Document requestDocument = readDocumentFromBytes(request.getBody());
        if (responseTemplate == null || requestDocument == null) {
            return null;
        }
        try {
            injectIMSI(responseTemplate, requestDocument);
            injectTime(responseTemplate, now);
        } catch (XPathExpressionException e) {
            LOG.error("Cannot parse XPath expression {}. Cause: ", REQ_IMSI_XPATH, e);
        }
        return responseTemplate;
    }

    private Document readDocument(String inputStreamPath) {
        try {
            DocumentBuilder builder = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder();
            // ignore missing dtd
            builder.setEntityResolver((publicId, systemId) -> {
                if (systemId.contains(COMPANY_MLP_320_SLIA_EXTENSION_DTD) ||
                        systemId.contains(MLP_SVC_RESULT_320_DTD)) {
                    return new InputSource(new StringReader(""));
                } else {
                    return null;
                }
            });
            return builder.parse(this.getClass().getResourceAsStream(inputStreamPath));
        } catch (Exception e) {
            LOG.error("Cannot construct document from resource path. ", e);
            return null;
        }
    }

    private Document readDocumentFromBytes(byte[] array) {
        try {
            DocumentBuilder builder = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder();
            // ignore missing dtd
            builder.setEntityResolver((publicId, systemId) -> {
                if (systemId.contains(COMPANY_MLP_320_SLIA_EXTENSION_DTD) ||
                        systemId.contains(MLP_SVC_RESULT_320_DTD)) {
                    return new InputSource(new StringReader(""));
                } else {
                    return null;
                }
            });
            return builder.parse(new ByteArrayInputStream(array));
        } catch (Exception e) {
            LOG.error("Cannot construct document from byte array. ", e);
            return null;
        }
    }

    private XPath newXPath() {
        return XPathFactory.newInstance().newXPath();
    }


    private void injectTime(Document responseTemplate, LocalDateTime now) throws XPathExpressionException {
        for (String timeXPath: RES_TIME_XPATHS) {
            Node timeTarget = (Node) (newXPath().evaluate(timeXPath, responseTemplate, XPathConstants.NODE));
            if (timeTarget != null) {
                // set offset in attribute
                Node offset = timeTarget.getAttributes().getNamedItem(UTC_OFF);
                offset.setNodeValue(getOffsetString());
                // set value
                timeTarget.setTextContent(TIME_FORMAT.format(now));
            }
        }
    }

    private void injectIMSI(Document responseTemplate, Document requestDocument) throws XPathExpressionException {
        Node imsiSource = (Node) (newXPath().evaluate(REQ_IMSI_XPATH, requestDocument, XPathConstants.NODE));
        String imsi = imsiSource.getTextContent();
        for (String xpath : RES_IMSI_XPATHS) {
            Node imsiTarget = (Node) (newXPath().evaluate(xpath, responseTemplate, XPathConstants.NODE));
            if (imsiTarget != null) {
                imsiTarget.setTextContent(imsi);
            }
        }
    }


    private String transformToString(Document document) {
        if (document == null) {
            return null;
        }
        document.setXmlStandalone(true); // make document to be standalone, so we can avoid outputing standalone="no" in first line
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer trans;
        try {
            trans = tf.newTransformer();
            trans.setOutputProperty(OutputKeys.INDENT, "no"); // no extra indent; file already has intent of 4
            // cannot find a workaround to inject dtd in doctype line. TODO
            //trans.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, "MLP_SVC_RESULT_320.DTD [<!ENTITY % extension SYSTEM \"company_mlp320_slia_extension.dtd\"> %extension;]");
            StringWriter sw = new StringWriter();
            trans.transform(new DOMSource(document), new StreamResult(sw));
            // Spaces between tags are considered as text node, so when outputing we need to remove the extra empty lines
            return sw.toString().replaceAll("\\n\\s*\\n", "\n");
        } catch (TransformerException e) {
            LOG.error("Cannot transform response document to String. ", e);
            return null;
        }
    }


    /**
     * Compare system default timezone with UTC and get zone offset in form of (+/-)XXXX.
     * Dependent on the machine default timezone/locale.
     * @return
     */
    private String getOffsetString() {
        // getting offset in (+/-)XX:XX format, or "Z" if is UTC
        String offset = ZonedDateTime.ofInstant(Instant.now(), ZoneId.systemDefault()).getOffset().toString();
        if (offset.equals("Z")) {
            return "+0000";
        }
        return offset.replace(":", "");
    }
}

И используйте это так:

  1. mvn package это как JAR (не запускаемый), отложите его в сторону wiremock автономный jar, например libs
  2. Запустите это:
java -cp libs/* com.github.tomakehurst.wiremock.standalone.WireMockServerRunner --extensions com.company.department.app.extensions NLGResponseTransformer --https-port 8443 --verbose
  • Поместите всю команду в одну строку.

  • Обратите внимание, что jar-файл приложения, содержащий этот преобразователь, и автономный jar-файл wiremock должны быть среди путей к классам. Также нужны другие зависимости под libs. (Я использую плагин jib maven, который копирует все зависимости в libs/; я также перемещаю jar-файлы приложений и wiremock в libs/, поэтому я могу поставить -cp libs/*). Если это не сработает, попробуйте указать расположение этих двух банок в -cp. Учтите, что Wiremock будет работать нормально, даже если класс расширения не найден. Так что, возможно, добавьте некоторые журналы.

  • Вы можете использовать --root-dir для указания на корень файлов-заглушек, например --root-dir resources/stubs в моем случае. По умолчанию он указывает на . (где работает java).

person WesternGun    schedule 19.07.2021