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

pvillard pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new 1e8c4e68fe NIFI-14336: Creating processor to list box folder contents
1e8c4e68fe is described below

commit 1e8c4e68fe86e5af52079aafa3d5912e59d5f846
Author: Noah Cover <[email protected]>
AuthorDate: Thu Mar 6 16:50:55 2025 -0800

    NIFI-14336: Creating processor to list box folder contents
    
    Signed-off-by: Pierre Villard <[email protected]>
    
    This closes #9784.
---
 .../nifi-box-bundle/nifi-box-processors/pom.xml    |   9 +-
 .../nifi/processors/box/ListBoxFileInfo.java       | 281 +++++++++++++++++++++
 .../services/org.apache.nifi.processor.Processor   |   1 +
 .../nifi/processors/box/ListBoxFileInfoTest.java   | 197 +++++++++++++++
 4 files changed, 486 insertions(+), 2 deletions(-)

diff --git a/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/pom.xml 
b/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/pom.xml
index ceb7a250b2..8aacfe64d3 100644
--- a/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/pom.xml
+++ b/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/pom.xml
@@ -13,7 +13,8 @@
   See the License for the specific language governing permissions and
   limitations under the License.
 -->
-<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
https://maven.apache.org/xsd/maven-4.0.0.xsd";>
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
https://maven.apache.org/xsd/maven-4.0.0.xsd";>
     <modelVersion>4.0.0</modelVersion>
 
     <parent>
@@ -50,7 +51,6 @@
         <dependency>
             <groupId>org.apache.nifi</groupId>
             <artifactId>nifi-record-serialization-service-api</artifactId>
-            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>org.apache.nifi</groupId>
@@ -64,5 +64,10 @@
             <artifactId>box-java-sdk</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-mock-record-utils</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 </project>
diff --git 
a/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/ListBoxFileInfo.java
 
