1import tempfile 2import time 3import uuid 4from pathlib import Path 5from subprocess import check_output 6 7import pytest 8 9import gitlab 10 11 12def reset_gitlab(gl): 13 # previously tools/reset_gitlab.py 14 for project in gl.projects.list(): 15 for deploy_token in project.deploytokens.list(): 16 deploy_token.delete() 17 project.delete() 18 for group in gl.groups.list(): 19 for deploy_token in group.deploytokens.list(): 20 deploy_token.delete() 21 group.delete() 22 for variable in gl.variables.list(): 23 variable.delete() 24 for user in gl.users.list(): 25 if user.username != "root": 26 user.delete(hard_delete=True) 27 28 29def set_token(container, rootdir): 30 set_token_rb = rootdir / "fixtures" / "set_token.rb" 31 32 with open(set_token_rb, "r") as f: 33 set_token_command = f.read().strip() 34 35 rails_command = [ 36 "docker", 37 "exec", 38 container, 39 "gitlab-rails", 40 "runner", 41 set_token_command, 42 ] 43 output = check_output(rails_command).decode().strip() 44 45 return output 46 47 48def pytest_report_collectionfinish(config, startdir, items): 49 return [ 50 "", 51 "Starting GitLab container.", 52 "Waiting for GitLab to reconfigure.", 53 "This may take a few minutes.", 54 ] 55 56 57def pytest_addoption(parser): 58 parser.addoption( 59 "--keep-containers", 60 action="store_true", 61 help="Keep containers running after testing", 62 ) 63 64 65@pytest.fixture(scope="session") 66def temp_dir(): 67 return Path(tempfile.gettempdir()) 68 69 70@pytest.fixture(scope="session") 71def test_dir(pytestconfig): 72 return pytestconfig.rootdir / "tests" / "functional" 73 74 75@pytest.fixture(scope="session") 76def docker_compose_file(test_dir): 77 return test_dir / "fixtures" / "docker-compose.yml" 78 79 80@pytest.fixture(scope="session") 81def docker_compose_project_name(): 82 """Set a consistent project name to enable optional reuse of containers.""" 83 return "pytest-python-gitlab" 84 85 86@pytest.fixture(scope="session") 87def docker_cleanup(request): 88 """Conditionally keep containers around by overriding the cleanup command.""" 89 if request.config.getoption("--keep-containers"): 90 # Print version and exit. 91 return "-v" 92 return "down -v" 93 94 95@pytest.fixture(scope="session") 96def check_is_alive(): 97 """ 98 Return a healthcheck function fixture for the GitLab container spinup. 99 """ 100 101 def _check(container): 102 logs = ["docker", "logs", container] 103 return "gitlab Reconfigured!" in check_output(logs).decode() 104 105 return _check 106 107 108@pytest.fixture 109def wait_for_sidekiq(gl): 110 """ 111 Return a helper function to wait until there are no busy sidekiq processes. 112 113 Use this with asserts for slow tasks (group/project/user creation/deletion). 114 """ 115 116 def _wait(timeout=30, step=0.5): 117 for _ in range(timeout): 118 time.sleep(step) 119 busy = False 120 processes = gl.sidekiq.process_metrics()["processes"] 121 for process in processes: 122 if process["busy"]: 123 busy = True 124 if not busy: 125 return True 126 return False 127 128 return _wait 129 130 131@pytest.fixture(scope="session") 132def gitlab_config(check_is_alive, docker_ip, docker_services, temp_dir, test_dir): 133 config_file = temp_dir / "python-gitlab.cfg" 134 port = docker_services.port_for("gitlab", 80) 135 136 docker_services.wait_until_responsive( 137 timeout=200, pause=5, check=lambda: check_is_alive("gitlab-test") 138 ) 139 140 token = set_token("gitlab-test", rootdir=test_dir) 141 142 config = f"""[global] 143default = local 144timeout = 60 145 146[local] 147url = http://{docker_ip}:{port} 148private_token = {token} 149api_version = 4""" 150 151 with open(config_file, "w") as f: 152 f.write(config) 153 154 return config_file 155 156 157@pytest.fixture(scope="session") 158def gl(gitlab_config): 159 """Helper instance to make fixtures and asserts directly via the API.""" 160 161 instance = gitlab.Gitlab.from_config("local", [gitlab_config]) 162 reset_gitlab(instance) 163 164 return instance 165 166 167@pytest.fixture(scope="session") 168def gitlab_runner(gl): 169 container = "gitlab-runner-test" 170 runner_name = "python-gitlab-runner" 171 token = "registration-token" 172 url = "http://gitlab" 173 174 docker_exec = ["docker", "exec", container, "gitlab-runner"] 175 register = [ 176 "register", 177 "--run-untagged", 178 "--non-interactive", 179 "--registration-token", 180 token, 181 "--name", 182 runner_name, 183 "--url", 184 url, 185 "--clone-url", 186 url, 187 "--executor", 188 "shell", 189 ] 190 unregister = ["unregister", "--name", runner_name] 191 192 yield check_output(docker_exec + register).decode() 193 194 check_output(docker_exec + unregister).decode() 195 196 197@pytest.fixture(scope="module") 198def group(gl): 199 """Group fixture for group API resource tests.""" 200 _id = uuid.uuid4().hex 201 data = { 202 "name": f"test-group-{_id}", 203 "path": f"group-{_id}", 204 } 205 group = gl.groups.create(data) 206 207 yield group 208 209 try: 210 group.delete() 211 except gitlab.exceptions.GitlabDeleteError as e: 212 print(f"Group already deleted: {e}") 213 214 215@pytest.fixture(scope="module") 216def project(gl): 217 """Project fixture for project API resource tests.""" 218 _id = uuid.uuid4().hex 219 name = f"test-project-{_id}" 220 221 project = gl.projects.create(name=name) 222 223 yield project 224 225 try: 226 project.delete() 227 except gitlab.exceptions.GitlabDeleteError as e: 228 print(f"Project already deleted: {e}") 229 230 231@pytest.fixture(scope="function") 232def merge_request(project, wait_for_sidekiq): 233 """Fixture used to create a merge_request. 234 235 It will create a branch, add a commit to the branch, and then create a 236 merge request against project.default_branch. The MR will be returned. 237 238 When finished any created merge requests and branches will be deleted. 239 240 NOTE: No attempt is made to restore project.default_branch to its previous 241 state. So if the merge request is merged then its content will be in the 242 project.default_branch branch. 243 """ 244 245 to_delete = [] 246 247 def _merge_request(*, source_branch: str): 248 # Wait for processes to be done before we start... 249 # NOTE(jlvillal): Sometimes the CI would give a "500 Internal Server 250 # Error". Hoping that waiting until all other processes are done will 251 # help with that. 252 result = wait_for_sidekiq(timeout=60) 253 assert result is True, "sidekiq process should have terminated but did not" 254 255 project.refresh() # Gets us the current default branch 256 project.branches.create( 257 {"branch": source_branch, "ref": project.default_branch} 258 ) 259 # NOTE(jlvillal): Must create a commit in the new branch before we can 260 # create an MR that will work. 261 project.files.create( 262 { 263 "file_path": f"README.{source_branch}", 264 "branch": source_branch, 265 "content": "Initial content", 266 "commit_message": "New commit in new branch", 267 } 268 ) 269 mr = project.mergerequests.create( 270 { 271 "source_branch": source_branch, 272 "target_branch": project.default_branch, 273 "title": "Should remove source branch", 274 "remove_source_branch": True, 275 } 276 ) 277 result = wait_for_sidekiq(timeout=60) 278 assert result is True, "sidekiq process should have terminated but did not" 279 280 mr_iid = mr.iid 281 for _ in range(60): 282 mr = project.mergerequests.get(mr_iid) 283 if mr.merge_status != "checking": 284 break 285 time.sleep(0.5) 286 assert mr.merge_status != "checking" 287 288 to_delete.append((mr.iid, source_branch)) 289 return mr 290 291 yield _merge_request 292 293 for mr_iid, source_branch in to_delete: 294 project.mergerequests.delete(mr_iid) 295 try: 296 project.branches.delete(source_branch) 297 except gitlab.exceptions.GitlabDeleteError: 298 # Ignore if branch was already deleted 299 pass 300 301 302@pytest.fixture(scope="module") 303def project_file(project): 304 """File fixture for tests requiring a project with files and branches.""" 305 project_file = project.files.create( 306 { 307 "file_path": "README", 308 "branch": "master", 309 "content": "Initial content", 310 "commit_message": "Initial commit", 311 } 312 ) 313 314 return project_file 315 316 317@pytest.fixture(scope="function") 318def release(project, project_file): 319 _id = uuid.uuid4().hex 320 name = f"test-release-{_id}" 321 322 project.refresh() # Gets us the current default branch 323 release = project.releases.create( 324 { 325 "name": name, 326 "tag_name": _id, 327 "description": "description", 328 "ref": project.default_branch, 329 } 330 ) 331 332 return release 333 334 335@pytest.fixture(scope="module") 336def user(gl): 337 """User fixture for user API resource tests.""" 338 _id = uuid.uuid4().hex 339 email = f"user{_id}@email.com" 340 username = f"user{_id}" 341 name = f"User {_id}" 342 password = "fakepassword" 343 344 user = gl.users.create(email=email, username=username, name=name, password=password) 345 346 yield user 347 348 try: 349 user.delete() 350 except gitlab.exceptions.GitlabDeleteError as e: 351 print(f"User already deleted: {e}") 352 353 354@pytest.fixture(scope="module") 355def issue(project): 356 """Issue fixture for issue API resource tests.""" 357 _id = uuid.uuid4().hex 358 data = {"title": f"Issue {_id}", "description": f"Issue {_id} description"} 359 360 return project.issues.create(data) 361 362 363@pytest.fixture(scope="module") 364def milestone(project): 365 _id = uuid.uuid4().hex 366 data = {"title": f"milestone{_id}"} 367 368 return project.milestones.create(data) 369 370 371@pytest.fixture(scope="module") 372def label(project): 373 """Label fixture for project label API resource tests.""" 374 _id = uuid.uuid4().hex 375 data = { 376 "name": f"prjlabel{_id}", 377 "description": f"prjlabel1 {_id} description", 378 "color": "#112233", 379 } 380 381 return project.labels.create(data) 382 383 384@pytest.fixture(scope="module") 385def group_label(group): 386 """Label fixture for group label API resource tests.""" 387 _id = uuid.uuid4().hex 388 data = { 389 "name": f"grplabel{_id}", 390 "description": f"grplabel1 {_id} description", 391 "color": "#112233", 392 } 393 394 return group.labels.create(data) 395 396 397@pytest.fixture(scope="module") 398def variable(project): 399 """Variable fixture for project variable API resource tests.""" 400 _id = uuid.uuid4().hex 401 data = {"key": f"var{_id}", "value": f"Variable {_id}"} 402 403 return project.variables.create(data) 404 405 406@pytest.fixture(scope="module") 407def deploy_token(project): 408 """Deploy token fixture for project deploy token API resource tests.""" 409 _id = uuid.uuid4().hex 410 data = { 411 "name": f"token-{_id}", 412 "username": "root", 413 "expires_at": "2021-09-09", 414 "scopes": "read_registry", 415 } 416 417 return project.deploytokens.create(data) 418 419 420@pytest.fixture(scope="module") 421def group_deploy_token(group): 422 """Deploy token fixture for group deploy token API resource tests.""" 423 _id = uuid.uuid4().hex 424 data = { 425 "name": f"group-token-{_id}", 426 "username": "root", 427 "expires_at": "2021-09-09", 428 "scopes": "read_registry", 429 } 430 431 return group.deploytokens.create(data) 432 433 434@pytest.fixture(scope="session") 435def GPG_KEY(): 436 return """-----BEGIN PGP PUBLIC KEY BLOCK----- 437 438mQENBFn5mzYBCADH6SDVPAp1zh/hxmTi0QplkOfExBACpuY6OhzNdIg+8/528b3g 439Y5YFR6T/HLv/PmeHskUj21end1C0PNG2T9dTx+2Vlh9ISsSG1kyF9T5fvMR3bE0x 440Dl6S489CXZrjPTS9SHk1kF+7dwjUxLJyxF9hPiSihFefDFu3NeOtG/u8vbC1mewQ 441ZyAYue+mqtqcCIFFoBz7wHKMWjIVSJSyTkXExu4OzpVvy3l2EikbvavI3qNz84b+ 442Mgkv/kiBlNoCy3CVuPk99RYKZ3lX1vVtqQ0OgNGQvb4DjcpyjmbKyibuZwhDjIOh 443au6d1OyEbayTntd+dQ4j9EMSnEvm/0MJ4eXPABEBAAG0G0dpdGxhYlRlc3QxIDxm 444YWtlQGZha2UudGxkPokBNwQTAQgAIQUCWfmbNgIbAwULCQgHAgYVCAkKCwIEFgID 445AQIeAQIXgAAKCRBgxELHf8f3hF3yB/wNJlWPKY65UsB4Lo0hs1OxdxCDqXogSi0u 4466crDEIiyOte62pNZKzWy8TJcGZvznRTZ7t8hXgKFLz3PRMcl+vAiRC6quIDUj+2V 447eYfwaItd1lUfzvdCaC7Venf4TQ74f5vvNg/zoGwE6eRoSbjlLv9nqsxeA0rUBUQL 448LYikWhVMP3TrlfgfduYvh6mfgh57BDLJ9kJVpyfxxx9YLKZbaas9sPa6LgBtR555 449JziUxHmbEv8XCsUU8uoFeP1pImbNBplqE3wzJwzOMSmmch7iZzrAwfN7N2j3Wj0H 450B5kQddJ9dmB4BbU0IXGhWczvdpxboI2wdY8a1JypxOdePoph/43iuQENBFn5mzYB 451CADnTPY0Zf3d9zLjBNgIb3yDl94uOcKCq0twNmyjMhHzGqw+UMe9BScy34GL94Al 452xFRQoaL+7P8hGsnsNku29A/VDZivcI+uxTx4WQ7OLcn7V0bnHV4d76iky2ufbUt/ 453GofthjDs1SonePO2N09sS4V4uK0d5N4BfCzzXgvg8etCLxNmC9BGt7AaKUUzKBO4 4542QvNNaC2C/8XEnOgNWYvR36ylAXAmo0sGFXUsBCTiq1fugS9pwtaS2JmaVpZZ3YT 455pMZlS0+SjC5BZYFqSmKCsA58oBRzCxQz57nR4h5VEflgD+Hy0HdW0UHETwz83E6/ 456U0LL6YyvhwFr6KPq5GxinSvfABEBAAGJAR8EGAEIAAkFAln5mzYCGwwACgkQYMRC 457x3/H94SJgwgAlKQb10/xcL/epdDkR7vbiei7huGLBpRDb/L5fM8B5W77Qi8Xmuqj 458cCu1j99ZCA5hs/vwVn8j8iLSBGMC5gxcuaar/wtmiaEvT9fO/h6q4opG7NcuiJ8H 459wRj8ccJmRssNqDD913PLz7T40Ts62blhrEAlJozGVG/q7T3RAZcskOUHKeHfc2RI 460YzGsC/I9d7k6uxAv1L9Nm5F2HaAQDzhkdd16nKkGaPGR35cT1JLInkfl5cdm7ldN 461nxs4TLO3kZjUTgWKdhpgRNF5hwaz51ZjpebaRf/ZqRuNyX4lIRolDxzOn/+O1o8L 462qG2ZdhHHmSK2LaQLFiSprUkikStNU9BqSQ== 463=5OGa 464-----END PGP PUBLIC KEY BLOCK-----""" 465 466 467@pytest.fixture(scope="session") 468def SSH_KEY(): 469 return ( 470 "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDZAjAX8vTiHD7Yi3/EzuVaDChtih" 471 "79HyJZ6H9dEqxFfmGA1YnncE0xujQ64TCebhkYJKzmTJCImSVkOu9C4hZgsw6eE76n" 472 "+Cg3VwEeDUFy+GXlEJWlHaEyc3HWioxgOALbUp3rOezNh+d8BDwwqvENGoePEBsz5l" 473 "a6WP5lTi/HJIjAl6Hu+zHgdj1XVExeH+S52EwpZf/ylTJub0Bl5gHwf/siVE48mLMI" 474 "sqrukXTZ6Zg+8EHAIvIQwJ1dKcXe8P5IoLT7VKrbkgAnolS0I8J+uH7KtErZJb5oZh" 475 "S4OEwsNpaXMAr+6/wWSpircV2/e7sFLlhlKBC4Iq1MpqlZ7G3p foo@bar" 476 ) 477 478 479@pytest.fixture(scope="session") 480def DEPLOY_KEY(): 481 return ( 482 "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFdRyjJQh+1niBpXqE2I8dzjG" 483 "MXFHlRjX9yk/UfOn075IdaockdU58sw2Ai1XIWFpZpfJkW7z+P47ZNSqm1gzeXI" 484 "rtKa9ZUp8A7SZe8vH4XVn7kh7bwWCUirqtn8El9XdqfkzOs/+FuViriUWoJVpA6" 485 "WZsDNaqINFKIA5fj/q8XQw+BcS92L09QJg9oVUuH0VVwNYbU2M2IRmSpybgC/gu" 486 "uWTrnCDMmLItksATifLvRZwgdI8dr+q6tbxbZknNcgEPrI2jT0hYN9ZcjNeWuyv" 487 "rke9IepE7SPBT41C+YtUX4dfDZDmczM1cE0YL/krdUCfuZHMa4ZS2YyNd6slufc" 488 "vn bar@foo" 489 ) 490