Implement test for HttpGet class.

Change-Id: I1df793fd32dafdea999b875452ee832b773d8156
diff --git a/rbutil/rbutilqt/test/test-httpget.cpp b/rbutil/rbutilqt/test/test-httpget.cpp
new file mode 100644
index 0000000..c9ae86b
--- /dev/null
+++ b/rbutil/rbutilqt/test/test-httpget.cpp
@@ -0,0 +1,512 @@
+/***************************************************************************
+ *             __________               __   ___.
+ *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
+ *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
+ *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
+ *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
+ *                     \/            \/     \/    \/            \/
+ *
+ * Copyright (C) 2013 Dominik Riebeling
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ ****************************************************************************/
+
+#include <QtTest/QtTest>
+#include <QtCore/QObject>
+#include "httpget.h"
+
+#define TEST_USER_AGENT "TestAgent/2.3"
+#define TEST_HTTP_TIMEOUT 1000
+#define TEST_BINARY_BLOB "\x01\x10\x20\x30\x40\x50\x60\x70" \
+                         "\x80\x90\xff\xee\xdd\xcc\xbb\xaa"
+
+ // HttpDaemon is the the class that implements the simple HTTP server.
+ class HttpDaemon : public QTcpServer
+{
+    Q_OBJECT
+    public:
+        HttpDaemon(quint16 port, QObject* parent = 0) : QTcpServer(parent)
+        {
+            listen(QHostAddress::Any, port);
+        }
+
+#if QT_VERSION < 0x050000
+        void incomingConnection(int socket)
+#else
+        // Qt 5 uses a different prototype for this function!
+        void incomingConnection(qintptr socket)
+#endif
+        {
+            // When a new client connects, the server constructs a QTcpSocket and all
+            // communication with the client is done over this QTcpSocket. QTcpSocket
+            // works asynchronously, this means that all the communication is done
+            // in the two slots readClient() and discardClient().
+            QTcpSocket* s = new QTcpSocket(this);
+            connect(s, SIGNAL(readyRead()), this, SLOT(readClient()));
+            connect(s, SIGNAL(disconnected()), this, SLOT(discardClient()));
+            s->setSocketDescriptor(socket);
+        }
+        QList<QString> lastRequestData(void)
+        {
+            return m_lastRequestData;
+        }
+        void setResponsesToSend(QList<QByteArray> response)
+        {
+            m_requestNumber = 0;
+            m_responsesToSend = response;
+        }
+        void reset(void)
+        {
+            m_requestNumber = 0;
+            m_lastRequestData.clear();
+            QString now =
+                QDateTime::currentDateTime().toString("ddd, d MMM yyyy hh:mm:ss");
+            m_defaultResponse = QByteArray(
+                "HTTP/1.1 404 Not Found\r\n"
+                "Date: " + now.toLatin1() + "\r\n"
+                "Last-Modified: " + now.toLatin1() + "\r\n"
+                "Connection: close\r\n"
+                "\r\n");
+        }
+
+    private slots:
+        void readClient()
+        {
+            // This slot is called when the client sent data to the server.
+            QTcpSocket* socket = (QTcpSocket*)sender();
+            // read whole request
+            QString request;
+            while(socket->canReadLine()) {
+                QString line = socket->readLine();
+                request.append(line);
+                if(request.endsWith("\r\n\r\n")) {
+                    m_lastRequestData.append(request);
+
+                        if(m_requestNumber < m_responsesToSend.size())
+                            socket->write(m_responsesToSend.at(m_requestNumber));
+                        else
+                            socket->write(m_defaultResponse);
+                    socket->close();
+                    m_requestNumber++;
+                }
+                if (socket->state() == QTcpSocket::UnconnectedState)
+                    delete socket;
+            }
+        }
+        void discardClient()
+        {
+            QTcpSocket* socket = (QTcpSocket*)sender();
+            socket->deleteLater();
+        }
+
+    private:
+        int m_requestNumber;
+        QList<QByteArray> m_responsesToSend;
+        QList<QString> m_lastRequestData;
+        QByteArray m_defaultResponse;
+};
+
+
+class TestHttpGet : public QObject
+{
+    Q_OBJECT
+    private slots:
+        void testCachedRequest(void);
+        void testUncachedRepeatedRequest(void);
+        void testUncachedMovedRequest(void);
+        void testUserAgent(void);
+        void testResponseCode(void);
+        void testContentToBuffer(void);
+        void testContentToFile(void);
+        void testNoServer(void);
+        void testServerTimestamp(void);
+        void testMovedQuery(void);
+        void init(void);
+        void cleanup(void);
+
+    public slots:
+        void waitTimeout(void) { m_waitTimeoutOccured = true; }
+        QDir temporaryFolder(void)
+        {
+            // Qt unfortunately doesn't support creating temporary folders so
+            // we need to do that ourselves.
+            QString tempdir;
+            for(int i = 0; i < 100000; i++) {
+                tempdir = QDir::tempPath() + QString("/qttest-temp-%1").arg(i);
+                if(!QFileInfo(tempdir).exists()) break;
+            }
+            QDir().mkpath(tempdir);
+            return QDir(tempdir);
+        }
+        void rmTree(QString folder)
+        {
+            // no function in Qt to recursively delete a folder :(
+            QDir dir(folder);
+            Q_FOREACH(QFileInfo info, dir.entryInfoList(QDir::NoDotAndDotDot
+                        | QDir::System | QDir::Hidden | QDir::AllDirs
+                        | QDir::Files, QDir::DirsFirst)) {
+                if(info.isDir()) rmTree(info.absoluteFilePath());
+                else QFile::remove(info.absoluteFilePath());
+            }
+            dir.rmdir(folder);
+        }
+    private:
+        HttpDaemon *m_daemon;
+        bool m_waitTimeoutOccured;
+        QString m_now;
+        QDir m_cachedir;
+        HttpGet *m_getter;
+        QSignalSpy *m_doneSpy;
+};
+
+
+void TestHttpGet::init(void)
+{
+    m_now = QDateTime::currentDateTime().toString("ddd, d MMM yyyy hh:mm:ss");
+    m_daemon = new HttpDaemon(8080, this);
+    m_daemon->reset();
+    m_cachedir = temporaryFolder();
+    m_getter = new HttpGet(this);
+    m_doneSpy = new QSignalSpy(m_getter, SIGNAL(done(bool)));
+    m_waitTimeoutOccured = false;
+}
+
+void TestHttpGet::cleanup(void)
+{
+    rmTree(m_cachedir.absolutePath());
+    if(m_getter) {
+        m_getter->abort(); delete m_getter;
+    }
+    if(m_daemon) delete m_daemon;
+    if(m_doneSpy) delete m_doneSpy;
+}
+
+/* On uncached requests, HttpGet is supposed to sent a GET request only.
+ */
+void TestHttpGet::testUncachedRepeatedRequest(void)
+{
+    QList<QByteArray> responses;
+    responses << QByteArray(
+        "HTTP/1.1 200 OK\r\n"
+        "Date: " + m_now.toLatin1() + "\r\n"
+        "Last-Modified: " + m_now.toLatin1() + "\r\n"
+        "\r\n\r\n");
+    responses << QByteArray(
+        "HTTP/1.1 200 OK\r\n"
+        "Last-Modified: " + m_now.toLatin1() + "\r\n"
+        "Date: " + m_now.toLatin1() + "\r\n"
+        "\r\n"
+        "<html></html>\r\n\r\n");
+    m_daemon->setResponsesToSend(responses);
+
+    QTimer::singleShot(TEST_HTTP_TIMEOUT, this, SLOT(waitTimeout(void)));
+
+    m_getter->getFile(QUrl("http://localhost:8080/test1.txt"));
+    while(m_doneSpy->count() == 0 && m_waitTimeoutOccured == false)
+        QCoreApplication::processEvents();
+
+    QCOMPARE(m_doneSpy->count(), 1);
+    QCOMPARE(m_waitTimeoutOccured, false);
+    QCOMPARE(m_daemon->lastRequestData().size(), 1);
+    QCOMPARE(m_daemon->lastRequestData().at(0).startsWith("GET"), true);
+
+    // request second time
+    m_getter->getFile(QUrl("http://localhost:8080/test1.txt"));
+    while(m_doneSpy->count() < 2 && m_waitTimeoutOccured == false)
+        QCoreApplication::processEvents();
+    QCOMPARE(m_doneSpy->count(), 2);
+    QCOMPARE(m_waitTimeoutOccured, false);
+    QCOMPARE(m_daemon->lastRequestData().size(), 2);
+    QCOMPARE(m_daemon->lastRequestData().at(1).startsWith("GET"), true);
+    QCOMPARE(m_getter->httpResponse(), 200);
+}
+
+/* With enabled cache HttpGet is supposed to check the server file using a HEAD
+ * request first, then request the file using GET if the server file is newer
+ * than the cached one (or the file does not exist in the cache)
+ */
+void TestHttpGet::testCachedRequest(void)
+{
+    QList<QByteArray> responses;
+    responses << QByteArray(
+        "HTTP/1.1 302 Found\r\n"
+        "Location: http://localhost:8080/test2.txt\r\n"
+        "Date: " + m_now.toLatin1() + "\r\n"
+        "Last-Modified: " + m_now.toLatin1() + "\r\n"
+        "\r\n");
+    responses << QByteArray(
+        "HTTP/1.1 200 OK\r\n"
+        "Last-Modified: " + m_now.toLatin1() + "\r\n"
+        "Date: " + m_now.toLatin1() + "\r\n"
+        "\r\n"
+        "<html></html>\r\n\r\n");
+    responses << QByteArray(
+        "HTTP/1.1 200 OK\r\n"
+        "Last-Modified: 1 Jan 2000 00:00:00\r\n"
+        "Date: " + m_now.toLatin1() + "\r\n"
+        "\r\n");
+    m_daemon->setResponsesToSend(responses);
+
+    QTimer::singleShot(TEST_HTTP_TIMEOUT, this, SLOT(waitTimeout(void)));
+
+    m_getter->setCache(m_cachedir);
+    m_getter->getFile(QUrl("http://localhost:8080/test1.txt"));
+    while(m_doneSpy->count() == 0 && m_waitTimeoutOccured == false)
+        QCoreApplication::processEvents();
+
+    QList<QString> requests = m_daemon->lastRequestData();
+    QCOMPARE(m_doneSpy->count(), 1);
+    QCOMPARE(m_doneSpy->at(0).at(0).toBool(), false);
+    QCOMPARE(m_waitTimeoutOccured, false);
+    QCOMPARE(requests.size(), 2);
+    QCOMPARE(requests.at(0).startsWith("GET"), true);
+    QCOMPARE(requests.at(1).startsWith("GET"), true);
+    QCOMPARE(m_getter->httpResponse(), 200);
+
+    // request real file, this time the response should come from cache.
+    m_getter->getFile(QUrl("http://localhost:8080/test2.txt"));
+    while(m_doneSpy->count() < 2 && m_waitTimeoutOccured == false)
+        QCoreApplication::processEvents();
+    QCOMPARE(m_doneSpy->count(), 2);  // 2 requests, 2 times done()
+    QCOMPARE(m_doneSpy->at(1).at(0).toBool(), false);
+    QCOMPARE(m_waitTimeoutOccured, false);
+    QCOMPARE(m_daemon->lastRequestData().size(), 3);
+    // redirect will not cache as the redirection target file.
+    QCOMPARE(m_daemon->lastRequestData().at(2).startsWith("GET"), true);
+    QCOMPARE(m_getter->httpResponse(), 200);
+}
+
+/* When a custom user agent is set all requests are supposed to contain it.
+ * Enable cache to make HttpGet performs a HEAD request. Answer with 302, so
+ * HttpGet follows and sends another HEAD request before finally doing a GET.
+ */
+void TestHttpGet::testUserAgent(void)
+{
+    QList<QByteArray> responses;
+    responses << QByteArray(
+        "HTTP/1.1 200 OK\r\n"
+        "Date: " + m_now.toLatin1() + "\r\n"
+        "Last-Modified: " + m_now.toLatin1() + "\r\n"
+        "\r\n\r\n");
+    responses << QByteArray(
+        "HTTP/1.1 200 OK\r\n"
+        "Last-Modified: " + m_now.toLatin1() + "\r\n"
+        "Date: " + m_now.toLatin1() + "\r\n"
+        "\r\n"
+        "<html></html>\r\n\r\n");
+    m_daemon->setResponsesToSend(responses);
+
+    QTimer::singleShot(TEST_HTTP_TIMEOUT, this, SLOT(waitTimeout(void)));
+
+    m_getter->setGlobalUserAgent(TEST_USER_AGENT);
+    m_getter->setCache(m_cachedir);
+    m_getter->getFile(QUrl("http://localhost:8080/test1.txt"));
+    while(m_doneSpy->count() == 0 && m_waitTimeoutOccured == false)
+        QCoreApplication::processEvents();
+
+    QList<QString> requests = m_daemon->lastRequestData();
+    QCOMPARE(m_doneSpy->count(), 1);
+    QCOMPARE(m_waitTimeoutOccured, false);
+    QCOMPARE(requests.size(), 1);
+    QCOMPARE(requests.at(0).startsWith("GET"), true);
+
+    for(int i = 0; i < requests.size(); ++i) {
+        QRegExp rx("User-Agent:[\t ]+([a-zA-Z0-9\\./]+)");
+        bool userAgentFound = rx.indexIn(requests.at(i)) > 0 ? true : false;
+        QCOMPARE(userAgentFound, true);
+        QString userAgentString = rx.cap(1);
+        QCOMPARE(userAgentString, QString(TEST_USER_AGENT));
+    }
+}
+
+void TestHttpGet::testUncachedMovedRequest(void)
+{
+    QList<QByteArray> responses;
+    responses << QByteArray(
+        "HTTP/1.1 302 Found\r\n"
+        "Location: http://localhost:8080/test2.txt\r\n"
+        "Date: " + m_now.toLatin1() + "\r\n"
+        "Last-Modified: " + m_now.toLatin1() + "\r\n"
+        "\r\n");
+    responses << QByteArray(
+        "HTTP/1.1 200 OK\r\n"
+        "Last-Modified: " + m_now.toLatin1() + "\r\n"
+        "Date: " + m_now.toLatin1() + "\r\n"
+        "\r\n"
+        "<html></html>\r\n\r\n");
+    m_daemon->setResponsesToSend(responses);
+
+    QTimer::singleShot(TEST_HTTP_TIMEOUT, this, SLOT(waitTimeout(void)));
+
+    m_getter->getFile(QUrl("http://localhost:8080/test1.php?var=1&b=foo"));
+    while(m_doneSpy->count() == 0 && m_waitTimeoutOccured == false)
+        QCoreApplication::processEvents();
+
+    QCOMPARE(m_doneSpy->count(), 1);
+    QCOMPARE(m_waitTimeoutOccured, false);
+    QCOMPARE(m_daemon->lastRequestData().size(), 2);
+    QCOMPARE(m_daemon->lastRequestData().at(0).startsWith("GET"), true);
+    QCOMPARE(m_daemon->lastRequestData().at(1).startsWith("GET"), true);
+}
+
+void TestHttpGet::testResponseCode(void)
+{
+    QTimer::singleShot(TEST_HTTP_TIMEOUT, this, SLOT(waitTimeout(void)));
+
+    m_getter->getFile(QUrl("http://localhost:8080/test1.txt"));
+    while(m_doneSpy->count() == 0 && m_waitTimeoutOccured == false)
+        QCoreApplication::processEvents();
+
+    QCOMPARE(m_doneSpy->count(), 1);
+    QCOMPARE(m_doneSpy->at(0).at(0).toBool(), true);
+    QCOMPARE(m_waitTimeoutOccured, false);
+    QCOMPARE(m_daemon->lastRequestData().size(), 1);
+    QCOMPARE(m_daemon->lastRequestData().at(0).startsWith("GET"), true);
+    QCOMPARE(m_getter->httpResponse(), 404);
+}
+
+void TestHttpGet::testContentToBuffer(void)
+{
+    QList<QByteArray> responses;
+    responses << QByteArray(
+        "HTTP/1.1 200 OK\r\n"
+        "Last-Modified: " + m_now.toLatin1() + "\r\n"
+        "Date: " + m_now.toLatin1() + "\r\n"
+        "\r\n"
+        TEST_BINARY_BLOB);
+    m_daemon->setResponsesToSend(responses);
+
+    QTimer::singleShot(TEST_HTTP_TIMEOUT, this, SLOT(waitTimeout(void)));
+
+    m_getter->getFile(QUrl("http://localhost:8080/test1.txt"));
+    while(m_doneSpy->count() == 0 && m_waitTimeoutOccured == false)
+        QCoreApplication::processEvents();
+
+    QCOMPARE(m_doneSpy->count(), 1);
+    QCOMPARE(m_waitTimeoutOccured, false);
+    QCOMPARE(m_getter->readAll(), QByteArray(TEST_BINARY_BLOB));
+    // sizeof(TEST_BINARY_BLOB) will include an additional terminating NULL.
+    QCOMPARE((unsigned long)m_getter->readAll().size(), sizeof(TEST_BINARY_BLOB) - 1);
+}
+
+void TestHttpGet::testContentToFile(void)
+{
+    QTemporaryFile tf(this);
+    QList<QByteArray> responses;
+    responses << QByteArray(
+        "HTTP/1.1 200 OK\r\n"
+        "Last-Modified: " + m_now.toLatin1() + "\r\n"
+        "Date: " + m_now.toLatin1() + "\r\n"
+        "\r\n"
+        TEST_BINARY_BLOB);
+    m_daemon->setResponsesToSend(responses);
+
+    QTimer::singleShot(TEST_HTTP_TIMEOUT, this, SLOT(waitTimeout(void)));
+
+    m_getter->setFile(&tf);
+    m_getter->getFile(QUrl("http://localhost:8080/test1.txt"));
+    while(m_doneSpy->count() == 0 && m_waitTimeoutOccured == false)
+        QCoreApplication::processEvents();
+
+    QCOMPARE(m_doneSpy->count(), 1);
+    QCOMPARE(m_waitTimeoutOccured, false);
+
+    tf.open();
+    QByteArray data = tf.readAll();
+    QCOMPARE(data, QByteArray(TEST_BINARY_BLOB));
+    QCOMPARE((unsigned long)data.size(), sizeof(TEST_BINARY_BLOB) - 1);
+    tf.close();
+}
+
+void TestHttpGet::testNoServer(void)
+{
+    QTimer::singleShot(TEST_HTTP_TIMEOUT, this, SLOT(waitTimeout(void)));
+    m_getter->getFile(QUrl("http://localhost:8081/test1.txt"));
+    while(m_doneSpy->count() == 0 && m_waitTimeoutOccured == false)
+        QCoreApplication::processEvents();
+
+    QCOMPARE(m_doneSpy->count(), 1);
+    QCOMPARE(m_doneSpy->at(0).at(0).toBool(), true);
+    QCOMPARE(m_waitTimeoutOccured, false);
+}
+
+void TestHttpGet::testServerTimestamp(void)
+{
+    QList<QByteArray> responses;
+    responses << QByteArray(
+        "HTTP/1.1 200 OK\r\n"
+        "Last-Modified: Wed, 20 Jan 2010 10:20:30\r\n" // RFC 822
+        "Date: Wed, 20 Jan 2010 10:20:30\r\n"
+        "\r\n"
+        "\r\n");
+    responses << QByteArray(
+        "HTTP/1.1 200 OK\r\n"
+        "Last-Modified: Sat Feb 19 09:08:07 2011\r\n" // asctime
+        "Date: Sat Feb 19 09:08:07 2011\r\n"
+        "\r\n"
+        "\r\n");
+
+    QList<QDateTime> times;
+    times << QDateTime::fromString("2010-01-20T11:20:30", Qt::ISODate);
+    times << QDateTime::fromString("2011-02-19T10:08:07", Qt::ISODate);
+
+    m_daemon->setResponsesToSend(responses);
+
+    QTimer::singleShot(TEST_HTTP_TIMEOUT, this, SLOT(waitTimeout(void)));
+
+    int count = m_doneSpy->count();
+    for(int i = 0; i < responses.size(); ++i) {
+        m_getter->getFile(QUrl("http://localhost:8080/test1.txt"));
+        while(m_doneSpy->count() == count && m_waitTimeoutOccured == false)
+            QCoreApplication::processEvents();
+        count = m_doneSpy->count();
+        QCOMPARE(m_getter->timestamp(), times.at(i));
+    }
+}
+
+void TestHttpGet::testMovedQuery(void)
+{
+    QList<QByteArray> responses;
+    responses << QByteArray(
+        "HTTP/1.1 302 Found\r\n"
+        "Location: http://localhost:8080/test2.php\r\n"
+        "Date: " + m_now.toLatin1() + "\r\n"
+        "Last-Modified: " + m_now.toLatin1() + "\r\n"
+        "\r\n");
+    responses << QByteArray(
+        "HTTP/1.1 200 OK\r\n"
+        "Last-Modified: " + m_now.toLatin1() + "\r\n"
+        "Date: " + m_now.toLatin1() + "\r\n"
+        "\r\n"
+        "<html></html>\r\n\r\n");
+    m_daemon->setResponsesToSend(responses);
+
+    QTimer::singleShot(TEST_HTTP_TIMEOUT, this, SLOT(waitTimeout(void)));
+
+    m_getter->getFile(QUrl("http://localhost:8080/test1.php?var=1&b=foo"));
+    while(m_doneSpy->count() == 0 && m_waitTimeoutOccured == false)
+        QCoreApplication::processEvents();
+
+    QCOMPARE(m_doneSpy->count(), 1);
+    QCOMPARE(m_waitTimeoutOccured, false);
+    QCOMPARE(m_getter->httpResponse(), 200);
+    QCOMPARE(m_daemon->lastRequestData().size(), 2);
+    QCOMPARE(m_daemon->lastRequestData().at(0).startsWith("GET"), true);
+    QCOMPARE(m_daemon->lastRequestData().at(1).startsWith("GET"), true);
+    // current implementation keeps order of query items.
+    QCOMPARE((bool)m_daemon->lastRequestData().at(1).contains("/test2.php?var=1&b=foo"), true);
+}
+
+QTEST_MAIN(TestHttpGet)
+
+// this include is needed because we don't use a separate header file for the
+// test class. It also needs to be at the end.
+#include "test-httpget.moc"
+
diff --git a/rbutil/rbutilqt/test/test-httpget.pro b/rbutil/rbutilqt/test/test-httpget.pro
new file mode 100644
index 0000000..60b6993
--- /dev/null
+++ b/rbutil/rbutilqt/test/test-httpget.pro
@@ -0,0 +1,31 @@
+#
+#             __________               __   ___.
+#   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
+#   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
+#   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
+#   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
+#                     \/            \/     \/    \/            \/
+#
+# All files in this archive are subject to the GNU General Public License.
+# See the file COPYING in the source tree root for full license agreement.
+#
+# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+# KIND, either express or implied.
+#
+
+#
+include(tests.pri)
+
+TEMPLATE = app
+TARGET = test-httpget
+INCLUDEPATH += . ../base
+QT += network
+
+# Input
+SOURCES += \
+    test-httpget.cpp
+
+SOURCES += ../base/httpget.cpp
+
+HEADERS += ../base/httpget.h
+