Skip to content

Commit c179eae

Browse files
committed
self CR + tests
1 parent 9212e5d commit c179eae

File tree

5 files changed

+190
-40
lines changed

5 files changed

+190
-40
lines changed

lms/lmsweb/config.py.example

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,8 @@ LIMITS_PER_HOUR = 50
6767
# Change password settings
6868
MAX_INVALID_PASSWORD_TRIES = 5
6969

70-
REPOSITORY_FOLDER = os.getenv("REPOSITORY_FOLDER", os.path.abspath(os.path.join(os.curdir, "repositories")))
70+
_REPOSITORY_FOLDER = os.getenv("REPOSITORY_FOLDER", os.path.abspath(os.path.join(os.curdir, "repositories")))
71+
72+
73+
def get_repository_folder():
74+
return _REPOSITORY_FOLDER

lms/lmsweb/git_service.py

Lines changed: 36 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -45,43 +45,16 @@ def repository_folder(self) -> str:
4545

4646
def handle_operation(self) -> flask.Response:
4747
git_operation = self._extract_git_operation()
48+
git_repository_folder = os.path.join(self.repository_folder, 'config')
4849

49-
git_repository_folder = os.path.join(self.repository_folder, '.git')
5050
first_time_repository = not os.path.exists(git_repository_folder)
5151
if first_time_repository:
52-
os.makedirs(self.repository_folder, exist_ok=True)
53-
proc = subprocess.Popen( # noqa: S603
54-
args=['git', 'init', '--bare'],
55-
stdin=subprocess.PIPE,
56-
stdout=subprocess.PIPE,
57-
stderr=subprocess.PIPE,
58-
cwd=self.repository_folder,
59-
)
60-
if proc.wait() != 0:
61-
_logger.error(
62-
'Failed to execute command. stdout=%s\nstderr=%s',
63-
proc.stdout.read(), proc.stderr.read(),
64-
)
65-
raise EnvironmentError
52+
self._initialize_bare_repository()
6653

6754
if not git_operation.supported:
6855
raise EnvironmentError
6956

70-
proc = subprocess.Popen( # noqa: S603
71-
args=git_operation.service_command,
72-
stdin=subprocess.PIPE,
73-
stdout=subprocess.PIPE,
74-
stderr=subprocess.PIPE,
75-
cwd=self._base_repository_folder,
76-
)
77-
data_out, _ = proc.communicate(self._request.data, 20)
78-
79-
if proc.wait() != 0:
80-
_logger.error(
81-
'Failed to execute command. stdout=%s\nstderr=%s',
82-
proc.stdout.read(), proc.stderr.read(),
83-
)
84-
raise EnvironmentError
57+
data_out = self._execute_git_operation(git_operation)
8558

8659
if git_operation.format_response:
8760
data_out = git_operation.format_response(data_out)
@@ -99,13 +72,45 @@ def handle_operation(self) -> flask.Response:
9972
res = self.build_response(data_out, git_operation)
10073
return res
10174

75+
def _execute_git_operation(self, git_operation: _GitOperation) -> bytes:
76+
proc = subprocess.Popen( # noqa: S603
77+
args=git_operation.service_command,
78+
stdin=subprocess.PIPE,
79+
stdout=subprocess.PIPE,
80+
stderr=subprocess.PIPE,
81+
cwd=self._base_repository_folder,
82+
)
83+
data_out, _ = proc.communicate(self._request.data, 20)
84+
if proc.wait() != 0:
85+
_logger.error(
86+
'Failed to execute command. stdout=%s\nstderr=%s',
87+
proc.stdout.read(), proc.stderr.read(),
88+
)
89+
raise EnvironmentError
90+
return data_out
91+
92+
def _initialize_bare_repository(self):
93+
os.makedirs(self.repository_folder, exist_ok=True)
94+
proc = subprocess.Popen( # noqa: S603
95+
args=['git', 'init', '--bare'],
96+
stdin=subprocess.PIPE,
97+
stdout=subprocess.PIPE,
98+
stderr=subprocess.PIPE,
99+
cwd=self.repository_folder,
100+
)
101+
if proc.wait() != 0:
102+
_logger.error(
103+
'Failed to execute command. stdout=%s\nstderr=%s',
104+
proc.stdout.read(), proc.stderr.read(),
105+
)
106+
raise EnvironmentError
107+
102108
@staticmethod
103109
def build_response(
104110
data_out: bytes,
105111
git_operation: _GitOperation,
106112
) -> flask.Response:
107113
res = flask.make_response(data_out)
108-
res.headers['Expires'] = 'Fri, 01 Jan 1980 00:00:00 GMT'
109114
res.headers['Pragma'] = 'no-cache'
110115
res.headers['Cache-Control'] = 'no-cache, max-age=0, must-revalidate'
111116
res.headers['Content-Type'] = git_operation.response_content_type

