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