diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 0abaed9..518b0bc 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -4,16 +4,20 @@ on: push: pull_request: schedule: - - cron: "0 0 * * *" + - cron: '0 0 * * *' jobs: test: runs-on: ubuntu-latest + strategy: fail-fast: true matrix: - php: [8.2, 8.1] - laravel: [10.0] + php: [8.3, 8.2, 8.1] + laravel: [10.0, '11.0'] + exclude: + - laravel: '11.0' + php: 8.1 name: P${{ matrix.php }} - L${{ matrix.laravel }} @@ -23,7 +27,7 @@ jobs: - name: Cache dependencies uses: actions/cache@v2 - with: + with: path: ~/.composer/cache/files key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} @@ -40,4 +44,4 @@ jobs: composer update --prefer-dist --no-interaction --no-progress - name: Execute tests - run: vendor/bin/phpunit + run: vendor/bin/pest diff --git a/composer.json b/composer.json index ba4a2ab..b85e035 100644 --- a/composer.json +++ b/composer.json @@ -4,11 +4,8 @@ "type": "utility", "require": { "php": "^8.1", - "illuminate/database": "^10.0", - "illuminate/events": "^10.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.6 || ^10.0" + "illuminate/database": "^10.0 || ^11.0", + "illuminate/events": "^10.0 || ^11.0" }, "autoload": { "psr-4": { @@ -33,6 +30,13 @@ "config": { "extra": { "sort-packages": true + }, + "allow-plugins": { + "pestphp/pest-plugin": true } + }, + "require-dev": { + "pestphp/pest": "^2.34", + "pestphp/pest-plugin-drift": "^2.5" } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 1baf874..0c12bb9 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,13 +1,17 @@ - - - - app/ - - - - - ./tests/ - - + + + + ./tests + + + + + ./src + + diff --git a/tests/CascadeSoftDeletesIntegrationTest.php b/tests/CascadeSoftDeletesIntegrationTest.php index f9dad8f..e2306bf 100644 --- a/tests/CascadeSoftDeletesIntegrationTest.php +++ b/tests/CascadeSoftDeletesIntegrationTest.php @@ -1,317 +1,292 @@ addConnection([ + 'driver' => 'sqlite', + 'database' => ':memory:', + ]); + + $manager->setEventDispatcher(new Dispatcher(new Container())); + + $manager->setAsGlobal(); + $manager->bootEloquent(); + + $manager->schema()->create('authors', function ($table) { + $table->increments('id'); + $table->string('name'); + $table->timestamps(); + $table->softDeletes(); + }); + + $manager->schema()->create('posts', function ($table) { + $table->increments('id'); + $table->integer('author_id')->unsigned()->nullable(); + $table->string('title'); + $table->string('body'); + $table->timestamps(); + $table->softDeletes(); + }); + + $manager->schema()->create('comments', function ($table) { + $table->increments('id'); + $table->integer('post_id')->unsigned(); + $table->string('body'); + $table->timestamps(); + }); + + $manager->schema()->create('post_types', function ($table) { + $table->increments('id'); + $table->integer('post_id')->unsigned()->nullable(); + $table->string('label'); + $table->timestamps(); + }); + + $manager->schema()->create('authors__post_types', function ($table) { + $table->increments('id'); + $table->integer('author_id'); + $table->integer('posttype_id'); + $table->timestamps(); + + $table->foreign('author_id')->references('id')->on('author'); + $table->foreign('posttype_id')->references('id')->on('post_types'); + }); +}); + +it('cascades deletes when deleting a parent model', function () { + $post = Post::create([ + 'title' => 'How to cascade soft deletes in Laravel', + 'body' => 'This is how you cascade soft deletes in Laravel', + ]); + + attachCommentsToPost($post); + + expect($post->comments)->toHaveCount(3); + + $post->delete(); + + expect(Comment::where('post_id', $post->id)->get())->toHaveCount(0); +}); + +it('cascades deletes entries from pivot table', function () { + $author = Author::create(['name' => 'ManyToManyTestAuthor']); + + attachPostTypesToAuthor($author); + expect($author->posttypes)->toHaveCount(2); + + $author->delete(); + + $pivotEntries = Manager::table('authors__post_types') + ->where('author_id', $author->id) + ->get(); + + expect($pivotEntries)->toHaveCount(0); +}); + +it('cascades deletes when force deleting a parent model', function () { + $post = Post::create([ + 'title' => 'How to cascade soft deletes in Laravel', + 'body' => 'This is how you cascade soft deletes in Laravel', + ]); + + attachCommentsToPost($post); + + expect($post->comments)->toHaveCount(3); + + $post->forceDelete(); + + expect(Comment::where('post_id', $post->id)->get())->toHaveCount(0); + expect(Post::withTrashed()->where('id', $post->id)->get())->toHaveCount(0); +}); + +it('takes exception to models that do not implement soft deletes', function () { + $post = NonSoftDeletingPost::create([ + 'title' => 'Testing when you can use this trait', + 'body' => 'Ensure that you can only use this trait if it uses SoftDeletes', + ]); + + attachCommentsToPost($post); -class CascadeSoftDeletesIntegrationTest extends TestCase -{ - public static function setupBeforeClass(): void - { - $manager = new Manager(); - $manager->addConnection([ - 'driver' => 'sqlite', - 'database' => ':memory:', - ]); - - $manager->setEventDispatcher(new Dispatcher(new Container())); - - $manager->setAsGlobal(); - $manager->bootEloquent(); - - $manager->schema()->create('authors', function ($table) { - $table->increments('id'); - $table->string('name'); - $table->timestamps(); - $table->softDeletes(); - }); - - $manager->schema()->create('posts', function ($table) { - $table->increments('id'); - $table->integer('author_id')->unsigned()->nullable(); - $table->string('title'); - $table->string('body'); - $table->timestamps(); - $table->softDeletes(); - }); - - $manager->schema()->create('comments', function ($table) { - $table->increments('id'); - $table->integer('post_id')->unsigned(); - $table->string('body'); - $table->timestamps(); - }); - - $manager->schema()->create('post_types', function ($table) { - $table->increments('id'); - $table->integer('post_id')->unsigned()->nullable(); - $table->string('label'); - $table->timestamps(); - }); - - $manager->schema()->create('authors__post_types', function ($table) { - $table->increments('id'); - $table->integer('author_id'); - $table->integer('posttype_id'); - $table->timestamps(); - - $table->foreign('author_id')->references('id')->on('author'); - $table->foreign('posttype_id')->references('id')->on('post_types'); - }); - } + $post->delete(); +})->throws( + CascadeSoftDeleteException::class, + 'Tests\Entities\NonSoftDeletingPost does not implement Illuminate\Database\Eloquent\SoftDeletes' +); + +it('takes exception to models trying to cascade deletes on invalid relationships', function () { + $post = InvalidRelationshipPost::create([ + 'title' => 'Testing invalid cascade relationships', + 'body' => 'Ensure you can only use this trait if the model defines valid relationships', + ]); + attachCommentsToPost($post); + + $post->delete(); +})->throws( + CascadeSoftDeleteException::class, + 'Relationships [invalidRelationship, anotherInvalidRelationship] must exist and return an object of type Illuminate\Database\Eloquent\Relations\Relation' +); - /** @test */ - public function it_cascades_deletes_when_deleting_a_parent_model() - { - $post = Tests\Entities\Post::create([ - 'title' => 'How to cascade soft deletes in Laravel', - 'body' => 'This is how you cascade soft deletes in Laravel', - ]); +it('ensures that no deletes are performed if there are invalid relationships', function () { + $post = InvalidRelationshipPost::create([ + 'title' => 'Testing deletes are not executed', + 'body' => 'If an invalid relationship is encountered, no deletes should be perofrmed', + ]); - $this->attachCommentsToPost($post); + attachCommentsToPost($post); - $this->assertCount(3, $post->comments); + try { $post->delete(); - $this->assertCount(0, Tests\Entities\Comment::where('post_id', $post->id)->get()); + } catch (CascadeSoftDeleteException) { + expect(InvalidRelationshipPost::find($post->id))->not->toBeNull(); + expect(Comment::where('post_id', $post->id)->get())->toHaveCount(3); } +}); - /** @test */ - public function it_cascades_deletes_entries_from_pivot_table() - { - $author = Tests\Entities\Author::create(['name' => 'ManyToManyTestAuthor']); +it('can accept cascade deletes as a single string', function () { + $post = PostWithStringCascade::create([ + 'title' => 'Testing you can use a string for a single relationship', + 'body' => 'This falls more closely in line with how other things work in Eloquent', + ]); - $this->attachPostTypesToAuthor($author); - $this->assertCount(2, $author->posttypes); + attachCommentsToPost($post); - $author->delete(); + $post->delete(); - $pivotEntries = Manager::table('authors__post_types') - ->where('author_id', $author->id) - ->get(); + expect(Post::find($post->id))->toBeNull(); + expect(Post::withTrashed()->where('id', $post->id)->get())->toHaveCount(1); + expect(Comment::where('post_id', $post->id)->get())->toHaveCount(0); +}); - $this->assertCount(0, $pivotEntries); - } +it('handles situations where the relationship method does not exist', function () { + $post = PostWithMissingRelationshipMethod::create([ + 'title' => 'Testing that missing relationship methods are accounted for', + 'body' => 'In this way, you need not worry about Laravel returning fatal errors', + ]); - /** @test */ - public function it_cascades_deletes_when_force_deleting_a_parent_model() - { - $post = Tests\Entities\Post::create([ - 'title' => 'How to cascade soft deletes in Laravel', - 'body' => 'This is how you cascade soft deletes in Laravel', - ]); + $post->delete(); +})->throws( + CascadeSoftDeleteException::class, + 'Relationship [comments] must exist and return an object of type Illuminate\Database\Eloquent\Relations\Relation' +); - $this->attachCommentsToPost($post); +it('handles soft deletes inherited from a parent model', function () { + $post = ChildPost::create([ + 'title' => 'Testing child model inheriting model trait', + 'body' => 'This should allow a child class to inherit the soft deletes trait', + ]); - $this->assertCount(3, $post->comments); - $post->forceDelete(); - $this->assertCount(0, Tests\Entities\Comment::where('post_id', $post->id)->get()); - $this->assertCount(0, Tests\Entities\Post::withTrashed()->where('id', $post->id)->get()); - } + attachCommentsToPost($post); - /** - * @test - */ - public function it_takes_exception_to_models_that_do_not_implement_soft_deletes() - { - $this->expectException(CascadeSoftDeleteException::class); - $this->expectExceptionMessage('Tests\Entities\NonSoftDeletingPost does not implement Illuminate\Database\Eloquent\SoftDeletes'); + $post->delete(); - $post = Tests\Entities\NonSoftDeletingPost::create([ - 'title' => 'Testing when you can use this trait', - 'body' => 'Ensure that you can only use this trait if it uses SoftDeletes', - ]); + expect(ChildPost::find($post->id))->toBeNull(); + expect(ChildPost::withTrashed()->where('id', $post->id)->get())->toHaveCount(1); + expect(Comment::where('post_id', $post->id)->get())->toHaveCount(0); +}); - $this->attachCommentsToPost($post); +it('handles grandchildren', function () { + $author = Author::create([ + 'name' => 'Testing grandchildren are deleted', + ]); - $post->delete(); - } + attachPostsAndCommentsToAuthor($author); - /** - * @test - */ - public function it_takes_exception_to_models_trying_to_cascade_deletes_on_invalid_relationships() - { - $this->expectException(CascadeSoftDeleteException::class); - $this->expectExceptionMessage('Relationships [invalidRelationship, anotherInvalidRelationship] must exist and return an object of type Illuminate\Database\Eloquent\Relations\Relation'); + $author->delete(); - $post = Tests\Entities\InvalidRelationshipPost::create([ - 'title' => 'Testing invalid cascade relationships', - 'body' => 'Ensure you can only use this trait if the model defines valid relationships', - ]); + expect(Author::find($author->id))->toBeNull(); + expect(Author::withTrashed()->where('id', $author->id)->get())->toHaveCount(1); + expect(Post::where('author_id', $author->id)->get())->toHaveCount(0); - $this->attachCommentsToPost($post); + $deletedPosts = Post::withTrashed()->where('author_id', $author->id)->get(); - $post->delete(); - } + expect($deletedPosts)->toHaveCount(2); - /** @test */ - public function it_ensures_that_no_deletes_are_performed_if_there_are_invalid_relationships() - { - $post = Tests\Entities\InvalidRelationshipPost::create([ - 'title' => 'Testing deletes are not executed', - 'body' => 'If an invalid relationship is encountered, no deletes should be perofrmed', - ]); - - $this->attachCommentsToPost($post); - - try { - $post->delete(); - } catch (CascadeSoftDeleteException $e) { - $this->assertNotNull(Tests\Entities\InvalidRelationshipPost::find($post->id)); - $this->assertCount(3, Tests\Entities\Comment::where('post_id', $post->id)->get()); - } + foreach ($deletedPosts as $deletedPost) { + expect(Comment::where('post_id', $deletedPost->id)->get())->toHaveCount(0); } +}); - /** @test */ - public function it_can_accept_cascade_deletes_as_a_single_string() - { - $post = Tests\Entities\PostWithStringCascade::create([ - 'title' => 'Testing you can use a string for a single relationship', - 'body' => 'This falls more closely in line with how other things work in Eloquent', - ]); +it('cascades a has one relationship', function () { + $post = Post::create([ + 'title' => 'Cascade a has one relationship', + 'body' => 'This is how you cascade a has one relationship', + ]); - $this->attachCommentsToPost($post); + $type = new PostType(['label' => 'Test']); - $post->delete(); - - $this->assertNull(Tests\Entities\Post::find($post->id)); - $this->assertCount(1, Tests\Entities\Post::withTrashed()->where('id', $post->id)->get()); - $this->assertCount(0, Tests\Entities\Comment::where('post_id', $post->id)->get()); - } + $post->postType()->save($type); - /** - * @test + $post->delete(); - */ - public function it_handles_situations_where_the_relationship_method_does_not_exist() - { - $this->expectException(CascadeSoftDeleteException::class); - $this->expectExceptionMessage('Relationship [comments] must exist and return an object of type Illuminate\Database\Eloquent\Relations\Relation'); - - $post = Tests\Entities\PostWithMissingRelationshipMethod::create([ - 'title' => 'Testing that missing relationship methods are accounted for', - 'body' => 'In this way, you need not worry about Laravel returning fatal errors', - ]); - - $post->delete(); - } - - /** @test */ - public function it_handles_soft_deletes_inherited_from_a_parent_model() - { - $post = Tests\Entities\ChildPost::create([ - 'title' => 'Testing child model inheriting model trait', - 'body' => 'This should allow a child class to inherit the soft deletes trait', - ]); - - $this->attachCommentsToPost($post); - - $post->delete(); + expect(PostType::where('id', $type->id)->get())->toHaveCount(0); +}); - $this->assertNull(Tests\Entities\ChildPost::find($post->id)); - $this->assertCount(1, Tests\Entities\ChildPost::withTrashed()->where('id', $post->id)->get()); - $this->assertCount(0, Tests\Entities\Comment::where('post_id', $post->id)->get()); - } - - /** @test */ - public function it_handles_grandchildren() - { - $author = Tests\Entities\Author::create([ - 'name' => 'Testing grandchildren are deleted', - ]); - - $this->attachPostsAndCommentsToAuthor($author); - - $author->delete(); - - $this->assertNull(Tests\Entities\Author::find($author->id)); - $this->assertCount(1, Tests\Entities\Author::withTrashed()->where('id', $author->id)->get()); - $this->assertCount(0, Tests\Entities\Post::where('author_id', $author->id)->get()); - - $deletedPosts = Tests\Entities\Post::withTrashed()->where('author_id', $author->id)->get(); - $this->assertCount(2, $deletedPosts); - - foreach ($deletedPosts as $deletedPost) { - $this->assertCount(0, Tests\Entities\Comment::where('post_id', $deletedPost->id)->get()); - } - } - - /** @test */ - public function it_cascades_a_has_one_relationship() - { - $post = Tests\Entities\Post::create([ - 'title' => 'Cascade a has one relationship', - 'body' => 'This is how you cascade a has one relationship', - ]); - - $type = new Tests\Entities\PostType(['label' => 'Test']); - - $post->postType()->save($type); - - $post->delete(); - $this->assertCount(0, Tests\Entities\PostType::where('id', $type->id)->get()); - } +/** + * Attach some post types to the given author. + * + * @return void + */ +function attachPostTypesToAuthor($author) +{ + $author->posttypes()->saveMany([ + PostType::create([ + 'label' => 'First Post Type', + ]), + + PostType::create([ + 'label' => 'Second Post Type', + ]), + ]); +} - /** - * Attach some post types to the given author. - * - * @return void - */ - public function attachPostTypesToAuthor($author) - { - $author->posttypes()->saveMany([ - - Tests\Entities\PostType::create([ - 'label' => 'First Post Type', - ]), - - Tests\Entities\PostType::create([ - 'label' => 'Second Post Type', - ]), - ]); - } +/** + * Attach some dummy posts (w/ comments) to the given author. + * + * @return void + */ +function attachPostsAndCommentsToAuthor($author) +{ + $author->posts()->saveMany([ + attachCommentsToPost(Post::create([ + 'title' => 'First post', + 'body' => 'This is the first test post', + ])), + attachCommentsToPost(Post::create([ + 'title' => 'Second post', + 'body' => 'This is the second test post', + ])), + ]); + + return $author; +} - /** - * Attach some dummy posts (w/ comments) to the given author. - * - * @return void - */ - private function attachPostsAndCommentsToAuthor($author) - { - $author->posts()->saveMany([ - $this->attachCommentsToPost( - Tests\Entities\Post::create([ - 'title' => 'First post', - 'body' => 'This is the first test post', - ]) - ), - $this->attachCommentsToPost( - Tests\Entities\Post::create([ - 'title' => 'Second post', - 'body' => 'This is the second test post', - ]) - ), - ]); - - return $author; - } +/** + * Attach some dummy comments to the given post. + * + * @return void + */ +function attachCommentsToPost($post) +{ + $post->comments()->saveMany([ + new Comment(['body' => 'This is the first test comment']), + new Comment(['body' => 'This is the second test comment']), + new Comment(['body' => 'This is the third test comment']), + ]); - /** - * Attach some dummy comments to the given post. - * - * @return void - */ - private function attachCommentsToPost($post) - { - $post->comments()->saveMany([ - new Tests\Entities\Comment(['body' => 'This is the first test comment']), - new Tests\Entities\Comment(['body' => 'This is the second test comment']), - new Tests\Entities\Comment(['body' => 'This is the third test comment']), - ]); - - return $post; - } + return $post; } diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..5949c61 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,45 @@ +in('Feature'); + +/* +|-------------------------------------------------------------------------- +| Expectations +|-------------------------------------------------------------------------- +| +| When you're writing tests, you often need to check that values meet certain conditions. The +| "expect()" function gives you access to a set of "expectations" methods that you can use +| to assert different things. Of course, you may extend the Expectation API at any time. +| +*/ + +expect()->extend('toBeOne', function () { + return $this->toBe(1); +}); + +/* +|-------------------------------------------------------------------------- +| Functions +|-------------------------------------------------------------------------- +| +| While Pest is very powerful out-of-the-box, you may have some testing code specific to your +| project that you don't want to repeat in every file. Here you can also expose helpers as +| global functions to help you to reduce the number of lines of code in your test files. +| +*/ + +function something() +{ + // .. +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..cfb05b6 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,10 @@ +