lms/lmsweb/views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
)
2525
from lms.lmsweb.config import (
2626
CONFIRMATION_TIME, LANGUAGES, LIMITS_PER_HOUR,
27-
LIMITS_PER_MINUTE, LOCALE, MAX_UPLOAD_SIZE, REPOSITORY_FOLDER,
27+
LIMITS_PER_MINUTE, LOCALE, MAX_UPLOAD_SIZE, get_repository_folder,
2828
)
2929
from lms.lmsweb.forms.change_password import ChangePasswordForm
3030
from lms.lmsweb.forms.register import RegisterForm
@@ -554,7 +554,7 @@ def git_handler(exercise_id: int):
554554
user=http_basic_auth.current_user(),
555555
exercise_id=exercise_id,
556556
request=request,
557-
base_repository_folder=REPOSITORY_FOLDER,
557+
base_repository_folder=get_repository_folder(),
558558
)
559559
return git_service.handle_operation()
560560

tests/conftest.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
from lms.models import notifications
2626

2727

28+
FAKE_PASSWORD = 'fake pass'
29+
30+
2831
@pytest.fixture(autouse=True, scope='session')
2932
def db_in_memory():
3033
"""Binds all models to in-memory SQLite and creates all tables`"""
@@ -126,7 +129,7 @@ def get_logged_user(username: str) -> FlaskClient:
126129
client = webapp.test_client()
127130
client.post('/login', data={ # noqa: S106
128131
'username': username,
129-
'password': 'fake pass',
132+
'password': FAKE_PASSWORD,
130133
}, follow_redirects=True)
131134
return client
132135

@@ -186,12 +189,11 @@ def create_user(
186189
role_name: str = RoleOptions.STUDENT.value, index: int = 1,
187190
) -> User:
188191
username = f'{role_name}-{index}'
189-
password = 'fake pass'
190192
return User.create( # NOQA: S106
191193
username=username,
192194
fullname=f'A{role_name}',
193195
mail_address=f'so-{role_name}-{index}@mail.com',
194-
password=password,
196+
password=FAKE_PASSWORD,
195197
api_key='fake key',
196198
role=Role.by_name(role_name),
197199
)
@@ -215,7 +217,7 @@ def create_staff_user(index: int = 0) -> User:
215217

216218
@pytest.fixture()
217219
def staff_password():
218-
return 'fake pass'
220+
return FAKE_PASSWORD
219221

220222

221223
@pytest.fixture
@@ -242,12 +244,11 @@ def student_user():
242244
def admin_user():
243245
admin_role = Role.get(Role.name == RoleOptions.ADMINISTRATOR.value)
244246
username = 'Yam'
245-
password = 'fake pass'
246247
return User.create( # NOQA: B106, S106
247248
username=username,
248249
fullname='Buya',
249250
mail_address='mymail@mail.com',
250-
password=password,
251+
password=FAKE_PASSWORD,
251252
api_key='fake key',
252253
role=admin_role,
253254
)

