1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2025-12-12 11:14:04 +00:00

Make tenants able to have custom mail credentials (#989)

* Replace MailManager singleton with an instance of a custom mail manager which always resolves the mailers instead of getting the cached ones

* Fix code style (php-cs-fixer)

* Add MailTenancyBootstrapper

* Add MailTenancyBootstrapper to tenancy.bootstrappers config (commented out)

* Fix code style (php-cs-fixer)

* Make credentials map a public static property

* Always resolve only the mailers specified in the mailersToNotCache public static property

* Fix typo in comment

* Update TenancyServiceProvider comment

* add todo

* Add comments to TenancyMailManager, rename property

* Remove the configKey array check

* Simplify bootstrap method

* Change $credentialsMap so that config keys are the keys, and the tenant property names are the values

* Rename $mailersToAlwaysResolve to $tenantMailers

* Update comment

* Update comment

* Rename variable in TenancyServiceProvider comment

* Scaffold tests

* Update comments after review

* Uncomment MailTenancyBootstrapper in config

* Use array_key_exists instead of null check

* Split config logic into methods

* Update mapping credentials

* Add tests for the added logic

* Fix code style (php-cs-fixer)

* Delete default 'smtp' mailer in $tenantMailers

* Add separate method to pick the appropriate mail credentials map preset

* Specify test name

* Move mail bootstrapper tests to BootstrapperTest

* Depend less on the default mailer by adding a static `$mailer` property

* Use static property for map presets

* Comment out MailTenancyBootstrapper from config

* Add return types to MailTenancyBootstrapper methods

* Update test name

* Move MailManager extension to MailTenancyBootstrapper

* Fix code style (php-cs-fixer)

* Update config reverting test

* Use `invade()` instead of ReflectionClass

* Fix constructor parameter formatting

* Delete TenancyMailManager, update tests

* Add return type

* Update comment

* Update MailTest

* Delete `group('mailer')`

* Delete bindNewMailManagerInstance()

* Delete remaining `group('mailer')`

* Fix comment

* Fix comment

Co-authored-by: PHP CS Fixer <phpcsfixer@example.com>
Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com>
This commit is contained in:
lukinovec 2023-01-04 02:12:25 +01:00 committed by GitHub
parent f42f08cb87
commit 0f892f1585
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 200 additions and 0 deletions

View file

@ -102,6 +102,7 @@ return [
Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper::class, Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper::class,
Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper::class, Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper::class,
Stancl\Tenancy\Bootstrappers\BatchTenancyBootstrapper::class, Stancl\Tenancy\Bootstrappers\BatchTenancyBootstrapper::class,
// Stancl\Tenancy\Bootstrappers\MailTenancyBootstrapper::class, // Queueing mail requires using QueueTenancyBootstrapper with $forceRefresh set to true
// Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed // Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed
], ],

View file

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Bootstrappers;
use Illuminate\Config\Repository;
use Illuminate\Foundation\Application;
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Contracts\Tenant;
class MailTenancyBootstrapper implements TenancyBootstrapper
{
/**
* Tenant properties to be mapped to config (similarly to the TenantConfig feature).
*
* For example:
* [
* 'config.key.name' => 'tenant_property',
* ]
*/
public static array $credentialsMap = [];
public static string|null $mailer = null;
protected array $originalConfig = [];
public static array $mapPresets = [
'smtp' => [
'mail.mailers.smtp.host' => 'smtp_host',
'mail.mailers.smtp.port' => 'smtp_port',
'mail.mailers.smtp.username' => 'smtp_username',
'mail.mailers.smtp.password' => 'smtp_password',
],
];
public function __construct(
protected Repository $config,
protected Application $app
) {
static::$mailer ??= $config->get('mail.default');
static::$credentialsMap = array_merge(static::$credentialsMap, static::$mapPresets[static::$mailer] ?? []);
}
public function bootstrap(Tenant $tenant): void
{
// Forget the mail manager instance to clear the cached mailers
$this->app->forgetInstance('mail.manager');
$this->setConfig($tenant);
}
public function revert(): void
{
$this->unsetConfig();
$this->app->forgetInstance('mail.manager');
}
protected function setConfig(Tenant $tenant): void
{
foreach (static::$credentialsMap as $configKey => $storageKey) {
$override = $tenant->$storageKey;
if (array_key_exists($storageKey, $tenant->getAttributes())) {
$this->originalConfig[$configKey] ??= $this->config->get($configKey);
$this->config->set($configKey, $override);
}
}
}
protected function unsetConfig(): void
{
foreach ($this->originalConfig as $key => $value) {
$this->config->set($key, $value);
}
}
}

View file

