mivita/tests/Feature/AboUpdateAuthorizationTest.php
Kevin 03416b0afe Audit 18.09.: Zahlungsdatum nur bei Zahlung, Neupartner-IDs im Snapshot, Pausenende folgt dem Termin
F1: partner_paid_at wird beim Rechnungskauf (paymentStatusPaidAction mit
paid=false) nicht mehr gesetzt, sondern erst bei der Rechnungszahlung; der
Backfill nimmt das Aktivierungsdatum nur noch ohne jede
Registrierungsbestellung (offene Rechnung bleibt leer).
F2: Snapshots speichern je Linie new_partner_ids, die Neupartner-Liste zeigt
genau diese; aeltere Snapshots ohne IDs rechnen mit der damaligen Regel
(Registrierungsmonat, scopeNewPartnerInByActiveDate), damit Kachel und Liste
uebereinstimmen. Keine Payload-Versionsaenderung, kein Neuaufbau noetig.
F3: Liefertag-/Termin-Aenderung waehrend einer Pause zieht paused_until mit;
eine Ausfuehrung loescht auch den paused_by_*-Grund.
F4: expiredCount der Firstlines-Seite (Controller, View, Sprachen) zum bereits
committeten Test nachgezogen.
F5: Backfills schreiben ohne updated_at (toBase()->update, timestamps=false).
Kuendigungsbestaetigung abo_cancel_confirm wird im Selfservice serverseitig
verlangt. AboStatusExplainer laedt die Historie je Abo und Aktion nur einmal.

Stellungnahme und Deploy-Folgen im Audit-Dokument, Regressionstests je Befund.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 08:59:45 +00:00

224 lines
6.5 KiB
PHP

