From 68a9f0094ea4aaadd0a2be76eb45dc40768e0302 Mon Sep 17 00:00:00 2001 From: Simon Kuehn Date: Mon, 18 May 2026 18:02:49 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20eBay=20sandbox=20integration=20?= =?UTF-8?q?=E2=80=94=20env=20config=20+=20taxonomy/adapter=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .env.test | 8 + bin/test-integration | 5 + .../Ebay/EbayAdapterIntegrationTest.php | 155 ++++++++++++++++++ .../Ebay/EbayTaxonomyIntegrationTest.php | 127 ++++++++++++++ 4 files changed, 295 insertions(+) create mode 100644 tests/Integration/Infrastructure/Channel/Ebay/EbayAdapterIntegrationTest.php create mode 100644 tests/Integration/Infrastructure/Channel/Ebay/EbayTaxonomyIntegrationTest.php diff --git a/.env.test b/.env.test index 64bd111..26a41d7 100644 --- a/.env.test +++ b/.env.test @@ -1,3 +1,11 @@ # define your env variables for the test env here KERNEL_CLASS='App\Kernel' 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 diff --git a/bin/test-integration b/bin/test-integration index cf1d038..55f5699 100755 --- a/bin/test-integration +++ b/bin/test-integration @@ -14,4 +14,9 @@ docker compose exec \ -e FRAPPE_ERP_API_KEY="${FRAPPE_ERP_API_KEY:-}" \ -e FRAPPE_ERP_API_SECRET="${FRAPPE_ERP_API_SECRET:-}" \ -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/}" diff --git a/tests/Integration/Infrastructure/Channel/Ebay/EbayAdapterIntegrationTest.php b/tests/Integration/Infrastructure/Channel/Ebay/EbayAdapterIntegrationTest.php new file mode 100644 index 0000000..1fb86f4 --- /dev/null +++ b/tests/Integration/Infrastructure/Channel/Ebay/EbayAdapterIntegrationTest.php @@ -0,0 +1,155 @@ +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); + } +} diff --git a/tests/Integration/Infrastructure/Channel/Ebay/EbayTaxonomyIntegrationTest.php b/tests/Integration/Infrastructure/Channel/Ebay/EbayTaxonomyIntegrationTest.php new file mode 100644 index 0000000..a31dde5 --- /dev/null +++ b/tests/Integration/Infrastructure/Channel/Ebay/EbayTaxonomyIntegrationTest.php @@ -0,0 +1,127 @@ +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); + } +}