'/'.$name.'/'.implode('/', $parameters) ); } function linkCcAbo(string $isFor = 'ot'): UserAbo { $shoppingUser = ShoppingUser::create([ 'auth_user_id' => 1, 'billing_firstname' => 'Karl', 'billing_lastname' => 'Kunde', 'billing_address' => 'Weg 1', 'billing_zipcode' => '11111', 'billing_city' => 'Stadt', 'billing_country_id' => 1, 'shipping_country_id' => 1, 'billing_email' => 'kunde@example.com', 'same_as_billing' => true, 'mode' => 'test', 'wp_order_number' => 0, ]); return UserAbo::create([ 'user_id' => 1, 'member_id' => 1, 'shopping_user_id' => $shoppingUser->id, 'email' => 'kunde@example.com', 'is_for' => $isFor, 'clearingtype' => 'cc', 'wallettype' => '', 'payone_userid' => 111, 'carddata' => ['pseudocardpan' => 'OLD', 'truncatedcardpan' => '411111XXXXXX0000', 'cardexpiredate' => '2501'], 'abo_interval' => 5, 'status' => 2, ]); } // --- Model / token hashing --- it('stores only the SHA-256 hash of the token, never the plaintext', function () { $abo = linkCcAbo(); [$link, $token] = AboCardUpdateLink::generateFor($abo, 7); expect($link->token_hash)->toBe(hash('sha256', $token)) ->and($link->token_hash)->not->toBe($token) ->and($link->created_by_user_id)->toBe(7) ->and($link->isRedeemable())->toBeTrue(); // Plaintext-Token darf nirgends in der DB stehen. expect(AboCardUpdateLink::where('token_hash', $token)->exists())->toBeFalse(); }); it('treats expired and used links as not redeemable', function () { $abo = linkCcAbo(); [$expired] = AboCardUpdateLink::generateFor($abo); $expired->forceFill(['expires_at' => now()->subHour()])->save(); expect($expired->fresh()->isRedeemable())->toBeFalse(); [$used] = AboCardUpdateLink::generateFor($abo); $used->forceFill(['used_at' => now()])->save(); expect($used->fresh()->isRedeemable())->toBeFalse(); }); // --- Login-free controller --- it('returns 410 Gone for an unknown, expired or used token', function () { stubLinkRoutes(); expect(fn () => (new AboCardUpdateController)->show('does-not-exist')) ->toThrow(HttpException::class); }); it('applies the new card and marks the link used on a successful submission', function () { stubLinkRoutes(); $abo = linkCcAbo(); [$link, $token] = AboCardUpdateLink::generateFor($abo); $ct = ['Content-Type' => 'text/plain;charset=UTF-8']; $mock = new MockHandler([ new Response(200, $ct, "status=VALID\npseudocardpan=NEW_LINK\ncardexpiredate=3012\ntruncatedcardpan=555555XXXXXX9999"), new Response(200, $ct, "status=APPROVED\ntxid=TX1\nuserid=999"), new Response(200, $ct, 'status=APPROVED'), // release capture amount=0 ]); $client = new Client(['handler' => HandlerStack::create($mock)]); $service = new AboPaymentMethodService(new PayonePaymentService( defaults: ['mid' => '1', 'aid' => '1', 'portalid' => '1', 'key' => 'abc', 'api_version' => '3.11'], client: $client, )); $request = UpdateAboCreditCardRequest::create('/abo/karte/'.$token, 'POST', [ 'cc_cardholder_first' => 'Karl', 'cc_cardholder_last' => 'Kunde', 'cc_cardpan' => '4111111111111111', 'cc_cardexpiremonth' => '12', 'cc_cardexpireyear' => '2030', 'cc_cardtype' => 'V', 'cc_cardcvc2' => '123', ]); $request->setContainer(app()); $request->validateResolved(); (new AboCardUpdateController)->store($request, $token, $service); $abo->refresh(); expect($abo->carddata['pseudocardpan'])->toBe('NEW_LINK') ->and($abo->payone_userid)->toEqual(999) ->and($abo->card_changes()->first()->source)->toBe('link') ->and($link->fresh()->used_at)->not->toBeNull(); }); // --- CRM: consultant sends link --- it('lets a consultant send a card update link for a customer abo', function () { stubLinkRoutes(); Mail::fake(); $berater = User::forceCreate(['email' => 'b-'.uniqid().'@example.com', 'password' => bcrypt('x'), 'lang' => 'de']); $abo = linkCcAbo('ot'); $abo->forceFill(['user_id' => $berater->id, 'member_id' => $berater->id])->save(); $this->actingAs($berater); (new AboController(app(AboRepository::class)))->sendCardUpdateLink('ot', $abo->id); Mail::assertSent(AboCardUpdateLinkMail::class); expect(AboCardUpdateLink::where('user_abo_id', $abo->id)->exists())->toBeTrue(); }); it('does not send a link for the consultants own abo (me)', function () { stubLinkRoutes(); Mail::fake(); $berater = User::forceCreate(['email' => 'b-'.uniqid().'@example.com', 'password' => bcrypt('x'), 'lang' => 'de']); $abo = linkCcAbo('me'); $abo->forceFill(['user_id' => $berater->id, 'member_id' => $berater->id])->save(); $this->actingAs($berater); (new AboController(app(AboRepository::class)))->sendCardUpdateLink('me', $abo->id); Mail::assertNothingSent(); expect(AboCardUpdateLink::where('user_abo_id', $abo->id)->exists())->toBeFalse(); });