Add XEP-0353: Jingle Message Initiation manager

This commit is contained in:
Tibor Csötönyi 2023-05-03 14:46:38 +02:00 committed by Linus Jahn
parent 2fde987d39
commit a4dcd90685
6 changed files with 1645 additions and 0 deletions

View File

@ -489,6 +489,14 @@ SPDX-License-Identifier: CC0-1.0
<xmpp:since>1.0</xmpp:since>
</xmpp:SupportedXep>
</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'/>

View File

@ -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

View File

@ -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)
///

View File

@ -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

View File

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

View File

@ -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"