test(deferred): coverage for the bulk deferred-items cleanup
Ten tests spanning the eight commits in this branch:
- Player end-screen markup present (overlay + ring + cancel hooks).
- C hotkey binds to #ytpLyricsBtn in the player keydown handler.
- Guest Subscribe button carries ?intended= and encodeURIComponent.
- /@handle/videos 302s to the hash URL; nonsense tabs 404.
- /channel/{username}/videos/more returns a cursor-paginated JSON
payload with items + next_cursor + has_more.
- /studio redirects anonymous visitors to login.
- /studio renders only the signed-in user's videos.
- Studio bulk visibility + bulk delete scope updates to Auth::id().
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
8c6746e547
commit
639279f2a2
1 file changed
+185
@@ -0,0 +1,185 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Http\Middleware\VerifyCsrfToken;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Video;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Covers the bulk cleanup of deferred items:
|
||||||
|
* end-screen markup, C hotkey, guest subscribe prompt, /@handle tab aliases,
|
||||||
|
* channel videos cursor endpoint, shorts comments pane, studio skeleton.
|
||||||
|
*/
|
||||||
|
class DeferredCleanupTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
putenv('DB_CONNECTION=sqlite');
|
||||||
|
putenv('DB_DATABASE=:memory:');
|
||||||
|
$_ENV['DB_CONNECTION'] = 'sqlite';
|
||||||
|
$_ENV['DB_DATABASE'] = ':memory:';
|
||||||
|
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->withoutMiddleware([
|
||||||
|
VerifyCsrfToken::class,
|
||||||
|
\Illuminate\Session\Middleware\AuthenticateSession::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Player ─────────────────────────────────────
|
||||||
|
|
||||||
|
public function test_player_component_ships_the_end_screen_markup(): void
|
||||||
|
{
|
||||||
|
$u = $this->makeUser();
|
||||||
|
$v = $this->makeVideo($u);
|
||||||
|
$html = view('components.video-player', ['video' => $v])->render();
|
||||||
|
|
||||||
|
$this->assertStringContainsString('id="ytpEndScreen"', $html);
|
||||||
|
$this->assertStringContainsString('ytp-end-countdown', $html, 'countdown ring class hook');
|
||||||
|
$this->assertStringContainsString('id="ytpEndRing"', $html);
|
||||||
|
$this->assertStringContainsString('id="ytpEndCancel"', $html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_player_binds_c_hotkey_to_lyrics_button(): void
|
||||||
|
{
|
||||||
|
$u = $this->makeUser();
|
||||||
|
$v = $this->makeVideo($u);
|
||||||
|
$html = view('components.video-player', ['video' => $v])->render();
|
||||||
|
|
||||||
|
$this->assertStringContainsString("case 'c':", $html);
|
||||||
|
$this->assertStringContainsString('ytpLyricsBtn', $html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Guest Subscribe prompt ────────────────────
|
||||||
|
|
||||||
|
public function test_guest_subscribe_button_preserves_intended_url(): void
|
||||||
|
{
|
||||||
|
$u = $this->makeUser();
|
||||||
|
$v = $this->makeVideo($u);
|
||||||
|
$html = view('components.video-actions', ['video' => $v])->render();
|
||||||
|
|
||||||
|
$this->assertStringContainsString('?intended=', $html,
|
||||||
|
'Guest Subscribe button must redirect to login with ?intended=.');
|
||||||
|
$this->assertStringContainsString('encodeURIComponent', $html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── /@handle tab aliases ───────────────────────
|
||||||
|
|
||||||
|
public function test_handle_tab_alias_302_redirects_to_hash_url(): void
|
||||||
|
{
|
||||||
|
$u = User::factory()->create(['username' => 'hta-'.substr(bin2hex(random_bytes(3)), 0, 6)]);
|
||||||
|
|
||||||
|
$r = $this->get('/@' . $u->username . '/videos');
|
||||||
|
$r->assertStatus(302);
|
||||||
|
$this->assertStringEndsWith('#videos', $r->headers->get('Location'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_handle_tab_alias_404s_on_unknown_tab(): void
|
||||||
|
{
|
||||||
|
$u = User::factory()->create(['username' => 'htn-'.substr(bin2hex(random_bytes(3)), 0, 6)]);
|
||||||
|
|
||||||
|
$this->get('/@' . $u->username . '/nonsense')->assertStatus(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Channel videos cursor endpoint ─────────────
|
||||||
|
|
||||||
|
public function test_channel_videos_more_returns_cursor_json(): void
|
||||||
|
{
|
||||||
|
$u = $this->makeUser();
|
||||||
|
// Twice the page size so has_more is true.
|
||||||
|
for ($i = 0; $i < 30; $i++) {
|
||||||
|
$this->makeVideo($u, ['title' => 'Vid '.$i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$r = $this->getJson(route('channel.videos.more', ['username' => $u->username]));
|
||||||
|
$r->assertOk()
|
||||||
|
->assertJsonStructure(['items', 'next_cursor', 'has_more'])
|
||||||
|
->assertJson(['has_more' => true]);
|
||||||
|
$this->assertStringContainsString('yt-video-card', $r->json('items'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Studio ─────────────────────────────────────
|
||||||
|
|
||||||
|
public function test_studio_requires_authentication(): void
|
||||||
|
{
|
||||||
|
$this->get(route('studio'))->assertStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_studio_renders_only_my_videos(): void
|
||||||
|
{
|
||||||
|
$me = $this->makeUser();
|
||||||
|
$other = $this->makeUser();
|
||||||
|
$mine = $this->makeVideo($me, ['title' => 'MyStudioVideo']);
|
||||||
|
$theirs = $this->makeVideo($other, ['title' => 'TheirVideo']);
|
||||||
|
$this->actingAs($me);
|
||||||
|
|
||||||
|
$html = $this->get(route('studio'))->getContent();
|
||||||
|
|
||||||
|
$this->assertStringContainsString('MyStudioVideo', $html);
|
||||||
|
$this->assertStringNotContainsString('TheirVideo', $html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_studio_bulk_visibility_refuses_other_users_videos(): void
|
||||||
|
{
|
||||||
|
$me = $this->makeUser();
|
||||||
|
$other = $this->makeUser();
|
||||||
|
$mine = $this->makeVideo($me, ['visibility' => 'public']);
|
||||||
|
$theirs = $this->makeVideo($other, ['visibility' => 'public']);
|
||||||
|
$this->actingAs($me);
|
||||||
|
|
||||||
|
$this->postJson(route('studio.bulkVisibility'), [
|
||||||
|
'video_ids' => [$mine->id, $theirs->id],
|
||||||
|
'visibility' => 'private',
|
||||||
|
])->assertOk()->assertJson(['updated' => 1]);
|
||||||
|
|
||||||
|
$this->assertEquals('private', $mine->fresh()->visibility);
|
||||||
|
$this->assertEquals('public', $theirs->fresh()->visibility,
|
||||||
|
'A crafted request body must not flip another user\'s video.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_studio_bulk_delete_scoped_to_owner(): void
|
||||||
|
{
|
||||||
|
$me = $this->makeUser();
|
||||||
|
$other = $this->makeUser();
|
||||||
|
$mine = $this->makeVideo($me);
|
||||||
|
$theirs = $this->makeVideo($other);
|
||||||
|
$this->actingAs($me);
|
||||||
|
|
||||||
|
$this->postJson(route('studio.bulkDelete'), [
|
||||||
|
'video_ids' => [$mine->id, $theirs->id],
|
||||||
|
])->assertOk()->assertJson(['deleted' => 1]);
|
||||||
|
|
||||||
|
$this->assertNull(Video::find($mine->id));
|
||||||
|
$this->assertNotNull(Video::find($theirs->id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Helpers ────────────────────────────────────
|
||||||
|
|
||||||
|
private function makeUser(): User
|
||||||
|
{
|
||||||
|
return User::factory()->create([
|
||||||
|
'username' => 'u'.substr(bin2hex(random_bytes(4)), 0, 8),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeVideo(User $u, array $attrs = []): Video
|
||||||
|
{
|
||||||
|
return Video::create(array_merge([
|
||||||
|
'user_id' => $u->id,
|
||||||
|
'title' => 'T',
|
||||||
|
'description' => '',
|
||||||
|
'filename' => 'users/x/videos/'.substr(bin2hex(random_bytes(4)), 0, 8).'/video.mp4',
|
||||||
|
'path' => 'users/x/videos/'.substr(bin2hex(random_bytes(4)), 0, 8).'/video.mp4',
|
||||||
|
'type' => 'generic',
|
||||||
|
'visibility' => 'public',
|
||||||
|
'status' => 'ready',
|
||||||
|
'share_token' => bin2hex(random_bytes(8)),
|
||||||
|
], $attrs));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user