aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt2
-rw-r--r--lib/quotient_common.h14
-rw-r--r--lib/uri.cpp191
-rw-r--r--lib/uri.h85
-rw-r--r--lib/uriresolver.cpp112
-rw-r--r--lib/uriresolver.h168
-rw-r--r--libquotient.pri4
-rw-r--r--tests/quotest.cpp178
8 files changed, 724 insertions, 30 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5b6410f1..808899ba 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -149,6 +149,8 @@ set(lib_SRCS
lib/room.cpp
lib/user.cpp
lib/avatar.cpp
+ lib/uri.cpp
+ lib/uriresolver.cpp
lib/syncdata.cpp
lib/settings.cpp
lib/networksettings.cpp
diff --git a/lib/quotient_common.h b/lib/quotient_common.h
index 44541b42..bb05af05 100644
--- a/lib/quotient_common.h
+++ b/lib/quotient_common.h
@@ -14,15 +14,15 @@ enum RunningPolicy { ForegroundRequest = 0x0, BackgroundRequest = 0x1 };
Q_ENUM_NS(RunningPolicy)
-enum ResourceResolveResult : short {
+enum UriResolveResult : short {
StillResolving = -1,
- Resolved = 0,
- UnknownMatrixId,
- MalformedMatrixId,
- NoAccount,
- EmptyMatrixId
+ UriResolved = 0,
+ CouldNotResolve,
+ IncorrectAction,
+ InvalidUri,
+ NoAccount
};
-Q_ENUM_NS(ResourceResolveResult)
+Q_ENUM_NS(UriResolveResult)
} // namespace Quotient
/// \deprecated Use namespace Quotient instead
diff --git a/lib/uri.cpp b/lib/uri.cpp
new file mode 100644
index 00000000..f813794c
--- /dev/null
+++ b/lib/uri.cpp
@@ -0,0 +1,191 @@
+#include "uri.h"
+
+#include "logging.h"
+
+#include <QtCore/QRegularExpression>
+
+using namespace Quotient;
+
+struct ReplacePair { QByteArray uriString; char sigil; };
+/// Defines bi-directional mapping of path prefixes and sigils
+static const auto replacePairs = {
+ ReplacePair { "user/", '@' },
+ { "roomid/", '!' },
+ { "room/", '#' },
+ // The notation for bare event ids is not proposed in MSC2312 but there's
+ // https://github.com/matrix-org/matrix-doc/pull/2644
+ { "event/", '$' }
+};
+
+Uri::Uri(QByteArray primaryId, QByteArray secondaryId, QString query)
+{
+ if (primaryId.isEmpty())
+ primaryType_ = Empty;
+ else {
+ setScheme("matrix");
+ QString pathToBe;
+ primaryType_ = Invalid;
+ if (primaryId.size() < 2) // There should be something after sigil
+ return;
+ for (const auto& p: replacePairs)
+ if (primaryId[0] == p.sigil) {
+ primaryType_ = Type(p.sigil);
+ pathToBe = p.uriString + primaryId.mid(1);
+ break;
+ }
+ if (!secondaryId.isEmpty()) {
+ if (secondaryId.size() < 2) {
+ primaryType_ = Invalid;
+ return;
+ }
+ pathToBe += "/event/" + secondaryId.mid(1);
+ }
+ setPath(pathToBe);
+ }
+ setQuery(std::move(query));
+}
+
+Uri::Uri(QUrl url) : QUrl(std::move(url))
+{
+ // NB: don't try to use `url` from here on, it's moved-from and empty
+ if (isEmpty())
+ return; // primaryType_ == Empty
+
+ if (!QUrl::isValid()) { // MatrixUri::isValid() checks primaryType_
+ primaryType_ = Invalid;
+ return;
+ }
+
+ if (scheme() == "matrix") {
+ // Check sanity as per https://github.com/matrix-org/matrix-doc/pull/2312
+ const auto& urlPath = path();
+ const auto& splitPath = urlPath.splitRef('/');
+ switch (splitPath.size()) {
+ case 2:
+ break;
+ case 4:
+ if (splitPath[2] == "event")
+ break;
+ [[fallthrough]];
+ default:
+ return; // Invalid
+ }
+
+ for (const auto& p: replacePairs)
+ if (urlPath.startsWith(p.uriString)) {
+ primaryType_ = Type(p.sigil);
+ return; // The only valid return path for matrix: URIs
+ }
+ qCDebug(MAIN) << "The matrix: URI is not recognised:"
+ << toDisplayString();
+ return;
+ }
+
+ primaryType_ = NonMatrix; // Default, unless overridden by the code below
+ if (scheme() == "https" && authority() == "matrix.to") {
+ // See https://matrix.org/docs/spec/appendices#matrix-to-navigation
+ static const QRegularExpression MatrixToUrlRE {
+ R"(^/(?<main>[^/?]+)(/(?<sec>[^?]+))?(\?(?<query>.+))?$)"
+ };
+ // matrix.to accepts both literal sigils (as well as & and ? used in
+ // its "query" substitute) and their %-encoded forms;
+ // so force QUrl to decode everything.
+ auto f = fragment(QUrl::FullyDecoded);
+ if (auto&& m = MatrixToUrlRE.match(f); m.hasMatch())
+ *this = Uri { m.captured("main").toUtf8(),
+ m.captured("sec").toUtf8(), m.captured("query") };
+ }
+}
+
+Uri::Uri(const QString& uriOrId) : Uri(fromUserInput(uriOrId)) {}
+
+Uri Uri::fromUserInput(const QString& uriOrId)
+{
+ if (uriOrId.isEmpty())
+ return {}; // type() == None
+
+ // A quick check if uriOrId is a plain Matrix id
+ // Bare event ids cannot be resolved without a room scope as per the current
+ // spec but there's a movement towards making them navigable (see, e.g.,
+ // https://github.com/matrix-org/matrix-doc/pull/2644) - so treat them
+ // as valid
+ if (QStringLiteral("!@#+$").contains(uriOrId[0]))
+ return Uri { uriOrId.toUtf8() };
+
+ return Uri { QUrl::fromUserInput(uriOrId) };
+}
+
+Uri::Type Uri::type() const { return primaryType_; }
+
+Uri::SecondaryType Uri::secondaryType() const
+{
+ return path().section('/', 2, 2) == "event" ? EventId : NoSecondaryId;
+}
+
+QUrl Uri::toUrl(UriForm form) const
+{
+ if (!isValid())
+ return {};
+
+ if (form == CanonicalUri || type() == NonMatrix)
+ return *this;
+
+ QUrl url;
+ url.setScheme("https");
+ url.setHost("matrix.to");
+ url.setPath("/");
+ auto fragment = primaryId();
+ if (const auto& secId = secondaryId(); !secId.isEmpty())
+ fragment += '/' + secId;
+ if (const auto& q = query(); !q.isEmpty())
+ fragment += '?' + q;
+ url.setFragment(fragment);
+ return url;
+}
+
+QString Uri::primaryId() const
+{
+ if (primaryType_ == Empty || primaryType_ == Invalid)
+ return {};
+
+ const auto& idStem = path().section('/', 1, 1);
+ return idStem.isEmpty() ? idStem : primaryType_ + idStem;
+}
+
+QString Uri::secondaryId() const
+{
+ const auto& idStem = path().section('/', 3);
+ return idStem.isEmpty() ? idStem : secondaryType() + idStem;
+}
+
+static const auto ActionKey = QStringLiteral("action");
+
+QString Uri::action() const
+{
+ return type() == NonMatrix || !isValid()
+ ? QString()
+ : QUrlQuery { query() }.queryItemValue(ActionKey);
+}
+
+void Uri::setAction(const QString& newAction)
+{
+ if (!isValid()) {
+ qCWarning(MAIN) << "Cannot set an action on an invalid Quotient::Uri";
+ return;
+ }
+ QUrlQuery q { query() };
+ q.removeQueryItem(ActionKey);
+ q.addQueryItem(ActionKey, newAction);
+ setQuery(q);
+}
+
+QStringList Uri::viaServers() const
+{
+ return QUrlQuery { query() }.allQueryItemValues(QStringLiteral("via"),
+ QUrl::EncodeReserved);
+}
+
+bool Uri::isValid() const
+{
+ return primaryType_ != Empty && primaryType_ != Invalid;
+}
diff --git a/lib/uri.h b/lib/uri.h
new file mode 100644
index 00000000..270766dd
--- /dev/null
+++ b/lib/uri.h
@@ -0,0 +1,85 @@
+#pragma once
+
+#include "quotient_common.h"
+
+#include <QtCore/QUrl>
+#include <QtCore/QUrlQuery>
+
+namespace Quotient {
+
+/*! \brief A wrapper around a Matrix URI or identifier
+ *
+ * This class encapsulates a Matrix resource identifier, passed in either of
+ * 3 forms: a plain Matrix identifier (sigil, localpart, serverpart or, for
+ * modern event ids, sigil and base64 hash); an MSC2312 URI (aka matrix: URI);
+ * or a matrix.to URL. The input can be either encoded (serverparts with
+ * punycode, the rest with percent-encoding) or unencoded (in this case it is
+ * the caller's responsibility to resolve all possible ambiguities).
+ *
+ * The class provides functions to check the validity of the identifier,
+ * its type, and obtain components, also in either unencoded (for displaying)
+ * or encoded (for APIs) form.
+ */
+class Uri : private QUrl {
+ Q_GADGET
+public:
+ enum Type : char {
+ Invalid = char(-1),
+ Empty = 0x0,
+ UserId = '@',
+ RoomId = '!',
+ RoomAlias = '#',
+ Group = '+',
+ BareEventId = '$', // https://github.com/matrix-org/matrix-doc/pull/2644
+ NonMatrix = ':'
+ };
+ Q_ENUM(Type)
+ enum SecondaryType : char { NoSecondaryId = 0x0, EventId = '$' };
+ Q_ENUM(SecondaryType)
+
+ enum UriForm : short { CanonicalUri, MatrixToUri };
+ Q_ENUM(UriForm)
+
+ /// Construct an empty Matrix URI
+ Uri() = default;
+ /*! \brief Decode a user input string to a Matrix identifier
+ *
+ * Accepts plain Matrix ids, MSC2312 URIs (aka matrix: URIs) and
+ * matrix.to URLs. In case of URIs/URLs, it uses QUrl's TolerantMode
+ * parser to decode common mistakes/irregularities (see QUrl documentation
+ * for more details).
+ */
+ Uri(const QString& uriOrId);
+
+ /// Construct a Matrix URI from components
+ explicit Uri(QByteArray primaryId, QByteArray secondaryId = {},
+ QString query = {});
+ /// Construct a Matrix URI from matrix.to or MSC2312 (matrix:) URI
+ explicit Uri(QUrl url);
+
+ static Uri fromUserInput(const QString& uriOrId);
+ static Uri fromUrl(QUrl url);
+
+ /// Get the primary type of the Matrix URI (user id, room id or alias)
+ /*! Note that this does not include an event as a separate type, since
+ * events can only be addressed inside of rooms, which, in turn, are
+ * addressed either by id or alias. If you need to check whether the URI
+ * is specifically an event URI, use secondaryType() instead.
+ */
+ Q_INVOKABLE Type type() const;
+ Q_INVOKABLE SecondaryType secondaryType() const;
+ Q_INVOKABLE QUrl toUrl(UriForm form = CanonicalUri) const;
+ Q_INVOKABLE QString primaryId() const;
+ Q_INVOKABLE QString secondaryId() const;
+ Q_INVOKABLE QString action() const;
+ Q_INVOKABLE void setAction(const QString& newAction);
+ Q_INVOKABLE QStringList viaServers() const;
+ Q_INVOKABLE bool isValid() const;
+ using QUrl::path, QUrl::query, QUrl::fragment;
+ using QUrl::isEmpty, QUrl::toDisplayString;
+
+private:
+ Type primaryType_ = Empty;
+};
+
+}
diff --git a/lib/uriresolver.cpp b/lib/uriresolver.cpp
new file mode 100644
index 00000000..ec30512c
--- /dev/null
+++ b/lib/uriresolver.cpp
@@ -0,0 +1,112 @@
+#include "uriresolver.h"
+
+#include "connection.h"
+#include "user.h"
+
+using namespace Quotient;
+
+UriResolveResult UriResolverBase::visitResource(Connection* account,
+ const Uri& uri)
+{
+ switch (uri.type()) {
+ case Uri::NonMatrix:
+ return visitNonMatrix(uri.toUrl()) ? UriResolved : CouldNotResolve;
+ case Uri::Invalid:
+ case Uri::Empty:
+ return InvalidUri;
+ default:;
+ }
+
+ if (!account)
+ return NoAccount;
+
+ switch (uri.type()) {
+ case Uri::UserId: {
+ if (uri.action() == "join")
+ return IncorrectAction;
+ auto* user = account->user(uri.primaryId());
+ Q_ASSERT(user != nullptr);
+ return visitUser(user, uri.action());
+ }
+ case Uri::RoomId:
+ case Uri::RoomAlias: {
+ auto* room = uri.type() == Uri::RoomId
+ ? account->room(uri.primaryId())
+ : account->roomByAlias(uri.primaryId());
+ if (room != nullptr) {
+ visitRoom(room, uri.secondaryId());
+ return UriResolved;
+ }
+ if (uri.action() == "join") {
+ joinRoom(account, uri.primaryId(), uri.viaServers());
+ return UriResolved;
+ }
+ [[fallthrough]];
+ }
+ default:
+ return CouldNotResolve;
+ }
+}
+
+// This template is only instantiated once, for Quotient::visitResource()
+template <typename... FnTs>
+class StaticUriDispatcher : public UriResolverBase {
+public:
+ StaticUriDispatcher(const FnTs&... fns) : fns_(fns...) {}
+
+private:
+ UriResolveResult visitUser(User* user, const QString& action) override
+ {
+ return std::get<0>(fns_)(user, action);
+ }
+ void visitRoom(Room* room, const QString& eventId) override
+ {
+ std::get<1>(fns_)(room, eventId);
+ }
+ void joinRoom(Connection* account, const QString& roomAliasOrId,
+ const QStringList& viaServers = {}) override
+ {
+ std::get<2>(fns_)(account, roomAliasOrId, viaServers);
+ }
+ bool visitNonMatrix(const QUrl& url) override
+ {
+ return std::get<3>(fns_)(url);
+ }
+
+ std::tuple<FnTs...> fns_;
+};
+
+UriResolveResult Quotient::visitResource(
+ Connection* account, const Uri& uri,
+ std::function<UriResolveResult(User*, QString)> userHandler,
+ std::function<void(Room*, QString)> roomEventHandler,
+ std::function<void(Connection*, QString, QStringList)> joinHandler,
+ std::function<bool(const QUrl&)> nonMatrixHandler)
+{
+ return StaticUriDispatcher(userHandler, roomEventHandler, joinHandler,
+ nonMatrixHandler)
+ .visitResource(account, uri);
+}
+
+UriResolveResult UriDispatcher::visitUser(User *user, const QString &action)
+{
+ emit userAction(user, action);
+ return UriResolved;
+}
+
+void UriDispatcher::visitRoom(Room *room, const QString &eventId)
+{
+ emit roomAction(room, eventId);
+}
+
+void UriDispatcher::joinRoom(Connection* account, const QString& roomAliasOrId,
+ const QStringList& viaServers)
+{
+ emit joinAction(account, roomAliasOrId, viaServers);
+}
+
+bool UriDispatcher::visitNonMatrix(const QUrl &url)
+{
+ emit nonMatrixAction(url);
+ return true;
+}
diff --git a/lib/uriresolver.h b/lib/uriresolver.h
new file mode 100644
index 00000000..9b2ced9d
--- /dev/null
+++ b/lib/uriresolver.h
@@ -0,0 +1,168 @@
+#pragma once
+
+#include "uri.h"
+
+#include <QtCore/QObject>
+
+#include <functional>
+
+namespace Quotient {
+class Connection;
+class Room;
+class User;
+
+/*! \brief Abstract class to resolve the resource and act on it
+ *
+ * This class encapsulates the logic of resolving a Matrix identifier or URI
+ * into a Quotient object (or objects) and calling an appropriate handler on it.
+ * It is a type-safe way of handling a URI with no prior context on its type
+ * in cases like, e.g., when a user clicks on a URI in the application.
+ *
+ * This class provides empty "handlers" for each type of URI to facilitate
+ * gradual implementation. Derived classes are encouraged to override as many
+ * of them as possible.
+ */
+class UriResolverBase {
+public:
+ /*! \brief Resolve the resource and dispatch an action depending on its type
+ *
+ * This method:
+ * 1. Resolves \p uri into an actual object (e.g., Room or User),
+ * with possible additional data such as event id, in the context of
+ * \p account.
+ * 2. If the resolving is successful, depending on the type of the object,
+ * calls the appropriate virtual function (defined in a derived
+ * concrete class) to perform an action on the resource (open a room,
+ * mention a user etc.).
+ * 3. Returns the result of resolving the resource.
+ */
+ UriResolveResult visitResource(Connection* account, const Uri& uri);
+
+protected:
+ /// Called by visitResource() when the passed URI identifies a Matrix user
+ /*!
+ * \return IncorrectAction if the action is not correct or not supported;
+ * UriResolved if it is accepted; other values are disallowed
+ */
+ virtual UriResolveResult visitUser(User* user, const QString& action)
+ {
+ return IncorrectAction;
+ }
+ /// Called by visitResource() when the passed URI identifies a room or
+ /// an event in a room
+ virtual void visitRoom(Room* room, const QString& eventId) {}
+ /// Called by visitResource() when the passed URI has `action() == "join"`
+ /// and identifies a room that the user defined by the Connection argument
+ /// is not a member of
+ virtual void joinRoom(Connection* account, const QString& roomAliasOrId,
+ const QStringList& viaServers = {})
+ {}
+ /// Called by visitResource() when the passed URI has `type() == NonMatrix`
+ /*!
+ * Should return true if the URI is considered resolved, false otherwise.
+ * A basic implementation in a graphical client can look like
+ * `return QDesktopServices::openUrl(url);` but it's strongly advised to
+ * ask for a user confirmation beforehand.
+ */
+ virtual bool visitNonMatrix(const QUrl& url) { return false; }
+};
+
+/*! \brief Resolve the resource and invoke an action on it, via function objects
+ *
+ * This function encapsulates the logic of resolving a Matrix identifier or URI
+ * into a Quotient object (or objects) and calling an appropriate handler on it.
+ * Unlike UriResolverBase it accepts the list of handlers from
+ * the caller; internally it's uses a minimal UriResolverBase class
+ *
+ * \param account The connection used as a context to resolve the identifier
+ *
+ * \param uri A URI that can represent a Matrix entity
+ *
+ * \param userHandler Called when the passed URI identifies a Matrix user
+ *
+ * \param roomEventHandler Called when the passed URI identifies a room or
+ * an event in a room
+ *
+ * \param joinHandler Called when the passed URI has `action() == "join"` and
+ * identifies a room that the user defined by
+ * the Connection argument is not a member of
+ *
+ * \param nonMatrixHandler Called when the passed URI has `type() == NonMatrix`;
+ * should return true if the URI is considered resolved,
+ * false otherwise
+ *
+ * \sa UriResolverBase, UriDispatcher
+ */
+UriResolveResult
+visitResource(Connection* account, const Uri& uri,
+ std::function<UriResolveResult(User*, QString)> userHandler,
+ std::function<void(Room*, QString)> roomEventHandler,
+ std::function<void(Connection*, QString, QStringList)> joinHandler,
+ std::function<bool(const QUrl&)> nonMatrixHandler);
+
+/*! \brief Check that the resource is resolvable with no action on it */
+inline UriResolveResult checkResource(Connection* account, const Uri& uri)
+{
+ return visitResource(
+ account, uri, [](auto, auto) { return UriResolved; }, [](auto, auto) {},
+ [](auto, auto, auto) {}, [](auto) { return false; });
+}
+
+/*! \brief Resolve the resource and invoke an action on it, via Qt signals
+ *
+ * This is an implementation of UriResolverBase that is based on
+ * QObject and uses Qt signals instead of virtual functions to provide an
+ * open-ended interface for visitors.
+ *
+ * This class is aimed primarily at clients where invoking the resolving/action
+ * and handling the action are happening in decoupled parts of the code; it's
+ * also useful to operate on Matrix identifiers and URIs from QML/JS code
+ * that cannot call resolveResource() due to QML/C++ interface limitations.
+ *
+ * This class does not restrain the client code to a certain type of
+ * connections: both direct and queued (or a mix) will work fine. One limitation
+ * caused by that is there's no way to indicate if a non-Matrix URI has been
+ * successfully resolved - a signal always returns void.
+ *
+ * Note that in case of using (non-blocking) queued connections the code that
+ * calls resolveResource() should not expect the action to be performed
+ * synchronously - the returned value is the result of resolving the URI,
+ * not acting on it.
+ */
+class UriDispatcher : public QObject, public UriResolverBase {
+ Q_OBJECT
+public:
+ explicit UriDispatcher(QObject* parent = nullptr) : QObject(parent) {}
+
+ // It's actually UriResolverBase::visitResource() but with Q_INVOKABLE
+ Q_INVOKABLE UriResolveResult resolveResource(Connection* account,
+ const Uri& uri)
+ {
+ return UriResolverBase::visitResource(account, uri);
+ }
+
+signals:
+ /// An action on a user has been requested
+ void userAction(Quotient::User* user, QString action);
+
+ /// An action on a room has been requested, with optional event id
+ void roomAction(Quotient::Room* room, QString eventId);
+
+ /// A join action has been requested, with optional 'via' servers
+ void joinAction(Quotient::Connection* account, QString roomAliasOrId,
+ QStringList viaServers);
+
+ /// An action on a non-Matrix URL has been requested
+ void nonMatrixAction(QUrl url);
+
+private:
+ UriResolveResult visitUser(User* user, const QString& action) override;
+ void visitRoom(Room* room, const QString& eventId) override;
+ void joinRoom(Connection* account, const QString& roomAliasOrId,
+ const QStringList& viaServers = {}) override;
+ bool visitNonMatrix(const QUrl& url) override;
+};
+
+} // namespace Quotient
+
+
diff --git a/libquotient.pri b/libquotient.pri
index a5a1459f..98fe3b03 100644
--- a/libquotient.pri
+++ b/libquotient.pri
@@ -34,6 +34,8 @@ HEADERS += \
$$SRCPATH/room.h \
$$SRCPATH/user.h \
$$SRCPATH/avatar.h \
+ $$SRCPATH/uri.h \
+ $$SRCPATH/uriresolver.h \
$$SRCPATH/syncdata.h \
$$SRCPATH/quotient_common.h \
$$SRCPATH/util.h \
@@ -90,6 +92,8 @@ SOURCES += \
$$SRCPATH/room.cpp \
$$SRCPATH/user.cpp \
$$SRCPATH/avatar.cpp \
+ $$SRCPATH/uri.cpp \
+ $$SRCPATH/uriresolver.cpp \
$$SRCPATH/syncdata.cpp \
$$SRCPATH/util.cpp \
$$SRCPATH/events/event.cpp \
diff --git a/tests/quotest.cpp b/tests/quotest.cpp
index b06665a9..ae272622 100644
--- a/tests/quotest.cpp
+++ b/tests/quotest.cpp
@@ -2,6 +2,7 @@
#include "connection.h"
#include "room.h"
#include "user.h"
+#include "uriresolver.h"
#include "csapi/joining.h"
#include "csapi/leaving.h"
@@ -98,6 +99,7 @@ private slots:
TEST_DECL(sendAndRedact)
TEST_DECL(addAndRemoveTag)
TEST_DECL(markDirectChat)
+ TEST_DECL(visitResources)
// Add more tests above here
public:
@@ -215,18 +217,32 @@ void TestManager::setupAndRun()
clog << "Access token: " << c->accessToken().toStdString() << endl;
c->setLazyLoading(true);
- c->syncLoop();
clog << "Joining " << targetRoomName.toStdString() << endl;
auto joinJob = c->joinRoom(targetRoomName);
- // Ensure, before this test is completed, that the room has been joined
- // and filled with some events so that other tests could use that
+ // Ensure that the room has been joined and filled with some events
+ // so that other tests could use that
connect(joinJob, &BaseJob::success, this, [this, joinJob] {
testSuite = new TestSuite(c->room(joinJob->roomId()), origin, this);
- connectSingleShot(c, &Connection::syncDone, this, [this] {
- if (testSuite->room()->timelineSize() > 0)
- doTests();
- else {
+ // Only start the sync after joining, to make sure the room just
+ // joined is in it
+ c->syncLoop();
+ connect(c, &Connection::syncDone, this, [this] {
+ static int i = 0;
+ clog << "Sync " << ++i << " complete" << endl;
+ if (auto* r = testSuite->room()) {
+ clog << "Test room timeline size = " << r->timelineSize();
+ if (r->pendingEvents().empty())
+ clog << ", pending size = " << r->pendingEvents().size();
+ clog << endl;
+ }
+ if (!running.empty()) {
+ clog << running.size() << " test(s) in the air:";
+ for (const auto& test: qAsConst(running))
+ clog << " " << testName(test);
+ clog << endl;
+ }
+ if (i == 1) {
testSuite->room()->getPreviousContent();
connectSingleShot(testSuite->room(), &Room::addedMessages, this,
&TestManager::doTests);
@@ -262,8 +278,8 @@ void TestManager::doTests()
const auto testName = metaMethod.name();
running.push_back(testName);
- // Some tests return the result immediately, so queue everything
- // so that we could process all tests asynchronously.
+ // Some tests return the result immediately but we queue everything
+ // and process all tests asynchronously.
QMetaObject::invokeMethod(testSuite, "doTest", Qt::QueuedConnection,
Q_ARG(QByteArray, testName));
}
@@ -283,20 +299,6 @@ void TestManager::doTests()
conclude();
}
});
-
- connect(c, &Connection::syncDone, this, [this] {
- static int i = 0;
- clog << "Sync " << ++i << " complete" << endl;
- if (auto* r = testSuite->room())
- clog << "Test room timeline size = " << r->timelineSize()
- << ", pending size = " << r->pendingEvents().size() << endl;
- if (!running.empty()) {
- clog << running.size() << " test(s) in the air:";
- for (const auto& test: qAsConst(running))
- clog << " " << testName(test);
- clog << endl;
- }
- });
}
TEST_IMPL(loadMembers)
@@ -612,6 +614,136 @@ TEST_IMPL(markDirectChat)
&& removedDCs.contains(connection()->user(), targetRoom->id()));
}
+TEST_IMPL(visitResources)
+{
+ // Same as the two tests above, ResourceResolver emits signals
+ // synchronously so we use signal spies to intercept them instead of
+ // connecting lambdas before calling openResource(). NB: this test
+ // assumes that ResourceResolver::openResource is implemented in terms
+ // of ResourceResolver::visitResource, so the latter doesn't need a
+ // separate test.
+ static UriDispatcher ud;
+
+ // This lambda returns true in case of error, false if it's fine so far
+ auto testResourceResolver = [this, thisTest](const QStringList& uris,
+ auto signal, auto* target,
+ QVariantList otherArgs = {}) {
+ int r = qRegisterMetaType<decltype(target)>();
+ Q_ASSERT(r != 0);
+ QSignalSpy spy(&ud, signal);
+ for (const auto& uriString: uris) {
+ Uri uri { uriString };
+ clog << "Checking " << uriString.toStdString()
+ << " -> " << uri.toDisplayString().toStdString() << endl;
+ ud.visitResource(connection(), uriString);
+ if (spy.count() != 1) {
+ clog << "Wrong number of signal emissions (" << spy.count()
+ << ')' << endl;
+ FAIL_TEST();
+ }
+ const auto& emission = spy.front();
+ Q_ASSERT(emission.count() >= 2);
+ if (emission.front().value<decltype(target)>() != target) {
+ clog << "Signal emitted with an incorrect target" << endl;
+ FAIL_TEST();
+ }
+ if (!otherArgs.empty()) {
+ if (emission.size() < otherArgs.size() + 1) {
+ clog << "Emission doesn't include all arguments" << endl;
+ FAIL_TEST();
+ }
+ for (auto i = 0; i < otherArgs.size(); ++i)
+ if (otherArgs[i] != emission[i + 1]) {
+ clog << "Mismatch in argument #" << i + 1 << endl;
+ FAIL_TEST();
+ }
+ }
+ spy.clear();
+ }
+ return false;
+ };
+
+ // Basic tests
+ for (const auto& u: { Uri {}, Uri { QUrl {} } })
+ if (u.isValid() || !u.isEmpty()) {
+ clog << "Empty Matrix URI test failed" << endl;
+ FAIL_TEST();
+ }
+ if (Uri { QStringLiteral("#") }.isValid()) {
+ clog << "Bare sigil URI test failed" << endl;
+ FAIL_TEST();
+ }
+ QUrl invalidUrl { "https://" };
+ invalidUrl.setAuthority("---:@@@");
+ const Uri matrixUriFromInvalidUrl { invalidUrl },
+ invalidMatrixUri { QStringLiteral("matrix:&invalid@") };
+ if (matrixUriFromInvalidUrl.isEmpty() || matrixUriFromInvalidUrl.isValid()) {
+ clog << "Invalid Matrix URI test failed" << endl;
+ FAIL_TEST();
+ }
+ if (invalidMatrixUri.isEmpty() || invalidMatrixUri.isValid()) {
+ clog << "Invalid sigil in a Matrix URI - test failed" << endl;
+ FAIL_TEST();
+ }
+
+ // Matrix identifiers used throughout all URI tests
+ const auto& roomId = room()->id();
+ const auto& roomAlias = room()->canonicalAlias();
+ const auto& userId = connection()->userId();
+ const auto& eventId = room()->messageEvents().back()->id();
+ Q_ASSERT(!roomId.isEmpty());
+ Q_ASSERT(!roomAlias.isEmpty());
+ Q_ASSERT(!userId.isEmpty());
+ Q_ASSERT(!eventId.isEmpty());
+
+ const QStringList roomUris {
+ roomId, "matrix:roomid/" + roomId.mid(1),
+ "https://matrix.to/#/%21"/*`!`*/ + roomId.mid(1),
+ roomAlias, "matrix:room/" + roomAlias.mid(1),
+ "https://matrix.to/#/" + roomAlias,
+ };
+ const QStringList userUris { userId, "matrix:user/" + userId.mid(1),
+ "https://matrix.to/#/" + userId };
+ const QStringList eventUris {
+ "matrix:room/" + roomAlias.mid(1) + "/event/" + eventId.mid(1),
+ "https://matrix.to/#/" + roomId + '/' + eventId
+ };
+ // The following URIs are not supposed to be actually joined (and even
+ // exist, as yet) - only to be syntactically correct
+ static const auto& joinRoomAlias =
+ QStringLiteral("unjoined:example.org"); // # will be added below
+ static const QString joinQuery { "?action=join" };
+ static const QStringList joinByAliasUris {
+ "matrix:room/" + joinRoomAlias + joinQuery,
+ "https://matrix.to/#/%23"/*`#`*/ + joinRoomAlias + joinQuery
+ };
+ static const auto& joinRoomId = QStringLiteral("!anyid:example.org");
+ static const QStringList viaServers { "matrix.org", "example.org" };
+ static const auto viaQuery =
+ std::accumulate(viaServers.cbegin(), viaServers.cend(), joinQuery,
+ [](QString q, const QString& s) {
+ return q + "&via=" + s;
+ });
+ static const QStringList joinByIdUris {
+ "matrix:roomid/" + joinRoomId.mid(1) + viaQuery,
+ "https://matrix.to/#/" + joinRoomId + viaQuery
+ };
+ // If any test breaks, the breaking call will return true, and further
+ // execution will be cut by ||'s short-circuiting
+ if (testResourceResolver(roomUris, &UriDispatcher::roomAction, room())
+ || testResourceResolver(userUris, &UriDispatcher::userAction,
+ connection()->user())
+ || testResourceResolver(eventUris, &UriDispatcher::roomAction,
+ room(), { eventId })
+ || testResourceResolver(joinByAliasUris, &UriDispatcher::joinAction,
+ connection(), { '#' + joinRoomAlias })
+ || testResourceResolver(joinByIdUris, &UriDispatcher::joinAction,
+ connection(), { joinRoomId, viaServers }))
+ return true;
+ // TODO: negative cases
+ FINISH_TEST(true);
+}
+
void TestManager::conclude()
{
QString succeededRec { QString::number(succeeded.size()) % " of "