Fix #5433 - Typo in decorators
[mediagoblin.git] / mediagoblin / tests / test_api.py
CommitLineData
57c6473a
JW
1# GNU MediaGoblin -- federated, autonomous media hosting
2# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU Affero General Public License as published by
6# the Free Software Foundation, either version 3 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Affero General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
ee9956c3 16import json
57c6473a 17
3a02813c
CAW
18try:
19 import mock
20except ImportError:
21 import unittest.mock as mock
5c2ece74
CAW
22import pytest
23
967df5ef
JT
24from webtest import AppError
25
247a3b78 26from .resources import GOOD_JPG
57c6473a 27from mediagoblin import mg_globals
0d053bff 28from mediagoblin.db.models import User, Activity, MediaEntry, TextComment
9c602458 29from mediagoblin.tools.routing import extract_url_arguments
5c2ece74 30from mediagoblin.tests.tools import fixture_add_user
ee9956c3 31from mediagoblin.moderation.tools import take_away_privileges
57c6473a 32
57c6473a 33class TestAPI(object):
a14d90c2 34 """ Test mediagoblin's pump.io complient APIs """
57c6473a 35
ee9956c3
JT
36 @pytest.fixture(autouse=True)
37 def setup(self, test_app):
38 self.test_app = test_app
57c6473a 39 self.db = mg_globals.database
57c6473a 40
5e5d4458 41 self.user = fixture_add_user(privileges=[u'active', u'uploader', u'commenter'])
8917ffb1
JT
42 self.other_user = fixture_add_user(
43 username="otheruser",
44 privileges=[u'active', u'uploader', u'commenter']
45 )
9246a6ba 46 self.active_user = self.user
57c6473a 47
51ab5192
JT
48 def _activity_to_feed(self, test_app, activity, headers=None):
49 """ Posts an activity to the user's feed """
50 if headers:
51 headers.setdefault("Content-Type", "application/json")
52 else:
53 headers = {"Content-Type": "application/json"}
54
9246a6ba 55 with self.mock_oauth():
51ab5192 56 response = test_app.post(
9246a6ba 57 "/api/user/{0}/feed".format(self.active_user.username),
51ab5192
JT
58 json.dumps(activity),
59 headers=headers
60 )
61
21cbf829 62 return response, json.loads(response.body.decode())
51ab5192
JT
63
64 def _upload_image(self, test_app, image):
65 """ Uploads and image to MediaGoblin via pump.io API """
66 data = open(image, "rb").read()
67 headers = {
68 "Content-Type": "image/jpeg",
69 "Content-Length": str(len(data))
70 }
71
72
9246a6ba 73 with self.mock_oauth():
51ab5192 74 response = test_app.post(
9246a6ba 75 "/api/user/{0}/uploads".format(self.active_user.username),
51ab5192
JT
76 data,
77 headers=headers
78 )
21cbf829 79 image = json.loads(response.body.decode())
51ab5192
JT
80
81 return response, image
82
83 def _post_image_to_feed(self, test_app, image):
84 """ Posts an already uploaded image to feed """
85 activity = {
86 "verb": "post",
87 "object": image,
88 }
89
90 return self._activity_to_feed(test_app, activity)
91
967df5ef
JT
92 def mocked_oauth_required(self, *args, **kwargs):
93 """ Mocks mediagoblin.decorator.oauth_required to always validate """
94
95 def fake_controller(controller, request, *args, **kwargs):
9246a6ba 96 request.user = User.query.filter_by(id=self.active_user.id).first()
967df5ef
JT
97 return controller(request, *args, **kwargs)
98
99 def oauth_required(c):
100 return lambda *args, **kwargs: fake_controller(c, *args, **kwargs)
101
102 return oauth_required
103
9246a6ba
JT
104 def mock_oauth(self):
105 """ Returns a mock.patch for the oauth_required decorator """
106 return mock.patch(
107 target="mediagoblin.decorators.oauth_required",
108 new_callable=self.mocked_oauth_required
109 )
110
ee9956c3
JT
111 def test_can_post_image(self, test_app):
112 """ Tests that an image can be posted to the API """
113 # First request we need to do is to upload the image
51ab5192 114 response, image = self._upload_image(test_app, GOOD_JPG)
ee9956c3 115
51ab5192
JT
116 # I should have got certain things back
117 assert response.status_code == 200
ee9956c3 118
51ab5192
JT
119 assert "id" in image
120 assert "fullImage" in image
121 assert "url" in image["fullImage"]
122 assert "url" in image
123 assert "author" in image
124 assert "published" in image
125 assert "updated" in image
126 assert image["objectType"] == "image"
247a3b78 127
51ab5192
JT
128 # Check that we got the response we're expecting
129 response, _ = self._post_image_to_feed(test_app, image)
130 assert response.status_code == 200
8d75091d 131
8917ffb1
JT
132 def test_unable_to_upload_as_someone_else(self, test_app):
133 """ Test that can't upload as someoen else """
134 data = open(GOOD_JPG, "rb").read()
135 headers = {
136 "Content-Type": "image/jpeg",
137 "Content-Length": str(len(data))
138 }
8d75091d 139
9246a6ba 140 with self.mock_oauth():
8917ffb1
JT
141 # Will be self.user trying to upload as self.other_user
142 with pytest.raises(AppError) as excinfo:
143 test_app.post(
144 "/api/user/{0}/uploads".format(self.other_user.username),
145 data,
146 headers=headers
147 )
8d75091d 148
6430ae97 149 assert "403 FORBIDDEN" in excinfo.value.args[0]
8d75091d 150
8917ffb1
JT
151 def test_unable_to_post_feed_as_someone_else(self, test_app):
152 """ Tests that can't post an image to someone else's feed """
153 response, data = self._upload_image(test_app, GOOD_JPG)
8d75091d 154
8917ffb1
JT
155 activity = {
156 "verb": "post",
157 "object": data
158 }
8d75091d 159
8917ffb1
JT
160 headers = {
161 "Content-Type": "application/json",
162 }
8d75091d 163
9246a6ba 164 with self.mock_oauth():
8917ffb1
JT
165 with pytest.raises(AppError) as excinfo:
166 test_app.post(
167 "/api/user/{0}/feed".format(self.other_user.username),
168 json.dumps(activity),
169 headers=headers
170 )
8d75091d 171
6430ae97 172 assert "403 FORBIDDEN" in excinfo.value.args[0]
8d75091d
JT
173
174 def test_only_able_to_update_own_image(self, test_app):
175 """ Test's that the uploader is the only person who can update an image """
176 response, data = self._upload_image(test_app, GOOD_JPG)
177 response, data = self._post_image_to_feed(test_app, data)
178
179 activity = {
180 "verb": "update",
181 "object": data["object"],
182 }
183
184 headers = {
185 "Content-Type": "application/json",
186 }
187
188 # Lets change the image uploader to be self.other_user, this is easier
189 # than uploading the image as someone else as the way self.mocked_oauth_required
190 # and self._upload_image.
64a456a4 191 media = MediaEntry.query.filter_by(public_id=data["object"]["id"]).first()
0f3bf8d4 192 media.actor = self.other_user.id
8d75091d
JT
193 media.save()
194
195 # Now lets try and edit the image as self.user, this should produce a 403 error.
9246a6ba 196 with self.mock_oauth():
8d75091d
JT
197 with pytest.raises(AppError) as excinfo:
198 test_app.post(
199 "/api/user/{0}/feed".format(self.user.username),
200 json.dumps(activity),
201 headers=headers
202 )
203
6430ae97 204 assert "403 FORBIDDEN" in excinfo.value.args[0]
57c6473a 205
51ab5192
JT
206 def test_upload_image_with_filename(self, test_app):
207 """ Tests that you can upload an image with filename and description """
208 response, data = self._upload_image(test_app, GOOD_JPG)
209 response, data = self._post_image_to_feed(test_app, data)
ee9956c3 210
51ab5192
JT
211 image = data["object"]
212
213 # Now we need to add a title and description
214 title = "My image ^_^"
215 description = "This is my super awesome image :D"
216 license = "CC-BY-SA"
217
218 image["displayName"] = title
219 image["content"] = description
220 image["license"] = license
221
222 activity = {"verb": "update", "object": image}
223
9246a6ba 224 with self.mock_oauth():
ee9956c3
JT
225 response = test_app.post(
226 "/api/user/{0}/feed".format(self.user.username),
51ab5192
JT
227 json.dumps(activity),
228 headers={"Content-Type": "application/json"}
ee9956c3
JT
229 )
230
21cbf829 231 image = json.loads(response.body.decode())["object"]
51ab5192
JT
232
233 # Check everything has been set on the media correctly
64a456a4 234 media = MediaEntry.query.filter_by(public_id=image["id"]).first()
51ab5192
JT
235 assert media.title == title
236 assert media.description == description
237 assert media.license == license
238
239 # Check we're being given back everything we should on an update
64a456a4 240 assert image["id"] == media.public_id
51ab5192
JT
241 assert image["displayName"] == title
242 assert image["content"] == description
243 assert image["license"] == license
244
ee9956c3
JT
245
246 def test_only_uploaders_post_image(self, test_app):
247 """ Test that only uploaders can upload images """
248 # Remove uploader permissions from user
249 take_away_privileges(self.user.username, u"uploader")
250
251 # Now try and upload a image
252 data = open(GOOD_JPG, "rb").read()
253 headers = {
254 "Content-Type": "image/jpeg",
255 "Content-Length": str(len(data)),
256 }
257
9246a6ba 258 with self.mock_oauth():
967df5ef 259 with pytest.raises(AppError) as excinfo:
a14d90c2 260 test_app.post(
967df5ef
JT
261 "/api/user/{0}/uploads".format(self.user.username),
262 data,
263 headers=headers
264 )
57c6473a 265
ee9956c3 266 # Assert that we've got a 403
6430ae97 267 assert "403 FORBIDDEN" in excinfo.value.args[0]
51ab5192 268
3c8bd177
JT
269 def test_object_endpoint(self, test_app):
270 """ Tests that object can be looked up at endpoint """
271 # Post an image
272 response, data = self._upload_image(test_app, GOOD_JPG)
273 response, data = self._post_image_to_feed(test_app, data)
274
275 # Now lookup image to check that endpoint works.
276 image = data["object"]
277
278 assert "links" in image
279 assert "self" in image["links"]
280
281 # Get URI and strip testing host off
282 object_uri = image["links"]["self"]["href"]
283 object_uri = object_uri.replace("http://localhost:80", "")
284
9246a6ba 285 with self.mock_oauth():
3c8bd177
JT
286 request = test_app.get(object_uri)
287
1db2bd3f 288 image = json.loads(request.body.decode())
64a456a4 289 entry = MediaEntry.query.filter_by(public_id=image["id"]).first()
3c8bd177
JT
290
291 assert request.status_code == 200
3c8bd177
JT
292
293 assert "image" in image
294 assert "fullImage" in image
295 assert "pump_io" in image
296 assert "links" in image
51ab5192
JT
297
298 def test_post_comment(self, test_app):
299 """ Tests that I can post an comment media """
300 # Upload some media to comment on
301 response, data = self._upload_image(test_app, GOOD_JPG)
302 response, data = self._post_image_to_feed(test_app, data)
303
304 content = "Hai this is a comment on this lovely picture ^_^"
305
306 activity = {
307 "verb": "post",
308 "object": {
309 "objectType": "comment",
310 "content": content,
311 "inReplyTo": data["object"],
312 }
313 }
314
315 response, comment_data = self._activity_to_feed(test_app, activity)
316 assert response.status_code == 200
317
318 # Find the objects in the database
64a456a4 319 media = MediaEntry.query.filter_by(public_id=data["object"]["id"]).first()
51ab5192
JT
320 comment = media.get_comments()[0]
321
322 # Tests that it matches in the database
0f3bf8d4 323 assert comment.actor == self.user.id
51ab5192
JT
324 assert comment.content == content
325
326 # Test that the response is what we should be given
51ab5192 327 assert comment.content == comment_data["object"]["content"]
8d75091d 328
8917ffb1
JT
329 def test_unable_to_post_comment_as_someone_else(self, test_app):
330 """ Tests that you're unable to post a comment as someone else. """
331 # Upload some media to comment on
332 response, data = self._upload_image(test_app, GOOD_JPG)
333 response, data = self._post_image_to_feed(test_app, data)
8d75091d 334
8917ffb1
JT
335 activity = {
336 "verb": "post",
337 "object": {
338 "objectType": "comment",
339 "content": "comment commenty comment ^_^",
340 "inReplyTo": data["object"],
341 }
342 }
8d75091d 343
8917ffb1
JT
344 headers = {
345 "Content-Type": "application/json",
346 }
8d75091d 347
9246a6ba 348 with self.mock_oauth():
8917ffb1
JT
349 with pytest.raises(AppError) as excinfo:
350 test_app.post(
351 "/api/user/{0}/feed".format(self.other_user.username),
352 json.dumps(activity),
353 headers=headers
354 )
8d75091d 355
6430ae97 356 assert "403 FORBIDDEN" in excinfo.value.args[0]
8917ffb1 357
8d75091d
JT
358 def test_unable_to_update_someone_elses_comment(self, test_app):
359 """ Test that you're able to update someoen elses comment. """
360 # Upload some media to comment on
361 response, data = self._upload_image(test_app, GOOD_JPG)
362 response, data = self._post_image_to_feed(test_app, data)
363
364 activity = {
365 "verb": "post",
366 "object": {
367 "objectType": "comment",
368 "content": "comment commenty comment ^_^",
369 "inReplyTo": data["object"],
370 }
371 }
372
373 headers = {
374 "Content-Type": "application/json",
375 }
376
377 # Post the comment.
378 response, comment_data = self._activity_to_feed(test_app, activity)
379
380 # change who uploaded the comment as it's easier than changing
64a456a4 381 comment = TextComment.query.filter_by(public_id=comment_data["object"]["id"]).first()
0f3bf8d4 382 comment.actor = self.other_user.id
9246a6ba 383 comment.save()
8d75091d
JT
384
385 # Update the comment as someone else.
386 comment_data["object"]["content"] = "Yep"
387 activity = {
388 "verb": "update",
389 "object": comment_data["object"]
390 }
391
9246a6ba 392 with self.mock_oauth():
8d75091d
JT
393 with pytest.raises(AppError) as excinfo:
394 test_app.post(
395 "/api/user/{0}/feed".format(self.user.username),
396 json.dumps(activity),
397 headers=headers
398 )
399
6430ae97 400 assert "403 FORBIDDEN" in excinfo.value.args[0]
51ab5192
JT
401
402 def test_profile(self, test_app):
403 """ Tests profile endpoint """
404 uri = "/api/user/{0}/profile".format(self.user.username)
9246a6ba 405 with self.mock_oauth():
51ab5192 406 response = test_app.get(uri)
21cbf829 407 profile = json.loads(response.body.decode())
51ab5192
JT
408
409 assert response.status_code == 200
410
411 assert profile["preferredUsername"] == self.user.username
412 assert profile["objectType"] == "person"
413
414 assert "links" in profile
8ac7a653 415
9246a6ba
JT
416 def test_user(self, test_app):
417 """ Test the user endpoint """
418 uri = "/api/user/{0}/".format(self.user.username)
419 with self.mock_oauth():
420 response = test_app.get(uri)
21cbf829 421 user = json.loads(response.body.decode())
57c6473a 422
9246a6ba 423 assert response.status_code == 200
57c6473a 424
9246a6ba
JT
425 assert user["nickname"] == self.user.username
426 assert user["updated"] == self.user.created.isoformat()
427 assert user["published"] == self.user.created.isoformat()
57c6473a 428
9246a6ba
JT
429 # Test profile exists but self.test_profile will test the value
430 assert "profile" in response
57c6473a 431
5e5d4458
JT
432 def test_whoami_without_login(self, test_app):
433 """ Test that whoami endpoint returns error when not logged in """
434 with pytest.raises(AppError) as excinfo:
435 response = test_app.get("/api/whoami")
57c6473a 436
6430ae97 437 assert "401 UNAUTHORIZED" in excinfo.value.args[0]
57c6473a 438
9246a6ba
JT
439 def test_read_feed(self, test_app):
440 """ Test able to read objects from the feed """
7c9af02a
LD
441 response, image_data = self._upload_image(test_app, GOOD_JPG)
442 response, data = self._post_image_to_feed(test_app, image_data)
57c6473a 443
9246a6ba
JT
444 uri = "/api/user/{0}/feed".format(self.active_user.username)
445 with self.mock_oauth():
446 response = test_app.get(uri)
21cbf829 447 feed = json.loads(response.body.decode())
57c6473a 448
9246a6ba 449 assert response.status_code == 200
57c6473a 450
9246a6ba
JT
451 # Check it has the attributes it should
452 assert "displayName" in feed
453 assert "objectTypes" in feed
454 assert "url" in feed
455 assert "links" in feed
456 assert "author" in feed
457 assert "items" in feed
57c6473a 458
9246a6ba
JT
459 # Check that image i uploaded is there
460 assert feed["items"][0]["verb"] == "post"
0d053bff
JT
461 assert feed["items"][0]["id"] == data["id"]
462 assert feed["items"][0]["object"]["objectType"] == "image"
463 assert feed["items"][0]["object"]["id"] == data["object"]["id"]
464
7c9af02a
LD
465 default_limit = 20
466 items_count = default_limit * 2
467 for i in range(items_count):
468 response, image_data = self._upload_image(test_app, GOOD_JPG)
469 self._post_image_to_feed(test_app, image_data)
470 items_count += 1 # because there already is one
471
472 #
473 # default returns default_limit items
474 #
475 with self.mock_oauth():
476 response = test_app.get(uri)
477 feed = json.loads(response.body.decode())
478 assert len(feed["items"]) == default_limit
479
480 #
481 # silentely ignore count and offset that that are
482 # not a number
483 #
484 with self.mock_oauth():
485 response = test_app.get(uri + "?count=BAD&offset=WORSE")
486 feed = json.loads(response.body.decode())
487 assert len(feed["items"]) == default_limit
488
489 #
490 # if offset is less than default_limit items
491 # from the end of the feed, return less than
492 # default_limit
493 #
494 with self.mock_oauth():
495 near_the_end = items_count - default_limit / 2
496 response = test_app.get(uri + "?offset=%d" % near_the_end)
497 feed = json.loads(response.body.decode())
498 assert len(feed["items"]) < default_limit
499
500 #
501 # count=5 returns 5 items
502 #
503 with self.mock_oauth():
504 response = test_app.get(uri + "?count=5")
505 feed = json.loads(response.body.decode())
506 assert len(feed["items"]) == 5
0d053bff
JT
507
508 def test_read_another_feed(self, test_app):
509 """ Test able to read objects from someone else's feed """
510 response, data = self._upload_image(test_app, GOOD_JPG)
511 response, data = self._post_image_to_feed(test_app, data)
512
513 # Change the active user to someone else.
514 self.active_user = self.other_user
515
516 # Fetch the feed
517 url = "/api/user/{0}/feed".format(self.user.username)
518 with self.mock_oauth():
519 response = test_app.get(url)
520 feed = json.loads(response.body.decode())
521
522 assert response.status_code == 200
523
524 # Check it has the attributes it ought to.
525 assert "displayName" in feed
526 assert "objectTypes" in feed
527 assert "url" in feed
528 assert "links" in feed
529 assert "author" in feed
530 assert "items" in feed
531
532 # Assert the uploaded image is there
533 assert feed["items"][0]["verb"] == "post"
534 assert feed["items"][0]["id"] == data["id"]
535 assert feed["items"][0]["object"]["objectType"] == "image"
536 assert feed["items"][0]["object"]["id"] == data["object"]["id"]
57c6473a 537
9246a6ba
JT
538 def test_cant_post_to_someone_elses_feed(self, test_app):
539 """ Test that can't post to someone elses feed """
540 response, data = self._upload_image(test_app, GOOD_JPG)
541 self.active_user = self.other_user
57c6473a 542
9246a6ba
JT
543 with self.mock_oauth():
544 with pytest.raises(AppError) as excinfo:
545 self._post_image_to_feed(test_app, data)
57c6473a 546
6430ae97 547 assert "403 FORBIDDEN" in excinfo.value.args[0]
57c6473a 548
f6bad0eb 549 def test_object_endpoint_requestable(self, test_app):
9246a6ba
JT
550 """ Test that object endpoint can be requested """
551 response, data = self._upload_image(test_app, GOOD_JPG)
552 response, data = self._post_image_to_feed(test_app, data)
553 object_id = data["object"]["id"]
57c6473a 554
9246a6ba
JT
555 with self.mock_oauth():
556 response = test_app.get(data["object"]["links"]["self"]["href"])
21cbf829 557 data = json.loads(response.body.decode())
57c6473a 558
9246a6ba 559 assert response.status_code == 200
57c6473a 560
9246a6ba
JT
561 assert object_id == data["id"]
562 assert "url" in data
563 assert "links" in data
564 assert data["objectType"] == "image"
4dec1cd6
JT
565
566 def test_delete_media_by_activity(self, test_app):
567 """ Test that an image can be deleted by a delete activity to feed """
568 response, data = self._upload_image(test_app, GOOD_JPG)
569 response, data = self._post_image_to_feed(test_app, data)
570 object_id = data["object"]["id"]
571
572 activity = {
573 "verb": "delete",
574 "object": {
575 "id": object_id,
576 "objectType": "image",
577 }
578 }
579
580 response = self._activity_to_feed(test_app, activity)[1]
581
582 # Check the media is no longer in the database
64a456a4 583 media = MediaEntry.query.filter_by(public_id=object_id).first()
4dec1cd6
JT
584
585 assert media is None
586
587 # Check we've been given the full delete activity back
588 assert "id" in response
589 assert response["verb"] == "delete"
590 assert "object" in response
591 assert response["object"]["id"] == object_id
592 assert response["object"]["objectType"] == "image"
593
594 def test_delete_comment_by_activity(self, test_app):
595 """ Test that a comment is deleted by a delete activity to feed """
596 # First upload an image to comment against
597 response, data = self._upload_image(test_app, GOOD_JPG)
598 response, data = self._post_image_to_feed(test_app, data)
599
600 # Post a comment to delete
601 activity = {
602 "verb": "post",
603 "object": {
604 "objectType": "comment",
605 "content": "This is a comment.",
606 "inReplyTo": data["object"],
607 }
608 }
609
610 comment = self._activity_to_feed(test_app, activity)[1]
611
612 # Now delete the image
613 activity = {
614 "verb": "delete",
615 "object": {
616 "id": comment["object"]["id"],
617 "objectType": "comment",
618 }
619 }
620
621 delete = self._activity_to_feed(test_app, activity)[1]
622
623 # Verify the comment no longer exists
64a456a4
JT
624 assert TextComment.query.filter_by(public_id=comment["object"]["id"]).first() is None
625 comment_id = comment["object"]["id"]
4dec1cd6
JT
626
627 # Check we've got a delete activity back
628 assert "id" in delete
629 assert delete["verb"] == "delete"
630 assert "object" in delete
631 assert delete["object"]["id"] == comment["object"]["id"]
632 assert delete["object"]["objectType"] == "comment"
9e715bb0
JT
633
634 def test_edit_comment(self, test_app):
635 """ Test that someone can update their own comment """
636 # First upload an image to comment against
637 response, data = self._upload_image(test_app, GOOD_JPG)
638 response, data = self._post_image_to_feed(test_app, data)
639
640 # Post a comment to edit
641 activity = {
642 "verb": "post",
643 "object": {
644 "objectType": "comment",
645 "content": "This is a comment",
646 "inReplyTo": data["object"],
647 }
648 }
649
650 comment = self._activity_to_feed(test_app, activity)[1]
651
652 # Now create an update activity to change the content
653 activity = {
654 "verb": "update",
655 "object": {
656 "id": comment["object"]["id"],
657 "content": "This is my fancy new content string!",
658 "objectType": "comment",
659 },
660 }
661
662 comment = self._activity_to_feed(test_app, activity)[1]
663
664 # Verify the comment reflects the changes
64a456a4 665 model = TextComment.query.filter_by(public_id=comment["object"]["id"]).first()
9e715bb0
JT
666
667 assert model.content == activity["object"]["content"]