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 ✅
168 lines
5.1 KiB
PHP
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);
|
|
});
|