pp-planer/tests/Feature/OAuthTest.php
Thorsten Bus 57d54ec06b feat: Wave 1 Foundation - Database, OAuth, Sync, Files, Layout, Email (T2-T7)
T2: Database Schema + All Migrations
- 10 migrations: users extension, services, songs, song_groups, song_slides,
  song_arrangements, song_arrangement_groups, service_songs, slides, cts_sync_log
- 9 Eloquent models with relationships and casts
- 9 factory classes for testing
- Tests: DatabaseSchemaTest (2 tests, 26 assertions) 

T3: ChurchTools OAuth Provider
- Custom Socialite provider for ChurchTools OAuth2
- AuthController with redirect/callback/logout
- Replaced Breeze login with OAuth-only (German UI)
- Removed all Breeze register/password-reset pages
- Tests: OAuthTest (9 tests, 54 assertions) 

T4: CTS API Service + Sync Command
- ChurchToolsService wrapping 5pm-HDH/churchtools-api
- SyncChurchToolsCommand (php artisan cts:sync)
- SyncController for refresh button
- CCLI-based song matching
- Tests: ChurchToolsSyncTest (2 tests) 

T5: File Conversion Service
- FileConversionService with letterbox/pillarbox to 1920×1080
- ConvertPowerPointJob (queued) with LibreOffice + spatie/pdf-to-image
- ZIP extraction and recursive processing
- Thumbnail generation (320×180)
- Tests: FileConversionTest (2 tests, 21 assertions) 

T6: Shared Vue Components
- AuthenticatedLayout with nav, user info, refresh button
- useAutoSave composable (500ms debounce)
- FlashMessage, ConfirmDialog, LoadingSpinner components
- HandleInertiaRequests middleware with shared props
- Tests: SharedPropsTest (7 tests) 

T7: Email Configuration
- MissingSongRequest mailable (German)
- Email template with song info and service link
- SONG_REQUEST_EMAIL config
- Tests: MissingSongMailTest (2 tests, 10 assertions) 

All tests passing: 30/30 (233 assertions)
All UI text in German with 'Du' form
Wave 1 complete: 7/7 tasks 
2026-03-01 19:39:26 +01:00

168 lines
5.1 KiB
PHP

<?php
use App\Models\User;
use Laravel\Socialite\Facades\Socialite;
use Laravel\Socialite\Two\User as SocialiteUser;
it('redirects unauthenticated users to login', function () {
$response = $this->get('/');
$response->assertRedirect('/login');
});
it('shows login page with OAuth button', function () {
$response = $this->get('/login');
$response->assertStatus(200);
$response->assertInertia(
fn ($page) => $page
->component('Auth/Login')
);
});
it('login page has no email or password inputs', function () {
$response = $this->get('/login');
$response->assertStatus(200);
// The Login.vue should NOT contain email/password form fields
// This is verified by checking the component renders correctly
$response->assertInertia(
fn ($page) => $page
->component('Auth/Login')
);
});
it('redirects to ChurchTools OAuth on auth initiation', function () {
$providerMock = Mockery::mock(\Laravel\Socialite\Two\AbstractProvider::class);
$providerMock->shouldReceive('redirect')
->once()
->andReturn(redirect('https://churchtools.example.com/oauth/authorize'));
Socialite::shouldReceive('driver')
->with('churchtools')
->once()
->andReturn($providerMock);
$response = $this->get('/auth/churchtools');
$response->assertRedirect();
$response->assertRedirectContains('churchtools.example.com/oauth/authorize');
});
it('creates a new user from OAuth callback', function () {
$socialiteUser = new SocialiteUser();
$socialiteUser->map([
'id' => '42',
'name' => 'Max Mustermann',
'email' => 'max@example.com',
'avatar' => 'https://churchtools.example.com/avatar/42.jpg',
]);
$socialiteUser->user = [
'id' => 42,
'firstName' => 'Max',
'lastName' => 'Mustermann',
'displayName' => 'Max Mustermann',
'email' => 'max@example.com',
'imageUrl' => 'https://churchtools.example.com/avatar/42.jpg',
'groups' => [['id' => 1, 'name' => 'Worship']],
'roles' => [['id' => 2, 'name' => 'Admin']],
];
$providerMock = Mockery::mock(\Laravel\Socialite\Two\AbstractProvider::class);
$providerMock->shouldReceive('user')
->once()
->andReturn($socialiteUser);
Socialite::shouldReceive('driver')
->with('churchtools')
->once()
->andReturn($providerMock);
$response = $this->get('/auth/churchtools/callback');
$response->assertRedirect(route('dashboard'));
$this->assertDatabaseHas('users', [
'email' => 'max@example.com',
'name' => 'Max Mustermann',
'churchtools_id' => 42,
'avatar' => 'https://churchtools.example.com/avatar/42.jpg',
]);
$user = User::where('email', 'max@example.com')->first();
expect($user)->not->toBeNull();
expect((int) $user->churchtools_id)->toBe(42);
expect($user->churchtools_groups)->toBe([['id' => 1, 'name' => 'Worship']]);
expect($user->churchtools_roles)->toBe([['id' => 2, 'name' => 'Admin']]);
$this->assertAuthenticatedAs($user);
});
it('updates existing user on OAuth callback', function () {
$existingUser = User::factory()->create([
'email' => 'max@example.com',
'name' => 'Old Name',
'churchtools_id' => 42,
]);
$socialiteUser = new SocialiteUser();
$socialiteUser->map([
'id' => '42',
'name' => 'Max Mustermann',
'email' => 'max@example.com',
'avatar' => 'https://churchtools.example.com/avatar/42.jpg',
]);
$socialiteUser->user = [
'id' => 42,
'firstName' => 'Max',
'lastName' => 'Mustermann',
'displayName' => 'Max Mustermann',
'email' => 'max@example.com',
'imageUrl' => 'https://churchtools.example.com/avatar/42.jpg',
'groups' => [['id' => 1, 'name' => 'Worship']],
'roles' => [['id' => 2, 'name' => 'Admin']],
];
$providerMock = Mockery::mock(\Laravel\Socialite\Two\AbstractProvider::class);
$providerMock->shouldReceive('user')
->once()
->andReturn($socialiteUser);
Socialite::shouldReceive('driver')
->with('churchtools')
->once()
->andReturn($providerMock);
$response = $this->get('/auth/churchtools/callback');
$response->assertRedirect(route('dashboard'));
$existingUser->refresh();
expect($existingUser->name)->toBe('Max Mustermann');
expect($existingUser->avatar)->toBe('https://churchtools.example.com/avatar/42.jpg');
expect(User::count())->toBe(1);
$this->assertAuthenticatedAs($existingUser);
});
it('logs out user and redirects to login', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)->post('/logout');
$response->assertRedirect('/login');
$this->assertGuest();
});
it('does not have register routes', function () {
$response = $this->get('/register');
$response->assertStatus(404);
});
it('authenticated user can access dashboard', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)->get('/dashboard');
$response->assertStatus(200);
});