diff --git a/doc/doap.xml b/doc/doap.xml
index 785c7e6a1..78de96f67 100644
--- a/doc/doap.xml
+++ b/doc/doap.xml
@@ -254,6 +254,14 @@ SPDX-License-Identifier: CC0-1.0
0.2
+
+
+
+ complete
+ 1.2
+ 1.8
+
+
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index e3d32018e..4bf1ab01e 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -90,6 +90,7 @@ set(INSTALL_HEADER_FILES
base/QXmppTrustMessages.h
base/QXmppUserTuneItem.h
base/QXmppUtils.h
+ base/QXmppUri.h
base/QXmppVCardIq.h
base/QXmppVersionIq.h
base/compat/QXmppSessionIq.h
@@ -230,6 +231,7 @@ set(SOURCE_FILES
base/QXmppTask.cpp
base/QXmppThumbnail.cpp
base/QXmppTrustMessages.cpp
+ base/QXmppUri.cpp
base/QXmppUserTuneItem.cpp
base/QXmppUtils.cpp
base/QXmppVCardIq.cpp
diff --git a/src/base/QXmppUri.cpp b/src/base/QXmppUri.cpp
new file mode 100644
index 000000000..a10096ff2
--- /dev/null
+++ b/src/base/QXmppUri.cpp
@@ -0,0 +1,527 @@
+// SPDX-FileCopyrightText: 2019 Linus Jahn
+// SPDX-FileCopyrightText: 2019 Melvin Keskin
+// SPDX-FileCopyrightText: 2020 Jonah BrĂ¼chert
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "QXmppUri.h"
+
+#include "QXmppUtils_p.h"
+
+#include "StringLiterals.h"
+
+#include
+#include
+
+#include
+
+using namespace QXmpp::Private;
+using namespace QXmpp::Uri;
+
+using std::ranges::transform;
+
+constexpr QStringView SCHEME = u"xmpp";
+constexpr QChar QUERY_ITEM_DELIMITER = u';';
+constexpr QChar QUERY_ITEM_KEY_DELIMITER = u'=';
+
+// QXmppMessage types as strings
+constexpr std::array MESSAGE_TYPES = {
+ u"error",
+ u"normal",
+ u"chat",
+ u"groupchat",
+ u"headline"
+};
+
+// Adds a key-value pair to a query if the value is not empty.
+static void addKeyValuePairToQuery(QUrlQuery &query, const QString &key, QStringView value)
+{
+ if (!value.isEmpty()) {
+ query.addQueryItem(key, value.toString());
+ }
+}
+
+// Extracts the fully-encoded value of a query's key-value pair.
+static QString queryItemValue(const QUrlQuery &query, const QString &key)
+{
+ return query.queryItemValue(key, QUrl::FullyDecoded);
+}
+
+///
+/// \namespace QXmpp::Uri
+///
+/// Contains URI classes that can be serialized to URI queries (see QXmppUri).
+///
+/// \since QXmpp 1.8
+///
+
+namespace QXmpp::Uri {
+
+///
+/// \struct Command
+///
+/// A "command" query from \xep{0050, Ad-Hoc Commands}.
+///
+/// \since QXmpp 1.8
+///
+
+///
+/// \struct Invite
+///
+/// An "invite" query from \xep{0045, Multi-User Chat}.
+///
+/// \since QXmpp 1.8
+///
+
+///
+/// \struct Join
+///
+/// A "join" query from \xep{0045, Multi-User Chat}.
+///
+/// \since QXmpp 1.8
+///
+
+///
+/// \struct Login
+///
+/// A "login" query, not formally specified.
+///
+/// Used in the wild, e.g. by Kaidan.
+///
+/// \since QXmpp 1.8
+///
+
+///
+/// \struct Message
+///
+/// A "message" query defined in \xep{0147, XMPP URI Scheme Query Components}.
+///
+/// \since QXmpp 1.8
+///
+
+///
+/// \struct Unregister
+///
+/// An "unregister" query defined in \xep{0077, In-Band Registration}.
+///
+/// \since QXmpp 1.8
+///
+
+///
+/// \struct Unsubscribe
+///
+/// An "unsubscribe" query defined in \xep{0147, XMPP URI Scheme Query Components}.
+///
+/// \since QXmpp 1.8
+///
+
+///
+/// \struct Register
+///
+/// A "register" query defined in \xep{0077, In-Band Registration}.
+///
+/// \since QXmpp 1.8
+///
+
+///
+/// \struct Remove
+///
+/// A "remove" query defined in \xep{0147, XMPP URI Scheme Query Components}.
+///
+/// \since QXmpp 1.8
+///
+
+///
+/// \struct Roster
+///
+/// A "roster" query defined in \xep{0147, XMPP URI Scheme Query Components}.
+///
+/// \since QXmpp 1.8
+///
+
+///
+/// \struct Subscribe
+///
+/// A "subscribe" query defined in \xep{0147, XMPP URI Scheme Query Components}.
+///
+/// \since QXmpp 1.8
+///
+
+///
+/// \struct TrustMessage
+///
+/// A "trust-message" query defined in \xep{0434, Trust Messages (TM)}.
+///
+/// \since QXmpp 1.8
+///
+
+///
+/// \struct CustomQuery
+///
+/// A query with a custom name and custom key-value pairs.
+///
+/// Queries will be parsed into this type if they are unknown.
+///
+/// \since QXmpp 1.8
+///
+
+} // namespace QXmpp::Uri
+
+static void serializeUrlQuery(const Command &command, QUrlQuery &query)
+{
+ query.addQueryItem(u"command"_s, {});
+
+ addKeyValuePairToQuery(query, u"node"_s, command.node);
+ addKeyValuePairToQuery(query, u"action"_s, command.action);
+}
+
+static void serializeUrlQuery(const Invite &invite, QUrlQuery &query)
+{
+ query.addQueryItem(u"invite"_s, {});
+
+ addKeyValuePairToQuery(query, u"jid"_s, invite.inviteeJid);
+ addKeyValuePairToQuery(query, u"password"_s, invite.password);
+}
+
+static void serializeUrlQuery(const Join &join, QUrlQuery &query)
+{
+ query.addQueryItem(u"join"_s, {});
+
+ addKeyValuePairToQuery(query, u"password"_s, join.password);
+}
+
+static void serializeUrlQuery(const Login &login, QUrlQuery &query)
+{
+ query.addQueryItem(u"login"_s, {});
+
+ addKeyValuePairToQuery(query, u"password"_s, login.password);
+}
+
+static void serializeUrlQuery(const Message &message, QUrlQuery &query)
+{
+ query.addQueryItem(u"message"_s, {});
+
+ addKeyValuePairToQuery(query, u"from"_s, message.from);
+ addKeyValuePairToQuery(query, u"id"_s, message.id);
+ if (message.type) {
+ addKeyValuePairToQuery(query, u"type"_s, MESSAGE_TYPES.at(size_t(*message.type)));
+ }
+ addKeyValuePairToQuery(query, QStringLiteral("subject"), message.subject);
+ addKeyValuePairToQuery(query, QStringLiteral("body"), message.body);
+ addKeyValuePairToQuery(query, QStringLiteral("thread"), message.thread);
+}
+
+static void serializeUrlQuery(const Unregister &, QUrlQuery &query)
+{
+ query.addQueryItem(u"unregister"_s, {});
+}
+
+static void serializeUrlQuery(const Unsubscribe &, QUrlQuery &query)
+{
+ query.addQueryItem(u"unsubscribe"_s, {});
+}
+
+static void serializeUrlQuery(const Register &, QUrlQuery &query)
+{
+ query.addQueryItem(u"register"_s, {});
+}
+
+static void serializeUrlQuery(const Remove &, QUrlQuery &query)
+{
+ query.addQueryItem(u"remove"_s, {});
+}
+
+static void serializeUrlQuery(const Roster &roster, QUrlQuery &query)
+{
+ query.addQueryItem(u"roster"_s, {});
+
+ addKeyValuePairToQuery(query, u"name"_s, roster.name);
+ addKeyValuePairToQuery(query, u"group"_s, roster.group);
+}
+
+static void serializeUrlQuery(const Subscribe &, QUrlQuery &query)
+{
+ query.addQueryItem(u"subscribe"_s, {});
+}
+
+static void serializeUrlQuery(const TrustMessage &trustMessage, QUrlQuery &query)
+{
+ query.addQueryItem(u"trust-message"_s, {});
+
+ addKeyValuePairToQuery(query, QStringLiteral("encryption"), trustMessage.encryption);
+
+ for (auto &identifier : trustMessage.trustKeyIds) {
+ addKeyValuePairToQuery(query, QStringLiteral("trust"), identifier);
+ }
+
+ for (auto &identifier : trustMessage.distrustKeyIds) {
+ addKeyValuePairToQuery(query, QStringLiteral("distrust"), identifier);
+ }
+}
+
+static void serializeUrlQuery(const CustomQuery &custom, QUrlQuery &query)
+{
+ query.addQueryItem(custom.query, {});
+ for (const auto &[key, value] : custom.parameters) {
+ query.addQueryItem(key, value);
+ }
+}
+
+Command parseCommandQuery(const QUrlQuery &q)
+{
+ return {
+ queryItemValue(q, u"node"_s),
+ queryItemValue(q, u"action"_s),
+ };
+}
+
+Invite parseInviteQuery(const QUrlQuery &q)
+{
+ return {
+ queryItemValue(q, u"jid"_s),
+ queryItemValue(q, u"password"_s),
+ };
+}
+
+Join parseJoinQuery(const QUrlQuery &q)
+{
+ return {
+ queryItemValue(q, u"password"_s),
+ };
+}
+
+Login parseLoginQuery(const QUrlQuery &q)
+{
+ return {
+ queryItemValue(q, u"login"_s),
+ };
+}
+
+Message parseMessageQuery(const QUrlQuery &q)
+{
+ return {
+ queryItemValue(q, u"subject"_s),
+ queryItemValue(q, u"body"_s),
+ queryItemValue(q, u"thread"_s),
+ queryItemValue(q, u"id"_s),
+ queryItemValue(q, u"from"_s),
+ enumFromString(MESSAGE_TYPES, queryItemValue(q, u"type"_s)),
+ };
+}
+
+Roster parseRosterQuery(const QUrlQuery &q)
+{
+ return Roster {
+ queryItemValue(q, u"name"_s),
+ queryItemValue(q, u"group"_s),
+ };
+}
+
+TrustMessage parseTrustMessageQuery(const QUrlQuery &q)
+{
+ return TrustMessage {
+ queryItemValue(q, QStringLiteral("encryption")),
+ q.allQueryItemValues(QStringLiteral("trust"), QUrl::FullyDecoded),
+ q.allQueryItemValues(QStringLiteral("distrust"), QUrl::FullyDecoded),
+ };
+}
+
+CustomQuery parseCustomQuery(const QUrlQuery &q)
+{
+ auto queryItems = q.queryItems();
+ auto queryName = queryItems.first().first;
+ queryItems.pop_front();
+#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+ return CustomQuery { queryName, queryItems };
+#else
+ QList> queryItemsStdPair;
+ queryItemsStdPair.reserve(queryItems.size());
+ transform(queryItems, std::back_inserter(queryItemsStdPair), [](const auto &pair) { return std::pair { pair.first, pair.second }; });
+ return CustomQuery { queryName, queryItemsStdPair };
+#endif
+}
+
+struct QXmppUriPrivate : QSharedData {
+ QString jid;
+ std::any query;
+};
+
+///
+/// \class QXmppUri
+///
+/// This class represents an XMPP URI as specified by RFC 5122 - Internationalized Resource
+/// Identifiers (IRIs) and Uniform Resource Identifiers (URIs) for the Extensible Messaging and
+/// Presence Protocol (XMPP) and XEP-0147: XMPP URI Scheme Query Components.
+///
+/// A QUrlQuery is used by this class to represent a query (component) of an XMPP URI. A query
+/// conisists of query items which can be the query type or a key-value pair.
+///
+/// A query type is used to perform an action while the key-value pairs are used to define its
+/// behavior.
+///
+/// Example:
+/// xmpp:alice@example.org?message;subject=Hello;body=world
+///
+/// query (component): message;subject=Hello;body=world
+/// query items: message, subject=Hello, body=world
+/// query type: message
+/// key-value pair 1: subject=Hello
+/// key-value pair 2: body=world
+///
+/// \since QXmpp 1.8
+///
+
+///
+/// Creates an empty XMPP URI
+///
+QXmppUri::QXmppUri()
+ : d(new QXmppUriPrivate)
+{
+}
+
+QXMPP_PRIVATE_DEFINE_RULE_OF_SIX(QXmppUri)
+
+///
+/// Parses an XMPP URI.
+///
+/// \return Parsed URI or an error if the string could not be parsed.
+///
+std::variant QXmppUri::fromString(const QString &input)
+{
+ QUrl url(input);
+ if (!url.isValid()) {
+ return QXmppError { u"Invalid URI"_s, {} };
+ }
+ if (url.scheme() != SCHEME) {
+ return QXmppError { u"Wrong URI scheme (is '%1', must be xmpp)"_s.arg(url.scheme()), {} };
+ }
+
+ QXmppUri uri;
+ uri.setJid(url.path());
+
+ if (url.hasQuery()) {
+ QUrlQuery urlQuery;
+ urlQuery.setQueryDelimiters(QUERY_ITEM_KEY_DELIMITER, QUERY_ITEM_DELIMITER);
+ urlQuery.setQuery(url.query(QUrl::FullyEncoded));
+
+ // Check if there is at least one query item.
+ if (!urlQuery.isEmpty()) {
+ auto queryItems = urlQuery.queryItems();
+ Q_ASSERT(!queryItems.isEmpty());
+ auto [queryString, queryValue] = queryItems.first();
+
+ if (!queryValue.isEmpty()) {
+ // invalid XMPP URI: first query query pair must have only key, no value
+ return QXmppError { u"Invalid URI query: got key-value pair (instead of key only) for first query parameter."_s, {} };
+ }
+
+ if (queryString == u"command") {
+ uri.d->query = parseCommandQuery(urlQuery);
+ } else if (queryString == u"invite") {
+ uri.d->query = parseInviteQuery(urlQuery);
+ } else if (queryString == u"join") {
+ uri.d->query = parseJoinQuery(urlQuery);
+ } else if (queryString == u"login") {
+ uri.d->query = parseLoginQuery(urlQuery);
+ } else if (queryString == u"message") {
+ uri.d->query = parseMessageQuery(urlQuery);
+ } else if (queryString == u"register") {
+ uri.d->query = Register();
+ } else if (queryString == u"remove") {
+ uri.d->query = Remove();
+ } else if (queryString == u"roster") {
+ uri.d->query = parseRosterQuery(urlQuery);
+ } else if (queryString == u"subscribe") {
+ uri.d->query = Subscribe();
+ } else if (queryString == u"trust-message") {
+ uri.d->query = parseTrustMessageQuery(urlQuery);
+ } else if (queryString == u"unregister") {
+ uri.d->query = Unregister();
+ } else if (queryString == u"unsubscribe") {
+ uri.d->query = Unsubscribe();
+ } else {
+ uri.d->query = parseCustomQuery(urlQuery);
+ }
+ }
+ }
+
+ return uri;
+}
+
+template
+bool serialize(const std::any &query, QUrlQuery &urlQuery)
+{
+ if (query.type() == typeid(T)) {
+ serializeUrlQuery(std::any_cast(query), urlQuery);
+ return true;
+ }
+ return false;
+}
+
+///
+/// Serializes the URI to a string.
+///
+QString QXmppUri::toString()
+{
+ QUrl url;
+ url.setScheme(SCHEME.toString());
+ url.setPath(d->jid);
+
+ // add URI query
+ QUrlQuery urlQuery;
+ urlQuery.setQueryDelimiters(QUERY_ITEM_KEY_DELIMITER, QUERY_ITEM_DELIMITER);
+
+ if (d->query.has_value()) {
+ serialize(d->query, urlQuery) ||
+ serialize(d->query, urlQuery) ||
+ serialize(d->query, urlQuery) ||
+ serialize(d->query, urlQuery) ||
+ serialize(d->query, urlQuery) ||
+ serialize(d->query, urlQuery) ||
+ serialize(d->query, urlQuery) ||
+ serialize(d->query, urlQuery) ||
+ serialize(d->query, urlQuery) ||
+ serialize(d->query, urlQuery) ||
+ serialize(d->query, urlQuery) ||
+ serialize(d->query, urlQuery) ||
+ serialize(d->query, urlQuery);
+ }
+
+ url.setQuery(urlQuery);
+
+ return QString::fromUtf8(url.toEncoded(QUrl::FullyEncoded));
+}
+
+///
+/// Returns the JID this URI is about.
+///
+/// This can also be e.g. a MUC room in case of a Join action.
+///
+QString QXmppUri::jid() const
+{
+ return d->jid;
+}
+
+///
+/// Sets the JID this URI links to.
+///
+void QXmppUri::setJid(const QString &jid)
+{
+ d->jid = jid;
+}
+
+///
+/// Returns the query of the URI.
+///
+/// It may be empty (has_value() returns false). Possible URI types are available in the namespace
+/// QXmpp::Uri.
+///
+std::any QXmppUri::query() const
+{
+ return d->query;
+}
+
+void QXmppUri::setQuery(std::any &&query)
+{
+ d->query = std::move(query);
+}
diff --git a/src/base/QXmppUri.h b/src/base/QXmppUri.h
new file mode 100644
index 000000000..a13e220b1
--- /dev/null
+++ b/src/base/QXmppUri.h
@@ -0,0 +1,181 @@
+// SPDX-FileCopyrightText: 2019 Linus Jahn
+// SPDX-FileCopyrightText: 2019 Melvin Keskin
+// SPDX-FileCopyrightText: 2020 Jonah BrĂ¼chert
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#ifndef QXMPPURI_H
+#define QXMPPURI_H
+
+#include
+#include
+
+#include
+
+class QUrlQuery;
+
+struct QXmppUriPrivate;
+
+namespace QXmpp::Uri {
+
+struct Command {
+ /// the command node
+ QString node;
+ /// the ad-hoc commands action type
+ QString action;
+
+ /// Default comparison operator.
+ bool operator==(const Command &) const = default;
+};
+
+struct Invite {
+ /// the JID of the invitee
+ QString inviteeJid;
+ /// the password required to enter a multi-user chat room
+ QString password;
+
+ /// Default comparison operator.
+ bool operator==(const Invite &) const = default;
+};
+
+struct Join {
+ /// the password required to enter a multi-user chat room
+ QString password;
+
+ /// Default comparison operator.
+ bool operator==(const Join &) const = default;
+};
+
+struct Login {
+ /// the password required to connect to the account
+ QString password;
+
+ /// Default comparison operator.
+ bool operator==(const Login &) const = default;
+};
+
+struct Message {
+ /// a subject for the message per the "jabber:client" schema
+ QString subject;
+ /// a body for the message per the "jabber:client" schema
+ QString body;
+ /// a Thread ID for the message per the "jabber:client" schema
+ QString thread;
+ /// a from address for the message per the "jabber:client" schema
+ QString id;
+ /// an ID for the message per the "jabber:client" schema
+ QString from;
+ /// the message type per the "jabber:client" schema
+ std::optional type;
+
+ /// Default comparison operator.
+ bool operator==(const Message &) const = default;
+};
+
+struct Unregister {
+ /// Default comparison operator.
+ bool operator==(const Unregister &) const = default;
+};
+
+struct Unsubscribe {
+ /// Default comparison operator.
+ bool operator==(const Unsubscribe &) const = default;
+};
+
+struct Register {
+ /// Default comparison operator.
+ bool operator==(const Register &) const = default;
+};
+
+struct Remove {
+ /// Default comparison operator.
+ bool operator==(const Remove &) const = default;
+};
+
+struct Roster {
+ /// the user-assigned group for the roster item
+ QString name;
+ /// the user-assigned name for the roster item
+ QString group;
+
+ /// Default comparison operator.
+ bool operator==(const Roster &) const = default;
+};
+
+struct Subscribe {
+ /// Default comparison operator.
+ bool operator==(const Subscribe &) const = default;
+};
+
+struct TrustMessage {
+ /// encryption of the keys to trust or distrust
+ QString encryption;
+ /// list of Base16 encoded key identifiers to be trusted
+ QList trustKeyIds;
+ /// list of Base16 encoded key identifiers to be distrusted
+ QList distrustKeyIds;
+
+ /// Default comparison operator.
+ bool operator==(const TrustMessage &) const = default;
+};
+
+struct CustomQuery {
+ /// query name as string
+ QString query;
+ /// list of parameters as key-value pairs
+ QList> parameters;
+
+ /// Default comparison operator.
+ bool operator==(const CustomQuery &) const = default;
+};
+
+} // namespace QXmpp::Uri
+
+class QXMPP_EXPORT QXmppUri
+{
+public:
+ QXmppUri();
+ QXMPP_PRIVATE_DECLARE_RULE_OF_SIX(QXmppUri)
+
+ static std::variant fromString(const QString &);
+
+ QString toString();
+
+ QString jid() const;
+ void setJid(const QString &jid);
+
+ std::any query() const;
+ /// Sets a "command" query.
+ void setQuery(QXmpp::Uri::Command &&q) { setQuery(std::any(std::move(q))); }
+ /// Sets a MUC invite query.
+ void setQuery(QXmpp::Uri::Invite &&q) { setQuery(std::any(std::move(q))); }
+ /// Sets a MUC join query.
+ void setQuery(QXmpp::Uri::Join &&q) { setQuery(std::any(std::move(q))); }
+ /// Sets a login query.
+ void setQuery(QXmpp::Uri::Login &&q) { setQuery(std::any(std::move(q))); }
+ /// Sets a message query.
+ void setQuery(QXmpp::Uri::Message &&q) { setQuery(std::any(std::move(q))); }
+ /// Sets a unregister query.
+ void setQuery(QXmpp::Uri::Unregister &&q) { setQuery(std::any(std::move(q))); }
+ /// Sets a register query.
+ void setQuery(QXmpp::Uri::Register &&q) { setQuery(std::any(std::move(q))); }
+ /// Sets a remove query.
+ void setQuery(QXmpp::Uri::Remove &&q) { setQuery(std::any(std::move(q))); }
+ /// Sets a roster query.
+ void setQuery(QXmpp::Uri::Roster &&q) { setQuery(std::any(std::move(q))); }
+ /// Sets a subscribe query.
+ void setQuery(QXmpp::Uri::Subscribe &&q) { setQuery(std::any(std::move(q))); }
+ /// Sets a trust message query.
+ void setQuery(QXmpp::Uri::TrustMessage &&q) { setQuery(std::any(std::move(q))); }
+ /// Sets a query with custom name and key-value pairs.
+ void setQuery(QXmpp::Uri::CustomQuery &&q) { setQuery(std::any(std::move(q))); }
+ /// Removes any query from the URI.
+ void resetQuery() { setQuery(std::any()); }
+
+private:
+ void setQuery(std::any &&);
+
+ QSharedDataPointer d;
+};
+
+#endif // QXMPPURI_H
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 6a80e68b1..ab646f6cf 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -80,6 +80,7 @@ add_simple_test(qxmppstreamfeatures)
add_simple_test(qxmppstunmessage)
add_simple_test(qxmpptrustmessages)
add_simple_test(qxmpptrustmemorystorage)
+add_simple_test(qxmppuri)
add_simple_test(qxmppuserlocationmanager TestClient.h)
add_simple_test(qxmppusertunemanager TestClient.h)
add_simple_test(qxmppvcardiq)
diff --git a/tests/qxmppuri/tst_qxmppuri.cpp b/tests/qxmppuri/tst_qxmppuri.cpp
new file mode 100644
index 000000000..4bdde8214
--- /dev/null
+++ b/tests/qxmppuri/tst_qxmppuri.cpp
@@ -0,0 +1,116 @@
+// SPDX-FileCopyrightText: 2024 Linus Jahn
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "QXmppUri.h"
+
+#include "util.h"
+
+#include
+
+namespace Uri = QXmpp::Uri;
+using namespace QXmpp::Private;
+
+class tst_QXmppUri : public QObject
+{
+ Q_OBJECT
+
+private:
+ Q_SLOT void base();
+ Q_SLOT void queryMessage();
+ Q_SLOT void queryRoster();
+ Q_SLOT void queryRemove();
+ Q_SLOT void queryOther();
+};
+
+void tst_QXmppUri::base()
+{
+ auto str = u"xmpp:lnj@qxmpp.org"_s;
+ auto uri = unwrap(QXmppUri::fromString(str));
+ QCOMPARE(uri.jid(), u"lnj@qxmpp.org");
+ QVERIFY(!uri.query().has_value());
+}
+
+void tst_QXmppUri::queryMessage()
+{
+ const auto string = u"xmpp:romeo@montague.net?message;subject=Test%20Message;body=Here's%20a%20test%20message"_s;
+ auto uri = unwrap(QXmppUri::fromString(string));
+ QCOMPARE(uri.jid(), u"romeo@montague.net");
+ auto message = unwrap(uri.query());
+ QCOMPARE(message, (Uri::Message { u"Test Message"_s, u"Here's a test message"_s, {}, {}, {}, {} }));
+
+ QCOMPARE(uri.toString(), string);
+}
+
+void tst_QXmppUri::queryRoster()
+{
+ const auto string = u"xmpp:romeo@montague.net?roster;name=Romeo%20Montague;group=Friends"_s;
+ auto uri = unwrap(QXmppUri::fromString(string));
+ QCOMPARE(uri.jid(), u"romeo@montague.net");
+ auto message = unwrap(uri.query());
+ QCOMPARE(message, (Uri::Roster { u"Romeo Montague"_s, u"Friends"_s }));
+
+ QCOMPARE(uri.toString(), string);
+}
+
+void tst_QXmppUri::queryRemove()
+{
+ const auto string = u"xmpp:romeo@montague.net?remove"_s;
+ auto uri = unwrap(QXmppUri::fromString(string));
+ QCOMPARE(uri.jid(), u"romeo@montague.net");
+ auto message = unwrap(uri.query());
+ QCOMPARE(message, Uri::Remove {});
+
+ QCOMPARE(uri.toString(), string);
+}
+
+void tst_QXmppUri::queryOther()
+{
+ auto string = u"xmpp:lnj@qxmpp.org?command;node=test2;action=next"_s;
+ auto uri = unwrap(QXmppUri::fromString(string));
+ QCOMPARE(unwrap(uri.query()), (Uri::Command { u"test2"_s, u"next"_s }));
+ QCOMPARE(uri.toString(), string);
+
+ string = u"xmpp:xsf@muc.xmpp.org?invite;jid=lnj@qxmpp.org;password=1234"_s;
+ uri = unwrap(QXmppUri::fromString(string));
+ QCOMPARE(unwrap(uri.query()), (Uri::Invite { u"lnj@qxmpp.org"_s, u"1234"_s }));
+ QCOMPARE(uri.toString(), string);
+
+ string = u"xmpp:xsf@muc.xmpp.org?join;password=1234"_s;
+ uri = unwrap(QXmppUri::fromString(string));
+ QCOMPARE(unwrap(uri.query()), (Uri::Join { u"1234"_s }));
+ QCOMPARE(uri.toString(), string);
+
+ string = u"xmpp:qxmpp.org?register"_s;
+ uri = unwrap(QXmppUri::fromString(string));
+ QCOMPARE(unwrap(uri.query()), (Uri::Register {}));
+ QCOMPARE(uri.toString(), string);
+
+ string = u"xmpp:qxmpp.org?remove"_s;
+ uri = unwrap(QXmppUri::fromString(string));
+ QCOMPARE(unwrap(uri.query()), (Uri::Remove {}));
+ QCOMPARE(uri.toString(), string);
+
+ string = u"xmpp:qxmpp.org?subscribe"_s;
+ uri = unwrap(QXmppUri::fromString(string));
+ QCOMPARE(unwrap(uri.query()), (Uri::Subscribe {}));
+ QCOMPARE(uri.toString(), string);
+
+ string = u"xmpp:qxmpp.org?unregister"_s;
+ uri = unwrap(QXmppUri::fromString(string));
+ QCOMPARE(unwrap(uri.query()), (Uri::Unregister {}));
+ QCOMPARE(uri.toString(), string);
+
+ string = u"xmpp:qxmpp.org?unsubscribe"_s;
+ uri = unwrap(QXmppUri::fromString(string));
+ QCOMPARE(unwrap(uri.query()), (Uri::Unsubscribe {}));
+ QCOMPARE(uri.toString(), string);
+
+ string = u"xmpp:qxmpp.org?x-new-query;a=b;action=add"_s;
+ uri = unwrap(QXmppUri::fromString(string));
+ QCOMPARE(unwrap(uri.query()), (Uri::CustomQuery { u"x-new-query"_s, { { u"a"_s, u"b"_s }, { u"action"_s, u"add"_s } } }));
+ QCOMPARE(uri.toString(), string);
+}
+
+QTEST_MAIN(tst_QXmppUri)
+#include "tst_qxmppuri.moc"
diff --git a/tests/util.h b/tests/util.h
index d9a007027..879a079a0 100644
--- a/tests/util.h
+++ b/tests/util.h
@@ -7,17 +7,21 @@
#ifndef TESTS_UTIL_H
#define TESTS_UTIL_H
+#include "QXmppError.h"
#include "QXmppPasswordChecker.h"
#include "QXmppTask.h"
#include "StringLiterals.h"
+#include
#include
#include
#include
#include
+struct QXmppError;
+
// QVERIFY2 with empty return value (return {};)
#define QVERIFY_RV(statement, description) \
if (!QTest::qVerify(statement, #statement, description, __FILE__, __LINE__)) \
@@ -138,6 +142,42 @@ T unwrap(std::optional &&v)
return *v;
}
+template
+T unwrap(std::variant &&v)
+{
+ if (std::holds_alternative(v)) {
+ auto message = u"Expected value, got error: %1."_s.arg(std::get(v).description);
+ VERIFY2(v.index() == 1, message.toLocal8Bit().constData());
+ }
+ return std::get(std::move(v));
+}
+
+template
+const T &unwrap(const std::variant &v)
+{
+ if (std::holds_alternative(v)) {
+ auto message = u"Expected value, got error: %1."_s.arg(std::get(v).description);
+ VERIFY2(v.index() == 1, message.toLocal8Bit().constData());
+ }
+ return std::get(v);
+}
+
+template
+const T &unwrap(const std::any &v)
+{
+ VERIFY2(v.has_value(), "Expected non-empty std::any");
+ VERIFY2(v.type() == typeid(T), "Got std::any with wrong type");
+ return std::any_cast(v);
+}
+
+template
+T unwrap(std::any &&v)
+{
+ VERIFY2(v.has_value(), "Expected non-empty std::any");
+ VERIFY2(v.type() == typeid(T), "Got std::any with wrong type");
+ return std::any_cast(std::move(v));
+}
+
template
T wait(const QFuture &future)
{