Repository: ftnext/diy-googledrive-operate

Issue:
コピーしてできたファイルの名前をオプショナルで指定できる
指定しない時は今の挙動（copied_元のファイル名）

Discussion:

Patch:
diff --git a/main.py b/main.py
index 4ffcb3f..04e6958 100644
--- a/main.py
+++ b/main.py
@@ -1,4 +1,5 @@
 import argparse
+from typing import Optional
 
 from operate_drive import create_diy_gdrive
 from operate_drive.drive import DiyGoogleDrive
@@ -8,7 +9,7 @@ from operate_drive.file import DiyGDriveFile
 def main():
     args = parse_args()
 
-    dest_file = cp_in_drive(args.source_id)
+    dest_file = cp_in_drive(args.source_id, args.dest_title)
 
     info = display_information(dest_file)
     print(info)
@@ -17,13 +18,17 @@ def main():
 def parse_args():
     parser = argparse.ArgumentParser()
     parser.add_argument("source_id")
+    parser.add_argument("--dest_title")
     return parser.parse_args()
 
 
-def cp_in_drive(source_id: str) -> DiyGDriveFile:
+def cp_in_drive(
+    source_id: str, dest_title: Optional[str] = None
+) -> DiyGDriveFile:
     """Copy a specified file in Google Drive and return the created file."""
     drive = create_diy_gdrive()
-    dest_title = build_dest_title(drive, source_id)
+    if dest_title is None:
+        dest_title = build_dest_title(drive, source_id)
     return drive.copy_file(source_id, dest_title)
 
 
diff --git a/operate_drive/drive.py b/operate_drive/drive.py
index fc5056e..1fa65ad 100644
--- a/operate_drive/drive.py
+++ b/operate_drive/drive.py
@@ -16,6 +16,9 @@ class DiyGoogleDrive:
         return DiyGDriveFile(gdrive_file)
 
     def copy_file(self, source_id: str, dest_title: str) -> DiyGDriveFile:
