This is an automated email from the ASF dual-hosted git repository.

markap14 pushed a commit to branch NIFI-15258
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/NIFI-15258 by this push:
     new 556ec1ea46 NIFI-15581: Add support for ControllerService Mocks for 
Connectors. (#10885)
556ec1ea46 is described below

commit 556ec1ea46e905cbfc3126e753b74be68786328f
Author: Bob Paulin <[email protected]>
AuthorDate: Thu Feb 12 11:07:31 2026 -0600

    NIFI-15581: Add support for ControllerService Mocks for Connectors. (#10885)
---
 .../mock/connector/server/ConnectorMockServer.java |   3 +
 .../server/MockConnectorInitializationContext.java |   7 +
 .../server/MockExtensionDiscoveringManager.java    |  14 +
 .../mock/connector/server/MockExtensionMapper.java |  17 +
 .../server/StandardConnectorMockServer.java        |   7 +
 .../nifi-connector-mock-integration-tests/pom.xml  |  12 +
 .../mock/connectors/tests/CreateConnectorIT.java   |  22 +-
 .../connectors/tests/MockControllerServiceIT.java  |  87 +++++
 .../main/resources/flows/Generate_and_Update.json  | 394 ++++++++++++++++++++-
 .../connector/StandardConnectorTestRunner.java     |  16 +-
 10 files changed, 565 insertions(+), 14 deletions(-)

diff --git 
a/nifi-connector-mock-bundle/nifi-connector-mock-api/src/main/java/org/apache/nifi/mock/connector/server/ConnectorMockServer.java
 
b/nifi-connector-mock-bundle/nifi-connector-mock-api/src/main/java/org/apache/nifi/mock/connector/server/ConnectorMockServer.java
index 7b24c973c9..ade061a9d4 100644
--- 
a/nifi-connector-mock-bundle/nifi-connector-mock-api/src/main/java/org/apache/nifi/mock/connector/server/ConnectorMockServer.java
+++ 
b/nifi-connector-mock-bundle/nifi-connector-mock-api/src/main/java/org/apache/nifi/mock/connector/server/ConnectorMockServer.java
@@ -18,6 +18,7 @@
 package org.apache.nifi.mock.connector.server;
 
 import org.apache.nifi.NiFiServer;
+import org.apache.nifi.controller.ControllerService;
 import org.apache.nifi.processor.Processor;
 
 import java.io.File;
@@ -30,4 +31,6 @@ public interface ConnectorMockServer extends NiFiServer, 
ConnectorTestRunner {
 
     void mockProcessor(String processorType, Class<? extends Processor> 
mockProcessorClass);
 
+    void mockControllerService(String controllerServiceType, Class<? extends 
ControllerService> mockControllerServiceClass);
+
 }
diff --git 
a/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/MockConnectorInitializationContext.java
 
b/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/MockConnectorInitializationContext.java
index 6390fad284..02d276bc28 100644
--- 
a/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/MockConnectorInitializationContext.java
+++ 
b/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/MockConnectorInitializationContext.java
@@ -21,6 +21,7 @@ import 
org.apache.nifi.components.connector.BundleCompatibility;
 import org.apache.nifi.components.connector.FlowUpdateException;
 import 
org.apache.nifi.components.connector.StandardConnectorInitializationContext;
 import org.apache.nifi.components.connector.components.FlowContext;
+import org.apache.nifi.flow.VersionedControllerService;
 import org.apache.nifi.flow.VersionedExternalFlow;
 import org.apache.nifi.flow.VersionedProcessGroup;
 import org.apache.nifi.flow.VersionedProcessor;
@@ -48,6 +49,12 @@ public class MockConnectorInitializationContext extends 
StandardConnectorInitial
             }
         }
 
