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

turcsanyi 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 653631cc67 NIFI-11059 PutBoxFile processor
653631cc67 is described below

commit 653631cc675d8067a6e4ac4d5f66eb9ecf2cb33c
Author: krisztina-zsihovszki <[email protected]>
AuthorDate: Tue Jan 17 09:24:17 2023 +0100

    NIFI-11059 PutBoxFile processor
    
    This closes #6892.
    
    Signed-off-by: Peter Turcsanyi <[email protected]>
---
 .../nifi-box-bundle/nifi-box-processors/pom.xml    |   5 +
 .../nifi/processors/box/BoxFileAttributes.java     |  40 ++
 .../apache/nifi/processors/box/BoxFileInfo.java    |  18 +-
 .../apache/nifi/processors/box/BoxFileUtils.java   |  55 +++
 .../nifi/processors/box/BoxFlowFileAttribute.java  |  11 +-
 .../apache/nifi/processors/box/FetchBoxFile.java   | 121 +++---
 .../apache/nifi/processors/box/ListBoxFile.java    |  56 +--
 .../org/apache/nifi/processors/box/PutBoxFile.java | 429 +++++++++++++++++++++
 .../services/org.apache.nifi.processor.Processor   |   1 +
 .../additionalDetails.html                         |  46 +++
 .../additionalDetails.html                         |  43 +++
 .../additionalDetails.html                         |  42 ++
 .../nifi/processors/box/AbstractBoxFileIT.java     |  32 +-
 .../nifi/processors/box/AbstractBoxFileTest.java   | 125 ++++++
 .../apache/nifi/processors/box/FetchBoxFileIT.java |  55 ++-
 .../nifi/processors/box/FetchBoxFileTest.java      | 120 ++++++
 ...ileTestTrait.java => FileListingTestTrait.java} |  31 +-
 .../apache/nifi/processors/box/ListBoxFileIT.java  |  13 -
 ...SimpleTest.java => ListBoxFileListingTest.java} |  48 ++-
 .../nifi/processors/box/ListBoxFileTest.java       | 110 ++++++
 .../processors/box/ListBoxFileTestRunnerTest.java  | 180 ---------
 .../apache/nifi/processors/box/PutBoxFileIT.java   | 120 ++++++
 .../apache/nifi/processors/box/PutBoxFileTest.java | 212 ++++++++++
 23 files changed, 1543 insertions(+), 370 deletions(-)

diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/pom.xml 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/pom.xml
index 93ddaeff53..399c265e46 100644
--- a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/pom.xml
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/pom.xml
@@ -41,6 +41,11 @@
             <groupId>org.apache.nifi</groupId>
             <artifactId>nifi-record</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-conflict-resolution</artifactId>
+            <version>1.20.0-SNAPSHOT</version>
+        </dependency>
         <dependency>
             <groupId>org.apache.nifi</groupId>
             <artifactId>nifi-mock</artifactId>
diff --git 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/BoxFileAttributes.java
 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/BoxFileAttributes.java
new file mode 100644
index 0000000000..4f608da3a1
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/BoxFileAttributes.java
@@ -0,0 +1,40 @@
+/*
+ * 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;
+
+public class BoxFileAttributes {
+
+    public static final String ID = "box.id";
+    public static final String ID_DESC = "The id of the file";
+
+    public static final String FILENAME_DESC = "The name of the file";
+
+    public static final String PATH_DESC = "The folder path where the file is 
located";
+
+    public static final String SIZE = "box.size";
+    public static final String SIZE_DESC = "The size of the file";
+
+    public static final String TIMESTAMP = "box.timestamp";
+    public static final String TIMESTAMP_DESC =   "The last modified time of 
the file";
+
+    public static final String ERROR_MESSAGE = "error.message";
+    public static final String ERROR_MESSAGE_DESC = "The error message 
returned by Box";
+
+    public static final String ERROR_CODE = "error.code";
+    public static final String ERROR_CODE_DESC = "The error code returned by 
Box";
+
+}
diff --git 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/BoxFileInfo.java
 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/BoxFileInfo.java
index 38cddb65f0..a8b850ed05 100644
--- 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/BoxFileInfo.java
+++ 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/BoxFileInfo.java
@@ -16,6 +16,11 @@
  */
 package org.apache.nifi.processors.box;
 
+import static org.apache.nifi.processors.box.BoxFileAttributes.ID;
+import static org.apache.nifi.processors.box.BoxFileAttributes.SIZE;
+import static org.apache.nifi.processors.box.BoxFileAttributes.TIMESTAMP;
+
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
 import org.apache.nifi.processor.util.list.ListableEntity;
 import org.apache.nifi.serialization.SimpleRecordSchema;
 import org.apache.nifi.serialization.record.MapRecord;