+        if self._drive.auth.service is None:
+            self._drive.GetAbout()  # Workaround to set auth (Issue #6)
+
         access_to_files = self._drive.auth.service.files()
         metadata = {"title": dest_title}
         request_to_copy = access_to_files.copy(


Test patch:
diff --git a/tests/operate_drive/test_drive.py b/tests/operate_drive/test_drive.py
index 0caa180..6d588ac 100644
--- a/tests/operate_drive/test_drive.py
+++ b/tests/operate_drive/test_drive.py
@@ -1,3 +1,4 @@
+from typing import Callable
 from unittest import TestCase
 from unittest.mock import MagicMock, patch
 
@@ -39,24 +40,64 @@ class DiyGoogleDriveFetchFileByIdTestCase(TestCase):
         self.assertEqual(actual, mock_diy_gdrive_file.return_value)
 
 
+@patch("operate_drive.drive.DiyGoogleDrive.fetch_file_by_id")
 class DiyGoogleDriveCopyFileTestCase(TestCase):
-    @patch("operate_drive.drive.DiyGoogleDrive.fetch_file_by_id")
-    def test_should_copy(self, fetch_file_by_id):
-        source_id = MagicMock(spec=str)
-        dest_title = MagicMock(spec=str)
+    def setUp(self):
+        self.source_id = MagicMock(spec=str)
+        self.dest_title = MagicMock(spec=str)
         # mockにauthプロパティを持たせるためにインスタンス化して渡している
-        gdrive = MagicMock(spec=GoogleDrive())
-        a_drive = d.DiyGoogleDrive(gdrive)
-        access_to_files = gdrive.auth.service.files.return_value
+        self.gdrive = MagicMock(spec=GoogleDrive())
+        self.a_drive = d.DiyGoogleDrive(self.gdrive)
+
+    def test_should_copy(self, fetch_file_by_id):
+        access_to_files = self.gdrive.auth.service.files.return_value
+        request_to_copy = access_to_files.copy.return_value
+        copied_file_info_dict = request_to_copy.execute.return_value
+
+        actual = self.a_drive.copy_file(self.source_id, self.dest_title)
+
+        self.gdrive.auth.service.files.assert_called_once_with()
+        access_to_files.copy.assert_called_once_with(
+            fileId=self.source_id,
+            body={"title": self.dest_title},
+            supportsAllDrives=True,
+        )
+        request_to_copy.execute.assert_called_once_with()
+        fetch_file_by_id.assert_called_once_with(copied_file_info_dict["id"])
+        self.assertEqual(actual, fetch_file_by_id.return_value)
+
+    def test_when_auth_service_is_none(self, fetch_file_by_id):
+        self.gdrive.auth.service = None
+        call_count_get_about = 0
+
+        # Noneだったservice属性をGetAboutメソッドが書き換える副作用をモックで表現した
+        def mocked_get_about(files_mock):
+            from googleapiclient.discovery import Resource
+
+            def wrapper():
+                nonlocal call_count_get_about
+                m = MagicMock(spec=Resource)
+                m.files = files_mock
+                self.gdrive.auth.service = m
+                call_count_get_about += 1
+
+            return wrapper
+
+        files_mock = MagicMock(spec=Callable)
+        access_to_files = files_mock.return_value
+        self.gdrive.GetAbout = mocked_get_about(files_mock)
+
         request_to_copy = access_to_files.copy.return_value
         copied_file_info_dict = request_to_copy.execute.return_value
 
-        actual = a_drive.copy_file(source_id, dest_title)
+        actual = self.a_drive.copy_file(self.source_id, self.dest_title)
 
-        gdrive.auth.service.files.assert_called_once_with()
+        # GetAbout.assert_called_once_with() 相当の検証
+        self.assertEqual(call_count_get_about, 1)
+        files_mock.assert_called_once_with()
         access_to_files.copy.assert_called_once_with(
-            fileId=source_id,
-            body={"title": dest_title},
+            fileId=self.source_id,
+            body={"title": self.dest_title},
             supportsAllDrives=True,
         )
         request_to_copy.execute.assert_called_once_with()
diff --git a/tests/test_main.py b/tests/test_main.py
index ca9f643..d0c9998 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -1,5 +1,5 @@
 from unittest import TestCase
-from unittest.mock import MagicMock, patch
+from unittest.mock import call, MagicMock, patch
 
 from operate_drive.drive import DiyGoogleDrive
 
@@ -21,7 +21,7 @@ class MainTestCase(TestCase):
         m.main()
 
         parse_args.assert_called_once_with()
-        cp_in_drive.assert_called_once_with(args.source_id)
+        cp_in_drive.assert_called_once_with(args.source_id, args.dest_title)
         display_information.assert_called_once_with(dest_file)
         mock_print.assert_called_once_with(info)
 
@@ -34,7 +34,9 @@ class ParseArgsTestCase(TestCase):
         actual = m.parse_args()
 
         mock_argument_parser.assert_called_once_with()
-        parser.add_argument.assert_called_once_with("source_id")
+        parser.add_argument.assert_has_calls(
+            [call("source_id"), call("--dest_title")]
+        )
         parser.parse_args.assert_called_once_with()
         self.assertEqual(actual, parser.parse_args.return_value)
 
@@ -54,6 +56,18 @@ class CpInDriveTestCase(TestCase):
         drive.copy_file.assert_called_once_with(source_id, dest_title)
         self.assertEqual(actual, drive.copy_file.return_value)
 
+    @patch("main.create_diy_gdrive")
+    def test_should_copy_when_title_specified(self, create_diy_gdrive):
+        source_id = MagicMock(spec=str)
+        dest_title = MagicMock(spec=str)
+        drive = create_diy_gdrive.return_value
+
+        actual = m.cp_in_drive(source_id, dest_title)
+
+        self.assertEqual(actual, drive.copy_file.return_value)
+        create_diy_gdrive.assert_called_once_with()
+        drive.copy_file.assert_called_once_with(source_id, dest_title)
+
 
 class BuildDestTitleTestCase(TestCase):
     @patch("main.title_of_copy_dest")