Newsletter-Listen: Empfaengerlisten filtern, speichern und exportieren
Admin-Bereich "Newsletter-Listen" (Plan dev/_tickets/entwicklungsplan- newsletter-listen.md): Berater und Kunden nach Filtern zusammenstellen, als Liste speichern und wieder laden, Export als XLSX/CSV. Jeder Export wird protokolliert (newsletter_list_exports). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ef7e333bd4
commit
d21d94a060
26 changed files with 3664 additions and 0 deletions
153
tests/Feature/Newsletter/NewsletterListControllerTest.php
Normal file
153
tests/Feature/Newsletter/NewsletterListControllerTest.php
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
use App\Exports\xExport;
|
||||
use App\Http\Controllers\Admin\NewsletterListController;
|
||||
use App\Http\Requests\Newsletter\NewsletterListFilterRequest;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Services\Newsletter\NewsletterListService;
|
||||
use App\User;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Routing\Redirector;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
function newsletterAdmin(int $admin = 2): User
|
||||
{
|
||||
return User::forceCreate([
|
||||
'email' => 'admin-'.uniqid('', true).'@example.com',
|
||||
'password' => bcrypt('secret'),
|
||||
'admin' => $admin,
|
||||
'lang' => 'de',
|
||||
'active' => 1,
|
||||
'blocked' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
*/
|
||||
function newsletterRequest(User $user, array $params = []): NewsletterListFilterRequest
|
||||
{
|
||||
$request = NewsletterListFilterRequest::create('/admin/newsletter-lists/datatable', 'GET', $params);
|
||||
$request->setUserResolver(fn () => $user);
|
||||
$request->setContainer(app())->setRedirector(app(Redirector::class));
|
||||
$request->validateResolved();
|
||||
app()->instance('request', $request);
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
function newsletterController(): NewsletterListController
|
||||
{
|
||||
return new NewsletterListController(app(NewsletterListService::class));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attributes
|
||||
*/
|
||||
function newsletterListCustomer(array $attributes = []): ShoppingUser
|
||||
{
|
||||
return ShoppingUser::forceCreate(array_merge([
|
||||
'auth_user_id' => 0,
|
||||
'billing_email' => 'kunde-'.uniqid('', true).'@example.com',
|
||||
'billing_firstname' => 'Erika',
|
||||
'billing_lastname' => 'Muster',
|
||||
'billing_salutation' => 'ms',
|
||||
'billing_address' => 'Musterweg 1',
|
||||
'billing_zipcode' => '12345',
|
||||
'billing_city' => 'Musterstadt',
|
||||
'billing_country_id' => 1,
|
||||
'shipping_country_id' => 1,
|
||||
'language' => 'de',
|
||||
'subscribed' => 1,
|
||||
'has_buyed' => 1,
|
||||
'faker_mail' => 0,
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
it('verweigert Nicht-Admins den Zugriff auf die Filter-Anfrage', function () {
|
||||
$this->actingAs(newsletterAdmin(0));
|
||||
|
||||
newsletterRequest(auth()->user(), ['source' => 'customers']);
|
||||
})->throws(AuthorizationException::class);
|
||||
|
||||
it('lehnt ungültige Filterwerte ab', function () {
|
||||
$this->actingAs(newsletterAdmin());
|
||||
|
||||
newsletterRequest(auth()->user(), ['source' => 'alle', 'created_from' => '2026-02-01', 'created_to' => '2026-01-01']);
|
||||
})->throws(ValidationException::class);
|
||||
|
||||
it('liefert die Vorschau als Datatable-JSON mit Kennzahlen', function () {
|
||||
$admin = newsletterAdmin();
|
||||
$this->actingAs($admin);
|
||||
newsletterListCustomer(['billing_email' => 'a@example.com']);
|
||||
newsletterListCustomer(['billing_email' => 'A@example.com']);
|
||||
newsletterListCustomer(['billing_email' => 'b@example.com', 'subscribed' => 0]);
|
||||
|
||||
$request = newsletterRequest($admin, ['source' => 'customers', 'draw' => 1, 'start' => 0, 'length' => 50]);
|
||||
$json = newsletterController()->datatable($request)->getData(true);
|
||||
|
||||
expect($json['recordsTotal'])->toBe(2)
|
||||
->and($json['stats']['total'])->toBe(2)
|
||||
->and($json['stats']['duplicates'])->toBe(1)
|
||||
->and(collect($json['data'])->pluck('email')->all())->toBe(['a@example.com', 'b@example.com'])
|
||||
->and($json['data'][0]['name'])->toBe('Erika Muster')
|
||||
->and($json['data'][0]['optin_label'])->toContain('badge-success')
|
||||
->and($json['data'][1]['optin_label'])->toContain('badge-danger')
|
||||
->and(get_user_attr('newsletter_list_filter')['source'])->toBe('customers');
|
||||
});
|
||||
|
||||
it('exportiert die Liste als CSV mit BOM, Semikolon und Injection-Schutz', function () {
|
||||
$admin = newsletterAdmin();
|
||||
$this->actingAs($admin);
|
||||
newsletterListCustomer(['billing_email' => 'csv@example.com', 'billing_firstname' => '=SUMME(A1)']);
|
||||
|
||||
$response = newsletterController()->export(newsletterRequest($admin, ['source' => 'customers', 'template_only' => 1]), 'csv');
|
||||
|
||||
expect($response)->toBeInstanceOf(StreamedResponse::class);
|
||||
ob_start();
|
||||
$response->sendContent();
|
||||
$content = (string) ob_get_clean();
|
||||
$lines = explode("\n", trim($content));
|
||||
|
||||
expect($content)->toStartWith("\xEF\xBB\xBF")
|
||||
->and($lines[0])->toBe("\xEF\xBB\xBF".'Emailadresse;Vorname;Nachname;Anrede;Straße;PLZ;Wohnort;Firma;Berater')
|
||||
->and($lines[1])->toBe("csv@example.com;'=SUMME(A1);Muster;Frau;\"Musterweg 1\";12345;Musterstadt;;")
|
||||
->and($response->headers->get('content-disposition'))->toContain('newsletter_customers_de_'.now()->format('Y-m-d').'.csv');
|
||||
});
|
||||
|
||||
it('exportiert die Liste als XLSX über die Export-Klasse', function () {
|
||||
Excel::fake();
|
||||
$admin = newsletterAdmin();
|
||||
$this->actingAs($admin);
|
||||
newsletterListCustomer(['billing_email' => 'xlsx@example.com']);
|
||||
|
||||
newsletterController()->export(newsletterRequest($admin, ['source' => 'customers', 'language' => '']), 'xlsx');
|
||||
|
||||
Excel::assertDownloaded('newsletter_customers_alle_'.now()->format('Y-m-d').'.xlsx', function (xExport $export) {
|
||||
$rows = $export->collection();
|
||||
|
||||
return $export->headings()[0][0] === 'Emailadresse'
|
||||
&& count($export->headings()[0]) === 17
|
||||
&& $rows->count() === 1
|
||||
&& $rows->first()[0] === 'xlsx@example.com';
|
||||
});
|
||||
});
|
||||
|
||||
it('zeigt die Index-Seite mit gemerktem Filter', function () {
|
||||
$admin = newsletterAdmin();
|
||||
$this->actingAs($admin);
|
||||
set_user_attr('newsletter_list_filter', ['source' => 'both', 'language' => 'es', 'optin' => 'subscribed']);
|
||||
|
||||
$view = newsletterController()->index(newsletterRequest($admin));
|
||||
|
||||
expect($view->getName())->toBe('admin.newsletter.index')
|
||||
->and($view->getData()['filter']['source'])->toBe('both')
|
||||
->and($view->getData()['filter']['language'])->toBe('es')
|
||||
->and($view->getData()['filter']['optin'])->toBe('subscribed');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue