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>
153 lines
5.9 KiB
PHP
153 lines
5.9 KiB
PHP
<?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');
|
|
});
|