From 0207e1704b917a717d6c370925a36d0ba26d0c25 Mon Sep 17 00:00:00 2001 From: Adam Coldrick Date: Tue, 19 Mar 2019 12:43:34 +0000 Subject: [PATCH] Add some tests for checking private story behaviour This adds some tests which verify the behaviour of private stories is as intended, and that they are correctly filtered out of any related results such as tasks and comments. Some changes were necessary in mock_data.py to account for this. The session used to insert data is now shared for all the test objects, rather than creating a new session per object type. This makes it easier to query the created data to populate the various many-to-many relationships in the StoryBoard data model, to allow for testing of things which require permissions to be created and populated. Change-Id: Ib4db5b3d07363d3bdc97ad825a12f8882a0aa8f1 --- storyboard/tests/api/test_comments.py | 18 ++++ storyboard/tests/api/test_stories.py | 60 ++++++++---- storyboard/tests/api/test_tasks.py | 59 ++++++++++++ storyboard/tests/api/test_timeline_events.py | 24 +++++ storyboard/tests/mock_data.py | 96 +++++++++++++++----- 5 files changed, 220 insertions(+), 37 deletions(-) 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)