diff --git a/build_tools/create_basic_gitlab.py b/build_tools/create_basic_gitlab.py index c24d2618cf4c50c329efb61d775adcc59197f094..bd3b3649368db609bc8994ba59cf8dbb0c7b18f8 100644 --- a/build_tools/create_basic_gitlab.py +++ b/build_tools/create_basic_gitlab.py @@ -6,6 +6,8 @@ from urllib.parse import urljoin from typing import Any, Dict, List, cast +from travo.gitlab import GitLabTest + def get_user(username: str) -> User: users = cast(List[User], gl.users.list(username=username)) @@ -58,35 +60,13 @@ gitlab_oauth_token = resp_data["access_token"] gl = gitlab.Gitlab(gitlab_url, oauth_token=gitlab_oauth_token) # create users -users_data = [ - { - "username": "student1", - "email": "travo@gmail.com", - "name": "Étudiant de test pour travo", - "password": "aqwzsx(t1", - "can_create_group": "True", - }, - { - "username": "student2", - "email": "student2@foo.bar", - "name": "Student 2", - "password": "aqwzsx(t2", - }, - { - "username": "instructor1", - "email": "instructor1@foo.bar", - "name": "Instructor 1", - "password": "aqwzsx(t3", - }, - { - "username": "instructor2", - "email": "instructor2@foo.bar", - "name": "Instructor 2", - "password": "aqwzsx(t4", - }, -] - -users = {user_data["username"]: create_user(user_data) for user_data in users_data} +users_data = GitLabTest.users + +users = { + user_data["username"]: create_user(user_data) + for user_data in users_data + if user_data["username"] != "root" +} user = users["student1"] diff --git a/conftest.py b/conftest.py index 1e1e93df63400dbc058b20c037ea28e411dee74c..47c714845b94ce09e66cc34f15de176992f2278a 100644 --- a/conftest.py +++ b/conftest.py @@ -165,13 +165,12 @@ def to_be_teared_down() -> Iterator[Callable[[Resource], None]]: for resource in reversed(resources): forge = resource.gitlab assert isinstance(forge, GitLabTest) - forge.login("root", "dr0w554p!&ew=]gdS") - if isinstance(resource, Group): - forge.remove_group(resource.id) - else: - assert isinstance(resource, Project) - forge.remove_project(resource.id) - forge.logout() + with forge.logged_as("root"): + if isinstance(resource, Group): + forge.remove_group(resource.id) + else: + assert isinstance(resource, Project) + forge.remove_project(resource.id) @pytest.fixture @@ -211,6 +210,28 @@ def rich_course(gitlab: GitLabTest, test_run_id: str) -> Course: ) +@pytest.fixture +def rich_course_deployed(gitlab: GitLabTest, rich_course: Course) -> Iterator[Course]: + with gitlab.logged_as("instructor1"): + rich_course.forge.ensure_group( + path=rich_course.path, + name=rich_course.name, + visibility="public", + ) + + assert rich_course.session_name is not None + rich_course.forge.ensure_group( + path=rich_course.assignments_group_path, + name=rich_course.session_name, + visibility="public", + ) + + yield rich_course + + with gitlab.logged_as("instructor1"): + rich_course.forge.remove_group(rich_course.path) + + @pytest.fixture def course_assignment_group(course: Course) -> Group: course.forge.ensure_group(path=course.path, name=course.name, visibility="public") diff --git a/travo/gitlab.py b/travo/gitlab.py index 2643e143be81db602728aa9182f18f26524660eb..4ee2f188bba01c092058c656ad5438abd6577def 100644 --- a/travo/gitlab.py +++ b/travo/gitlab.py @@ -1,4 +1,5 @@ import base64 +import contextlib from dataclasses import dataclass, field, InitVar, fields import enum import fnmatch @@ -17,6 +18,7 @@ import pathlib import typing import typing_utils # type: ignore from typing import ( + Iterator, Optional, List, Sequence, @@ -2658,8 +2660,40 @@ class GitLabTest(GitLab): """ base_url: str = "http://gitlab/" - username: str = "student1" - password: str = "aqwzsx(t1" + + # The following data should be shared with build_tools/create_basic_gitlab.py + users = [ + { + "username": "root", + "password": "dr0w554p!&ew=]gdS", + }, + { + "username": "student1", + "email": "travo@gmail.com", + "name": "Étudiant de test pour travo", + "password": "aqwzsx(t1", + "can_create_group": "True", + }, + { + "username": "student2", + "email": "student2@foo.bar", + "name": "Student 2", + "password": "aqwzsx(t2", + }, + { + "username": "instructor1", + "email": "instructor1@foo.bar", + "name": "Instructor 1", + "password": "aqwzsx(t3", + }, + { + "username": "instructor2", + "email": "instructor2@foo.bar", + "name": "Instructor 2", + "password": "aqwzsx(t4", + }, + ] + passwords = {user["username"]: user["password"] for user in users} def __init__(self, base_url: str = base_url) -> None: if "GITLAB_HOST" in os.environ and "GITLAB_80_TCP_PORT" in os.environ: @@ -2674,8 +2708,89 @@ class GitLabTest(GitLab): def login( self, - username: Optional[str] = username, - password: Optional[str] = password, + username: Optional[str] = None, + password: Optional[str] = None, anonymous_ok: bool = False, ) -> None: + """ + Ensure that this GitLabTest session is authenticated + + This behaves as GitLab.login, with two additional features: + + - If a username is given from the list of predefined users in + `self.users`, then the password is filled by default + automatically. + + - If no username is provided, and not logged in, then the + session is authenticated as "student1" (mimicking the usual + behavior of the current user typing the credentials + interactively). + """ + if username is None and self.token is None: + username = "student1" + if username is not None: + password = self.passwords.get(username) super().login(username=username, password=password, anonymous_ok=anonymous_ok) + self.log.info(f"LOGIN: {self.get_current_user().username} {username}") + assert ( + username is None + or anonymous_ok + or self.get_current_user().username == username + ) + + @contextlib.contextmanager + def logged_as( + self, + username: Optional[str] = None, + password: Optional[str] = None, + anonymous_ok: bool = False, + ) -> Iterator: + """ + Defines a context for this gitlab session with temporary + authentication as another user + + Example: + + >>> gitlab = getfixture('gitlab') + >>> gitlab.login('student1') + >>> gitlab.get_current_user().username + 'student1' + >>> with gitlab.logged_as('student2'): + ... gitlab.get_current_user().username + 'student2' + >>> gitlab.get_current_user().username + 'student1' + + >>> gitlab.logout() + >>> gitlab.get_current_user() + Traceback (most recent call last): + ... + requests.exceptions.HTTPError:... + >>> with gitlab.logged_as('student2'): + ... gitlab.get_current_user().username + 'student2' + >>> gitlab.get_current_user() + Traceback (most recent call last): + ... + requests.exceptions.HTTPError:... + + >>> gitlab.login('anonymous', anonymous_ok=True) + >>> gitlab.get_current_user().username + 'anonymous' + >>> with gitlab.logged_as('student2'): + ... gitlab.get_current_user().username + 'student2' + >>> gitlab.get_current_user().username # doctest: +SKIP + 'student1' + """ + save_token = self.token + try: + self.logout() + assert self._current_user is None + self.login(username=username, password=password, anonymous_ok=anonymous_ok) + yield self + finally: + self.logout() + if save_token is not None: + self.set_token(save_token) + # Maybe something needs to be done to restore anonymous login? diff --git a/travo/tests/test_course.py b/travo/tests/test_course.py index 901c9bfc451a21ad32e45fbab638415cbe092d3b..a7d67d9355765d07a19eac424f799ac3a0860f86 100644 --- a/travo/tests/test_course.py +++ b/travo/tests/test_course.py @@ -3,7 +3,7 @@ import pytest import os from travo.console_scripts import Travo from travo.course import Course -from travo.gitlab import GitLabTest +from travo.utils import working_directory @pytest.mark.parametrize("embed_option", [False, True]) @@ -47,10 +47,10 @@ def test_group_submission_true(rich_course): ) -def test_check_course_parameters(): +def test_check_course_parameters(gitlab): i18n.set("locale", "en") course = Course( - forge=GitLabTest(), + forge=gitlab, path="Info111", name="Info 111 Programmation Impérative", student_dir="~/ProgImperative", @@ -77,71 +77,70 @@ def test_course_share_with(course): i18n.set("locale", "en") with pytest.raises(RuntimeError, match="No submission on GitLab"): - course.share_with(username="travo-test-etu", assignment_name="Assignment1") + course.share_with(username="student1", assignment_name="Assignment1") -def test_course_collect(gitlab, rich_course, to_be_teared_down, user_name, tmp_path): - gitlab.login("instructor1", "aqwzsx(t3") - rich_course.forge.ensure_group( - path=rich_course.path, - name=rich_course.name, - visibility="public", - ) - session_path = rich_course.path + "/" + rich_course.session_path - rich_course.forge.ensure_group( - path=session_path, - name=rich_course.session_name, - visibility="public", - ) +def test_course_collect( + gitlab, rich_course_deployed, to_be_teared_down, user_name, tmp_path +): + rich_course = rich_course_deployed assignments = rich_course.assignments student_groups = rich_course.student_groups - - for group in student_groups: - group_path = session_path + "/" + group - group_name = group - rich_course.forge.ensure_group( - path=group_path, name=group_name, visibility="public" - ) - for assignment in assignments: - path = rich_course.assignments_group_path + "/" + assignment - project = rich_course.forge.ensure_project( - path=path, name=assignment, visibility="public" + # Deploy the course assignments ("Assignment1" and "Assignment2") + with gitlab.logged_as("instructor1"): + for group in student_groups: + group_path = rich_course.assignments_group_path + "/" + group + group_name = group + rich_course.forge.ensure_group( + path=group_path, name=group_name, visibility="public" ) - project.ensure_file("README.md", branch="master") - path = group_path + "/" + assignment - project.ensure_fork(path, assignment, visibility="public") - - gitlab.logout() - os.chdir(tmp_path) - gitlab.login("student1", "aqwzsx(t1") - rich_course.fetch("Assignment1") - rich_course.submit("Assignment1", student_group="Group1") - - rich_course.collect("Assignment1") - assert os.path.isdir(user_name) - - rich_course.collect( - "Assignment1", - student_group="Group1", - template="collect-{path}-Group1/{username}", - ) - assert os.path.isdir(f"collect-Assignment1-Group1/{user_name}") + for assignment in assignments: + path = rich_course.assignments_group_path + "/" + assignment + project = rich_course.forge.ensure_project( + path=path, name=assignment, visibility="public" + ) + project.ensure_file("README.md", branch="master") + path = group_path + "/" + assignment + project.ensure_fork( + path, assignment, visibility="public", initialized=True + ) + + assignment = rich_course.assignment("Assignment1", student_group="Group1") + for student_name in ["student1", "student2"]: + with gitlab.logged_as(student_name): + to_be_teared_down(assignment.ensure_submission_repo(initialized=True)) + + with gitlab.logged_as("instructor1"): + with working_directory(tmp_path): + rich_course.collect("Assignment1") + assert os.path.isdir(os.path.join(tmp_path, "student1")) + assert os.path.isdir(os.path.join(tmp_path, "student2")) + + rich_course.collect( + "Assignment1", + student_group="Group1", + template="collect-{path}-Group1/{username}", + ) + assert os.path.isdir("collect-Assignment1-Group1/student1") + assert os.path.isdir("collect-Assignment1-Group1/student2") - rich_course.collect( - "Assignment1", - student_group="Group2", - template="collect-{path}-Group2/{username}", - ) - assert not os.path.isdir(f"collect-Assignment1-Group2/{user_name}") + rich_course.collect( + "Assignment1", + student_group="Group2", + template="collect-{path}-Group2/{username}", + ) + assert not os.path.isdir("collect-Assignment1-Group2/student1") - rich_course.collect_in_submitted( - "Assignment1", - student_group="Group1", - ) - assert os.path.isdir(f"submitted/{user_name}") + rich_course.collect_in_submitted( + "Assignment1", + student_group="Group1", + ) + assert os.path.isdir("submitted/student1") + assert os.path.isdir("submitted/student2") -def test_course_generate_and_release(rich_course): +def test_course_generate_and_release(gitlab, rich_course_deployed): + rich_course = rich_course_deployed rich_course.group_submissions = True source_dir = "./source" release_dir = "./release" @@ -158,31 +157,19 @@ def test_course_generate_and_release(rich_course): rich_course.source_dir = source_dir rich_course.gitlab_ci_yml = "my gitlab-ci.yml file" - # generate the assignment before release - rich_course.generate_assignment(assignment_name) - - assert os.path.isdir(release_dir) - assert os.path.isdir(os.path.join(release_dir, assignment_name)) - assert os.path.isdir(os.path.join(release_dir, assignment_name, ".git")) - assert os.path.isfile(os.path.join(release_dir, assignment_name, ".gitlab-ci.yml")) - assert os.path.isfile(os.path.join(release_dir, assignment_name, ".gitignore")) + with gitlab.logged_as("instructor1"): + # generate the assignment before release + rich_course.generate_assignment(assignment_name) - rich_course.forge.ensure_group( - path=rich_course.path, - name=rich_course.name, - visibility="public", - ) - - rich_course.forge.ensure_group( - path=rich_course.assignments_group_path, - name=rich_course.session_name, - visibility="public", - ) - - rich_course.release( - assignment_name, - path=os.path.join(release_dir, assignment_name), - ) + assert os.path.isdir(release_dir) + assert os.path.isdir(os.path.join(release_dir, assignment_name)) + assert os.path.isdir(os.path.join(release_dir, assignment_name, ".git")) + assert os.path.isfile( + os.path.join(release_dir, assignment_name, ".gitlab-ci.yml") + ) + assert os.path.isfile(os.path.join(release_dir, assignment_name, ".gitignore")) - # Clean after test - rich_course.forge.remove_group(rich_course.assignments_group_path, force=True) + rich_course.release( + assignment_name, + path=os.path.join(release_dir, assignment_name), + )