Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package libqxmpp for openSUSE:Factory 
checked in at 2026-04-23 17:09:50
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/libqxmpp (Old)
 and      /work/SRC/openSUSE:Factory/.libqxmpp.new.11940 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "libqxmpp"

Thu Apr 23 17:09:50 2026 rev:47 rq:1348945 version:1.15.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/libqxmpp/libqxmpp.changes        2026-04-08 
17:16:05.128796782 +0200
+++ /work/SRC/openSUSE:Factory/.libqxmpp.new.11940/libqxmpp.changes     
2026-04-23 17:14:17.796588461 +0200
@@ -1,0 +2,12 @@
+Thu Apr 23 12:01:56 UTC 2026 - Christophe Marin <[email protected]>
+
+- Update to 1.15.1
+  * Fix use-after-free in sendSensitive() coroutine lambdas
+  * Move sendSensitive() coroutine lambdas to QXmppClientPrivate
+    methods
+  * QXmppTask::then(): co_return continuation result, document
+    return value
+  * Client: Detect same-type listener replacement during
+    handleElement()
+
+-------------------------------------------------------------------

Old:
----
  qxmpp-1.15.0.tar.xz
  qxmpp-1.15.0.tar.xz.sig

New:
----
  qxmpp-1.15.1.tar.xz
  qxmpp-1.15.1.tar.xz.sig

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ libqxmpp.spec ++++++
--- /var/tmp/diff_new_pack.tfDZhE/_old  2026-04-23 17:14:19.440656168 +0200
+++ /var/tmp/diff_new_pack.tfDZhE/_new  2026-04-23 17:14:19.448656498 +0200
@@ -20,7 +20,7 @@
 
 %define sover 9
 Name:           libqxmpp
-Version:        1.15.0
+Version:        1.15.1
 Release:        0
 Summary:        Qt XMPP Library
 License:        LGPL-2.1-or-later

++++++ qxmpp-1.15.0.tar.xz -> qxmpp-1.15.1.tar.xz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qxmpp-1.15.0/CHANGELOG.md 
new/qxmpp-1.15.1/CHANGELOG.md
--- old/qxmpp-1.15.0/CHANGELOG.md       2026-04-05 02:32:57.000000000 +0200
+++ new/qxmpp-1.15.1/CHANGELOG.md       2026-04-10 16:18:25.000000000 +0200
@@ -4,6 +4,14 @@
 SPDX-License-Identifier: CC0-1.0
 -->
 
+QXmpp 1.15.1 (April 10, 2026)
+-----------------------------
+
+ - Fix use-after-free in sendSensitive() coroutine lambdas causing encryption 
results to be silently lost (@lnj)
+ - Move sendSensitive() coroutine lambdas to QXmppClientPrivate methods to 
avoid closure lifetime issues (@lnj)
+ - QXmppTask::then(): co_return continuation result so chaining with non-void 
continuations compiles (@lnj)
+ - Fix QXmppPromise::finish() to construct T instead of assigning (@lnj)
+
 QXmpp 1.15.0 (April 5, 2026)
 ----------------------------
 
@@ -19,6 +27,11 @@
  - CMake: Rename `BUILD_TESTS` option to `BUILD_TESTING` (@lnj, !738)
  - Drop Qt 5 support, require Qt 6.4 (@lnj, !759)
 
+QXmpp 1.14.7 (April 10, 2026)
+-----------------------------
+
+ - Fix `Sasl2Manager` listener being replaced after FAST→password fallback 
retry (@lnj)
+
 QXmpp 1.14.6 (April 3, 2026)
 ----------------------------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qxmpp-1.15.0/CMakeLists.txt 
new/qxmpp-1.15.1/CMakeLists.txt
--- old/qxmpp-1.15.0/CMakeLists.txt     2026-04-05 02:32:57.000000000 +0200
+++ new/qxmpp-1.15.1/CMakeLists.txt     2026-04-10 16:18:25.000000000 +0200
@@ -3,7 +3,7 @@
 # SPDX-License-Identifier: CC0-1.0
 
 cmake_minimum_required(VERSION 3.16)
