Files
autocrm/tests/Feature/ShopPasswordResetTest.php
T
Vasyka 3da1f5412a feat: shop UX polish — password reset / order email / multi-image / customer admin
Shop password reset:
- Configured 'shop_customers' password broker on the existing
  password_reset_tokens table
- ShopCustomer::sendPasswordResetNotification overrides Laravel default to
  send a ShopPasswordResetMail with a tenant-subdomain reset URL
- Routes /shop/password/forgot, /shop/password/email, /shop/password/reset/{token}
  + ShopAuthController showForgotPassword/sendResetLink/showResetPassword/
  resetPassword. Forgot view stays generic ("if it exists, we sent…") to avoid
  email enumeration. Login view links to "Am uitat parola".

Order confirmation email:
- ShopOrderConfirmationMail + nicely formatted HTML email template
- ShopOrderNotifier::placed now also emails customer_email (best-effort,
  warning-only logged on failure) alongside existing Telegram + staff push

Multiple images per Part:
- Part media collection switched from singleFile to multiple (max 8 in form)
- imageUrls() helper for galleries; imageUrl() still returns first for cards
- PartResource form: reorderable multi-upload
- Shop part detail: vertical thumbnails switch the main image via vanilla JS

ShopCustomerResource (tenant Filament, "Magazin" nav group):
- List with name/phone/email/client_id/orders_count/last_login_at
- Edit (no password field exposed)
- "Trimite reset parolă" action uses the new broker
- OrdersRelationManager shows the customer's orders read-only

Tests (7 new):
- forgot sends mail; forgot doesn't disclose unknown email; reset with valid
  token changes password; bad token rejected; order email when customer_email
  set; email skipped without it; Part has imageUrls() collection

Full suite: 130 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 06:14:45 +00:00

144 lines
4.9 KiB
PHP

<?php
namespace Tests\Feature;
use App\Mail\ShopOrderConfirmationMail;
use App\Mail\ShopPasswordResetMail;
use App\Models\Central\Company;
use App\Models\Central\Plan;
use App\Models\Tenant\OnlineOrder;
use App\Models\Tenant\ShopCustomer;
use App\Services\Notifications\ShopOrderNotifier;
use App\Services\Notifications\TelegramService;
use App\Services\Notifications\WebPushService;
use App\Tenancy\TenantManager;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Password;
use Tests\TestCase;
class ShopPasswordResetTest extends TestCase
{
use RefreshDatabase;
public function test_forgot_password_sends_reset_mail(): void
{
$this->makeShop('pr');
ShopCustomer::create([
'name' => 'X', 'phone' => '+37377000001',
'email' => 'x@example.com', 'password' => Hash::make('old'),
]);
Mail::fake();
$this->post('http://pr.service.mir.md/shop/password/email', [
'email' => 'x@example.com',
])->assertSessionHas('status');
Mail::assertSent(ShopPasswordResetMail::class, fn ($m) => $m->customer->email === 'x@example.com');
}
public function test_forgot_does_not_disclose_unknown_email(): void
{
$this->makeShop('pru');
Mail::fake();
$this->post('http://pru.service.mir.md/shop/password/email', [
'email' => 'ghost@example.com',
])->assertSessionHas('status'); // same generic status, no error
Mail::assertNothingSent();
}
public function test_reset_with_valid_token_changes_password(): void
{
$this->makeShop('rs');
$cust = ShopCustomer::create([
'name' => 'R', 'phone' => '+37377000002',
'email' => 'r@example.com', 'password' => Hash::make('old'),
]);
$token = Password::broker('shop_customers')->createToken($cust);
$this->post('http://rs.service.mir.md/shop/password/reset', [
'token' => $token,
'email' => 'r@example.com',
'password' => 'newpassword',
'password_confirmation' => 'newpassword',
])->assertRedirect('/shop/login');
$cust->refresh();
$this->assertTrue(Hash::check('newpassword', $cust->password));
}
public function test_reset_with_bad_token_rejected(): void
{
$this->makeShop('bad');
ShopCustomer::create([
'name' => 'B', 'phone' => '+37377000003',
'email' => 'b@example.com', 'password' => Hash::make('old'),
]);
$this->post('http://bad.service.mir.md/shop/password/reset', [
'token' => 'not-a-real-token',
'email' => 'b@example.com',
'password' => 'newpassword',
'password_confirmation' => 'newpassword',
])->assertSessionHasErrors();
}
public function test_order_notifier_sends_email_when_customer_email_present(): void
{
$ctx = $this->makeShop('mail');
Mail::fake();
$order = OnlineOrder::create([
'number' => OnlineOrder::generateNumber($ctx->id),
'customer_name' => 'M', 'customer_phone' => '+37377000004',
'customer_email' => 'm@example.com',
'delivery_method' => 'pickup', 'status' => 'new',
]);
app(ShopOrderNotifier::class)->placed($order);
Mail::assertSent(ShopOrderConfirmationMail::class, fn ($m) => $m->order->id === $order->id);
}
public function test_order_notifier_skips_email_without_customer_email(): void
{
$ctx = $this->makeShop('noeml');
Mail::fake();
$order = OnlineOrder::create([
'number' => OnlineOrder::generateNumber($ctx->id),
'customer_name' => 'N', 'customer_phone' => '+37377000005',
'delivery_method' => 'pickup', 'status' => 'new',
]);
app(ShopOrderNotifier::class)->placed($order);
Mail::assertNotSent(ShopOrderConfirmationMail::class);
}
public function test_part_has_multiple_images_collection(): void
{
$this->makeShop('multi');
$part = \App\Models\Tenant\Part::create([
'name' => 'P', 'sell_price' => 10, 'qty' => 1,
'unit' => 'buc', 'is_active' => true, 'buy_price' => 5,
]);
$this->assertIsArray($part->imageUrls());
$this->assertCount(0, $part->imageUrls());
}
private function makeShop(string $slug): Company
{
$plan = Plan::firstOrCreate(['slug' => 'test'], ['name' => 'T', 'price' => 0, 'features' => []]);
$company = Company::create([
'plan_id' => $plan->id, 'slug' => $slug,
'name' => ucfirst($slug), 'status' => 'active',
'settings' => ['shop' => ['enabled' => true, 'delivery_methods' => ['pickup']]],
]);
app(TenantManager::class)->setCurrent($company);
return $company;
}
}