diff --git a/test/functional/swift_test_client.py b/test/functional/swift_test_client.py
index 3b90862b84..5998215a04 100644
--- a/test/functional/swift_test_client.py
+++ b/test/functional/swift_test_client.py
@@ -866,6 +866,10 @@ class File(Base):
 
     def copy(self, dest_cont, dest_file, hdrs=None, parms=None, cfg=None,
              return_resp=False):
+        """
+        Make a copy of this object using a COPY request with a Destination
+        header.
+        """
         if hdrs is None:
             hdrs = {}
         if parms is None:
@@ -891,6 +895,29 @@ class File(Base):
             return self.conn.response
         return True
 
+    def copy_using_x_copy_from(self, dest_cont, dest_file, hdrs=None,
+                               parms=None, cfg=None, return_resp=False):
+        """
+        Make a copy of this object using a PUT request with an X-Copy-From
+        header.
+        """
+        if hdrs is None:
+            hdrs = {}
+        if parms is None:
+            parms = {}
+        if cfg is None:
+            cfg = {}
+        headers = {'X-Copy-From': '/'.join(self.path)}
+        headers.update(hdrs)
+        path = [dest_cont, dest_file]
+        if self.conn.make_request('PUT', path, hdrs=headers,
+                                  cfg=cfg, parms=parms) != 201:
+            raise ResponseError(self.conn.response, 'PUT',
+                                self.conn.make_path(path))
+        if return_resp:
+            return self.conn.response
+        return True
+
     def copy_account(self, dest_account, dest_cont, dest_file,
                      hdrs=None, parms=None, cfg=None):
         if hdrs is None:
diff --git a/test/functional/test_slo.py b/test/functional/test_slo.py
index 57a208c423..4a040dc011 100644
--- a/test/functional/test_slo.py
+++ b/test/functional/test_slo.py
@@ -1031,6 +1031,42 @@ class TestSlo(Base):
         copied_contents = copied.read(parms={'multipart-manifest': 'get'})
         self.assertEqual(4 * 1024 * 1024 + 1, len(copied_contents))
 
+    def test_slo_copy_using_x_copy_from(self):
+        # as per test_slo_copy but using a PUT with x-copy-from
+        file_item = self.env.container.file("manifest-abcde")
+        file_item.copy_using_x_copy_from(
+            self.env.container.name, "copied-abcde")
+
+        copied = self.env.container.file("copied-abcde")
+        copied_contents = copied.read(parms={'multipart-manifest': 'get'})
+        self.assertEqual(4 * 1024 * 1024 + 1, len(copied_contents))
+
+    def test_slo_copy_part_number(self):
+        file_item = self.env.container.file("manifest-abcde")
+        file_item.copy(self.env.container.name, "copied-abcde",
+                       parms={'part-number': '1'})
+
+        copied = self.env.container.file("copied-abcde")
+        copied_contents = copied.read(parms={'multipart-manifest': 'get'})
+        # just the first part is copied
+        self.assertEqual(1024 * 1024, len(copied_contents))
+        self.assertEqual(b'a' * 10, copied_contents[:10])
+
+    def test_slo_copy_part_number_using_x_copy_from(self):
+        # as per test_slo_copy_part_number but using a PUT with x-copy-from
+        file_item = self.env.container.file("manifest-abcde")
+        # part-number on the client PUT target is actually applied to the
+        # internal GET source request
+        file_item.copy_using_x_copy_from(
+            self.env.container.name, "copied-abcde",
+            parms={'part-number': '1'})
+
+        copied = self.env.container.file("copied-abcde")
+        copied_contents = copied.read(parms={'multipart-manifest': 'get'})
+        # just the first part is copied
+        self.assertEqual(1024 * 1024, len(copied_contents))
+        self.assertEqual(b'a' * 10, copied_contents[:10])
+
     def test_slo_copy_account(self):
         acct = urllib.parse.unquote(self.env.conn.account_name)
         # same account copy
@@ -1129,6 +1165,38 @@ class TestSlo(Base):
         self.assertEqual(copied_json[0], {
             'data': base64.b64encode(b'APRE' * 8).decode('ascii')})
 
+    def test_slo_copy_the_manifest_using_x_copy_from(self):
+        # as per test_slo_copy_the_manifest but using a PUT with x-copy-from
+        source = self.env.container.file("manifest-abcde")
+        source.initialize(parms={'multipart-manifest': 'get'})
+        source_contents = source.read(parms={'multipart-manifest': 'get'})
+        source_json = json.loads(source_contents)
+        manifest_etag = md5(source_contents, usedforsecurity=False).hexdigest()
+        if tf.cluster_info.get('etag_quoter', {}).get('enable_by_default'):
+            manifest_etag = '"%s"' % manifest_etag
+        self.assertEqual(manifest_etag, source.etag)
+
+        source.initialize()
+        self.assertEqual('application/octet-stream', source.content_type)
+        self.assertNotEqual(manifest_etag, source.etag)
+
+        # multipart-manifest=get on the client PUT target request actually
+        # applies to the internal GET source request
+        self.assertTrue(
+            source.copy_using_x_copy_from(self.env.container.name,
+                                          "copied-abcde-manifest-only",
+                                          parms={'multipart-manifest': 'get'}))
+
+        copied = self.env.container.file("copied-abcde-manifest-only")
+        copied.initialize(parms={'multipart-manifest': 'get'})
+        copied_contents = copied.read(parms={'multipart-manifest': 'get'})
+        try:
+            copied_json = json.loads(copied_contents)
+        except ValueError:
+            self.fail("COPY didn't copy the manifest (invalid json on GET)")
+        self.assertEqual(source_json, copied_json)
+        self.assertEqual(manifest_etag, copied.etag)
+
     def test_slo_copy_the_manifest_updating_metadata(self):
         source = self.env.container.file("manifest-abcde")
         source.content_type = 'application/octet-stream'