From f70d5c7460e6ca68f64c66966f2b20297f38e9c2 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sun, 4 Oct 2026 18:09:41 +0200 Subject: [PATCH] Verify invitation title delivery and one-line full-link actions --- tests/test_dm_invite.c | 9 ++++ tools/chatgui/tests/test_invite_links.cpp | 55 +++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/tests/test_dm_invite.c b/tests/test_dm_invite.c index d84844b2..18c16949 100644 --- a/tests/test_dm_invite.c +++ b/tests/test_dm_invite.c @@ -172,6 +172,8 @@ int main(void) { chat_core_create_channel_auto(nodes[0], "target"); CHECK(!scalar(nodes[0], "SELECT channel_id FROM channels WHERE name='target'", &target), "create different target group"); CHECK(target != source && !message_count(), "initial empty PM"); + CHECK(sqlite3_exec(nodes[0]->topo_sqlite_db, "UPDATE channels SET name='target \"группа\"\n<>&' WHERE name='target'", + NULL, NULL, NULL) == SQLITE_OK, "group title with Unicode, quotes and newline"); char target_id[64]; snprintf(target_id, sizeof(target_id), "%llu", (unsigned long long)target); for (int i = 0; i < 5; i++) { char text[64]; snprintf(text, sizeof(text), "history before invitation %d", i); @@ -215,6 +217,13 @@ int main(void) { if (!dm_list_messages_json(nodes[1], conv, 10, 0, messages, sizeof(messages), &length)) link = strstr(messages, "utun://"); } while (!link && get_time_tb() < deadline); CHECK(link, "invite delivered to B over source group's UDP route"); + CHECK(strstr(messages, "\"ct\":\"invite\"") && + strstr(messages, "\"tags\":{\"group_name\":\"target \\\"группа\\\"\\n<>&\"}"), + "encrypted group name survives delivery and JSON escaping without sender-language text"); + char outgoing[8192]; size_t outgoing_len = 0; + CHECK(!dm_list_messages_json(nodes[0], conv, 10, 0, outgoing, sizeof(outgoing), &outgoing_len) && + strstr(outgoing, "\"tags\":{\"group_name\":\"target \\\"группа\\\"\\n<>&\"}"), + "sender and recipient receive the same group-name tag"); char* end = strchr(link, '"'); CHECK(end, "complete invite text in PM"); *end = '\0'; struct InviteData received; char error[128]; CHECK(!invite_link_decode(link, strlen(link), &received, error, sizeof(error)) && received.channelId == target, diff --git a/tools/chatgui/tests/test_invite_links.cpp b/tools/chatgui/tests/test_invite_links.cpp index c200b3ff..eb35d8b1 100644 --- a/tools/chatgui/tests/test_invite_links.cpp +++ b/tools/chatgui/tests/test_invite_links.cpp @@ -4,6 +4,7 @@ #include "../src/accountdelegate.h" #include #include +#include extern "C" { #include "chat/invite_link.h" } @@ -11,6 +12,60 @@ extern "C" { class InviteLinksTest : public QObject { Q_OBJECT private slots: + void compactInvitationUsesFullLinkForClickAndCopy() { + struct InviteData invite = {}; + invite.channelId = 42; invite.join_key = 123; invite.pubkey[0] = 1; invite.addrCount = 8; + for (auto& address : invite.addrs) { address.family = 6; address.proto = INVITE_PROTO_TCP; address.port = 443; } + char buffer[1024]; + QVERIFY(invite_link_encode(&invite, nullptr, buffer, sizeof(buffer)) > 0); + const QString link = QString::fromUtf8(buffer); + const QString text = QStringLiteral("Приглашение в группу «Группа»\n") + link; + for (int width : {240, 480}) { + QStandardItemModel model; + auto* message = new QStandardItem; + message->setData(text, MsgTextRole); message->setData(link, MsgInviteLinkRole); + message->setData(QStringLiteral("text/plain"), MsgContentTypeRole); message->setData(true, MsgIsOutgoingRole); + model.appendRow(message); + ChatView view; MessageDelegate delegate; + view.setModel(&model); view.setItemDelegate(&delegate); view.resize(width, 300); + view.setInviteHitTest([&](const QModelIndex& index, const QPoint& point) { + QStyleOptionViewItem option; option.initFrom(&view); option.widget = &view; option.rect = view.visualRect(index); + return delegate.inviteLinkAt(option, index, point); + }); + view.show(); QTest::qWait(30); + const QModelIndex index = model.index(0, 0); + QStyleOptionViewItem option; option.initFrom(&view); option.widget = &view; option.rect = view.visualRect(index); + QPoint firstPoint, lastPoint; + QSet rows; + for (int y = 0; y < option.rect.height(); ++y) { + for (int x = 0; x < view.viewport()->width(); x += 2) { + const QString hit = delegate.inviteLinkAt(option, index, QPoint(x, y)); + if (hit.isEmpty()) continue; + QCOMPARE(hit, link); + rows.insert(y); + if (firstPoint.isNull()) firstPoint = QPoint(x, y); + lastPoint = QPoint(x, y); + } + } + QVERIFY(!firstPoint.isNull()); + QVERIFY(rows.size() <= QFontMetrics(option.font).height() + 1); + QVERIFY(option.rect.height() < 160); + QSignalSpy clicked(&view, &ChatView::inviteLinkActivated); + QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, lastPoint); + QCOMPARE(clicked.count(), 1); QCOMPARE(clicked.last().first().toString(), link); + QTest::mousePress(view.viewport(), Qt::LeftButton, Qt::NoModifier, firstPoint); + QMouseEvent drag(QEvent::MouseMove, lastPoint, view.viewport()->mapToGlobal(lastPoint), + Qt::NoButton, Qt::LeftButton, Qt::NoModifier); + QApplication::sendEvent(view.viewport(), &drag); + QTest::mouseRelease(view.viewport(), Qt::LeftButton, Qt::NoModifier, lastPoint); + QTest::qWait(20); + QCOMPARE(clicked.count(), 1); + view.copySelection(); + QCOMPARE(QApplication::clipboard()->text(), link); + QCOMPARE(index.data(MsgTextRole).toString(), text); + } + } + void wrappedLinksPreserveTheirCompleteText() { struct InviteData invite = {}; invite.channelId = 42; invite.join_key = 123; invite.pubkey[0] = 1; invite.addrCount = 8;