diff --git a/.env b/.env
index 6fac3df6fedd79da39205f048781dcaf824c817f..6c0664620a171eb177e8ac3cd100ff8cc47f065f 100644
--- a/.env
+++ b/.env
@@ -2,3 +2,7 @@ PROJECT_NAME=config_pr
 PROJECT_BASE_URL=localhost
 PROJECT_PORT=8089
 DEPENDENCIES=bitbucket/client:^4.6 m4tthumphrey/php-gitlab-api:^11 php-http/guzzle7-adapter:^1.0 knplabs/github-api:^3.13
+GITHUB_TOKEN=XXX
+GITLAB_TOKEN=XXX
+REPO_OWNER=marcelovani
+REPO_NAME=config_pr_automation
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 79b7fa9b035baf5120c6e298565ba5abbc2a227c..f21bc643790e170752b0b6bcf3990e56661babb6 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -22,8 +22,6 @@ variables:
   OPT_IN_TEST_CURRENT: 1
   OPT_IN_TEST_NEXT_MINOR: 1
   OPT_IN_TEST_NEXT_MAJOR: 1
-  GITHUB_REPO_URL: https://github.com/marcelovani/config_pr_automation.git
-  GITLAB_REPO_URL: https://gitlab.com/marcelovani/config_pr_automation.git
 phpcs:
   allow_failure: true
 phpstan:
@@ -35,4 +33,4 @@ phpstan (next major):
 phpunit (next minor):
   allow_failure: false
 phpunit (next major):
-  allow_failure: false
+  allow_failure: true
diff --git a/Makefile b/Makefile
index 1f09e3f6239330c14a885d3ca37cb869e4c7e679..ddd3a4c6aa48c563e7f7b11a69224e920dccbfc7 100644
--- a/Makefile
+++ b/Makefile
@@ -57,6 +57,22 @@ reinstall-local:
 
 # Test local build
 test-local:
+	docker exec --env-file=./.env -it drupalci_${PROJECT_NAME} bash -c '\
+		sudo -u www-data \
+		REPO_OWNER=${REPO_OWNER} REPO_NAME=${REPO_NAME} GITHUB_TOKEN=${GITHUB_TOKEN} GITLAB_TOKEN=${GITLAB_TOKEN} \
+		php web/core/scripts/run-tests.sh \
+		--php /usr/local/bin/php \
+		--verbose \
+		--keep-results \
+		--color \
+		--concurrency "32" \
+		--repeat "1" \
+		--types "Simpletest,PHPUnit-Unit,PHPUnit-Kernel,PHPUnit-Functional" \
+		--sqlite sites/default/files/.ht.sqlite \
+		--url http://localhost \
+		--directory "modules/contrib/${PROJECT_NAME}"'
+
+test-local-class:
 	docker exec -it drupalci_${PROJECT_NAME} bash -c '\
 	    sudo -u www-data php web/core/scripts/run-tests.sh \
 	    --php /usr/local/bin/php \
@@ -68,7 +84,7 @@ test-local:
 	    --types "Simpletest,PHPUnit-Unit,PHPUnit-Kernel,PHPUnit-Functional" \
 	    --sqlite sites/default/files/.ht.sqlite \
 	    --url http://localhost \
-	    --directory "modules/contrib/${PROJECT_NAME}"'
+	    --class "Drupal\Tests\config_pr\Functional\ConfigPrTest"'
 
 # Test in non-interactive mode
 test-8:
