From 3c1889fbd0d7b96aae16f3479ac8aae70a7e15f7 Mon Sep 17 00:00:00 2001
From: Florent Le Coz <louiz@louiz.org>
Date: Wed, 28 Oct 2015 19:13:53 +0100
Subject: Use Catch for our test suite

`make check` is also added to compile and run the tests
Catch is fetched with cmake automatically into the build directory when needed
---
 CMakeLists.txt                 |  22 +-
 scripts/build_and_run_tests.sh |   2 +-
 src/test.cpp                   | 551 -----------------------------------------
 tests/colors.cpp               |  54 ++++
 tests/config.cpp               |  25 ++
 tests/database.cpp             |  30 +++
 tests/dns.cpp                  |  83 +++++++
 tests/encoding.cpp             |  56 +++++
 tests/iid.cpp                  | 122 +++++++++
 tests/jid.cpp                  |  39 +++
 tests/test.cpp                 |   2 +
 tests/timed_events.cpp         |  62 +++++
 tests/utils.cpp                |  72 ++++++
 tests/uuid.cpp                 |  13 +
 tests/xmpp.cpp                 |  47 ++++
 15 files changed, 625 insertions(+), 555 deletions(-)
 delete mode 100644 src/test.cpp
 create mode 100644 tests/colors.cpp
 create mode 100644 tests/config.cpp
 create mode 100644 tests/database.cpp
 create mode 100644 tests/dns.cpp
 create mode 100644 tests/encoding.cpp
 create mode 100644 tests/iid.cpp
 create mode 100644 tests/jid.cpp
 create mode 100644 tests/test.cpp
 create mode 100644 tests/timed_events.cpp
 create mode 100644 tests/utils.cpp
 create mode 100644 tests/uuid.cpp
 create mode 100644 tests/xmpp.cpp

diff --git a/CMakeLists.txt b/CMakeLists.txt
index b16b906..960b5d1 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -145,12 +145,13 @@ if(SYSTEMD_FOUND)
   target_link_libraries(xmpp ${SYSTEMD_LIBRARIES})
 endif()
 
-
 #
 ## Tests
 #