b/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/ListBoxFileInfo.java
new file mode 100644
index 0000000000..bd2b6a2051
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/ListBoxFileInfo.java
@@ -0,0 +1,281 @@
+/*
+ * 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.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxAPIResponseException;
+import com.box.sdk.BoxFile;
+import com.box.sdk.BoxFolder;
+import com.box.sdk.BoxItem;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.WritesAttribute;
+import org.apache.nifi.annotation.behavior.WritesAttributes;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.schema.access.SchemaNotFoundException;
+import org.apache.nifi.serialization.RecordSetWriter;
+import org.apache.nifi.serialization.RecordSetWriterFactory;
+import org.apache.nifi.serialization.SimpleRecordSchema;
+import org.apache.nifi.serialization.WriteResult;
+import org.apache.nifi.serialization.record.MapRecord;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.serialization.record.RecordField;
+import org.apache.nifi.serialization.record.RecordFieldType;
+import org.apache.nifi.serialization.record.RecordSchema;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import static java.lang.String.valueOf;
+import static org.apache.nifi.processors.box.BoxFileAttributes.ERROR_CODE;
+import static org.apache.nifi.processors.box.BoxFileAttributes.ERROR_CODE_DESC;
+import static org.apache.nifi.processors.box.BoxFileAttributes.ERROR_MESSAGE;
+import static 
org.apache.nifi.processors.box.BoxFileAttributes.ERROR_MESSAGE_DESC;
+
+@InputRequirement(InputRequirement.Requirement.INPUT_REQUIRED)
+@Tags({"box", "storage", "fetch", "folder", "files"})
+@CapabilityDescription("Fetches file metadata for each file in a Box Folder. 
Takes a flowFile with a folder ID attribute and outputs flowFiles with records 
containing all file metadata.")
+@SeeAlso({ListBoxFile.class, FetchBoxFile.class, PutBoxFile.class})
+@WritesAttributes({
+        @WritesAttribute(attribute = "box.folder.id", description = "The ID of 
the folder from which files were fetched"),
+        @WritesAttribute(attribute = "record.count", description = "The number 
of records in the FlowFile"),
+        @WritesAttribute(attribute = "mime.type", description = "The MIME Type 
specified by the Record Writer"),
+        @WritesAttribute(attribute = ERROR_CODE, description = 
ERROR_CODE_DESC),
+        @WritesAttribute(attribute = ERROR_MESSAGE, description = 
ERROR_MESSAGE_DESC)
+})
+public class ListBoxFileInfo extends AbstractProcessor {
+
+    public static final PropertyDescriptor FOLDER_ID = new 
PropertyDescriptor.Builder()
+            .name("Folder ID")
+            .description("The ID of the folder from which to fetch files.")
+            .required(true)
+            .defaultValue("${box.folder.id}")
+            
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            .build();
+
+    public static final PropertyDescriptor RECURSIVE_SEARCH = new 
PropertyDescriptor.Builder()
+            .name("Search Recursively")
+            .description("When 'true', will include files from sub-folders." +
+                    " Otherwise, will return only files that are within the 
folder defined by the 'Folder ID' property.")
+            .required(true)
+            .defaultValue("true")
+            .allowableValues("true", "false")
+            .build();
+
+    public static final PropertyDescriptor MIN_AGE = new 
PropertyDescriptor.Builder()
+            .name("Minimum File Age")
+            .description("The minimum age a file must be in order to be 
considered; any files younger than this will be ignored.")
+            .required(true)
+            .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR)
+            .defaultValue("0 sec")
+            .build();
+
+    public static final PropertyDescriptor RECORD_WRITER = new 
PropertyDescriptor.Builder()
+            .name("Record Writer")
+            .description("Specifies the Controller Service to use for writing 
the metadata records. Must be set.")
+            .identifiesControllerService(RecordSetWriterFactory.class)
+            .required(true)
+            .build();
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("A FlowFile containing the file metadata records will 
be routed to this relationship upon successful processing.")
+            .build();
+
+    public static final Relationship REL_FAILURE = new Relationship.Builder()
+            .name("failure")
+            .description("A FlowFile will be routed here if there is an error 
fetching file metadata from the folder.")
+            .build();
+
+    public static final Set<Relationship> RELATIONSHIPS = Set.of(
+            REL_SUCCESS,
+            REL_FAILURE
+    );
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = 
List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            FOLDER_ID,
+            RECURSIVE_SEARCH,
+            MIN_AGE,
+            RECORD_WRITER
+    );
+
+    private static final RecordSchema RECORD_SCHEMA = new 
SimpleRecordSchema(List.of(
+            new RecordField("id", RecordFieldType.STRING.getDataType(), false),
+            new RecordField("filename", RecordFieldType.STRING.getDataType(), 
false),
+            new RecordField("path", RecordFieldType.STRING.getDataType(), 
false),
+            new RecordField("size", RecordFieldType.LONG.getDataType(), false),
+            new RecordField("timestamp", 
RecordFieldType.TIMESTAMP.getDataType(), false)
+    ));
+
+    private volatile BoxAPIConnection boxAPIConnection;
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = 
context.getProperty(BoxClientService.BOX_CLIENT_SERVICE)
+                .asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+    }
+
+    @Override
+    public void onTrigger(final ProcessContext context, final ProcessSession 
session) throws ProcessException {
+        FlowFile flowFile = session.get();
+        if (flowFile == null) {
+            return;
+        }
+
+        final String folderId = 
context.getProperty(FOLDER_ID).evaluateAttributeExpressions(flowFile).getValue();
+        final Boolean recursive = 
context.getProperty(RECURSIVE_SEARCH).asBoolean();
+        final Long minAge = 
context.getProperty(MIN_AGE).asTimePeriod(TimeUnit.MILLISECONDS);
+        final RecordSetWriterFactory writerFactory = 
context.getProperty(RECORD_WRITER).asControllerService(RecordSetWriterFactory.class);
+
+        try {
+            final long startNanos = System.nanoTime();
+            long createdAtMax = Instant.now().toEpochMilli() - minAge;
+            final List<BoxFile.Info> fileInfos = new ArrayList<>();
+
+            listFolder(fileInfos, folderId, recursive, createdAtMax);
+
+            if (fileInfos.isEmpty()) {
+                flowFile = session.putAttribute(flowFile, "box.folder.id", 
folderId);
+                session.transfer(flowFile, REL_SUCCESS);
+                return;
+            }
+
+            flowFile = session.putAttribute(flowFile, "box.folder.id", 
folderId);
+
+            try {
+                final WriteResult writeResult;
+                final String mimeType;
+
+                try (final OutputStream out = session.write(flowFile);
+                     final RecordSetWriter writer = 
writerFactory.createWriter(getLogger(), RECORD_SCHEMA, out, flowFile)) {
+
+                    writer.beginRecordSet();
+
+                    for (final BoxFile.Info fileInfo : fileInfos) {
+                        final Map<String, Object> values = Map.of(
+                                "id", fileInfo.getID(),
+                                "filename", fileInfo.getName(),
+                                "path", BoxFileUtils.getParentPath(fileInfo),
+                                "size", fileInfo.getSize(),
+                                "timestamp", new 
Timestamp(fileInfo.getModifiedAt().getTime())
+                        );
+
+                        final Record record = new MapRecord(RECORD_SCHEMA, 
values);
+                        writer.write(record);
+                    }
+
+                    writeResult = writer.finishRecordSet();
+                    mimeType = writer.getMimeType();
+                }
+
+                final Map<String, String> recordAttributes = new 
HashMap<>(writeResult.getAttributes());
+                recordAttributes.put("record.count", 
String.valueOf(writeResult.getRecordCount()));
+                recordAttributes.put(CoreAttributes.MIME_TYPE.key(), mimeType);
+                flowFile = session.putAllAttributes(flowFile, 
recordAttributes);
+
+                final long transferMillis = 
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
+                session.getProvenanceReporter().receive(flowFile, 
BoxFileUtils.BOX_URL + folderId, transferMillis);
+
+                session.transfer(flowFile, REL_SUCCESS);
+            } catch (final SchemaNotFoundException | IOException e) {
+                getLogger().error("Failed writing records for files from 
folder [{}]", folderId, e);
+                flowFile = session.putAttribute(flowFile, ERROR_MESSAGE, 
e.getMessage());
+                flowFile = session.penalize(flowFile);
+                session.transfer(flowFile, REL_FAILURE);
+            }
+
+        } catch (final BoxAPIResponseException e) {
+            getLogger().error("Couldn't fetch files from folder with id [{}]", 
folderId, e);
+            flowFile = session.putAttribute(flowFile, ERROR_CODE, 
valueOf(e.getResponseCode()));
+            flowFile = session.putAttribute(flowFile, ERROR_MESSAGE, 
e.getMessage());
+            flowFile = session.penalize(flowFile);
+            session.transfer(flowFile, REL_FAILURE);
+        }
+    }
+
+    private void listFolder(final List<BoxFile.Info> fileInfos,
+                            final String folderId,
+                            final Boolean recursive,
+                            final long createdAtMax) {
+        final BoxFolder folder = getFolder(folderId);
+        for (final BoxItem.Info itemInfo : folder.getChildren(
+                "id",
+                "name",
+                "item_status",
+                "size",
+                "created_at",
+                "modified_at",
+                "content_created_at",
+                "content_modified_at",
+                "path_collection"
+        )) {
+            if (itemInfo instanceof BoxFile.Info fileInfo) {
+                long createdAt = itemInfo.getCreatedAt().getTime();
+
+                if (createdAt <= createdAtMax) {
+                    fileInfos.add(fileInfo);
+                }
+            } else if (recursive && itemInfo instanceof BoxFolder.Info 
subFolderInfo) {
+                listFolder(fileInfos, subFolderInfo.getID(), recursive, 
createdAtMax);
+            }
+        }
+    }
+
+    /**
+     * Returns a BoxFolder object for the given folder ID.
+     *
+     * @param folderId The ID of the folder.
+     * @return A BoxFolder object for the given folder ID.
+     */
+    BoxFolder getFolder(final String folderId) {
+        return new BoxFolder(boxAPIConnection, folderId);
+    }
+}
diff --git 
a/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
 
