Snapshot of main at 9f39a6e1e (2026-11-02)

This commit is contained in:
Ronald Huynen committed 2026-11-02 10:41:47 +01:00
commit 6ccf1792d7
2580 files changed
+1040202

No files matched your search

+146
View File
@@ -0,0 +1,146 @@
<?php
namespace Tests\Feature\Polls;
use App\Models\Admin;
use App\Models\Category;
use App\Models\CategoryTranslation;
use App\Models\Organization;
use App\Models\Poll;
use App\Models\PollLedgerEntry;
use App\Models\PollQuestion;
use App\Models\PostTranslation;
use App\Polls\Ledger;
use App\Polls\PollBackup;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Tests\TestCase;
/**
* Poll definitions in post backups: exported and restored as drafts, and an existing
* poll with votes is never overwritten (that would cascade to its votes and ledger).
*/
class PollBackupTest extends TestCase
{
use DatabaseTransactions;
private string $file;
protected function setUp(): void
{
parent::setUp();
// phpunit.xml configures Redis, which is the dev site's cache too
config(['cache.default' => 'array', 'scout.driver' => null]);
$this->file = storage_path('framework/testing/poll-backup-' . uniqid() . '.json');
File::ensureDirectoryExists(dirname($this->file));
}
protected function tearDown(): void
{
File::delete($this->file);
parent::tearDown();
}
private function makePollPost(string $slug, Organization $owner): Poll
{
$category = Category::firstOrCreate(['type' => Poll::class, 'categoryable_id' => null, 'categoryable_type' => null]);
CategoryTranslation::firstOrCreate(['category_id' => $category->id, 'locale' => 'en'], ['name' => 'Poll', 'slug' => 'poll-backup-test-' . $category->id]);
$postId = DB::table('posts')->insertGetId([
'postable_id' => 1, 'postable_type' => Admin::class, 'category_id' => $category->id, 'created_at' => now(), 'updated_at' => now(),
]);
DB::table('post_translations')->insert([
'post_id' => $postId, 'locale' => 'en', 'slug' => $slug, 'title' => 'Backed up poll', 'created_at' => now(), 'updated_at' => now(),
]);
$poll = Poll::create([
'post_id' => $postId, 'pollable_id' => $owner->id, 'pollable_type' => Organization::class,
'status' => Poll::STATUS_OPEN, 'closes_at' => '2026-12-01 18:00:00', 'vote_scope' => Poll::VOTE_SCOPE_PER_USER,
'anonymous' => true, 'verification' => Poll::VERIFICATION_RECEIPTS,
'filter_by_location' => true, 'location_city_ids' => [99999],
'target_profile_types' => [Organization::class], 'registered_until' => '2025-12-31',
]);
PollQuestion::create(['poll_id' => $poll->id, 'position' => 1, 'type' => 'yes_no', 'label' => ['en' => 'First?'], 'settings' => []]);
PollQuestion::create(['poll_id' => $poll->id, 'position' => 2, 'type' => 'scale', 'required' => false,
'label' => ['en' => 'Second?', 'nl' => 'Tweede?'], 'settings' => ['min' => 1, 'max' => 5]]);
return $poll;
}
public function test_round_trip_restores_the_definition_as_a_draft(): void
{
$owner = Organization::factory()->create(['name' => 'Backup Owner Org']);
$poll = $this->makePollPost('poll-backup-round-trip', $owner);
$admin = Admin::factory()->create();
$this->artisan('posts:backup', ['--post-ids' => (string) $poll->post_id, '--exclude-media' => true, '--output' => $this->file])
->assertSuccessful();
$backup = json_decode(File::get($this->file), true);
$this->assertSame(1, $backup['meta']['counts']['polls']);
$this->assertCount(2, $backup['posts'][0]['poll']['questions']);
$this->assertArrayNotHasKey('responses', $backup['posts'][0]['poll']);
// Gone from this database...
DB::table('post_translations')->where('post_id', $poll->post_id)->delete();
DB::table('posts')->where('id', $poll->post_id)->delete();
// ...and restored
$this->artisan('posts:restore', ['file' => $this->file, '--profile-id' => $admin->id, '--profile-type' => 'Admin'])
->expectsConfirmation('Do you want to proceed with the restore?', 'yes')
->assertSuccessful();
$postId = PostTranslation::where('slug', 'poll-backup-round-trip')->value('post_id');
$restored = Poll::where('post_id', $postId)->sole();
$this->assertSame(Poll::STATUS_DRAFT, $restored->status);
$this->assertTrue($restored->pollable->is($owner));
$this->assertSame('2026-12-01 18:00', $restored->closes_at->format('Y-m-d H:i'));
$this->assertSame(Poll::VOTE_SCOPE_PER_USER, $restored->vote_scope);
$this->assertTrue($restored->anonymous);
$this->assertSame(Poll::VERIFICATION_RECEIPTS, $restored->verification);
$this->assertSame([Organization::class], $restored->target_profile_types);
$this->assertFalse($restored->filter_by_location); // location ids differ between databases
$this->assertEmpty($restored->location_city_ids);
$this->assertSame(['yes_no', 'scale'], $restored->questions()->pluck('type')->all());
$this->assertSame('Tweede?', $restored->questions()->get()[1]->labelFor('nl'));
$this->assertNull($restored->sealed_at);
}
public function test_import_rejects_unknown_owner_types(): void
{
$post = \App\Models\Post::withoutSyncingToSearch(fn () => \App\Models\Post::create(['postable_id' => 1, 'postable_type' => Admin::class]));
$poll = PollBackup::import($post, ['owner_type' => 'Illuminate\\Support\\Facades\\File', 'owner_name' => 'x', 'vote_scope' => 'nonsense', 'questions' => [['type' => 'yes_no']]]);
$this->assertNull($poll->pollable_type);
$this->assertSame(Poll::VOTE_SCOPE_PER_PROFILE, $poll->vote_scope);
$this->assertSame(1, $poll->questions()->count());
}
public function test_overwrite_never_deletes_a_poll_with_votes(): void
{
$poll = $this->makePollPost('poll-backup-protected', Organization::factory()->create());
$admin = Admin::factory()->create();
$this->artisan('posts:backup', ['--post-ids' => (string) $poll->post_id, '--exclude-media' => true, '--output' => $this->file])
->assertSuccessful();
// Someone votes after the backup was made
$ledger = new Ledger();
DB::transaction(fn () => $ledger->append($poll, PollLedgerEntry::CAST, ['answers' => []], hash('sha256', 'r')));
$this->assertTrue(PollBackup::protectsSlugs(['poll-backup-protected']));
$this->artisan('posts:restore', ['file' => $this->file, '--profile-id' => $admin->id, '--profile-type' => 'Admin'])
->expectsConfirmation('Do you want to proceed with the restore?', 'yes')
->expectsChoice('What would you like to do?', 'overwrite', [
'skip', 'overwrite', 'skip_all', 'overwrite_all',
'Skip this post', 'Overwrite existing post(s)', 'Skip all duplicates', 'Overwrite all duplicates',
])
->expectsOutputToContain('Not overwritten: the existing post has a poll with votes.')
->assertSuccessful();
$this->assertNotNull(Poll::find($poll->id));
$this->assertSame(1, PollLedgerEntry::where('poll_id', $poll->id)->count());
$this->assertSame(1, PostTranslation::where('slug', 'poll-backup-protected')->count());
}
}