diff --git a/tests/Actions/SendOrderDetailsTest.php b/tests/Actions/SendOrderDetailsTest.php new file mode 100644 index 00000000..c6f437cc --- /dev/null +++ b/tests/Actions/SendOrderDetailsTest.php @@ -0,0 +1,62 @@ +action = new SendOrderDetails(); + } + + public function test_action_sends_order_details_notification(): void + { + Notification::fake(); + + $order = Order::factory()->create(); + + Product::factory()->count(2)->create()->each(function ($product) use ($order) { + $order->items()->create([ + 'buyable_id' => $product->id, + 'buyable_type' => Product::class, + 'quantity' => 2, + 'price' => $product->price, + 'name' => $product->name, + ]); + }); + + $request = Request::create('/'); + $models = new Collection([$order]); + + $this->action->handle($request, $models); + + $this->assertTrue(true); + } + + public function test_action_handles_multiple_orders(): void + { + Notification::fake(); + + $orders = Order::factory()->count(3)->create(); + + $request = Request::create('/'); + + $this->action->handle($request, $orders); + + $this->assertTrue(true); + } +} diff --git a/tests/Cart/CookieDriverTest.php b/tests/Cart/CookieDriverTest.php new file mode 100644 index 00000000..e5f0b146 --- /dev/null +++ b/tests/Cart/CookieDriverTest.php @@ -0,0 +1,53 @@ +driver = new CookieDriver($this->app['config']->get('bazar.cart.drivers.cookie', [])); + } + + public function test_cookie_driver_resolves_cart_from_cookie(): void + { + $cart = Cart::factory()->create(); + + $this->app['request']->cookies->set('cart_id', $cart->id); + + $resolvedCart = $this->driver->getModel(); + + $this->assertInstanceOf(Cart::class, $resolvedCart); + $this->assertSame($cart->id, $resolvedCart->id); + } + + public function test_cookie_driver_creates_new_cart_when_no_cookie(): void + { + $cart = $this->driver->getModel(); + + $this->assertInstanceOf(Cart::class, $cart); + $this->assertTrue($cart->exists); + } + + public function test_cookie_driver_queues_cookie_after_resolution(): void + { + $cart = $this->driver->getModel(); + + $queuedCookies = Cookie::getQueuedCookies(); + + $this->assertNotEmpty($queuedCookies); + $this->assertSame('cart_id', $queuedCookies[0]->getName()); + $this->assertSame($cart->getKey(), $queuedCookies[0]->getValue()); + } +} diff --git a/tests/Cart/NullDriverTest.php b/tests/Cart/NullDriverTest.php new file mode 100644 index 00000000..a20ef013 --- /dev/null +++ b/tests/Cart/NullDriverTest.php @@ -0,0 +1,50 @@ +driver = new NullDriver([]); + } + + public function test_null_driver_resolves_new_cart_instance(): void + { + $cart = $this->driver->getModel(); + + $this->assertInstanceOf(Cart::class, $cart); + $this->assertFalse($cart->exists); + } + + public function test_null_driver_associates_cart_with_authenticated_user(): void + { + $user = User::factory()->create(); + + $this->actingAs($user); + + $cart = $this->driver->getModel(); + + $this->assertInstanceOf(Cart::class, $cart); + $this->assertSame($user->id, $cart->user_id); + } + + public function test_null_driver_creates_cart_without_user_when_not_authenticated(): void + { + $cart = $this->driver->getModel(); + + $this->assertInstanceOf(Cart::class, $cart); + $this->assertNull($cart->user_id); + } +} diff --git a/tests/Cart/SessionDriverTest.php b/tests/Cart/SessionDriverTest.php new file mode 100644 index 00000000..2ef6e912 --- /dev/null +++ b/tests/Cart/SessionDriverTest.php @@ -0,0 +1,48 @@ +driver = new SessionDriver($this->app['config']->get('bazar.cart.drivers.session', [])); + } + + public function test_session_driver_resolves_cart_from_session(): void + { + $cart = Cart::factory()->create(); + + $this->app['request']->session()->put('cart_id', $cart->id); + + $resolvedCart = $this->driver->getModel(); + + $this->assertInstanceOf(Cart::class, $resolvedCart); + $this->assertSame($cart->id, $resolvedCart->id); + } + + public function test_session_driver_creates_new_cart_when_no_session(): void + { + $cart = $this->driver->getModel(); + + $this->assertInstanceOf(Cart::class, $cart); + $this->assertTrue($cart->exists); + } + + public function test_session_driver_stores_cart_id_in_session_after_resolution(): void + { + $cart = $this->driver->getModel(); + + $this->assertSame($cart->getKey(), $this->app['request']->session()->get('cart_id')); + } +} diff --git a/tests/Gateway/CashDriverTest.php b/tests/Gateway/CashDriverTest.php new file mode 100644 index 00000000..f516d6b4 --- /dev/null +++ b/tests/Gateway/CashDriverTest.php @@ -0,0 +1,66 @@ +driver = new CashDriver(['enabled' => true]); + + $this->order = Order::factory()->create(); + + Product::factory()->count(2)->create()->each(function ($product) { + $this->order->items()->create([ + 'buyable_id' => $product->id, + 'buyable_type' => Product::class, + 'quantity' => 2, + 'price' => $product->price, + 'name' => $product->name, + ]); + }); + } + + public function test_cash_driver_has_correct_name(): void + { + $this->assertSame('Cash', $this->driver->getName()); + } + + public function test_cash_driver_can_pay(): void + { + $transaction = $this->driver->pay($this->order, 100); + + $this->assertEquals(100, $transaction->amount); + $this->assertTrue($transaction->completed()); + } + + public function test_cash_driver_can_refund(): void + { + $this->driver->pay($this->order); + + $transaction = $this->driver->refund($this->order, 50); + + $this->assertEquals(50, $transaction->amount); + $this->assertTrue($transaction->completed()); + } + + public function test_cash_driver_marks_transactions_as_completed(): void + { + $transaction = $this->driver->pay($this->order); + + $this->assertNotNull($transaction->completed_at); + } +} diff --git a/tests/Gateway/ManualDriverTest.php b/tests/Gateway/ManualDriverTest.php new file mode 100644 index 00000000..d8ad5bf5 --- /dev/null +++ b/tests/Gateway/ManualDriverTest.php @@ -0,0 +1,68 @@ +driver = new ManualDriver(['enabled' => true]); + + $this->order = Order::factory()->create(); + + Product::factory()->count(2)->create()->each(function ($product) { + $this->order->items()->create([ + 'buyable_id' => $product->id, + 'buyable_type' => Product::class, + 'quantity' => 2, + 'price' => $product->price, + 'name' => $product->name, + ]); + }); + } + + public function test_manual_driver_has_correct_name(): void + { + $this->assertSame('Manual', $this->driver->getName()); + } + + public function test_manual_driver_can_pay(): void + { + $transaction = $this->driver->pay($this->order, 100); + + $this->assertEquals(100, $transaction->amount); + $this->assertTrue($transaction->completed()); + } + + public function test_manual_driver_can_refund(): void + { + $this->driver->pay($this->order); + + $transaction = $this->driver->refund($this->order, 50); + + $this->assertEquals(50, $transaction->amount); + $this->assertTrue($transaction->completed()); + } + + public function test_manual_driver_throws_exception_on_notification(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('This payment gateway does not support payment notifications.'); + + $this->driver->handleNotification($this->app['request'], $this->order); + } +} diff --git a/tests/Gateway/TransferDriverTest.php b/tests/Gateway/TransferDriverTest.php new file mode 100644 index 00000000..869295b9 --- /dev/null +++ b/tests/Gateway/TransferDriverTest.php @@ -0,0 +1,77 @@ +driver = new TransferDriver(['enabled' => true]); + + $this->order = Order::factory()->create(); + + Product::factory()->count(2)->create()->each(function ($product) { + $this->order->items()->create([ + 'buyable_id' => $product->id, + 'buyable_type' => Product::class, + 'quantity' => 2, + 'price' => $product->price, + 'name' => $product->name, + ]); + }); + } + + public function test_transfer_driver_has_correct_name(): void + { + $this->assertSame('Transfer', $this->driver->getName()); + } + + public function test_transfer_driver_can_pay(): void + { + $transaction = $this->driver->pay($this->order, 100, ['completed_at' => Date::now()]); + + $this->assertEquals(100, $transaction->amount); + $this->assertTrue($transaction->completed()); + } + + public function test_transfer_driver_can_refund(): void + { + $this->driver->pay($this->order, null, ['completed_at' => Date::now()]); + + $transaction = $this->driver->refund($this->order, 50, ['completed_at' => Date::now()]); + + $this->assertEquals(50, $transaction->amount); + $this->assertTrue($transaction->completed()); + } + + public function test_transfer_driver_throws_exception_on_notification(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('This payment gateway does not support payment notifications.'); + + $this->driver->handleNotification($this->app['request'], $this->order); + } + + public function test_transfer_driver_supports_pending_payments(): void + { + $transaction = $this->driver->pay($this->order, 100); + + $this->assertEquals(100, $transaction->amount); + $this->assertTrue($transaction->pending()); + } +} diff --git a/tests/Http/Controllers/ControllerTest.php b/tests/Http/Controllers/ControllerTest.php new file mode 100644 index 00000000..3e13f501 --- /dev/null +++ b/tests/Http/Controllers/ControllerTest.php @@ -0,0 +1,30 @@ +assertInstanceOf(\Illuminate\Routing\Controller::class, $controller); + } + + public function test_controller_can_be_instantiated(): void + { + $controller = new ConcreteController(); + + $this->assertInstanceOf(Controller::class, $controller); + } +} + +class ConcreteController extends Controller +{ + // +} diff --git a/tests/Http/Controllers/GatewayControllerTest.php b/tests/Http/Controllers/GatewayControllerTest.php new file mode 100644 index 00000000..be97723e --- /dev/null +++ b/tests/Http/Controllers/GatewayControllerTest.php @@ -0,0 +1,118 @@ +controller = new GatewayController(); + + $this->order = Order::factory()->create(); + + Product::factory()->count(2)->create()->each(function ($product) { + $this->order->items()->create([ + 'buyable_id' => $product->id, + 'buyable_type' => Product::class, + 'quantity' => 2, + 'price' => $product->price, + 'name' => $product->name, + ]); + }); + } + + public function test_controller_handles_capture_request(): void + { + $request = Request::create('/gateway/cash/capture', 'POST', [ + 'order_id' => $this->order->id, + ]); + + $this->mock(CashDriver::class, function ($mock) { + $mock->shouldReceive('resolveOrderForCapture') + ->once() + ->andReturn($this->order); + + $mock->shouldReceive('handleCapture') + ->once() + ->andReturn(new Response(['status' => 'success'])); + }); + + Gateway::shouldReceive('driver') + ->with('cash') + ->andReturn($this->app->make(CashDriver::class)); + + $response = $this->controller->capture($request, 'cash'); + + $this->assertEquals(200, $response->getStatusCode()); + } + + public function test_controller_handles_invalid_capture_request(): void + { + $request = Request::create('/gateway/invalid/capture', 'POST'); + + Gateway::shouldReceive('driver') + ->with('invalid') + ->andThrow(new \Exception('Invalid driver')); + + $response = $this->controller->capture($request, 'invalid'); + + $this->assertEquals(400, $response->getStatusCode()); + $this->assertStringContainsString('Invalid request', $response->getContent()); + } + + public function test_controller_handles_notification_request(): void + { + $request = Request::create('/gateway/cash/notification', 'POST', [ + 'order_id' => $this->order->id, + ]); + + $this->mock(CashDriver::class, function ($mock) { + $mock->shouldReceive('resolveOrderForNotification') + ->once() + ->andReturn($this->order); + + $mock->shouldReceive('handleNotification') + ->once() + ->andReturn(new Response(['status' => 'success'])); + }); + + Gateway::shouldReceive('driver') + ->with('cash') + ->andReturn($this->app->make(CashDriver::class)); + + $response = $this->controller->notification($request, 'cash'); + + $this->assertEquals(200, $response->getStatusCode()); + } + + public function test_controller_handles_invalid_notification_request(): void + { + $request = Request::create('/gateway/invalid/notification', 'POST'); + + Gateway::shouldReceive('driver') + ->with('invalid') + ->andThrow(new \Exception('Invalid driver')); + + $response = $this->controller->notification($request, 'invalid'); + + $this->assertEquals(400, $response->getStatusCode()); + $this->assertStringContainsString('Invalid request', $response->getContent()); + } +} diff --git a/tests/Listeners/ClearCookiesTest.php b/tests/Listeners/ClearCookiesTest.php new file mode 100644 index 00000000..7c240847 --- /dev/null +++ b/tests/Listeners/ClearCookiesTest.php @@ -0,0 +1,55 @@ +listener = new ClearCookies(); + $this->user = User::factory()->create(); + } + + public function test_listener_clears_cart_cookie_on_logout(): void + { + Cookie::queue('cart_id', 'test-cart-id', 864000); + + $this->assertNotEmpty(Cookie::getQueuedCookies()); + + $event = new Logout('web', $this->user); + + $this->listener->handle($event); + + $queuedCookies = Cookie::getQueuedCookies(); + + $this->assertNotEmpty($queuedCookies); + + $cartCookie = collect($queuedCookies)->firstWhere('name', 'cart_id'); + + $this->assertNotNull($cartCookie); + $this->assertTrue(time() > $cartCookie->getExpiresTime()); + } + + public function test_listener_handles_logout_event(): void + { + $event = new Logout('web', $this->user); + + $this->listener->handle($event); + + $this->assertTrue(true); + } +} diff --git a/tests/Listeners/FormatBazarStubsTest.php b/tests/Listeners/FormatBazarStubsTest.php new file mode 100644 index 00000000..d3565d89 --- /dev/null +++ b/tests/Listeners/FormatBazarStubsTest.php @@ -0,0 +1,55 @@ +listener = new FormatBazarStubs(); + } + + public function test_listener_formats_bazar_stubs(): void + { + $tempFile = tempnam(sys_get_temp_dir(), 'stub_'); + file_put_contents($tempFile, 'namespace {{ namespace }};'); + + $event = new VendorTagPublished('bazar-stubs', [$tempFile => $tempFile]); + + $this->listener->handle($event); + + $contents = file_get_contents($tempFile); + + $this->assertStringNotContainsString('{{ namespace }}', $contents); + $this->assertStringContainsString($this->app->getNamespace(), $contents); + + unlink($tempFile); + } + + public function test_listener_ignores_non_bazar_stubs_tags(): void + { + $tempFile = tempnam(sys_get_temp_dir(), 'stub_'); + file_put_contents($tempFile, 'namespace {{ namespace }};'); + + $event = new VendorTagPublished('other-tag', [$tempFile => $tempFile]); + + $this->listener->handle($event); + + $contents = file_get_contents($tempFile); + + $this->assertStringContainsString('{{ namespace }}', $contents); + + unlink($tempFile); + } +} diff --git a/tests/Listeners/RefreshInventoryTest.php b/tests/Listeners/RefreshInventoryTest.php new file mode 100644 index 00000000..2eeb46f7 --- /dev/null +++ b/tests/Listeners/RefreshInventoryTest.php @@ -0,0 +1,73 @@ +listener = new RefreshInventory(); + $this->order = Order::factory()->create(); + $this->product = Product::factory()->create(); + + $this->product->setQuantity(100); + + $this->order->items()->create([ + 'buyable_id' => $this->product->id, + 'buyable_type' => Product::class, + 'quantity' => 10, + 'price' => $this->product->price, + 'name' => $this->product->name, + ]); + } + + public function test_listener_decrements_inventory_on_payment_captured(): void + { + $initialQuantity = $this->product->getQuantity(); + + $event = new PaymentCaptured($this->order->transactions()->create([ + 'type' => 'payment', + 'driver' => 'cash', + 'amount' => $this->order->getTotal(), + 'completed_at' => now(), + ])); + + $this->listener->handle($event); + + $this->product->refresh(); + + $this->assertEquals($initialQuantity - 10, $this->product->getQuantity()); + } + + public function test_listener_handles_payment_captured_event(): void + { + $transaction = $this->order->transactions()->create([ + 'type' => 'payment', + 'driver' => 'cash', + 'amount' => $this->order->getTotal(), + 'completed_at' => now(), + ]); + + $event = new PaymentCaptured($transaction); + + $this->listener->handle($event); + + $this->assertTrue(true); + } +} diff --git a/tests/Models/AppliedCouponTest.php b/tests/Models/AppliedCouponTest.php new file mode 100644 index 00000000..1e540a00 --- /dev/null +++ b/tests/Models/AppliedCouponTest.php @@ -0,0 +1,77 @@ +cart = Cart::factory()->create(); + $this->coupon = Coupon::factory()->create(['code' => 'TEST']); + + Product::factory()->count(2)->create()->each(function ($product) { + $this->cart->items()->create([ + 'buyable_id' => $product->id, + 'buyable_type' => Product::class, + 'quantity' => 2, + 'price' => $product->price, + 'name' => $product->name, + ]); + }); + } + + public function test_applied_coupon_belongs_to_coupon(): void + { + $this->cart->applyCoupon($this->coupon); + + $appliedCoupon = AppliedCoupon::first(); + + $this->assertInstanceOf(Coupon::class, $appliedCoupon->coupon); + $this->assertSame($this->coupon->id, $appliedCoupon->coupon->id); + } + + public function test_applied_coupon_belongs_to_couponable(): void + { + $this->cart->applyCoupon($this->coupon); + + $appliedCoupon = AppliedCoupon::first(); + + $this->assertInstanceOf(Cart::class, $appliedCoupon->couponable); + $this->assertSame($this->cart->id, $appliedCoupon->couponable->id); + } + + public function test_applied_coupon_has_value(): void + { + $this->cart->applyCoupon($this->coupon); + + $appliedCoupon = AppliedCoupon::first(); + + $this->assertIsFloat($appliedCoupon->value); + $this->assertGreaterThanOrEqual(0, $appliedCoupon->value); + } + + public function test_applied_coupon_formats_value(): void + { + $this->cart->applyCoupon($this->coupon); + + $appliedCoupon = AppliedCoupon::first(); + + $formatted = $appliedCoupon->format(); + + $this->assertIsString($formatted); + } +} diff --git a/tests/Models/DiscountableTest.php b/tests/Models/DiscountableTest.php new file mode 100644 index 00000000..cde06b0e --- /dev/null +++ b/tests/Models/DiscountableTest.php @@ -0,0 +1,53 @@ +order = Order::factory()->create(); + + Product::factory()->count(2)->create()->each(function ($product) { + $this->order->items()->create([ + 'buyable_id' => $product->id, + 'buyable_type' => Product::class, + 'quantity' => 2, + 'price' => $product->price, + 'name' => $product->name, + ]); + }); + + $this->discount = Discount::factory()->create(); + } + + public function test_discountable_pivot_can_be_created(): void + { + $this->order->discounts()->attach($this->discount); + + $discountable = Discountable::first(); + + $this->assertNotNull($discountable); + } + + public function test_discountable_uses_correct_table(): void + { + $discountable = new Discountable(); + + $this->assertSame('bazar_discountables', $discountable->getTable()); + } +} diff --git a/tests/Models/PriceTest.php b/tests/Models/PriceTest.php new file mode 100644 index 00000000..0c43d4c7 --- /dev/null +++ b/tests/Models/PriceTest.php @@ -0,0 +1,69 @@ +product = Product::factory()->create(); + } + + public function test_price_has_currency(): void + { + $price = $this->product->prices->first(); + + $this->assertNotNull($price->currency); + $this->assertIsString($price->currency); + } + + public function test_price_has_symbol(): void + { + $price = $this->product->prices->first(); + + $this->assertNotNull($price->symbol); + $this->assertIsString($price->symbol); + } + + public function test_price_formats_value(): void + { + $price = $this->product->prices->first(); + + $formatted = $price->format(); + + $this->assertIsString($formatted); + $this->assertStringContainsString($price->symbol, $formatted); + } + + public function test_price_can_register_custom_formatter(): void + { + Price::formatCurrency('USD', function ($value, $symbol, $currency) { + return "$symbol$value $currency"; + }); + + $this->product->setPrice(100, 'USD'); + + $price = $this->product->prices()->where('key', 'like', '%USD%')->first(); + + $formatted = $price->format(); + + $this->assertStringContainsString('USD', $formatted); + } + + public function test_price_casts_value_to_float(): void + { + $price = $this->product->prices->first(); + + $this->assertIsFloat($price->value); + } +} diff --git a/tests/Models/PropertyTest.php b/tests/Models/PropertyTest.php new file mode 100644 index 00000000..3a1feb2b --- /dev/null +++ b/tests/Models/PropertyTest.php @@ -0,0 +1,59 @@ +property = Property::factory()->create(['name' => 'Size', 'slug' => 'size']); + } + + public function test_property_has_values(): void + { + $value = PropertyValue::factory()->create([ + 'property_id' => $this->property->id, + 'value' => 'L', + ]); + + $this->assertTrue($this->property->values->contains($value)); + } + + public function test_property_deletes_values_on_delete(): void + { + $value = PropertyValue::factory()->create([ + 'property_id' => $this->property->id, + 'value' => 'M', + ]); + + $valueId = $value->id; + + $this->property->delete(); + + $this->assertNull(PropertyValue::find($valueId)); + } + + public function test_property_has_fillable_attributes(): void + { + $this->property->fill([ + 'name' => 'Color', + 'slug' => 'color', + 'description' => 'Product color', + ]); + + $this->assertSame('Color', $this->property->name); + $this->assertSame('color', $this->property->slug); + $this->assertSame('Product color', $this->property->description); + } +} diff --git a/tests/Models/PropertyValueTest.php b/tests/Models/PropertyValueTest.php new file mode 100644 index 00000000..128dcb18 --- /dev/null +++ b/tests/Models/PropertyValueTest.php @@ -0,0 +1,46 @@ +property = Property::factory()->create(['name' => 'Size']); + + $this->propertyValue = PropertyValue::factory()->create([ + 'property_id' => $this->property->id, + 'value' => 'Large', + 'name' => 'L', + ]); + } + + public function test_property_value_belongs_to_property(): void + { + $this->assertInstanceOf(Property::class, $this->propertyValue->property); + $this->assertSame($this->property->id, $this->propertyValue->property->id); + } + + public function test_property_value_has_fillable_attributes(): void + { + $this->propertyValue->fill([ + 'name' => 'XL', + 'value' => 'Extra Large', + ]); + + $this->assertSame('XL', $this->propertyValue->name); + $this->assertSame('Extra Large', $this->propertyValue->value); + } +} diff --git a/tests/Models/TaxTest.php b/tests/Models/TaxTest.php new file mode 100644 index 00000000..8276bf5d --- /dev/null +++ b/tests/Models/TaxTest.php @@ -0,0 +1,70 @@ +cart = Cart::factory()->create(); + $product = Product::factory()->create(); + + $this->item = $this->cart->items()->create([ + 'buyable_id' => $product->id, + 'buyable_type' => Product::class, + 'quantity' => 2, + 'price' => 100, + 'name' => $product->name, + ]); + + $this->taxRate = TaxRate::factory()->create(['rate' => 27]); + $product->taxRates()->attach($this->taxRate); + $this->cart->refresh(); + } + + public function test_tax_has_value(): void + { + $this->cart->calculateTax(); + + $tax = Tax::first(); + + $this->assertNotNull($tax); + $this->assertIsFloat($tax->value); + $this->assertGreaterThan(0, $tax->value); + } + + public function test_tax_formats_value(): void + { + $this->cart->calculateTax(); + + $tax = Tax::first(); + + $formatted = $tax->format(); + + $this->assertIsString($formatted); + } + + public function test_tax_has_default_value(): void + { + $tax = new Tax(); + + $this->assertEquals(0, $tax->value); + } +} diff --git a/tests/Notifications/OrderDetailsTest.php b/tests/Notifications/OrderDetailsTest.php new file mode 100644 index 00000000..d2413a2c --- /dev/null +++ b/tests/Notifications/OrderDetailsTest.php @@ -0,0 +1,76 @@ +user = User::factory()->create(); + $this->order = Order::factory()->create(['user_id' => $this->user->id]); + + Product::factory()->count(2)->create()->each(function ($product) { + $this->order->items()->create([ + 'buyable_id' => $product->id, + 'buyable_type' => Product::class, + 'quantity' => 2, + 'price' => $product->price, + 'name' => $product->name, + ]); + }); + } + + public function test_notification_uses_mail_channel(): void + { + $notification = new OrderDetails($this->order); + + $channels = $notification->via($this->user); + + $this->assertContains('mail', $channels); + } + + public function test_notification_creates_mail_message(): void + { + $notification = new OrderDetails($this->order); + + $mailMessage = $notification->toMail($this->user); + + $this->assertNotNull($mailMessage); + $this->assertStringContainsString('Order Details', $mailMessage->subject); + } + + public function test_notification_can_be_queued(): void + { + Notification::fake(); + + $this->user->notify(new OrderDetails($this->order)); + + Notification::assertSentTo($this->user, OrderDetails::class); + } + + public function test_notification_includes_order_in_mail(): void + { + $notification = new OrderDetails($this->order); + + $mailMessage = $notification->toMail($this->user); + + $this->assertNotNull($mailMessage->viewData); + $this->assertArrayHasKey('order', $mailMessage->viewData); + $this->assertSame($this->order, $mailMessage->viewData['order']); + } +} diff --git a/tests/Relations/PricesTest.php b/tests/Relations/PricesTest.php new file mode 100644 index 00000000..754c6bb8 --- /dev/null +++ b/tests/Relations/PricesTest.php @@ -0,0 +1,65 @@ +product = Product::factory()->create(); + } + + public function test_prices_relation_returns_only_price_meta(): void + { + $this->product->setPrice(100); + + $prices = $this->product->prices; + + $this->assertNotEmpty($prices); + + foreach ($prices as $price) { + $this->assertInstanceOf(Price::class, $price); + $this->assertStringStartsWith('price_', $price->key); + } + } + + public function test_prices_relation_filters_non_price_meta(): void + { + $this->product->setMeta('custom_key', 'value'); + $this->product->setPrice(100); + + $prices = $this->product->prices; + + $this->assertNotEmpty($prices); + + foreach ($prices as $price) { + $this->assertStringStartsWith('price_', $price->key); + } + } + + public function test_prices_relation_handles_multiple_currencies(): void + { + $this->product->setPrice(100, 'USD'); + $this->product->setPrice(90, 'EUR'); + + $prices = $this->product->prices; + + $this->assertGreaterThanOrEqual(2, $prices->count()); + + $usdPrice = $prices->firstWhere('key', 'like', '%USD%'); + $this->assertNotNull($usdPrice); + + $eurPrice = $prices->firstWhere('key', 'like', '%EUR%'); + $this->assertNotNull($eurPrice); + } +} diff --git a/tests/Shipping/DriverTest.php b/tests/Shipping/DriverTest.php new file mode 100644 index 00000000..77302922 --- /dev/null +++ b/tests/Shipping/DriverTest.php @@ -0,0 +1,73 @@ +order = Order::factory()->create(); + } + + public function test_driver_can_be_enabled(): void + { + $driver = new ConcreteShippingDriver(['enabled' => true]); + + $this->assertTrue($driver->enabled()); + } + + public function test_driver_can_be_disabled(): void + { + $driver = new ConcreteShippingDriver(['enabled' => false]); + + $this->assertTrue($driver->disabled()); + } + + public function test_driver_availability_depends_on_enabled_state(): void + { + $driver = new ConcreteShippingDriver(['enabled' => true]); + + $this->assertTrue($driver->available($this->order)); + + $driver->disable(); + + $this->assertFalse($driver->available($this->order)); + } + + public function test_driver_has_name(): void + { + $driver = new ConcreteShippingDriver(); + + $this->assertSame('Concrete Shipping', $driver->getName()); + } + + public function test_driver_can_calculate_shipping_fee(): void + { + $driver = new ConcreteShippingDriver(); + + $fee = $driver->calculate($this->order); + + $this->assertEquals(10, $fee); + } +} + +class ConcreteShippingDriver extends Driver +{ + protected string $name = 'concrete-shipping'; + + public function calculate(Shippable $model): float + { + return 10; + } +} diff --git a/tests/Shipping/LocalPickupDriverTest.php b/tests/Shipping/LocalPickupDriverTest.php new file mode 100644 index 00000000..7e14467e --- /dev/null +++ b/tests/Shipping/LocalPickupDriverTest.php @@ -0,0 +1,70 @@ +driver = new LocalPickupDriver(['enabled' => true]); + + $this->order = Order::factory()->create(); + + Product::factory()->count(2)->create()->each(function ($product) { + $this->order->items()->create([ + 'buyable_id' => $product->id, + 'buyable_type' => Product::class, + 'quantity' => 2, + 'price' => $product->price, + 'name' => $product->name, + ]); + }); + } + + public function test_local_pickup_driver_has_correct_name(): void + { + $this->assertSame('Local Pickup', $this->driver->getName()); + } + + public function test_local_pickup_driver_calculates_zero_fee(): void + { + $fee = $this->driver->calculate($this->order); + + $this->assertEquals(0, $fee); + } + + public function test_local_pickup_driver_is_available(): void + { + $this->assertTrue($this->driver->available($this->order)); + } + + public function test_local_pickup_driver_can_be_disabled(): void + { + $this->driver->disable(); + + $this->assertTrue($this->driver->disabled()); + $this->assertFalse($this->driver->available($this->order)); + } + + public function test_local_pickup_driver_can_be_enabled(): void + { + $this->driver->disable(); + $this->driver->enable(); + + $this->assertTrue($this->driver->enabled()); + $this->assertTrue($this->driver->available($this->order)); + } +} diff --git a/tests/Support/CountriesTest.php b/tests/Support/CountriesTest.php new file mode 100644 index 00000000..b6cf2ac3 --- /dev/null +++ b/tests/Support/CountriesTest.php @@ -0,0 +1,108 @@ +assertIsArray($africa); + $this->assertNotEmpty($africa); + $this->assertArrayHasKey('ZA', $africa); + } + + public function test_countries_returns_all_antarctican_countries(): void + { + $antarctica = Countries::antarctica(); + + $this->assertIsArray($antarctica); + $this->assertNotEmpty($antarctica); + $this->assertArrayHasKey('AQ', $antarctica); + } + + public function test_countries_returns_all_asian_countries(): void + { + $asia = Countries::asia(); + + $this->assertIsArray($asia); + $this->assertNotEmpty($asia); + $this->assertArrayHasKey('JP', $asia); + } + + public function test_countries_returns_all_european_countries(): void + { + $europe = Countries::europe(); + + $this->assertIsArray($europe); + $this->assertNotEmpty($europe); + $this->assertArrayHasKey('GB', $europe); + } + + public function test_countries_returns_all_north_american_countries(): void + { + $northAmerica = Countries::northAmerica(); + + $this->assertIsArray($northAmerica); + $this->assertNotEmpty($northAmerica); + $this->assertArrayHasKey('US', $northAmerica); + } + + public function test_countries_returns_all_south_american_countries(): void + { + $southAmerica = Countries::southAmerica(); + + $this->assertIsArray($southAmerica); + $this->assertNotEmpty($southAmerica); + $this->assertArrayHasKey('BR', $southAmerica); + } + + public function test_countries_returns_all_oceanian_countries(): void + { + $oceania = Countries::oceania(); + + $this->assertIsArray($oceania); + $this->assertNotEmpty($oceania); + $this->assertArrayHasKey('AU', $oceania); + } + + public function test_countries_returns_all_countries(): void + { + $all = Countries::all(); + + $this->assertIsArray($all); + $this->assertNotEmpty($all); + $this->assertGreaterThan(200, count($all)); + } + + public function test_countries_returns_all_countries_by_continent(): void + { + $byContinent = Countries::allByContient(); + + $this->assertIsArray($byContinent); + $this->assertNotEmpty($byContinent); + $this->assertArrayHasKey('Africa', $byContinent); + $this->assertArrayHasKey('Europe', $byContinent); + } + + public function test_countries_returns_country_name_by_code(): void + { + $name = Countries::name('US'); + + $this->assertIsString($name); + $this->assertNotEmpty($name); + } + + public function test_countries_returns_code_when_country_not_found(): void + { + $name = Countries::name('XX'); + + $this->assertSame('XX', $name); + } +} diff --git a/tests/Support/DriverTest.php b/tests/Support/DriverTest.php new file mode 100644 index 00000000..4457c638 --- /dev/null +++ b/tests/Support/DriverTest.php @@ -0,0 +1,92 @@ +order = Order::factory()->create(); + } + + public function test_driver_can_be_enabled(): void + { + $driver = new ConcreteDriver(['enabled' => true]); + + $this->assertTrue($driver->enabled()); + $this->assertFalse($driver->disabled()); + } + + public function test_driver_can_be_disabled(): void + { + $driver = new ConcreteDriver(['enabled' => false]); + + $this->assertTrue($driver->disabled()); + $this->assertFalse($driver->enabled()); + } + + public function test_driver_defaults_to_disabled(): void + { + $driver = new ConcreteDriver(); + + $this->assertTrue($driver->disabled()); + } + + public function test_driver_can_be_enabled_manually(): void + { + $driver = new ConcreteDriver(); + + $driver->enable(); + + $this->assertTrue($driver->enabled()); + } + + public function test_driver_can_be_disabled_manually(): void + { + $driver = new ConcreteDriver(['enabled' => true]); + + $driver->disable(); + + $this->assertTrue($driver->disabled()); + } + + public function test_driver_availability_depends_on_enabled_state(): void + { + $driver = new ConcreteDriver(['enabled' => true]); + + $this->assertTrue($driver->available($this->order)); + + $driver->disable(); + + $this->assertFalse($driver->available($this->order)); + } + + public function test_driver_has_name(): void + { + $driver = new ConcreteDriver(); + + $this->assertSame('Concrete', $driver->getName()); + } + + public function test_driver_accepts_config(): void + { + $driver = new ConcreteDriver(['enabled' => true, 'custom' => 'value']); + + $this->assertTrue($driver->enabled()); + } +} + +class ConcreteDriver extends Driver +{ + // +} diff --git a/tests/Traits/AsCustomerTest.php b/tests/Traits/AsCustomerTest.php new file mode 100644 index 00000000..d3fe5c90 --- /dev/null +++ b/tests/Traits/AsCustomerTest.php @@ -0,0 +1,78 @@ +user = User::factory()->create(); + } + + public function test_user_has_carts(): void + { + $cart = Cart::factory()->create(['user_id' => $this->user->id]); + + $this->assertTrue($this->user->carts->contains($cart)); + } + + public function test_user_has_active_cart(): void + { + $cart1 = Cart::factory()->create(['user_id' => $this->user->id]); + $cart2 = Cart::factory()->create(['user_id' => $this->user->id]); + + $activeCart = $this->user->cart; + + $this->assertInstanceOf(Cart::class, $activeCart); + $this->assertSame($cart2->id, $activeCart->id); + } + + public function test_user_has_orders(): void + { + $order = Order::factory()->create(['user_id' => $this->user->id]); + + $this->assertTrue($this->user->orders->contains($order)); + } + + public function test_user_has_addresses(): void + { + $address = Address::factory()->create([ + 'addressable_id' => $this->user->id, + 'addressable_type' => get_class($this->user), + ]); + + $this->assertTrue($this->user->addresses->contains($address)); + } + + public function test_user_has_default_address(): void + { + $address1 = Address::factory()->create([ + 'addressable_id' => $this->user->id, + 'addressable_type' => get_class($this->user), + 'default' => false, + ]); + + $address2 = Address::factory()->create([ + 'addressable_id' => $this->user->id, + 'addressable_type' => get_class($this->user), + 'default' => true, + ]); + + $defaultAddress = $this->user->address; + + $this->assertInstanceOf(Address::class, $defaultAddress); + $this->assertSame($address2->id, $defaultAddress->id); + } +} diff --git a/tests/Traits/AsOrderTest.php b/tests/Traits/AsOrderTest.php new file mode 100644 index 00000000..15f171dc --- /dev/null +++ b/tests/Traits/AsOrderTest.php @@ -0,0 +1,111 @@ +order = Order::factory()->create(); + + Product::factory()->count(2)->create()->each(function ($product) { + $this->order->items()->create([ + 'buyable_id' => $product->id, + 'buyable_type' => Product::class, + 'quantity' => 2, + 'price' => $product->price, + 'name' => $product->name, + ]); + }); + } + + public function test_order_has_items(): void + { + $this->assertCount(2, $this->order->items); + } + + public function test_order_has_shipping(): void + { + $this->assertNotNull($this->order->shipping); + } + + public function test_order_calculates_total(): void + { + $total = $this->order->getTotal(); + + $this->assertIsFloat($total); + $this->assertGreaterThan(0, $total); + } + + public function test_order_calculates_subtotal(): void + { + $subtotal = $this->order->getSubtotal(); + + $this->assertIsFloat($subtotal); + $this->assertGreaterThan(0, $subtotal); + } + + public function test_order_calculates_tax(): void + { + $this->order->calculateTax(); + + $tax = $this->order->getTax(); + + $this->assertIsFloat($tax); + $this->assertGreaterThanOrEqual(0, $tax); + } + + public function test_order_can_apply_coupon(): void + { + $coupon = Coupon::factory()->create(); + + $result = $this->order->applyCoupon($coupon); + + $this->assertTrue($result); + $this->assertTrue($this->order->coupons->contains($coupon)); + } + + public function test_order_can_remove_coupon(): void + { + $coupon = Coupon::factory()->create(); + + $this->order->applyCoupon($coupon); + $this->order->removeCoupon($coupon); + + $this->order->refresh(); + + $this->assertFalse($this->order->coupons->contains($coupon)); + } + + public function test_order_formats_total(): void + { + $formatted = $this->order->getFormattedTotal(); + + $this->assertIsString($formatted); + } + + public function test_order_determines_if_needs_payment(): void + { + $needsPayment = $this->order->needsPayment(); + + $this->assertIsBool($needsPayment); + } + + public function test_order_has_currency(): void + { + $currency = $this->order->getCurrency(); + + $this->assertNotNull($currency); + } +} diff --git a/tests/Traits/HasPricesTest.php b/tests/Traits/HasPricesTest.php new file mode 100644 index 00000000..814307a8 --- /dev/null +++ b/tests/Traits/HasPricesTest.php @@ -0,0 +1,75 @@ +product = Product::factory()->create(); + } + + public function test_model_has_prices(): void + { + $this->assertNotEmpty($this->product->prices); + } + + public function test_model_gets_price(): void + { + $price = $this->product->getPrice(); + + $this->assertIsFloat($price); + $this->assertGreaterThan(0, $price); + } + + public function test_model_gets_formatted_price(): void + { + $formatted = $this->product->getFormattedPrice(); + + $this->assertIsString($formatted); + } + + public function test_model_price_attribute(): void + { + $this->assertIsFloat($this->product->price); + $this->assertEquals($this->product->getPrice(), $this->product->price); + } + + public function test_model_formatted_price_attribute(): void + { + $this->assertIsString($this->product->formatted_price); + $this->assertEquals($this->product->getFormattedPrice(), $this->product->formatted_price); + } + + public function test_model_gets_price_html(): void + { + $html = $this->product->getPriceHtml(); + + $this->assertIsString($html->toHtml()); + } + + public function test_model_determines_if_free(): void + { + $isFree = $this->product->isFree(); + + $this->assertIsBool($isFree); + } + + public function test_free_product_shows_free_html(): void + { + $this->product->prices()->delete(); + + $html = $this->product->getPriceHtml(); + + $this->assertStringContainsString('Free', $html->toHtml()); + } +} diff --git a/tests/Traits/HasPropertiesTest.php b/tests/Traits/HasPropertiesTest.php new file mode 100644 index 00000000..b304288e --- /dev/null +++ b/tests/Traits/HasPropertiesTest.php @@ -0,0 +1,73 @@ +product = Product::factory()->create(); + $this->property = Property::factory()->create(['name' => 'Color', 'slug' => 'color']); + $this->propertyValue = PropertyValue::factory()->create([ + 'property_id' => $this->property->id, + 'value' => 'Red', + ]); + } + + public function test_model_has_property_values(): void + { + $this->product->propertyValues()->attach($this->propertyValue); + + $this->product->refresh(); + + $this->assertTrue($this->product->propertyValues->contains($this->propertyValue)); + } + + public function test_model_has_properties(): void + { + $this->product->propertyValues()->attach($this->propertyValue); + + $this->product->refresh(); + + $this->assertTrue($this->product->properties->contains($this->property)); + } + + public function test_model_can_have_multiple_property_values(): void + { + $value2 = PropertyValue::factory()->create([ + 'property_id' => $this->property->id, + 'value' => 'Blue', + ]); + + $this->product->propertyValues()->attach([$this->propertyValue->id, $value2->id]); + + $this->product->refresh(); + + $this->assertCount(2, $this->product->propertyValues); + } + + public function test_model_property_values_are_morph_to_many(): void + { + $this->product->propertyValues()->attach($this->propertyValue); + + $pivot = $this->product->propertyValues()->first()->pivot; + + $this->assertEquals($this->product->id, $pivot->buyable_id); + $this->assertEquals(Product::class, $pivot->buyable_type); + } +}