diff options
| author | Tibor Csötönyi <work@taibsu.de> | 2023-05-03 14:46:38 +0200 |
|---|---|---|
| committer | Linus Jahn <lnj@kaidan.im> | 2023-05-14 23:58:01 +0200 |
| commit | a4dcd906850b5ebecfdf7331059a13192f094f07 (patch) | |
| tree | fa9a77ad38d1f702ec15672d907e4851e6c677c1 | |
| parent | 2fde987d39dc66f028ea3ff44929ebd6e2b37f90 (diff) | |
Add XEP-0353: Jingle Message Initiation manager
| -rw-r--r-- | doc/doap.xml | 8 | ||||
| -rw-r--r-- | src/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | src/client/QXmppJingleMessageInitiationManager.cpp | 584 | ||||
| -rw-r--r-- | src/client/QXmppJingleMessageInitiationManager.h | 126 | ||||
| -rw-r--r-- | tests/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | tests/qxmppjinglemessageinitiationmanager/tst_qxmppjinglemessageinitiationmanager.cpp | 924 |
6 files changed, 1645 insertions, 0 deletions
diff --git a/doc/doap.xml b/doc/doap.xml index e32eff05..0bbb1cc9 100644 --- a/doc/doap.xml +++ b/doc/doap.xml @@ -491,6 +491,14 @@ SPDX-License-Identifier: CC0-1.0 </implements> <implements> <xmpp:SupportedXep> + <xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0353.html'/> + <xmpp:status>complete</xmpp:status> + <xmpp:version>0.6</xmpp:version> + <xmpp:since>1.6</xmpp:since> + </xmpp:SupportedXep> + </implements> + <implements> + <xmpp:SupportedXep> <xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0357.html'/> <xmpp:status>complete</xmpp:status> <xmpp:version>0.4</xmpp:version> diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 929554c4..7014ae60 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -108,6 +108,7 @@ set(INSTALL_HEADER_FILES client/QXmppHttpUploadManager.h client/QXmppInvokable.h client/QXmppIqHandling.h + client/QXmppJingleMessageInitiationManager.h client/QXmppMamManager.h client/QXmppMessageHandler.h client/QXmppMessageReceiptManager.h @@ -243,6 +244,7 @@ set(SOURCE_FILES client/QXmppInternalClientExtension.cpp client/QXmppInvokable.cpp client/QXmppIqHandling.cpp + client/QXmppJingleMessageInitiationManager.cpp client/QXmppMamManager.cpp client/QXmppMessageReceiptManager.cpp client/QXmppMucManager.cpp diff --git a/src/client/QXmppJingleMessageInitiationManager.cpp b/src/client/QXmppJingleMessageInitiationManager.cpp new file mode 100644 index 00000000..d7a4cea1 --- /dev/null +++ b/src/client/QXmppJingleMessageInitiationManager.cpp @@ -0,0 +1,584 @@ +// SPDX-FileCopyrightText: 2023 Tibor Csötönyi <work@taibsu.de> +// SPDX-FileCopyrightText: 2023 Melvin Keskin <melvo@olomono.de> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "QXmppJingleMessageInitiationManager.h" + +#include "QXmppClient.h" +#include "QXmppConstants_p.h" +#include "QXmppMessage.h" +#include "QXmppPromise.h" +#include "QXmppUtils.h" + +#include <QStringBuilder> +#include <QUuid> + +using namespace QXmpp; +using Jmi = QXmppJingleMessageInitiation; +using JmiManager = QXmppJingleMessageInitiationManager; +using JmiElement = QXmppJingleMessageInitiationElement; +using JmiType = JmiElement::Type; + +class QXmppJingleMessageInitiationPrivate +{ +public: + QXmppJingleMessageInitiationPrivate(JmiManager *manager) + : manager(manager) + { + } + + QXmppTask<SendResult> request(JmiElement &&jmiElement); + + QXmppJingleMessageInitiationManager *manager; + QString id; + QString callPartnerJid; + bool isProceeded { false }; +}; + +/// +/// \brief Creates a Jingle Message Initiation request based on given type. +/// \param type The request type (proceed, accept, reject, retract, finish). +/// +QXmppTask<SendResult> QXmppJingleMessageInitiationPrivate::request(JmiElement &&jmiElement) +{ + jmiElement.setId(id); + return manager->sendMessage(jmiElement, callPartnerJid); +} + +/// +/// \class QXmppJingleMessageInitiation +/// +/// \brief The QXmppJingleMessageInitiation class holds information about the JMI element in the +/// current context. +/// +/// \since QXmpp 1.6 +/// + +/// +/// \brief Constructs a Jingle Message Initiation object. +/// +QXmppJingleMessageInitiation::QXmppJingleMessageInitiation(QXmppJingleMessageInitiationManager *manager) + : d(new QXmppJingleMessageInitiationPrivate(manager)) +{ +} + +QXmppJingleMessageInitiation::~QXmppJingleMessageInitiation() = default; + +/// +/// Creates a JMI element of type "ringing" and sends a request containing the element. +/// +QXmppTask<SendResult> QXmppJingleMessageInitiation::ring() +{ + QXmppJingleMessageInitiationElement jmiElement; + jmiElement.setType(JmiType::Ringing); + + return d->request(std::move(jmiElement)); +} + +/// +/// Creates a JMI element of type "proceed" and sends a request containing the element. +/// +QXmppTask<SendResult> QXmppJingleMessageInitiation::proceed() +{ + QXmppJingleMessageInitiationElement jmiElement; + jmiElement.setType(JmiType::Proceed); + + return d->request(std::move(jmiElement)); +} + +/// +/// Creates a JMI element of type "reject" and sends a request containing the element. +/// The default reason tag/type will be "busy" with text "Busy". +/// +/// \param reason Reason object for reject element +/// \param containsTieBreak Whether the reject element contains a tie-break tag or not +/// +QXmppTask<SendResult> QXmppJingleMessageInitiation::reject(std::optional<QXmppJingleReason> reason, bool containsTieBreak) +{ + JmiElement jmiElement; + jmiElement.setType(JmiType::Reject); + + if (!reason) { + reason = QXmppJingleReason(); + reason->setType(QXmppJingleReason::Busy); + reason->setText(QStringLiteral("Busy")); + } + + jmiElement.setReason(reason); + jmiElement.setContainsTieBreak(containsTieBreak); + + return d->request(std::move(jmiElement)); +} + +/// +/// Creates a JMI element of type "retract" and sends a request containing the element. +/// The default reason tag/type will be "cancel" with text "Retracted". +/// +/// \param reason Reason object for retract element +/// \param containsTieBreak Whether the retract element contains a tie-break tag or not +/// +QXmppTask<SendResult> QXmppJingleMessageInitiation::retract(std::optional<QXmppJingleReason> reason, bool containsTieBreak) +{ + JmiElement jmiElement; + jmiElement.setType(JmiType::Retract); + + if (!reason) { + reason = QXmppJingleReason(); + reason->setType(QXmppJingleReason::Cancel); + reason->setText(QStringLiteral("Retracted")); + } + + jmiElement.setReason(reason); + jmiElement.setContainsTieBreak(containsTieBreak); + + return d->request(std::move(jmiElement)); +} + +/// +/// Creates a JMI element of type "finish" and sends a request containing the element. +/// The default reason type/tag will be "success" with text "Success". +/// +/// \param reason Reason object for finish element +/// \param migratedTo JMI id the session has been migrated to +/// +QXmppTask<SendResult> QXmppJingleMessageInitiation::finish(std::optional<QXmppJingleReason> reason, const QString &migratedTo) +{ + JmiElement jmiElement; + jmiElement.setType(JmiType::Finish); + + if (!reason) { + reason = QXmppJingleReason(); + reason->setType(QXmppJingleReason::Success); + reason->setText(QStringLiteral("Success")); + } + + jmiElement.setReason(reason); + jmiElement.setMigratedTo(migratedTo); + + return d->request(std::move(jmiElement)); +} + +/// +/// Returns the JMI ID. +/// +QString QXmppJingleMessageInitiation::id() const +{ + return d->id; +} + +/// +/// Sets the JMI ID. +/// +void QXmppJingleMessageInitiation::setId(const QString &id) +{ + d->id = id; +} + +/// +/// Sets the call partner's bare JID. +/// +/// Normally, the JMI ID would be sufficient in order to differentiate the JMIs. +/// However, attackers pretending to be the call partner can be mitigated by caching the call +/// partner's JID. +/// +/// \param callPartnerJid bare JID of the call partner +/// +void QXmppJingleMessageInitiation::setCallPartnerJid(const QString &callPartnerJid) +{ + d->callPartnerJid = callPartnerJid; +} + +/// +/// Returns the call partner's bare JID. +/// +/// \return the call partner's bare JID. +/// +QString QXmppJingleMessageInitiation::callPartnerJid() const +{ + return d->callPartnerJid; +} + +/// +/// Returns the "isProceeded" flag, e.g., if the Jingle Message Initiation has already been +/// proceeded. +/// +bool QXmppJingleMessageInitiation::isProceeded() const +{ + return d->isProceeded; +} + +/// +/// Sets the "isProceeded" flag, e.g., if the Jingle Message Initiation has already been +/// proceeded. +/// +void QXmppJingleMessageInitiation::setIsProceeded(bool isProceeded) +{ + d->isProceeded = isProceeded; +} + +/// +/// \fn QXmppJingleMessageInitiation::ringing() +/// +/// Emitted when a propose request was accepted and the device starts ringing. +/// + +/// +/// \fn QXmppJingleMessageInitiation::proceeded(const QString &, const QString &) +/// +/// Emitted when a propose request was successfully processed and accepted. +/// +/// \param id belonging JMI id +/// \param callPartnerResource resource of the call partner about to be called +/// + +/// +/// \fn QXmppJingleMessageInitiation::closed(const Result &) +/// +/// Emitted when a call was ended either through rejection, retraction, finish or an error. +/// +/// \param result close reason +/// + +class QXmppJingleMessageInitiationManagerPrivate +{ +public: + QVector<std::shared_ptr<Jmi>> jmis; +}; + +/// +/// \typedef QXmppJingleMessageInitiationManager::ProposeResult +/// +/// Contains JMI object or an error if sending the propose message failed. +/// + +/// +/// \class QXmppJingleMessageInitiationManager +/// +/// \brief The QXmppJingleMessageInitiationManager class makes it possible to retrieve +/// Jingle Message Initiation elements as defined by \xep{0353, Jingle Message Initiation}. +/// +/// \since QXmpp 1.6 +/// +QXmppJingleMessageInitiationManager::QXmppJingleMessageInitiationManager() + : d(std::make_unique<QXmppJingleMessageInitiationManagerPrivate>()) +{ +} + +QXmppJingleMessageInitiationManager::~QXmppJingleMessageInitiationManager() = default; + +/// \cond +QStringList QXmppJingleMessageInitiationManager::discoveryFeatures() const +{ + return { ns_jingle_message_initiation }; +} +/// \endcond + +/// +/// Creates a proposal JMI element and passes it as a message. +/// +QXmppTask<QXmppJingleMessageInitiationManager::ProposeResult> QXmppJingleMessageInitiationManager::propose(const QString &callPartnerJid, const QXmppJingleDescription &description) +{ + QXmppPromise<ProposeResult> promise; + + QXmppJingleMessageInitiationElement jmiElement; + jmiElement.setType(JmiType::Propose); + jmiElement.setId(QXmppUtils::generateStanzaUuid()); + jmiElement.setDescription(description); + + sendMessage(jmiElement, callPartnerJid).then(this, [this, promise, callPartnerJid](SendResult result) mutable { + if (auto error = std::get_if<QXmppError>(&result)) { + warning(u"Error sending Jingle Message Initiation proposal: " % error->description); + promise.finish(*error); + } else { + promise.finish(addJmi(callPartnerJid)); + } + }); + + return promise.task(); +} + +/// \cond +bool QXmppJingleMessageInitiationManager::handleMessage(const QXmppMessage &message) +{ + // JMI messages must be of type "chat" and contain a <store/> hint. + if (message.type() != QXmppMessage::Chat || !message.hasHint(QXmppMessage::Store)) { + return false; + } + + // Only continue if the message contains a JMI element. + if (auto jmiElement = message.jingleMessageInitiationElement()) { + return handleJmiElement(std::move(*jmiElement), message.from()); + } + + return false; +} + +void QXmppJingleMessageInitiationManager::setClient(QXmppClient *client) +{ + QXmppClientExtension::setClient(client); +} +/// \endcond + +/// +/// Lets the client send a message to user with given callPartnerJid containing the JMI element. +/// +/// \param jmiElement the JMI element to be passed +/// \param callPartnerJid bare JID of the call partner +/// +QXmppTask<SendResult> QXmppJingleMessageInitiationManager::sendMessage(const QXmppJingleMessageInitiationElement &jmiElement, const QString &callPartnerJid) +{ + QXmppMessage message; + message.setTo(callPartnerJid); + message.addHint(QXmppMessage::Store); + message.setJingleMessageInitiationElement(jmiElement); + + return client()->send(std::move(message)); +} + +/// +/// Removes a JMI object from the JMIs vector. +/// +/// \param jmi object to be removed +/// +void QXmppJingleMessageInitiationManager::clear(const std::shared_ptr<QXmppJingleMessageInitiation> &jmi) +{ + d->jmis.erase( + std::remove_if( + d->jmis.begin(), + d->jmis.end(), + [&jmi](const auto &storedJmi) { + return jmi->id() == storedJmi->id() && jmi->callPartnerJid() == storedJmi->callPartnerJid(); + }), + d->jmis.end()); +} + +/// +/// Removes all JMI objects from the JMI vector. +/// +void QXmppJingleMessageInitiationManager::clearAll() +{ + d->jmis.clear(); +} + +bool QXmppJingleMessageInitiationManager::handleJmiElement(QXmppJingleMessageInitiationElement &&jmiElement, const QString &senderJid) +{ + auto jmiElementId = jmiElement.id(); + auto callPartnerJid = QXmppUtils::jidToBareJid(senderJid); + + // Check if there's already a JMI object with jmiElementId and callPartnerJid in JMIs vector. + // That means that a JMI has already been created with given (J)IDs. + auto itr = std::find_if(d->jmis.begin(), d->jmis.end(), [&jmiElementId, &callPartnerJid](const auto &jmi) { + return jmi->id() == jmiElementId && jmi->callPartnerJid() == callPartnerJid; + }); + + if (itr != d->jmis.end()) { + return handleExistingJmi(*itr, jmiElement, QXmppUtils::jidToResource(senderJid)); + } + + if (jmiElement.type() == JmiType::Propose) { + return handleProposeJmiElement(jmiElement, callPartnerJid); + } + + return false; +} + +/// +/// Handles a JMI object which already exists in the JMIs vector. +/// +/// \param existingJmi JMI object to be handled +/// \param jmiElement JMI element to be processed with the JMI object +/// \param callPartnerResource resource of the call partner (i.e., phone, tablet etc.) +/// \return success (true) or failure +/// +bool QXmppJingleMessageInitiationManager::handleExistingJmi(const std::shared_ptr<Jmi> &existingJmi, const QXmppJingleMessageInitiationElement &jmiElement, const QString &callPartnerResource) +{ + switch (jmiElement.type()) { + case JmiType::Ringing: + Q_EMIT existingJmi->ringing(); + return true; + case JmiType::Proceed: + Q_EMIT existingJmi->proceeded(jmiElement.id(), callPartnerResource); + existingJmi->setIsProceeded(true); + return true; + case JmiType::Reject: + Q_EMIT existingJmi->closed( + Jmi::Rejected { jmiElement.reason(), jmiElement.containsTieBreak() }); + return true; + case JmiType::Retract: + Q_EMIT existingJmi->closed( + Jmi::Retracted { jmiElement.reason(), jmiElement.containsTieBreak() }); + return true; + case JmiType::Finish: + existingJmi->finish(jmiElement.reason(), jmiElement.migratedTo()); + Q_EMIT existingJmi->closed( + Jmi::Finished { jmiElement.reason(), jmiElement.migratedTo() }); + return true; + default: + return false; + } +} + +/// +/// Handles a propose JMI element. +/// +/// \param jmiElement to be handled +/// \param callPartnerJid bare JID of the call partner +/// \return success (true) or failure +/// +bool QXmppJingleMessageInitiationManager::handleProposeJmiElement(const QXmppJingleMessageInitiationElement &jmiElement, const QString &callPartnerJid) +{ + // Check if there's already a JMI object with provided callPartnerJid in JMIs vector. + // That means that a propose has already been sent. + auto itr = std::find_if( + d->jmis.cbegin(), + d->jmis.cend(), + [&callPartnerJid](const auto &jmi) { + return jmi->callPartnerJid() == callPartnerJid; + }); + + // Tie break case or usual JMI proposal? + if (itr != d->jmis.end()) { + return handleTieBreak(*itr, jmiElement, callPartnerJid); + } + + Q_EMIT proposed(addJmi(callPartnerJid), jmiElement.id(), jmiElement.description()); + return true; +} + +/// +/// Handles a tie break case as defined in https://xmpp.org/extensions/xep-0353.html#tie-breaking. +/// \param existingJmi existing JMI object to be handled +/// \param jmiElement JMI element to be processed with existing JMI object +/// \param callPartnerResource resource of the call partner (i.e., phone, tablet etc.) +/// \return success (true) or failure +/// +bool QXmppJingleMessageInitiationManager::handleTieBreak(const std::shared_ptr<Jmi> &existingJmi, const QXmppJingleMessageInitiationElement &jmiElement, const QString &callPartnerResource) +{ + // Tie break -> session is set to be expired + QXmppJingleReason reason; + reason.setType(QXmppJingleReason::Expired); + + // Existing (proceeded) or non-existing session? + if (existingJmi->isProceeded()) { + return handleExistingSession(existingJmi, jmiElement.id()); + } + + // Tie break in propose state (no existing session) - two parties try calling each other + // at the same time, the proposal with the lower ID overrules the other one. + return handleNonExistingSession(existingJmi, jmiElement.id(), callPartnerResource); +} + +/// +/// Device switch: session already exists and will be migrated to new device with id jmiElementId. +/// +/// \param existingJmi Current JMI object +/// \param jmiElementId New (counterpart's) session JMI element ID +/// +bool QXmppJingleMessageInitiationManager::handleExistingSession(const std::shared_ptr<Jmi> &existingJmi, const QString &jmiElementId) +{ + // Old session will be finished with reason "Expired". + QXmppJingleReason reason; + reason.setType(QXmppJingleReason::Expired); + reason.setText(QStringLiteral("Session migrated")); + + // Tell the old session to be finished. + Q_EMIT existingJmi->closed(Jmi::Finished { reason, jmiElementId }); + + existingJmi->finish(reason, jmiElementId).then(this, [this, existingJmi, jmiElementId](SendResult result) { + if (auto *error = std::get_if<QXmppError>(&result)) { + Q_EMIT existingJmi->closed(*error); + } else { + // Then, proceed (accept) the new proposal and set the JMI ID + // to the ID of the received JMI element. + existingJmi->setId(jmiElementId); + existingJmi->proceed().then(this, [existingJmi](SendResult result) { + if (auto *error = std::get_if<QXmppError>(&result)) { + Q_EMIT existingJmi->closed(*error); + } else { + // The session is now closed as it is finished. + existingJmi->setIsProceeded(true); + } + }); + } + }); + + return true; +} + +/// +/// \brief Tie break in propose state (no existing session) - two parties try calling each other +/// at the same time, the proposal with the lower ID overrules the other one. +/// +/// \param existingJmi Current JMI object +/// \param jmiElementId Counterpart's JMI element ID +/// +bool QXmppJingleMessageInitiationManager::handleNonExistingSession(const std::shared_ptr<Jmi> &existingJmi, const QString &jmiElementId, const QString &callPartnerResource) +{ + QXmppJingleReason reason; + reason.setType(QXmppJingleReason::Expired); + reason.setText(QStringLiteral("Tie-Break")); + + if (QUuid::fromString(existingJmi->id()) < QUuid::fromString(jmiElementId)) { + // Jingle message initiator with lower ID rejects the other proposal. + existingJmi->setId(jmiElementId); + existingJmi->reject(std::move(reason), true).then(this, [existingJmi](auto result) { + if (auto *error = std::get_if<QXmppError>(&result)) { + Q_EMIT existingJmi->closed(*error); + } + }); + } else { + // Jingle message initiator with higher ID retracts its proposal. + existingJmi->retract(std::move(reason), true).then(this, [this, existingJmi, jmiElementId, callPartnerResource](auto result) { + if (auto error = std::get_if<QXmppError>(&result)) { + Q_EMIT existingJmi->closed(*error); + } else { + // Afterwards, JMI ID is changed to lower ID. + existingJmi->setId(jmiElementId); + + // Finally, the call is being accepted. + existingJmi->proceed().then(this, [existingJmi, jmiElementId, callPartnerResource](SendResult result) { + if (auto *error = std::get_if<QXmppError>(&result)) { + Q_EMIT existingJmi->closed(*error); + } else { + existingJmi->setIsProceeded(true); + Q_EMIT existingJmi->proceeded(jmiElementId, callPartnerResource); + } + }); + } + }); + } + + return true; +} + +/// +/// Adds a JMI object to the JMIs vector and sets the bare JID of the call partner in the JMI object. +/// \param callPartnerJid bare JID of the call partner +/// \return The newly created JMI +/// +std::shared_ptr<QXmppJingleMessageInitiation> QXmppJingleMessageInitiationManager::addJmi(const QString &callPartnerJid) +{ + auto jmi { std::make_shared<QXmppJingleMessageInitiation>(this) }; + jmi->setCallPartnerJid(callPartnerJid); + d->jmis.append(jmi); + return jmi; +} + +/// +/// Returns the JMIs vector. +/// +const QVector<std::shared_ptr<QXmppJingleMessageInitiation>> &QXmppJingleMessageInitiationManager::jmis() const +{ + return d->jmis; +} + +/// +/// \fn QXmppJingleMessageInitiationManager::proposed(const std::shared_ptr<QXmppJingleMessageInitiation> &, const QString &, const QXmppJingleDescription &) +/// +/// Emitted when a call has been proposed. +/// +/// \param jmi Jingle Message Initiation object of proposed session +/// \param id JMI element id +/// \param description JMI element's description containing media type (i.e., audio, video) +/// diff --git a/src/client/QXmppJingleMessageInitiationManager.h b/src/client/QXmppJingleMessageInitiationManager.h new file mode 100644 index 00000000..cd5bb494 --- /dev/null +++ b/src/client/QXmppJingleMessageInitiationManager.h @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: 2023 Tibor Csötönyi <work@taibsu.de> +// SPDX-FileCopyrightText: 2023 Melvin Keskin <melvo@olomono.de> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifndef QXMPPJINGLEMESSAGEINITIATIONMANAGER_H +#define QXMPPJINGLEMESSAGEINITIATIONMANAGER_H + +#include "QXmppClientExtension.h" +#include "QXmppError.h" +#include "QXmppJingleIq.h" +#include "QXmppMessageHandler.h" +#include "QXmppSendResult.h" +#include "QXmppTask.h" + +class QXmppJingleMessageInitiationManager; +class QXmppJingleMessageInitiationPrivate; +class QXmppJingleMessageInitiationManagerPrivate; + +class QXMPP_EXPORT QXmppJingleMessageInitiation : public QObject +{ + Q_OBJECT +public: + struct Rejected + { + std::optional<QXmppJingleReason> reason; + bool containsTieBreak; + }; + + struct Retracted + { + std::optional<QXmppJingleReason> reason; + bool containsTieBreak; + }; + + struct Finished + { + std::optional<QXmppJingleReason> reason; + QString migratedTo; + }; + + /// Variant of Rejected, Retracted, Finished or Error result types + using Result = std::variant<Rejected, Retracted, Finished, QXmppError>; + + QXmppJingleMessageInitiation(QXmppJingleMessageInitiationManager *manager); + ~QXmppJingleMessageInitiation(); + + QXmppTask<QXmpp::SendResult> ring(); + QXmppTask<QXmpp::SendResult> proceed(); + QXmppTask<QXmpp::SendResult> reject(std::optional<QXmppJingleReason> reason, bool containsTieBreak = false); + QXmppTask<QXmpp::SendResult> retract(std::optional<QXmppJingleReason> reason, bool containsTieBreak = false); + QXmppTask<QXmpp::SendResult> finish(std::optional<QXmppJingleReason> reason, const QString &migratedTo = {}); + + Q_SIGNAL void ringing(); + Q_SIGNAL void proceeded(const QString &id, const QString &callPartnerResource); + Q_SIGNAL void closed(const Result &result); + +private: + QString id() const; + void setId(const QString &id); + void setCallPartnerJid(const QString &callPartnerJid); + QString callPartnerJid() const; + bool isProceeded() const; + void setIsProceeded(bool isProceeded); + + std::unique_ptr<QXmppJingleMessageInitiationPrivate> d; + + friend class QXmppJingleMessageInitiationManager; + friend class tst_QXmppJingleMessageInitiationManager; +}; + +class QXMPP_EXPORT QXmppJingleMessageInitiationManager : public QXmppClientExtension, public QXmppMessageHandler +{ + Q_OBJECT +public: + using ProposeResult = std::variant<std::shared_ptr<QXmppJingleMessageInitiation>, QXmppError>; + + QXmppJingleMessageInitiationManager(); + ~QXmppJingleMessageInitiationManager(); + + /// \cond + QStringList discoveryFeatures() const override; + /// \endcond + + QXmppTask<ProposeResult> propose( + const QString &callPartnerJid, + const QXmppJingleDescription &description); + + Q_SIGNAL void proposed( + const std::shared_ptr<QXmppJingleMessageInitiation> &jmi, + const QString &id, + const std::optional<QXmppJingleDescription> &description); + +protected: + /// \cond + bool handleMessage(const QXmppMessage &) override; + void setClient(QXmppClient *client) override; + /// \endcond + +private: + QXmppTask<QXmpp::SendResult> sendMessage( + const QXmppJingleMessageInitiationElement &jmiElement, + const QString &callPartnerJid); + + void clear(const std::shared_ptr<QXmppJingleMessageInitiation> &jmi); + void clearAll(); + + bool handleJmiElement(QXmppJingleMessageInitiationElement &&jmiElement, const QString &senderJid); + bool handleExistingJmi(const std::shared_ptr<QXmppJingleMessageInitiation> &existingJmi, const QXmppJingleMessageInitiationElement &jmiElement, const QString &callPartnerResource); + bool handleProposeJmiElement(const QXmppJingleMessageInitiationElement &jmiElement, const QString &callPartnerJid); + bool handleTieBreak(const std::shared_ptr<QXmppJingleMessageInitiation> &existingJmi, const QXmppJingleMessageInitiationElement &jmiElement, const QString &callPartnerResource); + bool handleExistingSession(const std::shared_ptr<QXmppJingleMessageInitiation> &existingJmi, const QString &jmiElementId); + bool handleNonExistingSession(const std::shared_ptr<QXmppJingleMessageInitiation> &existingJmi, const QString &jmiElementId, const QString &callPartnerResource); + std::shared_ptr<QXmppJingleMessageInitiation> addJmi(const QString &callPartnerJid); + const QVector<std::shared_ptr<QXmppJingleMessageInitiation>> &jmis() const; + +private: + std::unique_ptr<QXmppJingleMessageInitiationManagerPrivate> d; + + friend class QXmppJingleMessageInitiationPrivate; + friend class tst_QXmppJingleMessageInitiationManager; +}; + +Q_DECLARE_METATYPE(QXmppJingleMessageInitiation::Result) + +#endif // QXMPPJINGLEMESSAGEINITIATIONMANAGER_H diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ce2ca155..ee5064be 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -42,6 +42,7 @@ add_simple_test(qxmpphttpuploadiq) add_simple_test(qxmppiceconnection) add_simple_test(qxmppiq) add_simple_test(qxmppjingleiq) +add_simple_test(qxmppjinglemessageinitiationmanager) add_simple_test(qxmppmammanager) add_simple_test(qxmppmixinvitation) add_simple_test(qxmppmixitems) diff --git a/tests/qxmppjinglemessageinitiationmanager/tst_qxmppjinglemessageinitiationmanager.cpp b/tests/qxmppjinglemessageinitiationmanager/tst_qxmppjinglemessageinitiationmanager.cpp new file mode 100644 index 00000000..00b599aa --- /dev/null +++ b/tests/qxmppjinglemessageinitiationmanager/tst_qxmppjinglemessageinitiationmanager.cpp @@ -0,0 +1,924 @@ +// SPDX-FileCopyrightText: 2023 Tibor Csötönyi <work@taibsu.de> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "QXmppClient.h" +#include "QXmppConstants_p.h" +#include "QXmppJingleMessageInitiationManager.h" +#include "QXmppMessage.h" +#include "QXmppUtils.h" + +#include "IntegrationTesting.h" +#include "util.h" +#include <QTest> + +using Jmi = QXmppJingleMessageInitiation; +using JmiType = QXmppJingleMessageInitiationElement::Type; +using Result = QXmppJingleMessageInitiation::Result; + +class tst_QXmppJingleMessageInitiationManager : public QObject +{ + Q_OBJECT + +private: + Q_SLOT void initTestCase(); + + Q_SLOT void testClear(); + Q_SLOT void testClearAll(); + + Q_SLOT void testRing(); + Q_SLOT void testProceed(); + Q_SLOT void testReject(); + Q_SLOT void testRetract(); + Q_SLOT void testFinish(); + + Q_SLOT void testPropose(); + Q_SLOT void testSendMessage(); + Q_SLOT void testHandleNonExistingSessionLowerId(); + Q_SLOT void testHandleNonExistingSessionHigherId(); + Q_SLOT void testHandleExistingSession(); + Q_SLOT void testHandleTieBreak(); + Q_SLOT void testHandleProposeJmiElement(); + Q_SLOT void testHandleExistingJmi(); + Q_SLOT void testHandleJmiElement(); + Q_SLOT void testHandleMessage_data(); + Q_SLOT void testHandleMessage(); + Q_SLOT void testHandleMessageRinging(); + Q_SLOT void testHandleMessageProceeded(); + Q_SLOT void testHandleMessageClosedRejected(); + Q_SLOT void testHandleMessageClosedRetracted(); + Q_SLOT void testHandleMessageClosedFinished(); + + QXmppClient m_client; + QXmppLogger m_logger; + QXmppJingleMessageInitiationManager m_manager; +}; + +void tst_QXmppJingleMessageInitiationManager::initTestCase() +{ + m_client.addExtension(&m_manager); + + m_logger.setLoggingType(QXmppLogger::SignalLogging); + m_client.setLogger(&m_logger); + + m_client.connectToServer(IntegrationTests::clientConfiguration()); + + qRegisterMetaType<QXmppJingleMessageInitiation::Result>(); +} + +void tst_QXmppJingleMessageInitiationManager::testClear() +{ + QCOMPARE(m_manager.jmis().size(), 0); + auto jmi1 { m_manager.addJmi("test1") }; + auto jmi2 { m_manager.addJmi("test2") }; + QCOMPARE(m_manager.jmis().size(), 2); + + m_manager.clear(jmi1); + m_manager.clear(jmi2); + QCOMPARE(m_manager.jmis().size(), 0); +} + +void tst_QXmppJingleMessageInitiationManager::testClearAll() +{ + QCOMPARE(m_manager.jmis().size(), 0); + m_manager.addJmi("test1"); + m_manager.addJmi("test2"); + m_manager.addJmi("test3"); + m_manager.addJmi("test4"); + m_manager.addJmi("test5"); + QCOMPARE(m_manager.jmis().size(), 5); + + m_manager.clearAll(); + QCOMPARE(m_manager.jmis().size(), 0); +} + +void tst_QXmppJingleMessageInitiationManager::testRing() +{ + auto jmi { m_manager.addJmi("julietRing@capulet.example") }; + jmi->setId("ca3cf894-5325-482f-a412-a6e9f832298d"); + + connect(&m_logger, &QXmppLogger::message, this, [jmicallPartnerJid = jmi->callPartnerJid()](QXmppLogger::MessageType type, const QString &text) { + if (type == QXmppLogger::SentMessage) { + QXmppMessage message; + parsePacket(message, text.toUtf8()); + + if (message.to() == jmicallPartnerJid) { + QVERIFY(message.jingleMessageInitiationElement()); + QCOMPARE(message.jingleMessageInitiationElement()->type(), JmiType::Ringing); + } + } + }); + + auto future = jmi->ring(); + + while (!future.isFinished()) { + QCoreApplication::processEvents(); + } + + QVERIFY(future.isFinished()); + m_manager.clearAll(); +} + +void tst_QXmppJingleMessageInitiationManager::testProceed() +{ + auto jmi { m_manager.addJmi("julietProceed@capulet.example") }; + jmi->setId("ca3cf894-5325-482f-a412-a6e9f832298d"); + + connect(&m_logger, &QXmppLogger::message, this, [jmiCallPartnerJid = jmi->callPartnerJid()](QXmppLogger::MessageType type, const QString &text) { + if (type == QXmppLogger::SentMessage) { + QXmppMessage message; + parsePacket(message, text.toUtf8()); + + if (message.to() == jmiCallPartnerJid) { + QVERIFY(message.jingleMessageInitiationElement()); + QCOMPARE(message.jingleMessageInitiationElement()->type(), JmiType::Proceed); + } + } + }); + + auto future = jmi->proceed(); + + while (!future.isFinished()) { + QCoreApplication::processEvents(); + } + + QVERIFY(future.isFinished()); + m_manager.clearAll(); +} + +void tst_QXmppJingleMessageInitiationManager::testReject() +{ + auto jmi { m_manager.addJmi("julietReject@capulet.example") }; + jmi->setId("ca3cf894-5325-482f-a412-a6e9f832298d"); + + QXmppJingleReason reason; + reason.setType(QXmppJingleReason::Decline); + reason.setText("Declined"); + reason.setNamespaceUri(ns_jingle); + + connect(&m_logger, &QXmppLogger::message, this, [jmiCallPartnerJid = jmi->callPartnerJid()](QXmppLogger::MessageType type, const QString &text) { + if (type == QXmppLogger::SentMessage) { + QXmppMessage message; + parsePacket(message, text.toUtf8()); + + if (message.to() == jmiCallPartnerJid) { + QVERIFY(message.jingleMessageInitiationElement()); + QCOMPARE(message.jingleMessageInitiationElement()->type(), JmiType::Reject); + QCOMPARE(message.jingleMessageInitiationElement()->reason()->type(), QXmppJingleReason::Decline); + QCOMPARE(message.jingleMessageInitiationElement()->reason()->text(), "Declined"); + QCOMPARE(message.jingleMessageInitiationElement()->reason()->namespaceUri(), ns_jingle); + QCOMPARE(message.jingleMessageInitiationElement()->containsTieBreak(), true); + } + } + }); + + auto future = jmi->reject(reason, true); + + while (!future.isFinished()) { + QCoreApplication::processEvents(); + } + + QVERIFY(future.isFinished()); + m_manager.clearAll(); +} + +void tst_QXmppJingleMessageInitiationManager::testRetract() +{ + auto jmi { m_manager.addJmi("julietRetract@capulet.example") }; + jmi->setId("ca3cf894-5325-482f-a412-a6e9f832298d"); + + QXmppJingleReason reason; + reason.setType(QXmppJingleReason::Gone); + reason.setText("Gone"); + reason.setNamespaceUri(ns_jingle); + + connect(&m_logger, &QXmppLogger::message, this, [jmicallPartnerJid = jmi->callPartnerJid()](QXmppLogger::MessageType type, const QString &text) { + if (type == QXmppLogger::SentMessage) { + QXmppMessage message; + parsePacket(message, text.toUtf8()); + + if (message.to() == jmicallPartnerJid) { + QVERIFY(message.jingleMessageInitiationElement()); + QCOMPARE(message.jingleMessageInitiationElement()->type(), JmiType::Retract); + QCOMPARE(message.jingleMessageInitiationElement()->reason()->type(), QXmppJingleReason::Gone); + QCOMPARE(message.jingleMessageInitiationElement()->reason()->text(), "Gone"); + QCOMPARE(message.jingleMessageInitiationElement()->reason()->namespaceUri(), ns_jingle); + QCOMPARE(message.jingleMessageInitiationElement()->containsTieBreak(), true); + } + } + }); + + auto future = jmi->retract(reason, true); + + while (!future.isFinished()) { + QCoreApplication::processEvents(); + } + + QVERIFY(future.isFinished()); + m_manager.clearAll(); +} + +void tst_QXmppJingleMessageInitiationManager::testFinish() +{ + auto jmi { m_manager.addJmi("julietFinish@capulet.example") }; + jmi->setId("ca3cf894-5325-482f-a412-a6e9f832298d"); + + QXmppJingleReason reason; + reason.setType(QXmppJingleReason::Success); + reason.setText("Finished"); + reason.setNamespaceUri(ns_jingle); + + connect(&m_logger, &QXmppLogger::message, this, [jmicallPartnerJid = jmi->callPartnerJid()](QXmppLogger::MessageType type, const QString &text) { + if (type == QXmppLogger::SentMessage) { + QXmppMessage message; + parsePacket(message, text.toUtf8()); + + if (message.to() == jmicallPartnerJid) { + QVERIFY(message.jingleMessageInitiationElement()); + QCOMPARE(message.jingleMessageInitiationElement()->type(), JmiType::Finish); + QCOMPARE(message.jingleMessageInitiationElement()->reason()->type(), QXmppJingleReason::Success); + QCOMPARE(message.jingleMessageInitiationElement()->reason()->text(), "Finished"); + QCOMPARE(message.jingleMessageInitiationElement()->reason()->namespaceUri(), ns_jingle); + QCOMPARE(message.jingleMessageInitiationElement()->migratedTo(), "fecbea35-08d3-404f-9ec7-2b57c566fa74"); + } + } + }); + + auto future = jmi->finish(reason, "fecbea35-08d3-404f-9ec7-2b57c566fa74"); + + while (!future.isFinished()) { + QCoreApplication::processEvents(); + } + + QVERIFY(future.isFinished()); + m_manager.clearAll(); +} + +void tst_QXmppJingleMessageInitiationManager::testPropose() +{ + QString jid { "julietPropose@capulet.example" }; + + QXmppJingleDescription description; + description.setMedia(QStringLiteral("audio")); + description.setSsrc(123); + description.setType(ns_jingle_rtp); + + connect(&m_logger, &QXmppLogger::message, this, [&, jid, description](QXmppLogger::MessageType type, const QString &text) { + if (type == QXmppLogger::SentMessage) { + QXmppMessage message; + parsePacket(message, text.toUtf8()); + + if (message.to() == jid) { + const auto &jmiElement { message.jingleMessageInitiationElement() }; + QVERIFY(jmiElement); + + QCOMPARE(jmiElement->type(), JmiType::Propose); + QVERIFY(!jmiElement->id().isEmpty()); + QVERIFY(jmiElement->description()); + QCOMPARE(jmiElement->description()->media(), description.media()); + QCOMPARE(jmiElement->description()->ssrc(), description.ssrc()); + QCOMPARE(jmiElement->description()->type(), description.type()); + + SKIP_IF_INTEGRATION_TESTS_DISABLED() + + // verify that the JMI ID has been changed and the JMI was processed + QCOMPARE(m_manager.jmis().size(), 1); + } + } + }); + + auto future = m_manager.propose(jid, description); + + while (!future.isFinished()) { + QCoreApplication::processEvents(); + } + + QVERIFY(future.isFinished()); + m_manager.clearAll(); +} + +void tst_QXmppJingleMessageInitiationManager::testSendMessage() +{ + QString jid { "julietSendMessage@capulet.example" }; + + QXmppJingleMessageInitiationElement jmiElement; + jmiElement.setType(JmiType::Propose); + jmiElement.setId(QStringLiteral("fecbea35-08d3-404f-9ec7-2b57c566fa74")); + + connect(&m_logger, &QXmppLogger::message, this, [jid, jmiElement](QXmppLogger::MessageType type, const QString &text) { + if (type == QXmppLogger::SentMessage) { + QXmppMessage message; + parsePacket(message, text.toUtf8()); + + if (message.to() == jid) { + QVERIFY(message.hasHint(QXmppMessage::Store)); + QVERIFY(message.jingleMessageInitiationElement()); + QCOMPARE(message.jingleMessageInitiationElement()->type(), jmiElement.type()); + } + } + }); + + auto future = m_manager.sendMessage(jmiElement, jid); + + while (!future.isFinished()) { + QCoreApplication::processEvents(); + } + + QVERIFY(future.isFinished()); + m_manager.clearAll(); +} + +void tst_QXmppJingleMessageInitiationManager::testHandleNonExistingSessionLowerId() +{ + // --- request with lower id sends propose to request with higher id --- + + QByteArray xmlProposeLowId { + "<message from='romeoNonExistingSession@montague.example/low' to='juliet@capulet.example' type='chat'>" + "<propose xmlns='urn:xmpp:jingle-message:0' id='ca3cf894-5325-482f-a412-a6e9f832298d'>" + "<description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'/>" + "</propose>" + "<store xmlns=\"urn:xmpp:hints\"/>" + "</message>" + }; + + auto jmiWithHigherId { m_manager.addJmi("romeoNonExistingSession@montague.example") }; + jmiWithHigherId->setId("fecbea35-08d3-404f-9ec7-2b57c566fa74"); + + QXmppJingleReason reason; + reason.setType(QXmppJingleReason::Expired); + reason.setText("Tie-Break"); + reason.setNamespaceUri(ns_jingle); + + // make sure that request with higher ID is being retracted + connect(&m_logger, &QXmppLogger::message, this, [jmiWithHigherId, reason](QXmppLogger::MessageType type, const QString &text) { + if (type == QXmppLogger::SentMessage) { + QXmppMessage message; + parsePacket(message, text.toUtf8()); + + if (message.to() == jmiWithHigherId->callPartnerJid()) { + const auto &jmiElement { message.jingleMessageInitiationElement() }; + QVERIFY(jmiElement); + + QCOMPARE(jmiElement->type(), JmiType::Retract); + QCOMPARE(jmiElement->id(), "fecbea35-08d3-404f-9ec7-2b57c566fa74"); + QVERIFY(jmiElement->reason()); + QCOMPARE(jmiElement->reason()->type(), reason.type()); + QCOMPARE(jmiElement->reason()->text(), reason.text()); + QCOMPARE(jmiElement->reason()->namespaceUri(), reason.namespaceUri()); + + SKIP_IF_INTEGRATION_TESTS_DISABLED() + + // verify that the JMI ID has been changed and the JMI was processed + QCOMPARE(jmiWithHigherId->id(), "ca3cf894-5325-482f-a412-a6e9f832298d"); + QVERIFY(jmiWithHigherId->isProceeded()); + } + } + }); + + QXmppMessage message; + message.parse(xmlToDom(xmlProposeLowId)); + + QVERIFY(m_manager.handleMessage(message)); + m_manager.clearAll(); +} + +void tst_QXmppJingleMessageInitiationManager::testHandleNonExistingSessionHigherId() +{ + // --- request with higher id sends propose to request with lower id --- + QByteArray xmlProposeHighId { + "<message from='julietNonExistingSession@capulet.example/high' to='romeo@montague.example' type='chat'>" + "<propose xmlns='urn:xmpp:jingle-message:0' id='fecbea35-08d3-404f-9ec7-2b57c566fa74'>" + "<description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'/>" + "</propose>" + "<store xmlns=\"urn:xmpp:hints\"/>" + "</message>" + }; + + QXmppJingleReason reason; + reason.setType(QXmppJingleReason::Expired); + reason.setText("Tie-Break"); + reason.setNamespaceUri(ns_jingle); + + auto jmiWithLowerId { m_manager.addJmi("julietNonExistingSession@capulet.example") }; + jmiWithLowerId->setId("ca3cf894-5325-482f-a412-a6e9f832298d"); + + // make sure that request with lower id rejects request with higher id + connect(&m_logger, &QXmppLogger::message, this, [jid = jmiWithLowerId->callPartnerJid(), reason](QXmppLogger::MessageType type, const QString &text) { + if (type == QXmppLogger::SentMessage) { + QXmppMessage message; + parsePacket(message, text.toUtf8()); + + if (message.to() == jid) { + const auto &jmiElement { message.jingleMessageInitiationElement() }; + QVERIFY(jmiElement); + + QCOMPARE(jmiElement->type(), JmiType::Reject); + QCOMPARE(jmiElement->id(), "fecbea35-08d3-404f-9ec7-2b57c566fa74"); + QVERIFY(jmiElement->reason()); + QCOMPARE(jmiElement->reason()->type(), reason.type()); + QCOMPARE(jmiElement->reason()->text(), reason.text()); + QCOMPARE(jmiElement->reason()->namespaceUri(), reason.namespaceUri()); + } + } + }); + + QXmppMessage message; + message.parse(xmlToDom(xmlProposeHighId)); + + QVERIFY(m_manager.handleMessage(message)); + m_manager.clearAll(); +} + +void tst_QXmppJingleMessageInitiationManager::testHandleExistingSession() +{ + QXmppMessage message; + + QByteArray xmlPropose { + "<message from='julietExistingSession@capulet.example/tablet' to='romeo@montague.example' type='chat'>" + "<propose xmlns='urn:xmpp:jingle-message:0' id='989a46a6-f202-4910-a7c3-83c6ba3f3947'>" + "<description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'/>" + "</propose>" + "<store xmlns=\"urn:xmpp:hints\"/>" + "</message>" + }; + + auto jmi { m_manager.addJmi("julietExistingSession@capulet.example") }; + jmi->setId("ca3cf894-5325-482f-a412-a6e9f832298d"); + jmi->setIsProceeded(true); + + QXmppJingleReason reason; + reason.setType(QXmppJingleReason::Expired); + reason.setText("Session migrated"); + reason.setNamespaceUri(ns_jingle); + + connect(&m_logger, &QXmppLogger::message, this, [jmi, reason](QXmppLogger::MessageType type, const QString &text) { + if (type == QXmppLogger::SentMessage) { + QXmppMessage message; + parsePacket(message, text.toUtf8()); + + if (message.to() == jmi->callPartnerJid()) { + const auto &jmiElement { message.jingleMessageInitiationElement() }; + QVERIFY(jmiElement); + + QCOMPARE(jmiElement->type(), JmiType::Finish); + QCOMPARE(jmiElement->id(), jmi->id()); + QCOMPARE(jmiElement->migratedTo(), "989a46a6-f202-4910-a7c3-83c6ba3f3947"); + QVERIFY(jmiElement->reason()); + QCOMPARE(jmiElement->reason()->type(), reason.type()); + QCOMPARE(jmiElement->reason()->text(), reason.text()); + QCOMPARE(jmiElement->reason()->namespaceUri(), reason.namespaceUri()); + } + } + }); + + message.parse(xmlToDom(xmlPropose)); + + QVERIFY(m_manager.handleMessage(message)); + m_manager.clearAll(); +} + +void tst_QXmppJingleMessageInitiationManager::testHandleTieBreak() +{ + QString callPartnerJid { "romeoHandleTieBreakExistingSession@montague.example/orchard" }; + QString jmiId { "ca3cf894-5325-482f-a412-a6e9f832298d" }; + auto jmi { m_manager.addJmi(QXmppUtils::jidToBareJid(callPartnerJid)) }; + jmi->setId(jmiId); + + QXmppJingleMessageInitiationElement jmiElement; + QString newJmiId("989a46a6-f202-4910-a7c3-83c6ba3f3947"); + jmiElement.setId(newJmiId); + + // Cannot use macro SKIP_IF_INTEGRATION_TESTS_DISABLED() here since + // this would also skip the manager cleanup. + if (IntegrationTests::enabled()) { + // --- ensure handleExistingSession --- + jmi->setIsProceeded(true); + QSignalSpy closedSpy(jmi.get(), &QXmppJingleMessageInitiation::closed); + QVERIFY(m_manager.handleTieBreak(jmi, jmiElement, QXmppUtils::jidToResource(callPartnerJid))); + QCOMPARE(closedSpy.count(), 1); + + // --- ensure handleNonExistingSession --- + jmi->setIsProceeded(false); + QSignalSpy proceededSpy(jmi.get(), &QXmppJingleMessageInitiation::proceeded); + QVERIFY(m_manager.handleTieBreak(jmi, jmiElement, QXmppUtils::jidToResource(callPartnerJid))); + QCOMPARE(proceededSpy.count(), 1); + } + + m_manager.clearAll(); +} + +void tst_QXmppJingleMessageInitiationManager::testHandleProposeJmiElement() +{ + QXmppJingleMessageInitiationElement jmiElement; + + QXmppJingleDescription description; + description.setMedia("audio"); + description.setSsrc(321); + description.setType("abcd"); + + jmiElement.setId("ca3cf123-5325-482f-a412-a6e9f832298d"); + jmiElement.setDescription(description); + + QString callPartnerJid { "juliet@capulet.example" }; + + // --- Tie break --- + + auto jmi { m_manager.addJmi(callPartnerJid) }; + jmi->setId("989a4123-f202-4910-a7c3-83c6ba3f3947"); + + QVERIFY(m_manager.handleProposeJmiElement(jmiElement, callPartnerJid)); + QCOMPARE(m_manager.jmis().size(), 1); + m_manager.clearAll(); + + // --- usual JMI proposal --- + + connect(&m_manager, &QXmppJingleMessageInitiationManager::proposed, this, [&, jmiElement](const std::shared_ptr<Jmi> &, const QString &jmiElementId, const std::optional<QXmppJingleDescription> &description) { + if (jmiElement.id() == jmiElementId) { + QCOMPARE(m_manager.jmis().size(), 1); + QVERIFY(description.has_value()); + QCOMPARE(description->media(), jmiElement.description()->media()); + QCOMPARE(description->ssrc(), jmiElement.description()->ssrc()); + QCOMPARE(description->type(), jmiElement.description()->type()); + } + }); + + callPartnerJid = "romeoHandleProposeJmiElement@montague.example"; + + QVERIFY(m_manager.handleProposeJmiElement(jmiElement, callPartnerJid)); + QCOMPARE(m_manager.jmis().size(), 1); + m_manager.clearAll(); +} + +void tst_QXmppJingleMessageInitiationManager::testHandleExistingJmi() +{ + QString callPartnerJid { "juliet@capulet.example" }; + QString jmiId { "989a46a6-f202-4910-a7c3-83c6ba3f3947" }; + + auto jmi { m_manager.addJmi(callPartnerJid) }; + jmi->setId(jmiId); + + QXmppJingleMessageInitiationElement jmiElement; + jmiElement.setId(jmiId); + + // --- ringing --- + + QSignalSpy ringingSpy(jmi.get(), &QXmppJingleMessageInitiation::ringing); + + jmiElement.setType(JmiType::Ringing); + + QVERIFY(m_manager.handleExistingJmi(jmi, jmiElement, callPartnerJid)); + QCOMPARE(ringingSpy.count(), 1); + m_manager.clearAll(); + + // --- proceeded --- + + jmi = m_manager.addJmi(callPartnerJid); + jmi->setId(jmiId); + + jmiElement.setType(JmiType::Proceed); + connect(jmi.get(), &QXmppJingleMessageInitiation::proceeded, this, [jmiElement](const QString &jmiElementId) { + if (jmiElementId == jmiElement.id()) { + QVERIFY(true); + } + }); + + QVERIFY(m_manager.handleExistingJmi(jmi, jmiElement, callPartnerJid)); + m_manager.clearAll(); + + // --- closed: rejected --- + + jmi = m_manager.addJmi(callPartnerJid); + jmi->setId(jmiId); + + QXmppJingleReason reason; + reason.setType(QXmppJingleReason::Expired); + reason.setText("Rejected because expired."); + reason.setNamespaceUri(ns_jingle); + + jmiElement.setType(JmiType::Reject); + jmiElement.setReason(reason); + + connect(jmi.get(), &QXmppJingleMessageInitiation::closed, this, [jmiElement](const Result &result) { + using ResultType = QXmppJingleMessageInitiation::Rejected; + + QVERIFY(std::holds_alternative<ResultType>(result)); + const ResultType &rejectedJmiElement { std::get<ResultType>(result) }; + + QVERIFY(rejectedJmiElement.reason); + QCOMPARE(rejectedJmiElement.reason->type(), jmiElement.reason()->type()); + QCOMPARE(rejectedJmiElement.reason->text(), jmiElement.reason()->text()); + QCOMPARE(rejectedJmiElement.reason->namespaceUri(), jmiElement.reason()->namespaceUri()); + QCOMPARE(rejectedJmiElement.containsTieBreak, jmiElement.containsTieBreak()); + }); + + QVERIFY(m_manager.handleExistingJmi(jmi, jmiElement, callPartnerJid)); + m_manager.clearAll(); + + // --- closed: retracted --- + + jmi = m_manager.addJmi(callPartnerJid); + jmi->setId(jmiId); + + reason.setType(QXmppJingleReason::ConnectivityError); + reason.setText("Retracted due to connectivity error."); + reason.setNamespaceUri(ns_jingle); + + jmiElement.setType(JmiType::Retract); + jmiElement.setReason(reason); + + connect(jmi.get(), &QXmppJingleMessageInitiation::closed, this, [jmiElement](const Result &result) { + using ResultType = QXmppJingleMessageInitiation::Retracted; + + QVERIFY(std::holds_alternative<ResultType>(result)); + const ResultType &rejectedJmiElement { std::get<ResultType>(result) }; + + QVERIFY(rejectedJmiElement.reason); + QCOMPARE(rejectedJmiElement.reason->type(), jmiElement.reason()->type()); + QCOMPARE(rejectedJmiElement.reason->text(), jmiElement.reason()->text()); + QCOMPARE(rejectedJmiElement.reason->namespaceUri(), jmiElement.reason()->namespaceUri()); + QCOMPARE(rejectedJmiElement.containsTieBreak, jmiElement.containsTieBreak()); + }); + + QVERIFY(m_manager.handleExistingJmi(jmi, jmiElement, callPartnerJid)); + m_manager.clearAll(); + + // --- closed: finished --- + + jmi = m_manager.addJmi(callPartnerJid); + jmi->setId(jmiId); + + reason.setType(QXmppJingleReason::Success); + reason.setText("Finished."); + reason.setNamespaceUri(ns_jingle); + + jmiElement.setType(JmiType::Finish); + jmiElement.setReason(reason); + jmiElement.setMigratedTo("ca3cf894-5325-482f-a412-a6e9f832298d"); + + connect(jmi.get(), &QXmppJingleMessageInitiation::closed, this, [jmiElement](const Result &result) { + using ResultType = QXmppJingleMessageInitiation::Finished; + + QVERIFY(std::holds_alternative<ResultType>(result)); + const ResultType &rejectedJmiElement { std::get<ResultType>(result) }; + + QVERIFY(rejectedJmiElement.reason); + QCOMPARE(rejectedJmiElement.reason->type(), jmiElement.reason()->type()); + QCOMPARE(rejectedJmiElement.reason->text(), jmiElement.reason()->text()); + QCOMPARE(rejectedJmiElement.reason->namespaceUri(), jmiElement.reason()->namespaceUri()); + QCOMPARE(rejectedJmiElement.migratedTo, jmiElement.migratedTo()); + }); + + QVERIFY(m_manager.handleExistingJmi(jmi, jmiElement, callPartnerJid)); + m_manager.clearAll(); + + // --- none --- + + jmi = m_manager.addJmi(callPartnerJid); + jmi->setId(jmiId); + + jmiElement.setType(JmiType::None); + + QCOMPARE(m_manager.handleExistingJmi(jmi, jmiElement, callPartnerJid), false); + m_manager.clearAll(); +} + +void tst_QXmppJingleMessageInitiationManager::testHandleJmiElement() +{ + QString callPartnerJid { "romeoHandleJmiElement@montague.example/orchard" }; + QString jmiId { "ca3cf894-5325-482f-a412-a6e9f832298d" }; + + // case 1: no JMI found in JMIs vector and jmiElement is not a propose element + QXmppJingleMessageInitiationElement jmiElement; + jmiElement.setType(JmiType::None); + + QCOMPARE(m_manager.handleJmiElement(std::move(jmiElement), {}), false); + + // case 2: no JMI found in JMIs vector and jmiElement is a propose element + jmiElement = {}; + jmiElement.setType(JmiType::Propose); + jmiElement.setId(jmiId); + + QSignalSpy proposedSpy(&m_manager, &QXmppJingleMessageInitiationManager::proposed); + QVERIFY(m_manager.handleJmiElement(std::move(jmiElement), callPartnerJid)); + QCOMPARE(proposedSpy.count(), 1); + m_manager.clearAll(); + + // case 3: JMI found in JMIs vector, existing session + jmiElement = {}; + jmiElement.setType(JmiType::Ringing); + jmiElement.setId(jmiId); + auto jmi { m_manager.addJmi(QXmppUtils::jidToBareJid(callPartnerJid)) }; + jmi->setId(jmiId); + + QSignalSpy ringingSpy(jmi.get(), &QXmppJingleMessageInitiation::ringing); + QVERIFY(m_manager.handleJmiElement(std::move(jmiElement), callPartnerJid)); + QCOMPARE(ringingSpy.count(), 1); + m_manager.clearAll(); +} + +void tst_QXmppJingleMessageInitiationManager::testHandleMessage_data() +{ + QTest::addColumn<QByteArray>("xml"); + QTest::addColumn<bool>("isValid"); + + QTest::newRow("xmlValid") + << QByteArray( + "<message to='julietHandleMessageValid@capulet.example' from='romeoHandleMessageValid@montague.example/orchard' type='chat'>" + "<store xmlns=\"urn:xmpp:hints\"/>" + "<propose xmlns='urn:xmpp:jingle-message:0' id='ca3cf894-5325-482f-a412-a6e9f832298d'>" + "<description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'/>" + "</propose>" + "</message>") + << true; + + QTest::newRow("xmlInvalidTypeNotChat") + << QByteArray( + "<message to='julietHandleMessageNoChat@capulet.example' from='romeoHandleMessageNoChat@montague.example/orchard' type='normal'>" + "<store xmlns=\"urn:xmpp:hints\"/>" + "<propose xmlns='urn:xmpp:jingle-message:0' id='ca3cf894-5325-482f-a412-a6e9f832298d'>" + "<description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'/>" + "</propose>" + "</message>") + << false; + + QTest::newRow("xmlInvalidNoStore") + << QByteArray( + "<message to='julietHandleMessageNoStore@capulet.example' from='romeoHandleMessageNoStore@montague.example/orchard' type='chat'>" + "<propose xmlns='urn:xmpp:jingle-message:0' id='ca3cf894-5325-482f-a412-a6e9f832298d'>" + "<description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'/>" + "</propose>" + "</message>") + << false; + + QTest::newRow("xmlInvalidNoJmiElement") + << QByteArray("<message to='julietHandleMessageNoJmi@capulet.example' from='romeoHandleMessageNoJmi@montague.example/orchard' type='chat'/>") + << false; +} + +void tst_QXmppJingleMessageInitiationManager::testHandleMessage() +{ + QFETCH(QByteArray, xml); + QFETCH(bool, isValid); + + QXmppMessage message; + + parsePacket(message, xml); + QCOMPARE(m_manager.handleMessage(message), isValid); + serializePacket(message, xml); + + m_manager.clearAll(); +} + +void tst_QXmppJingleMessageInitiationManager::testHandleMessageRinging() +{ + QXmppMessage message; + QByteArray xmlRinging { + "<message from='juliet@capulet.example/phone' to='romeo@montague.example' type='chat'>" + "<ringing xmlns='urn:xmpp:jingle-message:0' id='ca3cf894-5325-482f-a412-a6e9f832298d'/>" + "<store xmlns=\"urn:xmpp:hints\"/>" + "</message>" + }; + + auto jmi { m_manager.addJmi("juliet@capulet.example") }; + jmi->setId("ca3cf894-5325-482f-a412-a6e9f832298d"); + + QSignalSpy ringingSpy(jmi.get(), &QXmppJingleMessageInitiation::ringing); + + message.parse(xmlToDom(xmlRinging)); + + QVERIFY(m_manager.handleMessage(message)); + QCOMPARE(ringingSpy.count(), 1); + m_manager.clearAll(); +} + +void tst_QXmppJingleMessageInitiationManager::testHandleMessageProceeded() +{ + QXmppMessage message; + QByteArray xmlProceed { + "<message from='juliet@capulet.example/phone' to='romeo@montague.example' type='chat'>" + "<proceed xmlns='urn:xmpp:jingle-message:0' id='ca3cf894-5325-482f-a412-a6e9f832298d'/>" + "<store xmlns=\"urn:xmpp:hints\"/>" + "</message>" + }; + + auto jmi { m_manager.addJmi("juliet@capulet.example") }; + jmi->setId("ca3cf894-5325-482f-a412-a6e9f832298d"); + + QSignalSpy proceededSpy(jmi.get(), &QXmppJingleMessageInitiation::proceeded); + + message.parse(xmlToDom(xmlProceed)); + + QVERIFY(m_manager.handleMessage(message)); + QCOMPARE(proceededSpy.count(), 1); + m_manager.clearAll(); +} + +void tst_QXmppJingleMessageInitiationManager::testHandleMessageClosedRejected() +{ + QXmppMessage message; + QByteArray xmlReject { + "<message from='juliet@capulet.example/phone' to='romeo@montague.example' type='chat'>" + "<reject xmlns='urn:xmpp:jingle-message:0' id='ca3cf894-5325-482f-a412-a6e9f832298d'>" + "<reason xmlns=\"urn:xmpp:jingle:1\">" + "<busy/>" + "<text>Busy</text>" + "</reason>" + "</reject>" + "<store xmlns=\"urn:xmpp:hints\"/>" + "</message>" + }; + + auto jmi { m_manager.addJmi("juliet@capulet.example") }; + jmi->setId("ca3cf894-5325-482f-a412-a6e9f832298d"); + + connect(jmi.get(), &QXmppJingleMessageInitiation::closed, this, [](const Result &result) { + using ResultType = QXmppJingleMessageInitiation::Rejected; + + QVERIFY(std::holds_alternative<ResultType>(result)); + const ResultType &rejectedJmiElement { std::get<ResultType>(result) }; + + QCOMPARE(rejectedJmiElement.reason->type(), QXmppJingleReason::Busy); + QCOMPARE(rejectedJmiElement.reason->text(), "Busy"); + QCOMPARE(rejectedJmiElement.reason->namespaceUri(), ns_jingle); + }); + + message.parse(xmlToDom(xmlReject)); + + QVERIFY(m_manager.handleMessage(message)); + m_manager.clearAll(); +} + +void tst_QXmppJingleMessageInitiationManager::testHandleMessageClosedRetracted() +{ + QXmppMessage message; + QByteArray xmlRetract { + "<message from='romeo@montague.example/orchard' to='juliet@capulet.example' type='chat'>" + "<retract xmlns='urn:xmpp:jingle-message:0' id='ca3cf894-5325-482f-a412-a6e9f832298d'>" + "<reason xmlns=\"urn:xmpp:jingle:1\">" + "<cancel/>" + "<text>Retracted</text>" + "</reason>" + "</retract>" + "<store xmlns=\"urn:xmpp:hints\"/>" + "</message>" + }; + + auto jmi { m_manager.addJmi("romeo@montague.example") }; + jmi->setId("ca3cf894-5325-482f-a412-a6e9f832298d"); + + connect(jmi.get(), &QXmppJingleMessageInitiation::closed, this, [](const Result &result) { + using ResultType = QXmppJingleMessageInitiation::Retracted; + + QVERIFY(std::holds_alternative<ResultType>(result)); + const ResultType &retractedJmiElement { std::get<ResultType>(result) }; + + QCOMPARE(retractedJmiElement.reason->type(), QXmppJingleReason::Cancel); + QCOMPARE(retractedJmiElement.reason->text(), "Retracted"); + QCOMPARE(retractedJmiElement.reason->namespaceUri(), ns_jingle); + }); + + message.parse(xmlToDom(xmlRetract)); + + QVERIFY(m_manager.handleMessage(message)); + m_manager.clearAll(); +} + +void tst_QXmppJingleMessageInitiationManager::testHandleMessageClosedFinished() +{ + QXmppMessage message; + QByteArray xmlFinish { + "<message from='romeo@montague.example/orchard' to='juliet@capulet.example' type='chat'>" + "<finish xmlns='urn:xmpp:jingle-message:0' id='ca3cf894-5325-482f-a412-a6e9f832298d'>" + "<reason xmlns=\"urn:xmpp:jingle:1\">" + "<success/>" + "<text>Success</text>" + "</reason>" + "<migrated to='989a46a6-f202-4910-a7c3-83c6ba3f3947'/>" + "</finish>" + "<store xmlns=\"urn:xmpp:hints\"/>" + "</message>" + }; + + auto jmi { m_manager.addJmi("romeo@montague.example") }; + jmi->setId("ca3cf894-5325-482f-a412-a6e9f832298d"); + + connect(jmi.get(), &QXmppJingleMessageInitiation::closed, this, [](const Result &result) { + using ResultType = QXmppJingleMessageInitiation::Finished; + + QVERIFY(std::holds_alternative<ResultType>(result)); + const ResultType &finishedJmiElement { std::get<ResultType>(result) }; + + QCOMPARE(finishedJmiElement.reason->type(), QXmppJingleReason::Success); + QCOMPARE(finishedJmiElement.reason->text(), "Success"); + QCOMPARE(finishedJmiElement.reason->namespaceUri(), ns_jingle); + QCOMPARE(finishedJmiElement.migratedTo, "989a46a6-f202-4910-a7c3-83c6ba3f3947"); + }); + + message.parse(xmlToDom(xmlFinish)); + + QVERIFY(m_manager.handleMessage(message)); + m_manager.clearAll(); +} + +QTEST_MAIN(tst_QXmppJingleMessageInitiationManager) +#include "tst_qxmppjinglemessageinitiationmanager.moc" |
