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) {