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
This commit is contained in:
Adam Coldrick 2019-03-19 12:43:34 +00:00
parent 6cdc6f4bfb
commit 0207e1704b
5 changed files with 220 additions and 37 deletions

View File

@ -36,6 +36,24 @@ class TestComments(base.FunctionalTest):
response = self.get_json(self.comments_resource % self.story_id) response = self.get_json(self.comments_resource % self.story_id)
self.assertEqual(0, len(response)) 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): 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_01)
self.post_json(self.comments_resource % self.story_id, self.comment_02) self.post_json(self.comments_resource % self.story_id, self.comment_02)

View File

@ -33,7 +33,29 @@ class TestStories(base.FunctionalTest):
def test_stories_endpoint(self): def test_stories_endpoint(self):
response = self.get_json(self.resource) 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): def test_create(self):
response = self.post_json(self.resource, self.story_01) response = self.post_json(self.resource, self.story_01)
@ -62,22 +84,22 @@ class TestStories(base.FunctionalTest):
self.assertEqual(story['story_type_id'], self.assertEqual(story['story_type_id'],
created_story['story_type_id']) created_story['story_type_id'])
@unittest.skip("vulnerabilities are not supported.") def test_create_private_story(self):
def test_create_private_vulnerability(self):
story = { story = {
'title': 'StoryBoard', 'title': 'StoryBoard',
'description': 'Awesome Task Tracker', 'description': 'Awesome Task Tracker',
'story_type_id': 3 'private': True,
'users': [{'id': 1}]
} }
response = self.post_json(self.resource, story) response = self.post_json(self.resource, story)
created_story = response.json created_story = response.json
self.assertEqual(story['title'], created_story['title']) self.assertEqual(story['title'], created_story['title'])
self.assertEqual(story['description'], created_story['description']) self.assertEqual(story['description'], created_story['description'])
self.assertEqual(story['story_type_id'], self.assertEqual(story['private'],
created_story['story_type_id']) created_story['private'])
@unittest.skip("vulnerabilities are not supported.") @unittest.skip("public vulnerabilities are not supported.")
def test_create_public_vulnerability(self): def test_create_public_vulnerability(self):
story = { story = {
'title': 'StoryBoard', 'title': 'StoryBoard',
@ -129,25 +151,31 @@ class TestStories(base.FunctionalTest):
{'story_type_id': story_type_id}) {'story_type_id': story_type_id})
self.assertEqual(story_type_id, response.json['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(self):
def test_update_private_to_public_vulnerability(self):
story = { story = {
'title': 'StoryBoard', 'title': 'StoryBoard',
'description': 'Awesome Task Tracker', 'description': 'Awesome Task Tracker',
'story_type_id': 3 'private': True
} }
response = self.post_json(self.resource, story) response = self.post_json(self.resource, story)
created_story = response.json created_story = response.json
self.assertEqual(story["story_type_id"], self.assertEqual(story['private'],
created_story["story_type_id"]) created_story['private'])
response = self.put_json(self.resource + response = self.put_json(self.resource +
('/%s' % created_story["id"]), ('/%s' % created_story['id']),
{'story_type_id': 4}) {'private': False})
created_story = response.json updated_story = response.json
self.assertEqual(4, created_story['story_type_id']) 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): def test_update_restricted_branches(self):
response = self.put_json(self.resource + '/1', {'story_type_id': 2}, response = self.put_json(self.resource + '/1', {'story_type_id': 2},

View File

@ -107,7 +107,34 @@ class TestTasksPrimary(base.FunctionalTest):
def test_tasks_endpoint(self): def test_tasks_endpoint(self):
response = self.get_json(self.resource) 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.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): def test_create(self):
result = self.post_json(self.resource, self.task_01) result = self.post_json(self.resource, self.task_01)
@ -274,6 +301,38 @@ class TestTasksNestedController(base.FunctionalTest):
self.assertEqual(400, response.status_code) 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): def test_create(self):
result = self.post_json(self.resource, { result = self.post_json(self.resource, {
'title': 'StoryBoard', 'title': 'StoryBoard',

View File

@ -18,6 +18,10 @@ from storyboard.tests import base
class TestTimelineEvents(base.FunctionalTest): 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): def test_get_all_events(self):
"""Assert that we can retrieve a list of events from a story.""" """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(200, response.status_code)
self.assertEqual(3, len(response.json)) 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): def test_filter_by_event_type(self):
"""Assert that we can correctly filter an event by event type.""" """Assert that we can correctly filter an event by event type."""
response = self.get_json('/stories/1/events?event_type=story_created' response = self.get_json('/stories/1/events?event_type=story_created'

View File

@ -21,6 +21,7 @@ from storyboard.db.models import AccessToken
from storyboard.db.models import Branch from storyboard.db.models import Branch
from storyboard.db.models import Comment from storyboard.db.models import Comment
from storyboard.db.models import Milestone from storyboard.db.models import Milestone
from storyboard.db.models import Permission
from storyboard.db.models import Project from storyboard.db.models import Project
from storyboard.db.models import ProjectGroup from storyboard.db.models import ProjectGroup
from storyboard.db.models import Story 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 """Load a batch of useful data into the database that our tests can work
with. with.
""" """
session = db.get_session(autocommit=False, in_request=False)
now = datetime.datetime.now(tz=pytz.utc) now = datetime.datetime.now(tz=pytz.utc)
expires_at = now + datetime.timedelta(seconds=3600) expires_at = now + datetime.timedelta(seconds=3600)
expired_at = now + datetime.timedelta(seconds=-3600) expired_at = now + datetime.timedelta(seconds=-3600)
@ -57,7 +59,8 @@ def load():
openid='otheruser_openid', openid='otheruser_openid',
full_name='Other User', full_name='Other User',
is_superuser=False) is_superuser=False)
]) ], session)
users = session.query(User).all()
# Load some preferences for the above users. # Load some preferences for the above users.
load_data([ load_data([
@ -86,7 +89,7 @@ def load():
key='plugin_email_digest', key='plugin_email_digest',
value='False', value='False',
type='bool'), type='bool'),
]) ], session)
# Load a variety of sensibly named access tokens. # Load a variety of sensibly named access tokens.
load_data([ load_data([
@ -110,7 +113,7 @@ def load():
access_token='expired_user_token', access_token='expired_user_token',
expires_in=3600, expires_in=3600,
expires_at=expired_at) expires_at=expired_at)
]) ], session)
# Create some test projects. # Create some test projects.
projects = load_data([ projects = load_data([
@ -126,7 +129,7 @@ def load():
id=3, id=3,
name='tests/project3', name='tests/project3',
description='Project 1 Description - foo') description='Project 1 Description - foo')
]) ], session)
# Create some test project groups. # Create some test project groups.
load_data([ load_data([
@ -153,7 +156,17 @@ def load():
name='projectgroup3', name='projectgroup3',
title='A Sort - foo' 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. # Create some stories.
load_data([ load_data([
@ -181,8 +194,15 @@ def load():
id=5, id=5,
title="A Test story 5 - oh hai", title="A Test story 5 - oh hai",
description="Test Description - 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 # Create some tasks
load_data([ load_data([
@ -229,8 +249,19 @@ def load():
branch_id=2, branch_id=2,
assignee_id=1, assignee_id=1,
priority='medium' 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. # Generate some timeline events for the above stories.
load_data([ load_data([
@ -280,28 +311,48 @@ def load():
'"old_assignee_id": null, ' '"old_assignee_id": null, '
'"task_id": 1, ' '"task_id": 1, '
'"new_assignee_id": 2}' '"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([ load_data([
Comment( Comment(
id=1, id=1,
content="Test Comment", content="Test Comment",
is_active=True 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([ load_data([
TimeLineEvent( TimeLineEvent(
id=7, id=8,
story_id=1, story_id=1,
comment_id=1, comment_id=1,
author_id=1, author_id=1,
event_type=event.USER_COMMENT 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 some subscriptions.
load_data([ load_data([
@ -323,7 +374,7 @@ def load():
target_type='story', target_type='story',
target_id=1 target_id=1
), ),
]) ], session)
# Load some branches # Load some branches
load_data([ load_data([
@ -345,7 +396,7 @@ def load():
name='master', name='master',
restricted=True restricted=True
) )
]) ], session)
# Load some milestones # Load some milestones
load_data([ load_data([
@ -359,27 +410,30 @@ def load():
name='test_milestone_02', name='test_milestone_02',
branch_id=2 branch_id=2
) )
]) ], session)
# Load some teams # Load some teams
load_data([ load_data([
Team( Team(
id=1, id=1,
name='test_team_1' name='test_team_1',
users=[users[0]]
), ),
Team( Team(
id=2, 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. """Pre load test data into the database.
:param data An iterable collection of database models. :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: for entity in data:
session.add(entity) session.add(entity)