'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()); } }