#!/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/QUnit.qm
%requires ../../../../qlib/Mime.qm
%requires ../../../../qlib/Logger.qm
%requires ../../../../qlib/HttpServerUtil.qm
%requires ../../../../qlib/HttpServer.qm

%exec-class HttpServerTest

# we explicitly test deprecated behavior
%disable-warning deprecated

class AltHandler inherits AbstractHttpRequestHandler {
    hash handleRequest(hash cx, hash hdr, *data body) {
        return makeResponse(201, "a1, " + hdr.method + ", " + hdr.path + ", " + cx.raw_path);
    }
}

class SimpleStringHandler inherits AbstractHttpRequestHandler {
    private { string mString; }
    constructor (string str) { mString = str; }
    hash handleRequest(hash cx, hash hdr, *data body) {
        return makeResponse(200, mString);
    }
}

class ReqHandler inherits AbstractHttpRequestHandler {
    hash<HttpResponseInfo> handleRequest(hash<auto> cx, hash<auto> hdr, *data body) {
        string rpath = hdr.path;
        if (rpath =~ /^test400\//) {
            return make400("test400");
        } else if (rpath =~ /^test500\//) {
            return AbstractHttpRequestHandler::makeResponse(500, <74657374>);
        } else if (rpath =~ /^test501\//) {
            return make501("test501");
        } else if (rpath =~ /^nonexistingcode/) {
            return makeResponse(723, "test");
        } else if (rpath =~ /^test102\//) {
            return makeResponse(102, "test");
        } else if (rpath =~ /^test204\//) {
            return makeResponse(204, "test");
        } else if (rpath =~ /^test304\//) {
            return makeResponse(304, "test");
        } else if (rpath =~ /^test400close\//) {
            return makeResponse(400, new StringInputStream("test"), {"Connection": "close"});
        } else if (rpath =~ /^fileupload/) {
            string boundary = (hdr."_qore_orig_content_type" =~ x/boundary="?(.+)"?/i)[0];
            hash<MessageInfo> h = MultiPartMessage::parseBody(boundary, body, True);
            #printf("h: %N\n", h);
            return makeResponse(200, new BinaryInputStream(h.part[1].body), {"Content-Type": h.part[1].hdr."content-type"});
        }

        if (hdr.method == "POST") {
            return makeResponse(200, "POST, " + body + ", " + rpath + ", " + cx.raw_path);
        } else {
            return makeResponse(200, hdr.method + ", " + rpath + ", " + cx.raw_path);
        }
    }
}

