mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Rework email templates PR after discussions
This commit is contained in:
@@ -2268,7 +2268,7 @@ Http::post('/v1/account/tokens/magic-url')
|
||||
|
||||
$customTemplate =
|
||||
$project->getAttribute('templates', [])['email.magicSession-' . $locale->default] ??
|
||||
$project->getAttribute('templates', [])['email.magicSession-worldwide'] ?? [];
|
||||
$project->getAttribute('templates', [])['email.magicSession-' . $locale->fallback] ?? [];
|
||||
|
||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||
$agentOs = $detector->getOS();
|
||||
@@ -2580,7 +2580,7 @@ Http::post('/v1/account/tokens/email')
|
||||
|
||||
$customTemplate =
|
||||
$project->getAttribute('templates', [])['email.otpSession-' . $locale->default] ??
|
||||
$project->getAttribute('templates', [])['email.otpSession-worldwide'] ?? [];
|
||||
$project->getAttribute('templates', [])['email.otpSession-' . $locale->fallback] ?? [];
|
||||
$smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base');
|
||||
|
||||
$validator = new FileName();
|
||||
@@ -3728,7 +3728,7 @@ Http::post('/v1/account/recovery')
|
||||
$preview = $locale->getText("emails.recovery.preview");
|
||||
$customTemplate =
|
||||
$project->getAttribute('templates', [])['email.recovery-' . $locale->default] ??
|
||||
$project->getAttribute('templates', [])['email.recovery-worldwide'] ?? [];
|
||||
$project->getAttribute('templates', [])['email.recovery-' . $locale->fallback] ?? [];
|
||||
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl');
|
||||
$message
|
||||
@@ -4038,7 +4038,7 @@ Http::post('/v1/account/verifications/email')
|
||||
|
||||
$customTemplate =
|
||||
$project->getAttribute('templates', [])['email.verification-' . $locale->default] ??
|
||||
$project->getAttribute('templates', [])['email.verification-worldwide'] ?? [];
|
||||
$project->getAttribute('templates', [])['email.verification-' . $locale->fallback] ?? [];
|
||||
$smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base');
|
||||
|
||||
$validator = new FileName();
|
||||
|
||||
@@ -852,13 +852,13 @@ Http::get('/v1/projects/:projectId/templates/email/:type/:locale')
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('type', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Template type')
|
||||
->param('locale', 'worldwide', fn ($localeCodes) => new WhiteList(\array_merge([
|
||||
...$localeCodes,
|
||||
'worldwide'
|
||||
])), 'Template locale', true, ['localeCodes'])
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', true, ['localeCodes'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform) {
|
||||
->inject('locale')
|
||||
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform, Locale $localeObject) {
|
||||
$locale = $locale ?: $localeObject->default ?: $localeObject->fallback ?: System::getEnv('_APP_LOCALE', 'en');
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
@@ -868,9 +868,8 @@ Http::get('/v1/projects/:projectId/templates/email/:type/:locale')
|
||||
$templates = $project->getAttribute('templates', []);
|
||||
$template = $templates['email.' . $type . '-' . $locale] ?? null;
|
||||
|
||||
$fallbackLocale = System::getEnv('_APP_LOCALE', 'en');
|
||||
$localeObj = new Locale($locale === 'worldwide' ? $fallbackLocale : $locale);
|
||||
$localeObj->setFallback($fallbackLocale);
|
||||
$localeObj = new Locale($locale);
|
||||
$localeObj->setFallback(System::getEnv('_APP_LOCALE', 'en'));
|
||||
|
||||
if (is_null($template)) {
|
||||
/**
|
||||
@@ -954,10 +953,7 @@ Http::patch('/v1/projects/:projectId/templates/email/:type/:locale')
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('type', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Template type')
|
||||
->param('locale', 'worldwide', fn ($localeCodes) => new WhiteList(\array_merge([
|
||||
...$localeCodes,
|
||||
'worldwide'
|
||||
])), 'Template locale', true, ['localeCodes'])
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', true, ['localeCodes'])
|
||||
->param('subject', '', new Text(255), 'Email Subject')
|
||||
->param('message', '', new Text(0), 'Template message')
|
||||
->param('senderName', '', new Text(255, 0), 'Name of the email sender', true)
|
||||
@@ -965,7 +961,10 @@ Http::patch('/v1/projects/:projectId/templates/email/:type/:locale')
|
||||
->param('replyTo', '', new Email(), 'Reply to email', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $type, string $locale, string $subject, string $message, string $senderName, string $senderEmail, string $replyTo, Response $response, Database $dbForPlatform) {
|
||||
->inject('locale')
|
||||
->action(function (string $projectId, string $type, string $locale, string $subject, string $message, string $senderName, string $senderEmail, string $replyTo, Response $response, Database $dbForPlatform, Locale $localeObject) {
|
||||
$locale = $locale ?: $localeObject->default ?: $localeObject->fallback ?: System::getEnv('_APP_LOCALE', 'en');
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
@@ -1014,13 +1013,13 @@ Http::delete('/v1/projects/:projectId/templates/email/:type/:locale')
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('type', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Template type')
|
||||
->param('locale', 'worldwide', fn ($localeCodes) => new WhiteList(\array_merge([
|
||||
...$localeCodes,
|
||||
'worldwide'
|
||||
])), 'Template locale', true, ['localeCodes'])
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', true, ['localeCodes'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform) {
|
||||
->inject('locale')
|
||||
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform, Locale $localeObject) {
|
||||
$locale = $locale ?: $localeObject->default ?: $localeObject->fallback ?: System::getEnv('_APP_LOCALE', 'en');
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
|
||||
@@ -73,7 +73,7 @@ class Mails extends Listener
|
||||
|
||||
$customTemplate =
|
||||
$project->getAttribute('templates', [])["email.sessionAlert-$event->locale"] ??
|
||||
$project->getAttribute('templates', [])['email.sessionAlert-worldwide'] ?? [];
|
||||
$project->getAttribute('templates', [])['email.sessionAlert-' . $locale->fallback] ?? [];
|
||||
$isBranded = $smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE;
|
||||
|
||||
$subject = $customTemplate['subject'] ?? $locale->getText('emails.sessionAlert.subject');
|
||||
|
||||
@@ -220,7 +220,7 @@ class Create extends Action
|
||||
|
||||
$customTemplate =
|
||||
$project->getAttribute('templates', [])['email.mfaChallenge-' . $locale->default] ??
|
||||
$project->getAttribute('templates', [])['email.mfaChallenge-worldwide'] ?? [];
|
||||
$project->getAttribute('templates', [])['email.mfaChallenge-' . $locale->fallback] ?? [];
|
||||
$smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base');
|
||||
|
||||
$validator = new FileName();
|
||||
|
||||
@@ -326,7 +326,7 @@ class Create extends Action
|
||||
$subject = $locale->getText('emails.invitation.subject');
|
||||
$customTemplate =
|
||||
$project->getAttribute('templates', [])['email.invitation-' . $locale->default] ??
|
||||
$project->getAttribute('templates', [])['email.invitation-worldwide'] ?? [];
|
||||
$project->getAttribute('templates', [])['email.invitation-' . $locale->fallback] ?? [];
|
||||
|
||||
$message = Template::fromFile(APP_CE_CONFIG_DIR . '/locale/templates/email-inner-base.tpl');
|
||||
$message
|
||||
|
||||
@@ -19,7 +19,7 @@ abstract class Template extends Model
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Template locale',
|
||||
'default' => '',
|
||||
'example' => 'worldwide',
|
||||
'example' => 'en_us',
|
||||
])
|
||||
->addRule('message', [
|
||||
'type' => self::TYPE_STRING,
|
||||
|
||||
@@ -1163,253 +1163,6 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$this->assertEquals('Please verify your email {{url}}', $response['body']['message']);
|
||||
}
|
||||
|
||||
#[Group('smtpAndTemplates')]
|
||||
public function testWorldwideTemplates(): void
|
||||
{
|
||||
$data = $this->setupProjectData();
|
||||
$id = $data['projectId'];
|
||||
|
||||
/** Get default template without locale (should default to worldwide) */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/templates/email/verification', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals('verification', $response['body']['type']);
|
||||
$this->assertEquals('worldwide', $response['body']['locale']);
|
||||
|
||||
/** Get default template with explicit worldwide locale */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/templates/email/verification/worldwide', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals('verification', $response['body']['type']);
|
||||
$this->assertEquals('worldwide', $response['body']['locale']);
|
||||
|
||||
/** Set a worldwide email template */
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/templates/email/verification/worldwide', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'subject' => 'Worldwide verify subject',
|
||||
'message' => 'Worldwide verify message {{url}}',
|
||||
'senderName' => 'Worldwide Sender',
|
||||
'senderEmail' => 'worldwide@appwrite.io',
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals('Worldwide verify subject', $response['body']['subject']);
|
||||
$this->assertEquals('Worldwide verify message {{url}}', $response['body']['message']);
|
||||
$this->assertEquals('Worldwide Sender', $response['body']['senderName']);
|
||||
$this->assertEquals('worldwide@appwrite.io', $response['body']['senderEmail']);
|
||||
$this->assertEquals('verification', $response['body']['type']);
|
||||
|
||||
/** Get the worldwide template back */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/templates/email/verification/worldwide', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals('Worldwide verify subject', $response['body']['subject']);
|
||||
$this->assertEquals('Worldwide verify message {{url}}', $response['body']['message']);
|
||||
$this->assertEquals('Worldwide Sender', $response['body']['senderName']);
|
||||
$this->assertEquals('worldwide@appwrite.io', $response['body']['senderEmail']);
|
||||
$this->assertEquals('verification', $response['body']['type']);
|
||||
$this->assertEquals('worldwide', $response['body']['locale']);
|
||||
|
||||
/** Locale-specific template should not return the worldwide custom template */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/templates/email/verification/en-us', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals('verification', $response['body']['type']);
|
||||
$this->assertEquals('en-us', $response['body']['locale']);
|
||||
// en-us should NOT return the worldwide custom subject
|
||||
$this->assertNotEquals('Worldwide verify subject', $response['body']['subject']);
|
||||
|
||||
/** Delete the worldwide template */
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $id . '/templates/email/verification/worldwide', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
/** After deletion, worldwide GET should return default template */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/templates/email/verification/worldwide', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals('verification', $response['body']['type']);
|
||||
$this->assertEquals('worldwide', $response['body']['locale']);
|
||||
// Should be back to default (no custom subject)
|
||||
$this->assertNotEquals('Worldwide verify subject', $response['body']['subject']);
|
||||
}
|
||||
|
||||
#[Group('smtpAndTemplates')]
|
||||
public function testWorldwideFallbackOnMagicURL(): void
|
||||
{
|
||||
$smtpHost = 'maildev';
|
||||
$smtpPort = 1025;
|
||||
$smtpUsername = 'user';
|
||||
$smtpPassword = 'password';
|
||||
|
||||
/** Create a dedicated project for this test */
|
||||
$team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'teamId' => ID::unique(),
|
||||
'name' => 'Worldwide Fallback Test Team',
|
||||
]);
|
||||
$this->assertEquals(201, $team['headers']['status-code']);
|
||||
|
||||
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'projectId' => ID::unique(),
|
||||
'name' => 'Worldwide Fallback Test',
|
||||
'teamId' => $team['body']['$id'],
|
||||
'region' => System::getEnv('_APP_REGION', 'default'),
|
||||
]);
|
||||
$this->assertEquals(201, $project['headers']['status-code']);
|
||||
$projectId = $project['body']['$id'];
|
||||
|
||||
/** Enable SMTP on the project pointing to maildev */
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $projectId . '/smtp', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'enabled' => true,
|
||||
'senderEmail' => 'mailer@appwrite.io',
|
||||
'senderName' => 'Mailer',
|
||||
'host' => $smtpHost,
|
||||
'port' => $smtpPort,
|
||||
'username' => $smtpUsername,
|
||||
'password' => $smtpPassword,
|
||||
]);
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
/** Set worldwide magicSession template */
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $projectId . '/templates/email/magicSession/worldwide', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'subject' => 'Worldwide Magic Login',
|
||||
'message' => 'Worldwide magic link: {{url}}',
|
||||
'senderName' => 'Worldwide Mailer',
|
||||
'senderEmail' => 'worldwide@appwrite.io',
|
||||
]);
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals('Worldwide Magic Login', $response['body']['subject']);
|
||||
|
||||
/** Set German (de) magicSession template */
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $projectId . '/templates/email/magicSession/de', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'subject' => 'German Magic Login',
|
||||
'message' => 'German magic link: {{url}}',
|
||||
'senderName' => 'German Mailer',
|
||||
'senderEmail' => 'german@appwrite.io',
|
||||
]);
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals('German Magic Login', $response['body']['subject']);
|
||||
|
||||
/** Verify worldwide template is stored correctly */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId . '/templates/email/magicSession/worldwide', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals('Worldwide Magic Login', $response['body']['subject']);
|
||||
|
||||
/** Verify German template is stored correctly */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId . '/templates/email/magicSession/de', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals('German Magic Login', $response['body']['subject']);
|
||||
|
||||
/** Verify SMTP is enabled on the project */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertTrue($response['body']['smtpEnabled']);
|
||||
|
||||
/** Trigger magic URL with English locale — should use worldwide fallback */
|
||||
$emailEn = 'magic-en-' . uniqid() . '@appwrite.io';
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/tokens/magic-url', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-locale' => 'en',
|
||||
], [
|
||||
'userId' => ID::unique(),
|
||||
'email' => $emailEn,
|
||||
]);
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
/** Trigger magic URL with German locale — should use German template */
|
||||
$emailDe = 'magic-de-' . uniqid() . '@appwrite.io';
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/tokens/magic-url', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-locale' => 'de',
|
||||
], [
|
||||
'userId' => ID::unique(),
|
||||
'email' => $emailDe,
|
||||
]);
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
/** Trigger magic URL with Polish locale — should use worldwide fallback */
|
||||
$emailPl = 'magic-pl-' . uniqid() . '@appwrite.io';
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/tokens/magic-url', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-locale' => 'pl',
|
||||
], [
|
||||
'userId' => ID::unique(),
|
||||
'email' => $emailPl,
|
||||
]);
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
/** Verify English email uses worldwide fallback template */
|
||||
$lastEmailEn = $this->getLastEmailByAddress($emailEn);
|
||||
$this->assertEquals('Worldwide Magic Login', $lastEmailEn['subject']);
|
||||
$this->assertEquals('worldwide@appwrite.io', $lastEmailEn['from'][0]['address']);
|
||||
$this->assertEquals('Worldwide Mailer', $lastEmailEn['from'][0]['name']);
|
||||
$this->assertStringContainsString('Worldwide magic link:', $lastEmailEn['html']);
|
||||
|
||||
/** Verify German email uses the German-specific template */
|
||||
$lastEmailDe = $this->getLastEmailByAddress($emailDe);
|
||||
$this->assertEquals('German Magic Login', $lastEmailDe['subject']);
|
||||
$this->assertEquals('german@appwrite.io', $lastEmailDe['from'][0]['address']);
|
||||
$this->assertEquals('German Mailer', $lastEmailDe['from'][0]['name']);
|
||||
$this->assertStringContainsString('German magic link:', $lastEmailDe['html']);
|
||||
|
||||
/** Verify Polish email uses worldwide fallback template */
|
||||
$lastEmailPl = $this->getLastEmailByAddress($emailPl);
|
||||
$this->assertEquals('Worldwide Magic Login', $lastEmailPl['subject']);
|
||||
$this->assertEquals('worldwide@appwrite.io', $lastEmailPl['from'][0]['address']);
|
||||
$this->assertEquals('Worldwide Mailer', $lastEmailPl['from'][0]['name']);
|
||||
$this->assertStringContainsString('Worldwide magic link:', $lastEmailPl['html']);
|
||||
}
|
||||
|
||||
public function testUpdateProjectAuthDuration(): void
|
||||
{
|
||||
$data = $this->setupProjectData();
|
||||
|
||||
Reference in New Issue
Block a user