From cfe18e9f9bd2e53c954aea95c34ac741242d3a3e Mon Sep 17 00:00:00 2001 From: Boomaa23 Date: Sat, 31 Aug 2024 02:15:04 -0700 Subject: [PATCH] Run backend unit tests in Github Actions --- .github/workflows/unittest.yml | 19 +++ server/src/__init__.py | 0 server/src/db.py | 4 +- server/tst/__init__.py | 11 ++ server/tst/api_box_routes_test.py | 56 ------- server/tst/test_api_box_routes.py | 196 +++++++++++++++++++++++++ server/tst/{testutil.py => tstutil.py} | 14 ++ 7 files changed, 243 insertions(+), 57 deletions(-) create mode 100644 .github/workflows/unittest.yml create mode 100644 server/src/__init__.py create mode 100644 server/tst/__init__.py delete mode 100644 server/tst/api_box_routes_test.py create mode 100644 server/tst/test_api_box_routes.py rename server/tst/{testutil.py => tstutil.py} (83%) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml new file mode 100644 index 0000000..9cca93f --- /dev/null +++ b/.github/workflows/unittest.yml @@ -0,0 +1,19 @@ +name: unittest + +on: + pull_request: + push: + branches: [main] + +jobs: + unittest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 + - run: | + cd server + python -m pip install -r requirements.txt + - run: | + cd server + python -m unittest discover -v diff --git a/server/src/__init__.py b/server/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/src/db.py b/server/src/db.py index 633665c..f166116 100644 --- a/server/src/db.py +++ b/server/src/db.py @@ -17,7 +17,7 @@ def get(id_: str, entity_type: Type[models.Model]): res = cursor.execute(f'SELECT * FROM {entity_type.table_name} WHERE {entity_type.id_name}=?', (id_,)) db_entity = res.fetchone() if db_entity is None or len(db_entity) == 0: - flask.abort(404, 'Item does not exist') + flask.abort(404, f'{entity_type.__name__} does not exist') entity = entity_type(*db_entity) return entity.to_response() @@ -37,6 +37,8 @@ def update(entity_type: Type[models.Model], immutable_props: list[str]): # TODO have a better solution for mutability entity_properties = models.get_entity_parameters(entity_type) for immutable_prop in immutable_props: + if immutable_prop in form.form: + flask.abort(400, f'Immutable property {immutable_prop} found in request body') entity_properties.pop(immutable_prop) properties_to_update = {k: v for k, v in entity_properties.items() if k in form.form} diff --git a/server/tst/__init__.py b/server/tst/__init__.py new file mode 100644 index 0000000..c518bcd --- /dev/null +++ b/server/tst/__init__.py @@ -0,0 +1,11 @@ +import os +import sys + + +_project_path = os.getcwd() + +_source_path = os.path.join(_project_path, 'src') +sys.path.append(_source_path) + +_test_path = os.path.join(_project_path, 'tst') +sys.path.append(_test_path) diff --git a/server/tst/api_box_routes_test.py b/server/tst/api_box_routes_test.py deleted file mode 100644 index 11100bb..0000000 --- a/server/tst/api_box_routes_test.py +++ /dev/null @@ -1,56 +0,0 @@ -import unittest -from typing import Optional - -import auth -import models -import testutil -from identifier import Identifier - - -class TestBoxCreate(testutil.TestBase): - scope = auth.Scope.BOX_CREATE - - def test_200(self): - attrs = { - 'name': 'test-box-name', - 'api_key': self.superuser.api_key, - } - resp_json = self.call_route_assert_code(200, attrs) - - self.assertIn('body', resp_json) - self.assertIsNotNone(resp_json['body']) - self.assertEqual(1, len(resp_json['body'])) - created_box = resp_json['body'][0] - self.assertIsNotNone(created_box) - self.assertIn('name', created_box) - self.assertEqual(attrs['name'], created_box['name']) - - self.assertIsNotNone(Identifier(length=models.Box.id_length, id_=created_box[models.Box.id_name])) - - def test_400_no_name(self): - attrs = { - 'api_key': self.superuser.api_key, - } - self.call_route_assert_code(400, attrs) - - def test_400_malformed_name(self): - attrs = { - 'name': '*', - 'api_key': self.superuser.api_key, - } - self.call_route_assert_code(400, attrs) - - def test_400_duplicate_name(self): - attrs = { - 'name': 'test-box-duplicate-name', - 'api_key': self.superuser.api_key, - } - self.call_route_assert_code(200, attrs) - self.call_route_assert_code(400, attrs) - - def call_route(self, attrs: Optional[dict[str, str]] = None): - return self.client.post('/api/box/create', data=attrs) - - -if __name__ == '__main__': - unittest.main() diff --git a/server/tst/test_api_box_routes.py b/server/tst/test_api_box_routes.py new file mode 100644 index 0000000..419421f --- /dev/null +++ b/server/tst/test_api_box_routes.py @@ -0,0 +1,196 @@ +import json +import unittest +from typing import Optional + +import auth +import models +import tstutil +from identifier import Identifier + + +class TestBoxCreate(tstutil.TestBase): + scope = auth.Scope.BOX_CREATE + + def test_200(self): + attrs = { + 'name': 'tst-box-name', + 'api_key': self.superuser.api_key, + } + resp_json = self.call_route_assert_code(200, attrs) + entity_json = self.assert_single_entity( + resp_json, { + 'name': attrs['name'], + }, + ) + self.assertIsNotNone(Identifier(length=models.Box.id_length, id_=entity_json[models.Box.id_name])) + + def test_400_no_name(self): + attrs = { + 'api_key': self.superuser.api_key, + } + self.call_route_assert_code(400, attrs) + + def test_400_malformed_name(self): + attrs = { + 'name': '*', + 'api_key': self.superuser.api_key, + } + self.call_route_assert_code(400, attrs) + + def test_400_duplicate_name(self): + attrs = { + 'name': 'tst-box-duplicate-name', + 'api_key': self.superuser.api_key, + } + self.call_route_assert_code(200, attrs) + self.call_route_assert_code(400, attrs) + + def call_route(self, attrs: Optional[dict[str, str]] = None): + return self.client.post('/api/box/create', data=attrs) + + +class TestBoxGet(tstutil.TestBase): + scope = auth.Scope.BOX_GET + + def setUp(self): + super().setUp() + attrs = { + 'name': 'tst-box-get', + 'api_key': self.superuser.api_key, + } + response = self.client.post('/api/box/create', data=attrs) + self.box = models.Box(*json.loads(response.data)['body'][0].values()) + + def test_200(self): + attrs = { + models.Box.id_name: self.box.box_id, + } + resp_json = self.call_route_assert_code(200, attrs) + self.assert_single_entity( + resp_json, { + models.Box.id_name: self.box.box_id, + 'name': self.box.name, + }, + ) + + def test_400_malformed_id(self): + attrs = { + models.Box.id_name: '*', + } + self.call_route_assert_code(400, attrs) + + def test_400_no_id(self): + attrs = {} + self.call_route_assert_code(400, attrs) + + def test_404_nonexistent_box(self): + attrs = { + models.Box.id_name: 'tst-box-get-nonexistent-id', + } + self.call_route_assert_code(404, attrs) + + def test_400_no_apikey(self): + # No authentication required + pass + + def test_400_malformed_apikey(self): + # No authentication required + pass + + def test_401_user_not_found(self): + # No authentication required + pass + + def test_403_user_unauthorized(self): + # No authentication required + pass + + def call_route(self, attrs: dict[str, str]): + return self.client.post('/api/box/get', data=attrs) + + +class TestBoxUpdate(tstutil.TestBase): + scope = auth.Scope.BOX_UPDATE + + def test_200(self): + pass + + def test_200_without_verification(self): + pass + + def test_400_malformed_id(self): + pass + + def test_400_no_id(self): + pass + + def test_404_nonexistent_box(self): + pass + + def test_400_change_immutable_properties(self): + pass + + def test_400_no_update_properties(self): + pass + + def test_400_malformed_update_properties(self): + pass + + def call_route(self, attrs: dict[str, str]): + return self.client.post('/api/box/update', data=attrs) + + +class TestBoxRemove(tstutil.TestBase): + scope = auth.Scope.BOX_REMOVE + + def test_200(self): + pass + + def test_200_without_verification(self): + pass + + def test_400_malformed_id(self): + pass + + def test_400_no_id(self): + pass + + def test_404_nonexistent_box(self): + pass + + def test_500_duplicate_id(self): + pass + + def call_route(self, attrs: dict[str, str]): + return self.client.post('/api/box/remove', data=attrs) + + +# TODO finish +class TestBoxesList(tstutil.TestBase): + scope = auth.Scope.BOXES_LIST + + def test_200(self): + pass + + def test_400_no_apikey(self): + # No authentication required + pass + + def test_400_malformed_apikey(self): + # No authentication required + pass + + def test_401_user_not_found(self): + # No authentication required + pass + + def test_403_user_unauthorized(self): + # No authentication required + pass + + def call_route(self, attrs: dict[str, str]): + return self.client.post('/api/boxes/list', data=attrs) + + +if __name__ == '__main__': + unittest.main() diff --git a/server/tst/testutil.py b/server/tst/tstutil.py similarity index 83% rename from server/tst/testutil.py rename to server/tst/tstutil.py index 40fd26f..6cda5c4 100644 --- a/server/tst/testutil.py +++ b/server/tst/tstutil.py @@ -35,6 +35,17 @@ def call_route_assert_code(self, status_code: int, attrs: Optional[dict[str, Any self.assertEqual(status_code, resp_json['code']) return resp_json + def assert_single_entity(self, resp_json, expected: dict[str, str]): + self.assertIn('body', resp_json) + self.assertIsNotNone(resp_json['body']) + self.assertEqual(1, len(resp_json['body'])) + entity_json = resp_json['body'][0] + self.assertIsNotNone(entity_json) + for key in expected.keys(): + self.assertIn(key, entity_json) + self.assertEqual(expected[key], entity_json[key]) + return entity_json + def test_400_no_apikey(self): attrs = {} self.call_route_assert_code(400, attrs) @@ -61,6 +72,9 @@ def test_403_user_unauthorized(self): } self.call_route_assert_code(403, attrs) + def test_200(self): + raise NotImplementedError() + def call_route(self, attrs: dict[str, str]): raise NotImplementedError()