Fix bugs with the exifread library update
[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
8d75091d 28from mediagoblin.db.models import User, MediaEntry, MediaComment
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.
c7c26b17 191 id = int(data["object"]["id"].split("/")[-2])
9c602458 192 media = MediaEntry.query.filter_by(id=id).first()
8d75091d
JT
193 media.uploader = self.other_user.id
194 media.save()
195
196 # Now lets try and edit the image as self.user, this should produce a 403 error.
9246a6ba 197 with self.mock_oauth():
8d75091d
JT
198 with pytest.raises(AppError) as excinfo:
199 test_app.post(
200 "/api/user/{0}/feed".format(self.user.username),
201 json.dumps(activity),
202 headers=headers
203 )
204
6430ae97 205 assert "403 FORBIDDEN" in excinfo.value.args[0]
57c6473a 206
51ab5192
JT
207 def test_upload_image_with_filename(self, test_app):
208 """ Tests that you can upload an image with filename and description """
209 response, data = self._upload_image(test_app, GOOD_JPG)
210 response, data = self._post_image_to_feed(test_app, data)
ee9956c3 211
51ab5192
JT
212 image = data["object"]
213
214 # Now we need to add a title and description
215 title = "My image ^_^"
216 description = "This is my super awesome image :D"
217 license = "CC-BY-SA"
218
219 image["displayName"] = title
220 image["content"] = description
221 image["license"] = license
222
223 activity = {"verb": "update", "object": image}
224
9246a6ba 225 with self.mock_oauth():
ee9956c3
JT
226 response = test_app.post(
227 "/api/user/{0}/feed".format(self.user.username),
51ab5192
JT
228 json.dumps(activity),
229 headers={"Content-Type": "application/json"}
ee9956c3
JT
230 )
231
21cbf829 232 image = json.loads(response.body.decode())["object"]
51ab5192
JT
233
234 # Check everything has been set on the media correctly
c7c26b17 235 id = int(image["id"].split("/")[-2])
9c602458 236 media = MediaEntry.query.filter_by(id=id).first()
51ab5192
JT
237 assert media.title == title
238 assert media.description == description
239 assert media.license == license
240
241 # Check we're being given back everything we should on an update
c7c26b17 242 assert int(image["id"].split("/")[-2]) == media.id
51ab5192
JT
243 assert image["displayName"] == title
244 assert image["content"] == description
245 assert image["license"] == license
246
ee9956c3
JT
247
248 def test_only_uploaders_post_image(self, test_app):
249 """ Test that only uploaders can upload images """
250 # Remove uploader permissions from user
251 take_away_privileges(self.user.username, u"uploader")
252
253 # Now try and upload a image
254 data = open(GOOD_JPG, "rb").read()
255 headers = {
256 "Content-Type": "image/jpeg",
257 "Content-Length": str(len(data)),
258 }
259
9246a6ba 260 with self.mock_oauth():
967df5ef 261 with pytest.raises(AppError) as excinfo:
a14d90c2 262 test_app.post(
967df5ef
JT
263 "/api/user/{0}/uploads".format(self.user.username),
264 data,
265 headers=headers
266 )
57c6473a 267
ee9956c3 268 # Assert that we've got a 403
6430ae97 269 assert "403 FORBIDDEN" in excinfo.value.args[0]
51ab5192 270
3c8bd177
JT
271 def test_object_endpoint(self, test_app):
272 """ Tests that object can be looked up at endpoint """
273 # Post an image
274 response, data = self._upload_image(test_app, GOOD_JPG)
275 response, data = self._post_image_to_feed(test_app, data)
276
277 # Now lookup image to check that endpoint works.
278 image = data["object"]
279
280 assert "links" in image
281 assert "self" in image["links"]
282
283 # Get URI and strip testing host off
284 object_uri = image["links"]["self"]["href"]
285 object_uri = object_uri.replace("http://localhost:80", "")
286
9246a6ba 287 with self.mock_oauth():
3c8bd177
JT
288 request = test_app.get(object_uri)
289
1db2bd3f 290 image = json.loads(request.body.decode())
c7c26b17 291 entry_id = int(image["id"].split("/")[-2])
9c602458 292 entry = MediaEntry.query.filter_by(id=entry_id).first()
3c8bd177
JT
293
294 assert request.status_code == 200
3c8bd177
JT
295
296 assert "image" in image
297 assert "fullImage" in image
298 assert "pump_io" in image
299 assert "links" in image
51ab5192
JT
300
301 def test_post_comment(self, test_app):
302 """ Tests that I can post an comment media """
303 # Upload some media to comment on
304 response, data = self._upload_image(test_app, GOOD_JPG)
305 response, data = self._post_image_to_feed(test_app, data)
306
307 content = "Hai this is a comment on this lovely picture ^_^"
308
309 activity = {
310 "verb": "post",
311 "object": {
312 "objectType": "comment",
313 "content": content,
314 "inReplyTo": data["object"],
315 }
316 }
317
318 response, comment_data = self._activity_to_feed(test_app, activity)
319 assert response.status_code == 200
320
321 # Find the objects in the database
c7c26b17 322 media_id = int(data["object"]["id"].split("/")[-2])
9c602458 323 media = MediaEntry.query.filter_by(id=media_id).first()
51ab5192
JT
324 comment = media.get_comments()[0]
325
326 # Tests that it matches in the database
327 assert comment.author == self.user.id
328 assert comment.content == content
329
330 # Test that the response is what we should be given
51ab5192 331 assert comment.content == comment_data["object"]["content"]
8d75091d 332
8917ffb1
JT
333 def test_unable_to_post_comment_as_someone_else(self, test_app):
334 """ Tests that you're unable to post a comment as someone else. """
335 # Upload some media to comment on
336 response, data = self._upload_image(test_app, GOOD_JPG)
337 response, data = self._post_image_to_feed(test_app, data)
8d75091d 338
8917ffb1
JT
339 activity = {
340 "verb": "post",
341 "object": {
342 "objectType": "comment",
343 "content": "comment commenty comment ^_^",
344 "inReplyTo": data["object"],
345 }
346 }
8d75091d 347
8917ffb1
JT
348 headers = {
349 "Content-Type": "application/json",
350 }
8d75091d 351
9246a6ba 352 with self.mock_oauth():
8917ffb1
JT
353 with pytest.raises(AppError) as excinfo:
354 test_app.post(
355 "/api/user/{0}/feed".format(self.other_user.username),
356 json.dumps(activity),
357 headers=headers
358 )
8d75091d 359
6430ae97 360 assert "403 FORBIDDEN" in excinfo.value.args[0]
8917ffb1 361
8d75091d
JT
362 def test_unable_to_update_someone_elses_comment(self, test_app):
363 """ Test that you're able to update someoen elses comment. """
364 # Upload some media to comment on
365 response, data = self._upload_image(test_app, GOOD_JPG)
366 response, data = self._post_image_to_feed(test_app, data)
367
368 activity = {
369 "verb": "post",
370 "object": {
371 "objectType": "comment",
372 "content": "comment commenty comment ^_^",
373 "inReplyTo": data["object"],
374 }
375 }
376
377 headers = {
378 "Content-Type": "application/json",
379 }
380
381 # Post the comment.
382 response, comment_data = self._activity_to_feed(test_app, activity)
383
384 # change who uploaded the comment as it's easier than changing
c7c26b17 385 comment_id = int(comment_data["object"]["id"].split("/")[-2])
8d75091d
JT
386 comment = MediaComment.query.filter_by(id=comment_id).first()
387 comment.author = self.other_user.id
9246a6ba 388 comment.save()
8d75091d
JT
389
390 # Update the comment as someone else.
391 comment_data["object"]["content"] = "Yep"
392 activity = {
393 "verb": "update",
394 "object": comment_data["object"]
395 }
396
9246a6ba 397 with self.mock_oauth():
8d75091d
JT
398 with pytest.raises(AppError) as excinfo:
399 test_app.post(
400 "/api/user/{0}/feed".format(self.user.username),
401 json.dumps(activity),
402 headers=headers
403 )
404
6430ae97 405 assert "403 FORBIDDEN" in excinfo.value.args[0]
51ab5192
JT
406
407 def test_profile(self, test_app):
408 """ Tests profile endpoint """
409 uri = "/api/user/{0}/profile".format(self.user.username)
9246a6ba 410 with self.mock_oauth():
51ab5192 411 response = test_app.get(uri)
21cbf829 412 profile = json.loads(response.body.decode())
51ab5192
JT
413
414 assert response.status_code == 200
415
416 assert profile["preferredUsername"] == self.user.username
417 assert profile["objectType"] == "person"
418
419 assert "links" in profile
8ac7a653 420
9246a6ba
JT
421 def test_user(self, test_app):
422 """ Test the user endpoint """
423 uri = "/api/user/{0}/".format(self.user.username)
424 with self.mock_oauth():
425 response = test_app.get(uri)
21cbf829 426 user = json.loads(response.body.decode())
57c6473a 427
9246a6ba 428 assert response.status_code == 200
57c6473a 429
9246a6ba
JT
430 assert user["nickname"] == self.user.username
431 assert user["updated"] == self.user.created.isoformat()
432 assert user["published"] == self.user.created.isoformat()
57c6473a 433
9246a6ba
JT
434 # Test profile exists but self.test_profile will test the value
435 assert "profile" in response
57c6473a 436
5e5d4458
JT
437 def test_whoami_without_login(self, test_app):
438 """ Test that whoami endpoint returns error when not logged in """
439 with pytest.raises(AppError) as excinfo:
440 response = test_app.get("/api/whoami")
57c6473a 441
6430ae97 442 assert "401 UNAUTHORIZED" in excinfo.value.args[0]
57c6473a 443
9246a6ba
JT
444 def test_read_feed(self, test_app):
445 """ Test able to read objects from the feed """
446 response, data = self._upload_image(test_app, GOOD_JPG)
447 response, data = self._post_image_to_feed(test_app, data)
57c6473a 448
9246a6ba
JT
449 uri = "/api/user/{0}/feed".format(self.active_user.username)
450 with self.mock_oauth():
451 response = test_app.get(uri)
21cbf829 452 feed = json.loads(response.body.decode())
57c6473a 453
9246a6ba 454 assert response.status_code == 200
57c6473a 455
9246a6ba
JT
456 # Check it has the attributes it should
457 assert "displayName" in feed
458 assert "objectTypes" in feed
459 assert "url" in feed
460 assert "links" in feed
461 assert "author" in feed
462 assert "items" in feed
57c6473a 463
9246a6ba
JT
464 # Check that image i uploaded is there
465 assert feed["items"][0]["verb"] == "post"
466 assert feed["items"][0]["actor"]
57c6473a 467
9246a6ba
JT
468 def test_cant_post_to_someone_elses_feed(self, test_app):
469 """ Test that can't post to someone elses feed """
470 response, data = self._upload_image(test_app, GOOD_JPG)
471 self.active_user = self.other_user
57c6473a 472
9246a6ba
JT
473 with self.mock_oauth():
474 with pytest.raises(AppError) as excinfo:
475 self._post_image_to_feed(test_app, data)
57c6473a 476
6430ae97 477 assert "403 FORBIDDEN" in excinfo.value.args[0]
57c6473a 478
f6bad0eb 479 def test_object_endpoint_requestable(self, test_app):
9246a6ba
JT
480 """ Test that object endpoint can be requested """
481 response, data = self._upload_image(test_app, GOOD_JPG)
482 response, data = self._post_image_to_feed(test_app, data)
483 object_id = data["object"]["id"]
57c6473a 484
9246a6ba
JT
485 with self.mock_oauth():
486 response = test_app.get(data["object"]["links"]["self"]["href"])
21cbf829 487 data = json.loads(response.body.decode())
57c6473a 488
9246a6ba 489 assert response.status_code == 200
57c6473a 490
9246a6ba
JT
491 assert object_id == data["id"]
492 assert "url" in data
493 assert "links" in data
494 assert data["objectType"] == "image"
4dec1cd6
JT
495
496 def test_delete_media_by_activity(self, test_app):
497 """ Test that an image can be deleted by a delete activity to feed """
498 response, data = self._upload_image(test_app, GOOD_JPG)
499 response, data = self._post_image_to_feed(test_app, data)
500 object_id = data["object"]["id"]
501
502 activity = {
503 "verb": "delete",
504 "object": {
505 "id": object_id,
506 "objectType": "image",
507 }
508 }
509
510 response = self._activity_to_feed(test_app, activity)[1]
511
512 # Check the media is no longer in the database
513 media_id = int(object_id.split("/")[-2])
514 media = MediaEntry.query.filter_by(id=media_id).first()
515
516 assert media is None
517
518 # Check we've been given the full delete activity back
519 assert "id" in response
520 assert response["verb"] == "delete"
521 assert "object" in response
522 assert response["object"]["id"] == object_id
523 assert response["object"]["objectType"] == "image"
524
525 def test_delete_comment_by_activity(self, test_app):
526 """ Test that a comment is deleted by a delete activity to feed """
527 # First upload an image to comment against
528 response, data = self._upload_image(test_app, GOOD_JPG)
529 response, data = self._post_image_to_feed(test_app, data)
530
531 # Post a comment to delete
532 activity = {
533 "verb": "post",
534 "object": {
535 "objectType": "comment",
536 "content": "This is a comment.",
537 "inReplyTo": data["object"],
538 }
539 }
540
541 comment = self._activity_to_feed(test_app, activity)[1]
542
543 # Now delete the image
544 activity = {
545 "verb": "delete",
546 "object": {
547 "id": comment["object"]["id"],
548 "objectType": "comment",
549 }
550 }
551
552 delete = self._activity_to_feed(test_app, activity)[1]
553
554 # Verify the comment no longer exists
555 comment_id = int(comment["object"]["id"].split("/")[-2])
556 assert MediaComment.query.filter_by(id=comment_id).first() is None
557
558 # Check we've got a delete activity back
559 assert "id" in delete
560 assert delete["verb"] == "delete"
561 assert "object" in delete
562 assert delete["object"]["id"] == comment["object"]["id"]
563 assert delete["object"]["objectType"] == "comment"
9e715bb0
JT
564
565 def test_edit_comment(self, test_app):
566 """ Test that someone can update their own comment """
567 # First upload an image to comment against
568 response, data = self._upload_image(test_app, GOOD_JPG)
569 response, data = self._post_image_to_feed(test_app, data)
570
571 # Post a comment to edit
572 activity = {
573 "verb": "post",
574 "object": {
575 "objectType": "comment",
576 "content": "This is a comment",
577 "inReplyTo": data["object"],
578 }
579 }
580
581 comment = self._activity_to_feed(test_app, activity)[1]
582
583 # Now create an update activity to change the content
584 activity = {
585 "verb": "update",
586 "object": {
587 "id": comment["object"]["id"],
588 "content": "This is my fancy new content string!",
589 "objectType": "comment",
590 },
591 }
592
593 comment = self._activity_to_feed(test_app, activity)[1]
594
595 # Verify the comment reflects the changes
596 comment_id = int(comment["object"]["id"].split("/")[-2])
597 model = MediaComment.query.filter_by(id=comment_id).first()
598
599 assert model.content == activity["object"]["content"]
600