1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
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.
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.
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/>.
21 import unittest
.mock
as mock
24 from webtest
import AppError
26 from .resources
import GOOD_JPG
27 from mediagoblin
import mg_globals
28 from mediagoblin
.db
.models
import User
, Activity
, MediaEntry
, TextComment
29 from mediagoblin
.tools
.routing
import extract_url_arguments
30 from mediagoblin
.tests
.tools
import fixture_add_user
31 from mediagoblin
.moderation
.tools
import take_away_privileges
33 class TestAPI(object):
34 """ Test mediagoblin's pump.io complient APIs """
36 @pytest.fixture(autouse
=True)
37 def setup(self
, test_app
):
38 self
.test_app
= test_app
39 self
.db
= mg_globals
.database
41 self
.user
= fixture_add_user(privileges
=[u
'active', u
'uploader', u
'commenter'])
42 self
.other_user
= fixture_add_user(
44 privileges
=[u
'active', u
'uploader', u
'commenter']
46 self
.active_user
= self
.user
48 def _activity_to_feed(self
, test_app
, activity
, headers
=None):
49 """ Posts an activity to the user's feed """
51 headers
.setdefault("Content-Type", "application/json")
53 headers
= {"Content-Type": "application/json"}
55 with self
.mock_oauth():
56 response
= test_app
.post(
57 "/api/user/{0}/feed".format(self
.active_user
.username
),
62 return response
, json
.loads(response
.body
.decode())
64 def _upload_image(self
, test_app
, image
):
65 """ Uploads and image to MediaGoblin via pump.io API """
66 data
= open(image
, "rb").read()
68 "Content-Type": "image/jpeg",
69 "Content-Length": str(len(data
))
73 with self
.mock_oauth():
74 response
= test_app
.post(
75 "/api/user/{0}/uploads".format(self
.active_user
.username
),
79 image
= json
.loads(response
.body
.decode())
81 return response
, image
83 def _post_image_to_feed(self
, test_app
, image
):
84 """ Posts an already uploaded image to feed """
90 return self
._activity
_to
_feed
(test_app
, activity
)
92 def mocked_oauth_required(self
, *args
, **kwargs
):
93 """ Mocks mediagoblin.decorator.oauth_required to always validate """
95 def fake_controller(controller
, request
, *args
, **kwargs
):
96 request
.user
= User
.query
.filter_by(id=self
.active_user
.id).first()
97 return controller(request
, *args
, **kwargs
)
99 def oauth_required(c
):
100 return lambda *args
, **kwargs
: fake_controller(c
, *args
, **kwargs
)
102 return oauth_required
104 def mock_oauth(self
):
105 """ Returns a mock.patch for the oauth_required decorator """
107 target
="mediagoblin.decorators.oauth_required",
108 new_callable
=self
.mocked_oauth_required
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
114 response
, image
= self
._upload
_image
(test_app
, GOOD_JPG
)
116 # I should have got certain things back
117 assert response
.status_code
== 200
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"
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
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()
136 "Content-Type": "image/jpeg",
137 "Content-Length": str(len(data
))
140 with self
.mock_oauth():
141 # Will be self.user trying to upload as self.other_user
142 with pytest
.raises(AppError
) as excinfo
:
144 "/api/user/{0}/uploads".format(self
.other_user
.username
),
149 assert "403 FORBIDDEN" in excinfo
.value
.args
[0]
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
)
161 "Content-Type": "application/json",
164 with self
.mock_oauth():
165 with pytest
.raises(AppError
) as excinfo
:
167 "/api/user/{0}/feed".format(self
.other_user
.username
),
168 json
.dumps(activity
),
172 assert "403 FORBIDDEN" in excinfo
.value
.args
[0]
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
)
181 "object": data
["object"],
185 "Content-Type": "application/json",
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.
191 media
= MediaEntry
.query
.filter_by(public_id
=data
["object"]["id"]).first()
192 media
.actor
= self
.other_user
.id
195 # Now lets try and edit the image as self.user, this should produce a 403 error.
196 with self
.mock_oauth():
197 with pytest
.raises(AppError
) as excinfo
:
199 "/api/user/{0}/feed".format(self
.user
.username
),
200 json
.dumps(activity
),
204 assert "403 FORBIDDEN" in excinfo
.value
.args
[0]
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
)
211 image
= data
["object"]
213 # Now we need to add a title and description
214 title
= "My image ^_^"
215 description
= "This is my super awesome image :D"
218 image
["displayName"] = title
219 image
["content"] = description
220 image
["license"] = license
222 activity
= {"verb": "update", "object": image
}
224 with self
.mock_oauth():
225 response
= test_app
.post(
226 "/api/user/{0}/feed".format(self
.user
.username
),
227 json
.dumps(activity
),
228 headers
={"Content-Type": "application/json"}
231 image
= json
.loads(response
.body
.decode())["object"]
233 # Check everything has been set on the media correctly
234 media
= MediaEntry
.query
.filter_by(public_id
=image
["id"]).first()
235 assert media
.title
== title
236 assert media
.description
== description
237 assert media
.license
== license
239 # Check we're being given back everything we should on an update
240 assert image
["id"] == media
.public_id
241 assert image
["displayName"] == title
242 assert image
["content"] == description
243 assert image
["license"] == license
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")
251 # Now try and upload a image
252 data
= open(GOOD_JPG
, "rb").read()
254 "Content-Type": "image/jpeg",
255 "Content-Length": str(len(data
)),
258 with self
.mock_oauth():
259 with pytest
.raises(AppError
) as excinfo
:
261 "/api/user/{0}/uploads".format(self
.user
.username
),
266 # Assert that we've got a 403
267 assert "403 FORBIDDEN" in excinfo
.value
.args
[0]
269 def test_object_endpoint(self
, test_app
):
270 """ Tests that object can be looked up at endpoint """
272 response
, data
= self
._upload
_image
(test_app
, GOOD_JPG
)
273 response
, data
= self
._post
_image
_to
_feed
(test_app
, data
)
275 # Now lookup image to check that endpoint works.
276 image
= data
["object"]
278 assert "links" in image
279 assert "self" in image
["links"]
281 # Get URI and strip testing host off
282 object_uri
= image
["links"]["self"]["href"]
283 object_uri
= object_uri
.replace("http://localhost:80", "")
285 with self
.mock_oauth():
286 request
= test_app
.get(object_uri
)
288 image
= json
.loads(request
.body
.decode())
289 entry
= MediaEntry
.query
.filter_by(public_id
=image
["id"]).first()
291 assert request
.status_code
== 200
293 assert "image" in image
294 assert "fullImage" in image
295 assert "pump_io" in image
296 assert "links" in image
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
)
304 content
= "Hai this is a comment on this lovely picture ^_^"
309 "objectType": "comment",
311 "inReplyTo": data
["object"],
315 response
, comment_data
= self
._activity
_to
_feed
(test_app
, activity
)
316 assert response
.status_code
== 200
318 # Find the objects in the database
319 media
= MediaEntry
.query
.filter_by(public_id
=data
["object"]["id"]).first()
320 comment
= media
.get_comments()[0]
322 # Tests that it matches in the database
323 assert comment
.actor
== self
.user
.id
324 assert comment
.content
== content
326 # Test that the response is what we should be given
327 assert comment
.content
== comment_data
["object"]["content"]
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
)
338 "objectType": "comment",
339 "content": "comment commenty comment ^_^",
340 "inReplyTo": data
["object"],
345 "Content-Type": "application/json",
348 with self
.mock_oauth():
349 with pytest
.raises(AppError
) as excinfo
:
351 "/api/user/{0}/feed".format(self
.other_user
.username
),
352 json
.dumps(activity
),
356 assert "403 FORBIDDEN" in excinfo
.value
.args
[0]
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
)
367 "objectType": "comment",
368 "content": "comment commenty comment ^_^",
369 "inReplyTo": data
["object"],
374 "Content-Type": "application/json",
378 response
, comment_data
= self
._activity
_to
_feed
(test_app
, activity
)
380 # change who uploaded the comment as it's easier than changing
381 comment
= TextComment
.query
.filter_by(public_id
=comment_data
["object"]["id"]).first()
382 comment
.actor
= self
.other_user
.id
385 # Update the comment as someone else.
386 comment_data
["object"]["content"] = "Yep"
389 "object": comment_data
["object"]
392 with self
.mock_oauth():
393 with pytest
.raises(AppError
) as excinfo
:
395 "/api/user/{0}/feed".format(self
.user
.username
),
396 json
.dumps(activity
),
400 assert "403 FORBIDDEN" in excinfo
.value
.args
[0]
402 def test_profile(self
, test_app
):
403 """ Tests profile endpoint """
404 uri
= "/api/user/{0}/profile".format(self
.user
.username
)
405 with self
.mock_oauth():
406 response
= test_app
.get(uri
)
407 profile
= json
.loads(response
.body
.decode())
409 assert response
.status_code
== 200
411 assert profile
["preferredUsername"] == self
.user
.username
412 assert profile
["objectType"] == "person"
414 assert "links" in profile
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
)
421 user
= json
.loads(response
.body
.decode())
423 assert response
.status_code
== 200
425 assert user
["nickname"] == self
.user
.username
426 assert user
["updated"] == self
.user
.created
.isoformat()
427 assert user
["published"] == self
.user
.created
.isoformat()
429 # Test profile exists but self.test_profile will test the value
430 assert "profile" in response
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")
437 assert "401 UNAUTHORIZED" in excinfo
.value
.args
[0]
439 def test_read_feed(self
, test_app
):
440 """ Test able to read objects from the feed """
441 response
, data
= self
._upload
_image
(test_app
, GOOD_JPG
)
442 response
, data
= self
._post
_image
_to
_feed
(test_app
, data
)
444 uri
= "/api/user/{0}/feed".format(self
.active_user
.username
)
445 with self
.mock_oauth():
446 response
= test_app
.get(uri
)
447 feed
= json
.loads(response
.body
.decode())
449 assert response
.status_code
== 200
451 # Check it has the attributes it should
452 assert "displayName" in feed
453 assert "objectTypes" in feed
455 assert "links" in feed
456 assert "author" in feed
457 assert "items" in feed
459 # Check that image i uploaded is there
460 assert feed
["items"][0]["verb"] == "post"
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"]
466 def test_read_another_feed(self
, test_app
):
467 """ Test able to read objects from someone else's feed """
468 response
, data
= self
._upload
_image
(test_app
, GOOD_JPG
)
469 response
, data
= self
._post
_image
_to
_feed
(test_app
, data
)
471 # Change the active user to someone else.
472 self
.active_user
= self
.other_user
475 url
= "/api/user/{0}/feed".format(self
.user
.username
)
476 with self
.mock_oauth():
477 response
= test_app
.get(url
)
478 feed
= json
.loads(response
.body
.decode())
480 assert response
.status_code
== 200
482 # Check it has the attributes it ought to.
483 assert "displayName" in feed
484 assert "objectTypes" in feed
486 assert "links" in feed
487 assert "author" in feed
488 assert "items" in feed
490 # Assert the uploaded image is there
491 assert feed
["items"][0]["verb"] == "post"
492 assert feed
["items"][0]["id"] == data
["id"]
493 assert feed
["items"][0]["object"]["objectType"] == "image"
494 assert feed
["items"][0]["object"]["id"] == data
["object"]["id"]
496 def test_cant_post_to_someone_elses_feed(self
, test_app
):
497 """ Test that can't post to someone elses feed """
498 response
, data
= self
._upload
_image
(test_app
, GOOD_JPG
)
499 self
.active_user
= self
.other_user
501 with self
.mock_oauth():
502 with pytest
.raises(AppError
) as excinfo
:
503 self
._post
_image
_to
_feed
(test_app
, data
)
505 assert "403 FORBIDDEN" in excinfo
.value
.args
[0]
507 def test_object_endpoint_requestable(self
, test_app
):
508 """ Test that object endpoint can be requested """
509 response
, data
= self
._upload
_image
(test_app
, GOOD_JPG
)
510 response
, data
= self
._post
_image
_to
_feed
(test_app
, data
)
511 object_id
= data
["object"]["id"]
513 with self
.mock_oauth():
514 response
= test_app
.get(data
["object"]["links"]["self"]["href"])
515 data
= json
.loads(response
.body
.decode())
517 assert response
.status_code
== 200
519 assert object_id
== data
["id"]
521 assert "links" in data
522 assert data
["objectType"] == "image"
524 def test_delete_media_by_activity(self
, test_app
):
525 """ Test that an image can be deleted by a delete activity to feed """
526 response
, data
= self
._upload
_image
(test_app
, GOOD_JPG
)
527 response
, data
= self
._post
_image
_to
_feed
(test_app
, data
)
528 object_id
= data
["object"]["id"]
534 "objectType": "image",
538 response
= self
._activity
_to
_feed
(test_app
, activity
)[1]
540 # Check the media is no longer in the database
541 media
= MediaEntry
.query
.filter_by(public_id
=object_id
).first()
545 # Check we've been given the full delete activity back
546 assert "id" in response
547 assert response
["verb"] == "delete"
548 assert "object" in response
549 assert response
["object"]["id"] == object_id
550 assert response
["object"]["objectType"] == "image"
552 def test_delete_comment_by_activity(self
, test_app
):
553 """ Test that a comment is deleted by a delete activity to feed """
554 # First upload an image to comment against
555 response
, data
= self
._upload
_image
(test_app
, GOOD_JPG
)
556 response
, data
= self
._post
_image
_to
_feed
(test_app
, data
)
558 # Post a comment to delete
562 "objectType": "comment",
563 "content": "This is a comment.",
564 "inReplyTo": data
["object"],
568 comment
= self
._activity
_to
_feed
(test_app
, activity
)[1]
570 # Now delete the image
574 "id": comment
["object"]["id"],
575 "objectType": "comment",
579 delete
= self
._activity
_to
_feed
(test_app
, activity
)[1]
581 # Verify the comment no longer exists
582 assert TextComment
.query
.filter_by(public_id
=comment
["object"]["id"]).first() is None
583 comment_id
= comment
["object"]["id"]
585 # Check we've got a delete activity back
586 assert "id" in delete
587 assert delete
["verb"] == "delete"
588 assert "object" in delete
589 assert delete
["object"]["id"] == comment
["object"]["id"]
590 assert delete
["object"]["objectType"] == "comment"
592 def test_edit_comment(self
, test_app
):
593 """ Test that someone can update their own comment """
594 # First upload an image to comment against
595 response
, data
= self
._upload
_image
(test_app
, GOOD_JPG
)
596 response
, data
= self
._post
_image
_to
_feed
(test_app
, data
)
598 # Post a comment to edit
602 "objectType": "comment",
603 "content": "This is a comment",
604 "inReplyTo": data
["object"],
608 comment
= self
._activity
_to
_feed
(test_app
, activity
)[1]
610 # Now create an update activity to change the content
614 "id": comment
["object"]["id"],
615 "content": "This is my fancy new content string!",
616 "objectType": "comment",
620 comment
= self
._activity
_to
_feed
(test_app
, activity
)[1]
622 # Verify the comment reflects the changes
623 model
= TextComment
.query
.filter_by(public_id
=comment
["object"]["id"]).first()
625 assert model
.content
== activity
["object"]["content"]