diff --git a/inc/Config/DataSource/ConstantConfigStore.php b/inc/Config/DataSource/ConstantConfigStore.php new file mode 100644 index 00000000..0adaa439 --- /dev/null +++ b/inc/Config/DataSource/ConstantConfigStore.php @@ -0,0 +1,95 @@ + + * }> + */ + public static function get_configs(): array { + if ( ! self::is_available() ) { + return []; + } + + $configs = constant( self::CONFIG_CONSTANT_NAME ); + $valid_configs = []; + + foreach ( $configs as $config ) { + $validation_result = self::validate_config( $config ); + if ( ! is_wp_error( $validation_result ) ) { + $valid_configs[] = $config; + } else { + LoggerManager::instance()->error( + sprintf( + 'Invalid data source config found (uuid: %s): %s', + $config['uuid'] ?? 'unknown', + $validation_result->get_error_message() + ) + ); + } + } + + return $valid_configs; + } + + public static function get_config_by_uuid( string $uuid ): array|WP_Error { + $configs = constant( self::CONFIG_CONSTANT_NAME ); + $found = array_filter( $configs, fn ( $config ) => $config['uuid'] === $uuid ); + + if ( empty( $found ) ) { + return new WP_Error( + 'data_source_not_found', + __( 'Data source not found', 'remote-data-blocks' ), + [ 'status' => 404 ] + ); + } + + $config = reset( $found ); + $validation_result = self::validate_config( $config ); + + if ( is_wp_error( $validation_result ) ) { + return $validation_result; + } + + return $config; + } + + public static function validate_config( array $config ): DataSourceInterface|WP_Error { + $data_source_class = REMOTE_DATA_BLOCKS__DATA_SOURCE_CLASSMAP[ $config['service'] ] ?? null; + if ( null === $data_source_class ) { + return new WP_Error( + 'unsupported_data_source', + __( 'Unsupported data source service', 'remote-data-blocks' ) + ); + } + + return $data_source_class::from_array( $config ); + } + + /** + * Check if constant configuration is available and valid. + * + * @return bool Whether the constant configuration is available and valid. + */ + public static function is_available(): bool { + if ( ! defined( self::CONFIG_CONSTANT_NAME ) || '' === constant( self::CONFIG_CONSTANT_NAME ) ) { + return false; + } + + $value = constant( self::CONFIG_CONSTANT_NAME ); + return is_array( $value ); + } +} diff --git a/inc/Config/DataSource/DataSourceConfigManager.php b/inc/Config/DataSource/DataSourceConfigManager.php new file mode 100644 index 00000000..07e374ea --- /dev/null +++ b/inc/Config/DataSource/DataSourceConfigManager.php @@ -0,0 +1,233 @@ + array_merge( $config, [ 'config_source' => self::CONFIG_SOURCE_STORAGE ] ), + DataSourceCrud::get_configs() + ); + } + + private static function get_all_from_constant(): array { + return array_map( + fn ( array $config ) => array_merge( $config, [ 'config_source' => self::CONFIG_SOURCE_CONSTANT ] ), + ConstantConfigStore::get_configs() + ); + } + + private static function get_all_from_code(): array { + return array_map( + fn ( array $config ) => array_merge( $config, [ 'config_source' => self::CONFIG_SOURCE_CODE ] ), + ConfigStore::get_data_sources_as_array() + ); + } + + /** + * Quick and dirty de-duplication of data sources. If the data source does + * not have a UUID (because it is registered in code), we generate an + * identifier based on the display name and service name. + */ + private static function de_duplicate_configs( array $configs ): array { + return array_values( array_reduce( + $configs, + function ( array $acc, array $item ) { + $identifier = $item['uuid'] ?? md5( + sprintf( '%s_%s', $item['service_config']['display_name'], $item['service'] ) + ); + $acc[ $identifier ] = $item; + return $acc; + }, + [] + ) ); + } + + /** + * Get all data sources from all origins. + * + * @return array, + * config_source: string, + * __metadata?: array{ + * created_at: string, + * updated_at: string + * } + * }> + */ + public static function get_all(): array { + $code_configured = self::get_all_from_code(); + $constant_configured = self::get_all_from_constant(); + $storage_configured = self::get_all_from_storage(); + + /** + * De-duplicate configs. + * + * Precedence (lowest to highest): + * - Code-configured data sources + * - Constant-configured data sources + * - Storage-configured data sources + */ + return self::de_duplicate_configs( + array_merge( $code_configured, $constant_configured, $storage_configured ) + ); + } + + /** + * Get a data source by its UUID. + * + * @param string $uuid The UUID of the data source to get. + * @return array{ + * uuid: string, + * service: string, + * service_config: array, + * config_source: string, + * __metadata?: array{ + * created_at: string, + * updated_at: string + * } + * }|WP_Error + */ + public static function get( string $uuid ): array|WP_Error { + $from_constant = ConstantConfigStore::get_config_by_uuid( $uuid ); + if ( ! is_wp_error( $from_constant ) ) { + return array_merge( + $from_constant, + [ 'config_source' => self::CONFIG_SOURCE_CONSTANT ] + ); + } + + $from_storage = DataSourceCrud::get_config_by_uuid( $uuid ); + if ( ! is_wp_error( $from_storage ) ) { + return array_merge( + $from_storage, + [ 'config_source' => self::CONFIG_SOURCE_STORAGE ] + ); + } + + return new WP_Error( + 'data_source_not_found', + __( 'Data source not found', 'remote-data-blocks' ), + [ 'status' => 404 ] + ); + } + + /** + * Create a new data source. + * + * @param array $config The configuration for the new data source. + * @return array{ + * uuid: string, + * service: string, + * service_config: array, + * config_source: string, + * __metadata: array{ + * created_at: string, + * updated_at: string + * } + * }|WP_Error + */ + public static function create( array $config ): array|WP_Error { + $result = DataSourceCrud::create_config( $config ); + if ( is_wp_error( $result ) ) { + return $result; + } + + return array_merge( $result, [ 'config_source' => self::CONFIG_SOURCE_STORAGE ] ); + } + + /** + * Update a data source. + * + * @param string $uuid The UUID of the data source to update. + * @param array $config The new configuration for the data source. + * @return array{ + * uuid: string, + * service: string, + * service_config: array, + * config_source: string, + * __metadata: array{ + * created_at: string, + * updated_at: string + * } + * }|WP_Error + */ + public static function update( string $uuid, array $config ): array|WP_Error { + if ( + isset( $config['config_source'] ) && + ! in_array( $config['config_source'], self::MUTABLE_CONFIG_SOURCES, true ) + ) { + /** + * Only storage-configured data sources are mutable. + */ + return new WP_Error( + 'cannot_update_config', + __( 'Cannot update a data source with this config_source', 'remote-data-blocks' ), + [ 'status' => 400 ] + ); + } + + $result = DataSourceCrud::update_config_by_uuid( $uuid, $config ); + if ( is_wp_error( $result ) ) { + return $result; + } + + return array_merge( $result, [ 'config_source' => self::CONFIG_SOURCE_STORAGE ] ); + } + + /** + * Delete a data source. + * + * @param string $uuid The UUID of the data source to delete. + * @return true|WP_Error True on success, WP_Error on failure. + */ + public static function delete( string $uuid ): bool|WP_Error { + return DataSourceCrud::delete_config_by_uuid( $uuid ); + } + + /** + * Get all configured data sources for a specific service from both storage and constants. + * This includes sources configured via storage and constants, but not those defined in code. + * + * @param string $service The service identifier to filter by. + * @return array, + * config_source: string, + * __metadata?: array{ + * created_at: string, + * updated_at: string + * } + * }> + */ + public static function get_all_configured_by_service( string $service ): array { + $storage_configs = self::get_all_from_storage(); + $constant_configs = self::get_all_from_constant(); + + $all_configs = array_merge( $constant_configs, $storage_configs ); + + /** + * De-duplicate configs. + * + * Precedence (lowest to highest): + * - Constant-configured data sources + * - Storage-configured data sources + */ + $de_duplicated_configs = self::de_duplicate_configs( $all_configs ); + + return array_filter( $de_duplicated_configs, fn ( array $config ) => $config['service'] === $service ); + } +} diff --git a/inc/Editor/BlockManagement/BlockRegistration.php b/inc/Editor/BlockManagement/BlockRegistration.php index c0be1645..67d3e0ce 100644 --- a/inc/Editor/BlockManagement/BlockRegistration.php +++ b/inc/Editor/BlockManagement/BlockRegistration.php @@ -4,7 +4,7 @@ defined( 'ABSPATH' ) || exit(); -use RemoteDataBlocks\Analytics\TracksAnalytics; +use RemoteDataBlocks\Telemetry\TracksTelemetry; use RemoteDataBlocks\Editor\BlockPatterns\BlockPatterns; use RemoteDataBlocks\Editor\DataBinding\BlockBindings; use RemoteDataBlocks\REST\RemoteDataController; @@ -54,7 +54,7 @@ public static function register_container_blocks(): void { wp_localize_script( $script_handle, 'REMOTE_DATA_BLOCKS', [ 'config' => $all_remote_block_configs, 'rest_url' => RemoteDataController::get_url(), - 'tracks_global_properties' => TracksAnalytics::get_global_properties(), + 'tracks_global_properties' => TracksTelemetry::get_global_properties(), ] ); } } diff --git a/inc/Integrations/Airtable/AirtableIntegration.php b/inc/Integrations/Airtable/AirtableIntegration.php index c0ff3eb7..40979a95 100644 --- a/inc/Integrations/Airtable/AirtableIntegration.php +++ b/inc/Integrations/Airtable/AirtableIntegration.php @@ -2,7 +2,7 @@ namespace RemoteDataBlocks\Integrations\Airtable; -use RemoteDataBlocks\WpdbStorage\DataSourceCrud; +use RemoteDataBlocks\Config\DataSource\DataSourceConfigManager; use RemoteDataBlocks\Config\Query\HttpQuery; use RemoteDataBlocks\Formatting\StringFormatter; use RemoteDataBlocks\Snippet\Snippet; @@ -14,7 +14,7 @@ public static function init(): void { } public static function register_blocks(): void { - $data_source_configs = DataSourceCrud::get_configs_by_service( + $data_source_configs = DataSourceConfigManager::get_all_configured_by_service( REMOTE_DATA_BLOCKS_AIRTABLE_SERVICE ); diff --git a/inc/Integrations/Google/Sheets/GoogleSheetsIntegration.php b/inc/Integrations/Google/Sheets/GoogleSheetsIntegration.php index 8b4f25c0..e49f346b 100644 --- a/inc/Integrations/Google/Sheets/GoogleSheetsIntegration.php +++ b/inc/Integrations/Google/Sheets/GoogleSheetsIntegration.php @@ -2,7 +2,7 @@ namespace RemoteDataBlocks\Integrations\Google\Sheets; -use RemoteDataBlocks\WpdbStorage\DataSourceCrud; +use RemoteDataBlocks\Config\DataSource\DataSourceConfigManager; use RemoteDataBlocks\Config\Query\HttpQuery; use RemoteDataBlocks\Formatting\StringFormatter; use RemoteDataBlocks\Snippet\Snippet; @@ -14,7 +14,7 @@ public static function init(): void { } public static function register_blocks(): void { - $data_source_configs = DataSourceCrud::get_configs_by_service( + $data_source_configs = DataSourceConfigManager::get_all_configured_by_service( REMOTE_DATA_BLOCKS_GOOGLE_SHEETS_SERVICE ); diff --git a/inc/Integrations/SalesforceB2C/SalesforceB2CIntegration.php b/inc/Integrations/SalesforceB2C/SalesforceB2CIntegration.php index dd4eafba..a26ab5c3 100644 --- a/inc/Integrations/SalesforceB2C/SalesforceB2CIntegration.php +++ b/inc/Integrations/SalesforceB2C/SalesforceB2CIntegration.php @@ -2,8 +2,8 @@ namespace RemoteDataBlocks\Integrations\SalesforceB2C; +use RemoteDataBlocks\Config\DataSource\DataSourceConfigManager; use RemoteDataBlocks\Config\Query\HttpQuery; -use RemoteDataBlocks\WpdbStorage\DataSourceCrud; use RemoteDataBlocks\Integrations\SalesforceB2C\Auth\SalesforceB2CAuth; use RemoteDataBlocks\Formatting\StringFormatter; use WP_Error; @@ -14,7 +14,9 @@ public static function init(): void { } public static function register_blocks(): void { - $data_source_configs = DataSourceCrud::get_configs_by_service( REMOTE_DATA_BLOCKS_SALESFORCE_B2C_SERVICE ); + $data_source_configs = DataSourceConfigManager::get_all_configured_by_service( + REMOTE_DATA_BLOCKS_SALESFORCE_B2C_SERVICE + ); foreach ( $data_source_configs as $config ) { $data_source = SalesforceB2CDataSource::from_array( $config ); diff --git a/inc/Integrations/Shopify/ShopifyIntegration.php b/inc/Integrations/Shopify/ShopifyIntegration.php index 75f63edf..a22735be 100644 --- a/inc/Integrations/Shopify/ShopifyIntegration.php +++ b/inc/Integrations/Shopify/ShopifyIntegration.php @@ -2,8 +2,8 @@ namespace RemoteDataBlocks\Integrations\Shopify; +use RemoteDataBlocks\Config\DataSource\DataSourceConfigManager; use RemoteDataBlocks\Config\Query\GraphqlQuery; -use RemoteDataBlocks\WpdbStorage\DataSourceCrud; use RemoteDataBlocks\Formatting\StringFormatter; use RemoteDataBlocks\Snippet\Snippet; @@ -15,7 +15,9 @@ public static function init(): void { } public static function register_blocks(): void { - $data_source_configs = DataSourceCrud::get_configs_by_service( REMOTE_DATA_BLOCKS_SHOPIFY_SERVICE ); + $data_source_configs = DataSourceConfigManager::get_all_configured_by_service( + REMOTE_DATA_BLOCKS_SHOPIFY_SERVICE + ); foreach ( $data_source_configs as $config ) { $data_source = ShopifyDataSource::from_array( $config ); diff --git a/inc/REST/DataSourceController.php b/inc/REST/DataSourceController.php index 7d331583..c3572c39 100644 --- a/inc/REST/DataSourceController.php +++ b/inc/REST/DataSourceController.php @@ -2,9 +2,8 @@ namespace RemoteDataBlocks\REST; -use RemoteDataBlocks\Analytics\TracksAnalytics; -use RemoteDataBlocks\Editor\BlockManagement\ConfigStore; -use RemoteDataBlocks\WpdbStorage\DataSourceCrud; +use RemoteDataBlocks\Telemetry\DataSourceTelemetry; +use RemoteDataBlocks\Config\DataSource\DataSourceConfigManager; use RemoteDataBlocks\Snippet\Snippet; use WP_REST_Controller; use WP_REST_Request; @@ -127,7 +126,7 @@ public function register_routes(): void { '/' . $this->rest_base . '/(?P[a-zA-Z0-9,-]+)', [ 'methods' => 'DELETE', - 'callback' => [ $this, 'delete_multiple_items' ], + 'callback' => [ $this, 'delete_multiple_items' ], 'permission_callback' => [ $this, 'delete_item_permissions_check' ], 'args' => [ 'uuids' => [ @@ -147,12 +146,9 @@ public function register_routes(): void { */ public function create_item( mixed $request ): WP_REST_Response|WP_Error { $data_source_properties = $request->get_json_params(); - $item = DataSourceCrud::create_config( $data_source_properties ); + $item = DataSourceConfigManager::create( $data_source_properties ); - TracksAnalytics::record_event( 'remotedatablocks_data_source_interaction', array_merge( [ - 'data_source_type' => $data_source_properties['service'], - 'action' => 'add', - ], $this->get_data_source_interaction_track_props( $data_source_properties ) ) ); + DataSourceTelemetry::track_add( $data_source_properties ); return rest_ensure_response( $item ); } @@ -164,40 +160,9 @@ public function create_item( mixed $request ): WP_REST_Response|WP_Error { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_items( mixed $request ): WP_REST_Response|WP_Error { - $code_configured_data_sources = ConfigStore::get_data_sources_as_array(); - $ui_configured_data_sources = DataSourceCrud::get_configs(); - - /** - * Quick and dirty de-duplication of data sources. If the data source does - * not have a UUID (because it is registered in code), we generate an - * identifier based on the display name and service name. - * - * UI configured data sources take precedence over code configured ones - * here due to the ordering of the two arrays passed to array_reduce. - */ - $data_sources = array_values( array_reduce( - array_merge( $code_configured_data_sources, $ui_configured_data_sources ), - function ( $acc, $item ) { - $identifier = $item['uuid'] ?? md5( sprintf( '%s_%s', $item['service_config']['display_name'], $item['service'] ) ); - $acc[ $identifier ] = $item; - return $acc; - }, - [] - ) ); - - // Tracks Analytics. Only once per day to reduce noise. - $track_transient_key = 'remotedatablocks_view_data_sources_tracked'; - if ( ! get_transient( $track_transient_key ) ) { - $code_configured_data_sources_count = count( $code_configured_data_sources ); - $ui_configured_data_sources_count = count( $ui_configured_data_sources ); - - TracksAnalytics::record_event( 'remotedatablocks_view_data_sources', [ - 'total_data_sources_count' => $code_configured_data_sources_count + $ui_configured_data_sources_count, - 'code_configured_data_sources_count' => $code_configured_data_sources_count, - 'ui_configured_data_sources_count' => $ui_configured_data_sources_count, - ] ); - set_transient( $track_transient_key, true, DAY_IN_SECONDS ); - } + $data_sources = DataSourceConfigManager::get_all(); + + DataSourceTelemetry::track_view( $data_sources ); return rest_ensure_response( $data_sources ); } @@ -209,7 +174,7 @@ function ( $acc, $item ) { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_item( mixed $request ): WP_REST_Response|WP_Error { - $response = DataSourceCrud::get_config_by_uuid( $request->get_param( 'uuid' ) ); + $response = DataSourceConfigManager::get( $request->get_param( 'uuid' ) ); return rest_ensure_response( $response ); } @@ -226,16 +191,13 @@ public function get_snippets( mixed $request ): WP_REST_Response|WP_Error { */ public function update_item( mixed $request ): WP_REST_Response|WP_Error { $data_source_properties = $request->get_json_params(); - $item = DataSourceCrud::update_config_by_uuid( $request->get_param( 'uuid' ), $data_source_properties ); + $item = DataSourceConfigManager::update( $request->get_param( 'uuid' ), $data_source_properties ); if ( is_wp_error( $item ) ) { return $item; // Return WP_Error if update fails } - TracksAnalytics::record_event( 'remotedatablocks_data_source_interaction', array_merge( [ - 'data_source_type' => $item['service'], - 'action' => 'update', - ], $this->get_data_source_interaction_track_props( $item ) ) ); + DataSourceTelemetry::track_update( $data_source_properties ); return rest_ensure_response( $item ); } @@ -248,13 +210,9 @@ public function update_item( mixed $request ): WP_REST_Response|WP_Error { */ public function delete_item( mixed $request ): WP_REST_Response|WP_Error { $data_source_properties = $request->get_json_params(); - $result = DataSourceCrud::delete_config_by_uuid( $request->get_param( 'uuid' ) ); + $result = DataSourceConfigManager::delete( $request->get_param( 'uuid' ) ); - // Tracks Analytics. - TracksAnalytics::record_event( 'remotedatablocks_data_source_interaction', [ - 'data_source_type' => $data_source_properties['service'], - 'action' => 'delete', - ] ); + DataSourceTelemetry::track_delete( $data_source_properties ); return rest_ensure_response( $result ); } @@ -278,7 +236,7 @@ public function delete_multiple_items( WP_REST_Request $request ): WP_REST_Respo $failed = []; foreach ( $uuids as $uuid ) { - $result = DataSourceCrud::delete_config_by_uuid( $uuid ); + $result = DataSourceConfigManager::delete( $uuid ); if ( is_wp_error( $result ) ) { $failed[] = [ 'uuid' => $uuid, @@ -301,7 +259,6 @@ public function delete_multiple_items( WP_REST_Request $request ): WP_REST_Respo ]); } - // These all require manage_options for now, but we can adjust as needed public function get_item_permissions_check( mixed $request ): bool|WP_Error { @@ -323,16 +280,4 @@ public function update_item_permissions_check( mixed $request ): bool|WP_Error { public function delete_item_permissions_check( mixed $request ): bool|WP_Error { return current_user_can( 'manage_options' ); } - - private function get_data_source_interaction_track_props( array $data_source_properties ): array { - $props = []; - - if ( 'generic-http' === $data_source_properties['service'] ) { - $auth = $data_source_properties['service_config']['auth'] ?? []; - $props['authentication_type'] = $auth['type'] ?? ''; - $props['api_key_location'] = $auth['addTo'] ?? ''; - } - - return $props; - } } diff --git a/inc/Telemetry/DataSourceTelemetry.php b/inc/Telemetry/DataSourceTelemetry.php new file mode 100644 index 00000000..927ffab5 --- /dev/null +++ b/inc/Telemetry/DataSourceTelemetry.php @@ -0,0 +1,79 @@ + $action, + ], $data_source_properties ) ); + } + + /** + * Track data source add. + * + * @param array $data_source_properties Data source properties. + */ + public static function track_add( array $data_source_properties ): void { + self::track_interaction( $data_source_properties, 'add' ); + } + + /** + * Track data source view. + * + * This function tracks when a user views data sources in the WordPress Dashboard via + * Tracks Telemetry. Only once per day to reduce noise. + * + * @param array $data_sources Data sources. + */ + public static function track_view( array $data_sources ): void { + $data_source_types = array_unique( array_map( function ( $data_source ) { + return $data_source['type']; + }, $data_sources ) ); + + TracksTelemetry::record_event( self::DATA_SOURCE_VIEW_EVENT_NAME, [ + 'data_source_types' => implode( ',', $data_source_types ), + 'data_source_count' => count( $data_sources ), + ] ); + } + + /** + * Track data source update. + * + * @param array $data_source_properties Data source properties. + */ + public static function track_update( array $data_source_properties ): void { + self::track_interaction( $data_source_properties, 'update' ); + } + + /** + * Track data source delete. + * + * @param array $data_source_properties Data source properties. + */ + public static function track_delete( array $data_source_properties ): void { + self::track_interaction( $data_source_properties, 'delete' ); + } +} diff --git a/inc/Analytics/EnvironmentConfig.php b/inc/Telemetry/EnvironmentConfig.php similarity index 91% rename from inc/Analytics/EnvironmentConfig.php rename to inc/Telemetry/EnvironmentConfig.php index ab1d1040..21712c3c 100644 --- a/inc/Analytics/EnvironmentConfig.php +++ b/inc/Telemetry/EnvironmentConfig.php @@ -1,6 +1,6 @@ { table: {}, }; + const isItemEligibleForActions = ( item: DataSourceConfig ) => { + return item.config_source === ConfigSource.STORAGE; + }; + const actions: Action< DataSourceConfig >[] = [ { id: 'edit', label: __( 'Edit', 'remote-data-blocks' ), icon: 'edit', isPrimary: true, - isEligible: ( item: DataSourceConfig ) => { - return Boolean( item?.uuid ); - }, + isEligible: isItemEligibleForActions, callback: ( [ item ]: DataSourceConfig[] ) => { if ( item?.uuid ) { onEditDataSource( item.uuid ); @@ -167,9 +173,7 @@ const DataSourceList = () => { id: 'copy', label: __( 'Copy UUID', 'remote-data-blocks' ), icon: 'copy', - isEligible: ( item: DataSourceConfig ) => { - return Boolean( item?.uuid ); - }, + isEligible: isItemEligibleForActions, callback: ( [ item ]: DataSourceConfig[] ) => { if ( item && item.uuid ) { navigator.clipboard @@ -191,9 +195,7 @@ const DataSourceList = () => { label: __( 'Delete', 'remote-data-blocks' ), icon: 'trash', isDestructive: true, - isEligible: ( item: DataSourceConfig ) => { - return Boolean( item?.uuid ); - }, + isEligible: isItemEligibleForActions, callback: ( items: DataSourceConfig[] ) => { if ( items.length === 1 ) { if ( items[ 0 ] ) { @@ -208,9 +210,7 @@ const DataSourceList = () => { { id: 'duplicate', label: __( 'Duplicate', 'remote-data-blocks' ), - isEligible: ( item: DataSourceConfig ) => { - return Boolean( item?.uuid ); - }, + isEligible: isItemEligibleForActions, callback: ( [ item ]: DataSourceConfig[] ) => { if ( item ) { const duplicatedSource = { @@ -240,7 +240,7 @@ const DataSourceList = () => { { id: 'view-code', label: __( 'View Code', 'remote-data-blocks' ), - isEligible: ( item: DataSourceConfig ) => Boolean( item?.uuid ), + isEligible: isItemEligibleForActions, callback: ( [ item ]: DataSourceConfig[] ) => { if ( item?.uuid ) { setCurrentSource( item ); diff --git a/src/data-sources/DataSourceMetaTags.tsx b/src/data-sources/DataSourceMetaTags.tsx index 934edf28..7ba8c66c 100644 --- a/src/data-sources/DataSourceMetaTags.tsx +++ b/src/data-sources/DataSourceMetaTags.tsx @@ -2,6 +2,7 @@ import { Icon, Tooltip } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { chevronRightSmall } from '@wordpress/icons'; +import { ConfigSource } from '@/data-sources/constants'; import { DataSourceConfig } from '@/data-sources/types'; import './DataSourceList.scss'; @@ -61,10 +62,19 @@ const CodeBadge = () => { ); }; +const ConstantsBadge = () => { + return ( + + Constants + + ); +}; + const DataSourceMetaTags = ( props: DataSourceMetaTagsProps ) => { return ( <> - { ! props.source.uuid && } + { props.source.config_source === ConfigSource.CODE && } + { props.source.config_source === ConfigSource.CONSTANTS && } ); diff --git a/src/data-sources/airtable/AirtableSettings.tsx b/src/data-sources/airtable/AirtableSettings.tsx index a04a5382..ef736899 100644 --- a/src/data-sources/airtable/AirtableSettings.tsx +++ b/src/data-sources/airtable/AirtableSettings.tsx @@ -7,6 +7,7 @@ import { getAirtableOutputQueryMappingValues } from '@/data-sources/airtable/uti import { DataSourceForm } from '@/data-sources/components/DataSourceForm'; import { FieldsSelection } from '@/data-sources/components/FieldsSelection'; import PasswordInputControl from '@/data-sources/components/PasswordInputControl'; +import { ConfigSource } from '@/data-sources/constants'; import { useAirtableApiBases, useAirtableApiTables, @@ -76,6 +77,7 @@ export const AirtableSettings = ( { service: 'airtable', service_config: validState, uuid: uuid ?? null, + config_source: ConfigSource.STORAGE, }; return onSave( airtableConfig, mode ); diff --git a/src/data-sources/constants.ts b/src/data-sources/constants.ts index eb100b06..d8d80c85 100644 --- a/src/data-sources/constants.ts +++ b/src/data-sources/constants.ts @@ -45,3 +45,9 @@ export const HTTP_SOURCE_ADD_TO_SELECT_OPTIONS: SelectOption< HttpApiKeyDestinat { label: __( 'Header', 'remote-data-blocks' ), value: 'header' }, { label: __( 'Query Params', 'remote-data-blocks' ), value: 'queryparams' }, ]; + +export enum ConfigSource { + CODE = 'code', + STORAGE = 'storage', + CONSTANTS = 'constant', +} diff --git a/src/data-sources/google-sheets/GoogleSheetsSettings.tsx b/src/data-sources/google-sheets/GoogleSheetsSettings.tsx index af730392..e66c33af 100644 --- a/src/data-sources/google-sheets/GoogleSheetsSettings.tsx +++ b/src/data-sources/google-sheets/GoogleSheetsSettings.tsx @@ -4,7 +4,7 @@ import { __ } from '@wordpress/i18n'; import { DataSourceForm } from '@/data-sources/components/DataSourceForm'; import { FieldsSelection } from '@/data-sources/components/FieldsSelection'; -import { GOOGLE_SHEETS_API_SCOPES } from '@/data-sources/constants'; +import { GOOGLE_SHEETS_API_SCOPES, ConfigSource } from '@/data-sources/constants'; import { useDataSources } from '@/data-sources/hooks/useDataSources'; import { useGoogleSpreadsheetsOptions, @@ -97,6 +97,7 @@ export const GoogleSheetsSettings = ( { service: 'google-sheets', service_config: validState, uuid: uuid ?? null, + config_source: ConfigSource.STORAGE, }; return onSave( data, mode ); diff --git a/src/data-sources/http/HttpSettings.tsx b/src/data-sources/http/HttpSettings.tsx index 82e8a17a..ab4f50fd 100644 --- a/src/data-sources/http/HttpSettings.tsx +++ b/src/data-sources/http/HttpSettings.tsx @@ -3,6 +3,7 @@ import { __ } from '@wordpress/i18n'; import { DataSourceForm } from '../components/DataSourceForm'; import { HttpAuthSettingsInput } from '@/data-sources/components/HttpAuthSettingsInput'; +import { ConfigSource } from '@/data-sources/constants'; import { useDataSources } from '@/data-sources/hooks/useDataSources'; import { HttpAuth } from '@/data-sources/http/types'; import { HttpConfig, HttpServiceConfig, SettingsComponentProps } from '@/data-sources/types'; @@ -63,6 +64,7 @@ export const HttpSettings = ( { mode, uuid, config }: SettingsComponentProps< Ht service: 'generic-http', service_config: validState, uuid: uuid ?? null, + config_source: ConfigSource.STORAGE, }; return onSave( httpConfig, mode ); diff --git a/src/data-sources/salesforce-b2c/SalesforceB2CSettings.tsx b/src/data-sources/salesforce-b2c/SalesforceB2CSettings.tsx index 9ca6a8bf..adbb32ef 100644 --- a/src/data-sources/salesforce-b2c/SalesforceB2CSettings.tsx +++ b/src/data-sources/salesforce-b2c/SalesforceB2CSettings.tsx @@ -4,6 +4,7 @@ import { __ } from '@wordpress/i18n'; import { DataSourceForm } from '../components/DataSourceForm'; import PasswordInputControl from '@/data-sources/components/PasswordInputControl'; +import { ConfigSource } from '@/data-sources/constants'; import { useDataSources } from '@/data-sources/hooks/useDataSources'; import { SettingsComponentProps, @@ -42,6 +43,7 @@ export const SalesforceB2CSettings = ( { service: 'salesforce-b2c', service_config: validState, uuid: uuid ?? null, + config_source: ConfigSource.STORAGE, }; return onSave( data, mode ); diff --git a/src/data-sources/shopify/ShopifySettings.tsx b/src/data-sources/shopify/ShopifySettings.tsx index f8129269..8d4bdf61 100644 --- a/src/data-sources/shopify/ShopifySettings.tsx +++ b/src/data-sources/shopify/ShopifySettings.tsx @@ -4,6 +4,7 @@ import { __ } from '@wordpress/i18n'; import { DataSourceForm } from '../components/DataSourceForm'; import PasswordInputControl from '@/data-sources/components/PasswordInputControl'; +import { ConfigSource } from '@/data-sources/constants'; import { useDataSources } from '@/data-sources/hooks/useDataSources'; import { useShopifyShopName } from '@/data-sources/hooks/useShopify'; import { SettingsComponentProps, ShopifyConfig, ShopifyServiceConfig } from '@/data-sources/types'; @@ -59,6 +60,7 @@ export const ShopifySettings = ( { service: 'shopify', service_config: validState, uuid: uuid ?? null, + config_source: ConfigSource.STORAGE, }; return onSave( data, mode ); diff --git a/src/data-sources/types.ts b/src/data-sources/types.ts index 7790e6ec..729d19b9 100644 --- a/src/data-sources/types.ts +++ b/src/data-sources/types.ts @@ -1,4 +1,4 @@ -import { SUPPORTED_SERVICES } from '@/data-sources/constants'; +import { SUPPORTED_SERVICES, ConfigSource } from '@/data-sources/constants'; import { HttpAuth } from '@/data-sources/http/types'; import { StringIdName } from '@/types/common'; import { GoogleServiceAccountKey } from '@/types/google'; @@ -10,7 +10,6 @@ interface BaseServiceConfig extends Record< string, unknown > { display_name: string; enable_blocks: boolean; } - interface BaseDataSourceConfig< ServiceName extends DataSourceType, ServiceConfig extends BaseServiceConfig @@ -18,6 +17,7 @@ interface BaseDataSourceConfig< service: ServiceName; service_config: ServiceConfig; uuid: string | null; + config_source: ConfigSource; } export interface DataSourceQueryMappingValue { diff --git a/tests/inc/Config/DataSource/ConstantConfigStoreTest.php b/tests/inc/Config/DataSource/ConstantConfigStoreTest.php new file mode 100644 index 00000000..1f0d1c4c --- /dev/null +++ b/tests/inc/Config/DataSource/ConstantConfigStoreTest.php @@ -0,0 +1,140 @@ +valid_config = [ + 'uuid' => self::TEST_UUID, + 'service' => self::TEST_SERVICE, + 'service_config' => [ + '__version' => 1, + 'store_name' => 'test-store', + 'access_token' => 'gy56yrtyrtt', + 'display_name' => 'Test Store', + ], + ]; + + $this->invalid_config = [ + 'uuid' => self::TEST_UUID_2, + 'service' => self::INVALID_SERVICE, + 'service_config' => [], + ]; + + $mock_data_source = Mockery::mock( DataSourceInterface::class ); + $mock_class = Mockery::mock( 'overload:' . HttpDataSource::class ); + $mock_class->shouldReceive( 'from_array' )->andReturn( $mock_data_source ); + + if ( ! defined( 'REMOTE_DATA_BLOCKS__DATA_SOURCE_CLASSMAP' ) ) { + define( 'REMOTE_DATA_BLOCKS__DATA_SOURCE_CLASSMAP', [ + self::TEST_SERVICE => HttpDataSource::class, + ] ); + } + } + + protected function tearDown(): void { + Mockery::close(); + parent::tearDown(); + } + + private function defineConfigs( mixed $configs ): void { + if ( ! defined( self::CONFIG_CONSTANT ) ) { + define( self::CONFIG_CONSTANT, $configs ); + } + } + + public function testIsAvailableReturnsFalseWhenConstantNotDefined(): void { + $this->assertFalse( ConstantConfigStore::is_available() ); + } + + public function testIsAvailableReturnsFalseWhenConstantIsEmptyString(): void { + $this->defineConfigs( '' ); + $this->assertFalse( ConstantConfigStore::is_available() ); + } + + public function testIsAvailableReturnsTrueWhenConstantIsArray(): void { + $this->defineConfigs( [ [ 'test' => 'value' ] ] ); + $this->assertTrue( ConstantConfigStore::is_available() ); + } + + public function testGetConfigsReturnsEmptyArrayWhenConstantNotDefined(): void { + $this->assertSame( [], ConstantConfigStore::get_configs() ); + } + + public function testGetConfigsReturnsOnlyValidConfigs(): void { + $this->defineConfigs( [ $this->valid_config, $this->invalid_config ] ); + + $configs = ConstantConfigStore::get_configs(); + $this->assertCount( 1, $configs ); + $this->assertSame( $this->valid_config, $configs[0] ); + } + + public function testGetConfigByUuidReturnsErrorWhenConfigNotFound(): void { + $this->defineConfigs( [] ); + + $result = ConstantConfigStore::get_config_by_uuid( self::TEST_UUID ); + $this->assertInstanceOf( WP_Error::class, $result ); + $this->assertSame( 'data_source_not_found', $result->get_error_code() ); + } + + public function testGetConfigByUuidReturnsErrorForInvalidConfig(): void { + $invalid_config = [ + 'uuid' => self::TEST_UUID, + 'service' => self::INVALID_SERVICE, + 'service_config' => [], + ]; + + $this->defineConfigs( [ $invalid_config ] ); + + $result = ConstantConfigStore::get_config_by_uuid( self::TEST_UUID ); + $this->assertInstanceOf( WP_Error::class, $result ); + $this->assertSame( 'unsupported_data_source', $result->get_error_code() ); + } + + public function testGetConfigByUuidReturnsConfigWhenValid(): void { + $this->defineConfigs( [ $this->valid_config ] ); + + $result = ConstantConfigStore::get_config_by_uuid( self::TEST_UUID ); + $this->assertIsArray( $result ); + $this->assertSame( $this->valid_config, $result ); + } + + public function testValidateConfigReturnsErrorForUnsupportedService(): void { + $config = [ + 'uuid' => self::TEST_UUID, + 'service' => self::INVALID_SERVICE, + 'service_config' => [], + ]; + + $result = ConstantConfigStore::validate_config( $config ); + $this->assertInstanceOf( WP_Error::class, $result ); + $this->assertSame( 'unsupported_data_source', $result->get_error_code() ); + } + + public function testValidateConfigReturnsInstanceForValidConfig(): void { + $result = ConstantConfigStore::validate_config( $this->valid_config ); + $this->assertInstanceOf( DataSourceInterface::class, $result ); + } +} diff --git a/tests/inc/Config/DataSource/DataSourceConfigManagerTest.php b/tests/inc/Config/DataSource/DataSourceConfigManagerTest.php new file mode 100644 index 00000000..2cb01cf4 --- /dev/null +++ b/tests/inc/Config/DataSource/DataSourceConfigManagerTest.php @@ -0,0 +1,389 @@ +airtable_storage_config = [ + 'uuid' => self::AIRTABLE_UUID, + 'service' => self::AIRTABLE_SERVICE, + 'service_config' => [ + '__version' => 1, + 'enable_blocks' => true, + 'display_name' => 'Test Airtable', + 'access_token' => 'test.airtable.access-token', + 'tables' => [ + [ + 'id' => 'test_table_id', + 'name' => 'Test Table', + 'output_query_mappings' => [ + [ + 'path' => '$.fields["Name"]', + 'name' => 'Name', + 'key' => 'Name', + 'type' => 'string', + ], + ], + ], + ], + 'base' => [ + 'id' => 'test_base_id', + 'name' => 'Test Base', + ], + ], + '__metadata' => [ + 'created_at' => '2025-02-14 11:05:36', + 'updated_at' => '2025-02-14 11:05:49', + ], + 'config_source' => DataSourceConfigManager::CONFIG_SOURCE_STORAGE, + ]; + + $this->sheets_constant_config = [ + 'uuid' => self::SHEETS_UUID, + 'service' => self::SHEETS_SERVICE, + 'service_config' => [ + '__version' => 1, + 'enable_blocks' => true, + 'display_name' => 'Test Google Sheets', + 'credentials' => [ + 'type' => 'service_account', + 'project_id' => 'test-gcp-project', + 'private_key_id' => 'xyz987abc654def321ghi', + 'private_key' => '-----BEGIN PRIVATE KEY-----\nREDACTED\n-----END PRIVATE KEY-----\n', + 'client_email' => 'test-gcp-project@test-gcp-project.iam.gserviceaccount.com', + 'client_id' => '1234567890', + 'auth_uri' => 'https://accounts.google.com/o/oauth2/auth', + 'token_uri' => 'https://oauth2.googleapis.com/token', + 'auth_provider_x509_cert_url' => 'https://www.googleapis.com/oauth2/v1/certs', + 'client_x509_cert_url' => 'https://www.googleapis.com/robot/v1/metadata/x509/test%40test-gcp-project.iam.gserviceaccount.com', + 'universe_domain' => 'googleapis.com', + ], + 'sheets' => [ + [ + 'id' => '0', + 'name' => 'Test Sheet', + 'output_query_mappings' => [ + [ + 'key' => 'Name', + 'name' => 'Name', + 'path' => '$["Name"]', + 'type' => 'string', + ], + ], + ], + ], + 'spreadsheet' => [ + 'id' => 'some-spreadsheet-id', + 'name' => 'Test Spreadsheet', + ], + ], + 'config_source' => DataSourceConfigManager::CONFIG_SOURCE_CONSTANT, + ]; + + $this->shopify_code_config = [ + 'uuid' => self::SHOPIFY_UUID, + 'service' => self::SHOPIFY_SERVICE, + 'service_config' => [ + '__version' => 1, + 'access_token' => 'shpat_abc123def456ghi789jkl0', + 'store_name' => 'test-shopify-store', + 'display_name' => 'Test Shopify Store', + 'enable_blocks' => true, + ], + 'config_source' => DataSourceConfigManager::CONFIG_SOURCE_CODE, + ]; + } + + protected function tearDown(): void { + Mockery::close(); + parent::tearDown(); + } + + public function testGetAllConfiguredByServiceReturnsEmptyArrayWhenNoConfigsExist(): void { + $mock_storage_crud = Mockery::mock( 'alias:' . DataSourceCrud::class ); + $mock_storage_crud->shouldReceive( 'get_configs' ) + ->andReturn( [] ); + + $mock_constant = Mockery::mock( 'alias:' . ConstantConfigStore::class ); + $mock_constant->shouldReceive( 'get_configs' ) + ->andReturn( [] ); + + $result = DataSourceConfigManager::get_all_configured_by_service( self::AIRTABLE_SERVICE ); + $this->assertEmpty( $result ); + } + + public function testGetAllConfiguredByServiceFiltersConfigsByService(): void { + $sheets_storage_config = array_merge( $this->airtable_storage_config, [ + 'uuid' => '12345678-1234-1234-1234-123456789012', + 'service' => self::SHEETS_SERVICE, + ] ); + + $shopify_constant_config = array_merge( $this->sheets_constant_config, [ + 'uuid' => '98765432-9876-9876-9876-987654321098', + 'service' => self::SHOPIFY_SERVICE, + ] ); + + $mock_storage_crud = Mockery::mock( 'alias:' . DataSourceCrud::class ); + $mock_storage_crud->shouldReceive( 'get_configs' ) + ->andReturn( [ $this->airtable_storage_config, $sheets_storage_config ] ); + + $mock_constant = Mockery::mock( 'alias:' . ConstantConfigStore::class ); + $mock_constant->shouldReceive( 'get_configs' ) + ->andReturn( [ $this->sheets_constant_config, $shopify_constant_config ] ); + + // Test filtering Sheets service + $sheets_result = DataSourceConfigManager::get_all_configured_by_service( self::SHEETS_SERVICE ); + $this->assertCount( 2, $sheets_result ); + $this->assertContains( $sheets_storage_config, $sheets_result ); + $this->assertContains( $this->sheets_constant_config, $sheets_result ); + $this->assertNotContains( $this->airtable_storage_config, $sheets_result ); + $this->assertNotContains( $shopify_constant_config, $sheets_result ); + + // Test filtering Airtable service + $airtable_result = DataSourceConfigManager::get_all_configured_by_service( self::AIRTABLE_SERVICE ); + $this->assertCount( 1, $airtable_result ); + $this->assertContains( $this->airtable_storage_config, $airtable_result ); + $this->assertNotContains( $sheets_storage_config, $airtable_result ); + $this->assertNotContains( $this->sheets_constant_config, $airtable_result ); + + // Test filtering Shopify service + $shopify_result = DataSourceConfigManager::get_all_configured_by_service( self::SHOPIFY_SERVICE ); + $this->assertCount( 1, $shopify_result ); + $this->assertContains( $shopify_constant_config, $shopify_result ); + $this->assertNotContains( $this->airtable_storage_config, $shopify_result ); + $this->assertNotContains( $sheets_storage_config, $shopify_result ); + } + + public function testGetAllReturnsConfigsFromAllSources(): void { + $mock_storage_crud = Mockery::mock( 'alias:' . DataSourceCrud::class ); + $mock_storage_crud->shouldReceive( 'get_configs' ) + ->andReturn( [ $this->airtable_storage_config ] ); + + $mock_constant = Mockery::mock( 'alias:' . ConstantConfigStore::class ); + $mock_constant->shouldReceive( 'get_configs' ) + ->andReturn( [ $this->sheets_constant_config ] ); + + $mock_config_store = Mockery::mock( 'alias:' . ConfigStore::class ); + $mock_config_store->shouldReceive( 'get_data_sources_as_array' ) + ->andReturn( [ $this->shopify_code_config ] ); + + $result = DataSourceConfigManager::get_all(); + + $this->assertCount( 3, $result ); + $this->assertContains( $this->airtable_storage_config, $result ); + $this->assertContains( $this->sheets_constant_config, $result ); + $this->assertContains( $this->shopify_code_config, $result ); + } + + public function testGetReturnsConfigFromConstant(): void { + $mock_constant = Mockery::mock( 'alias:' . ConstantConfigStore::class ); + $mock_constant->shouldReceive( 'get_config_by_uuid' ) + ->with( self::SHEETS_UUID ) + ->andReturn( $this->sheets_constant_config ); + + $result = DataSourceConfigManager::get( self::SHEETS_UUID ); + $this->assertSame( $this->sheets_constant_config, $result ); + } + + public function testGetReturnsConfigFromStorage(): void { + $mock_constant = Mockery::mock( 'alias:' . ConstantConfigStore::class ); + $mock_constant->shouldReceive( 'get_config_by_uuid' ) + ->with( self::AIRTABLE_UUID ) + ->andReturn( new WP_Error( 'not_found' ) ); + + $mock_storage_crud = Mockery::mock( 'alias:' . DataSourceCrud::class ); + $mock_storage_crud->shouldReceive( 'get_config_by_uuid' ) + ->with( self::AIRTABLE_UUID ) + ->andReturn( $this->airtable_storage_config ); + + $result = DataSourceConfigManager::get( self::AIRTABLE_UUID ); + $this->assertSame( $this->airtable_storage_config, $result ); + } + + public function testGetReturnsErrorWhenConfigNotFound(): void { + $mock_constant = Mockery::mock( 'alias:' . ConstantConfigStore::class ); + $mock_constant->shouldReceive( 'get_config_by_uuid' ) + ->with( self::AIRTABLE_UUID ) + ->andReturn( new WP_Error( 'not_found' ) ); + + $mock_storage_crud = Mockery::mock( 'alias:' . DataSourceCrud::class ); + $mock_storage_crud->shouldReceive( 'get_config_by_uuid' ) + ->with( self::AIRTABLE_UUID ) + ->andReturn( new WP_Error( 'not_found' ) ); + + $result = DataSourceConfigManager::get( self::AIRTABLE_UUID ); + $this->assertInstanceOf( WP_Error::class, $result ); + $this->assertSame( 'data_source_not_found', $result->get_error_code() ); + } + + public function testCreateReturnsNewConfig(): void { + $mock_storage_crud = Mockery::mock( 'alias:' . DataSourceCrud::class ); + $mock_storage_crud->shouldReceive( 'create_config' ) + ->with( $this->airtable_storage_config ) + ->andReturn( $this->airtable_storage_config ); + + $result = DataSourceConfigManager::create( $this->airtable_storage_config ); + $this->assertSame( $this->airtable_storage_config, $result ); + } + + public function testCreateReturnsErrorOnFailure(): void { + $mock_storage_crud = Mockery::mock( 'alias:' . DataSourceCrud::class ); + $mock_storage_crud->shouldReceive( 'create_config' ) + ->with( $this->airtable_storage_config ) + ->andReturn( new WP_Error( 'create_failed' ) ); + + $result = DataSourceConfigManager::create( $this->airtable_storage_config ); + $this->assertInstanceOf( WP_Error::class, $result ); + } + + public function testUpdateReturnsUpdatedConfig(): void { + $mock_storage_crud = Mockery::mock( 'alias:' . DataSourceCrud::class ); + $mock_storage_crud->shouldReceive( 'update_config_by_uuid' ) + ->with( self::AIRTABLE_UUID, $this->airtable_storage_config ) + ->andReturn( $this->airtable_storage_config ); + + $result = DataSourceConfigManager::update( self::AIRTABLE_UUID, $this->airtable_storage_config ); + $this->assertSame( $this->airtable_storage_config, $result ); + } + + public function testUpdateReturnsErrorForImmutableConfig(): void { + $immutable_config = array_merge( + $this->sheets_constant_config, + [ 'config_source' => DataSourceConfigManager::CONFIG_SOURCE_CONSTANT ] + ); + + $result = DataSourceConfigManager::update( self::SHEETS_UUID, $immutable_config ); + $this->assertInstanceOf( WP_Error::class, $result ); + $this->assertSame( 'cannot_update_config', $result->get_error_code() ); + } + + public function testUpdateReturnsErrorOnFailure(): void { + $mock_storage_crud = Mockery::mock( 'alias:' . DataSourceCrud::class ); + $mock_storage_crud->shouldReceive( 'update_config_by_uuid' ) + ->with( self::AIRTABLE_UUID, $this->airtable_storage_config ) + ->andReturn( new WP_Error( 'update_failed' ) ); + + $result = DataSourceConfigManager::update( self::AIRTABLE_UUID, $this->airtable_storage_config ); + $this->assertInstanceOf( WP_Error::class, $result ); + } + + public function testDeleteReturnsTrue(): void { + $mock_storage_crud = Mockery::mock( 'alias:' . DataSourceCrud::class ); + $mock_storage_crud->shouldReceive( 'delete_config_by_uuid' ) + ->with( self::AIRTABLE_UUID ) + ->andReturn( true ); + + $result = DataSourceConfigManager::delete( self::AIRTABLE_UUID ); + $this->assertTrue( $result ); + } + + public function testDeleteReturnsErrorOnFailure(): void { + $mock_storage_crud = Mockery::mock( 'alias:' . DataSourceCrud::class ); + $mock_storage_crud->shouldReceive( 'delete_config_by_uuid' ) + ->with( self::AIRTABLE_UUID ) + ->andReturn( new WP_Error( 'delete_failed' ) ); + + $result = DataSourceConfigManager::delete( self::AIRTABLE_UUID ); + $this->assertInstanceOf( WP_Error::class, $result ); + } + + public function testGetAllHandlesConfigPrecedenceCorrectly(): void { + // Create configs with same UUID to test precedence + $storage_sheets = array_merge( $this->sheets_constant_config, [ + 'service_config' => array_merge( $this->sheets_constant_config['service_config'], [ + 'display_name' => 'Storage Sheets', + ] ), + 'config_source' => DataSourceConfigManager::CONFIG_SOURCE_STORAGE, + ] ); + + $constant_sheets = array_merge( $this->sheets_constant_config, [ + 'service_config' => array_merge( $this->sheets_constant_config['service_config'], [ + 'display_name' => 'Constant Sheets', + ] ), + 'config_source' => DataSourceConfigManager::CONFIG_SOURCE_CONSTANT, + ] ); + + $code_sheets = array_merge( $this->sheets_constant_config, [ + 'service_config' => array_merge( $this->sheets_constant_config['service_config'], [ + 'display_name' => 'Code Sheets', + ] ), + 'config_source' => DataSourceConfigManager::CONFIG_SOURCE_CODE, + ] ); + + $mock_storage_crud = Mockery::mock( 'alias:' . DataSourceCrud::class ); + $mock_storage_crud->shouldReceive( 'get_configs' ) + ->andReturn( [ $storage_sheets ] ); + + $mock_constant = Mockery::mock( 'alias:' . ConstantConfigStore::class ); + $mock_constant->shouldReceive( 'get_configs' ) + ->andReturn( [ $constant_sheets ] ); + + $mock_config_store = Mockery::mock( 'alias:' . ConfigStore::class ); + $mock_config_store->shouldReceive( 'get_data_sources_as_array' ) + ->andReturn( [ $code_sheets ] ); + + $result = DataSourceConfigManager::get_all(); + + // Should only get one config since they share the same UUID + $this->assertCount( 1, $result ); + + // Storage should win due to highest precedence + $this->assertContains( $storage_sheets, $result ); + $this->assertNotContains( $constant_sheets, $result ); + $this->assertNotContains( $code_sheets, $result ); + } + + public function testGetAllConfiguredByServiceHandlesPrecedenceCorrectly(): void { + // Create storage config with same UUID as constant config + $storage_sheets = array_merge( $this->sheets_constant_config, [ + 'service_config' => array_merge( $this->sheets_constant_config['service_config'], [ + 'display_name' => 'Storage Sheets', + ] ), + 'config_source' => DataSourceConfigManager::CONFIG_SOURCE_STORAGE, + ] ); + + $mock_storage_crud = Mockery::mock( 'alias:' . DataSourceCrud::class ); + $mock_storage_crud->shouldReceive( 'get_configs' ) + ->once() + ->andReturn( [ $storage_sheets ] ); + + $mock_constant = Mockery::mock( 'alias:' . ConstantConfigStore::class ); + $mock_constant->shouldReceive( 'get_configs' ) + ->once() + ->andReturn( [ $this->sheets_constant_config ] ); + + $result = DataSourceConfigManager::get_all_configured_by_service( self::SHEETS_SERVICE ); + + // Should only get one config since they share the same UUID + $this->assertCount( 1, $result ); + + // Storage should win due to highest precedence + $this->assertContains( $storage_sheets, $result ); + $this->assertNotContains( $this->sheets_constant_config, $result ); + } +} diff --git a/tests/inc/Analytics/EnvironmentConfigTest.php b/tests/inc/Telemetry/EnvironmentConfigTest.php similarity index 95% rename from tests/inc/Analytics/EnvironmentConfigTest.php rename to tests/inc/Telemetry/EnvironmentConfigTest.php index 7c017485..0810f08d 100644 --- a/tests/inc/Analytics/EnvironmentConfigTest.php +++ b/tests/inc/Telemetry/EnvironmentConfigTest.php @@ -1,10 +1,10 @@ assertEquals( null, TracksAnalytics::get_instance() ); + $this->assertEquals( null, TracksTelemetry::get_instance() ); } public function testInitDoesNotSetTracksOnLocalEnvironment(): void { @@ -31,9 +31,9 @@ public function testInitDoesNotSetTracksOnLocalEnvironment(): void { $env_config_mock->method( 'is_local_env' )->with()->willReturn( true ); $env_config_mock->method( 'is_enabled_via_filter' )->with()->willReturn( true ); - TracksAnalytics::init( $env_config_mock ); + TracksTelemetry::init( $env_config_mock ); - $this->assertEquals( null, TracksAnalytics::get_instance() ); + $this->assertEquals( null, TracksTelemetry::get_instance() ); } public function testInitDoesSetTracksIfTrackingIsEnabledViaFilter(): void { @@ -43,9 +43,9 @@ public function testInitDoesSetTracksIfTrackingIsEnabledViaFilter(): void { $env_config_mock->method( 'get_tracks_lib_class' )->with()->willReturn( MockTracks::class ); $env_config_mock->expects( $this->once() )->method( 'get_remote_data_blocks_properties' )->with(); - TracksAnalytics::init( $env_config_mock ); + TracksTelemetry::init( $env_config_mock ); - $this->assertInstanceOf( MockTracks::class, TracksAnalytics::get_instance() ); + $this->assertInstanceOf( MockTracks::class, TracksTelemetry::get_instance() ); } public function testInitDoesSetTracksIfTrackingIsEnabledOnVipSite(): void { @@ -55,9 +55,9 @@ public function testInitDoesSetTracksIfTrackingIsEnabledOnVipSite(): void { $env_config_mock->method( 'get_tracks_lib_class' )->with()->willReturn( MockTracks::class ); $env_config_mock->expects( $this->once() )->method( 'get_remote_data_blocks_properties' )->with(); - TracksAnalytics::init( $env_config_mock ); + TracksTelemetry::init( $env_config_mock ); - $this->assertInstanceOf( MockTracks::class, TracksAnalytics::get_instance() ); + $this->assertInstanceOf( MockTracks::class, TracksTelemetry::get_instance() ); } public function testGetGlobalProperties(): void { @@ -65,12 +65,12 @@ public function testGetGlobalProperties(): void { $env_config_mock = $this->getMockBuilder( EnvironmentConfig::class )->onlyMethods( [ 'get_tracks_core_properties' ] )->getMock(); $env_config_mock->expects( $this->exactly( 2 ) )->method( 'get_tracks_core_properties' )->with()->willReturn( [ 'vip_env' => '123' ] ); - TracksAnalytics::init( $env_config_mock ); + TracksTelemetry::init( $env_config_mock ); $this->assertEquals( [ 'plugin_version' => '', 'vip_env' => '123', - ], TracksAnalytics::get_global_properties() ); + ], TracksTelemetry::get_global_properties() ); } public function testTrackPluginActivationDoesNotRecordEventIfPluginIsNotRDB(): void { @@ -82,9 +82,9 @@ public function testTrackPluginActivationDoesNotRecordEventIfPluginIsNotRDB(): v $tracks_mock = $this->getMockBuilder( MockTracks::class )->onlyMethods( [ 'record_event' ] )->getMock(); $tracks_mock->expects( $this->exactly( 0 ) )->method( 'record_event' ); - set_private_property( TracksAnalytics::class, null, 'instance', $tracks_mock ); - TracksAnalytics::init( $env_config_mock ); - TracksAnalytics::track_plugin_activation( 'plugin_path' ); + set_private_property( TracksTelemetry::class, null, 'instance', $tracks_mock ); + TracksTelemetry::init( $env_config_mock ); + TracksTelemetry::track_plugin_activation( 'plugin_path' ); } public function testTrackPluginActivationDoesRecordEventIfPluginIsRDB(): void { @@ -96,9 +96,9 @@ public function testTrackPluginActivationDoesRecordEventIfPluginIsRDB(): void { $tracks_mock = $this->getMockBuilder( MockTracks::class )->onlyMethods( [ 'record_event' ] )->getMock(); $tracks_mock->expects( $this->exactly( 1 ) )->method( 'record_event' )->with( 'remotedatablocks_plugin_toggle', $this->isType( 'array' ) ); - set_private_property( TracksAnalytics::class, null, 'instance', $tracks_mock ); - TracksAnalytics::init( $env_config_mock ); - TracksAnalytics::track_plugin_activation( 'plugin_path' ); + set_private_property( TracksTelemetry::class, null, 'instance', $tracks_mock ); + TracksTelemetry::init( $env_config_mock ); + TracksTelemetry::track_plugin_activation( 'plugin_path' ); } public function testTrackPluginDeactivationDoesNotRecordEventIfPluginIsNotRDB(): void { @@ -110,9 +110,9 @@ public function testTrackPluginDeactivationDoesNotRecordEventIfPluginIsNotRDB(): $tracks_mock = $this->getMockBuilder( MockTracks::class )->onlyMethods( [ 'record_event' ] )->getMock(); $tracks_mock->expects( $this->exactly( 0 ) )->method( 'record_event' ); - set_private_property( TracksAnalytics::class, null, 'instance', $tracks_mock ); - TracksAnalytics::init( $env_config_mock ); - TracksAnalytics::track_plugin_deactivation( 'plugin_path' ); + set_private_property( TracksTelemetry::class, null, 'instance', $tracks_mock ); + TracksTelemetry::init( $env_config_mock ); + TracksTelemetry::track_plugin_deactivation( 'plugin_path' ); } public function testTrackPluginDeactivationDoesRecordEventIfPluginIsRDB(): void { @@ -124,9 +124,9 @@ public function testTrackPluginDeactivationDoesRecordEventIfPluginIsRDB(): void $tracks_mock = $this->getMockBuilder( MockTracks::class )->onlyMethods( [ 'record_event' ] )->getMock(); $tracks_mock->expects( $this->exactly( 1 ) )->method( 'record_event' )->with( 'remotedatablocks_plugin_toggle', $this->isType( 'array' ) ); - set_private_property( TracksAnalytics::class, null, 'instance', $tracks_mock ); - TracksAnalytics::init( $env_config_mock ); - TracksAnalytics::track_plugin_deactivation( 'plugin_path' ); + set_private_property( TracksTelemetry::class, null, 'instance', $tracks_mock ); + TracksTelemetry::init( $env_config_mock ); + TracksTelemetry::track_plugin_deactivation( 'plugin_path' ); } public function testTrackRemoteDataBlocksUsageDoesNotTrackEventIfUsageShouldNotBeTracked(): void { @@ -138,9 +138,9 @@ public function testTrackRemoteDataBlocksUsageDoesNotTrackEventIfUsageShouldNotB $tracks_mock = $this->getMockBuilder( MockTracks::class )->onlyMethods( [ 'record_event' ] )->getMock(); $tracks_mock->expects( $this->exactly( 0 ) )->method( 'record_event' ); - set_private_property( TracksAnalytics::class, null, 'instance', $tracks_mock ); - TracksAnalytics::init( $env_config_mock ); - TracksAnalytics::track_remote_data_blocks_usage( 1, (object) [] ); + set_private_property( TracksTelemetry::class, null, 'instance', $tracks_mock ); + TracksTelemetry::init( $env_config_mock ); + TracksTelemetry::track_remote_data_blocks_usage( 1, (object) [] ); } public function testTrackRemoteDataBlocksUsageDoesNotTrackEventIfPostStatusIsNotPublish(): void { @@ -152,9 +152,9 @@ public function testTrackRemoteDataBlocksUsageDoesNotTrackEventIfPostStatusIsNot $tracks_mock = $this->getMockBuilder( MockTracks::class )->onlyMethods( [ 'record_event' ] )->getMock(); $tracks_mock->expects( $this->exactly( 0 ) )->method( 'record_event' ); - set_private_property( TracksAnalytics::class, null, 'instance', $tracks_mock ); - TracksAnalytics::init( $env_config_mock ); - TracksAnalytics::track_remote_data_blocks_usage( 1, (object) [ 'post_status' => 'draft' ] ); + set_private_property( TracksTelemetry::class, null, 'instance', $tracks_mock ); + TracksTelemetry::init( $env_config_mock ); + TracksTelemetry::track_remote_data_blocks_usage( 1, (object) [ 'post_status' => 'draft' ] ); } public function testTrackRemoteDataBlocksUsageDoesNotTrackEventIfPostContentHaveNoRemoteBlocks(): void { @@ -179,9 +179,9 @@ public function testTrackRemoteDataBlocksUsageDoesNotTrackEventIfPostContentHave $tracks_mock = $this->getMockBuilder( MockTracks::class )->onlyMethods( [ 'record_event' ] )->getMock(); $tracks_mock->expects( $this->exactly( 0 ) )->method( 'record_event' ); - set_private_property( TracksAnalytics::class, null, 'instance', $tracks_mock ); - TracksAnalytics::init( $env_config_mock ); - TracksAnalytics::track_remote_data_blocks_usage( 1, (object) [ + set_private_property( TracksTelemetry::class, null, 'instance', $tracks_mock ); + TracksTelemetry::init( $env_config_mock ); + TracksTelemetry::track_remote_data_blocks_usage( 1, (object) [ 'post_type' => 'post', 'post_status' => 'publish', 'post_content' => '

No remote data blocks

', @@ -236,9 +236,9 @@ public function testTrackRemoteDataBlocksUsageDoesTrackEventIfPostContentHaveRem 'generic-http_data_source_count' => 1, ] ); - set_private_property( TracksAnalytics::class, null, 'instance', $tracks_mock ); - TracksAnalytics::init( $env_config_mock ); - TracksAnalytics::track_remote_data_blocks_usage( 1, (object) [ + set_private_property( TracksTelemetry::class, null, 'instance', $tracks_mock ); + TracksTelemetry::init( $env_config_mock ); + TracksTelemetry::track_remote_data_blocks_usage( 1, (object) [ 'post_type' => 'post', 'post_status' => 'publish', 'post_content' => '

Tonal Accessories Shelf (Coffee Oak)

Our floating shelf is the perfect way to store all your Tonal accessories. Its sleek, versatile design makes this an easy fit with any style room. Available in Coffee Oak (seen here), as well as Matte Black and Light Aged Ash. Made in the U.S.

$272.99

T-Locks (Pack of 4)

No detail is too small. Tonal’s proprietary T-Locks let you swap out Tonal accessories with a quick push and twist to lock everything in place.

$42.99

Break

Pearl room

Panel

', @@ -246,8 +246,8 @@ public function testTrackRemoteDataBlocksUsageDoesTrackEventIfPostContentHaveRem } public function testRecordEventDoesNothingIfInstanceIsNotSet(): void { - /** @var TracksAnalytics|MockObject */ - $obj = new TracksAnalytics(); + /** @var TracksTelemetry|MockObject */ + $obj = new TracksTelemetry(); $result = $obj->record_event( 'name', [] ); @@ -257,9 +257,9 @@ public function testRecordEventDoesNothingIfInstanceIsNotSet(): void { public function testRecordEventTracksTheEventIfInstanceIsSet(): void { $mock_tracks = $this->getMockBuilder( MockTracks::class )->onlyMethods( [ 'record_event' ] )->getMock(); $mock_tracks->expects( $this->exactly( 1 ) )->method( 'record_event' )->with( 'event_name', [ 'event_props' ] ); - /** @var TracksAnalytics|MockObject */ - $obj = new TracksAnalytics(); - set_private_property( TracksAnalytics::class, $obj, 'instance', $mock_tracks ); + /** @var TracksTelemetry|MockObject */ + $obj = new TracksTelemetry(); + set_private_property( TracksTelemetry::class, $obj, 'instance', $mock_tracks ); $result = $obj->record_event( 'event_name', [ 'event_props' ] ); @@ -267,16 +267,16 @@ public function testRecordEventTracksTheEventIfInstanceIsSet(): void { } public function testResetMethod(): void { - $obj = new TracksAnalytics(); - set_private_property( TracksAnalytics::class, $obj, 'instance', new MockTracks() ); - TracksAnalytics::init( new EnvironmentConfig() ); + $obj = new TracksTelemetry(); + set_private_property( TracksTelemetry::class, $obj, 'instance', new MockTracks() ); + TracksTelemetry::init( new EnvironmentConfig() ); - $this->assertInstanceOf( MockTracks::class, TracksAnalytics::get_instance() ); - $this->assertInstanceOf( EnvironmentConfig::class, TracksAnalytics::get_env_config() ); + $this->assertInstanceOf( MockTracks::class, TracksTelemetry::get_instance() ); + $this->assertInstanceOf( EnvironmentConfig::class, TracksTelemetry::get_env_config() ); - TracksAnalytics::reset(); + TracksTelemetry::reset(); - $this->assertEquals( null, TracksAnalytics::get_instance() ); - $this->assertEquals( null, TracksAnalytics::get_env_config() ); + $this->assertEquals( null, TracksTelemetry::get_instance() ); + $this->assertEquals( null, TracksTelemetry::get_env_config() ); } }