diff --git a/config/schema/config_pr.schema.yml b/config/schema/config_pr.schema.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8bfff13b9d9f04f92cf5ebccc96af2cc28a9f3d1
--- /dev/null
+++ b/config/schema/config_pr.schema.yml
@@ -0,0 +1,223 @@
+# Schema for the configuration files of the Configuration Pull Request module.
+
+field.field.user.user.field_config_pr_auth_token:
+  type: config_entity
+  label: 'Config PR Auth Token field'
+  mapping:
+    id:
+      type: string
+      label: 'ID'
+    field_name:
+      type: string
+      label: 'Field name'
+    entity_type:
+      type: string
+      label: 'Entity type'
+    bundle:
+      type: string
+      label: 'Bundle'
+    label:
+      type: label
+      label: 'Label'
+    description:
+      type: text
+      label: 'Help text'
+    required:
+      type: boolean
+      label: 'Required field'
+    translatable:
+      type: boolean
+      label: 'Translatable'
+    default_value:
+      type: sequence
+      label: 'Default value'
+      sequence:
+        type: mapping
+        mapping:
+          value:
+            type: string
+            label: 'Value'
+    default_value_callback:
+      type: string
+      label: 'Default value callback'
+    settings:
+      type: field.field_settings.[%parent.field_type]
+    field_type:
+      type: string
+      label: 'Field type'
+
+field.storage.user.field_config_pr_auth_token:
+  type: config_entity
+  label: 'Config PR Auth Token field storage'
+  mapping:
+    id:
+      type: string
+      label: 'ID'
+    field_name:
+      type: string
+      label: 'Field name'
+    entity_type:
+      type: string
+      label: 'Entity type'
+    type:
+      type: string
+      label: 'Type'
+    settings:
+      type: mapping
+      label: 'Settings'
+      mapping:
+        max_length:
+          type: integer
+          label: 'Maximum length'
+        case_sensitive:
+          type: boolean
+          label: 'Case sensitive'
+        is_ascii:
+          type: boolean
+          label: 'Contains US ASCII characters only'
+    module:
+      type: string
+      label: 'Module'
+    locked:
+      type: boolean
+      label: 'Locked'
+    cardinality:
+      type: integer
+      label: 'Maximum number of values users can enter'
+    translatable:
+      type: boolean
+      label: 'Translatable'
+    indexes:
+      type: sequence
+      label: 'Indexes'
+      sequence:
+        type: sequence
+        label: 'Indexes'
+        sequence:
+          type: string
+          label: 'Index'
+    persist_with_no_fields:
+      type: boolean
+      label: 'Persist field storage with no fields'
+    custom_storage:
+      type: boolean
+      label: 'Enable custom storage'
+
+core.entity_form_display.user.user.default:
+  type: config_entity
+  label: 'User form display'
+  mapping:
+    id:
+      type: string
+      label: 'ID'
+    targetEntityType:
+      type: string
+      label: 'Target entity type'
+    bundle:
+      type: string
+      label: 'Bundle'
+    mode:
+      type: string
+      label: 'Mode'
+    content:
+      type: sequence
+      label: 'Content'
+      sequence:
+        type: mapping
+        label: 'Field settings'
+        mapping:
+          type:
+            type: string
+            label: 'Type'
+          weight:
+            type: integer
+            label: 'Weight'
+          region:
+            type: string
+            label: 'Region'
+          settings:
+            type: field.widget.settings.[%parent.type]
+            label: 'Settings'
+          third_party_settings:
+            type: sequence
+            label: 'Third party settings'
+            sequence:
+              type: mapping
+              label: 'Settings'
+          label:
+            type: string
+            label: 'Label'
+    hidden:
+      type: sequence
+      label: 'Hidden'
+      sequence:
+        type: boolean
+        label: 'Component hidden'
+
+field.widget.settings.string_textfield:
+  type: mapping
+  label: 'Text field settings'
+  mapping:
+    size:
+      type: integer
+      label: 'Size'
+    placeholder:
+      type: string
+      label: 'Placeholder'
+
+field.widget.settings.image_image:
+  type: mapping
+  label: 'Image field settings'
+  mapping:
+    progress_indicator:
+      type: string
+      label: 'Progress indicator'
+    preview_image_style:
+      type: string
+      label: 'Preview image style'
+
+config_pr.settings:
+  type: config_object
+  label: 'Configuration Pull Request settings'
+  mapping:
+    repo:
+      type: mapping
+      label: 'Repository settings'
+      mapping:
+        controller:
+          type: string
+          label: 'Repository controller'
+        repo_url:
+          type: string
+          label: 'Repository URL'
+        repo_owner:
+          type: string
+          label: 'Repository owner'
+        repo_name:
+          type: string
+          label: 'Repository name'
+        branch:
+          type: string
+          label: 'Branch name'
+        pr_title:
+          type: string
+          label: 'Pull request title'
+        pr_body:
+          type: string
+          label: 'Pull request body'
+    commit_messages:
+      type: mapping
+      label: 'Commit messages'
+      mapping:
+        create:
+          type: string
+          label: 'Create message'
+        update:
+          type: string
+          label: 'Update message'
+        rename:
+          type: string
+          label: 'Rename message'
+        delete:
+          type: string
+          label: 'Delete message'
\ No newline at end of file
diff --git a/config_pr.info.yml b/config_pr.info.yml
index c4822265e9ddd0c22f2d5a8c70b9a5e4944358d1..9ec47c0718a0deb57cf6a540a63cd458f16cf364 100644
--- a/config_pr.info.yml
+++ b/config_pr.info.yml
@@ -6,3 +6,4 @@ core_version_requirement: ">=8"
 configure: config_pr.settings
 dependencies:
   - drupal:config
