diff --git a/doc/doap.xml b/doc/doap.xml
index a94e72398..e8d2da8e6 100644
--- a/doc/doap.xml
+++ b/doc/doap.xml
@@ -432,6 +432,14 @@ SPDX-License-Identifier: CC0-1.0
1.0
+
+
+
+ complete
+ 0.2.0
+ 1.8
+
+
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 82e645634..6617e8b96 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -130,6 +130,7 @@ set(INSTALL_HEADER_FILES
client/QXmppMessageHandler.h
client/QXmppMessageReceiptManager.h
client/QXmppMixManager.h
+ client/QXmppMovedManager.h
client/QXmppMucManager.h
client/QXmppOutgoingClient.h
client/QXmppRegistrationManager.h
@@ -267,6 +268,7 @@ set(SOURCE_FILES
client/QXmppMamManager.cpp
client/QXmppMessageReceiptManager.cpp
client/QXmppMixManager.cpp
+ client/QXmppMovedManager.cpp
client/QXmppMucManager.cpp
client/QXmppOutgoingClient.cpp
client/QXmppRosterManager.cpp
diff --git a/src/base/QXmppConstants_p.h b/src/base/QXmppConstants_p.h
index 276f61b49..2cf0c9f3b 100644
--- a/src/base/QXmppConstants_p.h
+++ b/src/base/QXmppConstants_p.h
@@ -171,6 +171,8 @@ inline constexpr QStringView ns_thumbs = u"urn:xmpp:thumbs:1";
inline constexpr QStringView ns_muji = u"urn:xmpp:jingle:muji:0";
// XEP-0280: Message Carbons
inline constexpr QStringView ns_carbons = u"urn:xmpp:carbons:2";
+// XEP-0283: Moved
+inline constexpr QStringView ns_moved = u"urn:xmpp:moved:1";
// XEP-0293: Jingle RTP Feedback Negotiation
inline constexpr QStringView ns_jingle_rtp_feedback_negotiation = u"urn:xmpp:jingle:apps:rtp:rtcp-fb:0";
// XEP-0294: Jingle RTP Header Extensions Negotiation
diff --git a/src/client/QXmppMovedItem_p.h b/src/client/QXmppMovedItem_p.h
new file mode 100644
index 000000000..c53e16144
--- /dev/null
+++ b/src/client/QXmppMovedItem_p.h
@@ -0,0 +1,30 @@
+// SPDX-FileCopyrightText: 2024 Filipe Azevedo
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#ifndef QXMPPMOVEDITEM_P_H
+#define QXMPPMOVEDITEM_P_H
+
+#include
+
+class QXmppMovedItem : public QXmppPubSubBaseItem
+{
+public:
+ QXmppMovedItem(const QString &newJid = {});
+
+ QString newJid() const { return m_newJid; }
+ void setNewJid(const QString &newJid) { m_newJid = newJid; }
+
+ static bool isItem(const QDomElement &itemElement);
+
+protected:
+ /// \cond
+ void parsePayload(const QDomElement &payloadElement) override;
+ void serializePayload(QXmlStreamWriter *writer) const override;
+ /// \endcond
+
+private:
+ QString m_newJid;
+};
+
+#endif // QXMPPMOVEDITEM_P_H
diff --git a/src/client/QXmppMovedManager.cpp b/src/client/QXmppMovedManager.cpp
new file mode 100644
index 000000000..76bc561eb
--- /dev/null
+++ b/src/client/QXmppMovedManager.cpp
@@ -0,0 +1,339 @@
+// SPDX-FileCopyrightText: 2024 Filipe Azevedo
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "QXmppMovedManager.h"
+
+#include "QXmppConstants_p.h"
+#include "QXmppDiscoveryIq.h"
+#include "QXmppDiscoveryManager.h"
+#include "QXmppMovedItem_p.h"
+#include "QXmppPubSubManager.h"
+#include "QXmppRosterManager.h"
+#include "QXmppTask.h"
+#include "QXmppUri.h"
+#include "QXmppUtils.h"
+#include "QXmppUtils_p.h"
+
+#include "StringLiterals.h"
+
+#include
+
+using namespace QXmpp;
+using namespace QXmpp::Private;
+
+QXmppMovedItem::QXmppMovedItem(const QString &newJid)
+ : m_newJid(newJid)
+{
+ setId(QXmppPubSubManager::standardItemIdToString(QXmppPubSubManager::Current));
+}
+
+///
+/// Returns true if the given DOM element is a valid \xep{0283, Moved} item.
+///
+bool QXmppMovedItem::isItem(const QDomElement &itemElement)
+{
+ return QXmppPubSubBaseItem::isItem(itemElement, [](const QDomElement &payload) {
+ if (payload.tagName() != u"moved" || payload.namespaceURI() != ns_moved) {
+ return false;
+ }
+ return payload.firstChildElement().tagName() == u"new-jid";
+ });
+}
+
+void QXmppMovedItem::parsePayload(const QDomElement &payloadElement)
+{
+ m_newJid = payloadElement.firstChildElement(u"new-jid"_s).text();
+}
+
+void QXmppMovedItem::serializePayload(QXmlStreamWriter *writer) const
+{
+ if (m_newJid.isEmpty()) {
+ return;
+ }
+
+ writer->writeStartElement(QSL65("moved"));
+ writer->writeDefaultNamespace(toString65(ns_moved));
+ writer->writeTextElement(QSL65("new-jid"), m_newJid);
+ writer->writeEndElement();
+}
+
+class QXmppMovedManagerPrivate
+{
+public:
+ QXmppDiscoveryManager *discoveryManager = nullptr;
+ bool supportedByServer = false;
+};
+
+///
+/// \class QXmppMovedManager
+///
+/// This class manages user account moving as specified in \xep{0283, Moved}
+///
+/// In order to use this manager, make sure to add all managers needed by this manager:
+/// \code
+/// client->addNewExtension();
+/// client->addNewExtension();
+/// \endcode
+///
+/// Afterwards, you need to add this manager to the client:
+/// \code
+/// auto *manager = client->addNewExtension();
+/// \endcode
+///
+/// If you want to publish a moved statement use the publishStatement call with the old account:
+/// \code
+/// manager->publishStatement("new@example.org");
+/// \endcode
+///
+/// Once you published your statement, you then need to subscribe to your old contacts with the new account:
+/// \code
+/// manager->notifyContact("contact@xmpp.example", "old@example.org", "Hey, I moved my account, please accept me.");
+/// \endcode
+///
+/// When a contact receive a subscription request from a moved user he needs to verify the authenticity of the request.
+/// The QXmppRosterManager handle it on its own if the client has the QXmppMovedManager extension available.
+/// The request will be ignored entirely if the old jid incoming subscription is not part of the roster with a 'from' or 'both' type.
+/// In case of the authenticity can't be established the moved element is ignored entirely. Alternatively, if the client
+/// does not has QXmppMovedManager support the request message will be changed to introduce a warning message before emitting
+/// the subscription{Request}Received signal.
+///
+/// \ingroup Managers
+///
+/// \since QXmpp 1.9
+///
+
+///
+/// Constructs a \xep{0283, Moved} manager.
+///
+QXmppMovedManager::QXmppMovedManager()
+ : d(new QXmppMovedManagerPrivate())
+{
+}
+
+QXmppMovedManager::~QXmppMovedManager() = default;
+
+QStringList QXmppMovedManager::discoveryFeatures() const
+{
+ return { ns_moved.toString() };
+}
+
+///
+/// \property QXmppMovedManager::supportedByServer
+///
+/// \see QXmppMovedManager::supportedByServer()
+///
+
+///
+/// Returns whether the own server supports \xep{0283, Moved} feature.
+///
+/// \return whether \xep{0283, Moved} feature is supported
+///
+bool QXmppMovedManager::supportedByServer() const
+{
+ return d->supportedByServer;
+}
+
+///
+/// \fn QXmppMovedManager::supportedByServerChanged()
+///
+/// Emitted when the server enabled or disabled support for \xep{0283, Moved}.
+///
+
+///
+/// Publish a moved statement.
+///
+/// \param newBareJid JID of the new account
+///
+/// \return the result of the action
+///
+QXmppTask QXmppMovedManager::publishStatement(const QString &newBareJid)
+{
+ return chainSuccess(client()->findExtension()->publishOwnPepItem(ns_moved.toString(), QXmppMovedItem { newBareJid }), this);
+}
+
+///
+/// Verify a user moved statement.
+///
+/// \param oldBareJid JID of the old account to check statement
+/// \param newBareJid JID of the new account that send the subscription request
+///
+/// \return the result of the action
+///
+QXmppTask QXmppMovedManager::verifyStatement(const QString &oldBareJid, const QString &newBareJid)
+{
+ return chain(
+ client()->findExtension()->requestItem(oldBareJid, ns_moved.toString(), u"current"_s),
+ this,
+ [=, this](QXmppPubSubManager::ItemResult &&result) {
+ return std::visit(
+ overloaded {
+ [newBareJid, this](QXmppMovedItem item) -> Result {
+ return movedJidsMatch(newBareJid, item.newJid());
+ },
+ [newBareJid, this](QXmppError err) -> Result {
+ // As a special case, if the attempt to retrieve the moved statement results in an error with the condition
+ // as defined in RFC 6120, and that element contains a valid XMPP URI (e.g. xmpp:user@example.com), then the
+ // error response MUST be handled equivalent to a statement containing a element with the JID
+ // provided in the URI (e.g. user@example.com).
+ if (auto e = err.value()) {
+ const auto newJid = [&e]() -> QString {
+ if (e->condition() != QXmppStanza::Error::Gone) {
+ return {};
+ }
+
+ const auto result = QXmppUri::fromString(e->redirectionUri());
+
+ if (std::holds_alternative(result)) {
+ return std::get(result).jid();
+ }
+
+ return {};
+ }();
+
+ if (!newJid.isEmpty()) {
+ return movedJidsMatch(newBareJid, newJid);
+ }
+ }
+
+ return err;
+ },
+ },
+ std::move(result));
+ });
+}
+
+///
+/// Notifies a contact that the user has moved to another account.
+///
+/// \param contactBareJid JID of the contact to send the subscription request
+/// \param oldBareJid JID of the old account we moved from
+/// \param sensitive If true the notification is sent sensitively
+/// \param reason The reason of the move
+///
+/// \return the result of the action
+///
+QXmppTask QXmppMovedManager::notifyContact(const QString &contactBareJid, const QString &oldBareJid, bool sensitive, const QString &reason)
+{
+ QXmppPresence packet;
+ packet.setTo(QXmppUtils::jidToBareJid(contactBareJid));
+ packet.setType(QXmppPresence::Subscribe);
+ packet.setStatusText(reason);
+ packet.setOldJid(oldBareJid);
+ return sensitive ? client()->sendSensitive(std::move(packet)) : client()->send(std::move(packet));
+}
+
+/// \cond
+void QXmppMovedManager::onRegistered(QXmppClient *client)
+{
+ connect(client, &QXmppClient::connected, this, [this, client]() {
+ if (client->streamManagementState() == QXmppClient::NewStream) {
+ resetCachedData();
+ }
+ });
+
+ d->discoveryManager = client->findExtension();
+ Q_ASSERT_X(d->discoveryManager, "QXmppMovedManager", "QXmppDiscoveryManager is missing");
+
+ connect(d->discoveryManager, &QXmppDiscoveryManager::infoReceived, this, &QXmppMovedManager::handleDiscoInfo);
+
+ Q_ASSERT_X(client->findExtension(), "QXmppMovedManager", "QXmppPubSubManager is missing");
+}
+
+void QXmppMovedManager::onUnregistered(QXmppClient *client)
+{
+ disconnect(d->discoveryManager, &QXmppDiscoveryManager::infoReceived, this, &QXmppMovedManager::handleDiscoInfo);
+ resetCachedData();
+ disconnect(client, &QXmppClient::connected, this, nullptr);
+}
+/// \endcond
+
+///
+/// Checks for moved elements in incoming subscription requests and verifies them.
+///
+/// This requires the QXmppRosterManager to be registered with the client.
+///
+/// \returns a task for the verification result if the subscription request contains a moved
+/// element with an 'old-jid' that is already in the account's roster.
+///
+std::optional> QXmppMovedManager::handleSubscriptionRequest(const QXmppPresence &presence)
+{
+ // check for moved element
+ if (presence.oldJid().isEmpty()) {
+ return {};
+ }
+
+ // find roster manager
+ auto *rosterManager = client()->findExtension();
+ Q_ASSERT(rosterManager);
+
+ // check subscription state of old-jid
+ const auto entry = rosterManager->getRosterEntry(presence.oldJid());
+
+ switch (entry.subscriptionType()) {
+ case QXmppRosterIq::Item::From:
+ case QXmppRosterIq::Item::Both:
+ break;
+ default:
+ // The subscription state of the old JID needs to be either from or both, else ignore
+ // the moved element
+ return {};
+ }
+
+ // return verification result
+ return chain(verifyStatement(presence.oldJid(), QXmppUtils::jidToBareJid(presence.from())), this, [this](Result &&result) mutable {
+ return std::holds_alternative(result);
+ });
+}
+
+///
+/// Handles incoming service infos specified by \xep{0030, Service Discovery}.
+///
+/// \param iq received Service Discovery IQ stanza
+///
+void QXmppMovedManager::handleDiscoInfo(const QXmppDiscoveryIq &iq)
+{
+ // Check the server's functionality to support MOVED feature.
+ if (iq.from().isEmpty() || iq.from() == client()->configuration().domain()) {
+ // Check whether MOVED is supported.
+ setSupportedByServer(iq.features().contains(ns_moved));
+ }
+}
+
+///
+/// Ensures that both JIDs match.
+///
+/// \param newBareJid JID of the contact that sent the subscription request
+/// \param pepBareJid JID of the new account as fetched from the old account statement
+///
+/// \return the result of the action
+///
+QXmppMovedManager::Result QXmppMovedManager::movedJidsMatch(const QString &newBareJid, const QString &pepBareJid) const
+{
+ if (newBareJid == pepBareJid) {
+ return Success();
+ }
+
+ return QXmppError { u"The JID does not match the user's statement."_s, {} };
+}
+
+///
+/// Sets whether the own server supports \xep{0283, Moved}.
+///
+/// \param supportedByServer whether \xep{0283, Moved} is supported by the server
+///
+void QXmppMovedManager::setSupportedByServer(bool supportedByServer)
+{
+ if (d->supportedByServer != supportedByServer) {
+ d->supportedByServer = supportedByServer;
+ Q_EMIT supportedByServerChanged();
+ }
+}
+
+///
+/// Resets the cached data.
+///
+void QXmppMovedManager::resetCachedData()
+{
+ setSupportedByServer(false);
+}
diff --git a/src/client/QXmppMovedManager.h b/src/client/QXmppMovedManager.h
new file mode 100644
index 000000000..3734ca032
--- /dev/null
+++ b/src/client/QXmppMovedManager.h
@@ -0,0 +1,54 @@
+// SPDX-FileCopyrightText: 2024 Filipe Azevedo
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#ifndef QXMPPMOVEDMANAGER_H
+#define QXMPPMOVEDMANAGER_H
+
+#include "QXmppClient.h"
+#include "QXmppClientExtension.h"
+
+class QXmppMovedManagerPrivate;
+
+class QXMPP_EXPORT QXmppMovedManager : public QXmppClientExtension
+{
+ Q_OBJECT
+ Q_PROPERTY(bool supportedByServer READ supportedByServer NOTIFY supportedByServerChanged)
+
+public:
+ using Result = std::variant;
+
+ explicit QXmppMovedManager();
+ ~QXmppMovedManager() override;
+
+ QStringList discoveryFeatures() const override;
+
+ bool supportedByServer() const;
+ Q_SIGNAL void supportedByServerChanged();
+
+ QXmppTask publishStatement(const QString &newBareJid);
+ QXmppTask verifyStatement(const QString &oldBareJid, const QString &newBareJid);
+
+ QXmppTask notifyContact(const QString &contactBareJid, const QString &oldBareJid, bool sensitive = true, const QString &reason = {});
+
+protected:
+ /// \cond
+ void onRegistered(QXmppClient *client) override;
+ void onUnregistered(QXmppClient *client) override;
+ /// \endcond
+
+private:
+ std::optional> handleSubscriptionRequest(const QXmppPresence &presence);
+ void handleDiscoInfo(const QXmppDiscoveryIq &iq);
+ QXmppClient::EmptyResult movedJidsMatch(const QString &newBareJid, const QString &pepBareJid) const;
+
+ void setSupportedByServer(bool supportedByServer);
+ void resetCachedData();
+
+ const std::unique_ptr d;
+
+ friend class QXmppRosterManager;
+ friend class tst_QXmppMovedManager;
+};
+
+#endif // QXMPPMOVEDMANAGER_H
diff --git a/src/client/QXmppRosterManager.cpp b/src/client/QXmppRosterManager.cpp
index 0083acf73..2b0c3cb67 100644
--- a/src/client/QXmppRosterManager.cpp
+++ b/src/client/QXmppRosterManager.cpp
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: 2010 Manjeet Dahiya
// SPDX-FileCopyrightText: 2010 Jeremy Lainé
// SPDX-FileCopyrightText: 2020 Melvin Keskin
+// SPDX-FileCopyrightText: 2024 Filipe Azevedo
//
// SPDX-License-Identifier: LGPL-2.1-or-later
@@ -10,6 +11,7 @@
#include "QXmppClient.h"
#include "QXmppConstants_p.h"
#include "QXmppFutureUtils_p.h"
+#include "QXmppMovedManager.h"
#include "QXmppPresence.h"
#include "QXmppRosterIq.h"
#include "QXmppUtils.h"
@@ -69,8 +71,8 @@ static void serializeRosterData(const RosterData &d, QXmlStreamWriter &writer)
/// The user can either accept the request by calling acceptSubscription() or refuse it
/// by calling refuseSubscription().
///
-/// \note If QXmppConfiguration::autoAcceptSubscriptions() is set to true, this
-/// signal will not be emitted.
+/// \note If QXmppConfiguration::autoAcceptSubscriptions() is set to true or the subscription
+/// request is automatically accepted by the QXmppMovedManager, this signal will not be emitted.
///
/// \param subscriberBareJid bare JID that wants to subscribe to the user's presence
/// \param presence presence stanza containing the reason / message (presence.statusText())
@@ -259,23 +261,46 @@ void QXmppRosterManager::_q_presenceReceived(const QXmppPresence &presence)
d->presences[bareJid].remove(resource);
Q_EMIT presenceChanged(bareJid, resource);
break;
- case QXmppPresence::Subscribe:
+ case QXmppPresence::Subscribe: {
+ // accept all incoming subscription requests if enabled
if (client()->configuration().autoAcceptSubscriptions()) {
- // accept subscription request
- acceptSubscription(bareJid);
-
- // ask for reciprocal subscription
- subscribe(bareJid);
- } else {
- Q_EMIT subscriptionReceived(bareJid);
- Q_EMIT subscriptionRequestReceived(bareJid, presence);
+ handleSubscriptionRequest(bareJid, presence, true);
+ break;
}
+
+ // check for XEP-0283: Moved subscription requests and verify them
+ if (auto *movedManager = client()->findExtension()) {
+ if (auto verificationTask = movedManager->handleSubscriptionRequest(presence)) {
+ verificationTask->then(this, [this, presence, bareJid](bool valid) {
+ handleSubscriptionRequest(bareJid, presence, valid);
+ });
+ break;
+ }
+ }
+
+ handleSubscriptionRequest(bareJid, presence, false);
break;
+ }
default:
break;
}
}
+void QXmppRosterManager::handleSubscriptionRequest(const QString &bareJid, const QXmppPresence &presence, bool accept)
+{
+ if (accept) {
+ // accept subscription request
+ acceptSubscription(bareJid);
+
+ // ask for reciprocal subscription
+ subscribe(bareJid);
+ } else {
+ // let user decide whether to accept the subscription request
+ Q_EMIT subscriptionReceived(bareJid);
+ Q_EMIT subscriptionRequestReceived(bareJid, presence);
+ }
+}
+
QXmppTask QXmppRosterManager::requestRoster()
{
QXmppRosterIq iq;
diff --git a/src/client/QXmppRosterManager.h b/src/client/QXmppRosterManager.h
index 3eedca292..2edce2cd4 100644
--- a/src/client/QXmppRosterManager.h
+++ b/src/client/QXmppRosterManager.h
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: 2010 Manjeet Dahiya
// SPDX-FileCopyrightText: 2010 Jeremy Lainé
// SPDX-FileCopyrightText: 2021 Melvin Keskin
+// SPDX-FileCopyrightText: 2024 Filipe Azevedo
//
// SPDX-License-Identifier: LGPL-2.1-or-later
@@ -112,7 +113,9 @@ public Q_SLOTS:
/// by calling refuseSubscription().
///
/// \note If you set QXmppConfiguration::autoAcceptSubscriptions() to true, this
- /// signal will not be emitted.
+ /// signal will not be emitted. This is only valid for non moved or verified moved subscription.
+ /// If the subscription is a moved one and the roster's old-jid's subscription is not either from
+ /// or both then QXmppConfiguration::autoAcceptSubscriptions() is ignored.
void subscriptionReceived(const QString &bareJid);
void subscriptionRequestReceived(const QString &subscriberBareJid, const QXmppPresence &presence);
@@ -140,6 +143,8 @@ private Q_SLOTS:
private:
using RosterResult = std::variant;
+
+ void handleSubscriptionRequest(const QString &bareJid, const QXmppPresence &presence, bool accept);
QXmppTask requestRoster();
const std::unique_ptr d;
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index ab646f6cf..0ba66a7c2 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -57,6 +57,7 @@ add_simple_test(qxmppmessage)
add_simple_test(qxmppmessagereaction)
add_simple_test(qxmppmessagereceiptmanager)
add_simple_test(qxmppmixiq)
+add_simple_test(qxmppmovedmanager TestClient.h)
add_simple_test(qxmppnonsaslauthiq)
add_simple_test(qxmpppushenableiq)
add_simple_test(qxmpppresence)
diff --git a/tests/qxmppmovedmanager/tst_qxmppmovedmanager.cpp b/tests/qxmppmovedmanager/tst_qxmppmovedmanager.cpp
new file mode 100644
index 000000000..a8775dedc
--- /dev/null
+++ b/tests/qxmppmovedmanager/tst_qxmppmovedmanager.cpp
@@ -0,0 +1,307 @@
+// SPDX-FileCopyrightText: 2024 Filipe Azevedo
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "QXmppDiscoveryManager.h"
+#include "QXmppMovedManager.h"
+#include "QXmppConstants_p.h"
+#include "QXmppPubSubManager.h"
+#ifdef BUILD_INTERNAL_TESTS
+#include "QXmppMovedItem_p.h"
+#endif
+
+#include "TestClient.h"
+
+struct Tester {
+ Tester()
+ {
+ client.addNewExtension();
+ client.addNewExtension();
+ manager = client.addNewExtension();
+ }
+
+ Tester(const QString &jid)
+ : Tester()
+ {
+ client.configuration().setJid(jid);
+ }
+
+ TestClient client;
+ QXmppMovedManager *manager;
+};
+
+class tst_QXmppMovedManager : public QObject
+{
+ Q_OBJECT
+
+private:
+#ifdef BUILD_INTERNAL_TESTS
+ Q_SLOT void testMovedItem();
+#endif
+ Q_SLOT void testMovedPresence();
+ Q_SLOT void testDiscoveryFeatures();
+ Q_SLOT void testSupportedByServer();
+ Q_SLOT void testResetCachedData();
+ Q_SLOT void testHandleDiscoInfo();
+ Q_SLOT void testOnRegistered();
+ Q_SLOT void testOnUnregistered();
+ Q_SLOT void testPublishMoved();
+ Q_SLOT void testVerifyMoved();
+ Q_SLOT void testNotify();
+
+ template
+ void testError(QXmppTask &task, TestClient &client, const QString &id, const QString &from);
+};
+
+#ifdef BUILD_INTERNAL_TESTS
+void tst_QXmppMovedManager::testMovedItem()
+{
+ const auto expected = u"- new@shakespeare.example
"_s;
+ const QDomElement expectedElement = xmlToDom(expected);
+
+ {
+ QXmppMovedItem packet;
+ packet.setNewJid(u"new@shakespeare.example"_s);
+
+ QCOMPARE(packetToXml(packet), expected);
+ }
+
+ {
+ QXmppMovedItem packet;
+ packet.parse(expectedElement);
+
+ QVERIFY(!packet.newJid().isEmpty());
+ }
+}
+#endif
+
+void tst_QXmppMovedManager::testMovedPresence()
+{
+ const auto expected =
+ u""
+ "old@shakespeare.example"
+ ""_s;
+ const QDomElement expectedElement = xmlToDom(expected);
+
+ {
+ QXmppPresence packet;
+ packet.setTo(u"contact@shakespeare.example"_s);
+ packet.setType(QXmppPresence::Subscribe);
+ packet.setOldJid(u"old@shakespeare.example"_s);
+
+ QCOMPARE(packetToXml(packet), expected);
+ }
+
+ {
+ QXmppPresence packet;
+ packet.parse(expectedElement);
+
+ QVERIFY(!packet.oldJid().isEmpty());
+ }
+}
+
+void tst_QXmppMovedManager::testDiscoveryFeatures()
+{
+ QXmppMovedManager manager;
+
+ QCOMPARE(manager.discoveryFeatures(), QStringList { ns_moved.toString() });
+}
+
+void tst_QXmppMovedManager::testSupportedByServer()
+{
+ QXmppMovedManager manager;
+ QSignalSpy spy(&manager, &QXmppMovedManager::supportedByServerChanged);
+
+ QVERIFY(!manager.supportedByServer());
+
+ manager.setSupportedByServer(true);
+
+ QVERIFY(manager.supportedByServer());
+ QCOMPARE(spy.size(), 1);
+}
+
+void tst_QXmppMovedManager::testResetCachedData()
+{
+ QXmppMovedManager manager;
+
+ manager.setSupportedByServer(true);
+ manager.resetCachedData();
+
+ QVERIFY(!manager.supportedByServer());
+}
+
+void tst_QXmppMovedManager::testHandleDiscoInfo()
+{
+ auto [client, manager] = Tester(u"hag66@shakespeare.example"_s);
+
+ QXmppDiscoveryIq iq;
+ iq.setFeatures({ ns_moved.toString() });
+
+ manager->handleDiscoInfo(iq);
+
+ QVERIFY(manager->supportedByServer());
+
+ iq.setFeatures({});
+
+ manager->handleDiscoInfo(iq);
+
+ QVERIFY(!manager->supportedByServer());
+}
+
+void tst_QXmppMovedManager::testOnRegistered()
+{
+ TestClient client;
+ QXmppMovedManager manager;
+
+ client.addNewExtension();
+ client.addNewExtension();
+ client.configuration().setJid(u"hag66@shakespeare.example"_s);
+ client.addExtension(&manager);
+
+ manager.setSupportedByServer(true);
+
+ client.setStreamManagementState(QXmppClient::NewStream);
+ Q_EMIT client.connected();
+
+ QVERIFY(!manager.supportedByServer());
+
+ QXmppDiscoveryIq iq;
+ iq.setFeatures({ ns_moved.toString() });
+ Q_EMIT manager.client()->findExtension()->infoReceived(iq);
+
+ QVERIFY(manager.supportedByServer());
+}
+
+void tst_QXmppMovedManager::testOnUnregistered()
+{
+ QXmppClient client;
+ QXmppMovedManager manager;
+
+ client.addNewExtension();
+ client.addNewExtension();
+ client.configuration().setJid(u"hag66@shakespeare.example"_s);
+ client.addExtension(&manager);
+
+ manager.setSupportedByServer(true);
+ manager.onUnregistered(&client);
+
+ QXmppDiscoveryIq iq;
+ iq.setFeatures({ ns_moved.toString() });
+ Q_EMIT manager.client()->findExtension()->infoReceived(iq);
+
+ QVERIFY(!manager.supportedByServer());
+
+ manager.setSupportedByServer(true);
+ Q_EMIT client.connected();
+
+ QVERIFY(manager.supportedByServer());
+}
+
+void tst_QXmppMovedManager::testPublishMoved()
+{
+ auto tester = Tester(u"old@shakespeare.example"_s);
+ auto &client = tester.client;
+ auto manager = tester.manager;
+
+ auto call = [manager]() {
+ return manager->publishStatement(u"moved@shakespeare.example"_s);
+ };
+
+ auto task = call();
+
+ client.expect(u""
+ ""
+ ""
+ "- "
+ ""
+ "moved@shakespeare.example"
+ ""
+ "
"
+ ""
+ ""
+ ""_s);
+ client.inject(u""
+ ""
+ ""
+ " "
+ ""
+ ""
+ ""_s);
+
+ expectFutureVariant(task);
+
+ testError(task = call(), client, u"qxmpp1"_s, u"old@shakespeare.example"_s);
+}
+
+void tst_QXmppMovedManager::testVerifyMoved()
+{
+ auto tester = Tester(u"contact@shakespeare.example"_s);
+ auto &client = tester.client;
+ auto manager = tester.manager;
+
+ auto call = [manager]() {
+ return manager->verifyStatement(u"old@shakespeare.example"_s, u"moved@shakespeare.example"_s);
+ };
+
+ auto task = call();
+
+ client.expect(u""
+ ""
+ ""
+ " "
+ ""
+ ""
+ ""_s);
+ client.inject(u""
+ ""
+ ""
+ "- "
+ ""
+ "moved@shakespeare.example"
+ ""
+ "
"
+ ""
+ ""
+ ""_s);
+
+ expectFutureVariant(task);
+
+ testError(task = call(), client, u"qxmpp1"_s, u"old@shakespeare.example"_s);
+}
+
+void tst_QXmppMovedManager::testNotify()
+{
+ auto tester = Tester(u"moved@shakespeare.example"_s);
+ auto &client = tester.client;
+ auto manager = tester.manager;
+
+ auto call = [manager]() {
+ return manager->notifyContact(u"contact@shakespeare.example"_s, u"old@shakespeare.example"_s, true, u"I moved."_s);
+ };
+
+ auto task = call();
+
+ client.expect(u""
+ "I moved."
+ ""
+ "old@shakespeare.example"
+ ""
+ ""_s);
+}
+
+template
+void tst_QXmppMovedManager::testError(QXmppTask &task, TestClient &client, const QString &id, const QString &from)
+{
+ client.ignore();
+ client.inject(u""
+ ""
+ ""
+ ""
+ ""_s
+ .arg(id, from));
+
+ expectFutureVariant(task);
+}
+
+QTEST_MAIN(tst_QXmppMovedManager)
+#include "tst_qxmppmovedmanager.moc"