b/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
index 4c62ac0aa2..cf5335907b 100644
--- 
a/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
+++ 
b/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
@@ -21,4 +21,5 @@ org.apache.nifi.processors.box.FetchBoxFileRepresentation
 org.apache.nifi.processors.box.GetBoxFileCollaborators
 org.apache.nifi.processors.box.GetBoxGroupMembers
 org.apache.nifi.processors.box.ListBoxFile
+org.apache.nifi.processors.box.ListBoxFileInfo
 org.apache.nifi.processors.box.PutBoxFile
diff --git 
a/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileInfoTest.java
 
b/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileInfoTest.java
new file mode 100644
index 0000000000..25e4d747b9
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileInfoTest.java
@@ -0,0 +1,197 @@
+/*
+ * 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.processors.box;
+
+import com.box.sdk.BoxAPIResponseException;
+import com.box.sdk.BoxFolder;
+import org.apache.nifi.serialization.record.MockRecordWriter;
+import org.apache.nifi.util.MockFlowFile;
+import org.apache.nifi.util.TestRunners;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.apache.nifi.processors.box.BoxFileAttributes.ERROR_MESSAGE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+
+@ExtendWith(MockitoExtension.class)
+public class ListBoxFileInfoTest extends AbstractBoxFileTest implements 
FileListingTestTrait {
+
+    private static final String RECORD_WRITER_ID = "record-writer";
+
+    @BeforeEach
+    void setUp() throws Exception {
+        final ListBoxFileInfo testSubject = new ListBoxFileInfo() {
+            @Override
+            BoxFolder getFolder(final String folderId) {
+                return mockBoxFolder;
+            }
+        };
+
+        testRunner = TestRunners.newTestRunner(testSubject);
+
+        final MockRecordWriter writerService = new 
MockRecordWriter("id,filename,path,size,timestamp", false);
+        testRunner.addControllerService(RECORD_WRITER_ID, writerService);
+        testRunner.enableControllerService(writerService);
+        testRunner.setProperty(ListBoxFileInfo.RECORD_WRITER, 
RECORD_WRITER_ID);
+
+        super.setUp();
+    }
+
+    @Test
+    void testFetchMetadataFromFolderWithFolderIdProperty() {
+        testRunner.setProperty(ListBoxFileInfo.FOLDER_ID, TEST_FOLDER_ID);
+        testRunner.setProperty(ListBoxFileInfo.RECURSIVE_SEARCH, "false");
+
+        final List<String> pathParts = Arrays.asList("path", "to", "file");
+        mockFetchedFileList(TEST_FILE_ID, TEST_FILENAME, pathParts, TEST_SIZE, 
CREATED_TIME, MODIFIED_TIME);
+
+        testRunner.enqueue("test file");
+        testRunner.run();
+
+        testRunner.assertTransferCount(ListBoxFileInfo.REL_SUCCESS, 1);
+        testRunner.assertTransferCount(ListBoxFileInfo.REL_FAILURE, 0);
+
+        final List<MockFlowFile> successFiles = 
testRunner.getFlowFilesForRelationship(ListBoxFileInfo.REL_SUCCESS);
+        final MockFlowFile outputFlowFile = successFiles.getFirst();
+
+        outputFlowFile.assertAttributeExists("record.count");
+        assertEquals("1", outputFlowFile.getAttribute("record.count"));
+        outputFlowFile.assertAttributeEquals("box.folder.id", TEST_FOLDER_ID);
+
+        final String content = new String(outputFlowFile.toByteArray());
+        assertTrue(content.contains(TEST_FILE_ID));
+        assertTrue(content.contains(TEST_FILENAME));
+        assertTrue(content.contains("/path"));
+        assertTrue(content.contains(String.valueOf(TEST_SIZE)));
+    }
+
+    @Test
+    void testFetchMetadataFromFolderWithFolderIdAttributeExpression() {
+        testRunner.setProperty(ListBoxFileInfo.FOLDER_ID, "${box.folder.id}");
+        testRunner.setProperty(ListBoxFileInfo.RECURSIVE_SEARCH, "true");
+
+        final List<String> pathParts = Arrays.asList("path", "to", "file");
+        mockFetchedFileList(TEST_FILE_ID, TEST_FILENAME, pathParts, TEST_SIZE, 
CREATED_TIME, MODIFIED_TIME);
+
+        final Map<String, String> attributeMap = new HashMap<>();
+        attributeMap.put("box.folder.id", TEST_FOLDER_ID);
+
+        testRunner.enqueue("test file", attributeMap);
+        testRunner.run();
+
+        testRunner.assertTransferCount(ListBoxFileInfo.REL_SUCCESS, 1);
+        testRunner.assertTransferCount(ListBoxFileInfo.REL_FAILURE, 0);
+
+        final List<MockFlowFile> successFiles = 
testRunner.getFlowFilesForRelationship(ListBoxFileInfo.REL_SUCCESS);
+        final MockFlowFile outputFlowFile = successFiles.getFirst();
+
+        outputFlowFile.assertAttributeExists("record.count");
+        assertEquals("1", outputFlowFile.getAttribute("record.count"));
+        outputFlowFile.assertAttributeEquals("box.folder.id", TEST_FOLDER_ID);
+
+        final String content = new String(outputFlowFile.toByteArray());
+        assertTrue(content.contains(TEST_FILE_ID));
+        assertTrue(content.contains(TEST_FILENAME));
+        assertTrue(content.contains("/path"));
+        assertTrue(content.contains(String.valueOf(TEST_SIZE)));
+    }
+
+    @Test
+    void testProcessingMultipleFiles() {
+        testRunner.setProperty(ListBoxFileInfo.FOLDER_ID, TEST_FOLDER_ID);
+        testRunner.setProperty(ListBoxFileInfo.RECURSIVE_SEARCH, "false");
+        mockMultipleFilesResponse();
+
+        testRunner.enqueue("test file");
+        testRunner.run();
+
+        testRunner.assertTransferCount(ListBoxFileInfo.REL_SUCCESS, 1);
+        testRunner.assertTransferCount(ListBoxFileInfo.REL_FAILURE, 0);
+
+        final List<MockFlowFile> successFiles = 
testRunner.getFlowFilesForRelationship(ListBoxFileInfo.REL_SUCCESS);
+        final MockFlowFile outputFlowFile = successFiles.getFirst();
+        outputFlowFile.assertAttributeEquals("record.count", "3");
+        outputFlowFile.assertAttributeEquals("box.folder.id", TEST_FOLDER_ID);
+        final String content = new String(outputFlowFile.toByteArray());
+        assertTrue(content.contains(TEST_FILE_ID + "1"));
+        assertTrue(content.contains(TEST_FILE_ID + "2"));
+        assertTrue(content.contains(TEST_FILE_ID + "3"));
+    }
+
+    @Test
+    void testBoxAPIResponseException() {
+        testRunner.setProperty(ListBoxFileInfo.FOLDER_ID, TEST_FOLDER_ID);
+
+        final BoxAPIResponseException apiException = new 
BoxAPIResponseException("API Error", 404, "Not Found", null);
+        doThrow(apiException).when(mockBoxFolder).getChildren(
+                "id",
+                "name",
+                "item_status",
+                "size",
+                "created_at",
+                "modified_at",
+                "content_created_at",
+                "content_modified_at",
+                "path_collection");
+
+        final MockFlowFile inputFlowFile = new MockFlowFile(0);
+        testRunner.enqueue(inputFlowFile);
+        testRunner.run();
+
+        testRunner.assertTransferCount(ListBoxFileInfo.REL_FAILURE, 1);
+        testRunner.assertTransferCount(ListBoxFileInfo.REL_SUCCESS, 0);
+
+        final List<MockFlowFile> failureFiles = 
testRunner.getFlowFilesForRelationship(ListBoxFileInfo.REL_FAILURE);
+        final MockFlowFile failureFlowFile = failureFiles.getFirst();
+        failureFlowFile.assertAttributeEquals(BoxFileAttributes.ERROR_CODE, 
"404");
+        failureFlowFile.assertAttributeExists(ERROR_MESSAGE);
+    }
+
+    private void mockMultipleFilesResponse() {
+        List<String> pathParts = Arrays.asList("path", "to", "file");
+
+        doReturn(Arrays.asList(
+                createFileInfo(TEST_FILE_ID + "1", TEST_FILENAME + "1", 
pathParts, TEST_SIZE, CREATED_TIME, MODIFIED_TIME),
+                createFileInfo(TEST_FILE_ID + "2", TEST_FILENAME + "2", 
pathParts, TEST_SIZE, CREATED_TIME, MODIFIED_TIME),
+                createFileInfo(TEST_FILE_ID + "3", TEST_FILENAME + "3", 
pathParts, TEST_SIZE, CREATED_TIME, MODIFIED_TIME)
+        )).when(mockBoxFolder).getChildren(
+                "id",
+                "name",
+                "item_status",
+                "size",
+                "created_at",
+                "modified_at",
+                "content_created_at",
+                "content_modified_at",
+                "path_collection");
+    }
+
+    @Override
+    public BoxFolder getMockBoxFolder() {
+        return mockBoxFolder;
+    }
+}

Reply via email to