From 174fb95793ff00c47d4a5eae08e7ee9864cf95ea Mon Sep 17 00:00:00 2001 From: mydoxie <351276057+mydoxie@users.noreply.github.com> Date: Mon, 2 Nov 2026 20:52:20 +0700 Subject: [PATCH] Fix the article for 'you' sounds and restore a custom Mail queue --- CHANGELOG.md | 4 +-- lib/src/queue.dart | 28 +++++++++++++--- lib/src/queue_fake.dart | 12 +++++-- test/queue_fake_test.dart | 70 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5139660..03b5d70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ ## 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. +- Fix: the `QueueFake` check messages had bad grammar, like "a AllTypesJob" and "1 times". Now they say "an AllTypesJob" and "1 time". A name that starts with a "you" sound (like `UserJob` or `EuJob`) gets "a". +- 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 back the queue it had before `Queue.fake()` (also one from `Mail.useQueue()`). If it had none: 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 e75986c..bee90cf 100644 --- a/lib/src/queue.dart +++ b/lib/src/queue.dart @@ -1,7 +1,8 @@ import 'dart:convert'; import 'package:dox_core/dox_core.dart' show AESEncryptor, Dox; -import 'package:dox_mailer/dox_mailer.dart' show Mail; +import 'package:dox_mailer/dox_mailer.dart' + show ArrayTransport, Mail, MailQueue; import 'job.dart'; import 'job_registry.dart'; @@ -23,6 +24,7 @@ class Queue { static QueueConfig? _config; static QueueDriver? _driver; static QueueFake? _fake; + static MailQueue? _mailQueueBeforeFake; /// Keep dispatched jobs in a [QueueFake] and run or save none of them, /// so a test can check what was pushed. It needs no [configure]. It @@ -37,17 +39,33 @@ class Queue { /// ``` static QueueFake fake() { QueueFake fake = QueueFake(); + // keep the mail queue set before the fake (a second fake() keeps the first) + if (_fake == null) _mailQueueBeforeFake = _currentMailQueue(); _fake = fake; Mail.useQueue(const QueueMailQueue()); return fake; } - /// 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]. + /// Stop the fake: [dispatch] uses the config again. `Mail.queue()` gets + /// the queue it had before [fake] (also one from `Mail.useQueue()`). If + /// it had none, it uses this queue when [configure] was called, else it + /// has no queue. static void restore() { + MailQueue? before = _mailQueueBeforeFake; _fake = null; - Mail.useQueue(_config == null ? null : const QueueMailQueue()); + _mailQueueBeforeFake = null; + Mail.useQueue(before ?? (_config == null ? null : const QueueMailQueue())); + } + + /// the queue of `Mail.useQueue()`, or null. While `Mail.fake()` is on, + /// `Mail.mailQueue` gives that fake, which is not a queue to keep. + static MailQueue? _currentMailQueue() { + try { + MailQueue queue = Mail.mailQueue; + return queue is ArrayTransport ? null : queue; + } on StateError { + return null; + } } /// Use [config] in this isolate. `QueueProvider` calls it. Call it in diff --git a/lib/src/queue_fake.dart b/lib/src/queue_fake.dart index 1080804..d01b5a3 100644 --- a/lib/src/queue_fake.dart +++ b/lib/src/queue_fake.dart @@ -87,10 +87,18 @@ class QueueFake { return 'Pushed: $names.'; } - /// `a SendWelcomeEmail`, `an AllTypesJob` + /// `a SendWelcomeEmail`, `an AllTypesJob`, `a UserJob`. A vowel gives + /// "an", but "U" + consonant + vowel (User, Uni) and "Eu" sound like + /// "you", so they give "a". + // ponytail: a small rule, not a full English dictionary (hour, honest) + static final RegExp _youSound = RegExp( + '^(eu|u[^aeiou][aeiou])', + caseSensitive: false, + ); + static String _a(String name) { bool vowel = 'AEIOUaeiou'.contains(name.isEmpty ? 'x' : name[0]); - return vowel ? 'an $name' : 'a $name'; + return vowel && !_youSound.hasMatch(name) ? 'an $name' : 'a $name'; } /// `1 time`, `2 times` diff --git a/test/queue_fake_test.dart b/test/queue_fake_test.dart index 38c767a..95db25b 100644 --- a/test/queue_fake_test.dart +++ b/test/queue_fake_test.dart @@ -14,6 +14,42 @@ String _failure(void Function() body) { fail('it did not throw'); } +class UserJob extends DoxJob { + @override + Future handle() async {} +} + +class UniJob extends DoxJob { + @override + Future handle() async {} +} + +class EuJob extends DoxJob { + @override + Future handle() async {} +} + +class UploadJob extends DoxJob { + @override + Future handle() async {} +} + +class OrderJob extends DoxJob { + @override + Future handle() async {} +} + +class _CustomQueue implements MailQueue { + @override + Future push( + String mailer, + Message message, { + Duration? delay, + String? queue, + bool encrypt = false, + }) async => 'id'; +} + bool _isUser2(SendWelcomeEmail job) => job.userId == 2; bool _isUser9(SendWelcomeEmail job) => job.userId == 9; @@ -137,4 +173,38 @@ void main() { expect(JobLog.handled, ['welcome 4']); expect(queue.pushed, isEmpty); }); + + test('the article is "a" for a "you" sound, "an" for other vowels', () { + expect( + _failure(queue.assertPushed), + startsWith('Expected a UserJob job'), + ); + expect( + _failure(queue.assertPushed), + startsWith('Expected a UniJob job'), + ); + expect( + _failure(queue.assertPushed), + startsWith('Expected a EuJob job'), + ); + expect( + _failure(queue.assertPushed), + startsWith('Expected an UploadJob job'), + ); + expect( + _failure(queue.assertPushed), + startsWith('Expected an OrderJob job'), + ); + }); + + test('restore puts back a Mail.useQueue() set before the fake', () { + Queue.restore(); + _CustomQueue custom = _CustomQueue(); + Mail.useQueue(custom); + Queue.fake(); + expect(Mail.mailQueue, isNot(same(custom))); + Queue.restore(); + expect(Mail.mailQueue, same(custom)); + Mail.useQueue(null); + }); }