|
1 | | -from unittest.mock import patch |
| 1 | +from unittest.mock import MagicMock, patch |
2 | 2 |
|
3 | 3 | import typer |
4 | 4 | from typer.testing import CliRunner |
|
8 | 8 | runner = CliRunner() |
9 | 9 |
|
10 | 10 |
|
| 11 | +def make_search_video(video_id, title, description=""): |
| 12 | + return { |
| 13 | + "id": video_id, |
| 14 | + "snippet": { |
| 15 | + "title": title, |
| 16 | + "description": description, |
| 17 | + "publishedAt": "2020-01-01T00:00:00Z", |
| 18 | + "tags": [], |
| 19 | + "categoryId": "22", |
| 20 | + }, |
| 21 | + "statistics": {"viewCount": "0", "likeCount": "0", "commentCount": "0"}, |
| 22 | + "contentDetails": {"duration": "PT1M"}, |
| 23 | + "status": {"privacyStatus": "public"}, |
| 24 | + } |
| 25 | + |
| 26 | + |
| 27 | +def setup_search_mock(mock_service, videos): |
| 28 | + """Configure the search and videos.list mocks for search-replace tests""" |
| 29 | + search_list = MagicMock() |
| 30 | + search_list.execute.return_value = { |
| 31 | + "items": [{"id": {"videoId": v["id"]}} for v in videos], |
| 32 | + } |
| 33 | + mock_service.search.return_value.list.return_value = search_list |
| 34 | + |
| 35 | + videos_list = MagicMock() |
| 36 | + videos_list.execute.return_value = {"items": videos} |
| 37 | + mock_service.videos.return_value.list.return_value = videos_list |
| 38 | + |
| 39 | + |
11 | 40 | class TestVideosCommands: |
12 | 41 | def test_list(self, mock_auth): |
13 | 42 | result = runner.invoke(app, ["videos", "list"]) |
@@ -41,3 +70,180 @@ def test_not_authenticated(self): |
41 | 70 | ): |
42 | 71 | result = runner.invoke(app, ["videos", "list"]) |
43 | 72 | assert result.exit_code == 1 |
| 73 | + |
| 74 | + |
| 75 | +class TestSearchReplace: |
| 76 | + def test_dry_run_shows_preview(self, mock_auth): |
| 77 | + videos = [make_search_video("vid1", "OLDNAME Episode 1")] |
| 78 | + setup_search_mock(mock_auth, videos) |
| 79 | + |
| 80 | + result = runner.invoke( |
| 81 | + app, |
| 82 | + [ |
| 83 | + "videos", |
| 84 | + "search-replace", |
| 85 | + "-s", |
| 86 | + "OLDNAME", |
| 87 | + "-r", |
| 88 | + "NewName", |
| 89 | + "-f", |
| 90 | + "title", |
| 91 | + ], |
| 92 | + ) |
| 93 | + assert result.exit_code == 0 |
| 94 | + assert "NewName Episode 1" in result.stdout |
| 95 | + assert "Pending" in result.stdout |
| 96 | + assert "--execute" in result.stdout |
| 97 | + mock_auth.videos.return_value.update.assert_not_called() |
| 98 | + |
| 99 | + def test_execute_applies_changes(self, mock_auth): |
| 100 | + videos = [make_search_video("vid1", "OLDNAME Episode 1")] |
| 101 | + setup_search_mock(mock_auth, videos) |
| 102 | + |
| 103 | + result = runner.invoke( |
| 104 | + app, |
| 105 | + [ |
| 106 | + "videos", |
| 107 | + "search-replace", |
| 108 | + "-s", |
| 109 | + "OLDNAME", |
| 110 | + "-r", |
| 111 | + "NewName", |
| 112 | + "-f", |
| 113 | + "title", |
| 114 | + "--execute", |
| 115 | + ], |
| 116 | + ) |
| 117 | + assert result.exit_code == 0 |
| 118 | + assert "1 updated" in result.stdout |
| 119 | + mock_auth.videos.return_value.update.assert_called_once() |
| 120 | + |
| 121 | + def test_no_matches(self, mock_auth): |
| 122 | + videos = [make_search_video("vid1", "Some Other Title")] |
| 123 | + setup_search_mock(mock_auth, videos) |
| 124 | + |
| 125 | + result = runner.invoke( |
| 126 | + app, |
| 127 | + [ |
| 128 | + "videos", |
| 129 | + "search-replace", |
| 130 | + "-s", |
| 131 | + "OLDNAME", |
| 132 | + "-r", |
| 133 | + "NewName", |
| 134 | + "-f", |
| 135 | + "title", |
| 136 | + ], |
| 137 | + ) |
| 138 | + assert result.exit_code == 0 |
| 139 | + assert "No matches found" in result.stdout |
| 140 | + |
| 141 | + def test_regex_replace(self, mock_auth): |
| 142 | + videos = [make_search_video("vid1", "Episode 01 - Test")] |
| 143 | + setup_search_mock(mock_auth, videos) |
| 144 | + |
| 145 | + result = runner.invoke( |
| 146 | + app, |
| 147 | + [ |
| 148 | + "videos", |
| 149 | + "search-replace", |
| 150 | + "-s", |
| 151 | + r"Episode (\d+)", |
| 152 | + "-r", |
| 153 | + r"Ep.\1", |
| 154 | + "-f", |
| 155 | + "title", |
| 156 | + "--regex", |
| 157 | + ], |
| 158 | + ) |
| 159 | + assert result.exit_code == 0 |
| 160 | + assert "Ep.01" in result.stdout |
| 161 | + |
| 162 | + def test_limit_caps_matches(self, mock_auth): |
| 163 | + videos = [make_search_video(f"vid{i}", f"OLDNAME Episode {i}") for i in range(5)] |
| 164 | + setup_search_mock(mock_auth, videos) |
| 165 | + |
| 166 | + result = runner.invoke( |
| 167 | + app, |
| 168 | + [ |
| 169 | + "videos", |
| 170 | + "search-replace", |
| 171 | + "-s", |
| 172 | + "OLDNAME", |
| 173 | + "-r", |
| 174 | + "NewName", |
| 175 | + "-f", |
| 176 | + "title", |
| 177 | + "-n", |
| 178 | + "2", |
| 179 | + ], |
| 180 | + ) |
| 181 | + assert result.exit_code == 0 |
| 182 | + assert "2 changes" in result.stdout |
| 183 | + |
| 184 | + def test_description_field(self, mock_auth): |
| 185 | + videos = [make_search_video("vid1", "Title", description="Visit OLDSITE.com")] |
| 186 | + setup_search_mock(mock_auth, videos) |
| 187 | + |
| 188 | + result = runner.invoke( |
| 189 | + app, |
| 190 | + [ |
| 191 | + "videos", |
| 192 | + "search-replace", |
| 193 | + "-s", |
| 194 | + "OLDSITE.com", |
| 195 | + "-r", |
| 196 | + "newsite.com", |
| 197 | + "-f", |
| 198 | + "description", |
| 199 | + ], |
| 200 | + ) |
| 201 | + assert result.exit_code == 0 |
| 202 | + assert "newsite.com" in result.stdout |
| 203 | + |
| 204 | + def test_pagination_reaches_all_videos(self, mock_auth): |
| 205 | + page1_videos = [make_search_video("vid1", "No Match Here")] |
| 206 | + page2_videos = [make_search_video("vid2", "OLDNAME Old Video")] |
| 207 | + |
| 208 | + call_count = 0 |
| 209 | + |
| 210 | + def search_side_effect(): |
| 211 | + nonlocal call_count |
| 212 | + call_count += 1 |
| 213 | + if call_count == 1: |
| 214 | + return { |
| 215 | + "items": [{"id": {"videoId": "vid1"}}], |
| 216 | + "nextPageToken": "page2token", |
| 217 | + } |
| 218 | + return { |
| 219 | + "items": [{"id": {"videoId": "vid2"}}], |
| 220 | + } |
| 221 | + |
| 222 | + mock_auth.search.return_value.list.return_value.execute.side_effect = search_side_effect |
| 223 | + |
| 224 | + videos_call_count = 0 |
| 225 | + |
| 226 | + def videos_side_effect(): |
| 227 | + nonlocal videos_call_count |
| 228 | + videos_call_count += 1 |
| 229 | + if videos_call_count == 1: |
| 230 | + return {"items": page1_videos} |
| 231 | + return {"items": page2_videos} |
| 232 | + |
| 233 | + mock_auth.videos.return_value.list.return_value.execute.side_effect = videos_side_effect |
| 234 | + |
| 235 | + result = runner.invoke( |
| 236 | + app, |
| 237 | + [ |
| 238 | + "videos", |
| 239 | + "search-replace", |
| 240 | + "-s", |
| 241 | + "OLDNAME", |
| 242 | + "-r", |
| 243 | + "NewName", |
| 244 | + "-f", |
| 245 | + "title", |
| 246 | + ], |
| 247 | + ) |
| 248 | + assert result.exit_code == 0 |
| 249 | + assert "NewName Old Video" in result.stdout |
0 commit comments