tests/test_git_solution.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import base64
2+
import os.path
3+
import shutil
4+
import tempfile
5+
6+
from flask.testing import FlaskClient
7+
8+
from lms.lmsdb import models
9+
from lms.lmsweb import config, webapp
10+
from tests import conftest
11+
12+
13+
POST_NEW_REPOSITORY_BUFFER = \
14+
b'00ab0000000000000000000000000000000000000000 ' \
15+
b'c1d42352fc88ae88fde7713c23232d7d0703849a refs/heads/master\x00 ' \
16+
b'report-status-v2 side-band-64k object-format=sha1 ' \
17+
b'agent=git/2.30.10000PACK\x00\x00\x00\x02\x00\x00\x00\x03\x9d\nx' \
18+
b'\x9c\x95\xccA\n\xc3 \x10@\xd1\xbd\xa7p_(3\x8e\x9a\x04J\xe8\xae' \
19+
b'\x07\xe8\t\xa6\x99\xd1\n\x9a\x80\xd8\xfb7\xd0\x13t\xfb\xe1\xfd' \
20+
b'\xd1U\xed$@\xc2\x92\x92\xdf\xd2\x1c\xf1\x15@\x84=\x12\xba\xa4' \
21+
b'\xea\xe6e\x89\x88\x12\x12\x1a\xfe\x8c\xf7\xd1\xed\x83\xab}\x96=k' \
22+
b'\xb7\xb7\xcc\xd5\x93\xbb\xe7\xc6\xa5^\xb7\xa3\xad\x16#\x91\x9b' \
23+
b'\xc0\x07\xb2\x17 \x00s\xd6V\xc6\xd0\xbf\xa1){)\xe34\xbf\x83\xf9' \
24+
b'\x02\xa5\x1f3_\xa0\x02x\x9c340031Q(\xc8,Id(M^\xc86;\xe0\xd1\x1d' \
25+
b'\xefZ\x8bP\x17\x8eU\xd2\x17\xcb\xb6\xc6\x01\x00\xab:\x0b\xe64x' \
26+
b'\x9c+O\xcc\xe6\x02\x00\x03\xe3\x01NvHX\x85>M\xf7I\xd6\x7fGZ' \
27+
b'\x0e^\xc8\x82Q\xe3\xcb\xd9'
28+
29+
30+
POST_CLONE_REPOSITORY_BUFFER = \
31+
b'0098want c1d42352fc88ae88fde7713c23232d7d0703849a multi_ack_detailed' \
32+
b' no-done side-band-64k thin-pack ofs-delta deepen-since deepen-not' \
33+
b' agent=git/2.30.1\n00000009done\n'
34+
35+
36+
class TestSendSolutionFromGit:
37+
INFO_URL = 'info/refs'
38+
GET_METHOD = FlaskClient.get.__name__
39+
POST_METHOD = FlaskClient.post.__name__
40+
41+
temp_folder = None
42+
43+
@classmethod
44+
def setup_class(cls):
45+
cls.temp_folder = tempfile.mkdtemp()
46+
47+
def setup_method(self, method):
48+
config._REPOSITORY_FOLDER = os.path.join(self.temp_folder, method.__name__)
49+
50+
@classmethod
51+
def teardown_class(cls):
52+
if cls.temp_folder and os.path.exists(cls.temp_folder):
53+
shutil.rmtree(cls.temp_folder)
54+
55+
@staticmethod
56+
def _get_formatted_git_url(exercise: models.Exercise, rel_path: str) -> str:
57+
return f'/git/{exercise.id}.git/{rel_path}'
58+
59+
@staticmethod
60+
def _send_git_request(
61+
username: str,
62+
method_name: str,
63+
url: str,
64+
data=None,
65+
service=None,
66+
password=conftest.FAKE_PASSWORD,
67+
):
68+
client = webapp.test_client()
69+
encoded_credentials = base64.b64encode(f'{username}:{password}'.encode()).decode()
70+
headers = (
71+
('Authorization', f'Basic {encoded_credentials}'),
72+
)
73+
query_string = None
74+
if service is not None:
75+
query_string = {'service': service}
76+
return getattr(client, method_name)(url, query_string=query_string, headers=headers, data=data)
77+
78+
def test_not_authorized_access(self, exercise: models.Exercise, student_user: models.User):
79+
client = conftest.get_logged_user(student_user.username)
80+
response = client.get(self._get_formatted_git_url(exercise, self.INFO_URL))
81+
assert response.status_code == 401
82+
83+
def test_not_existing_user(self, exercise: models.Exercise):
84+
response = self._send_git_request(
85+
username='not-exists',
86+
method_name=self.GET_METHOD,
87+
url=self._get_formatted_git_url(exercise, self.INFO_URL),
88+
)
89+
assert response.status_code == 401
90+
91+
def test_invalid_user_password(self, exercise: models.Exercise, student_user: models.User):
92+
response = self._send_git_request(
93+
username=student_user.username,
94+
method_name=self.GET_METHOD,
95+
url=self._get_formatted_git_url(exercise, self.INFO_URL),
96+
password='not real password'
97+
)
98+
assert response.status_code == 401
99+
100+
def test_push_exercise(self, exercise: models.Exercise, student_user: models.User):
101+
git_receive_pack = 'git-receive-pack'
102+
response = self._send_git_request(
103+
username=student_user.username,
104+
method_name=self.GET_METHOD,
105+
url=self._get_formatted_git_url(exercise, self.INFO_URL),
106+
service=git_receive_pack,
107+
)
108+
109+
assert response.status_code == 200
110+
assert response.data.startswith(b'001f#')
111+
112+
response = self._send_git_request(
113+
username=student_user.username,
114+
method_name=self.POST_METHOD,
115+
url=self._get_formatted_git_url(exercise, git_receive_pack),
116+
data=POST_NEW_REPOSITORY_BUFFER,
117+
)
118+
assert response.status_code == 200
119+
assert response.data.startswith(b'0030\x01000eunpack ok\n0019ok refs/heads/master\n00000000')
120+
121+
def test_get_exercise(self, exercise: models.Exercise, student_user: models.User):
122+
git_upload_pack = 'git-upload-pack'
123+
self.test_push_exercise(exercise, student_user)
124+
response = self._send_git_request(
125+
username=student_user.username,
126+
method_name=self.GET_METHOD,
127+
url=self._get_formatted_git_url(exercise, self.INFO_URL),
128+
service=git_upload_pack,
129+
)
130+
assert response.status_code == 200
131+
assert response.data.startswith(b'001e# service=git-upload-pack')
132+
133+
response = self._send_git_request(
134+
username=student_user.username,
135+
method_name=self.POST_METHOD,
136+
url=self._get_formatted_git_url(exercise, git_upload_pack),
137+
data=POST_CLONE_REPOSITORY_BUFFER,
138+
)
139+
assert response.status_code == 200
140+
assert response.data.startswith(b'0008NAK\n0023\x02Enumerating objects: 3, done.')

0 commit comments

Comments
 (0)