From 2791e5d41e96982ec3a80c2725ab4f3b9f3bfffc Mon Sep 17 00:00:00 2001 From: Vincent Richard Date: Fri, 5 May 2006 20:50:26 +0000 Subject: [PATCH] Unit tests for SMTP. --- SConstruct | 6 +- tests/net/smtp/SMTPResponseTest.cpp | 189 ++++++++++++++++++++ tests/net/smtp/SMTPTransportTest.cpp | 256 +++++++++++++++++++++++++++ tests/testRunner.cpp | 4 +- tests/testUtils.cpp | 183 +++++++++++++++++++ tests/testUtils.hpp | 110 ++++++++++++ 6 files changed, 745 insertions(+), 3 deletions(-) create mode 100644 tests/net/smtp/SMTPResponseTest.cpp create mode 100644 tests/net/smtp/SMTPTransportTest.cpp create mode 100644 tests/testUtils.cpp diff --git a/SConstruct b/SConstruct index 07fc4cc1..d85d36ab 100644 --- a/SConstruct +++ b/SConstruct @@ -333,6 +333,7 @@ libvmimetest_common = [ libvmimetest_sources = [ 'tests/testRunner.cpp', + 'tests/testUtils.cpp', # ============================== Parser ============================== 'tests/parser/attachmentHelperTest.cpp', 'tests/parser/bodyPartTest.cpp', @@ -360,7 +361,10 @@ libvmimetest_sources = [ 'tests/misc/importanceHelperTest.cpp', # ============================= Security ============================= 'tests/security/digest/md5Test.cpp', - 'tests/security/digest/sha1Test.cpp' + 'tests/security/digest/sha1Test.cpp', + # =============================== Net ================================ + 'tests/net/smtp/SMTPTransportTest.cpp', + 'tests/net/smtp/SMTPResponseTest.cpp' ] libvmime_autotools = [ diff --git a/tests/net/smtp/SMTPResponseTest.cpp b/tests/net/smtp/SMTPResponseTest.cpp new file mode 100644 index 00000000..495dd437 --- /dev/null +++ b/tests/net/smtp/SMTPResponseTest.cpp @@ -0,0 +1,189 @@ +// +// VMime library (http://www.vmime.org) +// Copyright (C) 2002-2006 Vincent Richard +// +// 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 program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// +// Linking this library statically or dynamically with other modules is making +// a combined work based on this library. Thus, the terms and conditions of +// the GNU General Public License cover the whole combination. +// + +#include "tests/testUtils.hpp" + +#include "vmime/net/smtp/SMTPResponse.hpp" + + +#define VMIME_TEST_SUITE SMTPResponseTest +#define VMIME_TEST_SUITE_MODULE "Net/SMTP" + + +VMIME_TEST_SUITE_BEGIN + + VMIME_TEST_LIST_BEGIN + VMIME_TEST(testSingleLineResponse) + VMIME_TEST(testSingleLineResponseLF) + VMIME_TEST(testMultiLineResponse) + VMIME_TEST(testMultiLineResponseDifferentCode) + VMIME_TEST(testIncompleteMultiLineResponse) + VMIME_TEST(testIntermediateResponse) + VMIME_TEST(testNoResponseText) + VMIME_TEST_LIST_END + + + void testSingleLineResponse() + { + vmime::ref socket = vmime::create (); + vmime::ref toh = + vmime::create (); + + socket->localSend("123 Response Text\r\n"); + + vmime::ref resp = + vmime::net::smtp::SMTPResponse::readResponse(socket, toh); + + VASSERT_EQ("Code", 123, resp->getCode()); + VASSERT_EQ("Lines", 1, resp->getLineCount()); + VASSERT_EQ("Text", "Response Text", resp->getText()); + } + + void testSingleLineResponseLF() + { + vmime::ref socket = vmime::create (); + vmime::ref toh = + vmime::create (); + + socket->localSend("123 Response Text\n"); + + vmime::ref resp = + vmime::net::smtp::SMTPResponse::readResponse(socket, toh); + + VASSERT_EQ("Code", 123, resp->getCode()); + VASSERT_EQ("Lines", 1, resp->getLineCount()); + VASSERT_EQ("Text", "Response Text", resp->getText()); + } + + void testMultiLineResponse() + { + vmime::ref socket = vmime::create (); + vmime::ref toh = + vmime::create (); + + socket->localSend + ( + "123-Response\r\n" + "123 Text\r\n" + ); + + vmime::ref resp = + vmime::net::smtp::SMTPResponse::readResponse(socket, toh); + + VASSERT_EQ("Code", 123, resp->getCode()); + VASSERT_EQ("Lines", 2, resp->getLineCount()); + VASSERT_EQ("Text", "Response\nText", resp->getText()); + + VASSERT_EQ("Code", 123, resp->getLineAt(0).getCode()); + VASSERT_EQ("Text", "Response", resp->getLineAt(0).getText()); + + VASSERT_EQ("Code", 123, resp->getLineAt(1).getCode()); + VASSERT_EQ("Text", "Text", resp->getLineAt(1).getText()); + } + + void testMultiLineResponseDifferentCode() + { + vmime::ref socket = vmime::create (); + vmime::ref toh = + vmime::create (); + + socket->localSend + ( + "123-Response\r\n" + "456 Text\r\n" + ); + + vmime::ref resp = + vmime::net::smtp::SMTPResponse::readResponse(socket, toh); + + VASSERT_EQ("Code", 0, resp->getCode()); + VASSERT_EQ("Lines", 2, resp->getLineCount()); + VASSERT_EQ("Text", "Response\nText", resp->getText()); + + VASSERT_EQ("Code", 123, resp->getLineAt(0).getCode()); + VASSERT_EQ("Text", "Response", resp->getLineAt(0).getText()); + + VASSERT_EQ("Code", 456, resp->getLineAt(1).getCode()); + VASSERT_EQ("Text", "Text", resp->getLineAt(1).getText()); + } + + void testIncompleteMultiLineResponse() + { + vmime::ref socket = vmime::create (); + vmime::ref toh = + vmime::create (1); + + socket->localSend + ( + "123-Response\r\n" + "123-Text\r\n" + // Missing data + ); + + VASSERT_THROW("Incomplete response", + vmime::net::smtp::SMTPResponse::readResponse(socket, toh), + vmime::exceptions::operation_timed_out); + } + + void testIntermediateResponse() + { + vmime::ref socket = vmime::create (); + vmime::ref toh = + vmime::create (1); + + socket->localSend + ( + "334\r\n" + "More information\r\n" + ); + + vmime::ref resp = + vmime::net::smtp::SMTPResponse::readResponse(socket, toh); + + VASSERT_EQ("Code", 334, resp->getCode()); + VASSERT_EQ("Lines", 1, resp->getLineCount()); + VASSERT_EQ("Text", "More information", resp->getText()); + } + + void testNoResponseText() + { + vmime::ref socket = vmime::create (); + vmime::ref toh = + vmime::create (1); + + socket->localSend + ( + "250\r\n" + ); + + vmime::ref resp = + vmime::net::smtp::SMTPResponse::readResponse(socket, toh); + + VASSERT_EQ("Code", 250, resp->getCode()); + VASSERT_EQ("Lines", 1, resp->getLineCount()); + VASSERT_EQ("Text", "", resp->getText()); + } + + +VMIME_TEST_SUITE_END + diff --git a/tests/net/smtp/SMTPTransportTest.cpp b/tests/net/smtp/SMTPTransportTest.cpp new file mode 100644 index 00000000..a69c2144 --- /dev/null +++ b/tests/net/smtp/SMTPTransportTest.cpp @@ -0,0 +1,256 @@ +// +// VMime library (http://www.vmime.org) +// Copyright (C) 2002-2006 Vincent Richard +// +// 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 program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// +// Linking this library statically or dynamically with other modules is making +// a combined work based on this library. Thus, the terms and conditions of +// the GNU General Public License cover the whole combination. +// + +#include "tests/testUtils.hpp" + + +#define VMIME_TEST_SUITE SMTPTransportTest +#define VMIME_TEST_SUITE_MODULE "Net/SMTP" + + +class greetingErrorSMTPTestSocket; +class MAILandRCPTSMTPTestSocket; + + +VMIME_TEST_SUITE_BEGIN + + VMIME_TEST_LIST_BEGIN + VMIME_TEST(testGreetingError) + VMIME_TEST(testMAILandRCPT) + VMIME_TEST_LIST_END + + + void testGreetingError() + { + vmime::ref session = + vmime::create (); + + vmime::ref tr = session->getTransport + (vmime::utility::url("smtp://localhost")); + + tr->setSocketFactory(vmime::create >()); + tr->setTimeoutHandlerFactory(vmime::create ()); + + VASSERT_THROW("Connection", tr->connect(), + vmime::exceptions::connection_greeting_error); + } + + void testMAILandRCPT() + { + vmime::ref session = + vmime::create (); + + vmime::ref tr = session->getTransport + (vmime::utility::url("smtp://localhost")); + + tr->setSocketFactory(vmime::create >()); + tr->setTimeoutHandlerFactory(vmime::create ()); + + VASSERT_NO_THROW("Connection", tr->connect()); + + vmime::mailbox exp("expeditor@test.vmime.org"); + + vmime::mailboxList recips; + recips.appendMailbox(vmime::create ("recipient1@test.vmime.org")); + recips.appendMailbox(vmime::create ("recipient2@test.vmime.org")); + recips.appendMailbox(vmime::create ("recipient3@test.vmime.org")); + + vmime::string data("Message data"); + vmime::utility::inputStreamStringAdapter is(data); + + tr->send(exp, recips, is, 0); + } + +VMIME_TEST_SUITE_END + + +/** Accepts connection and fails on greeting. + */ +class greetingErrorSMTPTestSocket : public lineBasedTestSocket +{ +public: + + void onConnected() + { + localSend("421 test.vmime.org Service not available, closing transmission channel\r\n"); + disconnect(); + } + + void processCommand() + { + if (!haveMoreLines()) + return; + + getNextLine(); + + localSend("502 Command not implemented\r\n"); + processCommand(); + } +}; + + +/** SMTP test server 1. + * + * Test send(). + * Ensure MAIL and RCPT commands are sent correctly. + */ +class MAILandRCPTSMTPTestSocket : public lineBasedTestSocket +{ +public: + + MAILandRCPTSMTPTestSocket() + { + m_recipients.insert("recipient1@test.vmime.org"); + m_recipients.insert("recipient2@test.vmime.org"); + m_recipients.insert("recipient3@test.vmime.org"); + + m_state = STATE_NOT_CONNECTED; + } + + void onConnected() + { + localSend("220 test.vmime.org Service ready\r\n"); + processCommand(); + + m_state = STATE_COMMAND; + } + + void processCommand() + { + if (!haveMoreLines()) + return; + + vmime::string line = getNextLine(); + std::istringstream iss(line); + + switch (m_state) + { + case STATE_NOT_CONNECTED: + + localSend("451 Requested action aborted: invalid state\r\n"); + break; + + case STATE_COMMAND: + { + std::string cmd; + iss >> cmd; + + if (cmd.empty()) + { + localSend("500 Syntax error, command unrecognized\r\n"); + } + else if (cmd == "HELO") + { + localSend("250 OK\r\n"); + } + else if (cmd == "MAIL") + { + VASSERT_EQ("MAIL", std::string("MAIL FROM: "), line); + + localSend("250 OK\r\n"); + } + else if (cmd == "RCPT") + { + const vmime::string::size_type lt = line.find('<'); + const vmime::string::size_type gt = line.find('>'); + + VASSERT("RCPT <", lt != vmime::string::npos); + VASSERT("RCPT >", gt != vmime::string::npos); + VASSERT("RCPT ><", gt >= lt); + + const vmime::string recip = vmime::string + (line.begin() + lt + 1, line.begin() + gt); + + std::set ::iterator it = + m_recipients.find(recip); + + VASSERT(std::string("Recipient not found: '") + recip + "'", + it != m_recipients.end()); + + m_recipients.erase(it); + + localSend("250 OK, recipient accepted\r\n"); + } + else if (cmd == "DATA") + { + VASSERT("All recipients", m_recipients.empty()); + + localSend("354 Ready to accept data; end with .\r\n"); + + m_state = STATE_DATA; + m_msgData.clear(); + } + else if (cmd == "NOOP") + { + localSend("250 Completed\r\n"); + } + else if (cmd == "QUIT") + { + localSend("221 test.vmime.org Service closing transmission channel\r\n"); + } + else + { + localSend("502 Command not implemented\r\n"); + } + + break; + } + case STATE_DATA: + { + if (line == ".") + { + VASSERT_EQ("Data", "Message data\r\n", m_msgData); + + localSend("250 Message accepted for delivery\r\n"); + m_state = STATE_COMMAND; + } + else + { + m_msgData += line + "\r\n"; + } + + break; + } + + } + + processCommand(); + } + +private: + + enum State + { + STATE_NOT_CONNECTED, + STATE_COMMAND, + STATE_DATA + }; + + int m_state; + + std::set m_recipients; + + std::string m_msgData; +}; + + diff --git a/tests/testRunner.cpp b/tests/testRunner.cpp index c49090f0..8700ea16 100644 --- a/tests/testRunner.cpp +++ b/tests/testRunner.cpp @@ -210,7 +210,7 @@ int main(int argc, char* argv[]) // Run the tests if (xmlOutput) { - // Get the test suites from the registry and add them to the list of test to run + // Get the test suites from the registry and add them to the list of tests to run CppUnit::TestRunner runner; for (unsigned int i = 0 ; i < getTestModules().size() ; ++i) @@ -231,7 +231,7 @@ int main(int argc, char* argv[]) xmlListener->output(std::cout); - // Return error code 1 if the one of test failed + // Return error code 1 if a test failed return result.wasSuccessful() ? 0 : 1; } else diff --git a/tests/testUtils.cpp b/tests/testUtils.cpp new file mode 100644 index 00000000..34d945c9 --- /dev/null +++ b/tests/testUtils.cpp @@ -0,0 +1,183 @@ +// +// VMime library (http://www.vmime.org) +// Copyright (C) 2002-2005 Vincent Richard +// +// 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 program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// +// Linking this library statically or dynamically with other modules is making +// a combined work based on this library. Thus, the terms and conditions of +// the GNU General Public License cover the whole combination. +// + +#include "testUtils.hpp" + + + +// testSocket + +void testSocket::connect(const vmime::string& address, const vmime::port_t port) +{ + m_address = address; + m_port = port; + m_connected = true; + + onConnected(); +} + + +void testSocket::disconnect() +{ + m_address.clear(); + m_port = 0; + m_connected = false; +} + + +const bool testSocket::isConnected() const +{ + return m_connected; +} + + +void testSocket::receive(vmime::string& buffer) +{ + buffer = m_inBuffer; + m_inBuffer.clear(); +} + + +void testSocket::send(const vmime::string& buffer) +{ + m_outBuffer += buffer; + + onDataReceived(); +} + + +const int testSocket::receiveRaw(char* buffer, const int count) +{ + const int n = std::min(count, static_cast (m_inBuffer.size())); + + std::copy(m_inBuffer.begin(), m_inBuffer.begin() + n, buffer); + m_inBuffer.erase(m_inBuffer.begin(), m_inBuffer.begin() + n); + + return n; +} + + +void testSocket::sendRaw(const char* buffer, const int count) +{ + send(vmime::string(buffer, count)); +} + + +void testSocket::localSend(const vmime::string& buffer) +{ + m_inBuffer += buffer; +} + + +void testSocket::localReceive(vmime::string& buffer) +{ + buffer = m_outBuffer; + m_outBuffer.clear(); +} + + +void testSocket::onDataReceived() +{ + // Override +} + + +void testSocket::onConnected() +{ + // Override +} + + +// lineBasedTestSocket + +void lineBasedTestSocket::onDataReceived() +{ + vmime::string chunk; + localReceive(chunk); + + m_buffer += chunk; + + vmime::string::size_type eol; + + while ((eol = m_buffer.find('\n')) != vmime::string::npos) + { + vmime::string line(std::string(m_buffer.begin(), m_buffer.begin() + eol)); + + if (!line.empty() && line[line.length() - 1] == '\r') + line.erase(line.end() - 1, line.end()); + + m_lines.push_back(line); + m_buffer.erase(m_buffer.begin(), m_buffer.begin() + eol + 1); + } + + while (!m_lines.empty()) + processCommand(); +} + + +const vmime::string lineBasedTestSocket::getNextLine() +{ + const vmime::string line = m_lines.front(); + m_lines.erase(m_lines.begin(), m_lines.begin() + 1); + return line; +} + +const bool lineBasedTestSocket::haveMoreLines() const +{ + return !m_lines.empty(); +} + + +// testTimeoutHandler + +testTimeoutHandler::testTimeoutHandler(const unsigned int delay) + : m_delay(delay), m_start(0) +{ +} + + +const bool testTimeoutHandler::isTimeOut() +{ + return (vmime::platformDependant::getHandler()->getUnixTime() - m_start) >= m_delay; +} + + +void testTimeoutHandler::resetTimeOut() +{ + m_start = vmime::platformDependant::getHandler()->getUnixTime(); +} + + +const bool testTimeoutHandler::handleTimeOut() +{ + return false; +} + + +// testTimeoutHandlerFactory : public vmime::net::timeoutHandlerFactory + +vmime::ref testTimeoutHandlerFactory::create() +{ + return vmime::create (); +} + diff --git a/tests/testUtils.hpp b/tests/testUtils.hpp index 9b8a5192..c2185a08 100644 --- a/tests/testUtils.hpp +++ b/tests/testUtils.hpp @@ -193,3 +193,113 @@ inline std::ostream& operator<<(std::ostream& os, const vmime::datetime& d) } + + +// Used to test network features. +// +// This works like a local pipe: client reads and writes data using receive() +// and send(). Server reads incoming data with localReceive() and sends data +// to client with localSend(). + +class testSocket : public vmime::net::socket +{ +public: + + void connect(const vmime::string& address, const vmime::port_t port); + void disconnect(); + + const bool isConnected() const; + + void receive(vmime::string& buffer); + void send(const vmime::string& buffer); + + const int receiveRaw(char* buffer, const int count); + void sendRaw(const char* buffer, const int count); + + /** Send data to client. + * + * @buffer data to send + */ + void localSend(const vmime::string& buffer); + + /** Receive data from client. + * + * @buffer buffer in which to store received data + */ + void localReceive(vmime::string& buffer); + +protected: + + /** Called when the client has sent some data. + */ + virtual void onDataReceived(); + + /** Called when the client has connected. + */ + virtual void onConnected(); + +private: + + vmime::string m_address; + vmime::port_t m_port; + bool m_connected; + + vmime::string m_inBuffer; + vmime::string m_outBuffer; +}; + + +template +class testSocketFactory : public vmime::net::socketFactory +{ +public: + + vmime::ref create() + { + return vmime::create (); + } +}; + + +class lineBasedTestSocket : public testSocket +{ +public: + + void onDataReceived(); + + const vmime::string getNextLine(); + const bool haveMoreLines() const; + + virtual void processCommand() = 0; + +private: + + std::vector m_lines; + std::string m_buffer; +}; + + +class testTimeoutHandler : public vmime::net::timeoutHandler +{ +public: + + testTimeoutHandler(const unsigned int delay = 3); + + const bool isTimeOut(); + void resetTimeOut(); + const bool handleTimeOut(); + +private: + + unsigned int m_delay; + unsigned int m_start; +}; + + +class testTimeoutHandlerFactory : public vmime::net::timeoutHandlerFactory +{ +public: + + vmime::ref create(); +}; +