public class HttpServerTest inherits QUnit::Test {
    private {
        HttpServer mServer;
        HTTPClient mClient;
        ReqHandler mHandler;
        int port;
        string url;

        const CertPem = "-----BEGIN CERTIFICATE-----
MIIFNjCCBB6gAwIBAgISA1ELHYM0GFfi2BdvZfzLGJMLMA0GCSqGSIb3DQEBCwUA
MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD
EwJSMzAeFw0yMjA4MDcwOTAwMDdaFw0yMjExMDUwOTAwMDZaMCIxIDAeBgNVBAMT
F2hxLnFvcmV0ZWNobm9sb2dpZXMuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAlw84m+9dCjeD4s8YYVZ3gbv8YkYQGyyaojgdJuarTyJPYnGzhMf7
PF+Y2398k+8ydAnOTwJXRcaU36f0hHDvTagP1f0EFg3kWlplzsrtrDI/HZBRH0W2
+54YE6FhPEtEkkO1aKy+VXb8QMQKSmsUZS+IxSe+69tWFI60tW92eq53tFAwEWN+
0oBeJBCASRQa7bq1J/BgZhlBSyUQ6Zf+wqUSfmVEf7tsFujyNZ2dfxHtPtLxRspA
7yhgnQQaXHemyEWeZWjIbToazG8SvutmcwIFK/TtE0anWdTR+Hn92mxH9mxnnyns
Ag+RlqxnEpBDIb1ufwCFUhMXBwRiiZIM1QIDAQABo4ICVDCCAlAwDgYDVR0PAQH/
BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8E
AjAAMB0GA1UdDgQWBBSj2nUL9X+zXI7p0fFhg03WZAS0xDAfBgNVHSMEGDAWgBQU
LrMXt1hWy65QCUDmH6+dixTCxjBVBggrBgEFBQcBAQRJMEcwIQYIKwYBBQUHMAGG
FWh0dHA6Ly9yMy5vLmxlbmNyLm9yZzAiBggrBgEFBQcwAoYWaHR0cDovL3IzLmku
bGVuY3Iub3JnLzAiBgNVHREEGzAZghdocS5xb3JldGVjaG5vbG9naWVzLmNvbTBM
BgNVHSAERTBDMAgGBmeBDAECATA3BgsrBgEEAYLfEwEBATAoMCYGCCsGAQUFBwIB
FhpodHRwOi8vY3BzLmxldHNlbmNyeXB0Lm9yZzCCAQYGCisGAQQB1nkCBAIEgfcE
gfQA8gB3AN+lXqtogk8fbK3uuF9OPlrqzaISpGpejjsSwCBEXCpzAAABgne/YgYA
AAQDAEgwRgIhAP/ERcskhKhF7M8VIejtrwEtDXJoX1IXec//r64jkOlUAiEAhheZ
VMT5cZ2uoPGoD6+SuQY/CYBuHdXNR/pUC3SGOQEAdwApeb7wnjk5IfBWc59jpXfl
vld9nGAK+PlNXSZcJV3HhAAAAYJ3v2H5AAAEAwBIMEYCIQCmiChg/6dLhE3TfGum
JR7k8s7ibmqw2KVJI3oUR/ogBAIhAIXIIhyqb3W/34ATNT9dIPCGFKpghQw82G8w
ANnChDgYMA0GCSqGSIb3DQEBCwUAA4IBAQAovvC8AiF7+uNLJCEXMe3VeI4Ne+l1
mqCRRujz7ijlr6VgNxZt+i/kx1HJKrSKnQZRQ48xWAipMVYfXxH3u20p4RkkW2nj
jjIVQvQvlFhDjLaJR74PYopp0lPuBW9RKg+C+l3vvjxjkin/MOBX2apGOvC4LJwb
2s6f8cArBRvdhA7nwEmlP3+aqxxkp9STZYpxuKR4F9fRGtg0Y39Db+3XkYp7Y/hV
DBVpHegoty2VFErehkJUmgdNoLdTuC8gHgA3p5bCbApyVGjBuO+QpSWG/3WiBDfm
1IpGT8P2OhFruQHryHEKmY6f5huZV2Y0gFmqBwRiR1ToF5gFveTUJM3d
-----END CERTIFICATE-----";

        const KeyPem = "-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXDzib710KN4Pi
zxhhVneBu/xiRhAbLJqiOB0m5qtPIk9icbOEx/s8X5jbf3yT7zJ0Cc5PAldFxpTf
p/SEcO9NqA/V/QQWDeRaWmXOyu2sMj8dkFEfRbb7nhgToWE8S0SSQ7VorL5VdvxA
xApKaxRlL4jFJ77r21YUjrS1b3Z6rne0UDARY37SgF4kEIBJFBrturUn8GBmGUFL
JRDpl/7CpRJ+ZUR/u2wW6PI1nZ1/Ee0+0vFGykDvKGCdBBpcd6bIRZ5laMhtOhrM
bxK+62ZzAgUr9O0TRqdZ1NH4ef3abEf2bGefKewCD5GWrGcSkEMhvW5/AIVSExcH
BGKJkgzVAgMBAAECggEAPhvyCJtYQ9UjkuPXgF4O8PacBMQN5z5lrgEoa1A4a2cO
AMoDJ7sZ327m6Ij4bdLRichmXTH3NCc8GuFxterBWcqaCD/pqC+6DjRQ27+wDTbz
oHIwCI2feME94QRfeGzyGrlgI1OzRmyPtwljuclhL2Fl+Loo08zxDa7HOjpEGphz
wkD2gQy5S7Mi5ekBZMhy8EklpzdKlOFdvhjo7vQu8vY8xJZ6jN65RV+RBscbmZQl
llK1uW/8UyRIAUK9sM6Ozxbiz7QQELHpSvY3JIiZC5A6rBk2DGqOu8ouzDrQuHsB
LHWjzceu6UcJY0tCeOUkVDnoxbj2UcINEAIABIPN2QKBgQDHHbrwdW8qRecLw8y2
FJDO/6+8VmRKtj8B7CeOYuEZet2UvnPssisCovu0jnMa2ysF/L4YQAWa5XY0bB+E
R/NRzeh4i0jIsW70h2qePTK+XRAGhCCKiCcb7nkED72TTplQ5wgRA/oWPLOrys34
f+YqCI7BVrbWzgDyrhEdRAXSbwKBgQDCNuMlLcGTTi04OIQFDZmzjdvx9KvHvksz
zff9Y0Rb1eUsXF2oG3S55umgeUcG9Nk2KigJzO6WAmTT6rcbn4tsRsnkLdqcEPmg
P1wbBikW3Bz935k7yPcx05bVshLrJsK0Gxa8YYE6hh/PbdiQFtHf2r4uYu1kxC01
518U8oPm+wKBgDfyJIpXlKp+BZMKqsQmNyHSOaBjbb6IQl/Z6KtbIQA1w3h9orjI
vsj43lw3AiRznD0MbKUHqAuDmZjVIG3cgYNkpYLpL8QkBpbyTYS0kUNnho8uJK6H
3uU8NghsG8n99ZoDsAKH6YbB+4Gzc/f0h8kbqnCsWqc0LpQBUJG2gSRFAoGBAI3P
4kBNjuFu3hoFOnEuIyM23HlqPNyXGPZ02TXOfCXKo5Kmx0Ru9+aes80XgUOVGd4x
Hhc56qTijpkm9BlZgEbJ0bWpvczjoELgwPKCpxIoG4tM7+j1r3pUk/jqFGJcZSN5
/DoFwITpVuTxwoZEA2+/m8rnNYy0qoaHsafsBWBtAoGAAPYkINDjhAslu2gQlsZi
7eA4coUg5eNUI+YYy4xonJAU488AsrcAWr5nGEids4ZM5PqKkJezG+6gctaYnjjZ
IQrghzR1AC0Y3XDSuJYGRcZeUF2XZmOjcXNm0oMxeMEeEuuNLOTZ2pp7aUcYIOfV
OKKqgBjNAlYDdPhOy8O8Agk=
-----END PRIVATE KEY-----";

        const BinImage = <89504e470d0a1a0a0000000d4948445200000010000000100403000000eddde25200000018504c5445ffffff> +
            <6b6b6b8c8c8cc0c0c0ef5a5a846331efd63900000089492ed30000000474524e53ffffff00402aa9f400000001624b47440716> +
            <6188eb0000004b4944415408d7633086020610615a0c65a8ba4319a1e55046581184619e1a0c61288119060c0ce6e94c504631> +
            <2394c100661828194318020c0c688c40410506a8628888912010881a1b0300fcbe1520c136ff8b0000000049454e44ae426082>;
    }

