Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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<T>([test])`, `assertPushedTimes<T>(n)`, `assertNotPushed<T>()` and `assertNothingPushed()`. Read them with `pushed` (each has the job, the queue name and the delay) or `jobsOf<T>()`. 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 <ann@example.com>: 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.
Expand Down
28 changes: 23 additions & 5 deletions lib/src/queue.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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
Expand All @@ -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
Expand Down
12 changes: 10 additions & 2 deletions lib/src/queue_fake.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
70 changes: 70 additions & 0 deletions test/queue_fake_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,42 @@ String _failure(void Function() body) {
fail('it did not throw');
}

class UserJob extends DoxJob {
@override
Future<void> handle() async {}
}

class UniJob extends DoxJob {
@override
Future<void> handle() async {}
}

class EuJob extends DoxJob {
@override
Future<void> handle() async {}
}

class UploadJob extends DoxJob {
@override
Future<void> handle() async {}
}

class OrderJob extends DoxJob {
@override
Future<void> handle() async {}
}

class _CustomQueue implements MailQueue {
@override
Future<String> 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;
Expand Down Expand Up @@ -137,4 +173,38 @@ void main() {
expect(JobLog.handled, <String>['welcome 4']);
expect(queue.pushed, isEmpty);
});

test('the article is "a" for a "you" sound, "an" for other vowels', () {
expect(
_failure(queue.assertPushed<UserJob>),
startsWith('Expected a UserJob job'),
);
expect(
_failure(queue.assertPushed<UniJob>),
startsWith('Expected a UniJob job'),
);
expect(
_failure(queue.assertPushed<EuJob>),
startsWith('Expected a EuJob job'),
);
expect(
_failure(queue.assertPushed<UploadJob>),
startsWith('Expected an UploadJob job'),
);
expect(
_failure(queue.assertPushed<OrderJob>),
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);
});
}
Loading