From 4cfbdc3aece0e45212dd3be121ec88efd24984c5 Mon Sep 17 00:00:00 2001 From: mydoxie <351276057+mydoxie@users.noreply.github.com> Date: Mon, 2 Nov 2026 18:38:46 +0700 Subject: [PATCH 1/3] Fix flaky test: random encrypted text can contain xxxx --- test/queue_mail_test.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/queue_mail_test.dart b/test/queue_mail_test.dart index 9879a73..37a21a8 100644 --- a/test/queue_mail_test.dart +++ b/test/queue_mail_test.dart @@ -209,6 +209,10 @@ void main() { String log = await _logOf(_retryThenFail); String error = (await Queue.driver.failed()).single.error; for (String text in [log, error]) { + // the encrypted details are random base64 text. About 1 in 300 + // has "xxxx" in it by chance, and each run makes several. Check + // the text around them only. + text = text.replaceAll(_encrypted, 'Details are encrypted: ...'); expect(text, isNot(contains('918273'))); expect(text, isNot(contains('xxxx'))); expect(text, contains('MailException (code 552). Details are')); @@ -235,6 +239,9 @@ void main() { } } +/// the encrypted details of a [RedactedMailError] text +final RegExp _encrypted = RegExp(r'Details are encrypted: [^\s"]+'); + /// two tries: the first is released, the second fails Future _retryThenFail() async { await _work('default', 2); From a6bdb86d9f56233b4f3de4d09272daf6fd0700e6 Mon Sep 17 00:00:00 2001 From: mydoxie <351276057+mydoxie@users.noreply.github.com> Date: Mon, 2 Nov 2026 18:39:22 +0700 Subject: [PATCH 2/3] Queue.restore undoes the Mail.useQueue of Queue.fake --- CHANGELOG.md | 1 + lib/src/queue.dart | 5 ++++- test/queue_fake_test.dart | 6 ++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c14dd85..ff4609e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## Unreleased +- Fix: `Queue.restore()` did not undo the `Mail.useQueue()` call of `Queue.fake()`. With no `Queue.configure()`, `Mail.queue()` still found a queue after the test. Now `restore()` puts it back: no queue, or this queue when `Queue.configure()` was called. - New: `Queue.fake()` for tests. It keeps every dispatched job in a `QueueFake` and does not run or save them. Check them with `assertPushed([test])`, `assertPushedTimes(n)`, `assertNotPushed()` and `assertNothingPushed()`. Read them with `pushed` (each has the job, the queue name and the delay) or `jobsOf()`. It also catches `Mail.queue()` and `Mail.later()`. `dispatchSync()` still runs the job. `Queue.restore()` stops the fake. - Fix: with `Mail.queue(message, encrypt: true)`, a send error could name the recipient (eg. an SMTP reply `550 : mailbox unavailable`). The worker wrote that error to its log and to the failed jobs in plain text. Now the encrypted mail job throws a `RedactedMailError`. Its text has only the error type and the SMTP or HTTP code. The full error text, with the answer of the server, is encrypted with the app key (at most the first 64K characters, counted in UTF-16 code units). Read it with `RedactedMailError.decrypt(job.error)`. When the error can not be encrypted, only the type and the code are kept. Mail jobs without `encrypt` keep the full error, as before. - New: `dox queue:table` writes a migration for the `jobs` and `failed_jobs` tables in `db/migration`. It calls `QueueTables.up` and `QueueTables.down`. It does not overwrite: when a `create_queue_tables` migration is already there, it prints so and exits with code 1. It is a package command (`dox: commands:` in pubspec.yaml), so it needs a `dox` CLI that finds package commands. diff --git a/lib/src/queue.dart b/lib/src/queue.dart index b9de167..e75986c 100644 --- a/lib/src/queue.dart +++ b/lib/src/queue.dart @@ -42,9 +42,12 @@ class Queue { return fake; } - /// Stop the fake: [dispatch] uses the config again. + /// Stop the fake: [dispatch] uses the config again. `Mail.queue()` uses + /// this queue again only when [configure] was called. Else it has no + /// queue, like before [fake]. static void restore() { _fake = null; + Mail.useQueue(_config == null ? null : const QueueMailQueue()); } /// Use [config] in this isolate. `QueueProvider` calls it. Call it in diff --git a/test/queue_fake_test.dart b/test/queue_fake_test.dart index afbba1b..a288cc2 100644 --- a/test/queue_fake_test.dart +++ b/test/queue_fake_test.dart @@ -124,6 +124,12 @@ void main() { expect(queue.pushed.last.delay, Duration(minutes: 1)); }); + test('restore takes back Mail.queue() when there is no config', () { + Queue.restore(); + // like before Queue.fake(): Mail.queue() has no queue + expect(() => Mail.mailQueue, throwsStateError); + }); + test('restore stops the fake', () async { Queue.restore(); Queue.configure(QueueConfig()); From 8dfa393a74961aec1b90d2fb7edcfa2073be8599 Mon Sep 17 00:00:00 2001 From: mydoxie <351276057+mydoxie@users.noreply.github.com> Date: Mon, 2 Nov 2026 18:39:53 +0700 Subject: [PATCH 3/3] Fix grammar of the QueueFake check messages --- CHANGELOG.md | 1 + lib/src/queue_fake.dart | 29 +++++++++++++++++++++++------ test/queue_fake_test.dart | 8 ++++---- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff4609e..5139660 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## Unreleased +- Fix: the `QueueFake` check messages had bad grammar, like "a AllTypesJob" and "1 times". Now they say "an AllTypesJob" and "1 time". - Fix: `Queue.restore()` did not undo the `Mail.useQueue()` call of `Queue.fake()`. With no `Queue.configure()`, `Mail.queue()` still found a queue after the test. Now `restore()` puts it back: no queue, or this queue when `Queue.configure()` was called. - New: `Queue.fake()` for tests. It keeps every dispatched job in a `QueueFake` and does not run or save them. Check them with `assertPushed([test])`, `assertPushedTimes(n)`, `assertNotPushed()` and `assertNothingPushed()`. Read them with `pushed` (each has the job, the queue name and the delay) or `jobsOf()`. It also catches `Mail.queue()` and `Mail.later()`. `dispatchSync()` still runs the job. `Queue.restore()` stops the fake. - Fix: with `Mail.queue(message, encrypt: true)`, a send error could name the recipient (eg. an SMTP reply `550 : mailbox unavailable`). The worker wrote that error to its log and to the failed jobs in plain text. Now the encrypted mail job throws a `RedactedMailError`. Its text has only the error type and the SMTP or HTTP code. The full error text, with the answer of the server, is encrypted with the app key (at most the first 64K characters, counted in UTF-16 code units). Read it with `RedactedMailError.decrypt(job.error)`. When the error can not be encrypted, only the type and the code are kept. Mail jobs without `encrypt` keep the full error, as before. diff --git a/lib/src/queue_fake.dart b/lib/src/queue_fake.dart index 09920ea..1080804 100644 --- a/lib/src/queue_fake.dart +++ b/lib/src/queue_fake.dart @@ -36,12 +36,15 @@ class QueueFake { void assertPushed([bool Function(T job)? test]) { List jobs = jobsOf(); if (jobs.isEmpty) { - throw AssertionError('Expected a $T job to be pushed. ${_summary()}'); + throw AssertionError( + 'Expected ${_a('$T')} job to be pushed. ${_summary()}', + ); } if (test != null && !jobs.any(test)) { throw AssertionError( - 'Expected a $T job that passes the test to be pushed, but none of ' - 'the ${jobs.length} pushed $T jobs passed it.', + 'Expected ${_a('$T')} job that passes the test to be pushed, but ' + 'none of the ${jobs.length} pushed $T ${_plural(jobs.length, 'job')} ' + 'passed it.', ); } } @@ -51,8 +54,8 @@ class QueueFake { int count = pushedOf().length; if (count != times) { throw AssertionError( - 'Expected $T to be pushed $times times, but it was pushed ' - '$count times.', + 'Expected $T to be pushed ${_times(times)}, but it was pushed ' + '${_times(count)}.', ); } } @@ -62,7 +65,8 @@ class QueueFake { int count = pushedOf().length; if (count > 0) { throw AssertionError( - 'Expected no $T job to be pushed, but it was pushed $count times.', + 'Expected no $T job to be pushed, but it was pushed ' + '${_times(count)}.', ); } } @@ -83,6 +87,19 @@ class QueueFake { return 'Pushed: $names.'; } + /// `a SendWelcomeEmail`, `an AllTypesJob` + static String _a(String name) { + bool vowel = 'AEIOUaeiou'.contains(name.isEmpty ? 'x' : name[0]); + return vowel ? 'an $name' : 'a $name'; + } + + /// `1 time`, `2 times` + static String _times(int count) => '$count ${_plural(count, 'time')}'; + + static String _plural(int count, String word) { + return count == 1 ? word : '${word}s'; + } + static String _name(PushedJob item) => '${item.job.runtimeType}'; static bool _isA(PushedJob item) => item.job is T; diff --git a/test/queue_fake_test.dart b/test/queue_fake_test.dart index a288cc2..38c767a 100644 --- a/test/queue_fake_test.dart +++ b/test/queue_fake_test.dart @@ -60,11 +60,11 @@ void main() { expect( _failure(() => queue.assertPushed(_isUser9)), 'Expected a SendWelcomeEmail job that passes the test to be pushed, ' - 'but none of the 1 pushed SendWelcomeEmail jobs passed it.', + 'but none of the 1 pushed SendWelcomeEmail job passed it.', ); expect( _failure(queue.assertPushed), - 'Expected a AllTypesJob job to be pushed. Pushed: SendWelcomeEmail.', + 'Expected an AllTypesJob job to be pushed. Pushed: SendWelcomeEmail.', ); }); @@ -75,7 +75,7 @@ void main() { queue.assertPushedTimes(0); expect( _failure(() => queue.assertPushedTimes(1)), - 'Expected SendWelcomeEmail to be pushed 1 times, but it was pushed ' + 'Expected SendWelcomeEmail to be pushed 1 time, but it was pushed ' '2 times.', ); }); @@ -86,7 +86,7 @@ void main() { expect( _failure(queue.assertNotPushed), 'Expected no SendWelcomeEmail job to be pushed, but it was pushed ' - '1 times.', + '1 time.', ); });