increments('id'); $table->string('text'); $table->string('tenant_id'); $table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade'); }); Schema::create('comments', function (Blueprint $table) { $table->increments('id'); $table->string('text'); $table->string('post_id'); $table->foreign('post_id')->references('id')->on('posts')->onUpdate('cascade')->onDelete('cascade'); }); config(['tenancy.tenant_model' => Tenant::class]); } /** @test */ public function primary_models_are_scoped_to_the_current_tenant() { // acme context tenancy()->initialize($acme = Tenant::create([ 'id' => 'acme', ])); $post = Post::create(['text' => 'Foo']); $this->assertSame('acme', $post->tenant_id); $this->assertSame('acme', $post->tenant->id); $post = Post::first(); $this->assertSame('acme', $post->tenant_id); $this->assertSame('acme', $post->tenant->id); // ====================================== // foobar context tenancy()->initialize($foobar = Tenant::create([ 'id' => 'foobar', ])); $post = Post::create(['text' => 'Bar']); $this->assertSame('foobar', $post->tenant_id); $this->assertSame('foobar', $post->tenant->id); $post = Post::first(); $this->assertSame('foobar', $post->tenant_id); $this->assertSame('foobar', $post->tenant->id); // ====================================== // acme context again tenancy()->initialize($acme); $post = Post::first(); $this->assertSame('acme', $post->tenant_id); $this->assertSame('acme', $post->tenant->id); // Assert foobar models are inaccessible in acme context $this->assertSame(1, Post::count()); } /** @test */ public function primary_models_are_not_scoped_in_the_central_context() { $this->primary_models_are_scoped_to_the_current_tenant(); tenancy()->end(); $this->assertSame(2, Post::count()); } /** @test */ public function secondary_models_are_scoped_to_the_current_tenant_when_accessed_via_primary_model() { // acme context tenancy()->initialize($acme = Tenant::create([ 'id' => 'acme', ])); $post = Post::create(['text' => 'Foo']); $post->comments()->create(['text' => 'Comment text']); // ================ // foobar context tenancy()->initialize($foobar = Tenant::create([ 'id' => 'foobar', ])); $post = Post::create(['text' => 'Bar']); $post->comments()->create(['text' => 'Comment text 2']); // ================ // acme context again tenancy()->initialize($acme); $this->assertSame(1, Post::count()); $this->assertSame(1, Post::first()->comments->count()); } /** @test */ public function secondary_models_are_NOT_scoped_to_the_current_tenant_when_accessed_directly() { $this->secondary_models_are_scoped_to_the_current_tenant_when_accessed_via_primary_model(); // We're in acme context $this->assertSame('acme', tenant('id')); $this->assertSame(2, Comment::count()); } /** @test */ public function secondary_models_ARE_scoped_to_the_current_tenant_when_accessed_directly_AND_PARENT_RELATIONSHIP_TRAIT_IS_USED() { $acme = Tenant::create([ 'id' => 'acme', ]); $acme->run(function () { $post = Post::create(['text' => 'Foo']); $post->scoped_comments()->create(['text' => 'Comment Text']); $this->assertSame(1, Post::count()); $this->assertSame(1, ScopedComment::count()); }); $foobar = Tenant::create([ 'id' => 'foobar', ]); $foobar->run(function () { $this->assertSame(0, Post::count()); $this->assertSame(0, ScopedComment::count()); $post = Post::create(['text' => 'Bar']); $post->scoped_comments()->create(['text' => 'Comment Text 2']); $this->assertSame(1, Post::count()); $this->assertSame(1, ScopedComment::count()); }); // Global context $this->assertSame(2, ScopedComment::count()); } /** @test */ public function secondary_models_are_NOT_scoped_in_the_central_context() { $this->secondary_models_are_scoped_to_the_current_tenant_when_accessed_via_primary_model(); tenancy()->end(); $this->assertSame(2, Comment::count()); } /** @test */ public function global_models_are_not_scoped_at_all() { Schema::create('global_resources', function (Blueprint $table) { $table->increments('id'); $table->string('text'); }); GlobalResource::create(['text' => 'First']); GlobalResource::create(['text' => 'Second']); $acme = Tenant::create([ 'id' => 'acme', ]); $acme->run(function () { $this->assertSame(2, GlobalResource::count()); GlobalResource::create(['text' => 'Third']); GlobalResource::create(['text' => 'Fourth']); }); $this->assertSame(4, GlobalResource::count()); } /** @test */ public function tenant_id_and_relationship_is_auto_added_when_creating_primary_resources_in_tenant_context() { tenancy()->initialize($acme = Tenant::create([ 'id' => 'acme', ])); $post = Post::create(['text' => 'Foo']); $this->assertSame('acme', $post->tenant_id); $this->assertTrue($post->relationLoaded('tenant')); $this->assertSame($acme, $post->tenant); $this->assertSame(tenant(), $post->tenant); } /** @test */ public function tenant_id_is_not_auto_added_when_creating_primary_resources_in_central_context() { $this->expectException(QueryException::class); Post::create(['text' => 'Foo']); } /** @test */ public function tenant_id_column_name_can_be_customized() { BelongsToTenant::$tenantIdColumn = 'team_id'; Schema::drop('posts'); Schema::create('posts', function (Blueprint $table) { $table->increments('id'); $table->string('text'); $table->string('team_id'); $table->foreign('team_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade'); }); tenancy()->initialize($acme = Tenant::create([ 'id' => 'acme', ])); $post = Post::create(['text' => 'Foo']); $this->assertSame('acme', $post->team_id); // ====================================== // foobar context tenancy()->initialize($foobar = Tenant::create([ 'id' => 'foobar', ])); $post = Post::create(['text' => 'Bar']); $this->assertSame('foobar', $post->team_id); $post = Post::first(); $this->assertSame('foobar', $post->team_id); // ====================================== // acme context again tenancy()->initialize($acme); $post = Post::first(); $this->assertSame('acme', $post->team_id); // Assert foobar models are inaccessible in acme context $this->assertSame(1, Post::count()); } /** @test */ public function the_model_returned_by_the_tenant_helper_has_unique_and_exists_validation_rules() { Schema::table('posts', function (Blueprint $table) { $table->string('slug')->nullable(); $table->unique(['tenant_id', 'slug']); }); tenancy()->initialize($acme = Tenant::create([ 'id' => 'acme', ])); Post::create(['text' => 'Foo', 'slug' => 'foo']); $data = ['text' => 'Foo 2', 'slug' => 'foo']; $uniqueFails = Validator::make($data, [ 'slug' => 'unique:posts', ])->fails(); $existsFails = Validator::make($data, [ 'slug' => 'exists:posts', ])->fails(); // Assert that 'unique' and 'exists' aren't scoped by default // $this->assertFalse($uniqueFails); // todo get these two assertions to pass. for some reason, the validator is passing for both 'unique' and 'exists' // $this->assertTrue($existsFails); // todo get these two assertions to pass. for some reason, the validator is passing for both 'unique' and 'exists' $uniqueFails = Validator::make($data, [ 'slug' => tenant()->unique('posts'), ])->fails(); $existsFails = Validator::make($data, [ 'slug' => tenant()->exists('posts'), ])->fails(); // Assert that tenant()->unique() and tenant()->exists() are scoped $this->assertTrue($uniqueFails); $this->assertFalse($existsFails); } } class Tenant extends TestTenant { use HasScopedValidationRules; } class Post extends Model { use BelongsToTenant; protected $guarded = []; public $timestamps = false; public function comments() { return $this->hasMany(Comment::class); } public function scoped_comments() { return $this->hasMany(Comment::class); } } class Comment extends Model { protected $guarded = []; public $timestamps = false; public function post() { return $this->belongsTo(Post::class); } } class ScopedComment extends Comment { use BelongsToPrimaryModel; protected $table = 'comments'; public function getRelationshipToPrimaryModel(): string { return 'post'; } } class GlobalResource extends Model { protected $guarded = []; public $timestamps = false; }