feat(controllers): add macro/label import + global assignment + service override controllers and routes
- MacroImportController + LabelImportController: POST endpoints accepting uploaded .bin files,
delegating to MacrosImportService / LabelsImportService and returning import stats / warnings as JSON.
Generic German 422 error if parser rejects the file.
- MacroAssignmentController: index renders Settings Inertia page with assignments / macros / labels /
collections / last-import metadata. store/update/destroy/reorder for global MacroAssignment rows.
- ServiceMacroOverrideController: store snapshots all matching global MacroAssignments into
service-specific rows when a service opts to override; destroy removes both override and
service-specific assignments. storeAssignment / updateAssignment / destroyAssignment manage the
per-service rows directly.
- routes/web.php: 12 new named routes inside the auth middleware group; reorder route placed before
{macroAssignment} parameter route to avoid capture conflict.
- Tests: 19 new Pest tests across 4 feature files (54 assertions). Full suite 376 passed.
This commit is contained in:
parent
cef247336e
commit
6ce5b6e018
37
app/Http/Controllers/LabelImportController.php
Normal file
37
app/Http/Controllers/LabelImportController.php
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Services\LabelsImportService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class LabelImportController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly LabelsImportService $importService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function store(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$request->validate(['file' => ['required', 'file', 'max:5120']]);
|
||||||
|
|
||||||
|
$file = $request->file('file');
|
||||||
|
$tempPath = $file->getPathname();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $this->importService->import($tempPath, $file->getClientOriginalName());
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Die Datei konnte nicht gelesen werden. Stelle sicher, dass es eine gültige ProPresenter Labels-Datei ist.',
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'new' => $result->newCount,
|
||||||
|
'updated' => $result->updatedCount,
|
||||||
|
'total' => $result->totalInFile,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
86
app/Http/Controllers/MacroAssignmentController.php
Normal file
86
app/Http/Controllers/MacroAssignmentController.php
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Label;
|
||||||
|
use App\Models\Macro;
|
||||||
|
use App\Models\MacroAssignment;
|
||||||
|
use App\Models\MacroCollection;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
|
||||||
|
class MacroAssignmentController extends Controller
|
||||||
|
{
|
||||||
|
public function index(): Response
|
||||||
|
{
|
||||||
|
return Inertia::render('Settings', [
|
||||||
|
'assignments' => MacroAssignment::with(['macro', 'label'])->orderBy('part_type')->orderBy('order')->get(),
|
||||||
|
'macros' => Macro::with('collections')->orderBy('name')->get(),
|
||||||
|
'labels' => Label::orderBy('name')->get(),
|
||||||
|
'collections' => MacroCollection::orderBy('name')->get(),
|
||||||
|
'last_macros_import' => [
|
||||||
|
'at' => Setting::get('macros_last_imported_at'),
|
||||||
|
'filename' => Setting::get('macros_last_imported_filename'),
|
||||||
|
],
|
||||||
|
'last_labels_import' => [
|
||||||
|
'at' => Setting::get('labels_last_imported_at'),
|
||||||
|
'filename' => Setting::get('labels_last_imported_filename'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'part_type' => ['required', 'in:information,moderation,sermon,song,agenda_item'],
|
||||||
|
'macro_id' => ['required', 'integer', 'exists:macros,id'],
|
||||||
|
'position' => ['required', 'in:all_slides,first_slide,last_slide,by_label'],
|
||||||
|
'label_id' => ['nullable', 'integer', 'exists:labels,id'],
|
||||||
|
'order' => ['integer', 'min:0'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$assignment = MacroAssignment::create($validated);
|
||||||
|
|
||||||
|
return response()->json(['id' => $assignment->id, 'success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, MacroAssignment $macroAssignment): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'part_type' => ['sometimes', 'in:information,moderation,sermon,song,agenda_item'],
|
||||||
|
'macro_id' => ['sometimes', 'integer', 'exists:macros,id'],
|
||||||
|
'position' => ['sometimes', 'in:all_slides,first_slide,last_slide,by_label'],
|
||||||
|
'label_id' => ['nullable', 'integer', 'exists:labels,id'],
|
||||||
|
'order' => ['sometimes', 'integer', 'min:0'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$macroAssignment->update($validated);
|
||||||
|
|
||||||
|
return response()->json(['success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(MacroAssignment $macroAssignment): JsonResponse
|
||||||
|
{
|
||||||
|
$macroAssignment->delete();
|
||||||
|
|
||||||
|
return response()->json(['success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reorder(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'assignments' => ['required', 'array'],
|
||||||
|
'assignments.*.id' => ['required', 'integer', 'exists:macro_assignments,id'],
|
||||||
|
'assignments.*.order' => ['required', 'integer', 'min:0'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($validated['assignments'] as $item) {
|
||||||
|
MacroAssignment::where('id', $item['id'])->update(['order' => $item['order']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json(['success' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
app/Http/Controllers/MacroImportController.php
Normal file
41
app/Http/Controllers/MacroImportController.php
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Services\MacrosImportService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class MacroImportController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly MacrosImportService $importService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function store(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$request->validate(['file' => ['required', 'file', 'max:5120']]);
|
||||||
|
|
||||||
|
$file = $request->file('file');
|
||||||
|
$tempPath = $file->getPathname();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $this->importService->import($tempPath, $file->getClientOriginalName());
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Die Datei konnte nicht gelesen werden. Stelle sicher, dass es eine gültige ProPresenter Makro-Datei ist.',
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'stats' => [
|
||||||
|
'new' => $result->new,
|
||||||
|
'updated' => $result->updated,
|
||||||
|
'disabled' => $result->disabled,
|
||||||
|
're_enabled' => $result->reEnabled,
|
||||||
|
],
|
||||||
|
'warnings' => $result->warnings,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
94
app/Http/Controllers/ServiceMacroOverrideController.php
Normal file
94
app/Http/Controllers/ServiceMacroOverrideController.php
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\MacroAssignment;
|
||||||
|
use App\Models\Service;
|
||||||
|
use App\Models\ServiceMacroAssignment;
|
||||||
|
use App\Models\ServiceMacroOverride;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ServiceMacroOverrideController extends Controller
|
||||||
|
{
|
||||||
|
public function store(Request $request, Service $service): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'part_type' => ['required', 'in:information,moderation,sermon,song,agenda_item'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
ServiceMacroOverride::firstOrCreate([
|
||||||
|
'service_id' => $service->id,
|
||||||
|
'part_type' => $validated['part_type'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$globals = MacroAssignment::where('part_type', $validated['part_type'])->orderBy('order')->get();
|
||||||
|
foreach ($globals as $global) {
|
||||||
|
ServiceMacroAssignment::firstOrCreate([
|
||||||
|
'service_id' => $service->id,
|
||||||
|
'part_type' => $validated['part_type'],
|
||||||
|
'macro_id' => $global->macro_id,
|
||||||
|
'position' => $global->position,
|
||||||
|
'label_id' => $global->label_id,
|
||||||
|
'order' => $global->order,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json(['success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Service $service, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'part_type' => ['required', 'in:information,moderation,sermon,song,agenda_item'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
ServiceMacroOverride::where('service_id', $service->id)
|
||||||
|
->where('part_type', $validated['part_type'])
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
ServiceMacroAssignment::where('service_id', $service->id)
|
||||||
|
->where('part_type', $validated['part_type'])
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
return response()->json(['success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeAssignment(Request $request, Service $service): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'part_type' => ['required', 'in:information,moderation,sermon,song,agenda_item'],
|
||||||
|
'macro_id' => ['required', 'integer', 'exists:macros,id'],
|
||||||
|
'position' => ['required', 'in:all_slides,first_slide,last_slide,by_label'],
|
||||||
|
'label_id' => ['nullable', 'integer', 'exists:labels,id'],
|
||||||
|
'order' => ['integer', 'min:0'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$assignment = ServiceMacroAssignment::create([
|
||||||
|
'service_id' => $service->id,
|
||||||
|
...$validated,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json(['id' => $assignment->id, 'success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateAssignment(Request $request, Service $service, ServiceMacroAssignment $serviceMacroAssignment): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'position' => ['sometimes', 'in:all_slides,first_slide,last_slide,by_label'],
|
||||||
|
'label_id' => ['nullable', 'integer', 'exists:labels,id'],
|
||||||
|
'order' => ['sometimes', 'integer', 'min:0'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$serviceMacroAssignment->update($validated);
|
||||||
|
|
||||||
|
return response()->json(['success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroyAssignment(Service $service, ServiceMacroAssignment $serviceMacroAssignment): JsonResponse
|
||||||
|
{
|
||||||
|
$serviceMacroAssignment->delete();
|
||||||
|
|
||||||
|
return response()->json(['success' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,11 @@
|
||||||
|
|
||||||
use App\Http\Controllers\ApiLogController;
|
use App\Http\Controllers\ApiLogController;
|
||||||
use App\Http\Controllers\AuthController;
|
use App\Http\Controllers\AuthController;
|
||||||
|
use App\Http\Controllers\LabelImportController;
|
||||||
|
use App\Http\Controllers\MacroAssignmentController;
|
||||||
|
use App\Http\Controllers\MacroImportController;
|
||||||
use App\Http\Controllers\ServiceController;
|
use App\Http\Controllers\ServiceController;
|
||||||
|
use App\Http\Controllers\ServiceMacroOverrideController;
|
||||||
use App\Http\Controllers\SettingsController;
|
use App\Http\Controllers\SettingsController;
|
||||||
use App\Http\Controllers\SongPdfController;
|
use App\Http\Controllers\SongPdfController;
|
||||||
use App\Http\Controllers\SyncController;
|
use App\Http\Controllers\SyncController;
|
||||||
|
|
@ -90,4 +94,34 @@
|
||||||
Route::post('/slides/reorder', '\\App\\Http\\Controllers\\SlideController@reorder')->name('slides.reorder');
|
Route::post('/slides/reorder', '\\App\\Http\\Controllers\\SlideController@reorder')->name('slides.reorder');
|
||||||
Route::delete('/slides/{slide}', '\\App\\Http\\Controllers\\SlideController@destroy')->name('slides.destroy');
|
Route::delete('/slides/{slide}', '\\App\\Http\\Controllers\\SlideController@destroy')->name('slides.destroy');
|
||||||
Route::patch('/slides/{slide}/expire-date', '\\App\\Http\\Controllers\\SlideController@updateExpireDate')->name('slides.update-expire-date');
|
Route::patch('/slides/{slide}/expire-date', '\\App\\Http\\Controllers\\SlideController@updateExpireDate')->name('slides.update-expire-date');
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Makro- und Label-Import (ProPresenter)
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
Route::post('/settings/macros/import', [MacroImportController::class, 'store'])->name('settings.macros.import');
|
||||||
|
Route::post('/settings/labels/import', [LabelImportController::class, 'store'])->name('settings.labels.import');
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Globale Makro-Zuweisungen
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
Route::get('/settings/macro-assignments', [MacroAssignmentController::class, 'index'])->name('settings.macro-assignments.index');
|
||||||
|
Route::post('/settings/macro-assignments/reorder', [MacroAssignmentController::class, 'reorder'])->name('settings.macro-assignments.reorder');
|
||||||
|
Route::post('/settings/macro-assignments', [MacroAssignmentController::class, 'store'])->name('settings.macro-assignments.store');
|
||||||
|
Route::patch('/settings/macro-assignments/{macroAssignment}', [MacroAssignmentController::class, 'update'])->name('settings.macro-assignments.update');
|
||||||
|
Route::delete('/settings/macro-assignments/{macroAssignment}', [MacroAssignmentController::class, 'destroy'])->name('settings.macro-assignments.destroy');
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Service-spezifische Makro-Overrides
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
Route::post('/services/{service}/macro-overrides', [ServiceMacroOverrideController::class, 'store'])->name('services.macro-overrides.store');
|
||||||
|
Route::delete('/services/{service}/macro-overrides', [ServiceMacroOverrideController::class, 'destroy'])->name('services.macro-overrides.destroy');
|
||||||
|
Route::post('/services/{service}/macro-assignments', [ServiceMacroOverrideController::class, 'storeAssignment'])->name('services.macro-assignments.store');
|
||||||
|
Route::patch('/services/{service}/macro-assignments/{serviceMacroAssignment}', [ServiceMacroOverrideController::class, 'updateAssignment'])->name('services.macro-assignments.update');
|
||||||
|
Route::delete('/services/{service}/macro-assignments/{serviceMacroAssignment}', [ServiceMacroOverrideController::class, 'destroyAssignment'])->name('services.macro-assignments.destroy');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
33
tests/Feature/LabelImportControllerTest.php
Normal file
33
tests/Feature/LabelImportControllerTest.php
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
test('label import requires authentication', function () {
|
||||||
|
$response = $this->post(route('settings.labels.import'), []);
|
||||||
|
$response->assertRedirect(route('login'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('label import returns json on valid file', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$response = $this->actingAs($user)
|
||||||
|
->post(route('settings.labels.import'), [
|
||||||
|
'file' => new UploadedFile(base_path('tests/fixtures/labels-sample.bin'), 'labels.bin', null, null, true),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJsonStructure(['new', 'updated', 'total']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('label import returns 422 on invalid file', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$response = $this->actingAs($user)
|
||||||
|
->post(route('settings.labels.import'), [
|
||||||
|
'file' => UploadedFile::fake()->create('notalabels.bin', 1),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422);
|
||||||
|
});
|
||||||
108
tests/Feature/MacroAssignmentControllerTest.php
Normal file
108
tests/Feature/MacroAssignmentControllerTest.php
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Label;
|
||||||
|
use App\Models\Macro;
|
||||||
|
use App\Models\MacroAssignment;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
test('store requires authentication', function () {
|
||||||
|
$response = $this->post(route('settings.macro-assignments.store'), []);
|
||||||
|
$response->assertRedirect(route('login'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('store creates macro assignment', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$macro = Macro::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)
|
||||||
|
->postJson(route('settings.macro-assignments.store'), [
|
||||||
|
'part_type' => 'song',
|
||||||
|
'macro_id' => $macro->id,
|
||||||
|
'position' => 'all_slides',
|
||||||
|
'order' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200)->assertJson(['success' => true]);
|
||||||
|
expect(MacroAssignment::count())->toBe(1);
|
||||||
|
expect(MacroAssignment::first()->part_type)->toBe('song');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('store with by_label position and label_id', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$macro = Macro::factory()->create();
|
||||||
|
$label = Label::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)
|
||||||
|
->postJson(route('settings.macro-assignments.store'), [
|
||||||
|
'part_type' => 'sermon',
|
||||||
|
'macro_id' => $macro->id,
|
||||||
|
'position' => 'by_label',
|
||||||
|
'label_id' => $label->id,
|
||||||
|
'order' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$assignment = MacroAssignment::first();
|
||||||
|
expect($assignment->label_id)->toBe($label->id);
|
||||||
|
expect($assignment->position)->toBe('by_label');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('update modifies existing assignment', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$macro = Macro::factory()->create();
|
||||||
|
$assignment = MacroAssignment::create([
|
||||||
|
'part_type' => 'song',
|
||||||
|
'macro_id' => $macro->id,
|
||||||
|
'position' => 'all_slides',
|
||||||
|
'order' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)
|
||||||
|
->patchJson(route('settings.macro-assignments.update', $assignment), [
|
||||||
|
'position' => 'first_slide',
|
||||||
|
'order' => 5,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200)->assertJson(['success' => true]);
|
||||||
|
expect($assignment->fresh()->position)->toBe('first_slide');
|
||||||
|
expect($assignment->fresh()->order)->toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroy deletes macro assignment', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$macro = Macro::factory()->create();
|
||||||
|
$assignment = MacroAssignment::create([
|
||||||
|
'part_type' => 'song',
|
||||||
|
'macro_id' => $macro->id,
|
||||||
|
'position' => 'all_slides',
|
||||||
|
'order' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)
|
||||||
|
->deleteJson(route('settings.macro-assignments.destroy', $assignment));
|
||||||
|
|
||||||
|
$response->assertStatus(200)->assertJson(['success' => true]);
|
||||||
|
expect(MacroAssignment::count())->toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reorder updates order on multiple assignments', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$macro = Macro::factory()->create();
|
||||||
|
$a1 = MacroAssignment::create(['part_type' => 'song', 'macro_id' => $macro->id, 'position' => 'all_slides', 'order' => 0]);
|
||||||
|
$a2 = MacroAssignment::create(['part_type' => 'song', 'macro_id' => $macro->id, 'position' => 'all_slides', 'order' => 1]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)
|
||||||
|
->postJson(route('settings.macro-assignments.reorder'), [
|
||||||
|
'assignments' => [
|
||||||
|
['id' => $a1->id, 'order' => 5],
|
||||||
|
['id' => $a2->id, 'order' => 3],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
expect($a1->fresh()->order)->toBe(5);
|
||||||
|
expect($a2->fresh()->order)->toBe(3);
|
||||||
|
});
|
||||||
41
tests/Feature/MacroImportControllerTest.php
Normal file
41
tests/Feature/MacroImportControllerTest.php
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
test('macro import requires authentication', function () {
|
||||||
|
$response = $this->post(route('settings.macros.import'), []);
|
||||||
|
$response->assertRedirect(route('login'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('macro import returns json with stats on valid file', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$response = $this->actingAs($user)
|
||||||
|
->post(route('settings.macros.import'), [
|
||||||
|
'file' => new UploadedFile(base_path('tests/fixtures/macros-sample.bin'), 'macros.bin', null, null, true),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJsonStructure(['stats' => ['new', 'updated', 'disabled', 're_enabled'], 'warnings']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('macro import returns 422 on invalid file', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$response = $this->actingAs($user)
|
||||||
|
->post(route('settings.macros.import'), [
|
||||||
|
'file' => UploadedFile::fake()->create('notamacro.bin', 1),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('macro import without file returns validation error', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$response = $this->actingAs($user)
|
||||||
|
->postJson(route('settings.macros.import'), []);
|
||||||
|
|
||||||
|
$response->assertStatus(422);
|
||||||
|
});
|
||||||
104
tests/Feature/ServiceMacroOverrideControllerTest.php
Normal file
104
tests/Feature/ServiceMacroOverrideControllerTest.php
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Macro;
|
||||||
|
use App\Models\MacroAssignment;
|
||||||
|
use App\Models\Service;
|
||||||
|
use App\Models\ServiceMacroAssignment;
|
||||||
|
use App\Models\ServiceMacroOverride;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
test('override requires authentication', function () {
|
||||||
|
$service = Service::factory()->create();
|
||||||
|
$response = $this->post(route('services.macro-overrides.store', $service), ['part_type' => 'song']);
|
||||||
|
$response->assertRedirect(route('login'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('store creates override and snapshots global assignments', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$service = Service::factory()->create();
|
||||||
|
$macro = Macro::factory()->create();
|
||||||
|
MacroAssignment::create(['part_type' => 'song', 'macro_id' => $macro->id, 'position' => 'all_slides', 'order' => 0]);
|
||||||
|
MacroAssignment::create(['part_type' => 'song', 'macro_id' => $macro->id, 'position' => 'first_slide', 'order' => 1]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)
|
||||||
|
->postJson(route('services.macro-overrides.store', $service), ['part_type' => 'song']);
|
||||||
|
|
||||||
|
$response->assertStatus(200)->assertJson(['success' => true]);
|
||||||
|
expect(ServiceMacroOverride::where('service_id', $service->id)->where('part_type', 'song')->exists())->toBeTrue();
|
||||||
|
expect(ServiceMacroAssignment::where('service_id', $service->id)->count())->toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('store does not snapshot assignments from different part_type', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$service = Service::factory()->create();
|
||||||
|
$macro = Macro::factory()->create();
|
||||||
|
MacroAssignment::create(['part_type' => 'song', 'macro_id' => $macro->id, 'position' => 'all_slides', 'order' => 0]);
|
||||||
|
MacroAssignment::create(['part_type' => 'sermon', 'macro_id' => $macro->id, 'position' => 'all_slides', 'order' => 0]);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->postJson(route('services.macro-overrides.store', $service), ['part_type' => 'song']);
|
||||||
|
|
||||||
|
expect(ServiceMacroAssignment::where('service_id', $service->id)->count())->toBe(1);
|
||||||
|
expect(ServiceMacroAssignment::where('service_id', $service->id)->first()->part_type)->toBe('song');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroy removes override and service-specific assignments', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$service = Service::factory()->create();
|
||||||
|
$macro = Macro::factory()->create();
|
||||||
|
ServiceMacroOverride::create(['service_id' => $service->id, 'part_type' => 'song']);
|
||||||
|
ServiceMacroAssignment::create([
|
||||||
|
'service_id' => $service->id,
|
||||||
|
'part_type' => 'song',
|
||||||
|
'macro_id' => $macro->id,
|
||||||
|
'position' => 'all_slides',
|
||||||
|
'order' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)
|
||||||
|
->deleteJson(route('services.macro-overrides.destroy', $service), ['part_type' => 'song']);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
expect(ServiceMacroOverride::where('service_id', $service->id)->count())->toBe(0);
|
||||||
|
expect(ServiceMacroAssignment::where('service_id', $service->id)->count())->toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('storeAssignment creates service-level assignment', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$service = Service::factory()->create();
|
||||||
|
$macro = Macro::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)
|
||||||
|
->postJson(route('services.macro-assignments.store', $service), [
|
||||||
|
'part_type' => 'sermon',
|
||||||
|
'macro_id' => $macro->id,
|
||||||
|
'position' => 'last_slide',
|
||||||
|
'order' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200)->assertJson(['success' => true]);
|
||||||
|
expect(ServiceMacroAssignment::count())->toBe(1);
|
||||||
|
expect(ServiceMacroAssignment::first()->service_id)->toBe($service->id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroyAssignment removes service-level assignment', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$service = Service::factory()->create();
|
||||||
|
$macro = Macro::factory()->create();
|
||||||
|
$assignment = ServiceMacroAssignment::create([
|
||||||
|
'service_id' => $service->id,
|
||||||
|
'part_type' => 'song',
|
||||||
|
'macro_id' => $macro->id,
|
||||||
|
'position' => 'all_slides',
|
||||||
|
'order' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)
|
||||||
|
->deleteJson(route('services.macro-assignments.destroy', [$service, $assignment]));
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
expect(ServiceMacroAssignment::count())->toBe(0);
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue