diff --git a/database/migrations/2026_10_27_160000_erase_the_link_previews_of_erased_commits.php b/database/migrations/2026_10_27_160000_erase_the_link_previews_of_erased_commits.php new file mode 100644 index 000000000..c6d293402 --- /dev/null +++ b/database/migrations/2026_10_27_160000_erase_the_link_previews_of_erased_commits.php @@ -0,0 +1,23 @@ +string('app.url'))->after('://')->before('/')->value()); + + DB::table('link_previews') + ->whereRaw('url ~* ?', ["^https?://([^/?#]+\\.)?{$host}/@[^/?#]+/commits/"]) + ->whereNotExists(fn (Builder $query): Builder => $query->from('commits') + ->whereNotNull('user_id') + ->whereRaw("commits.id::text = substring(lower(link_previews.url) from '/commits/([0-9a-f-]{36})')")) + ->delete(); + } +}; diff --git a/tests/Unit/Actions/DeleteUserTest.php b/tests/Unit/Actions/DeleteUserTest.php index df3242c95..2e99e53fd 100644 --- a/tests/Unit/Actions/DeleteUserTest.php +++ b/tests/Unit/Actions/DeleteUserTest.php @@ -353,3 +353,25 @@ resolve(DeleteUser::class)->handle($user); expect(PushSubscription::query()->count())->toBe(0); }); + +it('erases the previews of links to commits that deleted accounts already took with them', function (): void { + $tombstone = Commit::factory()->create(['user_id' => null, 'body' => '', 'deleted_at' => now()]); + $gone = fake()->uuid(); + $leftovers = collect([ + LinkPreview::factory()->create(['url' => url("/@gone/commits/{$tombstone->id}")]), + LinkPreview::factory()->create(['url' => url('/@gone/commits/'.mb_strtoupper($gone))]), + LinkPreview::factory()->create(['url' => str_replace('://', '://www.', url("/@gone/commits/{$gone}"))]), + ]); + $kept = collect([Commit::factory()->create(), Commit::factory()->create(['deleted_at' => now()])]) + ->map(fn (Commit $commit): LinkPreview => LinkPreview::factory()->create(['url' => url("/@{$commit->user->handle}/commits/{$commit->id}")])) + ->push(LinkPreview::factory()->create(['url' => "https://example.com/@gone/commits/{$gone}"])) + ->push(LinkPreview::factory()->create(['url' => str_replace('://', '://not', url("/@gone/commits/{$gone}"))])) + ->push(LinkPreview::factory()->create(['url' => 'https://example.com/redirect?to='.url("/@gone/commits/{$gone}")])) + ->push(LinkPreview::factory()->create(['url' => url("/@gone?redirect=/commits/{$gone}")])); + + $migration = require database_path('migrations/2026_10_27_160000_erase_the_link_previews_of_erased_commits.php'); + $migration->up(); + + $leftovers->each(fn (LinkPreview $preview) => $this->assertModelMissing($preview)); + $kept->each(fn (LinkPreview $preview) => $this->assertModelExists($preview)); +});