Commit e73a1a7048dfa721ce0343ca2a89d8c305d3b8d2
Exists in
master
and in
4 other branches
More tests and refactoring
Showing
13 changed files
with
93 additions
and
47 deletions
Show diff stats
CHANGELOG.md
@@ -23,6 +23,9 @@ Should be noted only functionnal changes, | @@ -23,6 +23,9 @@ Should be noted only functionnal changes, | ||
23 | or major refactoring improvments. | 23 | or major refactoring improvments. |
24 | 24 | ||
25 | ## Unreleased | 25 | ## Unreleased |
26 | +## [0.3.pre-7] - 2021-05-03 - Better Tests Coverage | ||
27 | +### Changed | ||
28 | +Add tests and fix some code issues | ||
26 | 29 | ||
27 | ## [0.3.pre-6] - 2021-04-30 - Nicer Display | 30 | ## [0.3.pre-6] - 2021-04-30 - Nicer Display |
28 | ### Changed | 31 | ### Changed |
VERSION.txt
app/auth/models.py
@@ -30,7 +30,7 @@ _nameToRole = { | @@ -30,7 +30,7 @@ _nameToRole = { | ||
30 | } | 30 | } |
31 | 31 | ||
32 | 32 | ||
33 | -def _checkRole(role): | 33 | +def _check_role(role): |
34 | if isinstance(role, int): | 34 | if isinstance(role, int): |
35 | rv = role | 35 | rv = role |
36 | elif str(role) == role: | 36 | elif str(role) == role: |
@@ -65,14 +65,14 @@ class User(UserMixin, db.Model): | @@ -65,14 +65,14 @@ class User(UserMixin, db.Model): | ||
65 | self.password = None | 65 | self.password = None |
66 | 66 | ||
67 | def set_role(self, role): | 67 | def set_role(self, role): |
68 | - self.role = _checkRole(role) | 68 | + self.role = _check_role(role) |
69 | 69 | ||
70 | def has_role(self, role): | 70 | def has_role(self, role): |
71 | - role = _checkRole(role) | 71 | + role = _check_role(role) |
72 | return self.role == role | 72 | return self.role == role |
73 | 73 | ||
74 | def has_role_or_higher(self, role): | 74 | def has_role_or_higher(self, role): |
75 | - role = _checkRole(role) | 75 | + role = _check_role(role) |
76 | return self.role and (self.role >= role) | 76 | return self.role and (self.role >= role) |
77 | 77 | ||
78 | def set_password(self, password): | 78 | def set_password(self, password): |
app/auth/routes.py
@@ -7,6 +7,8 @@ from flask_login import login_user, logout_user | @@ -7,6 +7,8 @@ from flask_login import login_user, logout_user | ||
7 | from app.auth.models import User | 7 | from app.auth.models import User |
8 | from app.auth import bp | 8 | from app.auth import bp |
9 | 9 | ||
10 | +main_index = 'main.index' | ||
11 | + | ||
10 | 12 | ||
11 | # | 13 | # |
12 | # Decorator used to protect routes by role | 14 | # Decorator used to protect routes by role |
@@ -30,10 +32,10 @@ def role_required(role): | @@ -30,10 +32,10 @@ def role_required(role): | ||
30 | try: | 32 | try: |
31 | is_authorised = current_user.has_role_or_higher(role) | 33 | is_authorised = current_user.has_role_or_higher(role) |
32 | except ValueError: | 34 | except ValueError: |
33 | - raise Exception("Unknowk role provided %s" % role) | 35 | + raise ValueError("Unknown role provided %s" % role) |
34 | if not is_authorised: | 36 | if not is_authorised: |
35 | flash("Vous n'avez pas les autorisations pour accéder à cette page", 'dark') | 37 | flash("Vous n'avez pas les autorisations pour accéder à cette page", 'dark') |
36 | - return redirect(url_for('main.index')) | 38 | + return redirect(url_for(main_index)) |
37 | return f(*args, **kwargs) | 39 | return f(*args, **kwargs) |
38 | 40 | ||
39 | return decorated_function | 41 | return decorated_function |
@@ -50,12 +52,11 @@ def login(): | @@ -50,12 +52,11 @@ def login(): | ||
50 | def login_post(): | 52 | def login_post(): |
51 | user_login = request.form.get('login') | 53 | user_login = request.form.get('login') |
52 | user_password = request.form.get('password') | 54 | user_password = request.form.get('password') |
53 | - # user_remember = request.form.get('remember') | ||
54 | user = User.query.filter_by(login=user_login).one_or_none() | 55 | user = User.query.filter_by(login=user_login).one_or_none() |
55 | if user and user.check_password(user_password): | 56 | if user and user.check_password(user_password): |
56 | login_user(user) | 57 | login_user(user) |
57 | flash("Connection Réussie !", 'success') | 58 | flash("Connection Réussie !", 'success') |
58 | - return redirect(url_for('main.index')) | 59 | + return redirect(url_for(main_index)) |
59 | else: | 60 | else: |
60 | flash("Mauvais login ou mot de passe.", 'warning') | 61 | flash("Mauvais login ou mot de passe.", 'warning') |
61 | return redirect(url_for('auth.login')) | 62 | return redirect(url_for('auth.login')) |
@@ -65,4 +66,4 @@ def login_post(): | @@ -65,4 +66,4 @@ def login_post(): | ||
65 | def logout(): | 66 | def logout(): |
66 | logout_user() | 67 | logout_user() |
67 | flash("Vous êtes maintenant déconnecté", 'info') | 68 | flash("Vous êtes maintenant déconnecté", 'info') |
68 | - return redirect(url_for('main.index')) | 69 | + return redirect(url_for(main_index)) |
app/commands/commands.py
@@ -38,7 +38,7 @@ def create_db(): | @@ -38,7 +38,7 @@ def create_db(): | ||
38 | sys.exit(-1) | 38 | sys.exit(-1) |
39 | 39 | ||
40 | if sqlite_uri: | 40 | if sqlite_uri: |
41 | - current_app.logger.info("Created sqlite db: " + sqlite_uri) | 41 | + print("Created sqlite db: " + sqlite_uri) |
42 | 42 | ||
43 | 43 | ||
44 | @bp.cli.command("feed_from_irap") | 44 | @bp.cli.command("feed_from_irap") |
@@ -433,7 +433,7 @@ def feed_periods(begin_year, end_year): | @@ -433,7 +433,7 @@ def feed_periods(begin_year, end_year): | ||
433 | @click.option('--agent', '-a', 'agent', default=None, help="the agent id you want to charge") | 433 | @click.option('--agent', '-a', 'agent', default=None, help="the agent id you want to charge") |
434 | def feed_random_charges(agent): | 434 | def feed_random_charges(agent): |
435 | """ Randomly fill in the agents charges. """ | 435 | """ Randomly fill in the agents charges. """ |
436 | - for i in range(0, 100): | 436 | + for _ in range(0, 100): |
437 | if agent is None: | 437 | if agent is None: |
438 | agent_id = random.choice([i for (i,) in db.session.query(Agent.id).all()]) | 438 | agent_id = random.choice([i for (i,) in db.session.query(Agent.id).all()]) |
439 | else: | 439 | else: |
app/commands/lesia_db.py
@@ -13,7 +13,8 @@ engine = create_engine(current_app.config['LESIA_AGENTS_DB_URI']) | @@ -13,7 +13,8 @@ engine = create_engine(current_app.config['LESIA_AGENTS_DB_URI']) | ||
13 | # reflect the tables | 13 | # reflect the tables |
14 | try: | 14 | try: |
15 | lesia_base.prepare(engine, reflect=True) | 15 | lesia_base.prepare(engine, reflect=True) |
16 | -except OperationalError: | 16 | +except OperationalError as oe: |
17 | + current_app.logger.error(oe) | ||
17 | current_app.logger.error("Please, configure the mysql database (see db_config.py)") | 18 | current_app.logger.error("Please, configure the mysql database (see db_config.py)") |
18 | sys.exit(-1) | 19 | sys.exit(-1) |
19 | 20 |
app/errors/handlers.py
@@ -2,30 +2,32 @@ from flask import render_template | @@ -2,30 +2,32 @@ from flask import render_template | ||
2 | from app import db | 2 | from app import db |
3 | from . import bp | 3 | from . import bp |
4 | 4 | ||
5 | - | ||
6 | # Inspired by: | 5 | # Inspired by: |
7 | # https://flask.palletsprojects.com/en/master/patterns/errorpages/ | 6 | # https://flask.palletsprojects.com/en/master/patterns/errorpages/ |
8 | 7 | ||
8 | +error_page = 'error.html' | ||
9 | + | ||
10 | + | ||
9 | @bp.app_errorhandler(403) | 11 | @bp.app_errorhandler(403) |
10 | def forbidden_error(error): | 12 | def forbidden_error(error): |
11 | error_title = "Page Interdite" | 13 | error_title = "Page Interdite" |
12 | - return render_template('error.html', error_title=error_title, error_msg=error), 403 | 14 | + return render_template(error_page, error_title=error_title, error_msg=error), 403 |
13 | 15 | ||
14 | 16 | ||
15 | @bp.app_errorhandler(404) | 17 | @bp.app_errorhandler(404) |
16 | def not_found_error(error): | 18 | def not_found_error(error): |
17 | error_title = "Page Introuvable." | 19 | error_title = "Page Introuvable." |
18 | - return render_template('error.html', error_title=error_title, error_msg=error), 404 | 20 | + return render_template(error_page, error_title=error_title, error_msg=error), 404 |
19 | 21 | ||
20 | 22 | ||
21 | @bp.app_errorhandler(405) | 23 | @bp.app_errorhandler(405) |
22 | def method_error(error): | 24 | def method_error(error): |
23 | error_title = "Erreur de Méthode." | 25 | error_title = "Erreur de Méthode." |
24 | - return render_template('error.html', error_title=error_title, error_msg=error), 405 | 26 | + return render_template(error_page, error_title=error_title, error_msg=error), 405 |
25 | 27 | ||
26 | 28 | ||
27 | @bp.app_errorhandler(500) | 29 | @bp.app_errorhandler(500) |
28 | def internal_error(error): | 30 | def internal_error(error): |
29 | db.session.rollback() | 31 | db.session.rollback() |
30 | error_title = "Erreur Interne. Administrateur Prévenu." | 32 | error_title = "Erreur Interne. Administrateur Prévenu." |
31 | - return render_template('error.html', error_title=error_title, error_msg=error), 500 | 33 | + return render_template(error_page, error_title=error_title, error_msg=error), 500 |
resources/db_config.py
@@ -49,7 +49,7 @@ MYSQL_LESIA = { | @@ -49,7 +49,7 @@ MYSQL_LESIA = { | ||
49 | 'host': '127.0.0.1', | 49 | 'host': '127.0.0.1', |
50 | 'port': '3306', | 50 | 'port': '3306', |
51 | } | 51 | } |
52 | -mysql_lesia_uri = 'mysql+pymysql://%(user)s:%(pw)s@%(host)s:%(port)s/%(db)s' % MYSQL | 52 | +mysql_lesia_uri = 'mysql+pymysql://%(user)s:%(pw)s@%(host)s:%(port)s/%(db)s' % MYSQL_LESIA |
53 | 53 | ||
54 | # To set you databases uri, | 54 | # To set you databases uri, |
55 | # uncomment the needed lines to fit your specific needs; | 55 | # uncomment the needed lines to fit your specific needs; |
resources/lesia-btp.sqlite
No preview for this file type
tests/backend_tests.py
@@ -6,20 +6,14 @@ from shutil import copyfile | @@ -6,20 +6,14 @@ from shutil import copyfile | ||
6 | from pdc_config import TestConfig | 6 | from pdc_config import TestConfig |
7 | from app import create_app, db_mgr, db | 7 | from app import create_app, db_mgr, db |
8 | from app.auth.models import User | 8 | from app.auth.models import User |
9 | +from tests.common_db_feed import resources_to_instancedb | ||
9 | 10 | ||
10 | 11 | ||
11 | class BaseTestCase(unittest.TestCase): | 12 | class BaseTestCase(unittest.TestCase): |
12 | def setUp(self): | 13 | def setUp(self): |
13 | # initialise app | 14 | # initialise app |
14 | self.app = create_app(TestConfig) | 15 | self.app = create_app(TestConfig) |
15 | - | ||
16 | - # copy resource demo db to test file | ||
17 | - appdir = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir) | ||
18 | - sqlite_file_name = os.path.abspath(os.path.join(appdir, 'resources', 'lesia-btp.sqlite')) | ||
19 | - if not os.path.isdir(self.app.instance_path): | ||
20 | - os.mkdir(self.app.instance_path) | ||
21 | - self.db_path = os.path.join(self.app.instance_path, 'test.db') | ||
22 | - copyfile(sqlite_file_name, self.db_path) | 16 | + self.db_path = resources_to_instancedb(self.app) |
23 | 17 | ||
24 | # force db path to newly create file | 18 | # force db path to newly create file |
25 | self.app.config.update( | 19 | self.app.config.update( |
@@ -59,4 +53,3 @@ class DbMgrTestCase(BaseTestCase): | @@ -59,4 +53,3 @@ class DbMgrTestCase(BaseTestCase): | ||
59 | stacked_charges = db_mgr.charges_by_project_stacked(60) | 53 | stacked_charges = db_mgr.charges_by_project_stacked(60) |
60 | # Waiting for 17 periods + headers line | 54 | # Waiting for 17 periods + headers line |
61 | self.assertEqual(18, len(stacked_charges)) | 55 | self.assertEqual(18, len(stacked_charges)) |
62 | - |
tests/cli_tests.py
1 | import unittest | 1 | import unittest |
2 | 2 | ||
3 | from app import create_app, db, User | 3 | from app import create_app, db, User |
4 | -from app.commands.commands import feed_from_irap, show_roles, user_add, user_show_all | 4 | +from app.commands.commands import feed_from_irap, show_roles, user_add, user_show_all, create_db |
5 | from pdc_config import TestConfig | 5 | from pdc_config import TestConfig |
6 | 6 | ||
7 | from app.commands import bp as cli_bp | 7 | from app.commands import bp as cli_bp |
@@ -10,7 +10,8 @@ from app.commands import bp as cli_bp | @@ -10,7 +10,8 @@ from app.commands import bp as cli_bp | ||
10 | class CliBaseTestCase(unittest.TestCase): | 10 | class CliBaseTestCase(unittest.TestCase): |
11 | 11 | ||
12 | def setUp(self): | 12 | def setUp(self): |
13 | - # Get rid of a strange 'ValueError: I/O operation' when app.logger outputs | 13 | + # Get rid of a strange 'ValueError: I/O operation' when pytest catches app.logger outputs |
14 | + # Error can also be bypassed with 'pytest -s' option | ||
14 | TestConfig.PDC_LOGS_LEVEL = 'ERROR' | 15 | TestConfig.PDC_LOGS_LEVEL = 'ERROR' |
15 | # Force sqlite in memory db | 16 | # Force sqlite in memory db |
16 | TestConfig.SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:' | 17 | TestConfig.SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:' |
@@ -24,16 +25,20 @@ class CliBaseTestCase(unittest.TestCase): | @@ -24,16 +25,20 @@ class CliBaseTestCase(unittest.TestCase): | ||
24 | def tearDown(self): | 25 | def tearDown(self): |
25 | pass | 26 | pass |
26 | 27 | ||
27 | - def test_hello3(self): | 28 | + def test_create_db(self): |
29 | + runner = self.app.test_cli_runner() | ||
30 | + result = runner.invoke(create_db) | ||
31 | + self.assertTrue('Created sqlite' in result.output) | ||
32 | + | ||
33 | + def test_show_all(self): | ||
28 | runner = self.app.test_cli_runner() | 34 | runner = self.app.test_cli_runner() |
29 | result = runner.invoke(user_show_all) | 35 | result = runner.invoke(user_show_all) |
30 | - print("\n") | ||
31 | - print(result.output) | 36 | + self.assertTrue('id' in result.output) |
32 | 37 | ||
33 | - def test_hello2(self): | 38 | + def test_show_roles(self): |
34 | runner = self.app.test_cli_runner() | 39 | runner = self.app.test_cli_runner() |
35 | result = runner.invoke(show_roles) | 40 | result = runner.invoke(show_roles) |
36 | - print(result.output) | 41 | + self.assertTrue('PUBLIC' in result.output) |
37 | 42 | ||
38 | def test_add_user(self): | 43 | def test_add_user(self): |
39 | runner = self.app.test_cli_runner() | 44 | runner = self.app.test_cli_runner() |
@@ -41,9 +46,7 @@ class CliBaseTestCase(unittest.TestCase): | @@ -41,9 +46,7 @@ class CliBaseTestCase(unittest.TestCase): | ||
41 | all_users = User.query.all() | 46 | all_users = User.query.all() |
42 | self.assertEqual(0, len(all_users)) | 47 | self.assertEqual(0, len(all_users)) |
43 | # invoke the command directly | 48 | # invoke the command directly |
44 | - arguments = ['geo@ici.fr', 'Joseph Hitier', 'joh', 'passwd', 'PUBLIC'] | ||
45 | - result = runner.invoke(user_add, arguments) | ||
46 | - print(result.output) | 49 | + arguments = ['geo@ici.fr', 'Géo Trouvetou', 'gt', 'passwd', 'PUBLIC'] |
50 | + runner.invoke(user_add, arguments) | ||
47 | all_users = User.query.all() | 51 | all_users = User.query.all() |
48 | - print(len(all_users)) | ||
49 | - # self.assertEqual(1, len(all_users)) | 52 | + self.assertEqual(1, len(all_users)) |
tests/common_db_feed.py
1 | +import os | ||
2 | +from shutil import copyfile | ||
3 | + | ||
1 | from app.models import Category, db, Label, Project, ProjectLabel | 4 | from app.models import Category, db, Label, Project, ProjectLabel |
2 | 5 | ||
6 | + | ||
7 | +def resources_to_instancedb(app): | ||
8 | + # copy resource demo db to test file | ||
9 | + appdir = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir) | ||
10 | + sqlite_file_name = os.path.abspath(os.path.join(appdir, 'resources', 'lesia-btp.sqlite')) | ||
11 | + if not os.path.isdir(app.instance_path): | ||
12 | + os.mkdir(app.instance_path) | ||
13 | + db_path = os.path.join(app.instance_path, 'test.db') | ||
14 | + copyfile(sqlite_file_name, db_path) | ||
15 | + return db_path | ||
16 | + | ||
17 | + | ||
3 | categorized_labels = {'pole': ['Spatial', 'Sol'], 'domaine': ['soleil-terre', 'atmosphere', 'r&t', 'géologie']} | 18 | categorized_labels = {'pole': ['Spatial', 'Sol'], 'domaine': ['soleil-terre', 'atmosphere', 'r&t', 'géologie']} |
4 | projects_categories = {'ChemCam': {'pole': 'Spatial', 'domaine': 'géologie'}, | 19 | projects_categories = {'ChemCam': {'pole': 'Spatial', 'domaine': 'géologie'}, |
5 | 'Pilot': {'pole': 'Spatial', 'domaine': 'atmosphere'}, | 20 | 'Pilot': {'pole': 'Spatial', 'domaine': 'atmosphere'}, |
@@ -30,7 +45,7 @@ def feed_projects(): | @@ -30,7 +45,7 @@ def feed_projects(): | ||
30 | n_l = db.session.query(Label).filter(Label.name == l_name).one() | 45 | n_l = db.session.query(Label).filter(Label.name == l_name).one() |
31 | n_pc = ProjectLabel(project=n_p, category=n_c, label=n_l) | 46 | n_pc = ProjectLabel(project=n_p, category=n_c, label=n_l) |
32 | db.session.add(n_pc) | 47 | db.session.add(n_pc) |
33 | - | 48 | + |
34 | db.session.commit() | 49 | db.session.commit() |
35 | 50 | ||
36 | 51 |
tests/frontend_tests.py
1 | import os | 1 | import os |
2 | import urllib.request | 2 | import urllib.request |
3 | 3 | ||
4 | +from flask import url_for | ||
4 | from flask_testing import LiveServerTestCase | 5 | from flask_testing import LiveServerTestCase |
5 | from selenium import webdriver | 6 | from selenium import webdriver |
6 | 7 | ||
7 | from app import create_app | 8 | from app import create_app |
8 | from pdc_config import TestConfig | 9 | from pdc_config import TestConfig |
10 | +from tests.common_db_feed import resources_to_instancedb | ||
9 | 11 | ||
10 | 12 | ||
11 | class BaseFrontTestCase(LiveServerTestCase): | 13 | class BaseFrontTestCase(LiveServerTestCase): |
@@ -18,18 +20,18 @@ class BaseFrontTestCase(LiveServerTestCase): | @@ -18,18 +20,18 @@ class BaseFrontTestCase(LiveServerTestCase): | ||
18 | os.environ['WERKZEUG_RUN_MAIN'] = 'true' | 20 | os.environ['WERKZEUG_RUN_MAIN'] = 'true' |
19 | 21 | ||
20 | # pass in test configurations | 22 | # pass in test configurations |
21 | - app = create_app(TestConfig) | ||
22 | - self.db_path = os.path.join(app.instance_path, 'test-app.db') | ||
23 | - app.config.update( | 23 | + self.app = create_app(TestConfig) |
24 | + self.db_path = resources_to_instancedb(self.app) | ||
25 | + self.app.config.update( | ||
24 | # Specify the test database as the default in_memory wont work for web tests | 26 | # Specify the test database as the default in_memory wont work for web tests |
25 | SQLALCHEMY_DATABASE_URI='sqlite:///' + self.db_path, | 27 | SQLALCHEMY_DATABASE_URI='sqlite:///' + self.db_path, |
26 | # Change the port that the liveserver listens on as we dont want to conflict with running:5000 | 28 | # Change the port that the liveserver listens on as we dont want to conflict with running:5000 |
27 | LIVESERVER_PORT=8943 | 29 | LIVESERVER_PORT=8943 |
28 | ) | 30 | ) |
29 | 31 | ||
30 | - self.app_context = app.app_context() | 32 | + self.app_context = self.app.app_context() |
31 | self.app_context.push() | 33 | self.app_context.push() |
32 | - return app | 34 | + return self.app |
33 | 35 | ||
34 | def setUp(self): | 36 | def setUp(self): |
35 | self.driver = self.create_chrome_driver() | 37 | self.driver = self.create_chrome_driver() |
@@ -86,4 +88,30 @@ class AccessTestCase(BaseFrontTestCase): | @@ -86,4 +88,30 @@ class AccessTestCase(BaseFrontTestCase): | ||
86 | 88 | ||
87 | def test_server_is_up_and_running(self): | 89 | def test_server_is_up_and_running(self): |
88 | response = urllib.request.urlopen(self.get_server_url()) | 90 | response = urllib.request.urlopen(self.get_server_url()) |
89 | - self.assertEqual(response.code, 200) | ||
90 | \ No newline at end of file | 91 | \ No newline at end of file |
92 | + self.assertEqual(response.code, 200) | ||
93 | + | ||
94 | + def test_agents_page(self): | ||
95 | + target_url = self.get_server_url() + url_for('main.agents') | ||
96 | + self.driver.get(target_url) | ||
97 | + self.assertEqual(self.driver.current_url, 'http://localhost:8943/agents') | ||
98 | + | ||
99 | + def test_agent_page(self): | ||
100 | + agent_id = 60 | ||
101 | + target_url = self.get_server_url() + url_for('main.agent', agent_id=agent_id) | ||
102 | + self.driver.get(target_url) | ||
103 | + self.assertEqual(self.driver.current_url, f'http://localhost:8943/agent/{agent_id}') | ||
104 | + td_elmts = self.driver.find_elements_by_xpath("//table[@id='charge_table']/tbody/tr/td") | ||
105 | + self.assertEqual(260, len(td_elmts)) | ||
106 | + | ||
107 | + def test_projects_page(self): | ||
108 | + target_url = self.get_server_url() + url_for('main.projects') | ||
109 | + self.driver.get(target_url) | ||
110 | + self.assertEqual(self.driver.current_url, 'http://localhost:8943/projects') | ||
111 | + | ||
112 | + def test_project_page(self): | ||
113 | + project_id = 8 | ||
114 | + target_url = self.get_server_url() + url_for('main.project', project_id=project_id) | ||
115 | + self.driver.get(target_url) | ||
116 | + self.assertEqual(self.driver.current_url, f'http://localhost:8943/project/{project_id}') | ||
117 | + td_elmts = self.driver.find_elements_by_xpath("//table[@id='charge_table']/tbody/tr/td") | ||
118 | + self.assertEqual(1085, len(td_elmts)) |