Skip to content

Commit fe1905d

Browse files
V19 Fix
1 parent 19d415a commit fe1905d

3 files changed

Lines changed: 306 additions & 34 deletions

File tree

Appointment/tests.py

Lines changed: 269 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,270 @@
1-
from django.test import TestCase
1+
from datetime import datetime, timedelta, time
2+
from unittest.mock import patch
3+
from urllib.parse import unquote
24

3-
# Create your tests here.
5+
from django.test import Client, TestCase
6+
from django.urls import reverse
7+
8+
from app.models import NaturalPerson, Organization, OrganizationType
9+
from Appointment.models import Appoint, LongTermAppoint, Participant, Room, User
10+
from Appointment.utils.web_func import get_hour_time
11+
12+
13+
def _person(username: str, name: str, *, grant_underground: bool = True):
14+
user = User.objects.create_user(
15+
username,
16+
name,
17+
User.Type.STUDENT,
18+
password='test',
19+
is_newuser=False,
20+
)
21+
person = NaturalPerson.objects.create(
22+
user,
23+
name=name,
24+
identity=NaturalPerson.Identity.STUDENT,
25+
status=NaturalPerson.GraduateStatus.UNDERGRADUATED,
26+
)
27+
if grant_underground:
28+
person.grant_permission('underground_appointment')
29+
else:
30+
person.revoke_permission('underground_appointment')
31+
return user, person
32+
33+
34+
class CheckoutSidIdorTest(TestCase):
35+
"""V19:POST /underground/check_out 不得信任客户端 Sid/Sname。"""
36+
37+
def setUp(self):
38+
self.attacker_user, _ = _person('S100001', '攻击者甲')
39+
self.victim_user, _ = _person('S100002', '受害者乙')
40+
self.attacker = Participant.objects.create(Sid=self.attacker_user)
41+
self.victim = Participant.objects.create(Sid=self.victim_user)
42+
self.room = Room.objects.create(
43+
Rid='B104T',
44+
Rtitle='B104 研讨/活动室',
45+
Rmin=0,
46+
Rmax=10,
47+
Rstart=time(8, 0),
48+
Rfinish=time(22, 0),
49+
Rstatus=Room.Status.PERMITTED,
50+
)
51+
start = datetime.now().replace(
52+
hour=20, minute=0, second=0, microsecond=0,
53+
) + timedelta(days=2)
54+
self.startid = (start.hour - self.room.Rstart.hour) * 2
55+
starttime, _ = get_hour_time(self.room, self.startid)
56+
endtime, _ = get_hour_time(self.room, self.startid + 1)
57+
self.checkout_url = reverse('Appointment:checkout_appoint')
58+
self.slot_query = {
59+
'Rid': self.room.Rid,
60+
'weekday': start.strftime('%a'),
61+
'startid': str(self.startid),
62+
'endid': str(self.startid),
63+
}
64+
self.slot = {
65+
**self.slot_query,
66+
'year': str(start.year),
67+
'month': str(start.month),
68+
'day': str(start.day),
69+
'starttime': starttime,
70+
'endtime': endtime,
71+
}
72+
self.base_form = {
73+
**self.slot,
74+
'non_yp_num': '0',
75+
'Ausage': 'V19 测试',
76+
'announcement': '',
77+
}
78+
patchers = [
79+
patch(
80+
'Appointment.appoint.manage.set_scheduler',
81+
return_value=True,
82+
),
83+
patch(
84+
'Appointment.appoint.manage.set_appoint_reminder',
85+
return_value=True,
86+
),
87+
patch(
88+
'Appointment.appoint.manage.notify_appoint',
89+
return_value=True,
90+
),
91+
patch('Appointment.appoint.manage.unlock_achievement'),
92+
patch('Appointment.views._notify_longterm_review'),
93+
]
94+
self.mocks = [p.start() for p in patchers]
95+
self.addCleanup(lambda: [p.stop() for p in patchers])
96+
self.notify_mock = self.mocks[2]
97+
98+
def _post(self, extra=None, client=None, **overrides):
99+
data = dict(self.base_form)
100+
if extra:
101+
data.update(extra)
102+
data.update(overrides)
103+
http = client if client is not None else self.client
104+
return http.post(self.checkout_url, data=data)
105+
106+
def _assert_success_redirect(self, response):
107+
self.assertEqual(response.status_code, 302)
108+
self.assertIn('成功', unquote(response.url))
109+
110+
def _assert_only_attacker_appoint(self):
111+
self.assertEqual(
112+
Appoint.objects.filter(major_student=self.victim).count(), 0,
113+
)
114+
appoint = Appoint.objects.get()
115+
self.assertEqual(appoint.major_student_id, self.attacker.pk)
116+
students = list(appoint.students.all())
117+
self.assertIn(self.attacker, students)
118+
self.assertNotIn(self.victim, students)
119+
return appoint
120+
121+
def test_own_sid_creates_for_current_user(self):
122+
self.client.force_login(self.attacker_user)
123+
response = self._post(
124+
Sid=self.attacker.get_id(), Sname=self.attacker.name,
125+
)
126+
self._assert_success_redirect(response)
127+
appoint = self._assert_only_attacker_appoint()
128+
self.notify_mock.assert_called()
129+
notified = self.notify_mock.call_args[0][0]
130+
self.assertEqual(notified.pk, appoint.pk)
131+
self.assertEqual(notified.major_student_id, self.attacker.pk)
132+
133+
def test_post_other_sid_cannot_create_for_victim(self):
134+
self.client.force_login(self.attacker_user)
135+
response = self._post(
136+
Sid=self.victim.get_id(), Sname=self.victim.name,
137+
)
138+
self._assert_success_redirect(response)
139+
appoint = self._assert_only_attacker_appoint()
140+
notified = self.notify_mock.call_args[0][0]
141+
self.assertEqual(notified.major_student_id, self.attacker.pk)
142+
self.assertNotEqual(notified.major_student_id, self.victim.pk)
143+
self.assertEqual(appoint.pk, notified.pk)
144+
145+
def test_missing_empty_or_unknown_sid_stays_current_user(self):
146+
self.client.force_login(self.attacker_user)
147+
cases = [
148+
{},
149+
{'Sid': '', 'Sname': self.victim.name},
150+
{'Sid': 'S999999', 'Sname': '不存在'},
151+
]
152+
for extra in cases:
153+
with self.subTest(extra=extra):
154+
Appoint.objects.all().delete()
155+
self.notify_mock.reset_mock()
156+
response = self._post(extra)
157+
self._assert_success_redirect(response)
158+
self._assert_only_attacker_appoint()
159+
160+
def test_sname_swap_does_not_change_initiator(self):
161+
self.client.force_login(self.attacker_user)
162+
response = self._post(
163+
Sid=self.attacker.get_id(), Sname=self.victim.name,
164+
)
165+
self._assert_success_redirect(response)
166+
self._assert_only_attacker_appoint()
167+
168+
def test_get_does_not_create_appointment(self):
169+
self.client.force_login(self.attacker_user)
170+
response = self.client.get(self.checkout_url, self.slot_query)
171+
self.assertEqual(response.status_code, 200)
172+
self.assertEqual(Appoint.objects.count(), 0)
173+
body = response.content.decode()
174+
self.assertNotIn('name="Sid"', body)
175+
self.assertNotIn("name='Sid'", body)
176+
self.assertNotIn('name="Sname"', body)
177+
178+
def test_unauthenticated_post_creates_nothing(self):
179+
response = self._post(
180+
Sid=self.victim.get_id(), Sname=self.victim.name,
181+
)
182+
self.assertEqual(response.status_code, 302)
183+
self.assertTrue(response.url.startswith('/'))
184+
self.assertEqual(Appoint.objects.count(), 0)
185+
186+
def test_no_underground_permission_creates_nothing(self):
187+
blocked_user, _ = _person(
188+
'S100003', '无权限丙', grant_underground=False,
189+
)
190+
Participant.objects.create(Sid=blocked_user)
191+
self.client.force_login(blocked_user)
192+
response = self._post(
193+
Sid=self.victim.get_id(), Sname=self.victim.name,
194+
)
195+
self.assertEqual(response.status_code, 302)
196+
self.assertEqual(Appoint.objects.count(), 0)
197+
198+
def test_csrf_rejected_without_or_with_bad_token(self):
199+
csrf_client = Client(enforce_csrf_checks=True)
200+
csrf_client.force_login(self.attacker_user)
201+
get_response = csrf_client.get(self.checkout_url, self.slot_query)
202+
self.assertEqual(get_response.status_code, 200)
203+
missing = csrf_client.post(self.checkout_url, self.base_form)
204+
self.assertEqual(missing.status_code, 403)
205+
bad = csrf_client.post(
206+
self.checkout_url,
207+
{**self.base_form, 'csrfmiddlewaretoken': 'invalid'},
208+
)
209+
self.assertEqual(bad.status_code, 403)
210+
self.assertEqual(Appoint.objects.count(), 0)
211+
212+
def test_csrf_valid_token_still_binds_session_user(self):
213+
csrf_client = Client(enforce_csrf_checks=True)
214+
csrf_client.force_login(self.attacker_user)
215+
get_response = csrf_client.get(self.checkout_url, self.slot_query)
216+
self.assertEqual(get_response.status_code, 200)
217+
token = csrf_client.cookies['csrftoken'].value
218+
response = csrf_client.post(self.checkout_url, {
219+
**self.base_form,
220+
'Sid': self.victim.get_id(),
221+
'Sname': self.victim.name,
222+
'csrfmiddlewaretoken': token,
223+
})
224+
self._assert_success_redirect(response)
225+
self._assert_only_attacker_appoint()
226+
227+
def test_longterm_initiator_stays_session_user(self):
228+
self.attacker.longterm = True
229+
self.attacker.save(update_fields=['longterm'])
230+
self.client.force_login(self.attacker_user)
231+
response = self._post(
232+
Sid=self.victim.get_id(),
233+
Sname=self.victim.name,
234+
longterm='on',
235+
times='1',
236+
interval='1',
237+
start_week='0',
238+
)
239+
self._assert_success_redirect(response)
240+
appoint = self._assert_only_attacker_appoint()
241+
longterm = LongTermAppoint.objects.get()
242+
self.assertEqual(longterm.applicant_id, self.attacker.pk)
243+
self.assertEqual(longterm.appoint_id, appoint.pk)
244+
245+
def test_org_account_cannot_impersonate_person(self):
246+
incharge = NaturalPerson.objects.get(person_id=self.attacker_user)
247+
otype = OrganizationType.objects.create(
248+
otype_id=9001,
249+
otype_name='测试类型',
250+
incharge=incharge,
251+
job_name_list=['负责人', '成员'],
252+
)
253+
org_user = User.objects.create_user(
254+
'org_a', '组织甲', User.Type.ORG,
255+
password='test', is_newuser=False,
256+
)
257+
Organization.objects.create(
258+
organization_id=org_user, oname='组织甲', otype=otype,
259+
)
260+
org_part = Participant.objects.create(Sid=org_user)
261+
self.client.force_login(org_user)
262+
response = self._post(
263+
Sid=self.victim.get_id(), Sname=self.victim.name,
264+
)
265+
self._assert_success_redirect(response)
266+
self.assertEqual(
267+
Appoint.objects.filter(major_student=self.victim).count(), 0,
268+
)
269+
appoint = Appoint.objects.get()
270+
self.assertEqual(appoint.major_student_id, org_part.pk)

0 commit comments

Comments
 (0)