@ -3,6 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Mail\MailManager;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Stancl\JobPipeline\JobPipeline; use Stancl\JobPipeline\JobPipeline;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
@ -23,6 +24,7 @@ use Stancl\Tenancy\Jobs\RemoveStorageSymlinks;
use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\DeleteTenantStorage; use Stancl\Tenancy\Listeners\DeleteTenantStorage;
use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Bootstrappers\MailTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
@ -326,6 +328,49 @@ test('local storage public urls are generated correctly', function() {
expect(File::isDirectory($tenantStoragePath))->toBeFalse(); expect(File::isDirectory($tenantStoragePath))->toBeFalse();
}); });
test('MailTenancyBootstrapper maps tenant mail credentials to config as specified in the $credentialsMap property and makes the mailer use tenant credentials', function() {
MailTenancyBootstrapper::$credentialsMap = [
'mail.mailers.smtp.username' => 'smtp_username',
'mail.mailers.smtp.password' => 'smtp_password'
];
config([
'mail.default' => 'smtp',
'mail.mailers.smtp.username' => $defaultUsername = 'default username',
'mail.mailers.smtp.password' => 'no password'
]);
$tenant = Tenant::create(['smtp_password' => $password = 'testing password']);
tenancy()->initialize($tenant);
expect(array_key_exists('smtp_password', tenant()->getAttributes()))->toBeTrue();
expect(array_key_exists('smtp_host', tenant()->getAttributes()))->toBeFalse();
expect(config('mail.mailers.smtp.username'))->toBe($defaultUsername);
expect(config('mail.mailers.smtp.password'))->toBe(tenant()->smtp_password);
// Assert that the current mailer uses tenant's smtp_password
assertMailerTransportUsesPassword($password);
});
test('MailTenancyBootstrapper reverts the config and mailer credentials to default when tenancy ends', function() {
MailTenancyBootstrapper::$credentialsMap = ['mail.mailers.smtp.password' => 'smtp_password'];
config(['mail.default' => 'smtp', 'mail.mailers.smtp.password' => $defaultPassword = 'no password']);
tenancy()->initialize(Tenant::create(['smtp_password' => $tenantPassword = 'testing password']));
expect(config('mail.mailers.smtp.password'))->toBe($tenantPassword);
assertMailerTransportUsesPassword($tenantPassword);
tenancy()->end();
expect(config('mail.mailers.smtp.password'))->toBe($defaultPassword);
// Assert that the current mailer uses the default SMTP password
assertMailerTransportUsesPassword($defaultPassword);
});
function getDiskPrefix(string $disk): string function getDiskPrefix(string $disk): string
{ {
/** @var FilesystemAdapter $disk */ /** @var FilesystemAdapter $disk */

72
tests/MailTest.php Normal file
View file

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
use Illuminate\Mail\MailManager;
use Illuminate\Support\Facades\Event;
use Stancl\Tenancy\Events\TenancyEnded;
use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Bootstrappers\MailTenancyBootstrapper;
beforeEach(function() {
config(['mail.default' => 'smtp']);
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
});
// Initialize tenancy as $tenant and assert that the smtp mailer's transport has the correct password
function assertMailerTransportUsesPassword(string|null $password) {
$manager = app(MailManager::class);
$mailer = invade($manager)->get('smtp');
$mailerPassword = invade($mailer->getSymfonyTransport())->password;
expect($mailerPassword)->toBe((string) $password);
};
test('mailer transport uses the correct credentials', function() {
config(['mail.default' => 'smtp', 'mail.mailers.smtp.password' => $defaultPassword = 'DEFAULT']);
MailTenancyBootstrapper::$credentialsMap = ['mail.mailers.smtp.password' => 'smtp_password'];
tenancy()->initialize($tenant = Tenant::create());
assertMailerTransportUsesPassword($defaultPassword); // $tenant->smtp_password is not set, so the default password should be used
tenancy()->end();
// Assert mailer uses the updated password
$tenant->update(['smtp_password' => $newPassword = 'changed']);
tenancy()->initialize($tenant);
assertMailerTransportUsesPassword($newPassword);
tenancy()->end();
// Assert mailer uses the correct password after switching to a different tenant
tenancy()->initialize(Tenant::create(['smtp_password' => $newTenantPassword = 'updated']));
assertMailerTransportUsesPassword($newTenantPassword);
tenancy()->end();
// Assert mailer uses the default password after tenancy ends
assertMailerTransportUsesPassword($defaultPassword);
});
test('initializing and ending tenancy binds a fresh MailManager instance without cached mailers', function() {
$mailers = fn() => invade(app(MailManager::class))->mailers;
app(MailManager::class)->mailer('smtp');
expect($mailers())->toHaveCount(1);
tenancy()->initialize(Tenant::create());
expect($mailers())->toHaveCount(0);
app(MailManager::class)->mailer('smtp');
expect($mailers())->toHaveCount(1);
tenancy()->end();
expect($mailers())->toHaveCount(0);
});

View file

@ -14,6 +14,7 @@ use Stancl\Tenancy\Facades\GlobalCache;
use Stancl\Tenancy\Facades\Tenancy; use Stancl\Tenancy\Facades\Tenancy;
use Stancl\Tenancy\TenancyServiceProvider; use Stancl\Tenancy\TenancyServiceProvider;
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
use Stancl\Tenancy\Bootstrappers\MailTenancyBootstrapper;
abstract class TestCase extends \Orchestra\Testbench\TestCase abstract class TestCase extends \Orchestra\Testbench\TestCase
{ {
@ -104,6 +105,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
'--force' => true, '--force' => true,
], ],
'tenancy.bootstrappers.redis' => RedisTenancyBootstrapper::class, // todo1 change this to []? two tests in TenantDatabaseManagerTest are failing with that 'tenancy.bootstrappers.redis' => RedisTenancyBootstrapper::class, // todo1 change this to []? two tests in TenantDatabaseManagerTest are failing with that
'tenancy.bootstrappers.mail' => MailTenancyBootstrapper::class,
'queue.connections.central' => [ 'queue.connections.central' => [
'driver' => 'sync', 'driver' => 'sync',
'central' => true, 'central' => true,
@ -113,6 +115,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
]); ]);
$app->singleton(RedisTenancyBootstrapper::class); // todo (Samuel) use proper approach eg config for singleton registration $app->singleton(RedisTenancyBootstrapper::class); // todo (Samuel) use proper approach eg config for singleton registration
$app->singleton(MailTenancyBootstrapper::class);
} }
protected function getPackageProviders($app) protected function getPackageProviders($app)