import unittest from unittest.mock import MagicMock, patch from mdrsclient.client import MdrsClient from mdrsclient.exceptions import IllegalArgumentException from mdrsclient.models import File, Folder, FolderSimple, Laboratories, Laboratory TIMESTAMP = "2026-01-01T00:00:00+09:00" LABORATORY = Laboratory(id=1, name="mylab", pi_name="PI", full_name="My Laboratory") def make_file(id: str, name: str) -> File: return File( id=id, name=name, type="text/plain", size=1, thumbnail=None, description="", metadata={}, download_url=f"v3/files/{id}/download/", created_at=TIMESTAMP, updated_at=TIMESTAMP, ) def make_sub_folder(id: str, name: str) -> FolderSimple: return FolderSimple( id=id, pid=None, name=name, access_level=1, lock=False, size=0, laboratory_id=1, description="", created_at=TIMESTAMP, updated_at=TIMESTAMP, restrict_opened_at=None, ) def make_folder(id: str, name: str, path: str, sub_folders: list[FolderSimple] | None = None) -> Folder: return Folder( id=id, pid=None, name=name, access_level=1, lock=False, size=0, laboratory_id=1, description="", created_at=TIMESTAMP, updated_at=TIMESTAMP, restrict_opened_at=None, metadata=[], sub_folders=sub_folders if sub_folders is not None else [], path=path, ) class TestLsColumnWidths(unittest.TestCase): """Each column is as wide as the widest thing printed under it.""" def test_the_size_column_fits_a_sub_folder_larger_than_its_parent(self): from unittest.mock import MagicMock as _MagicMock from mdrsclient.commands.ls import LsCommand, LsCommandContext laboratories = Laboratories() laboratories.append(LABORATORY) connection = _MagicMock() connection.laboratories = laboratories client = MdrsClient(connection) big = make_sub_folder("s1", "big") object.__setattr__(big, "size", 123456789) parent = make_folder("f1", "root", "/root/", [big]) context = LsCommandContext("remote:/mylab", client, LABORATORY, "", False, False, False) widths = LsCommand._column_widths(context, parent, []) self.assertEqual(widths["size"], len("123456789")) class TestCopyAndMove(unittest.TestCase): """`cp` and `mv` resolve both ends the same way and differ only in what they call.""" def make_client(self, folders_by_path: dict, files_by_folder: dict) -> MdrsClient: client = MdrsClient(MagicMock()) client.find_laboratory = MagicMock(return_value=LABORATORY) client.find_folder = MagicMock(side_effect=lambda lab, path, password=None: folders_by_path[path]) client.find_files = MagicMock(side_effect=lambda folder_id: files_by_folder.get(folder_id, [])) return client def make_tree(self, source_has: str) -> tuple[MdrsClient, dict, dict]: sub = make_sub_folder("fsub", "sub") folder_a = make_folder("fa", "a", "/a/", [sub] if source_has == "folder" else []) folder_b = make_folder("fb", "b", "/b/") folders = {"/a": folder_a, "/b": folder_b, "/b/": folder_b} files = {"fa": [make_file("x1", "data.txt")] if source_has == "file" else [], "fb": []} return self.make_client(folders, files), folders, files def test_a_file_is_copied_into_the_destination_folder(self): client, _, files = self.make_tree("file") with patch("mdrsclient.client.FilesApi") as files_api: client.cp("myremote:/mylab/a/data.txt", "myremote:/mylab/b/data.txt") files_api.return_value.copy.assert_called_once_with(files["fa"][0], "fb", "data.txt") def test_a_file_is_moved_into_the_destination_folder(self): client, _, files = self.make_tree("file") with patch("mdrsclient.client.FilesApi") as files_api: client.mv("myremote:/mylab/a/data.txt", "myremote:/mylab/b/data.txt") files_api.return_value.move.assert_called_once_with(files["fa"][0], "fb", "data.txt") def test_a_trailing_separator_keeps_the_source_name(self): client, _, files = self.make_tree("file") with patch("mdrsclient.client.FilesApi") as files_api: client.mv("myremote:/mylab/a/data.txt", "myremote:/mylab/b/") files_api.return_value.move.assert_called_once_with(files["fa"][0], "fb", "data.txt") def test_copying_a_folder_needs_the_recursive_flag(self): client, _, _ = self.make_tree("folder") with patch("mdrsclient.client.FoldersApi") as folders_api: with self.assertRaises(IllegalArgumentException) as caught: client.cp("myremote:/mylab/a/sub", "myremote:/mylab/b/sub") self.assertIn("Is a folder", str(caught.exception)) folders_api.return_value.copy.assert_not_called() def test_a_folder_is_copied_when_recursive(self): client, folders, _ = self.make_tree("folder") with patch("mdrsclient.client.FoldersApi") as folders_api: client.cp("myremote:/mylab/a/sub", "myremote:/mylab/b/sub", is_recursive=True) folders_api.return_value.copy.assert_called_once_with(folders["/a"].sub_folders[0], "fb", "sub") def test_a_folder_is_moved_without_the_recursive_flag(self): client, folders, _ = self.make_tree("folder") with patch("mdrsclient.client.FoldersApi") as folders_api: client.mv("myremote:/mylab/a/sub", "myremote:/mylab/b/sub") folders_api.return_value.move.assert_called_once_with(folders["/a"].sub_folders[0], "fb", "sub") def test_a_destination_that_already_holds_the_name_is_refused(self): sub = make_sub_folder("fsub", "sub") folders = {"/a": make_folder("fa", "a", "/a/", [sub]), "/b": make_folder("fb", "b", "/b/")} files = {"fa": [], "fb": [make_file("x2", "sub")]} client = self.make_client(folders, files) with self.assertRaises(IllegalArgumentException) as caught: client.mv("myremote:/mylab/a/sub", "myremote:/mylab/b/sub") self.assertIn("Cannot overwrite non-folder", str(caught.exception)) def test_a_transfer_across_laboratories_is_refused(self): client, _, _ = self.make_tree("file") with self.assertRaises(IllegalArgumentException) as caught: client.cp("myremote:/mylab/a/data.txt", "myremote:/otherlab/b/data.txt") self.assertIn("Laboratory mismatched", str(caught.exception)) def test_a_source_that_does_not_exist_is_refused(self): client, _, _ = self.make_tree("file") with self.assertRaises(IllegalArgumentException) as caught: client.mv("myremote:/mylab/a/missing.txt", "myremote:/mylab/b/missing.txt") self.assertIn("not found", str(caught.exception)) if __name__ == "__main__": unittest.main()