Resolve conflicts between 2.1 and 2.2

This commit is contained in:
Rafikooo 2025-11-26 11:01:16 +01:00
commit 16ee26b052
No known key found for this signature in database
GPG key ID: 4A26D8327BC2442B
222 changed files with 2400 additions and 770 deletions

View file

@ -27,14 +27,6 @@
"mariadb": "11.4.7",
"dbal": "^3.0",
"state_machine_adapter": "symfony_workflow"
},
{
"php": "8.5",
"symfony": "^7.3",
"mariadb": "11.4.7",
"dbal": "^3.0",
"state_machine_adapter": "symfony_workflow",
"api_platform": "~4.1.7"
}
]
},

View file

@ -2,5 +2,29 @@
### Deprecations
1. Not injecting a `tagged_iterator` with the tag `sylius_shop.modifier.address_form_values` into the constructor of `Sylius\Bundle\ShopBundle\Twig\Component\Checkout\Address\FormComponent` is deprecated since Sylius 2.2 and will be required in Sylius 3.0.
This change enables extending the checkout address form with custom fields or logic by registering services tagged with `sylius_shop.modifier.address_form_values`, which implement the `AddressFormValuesModifierInterface`.
```php
public function __construct(
OrderRepositoryInterface $repository,
FormFactoryInterface $formFactory,
string $resourceClass,
string $formClass,
protected readonly CustomerContextInterface $customerContext,
protected readonly UserRepositoryInterface $shopUserRepository,
protected readonly AddressRepositoryInterface $addressRepository,
+ protected readonly ?iterable $addressFormValuesModifiers = null,
)
```
1. Direct usage of `loader.svg` and `loader.gif` assets is deprecated.
Use `@SyliusAdmin/shared/helper/loader.html.twig` or `@SyliusShop/shared/macro/loader.html.twig` instead.
### Translations
1. The `TranslationLocaleProvider` now ensures that the default locale (configured as `locale` in `config/parameters.yaml`)
is always placed at the beginning of the returned locales array.
Other locales remain in the same order as returned by the repository.

41
UPGRADE-API-2.2.md Normal file
View file

@ -0,0 +1,41 @@
# UPGRADE FROM `2.1` TO `2.2`
## HTTP Status Code Changes
### Missing Required Fields Validation
The HTTP status code for missing required fields in API requests has been changed from `400 Bad Request` to `422 Unprocessable Content` to follow REST API best practices and RFC 9110 semantics.
Additionally, the redundant `code` field has been removed from the error response body, as the status code is already available in the HTTP response headers.
**Before:**
```json
{
"@context": "/api/v2/contexts/Error",
"@type": "hydra:Error",
"status": 400,
"detail": "Request does not have the following required fields specified: email."
}
```
**After:**
```json
{
"@context": "/api/v2/contexts/Error",
"@type": "hydra:Error",
"status": 422,
"detail": "Request does not have the following required fields specified: email."
}
```
**Breaking changes:**
1. HTTP status code changed: `400``422`
2. Response body field `code` removed (was redundant with HTTP status header)
**Affected endpoints:** All POST/PATCH endpoints that validate required fields (e.g., `/api/v2/shop/customers`, `/api/v2/shop/orders/{token}/items`, etc.)
**Migration guide:**
- If your API client checks `response.status === 400` for missing fields, change it to `response.status === 422`
- If your API client reads `response.data.code`, use `response.status` (HTTP header) instead
**References:** RFC 9110 (400 = syntactic errors, 422 = semantic/validation errors)

View file

@ -31,9 +31,9 @@
"ext-json": "*",
"ext-simplexml": "*",
"ext-sodium": "*",
"api-platform/doctrine-orm": "^4.1.7",
"api-platform/state": "^4.1.7",
"api-platform/symfony": "^4.1.7",
"api-platform/doctrine-orm": "^4.2.1",
"api-platform/state": "^4.2.1",
"api-platform/symfony": "^4.2.1",
"babdev/pagerfanta-bundle": "^4.4",
"behat/transliterator": "^1.5",
"doctrine/collections": "^2.2",
@ -214,7 +214,6 @@
"mockery/mockery": "^1.6",
"nyholm/psr7": "^1.8",
"phparkitect/phparkitect": "^0.6",
"phpspec/prophecy-phpunit": "^2.2",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^2.0",
"phpstan/phpstan-doctrine": "^2.0",

View file

@ -67,3 +67,13 @@ Feature: Editing a zone
And I save my changes
Then I should be notified that it has been successfully edited
And this zone name should be "EU"
@api @ui
Scenario: Setting zone to the highest priority
Given the store has a zone "European Union" with code "EU" and priority 0
And it has the "France" country member
When I want to modify the zone named "European Union"
And I set its priority to "5"
And I save my changes
Then I should be notified that it has been successfully edited
And the "European Union" zone should have priority 5

View file

@ -0,0 +1,26 @@
@managing_zones
Feature: Editing a zone
In order to change my my tax and shipping configuration
As an Administrator
I want to be able to edit a zone
Background:
Given the store also has country "France"
And the store has a zone "North America" with code "NA" and priority 2
And the store has a zone "South America" with code "SA" and priority 1
And the store has a zone "Australia" with code "AU" and priority 0
And I am logged in as an administrator
@api @ui
Scenario: Zones are sorted by priority in descending order by default
When I want to see all zones in store
Then I should see 3 zones in the list
Then the first zone on the list should have name "North America"
Then the last zone on the list should have name "Australia"
@api @ui
Scenario: Zone's default priority is 0 which puts it at the bottom of the list
Given the store has a zone "European Union" with code "EU"
When I want to see all zones in store
Then I should see 4 zones in the list
And the last zone on the list should have name "European Union"

View file

@ -0,0 +1,24 @@
@managing_shipping_methods
Feature: Validating delivery time range on shipping method form
In order to avoid invalid delivery time configuration
As an Administrator
I want to be prevented from saving a shipping method when maximum delivery time is less than minimum
Background:
Given the store operates on a channel named "Web-US" in "USD" currency
And the store is available in "English (United States)"
And the store has a zone "United States" with code "US"
And I am logged in as an administrator
@ui @mink:chromedriver
Scenario: Trying to add a shipping method with invalid delivery time range
When I want to create a new shipping method
And I specify its code as "DELIVERY_TIME_INVALID"
And I name it "Delivery Time Invalid" in "English (United States)"
And I define it for the zone named "United States"
And I choose "Flat rate per shipment" calculator
And I specify its amount as 10 for "Web-US" channel
And I fill in "Minimum delivery time (days)" with "5"
And I fill in "Maximum delivery time (days)" with "3"
And I try to add it
Then I should be notified that Maximum delivery time must be greater than or equal to the minimum.

View file

@ -0,0 +1,49 @@
@applying_taxes
Feature: Apply taxes based on zone priority
In order to pay correct taxes when a country belongs to multiple zones
As a Customer
I want the zone with higher priority to be used for tax calculation
Background:
Given the store operates on a single channel
Given the store operates in "France" and "Belgium"
And there is a zone "The Rest of the World" containing all other countries
And the store has a zone "European Union" with code "EU" and priority 2
And it has the "France" country member
And it also has the "Belgium" country member
And default tax zone is "RoW"
And the store has "No tax" tax rate of 0% for "Clothes" for the rest of the world
And the store has "VAT" tax rate of 23% for "Clothes" within the "European Union" zone
And the store has a product "T-Shirt" priced at "$100.00"
And it belongs to "Clothes" tax category
And I am a logged in customer
@api @ui
Scenario: Applying tax from the higher priority zone
Given I added product "T-Shirt" to the cart
And I addressed the cart to "Belgium"
When I check the details of my cart
Then my cart total should be "$123.00"
And my cart taxes should be "$23.00"
@api @ui
Scenario: Applying tax from new zone zone when it has higher priority
Given the store has a zone "Benelux countries" with code "BC" and priority 3
And it also has the "Belgium" country member
And the store has "VAT" tax rate of 0% for "Clothes" within the "Benelux countries" zone
And I added product "T-Shirt" to the cart
And I addressed the cart to "Belgium"
When I check the details of my cart
Then my cart total should be "$100.00"
And there should be no taxes charged
@api @ui
Scenario: Applying tax from current zone when the new one has lower priority
Given the store has a zone "Benelux countries" with code "GU" and priority 1
And it also has the "Belgium" country member
And the store has "VAT" tax rate of 0% for "Clothes" within the "Benelux countries" zone
And I added product "T-Shirt" to the cart
And I addressed the cart to "Belgium"
When I check the details of my cart
Then my cart total should be "$123.00"
And my cart taxes should be "$23.00"

View file

