aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-18 12:12:36 +0200
committerDanilo M. <danix@danix.xyz>2026-08-18 12:12:36 +0200
commita089820aba3bc51884ad880b9e3cf689eda7361d (patch)
tree1caa6c674b8d56f23da90ca5907375141b70ae7f /tests
parent262174407eabcb986f15c116d39b7ab98fdf0150 (diff)
downloadqtmaildir-a089820aba3bc51884ad880b9e3cf689eda7361d.tar.gz
qtmaildir-a089820aba3bc51884ad880b9e3cf689eda7361d.zip
fix(tags): let a tag be removed even when its name breaks the rules
validateTagName() ran on the removal list as well as the addition list, so a tag whose name contains a space could be seen on a message and never deleted: the one dialog that could clear it refused the only text that names it, and it did so with a modal warning, so the dialog would not even close. Whether a tag SHOULD exist is a separate question from whether the user may delete one that already does, and the answer to the second is always yes. Validation now runs on additions only, which is where the rule earns its keep: it stops a troublesome name being created. Reached by a real Maildir folder named "Inbox/SlackBuilds users", whose origin tag carried the space through. Only the TYPED route was ever blocked; unchecking the tag in the list appends to the removal list after validation has run and worked throughout. The test asserts both routes for that reason, and asserts that ADDING a spaced tag is still refused, since the fix must not weaken the rule it narrows. The natural mutation for this test hangs rather than fails: restoring the validation raises a modal warning with nothing to dismiss it. The test avoids calling accept() on the add-rejection case and asks the validator directly, and the mutation check drops the tag silently instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'tests')
-rw-r--r--tests/test_tagdialog.cpp57
1 files changed, 57 insertions, 0 deletions
diff --git a/tests/test_tagdialog.cpp b/tests/test_tagdialog.cpp
index 5289599..9b12109 100644
--- a/tests/test_tagdialog.cpp
+++ b/tests/test_tagdialog.cpp
@@ -47,6 +47,7 @@ private slots:
void acceptingACandidateKeepsTheOtherTags();
void removeCompletesOnlyTheSelectionsOwnTags();
void removeStillAcceptsATagItDoesNotSuggest();
+ void aTagWithASpaceCanStillBeRemoved();
};
void TestTagDialog::validNamesAreAccepted()
@@ -166,6 +167,62 @@ void TestTagDialog::multipleTagsSeparateOnComma()
QStringLiteral("three") }));
}
+void TestTagDialog::aTagWithASpaceCanStillBeRemoved()
+{
+ // validateTagName() rejects a space, and that rule is right: it stops a
+ // troublesome tag being CREATED. It ran on the removal list too, which is
+ // not the same question. A tag that already exists is a fact, and refusing
+ // to remove it because it breaks a naming rule leaves the user with a tag
+ // they can see and cannot get rid of.
+ //
+ // Reached by a real Maildir: a folder named "Inbox/SlackBuilds users"
+ // produced `deleted-from:Inbox/SlackBuilds users`, and the one dialog that
+ // could have cleared it refused the only text that names it.
+ //
+ // Only the TYPED route was blocked. Unchecking appends to the removal list
+ // after validation has run, so it worked throughout; that asymmetry is why
+ // both routes are asserted here rather than just the one that failed.
+ const QString spaced =
+ QStringLiteral("deleted-from:Inbox/SlackBuilds users");
+ QHash<QString, int> current;
+ current.insert(spaced, 1);
+
+ // Typed into the remove field, which is what a user does for a tag they
+ // can see on the message. Before the fix this raised a modal warning and
+ // returned without accepting, so the dialog simply would not close.
+ TagDialog typed({ spaced }, current, 1);
+ const QList<QLineEdit *> edits = typed.findChildren<QLineEdit *>();
+ QCOMPARE(edits.size(), 2);
+ edits.at(1)->setText(spaced);
+ typed.accept();
+
+ QCOMPARE(typed.tagsToRemove(), QStringList{ spaced });
+ QVERIFY(typed.tagsToAdd().isEmpty());
+
+ // And unchecking it in the list, the other way to the same place.
+ TagDialog unchecked({ spaced }, current, 1);
+ auto *list = unchecked.findChild<QListWidget *>();
+ QVERIFY(list);
+ QCOMPARE(list->count(), 1);
+ QCOMPARE(list->item(0)->data(Qt::UserRole).toString(), spaced);
+ list->item(0)->setCheckState(Qt::Unchecked);
+ unchecked.accept();
+
+ QCOMPARE(unchecked.tagsToRemove(), QStringList{ spaced });
+
+ // ADDING one is still refused, which is the rule this must not have
+ // weakened. accept() returns without setting the lists, so the dialog
+ // stays open with the text there to fix.
+ TagDialog added({}, {}, 1);
+ const QList<QLineEdit *> addEdits = added.findChildren<QLineEdit *>();
+ QCOMPARE(addEdits.size(), 2);
+ addEdits.at(0)->setText(QStringLiteral("two words"));
+ // Not calling accept(): it would raise a modal warning and block. The
+ // validator is the thing under test and is asked directly.
+ QVERIFY(validateTagName(QStringLiteral("two words"))
+ != TagNameProblem::Ok);
+}
+
void TestTagDialog::uncheckingACurrentTagRemovesIt()
{
// Every selected thread carries "inbox", so its box starts checked.