+  - drupal:field
diff --git a/config_pr.services.yml b/config_pr.services.yml
index 49640ae10cdc11520a364ce00ddb6d95c481fd62..61c8e777f022414b9d095cd64202e5dfb72b8653 100644
--- a/config_pr.services.yml
+++ b/config_pr.services.yml
@@ -1,6 +1,10 @@
 services:
   config_pr.repo_controller_manager:
     class: Drupal\config_pr\RepoControllerManager
-    arguments: ['@messenger']
+    arguments: ['@messenger', '@config.factory']
     tags:
       - { name: service_collector, tag: config_pr.repo_controller, call: addController }
+
+  config_pr.active_repo_controller:
+    class: Drupal\config_pr\RepoControllerInterface
+    factory: ['@config_pr.repo_controller_manager', 'getActiveController']
diff --git a/src/Form/ConfigPrForm.php b/src/Form/ConfigPrForm.php
index 0cc27608914ce60b68c15ad5b343c2087efd776b..22668d4dd5e3f3732274ed8f983a10f59b33c609 100644
--- a/src/Form/ConfigPrForm.php
+++ b/src/Form/ConfigPrForm.php
@@ -11,7 +11,6 @@ use Drupal\Core\Config\StorageInterface;
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Link;
-use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Site\Settings;
 use Drupal\Core\Url;
 use Drupal\user\Entity\User;