+file(GLOB source_tests
+  tests/*.cpp)
 add_executable(test_suite EXCLUDE_FROM_ALL
-  src/test.cpp)
+  ${source_tests})
 target_link_libraries(test_suite
   xmpplib
   xmpp
@@ -160,11 +161,26 @@ target_link_libraries(test_suite
   config
   logger
   network)
-
 if(USE_DATABASE)
   target_link_libraries(test_suite
   database)
 endif()
+include(ExternalProject)
+ExternalProject_Add(catch
+  GIT_REPOSITORY "https://github.com/philsquared/Catch.git"
+  PREFIX "external"
+  UPDATE_COMMAND ""
+  CONFIGURE_COMMAND ""
+  BUILD_COMMAND ""
+  INSTALL_COMMAND ""
+)
+ExternalProject_Get_Property(catch SOURCE_DIR)
+target_include_directories(test_suite
+  PUBLIC "${SOURCE_DIR}/include/"
+)
+add_dependencies(test_suite catch)
+add_custom_target(check COMMAND "test_suite"
+  DEPENDS test_suite)
 
 #
 ## Install target
diff --git a/scripts/build_and_run_tests.sh b/scripts/build_and_run_tests.sh
index 69a27fd..738ef52 100755
--- a/scripts/build_and_run_tests.sh
+++ b/scripts/build_and_run_tests.sh
@@ -4,4 +4,4 @@ set -e -x
 
 cmake .. $@
 make -j$(nproc) biboumi test_suite
-./test_suite
+make -j$(nproc) check
diff --git a/src/test.cpp b/src/test.cpp
deleted file mode 100644
index 14ba929..0000000
--- a/src/test.cpp
+++ /dev/null
@@ -1,551 +0,0 @@
-/**
- * Just a very simple test suite, by hand, using assert()
- */
-
-#include <xmpp/xmpp_component.hpp>
-#include <network/dns_handler.hpp>
-#include <utils/timed_events.hpp>
-#include <database/database.hpp>
-#include <network/resolver.hpp>
-#include <utils/encoding.hpp>
-#include <network/poller.hpp>
-#include <logger/logger.hpp>
-#include <config/config.hpp>
-#include <bridge/colors.hpp>
-#include <utils/tolower.hpp>
-#include <utils/revstr.hpp>
-#include <irc/irc_user.hpp>
-#include <utils/string.hpp>
-#include <utils/split.hpp>
-#include <utils/xdg.hpp>
-#include <xmpp/jid.hpp>
-#include <irc/iid.hpp>
-#include <unistd.h>
-
-#include <thread>
-
-#undef NDEBUG
-#include <assert.h>
-
-using namespace std::chrono_literals;
-
-static const std::string color("");
-static const std::string success_color("");
-static const std::string reset("");
-
-int main()
-{
-
-  /**
-   * Config
-   */
-  std::cout << color << "Testing config…" << reset << std::endl;
-  Config::filename = "test.cfg";
-  Config::file_must_exist = false;
-  Config::set("coucou", "bonjour", true);
-  Config::close();
-
-  bool error = false;
-  try
-    {
-      Config::file_must_exist = true;
-      assert(Config::get("coucou", "") == "bonjour");
-      assert(Config::get("does not exist", "default") == "default");
-      Config::close();
-    }
-  catch (const std::ios::failure& e)
-    {
-      error = true;
-    }
-  assert(error == false);
-
-  Config::set("log_level", "2");
-  Config::set("log_file", "");
-
-  std::cout << color << "Testing logging…" << reset << std::endl;
-  log_debug("If you see this, the test FAILED.");
-  log_info("If you see this, the test FAILED.");
-  log_warning("You must see this message. And the next one too.");
-  log_error("It’s not an error, don’t worry, the test passed.");
-
-
-  /**
-   * Timed events
-   */
-  std::cout << color << "Testing timed events…" << reset << std::endl;
-  // No event.
-  assert(TimedEventsManager::instance().get_timeout() == utils::no_timeout);
-  assert(TimedEventsManager::instance().execute_expired_events() == 0);
-
-  // Add a single event
-  TimedEventsManager::instance().add_event(TimedEvent(std::chrono::steady_clock::now() + 50ms, [](){ std::cout << "Timeout expired" << std::endl; }));
-  // The event should not yet be expired
-  assert(TimedEventsManager::instance().get_timeout() > 0ms);
-  assert(TimedEventsManager::instance().execute_expired_events() == 0);
-  std::chrono::milliseconds timoute = TimedEventsManager::instance().get_timeout();
-  std::cout << "Sleeping for " << timoute.count() << "ms" << std::endl;
-  std::this_thread::sleep_for(timoute);
-
-  // Event is now expired
-  assert(TimedEventsManager::instance().execute_expired_events() == 1);
-  assert(TimedEventsManager::instance().get_timeout() == utils::no_timeout);
-
-  // Test canceling events
-  TimedEventsManager::instance().add_event(TimedEvent(std::chrono::steady_clock::now() + 100ms, [](){ }, "un"));
-  TimedEventsManager::instance().add_event(TimedEvent(std::chrono::steady_clock::now() + 200ms, [](){ }, "deux"));
-  TimedEventsManager::instance().add_event(TimedEvent(std::chrono::steady_clock::now() + 300ms, [](){ }, "deux"));
-  assert(TimedEventsManager::instance().get_timeout() > 0ms);
-  assert(TimedEventsManager::instance().size() == 3);
-  assert(TimedEventsManager::instance().cancel("un") == 1);
-  assert(TimedEventsManager::instance().size() == 2);
-  assert(TimedEventsManager::instance().cancel("deux") == 2);
-  assert(TimedEventsManager::instance().get_timeout() == utils::no_timeout);
-
-  /**
-   * Encoding
-   */
-  std::cout << color << "Testing encoding…" << reset << std::endl;
-  const char* valid = "C̡͔͕̩͙̽ͫ̈́ͥ̿̆ͧ̚r̸̩̘͍̻͖̆͆͛͊̉̕͡o͇͈̳̤̱̊̈͢q̻͍̦̮͕ͥͬͬ̽ͭ͌̾ͅǔ͉͕͇͚̙͉̭͉̇̽ȇ͈̮̼͍͔ͣ͊͞͝ͅ ͫ̾ͪ̓ͥ̆̋̔҉̢̦̠͈͔̖̲̯̦ụ̶̯͐̃̋ͮ͆͝n̬̱̭͇̻̱̰̖̤̏͛̏̿̑͟ë́͐҉̸̥̪͕̹̻̙͉̰ ̹̼̱̦̥ͩ͑̈́͑͝ͅt͍̥͈̹̝ͣ̃̔̈̔ͧ̕͝ḙ̸̖̟̙͙ͪ͢ų̯̞̼̲͓̻̞͛̃̀́b̮̰̗̩̰̊̆͗̾̎̆ͯ͌͝.̗̙͎̦ͫ̈́ͥ͌̈̓ͬ";
-  assert(utils::is_valid_utf8(valid));
-  const char* invalid = "\xF0\x0F";
-  assert(utils::is_valid_utf8(invalid) == false);
-  const char* invalid2 = "\xFE\xFE\xFF\xFF";
-  assert(utils::is_valid_utf8(invalid2) == false);
-
-  std::string in = "Biboumi ╯°□°)╯︵ ┻━┻";
-  std::cout << in << std::endl;
-  assert(utils::is_valid_utf8(in.c_str()) == true);
-  std::string res = utils::convert_to_utf8(in, "UTF-8");
-  assert(utils::is_valid_utf8(res.c_str()) == true && res == in);
-
-  std::string original_utf8("couc¥ou");
-  std::string original_latin1("couc\xa5ou");
-
-  // When converting back to utf-8
-  std::string from_latin1 = utils::convert_to_utf8(original_latin1.c_str(), "ISO-8859-1");
-  assert(from_latin1 == original_utf8);
-
-  // Check the behaviour when the decoding fails (here because we provide a
-  // wrong charset)
-  std::string from_ascii = utils::convert_to_utf8(original_latin1, "US-ASCII");
-  assert(from_ascii == "couc�ou");
-  std::cout << from_ascii << std::endl;
-
-  std::string without_ctrl_char("𤭢€¢$");
-  assert(utils::remove_invalid_xml_chars(without_ctrl_char) == without_ctrl_char);
-  assert(utils::remove_invalid_xml_chars(in) == in);
-  assert(utils::remove_invalid_xml_chars("\acouco\u0008u\uFFFEt\uFFFFe\r\n♥") == "coucoute\r\n♥");
-
-  /**
-   * Id generation
-   */
-  std::cout << color << "Testing id generation…" << reset << std::endl;
-  const std::string first_uuid = XmppComponent::next_id();
-  const std::string second_uuid = XmppComponent::next_id();
-  std::cout << first_uuid << std::endl;
-  std::cout << second_uuid << std::endl;
-  assert(first_uuid.size() == 36);
-  assert(second_uuid.size() == 36);
-  assert(first_uuid != second_uuid);
-
-  /**
-   * Utils
-   */
-  std::cout << color << "Testing utils…" << reset << std::endl;
-  std::vector<std::string> splitted = utils::split("a::a", ':', false);
-  assert(splitted.size() == 2);
-  splitted = utils::split("a::a", ':', true);
-  assert(splitted.size() == 3);
-  assert(splitted[0] == "a");
-  assert(splitted[1] == "");
-  assert(splitted[2] == "a");
-  splitted = utils::split("\na", '\n', true);
-  assert(splitted.size() == 2);
-  assert(splitted[0] == "");
-  assert(splitted[1] == "a");
-
-  const std::string lowercase = utils::tolower("CoUcOu LeS CoPaiNs ♥");
-  std::cout << lowercase << std::endl;
-  assert(lowercase == "coucou les copains ♥");
-
-  const std::string ltr = "coucou";
-  assert(utils::revstr(ltr) == "uocuoc");
-
-  assert(to_bool("true"));
-  assert(!to_bool("trou"));
-  assert(to_bool("1"));
-  assert(!to_bool("0"));
-  assert(!to_bool("-1"));
-  assert(!to_bool("false"));
-
-  /**
-   * XML parsing
-   */
-  std::cout << color << "Testing XML parsing…" << reset << std::endl;
-  XmppParser xml;
-  const std::string doc = "<stream xmlns='stream_ns'><stanza b='c'>inner<child1><grandchild/></child1><child2 xmlns='child2_ns'/>tail</stanza></stream>";
-  auto check_stanza = [](const Stanza& stanza)
-    {
-      assert(stanza.get_name() == "stanza");
-      assert(stanza.get_tag("xmlns") == "stream_ns");
-      assert(stanza.get_tag("b") == "c");
-      assert(stanza.get_inner() == "inner");
-      assert(stanza.get_tail() == "");
-      assert(stanza.get_child("child1", "stream_ns") != nullptr);
-      assert(stanza.get_child("child2", "stream_ns") == nullptr);
-      assert(stanza.get_child("child2", "child2_ns") != nullptr);
-      assert(stanza.get_child("child2", "child2_ns")->get_tail() == "tail");
-    };
-  xml.add_stanza_callback([check_stanza](const Stanza& stanza)
-      {
-        std::cout << stanza.to_string() << std::endl;
-        check_stanza(stanza);
-        // Do the same checks on a copy of that stanza.
-        Stanza copy(stanza);
-        check_stanza(copy);
-      });
-  xml.feed(doc.data(), doc.size(), true);
-
-  const std::string doc2 = "<stream xmlns='s'><stanza>coucou\r\n\a</stanza></stream>";
-  xml.add_stanza_callback([](const Stanza& stanza)
-      {
-        std::cout << stanza.to_string() << std::endl;
-        assert(stanza.get_inner() == "coucou\r\n");
-      });
-  xml.feed(doc2.data(), doc.size(), true);
-
-  /**
-   * XML escape/escape
-   */
-  std::cout << color << "Testing XML escaping…" << reset << std::endl;
-  const std::string unescaped = "'coucou'<cc>/&\"gaga\"";
-  assert(xml_escape(unescaped) == "&apos;coucou&apos;&lt;cc&gt;/&amp;&quot;gaga&quot;");
-  assert(xml_unescape(xml_escape(unescaped)) == unescaped);
-
-  /**
-   * Irc user parsing
-   */
-  const std::map<char, char> prefixes{{'!', 'a'}, {'@', 'o'}};
-
-  IrcUser user1("!nick!~some@host.bla", prefixes);
-  assert(user1.nick == "nick");
-  assert(user1.host == "~some@host.bla");
-  assert(user1.modes.size() == 1);
-  assert(user1.modes.find('a') != user1.modes.end());
-  IrcUser user2("coucou!~other@host.bla", prefixes);
-  assert(user2.nick == "coucou");
-  assert(user2.host == "~other@host.bla");
-  assert(user2.modes.empty());
-  assert(user2.modes.find('a') == user2.modes.end());
-
-  /**
-   * Colors conversion
-   */
-  std::cout << color << "Testing IRC colors conversion…" << reset << std::endl;
-  std::unique_ptr<XmlNode> xhtml;
-  std::string cleaned_up;
-
-  std::tie(cleaned_up, xhtml) = irc_format_to_xhtmlim("bold");
-  std::cout << xhtml->to_string() << std::endl;
-  assert(xhtml && xhtml->to_string() == "<body xmlns='http://www.w3.org/1999/xhtml'><span style='font-weight:bold;'>bold</span></body>");
-
-  std::tie(cleaned_up, xhtml) =
-    irc_format_to_xhtmlim("normalboldunder-and-boldbold normal"
-                          "5red,5default-on-red10,2cyan-on-blue");
-  assert(xhtml);
-  assert(xhtml->to_string() == "<body xmlns='http://www.w3.org/1999/xhtml'>normal<span style='font-weight:bold;'>bold</span><span style='font-weight:bold;text-decoration:underline;'>under-and-bold</span><span style='font-weight:bold;'>bold</span> normal<span style='color:red;'>red</span><span style='background-color:red;'>default-on-red</span><span style='color:cyan;background-color:blue;'>cyan-on-blue</span></body>");
-  assert(cleaned_up == "normalboldunder-and-boldbold normalreddefault-on-redcyan-on-blue");
-
-  std::tie(cleaned_up, xhtml) = irc_format_to_xhtmlim("normal");
-  assert(!xhtml && cleaned_up == "normal");
-
-  std::tie(cleaned_up, xhtml) = irc_format_to_xhtmlim("");
-  assert(xhtml && !xhtml->has_children() && cleaned_up.empty());
-
-  std::tie(cleaned_up, xhtml) = irc_format_to_xhtmlim(",a");
-  assert(xhtml && !xhtml->has_children() && cleaned_up == "a");
-
-  std::tie(cleaned_up, xhtml) = irc_format_to_xhtmlim(",");
-  assert(xhtml && !xhtml->has_children() && cleaned_up.empty());
-
-  std::tie(cleaned_up, xhtml) = irc_format_to_xhtmlim("[\x1D13dolphin-emu/dolphin\x1D] 03foo commented on #283 (Add support for the guide button to XInput): 02http://example.com");
-  assert(xhtml->to_string() == "<body xmlns='http://www.w3.org/1999/xhtml'>[<span style='font-style:italic;'/><span style='font-style:italic;color:lightmagenta;'>dolphin-emu/dolphin</span><span style='color:lightmagenta;'>] </span><span style='color:green;'>foo</span> commented on #283 (Add support for the guide button to XInput): <span style='text-decoration:underline;'/><span style='text-decoration:underline;color:blue;'>http://example.com</span><span style='text-decoration:underline;'/></body>");
-  assert(cleaned_up == "[dolphin-emu/dolphin] foo commented on #283 (Add support for the guide button to XInput): http://example.com");
-
-  std::tie(cleaned_up, xhtml) = irc_format_to_xhtmlim("0e46ab by 03Pierre Dindon [090|091|040] 02http://example.net/Ojrh4P media: avoid pop-in effect when loading thumbnails by specifying an explicit size");
-  assert(cleaned_up == "0e46ab by Pierre Dindon [0|1|0] http://example.net/Ojrh4P media: avoid pop-in effect when loading thumbnails by specifying an explicit size");
-  assert(xhtml->to_string() == "<body xmlns='http://www.w3.org/1999/xhtml'>0e46ab by <span style='color:green;'>Pierre Dindon</span> [<span style='color:lightgreen;'>0</span>|<span style='color:lightgreen;'>1</span>|<span style='color:indianred;'>0</span>] <span style='text-decoration:underline;'/><span style='text-decoration:underline;color:blue;'>http://example.net/Ojrh4P</span><span style='text-decoration:underline;'/> media: avoid pop-in effect when loading thumbnails by specifying an explicit size</body>");
-
-  std::tie(cleaned_up, xhtml) = irc_format_to_xhtmlim("test\ncoucou");
-  assert(cleaned_up == "test\ncoucou");
-  assert(xhtml->to_string() == "<body xmlns='http://www.w3.org/1999/xhtml'>test<br/>coucou</body>");
-
-  /**
-   * JID parsing
-   */
-  std::cout << color << "Testing JID parsing…" << reset << std::endl;
-  // Full JID
-  Jid jid1("♥@ツ.coucou/coucou@coucou/coucou");
-  std::cout << jid1.local << "@" << jid1.domain << "/" << jid1.resource << std::endl;
-  assert(jid1.local == "♥");
-  assert(jid1.domain == "ツ.coucou");
-  assert(jid1.resource == "coucou@coucou/coucou");
-
-  // Domain and resource
-  Jid jid2("ツ.coucou/coucou@coucou/coucou");
-  std::cout << jid2.local << "@" << jid2.domain << "/" << jid2.resource << std::endl;
-  assert(jid2.local == "");
-  assert(jid2.domain == "ツ.coucou");
-  assert(jid2.resource == "coucou@coucou/coucou");
-
-  // Jidprep
-  const std::string badjid("~zigougou™@EpiK-7D9D1FDE.poez.io/Boujour/coucou/slt™");
-  const std::string correctjid = jidprep(badjid);
-  std::cout << correctjid << std::endl;
-#ifdef LIBIDN_FOUND
-  assert(correctjid == "~zigougoutm@epik-7d9d1fde.poez.io/Boujour/coucou/sltTM");
-  // Check that the cache does not break things when we prep the same string
-  // multiple times
-  assert(jidprep(badjid) == "~zigougoutm@epik-7d9d1fde.poez.io/Boujour/coucou/sltTM");
-  assert(jidprep(badjid) == "~zigougoutm@epik-7d9d1fde.poez.io/Boujour/coucou/sltTM");
-
-  const std::string badjid2("Zigougou@poez.io");
-  const std::string correctjid2 = jidprep(badjid2);
-  std::cout << correctjid2 << std::endl;
-  assert(correctjid2 == "zigougou@poez.io");
-
-  const std::string crappy("~Bisous@7ea8beb1:c5fd2849:da9a048e:ip");
-  const std::string fixed_crappy = jidprep(crappy);
-  std::cout << fixed_crappy << std::endl;
-  assert(fixed_crappy == "~bisous@7ea8beb1-c5fd2849-da9a048e-ip");
-#else // Without libidn, jidprep always returns an empty string
-  assert(jidprep(badjid) == "");
-#endif
-
-  /**
-   * IID parsing
-   */
-  {
-    std::cout << color << "Testing IID parsing…" << reset << std::endl;
-    Iid iid1("foo!irc.example.org");
-    std::cout << std::to_string(iid1) << std::endl;
-    assert(std::to_string(iid1) == "foo!irc.example.org");
-    assert(iid1.get_local() == "foo");
-    assert(iid1.get_server() == "irc.example.org");
-    assert(!iid1.is_channel);
-    assert(iid1.is_user);
-
-    Iid iid2("#test%irc.example.org");
-    std::cout << std::to_string(iid2) << std::endl;
-    assert(std::to_string(iid2) == "#test%irc.example.org");
-    assert(iid2.get_local() == "#test");
-    assert(iid2.get_server() == "irc.example.org");
-    assert(iid2.is_channel);
-    assert(!iid2.is_user);
-
-    Iid iid3("%irc.example.org");
-    std::cout << std::to_string(iid3) << std::endl;
-    assert(std::to_string(iid3) == "%irc.example.org");
-    assert(iid3.get_local() == "");
-    assert(iid3.get_server() == "irc.example.org");
-    assert(iid3.is_channel);
-    assert(!iid3.is_user);
-
-    Iid iid4("irc.example.org");
-    std::cout << std::to_string(iid4) << std::endl;
-    assert(std::to_string(iid4) == "irc.example.org");
-    assert(iid4.get_local() == "");
-    assert(iid4.get_server() == "irc.example.org");
-    assert(!iid4.is_channel);
-    assert(!iid4.is_user);
-
-    Iid iid5("nick!");
-    std::cout << std::to_string(iid5) << std::endl;
-    assert(std::to_string(iid5) == "nick!");
-    assert(iid5.get_local() == "nick");
-    assert(iid5.get_server() == "");
-    assert(!iid5.is_channel);
-    assert(iid5.is_user);
-
-    Iid iid6("##channel%");
-    std::cout << std::to_string(iid6) << std::endl;
-    assert(std::to_string(iid6) == "##channel%");
-    assert(iid6.get_local() == "##channel");
-    assert(iid6.get_server() == "");
-    assert(iid6.is_channel);
-    assert(!iid6.is_user);
-  }
-
-  {
-    std::cout << color << "Testing IID parsing with a fixed server configured…" << reset << std::endl;
-    // Now do the same tests, but with a configured fixed_irc_server
-    Config::set("fixed_irc_server", "fixed.example.com", false);
-
-    Iid iid1("foo!irc.example.org");
-    std::cout << std::to_string(iid1) << std::endl;
-    assert(std::to_string(iid1) == "foo!");
-    assert(iid1.get_local() == "foo");
-    assert(iid1.get_server() == "fixed.example.com");
-    assert(!iid1.is_channel);
-    assert(iid1.is_user);
-
-    Iid iid2("#test%irc.example.org");
-    std::cout << std::to_string(iid2) << std::endl;
-    assert(std::to_string(iid2) == "#test%irc.example.org");
-    assert(iid2.get_local() == "#test%irc.example.org");
-    assert(iid2.get_server() == "fixed.example.com");
-    assert(iid2.is_channel);
-    assert(!iid2.is_user);
-
-    // Note that it is impossible to adress the IRC server directly, or to
-    // use the virtual channel, in that mode
-
-    // Iid iid3("%irc.example.org");
-    // Iid iid4("irc.example.org");
-
-    Iid iid5("nick!");
-    std::cout << std::to_string(iid5) << std::endl;
-    assert(std::to_string(iid5) == "nick!");
-    assert(iid5.get_local() == "nick");
-    assert(iid5.get_server() == "fixed.example.com");
-    assert(!iid5.is_channel);
-    assert(iid5.is_user);
-
-    Iid iid6("##channel%");
-    std::cout << std::to_string(iid6) << std::endl;
-    assert(std::to_string(iid6) == "##channel%");
-    assert(iid6.get_local() == "##channel%");
-    assert(iid6.get_server() == "fixed.example.com");
-    assert(iid6.is_channel);
-    assert(!iid6.is_user);
-  }
-#ifdef USE_DATABASE
-  {
-    std::cout << color << "Testing the Database…" << reset << std::endl;
-    // Remove any potential existing db
-    unlink("./test.db");
-    Config::set("db_name", "test.db");
-    Database::set_verbose(true);
-    auto o = Database::get_irc_server_options("zouzou@example.com", "irc.example.com");
-    o.update();
-    auto a = Database::get_irc_server_options("zouzou@example.com", "irc.example.com");
-    auto b = Database::get_irc_server_options("moumou@example.com", "irc.example.com");
-
-    // b does not yet exist in the db, the object is created but not yet
-    // inserted
-    assert(1 == Database::count<db::IrcServerOptions>());
-
-    b.update();
-    assert(2 == Database::count<db::IrcServerOptions>());
-
-    assert(b.pass == "");
-    assert(b.pass.value() == "");
-  }
-#endif
-  {
-    std::cout << color << "Testing the xdg_path function…" << reset << std::endl;
-    std::string res;
-
-    ::unsetenv("XDG_CONFIG_HOME");
-    ::unsetenv("HOME");
-    res = xdg_config_path("coucou.txt");
-    std::cout << res << std::endl;
-    assert(res == "coucou.txt");
-
-    ::setenv("HOME", "/home/user", 1);
-    res = xdg_config_path("coucou.txt");
-    std::cout << res << std::endl;
-    assert(res == "/home/user/.config/biboumi/coucou.txt");
-
-    ::setenv("XDG_CONFIG_HOME", "/some_weird_dir", 1);
-    res = xdg_config_path("coucou.txt");
-    std::cout << res << std::endl;
-    assert(res == "/some_weird_dir/biboumi/coucou.txt");
-
-    ::setenv("XDG_DATA_HOME", "/datadir", 1);
-    res = xdg_data_path("bonjour.txt");
-    std::cout << res << std::endl;
-    assert(res == "/datadir/biboumi/bonjour.txt");
-  }
-
-
-  {
-    std::cout << color << "Testing the DNS resolver…" << reset << std::endl;
-
-    Resolver resolver;
-
-    /**
-     * If we are using cares, we need to run a poller loop until each
-     * resolution is finished. Without cares we get the result before
-     * resolve() returns because it’s blocking.
-     */
-#ifdef CARES_FOUND
-    auto p = std::make_shared<Poller>();
-
-    const auto loop = [&p]()
-      {
-        do
-          {
-            DNSHandler::instance.watch_dns_sockets(p);
-          }
-        while (p->poll(utils::no_timeout) != -1);
-      };
-#else
-    // We don’t need to do anything if we are not using cares.
-    const auto loop = [](){};
-#endif
-
-    std::string hostname;
-    std::string port = "6667";
-
-    bool success = true;
-
-    auto error_cb = [&success, &hostname, &port](const char* msg)
-      {
-        std::cout << "Failed to resolve " << hostname << ":" << port << ": " << msg << std::endl;
-        success = false;
-      };
-    auto success_cb = [&success, &hostname, &port](const struct addrinfo* addr)
-      {
-        std::cout << "Successfully resolved " << hostname << ":" << port << ": " << addr_to_string(addr) << std::endl;
-        success = true;
-      };
-
-    hostname = "example.com";
-    resolver.resolve(hostname, port,
-                     success_cb, error_cb);
-    loop();
-    assert(success);
-
-    hostname = "this.should.fail.because.it.is..misformatted";
-    resolver.resolve(hostname, port,
-                     success_cb, error_cb);
-    loop();
-    assert(!success);
-
-    hostname = "this.should.fail.because.it.is.does.not.exist.invalid";
-    resolver.resolve(hostname, port,
-                     success_cb, error_cb);
-    loop();
-    assert(!success);
-
-    hostname = "localhost6";
-    resolver.resolve(hostname, port,
-                     success_cb, error_cb);
-    loop();
-    assert(success);
-
-    hostname = "localhost";
-    resolver.resolve(hostname, port,
-                     success_cb, error_cb);
-    loop();
-    assert(success);
-
-#ifdef CARES_FOUND
-    DNSHandler::instance.destroy();
-#endif
-  }
-
-  std::cout << success_color << "All test passed successfully!" << reset << std::endl;
-  return 0;
-}
diff --git a/tests/colors.cpp b/tests/colors.cpp
new file mode 100644
index 0000000..bf52989
--- /dev/null
+++ b/tests/colors.cpp
@@ -0,0 +1,54 @@
+#include "catch.hpp"
+
+#include <bridge/colors.hpp>
+#include <xmpp/xmpp_stanza.hpp>
+
+#include <memory>
+
+TEST_CASE("IRC colors parsing")
+{
+  std::unique_ptr<XmlNode> xhtml;
+  std::string cleaned_up;
+
+  std::tie(cleaned_up, xhtml) = irc_format_to_xhtmlim("bold");
+  CHECK(xhtml);
+  CHECK(xhtml->to_string() == "<body xmlns='http://www.w3.org/1999/xhtml'><span style='font-weight:bold;'>bold</span></body>");
+
+  std::tie(cleaned_up, xhtml) =
+    irc_format_to_xhtmlim("normalboldunder-and-boldbold normal"
+                          "5red,5default-on-red10,2cyan-on-blue");
+  CHECK(xhtml);
+  CHECK(xhtml->to_string() == "<body xmlns='http://www.w3.org/1999/xhtml'>normal<span style='font-weight:bold;'>bold</span><span style='font-weight:bold;text-decoration:underline;'>under-and-bold</span><span style='font-weight:bold;'>bold</span> normal<span style='color:red;'>red</span><span style='background-color:red;'>default-on-red</span><span style='color:cyan;background-color:blue;'>cyan-on-blue</span></body>");
+  CHECK(cleaned_up == "normalboldunder-and-boldbold normalreddefault-on-redcyan-on-blue");
+
+  std::tie(cleaned_up, xhtml) = irc_format_to_xhtmlim("normal");
+  CHECK_FALSE(xhtml);
+  CHECK(cleaned_up == "normal");
+
+  std::tie(cleaned_up, xhtml) = irc_format_to_xhtmlim("");
+  CHECK(xhtml);
+  CHECK(!xhtml->has_children());
+  CHECK(cleaned_up.empty());
+
+  std::tie(cleaned_up, xhtml) = irc_format_to_xhtmlim(",a");
+  CHECK(xhtml);
+  CHECK(!xhtml->has_children());
+  CHECK(cleaned_up == "a");
+
+  std::tie(cleaned_up, xhtml) = irc_format_to_xhtmlim(",");
+  CHECK(xhtml);
+  CHECK(!xhtml->has_children());
+  CHECK(cleaned_up.empty());
+
+  std::tie(cleaned_up, xhtml) = irc_format_to_xhtmlim("[\x1D13dolphin-emu/dolphin\x1D] 03foo commented on #283 (Add support for the guide button to XInput): 02http://example.com");
+  CHECK(xhtml->to_string() == "<body xmlns='http://www.w3.org/1999/xhtml'>[<span style='font-style:italic;'/><span style='font-style:italic;color:lightmagenta;'>dolphin-emu/dolphin</span><span style='color:lightmagenta;'>] </span><span style='color:green;'>foo</span> commented on #283 (Add support for the guide button to XInput): <span style='text-decoration:underline;'/><span style='text-decoration:underline;color:blue;'>http://example.com</span><span style='text-decoration:underline;'/></body>");
+  CHECK(cleaned_up == "[dolphin-emu/dolphin] foo commented on #283 (Add support for the guide button to XInput): http://example.com");
+
+  std::tie(cleaned_up, xhtml) = irc_format_to_xhtmlim("0e46ab by 03Pierre Dindon [090|091|040] 02http://example.net/Ojrh4P media: avoid pop-in effect when loading thumbnails by specifying an explicit size");
+  CHECK(cleaned_up == "0e46ab by Pierre Dindon [0|1|0] http://example.net/Ojrh4P media: avoid pop-in effect when loading thumbnails by specifying an explicit size");
+  CHECK(xhtml->to_string() == "<body xmlns='http://www.w3.org/1999/xhtml'>0e46ab by <span style='color:green;'>Pierre Dindon</span> [<span style='color:lightgreen;'>0</span>|<span style='color:lightgreen;'>1</span>|<span style='color:indianred;'>0</span>] <span style='text-decoration:underline;'/><span style='text-decoration:underline;color:blue;'>http://example.net/Ojrh4P</span><span style='text-decoration:underline;'/> media: avoid pop-in effect when loading thumbnails by specifying an explicit size</body>");
+
+  std::tie(cleaned_up, xhtml) = irc_format_to_xhtmlim("test\ncoucou");
+  CHECK(cleaned_up == "test\ncoucou");
+  CHECK(xhtml->to_string() == "<body xmlns='http://www.w3.org/1999/xhtml'>test<br/>coucou</body>");
+}
diff --git a/tests/config.cpp b/tests/config.cpp
new file mode 100644
index 0000000..1419f0b
--- /dev/null
+++ b/tests/config.cpp
@@ -0,0 +1,25 @@
+#include "catch.hpp"
+
+#include <config/config.hpp>
+
+TEST_CASE("Config basic")
+{
+  Config::filename = "test.cfg";
+  Config::file_must_exist = false;
+  Config::set("coucou", "bonjour", true);
+  Config::close();
+
+  bool error = false;
+  try
+    {
+      Config::file_must_exist = true;
+      CHECK(Config::get("coucou", "") == "bonjour");
+      CHECK(Config::get("does not exist", "default") == "default");
+      Config::close();
+    }
+  catch (const std::ios::failure& e)
+    {
+      error = true;
+    }
+  CHECK_FALSE(error);
+}
diff --git a/tests/database.cpp b/tests/database.cpp
new file mode 100644
index 0000000..fd9e873
--- /dev/null
+++ b/tests/database.cpp
@@ -0,0 +1,30 @@
+#include "catch.hpp"
+
+#include <database/database.hpp>
+
+#include <unistd.h>
+#include <config/config.hpp>
+
+TEST_CASE("Database")
+{
+#ifdef USE_DATABASE
+  // Remove any potential existing db
+  ::unlink("./test.db");
+  Config::set("db_name", "test.db");
+  Database::set_verbose(false);
+  auto o = Database::get_irc_server_options("zouzou@example.com", "irc.example.com");
+  o.update();
+  auto a = Database::get_irc_server_options("zouzou@example.com", "irc.example.com");
+  auto b = Database::get_irc_server_options("moumou@example.com", "irc.example.com");
+
+  // b does not yet exist in the db, the object is created but not yet
+  // inserted
+  CHECK(1 == Database::count<db::IrcServerOptions>());
+
+  b.update();
+  CHECK(2 == Database::count<db::IrcServerOptions>());
+
+  CHECK(b.pass == "");
+  CHECK(b.pass.value() == "");
+#endif
+}
diff --git a/tests/dns.cpp b/tests/dns.cpp
new file mode 100644
index 0000000..e8cbc7f
--- /dev/null
+++ b/tests/dns.cpp
@@ -0,0 +1,83 @@
+#include "catch.hpp"
+
+#include <network/dns_handler.hpp>
+#include <network/resolver.hpp>
+#include <network/poller.hpp>
+
+#include <utils/timed_events.hpp>
+
+TEST_CASE("DNS resolver")
+{
+    Resolver resolver;
+
+    /**
+     * If we are using cares, we need to run a poller loop until each
+     * resolution is finished. Without cares we get the result before
+     * resolve() returns because it’s blocking.
+     */
+#ifdef CARES_FOUND
+    auto p = std::make_shared<Poller>();
+
+    const auto loop = [&p]()
+      {
+        do
+          {
+            DNSHandler::instance.watch_dns_sockets(p);
+          }
+        while (p->poll(utils::no_timeout) != -1);
+      };
+#else
+    // We don’t need to do anything if we are not using cares.
+    const auto loop = [](){};
+#endif
+
+    std::string hostname;
+    std::string port = "6667";
+
+    bool success = true;
+
+    auto error_cb = [&success, &hostname, &port](const char* msg)
+      {
+        INFO("Failed to resolve " << hostname << ":" << port << ": " << msg);
+        success = false;
+      };
+    auto success_cb = [&success, &hostname, &port](const struct addrinfo* addr)
+      {
+        INFO("Successfully resolved " << hostname << ":" << port << ": " << addr_to_string(addr));
+        success = true;
+      };
+
+    hostname = "example.com";
+    resolver.resolve(hostname, port,
+                     success_cb, error_cb);
+    loop();
+    CHECK(success);
+
+    hostname = "this.should.fail.because.it.is..misformatted";
+    resolver.resolve(hostname, port,
+                     success_cb, error_cb);
+    loop();
+    CHECK(!success);
+
+    hostname = "this.should.fail.because.it.is.does.not.exist.invalid";
+    resolver.resolve(hostname, port,
+                     success_cb, error_cb);
+    loop();
+    CHECK(!success);
+
+    hostname = "localhost6";
+    resolver.resolve(hostname, port,
+                     success_cb, error_cb);
+    loop();
+    CHECK(success);
+
+    hostname = "localhost";
+    resolver.resolve(hostname, port,
+                     success_cb, error_cb);
+    loop();
+    CHECK(success);
+
+#ifdef CARES_FOUND
+    DNSHandler::instance.destroy();
+#endif
+}
diff --git a/tests/encoding.cpp b/tests/encoding.cpp
new file mode 100644
index 0000000..389cf23
--- /dev/null
+++ b/tests/encoding.cpp
@@ -0,0 +1,56 @@
+#include "catch.hpp"
+
+#include <utils/encoding.hpp>
+
+
+TEST_CASE("UTF-8 validation")
+{
+  const char* valid = "C̡͔͕̩͙̽ͫ̈́ͥ̿̆ͧ̚r̸̩̘͍̻͖̆͆͛͊̉̕͡o͇͈̳̤̱̊̈͢q̻͍̦̮͕ͥͬͬ̽ͭ͌̾ͅǔ͉͕͇͚̙͉̭͉̇̽ȇ͈̮̼͍͔ͣ͊͞͝ͅ ͫ̾ͪ̓ͥ̆̋̔҉̢̦̠͈͔̖̲̯̦ụ̶̯͐̃̋ͮ͆͝n̬̱̭͇̻̱̰̖̤̏͛̏̿̑͟ë́͐҉̸̥̪͕̹̻̙͉̰ ̹̼̱̦̥ͩ͑̈́͑͝ͅt͍̥͈̹̝ͣ̃̔̈̔ͧ̕͝ḙ̸̖̟̙͙ͪ͢ų̯̞̼̲͓̻̞͛̃̀́b̮̰̗̩̰̊̆͗̾̎̆ͯ͌͝.̗̙͎̦ͫ̈́ͥ͌̈̓ͬ";
+  CHECK(utils::is_valid_utf8(valid));
+  CHECK_FALSE(utils::is_valid_utf8("\xF0\x0F"));
+  CHECK_FALSE(utils::is_valid_utf8("\xFE\xFE\xFF\xFF"));
+
+  std::string in = "Biboumi ╯°□°)╯︵ ┻━┻";
+  INFO(in);
+  CHECK(utils::is_valid_utf8(in.data()));
+}
+
+TEST_CASE("UTF-8 conversion")
+{
+  std::string in = "Biboumi ╯°□°)╯︵ ┻━┻";
+  REQUIRE(utils::is_valid_utf8(in.data()));
+
+  SECTION("Converting UTF-8 to UTF-8 should return the same string")
+    {
+      std::string res = utils::convert_to_utf8(in, "UTF-8");
+      CHECK(utils::is_valid_utf8(res.c_str()) == true);
+      CHECK(res == in);
+    }
+
+  SECTION("Testing latin-1 conversion")
+    {
+      std::string original_utf8("couc¥ou");
+      std::string original_latin1("couc\xa5ou");
+
+      SECTION("Convert proper latin-1 to UTF-8")
+        {
+          std::string from_latin1 = utils::convert_to_utf8(original_latin1.c_str(), "ISO-8859-1");
+          CHECK(from_latin1 == original_utf8);
+        }
+      SECTION("Check the behaviour when the decoding fails (here because we provide a wrong charset)")
+        {
+          std::string from_ascii = utils::convert_to_utf8(original_latin1, "US-ASCII");
+          CHECK(from_ascii == "couc�ou");
+        }
+    }
+}
+
+TEST_CASE("Remove invalid XML chars")
+{
+  std::string without_ctrl_char("𤭢€¢$");
+  std::string in = "Biboumi ╯°□°)╯︵ ┻━┻";
+  INFO(in);
+  CHECK(utils::remove_invalid_xml_chars(without_ctrl_char) == without_ctrl_char);
+  CHECK(utils::remove_invalid_xml_chars(in) == in);
+  CHECK(utils::remove_invalid_xml_chars("\acouco\u0008u\uFFFEt\uFFFFe\r\n♥") == "coucoute\r\n♥");
+}
diff --git a/tests/iid.cpp b/tests/iid.cpp
new file mode 100644
index 0000000..a90c208
--- /dev/null
+++ b/tests/iid.cpp
@@ -0,0 +1,122 @@
+#include "catch.hpp"
+
+#include <irc/iid.hpp>
+#include <irc/irc_user.hpp>
+
+#include <config/config.hpp>
+
+TEST_CASE("Irc user parsing")
+{
+  const std::map<char, char> prefixes{{'!', 'a'}, {'@', 'o'}};
+
+  IrcUser user1("!nick!~some@host.bla", prefixes);
+  CHECK(user1.nick == "nick");
+  CHECK(user1.host == "~some@host.bla");
+  CHECK(user1.modes.size() == 1);
+  CHECK(user1.modes.find('a') != user1.modes.end());
+
+  IrcUser user2("coucou!~other@host.bla", prefixes);
+  CHECK(user2.nick == "coucou");
+  CHECK(user2.host == "~other@host.bla");
+  CHECK(user2.modes.empty());
+  CHECK(user2.modes.find('a') == user2.modes.end());
+}
+
+/**
+ * Let Catch know how to display Iid objects
+ */
+namespace Catch
+{
+  template<>
+  struct StringMaker<Iid>
+  {
+    static std::string convert(const Iid& value)
+    {
+      return std::to_string(value);
+    }
+  };
+}
+
+TEST_CASE("Iid creation")
+{
+    Iid iid1("foo!irc.example.org");
+    CHECK(std::to_string(iid1) == "foo!irc.example.org");
+    CHECK(iid1.get_local() == "foo");
+    CHECK(iid1.get_server() == "irc.example.org");
+    CHECK(!iid1.is_channel);
+    CHECK(iid1.is_user);
+
+    Iid iid2("#test%irc.example.org");
+    CHECK(std::to_string(iid2) == "#test%irc.example.org");
+    CHECK(iid2.get_local() == "#test");
+    CHECK(iid2.get_server() == "irc.example.org");
+    CHECK(iid2.is_channel);
+    CHECK(!iid2.is_user);
+
+    Iid iid3("%irc.example.org");
+    CHECK(std::to_string(iid3) == "%irc.example.org");
+    CHECK(iid3.get_local() == "");
+    CHECK(iid3.get_server() == "irc.example.org");
+    CHECK(iid3.is_channel);
+    CHECK(!iid3.is_user);
+
+    Iid iid4("irc.example.org");
+    CHECK(std::to_string(iid4) == "irc.example.org");
+    CHECK(iid4.get_local() == "");
+    CHECK(iid4.get_server() == "irc.example.org");
+    CHECK(!iid4.is_channel);
+    CHECK(!iid4.is_user);
+
+    Iid iid5("nick!");
+    CHECK(std::to_string(iid5) == "nick!");
+    CHECK(iid5.get_local() == "nick");
+    CHECK(iid5.get_server() == "");
+    CHECK(!iid5.is_channel);
+    CHECK(iid5.is_user);
+
+    Iid iid6("##channel%");
+    CHECK(std::to_string(iid6) == "##channel%");
+    CHECK(iid6.get_local() == "##channel");
+    CHECK(iid6.get_server() == "");
+    CHECK(iid6.is_channel);
+    CHECK(!iid6.is_user);
+}
+
+TEST_CASE("Iid creation in fixed_server mode")
+{
+    Config::set("fixed_irc_server", "fixed.example.com", false);
+
+    Iid iid1("foo!irc.example.org");
+    CHECK(std::to_string(iid1) == "foo!");
+    CHECK(iid1.get_local() == "foo");
+    CHECK(iid1.get_server() == "fixed.example.com");
+    CHECK(!iid1.is_channel);
+    CHECK(iid1.is_user);
+
+    Iid iid2("#test%irc.example.org");
+    CHECK(std::to_string(iid2) == "#test%irc.example.org");
+    CHECK(iid2.get_local() == "#test%irc.example.org");
+    CHECK(iid2.get_server() == "fixed.example.com");
+    CHECK(iid2.is_channel);
+    CHECK(!iid2.is_user);
+
+    // Note that it is impossible to adress the IRC server directly, or to
+    // use the virtual channel, in that mode
+
+    // Iid iid3("%irc.example.org");
+    // Iid iid4("irc.example.org");
+
+    Iid iid5("nick!");
+    CHECK(std::to_string(iid5) == "nick!");
+    CHECK(iid5.get_local() == "nick");
+    CHECK(iid5.get_server() == "fixed.example.com");
+    CHECK(!iid5.is_channel);
+    CHECK(iid5.is_user);
+
+    Iid iid6("##channel%");
+    CHECK(std::to_string(iid6) == "##channel%");
+    CHECK(iid6.get_local() == "##channel%");
+    CHECK(iid6.get_server() == "fixed.example.com");
+    CHECK(iid6.is_channel);
+    CHECK(!iid6.is_user);
+}
diff --git a/tests/jid.cpp b/tests/jid.cpp
new file mode 100644
index 0000000..9015afd
--- /dev/null
+++ b/tests/jid.cpp
@@ -0,0 +1,39 @@
+#include "catch.hpp"
+
+#include <xmpp/jid.hpp>
+#include <louloulibs.h>
+
+TEST_CASE("Jid")
+{
+  Jid jid1("♥@ツ.coucou/coucou@coucou/coucou");
+  CHECK(jid1.local == "♥");
+  CHECK(jid1.domain == "ツ.coucou");
+  CHECK(jid1.resource == "coucou@coucou/coucou");
+
+  // Domain and resource
+  Jid jid2("ツ.coucou/coucou@coucou/coucou");
+  CHECK(jid2.local == "");
+  CHECK(jid2.domain == "ツ.coucou");
+  CHECK(jid2.resource == "coucou@coucou/coucou");
+
+  // Jidprep
+  const std::string badjid("~zigougou™@EpiK-7D9D1FDE.poez.io/Boujour/coucou/slt™");
+  const std::string correctjid = jidprep(badjid);
+#ifdef LIBIDN_FOUND
+  CHECK(correctjid == "~zigougoutm@epik-7d9d1fde.poez.io/Boujour/coucou/sltTM");
+  // Check that the cache does not break things when we prep the same string
+  // multiple times
+  CHECK(jidprep(badjid) == "~zigougoutm@epik-7d9d1fde.poez.io/Boujour/coucou/sltTM");
+  CHECK(jidprep(badjid) == "~zigougoutm@epik-7d9d1fde.poez.io/Boujour/coucou/sltTM");
+
+  const std::string badjid2("Zigougou@poez.io");
+  const std::string correctjid2 = jidprep(badjid2);
+  CHECK(correctjid2 == "zigougou@poez.io");
+
+  const std::string crappy("~Bisous@7ea8beb1:c5fd2849:da9a048e:ip");
+  const std::string fixed_crappy = jidprep(crappy);
+  CHECK(fixed_crappy == "~bisous@7ea8beb1-c5fd2849-da9a048e-ip");
+#else // Without libidn, jidprep always returns an empty string
+  CHECK(jidprep(badjid) == "");
+#endif
+}
diff --git a/tests/test.cpp b/tests/test.cpp
new file mode 100644
index 0000000..0c7c351
--- /dev/null
+++ b/tests/test.cpp
@@ -0,0 +1,2 @@
+#define CATCH_CONFIG_MAIN
+#include "catch.hpp"
diff --git a/tests/timed_events.cpp b/tests/timed_events.cpp
new file mode 100644
index 0000000..3844f3d
--- /dev/null
+++ b/tests/timed_events.cpp
@@ -0,0 +1,62 @@
+#include "catch.hpp"
+
+#include <utils/timed_events.hpp>
+
+/**
+ * Let Catch know how to display std::chrono::duration values
+ */
+namespace Catch
+{
+  template<typename Rep, typename Period> struct StringMaker<std::chrono::duration<Rep, Period>>
+  {
+    static std::string convert(const std::chrono::duration<Rep, Period>& value)
+    {
+      return  std::to_string(std::chrono::duration_cast<std::chrono::milliseconds>(value).count()) + "ms";
+    }
+  };
+}
+
+/**
+ * TODO, use a mock clock instead of relying on the real time with a sleep:
+ * it’s unreliable on heavy load.
+ */
+#include <thread>
+
+TEST_CASE("Test timed event expiration")
+{
+  SECTION("Check what happens when there is no events")
+    {
+      CHECK(TimedEventsManager::instance().get_timeout() == utils::no_timeout);
+      CHECK(TimedEventsManager::instance().execute_expired_events() == 0);
+    }
+
+  // Add a single event
+  TimedEventsManager::instance().add_event(TimedEvent(std::chrono::steady_clock::now() + 50ms, [](){}));
+
+  // The event should not yet be expired
+  CHECK(TimedEventsManager::instance().get_timeout() > 0ms);
+  CHECK(TimedEventsManager::instance().execute_expired_events() == 0);
+
+  std::chrono::milliseconds timoute = TimedEventsManager::instance().get_timeout();
+  INFO("Sleeping for " << timoute.count() << "ms");
+  std::this_thread::sleep_for(timoute);
+
+  // Event is now expired
+  CHECK(TimedEventsManager::instance().execute_expired_events() == 1);
+  CHECK(TimedEventsManager::instance().get_timeout() == utils::no_timeout);
+}
+
+TEST_CASE("Test timed event cancellation")
+{
+  auto now = std::chrono::steady_clock::now();
+  TimedEventsManager::instance().add_event(TimedEvent(now + 100ms, [](){ }, "un"));
+  TimedEventsManager::instance().add_event(TimedEvent(now + 200ms, [](){ }, "deux"));
+  TimedEventsManager::instance().add_event(TimedEvent(now + 300ms, [](){ }, "deux"));
+
+  CHECK(TimedEventsManager::instance().get_timeout() > 0ms);
+  CHECK(TimedEventsManager::instance().size() == 3);
+  CHECK(TimedEventsManager::instance().cancel("un") == 1);
+  CHECK(TimedEventsManager::instance().size() == 2);
+  CHECK(TimedEventsManager::instance().cancel("deux") == 2);
+  CHECK(TimedEventsManager::instance().get_timeout() == utils::no_timeout);
+}
diff --git a/tests/utils.cpp b/tests/utils.cpp
new file mode 100644
index 0000000..6e3c32a
--- /dev/null
+++ b/tests/utils.cpp
@@ -0,0 +1,72 @@
+#include "catch.hpp"
+
+#include <utils/tolower.hpp>
+#include <utils/revstr.hpp>
+#include <utils/string.hpp>
+#include <utils/split.hpp>
+#include <utils/xdg.hpp>
+
+TEST_CASE("String split")
+{
+  std::vector<std::string> splitted = utils::split("a::a", ':', false);
+  CHECK(splitted.size() == 2);
+  splitted = utils::split("a::a", ':', true);
+  CHECK(splitted.size() == 3);
+  CHECK(splitted[0] == "a");
+  CHECK(splitted[1] == "");
+  CHECK(splitted[2] == "a");
+  splitted = utils::split("\na", '\n', true);
+  CHECK(splitted.size() == 2);
+  CHECK(splitted[0] == "");
+  CHECK(splitted[1] == "a");
+}
+
+TEST_CASE("tolower")
+{
+  const std::string lowercase = utils::tolower("CoUcOu LeS CoPaiNs ♥");
+  CHECK(lowercase == "coucou les copains ♥");
+
+  const std::string ltr = "coucou";
+  CHECK(utils::revstr(ltr) == "uocuoc");
+}
+
+TEST_CASE("to_bool")
+{
+  CHECK(to_bool("true"));
+  CHECK(!to_bool("trou"));
+  CHECK(to_bool("1"));
+  CHECK(!to_bool("0"));
+  CHECK(!to_bool("-1"));
+  CHECK(!to_bool("false"));
+}
+
+TEST_CASE("xdg_*_path")
+{
+  ::unsetenv("XDG_CONFIG_HOME");
+  ::unsetenv("HOME");
+  std::string res;
+
+  SECTION("Without XDG_CONFIG_HOME nor HOME")
+    {
+      res = xdg_config_path("coucou.txt");
+      CHECK(res == "coucou.txt");
+    }
+  SECTION("With only HOME")
+    {
+      ::setenv("HOME", "/home/user", 1);
+      res = xdg_config_path("coucou.txt");
+      CHECK(res == "/home/user/.config/biboumi/coucou.txt");
+    }
+  SECTION("With only XDG_CONFIG_HOME")
+    {
+      ::setenv("XDG_CONFIG_HOME", "/some_weird_dir", 1);
+      res = xdg_config_path("coucou.txt");
+      CHECK(res == "/some_weird_dir/biboumi/coucou.txt");
+    }
+  SECTION("With XDG_DATA_HOME")
+    {
+      ::setenv("XDG_DATA_HOME", "/datadir", 1);
+      res = xdg_data_path("bonjour.txt");
+      CHECK(res == "/datadir/biboumi/bonjour.txt");
+    }
+}
diff --git a/tests/uuid.cpp b/tests/uuid.cpp
new file mode 100644
index 0000000..12c6c32
--- /dev/null
+++ b/tests/uuid.cpp
@@ -0,0 +1,13 @@
+#include "catch.hpp"
+
+#include <xmpp/xmpp_component.hpp>
+
+TEST_CASE("id generation")
+{
+  const std::string first_uuid = XmppComponent::next_id();
+  const std::string second_uuid = XmppComponent::next_id();
+
+  CHECK(first_uuid.size() == 36);
+  CHECK(second_uuid.size() == 36);
+  CHECK(first_uuid != second_uuid);
+}
diff --git a/tests/xmpp.cpp b/tests/xmpp.cpp
new file mode 100644
index 0000000..46ecd35
--- /dev/null
+++ b/tests/xmpp.cpp
@@ -0,0 +1,47 @@
+#include "catch.hpp"
+
+#include <xmpp/xmpp_parser.hpp>
+
+TEST_CASE("Test basic XML parsing")
+{
+  XmppParser xml;
+
+  const std::string doc = "<stream xmlns='stream_ns'><stanza b='c'>inner<child1><grandchild/></child1><child2 xmlns='child2_ns'/>tail</stanza></stream>";
+
+  auto check_stanza = [](const Stanza& stanza)
+    {
+      CHECK(stanza.get_name() == "stanza");
+      CHECK(stanza.get_tag("xmlns") == "stream_ns");
+      CHECK(stanza.get_tag("b") == "c");
+      CHECK(stanza.get_inner() == "inner");
+      CHECK(stanza.get_tail() == "");
+      CHECK(stanza.get_child("child1", "stream_ns") != nullptr);
+      CHECK(stanza.get_child("child2", "stream_ns") == nullptr);
+      CHECK(stanza.get_child("child2", "child2_ns") != nullptr);
+      CHECK(stanza.get_child("child2", "child2_ns")->get_tail() == "tail");
+    };
+  xml.add_stanza_callback([check_stanza](const Stanza& stanza)
+      {
+        check_stanza(stanza);
+        // Do the same checks on a copy of that stanza.
+        Stanza copy(stanza);
+        check_stanza(copy);
+      });
+  xml.feed(doc.data(), doc.size(), true);
+
+  const std::string doc2 = "<stream xmlns='s'><stanza>coucou\r\n\a</stanza></stream>";
+  xml.add_stanza_callback([](const Stanza& stanza)
+      {
+        CHECK(stanza.get_inner() == "coucou\r\n");
+      });
+
+  xml.feed(doc2.data(), doc.size(), true);
+}
+
+TEST_CASE("XML escape/unescape")
+{
+  const std::string unescaped = "'coucou'<cc>/&\"gaga\"";
+  CHECK(xml_escape(unescaped) == "&apos;coucou&apos;&lt;cc&gt;/&amp;&quot;gaga&quot;");
+  CHECK(xml_unescape(xml_escape(unescaped)) == unescaped);
+}
+
-- 
cgit v1.2.3