-project(qxmpp VERSION 1.15.0)
+project(qxmpp VERSION 1.15.1)
 
 set(SO_VERSION 9)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qxmpp-1.15.0/docs/doap.xml 
new/qxmpp-1.15.1/docs/doap.xml
--- old/qxmpp-1.15.0/docs/doap.xml      2026-04-05 02:32:57.000000000 +0200
+++ new/qxmpp-1.15.1/docs/doap.xml      2026-04-10 16:18:25.000000000 +0200
@@ -783,12 +783,26 @@
     </implements>
     <release>
       <Version>
+        <revision>1.15.1</revision>
+        <created>2026-04-10</created>
+        <file-release 
rdf:resource='https://download.kde.org/unstable/qxmpp/qxmpp-1.15.1.tar.xz'/>
+      </Version>
+    </release>
+    <release>
+      <Version>
         <revision>1.15.0</revision>
         <created>2026-04-05</created>
         <file-release 
rdf:resource='https://download.kde.org/unstable/qxmpp/qxmpp-1.15.0.tar.xz'/>
       </Version>
     </release>
     <release>
+      <Version>
+        <revision>1.14.7</revision>
+        <created>2026-04-10</created>
+        <file-release 
rdf:resource='https://download.kde.org/unstable/qxmpp/qxmpp-1.14.7.tar.xz'/>
+      </Version>
+    </release>
+    <release>
       <Version>
         <revision>1.14.6</revision>
         <created>2026-04-03</created>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qxmpp-1.15.0/src/base/QXmppTask.h 