@ -129,6 +129,14 @@ final readonly class ManagingZonesContext implements Context
$this->client->addRequestData('scope', $scope);
}
/**
* @When I set its priority to :priority
*/
public function iSetsItsPriorityTo(int $priority): void
{
$this->client->addRequestData('priority', $priority);
}
/**
* @When I (try to) add it
*/
@ -496,6 +504,37 @@ final readonly class ManagingZonesContext implements Context
);
}
/**
* @Then /^the (first|last) zone on the list should have ([^"]+) "([^"]+)"$/
*/
public function theFirstZoneOnTheListShouldHave(string $togglePosition, string $field, string $value): void
{
$items = $this->responseChecker->getValue($this->client->getLastResponse(), 'hydra:member');
if ('first' === $togglePosition) {
$item = reset($items);
} else {
$item = end($items);
}
Assert::same($item[$field], $value);
}
/**
* @Then the :zone zone should have priority :priority
*/
public function theZoneShouldHavePriority(ZoneInterface $zone, int $priority): void
{
Assert::true(
$this->responseChecker->hasItemWithValues(
$this->client->index(Resources::ZONES),
[
'name' => $zone->getName(),
'priority' => $priority,
],
),
);
}
private function removeZoneMember(CountryInterface|ProvinceInterface|ZoneInterface $objectToRemove): void
{
$members = $this->client->getContent()['members'];

View file

@ -22,6 +22,7 @@ use Sylius\Behat\Context\Ui\Admin\Helper\SecurePasswordTrait;
use Sylius\Behat\Service\SharedStorageInterface;
use Sylius\Component\Core\Model\CustomerInterface;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\HttpFoundation\Response;
use Webmozart\Assert\Assert;
final class RegistrationContext implements Context
@ -206,7 +207,7 @@ final class RegistrationContext implements Context
$content['hydra:description'],
'Request does not have the following required fields specified: ' . implode(', ', $fields) . '.',
);
Assert::same($this->shopClient->getLastResponse()->getStatusCode(), 400);
Assert::same($this->shopClient->getLastResponse()->getStatusCode(), Response::HTTP_UNPROCESSABLE_ENTITY);
}
/**

View file

@ -84,12 +84,17 @@ final readonly class ZoneContext implements Context
* @Given the store has (also) a zone :zoneName
* @Given the store has a zone :zoneName with code :code
* @Given the store also has a zone :zoneName with code :code
* @Given the store has a zone :zoneName with code :code and priority :priority
*/
public function theStoreHasAZoneWithCode(string $zoneName, ?string $code = null): void
public function theStoreHasAZoneWithCode(string $zoneName, ?string $code = null, ?int $priority = 0): void
{
$this->saveZone($this->createZone($zoneName, $code, Scope::ALL), 'zone');
$zone = $this->createZone($zoneName, $code, Scope::ALL);
$zone->setPriority($priority);
$this->saveZone($zone, 'zone');
}
/**
* @Given the store has zones :firstName, :secondName and :thirdName
*/

View file

@ -14,6 +14,7 @@ declare(strict_types=1);
namespace Sylius\Behat\Context\Ui\Admin;
use Behat\Behat\Context\Context;
use Behat\Step\When;
use Sylius\Behat\Element\Admin\ShippingMethod\FormElementInterface;
use Sylius\Behat\Page\Admin\ShippingMethod\CreatePageInterface;
use Sylius\Behat\Page\Admin\ShippingMethod\IndexPageInterface;
@ -124,6 +125,12 @@ final readonly class ManagingShippingMethodsContext implements Context
$this->shippingMethodFormElement->chooseCalculator($calculatorName);
}
#[When('I fill in :label with :value')]
public function iFillInWith(string $label, string $value): void
{
$this->shippingMethodFormElement->setField($label, $value);
}
/**
* @When I check (also) the :shippingMethodName shipping method
*/
@ -625,4 +632,12 @@ final readonly class ManagingShippingMethodsContext implements Context
{
Assert::same($this->shippingMethodFormElement->getValidationMessage($element), $expectedMessage);
}
/**
* @Then I should be notified that Maximum delivery time must be greater than or equal to the minimum.
*/
public function iShouldBeNotifiedThatMaxDeliveryIsGreaterOrEqualMin(): void
{
$this->assertFieldValidationMessage('max_delivery_time_days', 'Maximum delivery time must be greater than or equal to the minimum.');
}
}

View file

@ -132,6 +132,14 @@ final class ManagingZonesContext implements Context
$this->formElement->selectScope($scope);
}
/**
* @When I set its priority to :priority
*/
public function iSetItsPriorityTo(int $priority): void
{
$this->formElement->prioritizeIt($priority);
}
/**
* @When I add it
* @When I try to add it
@ -338,4 +346,45 @@ final class ManagingZonesContext implements Context
throw new \InvalidArgumentException(sprintf('Member "%s" should not be selectable.', $name));
}
/**
* @Then the first zone on the list should have :field :value
*/
public function theFirstZoneOnTheListShouldHave(string $field, string $value): void
{
$fields = $this->indexPage->getColumnFields($field);
$actualValue = reset($fields);
Assert::same(
$actualValue,
$value,
sprintf('Expected first zone\'s %s to be "%s", but it is "%s".', $field, $value, $actualValue),
);
}
/**
* @Then the last zone on the list should have :field :value
*/
public function theLastZoneOnTheListShouldHave(string $field, string $value): void
{
$fields = $this->indexPage->getColumnFields($field);
$actualValue = end($fields);
Assert::same(
$actualValue,
$value,
sprintf('Expected last zone\'s %s to be "%s", but it is "%s".', $field, $value, $actualValue),
);
}
/**
* @Given the :zone zone should have priority :priority
*/
public function theZoneShouldHavePriority(ZoneInterface $zone, int $priority)
{
$this->iWantToModifyAZoneNamed($zone);
Assert::same($this->formElement->getPriority(), $priority);
}
}

View file

@ -164,6 +164,8 @@ class FormElement extends BaseFormElement implements FormElementInterface
'last_rule' => '[data-test-rules] [data-test-entry-row]:last-child',
'last_rule_amount' => '[data-test-rules] [data-test-entry-row]:last-child [id$="_configuration_%channelCode%_amount"]',
'last_rule_weight' => '[data-test-rules] [data-test-entry-row]:last-child [id$="_configuration_weight"]',
'max_delivery_time_days' => '#sylius_admin_shipping_method_maxDeliveryTimeDays',
'min_delivery_time_days' => '#sylius_admin_shipping_method_minDeliveryTimeDays',
'name' => '#sylius_admin_shipping_method_translations_%localeCode%_name',
'position' => '#sylius_admin_shipping_method_position',
'zone' => '#sylius_admin_shipping_method_zone',

View file

