diff --git a/storyboard/tests/api/test_comments.py b/storyboard/tests/api/test_comments.py index d038ad85..0f397319 100644 --- a/storyboard/tests/api/test_comments.py +++ b/storyboard/tests/api/test_comments.py @@ -36,6 +36,24 @@ class TestComments(base.FunctionalTest): response = self.get_json(self.comments_resource % self.story_id) self.assertEqual(0, len(response)) + def test_comments_privacy(self): + url = '/stories/6/comments' + response = self.get_json(url, expect_errors=True) + self.assertEqual(200, response.status_code) + self.assertEqual(1, len(response.json)) + + # The user with token `valid_user_token` can't see the story, and + # so shouldn't be able to see the comment + headers = {'Authorization': 'Bearer valid_user_token'} + response = self.get_json(url, headers=headers, expect_errors=True) + self.assertEqual(0, len(response.json)) + + # Unauthenticated users shouldn't be able to see anything in private + # stories + self.default_headers.pop('Authorization') + response = self.get_json(url, expect_errors=True) + self.assertEqual(0, len(response.json)) + def test_create(self): self.post_json(self.comments_resource % self.story_id, self.comment_01) self.post_json(self.comments_resource % self.story_id, self.comment_02) diff --git a/storyboard/tests/api/test_stories.py b/storyboard/tests/api/test_stories.py index a65f23f5..7702ac35 100644 --- a/storyboard/tests/api/test_stories.py +++ b/storyboard/tests/api/test_stories.py @@ -33,7 +33,29 @@ class TestStories(base.FunctionalTest): def test_stories_endpoint(self): response = self.get_json(self.resource) - self.assertEqual(5, len(response)) + self.assertEqual(6, len(response)) + + def test_private_story_visibility(self): + url = self.resource + '/6' + story = self.get_json(url) + + # User with token `valid_superuser_token` has permission to see + # the story, so should be able to get it without issue. + self.assertEqual(story['title'], 'Test Private Story') + self.assertTrue(story['private']) + self.assertEqual(1, len(story['users'])) + self.assertEqual('Super User', story['users'][0]['full_name']) + self.assertEqual(0, len(story['teams'])) + + # User with token `valid_user_token` doesn't have permission + headers = {'Authorization': 'Bearer valid_user_token'} + response = self.get_json(url, headers=headers, expect_errors=True) + self.assertEqual(404, response.status_code) + + # Unauthenticated users shouldn't be able to see private stories + self.default_headers.pop('Authorization') + response = self.get_json(url, expect_errors=True) + self.assertEqual(404, response.status_code) def test_create(self): response = self.post_json(self.resource, self.story_01) @@ -62,22 +84,22 @@ class TestStories(base.FunctionalTest): self.assertEqual(story['story_type_id'], created_story['story_type_id']) - @unittest.skip("vulnerabilities are not supported.") - def test_create_private_vulnerability(self): + def test_create_private_story(self): story = { 'title': 'StoryBoard', 'description': 'Awesome Task Tracker', - 'story_type_id': 3 + 'private': True, + 'users': [{'id': 1}] } response = self.post_json(self.resource, story) created_story = response.json self.assertEqual(story['title'], created_story['title']) self.assertEqual(story['description'], created_story['description']) - self.assertEqual(story['story_type_id'], - created_story['story_type_id']) + self.assertEqual(story['private'], + created_story['private']) - @unittest.skip("vulnerabilities are not supported.") + @unittest.skip("public vulnerabilities are not supported.") def test_create_public_vulnerability(self): story = { 'title': 'StoryBoard', @@ -129,25 +151,31 @@ class TestStories(base.FunctionalTest): {'story_type_id': story_type_id}) self.assertEqual(story_type_id, response.json['story_type_id']) - @unittest.skip("vulnerabilities are not supported.") - def test_update_private_to_public_vulnerability(self): + def test_update_private_to_public(self): story = { 'title': 'StoryBoard', 'description': 'Awesome Task Tracker', - 'story_type_id': 3 + 'private': True } response = self.post_json(self.resource, story) created_story = response.json - self.assertEqual(story["story_type_id"], - created_story["story_type_id"]) + self.assertEqual(story['private'], + created_story['private']) response = self.put_json(self.resource + - ('/%s' % created_story["id"]), - {'story_type_id': 4}) - created_story = response.json - self.assertEqual(4, created_story['story_type_id']) + ('/%s' % created_story['id']), + {'private': False}) + updated_story = response.json + self.assertFalse(updated_story['private']) + + # Check that a different user can see the story + headers = {'Authorization': 'Bearer valid_user_token'} + api_story = self.get_json(self.resource + '/%s' % created_story['id'], + headers=headers) + self.assertEqual(story['title'], api_story['title']) + self.assertEqual(story['description'], api_story['description']) def test_update_restricted_branches(self): response = self.put_json(self.resource + '/1', {'story_type_id': 2}, diff --git a/storyboard/tests/api/test_tasks.py b/storyboard/tests/api/test_tasks.py index 3f26d84d..f457855e 100644 --- a/storyboard/tests/api/test_tasks.py +++ b/storyboard/tests/api/test_tasks.py @@ -107,7 +107,34 @@ class TestTasksPrimary(base.FunctionalTest): def test_tasks_endpoint(self): response = self.get_json(self.resource) + self.assertEqual(5, len(response)) + + # Check that tasks in private stories are correctly filtered + headers = {'Authorization': 'Bearer valid_user_token'} + response = self.get_json(self.resource, headers=headers) self.assertEqual(4, len(response)) + self.default_headers.pop('Authorization') + response = self.get_json(self.resource) + self.assertEqual(4, len(response)) + + def test_private_task_visibility(self): + url = self.resource + '/5' + # Task with id 5 is in a private story which the user with token + # `valid_superuser_token` can see + response = self.get_json(url) + self.assertEqual('Task in private story', response['title']) + + # The user with token `valid_user_token` can't see the story, and + # so shouldn't be able to see the task + headers = {'Authorization': 'Bearer valid_user_token'} + response = self.get_json(url, headers=headers, expect_errors=True) + self.assertEqual(404, response.status_code) + + # Unauthenticated users shouldn't be able to see anything in private + # stories + self.default_headers.pop('Authorization') + response = self.get_json(url, expect_errors=True) + self.assertEqual(404, response.status_code) def test_create(self): result = self.post_json(self.resource, self.task_01) @@ -274,6 +301,38 @@ class TestTasksNestedController(base.FunctionalTest): self.assertEqual(400, response.status_code) + def test_tasks_endpoint_privacy(self): + self.resource = '/stories/6/tasks' + response = self.get_json(self.resource) + self.assertEqual(1, len(response)) + + # Check that tasks in private stories are correctly filtered + headers = {'Authorization': 'Bearer valid_user_token'} + response = self.get_json(self.resource, headers=headers) + self.assertEqual(0, len(response)) + self.default_headers.pop('Authorization') + response = self.get_json(self.resource) + self.assertEqual(0, len(response)) + + def test_private_task_visibility(self): + url = '/stories/6/tasks/5' + # Task with id 5 is in a private story which the user with token + # `valid_superuser_token` can see + response = self.get_json(url) + self.assertEqual('Task in private story', response['title']) + + # The user with token `valid_user_token` can't see the story, and + # so shouldn't be able to see the task + headers = {'Authorization': 'Bearer valid_user_token'} + response = self.get_json(url, headers=headers, expect_errors=True) + self.assertEqual(404, response.status_code) + + # Unauthenticated users shouldn't be able to see anything in private + # stories + self.default_headers.pop('Authorization') + response = self.get_json(url, expect_errors=True) + self.assertEqual(404, response.status_code) + def test_create(self): result = self.post_json(self.resource, { 'title': 'StoryBoard', diff --git a/storyboard/tests/api/test_timeline_events.py b/storyboard/tests/api/test_timeline_events.py index 59fdeafd..cd5e9990 100644 --- a/storyboard/tests/api/test_timeline_events.py +++ b/storyboard/tests/api/test_timeline_events.py @@ -18,6 +18,10 @@ from storyboard.tests import base class TestTimelineEvents(base.FunctionalTest): + def setUp(self): + super(TestTimelineEvents, self).setUp() + self.default_headers['Authorization'] = 'Bearer valid_superuser_token' + def test_get_all_events(self): """Assert that we can retrieve a list of events from a story.""" @@ -27,6 +31,26 @@ class TestTimelineEvents(base.FunctionalTest): self.assertEqual(200, response.status_code) self.assertEqual(3, len(response.json)) + def test_get_all_events_privacy(self): + """Assert that events for private stories are access controlled.""" + + url = '/stories/6/events' + response = self.get_json(url, expect_errors=True) + self.assertEqual(200, response.status_code) + self.assertEqual(2, len(response.json)) + + # The user with token `valid_user_token` can't see the story, and + # so shouldn't be able to see the events + headers = {'Authorization': 'Bearer valid_user_token'} + response = self.get_json(url, headers=headers, expect_errors=True) + self.assertEqual(0, len(response.json)) + + # Unauthenticated users shouldn't be able to see anything in private + # stories + self.default_headers.pop('Authorization') + response = self.get_json(url, expect_errors=True) + self.assertEqual(0, len(response.json)) + def test_filter_by_event_type(self): """Assert that we can correctly filter an event by event type.""" response = self.get_json('/stories/1/events?event_type=story_created' diff --git a/storyboard/tests/mock_data.py b/storyboard/tests/mock_data.py index 7d1eb5eb..b13506c3 100644 --- a/storyboard/tests/mock_data.py +++ b/storyboard/tests/mock_data.py @@ -21,6 +21,7 @@ from storyboard.db.models import AccessToken from storyboard.db.models import Branch from storyboard.db.models import Comment from storyboard.db.models import Milestone +from storyboard.db.models import Permission from storyboard.db.models import Project from storyboard.db.models import ProjectGroup from storyboard.db.models import Story @@ -36,6 +37,7 @@ def load(): """Load a batch of useful data into the database that our tests can work with. """ + session = db.get_session(autocommit=False, in_request=False) now = datetime.datetime.now(tz=pytz.utc) expires_at = now + datetime.timedelta(seconds=3600) expired_at = now + datetime.timedelta(seconds=-3600) @@ -57,7 +59,8 @@ def load(): openid='otheruser_openid', full_name='Other User', is_superuser=False) - ]) + ], session) + users = session.query(User).all() # Load some preferences for the above users. load_data([ @@ -86,7 +89,7 @@ def load(): key='plugin_email_digest', value='False', type='bool'), - ]) + ], session) # Load a variety of sensibly named access tokens. load_data([ @@ -110,7 +113,7 @@ def load(): access_token='expired_user_token', expires_in=3600, expires_at=expired_at) - ]) + ], session) # Create some test projects. projects = load_data([ @@ -126,7 +129,7 @@ def load(): id=3, name='tests/project3', description='Project 1 Description - foo') - ]) + ], session) # Create some test project groups. load_data([ @@ -153,7 +156,17 @@ def load(): name='projectgroup3', title='A Sort - foo' ) - ]) + ], session) + + # Create some permissions + load_data([ + Permission( + name='view_story_6', + codename='view_story', + users=[users[0]] + ) + ], session) + permissions = session.query(Permission).all() # Create some stories. load_data([ @@ -181,8 +194,15 @@ def load(): id=5, title="A Test story 5 - oh hai", description="Test Description - oh hai" + ), + Story( + id=6, + title="Test Private Story", + description="For Super User's eyes only", + private=True, + permissions=[permissions[0]] ) - ]) + ], session) # Create some tasks load_data([ @@ -229,8 +249,19 @@ def load(): branch_id=2, assignee_id=1, priority='medium' + ), + Task( + id=5, + creator_id=1, + title='Task in private story', + status='todo', + story_id=6, + project_id=2, + branch_id=2, + assignee_id=1, + priority='medium' ) - ]) + ], session) # Generate some timeline events for the above stories. load_data([ @@ -280,28 +311,48 @@ def load(): '"old_assignee_id": null, ' '"task_id": 1, ' '"new_assignee_id": 2}' + ), + TimeLineEvent( + id=7, + story_id=6, + author_id=1, + event_type=event.STORY_CREATED, + event_info='{"story_id": 6, ' + '"story_title": "Test Private Story"}' ) - ]) + ], session) - # Create a comment. + # Create some comments. load_data([ Comment( id=1, content="Test Comment", is_active=True + ), + Comment( + id=2, + content="Comment on a private story", + is_active=True ) - ]) + ], session) - # Create a timeline event for the above comment. + # Create timeline events for the above comments. load_data([ TimeLineEvent( - id=7, + id=8, story_id=1, comment_id=1, author_id=1, event_type=event.USER_COMMENT + ), + TimeLineEvent( + id=9, + story_id=6, + comment_id=2, + author_id=1, + event_type=event.USER_COMMENT ) - ]) + ], session) # Load some subscriptions. load_data([ @@ -323,7 +374,7 @@ def load(): target_type='story', target_id=1 ), - ]) + ], session) # Load some branches load_data([ @@ -345,7 +396,7 @@ def load(): name='master', restricted=True ) - ]) + ], session) # Load some milestones load_data([ @@ -359,27 +410,30 @@ def load(): name='test_milestone_02', branch_id=2 ) - ]) + ], session) # Load some teams load_data([ Team( id=1, - name='test_team_1' + name='test_team_1', + users=[users[0]] ), Team( id=2, - name='test_team_2' + name='test_team_2', + users=users[1:] ) - ]) + ], session) -def load_data(data): +def load_data(data, session=None): """Pre load test data into the database. :param data An iterable collection of database models. """ - session = db.get_session(autocommit=False, in_request=False) + if session is None: + session = db.get_session(autocommit=False, in_request=False) for entity in data: session.add(entity)