    constructor() : Test("HttpServerTest", "1.0") {
        addTestCase("issue 4312", \issue4312());
        addTestCase("issue 3397", \issue3397());
        addTestCase("issue 3116", \issue3116());
        addTestCase("issue 2805", \issue2805());
        addTestCase("Test basics", \basicTest());
        addTestCase("Test routing", \routingTest());
        addTestCase("Test status codes", \testStatusCodes());
        addTestCase("misc", \misc());
        addTestCase("2nd wildcard listener", \secondWildcardListener());
        addTestCase("bug 2936 multipart form-data binary file upload", \multipartFormDataBinaryFileTest());

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

    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);
        mHandler = new ReqHandler();
        mServer.setHandler("my-handler", "", MimeTypeHtml, mHandler);
        mServer.setHandler("alt-handler", "/alt1", MimeTypeHtml, new AltHandler());
        mServer.setHandler("/route/a/c", "/route/a/c", MimeTypeHtml, new SimpleStringHandler("/route/a/c"));
        mServer.setHandler("/route/a", "/route/a", MimeTypeHtml, new SimpleStringHandler("/route/a"));
        mServer.setHandler("/route/b", "/route/b", MimeTypeHtml, new SimpleStringHandler("/route/b"));
        mServer.setHandler("/route", "/route", MimeTypeHtml, new SimpleStringHandler("/route"));
        mServer.setDefaultHandler("my-handler", mHandler);
        port = mServer.addListener(<HttpListenerOptionInfo>{"service": 0}).port;
        url = "http://localhost:" + port;
    }

    globalTearDown() {
        delete mServer;
        delete mHandler;
    }

    setUp() {
        mClient = new HTTPClient(("url": url, "additional_methods": {"TEST": True}));
        mClient.connect();
    }

    tearDown() {
        mClient.disconnect();
        delete mClient;
    }

    issue4312() {
        # try to start listener on port 80 to ensure errors below
        Socket s();
        try {
            s.bind("127.0.0.1:0");
            s.listen();
        } catch (hash<ExceptionInfo> ex) {
            # ignore exceptions; assume we have a listener on port 80 from here on out
        }

        int port = s.getSocketInfo().port;

        hash<string, string> errmap;
        list<hash<auto>> l = mServer.addListeners("127.0.0.1:" + port, <HttpListenerOptionInfo>{}, \errmap);
        assertEq((), l);
        assertEq(1, errmap.size());
        assertEq("127.0.0.1:" + port, errmap.firstKey());
        assertRegex("SOCKET-BIND-ERROR", errmap.firstValue());

        l = mServer.addListeners(<HttpListenerOptionInfo>{"node": "127.0.0.1", "service": port.toString()}, \errmap);
        assertEq((), l);
        assertEq(1, errmap.size());
        assertEq("127.0.0.1:" + port, errmap.firstKey());
        assertRegex("SOCKET-BIND-ERROR", errmap.firstValue());
    }


    issue3397() {
        # create zero-length file
        string empty_fn = tmp_location() + DirSep + get_random_string() + ".pem";
        File f();
        f.open2(empty_fn, O_CREAT | O_TRUNC | O_WRONLY);
        on_exit unlink(empty_fn);
        string invalid_fn = tmp_location() + DirSep + get_random_string() + ".pem";
        f.open2(invalid_fn, O_CREAT | O_TRUNC | O_WRONLY);
        f.write(get_random_string(256));
        on_exit unlink(invalid_fn);
        string real_fn = tmp_location() + DirSep + get_random_string() + ".pem";
        f.open2(real_fn, O_CREAT | O_TRUNC | O_WRONLY);
        f.write(CertPem);
        on_exit unlink(real_fn);
        string key_fn = tmp_location() + DirSep + get_random_string() + ".pem";
        f.open2(key_fn, O_CREAT | O_TRUNC | O_WRONLY);
        f.write(KeyPem);
        f.close();
        on_exit unlink(key_fn);
        assertThrows("HTTPSERVER-SSL-SETUP-ERROR", "empty", \http_get_ssl_objects(), {
            "cert_path": empty_fn,
            "key_path": empty_fn,
        });
        assertThrows("HTTPSERVER-SSL-SETUP-ERROR", \http_get_ssl_objects(), {
            "cert_path": invalid_fn,
            "key_path": empty_fn,
        });
        assertThrows("HTTPSERVER-SSL-SETUP-ERROR", "cannot.*location", \http_get_ssl_objects(), {
            "cert_path": get_random_string(),
            "key_path": empty_fn,
        });
        assertThrows("HTTPSERVER-SSL-SETUP-ERROR", "empty", \http_get_ssl_objects(), {
            "cert_path": real_fn,
            "key_path": empty_fn,
        });
        assertThrows("HTTPSERVER-SSL-SETUP-ERROR", \http_get_ssl_objects(), {
            "cert_path": real_fn,
            "key_path": invalid_fn,
        });
        assertThrows("HTTPSERVER-SSL-SETUP-ERROR", "cannot.*location", \http_get_ssl_objects(), {
            "cert_path": real_fn,
            "key_path": get_random_string(),
        });
%ifdef Unix
        assertThrows("HTTPSERVER-SSL-SETUP-ERROR", "empty", \http_get_ssl_objects(), {
            "cert_path": real_fn,
            "key_path": "/dev/null",
        });
        assertThrows("HTTPSERVER-SSL-SETUP-ERROR", "empty", \http_get_ssl_objects(), {
            "cert_path": "/dev/null",
            "key_path": "/dev/null",
        });
%endif

        hash<HttpListenerOptionInfo> opts({
            "service": 0,
            "cert_loc": real_fn,
            "key_loc": key_fn,
        });
        list<hash<auto>> l = mServer.addListeners(opts);
        on_error printf("l: %y\n", l);
        assertEq(1, l.size());
        hash<auto> info = l[0];
        assertTrue(info.ssl);
        assertEq("https", info.proto);
        mServer.reloadListenerCertificate(info.id);
        info = mServer.getListenerInfo(info.id);
        assertTrue(info.ssl);
        assertEq("https", info.proto);
        info = mServer.getListenerInfoName(info.name);
        assertTrue(info.ssl);
        assertEq("https", info.proto);
        mServer.reloadListenerCertificateName(info.name);
        info = mServer.getListenerInfo(info.id);
        assertTrue(info.ssl);
        assertEq("https", info.proto);
        info = mServer.getListenerInfoName(info.name);
        assertTrue(info.ssl);
        assertEq("https", info.proto);
    }

    issue3116() {
        assertThrows("HTTP-CLIENT-RECEIVE-ERROR", "501", \mClient.head(), "/test501/xyz");
        assertNothing(mClient.get("/test102/xyz"));
        assertNothing(mClient.get("/test204/xyz"));
        assertThrows("HTTP-CLIENT-RECEIVE-ERROR", "304", \mClient.head(), "/test304/xyz");
        assertEq("GET, abc, /abc", mClient.get("/abc"));

        mClient.setErrorPassthru(True);
        on_exit mClient.setErrorPassthru(False);
        hash<auto> info;
        hash<auto> resp = mClient.send(NOTHING, "PUT", "/test400close/abc", NOTHING, NOTHING, \info);
        assertEq("test", resp.body);

        list<auto> http_msgs;
        code rc = sub (auto http_data) {
            http_msgs += http_data;
        };
        mClient.sendWithRecvCallback(rc, NOTHING, "PUT", "/test400close/abc", NOTHING, NOTHING, NOTHING, \info);
        assertEq("test", http_msgs[1].data);
    }

    issue2805() {
        assertThrows("HTTP-CLIENT-RECEIVE-ERROR", \mClient.send(), ("", "TEST"));
        mServer.addHttpMethod("TEST");
        assertEq("TEST, , /", mClient.send("", "TEST").body);
    }

    secondWildcardListener() {
        hash<auto> h = mServer.addListener(<HttpListenerOptionInfo>{
            "node": "localhost",
            "service": 0,
        });
        assertEq(Type::String, h.bind.type());
        # issue #2155 this call will fail without the fix
        h = mServer.addListener(<HttpListenerOptionInfo>{
            "node": "localhost",
            "service": 0,
        });
        assertEq(Type::String, h.bind.type());
    }

    basicTest() {
        assertEq("GET, abc, /abc", mClient.get("/abc"));
        assertEq("GET, abc, abc", mClient.get("abc"));
        assertEq("GET, abc/test/1/5/t, /abc/test/1/5/t", mClient.get("/abc/test/1/5/t"));
        hash resp = mClient.send("dx123", "POST", "/abc");
        assertEq(200, resp.status_code);
        assertEq("POST, dx123, abc, /abc", resp.body);

        assertEq("a1, GET, alt1, /alt1", mClient.get("/alt1"));
        assertEq("a1, POST, alt1/test, alt1/test", mClient.post("alt1/test", ""));
    }

    routingTest() {
        assertEq("/route", mClient.get("/route"));
        assertEq("/route", mClient.get("/route/something"));
        assertEq("/route/a", mClient.get("/route/a"));
        assertEq("/route/a", mClient.get("/route/a/something"));
        assertEq("/route/a/c", mClient.get("/route/a/c"));
        assertEq("/route/a/c", mClient.get("/route/a/c/something"));
        assertEq("/route/b", mClient.get("/route/b"));
        assertEq("/route/b", mClient.get("/route/b/something"));
    }

    testStatusCodes() {
        code c = sub() { mClient.get("/test400/"); };
        testAssertion("400", c, NOTHING, new QUnit::TestResultExceptionRegexp("HTTP-CLIENT-RECEIVE-ERROR", "status code 400 received"));
        c = sub() { mClient.get("/test500/abc"); };
        testAssertion("500", c, NOTHING, new QUnit::TestResultExceptionRegexp("HTTP-CLIENT-RECEIVE-ERROR", "status code 500 received"));
        c = sub() { mClient.get("/test501/xyz"); };
        testAssertion("501", c, NOTHING, new QUnit::TestResultExceptionRegexp("HTTP-CLIENT-RECEIVE-ERROR", "status code 501 received"));
        c = sub() { mClient.get("/nonexistingcode/abc"); };
        testAssertion("non-existing status code", c, NOTHING, new QUnit::TestResultExceptionRegexp("HTTP-CLIENT-RECEIVE-ERROR", "status code 500 received"));
    }

    misc() {
        assertEq(("method": "path", "params": ("a": "1", "b": "2")), parse_uri_query("path?a=1;b=2"));
        assertEq(("method": "path", "params": ("a": "1", "b": True)), parse_uri_query("path?a=1;b"));
    }

    private multipartFormDataBinaryFileTest() {
        MultiPartFormDataMessage msg();
        msg.addPart(BinImage, "image0.png", {"Content-Type": MimeTypePng});
        msg.addPart(BinImage, "image1.png", {"Content-Type": MimeTypePng});
        hash<MessageInfo> h = msg.getMsgAndHeaders();
        hash<auto> info;
        binary img = binary(mClient.post("/fileupload", h.body, h.hdr, \info));
        assertEq(200, info."response-headers".status_code);
        assertEq(MimeTypePng, info."response-headers"."content-type");
        assertEq(BinImage, img);
        assertEq("chunked", info."response-headers"."transfer-encoding");
    }
}

