|
4 | 4 |
|
5 | 5 | import json |
6 | 6 | import typing as t |
| 7 | +import urllib.request |
7 | 8 |
|
8 | 9 | import pytest |
9 | 10 |
|
@@ -156,3 +157,120 @@ def test_gitlab_uses_path_not_name( |
156 | 157 | repos = list(importer.fetch_repos(options)) |
157 | 158 | assert len(repos) == 1 |
158 | 159 | assert repos[0].name == "my-project" # Uses 'path', not 'name' |
| 160 | + |
| 161 | + |
| 162 | +class MockHTTPResponse: |
| 163 | + """Mock HTTP response for subgroup test.""" |
| 164 | + |
| 165 | + def __init__(self, body: bytes, headers: dict[str, str] | None = None) -> None: |
| 166 | + self._body = body |
| 167 | + self._headers = headers or {} |
| 168 | + self.status = 200 |
| 169 | + self.code = 200 |
| 170 | + |
| 171 | + def read(self) -> bytes: |
| 172 | + return self._body |
| 173 | + |
| 174 | + def getheaders(self) -> list[tuple[str, str]]: |
| 175 | + return list(self._headers.items()) |
| 176 | + |
| 177 | + def __enter__(self) -> MockHTTPResponse: |
| 178 | + return self |
| 179 | + |
| 180 | + def __exit__(self, *args: t.Any) -> None: |
| 181 | + pass |
| 182 | + |
| 183 | + |
| 184 | +def test_gitlab_subgroup_url_encoding( |
| 185 | + monkeypatch: pytest.MonkeyPatch, |
| 186 | +) -> None: |
| 187 | + """Test that GitLab subgroups are URL-encoded correctly. |
| 188 | +
|
| 189 | + Subgroups use slash notation (e.g., parent/child) which must be |
| 190 | + URL-encoded as %2F in API requests. |
| 191 | + """ |
| 192 | + captured_urls: list[str] = [] |
| 193 | + |
| 194 | + response_json = [ |
| 195 | + { |
| 196 | + "path": "subgroup-project", |
| 197 | + "name": "Subgroup Project", |
| 198 | + "http_url_to_repo": "https://gitlab.com/parent/child/subgroup-project.git", |
| 199 | + "web_url": "https://gitlab.com/parent/child/subgroup-project", |
| 200 | + "description": "Project in subgroup", |
| 201 | + "topics": [], |
| 202 | + "star_count": 10, |
| 203 | + "archived": False, |
| 204 | + "default_branch": "main", |
| 205 | + "namespace": {"path": "parent/child"}, |
| 206 | + } |
| 207 | + ] |
| 208 | + |
| 209 | + def urlopen_capture( |
| 210 | + request: urllib.request.Request, |
| 211 | + timeout: int | None = None, |
| 212 | + ) -> MockHTTPResponse: |
| 213 | + captured_urls.append(request.full_url) |
| 214 | + return MockHTTPResponse(json.dumps(response_json).encode()) |
| 215 | + |
| 216 | + monkeypatch.setattr("urllib.request.urlopen", urlopen_capture) |
| 217 | + |
| 218 | + importer = GitLabImporter() |
| 219 | + options = ImportOptions(mode=ImportMode.ORG, target="parent/child") |
| 220 | + repos = list(importer.fetch_repos(options)) |
| 221 | + |
| 222 | + # Verify the URL was encoded correctly |
| 223 | + assert len(captured_urls) == 1 |
| 224 | + assert "parent%2Fchild" in captured_urls[0], ( |
| 225 | + f"Expected URL-encoded subgroup path 'parent%2Fchild', got: {captured_urls[0]}" |
| 226 | + ) |
| 227 | + assert "/groups/parent%2Fchild/projects" in captured_urls[0] |
| 228 | + |
| 229 | + # Verify repos were returned |
| 230 | + assert len(repos) == 1 |
| 231 | + assert repos[0].name == "subgroup-project" |
| 232 | + |
| 233 | + |
| 234 | +def test_gitlab_deeply_nested_subgroup( |
| 235 | + monkeypatch: pytest.MonkeyPatch, |
| 236 | +) -> None: |
| 237 | + """Test that deeply nested subgroups (multiple slashes) work correctly.""" |
| 238 | + captured_urls: list[str] = [] |
| 239 | + |
| 240 | + response_json = [ |
| 241 | + { |
| 242 | + "path": "deep-project", |
| 243 | + "name": "Deep Project", |
| 244 | + "http_url_to_repo": "https://gitlab.com/a/b/c/d/deep-project.git", |
| 245 | + "web_url": "https://gitlab.com/a/b/c/d/deep-project", |
| 246 | + "description": "Deeply nested project", |
| 247 | + "topics": [], |
| 248 | + "star_count": 5, |
| 249 | + "archived": False, |
| 250 | + "default_branch": "main", |
| 251 | + "namespace": {"path": "a/b/c/d"}, |
| 252 | + } |
| 253 | + ] |
| 254 | + |
| 255 | + def urlopen_capture( |
| 256 | + request: urllib.request.Request, |
| 257 | + timeout: int | None = None, |
| 258 | + ) -> MockHTTPResponse: |
| 259 | + captured_urls.append(request.full_url) |
| 260 | + return MockHTTPResponse(json.dumps(response_json).encode()) |
| 261 | + |
| 262 | + monkeypatch.setattr("urllib.request.urlopen", urlopen_capture) |
| 263 | + |
| 264 | + importer = GitLabImporter() |
| 265 | + # Test with 4 levels of nesting: a/b/c/d |
| 266 | + options = ImportOptions(mode=ImportMode.ORG, target="a/b/c/d") |
| 267 | + repos = list(importer.fetch_repos(options)) |
| 268 | + |
| 269 | + # Verify URL encoding - each slash should become %2F |
| 270 | + assert len(captured_urls) == 1 |
| 271 | + assert "a%2Fb%2Fc%2Fd" in captured_urls[0], ( |
| 272 | + f"Expected URL-encoded path 'a%2Fb%2Fc%2Fd', got: {captured_urls[0]}" |
| 273 | + ) |
| 274 | + |
| 275 | + assert len(repos) == 1 |
| 276 | + assert repos[0].name == "deep-project" |
0 commit comments