mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 14:14:04 +00:00
* Add &Model to docblock * Fix code style (php-cs-fixer) * Only delete synced resource if the central resource shouldSync * Add central resource detached event and listener * Add SyncedTenant interface * Use the event & listener in the test file * Add getGlobalIdentifierKey(Name) to TenantMorphPivot * Refactor TriggerSyncingEvents * Fix code style (php-cs-fixer) * Test queueing the detaching listener * Move finding the central resource into the event, naming changes * Fix code style (php-cs-fixer) * Simplify listener code * Refactor detaching logic * Create tenant resource after attaching central to tenant, test queueing related listener * Delete dd() * Fix code style (php-cs-fixer) * Move triggerAttachEvent from SyncMaster * Update attach event-related code * Move findResource from SyncedTenant to the pivot trait * Add annotation * Update annotation * Simplify getAttributesForCreation in CreateTenantResourceFromSyncMaster * Update naming * Add tenant trait for attaching/detaching resources * Update test names * Move creation attribute parsing method to trait * Rename variable * Fix code style (php-cs-fixer) * Delete complete to-do * Delete event comment * Rename event property * Find tenant resource in detach listener * Use global ID key of tenant resource in cascade deletes listener * Use global ID key name of the central resource while creating/deleting tenant resources * Add getSyncedCreationAttributes example in the annotation * Fix inconsistencies in SyncedTenant methods * Improve annotation * Don't return the query in `$scopeGetModelQuery` Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com> * Fix code style (php-cs-fixer) * Update scoping getModel query * Only use detach event instead of using both detach and delete events, refactor code * Test that detaching tenant from a central resource doesn't affect other tenants * Delete extra imports * Fix code style (php-cs-fixer) * Add PivotWithRelation, test attaching/detaching resources without polymorphic relations * Refactor TriggerSyncingEvents to work with non-polymorphic relations too * Fix code style (php-cs-fixer) * Rename synced resource changed event, fix tests * Enforce passing Tenant&Model to attach/detach events * Prevent firing saved event automatically in CreateTenantResource * Improve TriggerSyncingEvents trait * Delete unused import * Make TriggerSyncingEvents methods non-static, improve annotations * Pass saved model to event * Move attach/detach queueing tests to ResourceSyncingTest, pass models instead of IDs to attach/detach * Move events to ResourceSyncing\Events * Fix code style (php-cs-fixer) * Use SerializesModels in queueable listeners instead of events * Delete redundant $shouldQueue setting * Rename listener, test cascade deletes from both sides of many-to-many * Move creation attributes-related code to a separate test file, improve comments (wip) * Improve comments, fix variable name capitalization * Delete tracing comma * Extract duplicate code into a trait * Don't accept nullable tenant in SyncMasterDeleted * Fix annotation * Fix code style (php-cs-fixer) * Update annotation * Fix PHPStan error * Fix annotation * Update comments and test naming * Move triggerDeleteEvent to CascadeDeletes interface * Rename test file * Import TenantPivot in Tenant class (tests/Etc) * Add central resource not available in pivot exception * Rename SaveSyncedResource to UpdateOrCreateSyncedResource * Add new events and listeners to TSP stub * Improve comments and naming * Only keep SerializesModels in classes that utilize it * Use tenant->run() * Import events in stub * Move RS listeners to separate namespace, use `Event/`Listener/` in stub for consistency * Fix code style (php-cs-fixer) * Fix namespace changes * Use cursor instead of get * Update src/ResourceSyncing/ParsesCreationAttributes.php Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com> * Update naming, structure (discussed on Discord) * Update uses in in test file * remove double ;; * Add comments * Test if static properties work fine with queueable listeners * Update $shouldQuery test * Update creation attributes * Work on updating the tests * Make synced attributes configurable using static properties * Update resource syncing tests * Get rid of mixed attribute classes * Get rid of TenantUserWIthCreationAttributes * Fix imports * Get rid of the conditionally synced classes, improve tests * Simplify resource creation tests (only test the more complex cases instead of each case - if the complex case works, the simpler cases work too) * Clean up ResourceSyncingTest (mostly duplicate tests that were already in AutomaticResourceCreationTest) * Simplify class naming in polymorhpic tests * Move automatic resource creation tests to ResourceSyncingTest * Test that the sync event doesn't get triggered excessively * Only trigger the sync event if the synced attributes were changed or if the resource was recently created * Update synced attribute instead of unsynced in test * Fix sync event test * Update static property redefining test * Use getGlobalIdentifierKeyName() instead of hardcoding the key name * Delete static properties from the ResourceSyncing trait * Reuse user classes in polymorphic tests * Update tests/ResourceSyncingTest.php Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com> * Use the default tenants() method in central user, override the default in ResourceSyncingTest * Use BelongsToMany as tenants() return type * Fix code style (php-cs-fixer) * Delete extra static property from trait * Delete duplicate events/listeners from TSP stub * Delete weird expectation, use $model->trashed() * Change ResourceUser to TenantUser * Add defaults for getGlobalIdentifierKey(Name) * Use singular tenant in DeleteResourceInTenants name * Rename getSyncedCreationAttributes to getCreationAttributes * Fix comma position in comment * minor fixes in traits and interfaces * Fix code style (php-cs-fixer) * Correct comment * Use $tenant->run() * Update scopeGetModelQuery annotation * Use static property for testing shouldSync * Improve test * Get rid of datasets * Add trashed assertions * Always merge synced attributes with the creation attributes during parsing * Update creation attributes in test's beforeEach * Use only the necessary creation attributes (no need to include the synced attributes because they get merged automatically) * Rename ResourceTenant to MorphTenant * Add TriggerSyncingEvents docblock Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com> * Add force deletes test * Fix code style (php-cs-fixer) * Delete pivot if it can't access the resource class * Make parseCreationAttributes more readable * Comment out setting $scopeGetModelQuery in the stub * Add @var annotations to bootTriggerSyncingEvents * Fix attach()/detach() exception test * Interrupt creation of broken pivots instead of deleting the pivots at a later point * Add more comments * Update CreateTenantResource comment * Assert that forceDelete() doesn't affect other tenant resources * Rename test * Correct with() array formatting * Expand test with soft deletes * Merge SyncedResourceSaved tests * Improve naming, comments and minor details in the assertions * Move test * Fix failing test * Delete duplicate test * Minor test improvement * Delete duplicate test * Improve old test * Minor test improvement * Improve event test * Improve tests (naming, code, comments) * Delete extra test, add comments to the larger test * Refactor central -> tenant attach() test * Apply changes from central -> tenant attach() test on tenant -> central test * Fix assertions in central -> tenant * Correct comment and assertion * Refactor tenant -> central attach() test * Fix inconsistency * Delete unused import * Add comments * Update polymorphic test names * Rename polymorphic tests * Update listener test name * Delete redundant tenant ID assignments * Improve test names * Move polymorphic tests to ResourceSyncingTest * Mention alternative solutions in CentralResourceNotAvailableInPivotException * Add comments * Update test comments * minor changes to tests + review comments * Delete extra tests, update comments * Remove unneeded part of test * Fix comment * Improve comments * Add test for companies() realationship accessibility * Update test name * Complete to-do, add comment * Improve naming and comments (resolve some priority reviews) * Move test * Comment, resolve to-dos * Add low-level pivot assertions * Restore trashed resources if the central resource got restored, try improving tests * Fix code style (php-cs-fixer) * Dekete redundnat unsynced comments * Add to-do, test WIP * Fix restoring logic * Update todo * Add todo to fix phpdoc * Fix code style (php-cs-fixer) * PHPStan error fix wip * Fix PHPStan error * Add regression test * Delete unused trait * Add and test restoring WIP * Fix code style (php-cs-fixer) * Add to-do * Delete comment from test * Focus on restoring in the restore test * Improve maming * Fix stub * Delete redundant part of test * Delete incorrect test leftover * Add triggerRestoredEvent * Fix restore test * Correct tests and restore(() logic * Fix code style (php-cs-fixer) * Check if SoftDeletes are used before firing SyncMasterRestored * Fix comment * Revert restore action changes (phpstan errors) * Delete CascadeDeletes interface * Remove CascadeDeletes from most of the tests * Fix code style (php-cs-fixer) * Rename tests * Fix restoring + tests WIP * Fix restoring * Fix restoring tests * Fix code style (php-cs-fixer) * Test that detaching force deletes the tenant resources * Implement cacscade force deleting * Delete redundant changes * Fix typo * Fix SyncMaster * Improve test * Add force deleting logic back and fix tests * Improve comment * Delete extra assertion * Improve restoring test * Simplify assertion * Delete redundant query scoping from test * Test restore listener queueing * use strict in_array() checks * fix phpstan errors --------- Co-authored-by: lukinovec <lukinovec@gmail.com> Co-authored-by: PHP CS Fixer <phpcsfixer@example.com>
308 lines
8.6 KiB
PHP
308 lines
8.6 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use Illuminate\Bus\Queueable;
|
||
use Spatie\Valuestore\Valuestore;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Stancl\Tenancy\Tests\Etc\User;
|
||
use Stancl\JobPipeline\JobPipeline;
|
||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||
use Illuminate\Support\Facades\Event;
|
||
use Illuminate\Queue\SerializesModels;
|
||
use Illuminate\Support\Facades\Schema;
|
||
use Stancl\Tenancy\Events\TenancyEnded;
|
||
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||
use Illuminate\Queue\InteractsWithQueue;
|
||
use Stancl\Tenancy\Events\TenantCreated;
|
||
use Illuminate\Database\Schema\Blueprint;
|
||
use Illuminate\Queue\Events\JobProcessed;
|
||
use Illuminate\Queue\Events\JobProcessing;
|
||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||
use Illuminate\Foundation\Bus\Dispatchable;
|
||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||
use Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper;
|
||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||
use Stancl\Tenancy\Listeners\QueueableListener;
|
||
|
||
beforeEach(function () {
|
||
$this->mockConsoleOutput = false;
|
||
|
||
config([
|
||
'tenancy.bootstrappers' => [
|
||
QueueTenancyBootstrapper::class,
|
||
DatabaseTenancyBootstrapper::class,
|
||
],
|
||
'queue.default' => 'redis',
|
||
]);
|
||
|
||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||
|
||
createValueStore();
|
||
});
|
||
|
||
afterEach(function () {
|
||
pest()->valuestore->flush();
|
||
});
|
||
|
||
test('tenant id is passed to tenant queues', function () {
|
||
withTenantDatabases();
|
||
|
||
config(['queue.default' => 'sync']);
|
||
|
||
$tenant = Tenant::create();
|
||
|
||
tenancy()->initialize($tenant);
|
||
|
||
Event::fake([JobProcessing::class, JobProcessed::class]);
|
||
|
||
dispatch(new TestJob(pest()->valuestore));
|
||
|
||
Event::assertDispatched(JobProcessing::class, function ($event) {
|
||
return $event->job->payload()['tenant_id'] === tenant('id');
|
||
});
|
||
});
|
||
|
||
test('tenant id is not passed to central queues', function () {
|
||
withTenantDatabases();
|
||
|
||
$tenant = Tenant::create();
|
||
|
||
tenancy()->initialize($tenant);
|
||
|
||
Event::fake();
|
||
|
||
config(['queue.connections.central' => [
|
||
'driver' => 'sync',
|
||
'central' => true,
|
||
]]);
|
||
|
||
dispatch(new TestJob(pest()->valuestore))->onConnection('central');
|
||
|
||
Event::assertDispatched(JobProcessing::class, function ($event) {
|
||
return ! isset($event->job->payload()['tenant_id']);
|
||
});
|
||
});
|
||
|
||
test('tenancy is initialized inside queues', function (bool $shouldEndTenancy) {
|
||
withTenantDatabases();
|
||
withFailedJobs();
|
||
|
||
$tenant = Tenant::create();
|
||
|
||
tenancy()->initialize($tenant);
|
||
|
||
withUsers();
|
||
|
||
$user = User::create(['name' => 'Foo', 'email' => 'foo@bar.com', 'password' => 'secret']);
|
||
|
||
pest()->valuestore->put('userName', 'Bar');
|
||
|
||
dispatch(new TestJob(pest()->valuestore, $user));
|
||
|
||
expect(pest()->valuestore->has('tenant_id'))->toBeFalse();
|
||
|
||
if ($shouldEndTenancy) {
|
||
tenancy()->end();
|
||
}
|
||
|
||
pest()->artisan('queue:work --once');
|
||
|
||
expect(DB::connection('central')->table('failed_jobs')->count())->toBe(0);
|
||
|
||
expect(pest()->valuestore->get('tenant_id'))->toBe('The current tenant id is: ' . $tenant->id);
|
||
|
||
$tenant->run(function () use ($user) {
|
||
expect($user->fresh()->name)->toBe('Bar');
|
||
});
|
||
})->with([true, false]);
|
||
|
||
test('changing the shouldQueue static property in parent class affects child classes unless the property is redefined', function () {
|
||
// Parent – $shouldQueue is true
|
||
expect(app(ShouldQueueListener::class)->shouldQueue(new stdClass()))->toBeTrue();
|
||
|
||
// Child – $shouldQueue is redefined and set to false
|
||
expect(app(ShouldNotQueueListener::class)->shouldQueue(new stdClass()))->toBeFalse();
|
||
|
||
// Child – inherits $shouldQueue from ShouldQueueListener (true)
|
||
expect(app(InheritedQueueListener::class)->shouldQueue(new stdClass()))->toBeTrue();
|
||
|
||
// Update $shouldQueue of InheritedQueueListener's parent to see if it affects the child
|
||
ShouldQueueListener::$shouldQueue = false;
|
||
|
||
// Parent's $shouldQueue changed to false
|
||
expect(app(InheritedQueueListener::class)->shouldQueue(new stdClass()))->toBeFalse();
|
||
|
||
ShouldQueueListener::$shouldQueue = true;
|
||
|
||
// Parent's $shouldQueue changed back to true
|
||
// Child's $shouldQueue is still false because it was redefined
|
||
expect(app(ShouldNotQueueListener::class)->shouldQueue(new stdClass()))->toBeFalse();
|
||
});
|
||
|
||
test('tenancy is initialized when retrying jobs', function (bool $shouldEndTenancy) {
|
||
withFailedJobs();
|
||
withTenantDatabases();
|
||
|
||
$tenant = Tenant::create();
|
||
|
||
tenancy()->initialize($tenant);
|
||
|
||
withUsers();
|
||
|
||
$user = User::create(['name' => 'Foo', 'email' => 'foo@bar.com', 'password' => 'secret']);
|
||
|
||
pest()->valuestore->put('userName', 'Bar');
|
||
pest()->valuestore->put('shouldFail', true);
|
||
|
||
dispatch(new TestJob(pest()->valuestore, $user));
|
||
|
||
expect(pest()->valuestore->has('tenant_id'))->toBeFalse();
|
||
|
||
if ($shouldEndTenancy) {
|
||
tenancy()->end();
|
||
}
|
||
|
||
pest()->artisan('queue:work --once');
|
||
|
||
expect(DB::connection('central')->table('failed_jobs')->count())->toBe(1);
|
||
expect(pest()->valuestore->get('tenant_id'))->toBeNull(); // job failed
|
||
|
||
pest()->artisan('queue:retry all');
|
||
pest()->artisan('queue:work --once');
|
||
|
||
expect(DB::connection('central')->table('failed_jobs')->count())->toBe(0);
|
||
|
||
expect(pest()->valuestore->get('tenant_id'))->toBe('The current tenant id is: ' . $tenant->id); // job succeeded
|
||
|
||
$tenant->run(function () use ($user) {
|
||
expect($user->fresh()->name)->toBe('Bar');
|
||
});
|
||
})->with([true, false]);
|
||
|
||
test('the tenant used by the job doesnt change when the current tenant changes', function () {
|
||
withTenantDatabases();
|
||
|
||
$tenant1 = Tenant::create();
|
||
|
||
tenancy()->initialize($tenant1);
|
||
|
||
dispatch(new TestJob(pest()->valuestore));
|
||
|
||
$tenant2 = Tenant::create();
|
||
|
||
tenancy()->initialize($tenant2);
|
||
|
||
expect(pest()->valuestore->has('tenant_id'))->toBeFalse();
|
||
pest()->artisan('queue:work --once');
|
||
|
||
expect(pest()->valuestore->get('tenant_id'))->toBe('The current tenant id is: ' . $tenant1->getTenantKey());
|
||
});
|
||
|
||
function createValueStore(): void
|
||
{
|
||
$valueStorePath = __DIR__ . '/Etc/tmp/queuetest.json';
|
||
|
||
if (! file_exists($valueStorePath)) {
|
||
// The directory sometimes goes missing as well when the file is deleted in git
|
||
if (! is_dir(__DIR__ . '/Etc/tmp')) {
|
||
mkdir(__DIR__ . '/Etc/tmp');
|
||
}
|
||
|
||
file_put_contents($valueStorePath, '');
|
||
}
|
||
|
||
pest()->valuestore = Valuestore::make($valueStorePath)->flush();
|
||
}
|
||
|
||
function withFailedJobs()
|
||
{
|
||
Schema::connection('central')->create('failed_jobs', function (Blueprint $table) {
|
||
$table->increments('id');
|
||
$table->string('uuid')->unique();
|
||
$table->text('connection');
|
||
$table->text('queue');
|
||
$table->longText('payload');
|
||
$table->longText('exception');
|
||
$table->timestamp('failed_at')->useCurrent();
|
||
});
|
||
}
|
||
|
||
function withUsers()
|
||
{
|
||
Schema::create('users', function (Blueprint $table) {
|
||
$table->increments('id');
|
||
$table->string('name');
|
||
$table->string('email')->unique();
|
||
$table->string('password');
|
||
$table->rememberToken();
|
||
$table->timestamps();
|
||
});
|
||
}
|
||
|
||
class TestJob implements ShouldQueue
|
||
{
|
||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||
|
||
/** @var Valuestore */
|
||
protected $valuestore;
|
||
|
||
/** @var User|null */
|
||
protected $user;
|
||
|
||
public function __construct(Valuestore $valuestore, User $user = null)
|
||
{
|
||
$this->valuestore = $valuestore;
|
||
$this->user = $user;
|
||
}
|
||
|
||
public function handle()
|
||
{
|
||
if ($this->valuestore->get('shouldFail')) {
|
||
$this->valuestore->put('shouldFail', false);
|
||
|
||
throw new Exception('failing');
|
||
}
|
||
|
||
if ($this->user) {
|
||
assert($this->user->getConnectionName() === 'tenant');
|
||
}
|
||
|
||
$this->valuestore->put('tenant_id', 'The current tenant id is: ' . tenant('id'));
|
||
|
||
if ($userName = $this->valuestore->get('userName')) {
|
||
$this->user->update(['name' => $userName]);
|
||
}
|
||
}
|
||
}
|
||
|
||
class ShouldQueueListener extends QueueableListener
|
||
{
|
||
public static bool $shouldQueue = true;
|
||
|
||
public function handle()
|
||
{
|
||
return static::$shouldQueue;
|
||
}
|
||
}
|
||
|
||
class ShouldNotQueueListener extends ShouldQueueListener
|
||
{
|
||
public static bool $shouldQueue = false;
|
||
|
||
public function handle()
|
||
{
|
||
return static::$shouldQueue;
|
||
}
|
||
}
|
||
|
||
class InheritedQueueListener extends ShouldQueueListener
|
||
{
|
||
public function handle()
|
||
{
|
||
return static::$shouldQueue;
|
||
}
|
||
}
|