Skip to content

Commit 99423ae

Browse files
committed
Move file tests from test_room_routes -> test_files
1 parent 87199b0 commit 99423ae

File tree

2 files changed

+282
-284
lines changed

2 files changed

+282
-284
lines changed

tests/test_files.py

Lines changed: 282 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,294 @@
1-
from nacl.utils import random
2-
from request import sogs_post, sogs_post_raw
1+
from request import sogs_get, sogs_post, sogs_put, sogs_post_raw, sogs_delete
32
from util import config_override, from_now, pad64
43
from sogs.model.file import File
4+
import sogs.model.exc
55
from sogs import utils
6+
import sogs.config
7+
import urllib
8+
from werkzeug.http import parse_options_header
9+
from os import path
10+
from nacl.utils import random
11+
from random import Random
12+
import pytest
13+
import re
614

715

816
def _make_file_upload(filename):
917
return random(1024), {"Content-Disposition": ('attachment', {'filename': filename})}
1018

1119

20+
def test_owned_files(client, room, room2, user, admin):
21+
# - upload a file via new endpoints
22+
filedata, headers = _make_file_upload('fug-1.jpeg')
23+
r = sogs_post_raw(client, f'/room/{room.token}/file', filedata, user, extra_headers=headers)
24+
assert r.status_code == 201
25+
assert 'id' in r.json
26+
f1 = File(id=r.json.get('id'))
27+
# - verify that the file expiry is 1h from now (±1s)
28+
assert f1.expiry == from_now.hours(1)
29+
# - add a post that references the file
30+
d, s = (utils.encode_base64(x) for x in (b"post data", pad64("fugg")))
31+
post_info = {'data': d, 'signature': s, 'files': [f1.id]}
32+
r = sogs_post(client, f'/room/{room.token}/message', post_info, user)
33+
assert r.status_code == 201
34+
assert 'id' in r.json
35+
post_id = r.json.get('id')
36+
# - verify that the file expiry is 15 days from now (±1s)
37+
f1 = File(id=f1.id)
38+
assert f1.expiry == from_now.days(15)
39+
# - verify that the file is correctly associated with the post
40+
assert f1.post_id == post_id
41+
42+
# - upload another file
43+
filedata, headers = _make_file_upload('fug-2.jpeg')
44+
r = sogs_post_raw(client, f'/room/{room.token}/file', filedata, user, extra_headers=headers)
45+
assert r.status_code == 201
46+
assert 'id' in r.json
47+
f2 = File(id=r.json.get('id'))
48+
# - verify the new file exp is ~1h
49+
assert f2.expiry == from_now.hours(1)
50+
# - edit the post with the edit referencing both files
51+
d, s = (utils.encode_base64(x) for x in (b"better post data", pad64("fugg")))
52+
new_post_info = {'data': d, 'signature': s, 'files': [f2.id]}
53+
r = sogs_put(client, f'/room/{room.token}/message/{post_id}', new_post_info, user)
54+
assert r.status_code == 200
55+
# - verify the new file exp is ~15 days
56+
f2 = File(id=f2.id)
57+
assert f2.expiry == from_now.days(15)
58+
# - verify that the second file is correctly associated with the post
59+
assert f2.post_id == post_id
60+
# - verify that the old file exp hasn't changed
61+
f1 = File(id=f1.id)
62+
assert f1.expiry == from_now.days(15)
63+
# - pin the post
64+
room.pin(post_id, admin)
65+
# - verify that expiry of both files is now NULL
66+
f1 = File(id=f1.id)
67+
f2 = File(id=f2.id)
68+
assert f1.expiry is None and f2.expiry is None
69+
# - unpin the post
70+
room.unpin(post_id, admin)
71+
# - verify that expiry of both is reset to 15d
72+
f1 = File(id=f1.id)
73+
f2 = File(id=f2.id)
74+
assert (f1.expiry, f2.expiry) == (from_now.days(15), from_now.days(15))
75+
76+
# - make another post that references one of the first post's file
77+
filedata, headers = _make_file_upload('another.png')
78+
r = sogs_post_raw(client, f'/room/{room.token}/file', filedata, user, extra_headers=headers)
79+
assert r.status_code == 201
80+
f3 = File(id=r.json['id'])
81+
assert f3.expiry == from_now.hours(1)
82+
d, s = (utils.encode_base64(x) for x in (b"more post data", pad64("fsdf")))
83+
post_info = {'data': d, 'signature': s, 'files': [f1.id, f3.id]}
84+
r = sogs_put(client, f'/room/{room.token}/message/{post_id}', post_info, user)
85+
assert r.status_code == 200
86+
f3 = File(id=f3.id)
87+
assert f3.expiry == from_now.days(15)
88+
89+
# - make sure the first post associated message hasn't changed (i.e. no stealing owned uploads)
90+
f1a = File(id=f1.id)
91+
assert f1a.expiry == f1.expiry and f1a.post_id == post_id
92+
93+
# - upload a file and set it as the room image
94+
filedata, headers = _make_file_upload('room-image.png')
95+
r = sogs_post_raw(client, f'/room/{room.token}/file', filedata, user, extra_headers=headers)
96+
room_img = r.json['id']
97+
assert r.status_code == 201
98+
r = sogs_put(client, f'/room/{room.token}', {'image': room_img}, admin)
99+
assert r.status_code == 200
100+
101+
# - verify that the uploaded file expiry and message are both NULL
102+
f_room = File(id=room_img)
103+
assert f_room.post_id is None
104+
assert f_room.expiry is None
105+
106+
# - make a post referencing the room image ID
107+
d, s = (utils.encode_base64(x) for x in (b"post xyz", pad64("z")))
108+
post_info = {'data': d, 'signature': s, 'files': [room_img]}
109+
r = sogs_put(client, f'/room/{room.token}/message/{post_id}', post_info, user)
110+
assert r.status_code == 200
111+
112+
# - verify that the pinned image expiry and message are still both NULL
113+
f_room = File(id=f_room.id)
114+
assert f_room.post_id is None
115+
assert f_room.expiry is None
116+
117+
# - delete the first post
118+
r = sogs_delete(client, f'/room/{room.token}/message/{post_id}', user)
119+
assert r.status_code == 200
120+
121+
# - verify that both attachments are now expired
122+
f1 = File(id=f1.id)
123+
f2 = File(id=f2.id)
124+
assert (f1.expiry, f2.expiry) == (0.0, 0.0)
125+
126+
from sogs.cleanup import cleanup
127+
128+
# Cleanup should remove 3 attachments: the two originals plus the one we added via an edit:
129+
assert cleanup() == (3, 0, 0, 0, 0)
130+
131+
with pytest.raises(sogs.model.exc.NoSuchFile):
132+
f1 = File(id=f1.id)
133+
with pytest.raises(sogs.model.exc.NoSuchFile):
134+
f2 = File(id=f2.id)
135+
136+
137+
def test_no_file_crosspost(client, room, room2, user, global_admin):
138+
# Disallow cross-room references (i.e. a post attaching a file uploaded to another room)
139+
filedata, headers = _make_file_upload('room2-file.jpg')
140+
r = sogs_post_raw(client, f'/room/{room2.token}/file', filedata, user, extra_headers=headers)
141+
assert r.status_code == 201
142+
f = File(id=r.json['id'])
143+
d, s = (utils.encode_base64(x) for x in (b"room1 post", pad64("sig123")))
144+
post_info = {'data': d, 'signature': s, 'files': [f.id]}
145+
r = sogs_post(client, f'/room/{room.token}/message', post_info, user)
146+
assert r.status_code == 201
147+
148+
f = File(id=f.id)
149+
# The file isn't for a post in room 1, so shouldn't have been associated:
150+
assert f.post_id is None
151+
assert f.expiry == from_now.hours(1)
152+
153+
# Disallow setting the room image to some foreign room's upload
154+
r = sogs_put(client, f'/room/{room.token}', {'image': f.id}, global_admin)
155+
assert r.status_code == 406
156+
157+
158+
def _file_upload(client, room, user, *, unsafe=False, utf=False, filename):
159+
160+
url_post = f"/room/{room.token}/file"
161+
file_content = random(1024)
162+
filename_escaped = urllib.parse.quote(filename.encode('utf-8'))
163+
r = sogs_post_raw(
164+
client,
165+
url_post,
166+
file_content,
167+
user,
168+
extra_headers={"Content-Disposition": f"attachment; filename*=UTF-8''{filename_escaped}"},
169+
)
170+
assert r.status_code == 201
171+
assert 'id' in r.json
172+
id = r.json.get('id')
173+
assert id is not None
174+
assert id != 0
175+
r = sogs_get(client, f'/room/{room.token}/file/{id}', user)
176+
assert r.status_code == 200
177+
assert r.data == file_content
178+
expected = ('attachment', {'filename': filename.replace('\0', '\ufffd').replace('/', '\ufffd')})
179+
assert parse_options_header(r.headers.get('content-disposition')) == expected
180+
f = File(id=id)
181+
if unsafe or utf:
182+
exp_path = f'{id}_' + re.sub(sogs.config.UPLOAD_FILENAME_BAD, "_", filename)
183+
else:
184+
exp_path = f'{id}_{filename}'
185+
assert path.split(f.path)[-1] == exp_path
186+
187+
188+
def test_file_upload(client, room, user):
189+
_file_upload(client, room, user, filename='normal.txt')
190+
191+
192+
def test_file_upload_fuzz(client, room, user):
193+
rng = Random(42)
194+
for _ in range(500):
195+
filename = bytes(rng.getrandbits(8) for _ in range(32)).decode('latin1')
196+
_file_upload(client, room, user, filename=filename, unsafe=True)
197+
198+
199+
def test_file_upload_backslashes(client, room, user):
200+
# When the filename *begins* with 1 or more backslashes then for some reason they all get
201+
# doubled up by the test client, but later backslashes don't. We switched to produce the
202+
# UTF-8 encoded filename header ourself; this test is to make sure this doesn't reoccur.
203+
_file_upload(client, room, user, filename='\\abc', unsafe=True)
204+
_file_upload(client, room, user, filename='\\\\abc', unsafe=True)
205+
206+
207+
def test_file_upload_unsafe(client, room, user):
208+
_file_upload(client, room, user, filename='ass,asss---ass../../../asd', unsafe=True)
209+
_file_upload(client, room, user, filename='/dev/null', unsafe=True)
210+
_file_upload(client, room, user, filename='/proc/self/exe', unsafe=True)
211+
_file_upload(client, room, user, filename='%0a%0d%%%%', unsafe=True)
212+
213+
214+
def test_file_upload_emoji(client, room, user):
215+
_file_upload(client, room, user, filename='🎉.txt', utf=True)
216+
217+
218+
def test_file_upload_emoji_extra(client, room, user):
219+
_file_upload(client, room, user, filename='🎉.🎉', utf=True)
220+
221+
222+
def test_file_upload_emoji_unsafe(client, room, user):
223+
_file_upload(client, room, user, filename='🎉.🎉---../../../asd', unsafe=True, utf=True)
224+
_file_upload(client, room, user, filename='%00🎉.🎉---../../../asd', unsafe=True, utf=True)
225+
226+
227+
def test_file_upload_banned_user(client, room, banned_user):
228+
url_post = f"/room/{room.token}/file"
229+
r = sogs_post_raw(client, url_post, random(1024), banned_user)
230+
assert r.status_code == 403
231+
232+
233+
def test_file_not_found(client, room, user, banned_user):
234+
filename = 'bogus.exe'
235+
url_get = f'/room/{room.token}/file/99999/{filename}'
236+
r = sogs_get(client, url_get, user)
237+
assert r.status_code == 404
238+
r = sogs_get(client, url_get, banned_user)
239+
assert r.status_code == 403
240+
241+
242+
def test_file_read_false(client, room, user, mod):
243+
filename = 'bogus.XD'
244+
url_post = f"/room/{room.token}/file"
245+
file_content = random(1024)
246+
r = sogs_post_raw(
247+
client,
248+
url_post,
249+
file_content,
250+
user,
251+
extra_headers={"Content-Disposition": ('attachment', {'filename': filename})},
252+
)
253+
assert r.status_code == 201
254+
assert 'id' in r.json
255+
id = r.json['id']
256+
assert id
257+
room.set_permissions(user, mod=mod, read=False)
258+
r = sogs_get(client, f'/room/{room.token}/file/{id}/{filename}', user)
259+
assert r.status_code == 403
260+
261+
262+
def test_file_write_false(client, room, user, mod):
263+
room.set_permissions(user, mod=mod, write=False)
264+
filename = 'bogus.XD'
265+
url_post = f"/room/{room.token}/file"
266+
file_content = random(1024)
267+
r = sogs_post_raw(
268+
client,
269+
url_post,
270+
file_content,
271+
user,
272+
extra_headers={"Content-Disposition": ('attachment', {'filename': filename})},
273+
)
274+
assert r.status_code == 403
275+
276+
277+
def test_file_upload_false(client, room, user, mod):
278+
room.set_permissions(user, mod=mod, upload=False)
279+
filename = 'bogus.XD'
280+
url_post = f"/room/{room.token}/file"
281+
file_content = random(1024)
282+
r = sogs_post_raw(
283+
client,
284+
url_post,
285+
file_content,
286+
user,
287+
extra_headers={"Content-Disposition": ('attachment', {'filename': filename})},
288+
)
289+
assert r.status_code == 403
290+
291+
12292
def test_file_unexpiring(client, room, user):
13293
with config_override(UPLOAD_DEFAULT_EXPIRY=None):
14294
filedata, headers = _make_file_upload('up1.txt')

0 commit comments

Comments
 (0)