From 70dabc07dc49c543baac528643c953b2f51a969b Mon Sep 17 00:00:00 2001 From: Alexey Sokolov Date: Tue, 13 Feb 2018 20:53:05 +0000 Subject: [PATCH] Split integration test file to several files. --- Makefile.in | 2 +- test/integration/CMakeLists.txt | 9 +- test/integration/autoconf-all.cpp | 24 + test/integration/framework/base.cpp | 61 ++ test/integration/framework/base.h | 192 ++++++ test/integration/framework/main.cpp | 32 + test/integration/framework/znctest.cpp | 177 ++++++ test/integration/framework/znctest.h | 58 ++ test/integration/main.cpp | 825 ------------------------- test/integration/tests/core.cpp | 236 +++++++ test/integration/tests/modules.cpp | 203 ++++++ test/integration/tests/scripting.cpp | 61 ++ 12 files changed, 1053 insertions(+), 827 deletions(-) create mode 100644 test/integration/autoconf-all.cpp create mode 100644 test/integration/framework/base.cpp create mode 100644 test/integration/framework/base.h create mode 100644 test/integration/framework/main.cpp create mode 100644 test/integration/framework/znctest.cpp create mode 100644 test/integration/framework/znctest.h delete mode 100644 test/integration/main.cpp create mode 100644 test/integration/tests/core.cpp create mode 100644 test/integration/tests/modules.cpp create mode 100644 test/integration/tests/scripting.cpp diff --git a/Makefile.in b/Makefile.in index 47384b00..be3e7dab 100644 --- a/Makefile.in +++ b/Makefile.in @@ -154,7 +154,7 @@ test/gmock-main.o: $(GMOCK_DIR)/src/gmock_main.cc Makefile $(Q)$(CXX) $(CXXFLAGS) $(GTEST_FLAGS) -c -o $@ $< -MD -MF .depend/gmock-main.dep -MT $@ # Qt fails under TSAN, so CXXFLAGS/LDFLAGS can't be used. -test/Integration.o: test/integration/main.cpp Makefile +test/Integration.o: test/integration/autoconf-all.cpp Makefile @mkdir -p .depend test $(E) Building test object Integration... $(Q)g++ $(qt_CFLAGS) $(GTEST_FLAGS) -c -o $@ $< -MD -MF .depend/Integration.test.dep -MT $@ '-DZNC_BIN_DIR="$(bindir)"' '-DZNC_SRC_DIR="$(realpath $(srcdir))"' diff --git a/test/integration/CMakeLists.txt b/test/integration/CMakeLists.txt index c927b0fe..033eb5b2 100644 --- a/test/integration/CMakeLists.txt +++ b/test/integration/CMakeLists.txt @@ -32,12 +32,19 @@ find_package(Qt5Network 5.4 REQUIRED HINTS ${Qt5_HINTS}) set_property(DIRECTORY APPEND PROPERTY COMPILE_DEFINITIONS "GTEST_HAS_POSIX_RE=0") -add_executable(inttest "main.cpp" +add_executable(inttest + "framework/main.cpp" + "framework/base.cpp" + "framework/znctest.cpp" + "tests/core.cpp" + "tests/modules.cpp" + "tests/scripting.cpp" "${GTEST_ROOT}/src/gtest-all.cc" "${GMOCK_ROOT}/src/gmock-all.cc") target_link_libraries(inttest Qt5::Network Threads::Threads) target_include_directories(inttest PUBLIC + "${PROJECT_SOURCE_DIR}/framework" "${GTEST_ROOT}" "${GTEST_ROOT}/include" "${GMOCK_ROOT}" "${GMOCK_ROOT}/include") target_compile_definitions(inttest PRIVATE diff --git a/test/integration/autoconf-all.cpp b/test/integration/autoconf-all.cpp new file mode 100644 index 00000000..ce89941e --- /dev/null +++ b/test/integration/autoconf-all.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2004-2016 ZNC, see the NOTICE file for details. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Make autoconf-based build not enumerate all .cpp files, because I'm lazy + +#include "framework/base.cpp" +#include "framework/main.cpp" +#include "framework/znctest.cpp" +#include "tests/core.cpp" +#include "tests/modules.cpp" +#include "tests/scripting.cpp" diff --git a/test/integration/framework/base.cpp b/test/integration/framework/base.cpp new file mode 100644 index 00000000..ffa16fd8 --- /dev/null +++ b/test/integration/framework/base.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2004-2016 ZNC, see the NOTICE file for details. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "base.h" + +using testing::AnyOf; +using testing::Eq; + +namespace znc_inttest { + +Process::Process(QString cmd, QStringList args, + std::function setup) + : IO(&m_proc, true) { + auto env = QProcessEnvironment::systemEnvironment(); + env.insert("ZNC_DEBUG_TIMER", "1"); + // Default exit codes of sanitizers upon error: + // ASAN - 1 + // LSAN - 23 (part of ASAN, but uses a different value) + // TSAN - 66 + // + // ZNC uses 1 too to report startup failure. + // But we don't want to confuse expected startup failure with ASAN + // error. + env.insert("ASAN_OPTIONS", "exitcode=57"); + m_proc.setProcessEnvironment(env); + setup(&m_proc); + m_proc.start(cmd, args); + EXPECT_TRUE(m_proc.waitForStarted()) + << "Failed to start ZNC, did you install it?"; +} + +Process::~Process() { + if (m_kill) m_proc.terminate(); + [this]() { + ASSERT_TRUE(m_proc.waitForFinished()); + if (!m_allowDie) { + ASSERT_EQ(QProcess::NormalExit, m_proc.exitStatus()); + if (m_allowLeak) { + ASSERT_THAT(m_proc.exitStatus(), AnyOf(Eq(23), Eq(m_exit))); + } else { + ASSERT_EQ(m_exit, m_proc.exitCode()); + } + } + }(); +} + +} // namespace znc_inttest diff --git a/test/integration/framework/base.h b/test/integration/framework/base.h new file mode 100644 index 00000000..1a0a5fd2 --- /dev/null +++ b/test/integration/framework/base.h @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2004-2016 ZNC, see the NOTICE file for details. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include +#include + +#include + +namespace znc_inttest { + +template +class IO { + public: + IO(Device* device, bool verbose = false) + : m_device(device), m_verbose(verbose) {} + virtual ~IO() {} + void ReadUntil(QByteArray pattern); + /* + * Reads from Device until pattern is matched and returns this pattern + * up to and excluding the first newline. Pattern itself can contain a newline. + * Have to use second param as the ASSERT_*'s return a non-QByteArray. + */ + void ReadUntilAndGet(QByteArray pattern, QByteArray& match); + void Write(QByteArray s = "", bool new_line = true); + void Close(); + + private: + // Need to flush QTcpSocket, and QIODevice doesn't have flush at all... + static void FlushIfCan(QIODevice*) {} + static void FlushIfCan(QTcpSocket* sock) { sock->flush(); } + + Device* m_device; + bool m_verbose; + QByteArray m_readed; +}; + +template +IO WrapIO(Device* d) { + return IO(d); +} + +using Socket = IO; + +class Process : public IO { + public: + Process(QString cmd, QStringList args, + std::function setup = [](QProcess*) {}); + ~Process() override; + void ShouldFinishItself(int code = 0) { + m_kill = false; + m_exit = code; + } + void CanDie() { m_allowDie = true; } + + // I can't do much about SWIG... + void CanLeak() { m_allowLeak = true; } + + private: + bool m_kill = true; + int m_exit = 0; + bool m_allowDie = false; + bool m_allowLeak = false; + QProcess m_proc; +}; + +// Can't use QEventLoop without existing QCoreApplication +class App { + public: + App() : m_argv(new char{}), m_app(m_argc, &m_argv) {} + ~App() { delete m_argv; } + + private: + int m_argc = 1; + char* m_argv; + QCoreApplication m_app; +}; + +// Implementation + +template +void IO::ReadUntil(QByteArray pattern) { + auto deadline = QDateTime::currentDateTime().addSecs(60); + while (true) { + int search = m_readed.indexOf(pattern); + if (search != -1) { + m_readed.remove(0, search + pattern.length()); + return; + } + if (m_readed.length() > pattern.length()) { + m_readed = m_readed.right(pattern.length()); + } + const int timeout_ms = + QDateTime::currentDateTime().msecsTo(deadline); + ASSERT_GT(timeout_ms, 0) << "Wanted:" << pattern.toStdString(); + ASSERT_TRUE(m_device->waitForReadyRead(timeout_ms)) + << "Wanted: " << pattern.toStdString(); + QByteArray chunk = m_device->readAll(); + if (m_verbose) { + std::cout << chunk.toStdString() << std::flush; + } + m_readed += chunk; + } +} + +template +void IO::ReadUntilAndGet(QByteArray pattern, QByteArray& match) { + auto deadline = QDateTime::currentDateTime().addSecs(60); + while (true) { + int search = m_readed.indexOf(pattern); + if (search != -1) { + int start = 0; + /* Don't look for what we've already found */ + if (pattern != "\n") { + int patlen = pattern.length(); + start = search; + pattern = QByteArray("\n"); + search = m_readed.indexOf(pattern, start + patlen); + } + if (search != -1) { + match += m_readed.mid(start, search - start); + m_readed.remove(0, search + 1); + return; + } + /* No newline yet, add to retvalue and trunc output */ + match += m_readed.mid(start); + m_readed.resize(0); + } + if (m_readed.length() > pattern.length()) { + m_readed = m_readed.right(pattern.length()); + } + const int timeout_ms = + QDateTime::currentDateTime().msecsTo(deadline); + ASSERT_GT(timeout_ms, 0) << "Wanted:" << pattern.toStdString(); + ASSERT_TRUE(m_device->waitForReadyRead(timeout_ms)) + << "Wanted: " << pattern.toStdString(); + QByteArray chunk = m_device->readAll(); + if (m_verbose) { + std::cout << chunk.toStdString() << std::flush; + } + m_readed += chunk; + } +} + +template +void IO::Write(QByteArray s, bool new_line) { + if (!m_device) return; + if (m_verbose) { + std::cout << s.toStdString() << std::flush; + if (new_line) { + std::cout << std::endl; + } + } + s += "\n"; + while (!s.isEmpty()) { + auto res = m_device->write(s); + ASSERT_NE(res, -1); + s.remove(0, res); + } + FlushIfCan(m_device); +} + +template +void IO::Close() { +#ifdef __CYGWIN__ + // Qt on cygwin silently doesn't send the rest of buffer from socket + // without this line + sleep(1); +#endif + m_device->disconnectFromHost(); +} + +} // namespace znc_inttest + diff --git a/test/integration/framework/main.cpp b/test/integration/framework/main.cpp new file mode 100644 index 00000000..65364d6b --- /dev/null +++ b/test/integration/framework/main.cpp @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2004-2016 ZNC, see the NOTICE file for details. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +class ThrowListener : public testing::EmptyTestEventListener { + void OnTestPartResult(const testing::TestPartResult& result) override { + if (result.type() == testing::TestPartResult::kFatalFailure && + !std::uncaught_exception()) { + throw testing::AssertionException(result); + } + } +}; + +int main(int argc, char** argv) { + testing::InitGoogleTest(&argc, argv); + testing::UnitTest::GetInstance()->listeners().Append(new ThrowListener); + return RUN_ALL_TESTS(); +} diff --git a/test/integration/framework/znctest.cpp b/test/integration/framework/znctest.cpp new file mode 100644 index 00000000..39492eae --- /dev/null +++ b/test/integration/framework/znctest.cpp @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2004-2016 ZNC, see the NOTICE file for details. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "znctest.h" + +#ifndef ZNC_BIN_DIR +#define ZNC_BIN_DIR "" +#endif + +namespace znc_inttest { + +void WriteConfig(QString path) { + // clang-format off + Process p(ZNC_BIN_DIR "/znc", QStringList() << "--debug" + << "--datadir" << path + << "--makeconf"); + p.ReadUntil("Listen on port"); p.Write("12345"); + p.ReadUntil("Listen using SSL"); p.Write(); + p.ReadUntil("IPv6"); p.Write(); + p.ReadUntil("Username"); p.Write("user"); + p.ReadUntil("password"); p.Write("hunter2", false); + p.ReadUntil("Confirm"); p.Write("hunter2", false); + p.ReadUntil("Nick [user]"); p.Write(); + p.ReadUntil("Alternate nick [user_]"); p.Write(); + p.ReadUntil("Ident [user]"); p.Write(); + p.ReadUntil("Real name"); p.Write(); + p.ReadUntil("Bind host"); p.Write(); + p.ReadUntil("Set up a network?"); p.Write(); + p.ReadUntil("Name [freenode]"); p.Write("test"); + p.ReadUntil("Server host (host only)"); p.Write("127.0.0.1"); + p.ReadUntil("Server uses SSL?"); p.Write(); + p.ReadUntil("6667"); p.Write(); + p.ReadUntil("password"); p.Write(); + p.ReadUntil("channels"); p.Write(); + p.ReadUntil("Launch ZNC now?"); p.Write("no"); + p.ShouldFinishItself(); + // clang-format on +} + +void ZNCTest::SetUp() { + WriteConfig(m_dir.path()); + ASSERT_TRUE(m_server.listen(QHostAddress::LocalHost, 6667)) + << m_server.errorString().toStdString(); +} + +Socket ZNCTest::ConnectIRCd() { + [this] { + ASSERT_TRUE(m_server.waitForNewConnection(30000 /* msec */)); + }(); + return WrapIO(m_server.nextPendingConnection()); +} + +Socket ZNCTest::ConnectClient() { + m_clients.emplace_back(); + QTcpSocket& sock = m_clients.back(); + sock.connectToHost("127.0.0.1", 12345); + [&] { + ASSERT_TRUE(sock.waitForConnected()) + << sock.errorString().toStdString(); + }(); + return WrapIO(&sock); +} + +Socket ZNCTest::LoginClient() { + auto client = ConnectClient(); + client.Write("PASS :hunter2"); + client.Write("NICK nick"); + client.Write("USER user/test x x :x"); + return client; +} + +std::unique_ptr ZNCTest::Run() { + return std::unique_ptr(new Process( + ZNC_BIN_DIR "/znc", QStringList() << "--debug" + << "--datadir" << m_dir.path(), + [](QProcess* proc) { + proc->setProcessChannelMode(QProcess::ForwardedChannels); + })); +} + +std::unique_ptr ZNCTest::HttpGet(QNetworkRequest request) { + return HandleHttp(m_network.get(request)); +} +std::unique_ptr ZNCTest::HttpPost( + QNetworkRequest request, QList> data) { + request.setHeader(QNetworkRequest::ContentTypeHeader, + "application/x-www-form-urlencoded"); + QUrlQuery q; + q.setQueryItems(data); + return HandleHttp(m_network.post(request, q.toString().toUtf8())); +} +std::unique_ptr ZNCTest::HandleHttp(QNetworkReply* reply) { + QEventLoop loop; + QObject::connect(reply, &QNetworkReply::finished, [&]() { + std::cout << "Got HTTP reply" << std::endl; + loop.quit(); + }); + QObject::connect( + reply, + static_cast( + &QNetworkReply::error), + [&](QNetworkReply::NetworkError e) { + ADD_FAILURE() << reply->errorString().toStdString(); + }); + QTimer::singleShot(30000 /* msec */, &loop, [&]() { + ADD_FAILURE() << "connection timeout"; + loop.quit(); + }); + std::cout << "Start HTTP loop.exec()" << std::endl; + loop.exec(); + std::cout << "Finished HTTP loop.exec()" << std::endl; + return std::unique_ptr(reply); +} + +void ZNCTest::InstallModule(QString name, QString content) { + QDir dir(m_dir.path()); + ASSERT_TRUE(dir.mkpath("modules")); + ASSERT_TRUE(dir.cd("modules")); + if (name.endsWith(".cpp")) { + // Compile + QTemporaryDir srcdir; + QFile file(QDir(srcdir.path()).filePath(name)); + ASSERT_TRUE(file.open(QIODevice::WriteOnly | QIODevice::Text)); + QTextStream out(&file); + out << content; + file.close(); + Process p( + ZNC_BIN_DIR "/znc-buildmod", QStringList() << file.fileName(), + [&](QProcess* proc) { + proc->setWorkingDirectory(dir.absolutePath()); + proc->setProcessChannelMode(QProcess::ForwardedChannels); + }); + p.ShouldFinishItself(); + } else if (name.endsWith(".py")) { + // Dedent + QStringList lines = content.split("\n"); + int maxoffset = -1; + for (const QString& line : lines) { + int nonspace = line.indexOf(QRegExp("\\S")); + if (nonspace == -1) continue; + if (nonspace < maxoffset || maxoffset == -1) + maxoffset = nonspace; + } + if (maxoffset == -1) maxoffset = 0; + QFile file(dir.filePath(name)); + ASSERT_TRUE(file.open(QIODevice::WriteOnly | QIODevice::Text)); + QTextStream out(&file); + for (const QString& line : lines) { + // QTextStream::operator<<(const QStringRef &string) was + // introduced in Qt 5.6; let's keep minimum required version + // less than that for now. + out << line.mid(maxoffset) << "\n"; + } + } else { + // Write as is + QFile file(dir.filePath(name)); + ASSERT_TRUE(file.open(QIODevice::WriteOnly | QIODevice::Text)); + QTextStream out(&file); + out << content; + } +} + + +} // namespace znc_inttest diff --git a/test/integration/framework/znctest.h b/test/integration/framework/znctest.h new file mode 100644 index 00000000..69afb5e8 --- /dev/null +++ b/test/integration/framework/znctest.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2004-2016 ZNC, see the NOTICE file for details. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "base.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace znc_inttest { + +void WriteConfig(QString path); + +class ZNCTest : public testing::Test { + protected: + void SetUp() override; + + Socket ConnectIRCd(); + Socket ConnectClient(); + Socket LoginClient(); + + std::unique_ptr Run(); + + std::unique_ptr HttpGet(QNetworkRequest request); + std::unique_ptr HttpPost( + QNetworkRequest request, QList> data); + std::unique_ptr HandleHttp(QNetworkReply* reply); + + void InstallModule(QString name, QString content); + + App m_app; + QNetworkAccessManager m_network; + QTemporaryDir m_dir; + QTcpServer m_server; + std::list m_clients; +}; + +} // namespace znc_inttest diff --git a/test/integration/main.cpp b/test/integration/main.cpp deleted file mode 100644 index cdb28b6e..00000000 --- a/test/integration/main.cpp +++ /dev/null @@ -1,825 +0,0 @@ -/* - * Copyright (C) 2004-2016 ZNC, see the NOTICE file for details. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#ifndef ZNC_BIN_DIR -#define ZNC_BIN_DIR "" -#endif - -using testing::AnyOf; -using testing::Eq; -using testing::HasSubstr; - -namespace { - -template -class IO { - public: - IO(Device* device, bool verbose = false) - : m_device(device), m_verbose(verbose) {} - virtual ~IO() {} - void ReadUntil(QByteArray pattern) { - auto deadline = QDateTime::currentDateTime().addSecs(60); - while (true) { - int search = m_readed.indexOf(pattern); - if (search != -1) { - m_readed.remove(0, search + pattern.length()); - return; - } - if (m_readed.length() > pattern.length()) { - m_readed = m_readed.right(pattern.length()); - } - const int timeout_ms = - QDateTime::currentDateTime().msecsTo(deadline); - ASSERT_GT(timeout_ms, 0) << "Wanted:" << pattern.toStdString(); - ASSERT_TRUE(m_device->waitForReadyRead(timeout_ms)) - << "Wanted: " << pattern.toStdString(); - QByteArray chunk = m_device->readAll(); - if (m_verbose) { - std::cout << chunk.toStdString() << std::flush; - } - m_readed += chunk; - } - } - /* - * Reads from Device until pattern is matched and returns this pattern - * up to and excluding the first newline. Pattern itself can contain a newline. - * Have to use second param as the ASSERT_*'s return a non-QByteArray. - */ - void ReadUntilAndGet(QByteArray pattern, QByteArray& match) { - auto deadline = QDateTime::currentDateTime().addSecs(60); - while (true) { - int search = m_readed.indexOf(pattern); - if (search != -1) { - int start = 0; - /* Don't look for what we've already found */ - if (pattern != "\n") { - int patlen = pattern.length(); - start = search; - pattern = QByteArray("\n"); - search = m_readed.indexOf(pattern, start + patlen); - } - if (search != -1) { - match += m_readed.mid(start, search - start); - m_readed.remove(0, search + 1); - return; - } - /* No newline yet, add to retvalue and trunc output */ - match += m_readed.mid(start); - m_readed.resize(0); - } - if (m_readed.length() > pattern.length()) { - m_readed = m_readed.right(pattern.length()); - } - const int timeout_ms = - QDateTime::currentDateTime().msecsTo(deadline); - ASSERT_GT(timeout_ms, 0) << "Wanted:" << pattern.toStdString(); - ASSERT_TRUE(m_device->waitForReadyRead(timeout_ms)) - << "Wanted: " << pattern.toStdString(); - QByteArray chunk = m_device->readAll(); - if (m_verbose) { - std::cout << chunk.toStdString() << std::flush; - } - m_readed += chunk; - } - } - void Write(QByteArray s = "", bool new_line = true) { - if (!m_device) return; - if (m_verbose) { - std::cout << s.toStdString() << std::flush; - if (new_line) { - std::cout << std::endl; - } - } - s += "\n"; - while (!s.isEmpty()) { - auto res = m_device->write(s); - ASSERT_NE(res, -1); - s.remove(0, res); - } - FlushIfCan(m_device); - } - void Close() { -#ifdef __CYGWIN__ - // Qt on cygwin silently doesn't send the rest of buffer from socket - // without this line - sleep(1); -#endif - m_device->disconnectFromHost(); - } - - private: - // Need to flush QTcpSocket, and QIODevice doesn't have flush at all... - static void FlushIfCan(QIODevice*) {} - static void FlushIfCan(QTcpSocket* sock) { sock->flush(); } - - Device* m_device; - bool m_verbose; - QByteArray m_readed; -}; - -template -IO WrapIO(Device* d) { - return IO(d); -} - -using Socket = IO; - -class Process : public IO { - public: - Process(QString cmd, QStringList args, - std::function setup = [](QProcess*) {}) - : IO(&m_proc, true) { - auto env = QProcessEnvironment::systemEnvironment(); - env.insert("ZNC_DEBUG_TIMER", "1"); - // Default exit codes of sanitizers upon error: - // ASAN - 1 - // LSAN - 23 (part of ASAN, but uses a different value) - // TSAN - 66 - // - // ZNC uses 1 too to report startup failure. - // But we don't want to confuse expected startup failure with ASAN - // error. - env.insert("ASAN_OPTIONS", "exitcode=57"); - m_proc.setProcessEnvironment(env); - setup(&m_proc); - m_proc.start(cmd, args); - EXPECT_TRUE(m_proc.waitForStarted()) - << "Failed to start ZNC, did you install it?"; - } - ~Process() override { - if (m_kill) m_proc.terminate(); - [this]() { - ASSERT_TRUE(m_proc.waitForFinished()); - if (!m_allowDie) { - ASSERT_EQ(QProcess::NormalExit, m_proc.exitStatus()); - if (m_allowLeak) { - ASSERT_THAT(m_proc.exitStatus(), AnyOf(Eq(23), Eq(m_exit))); - } else { - ASSERT_EQ(m_exit, m_proc.exitCode()); - } - } - }(); - } - void ShouldFinishItself(int code = 0) { - m_kill = false; - m_exit = code; - } - void CanDie() { m_allowDie = true; } - - // I can't do much about SWIG... - void CanLeak() { m_allowLeak = true; } - - private: - bool m_kill = true; - int m_exit = 0; - bool m_allowDie = false; - bool m_allowLeak = false; - QProcess m_proc; -}; - -void WriteConfig(QString path) { - // clang-format off - Process p(ZNC_BIN_DIR "/znc", QStringList() << "--debug" - << "--datadir" << path - << "--makeconf"); - p.ReadUntil("Listen on port"); p.Write("12345"); - p.ReadUntil("Listen using SSL"); p.Write(); - p.ReadUntil("IPv6"); p.Write(); - p.ReadUntil("Username"); p.Write("user"); - p.ReadUntil("password"); p.Write("hunter2", false); - p.ReadUntil("Confirm"); p.Write("hunter2", false); - p.ReadUntil("Nick [user]"); p.Write(); - p.ReadUntil("Alternate nick [user_]"); p.Write(); - p.ReadUntil("Ident [user]"); p.Write(); - p.ReadUntil("Real name"); p.Write(); - p.ReadUntil("Bind host"); p.Write(); - p.ReadUntil("Set up a network?"); p.Write(); - p.ReadUntil("Name [freenode]"); p.Write("test"); - p.ReadUntil("Server host (host only)"); p.Write("127.0.0.1"); - p.ReadUntil("Server uses SSL?"); p.Write(); - p.ReadUntil("6667"); p.Write(); - p.ReadUntil("password"); p.Write(); - p.ReadUntil("channels"); p.Write(); - p.ReadUntil("Launch ZNC now?"); p.Write("no"); - p.ShouldFinishItself(); - // clang-format on -} - -TEST(Config, AlreadyExists) { - QTemporaryDir dir; - WriteConfig(dir.path()); - Process p(ZNC_BIN_DIR "/znc", QStringList() << "--debug" - << "--datadir" << dir.path() - << "--makeconf"); - p.ReadUntil("already exists"); - p.CanDie(); -} - -// Can't use QEventLoop without existing QCoreApplication -class App { - public: - App() : m_argv(new char{}), m_app(m_argc, &m_argv) {} - ~App() { delete m_argv; } - - private: - int m_argc = 1; - char* m_argv; - QCoreApplication m_app; -}; - -class ZNCTest : public testing::Test { - protected: - void SetUp() override { - WriteConfig(m_dir.path()); - ASSERT_TRUE(m_server.listen(QHostAddress::LocalHost, 6667)) - << m_server.errorString().toStdString(); - } - - Socket ConnectIRCd() { - [this] { - ASSERT_TRUE(m_server.waitForNewConnection(30000 /* msec */)); - }(); - return WrapIO(m_server.nextPendingConnection()); - } - - Socket ConnectClient() { - m_clients.emplace_back(); - QTcpSocket& sock = m_clients.back(); - sock.connectToHost("127.0.0.1", 12345); - [&] { - ASSERT_TRUE(sock.waitForConnected()) - << sock.errorString().toStdString(); - }(); - return WrapIO(&sock); - } - - std::unique_ptr Run() { - return std::unique_ptr(new Process( - ZNC_BIN_DIR "/znc", QStringList() << "--debug" - << "--datadir" << m_dir.path(), - [](QProcess* proc) { - proc->setProcessChannelMode(QProcess::ForwardedChannels); - })); - } - - Socket LoginClient() { - auto client = ConnectClient(); - client.Write("PASS :hunter2"); - client.Write("NICK nick"); - client.Write("USER user/test x x :x"); - return client; - } - - std::unique_ptr HttpGet(QNetworkRequest request) { - return HandleHttp(m_network.get(request)); - } - std::unique_ptr HttpPost( - QNetworkRequest request, QList> data) { - request.setHeader(QNetworkRequest::ContentTypeHeader, - "application/x-www-form-urlencoded"); - QUrlQuery q; - q.setQueryItems(data); - return HandleHttp(m_network.post(request, q.toString().toUtf8())); - } - std::unique_ptr HandleHttp(QNetworkReply* reply) { - QEventLoop loop; - QObject::connect(reply, &QNetworkReply::finished, [&]() { - std::cout << "Got HTTP reply" << std::endl; - loop.quit(); - }); - QObject::connect( - reply, - static_cast( - &QNetworkReply::error), - [&](QNetworkReply::NetworkError e) { - ADD_FAILURE() << reply->errorString().toStdString(); - }); - QTimer::singleShot(30000 /* msec */, &loop, [&]() { - ADD_FAILURE() << "connection timeout"; - loop.quit(); - }); - std::cout << "Start HTTP loop.exec()" << std::endl; - loop.exec(); - std::cout << "Finished HTTP loop.exec()" << std::endl; - return std::unique_ptr(reply); - } - - void InstallModule(QString name, QString content) { - QDir dir(m_dir.path()); - ASSERT_TRUE(dir.mkpath("modules")); - ASSERT_TRUE(dir.cd("modules")); - if (name.endsWith(".cpp")) { - // Compile - QTemporaryDir srcdir; - QFile file(QDir(srcdir.path()).filePath(name)); - ASSERT_TRUE(file.open(QIODevice::WriteOnly | QIODevice::Text)); - QTextStream out(&file); - out << content; - file.close(); - Process p( - ZNC_BIN_DIR "/znc-buildmod", QStringList() << file.fileName(), - [&](QProcess* proc) { - proc->setWorkingDirectory(dir.absolutePath()); - proc->setProcessChannelMode(QProcess::ForwardedChannels); - }); - p.ShouldFinishItself(); - } else if (name.endsWith(".py")) { - // Dedent - QStringList lines = content.split("\n"); - int maxoffset = -1; - for (const QString& line : lines) { - int nonspace = line.indexOf(QRegExp("\\S")); - if (nonspace == -1) continue; - if (nonspace < maxoffset || maxoffset == -1) - maxoffset = nonspace; - } - if (maxoffset == -1) maxoffset = 0; - QFile file(dir.filePath(name)); - ASSERT_TRUE(file.open(QIODevice::WriteOnly | QIODevice::Text)); - QTextStream out(&file); - for (const QString& line : lines) { - // QTextStream::operator<<(const QStringRef &string) was - // introduced in Qt 5.6; let's keep minimum required version - // less than that for now. - out << line.mid(maxoffset) << "\n"; - } - } else { - // Write as is - QFile file(dir.filePath(name)); - ASSERT_TRUE(file.open(QIODevice::WriteOnly | QIODevice::Text)); - QTextStream out(&file); - out << content; - } - } - - App m_app; - QNetworkAccessManager m_network; - QTemporaryDir m_dir; - QTcpServer m_server; - std::list m_clients; -}; - -TEST_F(ZNCTest, Connect) { - auto znc = Run(); - - auto ircd = ConnectIRCd(); - ircd.ReadUntil("CAP LS"); - - auto client = ConnectClient(); - client.Write("PASS :hunter2"); - client.Write("NICK nick"); - client.Write("USER user/test x x :x"); - client.ReadUntil("Welcome"); - client.Close(); - - client = ConnectClient(); - client.Write("PASS :user:hunter2"); - client.Write("NICK nick"); - client.Write("USER u x x x"); - client.ReadUntil("Welcome"); - client.Close(); - - client = ConnectClient(); - client.Write("NICK nick"); - client.Write("USER user x x x"); - client.ReadUntil("Configure your client to send a server password"); - client.Close(); - - ircd.Write(":server 001 nick :Hello"); - ircd.ReadUntil("WHO"); -} - -TEST_F(ZNCTest, Channel) { - auto znc = Run(); - auto ircd = ConnectIRCd(); - - auto client = LoginClient(); - client.ReadUntil("Welcome"); - client.Write("JOIN #znc"); - client.Close(); - - ircd.Write(":server 001 nick :Hello"); - ircd.ReadUntil("JOIN #znc"); - ircd.Write(":nick JOIN :#znc"); - ircd.Write(":server 353 nick #znc :nick"); - ircd.Write(":server 366 nick #znc :End of /NAMES list"); - ircd.Write(":server PING :1"); - ircd.ReadUntil("PONG 1"); - - client = LoginClient(); - client.ReadUntil(":nick JOIN :#znc"); -} - -TEST_F(ZNCTest, HTTP) { - auto znc = Run(); - auto ircd = ConnectIRCd(); - auto reply = HttpGet(QNetworkRequest(QUrl("http://127.0.0.1:12345/"))); - EXPECT_THAT(reply->rawHeader("Server").toStdString(), HasSubstr("ZNC")); -} - -TEST_F(ZNCTest, FixCVE20149403) { - auto znc = Run(); - auto ircd = ConnectIRCd(); - ircd.Write(":server 001 nick :Hello"); - ircd.Write(":server 005 nick CHANTYPES=# :supports"); - ircd.Write(":server PING :1"); - ircd.ReadUntil("PONG 1"); - - QNetworkRequest request; - request.setRawHeader("Authorization", - "Basic " + QByteArray("user:hunter2").toBase64()); - request.setUrl(QUrl("http://127.0.0.1:12345/mods/global/webadmin/addchan")); - HttpPost(request, { - {"user", "user"}, - {"network", "test"}, - {"submitted", "1"}, - {"name", "znc"}, - {"enabled", "1"}, - }); - EXPECT_THAT(HttpPost(request, - { - {"user", "user"}, - {"network", "test"}, - {"submitted", "1"}, - {"name", "znc"}, - {"enabled", "1"}, - }) - ->readAll() - .toStdString(), - HasSubstr("Channel [#znc] already exists")); -} - -TEST_F(ZNCTest, FixFixOfCVE20149403) { - auto znc = Run(); - auto ircd = ConnectIRCd(); - ircd.Write(":server 001 nick :Hello"); - ircd.Write(":nick JOIN @#znc"); - ircd.ReadUntil("MODE @#znc"); - ircd.Write(":server 005 nick STATUSMSG=@ :supports"); - ircd.Write(":server PING :12345"); - ircd.ReadUntil("PONG 12345"); - - QNetworkRequest request; - request.setRawHeader("Authorization", - "Basic " + QByteArray("user:hunter2").toBase64()); - request.setUrl(QUrl("http://127.0.0.1:12345/mods/global/webadmin/addchan")); - auto reply = HttpPost(request, { - {"user", "user"}, - {"network", "test"}, - {"submitted", "1"}, - {"name", "@#znc"}, - {"enabled", "1"}, - }); - EXPECT_THAT(reply->readAll().toStdString(), - HasSubstr("Could not add channel [@#znc]")); -} - -TEST_F(ZNCTest, InvalidConfigInChan) { - QFile conf(m_dir.path() + "/configs/znc.conf"); - ASSERT_TRUE(conf.open(QIODevice::Append | QIODevice::Text)); - QTextStream out(&conf); - out << R"( - - - - Invalid = Line - - - - )"; - out.flush(); - auto znc = Run(); - znc->ShouldFinishItself(1); -} - -TEST_F(ZNCTest, NotifyConnectModule) { - auto znc = Run(); - auto ircd = ConnectIRCd(); - auto client = LoginClient(); - client.Write("znc loadmod notify_connect"); - client.ReadUntil("Loaded module"); - - auto client2 = ConnectClient(); - client2.Write("PASS :hunter2"); - client2.Write("NICK nick"); - client2.Write("USER user/test x x :x"); - client.ReadUntil("NOTICE nick :*** user attached from 127.0.0.1"); - - auto client3 = ConnectClient(); - client3.Write("PASS :hunter2"); - client3.Write("NICK nick"); - client3.Write("USER user@identifier/test x x :x"); - client.ReadUntil( - "NOTICE nick :*** user@identifier attached from 127.0.0.1"); - client2.ReadUntil( - "NOTICE nick :*** user@identifier attached from 127.0.0.1"); - - client2.Write("QUIT"); - client.ReadUntil("NOTICE nick :*** user detached from 127.0.0.1"); - - client3.Close(); - client.ReadUntil( - "NOTICE nick :*** user@identifier detached from 127.0.0.1"); -} - -TEST_F(ZNCTest, ShellModule) { - auto znc = Run(); - auto ircd = ConnectIRCd(); - auto client = LoginClient(); - client.Write("znc loadmod shell"); - client.Write("PRIVMSG *shell :echo blahblah"); - client.ReadUntil("PRIVMSG nick :blahblah"); - client.ReadUntil("PRIVMSG nick :znc$"); -} - -TEST_F(ZNCTest, WatchModule) { - // TODO test other messages - // TODO test options - auto znc = Run(); - auto ircd = ConnectIRCd(); - auto client = LoginClient(); - client.Write("znc loadmod watch"); - client.Write("PRIVMSG *watch :add *"); - client.ReadUntil("Adding entry:"); - ircd.Write(":server 001 nick :Hello"); - ircd.Write(":nick JOIN :#znc"); - ircd.Write(":n!i@h PRIVMSG #znc :\001ACTION foo\001"); - client.ReadUntil( - ":$*!watch@znc.in PRIVMSG nick :* CTCP: n [ACTION foo] to [#znc]"); -} - -TEST_F(ZNCTest, Modperl) { - if (QProcessEnvironment::systemEnvironment().value( - "DISABLED_ZNC_PERL_PYTHON_TEST") == "1") { - return; - } - auto znc = Run(); - znc->CanLeak(); - auto ircd = ConnectIRCd(); - auto client = LoginClient(); - client.Write("znc loadmod modperl"); - client.Write("znc loadmod perleval"); - client.Write("PRIVMSG *perleval :2+2"); - client.ReadUntil(":*perleval!znc@znc.in PRIVMSG nick :Result: 4"); - client.Write("PRIVMSG *perleval :$self->GetUser->GetUserName"); - client.ReadUntil("Result: user"); -} - -TEST_F(ZNCTest, Modpython) { - if (QProcessEnvironment::systemEnvironment().value( - "DISABLED_ZNC_PERL_PYTHON_TEST") == "1") { - return; - } - auto znc = Run(); - znc->CanLeak(); - auto ircd = ConnectIRCd(); - auto client = LoginClient(); - client.Write("znc loadmod modpython"); - client.Write("znc loadmod pyeval"); - client.Write("PRIVMSG *pyeval :2+2"); - client.ReadUntil(":*pyeval!znc@znc.in PRIVMSG nick :4"); - client.Write("PRIVMSG *pyeval :module.GetUser().GetUserName()"); - client.ReadUntil("nick :'user'"); - ircd.Write(":server 001 nick :Hello"); - ircd.Write(":n!u@h PRIVMSG nick :Hi\xF0, github issue #1229"); - // "replacement character" - client.ReadUntil("Hi\xEF\xBF\xBD, github issue"); -} - -TEST_F(ZNCTest, Encoding) { - auto znc = Run(); - auto ircd = ConnectIRCd(); - auto client = LoginClient(); - ircd.Write(":server 001 nick :hello"); - // legacy - ircd.Write(":n!u@h PRIVMSG nick :Hello\xE6world"); - client.ReadUntil("Hello\xE6world"); - client.Write("PRIVMSG *controlpanel :SetNetwork Encoding $me $net UTF-8"); - client.ReadUntil("Encoding = UTF-8"); - ircd.Write(":n!u@h PRIVMSG nick :Hello\xE6world"); - client.ReadUntil("Hello\xEF\xBF\xBDworld"); - client.Write( - "PRIVMSG *controlpanel :SetNetwork Encoding $me $net ^CP-1251"); - client.ReadUntil("Encoding = ^CP-1251"); - ircd.Write(":n!u@h PRIVMSG nick :Hello\xE6world"); - client.ReadUntil("Hello\xD0\xB6world"); - ircd.Write(":n!u@h PRIVMSG nick :Hello\xD0\xB6world"); - client.ReadUntil("Hello\xD0\xB6world"); -} - -TEST_F(ZNCTest, BuildMod) { - auto znc = Run(); - auto ircd = ConnectIRCd(); - auto client = LoginClient(); - QTemporaryDir srcd; - QDir srcdir(srcd.path()); - QFile file(srcdir.filePath("testmod.cpp")); - ASSERT_TRUE(file.open(QIODevice::WriteOnly | QIODevice::Text)); - QTextStream out(&file); - out << R"( - #include - class TestModule : public CModule { - public: - MODCONSTRUCTOR(TestModule) {} - void OnModCommand(const CString& sLine) override { - PutModule("Lorem ipsum"); - } - }; - MODULEDEFS(TestModule, "Test") - )"; - file.close(); - QDir dir(m_dir.path()); - EXPECT_TRUE(dir.mkdir("modules")); - EXPECT_TRUE(dir.cd("modules")); - { - Process p(ZNC_BIN_DIR "/znc-buildmod", - QStringList() << srcdir.filePath("file-not-found.cpp"), - [&](QProcess* proc) { - proc->setWorkingDirectory(dir.absolutePath()); - proc->setProcessChannelMode(QProcess::ForwardedChannels); - }); - p.ShouldFinishItself(1); - } - { - Process p(ZNC_BIN_DIR "/znc-buildmod", - QStringList() << srcdir.filePath("testmod.cpp"), - [&](QProcess* proc) { - proc->setWorkingDirectory(dir.absolutePath()); - proc->setProcessChannelMode(QProcess::ForwardedChannels); - }); - p.ShouldFinishItself(); - } - client.Write("znc loadmod testmod"); - client.Write("PRIVMSG *testmod :hi"); - client.ReadUntil("Lorem ipsum"); -} - -TEST_F(ZNCTest, AutoAttachModule) { - auto znc = Run(); - auto ircd = ConnectIRCd(); - auto client = LoginClient(); - InstallModule("testmod.cpp", R"( - #include - #include - class TestModule : public CModule { - public: - MODCONSTRUCTOR(TestModule) {} - EModRet OnChanBufferPlayMessage(CMessage& Message) override { - PutIRC("TEST " + Message.GetClient()->GetNickMask()); - return CONTINUE; - } - }; - MODULEDEFS(TestModule, "Test") - )"); - client.Write("znc loadmod testmod"); - client.Write("PRIVMSG *controlpanel :Set AutoClearChanBuffer $me no"); - client.Write("znc loadmod autoattach"); - client.Write("PRIVMSG *autoattach :Add * * *"); - client.ReadUntil("Added to list"); - ircd.Write(":server 001 nick :Hello"); - ircd.Write(":nick JOIN :#znc"); - ircd.Write(":server 353 nick #znc :nick"); - ircd.Write(":server 366 nick #znc :End of /NAMES list"); - ircd.Write(":foo PRIVMSG #znc :hi"); - client.ReadUntil(":foo PRIVMSG"); - client.Write("detach #znc"); - client.ReadUntil("Detached"); - ircd.Write(":foo PRIVMSG #znc :hello"); - ircd.ReadUntil("TEST"); - client.ReadUntil("hello"); -} - -TEST_F(ZNCTest, KeepNickModule) { - auto znc = Run(); - auto ircd = ConnectIRCd(); - auto client = LoginClient(); - client.Write("znc loadmod keepnick"); - client.ReadUntil("Loaded module"); - ircd.ReadUntil("NICK user"); - ircd.Write(":server 433 * nick :Nickname is already in use."); - ircd.ReadUntil("NICK user_"); - ircd.Write(":server 001 user_ :Hello"); - client.ReadUntil("Connected!"); - ircd.ReadUntil("NICK user"); - ircd.Write(":server 435 user_ user #error :Nope :-P"); - client.ReadUntil( - ":*keepnick!znc@znc.in PRIVMSG user_ " - ":Unable to obtain nick user: Nope :-P, #error"); -} - -TEST_F(ZNCTest, ModuleCSRFOverride) { - auto znc = Run(); - auto ircd = ConnectIRCd(); - auto client = LoginClient(); - client.Write("znc loadmod samplewebapi"); - client.ReadUntil("Loaded module"); - auto request = QNetworkRequest(QUrl("http://127.0.0.1:12345/mods/global/samplewebapi/")); - auto reply = HttpPost(request, { - {"text", "ipsum"} - })->readAll().toStdString(); - EXPECT_THAT(reply, HasSubstr("ipsum")); -} - -TEST_F(ZNCTest, ModuleCrypt) { - QFile conf(m_dir.path() + "/configs/znc.conf"); - ASSERT_TRUE(conf.open(QIODevice::Append | QIODevice::Text)); - QTextStream(&conf) << "ServerThrottle = 1\n"; - auto znc = Run(); - - auto ircd1 = ConnectIRCd(); - auto client1 = LoginClient(); - client1.Write("znc loadmod controlpanel"); - client1.Write("PRIVMSG *controlpanel :CloneUser user user2"); - client1.ReadUntil("User user2 added!"); - client1.Write("PRIVMSG *controlpanel :Set Nick user2 nick2"); - client1.Write("znc loadmod crypt"); - client1.ReadUntil("Loaded module"); - - auto ircd2 = ConnectIRCd(); - auto client2 = ConnectClient(); - client2.Write("PASS user2:hunter2"); - client2.Write("NICK nick2"); - client2.Write("USER user2/test x x :x"); - client2.Write("znc loadmod crypt"); - client2.ReadUntil("Loaded module"); - - client1.Write("PRIVMSG *crypt :keyx nick2"); - client1.ReadUntil("Sent my DH1080 public key to nick2"); - - QByteArray pub1(""); - ircd1.ReadUntilAndGet("NOTICE nick2 :DH1080_INIT ", pub1); - ircd2.Write(":user!user@user/test " + pub1); - - client2.ReadUntil("Received DH1080 public key from user"); - client2.ReadUntil("Key for user successfully set."); - - QByteArray pub2(""); - ircd2.ReadUntilAndGet("NOTICE user :DH1080_FINISH ", pub2); - ircd1.Write(":nick2!user2@user2/test " + pub2); - - client1.ReadUntil("Key for nick2 successfully set."); - - client1.Write("PRIVMSG *crypt :listkeys"); - QByteArray key1(""); - client1.ReadUntilAndGet("| nick2 | ", key1); - client2.Write("PRIVMSG *crypt :listkeys"); - QByteArray key2(""); - client2.ReadUntilAndGet("| user | ", key2); - ASSERT_EQ(key1.mid(11), key2.mid(11)); - client1.Write("CAP REQ :echo-message"); - client1.Write("PRIVMSG .nick2 :Hello"); - QByteArray secretmsg; - ircd1.ReadUntilAndGet("PRIVMSG nick2 :+OK ", secretmsg); - ircd2.Write(":user!user@user/test " + secretmsg); - client2.ReadUntil("Hello"); - client1.ReadUntil(secretmsg); // by echo-message -} - -} // namespace - -class ThrowListener : public testing::EmptyTestEventListener { - void OnTestPartResult(const testing::TestPartResult& result) override { - if (result.type() == testing::TestPartResult::kFatalFailure && - !std::uncaught_exception()) { - throw testing::AssertionException(result); - } - } -}; - -int main(int argc, char** argv) { - testing::InitGoogleTest(&argc, argv); - testing::UnitTest::GetInstance()->listeners().Append(new ThrowListener); - return RUN_ALL_TESTS(); -} diff --git a/test/integration/tests/core.cpp b/test/integration/tests/core.cpp new file mode 100644 index 00000000..d271a326 --- /dev/null +++ b/test/integration/tests/core.cpp @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2004-2016 ZNC, see the NOTICE file for details. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "znctest.h" +#include + +using testing::HasSubstr; + +namespace znc_inttest { +namespace { + +TEST(Config, AlreadyExists) { + QTemporaryDir dir; + WriteConfig(dir.path()); + Process p(ZNC_BIN_DIR "/znc", QStringList() << "--debug" + << "--datadir" << dir.path() + << "--makeconf"); + p.ReadUntil("already exists"); + p.CanDie(); +} + +TEST_F(ZNCTest, Connect) { + auto znc = Run(); + + auto ircd = ConnectIRCd(); + ircd.ReadUntil("CAP LS"); + + auto client = ConnectClient(); + client.Write("PASS :hunter2"); + client.Write("NICK nick"); + client.Write("USER user/test x x :x"); + client.ReadUntil("Welcome"); + client.Close(); + + client = ConnectClient(); + client.Write("PASS :user:hunter2"); + client.Write("NICK nick"); + client.Write("USER u x x x"); + client.ReadUntil("Welcome"); + client.Close(); + + client = ConnectClient(); + client.Write("NICK nick"); + client.Write("USER user x x x"); + client.ReadUntil("Configure your client to send a server password"); + client.Close(); + + ircd.Write(":server 001 nick :Hello"); + ircd.ReadUntil("WHO"); +} + +TEST_F(ZNCTest, Channel) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + + auto client = LoginClient(); + client.ReadUntil("Welcome"); + client.Write("JOIN #znc"); + client.Close(); + + ircd.Write(":server 001 nick :Hello"); + ircd.ReadUntil("JOIN #znc"); + ircd.Write(":nick JOIN :#znc"); + ircd.Write(":server 353 nick #znc :nick"); + ircd.Write(":server 366 nick #znc :End of /NAMES list"); + ircd.Write(":server PING :1"); + ircd.ReadUntil("PONG 1"); + + client = LoginClient(); + client.ReadUntil(":nick JOIN :#znc"); +} + +TEST_F(ZNCTest, HTTP) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto reply = HttpGet(QNetworkRequest(QUrl("http://127.0.0.1:12345/"))); + EXPECT_THAT(reply->rawHeader("Server").toStdString(), HasSubstr("ZNC")); +} + +TEST_F(ZNCTest, FixCVE20149403) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + ircd.Write(":server 001 nick :Hello"); + ircd.Write(":server 005 nick CHANTYPES=# :supports"); + ircd.Write(":server PING :1"); + ircd.ReadUntil("PONG 1"); + + QNetworkRequest request; + request.setRawHeader("Authorization", + "Basic " + QByteArray("user:hunter2").toBase64()); + request.setUrl(QUrl("http://127.0.0.1:12345/mods/global/webadmin/addchan")); + HttpPost(request, { + {"user", "user"}, + {"network", "test"}, + {"submitted", "1"}, + {"name", "znc"}, + {"enabled", "1"}, + }); + EXPECT_THAT(HttpPost(request, + { + {"user", "user"}, + {"network", "test"}, + {"submitted", "1"}, + {"name", "znc"}, + {"enabled", "1"}, + }) + ->readAll() + .toStdString(), + HasSubstr("Channel [#znc] already exists")); +} + +TEST_F(ZNCTest, FixFixOfCVE20149403) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + ircd.Write(":server 001 nick :Hello"); + ircd.Write(":nick JOIN @#znc"); + ircd.ReadUntil("MODE @#znc"); + ircd.Write(":server 005 nick STATUSMSG=@ :supports"); + ircd.Write(":server PING :12345"); + ircd.ReadUntil("PONG 12345"); + + QNetworkRequest request; + request.setRawHeader("Authorization", + "Basic " + QByteArray("user:hunter2").toBase64()); + request.setUrl(QUrl("http://127.0.0.1:12345/mods/global/webadmin/addchan")); + auto reply = HttpPost(request, { + {"user", "user"}, + {"network", "test"}, + {"submitted", "1"}, + {"name", "@#znc"}, + {"enabled", "1"}, + }); + EXPECT_THAT(reply->readAll().toStdString(), + HasSubstr("Could not add channel [@#znc]")); +} + +TEST_F(ZNCTest, InvalidConfigInChan) { + QFile conf(m_dir.path() + "/configs/znc.conf"); + ASSERT_TRUE(conf.open(QIODevice::Append | QIODevice::Text)); + QTextStream out(&conf); + out << R"( + + + + Invalid = Line + + + + )"; + out.flush(); + auto znc = Run(); + znc->ShouldFinishItself(1); +} +TEST_F(ZNCTest, Encoding) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = LoginClient(); + ircd.Write(":server 001 nick :hello"); + // legacy + ircd.Write(":n!u@h PRIVMSG nick :Hello\xE6world"); + client.ReadUntil("Hello\xE6world"); + client.Write("PRIVMSG *controlpanel :SetNetwork Encoding $me $net UTF-8"); + client.ReadUntil("Encoding = UTF-8"); + ircd.Write(":n!u@h PRIVMSG nick :Hello\xE6world"); + client.ReadUntil("Hello\xEF\xBF\xBDworld"); + client.Write( + "PRIVMSG *controlpanel :SetNetwork Encoding $me $net ^CP-1251"); + client.ReadUntil("Encoding = ^CP-1251"); + ircd.Write(":n!u@h PRIVMSG nick :Hello\xE6world"); + client.ReadUntil("Hello\xD0\xB6world"); + ircd.Write(":n!u@h PRIVMSG nick :Hello\xD0\xB6world"); + client.ReadUntil("Hello\xD0\xB6world"); +} + +TEST_F(ZNCTest, BuildMod) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = LoginClient(); + QTemporaryDir srcd; + QDir srcdir(srcd.path()); + QFile file(srcdir.filePath("testmod.cpp")); + ASSERT_TRUE(file.open(QIODevice::WriteOnly | QIODevice::Text)); + QTextStream out(&file); + out << R"( + #include + class TestModule : public CModule { + public: + MODCONSTRUCTOR(TestModule) {} + void OnModCommand(const CString& sLine) override { + PutModule("Lorem ipsum"); + } + }; + MODULEDEFS(TestModule, "Test") + )"; + file.close(); + QDir dir(m_dir.path()); + EXPECT_TRUE(dir.mkdir("modules")); + EXPECT_TRUE(dir.cd("modules")); + { + Process p(ZNC_BIN_DIR "/znc-buildmod", + QStringList() << srcdir.filePath("file-not-found.cpp"), + [&](QProcess* proc) { + proc->setWorkingDirectory(dir.absolutePath()); + proc->setProcessChannelMode(QProcess::ForwardedChannels); + }); + p.ShouldFinishItself(1); + } + { + Process p(ZNC_BIN_DIR "/znc-buildmod", + QStringList() << srcdir.filePath("testmod.cpp"), + [&](QProcess* proc) { + proc->setWorkingDirectory(dir.absolutePath()); + proc->setProcessChannelMode(QProcess::ForwardedChannels); + }); + p.ShouldFinishItself(); + } + client.Write("znc loadmod testmod"); + client.Write("PRIVMSG *testmod :hi"); + client.ReadUntil("Lorem ipsum"); +} + +} // namespace +} // namespace znc_inttest diff --git a/test/integration/tests/modules.cpp b/test/integration/tests/modules.cpp new file mode 100644 index 00000000..2f6af109 --- /dev/null +++ b/test/integration/tests/modules.cpp @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2004-2016 ZNC, see the NOTICE file for details. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "znctest.h" +#include + +using testing::HasSubstr; + +namespace znc_inttest { +namespace { + +TEST_F(ZNCTest, NotifyConnectModule) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = LoginClient(); + client.Write("znc loadmod notify_connect"); + client.ReadUntil("Loaded module"); + + auto client2 = ConnectClient(); + client2.Write("PASS :hunter2"); + client2.Write("NICK nick"); + client2.Write("USER user/test x x :x"); + client.ReadUntil("NOTICE nick :*** user attached from 127.0.0.1"); + + auto client3 = ConnectClient(); + client3.Write("PASS :hunter2"); + client3.Write("NICK nick"); + client3.Write("USER user@identifier/test x x :x"); + client.ReadUntil( + "NOTICE nick :*** user@identifier attached from 127.0.0.1"); + client2.ReadUntil( + "NOTICE nick :*** user@identifier attached from 127.0.0.1"); + + client2.Write("QUIT"); + client.ReadUntil("NOTICE nick :*** user detached from 127.0.0.1"); + + client3.Close(); + client.ReadUntil( + "NOTICE nick :*** user@identifier detached from 127.0.0.1"); +} + +TEST_F(ZNCTest, ShellModule) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = LoginClient(); + client.Write("znc loadmod shell"); + client.Write("PRIVMSG *shell :echo blahblah"); + client.ReadUntil("PRIVMSG nick :blahblah"); + client.ReadUntil("PRIVMSG nick :znc$"); +} + +TEST_F(ZNCTest, WatchModule) { + // TODO test other messages + // TODO test options + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = LoginClient(); + client.Write("znc loadmod watch"); + client.Write("PRIVMSG *watch :add *"); + client.ReadUntil("Adding entry:"); + ircd.Write(":server 001 nick :Hello"); + ircd.Write(":nick JOIN :#znc"); + ircd.Write(":n!i@h PRIVMSG #znc :\001ACTION foo\001"); + client.ReadUntil( + ":$*!watch@znc.in PRIVMSG nick :* CTCP: n [ACTION foo] to [#znc]"); +} + +TEST_F(ZNCTest, ModuleCrypt) { + QFile conf(m_dir.path() + "/configs/znc.conf"); + ASSERT_TRUE(conf.open(QIODevice::Append | QIODevice::Text)); + QTextStream(&conf) << "ServerThrottle = 1\n"; + auto znc = Run(); + + auto ircd1 = ConnectIRCd(); + auto client1 = LoginClient(); + client1.Write("znc loadmod controlpanel"); + client1.Write("PRIVMSG *controlpanel :CloneUser user user2"); + client1.ReadUntil("User user2 added!"); + client1.Write("PRIVMSG *controlpanel :Set Nick user2 nick2"); + client1.Write("znc loadmod crypt"); + client1.ReadUntil("Loaded module"); + + auto ircd2 = ConnectIRCd(); + auto client2 = ConnectClient(); + client2.Write("PASS user2:hunter2"); + client2.Write("NICK nick2"); + client2.Write("USER user2/test x x :x"); + client2.Write("znc loadmod crypt"); + client2.ReadUntil("Loaded module"); + + client1.Write("PRIVMSG *crypt :keyx nick2"); + client1.ReadUntil("Sent my DH1080 public key to nick2"); + + QByteArray pub1(""); + ircd1.ReadUntilAndGet("NOTICE nick2 :DH1080_INIT ", pub1); + ircd2.Write(":user!user@user/test " + pub1); + + client2.ReadUntil("Received DH1080 public key from user"); + client2.ReadUntil("Key for user successfully set."); + + QByteArray pub2(""); + ircd2.ReadUntilAndGet("NOTICE user :DH1080_FINISH ", pub2); + ircd1.Write(":nick2!user2@user2/test " + pub2); + + client1.ReadUntil("Key for nick2 successfully set."); + + client1.Write("PRIVMSG *crypt :listkeys"); + QByteArray key1(""); + client1.ReadUntilAndGet("| nick2 | ", key1); + client2.Write("PRIVMSG *crypt :listkeys"); + QByteArray key2(""); + client2.ReadUntilAndGet("| user | ", key2); + ASSERT_EQ(key1.mid(11), key2.mid(11)); + client1.Write("CAP REQ :echo-message"); + client1.Write("PRIVMSG .nick2 :Hello"); + QByteArray secretmsg; + ircd1.ReadUntilAndGet("PRIVMSG nick2 :+OK ", secretmsg); + ircd2.Write(":user!user@user/test " + secretmsg); + client2.ReadUntil("Hello"); + client1.ReadUntil(secretmsg); // by echo-message +} + +TEST_F(ZNCTest, AutoAttachModule) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = LoginClient(); + InstallModule("testmod.cpp", R"( + #include + #include + class TestModule : public CModule { + public: + MODCONSTRUCTOR(TestModule) {} + EModRet OnChanBufferPlayMessage(CMessage& Message) override { + PutIRC("TEST " + Message.GetClient()->GetNickMask()); + return CONTINUE; + } + }; + MODULEDEFS(TestModule, "Test") + )"); + client.Write("znc loadmod testmod"); + client.Write("PRIVMSG *controlpanel :Set AutoClearChanBuffer $me no"); + client.Write("znc loadmod autoattach"); + client.Write("PRIVMSG *autoattach :Add * * *"); + client.ReadUntil("Added to list"); + ircd.Write(":server 001 nick :Hello"); + ircd.Write(":nick JOIN :#znc"); + ircd.Write(":server 353 nick #znc :nick"); + ircd.Write(":server 366 nick #znc :End of /NAMES list"); + ircd.Write(":foo PRIVMSG #znc :hi"); + client.ReadUntil(":foo PRIVMSG"); + client.Write("detach #znc"); + client.ReadUntil("Detached"); + ircd.Write(":foo PRIVMSG #znc :hello"); + ircd.ReadUntil("TEST"); + client.ReadUntil("hello"); +} + +TEST_F(ZNCTest, KeepNickModule) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = LoginClient(); + client.Write("znc loadmod keepnick"); + client.ReadUntil("Loaded module"); + ircd.ReadUntil("NICK user"); + ircd.Write(":server 433 * nick :Nickname is already in use."); + ircd.ReadUntil("NICK user_"); + ircd.Write(":server 001 user_ :Hello"); + client.ReadUntil("Connected!"); + ircd.ReadUntil("NICK user"); + ircd.Write(":server 435 user_ user #error :Nope :-P"); + client.ReadUntil( + ":*keepnick!znc@znc.in PRIVMSG user_ " + ":Unable to obtain nick user: Nope :-P, #error"); +} + +TEST_F(ZNCTest, ModuleCSRFOverride) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = LoginClient(); + client.Write("znc loadmod samplewebapi"); + client.ReadUntil("Loaded module"); + auto request = QNetworkRequest(QUrl("http://127.0.0.1:12345/mods/global/samplewebapi/")); + auto reply = HttpPost(request, { + {"text", "ipsum"} + })->readAll().toStdString(); + EXPECT_THAT(reply, HasSubstr("ipsum")); +} + +} // namespace +} // namespace znc_inttest diff --git a/test/integration/tests/scripting.cpp b/test/integration/tests/scripting.cpp new file mode 100644 index 00000000..2d9836bd --- /dev/null +++ b/test/integration/tests/scripting.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2004-2016 ZNC, see the NOTICE file for details. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "znctest.h" + +namespace znc_inttest { +namespace { + +TEST_F(ZNCTest, Modperl) { + if (QProcessEnvironment::systemEnvironment().value( + "DISABLED_ZNC_PERL_PYTHON_TEST") == "1") { + return; + } + auto znc = Run(); + znc->CanLeak(); + auto ircd = ConnectIRCd(); + auto client = LoginClient(); + client.Write("znc loadmod modperl"); + client.Write("znc loadmod perleval"); + client.Write("PRIVMSG *perleval :2+2"); + client.ReadUntil(":*perleval!znc@znc.in PRIVMSG nick :Result: 4"); + client.Write("PRIVMSG *perleval :$self->GetUser->GetUserName"); + client.ReadUntil("Result: user"); +} + +TEST_F(ZNCTest, Modpython) { + if (QProcessEnvironment::systemEnvironment().value( + "DISABLED_ZNC_PERL_PYTHON_TEST") == "1") { + return; + } + auto znc = Run(); + znc->CanLeak(); + auto ircd = ConnectIRCd(); + auto client = LoginClient(); + client.Write("znc loadmod modpython"); + client.Write("znc loadmod pyeval"); + client.Write("PRIVMSG *pyeval :2+2"); + client.ReadUntil(":*pyeval!znc@znc.in PRIVMSG nick :4"); + client.Write("PRIVMSG *pyeval :module.GetUser().GetUserName()"); + client.ReadUntil("nick :'user'"); + ircd.Write(":server 001 nick :Hello"); + ircd.Write(":n!u@h PRIVMSG nick :Hi\xF0, github issue #1229"); + // "replacement character" + client.ReadUntil("Hi\xEF\xBF\xBD, github issue"); +} + +} // namespace +} // namespace znc_inttest