tenant = Tenant::create([ 'slug' => 'wm', 'name' => 'Wilde Möhre', 'email' => 'wm@example.com', 'email_finance' => 'wm-f@example.com', 'url' => parse_url(config('app.url'), PHP_URL_HOST), 'account_name' => 'Wilde Möhre e.V.', 'account_iban' => 'DE00', 'account_bic' => 'XY', 'city' => 'Stadt', 'postcode' => '00000', 'is_active_local_group' => true, 'has_active_instance' => true, ]); app()->instance('tenant', $this->tenant); // `users.user_role_main` und `user_role_local_group` sind Fremdschlüssel auf `user_roles`. foreach ([UserRole::USER_ROLE_ADMIN, UserRole::USER_ROLE_GROUP_LEADER, UserRole::USER_ROLE_USER] as $role) { UserRole::create(['slug' => $role, 'name' => $role]); } DocumentTemplate::create([ 'document_type' => DocumentTemplate::TYPE_PARTICIPANT_INVOICE, 'block' => DocumentTemplate::BLOCK_LAYOUT, 'content' => '
{block:footer}
', 'sort_order' => 10, ]); DocumentTemplate::create([ 'document_type' => DocumentTemplate::TYPE_PARTICIPANT_INVOICE, 'block' => 'footer', 'content' => 'alt', 'sort_order' => 20, ]); DocumentTemplate::create([ 'document_type' => DocumentTemplate::TYPE_PARTICIPANT_INVOICE, 'block' => DocumentTemplate::BLOCK_BODY, 'content' => '{positions_table}', 'sort_order' => 30, 'editable' => false, ]); } private function makeUser(string $mainRole, string $localRole = UserRole::USER_ROLE_USER): User { return User::create([ 'username' => $mainRole . '-' . uniqid() . '@example.com', 'email' => $mainRole . '-' . uniqid() . '@example.com', 'firstname' => 'Test', 'lastname' => 'Person', 'password' => bcrypt('secret'), 'local_group' => $this->tenant->slug, 'user_role_main' => $mainRole, 'user_role_local_group' => $localRole, 'active' => true, ]); } public function test_group_leaders_cannot_reach_the_templates(): void { $this->actingAs($this->makeUser(UserRole::USER_ROLE_USER, UserRole::USER_ROLE_GROUP_LEADER)); $this->get('/admin/document-templates')->assertRedirect('/admin'); $this->getJson('/api/v1/admin/document-templates')->assertRedirect('/admin'); } public function test_main_administrators_can_read_the_templates(): void { $this->actingAs($this->makeUser(UserRole::USER_ROLE_ADMIN)); $response = $this->getJson('/api/v1/admin/document-templates'); $response->assertOk(); $response->assertJsonPath('blocks.1.block', 'footer'); $response->assertJsonPath('blocks.1.editable', true); // Der generierte Block wird angezeigt, aber als nicht editierbar gekennzeichnet. $response->assertJsonPath('blocks.2.editable', false); } public function test_saving_leaves_generated_blocks_untouched(): void { $this->actingAs($this->makeUser(UserRole::USER_ROLE_ADMIN)); $this->postJson('/api/v1/admin/document-templates', [ 'blocks' => [ 'footer' => 'neu', DocumentTemplate::BLOCK_BODY => 'manipuliert', 'gibtesnicht' => 'egal', ], ])->assertOk()->assertJsonPath('status', 'success'); $blocks = DocumentTemplate::forType(DocumentTemplate::TYPE_PARTICIPANT_INVOICE); $this->assertSame('neu', $blocks['footer']->content); $this->assertSame('{positions_table}', $blocks[DocumentTemplate::BLOCK_BODY]->content); } public function test_a_bare_asset_placeholder_in_the_css_is_refused(): void { $this->actingAs($this->makeUser(UserRole::USER_ROLE_ADMIN)); DocumentTemplate::create([ 'document_type' => DocumentTemplate::TYPE_PARTICIPANT_INVOICE, 'block' => DocumentTemplate::BLOCK_STYLE, 'content' => '.a { color: red; }', 'sort_order' => 15, ]); $response = $this->postJson('/api/v1/admin/document-templates', [ 'blocks' => [ DocumentTemplate::BLOCK_STYLE => '.a { color: red; }{asset:logo}', 'footer' => 'neu', ], ]); $response->assertOk()->assertJsonPath('status', 'error'); $this->assertStringContainsString('url("{asset:logo}")', $response->json('message')); // Nichts gespeichert -- auch nicht der unbedenkliche Block. $blocks = DocumentTemplate::forType(DocumentTemplate::TYPE_PARTICIPANT_INVOICE); $this->assertSame('.a { color: red; }', $blocks[DocumentTemplate::BLOCK_STYLE]->content); $this->assertSame('alt', $blocks['footer']->content); } public function test_an_asset_inside_url_is_accepted_in_the_css(): void { $this->actingAs($this->makeUser(UserRole::USER_ROLE_ADMIN)); DocumentTemplate::create([ 'document_type' => DocumentTemplate::TYPE_PARTICIPANT_INVOICE, 'block' => DocumentTemplate::BLOCK_STYLE, 'content' => '', 'sort_order' => 15, ]); $this->postJson('/api/v1/admin/document-templates', [ 'blocks' => [DocumentTemplate::BLOCK_STYLE => '.a { background: url("{asset:logo}"); }'], ])->assertOk()->assertJsonPath('status', 'success'); $this->assertStringContainsString( 'url("{asset:logo}")', DocumentTemplate::where('block', DocumentTemplate::BLOCK_STYLE)->first()->content ); } public function test_preview_renders_unsaved_content(): void { $this->actingAs($this->makeUser(UserRole::USER_ROLE_ADMIN)); $response = $this->post('/api/v1/admin/document-templates/preview', [ 'blocks' => ['footer' => 'noch nicht gespeichert'], ]); $response->assertOk(); $response->assertHeader('Content-Type', 'application/pdf'); // Der gespeicherte Stand bleibt unberührt. $this->assertSame('alt', DocumentTemplate::where('block', 'footer')->first()->content); } public function test_assets_still_used_in_the_template_are_kept(): void { $this->actingAs($this->makeUser(UserRole::USER_ROLE_ADMIN)); DocumentAsset::create(['name' => 'logo', 'mime' => 'image/png', 'data' => 'QUJD']); DocumentTemplate::where('block', 'footer')->update(['content' => '']); $this->deleteJson('/api/v1/admin/document-templates/assets/logo') ->assertOk() ->assertJsonPath('status', 'error'); $this->assertNotNull(DocumentAsset::where('name', 'logo')->first()); } public function test_unused_assets_can_be_deleted(): void { $this->actingAs($this->makeUser(UserRole::USER_ROLE_ADMIN)); DocumentAsset::create(['name' => 'altbestand', 'mime' => 'image/png', 'data' => 'QUJD']); $this->deleteJson('/api/v1/admin/document-templates/assets/altbestand') ->assertOk() ->assertJsonPath('status', 'success'); $this->assertNull(DocumentAsset::where('name', 'altbestand')->first()); } /* |-------------------------------------------------------------------------- | Mehrere Dokumentarten |-------------------------------------------------------------------------- */ public function test_the_default_document_type_is_the_invoice(): void { $this->actingAs($this->makeUser(UserRole::USER_ROLE_ADMIN)); $this->getJson('/api/v1/admin/document-templates') ->assertOk() ->assertJsonPath('documentType', DocumentTemplate::TYPE_PARTICIPANT_INVOICE) ->assertJsonPath('documentTypes.0.value', DocumentTemplate::TYPE_PARTICIPANT_INVOICE) ->assertJsonPath('documentTypes.1.value', DocumentTemplate::TYPE_PARTICIPANT_REFUND); } public function test_the_refund_type_returns_its_own_blocks_and_tokens(): void { $this->actingAs($this->makeUser(UserRole::USER_ROLE_ADMIN)); DocumentTemplate::create([ 'document_type' => DocumentTemplate::TYPE_PARTICIPANT_REFUND, 'block' => 'subject', 'content' => 'Rückerstattung {document_number}', 'sort_order' => 10, ]); $response = $this->getJson('/api/v1/admin/document-templates?type=participant_refund'); $response->assertOk(); $response->assertJsonPath('documentType', DocumentTemplate::TYPE_PARTICIPANT_REFUND); $response->assertJsonCount(1, 'blocks'); $response->assertJsonPath('blocks.0.block', 'subject'); // Die Platzhalter sind die des Belegs, nicht die der Rechnung. $response->assertJsonPath('tokenGroups.refund.tokens.refund_amount.description', 'Erstattungsbetrag'); $response->assertJsonMissingPath('tokenGroups.body.tokens.positions_table'); } public function test_saving_writes_only_into_the_selected_type(): void { $this->actingAs($this->makeUser(UserRole::USER_ROLE_ADMIN)); DocumentTemplate::create([ 'document_type' => DocumentTemplate::TYPE_PARTICIPANT_REFUND, 'block' => 'footer', 'content' => 'Beleg-Fuß', 'sort_order' => 20, ]); $this->postJson('/api/v1/admin/document-templates', [ 'type' => DocumentTemplate::TYPE_PARTICIPANT_REFUND, 'blocks' => ['footer' => 'neuer Beleg-Fuß'], ])->assertOk()->assertJsonPath('status', 'success'); $this->assertSame('neuer Beleg-Fuß', DocumentTemplate::where('document_type', DocumentTemplate::TYPE_PARTICIPANT_REFUND) ->where('block', 'footer')->first()->content); // Der gleichnamige Block der Rechnung bleibt unberührt. $this->assertSame('alt', DocumentTemplate::where('document_type', DocumentTemplate::TYPE_PARTICIPANT_INVOICE) ->where('block', 'footer')->first()->content); } public function test_an_unknown_document_type_is_refused(): void { $this->actingAs($this->makeUser(UserRole::USER_ROLE_ADMIN)); $this->getJson('/api/v1/admin/document-templates?type=erfunden')->assertStatus(422); $this->postJson('/api/v1/admin/document-templates', [ 'type' => 'erfunden', 'blocks' => ['footer' => 'egal'], ])->assertStatus(422); $this->post('/api/v1/admin/document-templates/preview', [ 'type' => 'erfunden', 'blocks' => [], ])->assertStatus(422); // Die Rechnungsvorlage darf davon nichts abbekommen haben. $this->assertSame('alt', DocumentTemplate::where('document_type', DocumentTemplate::TYPE_PARTICIPANT_INVOICE) ->where('block', 'footer')->first()->content); } public function test_preview_renders_the_refund_with_its_sample_data(): void { $this->actingAs($this->makeUser(UserRole::USER_ROLE_ADMIN)); DocumentTemplate::create([ 'document_type' => DocumentTemplate::TYPE_PARTICIPANT_REFUND, 'block' => DocumentTemplate::BLOCK_LAYOUT, 'content' => '
{document_number}
', 'sort_order' => 10, ]); $response = $this->post('/api/v1/admin/document-templates/preview', [ 'type' => DocumentTemplate::TYPE_PARTICIPANT_REFUND, 'blocks' => [], ]); $response->assertOk(); $response->assertHeader('Content-Type', 'application/pdf'); $this->assertStringStartsWith('%PDF', $response->getContent()); } }