@@ -26,8 +25,7 @@ class ConfigPrForm extends FormBase {
   /**
    * The repo controller.
    *
-   * @var \Drupal\config_pr\RepoControllerInterface
-   *   The Repo controller interface.
+   * @var \Drupal\config_pr\RepoControllerInterface|null
    */
   protected $repoController;
 
@@ -70,18 +68,15 @@ class ConfigPrForm extends FormBase {
    *   Configuration manager.
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The factory for configuration objects.
-   * @param \Drupal\config_pr\RepoControllerInterface $repoController
-   *   The repo controller.
-   * @param \Drupal\Core\Session\AccountInterface $current_user
-   *   The current user.
+   * @param \Drupal\config_pr\RepoControllerInterface|null $repoController
+   *   The repo controller, or NULL if not configured.
    */
   public function __construct(
     StorageInterface $sync_storage,
     StorageInterface $active_storage,
     ConfigManagerInterface $config_manager,
     ConfigFactoryInterface $config_factory,
-    RepoControllerInterface $repoController,
-    AccountInterface $current_user,
+    ?RepoControllerInterface $repoController = NULL,
   ) {
     $this->syncStorage = $sync_storage;
     $this->activeStorage = $active_storage;
@@ -94,17 +89,12 @@ class ConfigPrForm extends FormBase {
    * {@inheritdoc}
    */
   public static function create(ContainerInterface $container) {
-    $repoController = $container->get('config.factory')
-      ->get('config_pr.settings')
-      ->get('repo.controller') ?? 'config_pr.repo_controller.github';
-
     return new static(
       $container->get('config.storage.sync'),
       $container->get('config.storage'),
       $container->get('config.manager'),
       $container->get('config.factory'),
-      $container->get($repoController),
-      $container->get('current_user')
+      $container->get('config_pr.active_repo_controller'),
     );
   }
 
@@ -143,6 +133,14 @@ class ConfigPrForm extends FormBase {
    * {@inheritdoc}
    */
   public function buildForm(array $form, FormStateInterface $form_state) {
+    if (!$this->repoController) {
+      return [
+        '#markup' => $this->t('Repository configuration is missing. Please visit the @settings_link first.', [
+          '@settings_link' => Link::createFromRoute('configuration page', 'config_pr.settings')->toString(),
+        ]),
+      ];
+    }
+
     $repo_owner = $this->config('config_pr.settings')->get('repo.repo_owner');
     $repo_name = $this->config('config_pr.settings')->get('repo.repo_name');
     if (empty($repo_owner) || empty($repo_name)) {
diff --git a/src/Form/ConfigPrSettingsForm.php b/src/Form/ConfigPrSettingsForm.php
index 6e56a3cc5564ff222258a66956b74ac001033a37..3494d4ed968e3b083c10123cc23ad31f9399db1f 100644
--- a/src/Form/ConfigPrSettingsForm.php
+++ b/src/Form/ConfigPrSettingsForm.php
@@ -7,6 +7,7 @@ use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Config\TypedConfigManagerInterface;
 use Drupal\Core\Form\ConfigFormBase;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Link;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -78,6 +79,15 @@ class ConfigPrSettingsForm extends ConfigFormBase {
    *   The form.
    */
   public function buildForm(array $form, FormStateInterface $form_state) {
+    $repo_controllers = $this->repoController->getControllers();
+    if (!$repo_controllers) {
+      return [
+        '#markup' => $this->t('No repo controllers available, please enable one of the sub-modules in the @settings_link.', [
+          '@settings_link' => Link::createFromRoute('Modules page', 'system.modules_list')->toString(),
+        ]),
+      ];
+    }
+
     $form['repo'] = [
       '#title' => $this->t('Repository'),
       '#type' => 'fieldset',
@@ -86,14 +96,10 @@ class ConfigPrSettingsForm extends ConfigFormBase {
       '#type' => 'select',
       '#title' => $this->t('Repo provider'),
       '#description' => $this->t('Select controller.'),
-      '#options' => $this->repoController->getControllers(),
+      '#options' => $repo_controllers,
       '#default_value' => $this->config('config_pr.settings')->get('repo.controller') ?? 'config_pr.repo_controller.github',
       '#required' => TRUE,
     ];
-
-    // Try to get the information from the local repo. This only works with Git.
-    $repo_info = $this->repoController->getLocalRepoInfo();
-
     $form['repo']['repo_url'] = [
       '#type' => 'textfield',
       '#title' => $this->t('Repository URL (Optional)'),
@@ -101,6 +107,10 @@ class ConfigPrSettingsForm extends ConfigFormBase {
       '#default_value' => $this->config('config_pr.settings')->get('repo.repo_url') ?? '',
       '#required' => FALSE,
     ];
+
+    // Try to get the information from the local repo. This only works with Git.
+    $repo_info = $this->repoController->getLocalRepoInfo() ?? ['repo_owner' => '', 'repo_name' => ''];
+
     $form['repo']['repo_owner'] = [
       '#type' => 'textfield',
       '#title' => $this->t('Repo owner name'),
diff --git a/src/RepoControllerFactory.php b/src/RepoControllerFactory.php
new file mode 100644
index 0000000000000000000000000000000000000000..44f826398476bc8941d41ddfcf840cd17fc0569b
--- /dev/null
+++ b/src/RepoControllerFactory.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Drupal\config_pr;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Factory class for repository controllers.
+ */
+class RepoControllerFactory {
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The service container.
+   *
+   * @var \Symfony\Component\DependencyInjection\ContainerInterface
+   */
+  protected $container;
+
+  /**
+   * Constructs a new RepoControllerFactory.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory.
+   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
+   *   The service container.
+   */
+  public function __construct(
+    ConfigFactoryInterface $config_factory,
+    ContainerInterface $container
+  ) {
+    $this->configFactory = $config_factory;
+    $this->container = $container;
+  }
+
+  /**
+   * Gets the appropriate repository controller.
+   *
+   * @return \Drupal\config_pr\RepoControllerInterface
+   *   The repository controller.
+   */
+  public function getController(): RepoControllerInterface {
+    $controller_id = $this->configFactory->get('config_pr.settings')->get('repo.controller');
+    
+    // Default to dummy controller if no configuration exists.
+    if (empty($controller_id)) {
+      $controller_id = 'config_pr.dummy_repo_controller';
+    }
+
+    return $this->container->get($controller_id);
+  }
+
+}
diff --git a/src/RepoControllerManager.php b/src/RepoControllerManager.php
index 997933d31de103c6a357220380c2734c91723a16..1b5155733c10754b489666b23eb7098eda43a3e3 100644
--- a/src/RepoControllerManager.php
+++ b/src/RepoControllerManager.php
@@ -2,6 +2,9 @@
 
 namespace Drupal\config_pr;
 
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Messenger\MessengerInterface;
+
 /**
  * Class RepoControllerManager.
  *
@@ -17,26 +20,69 @@ class RepoControllerManager implements RepoControllerManagerInterface {
    *
    * @var array
    */
-  protected $controllers = [];
+  protected array $controllers = [];
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The messenger.
+   *
+   * @var \Drupal\Core\Messenger\MessengerInterface
+   */
+  protected $messenger;
+
+  /**
+   * Constructs a new RepoControllerManager.
+   *
+   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
+   *   The messenger.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory.
+   */
+  public function __construct(
+    MessengerInterface $messenger,
+    ConfigFactoryInterface $config_factory
+  ) {
+    $this->messenger = $messenger;
+    $this->configFactory = $config_factory;
+  }
 
   /**
    * {@inheritdoc}
    */
   public function addController(RepoControllerInterface $controller) {
-    $this->controllers[] = $controller;
+    $id = $controller->getControllerId();
+    $this->controllers[$id] = $controller;
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getControllers() {
+  public function getControllers(): array {
+    $controllers = [];
     foreach ($this->controllers as $controller) {
-      $controllers[$controller->getControllerId()] = $controller->getControllerName();
+      $id = $controller->getControllerId();
+      $controllers[$id] = $controller->getControllerName();
     }
-
     return $controllers;
   }
 
+  /**
+   * Gets the active controller if configured.
+   *
+   * @return \Drupal\config_pr\RepoControllerInterface|null
+   *   The active controller, or NULL if not configured.
+   */
+  public function getActiveController(): ?RepoControllerInterface {
+    $id = $this->configFactory->get('config_pr.settings')->get('repo.controller');
+    return $id ? ($this->controllers[$id] ?? NULL) : NULL;
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/src/RepoControllers/DummyController.php b/src/RepoControllers/DummyController.php
new file mode 100644
index 0000000000000000000000000000000000000000..bc4035acf942be084da6bc1eaf0f427f09926cce
--- /dev/null
+++ b/src/RepoControllers/DummyController.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace Drupal\config_pr\RepoControllers;
+
+use Drupal\config_pr\RepoControllerInterface;
+use Drupal\config_pr\RepoControllerTrait;
+use Drupal\Core\Messenger\MessengerTrait;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Class to define a dummy controller.
+ * 
+ * @see \Drupal\config_pr\RepoControllerInterface
+ */
+class DummyController implements RepoControllerInterface {
+  
+  use MessengerTrait;
+  use RepoControllerTrait;
+  use StringTranslationTrait;
+
+  /**
+   * Holds the controller name.
+   *
+   * @var string
+   *   The controller name.
+   */
+  protected $controllerName = 'Dummy';
+
+  /**
+   * Holds the controller Id.
+   *
+   * @var string
+   *   The controller id.
+   */
+  protected $controllerId = 'config_pr.dummy_repo_controller';
+
+    /**
+   * {@inheritdoc}
+   */
+  public function authenticate(): bool {}
+
+  /**
+   * @inheritDoc
+   */
+  public function getProjectDetails(): bool {}
+
+  /**
+   * @inheritDoc
+   */
+  public function getBranches(): array {}
+
+  /**
+   * @inheritDoc
+   */
+  public function createBranch($branch_name): bool {}
+ 
+    /**
+   * {@inheritdoc}
+   */
+  public function getOpenPrs(): array {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createPr($base, $branch, $title, $body): array|bool {}
+
+  /**
+   * @inheritDoc
+   */
+  public function getFileSha($path): string {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createFile($path, $content, $commitMessage, $branchName): array|bool {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fileExists($path): bool {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateFile($path, $content, $commitMessage, $branchName): array|bool {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteFile($path, $commitMessage, $branchName): bool {}
+
+}
diff --git a/tests/src/Functional/ConfigPrTest.php b/tests/src/Functional/ConfigPrTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..11c516853d67bdfa255fe45843fc1b0daa4038b1
--- /dev/null
+++ b/tests/src/Functional/ConfigPrTest.php
@@ -0,0 +1,161 @@
+<?php
+
+namespace Drupal\Tests\config_pr\Functional;
+
+use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\config_pr\Functional\Utils;
+
+/**
+ * Tests the Configuration Pull Request module functionality.
+ *
+ * @group config_pr
+ */
+class ConfigPrTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'config_pr',
+    'config_pr_github',
+    'config_pr_gitlab',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $profile = 'standard';
+
+  /**
+   * A test user with administrative privileges.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $adminUser;
+
+  /**
+   * Stores the repo owner.
+   * 
+   * @var string
+   */
+  protected $repo_owner;
+
+  /**
+   * Stores the repo user.
+   * 
+   * @var string
+   */
+  protected $repo_name;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->repo_owner = getenv('REPO_OWNER');
+    $this->repo_name = getenv('REPO_NAME');
+
+    // Create and log in our privileged user.
+    $this->adminUser = $this->drupalCreateUser([
+      'administer configuration pull request',
+      'issue configuration pull requests',
+      'synchronize configuration',
+    ]);
+    $this->drupalLogin($this->adminUser);
+
+    // Export initial config.
+    $configs_to_export = [
+      'system.site',
+      'system.theme',
+      // Add other configurations you want to export
+    ];
+    foreach ($configs_to_export as $config_name) {
+      Utils::exportConfig($config_name);
+    }
+
+    // Get the config factory service.
+    $config_factory = \Drupal::service('config.factory');
+    // Load the 'system.site' configuration.
+    $config = $config_factory->getEditable('system.site');
+    // Update the site name to 'foo'.
+    $config->set('name', 'Test Site');
+    // Save the configuration.
+    $config->save();
+  }
+
+  public function testConfigPrGithub(): void {
+    $this->runTests('github');
+  }
+
+  public function testConfigPrGitlab(): void {
+    $this->runTests('gitlab');
+  }
+
+  /**
+   * Tests the config pull request workflow.
+   */
+  private function runTests(string $type): void {
+    $repo_url = 'https://' . $type . '.com/' . $this->repo_owner . '/' . $this->repo_name . '.git';
+
+    // Configure access token in user profile.
+    $this->drupalGet('user/' . $this->adminUser->id() . '/edit');
+    $this->assertSession()->statusCodeEquals(200);
+    
+    $edit = [
+      'field_config_pr_auth_token[0][value]' => getenv(strtoupper($type) . '_TOKEN'),
+    ];
+    $this->submitForm($edit, 'Save');
+    $this->assertSession()->pageTextContains('The changes have been saved');
+
+    // Configure the repository settings.
+    $this->drupalGet('admin/config/development/configuration/pull_request');
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertSession()->pageTextContains('Repository configuration is missing');
+    $this->clickLink('configuration page');
+
+    $edit = [
+      'repo_controller' => 'config_pr_' . $type . '.repo_controller.' . $type,
+      'repo_url' => $repo_url,
+      'repo_owner' => $this->repo_owner,
+      'repo_name' => $this->repo_name,
+      'message_create' => 'New config @config_name.yml',
+      'message_delete' => 'Del config @config_name.yml',
+    ];
+    $this->submitForm($edit, 'Save configuration');
+    $this->assertSession()->pageTextContains('The configuration options have been saved');
+
+    // Verify the configuration is saved correctly.
+    $this->assertSession()->fieldValueEquals('repo_controller', 'config_pr_' . $type . '.repo_controller.' . $type);
+    $this->assertSession()->fieldValueEquals('repo_url', $repo_url);
+    $this->assertSession()->fieldValueEquals('repo_owner', $this->repo_owner);
+    $this->assertSession()->fieldValueEquals('repo_name', $this->repo_name);
+    $this->assertSession()->fieldValueEquals('message_create', 'New config @config_name.yml');
+    $this->assertSession()->fieldValueEquals('message_delete', 'Del config @config_name.yml');
+
+    // Navigate to Config pull request form.
+    $this->clickLink('Pull Requests page');
+
+    $this->assertSession()->pageTextContains('1 changed');
+    $this->assertSession()->linkExists('View differences');
+    $this->assertSession()->optionExists('source_branch', 'master');
+
+    // Select config.
+    $page = $this->getSession()->getPage();
+    $page->find('css', '[name="select-system_site"]')->check();
+    $this->assertSession()->checkboxChecked('select-system_site');
+
+    // Create pull request.
+    $edit = [
+      'pr_title' => 'Test PR',
+      'pr_description' => 'Automated test PR',
+    ];
+    $this->submitForm($edit, 'Create Pull Request');
+  }
+
+}
diff --git a/tests/src/Functional/Utils.php b/tests/src/Functional/Utils.php
new file mode 100644
index 0000000000000000000000000000000000000000..0a8b516eb5c352b65edffe6c759e2331a1d816ac
--- /dev/null
+++ b/tests/src/Functional/Utils.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Drupal\Tests\config_pr\Functional;
+
+use Drupal\Core\Config\FileStorage;
+use Drupal\Core\Site\Settings;
+
+/**
+ * Helper class for Config PR tests.
+ */
+class Utils {
+
+  /**
+   * Exports configuration to a specific directory.
+   *
+   * @param string $config_name
+   *   The configuration name to export (e.g., 'system.site').
+   */
+  public static function exportConfig($config_name) {
+    // Set up the sync directory
+    $config_sync_directory = Settings::get('config_sync_directory', NULL);
+
+    // Ensure the directory exists
+    if (!file_exists($config_sync_directory)) {
+      mkdir($config_sync_directory, 0777, TRUE);
+    }
+
+    // Get the configuration from active storage
+    $config = \Drupal::config($config_name);
+    if (!$config) {
+      throw new \Exception("Configuration '$config_name' not found.");
+    }
+
+    // Create a file storage for the target directory
+    $storage = new FileStorage($config_sync_directory);
+
+    // Write the configuration to file
+    $storage->write($config_name, $config->get());
+  }
+
+}