aboutsummaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
Diffstat (limited to 'examples')
-rw-r--r--examples/qmc-example.cpp619
1 files changed, 0 insertions, 619 deletions
diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp
deleted file mode 100644
index bf4d04c7..00000000
--- a/examples/qmc-example.cpp
+++ /dev/null
@@ -1,619 +0,0 @@
-
-#include "connection.h"
-#include "room.h"
-#include "user.h"
-
-#include "csapi/joining.h"
-#include "csapi/leaving.h"
-#include "csapi/room_send.h"
-
-#include "events/reactionevent.h"
-#include "events/simplestateevents.h"
-
-#include <QtCore/QCoreApplication>
-#include <QtCore/QFileInfo>
-#include <QtCore/QStringBuilder>
-#include <QtCore/QTemporaryFile>
-#include <QtCore/QTimer>
-
-#include <functional>
-#include <iostream>
-
-using namespace Quotient;
-using std::cout;
-using std::endl;
-using namespace std::placeholders;
-
-class QMCTest : public QObject {
-public:
- QMCTest(Connection* conn, QString testRoomName, QString source);
-
-private slots:
- // clang-format off
- void setupAndRun();
- void onNewRoom(Room* r);
- void run();
- void doTests();
- void loadMembers();
- void sendMessage();
- void sendReaction(const QString& targetEvtId);
- void sendFile();
- void checkFileSendingOutcome(const QString& txnId,
- const QString& fileName);
- void setTopic();
- void addAndRemoveTag();
- void sendAndRedact();
- bool checkRedactionOutcome(const QString& evtIdToRedact);
- void markDirectChat();
- void checkDirectChatOutcome(
- const Connection::DirectChatsMap& added);
- void conclude();
- void finalize();
- // clang-format on
-
-private:
- QScopedPointer<Connection, QScopedPointerDeleteLater> c;
- QStringList running;
- QStringList succeeded;
- QStringList failed;
- QString origin;
- QString targetRoomName;
- Room* targetRoom = nullptr;
-
- bool validatePendingEvent(const QString& txnId);
-};
-
-#define QMC_CHECK(description, condition) \
- { \
- Q_ASSERT(running.removeOne(description)); \
- if (!!(condition)) { \
- succeeded.push_back(description); \
- cout << (description) << " successful" << endl; \
- if (targetRoom) \
- targetRoom->postMessage(origin % ": " % (description) \
- % " successful", \
- MessageEventType::Notice); \
- } else { \
- failed.push_back(description); \
- cout << (description) << " FAILED" << endl; \
- if (targetRoom) \
- targetRoom->postPlainText(origin % ": " % (description) \
- % " FAILED"); \
- } \
- }
-
-bool QMCTest::validatePendingEvent(const QString& txnId)
-{
- auto it = targetRoom->findPendingEvent(txnId);
- return it != targetRoom->pendingEvents().end()
- && it->deliveryStatus() == EventStatus::Submitted
- && (*it)->transactionId() == txnId;
-}
-
-QMCTest::QMCTest(Connection* conn, QString testRoomName, QString source)
- : c(conn), origin(std::move(source)), targetRoomName(std::move(testRoomName))
-{
- if (!origin.isEmpty())
- cout << "Origin for the test message: " << origin.toStdString() << endl;
- if (!targetRoomName.isEmpty())
- cout << "Test room name: " << targetRoomName.toStdString() << endl;
-
- connect(c.data(), &Connection::connected, this, &QMCTest::setupAndRun);
- connect(c.data(), &Connection::loadedRoomState, this, &QMCTest::onNewRoom);
- // Big countdown watchdog
- QTimer::singleShot(180000, this, &QMCTest::conclude);
-}
-
-void QMCTest::setupAndRun()
-{
- Q_ASSERT(!c->homeserver().isEmpty() && c->homeserver().isValid());
- Q_ASSERT(c->domain() == c->userId().section(':', 1));
- cout << "Connected, server: "
- << c->homeserver().toDisplayString().toStdString() << endl;
- cout << "Access token: " << c->accessToken().toStdString() << endl;
-
- if (!targetRoomName.isEmpty()) {
- cout << "Joining " << targetRoomName.toStdString() << endl;
- running.push_back("Join room");
- auto joinJob = c->joinRoom(targetRoomName);
- connect(joinJob, &BaseJob::failure, this, [this] {
- QMC_CHECK("Join room", false);
- conclude();
- });
- // Connection::joinRoom() creates a Room object upon JoinRoomJob::success
- // but this object is empty until the first sync is done.
- connect(joinJob, &BaseJob::success, this, [this, joinJob] {
- targetRoom = c->room(joinJob->roomId(), JoinState::Join);
- QMC_CHECK("Join room", targetRoom != nullptr);
-
- run();
- });
- } else
- run();
-}
-
-void QMCTest::onNewRoom(Room* r)
-{
- cout << "New room: " << r->id().toStdString() << endl
- << " Name: " << r->name().toStdString() << endl
- << " Canonical alias: " << r->canonicalAlias().toStdString() << endl
- << endl;
- connect(r, &Room::aboutToAddNewMessages, r, [r](RoomEventsRange timeline) {
- cout << timeline.size() << " new event(s) in room "
- << r->canonicalAlias().toStdString() << endl;
- // for (const auto& item: timeline)
- // {
- // cout << "From: "
- // << r->roomMembername(item->senderId()).toStdString()
- // << endl << "Timestamp:"
- // << item->timestamp().toString().toStdString() << endl
- // << "JSON:" << endl <<
- // item->originalJson().toStdString() << endl;
- // }
- });
-}
-
-void QMCTest::run()
-{
- c->setLazyLoading(true);
- c->syncLoop();
- connectSingleShot(c.data(), &Connection::syncDone, this, &QMCTest::doTests);
- connect(c.data(), &Connection::syncDone, c.data(), [this] {
- cout << "Sync complete, " << running.size()
- << " test(s) in the air: " << running.join(", ").toStdString()
- << endl;
- if (running.isEmpty())
- conclude();
- });
-}
-
-void QMCTest::doTests()
-{
- cout << "Starting tests" << endl;
-
- loadMembers();
- // Add here tests not requiring the test room
- if (targetRoomName.isEmpty())
- return;
-
- sendMessage();
- sendFile();
- setTopic();
- addAndRemoveTag();
- sendAndRedact();
- markDirectChat();
- // Add here tests with the test room
-}
-
-void QMCTest::loadMembers()
-{
- running.push_back("Loading members");
- auto* r = c->roomByAlias(QStringLiteral("#quotient:matrix.org"),
- JoinState::Join);
- if (!r) {
- cout << "#test:matrix.org is not found in the test user's rooms" << endl;
- QMC_CHECK("Loading members", false);
- return;
- }
- // It's not exactly correct because an arbitrary server might not support
- // lazy loading; but in the absence of capabilities framework we assume
- // it does.
- if (r->memberNames().size() >= r->joinedCount()) {
- cout << "Lazy loading doesn't seem to be enabled" << endl;
- QMC_CHECK("Loading members", false);
- return;
- }
- r->setDisplayed();
- connect(r, &Room::allMembersLoaded, [this, r] {
- QMC_CHECK("Loading members",
- r->memberNames().size() >= r->joinedCount());
- });
-}
-
-void QMCTest::sendMessage()
-{
- running.push_back("Message sending");
- cout << "Sending a message" << endl;
- auto txnId = targetRoom->postPlainText("Hello, " % origin % " is here");
- if (!validatePendingEvent(txnId)) {
- cout << "Invalid pending event right after submitting" << endl;
- QMC_CHECK("Message sending", false);
- return;
- }
-
- connectUntil(
- targetRoom, &Room::pendingEventAboutToMerge, this,
- [this, txnId](const RoomEvent* evt, int pendingIdx) {
- const auto& pendingEvents = targetRoom->pendingEvents();
- Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size()));
-
- if (evt->transactionId() != txnId)
- return false;
-
- QMC_CHECK("Message sending",
- is<RoomMessageEvent>(*evt) && !evt->id().isEmpty()
- && pendingEvents[size_t(pendingIdx)]->transactionId()
- == evt->transactionId());
- sendReaction(evt->id());
- return true;
- });
-}
-
-void QMCTest::sendReaction(const QString& targetEvtId)
-{
- running.push_back("Reaction sending");
- cout << "Reacting to the newest message in the room" << endl;
- Q_ASSERT(targetRoom->timelineSize() > 0);
- const auto key = QStringLiteral("+1");
- auto txnId = targetRoom->postReaction(targetEvtId, key);
- if (!validatePendingEvent(txnId)) {
- cout << "Invalid pending event right after submitting" << endl;
- QMC_CHECK("Reaction sending", false);
- return;
- }
-
- // TODO: Check that it came back as a reaction event and that it attached to
- // the right event
- connectUntil(
- targetRoom, &Room::updatedEvent, this,
- [this, txnId, key, targetEvtId](const QString& actualTargetEvtId) {
- if (actualTargetEvtId != targetEvtId)
- return false;
- const auto reactions = targetRoom->relatedEvents(
- targetEvtId, EventRelation::Annotation());
- // It's a test room, assuming no interference there should
- // be exactly one reaction
- if (reactions.size() != 1) {
- QMC_CHECK("Reaction sending", false);
- } else {
- const auto* evt =
- eventCast<const ReactionEvent>(reactions.back());
- QMC_CHECK("Reaction sending",
- is<ReactionEvent>(*evt) && !evt->id().isEmpty()
- && evt->relation().key == key
- && evt->transactionId() == txnId);
- }
- return true;
- });
-}
-
-void QMCTest::sendFile()
-{
- running.push_back("File sending");
- cout << "Sending a file" << endl;
- auto* tf = new QTemporaryFile;
- if (!tf->open()) {
- cout << "Failed to create a temporary file" << endl;
- QMC_CHECK("File sending", false);
- return;
- }
- tf->write("Test");
- tf->close();
- // QFileInfo::fileName brings only the file name; QFile::fileName brings
- // the full path
- const auto tfName = QFileInfo(*tf).fileName();
- cout << "Sending file" << tfName.toStdString() << endl;
- const auto txnId =
- targetRoom->postFile("Test file", QUrl::fromLocalFile(tf->fileName()));
- if (!validatePendingEvent(txnId)) {
- cout << "Invalid pending event right after submitting" << endl;
- QMC_CHECK("File sending", false);
- delete tf;
- return;
- }
-
- // FIXME: Clean away connections (connectUntil doesn't help here).
- connect(targetRoom, &Room::fileTransferCompleted, this,
- [this, txnId, tf, tfName](const QString& id) {
- auto fti = targetRoom->fileTransferInfo(id);
- Q_ASSERT(fti.status == FileTransferInfo::Completed);
-
- if (id != txnId)
- return;
-
- delete tf;
-
- checkFileSendingOutcome(txnId, tfName);
- });
- connect(targetRoom, &Room::fileTransferFailed, this,
- [this, txnId, tf](const QString& id, const QString& error) {
- if (id != txnId)
- return;
-
- targetRoom->postPlainText(origin % ": File upload failed: "
- % error);
- delete tf;
-
- QMC_CHECK("File sending", false);
- });
-}
-
-void QMCTest::checkFileSendingOutcome(const QString& txnId,
- const QString& fileName)
-{
- auto it = targetRoom->findPendingEvent(txnId);
- if (it == targetRoom->pendingEvents().end()) {
- cout << "Pending file event dropped before upload completion" << endl;
- QMC_CHECK("File sending", false);
- return;
- }
- if (it->deliveryStatus() != EventStatus::FileUploaded) {
- cout << "Pending file event status upon upload completion is "
- << it->deliveryStatus() << " != FileUploaded("
- << EventStatus::FileUploaded << ')' << endl;
- QMC_CHECK("File sending", false);
- return;
- }
-
- connectUntil(
- targetRoom, &Room::pendingEventAboutToMerge, this,
- [this, txnId, fileName](const RoomEvent* evt, int pendingIdx) {
- const auto& pendingEvents = targetRoom->pendingEvents();
- Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size()));
-
- if (evt->transactionId() != txnId)
- return false;
-
- cout << "File event " << txnId.toStdString()
- << " arrived in the timeline" << endl;
- visit(
- *evt,
- [&](const RoomMessageEvent& e) {
- QMC_CHECK(
- "File sending",
- !e.id().isEmpty()
- && pendingEvents[size_t(pendingIdx)]->transactionId()
- == txnId
- && e.hasFileContent()
- && e.content()->fileInfo()->originalName == fileName);
- },
- [this](const RoomEvent&) { QMC_CHECK("File sending", false); });
- return true;
- });
-}
-
-void QMCTest::setTopic()
-{
- static const char* const stateTestName = "State setting test";
- running.push_back(stateTestName);
-
- const auto newTopic = c->generateTxnId(); // Just a way to get a unique id
- targetRoom->setTopic(newTopic); // Sets the state by proper means
- const auto fakeTopic = c->generateTxnId();
- const auto fakeTxnId =
- targetRoom->postJson(RoomTopicEvent::matrixTypeId(), // Fake state event
- RoomTopicEvent(fakeTopic).contentJson());
-
- connectUntil(targetRoom, &Room::topicChanged, this,
- [this, newTopic] {
- if (targetRoom->topic() == newTopic) {
- QMC_CHECK(stateTestName, true);
- return true;
- }
- return false;
- });
-
- // Older Synapses allowed sending fake state events through, although
- // did not process them; // https://github.com/matrix-org/synapse/pull/5805
- // changed that and now Synapse 400's in response to fake state events.
- // The following two-step approach handles both cases, assuming that
- // Room::pendingEventChanged() with EventStatus::ReachedServer is guaranteed
- // to be emitted before Room::pendingEventAboutToMerge.
- connectUntil(
- targetRoom, &Room::pendingEventChanged, this,
- [this, fakeTopic, fakeTxnId](int pendingIdx) {
- const auto& pendingEvents = targetRoom->pendingEvents();
- Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size()));
- const auto& evt = pendingEvents[pendingIdx];
- if (evt->transactionId() != fakeTxnId)
- return false;
-
- // If Synapse rejected the event, skip the immunity test.
- if (evt.deliveryStatus() == EventStatus::SendingFailed)
- return true;
-
- if (evt.deliveryStatus() != EventStatus::ReachedServer)
- return false;
-
- // All before was just a preparation, this is where the test starts.
- static const char* const fakeStateTestName =
- "Fake state event immunity test";
- running.push_back(fakeStateTestName);
- connectUntil(
- targetRoom, &Room::pendingEventAboutToMerge, this,
- [this, fakeTopic](const RoomEvent* e, int) {
- if (e->contentJson().value("topic").toString() != fakeTopic)
- return false; // Wait on for the right event
-
- QMC_CHECK(fakeStateTestName, !e->isStateEvent());
- return true;
- });
- return true;
- });
-}
-
-void QMCTest::addAndRemoveTag()
-{
- running.push_back("Tagging test");
- static const auto TestTag = QStringLiteral("org.quotient.test");
- // Pre-requisite
- if (targetRoom->tags().contains(TestTag))
- targetRoom->removeTag(TestTag);
-
- // Connect first because the signal is emitted synchronously.
- connect(targetRoom, &Room::tagsChanged, targetRoom, [=] {
- cout << "Room " << targetRoom->id().toStdString()
- << ", tag(s) changed:" << endl
- << " " << targetRoom->tagNames().join(", ").toStdString() << endl;
- if (targetRoom->tags().contains(TestTag)) {
- cout << "Test tag set, removing it now" << endl;
- targetRoom->removeTag(TestTag);
- QMC_CHECK("Tagging test", !targetRoom->tags().contains(TestTag));
- disconnect(targetRoom, &Room::tagsChanged, nullptr, nullptr);
- }
- });
- cout << "Adding a tag" << endl;
- targetRoom->addTag(TestTag);
-}
-
-void QMCTest::sendAndRedact()
-{
- running.push_back("Redaction");
- cout << "Sending a message to redact" << endl;
- auto txnId = targetRoom->postPlainText(origin % ": message to redact");
- if (txnId.isEmpty()) {
- QMC_CHECK("Redaction", false);
- return;
- }
- connect(targetRoom, &Room::messageSent, this,
- [this, txnId](const QString& tId, const QString& evtId) {
- if (tId != txnId)
- return;
-
- cout << "Redacting the message" << endl;
- targetRoom->redactEvent(evtId, origin);
-
- connectUntil(targetRoom, &Room::addedMessages, this,
- [this, evtId] {
- return checkRedactionOutcome(evtId);
- });
- });
-}
-
-bool QMCTest::checkRedactionOutcome(const QString& evtIdToRedact)
-{
- // There are two possible (correct) outcomes: either the event comes already
- // redacted at the next sync, or the nearest sync completes with
- // the unredacted event but the next one brings redaction.
- auto it = targetRoom->findInTimeline(evtIdToRedact);
- if (it == targetRoom->timelineEdge())
- return false; // Waiting for the next sync
-
- if ((*it)->isRedacted()) {
- cout << "The sync brought already redacted message" << endl;
- QMC_CHECK("Redaction", true);
- } else {
- cout << "Message came non-redacted with the sync, waiting for redaction"
- << endl;
- connectUntil(targetRoom, &Room::replacedEvent, this,
- [this, evtIdToRedact](const RoomEvent* newEvent,
- const RoomEvent* oldEvent) {
- if (oldEvent->id() != evtIdToRedact)
- return false;
-
- QMC_CHECK("Redaction",
- newEvent->isRedacted()
- && newEvent->redactionReason() == origin);
- return true;
- });
- }
- return true;
-}
-
-void QMCTest::markDirectChat()
-{
- if (targetRoom->directChatUsers().contains(c->user())) {
- cout << "Warning: the room is already a direct chat,"
- " only unmarking will be tested"
- << endl;
- checkDirectChatOutcome({ { c->user(), targetRoom->id() } });
- return;
- }
- // Connect first because the signal is emitted synchronously.
- connect(c.data(), &Connection::directChatsListChanged, this,
- &QMCTest::checkDirectChatOutcome);
- cout << "Marking the room as a direct chat" << endl;
- c->addToDirectChats(targetRoom, c->user());
-}
-
-void QMCTest::checkDirectChatOutcome(const Connection::DirectChatsMap& added)
-{
- running.push_back("Direct chat test");
- disconnect(c.data(), &Connection::directChatsListChanged, nullptr, nullptr);
- if (!targetRoom->isDirectChat()) {
- cout << "The room has not been marked as a direct chat" << endl;
- QMC_CHECK("Direct chat test", false);
- return;
- }
- if (!added.contains(c->user(), targetRoom->id())) {
- cout << "The room has not been listed in new direct chats" << endl;
- QMC_CHECK("Direct chat test", false);
- return;
- }
-
- cout << "Unmarking the direct chat" << endl;
- c->removeFromDirectChats(targetRoom->id(), c->user());
- QMC_CHECK("Direct chat test", !c->isDirectChat(targetRoom->id()));
-}
-
-void QMCTest::conclude()
-{
- c->stopSync();
- auto succeededRec = QString::number(succeeded.size()) + " tests succeeded";
- if (!failed.isEmpty() || !running.isEmpty())
- succeededRec +=
- " of "
- % QString::number(succeeded.size() + failed.size() + running.size())
- % " total";
- QString plainReport = origin % ": Testing complete, " % succeededRec;
- QString color = failed.isEmpty() && running.isEmpty() ? "00AA00" : "AA0000";
- QString htmlReport = origin % ": <strong><font data-mx-color='#" % color
- % "' color='#" % color
- % "'>Testing complete</font></strong>, " % succeededRec;
- if (!failed.isEmpty()) {
- plainReport += "\nFAILED: " % failed.join(", ");
- htmlReport += "<br><strong>Failed:</strong> " % failed.join(", ");
- }
- if (!running.isEmpty()) {
- plainReport += "\nDID NOT FINISH: " % running.join(", ");
- htmlReport += "<br><strong>Did not finish:</strong> "
- % running.join(", ");
- }
- cout << plainReport.toStdString() << endl;
-
- if (targetRoom) {
- // TODO: Waiting for proper futures to come so that it could be:
- // targetRoom->postHtmlText(...)
- // .then(this, &QMCTest::finalize); // Qt-style or
- // .then([this] { finalize(); }); // STL-style
- auto txnId = targetRoom->postHtmlText(plainReport, htmlReport);
- connect(targetRoom, &Room::messageSent, this,
- [this, txnId](QString serverTxnId) {
- if (txnId != serverTxnId)
- return;
-
- cout << "Leaving the room" << endl;
- connect(targetRoom->leaveRoom(), &BaseJob::finished, this,
- &QMCTest::finalize);
- });
- } else
- finalize();
-}
-
-void QMCTest::finalize()
-{
- cout << "Logging out" << endl;
- c->logout();
- connect(c.data(), &Connection::loggedOut, qApp, [this] {
- QCoreApplication::processEvents();
- QCoreApplication::exit(failed.size() + running.size());
- });
-}
-
-int main(int argc, char* argv[])
-{
- QCoreApplication app(argc, argv);
- if (argc < 4) {
- cout << "Usage: qmc-example <user> <passwd> <device_name> "
- "[<room_alias> [origin]]"
- << endl;
- return -1;
- }
-
- cout << "Connecting to the server as " << argv[1] << endl;
- auto conn = new Connection;
- conn->connectToServer(argv[1], argv[2], argv[3]);
- QMCTest test { conn, argc >= 5 ? argv[4] : nullptr,
- argc >= 6 ? argv[5] : nullptr };
- return app.exec();
-}