#!/usr/bin/env qore
# -*- mode: qore; indent-tabs-mode: nil -*-

%require-types
%enable-all-warnings
%new-style
%strict-args

%requires ../../../../qlib/Util.qm
%requires ../../../../qlib/MapperUtil.qm
%requires ../../../../qlib/Logger.qm
%requires ../../../../qlib/DataProvider
%requires ../../../../qlib/ConnectionProvider
%requires ../../../../qlib/QUnit.qm
%requires ../../../../qlib/Mime.qm
%requires ../../../../qlib/HttpServerUtil.qm
%requires ../../../../qlib/HttpServer.qm
%requires ../../../../qlib/RestSchemaValidator.qm
%requires ../../../../qlib/Swagger.qm
%requires ../../../../qlib/RestClient.qm
%requires ../../../../qlib/SwaggerDataProvider
%requires ../../../../qlib/RestHandler.qm
%requires ../../../../qlib/Mapper.qm

%try-module xml >= 1.3
%define NoXml
%endtry

%try-module json
%define NoJson
%endtry

%try-module yaml
%define NoYaml
%endtry

%exec-class RestHandlerTest

class MyStaticRestClass inherits AbstractRestClass {
    string name() {
        return "static";
    }

    hash<HttpHandlerResponseInfo> get(hash<auto> cx, *hash<auto> ah) {
        return RestHandler::makeResponse(200, "static");
    }
}

class TestRestClass inherits AbstractRestClass {
    public {
        static int test_cnt = 0;
    }

    constructor() {
        addClass(new MyStaticRestClass());
    }

    string name() {
        return "test";
    }

    hash<HttpHandlerResponseInfo> getAction(hash<auto> cx, *hash<auto> ah) {
        return RestHandler::makeResponse(200, exists ah.action);
    }

    hash<HttpHandlerResponseInfo> getEcho(hash<auto> cx, *hash<auto> ah) {
        return RestHandler::makeResponse(200, cx.body);
    }

    hash<HttpHandlerResponseInfo> getEchoArgs(hash<auto> cx, *hash<auto> ah) {
        return RestHandler::makeResponse(200, ah);
    }

    hash<HttpHandlerResponseInfo> putEcho(hash<auto> cx, *hash<auto> ah) {
        return RestHandler::makeResponse(200, cx.body);
    }

    hash<HttpHandlerResponseInfo> put(hash<auto> cx, *hash<auto> ah) {
        return RestHandler::makeResponse(200, cx.body);
    }

    hash<HttpHandlerResponseInfo> putEx(hash<auto> cx, *hash<auto> ah) {
        # throw an exception with unserializable content
        throw "EXCEPTION-TEST-ERROR", "exception test error", new Mutex();
    }

    hash<HttpHandlerResponseInfo> putTest(hash<auto> cx, *hash<auto> ah) {
        ++test_cnt;
        return RestHandler::makeResponse(200, ah);
    }

    hash<auto> options(hash<auto> cx, *hash<auto> ah) {
        hash<auto> hdr = {
            "Access-Control-Allow-Origin": cx.hdr.origin ?? "*",
                "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
            "Access-Control-Max-Age": 9999999,
        };
        return RestHandler::makeResponse(200, NOTHING, hdr);
    }
}

class B2893RestClass inherits AbstractRestClass {
    string name() {
        return "b2893";
    }

    hash<HttpHandlerResponseInfo> get(hash<auto> cx, *hash<auto> ah) {
        if (ah.a)
            return RestHandler::makeResponse(200, "blabla");
        else
            return RestHandler::makeResponse(201, {"a": "abc123"});
    }
}

class B3426RestClass inherits AbstractRestClass {
    string name() {
        return "b3426";
    }

    hash<HttpHandlerResponseInfo> get(hash<auto> cx, *hash<auto> ah) {
        return RestHandler::makeResponse(200, "plain", {"Content-Type": "text/plain"});
    }
}

class B3427RestClass inherits AbstractRestClass {
    string name() {
        return "b3427";
    }

    hash get(hash<auto> cx, *hash<auto> ah) {
        return RestHandler::makeResponse(200, "openmetrics", {"Content-Type": "application/openmetrics-text"});
    }
}

