feat: Frappe customer integration tests + FrappeHttpClient get/delete

Adds GET and DELETE methods to FrappeHttpClient. Integration tests cover
create, find, not-found (wrong name), and delete against the live staging
ERPNext instance. Run with: bin/test-integration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Simon Kuehn 2026-05-18 14:47:20 +00:00
parent 25cc47e7d6
commit cba8ebcf5e
3 changed files with 160 additions and 0 deletions

16
bin/test-integration Executable file
View file

@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
# Load .env.local secrets and run integration tests inside the app container.
if [ -f "$(dirname "$0")/../.env.local" ]; then
set -a
# shellcheck disable=SC1091
source "$(dirname "$0")/../.env.local"
set +a
fi
docker compose exec \
-e FRAPPE_ERP_BASE_URL="${FRAPPE_ERP_BASE_URL:-}" \
-e FRAPPE_ERP_API_KEY="${FRAPPE_ERP_API_KEY:-}" \
-e FRAPPE_ERP_API_SECRET="${FRAPPE_ERP_API_SECRET:-}" \
app php vendor/bin/phpunit tests/Integration/ --testdox "$@"

View file

@ -42,6 +42,40 @@ class FrappeHttpClient
return $result; return $result;
} }
/**
* GET a Frappe resource endpoint.
*
* @return array<string, mixed>
*/
public function get(string $path): array
{
$response = $this->httpClient->request('GET', $this->baseUrl.$path, [
'headers' => ['Authorization' => $this->authHeader],
]);
/** @var array<string, mixed> $result */
$result = $response->toArray();
return $result;
}
/**
* DELETE a Frappe resource.
*
* @return array<string, mixed>
*/
public function delete(string $path): array
{
$response = $this->httpClient->request('DELETE', $this->baseUrl.$path, [
'headers' => ['Authorization' => $this->authHeader],
]);
/** @var array<string, mixed> $result */
$result = $response->toArray();
return $result;
}
/** GET raw binary content (for PDF downloads). */ /** GET raw binary content (for PDF downloads). */
public function getContent(string $path): string public function getContent(string $path): string
{ {

View file

@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace App\Tests\Integration\Infrastructure\Channel\Frappe;
use App\Infrastructure\Channel\Frappe\FrappeHttpClient;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
/**
* Integration tests against the live ERPNext staging instance.
* Requires FRAPPE_ERP_BASE_URL, FRAPPE_ERP_API_KEY, FRAPPE_ERP_API_SECRET in .env.local.
*
* Run with: vendor/bin/phpunit --testsuite Integration
*/
final class FrappeCustomerIntegrationTest extends TestCase
{
private FrappeHttpClient $client;
private string $createdCustomerName = '';
protected function setUp(): void
{
$baseUrl = $_SERVER['FRAPPE_ERP_BASE_URL'] ?? getenv('FRAPPE_ERP_BASE_URL');
$apiKey = $_SERVER['FRAPPE_ERP_API_KEY'] ?? getenv('FRAPPE_ERP_API_KEY');
$apiSecret = $_SERVER['FRAPPE_ERP_API_SECRET'] ?? getenv('FRAPPE_ERP_API_SECRET');
if (!$baseUrl || !$apiKey || !$apiSecret) {
$this->markTestSkipped('FRAPPE_ERP_* env vars not set');
}
$this->client = new FrappeHttpClient(
HttpClient::create(),
(string) $baseUrl,
(string) $apiKey,
(string) $apiSecret,
);
}
protected function tearDown(): void
{
if ('' !== $this->createdCustomerName) {
try {
$this->client->delete('/api/resource/Customer/'.$this->createdCustomerName);
} catch (\Throwable) {
// best-effort cleanup
}
}
}
public function test_create_customer(): void
{
$response = $this->client->post('/api/resource/Customer', [
'customer_name' => 'Test Superseller Integration',
'customer_type' => 'Individual',
'customer_group' => 'Individual',
'territory' => 'Germany',
]);
$this->assertArrayHasKey('data', $response);
$this->assertNotEmpty($response['data']['name']);
$this->createdCustomerName = $response['data']['name'];
}
public function test_find_created_customer(): void
{
// Create first
$created = $this->client->post('/api/resource/Customer', [
'customer_name' => 'Test Superseller Integration',
'customer_type' => 'Individual',
'customer_group' => 'Individual',
'territory' => 'Germany',
]);
$this->createdCustomerName = $created['data']['name'];
// Find by name
$response = $this->client->get('/api/resource/Customer/'.$this->createdCustomerName);
$this->assertSame($this->createdCustomerName, $response['data']['name']);
$this->assertSame('Test Superseller Integration', $response['data']['customer_name']);
}
public function test_find_nonexistent_customer_throws(): void
{
$this->expectException(ClientExceptionInterface::class);
$this->client->get('/api/resource/Customer/CUST-DOES-NOT-EXIST-99999');
}
public function test_delete_customer(): void
{
// Create
$created = $this->client->post('/api/resource/Customer', [
'customer_name' => 'Test Superseller Integration',
'customer_type' => 'Individual',
'customer_group' => 'Individual',
'territory' => 'Germany',
]);
$name = $created['data']['name'];
// Delete
$this->client->delete('/api/resource/Customer/'.$name);
// Confirm gone
$this->expectException(ClientExceptionInterface::class);
$this->client->get('/api/resource/Customer/'.$name);
}
}