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:
parent
25cc47e7d6
commit
cba8ebcf5e
3 changed files with 160 additions and 0 deletions
16
bin/test-integration
Executable file
16
bin/test-integration
Executable 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 "$@"
|
||||
|
|
@ -42,6 +42,40 @@ class FrappeHttpClient
|
|||
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). */
|
||||
public function getContent(string $path): string
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue