feat: eBay sandbox integration — env config + taxonomy/adapter tests
Add sandbox credentials to .env.test and .env.local (sandbox URLs). Pass EBAY_* vars through bin/test-integration. EbayTaxonomyIntegrationTest: 6 tests against sandbox Taxonomy API using app token (client_credentials) — verifies OAuth, aspects for notebooks (cat 177) and RAM (cat 170083), required flags, value lists, caching. EbayAdapterIntegrationTest: listing publish/update/deactivate tests skip gracefully when EBAY_USER_TOKEN not set (Inventory API requires Authorization Code user token). Noop-deactivate test always runs. All 6 taxonomy tests pass against live sandbox. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f0d9f374e6
commit
68a9f0094e
4 changed files with 295 additions and 0 deletions
|
|
@ -1,3 +1,11 @@
|
||||||
# define your env variables for the test env here
|
# define your env variables for the test env here
|
||||||
KERNEL_CLASS='App\Kernel'
|
KERNEL_CLASS='App\Kernel'
|
||||||
APP_SECRET='$ecretf0rt3st'
|
APP_SECRET='$ecretf0rt3st'
|
||||||
|
|
||||||
|
EBAY_CLIENT_ID=SimonKhn-ss3k-SBX-a2ed07085-b96997c2
|
||||||
|
EBAY_CLIENT_SECRET=SBX-2ed070856f30-f6be-4808-ae60-3059
|
||||||
|
EBAY_API_BASE_URL=https://api.sandbox.ebay.com
|
||||||
|
EBAY_OAUTH_BASE_URL=https://api.sandbox.ebay.com
|
||||||
|
EBAY_MARKETPLACE_ID=EBAY_DE
|
||||||
|
EBAY_ENDPOINT_URL=https://ss3k.schaunwama.de/webhooks/ebay
|
||||||
|
EBAY_VERIFICATION_TOKEN=0f06b7a55e4620282c714dbfd90b77b95562f5072d036629e41eb433ef6d2a80
|
||||||
|
|
|
||||||
|
|
@ -14,4 +14,9 @@ docker compose exec \
|
||||||
-e FRAPPE_ERP_API_KEY="${FRAPPE_ERP_API_KEY:-}" \
|
-e FRAPPE_ERP_API_KEY="${FRAPPE_ERP_API_KEY:-}" \
|
||||||
-e FRAPPE_ERP_API_SECRET="${FRAPPE_ERP_API_SECRET:-}" \
|
-e FRAPPE_ERP_API_SECRET="${FRAPPE_ERP_API_SECRET:-}" \
|
||||||
-e FRAPPE_GENERIC_ITEM_CODE="${FRAPPE_GENERIC_ITEM_CODE:-}" \
|
-e FRAPPE_GENERIC_ITEM_CODE="${FRAPPE_GENERIC_ITEM_CODE:-}" \
|
||||||
|
-e EBAY_CLIENT_ID="${EBAY_CLIENT_ID:-}" \
|
||||||
|
-e EBAY_CLIENT_SECRET="${EBAY_CLIENT_SECRET:-}" \
|
||||||
|
-e EBAY_API_BASE_URL="${EBAY_API_BASE_URL:-}" \
|
||||||
|
-e EBAY_OAUTH_BASE_URL="${EBAY_OAUTH_BASE_URL:-}" \
|
||||||
|
-e EBAY_MARKETPLACE_ID="${EBAY_MARKETPLACE_ID:-EBAY_DE}" \
|
||||||
app php vendor/bin/phpunit --testdox "${@:-tests/Integration/}"
|
app php vendor/bin/phpunit --testdox "${@:-tests/Integration/}"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Integration\Infrastructure\Channel\Ebay;
|
||||||
|
|
||||||
|
use App\Domain\Article\Article;
|
||||||
|
use App\Domain\Article\ArticleCondition;
|
||||||
|
use App\Domain\Article\ArticleType;
|
||||||
|
use App\Infrastructure\Channel\Ebay\EbayAdapter;
|
||||||
|
use App\Infrastructure\Channel\Ebay\EbayInventoryApiClient;
|
||||||
|
use App\Infrastructure\Channel\Ebay\EbayOAuthClient;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||||
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests for EbayAdapter against the eBay Sandbox.
|
||||||
|
*
|
||||||
|
* The Inventory/Offer APIs require a user-level OAuth token (Authorization Code
|
||||||
|
* grant with sell.inventory scope) — app token alone is insufficient.
|
||||||
|
*
|
||||||
|
* Set EBAY_USER_TOKEN in .env.local to enable listing tests:
|
||||||
|
* EBAY_USER_TOKEN=v^1.1#i^1#I^3#...
|
||||||
|
*
|
||||||
|
* Taxonomy / auth tests run with just the app token.
|
||||||
|
*
|
||||||
|
* Run with: bin/test-integration tests/Integration/Infrastructure/Channel/Ebay/
|
||||||
|
*/
|
||||||
|
final class EbayAdapterIntegrationTest extends TestCase
|
||||||
|
{
|
||||||
|
private EbayAdapter $adapter;
|
||||||
|
private EbayInventoryApiClient $apiClient;
|
||||||
|
private Article $article;
|
||||||
|
private string $createdListingId = '';
|
||||||
|
private bool $userTokenAvailable = false;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$clientId = $_SERVER['EBAY_CLIENT_ID'] ?? getenv('EBAY_CLIENT_ID');
|
||||||
|
$clientSecret = $_SERVER['EBAY_CLIENT_SECRET'] ?? getenv('EBAY_CLIENT_SECRET');
|
||||||
|
$apiBaseUrl = $_SERVER['EBAY_API_BASE_URL'] ?? getenv('EBAY_API_BASE_URL');
|
||||||
|
$oauthBaseUrl = $_SERVER['EBAY_OAUTH_BASE_URL'] ?? getenv('EBAY_OAUTH_BASE_URL');
|
||||||
|
$marketplaceId = $_SERVER['EBAY_MARKETPLACE_ID'] ?? getenv('EBAY_MARKETPLACE_ID') ?: 'EBAY_DE';
|
||||||
|
|
||||||
|
if (!$clientId || !$clientSecret || !$apiBaseUrl || !$oauthBaseUrl) {
|
||||||
|
$this->markTestSkipped('EBAY_* env vars not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
$http = HttpClient::create();
|
||||||
|
$cache = new ArrayAdapter();
|
||||||
|
|
||||||
|
$oauth = new EbayOAuthClient(
|
||||||
|
$http,
|
||||||
|
$cache,
|
||||||
|
(string) $clientId,
|
||||||
|
(string) $clientSecret,
|
||||||
|
(string) $oauthBaseUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->apiClient = new EbayInventoryApiClient(
|
||||||
|
$http,
|
||||||
|
$oauth,
|
||||||
|
(string) $apiBaseUrl,
|
||||||
|
(string) $marketplaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->adapter = new EbayAdapter($this->apiClient);
|
||||||
|
|
||||||
|
$userToken = $_SERVER['EBAY_USER_TOKEN'] ?? getenv('EBAY_USER_TOKEN');
|
||||||
|
$this->userTokenAvailable = (bool) $userToken;
|
||||||
|
|
||||||
|
// Build a realistic sandbox article for listing tests
|
||||||
|
$type = new ArticleType('Notebook');
|
||||||
|
$this->article = new Article(
|
||||||
|
$type,
|
||||||
|
'SS3K-TEST-'.time(),
|
||||||
|
'INV-TEST-001',
|
||||||
|
1,
|
||||||
|
ArticleCondition::Good,
|
||||||
|
);
|
||||||
|
$this->article->setManufacturer('Lenovo');
|
||||||
|
$this->article->setModelName('ThinkBook 14 G6 IRL');
|
||||||
|
$this->article->setModelNumber('21KG00NQGE');
|
||||||
|
$this->article->setEbayTitle('Lenovo ThinkBook 14 G6 IRL — Sandbox Test');
|
||||||
|
$this->article->setEbayDescription('Integration test listing — do not bid.');
|
||||||
|
$this->article->setListingPrice('1.00');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
if ('' !== $this->createdListingId) {
|
||||||
|
try {
|
||||||
|
$this->adapter->deactivateListing($this->article);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// best-effort cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_publish_listing_creates_live_sandbox_listing(): void
|
||||||
|
{
|
||||||
|
if (!$this->userTokenAvailable) {
|
||||||
|
$this->markTestSkipped(
|
||||||
|
'EBAY_USER_TOKEN not set. Generate a sandbox user token via Authorization Code '.
|
||||||
|
'flow (sell.inventory scope) and add it to .env.local to enable this test.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$listingId = $this->adapter->publishListing($this->article);
|
||||||
|
|
||||||
|
$this->assertNotEmpty($listingId);
|
||||||
|
$this->createdListingId = $listingId;
|
||||||
|
|
||||||
|
// Store so deactivateListing in tearDown finds it
|
||||||
|
$this->article->setEbayListingId($listingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_update_stock_changes_quantity(): void
|
||||||
|
{
|
||||||
|
if (!$this->userTokenAvailable) {
|
||||||
|
$this->markTestSkipped('EBAY_USER_TOKEN not set — see test_publish_listing_creates_live_sandbox_listing');
|
||||||
|
}
|
||||||
|
|
||||||
|
// First publish to create the item
|
||||||
|
$this->adapter->publishListing($this->article);
|
||||||
|
$this->article->setEbayListingId($this->createdListingId);
|
||||||
|
|
||||||
|
// Should not throw
|
||||||
|
$this->adapter->updateStock($this->article, 2);
|
||||||
|
$this->addToAssertionCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_deactivate_listing_withdraws_offer(): void
|
||||||
|
{
|
||||||
|
if (!$this->userTokenAvailable) {
|
||||||
|
$this->markTestSkipped('EBAY_USER_TOKEN not set — see test_publish_listing_creates_live_sandbox_listing');
|
||||||
|
}
|
||||||
|
|
||||||
|
$listingId = $this->adapter->publishListing($this->article);
|
||||||
|
$this->article->setEbayListingId($listingId);
|
||||||
|
|
||||||
|
// Should not throw
|
||||||
|
$this->adapter->deactivateListing($this->article);
|
||||||
|
$this->createdListingId = ''; // already deactivated, skip tearDown cleanup
|
||||||
|
$this->addToAssertionCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_deactivate_listing_is_noop_when_no_listing_id(): void
|
||||||
|
{
|
||||||
|
// No listing ID set — should silently return, no API call
|
||||||
|
$this->adapter->deactivateListing($this->article);
|
||||||
|
$this->addToAssertionCount(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Integration\Infrastructure\Channel\Ebay;
|
||||||
|
|
||||||
|
use App\Infrastructure\Channel\Ebay\EbayOAuthClient;
|
||||||
|
use App\Infrastructure\Channel\Ebay\EbayTaxonomyService;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||||
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests against the eBay Sandbox Taxonomy API.
|
||||||
|
* Uses application token (client_credentials) — no user OAuth needed.
|
||||||
|
*
|
||||||
|
* Requires EBAY_CLIENT_ID, EBAY_CLIENT_SECRET, EBAY_API_BASE_URL,
|
||||||
|
* EBAY_OAUTH_BASE_URL, EBAY_MARKETPLACE_ID in .env.local.
|
||||||
|
*
|
||||||
|
* Run with: bin/test-integration tests/Integration/Infrastructure/Channel/Ebay/
|
||||||
|
*/
|
||||||
|
final class EbayTaxonomyIntegrationTest extends TestCase
|
||||||
|
{
|
||||||
|
private EbayTaxonomyService $taxonomy;
|
||||||
|
private EbayOAuthClient $oauth;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$clientId = $_SERVER['EBAY_CLIENT_ID'] ?? getenv('EBAY_CLIENT_ID');
|
||||||
|
$clientSecret = $_SERVER['EBAY_CLIENT_SECRET'] ?? getenv('EBAY_CLIENT_SECRET');
|
||||||
|
$apiBaseUrl = $_SERVER['EBAY_API_BASE_URL'] ?? getenv('EBAY_API_BASE_URL');
|
||||||
|
$oauthBaseUrl = $_SERVER['EBAY_OAUTH_BASE_URL'] ?? getenv('EBAY_OAUTH_BASE_URL');
|
||||||
|
$marketplaceId = $_SERVER['EBAY_MARKETPLACE_ID'] ?? getenv('EBAY_MARKETPLACE_ID') ?: 'EBAY_DE';
|
||||||
|
|
||||||
|
if (!$clientId || !$clientSecret || !$apiBaseUrl || !$oauthBaseUrl) {
|
||||||
|
$this->markTestSkipped('EBAY_* env vars not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
$http = HttpClient::create();
|
||||||
|
$cache = new ArrayAdapter();
|
||||||
|
|
||||||
|
$this->oauth = new EbayOAuthClient(
|
||||||
|
$http,
|
||||||
|
$cache,
|
||||||
|
(string) $clientId,
|
||||||
|
(string) $clientSecret,
|
||||||
|
(string) $oauthBaseUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->taxonomy = new EbayTaxonomyService(
|
||||||
|
$http,
|
||||||
|
$this->oauth,
|
||||||
|
$cache,
|
||||||
|
(string) $apiBaseUrl,
|
||||||
|
(string) $marketplaceId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_fetches_application_token(): void
|
||||||
|
{
|
||||||
|
$token = $this->oauth->getAccessToken();
|
||||||
|
|
||||||
|
$this->assertNotEmpty($token);
|
||||||
|
$this->assertStringStartsWith('v^1.1', $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_fetches_aspects_for_notebooks_category(): void
|
||||||
|
{
|
||||||
|
// 177 = Notebooks & Netbooks in EBAY_DE
|
||||||
|
$aspects = $this->taxonomy->getCategoryAspects('177');
|
||||||
|
|
||||||
|
$this->assertNotEmpty($aspects, 'Category 177 should have aspects');
|
||||||
|
|
||||||
|
// Each aspect must have the expected shape
|
||||||
|
foreach ($aspects as $aspect) {
|
||||||
|
$this->assertArrayHasKey('name', $aspect);
|
||||||
|
$this->assertArrayHasKey('required', $aspect);
|
||||||
|
$this->assertArrayHasKey('values', $aspect);
|
||||||
|
$this->assertIsString($aspect['name']);
|
||||||
|
$this->assertIsBool($aspect['required']);
|
||||||
|
$this->assertIsArray($aspect['values']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notebooks should have at least a Prozessor/CPU aspect
|
||||||
|
$names = array_column($aspects, 'name');
|
||||||
|
$this->assertNotEmpty(array_filter($names, static fn (string $n) => str_contains(strtolower($n), 'prozes') || str_contains(strtolower($n), 'cpu') || str_contains(strtolower($n), 'speicher')), 'Expected at least one hardware aspect (CPU/RAM)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_required_aspects_are_flagged(): void
|
||||||
|
{
|
||||||
|
$aspects = $this->taxonomy->getCategoryAspects('177');
|
||||||
|
|
||||||
|
$required = array_filter($aspects, static fn (array $a) => $a['required']);
|
||||||
|
|
||||||
|
$this->assertNotEmpty($required, 'Category 177 should have at least one required aspect');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_aspects_with_predefined_values_have_options(): void
|
||||||
|
{
|
||||||
|
$aspects = $this->taxonomy->getCategoryAspects('177');
|
||||||
|
|
||||||
|
// At least some aspects should have predefined value lists (e.g. Zustand, Prozessorfamilie)
|
||||||
|
$withOptions = array_filter($aspects, static fn (array $a) => [] !== $a['values']);
|
||||||
|
|
||||||
|
$this->assertNotEmpty($withOptions, 'Some aspects should have predefined selectable values');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_caches_aspects_on_second_call(): void
|
||||||
|
{
|
||||||
|
// First call hits the API
|
||||||
|
$first = $this->taxonomy->getCategoryAspects('177');
|
||||||
|
// Second call should return from cache (same ArrayAdapter instance)
|
||||||
|
$second = $this->taxonomy->getCategoryAspects('177');
|
||||||
|
|
||||||
|
$this->assertSame($first, $second);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_fetches_aspects_for_ram_category(): void
|
||||||
|
{
|
||||||
|
// 170083 = RAM/Speicher in EBAY_DE — useful for our memory article types
|
||||||
|
$aspects = $this->taxonomy->getCategoryAspects('170083');
|
||||||
|
|
||||||
|
$this->assertNotEmpty($aspects);
|
||||||
|
$names = array_map(static fn (array $a) => $a['name'], $aspects);
|
||||||
|
$this->assertNotEmpty($names);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue