From ae8f0085f40c8cb61e07668e1c9c50e3b8f9e165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Einar=20Uvsl=C3=B8kk?= Date: Fri, 12 Mar 2021 08:56:54 +0100 Subject: [PATCH 1/4] Add black box test for FR5 Implement black box test for FR5 "View Workout" using Selenium. --- backend/secfit/tests/test-data.json | 122 +++++++++++++++ backend/secfit/tests/test_view_workout.py | 180 ++++++++++++++++++++++ 2 files changed, 302 insertions(+) create mode 100644 backend/secfit/tests/test-data.json create mode 100644 backend/secfit/tests/test_view_workout.py diff --git a/backend/secfit/tests/test-data.json b/backend/secfit/tests/test-data.json new file mode 100644 index 0000000..46e28d2 --- /dev/null +++ b/backend/secfit/tests/test-data.json @@ -0,0 +1,122 @@ +[ + { + "model": "users.user", + "pk": 1, + "fields": { + "username": "syrio", + "email": "syrio@forel.net", + "password": "pbkdf2_sha256$216000$4rwboPYCivJX$/ixnhVdqQoX3G+t4dlNwcgb0lpPg00pZuOBqpv/ML9E=" + } + }, + { + "model": "users.user", + "pk": 2, + "fields": { + "username": "arya", + "email": "arya@stark.io", + "password": "pbkdf2_sha256$216000$glNd7zmKsCPk$Et1Wg9R1857TSb3eiaV6p4OZKi46AEwupcix2pAIS+I=", + "coach": 1 + } + }, + { + "model": "workouts.workout", + "pk": 1, + "fields": { + "name": "Sword practice with Mycah", + "date": "2011-04-24T20:00:00.00000Z", + "notes": "Got interrupted by Sansa and Joffrey. Nymeria bit Joffrey in the arm, and I threw his sword into the river!", + "visibility": "PU", + "owner": 2 + } + }, + { + "model": "workouts.workout", + "pk": 2, + "fields": { + "name": "Sword practice with Syrio", + "date": "2021-03-09T20:15:00.00000Z", + "visibility": "CO", + "owner": 2 + } + }, + { + "model": "workouts.workout", + "pk": 3, + "fields": { + "name": "My last duel", + "date": "2011-06-11T20:00:00.00000Z", + "notes": "Got interrupted by the Kingsquard during practice with Arya, and had to battle Ser Meryn Trant...", + "visibility": "PU", + "owner": 1 + } + }, + { + "model": "workouts.workout", + "pk": 4, + "fields": { + "name": "Blind training in Braavos", + "date": "2016-04-24T20:00:00.00000Z", + "notes": "Got really beaten up by the Waif today. Hard to fight back when I'm blind.", + "visibility": "PR", + "owner": 2 + } + }, + { + "model": "workouts.exercise", + "pk": 1, + "fields": { + "name": "Fencing", + "description": "Stick them with the pointy end.", + "unit": "reps" + } + }, + { + "model": "workouts.exercise", + "pk": 2, + "fields": { + "name": "Stick fighting", + "description": "Hit opponent and pair strokes from opponent. Repeat.", + "unit": "reps" + } + }, + { + "model": "workouts.exerciseinstance", + "pk": 1, + "fields": { + "workout": 1, + "exercise": 1, + "sets": 3, + "number": 8 + } + }, + { + "model": "workouts.exerciseinstance", + "pk": 2, + "fields": { + "workout": 2, + "exercise": 1, + "sets": 3, + "number": 8 + } + }, + { + "model": "workouts.exerciseinstance", + "pk": 2, + "fields": { + "workout": 3, + "exercise": 2, + "sets": 3, + "number": 8 + } + }, + { + "model": "workouts.exerciseinstance", + "pk": 2, + "fields": { + "workout": 4, + "exercise": 2, + "sets": 3, + "number": 8 + } + } +] diff --git a/backend/secfit/tests/test_view_workout.py b/backend/secfit/tests/test_view_workout.py new file mode 100644 index 0000000..b0aadc7 --- /dev/null +++ b/backend/secfit/tests/test_view_workout.py @@ -0,0 +1,180 @@ +import enum +import os +import pathlib +from dataclasses import dataclass + +from django.test import LiveServerTestCase +from selenium.webdriver.chrome.webdriver import WebDriver +from selenium.webdriver.common.by import By +from selenium.webdriver.support.wait import WebDriverWait + +here = pathlib.Path(__file__).parent.absolute() +backend_port = os.environ.get("BACKEND_PORT", 8000) +frontend_port = os.environ.get("FRONTEND_PORT", 8001) +frontend_address = f"http://localhost:{frontend_port}" + + +class Visibility(str, enum.Enum): + PUBLIC = "PU" + COACH = "CO" + PRIVATE = "PR" + + +@dataclass +class LoginData(dict): + username: str + password: str + + +login_data = { + "athlete": LoginData("arya", "dOnOtCaLlMeMiLaDy"), + "coach": LoginData("syrio", "wHaTdOwEsAyToThEgOdOfDeAtH"), +} + +workout_input_ids = ( + "inputName", + "inputDateTime", + "inputOwner", + "inputVisibility", + "inputNotes", +) + + +class ViewWorkoutTest(LiveServerTestCase): + """Test case for FR5 View Workout. + + Requirement description: + + The user should be able to view all of the details, files, and comments + on workouts of sufficient visibility. For athletes, this means that the + workout needs to be either their own or public. For coaches, this means + that the workout is at least one of their athletes non-private workouts + OR the workout is public. For visitors, this means that the workout + needs to be public. + """ + + port = backend_port + timeout = 5 + fixtures = [ + here / "test-data.json" + ] + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.driver = WebDriver() + cls.wait = WebDriverWait(cls.driver, cls.timeout) + + @classmethod + def tearDownClass(cls): + cls.driver.quit() + super().tearDownClass() + + def goto(self, page: str): + self.driver.get(f"{frontend_address}/{page}") + + def goto_workout(self, id): + self.goto(f"workout.html?id={id}") + + def wait_until_value_not_empty(self, by: By, locator: str): + self.wait.until(lambda driver: self.get_input_value(by, locator) != "") + + def get_input_value(self, by: By, locator: str) -> str: + element = self.driver.find_element(by, locator) + self.assertIsNotNone(element) + return element.get_attribute("value") + + def login(self, user: LoginData): + """Log in a user and wait until redirected to the workouts page.""" + self.goto("login.html") + self.assertEqual("Login", self.driver.title) + username_input = self.driver.find_element_by_name("username") + username_input.send_keys(user.username) + password_input = self.driver.find_element_by_name("password") + password_input.send_keys(user.password) + self.driver.find_element_by_id("btn-login").click() + self.wait.until(lambda driver: driver.title == "Workouts") + + def logout(self): + """Log out a user.""" + self.goto("logout.html") + self.assertEqual("Logout", self.driver.title) + self.wait.until(lambda driver: driver.title == "Home") + + def test_user_can_view_own_public_workout(self): + """Test if a user is able to view their own public workout.""" + user = login_data["athlete"] + self.login(user) + self.goto_workout(1) + self.assertEqual("Workout", self.driver.title) + self.wait_until_value_not_empty(By.ID, "inputOwner") + for input_id in workout_input_ids: + element = self.driver.find_element_by_id(input_id) + value = element.get_attribute("value") + self.assertIsNotNone(value) + if input_id == "inputOwner": + self.assertEqual(value, user.username) + if input_id == "inputVisibility": + self.assertEqual(value, Visibility.PUBLIC) + + def test_coach_can_view_athletes_workout(self): + """Test if a coach is able to view their athlete's workout.""" + user = login_data["coach"] + self.login(user) + self.goto_workout(2) + self.assertEqual("Workout", self.driver.title) + self.wait_until_value_not_empty(By.ID, "inputOwner") + for input_id in workout_input_ids: + value = self.get_input_value(By.ID, input_id) + self.assertIsNotNone(value) + if input_id == "inputOwner": + self.assertNotEqual(value, user.username) + if input_id == "inputVisibility": + self.assertEqual(value, Visibility.COACH) + + def test_user_can_view_others_public_workout(self): + """Test if a user is able to view another users' public workout.""" + user = login_data["athlete"] + self.login(user) + self.goto_workout(3) + self.assertEqual("Workout", self.driver.title) + self.wait_until_value_not_empty(By.ID, "inputOwner") + for input_id in workout_input_ids: + value = self.get_input_value(By.ID, input_id) + self.assertIsNotNone(value) + if input_id == "inputOwner": + self.assertNotEqual(value, user.username) + if input_id == "inputVisibility": + self.assertEqual(value, Visibility.PUBLIC) + + def test_user_can_view_own_private_workout(self): + """Test if a user is able to view their own private workout.""" + user = login_data["athlete"] + self.login(user) + self.goto_workout(4) + self.assertEqual("Workout", self.driver.title) + self.wait_until_value_not_empty(By.ID, "inputOwner") + for input_id in workout_input_ids: + value = self.get_input_value(By.ID, input_id) + self.assertIsNotNone(value) + if input_id == "inputOwner": + self.assertEqual(value, user.username) + if input_id == "inputVisibility": + self.assertEqual(value, Visibility.PRIVATE) + + def test_user_cannot_view_other_users_private_workout(self): + """Test if a user is unable to view another users' private workout.""" + user = login_data["coach"] + self.login(user) + self.goto_workout(4) + self.assertEqual("Workout", self.driver.title) + alert = self.driver.find_elements_by_xpath("//div[@role='alert']") + self.assertIsNotNone(alert) + for input_id in workout_input_ids: + # For workouts that don't exist or have insufficient visibility, + # the form input elements will be populated with empty values. + # Except for the visibility field, which will have "PU" as default + # value regardess. It therefor makes no sense to check its value. + if input_id != "inputVisibility": + value = self.get_input_value(By.ID, input_id) + self.assertEqual(value, "") -- GitLab From 7b0c8ee641f8cd6f9067435d26dd99d0a4047a56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Einar=20Uvsl=C3=B8kk?= Date: Sat, 13 Mar 2021 13:13:16 +0100 Subject: [PATCH 2/4] Fix error in workout permissions During black box testing of FR5 (View Workout) an error related to workout permissions was discovered. Due to using wrong values when checking Coach visibility on workouts, coaches was not able to view workout details for their athletes. Unit tests are also updated to reflect the changes. --- backend/secfit/workouts/permissions.py | 4 ++-- backend/secfit/workouts/tests.py | 31 +++++++++++++------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/backend/secfit/workouts/permissions.py b/backend/secfit/workouts/permissions.py index 605264e..7a79ef6 100644 --- a/backend/secfit/workouts/permissions.py +++ b/backend/secfit/workouts/permissions.py @@ -35,7 +35,7 @@ class IsCoachAndVisibleToCoach(permissions.BasePermission): """ def has_object_permission(self, request, view, obj): - return obj.owner.coach == request.user and (obj.visibility == 'PU' or obj.visibility == 'Coach') + return obj.owner.coach == request.user and (obj.visibility == 'PU' or obj.visibility == 'CO') class IsCoachOfWorkoutAndVisibleToCoach(permissions.BasePermission): @@ -44,7 +44,7 @@ class IsCoachOfWorkoutAndVisibleToCoach(permissions.BasePermission): """ def has_object_permission(self, request, view, obj): - return obj.workout.owner.coach == request.user and (obj.workout.visibility == 'PU' or obj.workout.visibility == 'Coach') + return obj.workout.owner.coach == request.user and (obj.workout.visibility == 'PU' or obj.workout.visibility == 'CO') class IsPublic(permissions.BasePermission): diff --git a/backend/secfit/workouts/tests.py b/backend/secfit/workouts/tests.py index 4ca19ba..9aed493 100644 --- a/backend/secfit/workouts/tests.py +++ b/backend/secfit/workouts/tests.py @@ -29,7 +29,7 @@ class PermissionTests(TestCase): date='2021-02-03T12:00:00.000000Z', notes='Go down to until your legs has 90 degrees.', owner=user, visibility='PU') - + Workout.objects.create(name='squat', date='2021-02-03T12:00:00.000000Z', notes='Go down to until your legs has 90 degrees.', @@ -38,13 +38,12 @@ class PermissionTests(TestCase): Workout.objects.create(name='squat', date='2021-02-03T12:00:00.000000Z', notes='Go down to until your legs has 90 degrees.', - owner=coach, visibility='Coach') - + owner=coach, visibility='CO') + Workout.objects.create(name='squat', date='2021-02-03T12:00:00.000000Z', notes='Go down to until your legs has 90 degrees.', - owner=coach, visibility='Private') - + owner=coach, visibility='PR') def setUp(self): # Setup run before every test method. @@ -65,7 +64,7 @@ class PermissionTests(TestCase): permission = IsOwner.has_object_permission(self, request=request, view=None, obj=workout) self.assertTrue(permission) - + def test_isNotOwner(self): factory = RequestFactory() request = factory.get('/') @@ -85,7 +84,7 @@ class PermissionTests(TestCase): permission = IsOwnerOfWorkout.has_permission(self, request=request, view=None) self.assertTrue(permission) - + def test_IsNotOwnerOfWorkout(self): factory = RequestFactory() request = factory.post('/workout/1') @@ -107,7 +106,7 @@ class PermissionTests(TestCase): permission = IsOwnerOfWorkout.has_permission(self, request=request, view=None) self.assertFalse(permission) - + def test_IsOwnerOfWorkout_MissingPOSTMethod(self): factory = RequestFactory() request = factory.get('/workout/1') @@ -128,7 +127,7 @@ class PermissionTests(TestCase): permission = IsOwnerOfWorkout.has_object_permission(self, request=request, view=None, obj=request) self.assertTrue(permission) - + def test_IsOwnerOfWorkout_hasNotObjectPermission(self): factory = RequestFactory() request = factory.get('/') @@ -147,7 +146,7 @@ class PermissionTests(TestCase): permission = IsCoachAndVisibleToCoach.has_object_permission(self, request=request, view=None, obj=workout) self.assertTrue(permission) - + def test_IsCoachAndVisibleToCoach_visibilityCoach(self): factory = RequestFactory() request = factory.get('/') @@ -156,7 +155,7 @@ class PermissionTests(TestCase): permission = IsCoachAndVisibleToCoach.has_object_permission(self, request=request, view=None, obj=workout) self.assertTrue(permission) - + def test_IsCoachAndVisibleToCoach_visibilityPrivate(self): factory = RequestFactory() request = factory.get('/') @@ -201,7 +200,7 @@ class PermissionTests(TestCase): permission = IsCoachOfWorkoutAndVisibleToCoach.has_object_permission(self, request=request, view=None, obj=request) self.assertFalse(permission) - + def test_IsPublic(self): workout = self.workout @@ -213,7 +212,7 @@ class PermissionTests(TestCase): permission = IsPublic.has_object_permission(self, request=None, view=None, obj=workout) self.assertFalse(permission) - + def test_IsWorkoutPublic(self): workout = self.workout2 factory = RequestFactory() @@ -231,17 +230,17 @@ class PermissionTests(TestCase): permission = IsWorkoutPublic.has_object_permission(self, request=None, view=None, obj=obj) self.assertFalse(permission) - + def test_IsReadOnly(self): factory = RequestFactory() request = factory.get('/') permission = IsReadOnly.has_object_permission(self, request=request, view=None, obj=None) self.assertTrue(permission) - + def test_IsNotReadOnly(self): factory = RequestFactory() request = factory.post('/') permission = IsReadOnly.has_object_permission(self, request=request, view=None, obj=None) - self.assertFalse(permission) \ No newline at end of file + self.assertFalse(permission) -- GitLab From f8456d46fe0ac2ac9a70a4bac0f4e2a90100cbdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Einar=20Uvsl=C3=B8kk?= Date: Sat, 13 Mar 2021 23:18:48 +0100 Subject: [PATCH 3/4] Refactor black box test code Create a base class for black box test cases, with common Selenium setup and various utility methods for interacting with the web app. The test data fixtures are also split into separate files, in order to enable sharing between various tests. --- backend/secfit/requirements.txt | 1 + backend/secfit/tests/common.py | 121 +++++++++++++++++++++ backend/secfit/tests/test-data.json | 122 ---------------------- backend/secfit/tests/test-exercises.json | 20 ++++ backend/secfit/tests/test-users.json | 21 ++++ backend/secfit/tests/test-workouts.json | 45 ++++++++ backend/secfit/tests/test_view_workout.py | 81 +++----------- 7 files changed, 223 insertions(+), 188 deletions(-) create mode 100644 backend/secfit/tests/common.py delete mode 100644 backend/secfit/tests/test-data.json create mode 100644 backend/secfit/tests/test-exercises.json create mode 100644 backend/secfit/tests/test-users.json create mode 100644 backend/secfit/tests/test-workouts.json diff --git a/backend/secfit/requirements.txt b/backend/secfit/requirements.txt index 6f0113d..07380af 100644 --- a/backend/secfit/requirements.txt +++ b/backend/secfit/requirements.txt @@ -25,6 +25,7 @@ pylint-plugin-utils==0.6 pytz==2020.1 requests==2.24.0 rope==0.17.0 +selenium==3.141.0 six==1.15.0 sqlparse==0.3.1 toml==0.10.1 diff --git a/backend/secfit/tests/common.py b/backend/secfit/tests/common.py new file mode 100644 index 0000000..0291538 --- /dev/null +++ b/backend/secfit/tests/common.py @@ -0,0 +1,121 @@ +import enum +import os +from dataclasses import dataclass +from urllib.parse import urljoin + +from django.test import LiveServerTestCase +from selenium import webdriver +from selenium.webdriver.support.wait import WebDriverWait + +backend_port = os.environ.get("BACKEND_PORT", 8000) +frontend_port = os.environ.get("FRONTEND_PORT", 8001) +frontend_address = f"http://localhost:{frontend_port}" +webdriver_path = os.environ.get("WEBDRIVER", "chromedriver") + + +class Visibility(str, enum.Enum): + """Enum representing workout visibility.""" + + PUBLIC = "PU" + COACH = "CO" + PRIVATE = "PR" + + +@dataclass +class LoginData(dict): + username: str + password: str + + +login_data = { + "athlete": LoginData("arya", "dOnOtCaLlMeMiLaDy"), + "coach": LoginData("syrio", "wHaTdOwEsAyToThEgOdOfDeAtH"), +} + + +class BlackBoxTestServer(LiveServerTestCase): + """Base class for running black box tests with Selenium. + + The frontend needs be started before running the tests. The frontend port + can be specified using the environment variable FRONTEND_PORT. If this + environment variable is not present, the test case expect the frontend to + be running on port 8001. + + Likewise, the port for the backend test server can be specified using the + BACKEND_PORT environment variable, which defaults to 8000. + + Examples: + + The following examples assumes you stand in the root folder of repo. + + Run tests with default values: + + (cd frontend && cordova run browser --port=8001) + (cd backend/secfit && python manage.py test tests) + + Run tests with backend listening on port 12345: + + ( + cd frontend && + BACKEND_HOST=http://localhost:12345 \ + cordova run browser --port=8001 + ) + ( + cd backend/secfit && + BACKEND_PORT=12345 python manage.py test tests + ) + + """ + + port = backend_port + timeout = 5 + headless = False + + @classmethod + def setUpClass(cls): + super().setUpClass() + options = webdriver.ChromeOptions() + options.headless = cls.headless + cls.driver = webdriver.Chrome(webdriver_path, options=options) + cls.wait = WebDriverWait(cls.driver, cls.timeout) + + @classmethod + def tearDownClass(cls): + cls.driver.quit() + super().tearDownClass() + + @property + def address(self): + return frontend_address + + def get(self, page: str, **params): + """Navigate to a given web page. + + Args: + page (str): The web page to navigate to. + **params: Arbitrary keyword arguments used to create and attach a + query string to the web page URL. + + """ + url = urljoin(self.address, page) + if params: + query_str = "&".join([f"{k}={v}" for k, v in params.items()]) + url = f"{url}?{query_str}" + self.driver.get(url) + + def login(self, user: LoginData): + """Log in a user and wait until redirected to the workouts page.""" + self.get("login.html") + self.assertEqual("Login", self.driver.title) + username_input = self.driver.find_element_by_name("username") + username_input.send_keys(user.username) + password_input = self.driver.find_element_by_name("password") + password_input.send_keys(user.password) + self.driver.find_element_by_id("btn-login").click() + self.wait.until(lambda driver: driver.title == "Workouts") + + def logout(self): + """Log out a user and wait until redirected to the home page.""" + self.get("logout.html") + self.assertEqual("Logout", self.driver.title) + self.wait.until(lambda driver: driver.title == "Home") diff --git a/backend/secfit/tests/test-data.json b/backend/secfit/tests/test-data.json deleted file mode 100644 index 46e28d2..0000000 --- a/backend/secfit/tests/test-data.json +++ /dev/null @@ -1,122 +0,0 @@ -[ - { - "model": "users.user", - "pk": 1, - "fields": { - "username": "syrio", - "email": "syrio@forel.net", - "password": "pbkdf2_sha256$216000$4rwboPYCivJX$/ixnhVdqQoX3G+t4dlNwcgb0lpPg00pZuOBqpv/ML9E=" - } - }, - { - "model": "users.user", - "pk": 2, - "fields": { - "username": "arya", - "email": "arya@stark.io", - "password": "pbkdf2_sha256$216000$glNd7zmKsCPk$Et1Wg9R1857TSb3eiaV6p4OZKi46AEwupcix2pAIS+I=", - "coach": 1 - } - }, - { - "model": "workouts.workout", - "pk": 1, - "fields": { - "name": "Sword practice with Mycah", - "date": "2011-04-24T20:00:00.00000Z", - "notes": "Got interrupted by Sansa and Joffrey. Nymeria bit Joffrey in the arm, and I threw his sword into the river!", - "visibility": "PU", - "owner": 2 - } - }, - { - "model": "workouts.workout", - "pk": 2, - "fields": { - "name": "Sword practice with Syrio", - "date": "2021-03-09T20:15:00.00000Z", - "visibility": "CO", - "owner": 2 - } - }, - { - "model": "workouts.workout", - "pk": 3, - "fields": { - "name": "My last duel", - "date": "2011-06-11T20:00:00.00000Z", - "notes": "Got interrupted by the Kingsquard during practice with Arya, and had to battle Ser Meryn Trant...", - "visibility": "PU", - "owner": 1 - } - }, - { - "model": "workouts.workout", - "pk": 4, - "fields": { - "name": "Blind training in Braavos", - "date": "2016-04-24T20:00:00.00000Z", - "notes": "Got really beaten up by the Waif today. Hard to fight back when I'm blind.", - "visibility": "PR", - "owner": 2 - } - }, - { - "model": "workouts.exercise", - "pk": 1, - "fields": { - "name": "Fencing", - "description": "Stick them with the pointy end.", - "unit": "reps" - } - }, - { - "model": "workouts.exercise", - "pk": 2, - "fields": { - "name": "Stick fighting", - "description": "Hit opponent and pair strokes from opponent. Repeat.", - "unit": "reps" - } - }, - { - "model": "workouts.exerciseinstance", - "pk": 1, - "fields": { - "workout": 1, - "exercise": 1, - "sets": 3, - "number": 8 - } - }, - { - "model": "workouts.exerciseinstance", - "pk": 2, - "fields": { - "workout": 2, - "exercise": 1, - "sets": 3, - "number": 8 - } - }, - { - "model": "workouts.exerciseinstance", - "pk": 2, - "fields": { - "workout": 3, - "exercise": 2, - "sets": 3, - "number": 8 - } - }, - { - "model": "workouts.exerciseinstance", - "pk": 2, - "fields": { - "workout": 4, - "exercise": 2, - "sets": 3, - "number": 8 - } - } -] diff --git a/backend/secfit/tests/test-exercises.json b/backend/secfit/tests/test-exercises.json new file mode 100644 index 0000000..f563867 --- /dev/null +++ b/backend/secfit/tests/test-exercises.json @@ -0,0 +1,20 @@ +[ + { + "model": "workouts.exercise", + "pk": 1, + "fields": { + "name": "Fencing", + "description": "Stick them with the pointy end.", + "unit": "reps" + } + }, + { + "model": "workouts.exercise", + "pk": 2, + "fields": { + "name": "Stick fighting", + "description": "Hit opponent and pair strokes from opponent. Repeat.", + "unit": "reps" + } + } +] diff --git a/backend/secfit/tests/test-users.json b/backend/secfit/tests/test-users.json new file mode 100644 index 0000000..bf3d831 --- /dev/null +++ b/backend/secfit/tests/test-users.json @@ -0,0 +1,21 @@ +[ + { + "model": "users.user", + "pk": 1, + "fields": { + "username": "syrio", + "email": "syrio@forel.net", + "password": "pbkdf2_sha256$216000$4rwboPYCivJX$/ixnhVdqQoX3G+t4dlNwcgb0lpPg00pZuOBqpv/ML9E=" + } + }, + { + "model": "users.user", + "pk": 2, + "fields": { + "username": "arya", + "email": "arya@stark.io", + "password": "pbkdf2_sha256$216000$glNd7zmKsCPk$Et1Wg9R1857TSb3eiaV6p4OZKi46AEwupcix2pAIS+I=", + "coach": 1 + } + } +] diff --git a/backend/secfit/tests/test-workouts.json b/backend/secfit/tests/test-workouts.json new file mode 100644 index 0000000..3dbcbdf --- /dev/null +++ b/backend/secfit/tests/test-workouts.json @@ -0,0 +1,45 @@ +[ + { + "model": "workouts.workout", + "pk": 1, + "fields": { + "name": "Sword practice with Mycah", + "date": "2011-04-24T20:00:00.00000Z", + "notes": "Got interrupted by Sansa and Joffrey. Nymeria bit Joffrey in the arm, and I threw his sword into the river!", + "visibility": "PU", + "owner": 2 + } + }, + { + "model": "workouts.workout", + "pk": 2, + "fields": { + "name": "Sword practice with Syrio", + "date": "2021-03-09T20:15:00.00000Z", + "visibility": "CO", + "owner": 2 + } + }, + { + "model": "workouts.workout", + "pk": 3, + "fields": { + "name": "My last duel", + "date": "2011-06-11T20:00:00.00000Z", + "notes": "Got interrupted by the Kingsquard during practice with Arya, and had to battle Ser Meryn Trant...", + "visibility": "PU", + "owner": 1 + } + }, + { + "model": "workouts.workout", + "pk": 4, + "fields": { + "name": "Blind training in Braavos", + "date": "2016-04-24T20:00:00.00000Z", + "notes": "Got really beaten up by the Waif today. Hard to fight back when I'm blind.", + "visibility": "PR", + "owner": 2 + } + } +] diff --git a/backend/secfit/tests/test_view_workout.py b/backend/secfit/tests/test_view_workout.py index b0aadc7..399ac4e 100644 --- a/backend/secfit/tests/test_view_workout.py +++ b/backend/secfit/tests/test_view_workout.py @@ -1,12 +1,11 @@ -import enum import os import pathlib -from dataclasses import dataclass -from django.test import LiveServerTestCase -from selenium.webdriver.chrome.webdriver import WebDriver from selenium.webdriver.common.by import By -from selenium.webdriver.support.wait import WebDriverWait + +from tests.common import BlackBoxTestServer +from tests.common import login_data +from tests.common import Visibility here = pathlib.Path(__file__).parent.absolute() backend_port = os.environ.get("BACKEND_PORT", 8000) @@ -14,23 +13,6 @@ frontend_port = os.environ.get("FRONTEND_PORT", 8001) frontend_address = f"http://localhost:{frontend_port}" -class Visibility(str, enum.Enum): - PUBLIC = "PU" - COACH = "CO" - PRIVATE = "PR" - - -@dataclass -class LoginData(dict): - username: str - password: str - - -login_data = { - "athlete": LoginData("arya", "dOnOtCaLlMeMiLaDy"), - "coach": LoginData("syrio", "wHaTdOwEsAyToThEgOdOfDeAtH"), -} - workout_input_ids = ( "inputName", "inputDateTime", @@ -40,7 +22,7 @@ workout_input_ids = ( ) -class ViewWorkoutTest(LiveServerTestCase): +class ViewWorkoutTest(BlackBoxTestServer): """Test case for FR5 View Workout. Requirement description: @@ -53,59 +35,26 @@ class ViewWorkoutTest(LiveServerTestCase): needs to be public. """ - port = backend_port - timeout = 5 + headless = True fixtures = [ - here / "test-data.json" + here / "test-users.json", + here / "test-exercises.json", + here / "test-workouts.json", ] - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.driver = WebDriver() - cls.wait = WebDriverWait(cls.driver, cls.timeout) - - @classmethod - def tearDownClass(cls): - cls.driver.quit() - super().tearDownClass() - - def goto(self, page: str): - self.driver.get(f"{frontend_address}/{page}") - - def goto_workout(self, id): - self.goto(f"workout.html?id={id}") - def wait_until_value_not_empty(self, by: By, locator: str): - self.wait.until(lambda driver: self.get_input_value(by, locator) != "") + self.wait.until(lambda _: self.get_input_value(by, locator) != "") def get_input_value(self, by: By, locator: str) -> str: element = self.driver.find_element(by, locator) self.assertIsNotNone(element) return element.get_attribute("value") - def login(self, user: LoginData): - """Log in a user and wait until redirected to the workouts page.""" - self.goto("login.html") - self.assertEqual("Login", self.driver.title) - username_input = self.driver.find_element_by_name("username") - username_input.send_keys(user.username) - password_input = self.driver.find_element_by_name("password") - password_input.send_keys(user.password) - self.driver.find_element_by_id("btn-login").click() - self.wait.until(lambda driver: driver.title == "Workouts") - - def logout(self): - """Log out a user.""" - self.goto("logout.html") - self.assertEqual("Logout", self.driver.title) - self.wait.until(lambda driver: driver.title == "Home") - def test_user_can_view_own_public_workout(self): """Test if a user is able to view their own public workout.""" user = login_data["athlete"] self.login(user) - self.goto_workout(1) + self.get("workout.html", id=1) self.assertEqual("Workout", self.driver.title) self.wait_until_value_not_empty(By.ID, "inputOwner") for input_id in workout_input_ids: @@ -121,7 +70,7 @@ class ViewWorkoutTest(LiveServerTestCase): """Test if a coach is able to view their athlete's workout.""" user = login_data["coach"] self.login(user) - self.goto_workout(2) + self.get("workout.html", id=2) self.assertEqual("Workout", self.driver.title) self.wait_until_value_not_empty(By.ID, "inputOwner") for input_id in workout_input_ids: @@ -136,7 +85,7 @@ class ViewWorkoutTest(LiveServerTestCase): """Test if a user is able to view another users' public workout.""" user = login_data["athlete"] self.login(user) - self.goto_workout(3) + self.get("workout.html", id=3) self.assertEqual("Workout", self.driver.title) self.wait_until_value_not_empty(By.ID, "inputOwner") for input_id in workout_input_ids: @@ -151,7 +100,7 @@ class ViewWorkoutTest(LiveServerTestCase): """Test if a user is able to view their own private workout.""" user = login_data["athlete"] self.login(user) - self.goto_workout(4) + self.get("workout.html", id=4) self.assertEqual("Workout", self.driver.title) self.wait_until_value_not_empty(By.ID, "inputOwner") for input_id in workout_input_ids: @@ -166,7 +115,7 @@ class ViewWorkoutTest(LiveServerTestCase): """Test if a user is unable to view another users' private workout.""" user = login_data["coach"] self.login(user) - self.goto_workout(4) + self.get("workout.html", id=4) self.assertEqual("Workout", self.driver.title) alert = self.driver.find_elements_by_xpath("//div[@role='alert']") self.assertIsNotNone(alert) -- GitLab From cb4d74cdf013b68006a30a37d22b20ff48f3b352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Einar=20Uvsl=C3=B8kk?= Date: Mon, 15 Mar 2021 11:32:16 +0100 Subject: [PATCH 4/4] Move fixtures to separate data folder --- .../tests/{test-exercises.json => data/exercises.json} | 0 backend/secfit/tests/{test-users.json => data/users.json} | 0 .../tests/{test-workouts.json => data/workouts.json} | 0 backend/secfit/tests/test_view_workout.py | 7 ++++--- 4 files changed, 4 insertions(+), 3 deletions(-) rename backend/secfit/tests/{test-exercises.json => data/exercises.json} (100%) rename backend/secfit/tests/{test-users.json => data/users.json} (100%) rename backend/secfit/tests/{test-workouts.json => data/workouts.json} (100%) diff --git a/backend/secfit/tests/test-exercises.json b/backend/secfit/tests/data/exercises.json similarity index 100% rename from backend/secfit/tests/test-exercises.json rename to backend/secfit/tests/data/exercises.json diff --git a/backend/secfit/tests/test-users.json b/backend/secfit/tests/data/users.json similarity index 100% rename from backend/secfit/tests/test-users.json rename to backend/secfit/tests/data/users.json diff --git a/backend/secfit/tests/test-workouts.json b/backend/secfit/tests/data/workouts.json similarity index 100% rename from backend/secfit/tests/test-workouts.json rename to backend/secfit/tests/data/workouts.json diff --git a/backend/secfit/tests/test_view_workout.py b/backend/secfit/tests/test_view_workout.py index 399ac4e..08fff0d 100644 --- a/backend/secfit/tests/test_view_workout.py +++ b/backend/secfit/tests/test_view_workout.py @@ -8,6 +8,7 @@ from tests.common import login_data from tests.common import Visibility here = pathlib.Path(__file__).parent.absolute() +data_dir = here / "data" backend_port = os.environ.get("BACKEND_PORT", 8000) frontend_port = os.environ.get("FRONTEND_PORT", 8001) frontend_address = f"http://localhost:{frontend_port}" @@ -37,9 +38,9 @@ class ViewWorkoutTest(BlackBoxTestServer): headless = True fixtures = [ - here / "test-users.json", - here / "test-exercises.json", - here / "test-workouts.json", + data_dir / "users.json", + data_dir / "exercises.json", + data_dir / "workouts.json", ] def wait_until_value_not_empty(self, by: By, locator: str): -- GitLab