+        if (group.getControllerServices() != null) {
+            for (final VersionedControllerService controllerService : 
group.getControllerServices()) {
+                mockExtensionMapper.mapControllerService(controllerService);
+            }
+        }
+
         if (group.getProcessGroups() != null) {
             for (final VersionedProcessGroup childGroup : 
group.getProcessGroups()) {
                 replaceMocks(childGroup);
diff --git 
a/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/MockExtensionDiscoveringManager.java
 
b/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/MockExtensionDiscoveringManager.java
index 7af2240d2d..22b2ca043c 100644
--- 
a/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/MockExtensionDiscoveringManager.java
+++ 
b/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/MockExtensionDiscoveringManager.java
@@ -20,6 +20,7 @@ package org.apache.nifi.mock.connector.server;
 import org.apache.nifi.bundle.Bundle;
 import org.apache.nifi.bundle.BundleCoordinate;
 import org.apache.nifi.bundle.BundleDetails;
+import org.apache.nifi.controller.ControllerService;
 import org.apache.nifi.nar.InstanceClassLoader;
 import org.apache.nifi.nar.StandardExtensionDiscoveringManager;
 import org.apache.nifi.processor.Processor;
@@ -46,6 +47,19 @@ public class MockExtensionDiscoveringManager extends 
StandardExtensionDiscoverin
         registerExtensionClass(Processor.class, mockProcessorClass.getName(), 
mockBundle);
     }
 
+    public synchronized void addControllerService(final Class<? extends 
ControllerService> mockControllerServiceClass) {
+        final BundleDetails bundleDetails = new BundleDetails.Builder()
+            .workingDir(new File("target/work/extensions/mock-bundle"))
+            .coordinate(new BundleCoordinate("org.apache.nifi.mock", 
mockControllerServiceClass.getName(), "1.0.0"))
+            .build();
+
+        final Bundle mockBundle = new Bundle(bundleDetails, 
mockControllerServiceClass.getClassLoader());
+        discoverExtensions(Set.of(mockBundle));
+
+        mockComponentClassLoaders.put(mockControllerServiceClass.getName(), 
mockControllerServiceClass.getClassLoader());
+        registerExtensionClass(ControllerService.class, 
mockControllerServiceClass.getName(), mockBundle);
+    }
+
     @Override
     public InstanceClassLoader createInstanceClassLoader(final String 
classType, final String instanceIdentifier, final Bundle bundle, final Set<URL> 
additionalUrls,
             final boolean register, final String classloaderIsolationKey) {
diff --git 
a/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/MockExtensionMapper.java
 
b/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/MockExtensionMapper.java
index ed532cc253..edf81cdeaa 100644
--- 
a/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/MockExtensionMapper.java
+++ 
b/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/MockExtensionMapper.java
@@ -18,6 +18,7 @@
 package org.apache.nifi.mock.connector.server;
 
 import org.apache.nifi.flow.Bundle;
+import org.apache.nifi.flow.VersionedControllerService;
 import org.apache.nifi.flow.VersionedProcessor;
 
 import java.util.HashMap;
@@ -26,11 +27,16 @@ import java.util.Map;
 public class MockExtensionMapper {
 
     private final Map<String, String> processorMocks = new HashMap<>();
+    private final Map<String, String> controllerServiceMocks = new HashMap<>();
 
     public void mockProcessor(final String processorType, final String 
mockProcessorClassName) {
         processorMocks.put(processorType, mockProcessorClassName);
     }
 
+    public void mockControllerService(final String controllerServiceType, 
final String mockControllerServiceClassName) {
+        controllerServiceMocks.put(controllerServiceType, 
mockControllerServiceClassName);
+    }
+
     public void mapProcessor(final VersionedProcessor processor) {
         final String type = processor.getType();
         final String implementationClassName = processorMocks.get(type);
@@ -41,4 +47,15 @@ public class MockExtensionMapper {
         processor.setType(implementationClassName);
         processor.setBundle(new Bundle("org.apache.nifi.mock", 
implementationClassName, "1.0.0"));
     }
+
+    public void mapControllerService(final VersionedControllerService 
controllerService) {
+        final String type = controllerService.getType();
+        final String implementationClassName = 
controllerServiceMocks.get(type);
+        if (implementationClassName == null) {
+            return;
+        }
+
+        controllerService.setType(implementationClassName);
+        controllerService.setBundle(new Bundle("org.apache.nifi.mock", 
implementationClassName, "1.0.0"));
+    }
 }
diff --git 
a/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/StandardConnectorMockServer.java
 
b/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/StandardConnectorMockServer.java
index e788b446fa..2ff196f435 100644
--- 
a/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/StandardConnectorMockServer.java
+++ 
b/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/StandardConnectorMockServer.java
@@ -57,6 +57,7 @@ import org.apache.nifi.encrypt.PropertyEncryptor;
 import org.apache.nifi.engine.FlowEngine;
 import org.apache.nifi.events.VolatileBulletinRepository;
 import 
org.apache.nifi.mock.connector.server.secrets.ConnectorTestRunnerSecretsManager;
+import org.apache.nifi.controller.ControllerService;
 import org.apache.nifi.nar.ExtensionMapping;
 import org.apache.nifi.processor.Processor;
 import org.apache.nifi.reporting.BulletinRepository;
@@ -394,6 +395,12 @@ public class StandardConnectorMockServer implements 
ConnectorMockServer {
         extensionManager.addProcessor(mockProcessorClass);
     }
 
+    @Override
+    public void mockControllerService(final String controllerServiceType, 
final Class<? extends ControllerService> mockControllerServiceClass) {
+        mockExtensionMapper.mockControllerService(controllerServiceType, 
mockControllerServiceClass.getName());
+        extensionManager.addControllerService(mockControllerServiceClass);
+    }
+
     @Override
     public void close() {
         stop();
diff --git 
a/nifi-connector-mock-bundle/nifi-connector-mock-test-bundle/nifi-connector-mock-integration-tests/pom.xml
 
b/nifi-connector-mock-bundle/nifi-connector-mock-test-bundle/nifi-connector-mock-integration-tests/pom.xml
index 7b092337a4..a46a06baaa 100644
--- 
a/nifi-connector-mock-bundle/nifi-connector-mock-test-bundle/nifi-connector-mock-integration-tests/pom.xml
+++ 
b/nifi-connector-mock-bundle/nifi-connector-mock-test-bundle/nifi-connector-mock-integration-tests/pom.xml
@@ -43,6 +43,12 @@
             <artifactId>nifi-connector-mock-api</artifactId>
             <version>2.8.0-SNAPSHOT</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-lookup-service-api</artifactId>
+            <version>2.8.0-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
 
         <!-- Test Dependencies -->
         <dependency>
@@ -90,6 +96,12 @@
             <version>2.8.0-SNAPSHOT</version>
             <type>nar</type>
         </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-lookup-services-nar</artifactId>
+            <version>2.8.0-SNAPSHOT</version>
+            <type>nar</type>
+        </dependency>
     </dependencies>
 
     <build>
diff --git 
a/nifi-connector-mock-bundle/nifi-connector-mock-test-bundle/nifi-connector-mock-integration-tests/src/test/java/org/apache/nifi/mock/connectors/tests/CreateConnectorIT.java
 
b/nifi-connector-mock-bundle/nifi-connector-mock-test-bundle/nifi-connector-mock-integration-tests/src/test/java/org/apache/nifi/mock/connectors/tests/CreateConnectorIT.java
index 950b945003..61e6aeef60 100644
--- 
a/nifi-connector-mock-bundle/nifi-connector-mock-test-bundle/nifi-connector-mock-integration-tests/src/test/java/org/apache/nifi/mock/connectors/tests/CreateConnectorIT.java
+++ 
b/nifi-connector-mock-bundle/nifi-connector-mock-test-bundle/nifi-connector-mock-integration-tests/src/test/java/org/apache/nifi/mock/connectors/tests/CreateConnectorIT.java
@@ -17,14 +17,16 @@
 
 package org.apache.nifi.mock.connectors.tests;
 
+import org.apache.nifi.components.ValidationResult;
 import org.apache.nifi.mock.connector.StandardConnectorTestRunner;
 import org.apache.nifi.mock.connector.server.ConnectorTestRunner;
 import org.junit.jupiter.api.Test;
 
 import java.io.File;
 import java.io.IOException;
+import java.util.List;
 
-import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 public class CreateConnectorIT {
@@ -42,16 +44,18 @@ public class CreateConnectorIT {
     }
 
     @Test
-    public void testConnectorWithMissingBundleThrowsException() {
-        final IllegalStateException exception = 
assertThrows(IllegalStateException.class, () -> {
-            new StandardConnectorTestRunner.Builder()
+    public void testConnectorWithMissingBundleFailsValidate() throws 
IOException {
+        
+        try (final ConnectorTestRunner testRunner = new 
StandardConnectorTestRunner.Builder()
                 
.connectorClassName("org.apache.nifi.mock.connectors.MissingBundleConnector")
                 .narLibraryDirectory(new File("target/libDir"))
-                .build();
-        });
+                .build()) {
 
-        final String message = exception.getMessage();
-        
assertTrue(message.contains("com.example.nonexistent:missing-nar:1.0.0"), 
"Expected exception message to contain missing bundle coordinates but was: " + 
message);
-        
assertTrue(message.contains("com.example.nonexistent.MissingProcessor"), 
"Expected exception message to contain missing processor type but was: " + 
message);
+            final List<ValidationResult> results = testRunner.validate();
+            assertEquals(results.size(), 1);
+            final String message = results.getFirst().getExplanation();
+            
assertTrue(message.contains("com.example.nonexistent:missing-nar:1.0.0"), 
"Expected exception message to contain missing bundle coordinates but was: " + 
message);
+            
assertTrue(message.contains("com.example.nonexistent.MissingProcessor"), 
"Expected exception message to contain missing processor type but was: " + 
message);
+        }
     }
 }
diff --git 
a/nifi-connector-mock-bundle/nifi-connector-mock-test-bundle/nifi-connector-mock-integration-tests/src/test/java/org/apache/nifi/mock/connectors/tests/MockControllerServiceIT.java
 
b/nifi-connector-mock-bundle/nifi-connector-mock-test-bundle/nifi-connector-mock-integration-tests/src/test/java/org/apache/nifi/mock/connectors/tests/MockControllerServiceIT.java
new file mode 100644
index 0000000000..1f044ccf21
--- /dev/null
+++ 
b/nifi-connector-mock-bundle/nifi-connector-mock-test-bundle/nifi-connector-mock-integration-tests/src/test/java/org/apache/nifi/mock/connectors/tests/MockControllerServiceIT.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.mock.connectors.tests;
+
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.Validator;
+import org.apache.nifi.controller.AbstractControllerService;
+import org.apache.nifi.lookup.LookupFailureException;
+import org.apache.nifi.lookup.StringLookupService;
+import org.apache.nifi.mock.connector.StandardConnectorTestRunner;
+import org.apache.nifi.mock.connector.server.ConnectorTestRunner;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class MockControllerServiceIT {
+
+    @Test
+    @Timeout(10)
+    public void testMockControllerService() throws IOException {
+        try (final ConnectorTestRunner runner = new 
StandardConnectorTestRunner.Builder()
+                .narLibraryDirectory(new File("target/libDir"))
+                
.connectorClassName("org.apache.nifi.mock.connectors.GenerateAndLog")
+                
.mockControllerService("org.apache.nifi.lookup.SimpleKeyValueLookupService", 
MockStringLookupService.class)
+                .build()) {
+
+            runner.startConnector();
+
+            // Wait until MockStringLookupService.lookup is invoked at least 
once.
+            // We use @Timeout on the test to avoid hanging indefinitely in 
case of failure.
+            while (MockStringLookupService.lookupCounter.get() < 1) {
+                Thread.yield();
+            }
+
+            assertTrue(MockStringLookupService.lookupCounter.get() >= 1);
+
+            runner.stopConnector();
+        }
+    }
+
+    public static class MockStringLookupService extends 
AbstractControllerService implements StringLookupService {
+        private static final AtomicLong lookupCounter = new AtomicLong(0L);
+
+        @Override
+        protected PropertyDescriptor 
getSupportedDynamicPropertyDescriptor(final String propertyDescriptorName) {
+            return new PropertyDescriptor.Builder()
+                    .name(propertyDescriptorName)
+                    .addValidator(Validator.VALID)
+                    .dynamic(true)
+                    .build();
+        }
+
+        @Override
+        public Optional<String> lookup(final Map<String, Object> coordinates) 
throws LookupFailureException {
+            lookupCounter.incrementAndGet();
+            return Optional.of("mock-value");
+        }
+
+        @Override
+        public Set<String> getRequiredKeys() {
+            return Set.of("key");
+        }
+    }
+}
diff --git 
a/nifi-connector-mock-bundle/nifi-connector-mock-test-bundle/nifi-connector-mock-test-connectors/src/main/resources/flows/Generate_and_Update.json
 
b/nifi-connector-mock-bundle/nifi-connector-mock-test-bundle/nifi-connector-mock-test-connectors/src/main/resources/flows/Generate_and_Update.json
index 686247011e..d4f5fc798e 100644
--- 
a/nifi-connector-mock-bundle/nifi-connector-mock-test-bundle/nifi-connector-mock-test-connectors/src/main/resources/flows/Generate_and_Update.json
+++ 
b/nifi-connector-mock-bundle/nifi-connector-mock-test-bundle/nifi-connector-mock-test-connectors/src/main/resources/flows/Generate_and_Update.json
@@ -1 +1,393 @@
-{"flowContents":{"identifier":"cdf7f935-0045-3c88-bdad-fd4e34105756","instanceIdentifier":"3a87b58f-019a-1000-30d2-87dfdd9816aa","name":"Generate
 and 
Update","comments":"","position":{"x":-1124.0,"y":65.5},"processGroups":[],"remoteProcessGroups":[],"processors":[{"identifier":"4d4160b1-736c-3be4-bc3a-84fc6eb4ad2a","instanceIdentifier":"3a87db5f-019a-1000-75df-90e055f21fc9","name":"UpdateAttribute","comments":"","position":{"x":-358.0,"y":-106.5},"type":"org.apache.nifi.processors.attrib
 [...]
\ No newline at end of file
+{
+    "flowContents": {
+        "identifier": "1800c04e-f9b9-3293-bfc7-b35f43e0706c",
+        "instanceIdentifier": "4f8ca484-019c-1000-955f-68c5defeb22b",
+        "name": "Generate_and_Update",
+        "comments": "",
+        "position": {
+            "x": -1254.0,
+            "y": -437.70139741897583
+        },
+        "processGroups": [],
+        "remoteProcessGroups": [],
+        "processors": [
+            {
+                "identifier": "4d4160b1-736c-3be4-bc3a-84fc6eb4ad2a",
+                "instanceIdentifier": "499f9781-71c5-367f-aa17-5441bff29de9",
+                "name": "UpdateAttribute",
+                "comments": "",
+                "position": {
+                    "x": -360.0,
+                    "y": 24.0
+                },
+                "type": 
"org.apache.nifi.processors.attributes.UpdateAttribute",
+                "bundle": {
+                    "group": "org.apache.nifi",
+                    "artifact": "nifi-update-attribute-nar",
+                    "version": "2026.1.20.21-SNAPSHOT"
+                },
+                "properties": {
+                    "Delete Attributes Expression": null,
+                    "Store State": "Do not store state",
+                    "Cache Value Lookup Cache Size": "100",
+                    "Stateful Variables Initial Value": null
+                },
+                "propertyDescriptors": {
+                    "Delete Attributes Expression": {
+                        "name": "Delete Attributes Expression",
+                        "displayName": "Delete Attributes Expression",
+                        "identifiesControllerService": false,
+                        "sensitive": false,
+                        "dynamic": false
+                    },
+                    "Store State": {
+                        "name": "Store State",
+                        "displayName": "Store State",
+                        "identifiesControllerService": false,
+                        "sensitive": false,
+                        "dynamic": false
+                    },
+                    "Cache Value Lookup Cache Size": {
+                        "name": "Cache Value Lookup Cache Size",
+                        "displayName": "Cache Value Lookup Cache Size",
+                        "identifiesControllerService": false,
+                        "sensitive": false,
+                        "dynamic": false
+                    },
+                    "Stateful Variables Initial Value": {
+                        "name": "Stateful Variables Initial Value",
+                        "displayName": "Stateful Variables Initial Value",
+                        "identifiesControllerService": false,
+                        "sensitive": false,
+                        "dynamic": false
+                    }
+                },
+                "style": {},
+                "schedulingPeriod": "0 sec",
+                "schedulingStrategy": "TIMER_DRIVEN",
+                "executionNode": "ALL",
+                "penaltyDuration": "30 sec",
+                "yieldDuration": "1 sec",
+                "bulletinLevel": "WARN",
+                "runDurationMillis": 25,
+                "concurrentlySchedulableTaskCount": 1,
+                "autoTerminatedRelationships": [
+                    "success"
+                ],
+                "scheduledState": "ENABLED",
+                "retryCount": 10,
+                "retriedRelationships": [],
+                "backoffMechanism": "PENALIZE_FLOWFILE",
+                "maxBackoffPeriod": "10 mins",
+                "componentType": "PROCESSOR",
+                "groupIdentifier": "1800c04e-f9b9-3293-bfc7-b35f43e0706c"
+            },
+            {
+                "identifier": "54ab8572-0e10-39a5-b553-931f9c253023",
+                "instanceIdentifier": "6d6b9cd3-6d31-330a-40f9-185959ad1c78",
+                "name": "GenerateFlowFile",
+                "comments": "",
+                "position": {
+                    "x": -360.0,
+                    "y": -371.5
+                },
+                "type": "org.apache.nifi.processors.standard.GenerateFlowFile",
+                "bundle": {
+                    "group": "org.apache.nifi",
+                    "artifact": "nifi-standard-nar",
+                    "version": "2026.1.20.21-SNAPSHOT"
+                },
+                "properties": {
+                    "File Size": "0B",
+                    "Batch Size": "1",
+                    "Unique FlowFiles": "false",
+                    "Mime Type": null,
+                    "Custom Text": null,
+                    "Character Set": "UTF-8",
+                    "Data Format": "Text",
+                    "key": "test.key"
+                },
+                "propertyDescriptors": {
+                    "File Size": {
+                        "name": "File Size",
+                        "displayName": "File Size",
+                        "identifiesControllerService": false,
+                        "sensitive": false,
+                        "dynamic": false
+                    },
+                    "Batch Size": {
+                        "name": "Batch Size",
+                        "displayName": "Batch Size",
+                        "identifiesControllerService": false,
+                        "sensitive": false,
+                        "dynamic": false
+                    },
+                    "Unique FlowFiles": {
+                        "name": "Unique FlowFiles",
+                        "displayName": "Unique FlowFiles",
+                        "identifiesControllerService": false,
+                        "sensitive": false,
+                        "dynamic": false
+                    },
+                    "Mime Type": {
+                        "name": "Mime Type",
+                        "displayName": "Mime Type",
+                        "identifiesControllerService": false,
+                        "sensitive": false,
+                        "dynamic": false
+                    },
+                    "Custom Text": {
+                        "name": "Custom Text",
+                        "displayName": "Custom Text",
+                        "identifiesControllerService": false,
+                        "sensitive": false,
+                        "dynamic": false
+                    },
+                    "Character Set": {
+                        "name": "Character Set",
+                        "displayName": "Character Set",
+                        "identifiesControllerService": false,
+                        "sensitive": false,
+                        "dynamic": false
+                    },
+                    "Data Format": {
+                        "name": "Data Format",
+                        "displayName": "Data Format",
+                        "identifiesControllerService": false,
+                        "sensitive": false,
+                        "dynamic": false
+                    },
+                    "key": {
+                        "name": "key",
+                        "displayName": "key",
+                        "identifiesControllerService": false,
+                        "sensitive": false,
+                        "dynamic": true
+                    }
+                },
+                "style": {},
+                "schedulingPeriod": "0 sec",
+                "schedulingStrategy": "TIMER_DRIVEN",
+                "executionNode": "ALL",
+                "penaltyDuration": "30 sec",
+                "yieldDuration": "1 sec",
+                "bulletinLevel": "WARN",
+                "runDurationMillis": 0,
+                "concurrentlySchedulableTaskCount": 1,
+                "autoTerminatedRelationships": [],
+                "scheduledState": "ENABLED",
+                "retryCount": 10,
+                "retriedRelationships": [],
+                "backoffMechanism": "PENALIZE_FLOWFILE",
+                "maxBackoffPeriod": "10 mins",
+                "componentType": "PROCESSOR",
+                "groupIdentifier": "1800c04e-f9b9-3293-bfc7-b35f43e0706c"
+            },
+            {
+                "identifier": "34c1ac1b-1f21-3fd6-a734-726d5b142b7a",
+                "instanceIdentifier": "4f8d0670-019c-1000-1ac6-c81ef75a70d0",
+                "name": "LookupAttribute",
+                "comments": "",
+                "position": {
+                    "x": -360.0,
+                    "y": -168.0
+                },
+                "type": "org.apache.nifi.processors.standard.LookupAttribute",
+                "bundle": {
+                    "group": "org.apache.nifi",
+                    "artifact": "nifi-standard-nar",
+                    "version": "2026.1.20.21-SNAPSHOT"
+                },
+                "properties": {
+                    "Lookup Service": "b013f870-aee8-3cc4-b022-ac385ded928d",
+                    "test.attribute": "${key}",
+                    "Include Empty Values": "true"
+                },
+                "propertyDescriptors": {
+                    "Lookup Service": {
+                        "name": "Lookup Service",
+                        "displayName": "Lookup Service",
+                        "identifiesControllerService": true,
+                        "sensitive": false,
+                        "dynamic": false
+                    },
+                    "test.attribute": {
+                        "name": "test.attribute",
+                        "displayName": "test.attribute",
+                        "identifiesControllerService": false,
+                        "sensitive": false,
+                        "dynamic": true
+                    },
+                    "Include Empty Values": {
+                        "name": "Include Empty Values",
+                        "displayName": "Include Empty Values",
+                        "identifiesControllerService": false,
+                        "sensitive": false,
+                        "dynamic": false
+                    }
+                },
+                "style": {},
+                "schedulingPeriod": "0 sec",
+                "schedulingStrategy": "TIMER_DRIVEN",
+                "executionNode": "ALL",
+                "penaltyDuration": "30 sec",
+                "yieldDuration": "1 sec",
+                "bulletinLevel": "WARN",
+                "runDurationMillis": 0,
+                "concurrentlySchedulableTaskCount": 1,
+                "autoTerminatedRelationships": [
+                    "failure"
+                ],
+                "scheduledState": "ENABLED",
+                "retryCount": 10,
+                "retriedRelationships": [],
+                "backoffMechanism": "PENALIZE_FLOWFILE",
+                "maxBackoffPeriod": "10 mins",
+                "componentType": "PROCESSOR",
+                "groupIdentifier": "1800c04e-f9b9-3293-bfc7-b35f43e0706c"
+            }
+        ],
+        "inputPorts": [],
+        "outputPorts": [],
+        "connections": [
+            {
+                "identifier": "893ad2c9-4a07-3447-b772-6b7149cfd6c1",
+                "instanceIdentifier": "5b9c96a4-7982-3746-2937-45511fb20c96",
+                "name": "",
+                "source": {
+                    "id": "54ab8572-0e10-39a5-b553-931f9c253023",
+                    "type": "PROCESSOR",
+                    "groupId": "1800c04e-f9b9-3293-bfc7-b35f43e0706c",
+                    "name": "GenerateFlowFile",
+                    "comments": "",
+                    "instanceIdentifier": 
"6d6b9cd3-6d31-330a-40f9-185959ad1c78"
+                },
+                "destination": {
+                    "id": "34c1ac1b-1f21-3fd6-a734-726d5b142b7a",
+                    "type": "PROCESSOR",
+                    "groupId": "1800c04e-f9b9-3293-bfc7-b35f43e0706c",
+                    "name": "LookupAttribute",
+                    "comments": "",
+                    "instanceIdentifier": 
"4f8d0670-019c-1000-1ac6-c81ef75a70d0"
+                },
+                "labelIndex": 0,
+                "zIndex": 1,
+                "selectedRelationships": [
+                    "success"
+                ],
+                "backPressureObjectThreshold": 10000,
+                "backPressureDataSizeThreshold": "1 GB",
+                "flowFileExpiration": "0 sec",
+                "prioritizers": [],
+                "bends": [],
+                "loadBalanceStrategy": "DO_NOT_LOAD_BALANCE",
+                "partitioningAttribute": "",
+                "loadBalanceCompression": "DO_NOT_COMPRESS",
+                "componentType": "CONNECTION",
+                "groupIdentifier": "1800c04e-f9b9-3293-bfc7-b35f43e0706c"
+            },
+            {
+                "identifier": "f63459de-cf1f-3773-8f93-405518e085e4",
+                "instanceIdentifier": "4f93c5e2-019c-1000-2bea-06d8d611d5f6",
+                "name": "",
+                "source": {
+                    "id": "34c1ac1b-1f21-3fd6-a734-726d5b142b7a",
+                    "type": "PROCESSOR",
+                    "groupId": "1800c04e-f9b9-3293-bfc7-b35f43e0706c",
+                    "name": "LookupAttribute",
+                    "comments": "",
+                    "instanceIdentifier": 
"4f8d0670-019c-1000-1ac6-c81ef75a70d0"
+                },
+                "destination": {
+                    "id": "4d4160b1-736c-3be4-bc3a-84fc6eb4ad2a",
+                    "type": "PROCESSOR",
+                    "groupId": "1800c04e-f9b9-3293-bfc7-b35f43e0706c",
+                    "name": "UpdateAttribute",
+                    "comments": "",
+                    "instanceIdentifier": 
"499f9781-71c5-367f-aa17-5441bff29de9"
+                },
+                "labelIndex": 0,
+                "zIndex": 2,
+                "selectedRelationships": [
+                    "matched",
+                    "unmatched"
+                ],
+                "backPressureObjectThreshold": 10000,
+                "backPressureDataSizeThreshold": "1 GB",
+                "flowFileExpiration": "0 sec",
+                "prioritizers": [],
+                "bends": [],
+                "loadBalanceStrategy": "DO_NOT_LOAD_BALANCE",
+                "partitioningAttribute": "",
+                "loadBalanceCompression": "DO_NOT_COMPRESS",
+                "componentType": "CONNECTION",
+                "groupIdentifier": "1800c04e-f9b9-3293-bfc7-b35f43e0706c"
+            }
+        ],
+        "labels": [],
+        "funnels": [],
+        "controllerServices": [
+            {
+                "identifier": "b013f870-aee8-3cc4-b022-ac385ded928d",
+                "instanceIdentifier": "4f8d53d4-019c-1000-d376-abfaa091dc37",
+                "name": "SimpleKeyValueLookupService",
+                "comments": "",
+                "type": "org.apache.nifi.lookup.SimpleKeyValueLookupService",
+                "bundle": {
+                    "group": "org.apache.nifi",
+                    "artifact": "nifi-lookup-services-nar",
+                    "version": "2026.1.20.21-SNAPSHOT"
+                },
+                "properties": {
+                    "test.key": "Test Value"
+                },
+                "propertyDescriptors": {
+                    "test.key": {
+                        "name": "test.key",
+                        "displayName": "test.key",
+                        "identifiesControllerService": false,
+                        "sensitive": false,
+                        "dynamic": true
+                    }
+                },
+                "controllerServiceApis": [
+                    {
+                        "type": "org.apache.nifi.lookup.LookupService",
+                        "bundle": {
+                            "group": "org.apache.nifi",
+                            "artifact": "nifi-standard-services-api-nar",
+                            "version": "2026.1.20.21-SNAPSHOT"
+                        }
+                    },
+                    {
+                        "type": "org.apache.nifi.lookup.StringLookupService",
+                        "bundle": {
+                            "group": "org.apache.nifi",
+                            "artifact": "nifi-standard-services-api-nar",
+                            "version": "2026.1.20.21-SNAPSHOT"
+                        }
+                    }
+                ],
+                "scheduledState": "DISABLED",
+                "bulletinLevel": "WARN",
+                "componentType": "CONTROLLER_SERVICE",
+                "groupIdentifier": "1800c04e-f9b9-3293-bfc7-b35f43e0706c"
+            }
+        ],
+        "defaultFlowFileExpiration": "0 sec",
+        "defaultBackPressureObjectThreshold": 10000,
+        "defaultBackPressureDataSizeThreshold": "1 GB",
+        "scheduledState": "ENABLED",
+        "executionEngine": "INHERITED",
+        "maxConcurrentTasks": 1,
+        "statelessFlowTimeout": "1 min",
+        "flowFileConcurrency": "UNBOUNDED",
+        "flowFileOutboundPolicy": "STREAM_WHEN_AVAILABLE",
+        "componentType": "PROCESS_GROUP"
+    },
+    "externalControllerServices": {},
+    "parameterContexts": {},
+    "flowEncodingVersion": "1.0",
+    "parameterProviders": {},
+    "latest": false
+}
\ No newline at end of file
diff --git 
a/nifi-connector-mock-bundle/nifi-connector-mock/src/main/java/org/apache/nifi/mock/connector/StandardConnectorTestRunner.java
 
b/nifi-connector-mock-bundle/nifi-connector-mock/src/main/java/org/apache/nifi/mock/connector/StandardConnectorTestRunner.java
index d2b7e79f0b..4cea7502b6 100644
--- 
a/nifi-connector-mock-bundle/nifi-connector-mock/src/main/java/org/apache/nifi/mock/connector/StandardConnectorTestRunner.java
+++ 
b/nifi-connector-mock-bundle/nifi-connector-mock/src/main/java/org/apache/nifi/mock/connector/StandardConnectorTestRunner.java
@@ -32,6 +32,7 @@ import org.apache.nifi.nar.ExtensionMapping;
 import org.apache.nifi.nar.NarClassLoaders;
 import org.apache.nifi.nar.NarUnpackMode;
 import org.apache.nifi.nar.NarUnpacker;
+import org.apache.nifi.controller.ControllerService;
 import org.apache.nifi.nar.SystemBundle;
 import org.apache.nifi.processor.Processor;
 import org.apache.nifi.util.NiFiProperties;
@@ -61,13 +62,14 @@ public class StandardConnectorTestRunner implements 
ConnectorTestRunner, Closeab
             throw new RuntimeException("Failed to bootstrap 
ConnectorTestRunner", e);
         }
 
-        // It is important that we register the processor mocks before 
instantiating the connector.
+        // It is important that we register the processor and controller 
service mocks before instantiating the connector.
         // Otherwise, the call to instantiateConnector will initialize the 
Connector, which may update the flow.
-        // If the flow is updated before the processor mocks are registered, 
the Processors will be created without
-        // using the mocks. Subsequent updates to the flow will not replace 
the Processors already created because
-        // these are not recognized as updates to the flow, since the 
framework assumes that the type of a Processor
+        // If the flow is updated before the mocks are registered, the 
components will be created without
+        // using the mocks. Subsequent updates to the flow will not replace 
the components already created because
+        // these are not recognized as updates to the flow, since the 
framework assumes that the type of a component
         // with a given ID does not change.
         builder.processorMocks.forEach(mockServer::mockProcessor);
+        
builder.controllerServiceMocks.forEach(mockServer::mockControllerService);
 
         mockServer.instantiateConnector(builder.connectorClassName);
     }
@@ -211,6 +213,7 @@ public class StandardConnectorTestRunner implements 
ConnectorTestRunner, Closeab
         private String connectorClassName;
         private File narLibraryDirectory;
         private final Map<String, Class<? extends Processor>> processorMocks = 
new HashMap<>();
+        private final Map<String, Class<? extends ControllerService>> 
controllerServiceMocks = new HashMap<>();
 
         public Builder connectorClassName(final String connectorClassName) {
             this.connectorClassName = connectorClassName;
@@ -227,6 +230,11 @@ public class StandardConnectorTestRunner implements 
ConnectorTestRunner, Closeab
             return this;
         }
 
+        public Builder mockControllerService(final String 
controllerServiceType, final Class<? extends ControllerService> 
mockControllerServiceClass) {
+            controllerServiceMocks.put(controllerServiceType, 
mockControllerServiceClass);
+            return this;
+        }
+
         public StandardConnectorTestRunner build() {
             if (!narLibraryDirectory.exists() || 
!narLibraryDirectory.isDirectory()) {
                 throw new IllegalArgumentException("NAR file does not exist or 
is not a directory: " + narLibraryDirectory.getAbsolutePath());


Reply via email to