diff --git a/database/migrations/2024_01_01_000023_add_channel_foreign_key_to_article_publications.php b/database/migrations/2024_01_01_000023_add_channel_foreign_key_to_article_publications.php new file mode 100644 index 00000000..33efee13 --- /dev/null +++ b/database/migrations/2024_01_01_000023_add_channel_foreign_key_to_article_publications.php @@ -0,0 +1,37 @@ +whereNotIn('platform_channel_id', DB::table('platform_channels')->select('id')); + + if (($count = $orphans->count()) > 0) { + Log::warning("Deleting {$count} orphaned article_publications rows before adding the channel foreign key."); + $orphans->delete(); + } + + Schema::table('article_publications', function (Blueprint $table) { + $table->foreign('platform_channel_id') + ->references('id') + ->on('platform_channels') + ->onDelete('cascade'); + }); + } + + public function down(): void + { + // Orphan rows deleted in up() are not restored. + Schema::table('article_publications', function (Blueprint $table) { + $table->dropForeign(['platform_channel_id']); + }); + } +}; diff --git a/tests/Feature/ChannelDeletionCascadeTest.php b/tests/Feature/ChannelDeletionCascadeTest.php new file mode 100644 index 00000000..01b2ca59 --- /dev/null +++ b/tests/Feature/ChannelDeletionCascadeTest.php @@ -0,0 +1,120 @@ +create(); + /** @var PlatformChannel $channel */ + $channel = PlatformChannel::factory()->create(); + + Route::create([ + 'feed_id' => $feed->id, + 'platform_channel_id' => $channel->id, + 'priority' => 50, + 'is_active' => true, + ]); + + Keyword::create([ + 'feed_id' => $feed->id, + 'platform_channel_id' => $channel->id, + 'keyword' => 'brussels', + 'is_active' => true, + ]); + + $article = Article::factory()->create(['feed_id' => $feed->id]); + + RouteArticle::factory()->create([ + 'feed_id' => $feed->id, + 'platform_channel_id' => $channel->id, + 'article_id' => $article->id, + ]); + + ArticlePublication::factory()->create([ + 'article_id' => $article->id, + 'platform_channel_id' => $channel->id, + ]); + + $account = PlatformAccount::factory()->create(); + $channel->platformAccounts()->attach($account->id, [ + 'is_active' => true, + 'priority' => 1, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + return [$channel, $feed]; + } + + public function test_deleting_a_channel_cascades_to_every_dependent_table(): void + { + [$channel] = $this->channelWithEverything(); + + $this->assertSame(1, Route::count()); + $this->assertSame(1, Keyword::count()); + $this->assertSame(1, RouteArticle::count()); + $this->assertSame(1, ArticlePublication::count()); + + $channel->delete(); + + $this->assertSame(0, Route::count()); + $this->assertSame(0, Keyword::count()); + $this->assertSame(0, RouteArticle::count()); + $this->assertSame(0, ArticlePublication::count()); + $this->assertDatabaseCount('platform_account_channels', 0); + } + + public function test_deleting_a_channel_leaves_the_feed_and_article_intact(): void + { + [$channel, $feed] = $this->channelWithEverything(); + + $channel->delete(); + + $this->assertDatabaseHas('feeds', ['id' => $feed->id]); + $this->assertSame(1, Article::count()); + } + + public function test_deleting_a_channel_leaves_other_channels_untouched(): void + { + [$channel] = $this->channelWithEverything(); + $other = PlatformChannel::factory()->create(); + + $channel->delete(); + + $this->assertDatabaseHas('platform_channels', ['id' => $other->id]); + } + + public function test_publications_for_another_channel_survive(): void + { + [$channel] = $this->channelWithEverything(); + + $other = PlatformChannel::factory()->create(); + $article = Article::factory()->create(); + ArticlePublication::factory()->create([ + 'article_id' => $article->id, + 'platform_channel_id' => $other->id, + ]); + + $channel->delete(); + + $this->assertSame(1, ArticlePublication::count()); + } +}