@@ -30,11 +35,6 @@ import java.util.List;
 import java.util.Map;
 
 public class BoxFileInfo implements ListableEntity {
-    public static final String ID = "box.id";
-    public static final String FILENAME = "filename";
-    public static final String PATH = "path";
-    public static final String SIZE = "box.size";
-    public static final String TIMESTAMP = "box.timestamp";
 
     private  static final RecordSchema SCHEMA;
 
@@ -42,8 +42,8 @@ public class BoxFileInfo implements ListableEntity {
         final List<RecordField> recordFields = new ArrayList<>();
 
         recordFields.add(new RecordField(ID, 
RecordFieldType.STRING.getDataType(), false));
-        recordFields.add(new RecordField(FILENAME, 
RecordFieldType.STRING.getDataType(), false));
-        recordFields.add(new RecordField(PATH, 
RecordFieldType.STRING.getDataType(), false));
+        recordFields.add(new RecordField(CoreAttributes.FILENAME.key(), 
RecordFieldType.STRING.getDataType(), false));
+        recordFields.add(new RecordField(CoreAttributes.PATH.key(), 
RecordFieldType.STRING.getDataType(), false));
         recordFields.add(new RecordField(SIZE, 
RecordFieldType.LONG.getDataType(), false));
         recordFields.add(new RecordField(TIMESTAMP, 
RecordFieldType.LONG.getDataType(), false));
 
@@ -82,8 +82,8 @@ public class BoxFileInfo implements ListableEntity {
         final Map<String, Object> values = new HashMap<>();
 
         values.put(ID, getId());
-        values.put(FILENAME, getName());
-        values.put(PATH, getPath());
+        values.put(CoreAttributes.FILENAME.key(), getName());
+        values.put(CoreAttributes.PATH.key(), getPath());
         values.put(SIZE, getSize());
         values.put(TIMESTAMP, getTimestamp());
 
diff --git 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/BoxFileUtils.java
 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/BoxFileUtils.java
new file mode 100644
index 0000000000..81d72172f9
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/BoxFileUtils.java
@@ -0,0 +1,55 @@
+/*
+ * 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 static java.lang.String.valueOf;
+
+import com.box.sdk.BoxFile;
+import com.box.sdk.BoxFolder;
+import com.box.sdk.BoxItem;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+
+public final class BoxFileUtils {
+
+    public static final String BOX_URL = "https://app.box.com/file/";;
+
+    public static String getParentPath(BoxItem.Info info) {
+        return "/" + info.getPathCollection().stream()
+                .filter(pathItemInfo -> !pathItemInfo.getID().equals("0"))
+                .map(BoxItem.Info::getName)
+                .collect(Collectors.joining("/"));
+    }
+
+    public static String getFolderPath(BoxFolder.Info folderInfo) {
+        final String parentFolderPath = getParentPath(folderInfo);
+        return "/".equals(parentFolderPath) ? parentFolderPath + 
folderInfo.getName() : parentFolderPath + "/" + folderInfo.getName();
+    }
+
+    public static Map<String, String> createAttributeMap(BoxFile.Info 
fileInfo) {
+        final Map<String, String> attributes = new LinkedHashMap<>();
+        attributes.put(BoxFileAttributes.ID, fileInfo.getID());
+        attributes.put(CoreAttributes.FILENAME.key(), fileInfo.getName());
+        attributes.put(CoreAttributes.PATH.key(), getParentPath(fileInfo));
+        attributes.put(BoxFileAttributes.TIMESTAMP, 
valueOf(fileInfo.getModifiedAt()));
+        attributes.put(BoxFileAttributes.SIZE, valueOf(fileInfo.getSize()));
+        return attributes;
+    }
+
+}
diff --git 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/BoxFlowFileAttribute.java
 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/BoxFlowFileAttribute.java
index e86ecc673a..be1fc46042 100644
--- 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/BoxFlowFileAttribute.java
+++ 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/BoxFlowFileAttribute.java
@@ -17,13 +17,14 @@
 package org.apache.nifi.processors.box;
 
 import java.util.function.Function;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
 
 public enum BoxFlowFileAttribute {
-    ID(BoxFileInfo.ID, BoxFileInfo::getId),
-    FILENAME(BoxFileInfo.FILENAME, BoxFileInfo::getName),
-    PATH(BoxFileInfo.PATH, BoxFileInfo::getPath),
-    SIZE(BoxFileInfo.SIZE, fileInfo -> String.valueOf(fileInfo.getSize())),
-    TIMESTAMP(BoxFileInfo.TIMESTAMP, fileInfo -> 
String.valueOf(fileInfo.getTimestamp()));
+    ID(BoxFileAttributes.ID, BoxFileInfo::getId),
+    FILENAME(CoreAttributes.FILENAME.key(), BoxFileInfo::getName),
+    PATH(CoreAttributes.PATH.key(), BoxFileInfo::getPath),
+    SIZE(BoxFileAttributes.SIZE, fileInfo -> 
String.valueOf(fileInfo.getSize())),
+    TIMESTAMP(BoxFileAttributes.TIMESTAMP, fileInfo -> 
String.valueOf(fileInfo.getTimestamp()));
 
     private final String name;
     private final Function<BoxFileInfo, String> fromFileInfo;
diff --git 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/FetchBoxFile.java
 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/FetchBoxFile.java
index cad434b553..2ef2f567cf 100644
--- 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/FetchBoxFile.java
+++ 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/FetchBoxFile.java
@@ -16,10 +16,26 @@
  */
 package org.apache.nifi.processors.box;
 
+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;
+import static org.apache.nifi.processors.box.BoxFileAttributes.FILENAME_DESC;
+import static org.apache.nifi.processors.box.BoxFileAttributes.ID;
+import static org.apache.nifi.processors.box.BoxFileAttributes.ID_DESC;
+import static org.apache.nifi.processors.box.BoxFileAttributes.PATH_DESC;
+import static org.apache.nifi.processors.box.BoxFileAttributes.SIZE;
+import static org.apache.nifi.processors.box.BoxFileAttributes.SIZE_DESC;
+import static org.apache.nifi.processors.box.BoxFileAttributes.TIMESTAMP;
+import static org.apache.nifi.processors.box.BoxFileAttributes.TIMESTAMP_DESC;
+
 import com.box.sdk.BoxAPIConnection;
 import com.box.sdk.BoxAPIResponseException;
 import com.box.sdk.BoxFile;
+import java.util.concurrent.TimeUnit;
 import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.ReadsAttribute;
 import org.apache.nifi.annotation.behavior.WritesAttribute;
 import org.apache.nifi.annotation.behavior.WritesAttributes;
 import org.apache.nifi.annotation.documentation.CapabilityDescription;
@@ -37,7 +53,6 @@ import org.apache.nifi.processor.Relationship;
 import org.apache.nifi.processor.exception.ProcessException;
 import org.apache.nifi.processor.util.StandardValidators;
 
-import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
@@ -47,46 +62,51 @@ import java.util.Set;
 @InputRequirement(InputRequirement.Requirement.INPUT_REQUIRED)
 @Tags({"box", "storage", "fetch"})
 @CapabilityDescription("Fetches files from a Box Folder. Designed to be used 
in tandem with ListBoxFile.")
-@SeeAlso({ListBoxFile.class})
+@SeeAlso({ListBoxFile.class, PutBoxFile.class})
+@ReadsAttribute(attribute = ID, description = ID_DESC)
 @WritesAttributes({
-    @WritesAttribute(attribute = FetchBoxFile.ERROR_CODE_ATTRIBUTE, 
description = "The error code returned by Box when the fetch of a file fails"),
-    @WritesAttribute(attribute = FetchBoxFile.ERROR_MESSAGE_ATTRIBUTE, 
description = "The error message returned by Box when the fetch of a file 
fails")
+        @WritesAttribute(attribute = ID, description = ID_DESC),
+        @WritesAttribute(attribute = "filename", description = FILENAME_DESC),
+        @WritesAttribute(attribute = "path", description = PATH_DESC),
+        @WritesAttribute(attribute = SIZE, description = SIZE_DESC),
+        @WritesAttribute(attribute = TIMESTAMP, description = TIMESTAMP_DESC),
+        @WritesAttribute(attribute = ERROR_CODE, description = 
ERROR_CODE_DESC),
+        @WritesAttribute(attribute = ERROR_MESSAGE, description = 
ERROR_MESSAGE_DESC)
 })
 public class FetchBoxFile extends AbstractProcessor {
-    public static final String ERROR_CODE_ATTRIBUTE = "error.code";
-    public static final String ERROR_MESSAGE_ATTRIBUTE = "error.message";
 
     public static final PropertyDescriptor FILE_ID = new PropertyDescriptor
-        .Builder().name("box-file-id")
-        .displayName("File ID")
-        .description("The ID of the File to fetch")
-        .required(true)
-        .defaultValue("${box.id}")
-        
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
-        .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
-        .build();
+            .Builder().name("box-file-id")
+            .displayName("File ID")
+            .description("The ID of the File to fetch")
+            .required(true)
+            .defaultValue("${box.id}")
+            
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            .build();
 
     public static final Relationship REL_SUCCESS =
-        new Relationship.Builder()
-            .name("success")
-            .description("A FlowFile will be routed here for each successfully 
fetched File.")
-            .build();
+            new Relationship.Builder()
+                    .name("success")
+                    .description("A FlowFile will be routed here for each 
successfully fetched File.")
+                    .build();
 
     public static final Relationship REL_FAILURE =
-        new Relationship.Builder().name("failure")
-            .description("A FlowFile will be routed here for each File for 
which fetch was attempted but failed.")
-            .build();
+            new Relationship.Builder()
+                    .name("failure")
+                    .description("A FlowFile will be routed here for each File 
for which fetch was attempted but failed.")
+                    .build();
+
+    public static final Set<Relationship> RELATIONSHIPS = 
Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
+            REL_SUCCESS,
+            REL_FAILURE
+    )));
 
     private static final List<PropertyDescriptor> PROPERTIES = 
Collections.unmodifiableList(Arrays.asList(
-        BoxClientService.BOX_CLIENT_SERVICE,
-        FILE_ID
+            BoxClientService.BOX_CLIENT_SERVICE,
+            FILE_ID
     ));
 
-    public static final Set<Relationship> relationships = 
Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
-        REL_SUCCESS,
-        REL_FAILURE
-    )));
-
     private volatile BoxAPIConnection boxAPIConnection;
 
     @Override
@@ -96,11 +116,11 @@ public class FetchBoxFile extends AbstractProcessor {
 
     @Override
     public Set<Relationship> getRelationships() {
-        return relationships;
+        return RELATIONSHIPS;
     }
 
     @OnScheduled
-    public void onScheduled(final ProcessContext context) throws IOException {
+    public void onScheduled(final ProcessContext context) {
         BoxClientService boxClientService = 
context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
 
         boxAPIConnection = boxClientService.getBoxApiConnection();
@@ -114,11 +134,13 @@ public class FetchBoxFile extends AbstractProcessor {
         }
 
         String fileId = 
context.getProperty(FILE_ID).evaluateAttributeExpressions(flowFile).getValue();
-        FlowFile outFlowFile = flowFile;
         try {
-            outFlowFile = fetchFile(fileId, session, outFlowFile);
-
-            session.transfer(outFlowFile, REL_SUCCESS);
+            final long startNanos = System.nanoTime();
+            flowFile = fetchFile(fileId, session, flowFile);
+            final String boxUrlOfFile = BoxFileUtils.BOX_URL + fileId;
+            final long transferMillis = 
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
+            session.getProvenanceReporter().fetch(flowFile, boxUrlOfFile, 
transferMillis);
+            session.transfer(flowFile, REL_SUCCESS);
         } catch (BoxAPIResponseException e) {
             handleErrorResponse(session, fileId, flowFile, e);
         } catch (Exception e) {
@@ -126,28 +148,31 @@ public class FetchBoxFile extends AbstractProcessor {
         }
     }
 
-    FlowFile fetchFile(String fileId, ProcessSession session, FlowFile 
outFlowFile) {
-        BoxFile boxFile = new BoxFile(boxAPIConnection, fileId);
-
-        outFlowFile = session.write(outFlowFile, outputStream -> 
boxFile.download(outputStream));
-
-        return outFlowFile;
+    BoxFile getBoxFile(String fileId) {
+        return new BoxFile(boxAPIConnection, fileId);
     }
 
-    private void handleErrorResponse(ProcessSession session, String fileId, 
FlowFile outFlowFile, BoxAPIResponseException e) {
-        getLogger().error("Couldn't fetch file with id '{}'", fileId, e);
+    private FlowFile fetchFile(String fileId, ProcessSession session, FlowFile 
flowFile) {
+        final BoxFile boxFile = getBoxFile(fileId);
+        flowFile = session.write(flowFile, outputStream -> 
boxFile.download(outputStream));
+        flowFile = session.putAllAttributes(flowFile, 
BoxFileUtils.createAttributeMap(boxFile.getInfo()));
+        return flowFile;
+    }
 
-        outFlowFile = session.putAttribute(outFlowFile, ERROR_CODE_ATTRIBUTE, 
"" + e.getResponseCode());
-        outFlowFile = session.putAttribute(outFlowFile, 
ERROR_MESSAGE_ATTRIBUTE, e.getMessage());
+    private void handleErrorResponse(ProcessSession session, String fileId, 
FlowFile flowFile, BoxAPIResponseException e) {
+        getLogger().error("Couldn't fetch file with id [{}]", fileId, e);
 
-        session.transfer(outFlowFile, REL_FAILURE);
+        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 handleUnexpectedError(ProcessSession session, FlowFile 
flowFile, String fileId, Exception e) {
-        getLogger().error("Unexpected error while fetching and processing file 
with id '{}'", fileId, e);
-
-        flowFile = session.putAttribute(flowFile, ERROR_MESSAGE_ATTRIBUTE, 
e.getMessage());
+        getLogger().error("Failed fetching and processing file with id [{}]", 
fileId, e);
 
+        flowFile = session.putAttribute(flowFile, ERROR_MESSAGE, 
e.getMessage());
+        flowFile = session.penalize(flowFile);
         session.transfer(flowFile, REL_FAILURE);
     }
 }
diff --git 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/ListBoxFile.java
 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/ListBoxFile.java
index d023af04a2..192c99ab55 100644
--- 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/ListBoxFile.java
+++ 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/ListBoxFile.java
@@ -16,10 +16,28 @@
  */
 package org.apache.nifi.processors.box;
 
+import static org.apache.nifi.processors.box.BoxFileAttributes.FILENAME_DESC;
+import static org.apache.nifi.processors.box.BoxFileAttributes.ID;
+import static org.apache.nifi.processors.box.BoxFileAttributes.ID_DESC;
+import static org.apache.nifi.processors.box.BoxFileAttributes.PATH_DESC;
+import static org.apache.nifi.processors.box.BoxFileAttributes.SIZE;
+import static org.apache.nifi.processors.box.BoxFileAttributes.SIZE_DESC;
+import static org.apache.nifi.processors.box.BoxFileAttributes.TIMESTAMP;
+import static org.apache.nifi.processors.box.BoxFileAttributes.TIMESTAMP_DESC;
+
 import com.box.sdk.BoxAPIConnection;
 import com.box.sdk.BoxFile;
 import com.box.sdk.BoxFolder;
 import com.box.sdk.BoxItem;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
 import org.apache.nifi.annotation.behavior.InputRequirement;
 import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
 import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
@@ -44,18 +62,6 @@ import 
org.apache.nifi.processor.util.list.ListedEntityTracker;
 import org.apache.nifi.scheduling.SchedulingStrategy;
 import org.apache.nifi.serialization.record.RecordSchema;
 
-import java.io.IOException;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
-
 @PrimaryNodeOnly
 @TriggerSerially
 @Tags({"box", "storage"})
@@ -64,13 +70,14 @@ import java.util.stream.Collectors;
     "Or - in case the 'Record Writer' property is set - the entire result is 
written as records to a single FlowFile. " +
     "This Processor is designed to run on Primary Node only in a cluster. If 
the primary node changes, the new Primary Node will pick up where the " +
     "previous node left off without duplicating all of the data.")
-@SeeAlso({FetchBoxFile.class})
+@SeeAlso({FetchBoxFile.class, PutBoxFile.class})
 @InputRequirement(Requirement.INPUT_FORBIDDEN)
-@WritesAttributes({@WritesAttribute(attribute = BoxFileInfo.ID, description = 
"The id of the file"),
-    @WritesAttribute(attribute = BoxFileInfo.FILENAME, description = "The name 
of the file"),
-    @WritesAttribute(attribute = BoxFileInfo.PATH, description = "The path of 
the file on Box"),
-    @WritesAttribute(attribute = BoxFileInfo.SIZE, description = "The size of 
the file (in bytes)"),
-    @WritesAttribute(attribute = BoxFileInfo.TIMESTAMP, description = "The 
last modified time of the file.")})
+@WritesAttributes({
+    @WritesAttribute(attribute = ID, description = ID_DESC),
+    @WritesAttribute(attribute = "filename", description = FILENAME_DESC),
+    @WritesAttribute(attribute = "path", description = PATH_DESC),
+    @WritesAttribute(attribute = SIZE, description = SIZE_DESC),
+    @WritesAttribute(attribute = TIMESTAMP, description = TIMESTAMP_DESC)})
 @Stateful(scopes = {Scope.CLUSTER}, description = "The processor stores 
necessary data to be able to keep track what files have been listed already." +
     " What exactly needs to be stored depends on the 'Listing Strategy'.")
 @DefaultSchedule(strategy = SchedulingStrategy.TIMER_DRIVEN, period = "1 min")
@@ -158,7 +165,7 @@ public class ListBoxFile extends 
AbstractListProcessor<BoxFileInfo> {
     }
 
     @OnScheduled
-    public void onScheduled(final ProcessContext context) throws IOException {
+    public void onScheduled(final ProcessContext context) {
         BoxClientService boxClientService = 
context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
 
         boxAPIConnection = boxClientService.getBoxApiConnection();
@@ -200,8 +207,8 @@ public class ListBoxFile extends 
AbstractListProcessor<BoxFileInfo> {
     protected List<BoxFileInfo> performListing(
         final ProcessContext context,
         final Long minTimestamp,
-        final ListingMode listingMode
-    ) throws IOException {
+        final ListingMode listingMode)  {
+
         final List<BoxFileInfo> listing = new ArrayList<>();
 
         final String folderId = 
context.getProperty(FOLDER_ID).evaluateAttributeExpressions().getValue();
@@ -235,10 +242,7 @@ public class ListBoxFile extends 
AbstractListProcessor<BoxFileInfo> {
                     BoxFileInfo boxFileInfo = new BoxFileInfo.Builder()
                         .id(info.getID())
                         .fileName(info.getName())
-                        .path("/" + info.getPathCollection().stream()
-                            .filter(pathItemInfo -> 
!pathItemInfo.getID().equals("0"))
-                            .map(BoxItem.Info::getName)
-                            .collect(Collectors.joining("/")))
+                        .path(BoxFileUtils.getParentPath(info))
                         .size(info.getSize())
                         .createdTime(info.getCreatedAt().getTime())
                         .modifiedTime(info.getModifiedAt().getTime())
@@ -257,7 +261,7 @@ public class ListBoxFile extends 
AbstractListProcessor<BoxFileInfo> {
     }
 
     @Override
-    protected Integer countUnfilteredListing(final ProcessContext context) 
throws IOException {
+    protected Integer countUnfilteredListing(final ProcessContext context) {
         return performListing(context, null, 
ListingMode.CONFIGURATION_VERIFICATION).size();
     }
 }
diff --git 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/PutBoxFile.java
 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/PutBoxFile.java
new file mode 100644
index 0000000000..13e7609120
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/PutBoxFile.java
@@ -0,0 +1,429 @@
+/*
+ * 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 static java.lang.String.format;
+import static java.lang.String.valueOf;
+import static java.util.Arrays.asList;
+import static 
org.apache.nifi.processor.util.StandardValidators.createRegexMatchingValidator;
+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;
+import static org.apache.nifi.processors.box.BoxFileAttributes.FILENAME_DESC;
+import static org.apache.nifi.processors.box.BoxFileAttributes.ID;
+import static org.apache.nifi.processors.box.BoxFileAttributes.ID_DESC;
+import static org.apache.nifi.processors.box.BoxFileAttributes.PATH_DESC;
+import static org.apache.nifi.processors.box.BoxFileAttributes.SIZE;
+import static org.apache.nifi.processors.box.BoxFileAttributes.SIZE_DESC;
+import static org.apache.nifi.processors.box.BoxFileAttributes.TIMESTAMP;
+import static org.apache.nifi.processors.box.BoxFileAttributes.TIMESTAMP_DESC;
+import static org.apache.nifi.processors.box.BoxFileUtils.BOX_URL;
+import static 
org.apache.nifi.processors.conflict.resolution.ConflictResolutionStrategy.IGNORE;
+import static 
org.apache.nifi.processors.conflict.resolution.ConflictResolutionStrategy.REPLACE;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxAPIException;
+import com.box.sdk.BoxAPIResponseException;
+import com.box.sdk.BoxFile;
+import com.box.sdk.BoxFolder;
+import com.box.sdk.BoxItem;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+import java.util.stream.StreamSupport;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.ReadsAttribute;
+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.processor.AbstractProcessor;
+import org.apache.nifi.processor.DataUnit;
+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.processors.conflict.resolution.ConflictResolutionStrategy;
+
+
+@SeeAlso({ListBoxFile.class, FetchBoxFile.class})
+@InputRequirement(Requirement.INPUT_REQUIRED)
+@Tags({"box", "storage", "put"})
+@CapabilityDescription("Puts content to a Box folder.")
+@ReadsAttribute(attribute = "filename", description = "Uses the FlowFile's 
filename as the filename for the Box object.")
+@WritesAttributes({
+        @WritesAttribute(attribute = ID, description = ID_DESC),
+        @WritesAttribute(attribute = "filename", description = FILENAME_DESC),
+        @WritesAttribute(attribute = "path", description = PATH_DESC),
+        @WritesAttribute(attribute = SIZE, description = SIZE_DESC),
+        @WritesAttribute(attribute = TIMESTAMP, description = TIMESTAMP_DESC),
+        @WritesAttribute(attribute = ERROR_CODE, description = 
ERROR_CODE_DESC),
+        @WritesAttribute(attribute = ERROR_MESSAGE, description = 
ERROR_MESSAGE_DESC)})
+public class PutBoxFile extends AbstractProcessor {
+    public static final int CHUNKED_UPLOAD_LOWER_LIMIT_IN_BYTES = 20 * 1024 * 
1024;
+    public static final int CHUNKED_UPLOAD_UPPER_LIMIT_IN_BYTES = 50 * 1024 * 
1024;
+
+    public static final int NUMBER_OF_RETRIES = 10;
+    public static final int WAIT_TIME_MS = 1000;
+
+    public static final PropertyDescriptor FOLDER_ID = new 
PropertyDescriptor.Builder()
+            .name("box-folder-id")
+            .displayName("Folder ID")
+            .description("The ID of the folder where the file is uploaded." +
+            " Please see Additional Details to obtain Folder ID.")
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+            .required(true)
+            .build();
+
+    public static final PropertyDescriptor FILE_NAME = new 
PropertyDescriptor.Builder()
+            .name("file-name")
+            .displayName("Filename")
+            .description("The name of the file to upload to the specified Box 
folder.")
+            .required(true)
+            .defaultValue("${filename}")
+            
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            .build();
+
+    public static final PropertyDescriptor SUBFOLDER_NAME = new 
PropertyDescriptor.Builder()
+            .name("subfolder-name")
+            .displayName("Subfolder Name")
+            .description("The name (path) of the subfolder where files are 
uploaded. The subfolder name is relative to the folder specified by 'Folder 
ID'."
+                    + " Example: subFolder, subFolder1/subfolder2")
+            
.addValidator(createRegexMatchingValidator(Pattern.compile("^(?!/).+(?<!/)$"), 
false,
+                    "Subfolder Name should not contain leading or trailing 
slash ('/') character."))
+            
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+            .required(false)
+            .build();
+
+    public static final PropertyDescriptor CREATE_SUBFOLDER = new 
PropertyDescriptor.Builder()
+            .name("create-folder")
+            .displayName("Create Subfolder")
+            .expressionLanguageSupported(ExpressionLanguageScope.NONE)
+            .required(true)
+            .addValidator(StandardValidators.BOOLEAN_VALIDATOR)
+            .allowableValues("true", "false")
+            .defaultValue("false")
+            .dependsOn(SUBFOLDER_NAME)
+            .description("Specifies whether to check if the subfolder exists 
and to automatically create it if it does not. " +
+                    "Permission to list folders is required. ")
+            .build();
+
+    public static final PropertyDescriptor CONFLICT_RESOLUTION = new 
PropertyDescriptor.Builder()
+            .name("conflict-resolution-strategy")
+            .displayName("Conflict Resolution Strategy")
+            .description("Indicates what should happen when a file with the 
same name already exists in the specified Box folder.")
+            .required(true)
+            .defaultValue(ConflictResolutionStrategy.FAIL.getValue())
+            .allowableValues(ConflictResolutionStrategy.class)
+            .build();
+
+    public static final PropertyDescriptor CHUNKED_UPLOAD_THRESHOLD = new 
PropertyDescriptor.Builder()
+            .name("chunked-upload-threshold")
+            .displayName("Chunked Upload Threshold")
+            .description("The maximum size of the content which is uploaded at 
once. FlowFiles larger than this threshold are uploaded in chunks."
+                    + " Chunked upload is allowed for files larger than 20 MB. 
It is recommended to use chunked upload for files exceeding 50 MB.")
+            .defaultValue("20 MB")
+            
.addValidator(StandardValidators.createDataSizeBoundsValidator(CHUNKED_UPLOAD_LOWER_LIMIT_IN_BYTES,
 CHUNKED_UPLOAD_UPPER_LIMIT_IN_BYTES))
+            .required(false)
+            .build();
+
+    public static final List<PropertyDescriptor> PROPERTIES = 
Collections.unmodifiableList(asList(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            FOLDER_ID,
+            SUBFOLDER_NAME,
+            CREATE_SUBFOLDER,
+            FILE_NAME,
+            CONFLICT_RESOLUTION,
+            CHUNKED_UPLOAD_THRESHOLD
+    ));
+
+    public static final Relationship REL_SUCCESS =
+            new Relationship.Builder()
+                    .name("success")
+                    .description("Files that have been successfully written to 
Box are transferred to this relationship.")
+                    .build();
+
+    public static final Relationship REL_FAILURE =
+            new Relationship.Builder()
+                    .name("failure")
+                    .description("Files that could not be written to Box for 
some reason are transferred to this relationship.")
+                    .build();
+
+    public static final Set<Relationship> RELATIONSHIPS = 
Collections.unmodifiableSet(new HashSet<>(asList(
+            REL_SUCCESS,
+            REL_FAILURE
+    )));
+
+    private static final int CONFLICT_RESPONSE_CODE = 409;
+    private static final int NOT_FOUND_RESPONSE_CODE = 404;
+
+    private volatile BoxAPIConnection boxAPIConnection;
+
+    @Override
+    public Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @Override
+    public List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTIES;
+    }
+
+    @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 filename = 
context.getProperty(FILE_NAME).evaluateAttributeExpressions(flowFile).getValue();
+        final long chunkUploadThreshold = 
context.getProperty(CHUNKED_UPLOAD_THRESHOLD)
+                .asDataSize(DataUnit.B)
+                .longValue();
+        final ConflictResolutionStrategy conflictResolution = 
ConflictResolutionStrategy.forValue(context.getProperty(CONFLICT_RESOLUTION).getValue());
+
+        final long startNanos = System.nanoTime();
+        String fullPath = null;
+
+        try {
+            final long size = flowFile.getSize();
+            final BoxFolder parentFolder = 
getOrCreateDirectParentFolder(context, flowFile);
+            fullPath = BoxFileUtils.getFolderPath(parentFolder.getInfo());
+            BoxFile.Info uploadedFileInfo = null;
+
+            try (InputStream rawIn = session.read(flowFile)){
+
+                if (REPLACE.equals(conflictResolution)) {
+                    uploadedFileInfo = replaceBoxFileIfExists(parentFolder, 
filename, rawIn, size, chunkUploadThreshold);
+                }
+
+                if (uploadedFileInfo == null) {
+                   uploadedFileInfo = createBoxFile(parentFolder, filename, 
rawIn, size, chunkUploadThreshold);
+                }
+            } catch (BoxAPIResponseException e) {
+                if (e.getResponseCode() == CONFLICT_RESPONSE_CODE) {
+                    handleConflict(conflictResolution, filename, fullPath, e);
+                } else {
+                    throw e;
+                }
+            }
+
+            if (uploadedFileInfo != null) {
+                final Map<String, String> attributes = 
BoxFileUtils.createAttributeMap(uploadedFileInfo);
+                final String url = BOX_URL + uploadedFileInfo.getID();
+                flowFile = session.putAllAttributes(flowFile, attributes);
+                final long transferMillis = 
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
+                session.getProvenanceReporter().send(flowFile, url, 
transferMillis);
+            }
+
+            session.transfer(flowFile, REL_SUCCESS);
+        } catch (BoxAPIResponseException e) {
+            getLogger().error("Upload failed: File [{}] Folder [{}] Response 
Code [{}]", filename, fullPath, e.getResponseCode(), e);
+            handleExpectedError(session, flowFile, e);
+        } catch (Exception e) {
+            getLogger().error("Upload failed: File [{}], Folder [{}]", 
filename, fullPath, e);
+            handleUnexpectedError(session, flowFile, e);
+        }
+    }
+
+    BoxFolder getFolder(String folderId) {
+        return new BoxFolder(boxAPIConnection, folderId);
+    }
+
+    private BoxFolder getOrCreateDirectParentFolder(ProcessContext context, 
FlowFile flowFile ) {
+        final String subfolderPath = 
context.getProperty(SUBFOLDER_NAME).evaluateAttributeExpressions(flowFile).getValue();
+        final boolean createFolder = 
context.getProperty(CREATE_SUBFOLDER).asBoolean();
+        final String folderId = 
context.getProperty(FOLDER_ID).evaluateAttributeExpressions(flowFile).getValue();
+        BoxFolder parentFolder = getFolderById(folderId);
+
+        if (subfolderPath != null) {
+            final Queue<String> subFolderNames = 
getSubFolderNames(subfolderPath);
+            parentFolder = getOrCreateSubfolders(subFolderNames, parentFolder, 
createFolder);
+        }
+
+        return parentFolder;
+    }
+
+    private BoxFile.Info replaceBoxFileIfExists(BoxFolder parentFolder, String 
filename, final InputStream inputStream, final long size, final long 
chunkUploadThreshold)
+            throws IOException, InterruptedException {
+        final Optional<BoxFile> existingBoxFileInfo = getFileByName(filename, 
parentFolder);
+        if (existingBoxFileInfo.isPresent()) {
+            final BoxFile existingBoxFile = existingBoxFileInfo.get();
+
+            if (size > chunkUploadThreshold) {
+                return existingBoxFile.uploadLargeFile(inputStream, size);
+            } else {
+                return existingBoxFile.uploadNewVersion(inputStream);
+            }
+        }
+        return null;
+    }
+
+    private BoxFile.Info createBoxFile(BoxFolder parentFolder, String 
filename, InputStream inputStream, long size, final long chunkUploadThreshold)
+            throws IOException, InterruptedException {
+        if (size > chunkUploadThreshold) {
+            return parentFolder.uploadLargeFile(inputStream, filename, size);
+        } else {
+            return parentFolder.uploadFile(inputStream, filename);
+        }
+    }
+
+    private Queue<String> getSubFolderNames(String subfolderPath)  {
+        final Queue<String> subfolderNames = new LinkedList<>();
+        Collections.addAll(subfolderNames, subfolderPath.split("/"));
+        return subfolderNames;
+    }
+
+    private BoxFolder getOrCreateSubfolders(Queue<String> subFolderNames, 
BoxFolder parentFolder, boolean createFolder) {
+        final BoxFolder newParentFolder = 
getOrCreateFolder(subFolderNames.poll(), parentFolder, createFolder);
+
+        if (!subFolderNames.isEmpty()) {
+           return getOrCreateSubfolders(subFolderNames, newParentFolder, 
createFolder);
+        } else {
+            return newParentFolder;
+        }
+    }
+
+    private BoxFolder getOrCreateFolder(String folderName, BoxFolder 
parentFolder, boolean createFolder) {
+        final Optional<BoxFolder> existingFolder = getFolderByName(folderName, 
parentFolder);
+
+        if (existingFolder.isPresent()) {
+            return existingFolder.get();
+        }
+
+        if (!createFolder) {
+           throw new ProcessException(format("The specified subfolder [%s] 
does not exist and [%s] is false.",
+                   folderName, CREATE_SUBFOLDER.getDisplayName()));
+        }
+
+        return createFolder(folderName, parentFolder);
+    }
+
+    private BoxFolder createFolder(final String folderName, final BoxFolder 
parentFolder) {
+        getLogger().info("Creating Folder [{}], Parent [{}]", folderName, 
parentFolder.getID());
+
+        try {
+           return parentFolder.createFolder(folderName).getResource();
+        } catch (BoxAPIResponseException e) {
+            if (e.getResponseCode() != CONFLICT_RESPONSE_CODE) {
+                throw e;
+            } else {
+                Optional<BoxFolder> createdFolder = 
waitForOngoingFolderCreationToFinish(folderName, parentFolder);
+                return createdFolder.orElseThrow(() -> new 
ProcessException(format("Created subfolder [%s] can not be found under [%s]",
+                        folderName, parentFolder.getID())));
+            }
+        }
+    }
+
+    private Optional<BoxFolder> waitForOngoingFolderCreationToFinish(final 
String folderName, final BoxFolder parentFolder) {
+        try {
+            Optional<BoxFolder> createdFolder = getFolderByName(folderName, 
parentFolder);
+
+            for (int i = 0; i < NUMBER_OF_RETRIES && 
!createdFolder.isPresent(); i++) {
+                getLogger().debug("Subfolder [{}] under [{}] has not been 
created yet, waiting {} ms",
+                        folderName, parentFolder.getID(), WAIT_TIME_MS);
+                Thread.sleep(WAIT_TIME_MS);
+                createdFolder = getFolderByName(folderName, parentFolder);
+            }
+            return createdFolder;
+        } catch (InterruptedException ie) {
+            throw new RuntimeException(format("Waiting for creation of 
subfolder [%s] under [%s] was interrupted",
+                    folderName, parentFolder.getID()), ie);
+        }
+    }
+
+    private BoxFolder getFolderById(final String folderId) {
+        final BoxFolder folder = getFolder(folderId);
+        try {
+            //Error is returned for nonexistent folder only when a method is 
called on BoxFolder.
+            folder.getInfo();
+        } catch (BoxAPIResponseException e) {
+            if (e.getResponseCode() == NOT_FOUND_RESPONSE_CODE) {
+                throw new ProcessException(format("The Folder [%s] specified 
by [%s] does not exist", folderId, FOLDER_ID.getDisplayName()));
+            }
+        }
+        return folder;
+    }
+
+    private Optional<BoxFolder> getFolderByName(final String folderName, final 
BoxFolder parentFolder) {
+        return getItemByName(folderName, parentFolder, BoxFolder.Info.class)
+                .map(BoxFolder.Info::getResource);
+    }
+
+    private Optional<BoxFile> getFileByName(final String filename, final 
BoxFolder parentFolder) {
+        return getItemByName(filename, parentFolder, BoxFile.Info.class)
+                .map(BoxFile.Info::getResource);
+    }
+
+    private <T extends BoxItem.Info> Optional<T> getItemByName(final String 
itemName, final BoxFolder parentFolder, Class<T> type) {
+        return 
StreamSupport.stream(parentFolder.getChildren("name").spliterator(), false)
+                .filter(type::isInstance)
+                .map(type::cast)
+                .filter(info -> info.getName().equals(itemName))
+                .findAny();
+    }
+
+    private void handleConflict(final ConflictResolutionStrategy 
conflictResolution, final String filename, String path, final BoxAPIException 
e) {
+        if (conflictResolution == IGNORE) {
+            getLogger().info("File with the same name [{}] already exists in 
[{}]. Remote file is not modified due to [{}] being set to [{}]",
+                    filename, path, CONFLICT_RESOLUTION.getDisplayName(), 
conflictResolution.getDisplayName());
+        } else {
+            throw new ProcessException(format("File with the same name [%s] 
already exists in [%s]", filename, path), e);
+        }
+    }
+
+    private void handleUnexpectedError(final ProcessSession session, FlowFile 
flowFile, final Exception e) {
+        flowFile = session.putAttribute(flowFile, 
BoxFileAttributes.ERROR_MESSAGE, e.getMessage());
+        flowFile = session.penalize(flowFile);
+        session.transfer(flowFile, REL_FAILURE);
+    }
+
+    private void handleExpectedError(final ProcessSession session, FlowFile 
flowFile, final BoxAPIResponseException e) {
+        flowFile = session.putAttribute(flowFile, 
BoxFileAttributes.ERROR_MESSAGE, e.getMessage());
+        flowFile = session.putAttribute(flowFile, 
BoxFileAttributes.ERROR_CODE, valueOf(e.getResponseCode()));
+        flowFile = session.penalize(flowFile);
+        session.transfer(flowFile, REL_FAILURE);
+    }
+}
\ No newline at end of file
diff --git 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
index 09755f9be5..4bde2b7133 100644
--- 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
+++ 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
@@ -14,3 +14,4 @@
 # limitations under the License.
 org.apache.nifi.processors.box.ListBoxFile
 org.apache.nifi.processors.box.FetchBoxFile
+org.apache.nifi.processors.box.PutBoxFile
diff --git 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/resources/docs/org.apache.nifi.processors.box.FetchBoxFile/additionalDetails.html
 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/resources/docs/org.apache.nifi.processors.box.FetchBoxFile/additionalDetails.html
new file mode 100644
index 0000000000..1b6c6916c2
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/resources/docs/org.apache.nifi.processors.box.FetchBoxFile/additionalDetails.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html lang="en" xmlns="http://www.w3.org/1999/html";>
+<!--
+      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.
+    -->
+
+<head>
+    <meta charset="utf-8"/>
+    <title>FetchBoxFile</title>
+    <link rel="stylesheet" href="../../../../../css/component-usage.css" 
type="text/css"/>
+</head>
+<body>
+
+<h1>Fetch Box files in NiFi</h1>
+
+<ol>
+    <li><b>Find File ID</b>
+        </br>
+        Usually FetchBoxFile is used with ListBoxFile and 'box.id' is 
set.</br></br>
+        In case 'box.id' is not available, you can find the ID of the file in 
the following way:
+        </br>
+        <ul>
+            <li>Click on the file.</li>
+            <li>The URL in the browser will include the File ID.
+            </br>For example, if the URL were 
<code>https://app.box.com/file/1012106094023?s=ldiqjwuor2vwdxeeap2rtcz66dql89h3</code>,</br>
+                the File ID would be <code>1012106094023</code>
+            </li>
+        </ul>
+    </li>
+    <li><b>Set File ID in 'File ID' property</b>
+    </li>
+</ol>
+
+</body>
+</html>
\ No newline at end of file
diff --git 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/resources/docs/org.apache.nifi.processors.box.ListBoxFile/additionalDetails.html
 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/resources/docs/org.apache.nifi.processors.box.ListBoxFile/additionalDetails.html
new file mode 100644
index 0000000000..ae29849671
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/resources/docs/org.apache.nifi.processors.box.ListBoxFile/additionalDetails.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html lang="en" xmlns="http://www.w3.org/1999/html";>
+<!--
+      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.
+    -->
+
+<head>
+    <meta charset="utf-8"/>
+    <title>ListBoxFile</title>
+    <link rel="stylesheet" href="../../../../../css/component-usage.css" 
type="text/css"/>
+</head>
+<body>
+
+<h1>List Box folders in NiFi</h1>
+
+
+<ol>
+    <li><b>Find Folder ID</b>
+        <ul>
+            <li>Navigate to the folder to be listed in Box and enter it. The 
URL in your browser will include the ID at the end of
+                the URL.
+                For example, if the URL were 
<code>https://app.box.com/folder/191632099757</code>, the
+                Folder ID would be <code>191632099757</code>
+            </li>
+        </ul>
+    </li>
+    <li><b>Set Folder ID in 'Folder ID' property</b>
+    </li>
+</ol>
+
+</body>
+</html>
diff --git 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/resources/docs/org.apache.nifi.processors.box.PutBoxFile/additionalDetails.html
 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/resources/docs/org.apache.nifi.processors.box.PutBoxFile/additionalDetails.html
new file mode 100644
index 0000000000..32035e3950
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/resources/docs/org.apache.nifi.processors.box.PutBoxFile/additionalDetails.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html lang="en" xmlns="http://www.w3.org/1999/html";>
+<!--
+      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.
+    -->
+
+<head>
+    <meta charset="utf-8"/>
+    <title>PutBoxFile</title>
+    <link rel="stylesheet" href="../../../../../css/component-usage.css" 
type="text/css"/>
+</head>
+<body>
+
+<h1>Upload files to Box from NiFi</h1>
+
+<ol>
+    <li><b>Find Folder ID</b>
+        <ul>
+            <li>Navigate to the folder in Box and enter it. The URL in your 
browser will include the ID at the end of
+                the URL.
+                For example, if the URL were 
<code>https://app.box.com/folder/191632099757</code>, the
+                Folder ID would be <code>191632099757</code>
+            </li>
+        </ul>
+    </li>
+    <li><b>Set Folder ID in 'Folder ID' property</b>
+    </li>
+</ol>
+
+</body>
+</html>
diff --git 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/AbstractBoxFileIT.java
 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/AbstractBoxFileIT.java
index 8c0c94f567..053083f5c8 100644
--- 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/AbstractBoxFileIT.java
+++ 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/AbstractBoxFileIT.java
@@ -16,28 +16,27 @@
  */
 package org.apache.nifi.processors.box;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
 import com.box.sdk.BoxAPIConnection;
 import com.box.sdk.BoxConfig;
 import com.box.sdk.BoxDeveloperEditionAPIConnection;
 import com.box.sdk.BoxFile;
 import com.box.sdk.BoxFolder;
-import org.apache.nifi.box.controllerservices.BoxClientService;
-import org.apache.nifi.processor.Processor;
-import org.apache.nifi.util.TestRunner;
-import org.apache.nifi.util.TestRunners;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-
 import java.io.ByteArrayInputStream;
 import java.io.FileReader;
 import java.io.Reader;
-import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.Set;
 import java.util.stream.Collectors;
-
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.processor.Processor;
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
 
 /**
  * Set the following constants before running:<br />
@@ -54,7 +53,9 @@ public abstract class AbstractBoxFileIT<T extends Processor> {
     static final String ACCOUNT_ID = "";
     static final String APP_CONFIG_FILE = "";
 
-    protected static final String DEFAULT_FILE_CONTENT = "test_content";
+    public static final String DEFAULT_FILE_CONTENT = "test_content";
+    public static final String CHANGED_FILE_CONTENT = "changed_test_content";
+    public static final String TEST_FILENAME = "test_filename.txt";
     public static final String MAIN_FOLDER_NAME = "main";
 
     protected T testSubject;
@@ -111,18 +112,17 @@ public abstract class AbstractBoxFileIT<T extends 
Processor> {
     protected BoxFolder.Info createFolder(String folderName, String 
parentFolderId) {
         BoxFolder parentFolder = new BoxFolder(boxAPIConnection, 
parentFolderId);
         BoxFolder.Info folderInfo = parentFolder.createFolder(folderName);
-
         return folderInfo;
     }
 
     protected BoxFile.Info createFileWithDefaultContent(String name, String 
folderId) {
-        return createFile(name, DEFAULT_FILE_CONTENT, folderId);
+        return createFile(name, folderId);
     }
 
-    protected BoxFile.Info createFile(String name, String fileContent, String 
folderId) {
+    protected BoxFile.Info createFile(String name, String folderId) {
         BoxFolder folder = new BoxFolder(boxAPIConnection, folderId);
 
-        BoxFile.Info fileInfo = folder.uploadFile(new 
ByteArrayInputStream(fileContent.getBytes(StandardCharsets.UTF_8)), name);
+        BoxFile.Info fileInfo = folder.uploadFile(new 
ByteArrayInputStream(DEFAULT_FILE_CONTENT.getBytes(UTF_8)), name);
 
         return fileInfo;
     }
diff --git 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/AbstractBoxFileTest.java
 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/AbstractBoxFileTest.java
new file mode 100644
index 0000000000..199ac02cf0
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/AbstractBoxFileTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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 static java.lang.String.valueOf;
+import static java.util.Collections.singletonList;
+import static java.util.stream.Collectors.toSet;
+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.when;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxFile;
+import com.box.sdk.BoxFolder;
+import com.box.sdk.BoxFolder.Info;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.provenance.ProvenanceEventRecord;
+import org.apache.nifi.provenance.ProvenanceEventType;
+import org.apache.nifi.util.MockFlowFile;
+import org.apache.nifi.util.TestRunner;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class AbstractBoxFileTest {
+    public final String TEST_FILE_ID = "fileId";
+    public final String TEST_FOLDER_ID = "folderId";
+    public final String TEST_FILENAME = "filename";
+    public final String TEST_FOLDER_NAME = "folderName";
+    public final long TEST_SIZE = 12L;
+    public final long CREATED_TIME = 123456L;
+    public final long MODIFIED_TIME = 7891011L;
+    public final String CONTENT = "content";
+
+    protected TestRunner testRunner;
+
+    @Mock
+    protected BoxFolder mockBoxFolder;
+
+    @Mock
+    protected BoxClientService mockBoxClientService;
+
+    @Mock
+    protected BoxAPIConnection mockBoxAPIConnection;
+
+    @Mock
+    protected BoxFile.Info mockFileInfo;
+
+    @Mock
+    protected BoxFolder.Info mockBoxFolderInfo;
+
+
+    @BeforeEach
+    void setUp() throws Exception {
+        
doReturn(mockBoxClientService.toString()).when(mockBoxClientService).getIdentifier();
+        
doReturn(mockBoxAPIConnection).when(mockBoxClientService).getBoxApiConnection();
+
+        testRunner.addControllerService(mockBoxClientService.getIdentifier(), 
mockBoxClientService);
+        testRunner.enableControllerService(mockBoxClientService);
+        testRunner.setProperty(BoxClientService.BOX_CLIENT_SERVICE, 
mockBoxClientService.getIdentifier());
+    }
+
+    protected BoxFile.Info createFileInfo(String path, Long createdTime) {
+        return createFileInfo(path, createdTime, 
singletonList(mockBoxFolderInfo));
+    }
+
+    protected BoxFile.Info createFileInfo(String path, Long createdTime, 
List<Info> pathCollection) {
+        when(mockBoxFolderInfo.getName()).thenReturn(path);
+        when(mockBoxFolderInfo.getID()).thenReturn("not0");
+
+        when(mockFileInfo.getID()).thenReturn(TEST_FILE_ID);
+        when(mockFileInfo.getName()).thenReturn(TEST_FILENAME);
+        when(mockFileInfo.getPathCollection()).thenReturn(pathCollection);
+        when(mockFileInfo.getSize()).thenReturn(TEST_SIZE);
+        when(mockFileInfo.getModifiedAt()).thenReturn(new Date(createdTime));
+
+        return mockFileInfo;
+    }
+
+    protected void assertProvenanceEvent(ProvenanceEventType eventType) {
+        Set<ProvenanceEventType> expectedEventTypes = 
Collections.singleton(eventType);
+        Set<ProvenanceEventType> actualEventTypes = 
testRunner.getProvenanceEvents().stream()
+                .map(ProvenanceEventRecord::getEventType)
+                .collect(toSet());
+        assertEquals(expectedEventTypes, actualEventTypes);
+    }
+
+    protected void assertNoProvenanceEvent() {
+        assertTrue(testRunner.getProvenanceEvents().isEmpty());
+    }
+
+    protected void assertOutFlowFileAttributes(MockFlowFile flowFile) {
+        assertOutFlowFileAttributes(flowFile, "/" + TEST_FOLDER_NAME);
+    }
+
+    protected void assertOutFlowFileAttributes(MockFlowFile flowFile, String 
path) {
+        flowFile.assertAttributeEquals(BoxFileAttributes.ID, TEST_FILE_ID);
+        flowFile.assertAttributeEquals(CoreAttributes.FILENAME.key(), 
TEST_FILENAME);
+        flowFile.assertAttributeEquals(CoreAttributes.PATH.key(), path);
+        flowFile.assertAttributeEquals(BoxFileAttributes.TIMESTAMP, 
valueOf(new Date(MODIFIED_TIME)));
+        flowFile.assertAttributeEquals(BoxFileAttributes.SIZE, 
valueOf(TEST_SIZE));
+    }
+}
diff --git 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/FetchBoxFileIT.java
 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/FetchBoxFileIT.java
index fb34d46344..a1773f5fbb 100644
--- 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/FetchBoxFileIT.java
+++ 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/FetchBoxFileIT.java
@@ -16,17 +16,18 @@
  */
 package org.apache.nifi.processors.box;
 
-import com.box.sdk.BoxFile;
-import org.apache.nifi.util.MockFlowFile;
-import org.junit.jupiter.api.Test;
+import static java.util.Collections.singletonList;
 
-import java.util.Arrays;
+import com.box.sdk.BoxFile;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.util.MockFlowFile;
+import org.junit.jupiter.api.Test;
 
 /**
  * See Javadoc {@link AbstractBoxFileIT} for instructions how to run this test.
@@ -40,22 +41,19 @@ public class FetchBoxFileIT extends 
AbstractBoxFileIT<FetchBoxFile> {
     }
 
     @Test
-    void testFetchSingleFile() throws Exception {
-        // GIVEN
+    void testFetchSingleFile() {
         BoxFile.Info file = createFileWithDefaultContent("test_file.txt", 
mainFolderId);
 
         Map<String, String> inputFlowFileAttributes = new HashMap<>();
-        inputFlowFileAttributes.put("box.id", file.getID());
-        inputFlowFileAttributes.put("filename", file.getName());
+        inputFlowFileAttributes.put(BoxFileAttributes.ID, file.getID());
+        inputFlowFileAttributes.put(CoreAttributes.FILENAME.key(), 
file.getName());
 
-        HashSet<Map<String, String>> expectedAttributes = new 
HashSet<>(Arrays.asList(inputFlowFileAttributes));
-        List<String> expectedContent = Arrays.asList(DEFAULT_FILE_CONTENT);
+        HashSet<Map<String, String>> expectedAttributes = new 
HashSet<>(singletonList(inputFlowFileAttributes));
+        List<String> expectedContent = singletonList(DEFAULT_FILE_CONTENT);
 
-        // WHEN
         testRunner.enqueue("unimportant_data", inputFlowFileAttributes);
         testRunner.run();
 
-        // THEN
         testRunner.assertTransferCount(FetchBoxFile.REL_FAILURE, 0);
 
         testRunner.assertAttributes(FetchBoxFile.REL_SUCCESS, 
getCheckedAttributeNames(), expectedAttributes);
@@ -63,41 +61,38 @@ public class FetchBoxFileIT extends 
AbstractBoxFileIT<FetchBoxFile> {
     }
 
     @Test
-    void testInputFlowFileReferencesMissingFile() throws Exception {
-        // GIVEN
+    void testInputFlowFileReferencesMissingFile() {
         Map<String, String> inputFlowFileAttributes = new HashMap<>();
-        inputFlowFileAttributes.put("box.id", "111");
-        inputFlowFileAttributes.put("filename", "missing_filename");
+        inputFlowFileAttributes.put(BoxFileAttributes.ID, "111");
+        inputFlowFileAttributes.put(CoreAttributes.FILENAME.key(), 
"missing_filename");
 
-        Set<Map<String, String>> expectedFailureAttributes = new 
HashSet<>(Arrays.asList(
+        Set<Map<String, String>> expectedFailureAttributes = new 
HashSet<>(singletonList(
             new HashMap<String, String>() {{
-                put("box.id", "111");
-                put("filename", "missing_filename");
-                put("error.code", "404");
+                put(BoxFileAttributes.ID, "111");
+                put(CoreAttributes.FILENAME.key(), "missing_filename");
+                put(BoxFileAttributes.ERROR_CODE, "404");
             }}
         ));
 
-        // WHEN
+
         testRunner.enqueue("unimportant_data", inputFlowFileAttributes);
         testRunner.run();
 
-        // THEN
         testRunner.assertTransferCount(FetchBoxFile.REL_SUCCESS, 0);
 
         testRunner.assertAttributes(FetchBoxFile.REL_FAILURE, 
getCheckedAttributeNames(), expectedFailureAttributes);
     }
 
     @Test
-    void testInputFlowFileThrowsExceptionBeforeFetching() throws Exception {
-        // GIVEN
+    void testInputFlowFileThrowsExceptionBeforeFetching() {
         BoxFile.Info file = createFileWithDefaultContent("test_file.txt", 
mainFolderId);
 
         Map<String, String> inputFlowFileAttributes = new HashMap<>();
-        inputFlowFileAttributes.put("box.id", file.getID());
-        inputFlowFileAttributes.put("filename", file.getName());
+        inputFlowFileAttributes.put(BoxFileAttributes.ID, file.getID());
+        inputFlowFileAttributes.put(CoreAttributes.FILENAME.key(), 
file.getName());
 
         MockFlowFile input = new MockFlowFile(1) {
-            AtomicBoolean throwException = new AtomicBoolean(true);
+            final AtomicBoolean throwException = new AtomicBoolean(true);
 
             @Override
             public boolean isPenalized() {
@@ -116,17 +111,15 @@ public class FetchBoxFileIT extends 
AbstractBoxFileIT<FetchBoxFile> {
             }
         };
 
-        Set<Map<String, String>> expectedFailureAttributes = new 
HashSet<>(Arrays.asList(
+        Set<Map<String, String>> expectedFailureAttributes = new 
HashSet<>(singletonList(
             new HashMap<String, String>() {{
                 putAll(inputFlowFileAttributes);
             }}
         ));
 
-        // WHEN
         testRunner.enqueue(input);
         testRunner.run();
 
-        // THEN
         testRunner.assertTransferCount(FetchBoxFile.REL_SUCCESS, 0);
 
         testRunner.assertAttributes(FetchBoxFile.REL_FAILURE, 
getCheckedAttributeNames(), expectedFailureAttributes);
@@ -137,7 +130,7 @@ public class FetchBoxFileIT extends 
AbstractBoxFileIT<FetchBoxFile> {
 
         checkedAttributeNames.add(BoxFlowFileAttribute.ID.getName());
         checkedAttributeNames.add(BoxFlowFileAttribute.FILENAME.getName());
-        checkedAttributeNames.add(FetchBoxFile.ERROR_CODE_ATTRIBUTE);
+        checkedAttributeNames.add(BoxFileAttributes.ERROR_CODE);
 
         return checkedAttributeNames;
     }
diff --git 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/FetchBoxFileTest.java
 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/FetchBoxFileTest.java
new file mode 100644
index 0000000000..7b45eb85c8
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/FetchBoxFileTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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 static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+
+import com.box.sdk.BoxFile;
+import java.io.OutputStream;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.nifi.provenance.ProvenanceEventType;
+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.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class FetchBoxFileTest extends AbstractBoxFileTest{
+    @Mock
+    BoxFile mockBoxFile;
+
+    @BeforeEach
+    void setUp() throws Exception {
+
+        final FetchBoxFile testSubject = new FetchBoxFile() {
+            @Override
+            protected BoxFile getBoxFile(String fileId) {
+                return mockBoxFile;
+            }
+        };
+
+        testRunner = TestRunners.newTestRunner(testSubject);
+        super.setUp();
+    }
+
+    @Test
+    void testBoxIdFromFlowFileAttribute()  {
+        testRunner.setProperty(FetchBoxFile.FILE_ID, "${box.id}");
+        final MockFlowFile inputFlowFile = new MockFlowFile(0);
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put(BoxFileAttributes.ID, TEST_FILE_ID);
+        inputFlowFile.putAttributes(attributes);
+
+        final BoxFile.Info fetchedFileInfo = createFileInfo(TEST_FOLDER_NAME,  
MODIFIED_TIME);
+        doReturn(fetchedFileInfo).when(mockBoxFile).getInfo();
+
+
+        testRunner.enqueue(inputFlowFile);
+        testRunner.run();
+
+
+        testRunner.assertAllFlowFilesTransferred(FetchBoxFile.REL_SUCCESS, 1);
+        final List<MockFlowFile> flowFiles = 
testRunner.getFlowFilesForRelationship(FetchBoxFile.REL_SUCCESS);
+        final MockFlowFile ff0 = flowFiles.get(0);
+        assertOutFlowFileAttributes(ff0);
+        verify(mockBoxFile).download(any(OutputStream.class));
+        assertProvenanceEvent(ProvenanceEventType.FETCH);
+    }
+
+    @Test
+    void testBoxIdFromProperty()  {
+        testRunner.setProperty(FetchBoxFile.FILE_ID, TEST_FILE_ID);
+
+        final BoxFile.Info fetchedFileInfo = createFileInfo(TEST_FOLDER_NAME, 
MODIFIED_TIME);
+        doReturn(fetchedFileInfo).when(mockBoxFile).getInfo();
+
+
+        final MockFlowFile inputFlowFile = new MockFlowFile(0);
+        testRunner.enqueue(inputFlowFile);
+        testRunner.run();
+
+
+        testRunner.assertAllFlowFilesTransferred(FetchBoxFile.REL_SUCCESS, 1);
+        final List<MockFlowFile> flowFiles = 
testRunner.getFlowFilesForRelationship(FetchBoxFile.REL_SUCCESS);
+        final MockFlowFile ff0 = flowFiles.get(0);
+        assertOutFlowFileAttributes(ff0);
+        verify(mockBoxFile).download(any(OutputStream.class));
+        assertProvenanceEvent(ProvenanceEventType.FETCH);
+    }
+
+    @Test
+    void testFileDownloadFailure()  {
+        testRunner.setProperty(FetchBoxFile.FILE_ID, TEST_FILE_ID);
+
+        doThrow(new RuntimeException("Download 
failed")).when(mockBoxFile).download(any(OutputStream.class));
+
+
+        MockFlowFile inputFlowFile = new MockFlowFile(0);
+        testRunner.enqueue(inputFlowFile);
+        testRunner.run();
+
+
+        testRunner.assertAllFlowFilesTransferred(FetchBoxFile.REL_FAILURE, 1);
+        final List<MockFlowFile> flowFiles = 
testRunner.getFlowFilesForRelationship(FetchBoxFile.REL_FAILURE);
+        final MockFlowFile ff0 = flowFiles.get(0);
+        ff0.assertAttributeEquals(BoxFileAttributes.ERROR_MESSAGE, "Download 
failed");
+        assertNoProvenanceEvent();
+    }
+}
diff --git 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/SimpleListBoxFileTestTrait.java
 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/FileListingTestTrait.java
similarity index 86%
rename from 
nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/SimpleListBoxFileTestTrait.java
rename to 
nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/FileListingTestTrait.java
index 64a2474927..003d337b18 100644
--- 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/SimpleListBoxFileTestTrait.java
+++ 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/FileListingTestTrait.java
@@ -16,20 +16,19 @@
  */
 package org.apache.nifi.processors.box;
 
+import static java.util.Collections.singletonList;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
 import com.box.sdk.BoxFile;
 import com.box.sdk.BoxFolder;
-
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Date;
 import java.util.List;
 import java.util.stream.Collectors;
 
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-public interface SimpleListBoxFileTestTrait {
+public interface FileListingTestTrait {
     BoxFolder getMockBoxFolder();
 
     default void mockFetchedFileList(
@@ -40,17 +39,15 @@ public interface SimpleListBoxFileTestTrait {
         Long createdTime,
         Long modifiedTime
     ) {
-        doReturn(
-            Arrays.asList(
-                createFileInfo(
-                    id,
-                    filename,
-                    pathParts,
-                    size,
-                    createdTime,
-                    modifiedTime
+        doReturn(singletonList(createFileInfo(
+                                id,
+                                filename,
+                                pathParts,
+                                size,
+                                createdTime,
+                                modifiedTime
+                        )
                 )
-            )
         ).when(getMockBoxFolder()).getChildren("id",
             "name",
             "item_status",
diff --git 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileIT.java
 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileIT.java
index c0337524d6..0692c731e3 100644
--- 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileIT.java
+++ 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileIT.java
@@ -49,7 +49,6 @@ public class ListBoxFileIT extends 
AbstractBoxFileIT<ListBoxFile> {
 
     @Test
     void listFilesFrom3LayerDeepDirectoryTree() throws Exception {
-        // GIVEN
         BoxFolder.Info main_sub1 = createFolder("main_sub1", mainFolderId);
         BoxFolder.Info main_sub2 = createFolder("main_sub2", mainFolderId);
 
@@ -78,10 +77,8 @@ public class ListBoxFileIT extends 
AbstractBoxFileIT<ListBoxFile> {
         // The creation of the files are not (completely) synchronized.
         Thread.sleep(2000);
 
-        // WHEN
         testRunner.run();
 
-        // THEN
         Set<String> actualFileNames = getActualFileNames();
 
         assertEquals(expectedFileNames, actualFileNames);
@@ -89,19 +86,16 @@ public class ListBoxFileIT extends 
AbstractBoxFileIT<ListBoxFile> {
         // Next, list a sub folder, non-recursively this time. (Changing these 
properties will clear the Processor state as well
         //  so all files are eligible for listing again.)
 
-        // GIVEN
         testRunner.clearTransferState();
 
         expectedFileNames = new HashSet<>(Arrays.asList(
             "main_sub1_file1"
         ));
 
-        // WHEN
         testRunner.setProperty(ListBoxFile.FOLDER_ID, main_sub1.getID());
         testRunner.setProperty(ListBoxFile.RECURSIVE_SEARCH, "false");
         testRunner.run();
 
-        // THEN
         actualFileNames = getActualFileNames();
 
         assertEquals(expectedFileNames, actualFileNames);
@@ -109,7 +103,6 @@ public class ListBoxFileIT extends 
AbstractBoxFileIT<ListBoxFile> {
 
     @Test
     void doNotListTooYoungFilesWhenMinAgeIsSet() throws Exception {
-        // GIVEN
         testRunner.setProperty(ListBoxFile.MIN_AGE, "15 s");
 
         createFileWithDefaultContent("main_file", mainFolderId);
@@ -117,27 +110,22 @@ public class ListBoxFileIT extends 
AbstractBoxFileIT<ListBoxFile> {
         // Make sure the file 'arrives' and could be listed
         Thread.sleep(5000);
 
-        // WHEN
         testRunner.run();
 
-        // THEN
         Set<String> actualFileNames = getActualFileNames();
 
         assertEquals(Collections.emptySet(), actualFileNames);
 
         // Next, wait for another 10+ seconds for MIN_AGE to expire then list 
again
 
-        // GIVEN
         Thread.sleep(10000);
 
         Set<String> expectedFileNames = new HashSet<>(Arrays.asList(
             "main_file"
         ));
 
-        // WHEN
         testRunner.run();
 
-        // THEN
         actualFileNames = getActualFileNames();
 
         assertEquals(expectedFileNames, actualFileNames);
@@ -152,5 +140,4 @@ public class ListBoxFileIT extends 
AbstractBoxFileIT<ListBoxFile> {
 
         return actualFileNames;
     }
-
 }
diff --git 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileSimpleTest.java
 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileListingTest.java
similarity index 84%
rename from 
nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileSimpleTest.java
rename to 
nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileListingTest.java
index 0fb2f7daa1..e58dedc71d 100644
--- 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileSimpleTest.java
+++ 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileListingTest.java
@@ -16,44 +16,44 @@
  */
 package org.apache.nifi.processors.box;
 
+import static java.util.Collections.singletonList;
+import static org.apache.nifi.util.EqualsWrapper.wrapList;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.doReturn;
+
 import com.box.sdk.BoxAPIConnection;
 import com.box.sdk.BoxFolder;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Function;
 import org.apache.nifi.box.controllerservices.BoxClientService;
 import org.apache.nifi.components.PropertyValue;
 import org.apache.nifi.processor.ProcessContext;
 import org.apache.nifi.util.EqualsWrapper;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Answers;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
 
-import java.util.Arrays;
-import java.util.List;
-import java.util.function.Function;
-
-import static org.apache.nifi.util.EqualsWrapper.wrapList;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-
-public class ListBoxFileSimpleTest implements SimpleListBoxFileTestTrait {
+@ExtendWith(MockitoExtension.class)
+public class ListBoxFileListingTest implements FileListingTestTrait {
     private ListBoxFile testSubject;
 
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
     private ProcessContext mockProcessContext;
+    @Mock
     private BoxClientService mockBoxClientService;
+    @Mock
     private PropertyValue mockBoxClientServicePropertyValue;
+    @Mock
     private BoxAPIConnection mockBoxAPIConnection;
-
+    @Mock
     private BoxFolder mockBoxFolder;
 
     @BeforeEach
-    void setUp() throws Exception {
-        mockProcessContext = mock(ProcessContext.class, 
Answers.RETURNS_DEEP_STUBS);
-        mockBoxClientService = mock(BoxClientService.class);
-        mockBoxClientServicePropertyValue = mock(PropertyValue.class);
-        mockBoxAPIConnection = mock(BoxAPIConnection.class);
-
-        mockBoxFolder = mock(BoxFolder.class);
-
+    void setUp() {
         testSubject = new ListBoxFile() {
             @Override
             BoxFolder getFolder(String folderId) {
@@ -69,20 +69,20 @@ public class ListBoxFileSimpleTest implements 
SimpleListBoxFileTestTrait {
     }
 
     @Test
-    void testCreatedListableEntityContainsCorrectData() throws Exception {
-        // GIVEN
+    void testCreatedListableEntityContainsCorrectData() {
+
         Long minTimestamp = 0L;
 
         String id = "id_1";
         String filename = "file_name_1";
         List<String> pathParts = Arrays.asList("path", "to", "file");
-        Long size = 125L;
+        long size = 125L;
         long createdTime = 123456L;
         long modifiedTime = 234567L;
 
         mockFetchedFileList(id, filename, pathParts, size, createdTime, 
modifiedTime);
 
-        List<BoxFileInfo> expected = Arrays.asList(
+        List<BoxFileInfo> expected = singletonList(
             new BoxFileInfo.Builder()
                 .id(id)
                 .fileName(filename)
@@ -93,10 +93,8 @@ public class ListBoxFileSimpleTest implements 
SimpleListBoxFileTestTrait {
                 .build()
         );
 
-        // WHEN
         List<BoxFileInfo> actual = 
testSubject.performListing(mockProcessContext, minTimestamp, null);
 
-        // THEN
         List<Function<BoxFileInfo, Object>> propertyProviders = Arrays.asList(
             BoxFileInfo::getId,
             BoxFileInfo::getIdentifier,
diff --git 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileTest.java
 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileTest.java
new file mode 100644
index 0000000000..fed2265d6f
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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 static java.lang.String.valueOf;
+import static java.util.Collections.singletonList;
+import static org.apache.nifi.processors.box.BoxFileAttributes.ID;
+import static org.apache.nifi.processors.box.BoxFileAttributes.SIZE;
+import static org.apache.nifi.processors.box.BoxFileAttributes.TIMESTAMP;
+
+import com.box.sdk.BoxFolder;
+import java.util.Arrays;
+import java.util.List;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.json.JsonRecordSetWriter;
+import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.serialization.RecordSetWriterFactory;
+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;
+
+@ExtendWith(MockitoExtension.class)
+public class ListBoxFileTest extends AbstractBoxFileTest implements 
FileListingTestTrait {
+
+    @BeforeEach
+    void setUp() throws Exception {
+
+        final ListBoxFile testSubject = new ListBoxFile() {
+            @Override
+            BoxFolder getFolder(String folderId) {
+                return mockBoxFolder;
+            }
+        };
+
+        testRunner = TestRunners.newTestRunner(testSubject);
+        super.setUp();
+        testRunner.setProperty(ListBoxFile.FOLDER_ID, TEST_FOLDER_ID);
+    }
+
+    @Test
+    void testOutputAsAttributesWhereTimestampIsModifiedTime()  {
+        final List<String> pathParts = Arrays.asList("path", "to", "file");
+        mockFetchedFileList(TEST_FILE_ID, TEST_FILENAME, pathParts, TEST_SIZE, 
CREATED_TIME, MODIFIED_TIME);
+
+        testRunner.run();
+
+        testRunner.assertAllFlowFilesTransferred(ListBoxFile.REL_SUCCESS);
+        final MockFlowFile ff0 = 
testRunner.getFlowFilesForRelationship(ListBoxFile.REL_SUCCESS).get(0);
+
+        ff0.assertAttributeEquals(ID, TEST_FILE_ID);
+        ff0.assertAttributeEquals(CoreAttributes.FILENAME.key(), 
TEST_FILENAME);
+        ff0.assertAttributeEquals(CoreAttributes.PATH.key(), "/path/to/file");
+        ff0.assertAttributeEquals(SIZE, valueOf(TEST_SIZE));
+        ff0.assertAttributeEquals(TIMESTAMP, valueOf(MODIFIED_TIME));
+    }
+
+    @Test
+    void testOutputAsContent() throws Exception {
+        final List<String> pathParts = Arrays.asList("path", "to", "file");
+
+        addJsonRecordWriterFactory();
+
+        mockFetchedFileList(TEST_FILE_ID, TEST_FILENAME, pathParts, TEST_SIZE, 
CREATED_TIME, MODIFIED_TIME);
+
+        final List<String> expectedContents = singletonList(
+                "[" +
+                        "{" +
+                        "\"box.id\":\"" + TEST_FILE_ID + "\"," +
+                        "\"filename\":\"" + TEST_FILENAME + "\"," +
+                        "\"path\":\"/path/to/file\"," +
+                        "\"box.size\":" + TEST_SIZE + "," +
+                        "\"box.timestamp\":" + MODIFIED_TIME +
+                        "}" +
+                        "]");
+
+
+        testRunner.run();
+
+        testRunner.assertContents(ListBoxFile.REL_SUCCESS, expectedContents);
+    }
+
+    private void addJsonRecordWriterFactory() throws InitializationException {
+        final RecordSetWriterFactory recordSetWriter = new 
JsonRecordSetWriter();
+        testRunner.addControllerService("record_writer", recordSetWriter);
+        testRunner.enableControllerService(recordSetWriter);
+        testRunner.setProperty(ListBoxFile.RECORD_WRITER, "record_writer");
+    }
+
+    @Override
+    public BoxFolder getMockBoxFolder() {
+        return mockBoxFolder;
+    }
+}
diff --git 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileTestRunnerTest.java
 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileTestRunnerTest.java
deleted file mode 100644
index bfab87c957..0000000000
--- 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileTestRunnerTest.java
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * 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.BoxFolder;
-import org.apache.nifi.box.controllerservices.BoxClientService;
-import org.apache.nifi.json.JsonRecordSetWriter;
-import org.apache.nifi.reporting.InitializationException;
-import org.apache.nifi.serialization.RecordSetWriterFactory;
-import org.apache.nifi.util.TestRunner;
-import org.apache.nifi.util.TestRunners;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-
-public class ListBoxFileTestRunnerTest implements SimpleListBoxFileTestTrait {
-    private ListBoxFile testSubject;
-    private TestRunner testRunner;
-
-    private BoxAPIConnection mockBoxAPIConnection;
-
-    private BoxFolder mockBoxFolder;
-
-    private String folderId = "folderId";
-
-    @BeforeEach
-    void setUp() throws Exception {
-        mockBoxAPIConnection = mock(BoxAPIConnection.class);
-        mockBoxFolder = mock(BoxFolder.class);
-
-        testSubject = new ListBoxFile() {
-            @Override
-            BoxFolder getFolder(String folderId) {
-                return mockBoxFolder;
-            }
-        };
-
-        testRunner = TestRunners.newTestRunner(testSubject);
-        testRunner.setProperty(ListBoxFile.FOLDER_ID, folderId);
-
-        BoxClientService boxClientService = mock(BoxClientService.class);
-        
doReturn(boxClientService.toString()).when(boxClientService).getIdentifier();
-        
doReturn(mockBoxAPIConnection).when(boxClientService).getBoxApiConnection();
-
-        testRunner.addControllerService(boxClientService.getIdentifier(), 
boxClientService);
-        testRunner.enableControllerService(boxClientService);
-        testRunner.setProperty(BoxClientService.BOX_CLIENT_SERVICE, 
boxClientService.getIdentifier());
-    }
-
-    @Test
-    void testOutputAsAttributesWhereTimestampIsModifiedTime() throws Exception 
{
-        // GIVEN
-        String id = "id_1";
-        String filename = "file_name_1";
-        List<String> pathParts = Arrays.asList("path", "to", "file");
-        Long size = 125L;
-        Long createdTime = 123456L;
-        Long modifiedTime = 123456L + 1L;
-
-        // WHEN
-        // THEN
-        testOutputAsAttributes(
-            id,
-            filename,
-            pathParts,
-            size,
-            createdTime,
-            modifiedTime,
-            modifiedTime,
-            "/path/to/file"
-        );
-    }
-
-    @Test
-    void testOutputAsContent() throws Exception {
-        // GIVEN
-        String id = "id_1";
-        String filename = "file_name_1";
-        List<String> pathParts = Arrays.asList("path", "to", "file");
-        Long size = 125L;
-        Long createdTime = 123456L;
-        Long modifiedTime = 123456L + 1L;
-
-        addJsonRecordWriterFactory();
-
-        mockFetchedFileList(id, filename, pathParts, size, createdTime, 
modifiedTime);
-
-        List<String> expectedContents = Arrays.asList(
-            "[" +
-                "{" +
-                "\"box.id\":\"" + id + "\"," +
-                "\"filename\":\"" + filename + "\"," +
-                "\"path\":\"/path/to/file\"," +
-                "\"box.size\":" + size + "," +
-                "\"box.timestamp\":" + modifiedTime +
-                "}" +
-                "]");
-
-        // WHEN
-        testRunner.run();
-
-        // THEN
-        testRunner.assertContents(ListBoxFile.REL_SUCCESS, expectedContents);
-    }
-
-    private void addJsonRecordWriterFactory() throws InitializationException {
-        RecordSetWriterFactory recordSetWriter = new JsonRecordSetWriter();
-        testRunner.addControllerService("record_writer", recordSetWriter);
-        testRunner.enableControllerService(recordSetWriter);
-        testRunner.setProperty(ListBoxFile.RECORD_WRITER, "record_writer");
-    }
-
-    private void testOutputAsAttributes(
-        String id,
-        String filename,
-        Collection<String> pathParts,
-        Long size,
-        Long createdTime,
-        Long modifiedTime,
-        Long expectedTimestamp,
-        String expectedPath
-    ) {
-        // GIVEN
-        mockFetchedFileList(id, filename, pathParts, size, createdTime, 
modifiedTime);
-
-        Map<String, String> inputFlowFileAttributes = new HashMap<>();
-        inputFlowFileAttributes.put("box.id", id);
-        inputFlowFileAttributes.put("filename", filename);
-        inputFlowFileAttributes.put("path", expectedPath);
-        inputFlowFileAttributes.put("box.size", "" + size);
-        inputFlowFileAttributes.put("box.timestamp", "" + expectedTimestamp);
-
-        HashSet<Map<String, String>> expectedAttributes = new 
HashSet<>(Arrays.asList(inputFlowFileAttributes));
-
-        // WHEN
-        testRunner.run();
-
-        // THEN
-        testRunner.assertAttributes(ListBoxFile.REL_SUCCESS, 
getCheckedAttributeNames(), expectedAttributes);
-    }
-
-    private Set<String> getCheckedAttributeNames() {
-        Set<String> checkedAttributeNames = 
Arrays.stream(BoxFlowFileAttribute.values())
-            .map(BoxFlowFileAttribute::getName)
-            .collect(Collectors.toSet());
-
-        return checkedAttributeNames;
-    }
-
-    @Override
-    public BoxFolder getMockBoxFolder() {
-        return mockBoxFolder;
-    }
-}
diff --git 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/PutBoxFileIT.java
 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/PutBoxFileIT.java
new file mode 100644
index 0000000000..1eea15dbf1
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/PutBoxFileIT.java
@@ -0,0 +1,120 @@
+/*
+ * 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 
org.apache.nifi.processors.conflict.resolution.ConflictResolutionStrategy;
+import org.junit.jupiter.api.Test;
+
+/**
+ * See Javadoc {@link AbstractBoxFileIT} for instructions how to run this test.
+ */
+public class PutBoxFileIT extends AbstractBoxFileIT<PutBoxFile>{
+
+    @Override
+    public PutBoxFile createTestSubject() {
+        return new PutBoxFile();
+    }
+
+    @Test
+    void testUploadFile() {
+        testRunner.setProperty(PutBoxFile.FOLDER_ID, mainFolderId);
+        testRunner.setProperty(PutBoxFile.FILE_NAME, TEST_FILENAME);
+        testRunner.enqueue(DEFAULT_FILE_CONTENT);
+        testRunner.run();
+        testRunner.assertTransferCount(FetchBoxFile.REL_SUCCESS, 1);
+        testRunner.assertTransferCount(FetchBoxFile.REL_FAILURE, 0);
+    }
+
+    @Test
+    void testSubfoldersAreCreated()  {
+        testRunner.setProperty(PutBoxFile.FOLDER_ID, mainFolderId);
+        testRunner.setProperty(PutBoxFile.SUBFOLDER_NAME, "sub1/sub2/sub3");
+        testRunner.setProperty(PutBoxFile.CREATE_SUBFOLDER, "true");
+        testRunner.setProperty(PutBoxFile.FILE_NAME, TEST_FILENAME);
+        testRunner.enqueue(DEFAULT_FILE_CONTENT);
+        testRunner.run();
+        testRunner.assertTransferCount(FetchBoxFile.REL_SUCCESS, 1);
+        testRunner.assertTransferCount(FetchBoxFile.REL_FAILURE, 0);
+    }
+
+    @Test
+    void testSubfolderExists()  {
+        createFolder("sub1", mainFolderId);
+
+        testRunner.setProperty(PutBoxFile.FOLDER_ID, mainFolderId);
+        testRunner.setProperty(PutBoxFile.SUBFOLDER_NAME, "sub1/sub2/sub3");
+        testRunner.setProperty(PutBoxFile.CREATE_SUBFOLDER, "true");
+        testRunner.setProperty(PutBoxFile.FILE_NAME, TEST_FILENAME);
+        testRunner.enqueue(DEFAULT_FILE_CONTENT);
+        testRunner.run();
+        testRunner.assertTransferCount(FetchBoxFile.REL_SUCCESS, 1);
+        testRunner.assertTransferCount(FetchBoxFile.REL_FAILURE, 0);
+    }
+
+    @Test
+    void testUploadExistingFileFailResolution()  {
+        testRunner.setProperty(PutBoxFile.FOLDER_ID, mainFolderId);
+        testRunner.setProperty(PutBoxFile.FILE_NAME, TEST_FILENAME);
+        testRunner.enqueue(DEFAULT_FILE_CONTENT);
+        testRunner.run();
+        testRunner.assertTransferCount(PutBoxFile.REL_SUCCESS, 1);
+        testRunner.assertTransferCount(PutBoxFile.REL_FAILURE, 0);
+        testRunner.clearTransferState();
+        testRunner.enqueue(CHANGED_FILE_CONTENT);
+        testRunner.run();
+        testRunner.assertTransferCount(PutBoxFile.REL_SUCCESS, 0);
+        testRunner.assertTransferCount(PutBoxFile.REL_FAILURE, 1);
+    }
+
+    @Test
+    void testUploadExistingFileIgnoreResolution()  {
+        testRunner.setProperty(PutBoxFile.FOLDER_ID, mainFolderId);
+        testRunner.setProperty(PutBoxFile.FILE_NAME, TEST_FILENAME);
+        testRunner.setProperty(PutBoxFile.CONFLICT_RESOLUTION, 
ConflictResolutionStrategy.IGNORE.getValue());
+
+        testRunner.enqueue(DEFAULT_FILE_CONTENT);
+        testRunner.run();
+
+        testRunner.assertTransferCount(PutBoxFile.REL_SUCCESS, 1);
+        testRunner.assertTransferCount(PutBoxFile.REL_FAILURE, 0);
+        testRunner.clearTransferState();
+
+        testRunner.enqueue(CHANGED_FILE_CONTENT);
+        testRunner.run();
+        testRunner.assertTransferCount(PutBoxFile.REL_SUCCESS, 1);
+        testRunner.assertTransferCount(PutBoxFile.REL_FAILURE, 0);
+    }
+
+    @Test
+    void testUploadExistingFileReplaceResolution()  {
+        testRunner.setProperty(PutBoxFile.FOLDER_ID, mainFolderId);
+        testRunner.setProperty(PutBoxFile.FILE_NAME, TEST_FILENAME);
+        testRunner.setProperty(PutBoxFile.CONFLICT_RESOLUTION, 
ConflictResolutionStrategy.REPLACE.getValue());
+
+        testRunner.enqueue(DEFAULT_FILE_CONTENT);
+        testRunner.run();
+
+        testRunner.assertTransferCount(PutBoxFile.REL_SUCCESS, 1);
+        testRunner.assertTransferCount(PutBoxFile.REL_FAILURE, 0);
+        testRunner.clearTransferState();
+
+        testRunner.enqueue(CHANGED_FILE_CONTENT);
+        testRunner.run();
+        testRunner.assertTransferCount(PutBoxFile.REL_SUCCESS, 1);
+        testRunner.assertTransferCount(PutBoxFile.REL_FAILURE, 0);
+    }
+}
diff --git 
a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/PutBoxFileTest.java
 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/PutBoxFileTest.java
new file mode 100644
index 0000000000..4cfc973c6c
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/PutBoxFileTest.java
@@ -0,0 +1,212 @@
+/*
+ * 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 static java.lang.String.format;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.box.sdk.BoxFile;
+import com.box.sdk.BoxFolder;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.provenance.ProvenanceEventType;
+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.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class PutBoxFileTest extends AbstractBoxFileTest {
+
+    public static final String SUBFOLDER1_ID = "aaaa";
+    public static final String SUBFOLDER2_ID = "bbbb";
+    @Mock
+    protected BoxFolder.Info mockSubfolder1Info;
+
+    @Mock
+    protected BoxFolder.Info mockSubfolder2Info;
+
+    @Mock
+    protected BoxFolder mockSubfolder1;
+
+    @Mock
+    protected BoxFolder mockSubfolder2;
+
+    private final Map<String, BoxFolder> mockBoxFolders = new HashMap<>();
+
+
+    @BeforeEach
+    void setUp() throws Exception {
+        initMockBoxFolderMap();
+        final PutBoxFile testSubject = new PutBoxFile() {
+            @Override
+            BoxFolder getFolder(String folderId) {
+               return mockBoxFolders.get(folderId);
+            }
+        };
+
+        testRunner = TestRunners.newTestRunner(testSubject);
+        super.setUp();
+    }
+
+    private void initMockBoxFolderMap() {
+        mockBoxFolders.put(TEST_FOLDER_ID, mockBoxFolder);
+        mockBoxFolders.put(SUBFOLDER1_ID, mockSubfolder1);
+        mockBoxFolders.put(SUBFOLDER2_ID, mockSubfolder2);
+    }
+
+    @Test
+    void testUploadFilenameFromFlowFileAttribute()  {
+        testRunner.setProperty(PutBoxFile.FOLDER_ID, TEST_FOLDER_ID);
+        testRunner.setProperty(PutBoxFile.FILE_NAME, "${filename}");
+
+        final MockFlowFile inputFlowFile = new MockFlowFile(0);
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put(CoreAttributes.FILENAME.key(), TEST_FILENAME);
+        inputFlowFile.putAttributes(attributes);
+        inputFlowFile.setData(CONTENT.getBytes(UTF_8));
+
+        final BoxFile.Info mockUploadedFileInfo = 
createFileInfo(TEST_FOLDER_NAME, MODIFIED_TIME);
+        when(mockBoxFolder.uploadFile(any(InputStream.class), 
eq(TEST_FILENAME))).thenReturn(mockUploadedFileInfo);
+        when(mockBoxFolder.getInfo()).thenReturn(mockBoxFolderInfo);
+        when(mockBoxFolderInfo.getID()).thenReturn(TEST_FOLDER_ID);
+        when(mockBoxFolderInfo.getName()).thenReturn(TEST_FOLDER_NAME);
+
+        testRunner.enqueue(inputFlowFile);
+        testRunner.run();
+
+        testRunner.assertAllFlowFilesTransferred(PutBoxFile.REL_SUCCESS, 1);
+        final List<MockFlowFile> flowFiles = 
testRunner.getFlowFilesForRelationship(PutBoxFile.REL_SUCCESS);
+        final MockFlowFile ff0 = flowFiles.get(0);
+        assertOutFlowFileAttributes(ff0);
+        assertProvenanceEvent(ProvenanceEventType.SEND);
+    }
+
+    @Test
+    void testUploadFileExistingSubfolders()  {
+        testRunner.setProperty(PutBoxFile.FOLDER_ID, TEST_FOLDER_ID);
+        testRunner.setProperty(PutBoxFile.SUBFOLDER_NAME, "sub1/sub2");
+        testRunner.setProperty(PutBoxFile.FILE_NAME, TEST_FILENAME);
+        testRunner.setProperty(PutBoxFile.CREATE_SUBFOLDER, "true");
+
+        
when(mockBoxFolder.getChildren("name")).thenReturn(singletonList(mockSubfolder1Info));
+        
when(mockSubfolder1.getChildren("name")).thenReturn(singletonList(mockSubfolder2Info));
+
+        when(mockSubfolder1Info.getName()).thenReturn("sub1");
+        when(mockSubfolder2Info.getName()).thenReturn("sub2");
+        when(mockSubfolder1Info.getID()).thenReturn(SUBFOLDER1_ID);
+        when(mockSubfolder2Info.getID()).thenReturn(SUBFOLDER2_ID);
+        when(mockSubfolder1Info.getResource()).thenReturn(mockSubfolder1);
+        when(mockSubfolder2Info.getResource()).thenReturn(mockSubfolder2);
+        when(mockSubfolder2.getInfo()).thenReturn(mockSubfolder2Info);
+
+        final MockFlowFile inputFlowFile = new MockFlowFile(0);
+        inputFlowFile.setData(CONTENT.getBytes(UTF_8));
+
+        final BoxFile.Info mockUploadedFileInfo = 
createFileInfo(TEST_FOLDER_NAME, MODIFIED_TIME, asList(mockBoxFolderInfo, 
mockSubfolder1Info, mockSubfolder2Info));
+        when(mockSubfolder2.uploadFile(any(InputStream.class), 
eq(TEST_FILENAME))).thenReturn(mockUploadedFileInfo);
+
+        testRunner.enqueue(inputFlowFile);
+        testRunner.run();
+
+        testRunner.assertAllFlowFilesTransferred(PutBoxFile.REL_SUCCESS, 1);
+        final List<MockFlowFile> flowFiles = 
testRunner.getFlowFilesForRelationship(PutBoxFile.REL_SUCCESS);
+        final MockFlowFile ff0 = flowFiles.get(0);
+        assertOutFlowFileAttributes(ff0, format("/%s/%s/%s", TEST_FOLDER_NAME, 
"sub1", "sub2"));
+        assertProvenanceEvent(ProvenanceEventType.SEND);
+    }
+
+    @Test
+    void testUploadFileCreateSubfolders()  {
+        testRunner.setProperty(PutBoxFile.FOLDER_ID, TEST_FOLDER_ID);
+        testRunner.setProperty(PutBoxFile.SUBFOLDER_NAME, "new1/new2");
+        testRunner.setProperty(PutBoxFile.FILE_NAME, TEST_FILENAME);
+        testRunner.setProperty(PutBoxFile.CREATE_SUBFOLDER, "true");
+
+        when(mockBoxFolder.getChildren("name")).thenReturn(emptyList());
+        when(mockSubfolder1.getChildren("name")).thenReturn(emptyList());
+
+        
when(mockBoxFolder.createFolder("new1")).thenReturn(mockSubfolder1Info);
+        
when(mockSubfolder1.createFolder("new2")).thenReturn(mockSubfolder2Info);
+
+        when(mockSubfolder1Info.getResource()).thenReturn(mockSubfolder1);
+        when(mockSubfolder2Info.getResource()).thenReturn(mockSubfolder2);
+
+        when(mockSubfolder1Info.getName()).thenReturn("new1");
+        when(mockSubfolder1Info.getID()).thenReturn(SUBFOLDER1_ID);
+        when(mockSubfolder2Info.getName()).thenReturn("new2");
+        when(mockSubfolder2Info.getID()).thenReturn(SUBFOLDER2_ID);
+        when(mockSubfolder1.getID()).thenReturn(SUBFOLDER1_ID);
+
+        when(mockSubfolder2.getInfo()).thenReturn(mockSubfolder2Info);
+
+        when(mockBoxFolder.getID()).thenReturn(TEST_FOLDER_ID);
+        when(mockBoxFolderInfo.getID()).thenReturn(TEST_FOLDER_ID);
+
+        final MockFlowFile inputFlowFile = new MockFlowFile(0);
+        inputFlowFile.setData(CONTENT.getBytes(UTF_8));
+
+        final BoxFile.Info mockUploadedFileInfo = 
createFileInfo(TEST_FOLDER_NAME, MODIFIED_TIME, asList(mockBoxFolderInfo, 
mockSubfolder1Info, mockSubfolder2Info));
+        when(mockSubfolder2.uploadFile(any(InputStream.class), 
eq(TEST_FILENAME))).thenReturn(mockUploadedFileInfo);
+
+        testRunner.enqueue(inputFlowFile);
+        testRunner.run();
+
+        testRunner.assertAllFlowFilesTransferred(PutBoxFile.REL_SUCCESS, 1);
+        final List<MockFlowFile> flowFiles = 
testRunner.getFlowFilesForRelationship(PutBoxFile.REL_SUCCESS);
+        final MockFlowFile ff0 = flowFiles.get(0);
+        assertOutFlowFileAttributes(ff0, format("/%s/%s/%s", TEST_FOLDER_NAME, 
"new1", "new2"));
+        assertProvenanceEvent(ProvenanceEventType.SEND);
+        verify(mockBoxFolder).createFolder("new1");
+        verify(mockSubfolder1).createFolder("new2");
+    }
+
+    @Test
+    void testUploadError()  {
+        testRunner.setProperty(PutBoxFile.FOLDER_ID, TEST_FOLDER_ID);
+        testRunner.setProperty(PutBoxFile.FILE_NAME, TEST_FILENAME);
+
+        final MockFlowFile inputFlowFile = new MockFlowFile(0);
+        inputFlowFile.setData(CONTENT.getBytes(UTF_8));
+
+        when(mockBoxFolderInfo.getName()).thenReturn(TEST_FOLDER_NAME);
+        when(mockBoxFolder.getInfo()).thenReturn(mockBoxFolderInfo);
+        when(mockBoxFolder.uploadFile(any(InputStream.class), 
eq(TEST_FILENAME))).thenThrow(new RuntimeException("Upload error"));
+
+        testRunner.enqueue(inputFlowFile);
+        testRunner.run();
+
+        testRunner.assertAllFlowFilesTransferred(PutBoxFile.REL_FAILURE, 1);
+        final List<MockFlowFile> flowFiles = 
testRunner.getFlowFilesForRelationship(PutBoxFile.REL_FAILURE);
+        final MockFlowFile ff0 = flowFiles.get(0);
+        ff0.assertAttributeEquals(BoxFileAttributes.ERROR_MESSAGE, "Upload 
error");
+        assertNoProvenanceEvent();
+    }
+}

Reply via email to