<?php
use App\Http\Controllers\Portal\AboController as PortalAboController;
use App\Http\Controllers\User\AboController;
use App\Models\Customer;
use App\Models\ShoppingUser;
use App\Models\UserAbo;
use App\Models\UserAboOrder;
use App\Repositories\AboRepository;
use App\User;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\URL;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
beforeEach(function () {
Carbon::setTestNow(Carbon::parse('2026-07-22'));
URL::resolveMissingNamedRoutesUsing(
fn (string $name, array $parameters) => '/'.$name.'/'.implode('/', $parameters)
);
});
afterEach(function () {
Carbon::setTestNow();
});
function auaMakeUser(): User
{
return User::forceCreate([
'email' => 'aua-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
'm_level' => 1,
'payment_account' => now()->addYear(),
]);
}
function auaMakeAbo(User $user, int $paidDeliveries = 6, string $nextDate = '2026-08-05', ?string $lastPauseDate = null): UserAbo
{
$abo = UserAbo::create([
'user_id' => $user->id,
'member_id' => $user->id,
'shopping_user_id' => 0,
'email' => 'aua-abo-'.uniqid('', true).'@example.com',
'is_for' => 'me',
'payone_userid' => 0,
'clearingtype' => 'cc',
'amount' => 5000,
'active' => true,
'status' => 2,
'abo_interval' => 5,
'start_date' => '2026-01-05',
'last_date' => '2026-07-05',
'next_date' => $nextDate,
'last_pause_date' => $lastPauseDate,
]);
for ($i = 0; $i < $paidDeliveries; $i++) {
UserAboOrder::create([
'user_abo_id' => $abo->id,
'shopping_order_id' => 0,
'status' => 2,
'paid' => true,
]);
}
return $abo->fresh();
}
/**
* Simuliert den POST des Abo-Einstellungs-Modals an AboController@update.
*
* @param array<string, mixed> $payload
*/
function auaCallUpdate(string $routeView, int $routeId, array $payload): mixed
{
app('request')->merge($payload);
return (new AboController(app(AboRepository::class)))->update($routeView, $routeId);
}
it('blockiert das Kündigen eines fremden Abos über eine manipulierte Body-id', function (): void {
$attacker = auaMakeUser();
$victim = auaMakeUser();
$attackerAbo = auaMakeAbo($attacker);
$victimAbo = auaMakeAbo($victim);
$this->actingAs($attacker);
expect(fn () => auaCallUpdate('me', $attackerAbo->id, [
'action' => 'abo_update_settings',
'id' => $victimAbo->id,
'view' => 'admin',
'abo_interval' => 5,
'abo_next_month' => now()->addMonth()->format('Y-m'),
'abo_cancel' => 'true',
]))->toThrow(HttpException::class);
expect((int) $victimAbo->fresh()->status)->toBe(2)
->and($victimAbo->fresh()->active)->toBeTruthy();
});
it('ignoriert ein gespooftes view=admin im Body: Mindestlaufzeit gilt weiterhin', function (): void {
$user = auaMakeUser();
$abo = auaMakeAbo($user, paidDeliveries: 2);
$this->actingAs($user);
auaCallUpdate('me', $abo->id, [
'action' => 'abo_update_settings',
'id' => $abo->id,
'view' => 'admin',
'abo_interval' => 5,
'abo_next_month' => now()->addMonth()->format('Y-m'),
'abo_cancel' => 'true',
]);
expect((int) $abo->fresh()->status)->toBe(2)
->and($abo->fresh()->active)->toBeTruthy();
});
it('ignoriert ein gespooftes view=admin im Body: Pause-Intervall gilt weiterhin', function (): void {
$user = auaMakeUser();
$abo = auaMakeAbo($user, lastPauseDate: '2026-06-01');
$this->actingAs($user);
auaCallUpdate('me', $abo->id, [
'action' => 'abo_update_settings',
'id' => $abo->id,
'view' => 'admin',
'abo_interval' => 5,
'abo_next_month' => now()->addMonth()->format('Y-m'),
'abo_pause' => 'true',
]);
expect($abo->fresh()->getRawOriginal('next_date'))->toStartWith('2026-08-05');
});
it('kündigt das eigene Abo nach erfüllter Mindestlaufzeit weiterhin regulär', function (): void {
$user = auaMakeUser();
$abo = auaMakeAbo($user);
$this->actingAs($user);
auaCallUpdate('me', $abo->id, [
'action' => 'abo_update_settings',
'id' => $abo->id,
'view' => 'me',
'abo_interval' => 5,
'abo_cancel' => 'true',
'abo_cancel_confirm' => '1',
]);
expect((int) $abo->fresh()->status)->toBe(4)
->and($abo->fresh()->active)->toBeFalsy();
});
it('blockiert die Kündigung serverseitig vor erreichter Mindestlaufzeit', function (): void {
$user = auaMakeUser();
$abo = auaMakeAbo($user, paidDeliveries: 2);
$this->actingAs($user);
$repository = new AboRepository;
$repository->setModel($abo);
$repository->update([
'action' => 'abo_update_settings',
'id' => $abo->id,
'view' => 'me',
'abo_interval' => 5,
'abo_cancel' => 'true',
]);
expect((int) $abo->fresh()->status)->toBe(2)
->and($abo->fresh()->active)->toBeTruthy();
});
it('zeigt im Portal nach der Kündigung wieder die Abo-Neuanlage an', function (): void {
$shoppingUser = ShoppingUser::create([
'auth_user_id' => 1,
'billing_firstname' => 'Karla',
'billing_lastname' => 'Kundin',
'billing_address' => 'Kundenweg 2',
'billing_zipcode' => '11111',
'billing_city' => 'Kundenstadt',
'billing_country_id' => 1,
'shipping_country_id' => 1,
'billing_email' => 'aua-kundin@example.com',
'same_as_billing' => true,
'mode' => 'test',
'wp_order_number' => 0,
]);
UserAbo::create([
'user_id' => 1,
'member_id' => 1,
'shopping_user_id' => $shoppingUser->id,
'email' => 'aua-kundin@example.com',
'is_for' => 'ot',
'payone_userid' => 0,
'clearingtype' => 'cc',
'amount' => 5000,
'active' => false,
'status' => 4,
'abo_interval' => 5,
'cancel_date' => now(),
]);
$customer = Customer::create([
'name' => 'Karla Kundin',
'email' => 'aua-kundin@example.com',
'shopping_user_id' => $shoppingUser->id,
]);
$this->actingAs($customer, 'customers');
$response = (new PortalAboController)->myAbo();
expect($response->name())->toBe('portal.abo.my_abo_create');
});