# Tests

All tests live under `tests/`. `src/` is touched only by one minimal seam: `BpcRestClient` receives its `HttpClientInterface` by dependency injection instead of building one with the static `HttpClient::create()`, so tests can substitute a mock transport. The seam mirrors `ecommerce-acquiring-gateway` (which injects its Guzzle client the same way); this service keeps its existing Symfony HttpClient. All other test wiring lives in test-only config (`config/services.yaml` `when@test`).

## Structure

```
tests/
├── bootstrap.php
├── Functional/
│   ├── BaseWebTestCase.php
│   └── Controller/
│       ├── OctController/OctControllerCreditTest.php
│       └── LogsController/LogsControllerListTest.php
├── Integration/
│   ├── BPC/BpcRestClientTest.php
│   ├── RequestManager/BPCRequestManagerTest.php
│   └── Repository/CommunicationLogRepositoryTest.php
├── Unit/
│   ├── BPC/
│   │   └── BpcRestClientFactoryTest.php
│   └── Builder/
│       ├── OrderBuilderTest.php
│       ├── CreditBuilderTest.php
│       └── ResponseBuilderTest.php
└── Helper/
    ├── Constants/OctTestConstants.php
    ├── Factory/
    │   ├── OctRequestFactory.php
    │   └── CommunicationLogFactory.php
    ├── Responses/BpcServiceResponses.php
    └── HttpExchange.php
```

## BaseWebTestCase

Functional tests extend `tests/Functional/BaseWebTestCase.php`, which boots the kernel in the `test` environment, creates the `KernelBrowser`, and provides:
- `assertResponseSuccess(?array $expectedData = null)` — asserts HTTP 2xx + `code: 0`; each `$expectedData` value is compared with `assertSame`, or is a callable run against the actual value.
- `assertResponseError(int $code, ?string $expectedMessage = null, ?array $expectedErrors = null)` — asserts the error code, optional message, and optional per-field errors.
- `getDecodedJsonResponse()` and `buildUrl()`.

## How the OCT tests mock the HTTP transport

Functional OCT tests mock the **transport** and assert the outbound BPC request (mirroring `ecommerce-acquiring-gateway`), rather than stubbing the request manager. `BaseWebTestCase` installs a Symfony `MockHttpClient` that records every outbound call and returns queued responses, and overrides the `HttpClientInterface` service so `BPCRequestManager` → `BpcRestClient` use it — exercising the real `OrderBuilder` / `CreditBuilder` / `ResponseBuilder` end-to-end:

```php
$this->expectHttpExchanges(
    HttpExchange::post('registerP2P.do', BpcServiceResponses::registerP2P(), body: ['amount' => 10000, 'currency' => 978], exactBody: false),
    HttpExchange::post('performP2P.do', BpcServiceResponses::performP2P()),
    HttpExchange::post('getP2PStatus.do', BpcServiceResponses::getP2PStatus()),
);
```

`expectHttpExchanges()` queues each response and records the expected method/path/body; `assertPostConditions()` then asserts the app made exactly those calls, in order, each to the expected path with the expected body fields — so the **real outbound BPC payloads** (cents / ISO-numeric / alpha-3 transforms) are verified on the wire. Body/query matching is exact by default, or subset with `exactBody: false`; a field value may be a `Closure` for custom assertions (used for the BPC `params` name/value list). A declined transfer queues `getP2PStatusDeclined()`; a BPC outage queues a non-zero `errorCode` body, which makes `BpcRestClient` throw and the controller surface the general error code. The `winbet` payer-name rule stays covered in the `CreditBuilder` unit test.

`config/services.yaml` `when@test` makes both `BPCRequestManager` and `HttpClientInterface` (as a `MockHttpClient`) public so the test container can replace the transport.

## Unit tests

Unit tests (`tests/Unit/`) extend `PHPUnit\Framework\TestCase` and follow the `ecommerce-acquiring-gateway` builder-test style: build the input via the real DTO setters, call the unit, assert with `assertSame`. They cover the builders (`OrderBuilder` / `CreditBuilder` / `ResponseBuilder`) and the BPC client factory (`BpcRestClientFactoryTest`).

## Integration tests