new/qxmpp-1.15.1/src/base/QXmppTask.h
--- old/qxmpp-1.15.0/src/base/QXmppTask.h       2026-04-05 02:32:57.000000000 
+0200
+++ new/qxmpp-1.15.1/src/base/QXmppTask.h       2026-04-10 16:18:25.000000000 
+0200
@@ -209,16 +209,16 @@
     {
         if (shared()) {
             sharedData().finished = true;
-            sharedData().result = std::forward<U>(value);
+            sharedData().result.emplace(std::forward<U>(value));
         } else {
             if (auto *task = inlineData().task) {
                 inlineData().task->inlineData().finished = true;
-                inlineData().task->inlineData().result = 
std::forward<U>(value);
+                
inlineData().task->inlineData().result.emplace(std::forward<U>(value));
             } else {
                 // finish called without generating task
                 detachData();
                 sharedData().finished = true;
-                sharedData().result = std::forward<U>(value);
+                sharedData().result.emplace(std::forward<U>(value));
             }
         }
         invokeHandle();
@@ -229,7 +229,7 @@
     ///
     /// If a task is cancelled, no call to `finish()` is needed and no 
continuation is resumed.
     ///
-    /// \since QXmpp 1.11
+    /// \since QXmpp 1.15
     ///
     bool cancelled() const
     {
@@ -445,16 +445,29 @@
     /// deleted. This way your lambda will never be executed after your object 
has been deleted.
     /// \param continuation A function accepting a result in the form of `T 
&&`.
     ///
+    /// \returns A new QXmppTask that finishes with the value returned by \p 
continuation, allowing
+    /// further chaining via another `.then()` or `co_await`. If \p 
continuation returns `void`,
+    /// the returned task is `QXmppTask<void>` which finishes once the 
continuation has run.
+    ///
     template<typename Continuation>
     auto then(const QObject *context, Continuation continuation)
         -> QXmppTask<QXmpp::Private::InvokeContinuationResult<Continuation, T>>
     {
+        using Result = QXmpp::Private::InvokeContinuationResult<Continuation, 
T>;
         QXmppTask<T> task = std::move(*this);
         if constexpr (std::is_void_v<T>) {
             co_await task.withContext(context);
-            continuation();
+            if constexpr (std::is_void_v<Result>) {
+                continuation();
+            } else {
+                co_return continuation();
+            }
         } else {
-            continuation(co_await task.withContext(context));
+            if constexpr (std::is_void_v<Result>) {
+                continuation(co_await task.withContext(context));
+            } else {
+                co_return continuation(co_await task.withContext(context));
+            }
         }
     }
 
@@ -467,7 +480,7 @@
     ///
     /// \returns reference to this task
     ///
-    /// \since QXmpp 1.11
+    /// \since QXmpp 1.15
     ///
     QXmppTask<T> &withContext(const QObject *c)
     {
@@ -491,7 +504,7 @@
     /// If there is a waiting coroutine, it is cancelled immediately. Any 
continuation set in the
     /// future also won't be executed.
     ///
-    /// \since QXmpp 1.11
+    /// \since QXmpp 1.15
     ///
     void cancel()
     {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qxmpp-1.15.0/src/client/QXmppClient.cpp 
new/qxmpp-1.15.1/src/client/QXmppClient.cpp
--- old/qxmpp-1.15.0/src/client/QXmppClient.cpp 2026-04-05 02:32:57.000000000 
+0200
+++ new/qxmpp-1.15.1/src/client/QXmppClient.cpp 2026-04-10 16:18:25.000000000 
+0200
@@ -499,36 +499,38 @@
 ///
 QXmppTask<QXmpp::SendResult> QXmppClient::sendSensitive(QXmppStanza &&stanza, 
const std::optional<QXmppSendStanzaParams> &params)
 {
-    auto sendEncryptedMessage = [this](auto &&task) -> 
QXmppTask<QXmpp::SendResult> {
-        auto result = co_await task.withContext(this);
-        if (std::holds_alternative<QXmppError>(result)) {
-            co_return std::get<QXmppError>(std::move(result));
-        }
-        QByteArray xml;
-        QXmlStreamWriter writer(&xml);
-        std::get<std::unique_ptr<QXmppMessage>>(result)->toXml(&writer, 
QXmpp::ScePublic);
-
-        co_return co_await d->stream->streamAckManager().send(QXmppPacket(xml, 
true));
-    };
-
-    auto sendEncryptedIq = [this](auto &&task) -> QXmppTask<QXmpp::SendResult> 
{
-        auto result = co_await task.withContext(this);
-        if (std::holds_alternative<QXmppError>(result)) {
-            co_return std::get<QXmppError>(std::move(result));
-        }
-        co_return co_await 
d->stream->streamAckManager().send(QXmppPacket(*std::get<std::unique_ptr<QXmppIq>>(result)));
-    };
-
     if (d->encryptionExtension) {
         if (dynamic_cast<QXmppMessage *>(&stanza)) {
-            return 
sendEncryptedMessage(d->encryptionExtension->encryptMessage(dynamic_cast<QXmppMessage
 &&>(stanza), params));
+            return 
d->sendEncryptedMessage(d->encryptionExtension->encryptMessage(dynamic_cast<QXmppMessage
 &&>(stanza), params));
         } else if (dynamic_cast<QXmppIq *>(&stanza)) {
-            return 
sendEncryptedIq(d->encryptionExtension->encryptIq(dynamic_cast<QXmppIq 
&&>(stanza), params));
+            return 
d->sendEncryptedIq(d->encryptionExtension->encryptIq(dynamic_cast<QXmppIq 
&&>(stanza), params));
         }
     }
     return d->stream->streamAckManager().send(stanza);
 }
 
+QXmppTask<QXmpp::SendResult> 
QXmppClientPrivate::sendEncryptedMessage(QXmppTask<QXmppE2eeExtension::MessageEncryptResult>
 task)
+{
+    auto result = co_await task.withContext(q);
+    if (std::holds_alternative<QXmppError>(result)) {
+        co_return std::get<QXmppError>(std::move(result));
+    }
+    QByteArray xml;
+    QXmlStreamWriter writer(&xml);
+    std::get<std::unique_ptr<QXmppMessage>>(result)->toXml(&writer, 
QXmpp::ScePublic);
+
+    co_return co_await stream->streamAckManager().send(QXmppPacket(xml, true));
+}
+
+QXmppTask<QXmpp::SendResult> 
QXmppClientPrivate::sendEncryptedIq(QXmppTask<QXmppE2eeExtension::IqEncryptResult>
 task)
+{
+    auto result = co_await task.withContext(q);
+    if (std::holds_alternative<QXmppError>(result)) {
+        co_return std::get<QXmppError>(std::move(result));
+    }
+    co_return co_await 
stream->streamAckManager().send(QXmppPacket(*std::get<std::unique_ptr<QXmppIq>>(result)));
+}
+
 ///
 /// Sends a packet always without end-to-end-encryption.
 ///
@@ -600,43 +602,44 @@
 ///
 QXmppTask<QXmppClient::IqResult> QXmppClient::sendSensitiveIq(QXmppIq &&iq, 
const std::optional<QXmppSendStanzaParams> &params)
 {
-    auto sendEncryptedIq = [this, params](QXmppIq &&iq) -> QXmppTask<IqResult> 
{
-        auto encryptResult = co_await 
d->encryptionExtension->encryptIq(std::move(iq), params);
+    return d->encryptionExtension ? d->sendSensitiveIq(std::move(iq), params) 
: d->stream->sendIq(std::move(iq));
+}
 
-        if (std::holds_alternative<QXmppError>(encryptResult)) {
-            co_return std::get<QXmppError>(std::move(encryptResult));
-        }
+QXmppTask<QXmppClient::IqResult> QXmppClientPrivate::sendSensitiveIq(QXmppIq 
iq, std::optional<QXmppSendStanzaParams> params)
+{
+    auto encryptResult = co_await 
encryptionExtension->encryptIq(std::move(iq), params);
 
-        auto encryptedIq = 
std::get<std::unique_ptr<QXmppIq>>(std::move(encryptResult));
-        auto sendResult = co_await 
d->stream->sendIq(std::move(*encryptedIq)).withContext(this);
+    if (std::holds_alternative<QXmppError>(encryptResult)) {
+        co_return std::get<QXmppError>(std::move(encryptResult));
+    }
 
-        if (std::holds_alternative<QXmppError>(sendResult)) {
-            co_return std::get<QXmppError>(std::move(sendResult));
-        }
-        auto responseIqElement = std::get<QDomElement>(sendResult);
+    auto encryptedIq = 
std::get<std::unique_ptr<QXmppIq>>(std::move(encryptResult));
+    auto sendResult = co_await 
stream->sendIq(std::move(*encryptedIq)).withContext(q);
 
-        // iq sent, response received
-        if (!isIqResponse(responseIqElement)) {
-            co_return QXmppError { u"Invalid IQ response received."_s, 
QXmpp::SendError::EncryptionError };
-        }
-        if (!d->encryptionExtension) {
-            co_return QXmppError { u"No decryption extension found."_s, 
QXmpp::SendError::EncryptionError };
-        }
+    if (std::holds_alternative<QXmppError>(sendResult)) {
+        co_return std::get<QXmppError>(std::move(sendResult));
+    }
+    auto responseIqElement = std::get<QDomElement>(sendResult);
 
-        // try to decrypt the result (should be encrypted)
-        auto decryptResult = co_await 
d->encryptionExtension->decryptIq(responseIqElement);
+    // iq sent, response received
+    if (!isIqResponse(responseIqElement)) {
+        co_return QXmppError { u"Invalid IQ response received."_s, 
QXmpp::SendError::EncryptionError };
+    }
+    if (!encryptionExtension) {
+        co_return QXmppError { u"No decryption extension found."_s, 
QXmpp::SendError::EncryptionError };
+    }
 
-        co_return map<IqResult>(
-            [&](QXmppE2eeExtension::NotEncrypted) -> IqResult {
-                // the IQ response from the other entity was not encrypted
-                // then report IQ response without modifications
-                // TODO: should we return an QXmppError instead?
-                return responseIqElement;
-            },
-            std::move(decryptResult));
-    };
+    // try to decrypt the result (should be encrypted)
+    auto decryptResult = co_await 
encryptionExtension->decryptIq(responseIqElement);
 
-    return d->encryptionExtension ? sendEncryptedIq(std::move(iq)) : 
d->stream->sendIq(std::move(iq));
+    co_return map<QXmppClient::IqResult>(
+        [&](QXmppE2eeExtension::NotEncrypted) -> QXmppClient::IqResult {
+            // the IQ response from the other entity was not encrypted
+            // then report IQ response without modifications
+            // TODO: should we return an QXmppError instead?
+            return responseIqElement;
+        },
+        std::move(decryptResult));
 }
 
 ///
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qxmpp-1.15.0/src/client/QXmppClient_p.h 
new/qxmpp-1.15.1/src/client/QXmppClient_p.h
--- old/qxmpp-1.15.0/src/client/QXmppClient_p.h 2026-04-05 02:32:57.000000000 
+0200
+++ new/qxmpp-1.15.1/src/client/QXmppClient_p.h 2026-04-10 16:18:25.000000000 
+0200
@@ -19,14 +19,15 @@
 #ifndef QXMPPCLIENT_P_H
 #define QXMPPCLIENT_P_H
 
+#include "QXmppE2eeExtension.h"
 #include "QXmppOutgoingClient.h"
 #include "QXmppPresence.h"
+#include "QXmppSendResult.h"
 
 #include <chrono>
 
 class QXmppClient;
 class QXmppClientExtension;
-class QXmppE2eeExtension;
 class QXmppLogger;
 class QTimer;
 
@@ -37,6 +38,10 @@
 
     void resendPresence();
 
+    QXmppTask<QXmpp::SendResult> 
sendEncryptedMessage(QXmppTask<QXmppE2eeExtension::MessageEncryptResult> task);
+    QXmppTask<QXmpp::SendResult> 
sendEncryptedIq(QXmppTask<QXmppE2eeExtension::IqEncryptResult> task);
+    QXmppTask<QXmppClient::IqResult> sendSensitiveIq(QXmppIq iq, 
std::optional<QXmppSendStanzaParams> params);
+
     /// Current presence of the client
     QXmppPresence clientPresence;
     QList<QXmppClientExtension *> extensions;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qxmpp-1.15.0/src/client/QXmppOutgoingClient.cpp 
new/qxmpp-1.15.1/src/client/QXmppOutgoingClient.cpp
--- old/qxmpp-1.15.0/src/client/QXmppOutgoingClient.cpp 2026-04-05 
02:32:57.000000000 +0200
+++ new/qxmpp-1.15.1/src/client/QXmppOutgoingClient.cpp 2026-04-10 
16:18:25.000000000 +0200
@@ -685,7 +685,11 @@
     // if we receive any kind of data, stop the timeout timer
     d->pingManager.onDataReceived();
 
-    auto index = d->listener.index();
+    // Remember the listener generation so we can tell whether a synchronous 
task continuation
+    // inside handleElement() installed a replacement listener. 
std::variant::index() is not
+    // sufficient here because the replacement may be of the same type (e.g. 
Sasl2Manager retrying
+    // with password auth after a FAST token rejection, which re-runs 
setListener<Sasl2Manager>).
+    const uint generation = d->listenerGeneration;
 
     switch (visit(overloaded {
                       [&](auto *manager) { return 
manager->handleElement(nodeRecv); },
@@ -706,8 +710,9 @@
         return;
     }
     case Finished:
-        // if the job is done, set OutgoingClient, but do not override a 
continuation job
-        if (d->listener.index() == index) {
+        // if the job is done, fall back to OutgoingClient — but not if a 
continuation already
+        // installed a new listener during the handleElement() call above.
+        if (d->listenerGeneration == generation) {
             d->listener = this;
         }
         return;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qxmpp-1.15.0/src/client/QXmppOutgoingClient_p.h 
new/qxmpp-1.15.1/src/client/QXmppOutgoingClient_p.h
--- old/qxmpp-1.15.0/src/client/QXmppOutgoingClient_p.h 2026-04-05 
02:32:57.000000000 +0200
+++ new/qxmpp-1.15.1/src/client/QXmppOutgoingClient_p.h 2026-04-10 
16:18:25.000000000 +0200
@@ -206,6 +206,8 @@
     std::optional<Bind2Bound> bind2Bound;
 
     std::variant<QXmppOutgoingClient *, StarttlsManager, NonSaslAuthManager, 
SaslManager, Sasl2Manager, C2sStreamManager *, BindManager> listener;
+    // Incremented every time setListener() installs a new listener to detect 
a replacements.
+    uint listenerGeneration = 0;
     FastTokenManager fastTokenManager;
     C2sStreamManager c2sStreamManager;
     CarbonManager carbonManager;
@@ -216,6 +218,7 @@
     T &setListener(Args... args)
     {
         listener = T { args... };
+        ++listenerGeneration;
         return std::get<T>(listener);
     }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qxmpp-1.15.0/tests/TestClient.h 
new/qxmpp-1.15.1/tests/TestClient.h
--- old/qxmpp-1.15.0/tests/TestClient.h 2026-04-05 02:32:57.000000000 +0200
+++ new/qxmpp-1.15.1/tests/TestClient.h 2026-04-10 16:18:25.000000000 +0200
@@ -9,6 +9,7 @@
 #include "QXmppClientExtension.h"  // needed for qDeleteAll(d->extensions)
 #include "QXmppClient_p.h"
 #include "QXmppOutgoingClient.h"
+#include "QXmppSasl_p.h"
 
 #include "util.h"
 
@@ -38,6 +39,17 @@
     QXmppOutgoingClient *stream() const { return d->stream; }
     QXmppOutgoingClientPrivate *streamPrivate() const { return 
d->stream->d.get(); }
 
+    // Test wrappers for the private outgoing-client entry points exercised by 
the SASL2 + FAST
+    // listener-replacement regression test.
+    void startSasl2Auth(const QXmpp::Private::Sasl2::StreamFeature &feature)
+    {
+        d->stream->startSasl2Auth(feature);
+    }
+    void handlePacketReceived(const QDomElement &el)
+    {
+        d->stream->handlePacketReceived(el);
+    }
+
     template<typename String>
     void inject(const String &xml) { inject(xmlToDom(xml)); }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qxmpp-1.15.0/tests/tst_qxmppclient.cpp 
new/qxmpp-1.15.1/tests/tst_qxmppclient.cpp
--- old/qxmpp-1.15.0/tests/tst_qxmppclient.cpp  2026-04-05 02:32:57.000000000 
+0200
+++ new/qxmpp-1.15.1/tests/tst_qxmppclient.cpp  2026-04-10 16:18:25.000000000 
+0200
@@ -15,6 +15,8 @@
 #include "QXmppPromise.h"
 #include "QXmppRegisterIq.h"
 #include "QXmppRosterManager.h"
+#include "QXmppSasl2UserAgent.h"
+#include "QXmppSaslManager_p.h"
 #include "QXmppSasl_p.h"
 #include "QXmppStreamFeatures.h"
 #include "QXmppVCardManager.h"
@@ -38,6 +40,8 @@
     Q_SLOT void testE2eeExtension();
     Q_SLOT void testTaskDirect();
     Q_SLOT void testTaskStore();
+    Q_SLOT void testTaskOptionalNullopt();
+    Q_SLOT void testTaskThenChainSuspended();
     Q_SLOT void testChainIq();
     Q_SLOT void colorGeneration();
 #if QT_GUI_LIB
@@ -46,6 +50,7 @@
 
     // outgoing client
     Q_SLOT void csiManager();
+    Q_SLOT void sasl2FastFallbackKeepsListener();
 
     Q_SLOT void credentialsSerialization();
 };
@@ -234,6 +239,48 @@
     QVERIFY(!p.task().hasResult());
 }
 
+void tst_QXmppClient::testTaskOptionalNullopt()
+{
+    // Regression test: finishing a promise whose T is std::optional<X>
+    // with std::nullopt must produce an engaged result holding an empty
+    // inner optional, not disengage the internal storage. takeResult()
+    // previously dereferenced an empty optional and crashed.
+    QXmppPromise<std::optional<int>> p;
+    auto task = p.task();
+    p.finish(std::nullopt);
+
+    QVERIFY(task.isFinished());
+    QVERIFY(task.hasResult());
+    QVERIFY(!task.takeResult().has_value());
+}
+
+void tst_QXmppClient::testTaskThenChainSuspended()
+{
+    // Regression test: QXmppTask::then() with a non-void-returning 
continuation
+    // must co_return the continuation's value, so the chained task carries it
+    // through. This also exercises the suspension path: the source task is not
+    // yet finished when then() is called, so the then() coroutine actually
+    // suspends and is resumed later by promise.finish().
+    QXmppPromise<int> sourcePromise;
+    auto sourceTask = sourcePromise.task();
+
+    auto chainedTask = sourceTask.then(this, [](int &&value) -> QString {
+        return QString::number(value * 2);
+    });
+
+    // The source has not been finished yet, so the chained task must still be
+    // suspended on the inner co_await.
+    QVERIFY(!chainedTask.isFinished());
+
+    sourcePromise.finish(21);
+
+    // After finishing the source, then() resumes, runs the continuation and
+    // co_returns its result into the chained task.
+    QVERIFY(chainedTask.isFinished());
+    QVERIFY(chainedTask.hasResult());
+    QCOMPARE(chainedTask.takeResult(), u"42"_s);
+}
+
 using DiscoResult = std::variant<QXmppDiscoveryIq, QXmppError>;
 
 static QXmppTask<DiscoResult> parseIqResult(QXmppTask<QXmppClient::IqResult> 
&&sendTask, QObject *context)
@@ -320,6 +367,93 @@
     client.expectNoPacket();
 }
 
+// Regression test for the SASL2 + FAST listener-replacement bug.
+//
+// When a stored FAST token (XEP-0484) is rejected by the server, 
QXmppOutgoingClient retries
+// SASL2 authentication with a password-based mechanism. The retry happens 
from inside the
+// failed task's .then() continuation, which calls startSasl2Auth() 
recursively, which calls
+// setListener<Sasl2Manager>() — installing a NEW Sasl2Manager into 
d->listener while the OLD
+// Sasl2Manager's handleElement() call is still on the stack.
+//
+// Before the fix, handlePacketReceived() compared d->listener.index() 
before/after the call to
+// decide whether to fall back to OutgoingClient as the active listener. Both 
old and new
+// listeners were Sasl2Manager — same variant index — so the check failed to 
notice the
+// replacement and overwrote the new Sasl2Manager with OutgoingClient. The 
next stanza (the
+// SCRAM challenge) then landed on the wrong handler and produced
+// "Unexpected element received while handling client session." A monotonic 
listener generation
+// counter, captured before the call and re-checked after, fixes this.
+void tst_QXmppClient::sasl2FastFallbackKeepsListener()
+{
+    TestClient client;
+    auto &config = client.stream()->configuration();
+    config.setUser(u"bowman"_s);
+    config.setPassword(u"1234"_s);
+    config.setDomain(u"example.org"_s);
+    config.setDisabledSaslMechanisms({});
+    config.setSasl2UserAgent(QXmppSasl2UserAgent {
+        QUuid::fromString(u"d4565fa7-4d72-4749-b3d3-740edbf87770"_s),
+        u"QXmpp"_s,
+        u"HAL 9000"_s,
+    });
+
+    // Pre-populate a (stale) FAST token, as if from a previous session.
+    config.credentialData().htToken = HtToken {
+        SaslHtMechanism { IanaHashAlgorithm::Sha3_512, SaslHtMechanism::None },
+        u"old-invalid-token"_s,
+        QDateTime::fromString(u"2024-07-11T14:00:00Z"_s, Qt::ISODate),
+    };
+
+    Sasl2::StreamFeature sasl2Feature {
+        { u"PLAIN"_s },
+        {},
+        FastFeature { { u"HT-SHA3-512-NONE"_s }, false },
+        false,
+    };
+
+    // Kick off SASL2 auth. The first attempt picks the FAST HT mechanism.
+    client.startSasl2Auth(sasl2Feature);
+
+    
QVERIFY(std::holds_alternative<Sasl2Manager>(client.streamPrivate()->listener));
+    auto firstAuth = client.takePacket();
+    QVERIFY(firstAuth.contains(u"mechanism=\"HT-SHA3-512-NONE\""_s));
+    QVERIFY(firstAuth.contains(u"<fast xmlns=\"urn:xmpp:fast:0\"/>"_s));
+
+    // Server rejects the token. This synchronously runs the failed task's 
.then() continuation,
+    // which retries by calling startSasl2Auth() → 
setListener<Sasl2Manager>(). With the fix in
+    // place, handlePacketReceived() must NOT overwrite the new Sasl2Manager 
with OutgoingClient.
+    client.handlePacketReceived(xmlToDom(
+        "<failure xmlns='urn:xmpp:sasl:2'>"
+        "<not-authorized xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>"
+        "</failure>"));
+
+    // Critical assertion: the listener is still a Sasl2Manager (the second 
one). Without the
+    // generation-counter fix this would be QXmppOutgoingClient* and the next 
stanza would be
+    // rejected as "Unexpected element received while handling client session."
+    
QVERIFY(std::holds_alternative<Sasl2Manager>(client.streamPrivate()->listener));
+
+    // The retry should have sent a second <authenticate>, this time with 
PLAIN, no <fast/>,
+    // and a fresh FAST token request.
+    auto secondAuth = client.takePacket();
+    QVERIFY(secondAuth.contains(u"mechanism=\"PLAIN\""_s));
+    QVERIFY(!secondAuth.contains(u"<fast xmlns=\"urn:xmpp:fast:0\"/>"_s));
+    QVERIFY(secondAuth.contains(u"<request-token xmlns=\"urn:xmpp:fast:0\" 
mechanism=\"HT-SHA3-512-NONE\"/>"_s));
+    // Stale token must still be present — server may have been temporarily 
misconfigured.
+    QVERIFY(config.credentialData().htToken.has_value());
+    QCOMPARE(config.credentialData().htToken->secret, u"old-invalid-token"_s);
+
+    // Server now accepts the password attempt and provides a fresh token. The 
same Sasl2Manager
+    // instance handles this success element.
+    client.handlePacketReceived(xmlToDom(
+        "<success xmlns='urn:xmpp:sasl:2'>"
+        
"<authorization-identifier>[email protected]</authorization-identifier>"
+        "<token xmlns='urn:xmpp:fast:0' token='new-valid-token' 
expiry='2024-08-01T14:00:00Z'/>"
+        "</success>"));
+
+    QVERIFY(client.streamPrivate()->isAuthenticated);
+    QVERIFY(config.credentialData().htToken.has_value());
+    QCOMPARE(config.credentialData().htToken->secret, u"new-valid-token"_s);
+}
+
 void tst_QXmppClient::credentialsSerialization()
 {
     QByteArray xml =

Reply via email to