class MyRestHandler inherits RestHandler {
    constructor() {
        addClass(new TestRestClass());
        addClass(new B3426RestClass());
        addClass(new B3427RestClass());
    }

    hash<HttpHandlerResponseInfo> putEcho(hash<auto> cx, *hash<auto> ah) {
        return RestHandler::makeResponse(200, cx.body);
    }

    hash<HttpHandlerResponseInfo> getAction(hash<auto> cx, *hash<auto> ah) {
        return RestHandler::makeResponse(200, exists ah.action);
    }

    private *hash<auto> errorResponseHeaders(hash<auto> cx) {
        return {
            "Error": "True",
        };
    }
}

class SwaggerRestHandler inherits RestHandler {
    constructor(AbstractRestSchemaValidator validator) : RestHandler(NOTHING, validator) {
        addClass(new TestRestClass());
        addClass(new B2893RestClass());
    }
}

class DummyListenerInterface inherits HttpListenerInterface {
    addUserThreadContext(hash<auto> uctx) {}
    auto removeUserThreadContext(*string k) {}
    log(string fmt, ...) {}
    logError(string fmt, ...) {}
}

public class RestHandlerTest inherits QUnit::Test {
    private {
        HttpServer mServer;
        RestClient mClient;
        MyRestHandler mHandler();
%ifndef NoJson
        SwaggerRestHandler swaggerHandler(RestHandlerTest::getSwagger());
%else
        SwaggerRestHandler swaggerHandler;
%endif
        int port;

        const SimpleHashValue = ("a": "1");
        const LargerHashValue = ("a": "1", "b": "2");
        const HashValue = ("a": ("1", "2"));

        const ListValue = ("1", "two", "3");

        const AllValues = ("1", "one", ListValue, SimpleHashValue, LargerHashValue, HashValue);

        const AllYamlValues = AllValues + (now_us(), <bead>);

        const DataValues = (
%ifndef NoXml
            "xml": AllValues,
%endif
            "auto": AllValues,
%ifndef NoYaml
            "yaml": AllYamlValues,
%endif
%ifndef NoJson
            "json": AllValues,
%endif
%ifndef NoXml
            "rawxml": SimpleHashValue,
%endif
            "url": (SimpleHashValue, LargerHashValue),
            "text": "string",
            "bin": <abcd>,
        );

        const TestSchema = (
            "swagger": "2.0",
            "info": (
                "title": "title",
                "version": "1.0",
            ),
            "basePath": "/swagger",
            "paths": (
                "/test": (
                    "parameters": (
                        (
                            "in": "query",
                            "name": "action",
                            "type": "string",
                        ),
                        (
                            "in": "body",
                            "name": "body",
                            "schema": (
                                "$ref": "#/definitions/Any",
                            ),
                        ),
                    ),
                    "put": (
                        "produces": (MimeTypeJson,),
                        "consumes": (MimeTypeJson,),
                        "responses": (
                            "200": (
                                "description": "",
                                "schema": (
                                    "$ref": "#/definitions/Any",
                                ),
                            ),
                        ),
                    ),
                ),
                "/b2543": (
                    "post": (
                        "produces": (MimeTypeJson,),
                        "consumes": (MimeTypeJson,),
                        "parameters": (
                            {
                                "in": "body",
                                "name": "body",
                                "required": True,
                                "schema": (
                                    "type": "object",
                                    "required": ("a",),
                                    "properties": {"a": {"type": "string"}}
                                ),
                            },
                        ),
                        "responses": (
                            "200": ("description": "success",),
                        ),
                    ),
                ),
                "/b2893": (
                    "get": (
                        "produces": ("text/plain", MimeTypeJson,),
                        "consumes": (MimeTypeJson,),
                        "parameters": (
                            {
                                "in": "body",
                                "name": "body",
                                "required": True,
                                "schema": (
                                    "type": "object",
                                    "required": ("a",),
                                    "properties": {
                                        "a": {
                                            "type": "boolean",
                                            "description": "whether to return a simple string (or a hash value instead)"
                                        }
                                    }
                                ),
                            },
                        ),
                        "responses": (
                            "200": (
                                "description": "success",
                                "schema": {
                                    "type": "string"
                                }
                            ),
                            "201": (
                                "description": "success",
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "a": {
                                            "type": "string",
                                        }
                                    }
                                }
                            ),
                        ),
                    ),
                ),
            ),
            "definitions": (
                "Any": (
                    "type": "object",
                    "additionalProperties": (
                        "type": "string",
                    ),
                ),
            ),
        );
    }

    public {
    }

    constructor() : Test("RestHandlerTest", "1.0") {
        addTestCase("pre-encoded-URLs test", \preEncodedUrlsTest());
        addTestCase("test bad request", \badRequestTest());
        addTestCase("Test direct interface", \directTest());
        addTestCase("Test external serialization", \serializationTest());
        addTestCase("Test xml", \xmlTest());
        addTestCase("Test swagger", \swaggerTest());
        addTestCase("issue 2398", \issue2398());
        addTestCase("issue 2543", \issue2543());
        addTestCase("issue 2893", \issue2893());
        addTestCase("issue 3426", \issue3426());
        addTestCase("issue 3427", \issue3427());
        addTestCase("Missing subclass test", \missingSubclassTest());

        # Return for compatibility with test harness that checks return value.
        set_return_value(main());
    }

    static SwaggerSchema getSwagger() {
%ifndef NoJson
        return SwaggerLoader::fromString(make_json(TestSchema));
%endif
    }

    globalSetUp() {
        Logger logger("test", LoggerLevel::getLevelInfo());
        if (m_options.verbose > 2) {
            logger.addAppender(new StdoutAppender());
        }
        hash<HttpServerOptionInfo> http_opts = <HttpServerOptionInfo>{
            "logger": logger,
            "debug": True,
        };
        mServer = new HttpServer(http_opts);
        mServer.setHandler("rest-handler", "/test", MimeTypeHtml, mHandler, NOTHING, False);
%ifndef NoJson
        mServer.setHandler("swagger-handler", "/swagger", MimeTypeJson, swaggerHandler, NOTHING, False);
%endif
        mServer.setDefaultHandler("rest-handler", mHandler);
        port = mServer.addListener(<HttpListenerOptionInfo>{"service": 0}).port;

        mClient = new RestClient({"url": "http://localhost:" + port});
    }

    globalTearDown() {
        mServer.stop();
        delete mServer;
    }

    preEncodedUrlsTest() {
        {
            RestClient rest({
                "url": mClient.getURL(),
                "pre_encoded_urls": True,
            });

            assertEq({"arg": "hello"}, rest.put("/test/echo?arg=hello").body);
            assertEq({"arg": "[hello]"}, rest.put("/test/echo?arg=%5Bhello%5D").body);
            assertEq({"arg": "{hello}"}, rest.put("/test/echo?arg=%7Bhello%7D").body);
            assertEq({"arg": "hello|0"}, rest.put("/test/echo?arg=hello%7C0").body);
            assertEq({"arg": "hello\\0"}, rest.put("/test/echo?arg=hello%5C0").body);
            assertEq({"arg": "hello~0"}, rest.put("/test/echo?arg=hello%7E0").body);
            assertEq({"arg": "hello`0"}, rest.put("/test/echo?arg=hello%600").body);
            assertEq({"arg": "hello^0"}, rest.put("/test/echo?arg=hello%5E0").body);

            assertThrows("URL-ENCODING-ERROR", "path\\[0\\]", \rest.get(), "path[0]");
            assertThrows("URL-ENCODING-ERROR", \rest.get(), "path{0}");
            assertThrows("URL-ENCODING-ERROR", \rest.get(), "path|0");
            assertThrows("URL-ENCODING-ERROR", \rest.get(), "path\\0");
            assertThrows("URL-ENCODING-ERROR", \rest.get(), "path~0");
            assertThrows("URL-ENCODING-ERROR", \rest.get(), "path`0");
            assertThrows("URL-ENCODING-ERROR", \rest.get(), "path^0");
        }

        {
            RestClient rest({
                "url": mClient.getURL(),
            });

            assertEq({"arg": "hello"}, rest.put("/test/echo?arg=hello").body);
            assertEq({"arg": "%5Bhello%5D"}, rest.put("/test/echo?arg=%5Bhello%5D").body);
            assertEq({"arg": "%7Bhello%7D"}, rest.put("/test/echo?arg=%7Bhello%7D").body);
            assertEq({"arg": "hello%7C0"}, rest.put("/test/echo?arg=hello%7C0").body);
            assertEq({"arg": "hello%5C0"}, rest.put("/test/echo?arg=hello%5C0").body);
            assertEq({"arg": "hello%7E0"}, rest.put("/test/echo?arg=hello%7E0").body);
            assertEq({"arg": "hello%600"}, rest.put("/test/echo?arg=hello%600").body);
            assertEq({"arg": "hello%5E0"}, rest.put("/test/echo?arg=hello%5E0").body);

            assertEq("path[0]", rest.get("test/echo?arg=path[0]").body.arg);
            assertEq("path{0}", rest.get("test/echo?arg=path{0}").body.arg);
            assertEq("path|0", rest.get("test/echo?arg=path|0").body.arg);
            assertEq("path\\0", rest.get("test/echo?arg=path\\0").body.arg);
            assertEq("path~0", rest.get("test/echo?arg=path~0").body.arg);
            assertEq("path`0", rest.get("test/echo?arg=path`0").body.arg);
            assertEq("path^0", rest.get("test/echo?arg=path^0").body.arg);
        }
    }

    badRequestTest() {
        {
            HTTPClient client({"url": mClient.getURL()});

            hash<auto> info;
            on_error printf("info: %N\n", info);

            string body = make_yaml(LargerHashValue);
            hash<auto> hdr = {
                "Content-Type": MimeTypeYaml,
                "Accept": MimeTypeYaml,
            };
            hash<auto> h = client.send(body, "PUT", "test?action=echo", hdr, False, \info);
            assertEq(body, h.body);

            hdr = {
                "Content-Type": MimeTypeJson,
                "Accept": MimeTypeJson,
            };
            assertThrows("HTTP-CLIENT-RECEIVE-ERROR", \client.send(),
                ("[x x]", "PUT", "test?action=echo", hdr, False, \info));
            assertEq(400, info."response-headers".status_code);
            assertEq("True", info."response-headers".error);
        }
        {
            RestClient client({"url": mClient.getURL()});

            hash<auto> info;
            on_error printf("info: %N\n", info);

            client.setSerialization("bin");
            hash<auto> hdr = {
                "Content-Type": MimeTypeJson,
                "Accept": MimeTypeJson,
            };
            assertThrows("REST-RESPONSE-ERROR", "test.action=echo", \client.put(),
                ("test?action=echo", "[x x]", \info, hdr));
            assertEq(400, info."response-headers".status_code);
            assertEq("True", info."response-headers".error);

        }
    }

    directTest() {
        # test direct interface
        auto val = mHandler.handleExternalRequest("GET", "test?action=echo", LargerHashValue);
        assertEq(LargerHashValue, val);
        val = mHandler.handleExternalRequest("PUT", "test?action=echo", LargerHashValue);
        assertEq(LargerHashValue, val);
        val = mHandler.handleExternalRequest("GET", "test?action=echo", HashValue);
        assertEq(HashValue, val);
        val = mHandler.handleExternalRequest("PUT", "test?action=echo", HashValue);
        assertEq(HashValue, val);
        val = mHandler.handleExternalRequest("GET", "test?action=action");
        assertEq(False, val);
        val = mHandler.handleExternalRequest("OPTIONS", "test");
        assertNothing(val);

        # issue #2994: test URI path access to actions
        val = mHandler.handleExternalRequest("GET", "test/echo", LargerHashValue);
        assertEq(LargerHashValue, val);
        val = mHandler.handleExternalRequest("PUT", "test/echo", LargerHashValue);
        assertEq(LargerHashValue, val);
        val = mHandler.handleExternalRequest("GET", "test/echo", HashValue);
        assertEq(HashValue, val);
        val = mHandler.handleExternalRequest("PUT", "test/echo", HashValue);
        assertEq(HashValue, val);
        val = mHandler.handleExternalRequest("GET", "test/action");
        assertEq(False, val);

        # issue #2994: tests on the root
        val = mHandler.handleExternalRequest("GET", "?action=action");
        assertEq(False, val);

        val = mHandler.handleExternalRequest("GET", "action");
        assertEq(False, val);

        # invalid subclass
        assertThrows("REST-CLASS-ERROR", "action.*test", \mHandler.handleExternalRequest(), ("GET", "invalid"));

        try {
            mHandler.handleExternalRequest("PUT", "test/echo1");
            assertTrue(False);
        } catch (hash<ExceptionInfo> ex) {
            # get hash of subclasses
            assertRegex("known subclasses: \"", ex.desc);
            hash<string, bool> sc_map = map {
                $1[1..$1.size()-2]: True,
            }, (map trim($1), (ex.desc =~ x/known subclasses: (.*)$/)[0].split(","));
            assertEq({
                "static": True,
                "test": True,
                "ex": True,
                "echo": True,
            }, sc_map);
        }

        assertEq("static", mHandler.handleExternalRequest("GET", "test/static"));

        # issue #3373 exception test
        assertThrows("EXCEPTION-TEST-ERROR", "exception test error", \mHandler.handleExternalRequest(), ("PUT", "test/ex"));
    }

    serializationTest() {
%ifdef NoYaml
%ifdef NoXml
%ifdef NoJson
        # cannot test serialization with no binary modules present
        testSkip("no serialization modules present");
%endif
%endif
%endif
        mClient.setSerialization("auto");

        map testSerialization($1.key, $1.value), DataValues.pairIterator();

        mClient.setSerialization("auto");

        {
            hash info;
            on_error printf("info: %N\n", info);

            hash h = mClient.get("test?action=echo", LargerHashValue, \info);
            assertEq(LargerHashValue, h.body);
            h = mClient.put("test?action=echo", LargerHashValue, \info);
            assertEq(LargerHashValue, h.body);
            h = mClient.get("test?action=echo", HashValue, \info);
            assertEq(HashValue, h.body);
            h = mClient.put("test?action=echo", "1", \info);
            assertEq("1", h.body);
            h = mClient.put("test?action=echo", ListValue, \info);
            assertEq(ListValue, h.body);

            h = mClient.doRequest("OPTIONS", "test", NOTHING, \info);
            assertEq(200, h.status_code);
            assertNothing(h.body);
            assertEq("0", h."content-length");

            # issue #3373 exception test
            assertThrows("EXCEPTION-TEST-ERROR", "exception test error", \mClient.doRequest(), ("PUT", "test/ex", NOTHING, \info));
        }
    }

    xmlTest() {
%ifdef NoXml
        testSkip("no xml module present");
%endif

        # special tests for rawxml (typeless / raw XML encoding)
        mClient.setSerialization("rawxml");

        {
            hash info;
            on_error printf("info: %N\n", info);

            hash h = mClient.get("test?action=echo", LargerHashValue, \info);
            assertEq(("value": LargerHashValue), h.body);
            h = mClient.put("test?action=echo", LargerHashValue, \info);
            assertEq(("value": LargerHashValue), h.body);
            h = mClient.get("test?action=echo", HashValue, \info);
            assertEq(("value": HashValue), h.body);
            h = mClient.put("test?action=echo", HashValue, \info);
            assertEq(("value": HashValue), h.body);
            h = mClient.put("test?action=echo", "1", \info);
            assertEq(("value": "1"), h.body);
            h = mClient.put("test?action=echo", ListValue, \info);
            assertEq(("list": ("element": ListValue)), h.body);
        }
    }

    swaggerTest() {
%ifdef NoJson
        # cannot test serialization with no binary modules present
        testSkip("no json module present");
%endif

        # test direct interface (no Swagger validation performed); base path must be removed
        auto val = swaggerHandler.handleExternalRequest("GET", "/test?action=echo", LargerHashValue);
        assertEq(LargerHashValue, val);
        val = swaggerHandler.handleExternalRequest("PUT", "/test?action=echo", LargerHashValue);
        assertEq(LargerHashValue, val);
        val = swaggerHandler.handleExternalRequest("GET", "/test?action=echo", HashValue);
        assertEq(HashValue, val);
        val = swaggerHandler.handleExternalRequest("PUT", "/test?action=echo", HashValue);
        assertEq(HashValue, val);

        {
            mClient.setSerialization("json");
            hash<auto> info;
            {
                on_error printf("info: %N\n", info);
                hash<auto> h = mClient.put("/swagger/test?action=echo", SimpleHashValue, \info);
                assertEq(SimpleHashValue, h.body);
                assertThrows("REST-RESPONSE-ERROR", \mClient.put(), ("/swagger/test?invalid=sdf", SimpleHashValue, \info));
            }
            assertEq(400, info."response-code");
        }

        string url = "http://localhost:" + port + "/swagger";
        SwaggerSchema swagger = RestHandlerTest::getSwagger();
        RestConnection conn("test", "test", url, {}, {"validator": swagger});
        AbstractDataProvider rest_provider = conn.getDataProvider().getChildProviderEx("test").getChildProviderEx("PUT");
        assertEq("/swagger", cast<SwaggerRequestDataProvider>(rest_provider).getSchema().getBasePath());

        #printf("req: %N\nresp: %N\n", rest_provider.getRequestType(), rest_provider.getResponseType());

        int cnt = TestRestClass::test_cnt;

        {
            hash<auto> mappings = {
                "A": "body",
            };
            NullOutputProvider null_provider();
            Mapper m(mappings, {
                "input_provider": rest_provider,
                "input_request": {"query": {"action": "test"}, "body": {"A": "1"}},
                "output_provider": null_provider,
            });
            m.runAutonomous();
            assertEq(++cnt, TestRestClass::test_cnt);
        }

        {
            hash<auto> mappings = {
                "query.action": "action",
                "body.A": "a",
            };
            Mapper m(mappings, {
                "output_provider": rest_provider,
            });
            m.mapData({"action": "test", "a": "1"});
            assertEq(++cnt, TestRestClass::test_cnt);
        }
    }

    urlTest() {
        # special tests for "url" (form URL encoding)
        mClient.setSerialization("url");

        hash info;
        on_error printf("info: %N\n", info);

        assertThrows("FORMURLENCODING-ERROR", \mClient.put(), ("test?action=echo", "1", \info));
        assertThrows("FORMURLENCODING-ERROR", \mClient.put(), ("test?action=echo", ListValue, \info));
        assertThrows("INVALID-VALUE", \mClient.put(), ("test?action=echo", HashValue, \info));

        auto rv = mClient.put("test?action=echo", ("a": "1"), \info);
        assertEq(("a": "1"), rv.body);
    }

    issue2398() {
        hash info;
        on_error printf("info: %N\n", info);

        HTTPClient hc(("url": "http://localhost:" + port));

        hash<ExceptionInfo> ex;
        try {
            hc.send(<01ff008001020304>, "PUT", "test?action=echo", ("Content-Type": MimeTypeFormUrlEncoded + ";charset=UTF-8"), False, \info);
        } catch (hash<ExceptionInfo> ex1) {
            #printf("%s", get_exception_string(ex1));
            ex = ex1;
        }
        assertRegex("invalid.*encoding", ex.arg.body);
        assertEq(400, ex.arg.status_code);
    }

    issue2543() {
%ifdef NoJson
        testSkip("no json module present");
%endif

        # dummy HttpListenerInterface object
        DummyListenerInterface listener();
        # socket object needed for call; will not be used
        Socket s();

        hash hdr = {
            "http_version" : "1.1",
            "method" : "POST",
            "path" : "b2543",
            "host" : "localhost:8200",
            "user-agent" : "curl/7.52.1",
            "accept" : "*/*",
            "content-type" : "application/json",
            "content-length" : "24",
            "_qore_orig_content_type" : "application/json",
        };

        string body_str = convert_encoding('{"a":"\u017dil\u00e1k"}', "iso-8859-1");

        hash cx = {
            "raw_path" : "/b2543",
            "char_encoding" : "ISO-8859-1",
            "response-encoding" : "utf8",
            "url" : {"path" : "b2543"},
            "rest_action_method" : "post",
            "hdr" : hdr,
            "aih" : {"*/*" : True},
        };

        # this would throw exception before fix
        auto result = swaggerHandler.handleRequest(listener, s, cx, hdr, body_str);
        assertEq(400, result.code);
    }

    issue2893() {
%ifdef NoJson
        testSkip("no json module present");
%endif

        mClient.setSerialization("json");
        hash info;
        {
            on_error printf("info: %N\n", info);
            hash h = mClient.get("/swagger/b2893", {"a": True}, \info);
            assertEq(200, info."response-code");
            assertEq("text/plain", info."body-content-type");
            assertEq("blabla", h.body);

            h = mClient.get("/swagger/b2893", {"a": False}, \info);
            assertEq(201, info."response-code");
            assertEq("application/json", info."body-content-type");
            assertEq({"a": "abc123"}, h.body);
        }
    }

    issue3426() {
        hash info;
        on_error printf("info: %N\n", info);

        HTTPClient hc(("url": "http://localhost:" + port));

        string resp = hc.get("/b3426", {"accept": "application/openmetrics-text; version=0.0.1,text/plain;version=0.0.4;q=0.5"}, \info);
        assertEq(200, info."response-headers".status_code);
        assertEq("text/plain", info."body-content-type");
        assertEq("plain", resp);
    }

    issue3427() {
        hash info;
        on_error printf("info: %N\n", info);

        HTTPClient hc(("url": "http://localhost:" + port));

        string resp;
        resp = hc.get("/b3427", {"accept": "application/openmetrics-text; version=0.0.1,text/plain;version=0.0.4;q=0.5"}, \info);
        assertEq(200, info."response-headers".status_code);
        assertEq("application/openmetrics-text", info."body-content-type");
        assertEq("openmetrics", resp);

        resp = hc.get("/b3427", {"accept": "application/*; version=0.0.1,text/plain;version=0.0.4;q=0.5"}, \info);
        assertEq(200, info."response-headers".status_code);
        assertEq("application/openmetrics-text", info."body-content-type");
        assertEq("openmetrics", resp);

        resp = hc.get("/b3427", {"accept": "*/*; version=0.0.1,text/plain;version=0.0.4;q=0.5"}, \info);
        assertEq(200, info."response-headers".status_code);
        assertEq("application/openmetrics-text", info."body-content-type");
        assertEq("openmetrics", resp);
    }

    missingSubclassTest() {
        mClient.setSerialization("auto");
        assertThrows("REST-RESPONSE-ERROR", "404.*has no subclass", \mClient.post(), ("test/bla", NOTHING, NOTHING));
    }

    testSerialization(string data, softlist values) {
        mClient.setSerialization(data);

        hash<auto> info;
        on_error printf("info: %N\n", info);

        foreach auto val in (values) {
            hash<auto> h = mClient.get("test?action=echo", val, \info);
            assertEq(val, h.body, sprintf("GET %s: %s", data, val.type()));
            h = mClient.put("test?action=echo", val, \info);
            assertEq(val, h.body, sprintf("PUT %s: %s", data, val.type()));
        }

        hash<auto> h = mClient.get("test?action=echoArgs;arg=1", NOTHING, \info);
        assertEq(("arg": "1"), h.body);
        if (data != "text" && data != "bin") {
            h = mClient.get("test?action=echoArgs", ("arg": "1"), \info);
            assertEq(("arg": "1"), h.body);
        }
        h = mClient.get("test?action=echoArgs", NOTHING, \info);
        assertEq({}, h.body);
    }
}

class NullOutputProvider inherits AbstractDataProvider {
    public {
        int test_cnt = 0;
    }

    string getName() {
        return "test";
    }

    private auto doRequestImpl(auto req, *hash<auto> request_options) {
        ++test_cnt;
    }

    hash<DataProviderInfo> getStaticInfoImpl() {
        return <DataProviderInfo>{
            "name": getName(),
            "type": self.className(),
            "supports_request": True,
        };
    }

    private *AbstractDataProviderType getRequestTypeImpl() {
    }
}