Integration tests (`tests/Integration/`) extend `KernelTestCase`, boot the kernel and use **real** services from the container (the DB rolls back via `dama/doctrine-test-bundle`):

- `BPC/BpcRestClientTest` builds the client with the real `CommunicationLogRepository` from the container (`new BpcRestClient($repo, $mock)`) and mocks **only** the outbound HTTP transport; the base URL comes from the real `BPC_URL` config (no reflection). Asserts the outbound endpoint/headers/credentials and that each call's request/response is actually persisted as a `CommunicationLog` row in the rolled-back test database (including the orderId-from-response path).
- `RequestManager/BPCRequestManagerTest` fetches the real `BPCRequestManager` (public in `when@test`) — real `EntityManager` and `CommunicationLogRepository` — and mocks **only** the outbound HTTP transport (`$manager->httpClient = new MockHttpClient(...)`), so the P2P calls run against the test database with minimal mocking.
- `Repository/CommunicationLogRepositoryTest` resolves the real `CommunicationLogRepository` and runs `add()` / `remove()` / `find()` against the rolled-back test database.

## Helper classes

| Class | Purpose |
|---|---|
| `OctTestConstants` | All shared test data: amounts, currencies, PAN, cardholder/merchant details, BPC credentials, result codes. |
| `OctRequestFactory` | OCT request bodies (`validOctBody`, `validConfiguration`). |
| `CommunicationLogFactory` | Persists `CommunicationLog` fixtures for the logs/repository tests. |
| `BpcServiceResponses` | Raw BPC JSON bodies reused across the functional/integration tests: `registerP2P()`, `performP2P()`, `getP2PStatus()`, `getP2PStatusDeclined()`. |

## OctController flow

`POST /v1/oct` — no authentication. The controller deserializes `EcommercePaymentDto`, validates, then calls `BPCRequestManager` `order` → `credit` → `status` and builds the response. Key transforms (asserted in the builder unit tests): amount → minor units (`100.0` → `10000`), currency → ISO numeric int (`EUR` → `978`), card acceptor country → alpha-3 (`BG` → `BGR`), single-digit `resultCode` left-padded to 2 chars, card acceptor `winbet` → payer name `winbet EOOD`. A non-zero status error code is surfaced as an `ERROR` payload inside a `code: 0` envelope.

## LogsController flow

`GET /v1/logs/list` — no authentication. Query params: `orderId`, `stan`, `fromDate`, `toDate`, `order` (default DESC), `fromId`, `limit` (default 20), `direction` (default DESC). When the result set is empty, `AbstractApiController::response()` omits the `data` key.

## Running tests

Inside the Docker container (working dir `/var/www/html`):

```bash
./vendor/bin/phpunit
```

Coverage needs a driver (the image ships none by default); install pcov once, then run it for a single invocation without persisting config:

```bash
pecl install pcov
php -d extension=pcov.so -d pcov.enabled=1 -d xdebug.mode=off ./vendor/bin/phpunit --coverage-text
```

Coverage scope (`<source>` in `phpunit.xml.dist`) excludes `src/Dto`, `src/Kernel.php`, and the vestigial SOAP/config-era code the REST flow no longer uses (`src/Builder/LogBuilder.php`, `src/Entity/Configuration.php`, `src/Repository/ConfigurationRepository.php`). The only remaining gap is the two unreachable error guards in `OctController::credit()` (the BPC client throws before they can run) — removing them would need a `src` change.

## Conventions

- Never use magic strings or numbers in tests — use constants from `OctTestConstants`.
- Unit tests extend `PHPUnit\Framework\TestCase`; functional tests extend `BaseWebTestCase`.
- Test method names: `test{WhatIsTestedAndExpectedOutcome}()`.
- Data providers: `camelCaseNameDataProvider()`, referenced via `#[DataProvider('...')]`.
- Endpoint constant: `private const ENDPOINT = '/v1/...'` at the top of each test class.
- `dama/doctrine-test-bundle` rolls back the DB after each test — no manual cleanup.
- Nested validation error keys are flattened to one snake_case key by `Paynetics\Normalizer\ErrorNormalizer` (e.g. `configuration.externalConfiguration[username]` → `configuration_external_configuration_username`).
