diff --git a/forms-bridge/addons/airtable/class-airtable-form-bridge.php b/forms-bridge/addons/airtable/class-airtable-form-bridge.php
index 4afd2311..7135069d 100644
--- a/forms-bridge/addons/airtable/class-airtable-form-bridge.php
+++ b/forms-bridge/addons/airtable/class-airtable-form-bridge.php
@@ -275,7 +275,7 @@ function ( $name ) use ( $attachment_name ) {
$uploads = Forms_Bridge::attachments( FBAPI::get_uploads() );
foreach ( $attachments as $attachment ) {
- $filetype = array( 'type' => 'octet/stream' );
+ $filetype = array( 'type' => 'application/octet-stream' );
$filename = $attachment['name'];
foreach ( $uploads as $upload_name => $path ) {
@@ -283,7 +283,7 @@ function ( $name ) use ( $attachment_name ) {
$filename = basename( $path );
$filetype = wp_check_filetype( $path );
if ( empty( $filetype['type'] ) ) {
- $filetype['type'] = mime_content_type( $path ) ?: 'octet/stream';
+ $filetype['type'] = mime_content_type( $path ) ?: 'application/octet-stream';
}
}
}
@@ -296,7 +296,7 @@ function ( $name ) use ( $attachment_name ) {
)->post(
"/v0/{$base_id}/{$record_id}/{$attachment['id']}/uploadAttachment",
array(
- 'contentType' => $filetype['type'] ?? 'octet/stream',
+ 'contentType' => $filetype['type'] ?? 'application/octet-stream',
'file' => $attachment['file'],
'filename' => $filename,
),
diff --git a/forms-bridge/addons/nextcloud/class-nextcloud-form-bridge.php b/forms-bridge/addons/nextcloud/class-nextcloud-form-bridge.php
index d5d5d57b..dd63a4f0 100644
--- a/forms-bridge/addons/nextcloud/class-nextcloud-form-bridge.php
+++ b/forms-bridge/addons/nextcloud/class-nextcloud-form-bridge.php
@@ -38,7 +38,7 @@ private function filepath( &$touched = false ) {
$uploads = Forms_Bridge::upload_dir() . '/nextcloud';
if ( ! is_dir( $uploads ) ) {
- if ( ! mkdir( $uploads, 755 ) ) {
+ if ( ! wp_mkdir_p( $uploads, 755 ) ) {
return;
}
}
@@ -229,14 +229,12 @@ public function submit( $payload = array(), $attachments = array() ) {
if ( ! $backend ) {
return new WP_Error(
- 'invalid_bridge',
+ 'invalid_backend',
'Bridge has no valid backend',
(array) $this->data,
);
}
- $payload = self::flatten_payload( $payload );
-
add_filter(
'http_bridge_backend_url',
function ( $url, $backend ) {
@@ -260,62 +258,68 @@ function ( $url, $backend ) {
2
);
- $filepath = $this->filepath( $touched );
+ if ( 'PUT' === $this->method ) {
+ $payload = self::flatten_payload( $payload );
- if ( is_wp_error( $filepath ) ) {
- return $filepath;
- }
+ $filepath = $this->filepath( $touched );
- $dav_modified = $this->get_dav_modified_date( $backend );
- if ( is_wp_error( $dav_modified ) ) {
- return $dav_modified;
- }
+ if ( is_wp_error( $filepath ) ) {
+ return $filepath;
+ }
- if ( ! $dav_modified ) {
- $headers = $this->payload_to_headers( $payload );
- $row = $this->payload_to_row( $payload );
- $csv = implode( "\n", array( $headers, $row ) );
+ $dav_modified = $this->get_dav_modified_date( $backend );
+ if ( is_wp_error( $dav_modified ) ) {
+ return $dav_modified;
+ }
- file_put_contents( $filepath, $csv );
- $response = parent::submit( $csv );
- } elseif ( $touched ) {
+ if ( ! $dav_modified ) {
$headers = $this->payload_to_headers( $payload );
$row = $this->payload_to_row( $payload );
$csv = implode( "\n", array( $headers, $row ) );
file_put_contents( $filepath, $csv );
$response = parent::submit( $csv );
- } else {
- $local_modified = filemtime( $filepath );
-
- if ( $dav_modified > $local_modified ) {
- $response = $backend->get(
- $this->endpoint,
- array(),
- array(),
- array(
- 'stream' => true,
- 'filename' => $filepath,
- )
- );
-
- if ( is_wp_error( $response ) ) {
- return $response;
+ } elseif ( $touched ) {
+ $headers = $this->payload_to_headers( $payload );
+ $row = $this->payload_to_row( $payload );
+ $csv = implode( "\n", array( $headers, $row ) );
+
+ file_put_contents( $filepath, $csv );
+ $response = parent::submit( $csv );
+ } else {
+ $local_modified = filemtime( $filepath );
+
+ if ( $dav_modified > $local_modified ) {
+ $response = $backend->get(
+ $this->endpoint,
+ array(),
+ array(),
+ array(
+ 'stream' => true,
+ 'filename' => $filepath,
+ )
+ );
+
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
}
- }
- $this->add_row( $payload );
+ $this->add_row( $payload );
- $csv = file_get_contents( $filepath );
- $response = parent::submit( $csv );
- }
+ $csv = file_get_contents( $filepath );
+ $response = parent::submit( $csv );
+ }
- if ( is_wp_error( $response ) ) {
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ touch( $filepath, time() );
return $response;
}
- touch( $filepath, time() );
- return $response;
+ return parent::submit( $payload );
}
/**
diff --git a/forms-bridge/addons/nextcloud/hooks.php b/forms-bridge/addons/nextcloud/hooks.php
index 76e75862..20447542 100644
--- a/forms-bridge/addons/nextcloud/hooks.php
+++ b/forms-bridge/addons/nextcloud/hooks.php
@@ -30,7 +30,7 @@ function ( $schema, $addon ) {
);
$schema['properties']['endpoint']['pattern'] = '.+\.csv$';
- $schema['properties']['method']['enum'] = array( 'PUT' );
+ $schema['properties']['method']['enum'] = array( 'GET', 'PUT', 'DELETE', 'MOVE', 'MKCOL', 'PROPFIND' );
$schema['properties']['method']['default'] = 'PUT';
return $schema;
diff --git a/tests/addons/test-holded.php b/tests/addons/test-holded.php
index f1aad85b..574987fc 100644
--- a/tests/addons/test-holded.php
+++ b/tests/addons/test-holded.php
@@ -6,9 +6,763 @@
*/
use FORMS_BRIDGE\Holded_Form_Bridge;
+use FORMS_BRIDGE\Holded_Addon;
+use FORMS_BRIDGE\Addon;
+use HTTP_BRIDGE\Backend;
+use HTTP_BRIDGE\Credential;
/**
* Holded test case.
*/
class HoldedTest extends WP_UnitTestCase {
+
+ /**
+ * Handles the last intercepted http request data.
+ *
+ * @var array
+ */
+ private static $request;
+
+ /**
+ * Handles the mock response to return.
+ *
+ * @var array|null
+ */
+ private static $mock_response;
+
+ /**
+ * Holds the mocked backend name.
+ *
+ * @var string
+ */
+ private const BACKEND_NAME = 'test-holded-backend';
+
+ /**
+ * Holds the mocked backend base URL.
+ *
+ * @var string
+ */
+ private const BACKEND_URL = 'https://api.holded.com';
+
+ /**
+ * Holds the mocked bridge name.
+ *
+ * @var string
+ */
+ private const BRIDGE_NAME = 'test-holded-bridge';
+
+ /**
+ * Test backend provider.
+ *
+ * @return Backend[]
+ */
+ public static function backends_provider() {
+ return array(
+ new Backend(
+ array(
+ 'name' => self::BACKEND_NAME,
+ 'base_url' => self::BACKEND_URL,
+ 'headers' => array(
+ array(
+ 'name' => 'Content-Type',
+ 'value' => 'application/json',
+ ),
+ array(
+ 'name' => 'key',
+ 'value' => 'test-holded-api-key',
+ ),
+ ),
+ )
+ ),
+ );
+ }
+
+ /**
+ * HTTP requests interceptor.
+ *
+ * @param mixed $pre Initial pre hook value.
+ * @param array $args Request arguments.
+ * @param string $url Request URL.
+ *
+ * @return array
+ */
+ public static function pre_http_request( $pre, $args, $url ) {
+ self::$request = array(
+ 'args' => $args,
+ 'url' => $url,
+ );
+
+ $http = array(
+ 'code' => 200,
+ 'message' => 'OK',
+ );
+
+ $method = $args['method'] ?? 'GET';
+
+ // Parse URL to determine the endpoint being called.
+ $parsed_url = wp_parse_url( $url );
+ $path = $parsed_url['path'] ?? '';
+
+ // Parse the body to determine the method being called.
+ $body = array();
+ if ( ! empty( $args['body'] ) ) {
+ if ( is_string( $args['body'] ) ) {
+ $body = json_decode( $args['body'], true );
+ } else {
+ $body = $args['body'];
+ }
+ }
+
+ // Return appropriate mock response based on endpoint.
+ if ( self::$mock_response ) {
+ $http = self::$mock_response['http'] ?? $http;
+ unset( self::$mock_response['http'] );
+ $response_body = self::$mock_response;
+
+ self::$mock_response = null;
+ } else {
+ $response_body = self::get_mock_response( $method, $path, $body );
+ }
+
+ return array(
+ 'response' => $http,
+ 'headers' => array( 'Content-Type' => 'application/json' ),
+ 'cookies' => array(),
+ 'body' => wp_json_encode( $response_body ),
+ 'http_response' => null,
+ );
+ }
+
+ /**
+ * Get mock response based on API endpoint.
+ *
+ * @param string $method HTTP method.
+ * @param string $path API endpoint path.
+ * @param array $body Request body.
+ *
+ * @return array Mock response.
+ */
+ private static function get_mock_response( $method, $path, $body ) {
+ switch ( $path ) {
+ case '/api/invoicing/v1/contacts':
+ if ( 'POST' === $method ) {
+ return array(
+ 'status' => 1,
+ 'info' => 'Created',
+ 'id' => '123456789',
+ );
+ }
+
+ return array(
+ array(
+ 'id' => '123456789',
+ 'name' => 'John Doe',
+ 'email' => 'john.doe@example.coop',
+ 'phone' => '+1234567890',
+ 'isPerson' => true,
+ ),
+ );
+
+ case '/api/invoicing/v1/documents/invoices':
+ if ( 'POST' === $method ) {
+ return array(
+ 'status' => 1,
+ 'info' => 'Created',
+ 'id' => '987654321',
+ 'contactId' => $body['contactId'],
+ );
+ }
+
+ return array(
+ array(
+ 'id' => '987654321',
+ 'docNumber' => 'INV-001',
+ 'contact' => '123456789',
+ 'date' => '2024-01-01',
+ 'dueDate' => '2024-01-31',
+ 'tax' => 234.61,
+ 'subtotal' => 1117.18,
+ 'discount' => 0,
+ 'total' => 1351.79,
+ 'products' => array(
+ array(
+ 'name' => 'Product 1',
+ 'sku' => 'product-1',
+ 'tax' => 21,
+ 'subtotal' => 100,
+ 'discount' => 10,
+ ),
+ ),
+ ),
+ );
+
+ case '/api/crm/v1/leads':
+ if ( 'POST' === $method ) {
+ return array(
+ 'status' => 1,
+ 'info' => 'Created',
+ 'id' => '456789123',
+ );
+ }
+
+ return array(
+ array(
+ 'id' => '456789123',
+ 'name' => 'John Doe Opportunity',
+ 'funnelId' => '5ab13e373697ac00e305333b',
+ 'stageId' => '5ab13e373697ac00e3053336',
+ 'contactId' => '123456789',
+ 'person' => 1,
+ 'personName' => 'John Doe',
+ 'value' => 4000,
+ 'potential' => 100,
+ 'dueDate' => 1521646788,
+ 'createdAt' => 1521646788,
+ ),
+ );
+
+ case '/api/projects/v1/projects':
+ if ( 'POST' === $method ) {
+ return array(
+ 'id' => '789123456',
+ 'info' => 'Created',
+ 'status' => 1,
+ );
+ }
+
+ return array(
+ array(
+ 'id' => 789123456,
+ 'name' => 'Website Redesign',
+ 'desc' => 'Redesign company website',
+ 'status' => 2,
+ 'tags' => array( 'A', 'B' ),
+ ),
+ );
+
+ case '/holded/api-next/v2/branches/1.0/reference/list-contacts-1':
+ return array(
+ 'data' => array(
+ 'api' => array(
+ 'schema' => array(
+ 'openapi' => '3.0.1',
+ 'paths' => array(
+ '/contacts' => array(
+ 'post' => array(
+ 'requestBody' => array(
+ 'content' => array(
+ 'application/json' => array(
+ 'schema' => array(
+ 'type' => 'object',
+ 'properties' => array(
+ 'name' => array( 'type' => 'string' ),
+ 'email' => array( 'type' => 'string' ),
+ 'phone' => array( 'type' => 'string' ),
+ 'mobile' => array( 'type' => 'string' ),
+ 'isPerson' => array( 'type' => 'boolean' ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ '/documents/{docType}' => array(
+ 'parameters' => array(
+ array(
+ 'name' => 'docType',
+ 'in' => 'path',
+ 'schema' => array( 'type' => 'string' ),
+ 'required' => true,
+ ),
+ ),
+ 'post' => array(
+ 'requestBody' => array(
+ 'content' => array(
+ 'application/json' => array(
+ 'schema' => array(
+ 'type' => 'object',
+ 'properties' => array(
+ 'notes' => array( 'type' => 'string' ),
+ 'contactId' => array( 'type' => 'string' ),
+ 'contactName' => array( 'type' => 'string' ),
+ 'contactEmail' => array( 'type' => 'string' ),
+ 'date' => array( 'type' => 'integer' ),
+ 'items' => array(
+ 'type' => 'array',
+ 'items' => array(
+ 'type' => 'object',
+ 'properties' => array(
+ 'name' => array( 'type' => 'string' ),
+ 'desc' => array( 'type' => 'string' ),
+ 'sku' => array( 'type' => 'string' ),
+ 'tax' => array( 'type' => 'integer' ),
+ 'subtotal' => array( 'type' => 'number' ),
+ 'discount' => array( 'type' => 'number' ),
+ ),
+ ),
+ ),
+ ),
+ 'required' => array( 'date' ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ case '/holded/api-next/v2/branches/1.0/reference/list-leads-1':
+ return array(
+ 'data' => array(
+ 'api' => array(
+ 'schema' => array(
+ 'openapi' => '3.0.1',
+ 'paths' => array(
+ '/leads' => array(
+ 'post' => array(
+ 'requestBody' => array(
+ 'content' => array(
+ 'application/json' => array(
+ 'schema' => array(
+ 'type' => 'object',
+ 'properties' => array(
+ 'name' => array( 'type' => 'string' ),
+ 'value' => array( 'type' => 'integer' ),
+ 'potential' => array( 'type' => 'integer' ),
+ 'contactName' => array( 'type' => 'string' ),
+ 'contactId' => array( 'type' => 'string' ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ case '/holded/api-next/v2/branches/1.0/reference/list-projects':
+ return array(
+ 'data' => array(
+ 'api' => array(
+ 'schema' => array(
+ 'openapi' => '3.0.1',
+ 'paths' => array(
+ '/projects' => array(
+ 'post' => array(
+ 'requestBody' => array(
+ 'content' => array(
+ 'application/json' => array(
+ 'schema' => array(
+ 'type' => 'object',
+ 'properties' => array(
+ 'name' => array( 'type' => 'string' ),
+ ),
+ 'required' => array( 'name' ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ default:
+ return array();
+ }
+ }
+
+ /**
+ * Set up test fixtures.
+ */
+ public function set_up() {
+ parent::set_up();
+
+ self::$request = null;
+ self::$mock_response = null;
+
+ tests_add_filter( 'http_bridge_credentials', array( self::class, 'credentials_provider' ), 10, 0 );
+ tests_add_filter( 'http_bridge_backends', array( self::class, 'backends_provider' ), 10, 0 );
+ tests_add_filter( 'pre_http_request', array( self::class, 'pre_http_request' ), 10, 3 );
+ }
+
+ /**
+ * Tear down test filters.
+ */
+ public function tear_down() {
+ remove_filter( 'http_bridge_credentials', array( self::class, 'credentials_provider' ), 10, 0 );
+ remove_filter( 'http_bridge_backends', array( self::class, 'backends_provider' ), 10, 0 );
+ remove_filter( 'pre_http_request', array( self::class, 'pre_http_request' ), 10, 3 );
+
+ parent::tear_down();
+ }
+
+ /**
+ * Test that the addon class exists and has correct constants.
+ */
+ public function test_addon_class_exists() {
+ $this->assertTrue( class_exists( 'FORMS_BRIDGE\Holded_Addon' ) );
+ $this->assertEquals( 'Holded', Holded_Addon::TITLE );
+ $this->assertEquals( 'holded', Holded_Addon::NAME );
+ $this->assertEquals( '\FORMS_BRIDGE\Holded_Form_Bridge', Holded_Addon::BRIDGE );
+ }
+
+ /**
+ * Test that the form bridge class exists.
+ */
+ public function test_form_bridge_class_exists() {
+ $this->assertTrue( class_exists( 'FORMS_BRIDGE\Holded_Form_Bridge' ) );
+ }
+
+ /**
+ * Test bridge validation with valid data.
+ */
+ public function test_bridge_validation() {
+ $bridge = new Holded_Form_Bridge(
+ array(
+ 'name' => self::BRIDGE_NAME,
+ 'backend' => self::BACKEND_NAME,
+ 'endpoint' => '/api/invoicing/v1/contacts',
+ 'method' => 'POST',
+ )
+ );
+
+ $this->assertTrue( $bridge->is_valid );
+ }
+
+ /**
+ * Test bridge validation with invalid data.
+ */
+ public function test_bridge_validation_invalid() {
+ $bridge = new Holded_Form_Bridge(
+ array(
+ 'name' => 'invalid-bridge',
+ // Missing required fields.
+ )
+ );
+
+ $this->assertFalse( $bridge->is_valid );
+ }
+
+ /**
+ * Test POST request to create a contact.
+ */
+ public function test_post_create_contact() {
+ $bridge = new Holded_Form_Bridge(
+ array(
+ 'name' => self::BRIDGE_NAME,
+ 'backend' => self::BACKEND_NAME,
+ 'endpoint' => '/api/invoicing/v1/contacts',
+ 'method' => 'POST',
+ )
+ );
+
+ $payload = array(
+ 'name' => 'John Doe',
+ 'email' => 'john.doe@example.coop',
+ 'phone' => '+1234567890',
+ 'isPerson' => true,
+ );
+
+ $response = $bridge->submit( $payload );
+
+ $this->assertFalse( is_wp_error( $response ) );
+ $this->assertArrayHasKey( 'data', $response );
+ $this->assertEquals( '123456789', $response['data']['id'] );
+ $this->assertEquals( 'Created', $response['data']['info'] );
+ $this->assertEquals( 1, $response['data']['status'] );
+ }
+
+ /**
+ * Test GET request to fetch contacts.
+ */
+ public function test_get_contacts() {
+ $bridge = new Holded_Form_Bridge(
+ array(
+ 'name' => self::BRIDGE_NAME,
+ 'backend' => self::BACKEND_NAME,
+ 'endpoint' => '/api/invoicing/v1/contacts',
+ 'method' => 'GET',
+ )
+ );
+
+ $response = $bridge->submit();
+
+ $this->assertFalse( is_wp_error( $response ) );
+ $this->assertArrayHasKey( 'data', $response );
+ $this->assertNotEmpty( $response['data'] );
+ $this->assertEquals( 'John Doe', $response['data'][0]['name'] );
+ $this->assertEquals( 'john.doe@example.coop', $response['data'][0]['email'] );
+ $this->assertEquals( '+1234567890', $response['data'][0]['phone'] );
+ $this->assertTrue( $response['data'][0]['isPerson'] );
+ }
+
+ /**
+ * Test POST request to create an invoice.
+ */
+ public function test_post_create_invoice() {
+ $bridge = new Holded_Form_Bridge(
+ array(
+ 'name' => self::BRIDGE_NAME,
+ 'backend' => self::BACKEND_NAME,
+ 'endpoint' => '/api/invoicing/v1/documents/invoices',
+ 'method' => 'POST',
+ )
+ );
+
+ $payload = array(
+ 'contactId' => '123456789',
+ 'date' => 1769868644,
+ 'dueDate' => 1770473468,
+ 'items' => array(
+ array(
+ 'name' => 'Product 1',
+ 'sku' => 'product-1',
+ 'tax' => 21,
+ 'subtotal' => 100,
+ 'discount' => 10,
+ ),
+ ),
+ );
+
+ $response = $bridge->submit( $payload );
+
+ $this->assertFalse( is_wp_error( $response ) );
+ $this->assertArrayHasKey( 'data', $response );
+ $this->assertEquals( '987654321', $response['data']['id'] );
+ $this->assertEquals( 'Created', $response['data']['info'] );
+ $this->assertEquals( 1, $response['data']['status'] );
+ }
+
+ /**
+ * Test POST request to create a lead.
+ */
+ public function test_post_create_lead() {
+ $bridge = new Holded_Form_Bridge(
+ array(
+ 'name' => self::BRIDGE_NAME,
+ 'backend' => self::BACKEND_NAME,
+ 'endpoint' => '/api/crm/v1/leads',
+ 'method' => 'POST',
+ )
+ );
+
+ $payload = array(
+ 'name' => 'Jane Smith',
+ 'email' => 'jane.smith@example.coop',
+ 'status' => 'New',
+ );
+
+ $response = $bridge->submit( $payload );
+
+ $this->assertFalse( is_wp_error( $response ) );
+ $this->assertArrayHasKey( 'data', $response );
+ $this->assertEquals( '456789123', $response['data']['id'] );
+ $this->assertEquals( 'Created', $response['data']['info'] );
+ $this->assertEquals( 1, $response['data']['status'] );
+ }
+
+ /**
+ * Test addon ping method.
+ */
+ public function test_addon_ping() {
+ $addon = Addon::addon( 'holded' );
+ $response = $addon->ping( self::BACKEND_NAME );
+
+ $this->assertTrue( $response );
+ }
+
+ /**
+ * Test addon get_endpoints method.
+ */
+ public function test_addon_get_endpoints() {
+ $addon = Addon::addon( 'holded' );
+ $endpoints = $addon->get_endpoints( self::BACKEND_NAME );
+
+ $this->assertIsArray( $endpoints );
+ $this->assertNotEmpty( $endpoints );
+ $this->assertContains( '/api/invoicing/v1/contacts', $endpoints );
+ $this->assertContains( '/api/invoicing/v1/documents/{docType}', $endpoints );
+ $this->assertContains( '/api/crm/v1/leads', $endpoints );
+ $this->assertContains( '/api/projects/v1/projects', $endpoints );
+ }
+
+ /**
+ * Test addon get_endpoint_schema method for contacts.
+ */
+ public function test_addon_get_endpoint_schema_contacts() {
+ $addon = Addon::addon( 'holded' );
+ $schema = $addon->get_endpoint_schema( '/api/invoicing/v1/contacts', self::BACKEND_NAME, 'POST' );
+
+ $this->assertIsArray( $schema );
+ $this->assertNotEmpty( $schema );
+
+ $field_names = array_column( $schema, 'name' );
+ $this->assertContains( 'name', $field_names );
+ $this->assertContains( 'email', $field_names );
+ $this->assertContains( 'phone', $field_names );
+ $this->assertContains( 'mobile', $field_names );
+ $this->assertContains( 'isPerson', $field_names );
+
+ // Check field types.
+ $schema_map = array();
+ foreach ( $schema as $field ) {
+ $schema_map[ $field['name'] ] = $field['schema']['type'];
+ }
+
+ $this->assertEquals( 'string', $schema_map['name'] );
+ $this->assertEquals( 'string', $schema_map['email'] );
+ $this->assertEquals( 'string', $schema_map['phone'] );
+ $this->assertEquals( 'string', $schema_map['mobile'] );
+ $this->assertEquals( 'boolean', $schema_map['isPerson'] );
+ }
+
+ /**
+ * Test addon get_endpoint_schema method for leads.
+ */
+ public function test_addon_get_endpoint_schema_leads() {
+ $addon = Addon::addon( 'holded' );
+ $schema = $addon->get_endpoint_schema( '/api/crm/v1/leads', self::BACKEND_NAME, 'POST' );
+
+ $this->assertIsArray( $schema );
+ $this->assertNotEmpty( $schema );
+
+ $field_names = array_column( $schema, 'name' );
+ $this->assertContains( 'name', $field_names );
+ $this->assertContains( 'value', $field_names );
+ $this->assertContains( 'potential', $field_names );
+ $this->assertContains( 'contactName', $field_names );
+ $this->assertContains( 'contactId', $field_names );
+
+ // Check field types.
+ $schema_map = array();
+ foreach ( $schema as $field ) {
+ $schema_map[ $field['name'] ] = $field['schema']['type'];
+ }
+
+ $this->assertEquals( 'string', $schema_map['name'] );
+ $this->assertEquals( 'integer', $schema_map['value'] );
+ $this->assertEquals( 'integer', $schema_map['potential'] );
+ $this->assertEquals( 'string', $schema_map['contactName'] );
+ $this->assertEquals( 'string', $schema_map['contactId'] );
+ }
+
+ /**
+ * Test error response handling.
+ */
+ public function test_error_response_handling() {
+ self::$mock_response = array(
+ 'http' => array(
+ 'code' => 401,
+ 'message' => 'Unauthorized',
+ ),
+ 'error' => array(
+ 'code' => 'unauthorized',
+ 'message' => 'Invalid API key',
+ ),
+ );
+
+ $bridge = new Holded_Form_Bridge(
+ array(
+ 'name' => self::BRIDGE_NAME,
+ 'backend' => self::BACKEND_NAME,
+ 'endpoint' => '/api/invoicing/v1/contacts',
+ 'method' => 'POST',
+ )
+ );
+
+ $response = $bridge->submit( array() );
+
+ $this->assertTrue( is_wp_error( $response ) );
+ }
+
+ /**
+ * Test invalid backend handling.
+ */
+ public function test_invalid_backend() {
+ $bridge = new Holded_Form_Bridge(
+ array(
+ 'name' => 'test-invalid-backend-bridge',
+ 'backend' => 'non-existent-backend',
+ 'endpoint' => '/api/invoicing/v1/contacts',
+ 'method' => 'POST',
+ )
+ );
+
+ $response = $bridge->submit( array() );
+
+ $this->assertTrue( is_wp_error( $response ) );
+ $this->assertEquals( 'invalid_backend', $response->get_error_code() );
+ }
+
+ /**
+ * Test validation error handling.
+ */
+ public function test_validation_error_handling() {
+ self::$mock_response = array(
+ 'http' => array(
+ 'code' => 422,
+ 'message' => 'Unprocessable Entity',
+ ),
+ 'error' => array(
+ 'code' => 'validation_error',
+ 'message' => 'Validation failed',
+ 'details' => array(
+ 'email' => 'Invalid email format',
+ ),
+ ),
+ );
+
+ $bridge = new Holded_Form_Bridge(
+ array(
+ 'name' => self::BRIDGE_NAME,
+ 'backend' => self::BACKEND_NAME,
+ 'endpoint' => '/api/invoicing/v1/contacts',
+ 'method' => 'POST',
+ )
+ );
+
+ $response = $bridge->submit( array( 'email' => 'invalid-email' ) );
+
+ $this->assertTrue( is_wp_error( $response ) );
+
+ $response_body = json_decode( $response->get_error_data()['response']['body'], true );
+ $this->assertEquals( 'validation_error', $response_body['error']['code'] );
+ }
+
+ /**
+ * Test authorization header transformation.
+ */
+ public function test_authorization_header_transformation() {
+ $bridge = new Holded_Form_Bridge(
+ array(
+ 'name' => self::BRIDGE_NAME,
+ 'backend' => self::BACKEND_NAME,
+ 'endpoint' => '/api/invoicing/v1/contacts',
+ 'method' => 'GET',
+ )
+ );
+
+ $bridge->submit();
+
+ // Check that the request was made
+ $this->assertNotNull( self::$request );
+
+ // Verify the Authorization header was transformed
+ $headers = self::$request['args']['headers'] ?? array();
+ $this->assertArrayHasKey( 'Key', $headers );
+ $this->assertEquals( 'test-holded-api-key', $headers['Key'] );
+ }
}
diff --git a/tests/addons/test-nextcloud.php b/tests/addons/test-nextcloud.php
index fc243e17..aaf4f372 100644
--- a/tests/addons/test-nextcloud.php
+++ b/tests/addons/test-nextcloud.php
@@ -6,9 +6,542 @@
*/
use FORMS_BRIDGE\Nextcloud_Form_Bridge;
+use FORMS_BRIDGE\Nextcloud_Addon;
+use FORMS_BRIDGE\Addon;
+use HTTP_BRIDGE\Backend;
+use HTTP_BRIDGE\Credential;
/**
* Nextcloud test case.
*/
class NextcloudTest extends WP_UnitTestCase {
+
+ /**
+ * Handles the last intercepted http request data.
+ *
+ * @var array
+ */
+ private static $request;
+
+ /**
+ * Handles the mock response to return.
+ *
+ * @var array|null
+ */
+ private static $mock_response;
+
+ /**
+ * Holds the mocked backend name.
+ *
+ * @var string
+ */
+ private const BACKEND_NAME = 'test-nextcloud-backend';
+
+ /**
+ * Holds the mocked backend base URL.
+ *
+ * @var string
+ */
+ private const BACKEND_URL = 'https://nextcloud.example.com';
+
+ /**
+ * Holds the mocked credential name.
+ *
+ * @var string
+ */
+ private const CREDENTIAL_NAME = 'test-nextcloud-credential';
+
+ /**
+ * Holds the mocked bridge name.
+ *
+ * @var string
+ */
+ private const BRIDGE_NAME = 'test-nextcloud-bridge';
+
+ /**
+ * Holds the mocked user ID.
+ *
+ * @var string
+ */
+ private const USER_ID = 'testuser';
+
+ /**
+ * Test credential provider.
+ *
+ * @return Credential[]
+ */
+ public static function credentials_provider() {
+ return array(
+ new Credential(
+ array(
+ 'name' => self::CREDENTIAL_NAME,
+ 'schema' => 'Basic',
+ 'client_id' => self::USER_ID,
+ 'client_secret' => 'test-user-password',
+ )
+ ),
+ );
+ }
+
+ /**
+ * Test backend provider.
+ *
+ * @return Backend[]
+ */
+ public static function backends_provider() {
+ return array(
+ new Backend(
+ array(
+ 'name' => self::BACKEND_NAME,
+ 'base_url' => self::BACKEND_URL,
+ 'credential' => self::CREDENTIAL_NAME,
+ 'headers' => array(
+ array(
+ 'name' => 'Content-Type',
+ 'value' => 'application/octet-stream',
+ ),
+ ),
+ )
+ ),
+ );
+ }
+
+ /**
+ * HTTP requests interceptor.
+ *
+ * @param mixed $pre Initial pre hook value.
+ * @param array $args Request arguments.
+ * @param string $url Request URL.
+ *
+ * @return array
+ */
+ public static function pre_http_request( $pre, $args, $url ) {
+ self::$request = array(
+ 'args' => $args,
+ 'url' => $url,
+ );
+
+ $response = array(
+ 'response' => array(
+ 'code' => 201,
+ 'message' => 'Created',
+ ),
+ 'headers' => array( 'Content-Type' => 'text/html' ),
+ 'cookies' => array(),
+ 'body' => '',
+ 'http_response' => null,
+ );
+
+ $method = $args['method'] ?? 'PUT';
+
+ // Parse URL to determine the endpoint being called.
+ $parsed_url = wp_parse_url( $url );
+ $path = $parsed_url['path'] ?? '';
+
+ // Parse the body to determine the method being called.
+ $body = '';
+ if ( ! empty( $args['body'] ) ) {
+ $body = $args['body'];
+ }
+
+ // Return appropriate mock response based on endpoint.
+ if ( self::$mock_response ) {
+ if ( ! empty( self::$mock_response['http'] ) ) {
+ $response['response'] = self::$mock_response['http'];
+ }
+
+ $response_body = self::$mock_response['body'];
+
+ self::$mock_response = null;
+ } else {
+ $response_body = self::get_mock_response( $method, $path, $body );
+ }
+
+ if ( $response_body ) {
+ $response['headers'] = array( 'Content-Type' => 'text/xml' );
+ $response['body'] = $response_body;
+ }
+
+ return $response;
+ }
+
+ /**
+ * Get mock response based on API endpoint.
+ *
+ * @param string $method HTTP method.
+ * @param string $path API endpoint path.
+ * @param array $body Request body.
+ *
+ * @return array Mock response.
+ */
+ private static function get_mock_response( $method, $path, $body ) {
+ // Mock PROPFIND response for file listing
+ if ( false !== strpos( $path, '/remote.php/dav/files/' ) && 'PROPFIND' === $method ) {
+ return '
+
+
+ /remote.php/dav/files/' . self::USER_ID . '/
+
+
+
+
+ HTTP/1.1 200 OK
+
+
+
+ /remote.php/dav/files/' . self::USER_ID . '/test.csv
+
+
+
+ 1024
+ Mon, 01 Jan 2024 00:00:00 GMT
+
+ HTTP/1.1 200 OK
+
+
+
+ /remote.php/dav/files/' . self::USER_ID . '/directory/
+
+
+
+
+ HTTP/1.1 200 OK
+
+
+ ';
+ }
+
+ // Default empty response
+ return '';
+ }
+
+ /**
+ * Set up test fixtures.
+ */
+ public function set_up() {
+ parent::set_up();
+
+ self::$request = null;
+ self::$mock_response = null;
+
+ tests_add_filter( 'http_bridge_credentials', array( self::class, 'credentials_provider' ), 10, 0 );
+ tests_add_filter( 'http_bridge_backends', array( self::class, 'backends_provider' ), 10, 0 );
+ tests_add_filter( 'pre_http_request', array( self::class, 'pre_http_request' ), 10, 3 );
+ }
+
+ /**
+ * Tear down test filters.
+ */
+ public function tear_down() {
+ remove_filter( 'http_bridge_credentials', array( self::class, 'credentials_provider' ), 10, 0 );
+ remove_filter( 'http_bridge_backends', array( self::class, 'backends_provider' ), 10, 0 );
+ remove_filter( 'pre_http_request', array( self::class, 'pre_http_request' ), 10, 3 );
+
+ $uploads_path = FORMS_BRIDGE\Forms_Bridge::upload_dir() . '/nextcloud';
+ $test_file = $uploads_path . '/test.csv';
+
+ if ( is_file( $test_file ) ) {
+ unlink( $test_file );
+ }
+
+ parent::tear_down();
+ }
+
+ /**
+ * Test that the addon class exists and has correct constants.
+ */
+ public function test_addon_class_exists() {
+ $this->assertTrue( class_exists( 'FORMS_BRIDGE\Nextcloud_Addon' ) );
+ $this->assertEquals( 'Nextcloud', Nextcloud_Addon::TITLE );
+ $this->assertEquals( 'nextcloud', Nextcloud_Addon::NAME );
+ $this->assertEquals( '\FORMS_BRIDGE\Nextcloud_Form_Bridge', Nextcloud_Addon::BRIDGE );
+ }
+
+ /**
+ * Test that the form bridge class exists.
+ */
+ public function test_form_bridge_class_exists() {
+ $this->assertTrue( class_exists( 'FORMS_BRIDGE\Nextcloud_Form_Bridge' ) );
+ }
+
+ /**
+ * Test bridge validation with valid data.
+ */
+ public function test_bridge_validation() {
+ $bridge = new Nextcloud_Form_Bridge(
+ array(
+ 'name' => self::BRIDGE_NAME,
+ 'backend' => self::BACKEND_NAME,
+ 'endpoint' => '/test.csv',
+ 'method' => 'PUT',
+ )
+ );
+
+ $this->assertTrue( $bridge->is_valid );
+ }
+
+ /**
+ * Test bridge validation with invalid data.
+ */
+ public function test_bridge_validation_invalid() {
+ $bridge = new Nextcloud_Form_Bridge(
+ array(
+ 'name' => 'invalid-bridge',
+ // Missing required fields.
+ )
+ );
+
+ $this->assertFalse( $bridge->is_valid );
+ }
+
+ /**
+ * Test PUT request to upload CSV file.
+ */
+ public function test_put_upload_csv() {
+ $bridge = new Nextcloud_Form_Bridge(
+ array(
+ 'name' => self::BRIDGE_NAME,
+ 'backend' => self::BACKEND_NAME,
+ 'endpoint' => '/test.csv',
+ 'method' => 'PUT',
+ )
+ );
+
+ $payload = array(
+ 'Name' => 'John Doe',
+ 'Email' => 'john.doe@example.com',
+ 'Score' => 99,
+ );
+
+ $response = $bridge->submit( $payload );
+
+ $this->assertFalse( is_wp_error( $response ) );
+ $this->assertEquals( 201, $response['response']['code'] );
+ $this->assertEmpty( $response['body'] );
+ }
+
+ /**
+ * Test addon ping method.
+ */
+ public function test_addon_ping() {
+ $addon = Addon::addon( 'nextcloud' );
+ $response = $addon->ping( self::BACKEND_NAME );
+
+ $this->assertTrue( $response );
+ }
+
+ /**
+ * Test addon fetch method to get files.
+ */
+ public function test_addon_fetch_files() {
+ $addon = Addon::addon( 'nextcloud' );
+ $response = $addon->fetch( 'files', self::BACKEND_NAME );
+
+ $this->assertFalse( is_wp_error( $response ) );
+ $this->assertArrayHasKey( 'data', $response );
+ $this->assertArrayHasKey( 'files', $response['data'] );
+ $this->assertCount( 1, $response['data']['files'] );
+ $this->assertEquals( 'test.csv', $response['data']['files'][0]['path'] );
+ }
+
+ /**
+ * Test addon get_endpoints method.
+ */
+ public function test_addon_get_endpoints() {
+ $addon = Addon::addon( 'nextcloud' );
+ $endpoints = $addon->get_endpoints( self::BACKEND_NAME );
+
+ $this->assertIsArray( $endpoints );
+ $this->assertNotEmpty( $endpoints );
+ $this->assertCount( 2, $endpoints );
+ $this->assertContains( 'test.csv', $endpoints );
+ $this->assertContains( 'directory/', $endpoints );
+ }
+
+ /**
+ * Test addon get_endpoint_schema method for PUT.
+ */
+ public function test_addon_get_endpoint_schema_put() {
+ // Create a temporary CSV file for testing
+ $uploads_path = FORMS_BRIDGE\Forms_Bridge::upload_dir() . '/nextcloud';
+ $test_file = $uploads_path . '/test.csv';
+
+ // Create test CSV file
+ if ( ! is_dir( dirname( $test_file ) ) ) {
+ wp_mkdir_p( dirname( $test_file ) );
+ }
+
+ $csv_content = '"Name","Email","Score"
+"John Doe","john.doe@example.com","99"';
+
+ file_put_contents( $test_file, $csv_content );
+
+ $addon = Addon::addon( 'nextcloud' );
+ $schema = $addon->get_endpoint_schema( '/test.csv', self::BACKEND_NAME, 'PUT' );
+
+ $this->assertIsArray( $schema );
+ $this->assertNotEmpty( $schema );
+
+ // Check that schema contains expected fields
+ $field_names = array_column( $schema, 'name' );
+ $this->assertContains( 'Name', $field_names );
+ $this->assertContains( 'Email', $field_names );
+ $this->assertContains( 'Score', $field_names );
+
+ // Check field types
+ $schema_map = array();
+ foreach ( $schema as $field ) {
+ $schema_map[ $field['name'] ] = $field['schema']['type'];
+ }
+
+ $this->assertEquals( 'string', $schema_map['Name'] );
+ $this->assertEquals( 'string', $schema_map['Email'] );
+ $this->assertEquals( 'string', $schema_map['Score'] );
+ }
+
+ /**
+ * Test addon get_endpoint_schema method for non-PUT methods.
+ */
+ public function test_addon_get_endpoint_schema_non_put() {
+ $addon = Addon::addon( 'nextcloud' );
+ $schema = $addon->get_endpoint_schema( '/test.csv', self::BACKEND_NAME, 'GET' );
+
+ // Should return empty array for non-PUT methods
+ $this->assertIsArray( $schema );
+ $this->assertEmpty( $schema );
+ }
+
+ /**
+ * Test error response handling.
+ */
+ public function test_error_response_handling() {
+ self::$mock_response = array(
+ 'http' => array(
+ 'code' => 401,
+ 'message' => 'Unauthorized',
+ ),
+ 'body' => '
+
+ Sabre\DAV\Exception\NotAuthenticated
+ No public access to this resource., AppAPIAuth has not passed, This request is not for a federated calendar, Username or password was incorrect, No \'Authorization: Basic\' header found. Either the client didn\'t send one, or the server is mis-configured, Username or password was incorrect
+ ',
+ );
+
+ $bridge = new Nextcloud_Form_Bridge(
+ array(
+ 'name' => self::BRIDGE_NAME,
+ 'backend' => self::BACKEND_NAME,
+ 'endpoint' => '/test.csv',
+ 'method' => 'PUT',
+ )
+ );
+
+ $response = $bridge->submit( array() );
+
+ $this->assertTrue( is_wp_error( $response ) );
+ }
+
+ /**
+ * Test invalid backend handling.
+ */
+ public function test_invalid_backend() {
+ $bridge = new Nextcloud_Form_Bridge(
+ array(
+ 'name' => 'test-invalid-backend-bridge',
+ 'backend' => 'non-existent-backend',
+ 'endpoint' => '/test.csv',
+ 'method' => 'PUT',
+ )
+ );
+
+ $response = $bridge->submit( array() );
+
+ $this->assertTrue( is_wp_error( $response ) );
+ $this->assertEquals( 'invalid_backend', $response->get_error_code() );
+ }
+
+ /**
+ * Test authorization header transformation.
+ */
+ public function test_authorization_header_transformation() {
+ $bridge = new Nextcloud_Form_Bridge(
+ array(
+ 'name' => self::BRIDGE_NAME,
+ 'backend' => self::BACKEND_NAME,
+ 'endpoint' => '/test.csv',
+ 'method' => 'GET',
+ )
+ );
+
+ $bridge->submit();
+
+ // Check that the request was made
+ $this->assertNotNull( self::$request );
+
+ // Verify the Authorization header was transformed
+ $headers = self::$request['args']['headers'] ?? array();
+ $this->assertArrayHasKey( 'Authorization', $headers );
+ $this->assertStringContainsString( 'Basic', $headers['Authorization'] );
+ }
+
+ /**
+ * Test CSV encoding and decoding.
+ */
+ public function test_csv_encoding_decoding() {
+ $bridge = new Nextcloud_Form_Bridge(
+ array(
+ 'name' => self::BRIDGE_NAME,
+ 'backend' => self::BACKEND_NAME,
+ 'endpoint' => '/test.csv',
+ 'method' => 'PUT',
+ )
+ );
+
+ $payload = array(
+ 'Name' => 'John Doe',
+ 'Email' => 'john.doe@example.coop',
+ 'Score' => 99,
+ );
+
+ $response = $bridge->submit( $payload );
+
+ $this->assertFalse( is_wp_error( $response ) );
+
+ $body = '"Name","Email","Score"
+"John Doe","john.doe@example.coop",99';
+
+ $this->assertEquals( $body, self::$request['args']['body'] );
+ }
+
+ /**
+ * Test payload flattening.
+ */
+ public function test_payload_flattening() {
+ $bridge = new Nextcloud_Form_Bridge(
+ array(
+ 'name' => self::BRIDGE_NAME,
+ 'backend' => self::BACKEND_NAME,
+ 'endpoint' => '/test.csv',
+ 'method' => 'PUT',
+ )
+ );
+
+ $payload = array(
+ 'Name' => 'John Doe',
+ 'Contact' => array(
+ 'Email' => 'john.doe@example.coop',
+ 'Phone' => '1234567890',
+ ),
+ 'Score' => 99,
+ );
+
+ $response = $bridge->submit( $payload );
+
+ $this->assertFalse( is_wp_error( $response ) );
+
+ $body = '"Name","Contact.Email","Contact.Phone","Score"
+"John Doe","john.doe@example.coop","1234567890",99';
+
+ $this->assertEquals( $body, self::$request['args']['body'] );
+ }
}