mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 12:54:05 +00:00
Refactor more old code and get tests to pass
This commit is contained in:
parent
c5377a16f7
commit
c32f229dd5
72 changed files with 425 additions and 531 deletions
|
|
@ -1,12 +1,12 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Tests\v3;
|
||||
namespace Stancl\Tenancy\Tests;
|
||||
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
use Stancl\Tenancy\Events\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Events\Listeners\RevertToCentralContext;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||
use Stancl\Tenancy\Events\TenancyEnded;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Tests\TestCase;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Tests\v3;
|
||||
namespace Stancl\Tenancy\Tests;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
|
@ -8,9 +8,9 @@ use Illuminate\Support\Facades\Event;
|
|||
use Illuminate\Support\Facades\Redis;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
use Stancl\Tenancy\Events\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Events\Listeners\JobPipeline;
|
||||
use Stancl\Tenancy\Events\Listeners\RevertToCentralContext;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Listeners\JobPipeline;
|
||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||
use Stancl\Tenancy\Events\TenancyEnded;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Events\TenantCreated;
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Tests\v3;
|
||||
namespace Stancl\Tenancy\Tests;
|
||||
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
use Stancl\Tenancy\Events\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\TenancyBootstrappers\CacheTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Tests\TestCase;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Tests\v3;
|
||||
namespace Stancl\Tenancy\Tests;
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Stancl\Tenancy\Database\Models;
|
||||
use Stancl\Tenancy\Database\Models\Concerns\HasDomains;
|
||||
use Stancl\Tenancy\Database\Concerns\HasDomains;
|
||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain;
|
||||
use Stancl\Tenancy\Tests\TestCase;
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ class CombinedDomainAndSubdomainIdentificationTest extends TestCase
|
|||
});
|
||||
});
|
||||
|
||||
config(['tenancy.tenant_model' => Tenant::class]);
|
||||
config(['tenancy.tenant_model' => CombinedTenant::class]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
@ -30,7 +30,7 @@ class CombinedDomainAndSubdomainIdentificationTest extends TestCase
|
|||
{
|
||||
config(['tenancy.central_domains' => ['localhost']]);
|
||||
|
||||
$tenant = Tenant::create([
|
||||
$tenant = CombinedTenant::create([
|
||||
'id' => 'acme',
|
||||
]);
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ class CombinedDomainAndSubdomainIdentificationTest extends TestCase
|
|||
{
|
||||
config(['tenancy.central_domains' => []]);
|
||||
|
||||
$tenant = Tenant::create([
|
||||
$tenant = CombinedTenant::create([
|
||||
'id' => 'acme',
|
||||
]);
|
||||
|
||||
|
|
@ -72,7 +72,7 @@ class CombinedDomainAndSubdomainIdentificationTest extends TestCase
|
|||
}
|
||||
}
|
||||
|
||||
class Tenant extends Models\Tenant
|
||||
class CombinedTenant extends Models\Tenant
|
||||
{
|
||||
use HasDomains;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Tests\v3;
|
||||
namespace Stancl\Tenancy\Tests;
|
||||
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
|
@ -10,9 +10,9 @@ use Illuminate\Support\Facades\Event;
|
|||
use Illuminate\Support\Facades\Schema;
|
||||
use Stancl\Tenancy\Tests\Etc\ExampleSeeder;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
use Stancl\Tenancy\Events\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Events\Listeners\JobPipeline;
|
||||
use Stancl\Tenancy\Events\Listeners\RevertToCentralContext;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Listeners\JobPipeline;
|
||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||
use Stancl\Tenancy\Events\TenancyEnded;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Events\TenantCreated;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Tests\v3;
|
||||
namespace Stancl\Tenancy\Tests;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Foundation\Auth\User as Authenticable;
|
||||
|
|
@ -8,7 +8,7 @@ use Illuminate\Support\Facades\DB;
|
|||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
use Stancl\Tenancy\Events\Listeners\JobPipeline;
|
||||
use Stancl\Tenancy\Listeners\JobPipeline;
|
||||
use Stancl\Tenancy\Events\TenantCreated;
|
||||
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||
use Stancl\Tenancy\Jobs\MigrateDatabase;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Tests\v3;
|
||||
namespace Stancl\Tenancy\Tests;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
|
|
@ -12,9 +12,12 @@ use Stancl\Tenancy\Exceptions\TenantDatabaseUserAlreadyExistsException;
|
|||
use Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager;
|
||||
use Stancl\Tenancy\TenantDatabaseManagers\PermissionControlledMySQLDatabaseManager;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
use Stancl\Tenancy\Events\Listeners\JobPipeline;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Listeners\JobPipeline;
|
||||
use Stancl\Tenancy\Events\TenantCreated;
|
||||
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\TenancyBootstrappers\DatabaseTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Tests\TestCase;
|
||||
|
||||
class DatabaseUsersTest extends TestCase
|
||||
|
|
@ -96,19 +99,20 @@ class DatabaseUsersTest extends TestCase
|
|||
config([
|
||||
'tenancy.database_managers.mysql' => MySQLDatabaseManager::class,
|
||||
'tenancy.database.suffix' => '',
|
||||
'tenancy.database.template_connection' => 'mysql',
|
||||
'tenancy.template_tenant_connection' => 'mysql',
|
||||
'tenancy.bootstrappers' => [
|
||||
DatabaseTenancyBootstrapper::class,
|
||||
],
|
||||
]);
|
||||
|
||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||
|
||||
$tenant = Tenant::create([
|
||||
'id' => 'foo' . Str::random(10),
|
||||
]);
|
||||
|
||||
$this->assertTrue($tenant->database()->manager() instanceof MySQLDatabaseManager);
|
||||
|
||||
$tenant = Tenant::create([
|
||||
'id' => 'foo' . Str::random(10),
|
||||
]);
|
||||
|
||||
tenancy()->initialize($tenant); // check if everything works
|
||||
tenancy()->end();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Tests\v3;
|
||||
namespace Stancl\Tenancy\Tests;
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Stancl\Tenancy\Database\Models;
|
||||
use Stancl\Tenancy\Database\Models\Concerns\HasDomains;
|
||||
use Stancl\Tenancy\Database\Concerns\HasDomains;
|
||||
use Stancl\Tenancy\Exceptions\DomainOccupiedByOtherTenantException;
|
||||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
|
||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
||||
|
|
@ -25,13 +25,13 @@ class DomainTest extends TestCase
|
|||
});
|
||||
});
|
||||
|
||||
config(['tenancy.tenant_model' => Tenant::class]);
|
||||
config(['tenancy.tenant_model' => DomainTenant::class]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function tenant_can_be_identified_using_hostname()
|
||||
{
|
||||
$tenant = Tenant::create();
|
||||
$tenant = DomainTenant::create();
|
||||
|
||||
$id = $tenant->id;
|
||||
|
||||
|
|
@ -48,13 +48,13 @@ class DomainTest extends TestCase
|
|||
/** @test */
|
||||
public function a_domain_can_belong_to_only_one_tenant()
|
||||
{
|
||||
$tenant = Tenant::create();
|
||||
$tenant = DomainTenant::create();
|
||||
|
||||
$tenant->domains()->create([
|
||||
'domain' => 'foo.localhost',
|
||||
]);
|
||||
|
||||
$tenant2 = Tenant::create();
|
||||
$tenant2 = DomainTenant::create();
|
||||
|
||||
$this->expectException(DomainOccupiedByOtherTenantException::class);
|
||||
$tenant2->domains()->create([
|
||||
|
|
@ -73,7 +73,7 @@ class DomainTest extends TestCase
|
|||
/** @test */
|
||||
public function tenant_can_be_identified_by_domain()
|
||||
{
|
||||
$tenant = Tenant::create([
|
||||
$tenant = DomainTenant::create([
|
||||
'id' => 'acme',
|
||||
]);
|
||||
|
||||
|
|
@ -104,7 +104,7 @@ class DomainTest extends TestCase
|
|||
}
|
||||
}
|
||||
|
||||
class Tenant extends Models\Tenant
|
||||
class DomainTenant extends Models\Tenant
|
||||
{
|
||||
use HasDomains;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use Illuminate\Database\Migrations\Migration;
|
|||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateUsersTable extends Migration
|
||||
class TestCreateUsersTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"foo":"bar"}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Tests\v3;
|
||||
namespace Stancl\Tenancy\Tests;
|
||||
|
||||
use Illuminate\Events\CallQueuedListener;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
use Stancl\Tenancy\Events\Listeners\QueueableListener;
|
||||
use Stancl\Tenancy\Listeners\QueueableListener;
|
||||
use Stancl\Tenancy\Events\TenantCreated;
|
||||
use Stancl\Tenancy\Tests\TestCase;
|
||||
|
||||
|
|
@ -41,8 +41,6 @@ class EventListenerTest extends TestCase
|
|||
|
||||
$this->assertFalse(app()->bound('foo'));
|
||||
}
|
||||
|
||||
// todo test that the way the published SP registers events works
|
||||
}
|
||||
|
||||
class FooListener extends QueueableListener
|
||||
|
|
|
|||
|
|
@ -4,32 +4,39 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy\Tests\Features;
|
||||
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
use Stancl\Tenancy\Events\TenancyEnded;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Features\TenantConfig;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||
use Stancl\Tenancy\Tests\TestCase;
|
||||
|
||||
class TenantConfigTest extends TestCase
|
||||
{
|
||||
public $autoInitTenancy = false;
|
||||
public $autoCreateTenant = false;
|
||||
|
||||
/** @test */
|
||||
public function config_is_merged_and_removed()
|
||||
{
|
||||
$this->assertSame(null, config('services.paypal'));
|
||||
config([
|
||||
'tenancy.features' => [TenantConfig::class],
|
||||
'tenancy.bootstrappers' => [],
|
||||
]);
|
||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||
|
||||
TenantConfig::$storageToConfigMap = [
|
||||
'paypal_api_public' => 'services.paypal.public',
|
||||
'paypal_api_private' => 'services.paypal.private',
|
||||
];
|
||||
|
||||
tenancy()->create('foo.localhost', [
|
||||
$tenant = Tenant::create([
|
||||
'paypal_api_public' => 'foo',
|
||||
'paypal_api_private' => 'bar',
|
||||
]);
|
||||
|
||||
tenancy()->init('foo.localhost');
|
||||
tenancy()->initialize($tenant);
|
||||
$this->assertSame(['public' => 'foo', 'private' => 'bar'], config('services.paypal'));
|
||||
|
||||
tenancy()->end();
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Tests\Features;
|
||||
|
||||
use Stancl\Tenancy\Features\Timestamps;
|
||||
use Stancl\Tenancy\Tenant;
|
||||
use Stancl\Tenancy\Tests\TestCase;
|
||||
|
||||
class TimestampTest extends TestCase
|
||||
{
|
||||
public $autoCreateTenant = false;
|
||||
public $autoInitTenancy = false;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config(['tenancy.features' => [
|
||||
Timestamps::class,
|
||||
]]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function create_and_update_timestamps_are_added_on_create()
|
||||
{
|
||||
$tenant = Tenant::new()->save();
|
||||
$this->assertArrayHasKey('created_at', $tenant->data);
|
||||
$this->assertArrayHasKey('updated_at', $tenant->data);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function update_timestamps_are_added()
|
||||
{
|
||||
$tenant = Tenant::new()->save();
|
||||
$this->assertSame($tenant->created_at, $tenant->updated_at);
|
||||
$this->assertSame('string', gettype($tenant->created_at));
|
||||
|
||||
sleep(1);
|
||||
|
||||
$tenant->put('abc', 'def');
|
||||
|
||||
$this->assertTrue($tenant->updated_at > $tenant->created_at);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function softdelete_timestamps_are_added()
|
||||
{
|
||||
$tenant = Tenant::new()->save();
|
||||
$this->assertNull($tenant->deleted_at);
|
||||
|
||||
$tenant->softDelete();
|
||||
$this->assertNotNull($tenant->deleted_at);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Tests\v3;
|
||||
namespace Stancl\Tenancy\Tests;
|
||||
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Stancl\Tenancy\Facades\GlobalCache;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
use Stancl\Tenancy\Events\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\TenancyBootstrappers\CacheTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Tests\TestCase;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Tests\v3;
|
||||
namespace Stancl\Tenancy\Tests;
|
||||
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Spatie\Valuestore\Valuestore;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
use Stancl\Tenancy\Events\Listeners\JobPipeline;
|
||||
use Stancl\Tenancy\Listeners\JobPipeline;
|
||||
use Stancl\Tenancy\Events\TenantCreated;
|
||||
use Stancl\Tenancy\Tests\TestCase;
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ class JobPipelineTest extends TestCase
|
|||
|
||||
config(['queue.default' => 'redis']);
|
||||
|
||||
$this->valuestore = Valuestore::make(__DIR__ . '/../Etc/tmp/jobpipelinetest.json')->flush();
|
||||
$this->valuestore = Valuestore::make(__DIR__ . '/Etc/tmp/jobpipelinetest.json')->flush();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
@ -51,7 +51,7 @@ class JobPipelineTest extends TestCase
|
|||
FooJob::class,
|
||||
])->send(function () {
|
||||
return $this->valuestore;
|
||||
})->shouldBeQueued(true)->toListener());
|
||||
})->queue(true)->toListener());
|
||||
|
||||
Queue::assertNothingPushed();
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ class JobPipelineTest extends TestCase
|
|||
FooJob::class,
|
||||
])->send(function () {
|
||||
return $this->valuestore;
|
||||
})->shouldBeQueued(true)->toListener());
|
||||
})->queue(true)->toListener());
|
||||
|
||||
$this->assertFalse($this->valuestore->has('foo'));
|
||||
Tenant::create();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Tests\v3;
|
||||
namespace Stancl\Tenancy\Tests;
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Tests\v3;
|
||||
namespace Stancl\Tenancy\Tests;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
|
@ -11,7 +11,7 @@ use Illuminate\Queue\SerializesModels;
|
|||
use Illuminate\Support\Facades\Event;
|
||||
use Spatie\Valuestore\Valuestore;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
use Stancl\Tenancy\Events\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\TenancyBootstrappers\QueueTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Tests\TestCase;
|
||||
|
|
@ -36,17 +36,19 @@ class QueueTest extends TestCase
|
|||
|
||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||
|
||||
$this->valuestore = Valuestore::make(__DIR__ . '/../Etc/tmp/queuetest.json')->flush();
|
||||
$this->valuestore = Valuestore::make(__DIR__ . '/Etc/tmp/queuetest.json')->flush();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function tenant_id_is_passed_to_tenant_queues()
|
||||
{
|
||||
config(['queue.default' => 'sync']);
|
||||
|
||||
$tenant = Tenant::create();
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
Event::fake();
|
||||
Event::fake([JobProcessing::class]);
|
||||
|
||||
dispatch(new TestJob($this->valuestore));
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Tests\v3;
|
||||
namespace Stancl\Tenancy\Tests;
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
|
|
|
|||
|
|
@ -9,14 +9,14 @@ use Illuminate\Support\Facades\Event;
|
|||
use Illuminate\Support\Facades\Queue;
|
||||
use Stancl\Tenancy\Contracts\Syncable;
|
||||
use Stancl\Tenancy\Contracts\SyncMaster;
|
||||
use Stancl\Tenancy\Database\Models\Concerns\CentralConnection;
|
||||
use Stancl\Tenancy\Database\Models\Concerns\ResourceSyncing;
|
||||
use Stancl\Tenancy\Database\Concerns\CentralConnection;
|
||||
use Stancl\Tenancy\Database\Concerns\ResourceSyncing;
|
||||
use Stancl\Tenancy\Database\Models;
|
||||
use Stancl\Tenancy\Database\Models\TenantPivot;
|
||||
use Stancl\Tenancy\Events\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Events\Listeners\JobPipeline;
|
||||
use Stancl\Tenancy\Events\Listeners\RevertToCentralContext;
|
||||
use Stancl\Tenancy\Events\Listeners\UpdateSyncedResource;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Listeners\JobPipeline;
|
||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||
use Stancl\Tenancy\Listeners\UpdateSyncedResource;
|
||||
use Stancl\Tenancy\Events\SyncedResourceChangedInForeignDatabase;
|
||||
use Stancl\Tenancy\Events\SyncedResourceSaved;
|
||||
use Stancl\Tenancy\Events\TenancyEnded;
|
||||
|
|
@ -49,8 +49,8 @@ class ResourceSyncingTest extends TestCase
|
|||
|
||||
$this->artisan('migrate', [
|
||||
'--path' => [
|
||||
__DIR__ . '/../Etc/synced_resource_migrations',
|
||||
__DIR__ . '/../Etc/synced_resource_migrations/users'
|
||||
__DIR__ . '/Etc/synced_resource_migrations',
|
||||
__DIR__ . '/Etc/synced_resource_migrations/users'
|
||||
],
|
||||
'--realpath' => true,
|
||||
])->assertExitCode(0);
|
||||
|
|
@ -59,7 +59,7 @@ class ResourceSyncingTest extends TestCase
|
|||
protected function migrateTenants()
|
||||
{
|
||||
$this->artisan('tenants:migrate', [
|
||||
'--path' => __DIR__ . '/../Etc/synced_resource_migrations/users',
|
||||
'--path' => __DIR__ . '/Etc/synced_resource_migrations/users',
|
||||
'--realpath' => true,
|
||||
])->assertExitCode(0);
|
||||
}
|
||||
|
|
@ -69,7 +69,7 @@ class ResourceSyncingTest extends TestCase
|
|||
{
|
||||
Event::fake([SyncedResourceSaved::class]);
|
||||
|
||||
$user = User::create([
|
||||
$user = ResourceUser::create([
|
||||
'name' => 'Foo',
|
||||
'email' => 'foo@email.com',
|
||||
'password' => 'secret',
|
||||
|
|
@ -94,13 +94,13 @@ class ResourceSyncingTest extends TestCase
|
|||
'role' => 'superadmin', // unsynced
|
||||
]);
|
||||
|
||||
$tenant = Tenant::create();
|
||||
$tenant = ResourceTenant::create();
|
||||
$this->migrateTenants();
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
// Create the same user in tenant DB
|
||||
$user = User::create([
|
||||
$user = ResourceUser::create([
|
||||
'global_id' => 'acme',
|
||||
'name' => 'John Doe',
|
||||
'email' => 'john@localhost',
|
||||
|
|
@ -135,22 +135,22 @@ class ResourceSyncingTest extends TestCase
|
|||
'email' => 'john@foreignhost', // synced
|
||||
'password' => 'secret', // no changes
|
||||
'role' => 'superadmin', // unsynced
|
||||
], User::first()->getAttributes());
|
||||
], ResourceUser::first()->getAttributes());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function creating_the_resource_in_tenant_database_creates_it_in_central_database_and_creates_the_mapping()
|
||||
{
|
||||
// Assert no user in central DB
|
||||
$this->assertCount(0, User::all());
|
||||
$this->assertCount(0, ResourceUser::all());
|
||||
|
||||
$tenant = Tenant::create();
|
||||
$tenant = ResourceTenant::create();
|
||||
$this->migrateTenants();
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
// Create the same user in tenant DB
|
||||
User::create([
|
||||
ResourceUser::create([
|
||||
'global_id' => 'acme',
|
||||
'name' => 'John Doe',
|
||||
'email' => 'john@localhost',
|
||||
|
|
@ -170,7 +170,7 @@ class ResourceSyncingTest extends TestCase
|
|||
// Assert role change doesn't cascade
|
||||
CentralUser::first()->update(['role' => 'central superadmin']);
|
||||
tenancy()->initialize($tenant);
|
||||
$this->assertSame('commenter', User::first()->role);
|
||||
$this->assertSame('commenter', ResourceUser::first()->role);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
@ -182,7 +182,7 @@ class ResourceSyncingTest extends TestCase
|
|||
$this->assertFalse(tenancy()->initialized);
|
||||
|
||||
$this->expectException(ModelNotSyncMaster::class);
|
||||
User::first()->update(['role' => 'foobar']);
|
||||
ResourceUser::first()->update(['role' => 'foobar']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
@ -196,19 +196,19 @@ class ResourceSyncingTest extends TestCase
|
|||
'role' => 'commenter', // unsynced
|
||||
]);
|
||||
|
||||
$tenant = Tenant::create([
|
||||
$tenant = ResourceTenant::create([
|
||||
'id' => 't1',
|
||||
]);
|
||||
$this->migrateTenants();
|
||||
|
||||
$tenant->run(function () {
|
||||
$this->assertCount(0, User::all());
|
||||
$this->assertCount(0, ResourceUser::all());
|
||||
});
|
||||
|
||||
$centralUser->tenants()->attach('t1');
|
||||
|
||||
$tenant->run(function () {
|
||||
$this->assertCount(1, User::all());
|
||||
$this->assertCount(1, ResourceUser::all());
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -223,13 +223,13 @@ class ResourceSyncingTest extends TestCase
|
|||
'role' => 'commenter', // unsynced
|
||||
]);
|
||||
|
||||
$tenant = Tenant::create([
|
||||
$tenant = ResourceTenant::create([
|
||||
'id' => 't1',
|
||||
]);
|
||||
$this->migrateTenants();
|
||||
|
||||
$tenant->run(function () {
|
||||
$this->assertCount(0, User::all());
|
||||
$this->assertCount(0, ResourceUser::all());
|
||||
});
|
||||
|
||||
// The child model is inaccessible in the Pivot Model, so we can't fire any events.
|
||||
|
|
@ -237,7 +237,7 @@ class ResourceSyncingTest extends TestCase
|
|||
|
||||
$tenant->run(function () {
|
||||
// Still zero
|
||||
$this->assertCount(0, User::all());
|
||||
$this->assertCount(0, ResourceUser::all());
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -252,15 +252,15 @@ class ResourceSyncingTest extends TestCase
|
|||
'role' => 'commenter', // unsynced
|
||||
]);
|
||||
|
||||
$t1 = Tenant::create([
|
||||
$t1 = ResourceTenant::create([
|
||||
'id' => 't1',
|
||||
]);
|
||||
|
||||
$t2 = Tenant::create([
|
||||
$t2 = ResourceTenant::create([
|
||||
'id' => 't2',
|
||||
]);
|
||||
|
||||
$t3 = Tenant::create([
|
||||
$t3 = ResourceTenant::create([
|
||||
'id' => 't3',
|
||||
]);
|
||||
$this->migrateTenants();
|
||||
|
|
@ -271,17 +271,17 @@ class ResourceSyncingTest extends TestCase
|
|||
|
||||
$t1->run(function () {
|
||||
// assert user exists
|
||||
$this->assertCount(1, User::all());
|
||||
$this->assertCount(1, ResourceUser::all());
|
||||
});
|
||||
|
||||
$t2->run(function () {
|
||||
// assert user exists
|
||||
$this->assertCount(1, User::all());
|
||||
$this->assertCount(1, ResourceUser::all());
|
||||
});
|
||||
|
||||
$t3->run(function () {
|
||||
// assert user does NOT exist
|
||||
$this->assertCount(0, User::all());
|
||||
$this->assertCount(0, ResourceUser::all());
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -297,10 +297,10 @@ class ResourceSyncingTest extends TestCase
|
|||
'role' => 'commenter', // unsynced
|
||||
]);
|
||||
|
||||
$t1 = Tenant::create([
|
||||
$t1 = ResourceTenant::create([
|
||||
'id' => 't1',
|
||||
]);
|
||||
$t2 = Tenant::create([
|
||||
$t2 = ResourceTenant::create([
|
||||
'id' => 't2',
|
||||
]);
|
||||
$this->migrateTenants();
|
||||
|
|
@ -310,7 +310,7 @@ class ResourceSyncingTest extends TestCase
|
|||
|
||||
$t2->run(function () {
|
||||
// Create user with the same global ID in t2 database
|
||||
User::create([
|
||||
ResourceUser::create([
|
||||
'global_id' => 'acme',
|
||||
'name' => 'John Foo', // changed
|
||||
'email' => 'john@foo', // changed
|
||||
|
|
@ -325,7 +325,7 @@ class ResourceSyncingTest extends TestCase
|
|||
$this->assertSame('commenter', $centralUser->role); // role didn't change
|
||||
|
||||
$t1->run(function () {
|
||||
$user = User::first();
|
||||
$user = ResourceUser::first();
|
||||
$this->assertSame('John Foo', $user->name); // name changed
|
||||
$this->assertSame('john@foo', $user->email); // email changed
|
||||
$this->assertSame('commenter', $user->role); // role didn't change, i.e. is the same as from the original copy from central
|
||||
|
|
@ -344,13 +344,13 @@ class ResourceSyncingTest extends TestCase
|
|||
'role' => 'commenter', // unsynced
|
||||
]);
|
||||
|
||||
$t1 = Tenant::create([
|
||||
$t1 = ResourceTenant::create([
|
||||
'id' => 't1',
|
||||
]);
|
||||
$t2 = Tenant::create([
|
||||
$t2 = ResourceTenant::create([
|
||||
'id' => 't2',
|
||||
]);
|
||||
$t3 = Tenant::create([
|
||||
$t3 = ResourceTenant::create([
|
||||
'id' => 't3',
|
||||
]);
|
||||
$this->migrateTenants();
|
||||
|
|
@ -361,17 +361,17 @@ class ResourceSyncingTest extends TestCase
|
|||
$centralUser->tenants()->attach('t3');
|
||||
|
||||
$t3->run(function () {
|
||||
User::first()->update([
|
||||
ResourceUser::first()->update([
|
||||
'name' => 'John 3',
|
||||
'role' => 'employee', // unsynced
|
||||
]);
|
||||
|
||||
$this->assertSame('employee', User::first()->role);
|
||||
$this->assertSame('employee', ResourceUser::first()->role);
|
||||
});
|
||||
|
||||
// Check that change was cascaded to other tenants
|
||||
$t1->run($check = function () {
|
||||
$user = User::first();
|
||||
$user = ResourceUser::first();
|
||||
|
||||
$this->assertSame('John 3', $user->name); // synced
|
||||
$this->assertSame('commenter', $user->role); // unsynced
|
||||
|
|
@ -408,7 +408,7 @@ class ResourceSyncingTest extends TestCase
|
|||
'role' => 'employee',
|
||||
]);
|
||||
|
||||
$t1 = Tenant::create([
|
||||
$t1 = ResourceTenant::create([
|
||||
'id' => 't1',
|
||||
]);
|
||||
|
||||
|
|
@ -417,21 +417,21 @@ class ResourceSyncingTest extends TestCase
|
|||
$centralUser->tenants()->attach('t1');
|
||||
|
||||
$t1->run(function () {
|
||||
$this->assertSame('employee', User::first()->role);
|
||||
$this->assertSame('employee', ResourceUser::first()->role);
|
||||
});
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function when_the_resource_doesnt_exist_in_the_central_db_non_synced_columns_will_bubble_up_too()
|
||||
{
|
||||
$t1 = Tenant::create([
|
||||
$t1 = ResourceTenant::create([
|
||||
'id' => 't1',
|
||||
]);
|
||||
|
||||
$this->migrateTenants();
|
||||
|
||||
$t1->run(function () {
|
||||
User::create([
|
||||
ResourceUser::create([
|
||||
'name' => 'John Doe',
|
||||
'email' => 'john@doe',
|
||||
'password' => 'secret',
|
||||
|
|
@ -448,7 +448,7 @@ class ResourceSyncingTest extends TestCase
|
|||
Queue::fake();
|
||||
UpdateSyncedResource::$shouldQueue = true;
|
||||
|
||||
$t1 = Tenant::create([
|
||||
$t1 = ResourceTenant::create([
|
||||
'id' => 't1',
|
||||
]);
|
||||
|
||||
|
|
@ -457,7 +457,7 @@ class ResourceSyncingTest extends TestCase
|
|||
Queue::assertNothingPushed();
|
||||
|
||||
$t1->run(function () {
|
||||
User::create([
|
||||
ResourceUser::create([
|
||||
'name' => 'John Doe',
|
||||
'email' => 'john@doe',
|
||||
'password' => 'secret',
|
||||
|
|
@ -484,13 +484,13 @@ class ResourceSyncingTest extends TestCase
|
|||
'role' => 'commenter', // unsynced
|
||||
]);
|
||||
|
||||
$t1 = Tenant::create([
|
||||
$t1 = ResourceTenant::create([
|
||||
'id' => 't1',
|
||||
]);
|
||||
$t2 = Tenant::create([
|
||||
$t2 = ResourceTenant::create([
|
||||
'id' => 't2',
|
||||
]);
|
||||
$t3 = Tenant::create([
|
||||
$t3 = ResourceTenant::create([
|
||||
'id' => 't3',
|
||||
]);
|
||||
$this->migrateTenants();
|
||||
|
|
@ -520,12 +520,12 @@ class ResourceSyncingTest extends TestCase
|
|||
Event::fake([SyncedResourceChangedInForeignDatabase::class]);
|
||||
|
||||
$t3->run(function () {
|
||||
User::first()->update([
|
||||
ResourceUser::first()->update([
|
||||
'name' => 'John 3',
|
||||
'role' => 'employee', // unsynced
|
||||
]);
|
||||
|
||||
$this->assertSame('employee', User::first()->role);
|
||||
$this->assertSame('employee', ResourceUser::first()->role);
|
||||
});
|
||||
|
||||
Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
|
||||
|
|
@ -545,11 +545,30 @@ class ResourceSyncingTest extends TestCase
|
|||
return $event->tenant === null;
|
||||
});
|
||||
|
||||
// todo update in global
|
||||
// Flush
|
||||
Event::fake([SyncedResourceChangedInForeignDatabase::class]);
|
||||
|
||||
$centralUser->update([
|
||||
'name' => 'John Central',
|
||||
]);
|
||||
|
||||
Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
|
||||
return optional($event->tenant)->getTenantKey() === 't1';
|
||||
});
|
||||
Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
|
||||
return optional($event->tenant)->getTenantKey() === 't2';
|
||||
});
|
||||
Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
|
||||
return optional($event->tenant)->getTenantKey() === 't3';
|
||||
});
|
||||
// Assert NOT dispatched in central
|
||||
Event::assertNotDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
|
||||
return $event->tenant === null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Tenant extends Models\Tenant
|
||||
class ResourceTenant extends Models\Tenant
|
||||
{
|
||||
public function users()
|
||||
{
|
||||
|
|
@ -568,13 +587,13 @@ class CentralUser extends Model implements SyncMaster
|
|||
|
||||
public function tenants(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Tenant::class, 'tenant_users', 'global_user_id', 'tenant_id')
|
||||
return $this->belongsToMany(ResourceTenant::class, 'tenant_users', 'global_user_id', 'tenant_id')
|
||||
->using(TenantPivot::class);
|
||||
}
|
||||
|
||||
public function getTenantModelName(): string
|
||||
{
|
||||
return User::class;
|
||||
return ResourceUser::class;
|
||||
}
|
||||
|
||||
public function getTenantIdColumnInMapTable(): string
|
||||
|
|
@ -607,10 +626,11 @@ class CentralUser extends Model implements SyncMaster
|
|||
}
|
||||
}
|
||||
|
||||
class User extends Model implements Syncable
|
||||
class ResourceUser extends Model implements Syncable
|
||||
{
|
||||
use ResourceSyncing;
|
||||
|
||||
protected $table = 'users';
|
||||
protected $guarded = [];
|
||||
public $timestamps = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Tests\v3;
|
||||
namespace Stancl\Tenancy\Tests;
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Stancl\Tenancy\Database\Models;
|
||||
use Stancl\Tenancy\Database\Models\Concerns\HasDomains;
|
||||
use Stancl\Tenancy\Database\Concerns\HasDomains;
|
||||
use Stancl\Tenancy\Exceptions\NotASubdomainException;
|
||||
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
|
||||
use Stancl\Tenancy\Tests\TestCase;
|
||||
|
|
@ -26,13 +26,13 @@ class SubdomainTest extends TestCase
|
|||
});
|
||||
});
|
||||
|
||||
config(['tenancy.tenant_model' => Tenant::class]);
|
||||
config(['tenancy.tenant_model' => SubdomainTenant::class]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function tenant_can_be_identified_by_subdomain()
|
||||
{
|
||||
$tenant = Tenant::create([
|
||||
$tenant = SubdomainTenant::create([
|
||||
'id' => 'acme',
|
||||
]);
|
||||
|
||||
|
|
@ -105,7 +105,7 @@ class SubdomainTest extends TestCase
|
|||
// not 'localhost'
|
||||
]]);
|
||||
|
||||
$tenant = Tenant::create([
|
||||
$tenant = SubdomainTenant::create([
|
||||
'id' => 'acme',
|
||||
]);
|
||||
|
||||
|
|
@ -121,7 +121,7 @@ class SubdomainTest extends TestCase
|
|||
}
|
||||
}
|
||||
|
||||
class Tenant extends Models\Tenant
|
||||
class SubdomainTenant extends Models\Tenant
|
||||
{
|
||||
use HasDomains;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Tests\v3;
|
||||
namespace Stancl\Tenancy\Tests;
|
||||
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Stancl\Tenancy\Controllers\TenantAssetsController;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
use Stancl\Tenancy\Events\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
||||
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Tests\v3;
|
||||
namespace Stancl\Tenancy\Tests;
|
||||
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Tests\v3;
|
||||
namespace Stancl\Tenancy\Tests;
|
||||
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
use Stancl\Tenancy\DatabaseManager;
|
||||
use Stancl\Tenancy\Events\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Events\Listeners\JobPipeline;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Listeners\JobPipeline;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Events\TenantCreated;
|
||||
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Tests\v3;
|
||||
namespace Stancl\Tenancy\Tests;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
|
@ -39,7 +39,7 @@ class TenantModelTest extends TestCase
|
|||
|
||||
tenancy()->end();
|
||||
|
||||
$this->assertSame(null, app(Tenant::class));
|
||||
$this->assertSame(null, app(Contracts\Tenant::class));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
@ -53,7 +53,7 @@ class TenantModelTest extends TestCase
|
|||
$this->assertSame('bar', $tenant->foo);
|
||||
$this->assertSame(null, $tenant->data);
|
||||
|
||||
// Low level test to test database structure
|
||||
// Low level test to assert database structure
|
||||
$this->assertSame(json_encode(['foo' => 'bar']), DB::table('tenants')->where('id', $tenant->id)->first()->data);
|
||||
$this->assertSame(null, DB::table('tenants')->where('id', $tenant->id)->first()->foo ?? null);
|
||||
|
||||
|
|
@ -103,8 +103,8 @@ class TenantModelTest extends TestCase
|
|||
|
||||
unset(app()[UniqueIdentifierGenerator::class]);
|
||||
|
||||
$tenant1 = MyTenant::create();
|
||||
$tenant2 = MyTenant::create();
|
||||
$tenant1 = Tenant::create();
|
||||
$tenant2 = Tenant::create();
|
||||
|
||||
$this->assertSame(1, $tenant1->id);
|
||||
$this->assertSame(2, $tenant2->id);
|
||||
|
|
@ -138,7 +138,6 @@ class TenantModelTest extends TestCase
|
|||
class MyTenant extends Tenant
|
||||
{
|
||||
protected $table = 'tenants';
|
||||
public $increments = true;
|
||||
}
|
||||
|
||||
class AnotherTenant extends Model implements Contracts\Tenant
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue