147 lines
7.1 KiB
PHP
147 lines
7.1 KiB
PHP
<?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());
|
|
}
|
|
}
|