@ -31,6 +31,11 @@ class FormElement extends BaseFormElement implements FormElementInterface
return $this->getElement('name')->getValue();
}
public function getPriority(): int
{
return (int) $this->getElement('priority')->getValue();
}
public function getType(): string
{
return $this->getElement('type')->getValue();
@ -67,6 +72,11 @@ class FormElement extends BaseFormElement implements FormElementInterface
$this->waitForElement(5, 'zone_member_added');
}
public function prioritizeIt(int $priority): void
{
$this->getElement('priority')->setValue($priority);
}
public function removeMember(string $member): void
{
$this->getElement('zone_member_delete', ['%name%' => $member])->click();
@ -100,6 +110,7 @@ class FormElement extends BaseFormElement implements FormElementInterface
'code' => '[data-test-code]',
'form_validation_message' => 'form > div.alert.alert-danger.d-block',
'name' => '[data-test-name]',
'priority' => '#sylius_admin_zone_priority',
'scope' => '[data-test-scope]',
'type' => '[data-test-type]',
'zone_member' => '[data-test-zone-member]:contains("%name%")',

View file

@ -19,6 +19,8 @@ interface FormElementInterface extends BaseFormElementInterface
{
public function getName(): string;
public function getPriority(): int;
public function nameIt(string $name): void;
public function getType(): string;
@ -31,6 +33,8 @@ interface FormElementInterface extends BaseFormElementInterface
public function addMember(): void;
public function prioritizeIt(int $priority): void;
public function getScope(): string;
public function selectScope(string $scope): void;

View file

@ -19,6 +19,7 @@ use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType;
use Sylius\Component\Addressing\Model\ZoneInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
@ -46,6 +47,10 @@ final class ZoneType extends AbstractResourceType
->add('type', ZoneTypeChoiceType::class, [
'disabled' => true,
])
->add('priority', IntegerType::class, [
'label' => 'sylius.form.zone.priority',
'required' => true,
])
;
if (!empty($this->scopeChoices)) {

View file

@ -33,6 +33,7 @@ class ZoneRepository extends EntityRepository implements ZoneRepositoryInterface
->andWhere($queryBuilder->expr()->eq('o.type', ':type'))
->setParameter('type', $type)
->setMaxResults(1)
->addOrderBy('o.priority', 'DESC')
;
return $queryBuilder->getQuery()->getOneOrNullResult();

View file

@ -22,6 +22,7 @@
<field name="name" column="name" type="string" />
<field name="type" column="type" type="string" length="8" />
<field name="scope" column="scope" type="string" nullable="true"/>
<field name="priority" column="priority" type="integer" />
<one-to-many field="members" target-entity="Sylius\Component\Addressing\Model\ZoneMemberInterface" mapped-by="belongsTo" orphan-removal="true">
<cascade>

View file

@ -30,5 +30,6 @@ sylius:
zone: Area
scope: Gebied
select: Kies
priority: Prioriteit
zone_member:
select: Kies

View file

@ -38,5 +38,6 @@ sylius:
all: الكل
select: اختر
select_scope: اختر النطاق
priority: الأولوية
zone_member:
select: اختر

View file

@ -38,5 +38,6 @@ sylius:
all: Усе
select: Выберыце
select_scope: Выбярыце сферу ўжывання
priority: Прыярытэт
zone_member:
select: Выберыце

View file

@ -38,5 +38,6 @@ sylius:
all: Всички
select: Изберете
select_scope: Изберете обхват
priority: Приоритет
zone_member:
select: Изберете

View file

@ -38,5 +38,6 @@ sylius:
all: Totes
select: Tria
select_scope: Seleccioneu l'àmbit
priority: Prioritat
zone_member:
select: Tria

View file

@ -37,5 +37,6 @@ sylius:
scopes:
all: 'Všechny'
select: 'Vybrat'
priority: 'Priorita'
zone_member:
select: 'Vybrat'

View file

@ -38,5 +38,6 @@ sylius:
all: Alle
select: Vælg
select_scope: Vælg omfang
priority: Prioritet
zone_member:
select: Vælg

View file

@ -38,5 +38,6 @@ sylius:
all: 'Alle'
select: 'Zone auswählen'
select_scope: 'Geltungsbereich auswählen'
priority: 'Priorität'
zone_member:
select: 'Mitglied auswählen'

View file

@ -37,5 +37,6 @@ sylius:
scopes:
all: 'Alle'
select: 'Auswählen'
priority: 'Priorität'
zone_member:
select: 'Auswählen'

View file

@ -37,5 +37,6 @@ sylius:
scopes:
all: 'Alle'
select: 'Auswählen'
priority: 'Priorität'
zone_member:
select: 'Auswählen'

View file

@ -38,5 +38,6 @@ sylius:
all: Όλα
select: Επιλέξτε
select_scope: Επιλέξτε πεδίο
priority: Προτεραιότητα
zone_member:
select: Επιλέξτε

View file

@ -34,5 +34,6 @@ sylius:
scopes:
all: All
select: Select
priority: Priority
zone_member:
select: Select

View file

@ -36,5 +36,6 @@ sylius:
scopes:
all: All
select: Choose
priority: Priority
zone_member:
select: Choose

View file

@ -38,5 +38,6 @@ sylius:
all: Todos
select: Seleccionar
select_scope: Seleccione ámbito
priority: Prioridad
zone_member:
select: Selección

View file

@ -38,5 +38,6 @@ sylius:
all: Kõik
select: Vali
select_scope: Valige
priority: Prioriteet
zone_member:
select: Vali

View file

@ -38,5 +38,6 @@ sylius:
all: همه
select: انتخاب
select_scope: انتخاب محدوده
priority: اولویت
zone_member:
select: انتخاب

View file

@ -38,5 +38,6 @@ sylius:
all: Kaikki
select: Valitse
select_scope: Valitse alue
priority: Prioriteetti
zone_member:
select: Valitse

View file

@ -37,5 +37,6 @@ sylius:
scopes:
all: Tous
select: Sélectionner
priority: Priorité
zone_member:
select: Sélectionner

View file

@ -38,5 +38,6 @@ sylius:
all: Tout
select: Sélectionner
select_scope: Sélectionner la portée
priority: Priorité
zone_member:
select: Sélectionner

View file

@ -38,5 +38,6 @@ sylius:
all: Tous
select: Choisir
select_scope: Choisissez la portée
priority: Priorité
zone_member:
select: Choisir

View file

@ -38,5 +38,6 @@ sylius:
all: הכל
select: בחירה
select_scope: בחרו תחום
priority: עדיפות
zone_member:
select: בחירה

View file

@ -37,5 +37,6 @@ sylius:
scopes:
all: 'सभी'
select: 'चयन करें'
priority: 'प्राथमिकता'
zone_member:
select: 'चयन करें'

View file

@ -38,5 +38,6 @@ sylius:
all: Sve
select: Odaberite
select_scope: Odaberite doseg
priority: Prioritet
zone_member:
select: Odaberite

View file

@ -38,5 +38,6 @@ sylius:
all: Mind
select: Válassza ki
select_scope: Hatókör kiválasztása
priority: Prioritás
zone_member:
select: Válassza ki

View file

@ -38,5 +38,6 @@ sylius:
all: Semua
select: Pilih
select_scope: Pilih lingkup
priority: Prioritas
zone_member:
select: Pilih

View file

@ -38,5 +38,6 @@ sylius:
all: Allt
select: Vallisti
select_scope: Velja umfang
priority: Forgangur
zone_member:
select: Vallisti

View file

@ -37,5 +37,6 @@ sylius:
scopes:
all: 'Tutti'
select: 'Seleziona'
priority: 'Priorità'
zone_member:
select: 'Seleziona'

View file

@ -37,6 +37,7 @@ sylius:
scopes:
all: 'すべて'
select: '選択'
priority: '優先度'
zone_member:
select: '選択'

View file

@ -37,5 +37,6 @@ sylius:
scopes:
all: '전체'
select: '선택'
priority: '우선순위'
zone_member:
select: '선택'

View file

@ -38,5 +38,6 @@ sylius:
all: Visi
select: Pasirinkite
select_scope: Pasirinkite sritį
priority: Prioritetas
zone_member:
select: Pasirinkite

View file

@ -34,3 +34,4 @@ sylius:
scopes:
all: Visas
select_scope: Izvēlēties tvērumu
priority: Prioritāte

View file

@ -38,5 +38,6 @@ sylius:
all: Сите
select: Изберете
select_scope: Изберете опсег
priority: Приоритет
zone_member:
select: Изберете

View file

@ -38,5 +38,6 @@ sylius:
all: Бүгд
select: Сонгох
select_scope: Бүс сонгох
priority: Эрэмбэ
zone_member:
select: Сонгох

View file

@ -38,5 +38,6 @@ sylius:
all: Semua
select: Pilih
select_scope: Pilih skop
priority: Keutamaan
zone_member:
select: Pilih

View file

@ -37,5 +37,6 @@ sylius:
scopes:
all: 'Alle'
select: 'Selecteer'
priority: 'Prioriteit'
zone_member:
select: 'Selecteer'

View file

@ -37,6 +37,7 @@ sylius:
scopes:
all: 'Alle'
select: 'Velg'
priority: 'Prioritet'
zone_member:
select: 'Velg'

View file

@ -38,5 +38,6 @@ sylius:
all: Wszystko
select: Wybierz
select_scope: Wybierz zakres
priority: Priorytet
zone_member:
select: Wybierz

View file

@ -37,6 +37,7 @@ sylius:
scopes:
all: 'Todos'
select: 'Selecionar'
priority: 'Prioridade'
zone_member:
select: 'Selecionar'

View file

@ -38,5 +38,6 @@ sylius:
all: Todos(as)
select: Selecione
select_scope: Selecione o escopo
priority: Prioridade
zone_member:
select: Selecione

View file

@ -37,5 +37,6 @@ sylius:
scopes:
all: 'Toate'
select: 'Selectați'
priority: 'Prioritate'
zone_member:
select: 'Selectați'

View file

@ -38,5 +38,6 @@ sylius:
all: Все
select: Выбрать
select_scope: Выберите область
priority: Приоритет
zone_member:
select: Выбрать

View file

@ -37,5 +37,6 @@ sylius:
scopes:
all: 'Všetko'
select: 'Vybrať'
priority: 'Priorita'
zone_member:
select: 'Vybrať'

View file

@ -36,5 +36,6 @@ sylius:
scope: Področje
select: Izberite
select_scope: Izberite področje
priority: Prednost
zone_member:
select: Izberite

View file

@ -37,5 +37,6 @@ sylius:
all: Të gjitha
select: Zgjidh
select_scope: Zgjidh zonën
priority: Prioriteti
zone_member:
select: Zgjidh

View file

@ -35,5 +35,6 @@ sylius:
scope: Обухват
select: Изаберите
select_scope: Изабери обухват
priority: Приоритет
zone_member:
select: Изаберите

View file

@ -38,5 +38,6 @@ sylius:
all: Sve
select: Izaberite
select_scope: Izaberite adresu isporuke
priority: Prioritet
zone_member:
select: Izaberite

View file

@ -37,5 +37,6 @@ sylius:
scopes:
all: Alla
select: Välj
priority: Prioritet
zone_member:
select: Välj

View file

@ -38,5 +38,6 @@ sylius:
all: ทั้งหมด
select: เลือก
select_scope: เลือกขอบเขต
priority: ลำดับความสำคัญ
zone_member:
select: เลือก

View file

@ -38,5 +38,6 @@ sylius:
all: Tümü
select: Seçiniz
select_scope: Ölçek seçiniz
priority: Öncelik
zone_member:
select: Seçiniz

View file

@ -38,5 +38,6 @@ sylius:
all: Всі(-е)
select: Виберіть
select_scope: Виберіть покриття
priority: Пріоритет
zone_member:
select: Виберіть

View file

@ -38,5 +38,6 @@ sylius:
all: Tất cả
select: Chọn
select_scope: Chọn phạm vi
priority: Ưu tiên
zone_member:
select: Chọn

View file

@ -37,5 +37,6 @@ sylius:
scopes:
all: '所有'
select: '选择'
priority: '优先级'
zone_member:
select: '选择'

View file

@ -37,5 +37,6 @@ sylius:
all: 全部
select: 請選擇
select_scope: 選擇範圍
priority: 優先順序
zone_member:
select: 請選擇

View file

@ -41,7 +41,6 @@
"doctrine/doctrine-bundle": "^2.13",
"doctrine/orm": "^2.18 || ^3.3",
"matthiasnoback/symfony-dependency-injection-test": "^6.0",
"phpspec/prophecy-phpunit": "^2.2",
"phpunit/phpunit": "^11.5",
"symfony/browser-kit": "^6.4 || ^7.2",
"symfony/dependency-injection": "^6.4.1 || ^7.2",

View file

@ -15,9 +15,7 @@ namespace Tests\Sylius\Bundle\AddressingBundle\Form\Type;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\Attributes\Test;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Prophecy\Prophecy\ProphecyInterface;
use PHPUnit\Framework\MockObject\MockObject;
use Sylius\Bundle\AddressingBundle\Form\Type\CountryChoiceType;
use Sylius\Component\Addressing\Model\CountryInterface;
use Sylius\Resource\Doctrine\Persistence\RepositoryInterface;
@ -27,48 +25,37 @@ use Symfony\Component\Form\Test\TypeTestCase;
final class CountryChoiceTypeTest extends TypeTestCase
{
use ProphecyTrait;
private RepositoryInterface&MockObject $countryRepository;
private ObjectProphecy $countryRepository;
private CountryInterface&MockObject $france;
/** @var ProphecyInterface|CountryInterface */
private $france;
private CountryInterface&MockObject $poland;
/** @var ProphecyInterface|CountryInterface */
private $poland;
/** @var ProphecyInterface|CountryInterface */
private $austria;
private CountryInterface&MockObject $austria;
protected function setUp(): void
{
$this->countryRepository = $this->prophesize(RepositoryInterface::class);
$this->countryRepository = $this->createMock(RepositoryInterface::class);
/** @var ProphecyInterface|CountryInterface $france */
$france = $this->prophesize(CountryInterface::class);
$france->getCode()->willReturn('FR');
$france->getName()->willReturn('France');
$this->france = $france;
$this->france = $this->createMock(CountryInterface::class);
$this->france->method('getCode')->willReturn('FR');
$this->france->method('getName')->willReturn('France');
/** @var ProphecyInterface|CountryInterface $poland */
$poland = $this->prophesize(CountryInterface::class);
$poland->getCode()->willReturn('PL');
$poland->getName()->willReturn('Poland');
$this->poland = $poland;
$this->poland = $this->createMock(CountryInterface::class);
$this->poland->method('getCode')->willReturn('PL');
$this->poland->method('getName')->willReturn('Poland');
/** @var ProphecyInterface|CountryInterface $austria */
$austria = $this->prophesize(CountryInterface::class);
$austria->getCode()->willReturn('AT');
$austria->getName()->willReturn('Austria');
$austria->isEnabled()->willReturn(false);
$this->austria = $austria;
$this->austria = $this->createMock(CountryInterface::class);
$this->austria->method('getCode')->willReturn('AT');
$this->austria->method('getName')->willReturn('Austria');
$this->austria->method('isEnabled')->willReturn(false);
parent::setUp();
}
protected function getExtensions(): array
{
$type = new CountryChoiceType($this->countryRepository->reveal());
$type = new CountryChoiceType($this->countryRepository);
return [
new PreloadedExtension([$type], []),
@ -78,10 +65,14 @@ final class CountryChoiceTypeTest extends TypeTestCase
#[Test]
public function it_returns_only_enabled_countries_by_default(): void
{
$this->countryRepository->findBy(['enabled' => true])->willReturn([
$this->france->reveal(),
$this->poland->reveal(),
]);
$this->countryRepository
->expects($this->once())
->method('findBy')
->with(['enabled' => true])
->willReturn([
$this->france,
$this->poland,
]);
$this->assertChoicesLabels(['France', 'Poland']);
}
@ -89,11 +80,14 @@ final class CountryChoiceTypeTest extends TypeTestCase
#[Test]
public function it_returns_all_countries_when_option_enabled_is_false(): void
{
$this->countryRepository->findAll()->willReturn([
$this->france->reveal(),
$this->poland->reveal(),
$this->austria->reveal(),
]);
$this->countryRepository
->expects($this->once())
->method('findAll')
->willReturn([
$this->france,
$this->poland,
$this->austria,
]);
$this->assertChoicesLabels(['Austria', 'France', 'Poland'], ['enabled' => false]);
}
@ -101,10 +95,14 @@ final class CountryChoiceTypeTest extends TypeTestCase
#[Test]
public function it_returns_enabled_countries_in_an_alphabetical_order(): void
{
$this->countryRepository->findBy(['enabled' => true])->willReturn([
$this->poland->reveal(),
$this->france->reveal(),
]);
$this->countryRepository
->expects($this->once())
->method('findBy')
->with(['enabled' => true])
->willReturn([
$this->poland,
$this->france,
]);
$this->assertChoicesLabels(['France', 'Poland']);
}
@ -112,11 +110,14 @@ final class CountryChoiceTypeTest extends TypeTestCase
#[Test]
public function it_returns_all_countries_in_an_alphabetical_order(): void
{
$this->countryRepository->findAll()->willReturn([
$this->poland->reveal(),
$this->france->reveal(),
$this->austria->reveal(),
]);
$this->countryRepository
->expects($this->once())
->method('findAll')
->willReturn([
$this->poland,
$this->france,
$this->austria,
]);
$this->assertChoicesLabels(['Austria', 'France', 'Poland'], ['enabled' => false]);
}
@ -124,11 +125,14 @@ final class CountryChoiceTypeTest extends TypeTestCase
#[Test]
public function it_returns_all_filtered_out_countries(): void
{
$this->countryRepository->findAll()->willReturn([
$this->france->reveal(),
$this->poland->reveal(),
$this->austria->reveal(),
]);
$this->countryRepository
->expects($this->once())
->method('findAll')
->willReturn([
$this->france,
$this->poland,
$this->austria,
]);
$this->assertChoicesLabels(['Poland'], ['choice_filter' => static fn (?CountryInterface $country): bool => $country !== null && $country->getName() === 'Poland', 'enabled' => false]);
}
@ -136,10 +140,14 @@ final class CountryChoiceTypeTest extends TypeTestCase
#[Test]
public function it_returns_enabled_filtered_out_countries(): void
{
$this->countryRepository->findBy(['enabled' => true])->willReturn([
$this->france->reveal(),
$this->poland->reveal(),
]);
$this->countryRepository
->expects($this->once())
->method('findBy')
->with(['enabled' => true])
->willReturn([
$this->france,
$this->poland,
]);
$this->assertChoicesLabels(['Poland'], ['choice_filter' => static fn (?CountryInterface $country): bool => $country !== null && $country->getName() === 'Poland']);
}

View file

@ -15,9 +15,7 @@ namespace Tests\Sylius\Bundle\AddressingBundle\Form\Type;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\Attributes\Test;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Prophecy\Prophecy\ProphecyInterface;
use PHPUnit\Framework\MockObject\MockObject;
use Sylius\Bundle\AddressingBundle\Form\Type\ZoneChoiceType;
use Sylius\Component\Addressing\Model\Scope as AddressingScope;
use Sylius\Component\Addressing\Model\ZoneInterface;
@ -28,39 +26,31 @@ use Symfony\Component\Form\Test\TypeTestCase;
final class ZoneChoiceTypeTest extends TypeTestCase
{
use ProphecyTrait;
private RepositoryInterface&MockObject $zoneRepository;
private ObjectProphecy $zoneRepository;
private ZoneInterface&MockObject $zoneAllScopes;
/** @var ProphecyInterface|ZoneInterface */
private $zoneAllScopes;
private ZoneInterface&MockObject $zoneTaxScope;
/** @var ProphecyInterface|ZoneInterface */
private $zoneTaxScope;
/** @var ProphecyInterface|ZoneInterface */
private $zoneShippingScope;
private ZoneInterface&MockObject $zoneShippingScope;
protected function setUp(): void
{
$this->zoneRepository = $this->prophesize(RepositoryInterface::class);
$this->zoneRepository = $this->createMock(RepositoryInterface::class);
/** @var ProphecyInterface|ZoneInterface $zoneAllScopes */
$zoneAllScopes = $this->prophesize(ZoneInterface::class);
$zoneAllScopes->getCode()->willReturn('all');
$zoneAllScopes->getName()->willReturn('All');
$zoneAllScopes = $this->createMock(ZoneInterface::class);
$zoneAllScopes->method('getCode')->willReturn('all');
$zoneAllScopes->method('getName')->willReturn('All');
$this->zoneAllScopes = $zoneAllScopes;
/** @var ProphecyInterface|ZoneInterface $zoneTaxScope */
$zoneTaxScope = $this->prophesize(ZoneInterface::class);
$zoneTaxScope->getCode()->willReturn('tax');
$zoneTaxScope->getName()->willReturn('Tax');
$zoneTaxScope = $this->createMock(ZoneInterface::class);
$zoneTaxScope->method('getCode')->willReturn('tax');
$zoneTaxScope->method('getName')->willReturn('Tax');
$this->zoneTaxScope = $zoneTaxScope;
/** @var ProphecyInterface|ZoneInterface $zoneShippingScope */
$zoneShippingScope = $this->prophesize(ZoneInterface::class);
$zoneShippingScope->getCode()->willReturn('shipping');
$zoneShippingScope->getName()->willReturn('Shipping');
$zoneShippingScope = $this->createMock(ZoneInterface::class);
$zoneShippingScope->method('getCode')->willReturn('shipping');
$zoneShippingScope->method('getName')->willReturn('Shipping');
$this->zoneShippingScope = $zoneShippingScope;
parent::setUp();
@ -74,7 +64,7 @@ final class ZoneChoiceTypeTest extends TypeTestCase
'shipping' => 'Shipping',
];
$type = new ZoneChoiceType($this->zoneRepository->reveal(), $scopeTypes);
$type = new ZoneChoiceType($this->zoneRepository, $scopeTypes);
return [
new PreloadedExtension([$type], []),
@ -84,10 +74,10 @@ final class ZoneChoiceTypeTest extends TypeTestCase
#[Test]
public function it_returns_all_scopes_by_default(): void
{
$this->zoneRepository->findBy([])->willReturn([
$this->zoneAllScopes->reveal(),
$this->zoneTaxScope->reveal(),
$this->zoneShippingScope->reveal(),
$this->zoneRepository->method('findBy')->with([])->willReturn([
$this->zoneAllScopes,
$this->zoneTaxScope,
$this->zoneShippingScope,
]);
$this->assertChoicesLabels(['All', 'Tax', 'Shipping']);
@ -96,10 +86,10 @@ final class ZoneChoiceTypeTest extends TypeTestCase
#[Test]
public function it_returns_all_scopes_when_zone_scope_set_to_all(): void
{
$this->zoneRepository->findBy([])->willReturn([
$this->zoneAllScopes->reveal(),
$this->zoneTaxScope->reveal(),
$this->zoneShippingScope->reveal(),
$this->zoneRepository->method('findBy')->with([])->willReturn([
$this->zoneAllScopes,
$this->zoneTaxScope,
$this->zoneShippingScope,
]);
$this->assertChoicesLabels(['All', 'Tax', 'Shipping'], ['zone_scope' => AddressingScope::ALL]);
@ -108,9 +98,9 @@ final class ZoneChoiceTypeTest extends TypeTestCase
#[Test]
public function it_returns_tax_scopes_when_zone_scope_set_to_tax(): void
{
$this->zoneRepository->findBy(['scope' => ['tax', AddressingScope::ALL]])->willReturn([
$this->zoneAllScopes->reveal(),
$this->zoneTaxScope->reveal(),
$this->zoneRepository->method('findBy')->with(['scope' => ['tax', AddressingScope::ALL]])->willReturn([
$this->zoneAllScopes,
$this->zoneTaxScope,
]);
$this->assertChoicesLabels(['All', 'Tax'], ['zone_scope' => 'tax']);
@ -119,9 +109,9 @@ final class ZoneChoiceTypeTest extends TypeTestCase
#[Test]
public function it_returns_shipping_scopes_when_zone_scope_set_to_shipping(): void
{
$this->zoneRepository->findBy(['scope' => ['shipping', AddressingScope::ALL]])->willReturn([
$this->zoneAllScopes->reveal(),
$this->zoneShippingScope->reveal(),
$this->zoneRepository->method('findBy')->with(['scope' => ['shipping', AddressingScope::ALL]])->willReturn([
$this->zoneAllScopes,
$this->zoneShippingScope,
]);
$this->assertChoicesLabels(['All', 'Shipping'], ['zone_scope' => 'shipping']);

View file

@ -15,3 +15,11 @@ sylius_twig_hooks:
image:
template: '@SyliusAdmin/shared/crud/common/sidebar/logo/image.html.twig'
priority: 0
'sylius_admin.common.component.sidebar.menu':
search:
template: '@SyliusAdmin/shared/crud/common/sidebar/search.html.twig'
priority: 100
items:
template: '@SyliusAdmin/shared/crud/common/sidebar/menu/items.html.twig'
priority: 0

View file

@ -44,6 +44,12 @@ sylius_twig_hooks:
enabled:
template: '@SyliusAdmin/shipping_method/form/general/enabled.html.twig'
priority: 200
min_delivery_time_days:
template: '@SyliusAdmin/shipping_method/form/general/min_delivery_time_days.html.twig'
priority: 150
max_delivery_time_days:
template: '@SyliusAdmin/shipping_method/form/general/max_delivery_time_days.html.twig'
priority: 125
zone:
template: '@SyliusAdmin/shipping_method/form/general/zone.html.twig'
priority: 100

View file

@ -46,6 +46,12 @@ sylius_twig_hooks:
enabled:
template: '@SyliusAdmin/shipping_method/form/general/enabled.html.twig'
priority: 200
min_delivery_time_days:
template: '@SyliusAdmin/shipping_method/form/general/min_delivery_time_days.html.twig'
priority: 150
max_delivery_time_days:
template: '@SyliusAdmin/shipping_method/form/general/max_delivery_time_days.html.twig'
priority: 125
zone:
template: '@SyliusAdmin/shipping_method/form/general/zone.html.twig'
priority: 100

View file

@ -43,3 +43,6 @@ sylius_twig_hooks:
scope:
template: '@SyliusAdmin/zone/form/sections/general/scope.html.twig'
priority: 0
priority:
template: '@SyliusAdmin/zone/form/sections/general/priority.html.twig'
priority: -100

View file

@ -45,3 +45,6 @@ sylius_twig_hooks:
scope:
template: '@SyliusAdmin/zone/form/sections/general/scope.html.twig'
priority: 0
priority:
template: '@SyliusAdmin/zone/form/sections/general/priority.html.twig'
priority: -100

View file

@ -5,7 +5,18 @@ sylius_grid:
name: doctrine/orm
options:
class: "%sylius.model.zone.class%"
sorting:
priority: desc
fields:
priority:
type: twig
label: sylius.ui.priority
sortable: ~
options:
template: "@SyliusAdmin/zone/grid/field/priority.html.twig"
vars:
th_class: "w-1 text-center"
td_class: "text-center"
name:
type: twig
label: sylius.ui.name

View file

@ -51,7 +51,6 @@
"doctrine/orm": "^2.18 || ^3.3",
"matthiasnoback/symfony-dependency-injection-test": "^6.0",
"php-http/message-factory": "^1.1",
"phpspec/prophecy-phpunit": "^2.2",
"phpunit/phpunit": "^11.5",
"symfony/dependency-injection": "^6.4.1 || ^7.2",
"symfony/dotenv": "^6.4 || ^7.2"

View file

@ -1,4 +1,3 @@
<div class="collapse navbar-collapse" id="sidebar-menu">
{% include '@SyliusAdmin/shared/crud/common/sidebar/search.html.twig' %}
{{ knp_menu_render('sylius_admin.main', {'template': '@SyliusAdmin/shared/crud/common/sidebar/menu/menu.html.twig', 'currentClass': 'active'}) }}
{% hook 'menu' %}
</div>

View file

@ -0,0 +1 @@
{{ knp_menu_render('sylius_admin.main', {'template': '@SyliusAdmin/shared/crud/common/sidebar/menu/menu.html.twig', 'currentClass': 'active'}) }}

View file

@ -0,0 +1,3 @@
<div class="col-12 col-lg-6">
{{ form_row(hookable_metadata.context.form.maxDeliveryTimeDays) }}
</div>

View file

@ -0,0 +1,3 @@
<div class="col-12 col-lg-6">
{{ form_row(hookable_metadata.context.form.minDeliveryTimeDays) }}
</div>

View file

@ -0,0 +1,3 @@
<div class="col-12 col-md-6">
{{ form_row(hookable_metadata.context.form.priority, sylius_test_form_attribute('priority')) }}
</div>

View file

@ -0,0 +1 @@
<span class="badge bg-blue-lt">{{ data }}</span>

View file

@ -15,10 +15,8 @@ namespace Tests\Sylius\Bundle\AdminBundle\Notification;
use GuzzleHttp\Exception\ConnectException;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Clock\ClockInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
@ -34,19 +32,17 @@ use Symfony\Contracts\Cache\CacheInterface;
final class HubNotificationProviderTest extends TestCase
{
use ProphecyTrait;
private ClientInterface&MockObject $client;
private ClientInterface|ObjectProphecy $client;
private RequestStack&MockObject $requestStack;
private ObjectProphecy|RequestStack $requestStack;
private RequestFactoryInterface&MockObject $requestFactory;
private ObjectProphecy|RequestFactoryInterface $requestFactory;
private StreamFactoryInterface&MockObject $streamFactory;
private ObjectProphecy|StreamFactoryInterface $streamFactory;
private CacheInterface&MockObject $cache;
private CacheInterface|ObjectProphecy $cache;
private ClockInterface|ObjectProphecy $clock;
private ClockInterface&MockObject $clock;
private HubNotificationProvider $hubNotificationsProvider;
@ -56,20 +52,20 @@ final class HubNotificationProviderTest extends TestCase
{
parent::setUp();
$this->client = $this->prophesize(ClientInterface::class);
$this->requestStack = $this->prophesize(RequestStack::class);
$this->requestFactory = $this->prophesize(RequestFactoryInterface::class);
$this->streamFactory = $this->prophesize(StreamFactoryInterface::class);
$this->cache = $this->prophesize(CacheInterface::class);
$this->clock = $this->prophesize(ClockInterface::class);
$this->client = $this->createMock(ClientInterface::class);
$this->requestStack = $this->createMock(RequestStack::class);
$this->requestFactory = $this->createMock(RequestFactoryInterface::class);
$this->streamFactory = $this->createMock(StreamFactoryInterface::class);
$this->cache = $this->createMock(CacheInterface::class);
$this->clock = $this->createMock(ClockInterface::class);
$this->hubNotificationsProvider = new HubNotificationProvider(
$this->client->reveal(),
$this->requestStack->reveal(),
$this->requestFactory->reveal(),
$this->streamFactory->reveal(),
$this->cache->reveal(),
$this->clock->reveal(),
$this->client,
$this->requestStack,
$this->requestFactory,
$this->streamFactory,
$this->cache,
$this->clock,
self::$hubUri,
'prod',
true,
@ -80,20 +76,51 @@ final class HubNotificationProviderTest extends TestCase
#[Test]
public function it_returns_an_empty_array_if_client_exception_occurs(): void
{
$request = $this->prophesize(RequestInterface::class);
$stream = $this->prophesize(StreamInterface::class);
$request = $this->createMock(RequestInterface::class);
$stream = $this->createMock(StreamInterface::class);
$this->cache->get('latest_sylius_version', Argument::type('callable'))->will(fn ($args) => $args[1]());
$this->cache
->expects($this->once())
->method('get')
->with('latest_sylius_version', $this->isType('callable'))
->willReturnCallback(fn ($key, $callback) => $callback());
$this->requestStack->getCurrentRequest()->willReturn(new Request());
$this->clock->now()->willReturn(new \DateTimeImmutable());
$this->streamFactory->createStream(Argument::cetera())->willReturn($stream);
$this->requestStack
->expects($this->once())
->method('getCurrentRequest')
->willReturn(new Request());
$this->requestFactory->createRequest(Argument::cetera())->willReturn($request);
$request->withHeader('Content-Type', 'application/json')->willReturn($request);
$request->withBody($stream)->willReturn($request);
$this->clock
->expects($this->once())
->method('now')
->willReturn(new \DateTimeImmutable());
$this->client->sendRequest(Argument::cetera())->willThrow(ConnectException::class);
$this->streamFactory
->expects($this->once())
->method('createStream')
->willReturn($stream);
$this->requestFactory
->expects($this->once())
->method('createRequest')
->willReturn($request);
$request
->expects($this->once())
->method('withHeader')
->with('Content-Type', 'application/json')
->willReturn($request);
$request
->expects($this->once())
->method('withBody')
->with($stream)
->willReturn($request);
$this->client
->expects($this->once())
->method('sendRequest')
->willThrowException(new ConnectException('Connection failed', $this->createMock(\Psr\Http\Message\RequestInterface::class)));
$this->assertEmpty($this->hubNotificationsProvider->getNotifications());
}
@ -101,25 +128,63 @@ final class HubNotificationProviderTest extends TestCase
#[Test]
public function it_returns_an_empty_array_if_the_current_version_is_the_same_as_latest(): void
{
$request = $this->prophesize(RequestInterface::class);
$stream = $this->prophesize(StreamInterface::class);
$externalResponse = $this->prophesize(ResponseInterface::class);
$request = $this->createMock(RequestInterface::class);
$stream = $this->createMock(StreamInterface::class);
$externalResponse = $this->createMock(ResponseInterface::class);
$this->cache->get('latest_sylius_version', Argument::type('callable'))->will(fn ($args) => $args[1]());
$this->cache
->expects($this->once())
->method('get')
->with('latest_sylius_version', $this->isType('callable'))
->willReturnCallback(fn ($key, $callback) => $callback());
$this->requestStack->getCurrentRequest()->willReturn(new Request());
$this->clock->now()->willReturn(new \DateTimeImmutable());
$this->streamFactory->createStream(Argument::cetera())->willReturn($stream);
$this->requestStack
->expects($this->once())
->method('getCurrentRequest')
->willReturn(new Request());
$this->clock
->expects($this->once())
->method('now')
->willReturn(new \DateTimeImmutable());
$this->streamFactory
->expects($this->once())
->method('createStream')
->willReturn($stream);
$content = json_encode(['version' => SyliusCoreBundle::VERSION]);
$stream->getContents()->willReturn($content);
$stream
->expects($this->once())
->method('getContents')
->willReturn($content);
$this->requestFactory->createRequest(Argument::cetera())->willReturn($request);
$request->withHeader('Content-Type', 'application/json')->willReturn($request);
$request->withBody($stream)->willReturn($request);
$this->requestFactory
->expects($this->once())
->method('createRequest')
->willReturn($request);
$externalResponse->getBody()->willReturn($stream->reveal());
$this->client->sendRequest(Argument::cetera())->willReturn($externalResponse->reveal());
$request
->expects($this->once())
->method('withHeader')
->with('Content-Type', 'application/json')
->willReturn($request);
$request
->expects($this->once())
->method('withBody')
->with($stream)
->willReturn($request);
$externalResponse
->expects($this->once())
->method('getBody')
->willReturn($stream);
$this->client
->expects($this->once())
->method('sendRequest')
->willReturn($externalResponse);
$this->assertEmpty($this->hubNotificationsProvider->getNotifications());
}
@ -127,25 +192,63 @@ final class HubNotificationProviderTest extends TestCase
#[Test]
public function it_returns_a_notification_if_the_current_version_is_different_than_latest(): void
{
$request = $this->prophesize(RequestInterface::class);
$stream = $this->prophesize(StreamInterface::class);
$externalResponse = $this->prophesize(ResponseInterface::class);
$request = $this->createMock(RequestInterface::class);
$stream = $this->createMock(StreamInterface::class);
$externalResponse = $this->createMock(ResponseInterface::class);
$this->cache->get('latest_sylius_version', Argument::type('callable'))->will(fn ($args) => $args[1]());
$this->cache
->expects($this->once())
->method('get')
->with('latest_sylius_version', $this->isType('callable'))
->willReturnCallback(fn ($key, $callback) => $callback());
$this->requestStack->getCurrentRequest()->willReturn(new Request());
$this->clock->now()->willReturn(new \DateTimeImmutable());
$this->streamFactory->createStream(Argument::cetera())->willReturn($stream);
$this->requestStack
->expects($this->once())
->method('getCurrentRequest')
->willReturn(new Request());
$this->clock
->expects($this->once())
->method('now')
->willReturn(new \DateTimeImmutable());
$this->streamFactory
->expects($this->once())
->method('createStream')
->willReturn($stream);
$content = json_encode(['version' => '1.0.0']);
$stream->getContents()->willReturn($content);
$stream
->expects($this->once())
->method('getContents')
->willReturn($content);
$this->requestFactory->createRequest(Argument::cetera())->willReturn($request);
$request->withHeader('Content-Type', 'application/json')->willReturn($request);
$request->withBody($stream)->willReturn($request);
$this->requestFactory
->expects($this->once())
->method('createRequest')
->willReturn($request);
$externalResponse->getBody()->willReturn($stream->reveal());
$this->client->sendRequest(Argument::cetera())->willReturn($externalResponse->reveal());
$request
->expects($this->once())
->method('withHeader')
->with('Content-Type', 'application/json')
->willReturn($request);
$request
->expects($this->once())
->method('withBody')
->with($stream)
->willReturn($request);
$externalResponse
->expects($this->once())
->method('getBody')
->willReturn($stream);
$this->client
->expects($this->once())
->method('sendRequest')
->willReturn($externalResponse);
$notifications = $this->hubNotificationsProvider->getNotifications();

View file

@ -14,17 +14,14 @@ declare(strict_types=1);
namespace Tests\Sylius\Bundle\AdminBundle\Twig\Component\Shared\Navbar;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Sylius\Bundle\AdminBundle\Notification\NotificationProviderInterface;
use Sylius\Bundle\AdminBundle\Twig\Component\Shared\Navbar\NotificationsComponent;
final class NotificationsComponentTest extends TestCase
{
use ProphecyTrait;
private NotificationProviderInterface|ObjectProphecy $notificationProvider;
private NotificationProviderInterface&MockObject $notificationProvider;
private NotificationsComponent $notificationsComponent;
@ -34,15 +31,15 @@ final class NotificationsComponentTest extends TestCase
{
parent::setUp();
$this->notificationProvider = $this->prophesize(NotificationProviderInterface::class);
$this->notificationProvider = $this->createMock(NotificationProviderInterface::class);
$this->notificationsComponent = new NotificationsComponent($this->notificationProvider->reveal(), true);
$this->notificationsComponent = new NotificationsComponent($this->notificationProvider, true);
}
#[Test]
public function it_gets_notifications_from_provider(): void
{
$this->notificationProvider->getNotifications()->willReturn(['version' => ['message' => 'sylius.ui.notifications.new_version_of_sylius_available']]);
$this->notificationProvider->method('getNotifications')->willReturn(['version' => ['message' => 'sylius.ui.notifications.new_version_of_sylius_available']]);
$notifications = $this->notificationsComponent->getNotifications();

View file

@ -49,6 +49,7 @@ api_platform:
ApiPlatform\Metadata\Exception\InvalidArgumentException: 400
ApiPlatform\Validator\Exception\ValidationException: 422
Doctrine\ORM\OptimisticLockException: 409
Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException: 422
Symfony\Component\Serializer\Exception\ExceptionInterface: 400
# Sylius exception to status code mapping
@ -71,7 +72,6 @@ api_platform:
Sylius\Component\Payment\Exception\InvalidPaymentRequestPayloadException: 422
Sylius\Component\Promotion\Exception\FailedGenerationException: 422
Symfony\Component\Serializer\Exception\UnexpectedValueException: 400
Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException: 400
Sylius\Bundle\ApiBundle\CommandHandler\Checkout\Exception\OrderTotalHasChangedException: 409
Sylius\Bundle\ApiBundle\Serializer\Exception\InvalidAmountTypeException: 400
defaults:

View file

@ -41,6 +41,12 @@
<group>sylius:admin:zone:index</group>
<group>sylius:admin:zone:show</group>
</attribute>
<attribute name="priority">
<group>sylius:admin:zone:create</group>
<group>sylius:admin:zone:index</group>
<group>sylius:admin:zone:show</group>
<group>sylius:admin:zone:update</group>
</attribute>
<attribute name="createdAt">
<group>sylius:admin:zone:index</group>
<group>sylius:admin:zone:show</group>

View file

@ -42,7 +42,7 @@ final readonly class CommandNormalizer implements NormalizerInterface
}
/**
* @return array{code: int, message: string}
* @return array{message: string}
*/
public function normalize(mixed $object, ?string $format = null, array $context = []): array
{
@ -50,7 +50,6 @@ final readonly class CommandNormalizer implements NormalizerInterface
$data = $this->objectNormalizer->normalize($object, $format, $context);
return [
'code' => 400,
'message' => $data['message'],
];
}

View file

@ -25,8 +25,8 @@
"require": {
"php": "^8.2",
"doctrine/dbal": "^3.9",
"api-platform/doctrine-orm": "^4.1.7",
"api-platform/symfony": "^4.1.7",
"api-platform/doctrine-orm": "^4.2.1",
"api-platform/symfony": "^4.2.1",
"lexik/jwt-authentication-bundle": "^3.1",
"sylius/core-bundle": "^2.0",
"sylius/payum-bundle": "^2.0",
@ -38,7 +38,6 @@
"matthiasnoback/symfony-dependency-injection-test": "^6.0",
"nelmio/alice": "^3.13",
"phparkitect/phparkitect": "^0.6",
"phpspec/prophecy-phpunit": "^2.2",
"phpunit/phpunit": "^11.5",
"symfony/browser-kit": "^6.4 || ^7.2",
"symfony/debug-bundle": "^6.4 || ^7.2",

View file

@ -14,9 +14,8 @@ declare(strict_types=1);
namespace Tests\Sylius\Bundle\ApiBundle\ApiPlatform\Routing;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Sylius\Bundle\ApiBundle\ApiPlatform\Routing\ApiLoader;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Config\Loader\LoaderResolverInterface;
@ -24,18 +23,16 @@ use Symfony\Component\Routing\RouteCollection;
final class ApiLoaderTest extends TestCase
{
use ProphecyTrait;
private LoaderInterface|ObjectProphecy $baseApiLoader;
private LoaderInterface&MockObject $baseApiLoader;
private LoaderInterface $apiLoader;
protected function setUp(): void
{
$this->baseApiLoader = $this->prophesize(LoaderInterface::class);
$this->baseApiLoader = $this->createMock(LoaderInterface::class);
$this->apiLoader = new ApiLoader(
$this->baseApiLoader->reveal(),
$this->baseApiLoader,
[
'sylius_api_shop_currency_get',
'sylius_api_shop_currency_get_collection',
@ -52,24 +49,33 @@ final class ApiLoaderTest extends TestCase
#[Test]
public function it_removes_routes_from_route_collection_loaded_by_base_api_loader(): void
{
$routeCollection = $this->prophesize(RouteCollection::class);
$routeCollection = $this->createMock(RouteCollection::class);
$this->baseApiLoader
->expects($this->once())
->method('load')
->with('.', 'api_platform')
->willReturn($routeCollection);
$this->baseApiLoader->load('.', 'api_platform')->willReturn($routeCollection);
$routeCollection
->remove([
->expects($this->once())
->method('remove')
->with([
'sylius_api_shop_currency_get',
'sylius_api_shop_currency_get_collection',
])
->shouldBeCalled()
;
]);
$this->assertSame($routeCollection->reveal(), $this->apiLoader->load('.', 'api_platform'));
$this->assertSame($routeCollection, $this->apiLoader->load('.', 'api_platform'));
}
#[Test]
public function it_uses_base_api_loader_for_supports_method(): void
{
$this->baseApiLoader->supports('.', 'api_platform')->willReturn(true);
$this->baseApiLoader
->expects($this->once())
->method('supports')
->with('.', 'api_platform')
->willReturn(true);
$this->assertTrue($this->apiLoader->supports('.', 'api_platform'));
}
@ -77,20 +83,26 @@ final class ApiLoaderTest extends TestCase
#[Test]
public function it_uses_base_api_loader_to_get_resolver(): void
{
$loaderResolver = $this->prophesize(LoaderResolverInterface::class);
$loaderResolver = $this->createMock(LoaderResolverInterface::class);
$this->baseApiLoader->getResolver()->willReturn($loaderResolver);
$this->baseApiLoader
->expects($this->once())
->method('getResolver')
->willReturn($loaderResolver);
$this->assertSame($loaderResolver->reveal(), $this->apiLoader->getResolver());
$this->assertSame($loaderResolver, $this->apiLoader->getResolver());
}
#[Test]
public function it_uses_base_api_loader_to_set_resolver(): void
{
$loaderResolver = $this->prophesize(LoaderResolverInterface::class);
$loaderResolver = $this->createMock(LoaderResolverInterface::class);
$this->baseApiLoader->setResolver($loaderResolver->reveal())->shouldBeCalled();
$this->baseApiLoader
->expects($this->once())
->method('setResolver')
->with($loaderResolver);
$this->apiLoader->setResolver($loaderResolver->reveal());
$this->apiLoader->setResolver($loaderResolver);
}
}

View file

@ -38,7 +38,7 @@ final class FooApiCommandTest extends ApiTestCase
$this->assertResponseHeaderSame('content-type', 'application/problem+json; charset=utf-8');
$this->assertResponseStatusCodeSame(Response::HTTP_BAD_REQUEST);
$this->assertResponseStatusCodeSame(Response::HTTP_UNPROCESSABLE_ENTITY);
$this->assertJsonContains([
'hydra:description' => 'Request does not have the following required fields specified: bar.',
'hydra:title' => 'An error occurred',

View file

@ -15,6 +15,7 @@ namespace Sylius\Bundle\ApiBundle\Application\Tests;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use PHPUnit\Framework\Attributes\Test;
use Symfony\Component\HttpFoundation\Response;
final class MergingConfigsTest extends ApiTestCase
{
@ -79,7 +80,7 @@ final class MergingConfigsTest extends ApiTestCase
],
]);
self::assertResponseStatusCodeSame(400);
self::assertResponseStatusCodeSame(Response::HTTP_UNPROCESSABLE_ENTITY);
static::createClient()->request('POST', '/api/v2/shop/bar', [
'json' => [

View file

@ -14,9 +14,8 @@ declare(strict_types=1);
namespace Tests\Sylius\Bundle\ApiBundle\CommandHandler;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Sylius\Bundle\ApiBundle\Command\Payment\AddPaymentRequest;
use Sylius\Bundle\ApiBundle\CommandHandler\Payment\AddPaymentRequestHandler;
use Sylius\Bundle\ApiBundle\Exception\PaymentMethodNotFoundException;
@ -33,38 +32,36 @@ use Sylius\Component\Payment\Repository\PaymentRequestRepositoryInterface;
final class AddPaymentRequestHandlerTest extends TestCase
{
use ProphecyTrait;
private PaymentMethodRepositoryInterface&MockObject $paymentMethodRepository;
private ObjectProphecy|PaymentMethodRepositoryInterface $paymentMethodRepository;
private PaymentRepositoryInterface&MockObject $paymentRepository;
private ObjectProphecy|PaymentRepositoryInterface $paymentRepository;
private PaymentRequestFactoryInterface&MockObject $paymentRequestFactory;
private ObjectProphecy|PaymentRequestFactoryInterface $paymentRequestFactory;
private PaymentRequestRepositoryInterface&MockObject $paymentRequestRepository;
private ObjectProphecy|PaymentRequestRepositoryInterface $paymentRequestRepository;
private DefaultActionProviderInterface&MockObject $defaultActionProvider;
private DefaultActionProviderInterface|ObjectProphecy $defaultActionProvider;
private DefaultPayloadProviderInterface|ObjectProphecy $defaultPayloadProvider;
private DefaultPayloadProviderInterface&MockObject $defaultPayloadProvider;
private AddPaymentRequestHandler $addPaymentRequestHandler;
protected function setUp(): void
{
$this->paymentMethodRepository = $this->prophesize(PaymentMethodRepositoryInterface::class);
$this->paymentRepository = $this->prophesize(PaymentRepositoryInterface::class);
$this->paymentRequestFactory = $this->prophesize(PaymentRequestFactoryInterface::class);
$this->paymentRequestRepository = $this->prophesize(PaymentRequestRepositoryInterface::class);
$this->defaultActionProvider = $this->prophesize(DefaultActionProviderInterface::class);
$this->defaultPayloadProvider = $this->prophesize(DefaultPayloadProviderInterface::class);
$this->paymentMethodRepository = $this->createMock(PaymentMethodRepositoryInterface::class);
$this->paymentRepository = $this->createMock(PaymentRepositoryInterface::class);
$this->paymentRequestFactory = $this->createMock(PaymentRequestFactoryInterface::class);
$this->paymentRequestRepository = $this->createMock(PaymentRequestRepositoryInterface::class);
$this->defaultActionProvider = $this->createMock(DefaultActionProviderInterface::class);
$this->defaultPayloadProvider = $this->createMock(DefaultPayloadProviderInterface::class);
$this->addPaymentRequestHandler = new AddPaymentRequestHandler(
$this->paymentMethodRepository->reveal(),
$this->paymentRepository->reveal(),
$this->paymentRequestFactory->reveal(),
$this->paymentRequestRepository->reveal(),
$this->defaultActionProvider->reveal(),
$this->defaultPayloadProvider->reveal(),
$this->paymentMethodRepository,
$this->paymentRepository,
$this->paymentRequestFactory,
$this->paymentRequestRepository,
$this->defaultActionProvider,
$this->defaultPayloadProvider,
);
}
@ -73,7 +70,7 @@ final class AddPaymentRequestHandlerTest extends TestCase
{
self::expectException(PaymentNotFoundException::class);
$this->paymentRepository->findOneByOrderToken(1, 'token')->willReturn(null);
$this->paymentRepository->method('findOneByOrderToken')->with(1, 'token')->willReturn(null);
$this->addPaymentRequestHandler->__invoke(new AddPaymentRequest('token', 1, 'bank_transfer'));
}
@ -83,10 +80,10 @@ final class AddPaymentRequestHandlerTest extends TestCase
{
self::expectException(PaymentMethodNotFoundException::class);
$payment = $this->prophesize(PaymentInterface::class);
$payment = $this->createMock(PaymentInterface::class);
$this->paymentRepository->findOneByOrderToken(1, 'token')->willReturn($payment->reveal());
$this->paymentMethodRepository->findOneBy(['code' => 'bank_transfer'])->willReturn(null);
$this->paymentRepository->method('findOneByOrderToken')->with(1, 'token')->willReturn($payment);
$this->paymentMethodRepository->method('findOneBy')->with(['code' => 'bank_transfer'])->willReturn(null);
$this->addPaymentRequestHandler->__invoke(new AddPaymentRequest('token', 1, 'bank_transfer'));
}
@ -94,21 +91,21 @@ final class AddPaymentRequestHandlerTest extends TestCase
#[Test]
public function it_creates_a_payment_request(): void
{
$payment = $this->prophesize(PaymentInterface::class);
$paymentMethod = $this->prophesize(PaymentMethodInterface::class);
$paymentRequest = $this->prophesize(PaymentRequestInterface::class);
$payment = $this->createMock(PaymentInterface::class);
$paymentMethod = $this->createMock(PaymentMethodInterface::class);
$paymentRequest = $this->createMock(PaymentRequestInterface::class);
$this->paymentRepository->findOneByOrderToken(1, 'token')->willReturn($payment->reveal());
$this->paymentMethodRepository->findOneBy(['code' => 'bank_transfer'])->willReturn($paymentMethod->reveal());
$this->defaultActionProvider->getAction($paymentRequest)->willReturn('authorize');
$this->defaultPayloadProvider->getPayload($paymentRequest)->willReturn(['foo' => 'bar']);
$this->paymentRepository->method('findOneByOrderToken')->with(1, 'token')->willReturn($payment);
$this->paymentMethodRepository->method('findOneBy')->with(['code' => 'bank_transfer'])->willReturn($paymentMethod);
$this->defaultActionProvider->method('getAction')->with($paymentRequest)->willReturn('authorize');
$this->defaultPayloadProvider->method('getPayload')->with($paymentRequest)->willReturn(['foo' => 'bar']);
$this->paymentRequestFactory->create($payment->reveal(), $paymentMethod->reveal())->willReturn($paymentRequest->reveal());
$paymentRequest->setAction('authorize')->shouldBeCalled();
$paymentRequest->setPayload(['foo' => 'bar'])->shouldBeCalled();
$this->paymentRequestFactory->method('create')->with($payment, $paymentMethod)->willReturn($paymentRequest);
$paymentRequest->expects($this->once())->method('setAction')->with('authorize');
$paymentRequest->expects($this->once())->method('setPayload')->with(['foo' => 'bar']);
self::assertSame(
$paymentRequest->reveal(),
$paymentRequest,
$this->addPaymentRequestHandler->__invoke(
new AddPaymentRequest('token', 1, 'bank_transfer'),
),

View file

@ -20,10 +20,8 @@ use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInter
use ApiPlatform\Metadata\UriVariablesConverterInterface;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Sylius\Bundle\ApiBundle\Command\Catalog\AddProductReview;
use Sylius\Bundle\ApiBundle\Converter\IriToIdentifierConverter;
use Sylius\Bundle\ApiBundle\Converter\IriToIdentifierConverterInterface;
@ -33,26 +31,24 @@ use Symfony\Component\Routing\RouterInterface;
final class IriToIdentifierConverterTest extends TestCase
{
use ProphecyTrait;
private RouterInterface&MockObject $router;
private ObjectProphecy|RouterInterface $router;
private ResourceMetadataCollectionFactoryInterface&MockObject $metadataFactory;
private ObjectProphecy|ResourceMetadataCollectionFactoryInterface $metadataFactory;
private ObjectProphecy|UriVariablesConverterInterface $uriVariablesConverter;
private UriVariablesConverterInterface&MockObject $uriVariablesConverter;
private IriToIdentifierConverterInterface $converter;
protected function setUp(): void
{
$this->router = $this->prophesize(RouterInterface::class);
$this->metadataFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
$this->uriVariablesConverter = $this->prophesize(UriVariablesConverterInterface::class);
$this->router = $this->createMock(RouterInterface::class);
$this->metadataFactory = $this->createMock(ResourceMetadataCollectionFactoryInterface::class);
$this->uriVariablesConverter = $this->createMock(UriVariablesConverterInterface::class);
$this->converter = new IriToIdentifierConverter(
$this->router->reveal(),
$this->metadataFactory->reveal(),
$this->uriVariablesConverter->reveal(),
$this->router,
$this->metadataFactory,
$this->uriVariablesConverter,
);
}
@ -60,7 +56,9 @@ final class IriToIdentifierConverterTest extends TestCase
#[Test]
public function it_treats_non_string_values_as_not_identifiers(mixed $invalidValue): void
{
$this->router->match(Argument::any())->shouldNotBeCalled();
$this->router
->expects($this->never())
->method('match');
$this->assertFalse($this->converter->isIdentifier($invalidValue));
}
@ -68,7 +66,11 @@ final class IriToIdentifierConverterTest extends TestCase
#[Test]
public function it_treats_not_matched_strings_as_not_identifiers(): void
{
$this->router->match('test')->willThrow(new SymfonyRouteNotFoundException());
$this->router
->expects($this->once())
->method('match')
->with('test')
->willThrowException(new SymfonyRouteNotFoundException());
$this->assertFalse($this->converter->isIdentifier('test'));
}
@ -76,7 +78,11 @@ final class IriToIdentifierConverterTest extends TestCase
#[Test]
public function it_treats_strings_matched_on_routes_with_no_resource_class_parameter_as_not_identifiers(): void
{
$this->router->match('test')->willReturn([]);
$this->router
->expects($this->once())
->method('match')
->with('test')
->willReturn([]);
$this->assertFalse($this->converter->isIdentifier('test'));
}
@ -84,9 +90,13 @@ final class IriToIdentifierConverterTest extends TestCase
#[Test]
public function it_treats_strings_matched_on_routes_with_resource_class_parameter_as_identifiers(): void
{
$this->router->match('test')->willReturn([
'_api_resource_class' => 'test',
]);
$this->router
->expects($this->once())
->method('match')
->with('test')
->willReturn([
'_api_resource_class' => 'test',
]);
$this->assertTrue($this->converter->isIdentifier('test'));
}
@ -96,9 +106,11 @@ final class IriToIdentifierConverterTest extends TestCase
public function it_sanitizes_url_before_matching_for_various_characters(string $rawUrl): void
{
$sanitized = filter_var($rawUrl, \FILTER_SANITIZE_URL);
$this->router->match(Argument::that(fn ($arg) => $arg === $sanitized))
->willReturn(['_api_resource_class' => 'test'])
->shouldBeCalled();
$this->router
->expects($this->once())
->method('match')
->with($sanitized)
->willReturn(['_api_resource_class' => 'test']);
$this->assertTrue($this->converter->isIdentifier($rawUrl));
}
@ -109,9 +121,11 @@ final class IriToIdentifierConverterTest extends TestCase
{
$sanitized = filter_var($rawUrl, \FILTER_SANITIZE_URL);
$this->router->match(Argument::that(fn ($arg) => $arg === $sanitized))
->willThrow(new SymfonyRouteNotFoundException())
->shouldBeCalled();
$this->router
->expects($this->once())
->method('match')
->with($sanitized)
->willThrowException(new SymfonyRouteNotFoundException());
$this->assertFalse($this->converter->isIdentifier($rawUrl));
}
@ -122,9 +136,11 @@ final class IriToIdentifierConverterTest extends TestCase
$rawUrl = "api/v2/produc\nts/2";
$sanitized = filter_var($rawUrl, \FILTER_SANITIZE_URL);
$this->router->match(Argument::that(fn ($arg) => $arg === $sanitized))
->willReturn(['_api_resource_class' => 'test'])
->shouldBeCalled();
$this->router
->expects($this->once())
->method('match')
->with($sanitized)
->willReturn(['_api_resource_class' => 'test']);
$this->assertTrue($this->converter->isIdentifier($rawUrl));
}
@ -135,9 +151,11 @@ final class IriToIdentifierConverterTest extends TestCase
$rawUrl = "api/v2/produc\nts/3";
$sanitized = filter_var($rawUrl, \FILTER_SANITIZE_URL);
$this->router->match(Argument::that(fn ($arg) => $arg === $sanitized))
->willThrow(new SymfonyRouteNotFoundException())
->shouldBeCalled();
$this->router
->expects($this->once())
->method('match')
->with($sanitized)
->willThrowException(new SymfonyRouteNotFoundException());
$this->assertFalse($this->converter->isIdentifier($rawUrl));
}
@ -148,7 +166,11 @@ final class IriToIdentifierConverterTest extends TestCase
self::expectException(ApiRouteNotFoundException::class);
self::expectExceptionMessage('No route matches "/users/3".');
$this->router->match('/users/3')->willThrow(new SymfonyRouteNotFoundException())->shouldBeCalledTimes(1);
$this->router
->expects($this->once())
->method('match')
->with('/users/3')
->willThrowException(new SymfonyRouteNotFoundException());
$this->converter->getIdentifier('/users/3');
}
@ -159,9 +181,13 @@ final class IriToIdentifierConverterTest extends TestCase
self::expectException(InvalidArgumentException::class);
self::expectExceptionMessage('No resource associated to "/users/3".');
$this->router->match('/users/3')->willReturn([
'_api_operation_name' => 'get',
])->shouldBeCalledTimes(1);
$this->router
->expects($this->once())
->method('match')
->with('/users/3')
->willReturn([
'_api_operation_name' => 'get',
]);
$this->converter->getIdentifier('/users/3');
}
@ -172,9 +198,13 @@ final class IriToIdentifierConverterTest extends TestCase
self::expectException(InvalidArgumentException::class);
self::expectExceptionMessage('No resource associated to "/users/3".');
$this->router->match('/users/3')->willReturn([
'_api_resource_class' => AddProductReview::class,
])->shouldBeCalledTimes(1);
$this->router
->expects($this->once())
->method('match')
->with('/users/3')
->willReturn([
'_api_resource_class' => AddProductReview::class,
]);
$this->converter->getIdentifier('/users/3');
}
@ -185,51 +215,81 @@ final class IriToIdentifierConverterTest extends TestCase
self::expectException(InvalidArgumentException::class);
self::expectExceptionMessage('IriToIdentifierConverter does not support subresources');
$operation = $this->prophesize(HttpOperation::class);
$operation->getClass()->willReturn(AddProductReview::class);
$operation->getUriVariables()->willReturn([
'id' => new Link('id', identifiers: ['id'], compositeIdentifier: true),
'nextId' => new Link('nextId', identifiers: ['nextId'], compositeIdentifier: true),
]);
$operation = $this->createMock(HttpOperation::class);
$operation
->expects($this->any())
->method('getClass')
->willReturn(AddProductReview::class);
$this->router->match('/users/3/nexts/5')->willReturn([
'_api_resource_class' => AddProductReview::class,
'_api_operation_name' => 'get',
'id' => 3,
'nextId' => 5,
])->shouldBeCalledTimes(1);
$operation
->expects($this->once())
->method('getUriVariables')
->willReturn([
'id' => new Link('id', identifiers: ['id'], compositeIdentifier: true),
'nextId' => new Link('nextId', identifiers: ['nextId'], compositeIdentifier: true),
]);
$this->uriVariablesConverter->convert(
['id' => 3, 'nextId' => 5],
AddProductReview::class,
Argument::cetera(),
)->willReturn(['3', '5']);
$this->router
->expects($this->once())
->method('match')
->with('/users/3/nexts/5')
->willReturn([
'_api_resource_class' => AddProductReview::class,
'_api_operation_name' => 'get',
'id' => 3,
'nextId' => 5,
]);
$this->converter->getIdentifier('/users/3/nexts/5', $operation->reveal());
$this->uriVariablesConverter
->expects($this->once())
->method('convert')
->with(
['id' => 3, 'nextId' => 5],
AddProductReview::class,
$this->anything(),
)
->willReturn(['3', '5']);
$this->converter->getIdentifier('/users/3/nexts/5', $operation);
}
#[Test]
public function it_gets_identifier(): void
{
$operation = $this->prophesize(HttpOperation::class);
$operation->getClass()->willReturn(AddProductReview::class);
$operation->getUriVariables()->willReturn([
'id' => new Link('id', identifiers: ['id'], compositeIdentifier: true),
]);
$operation = $this->createMock(HttpOperation::class);
$operation
->expects($this->any())
->method('getClass')
->willReturn(AddProductReview::class);
$this->router->match('/users/3')->willReturn([
'_api_resource_class' => AddProductReview::class,
'_api_operation_name' => 'get',
'id' => 3,
])->shouldBeCalledTimes(1);
$operation
->expects($this->once())
->method('getUriVariables')
->willReturn([
'id' => new Link('id', identifiers: ['id'], compositeIdentifier: true),
]);
$this->uriVariablesConverter->convert(
['id' => 3],
AddProductReview::class,
Argument::cetera(),
)->willReturn(['3']);
$this->router
->expects($this->once())
->method('match')
->with('/users/3')
->willReturn([
'_api_resource_class' => AddProductReview::class,
'_api_operation_name' => 'get',
'id' => 3,
]);
$this->assertSame('3', $this->converter->getIdentifier('/users/3', $operation->reveal()));
$this->uriVariablesConverter
->expects($this->once())
->method('convert')
->with(
['id' => 3],
AddProductReview::class,
$this->anything(),
)
->willReturn(['3']);
$this->assertSame('3', $this->converter->getIdentifier('/users/3', $operation));
}
public static function invalidIdentifierValues(): iterable

Some files were not shown because too many files have changed in this diff Show more