diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 200872cdc9..679e5640e5 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -6,6 +6,10 @@ name: Python application on: push: branches: [ "master" ] + paths: [ "app_python/**" ] + pull_request: + branches: [ "master" ] + paths: [ "app_python/**" ] pull_request: branches: [ "master" ] @@ -14,25 +18,53 @@ permissions: jobs: build: - runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 - name: Set up Python 3.10 uses: actions/setup-python@v3 with: python-version: "3.10" + - name: Install dependencies run: | python -m pip install --upgrade pip pip install flake8 pytest if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + + - name: Snyk + uses: snyk/actions/python-3.10@master + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + args: app_python/ --skip-unresolved + + - name: Test + run: | + cd app_python + python -m unittest tests.py + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v6.13.0 + with: + context: "{{defaultContext}}:app_python" + push: true + tags: ${{ vars.DOCKERHUB_USERNAME }}/moscow_time:latest + cache-from: type=gha + cache-to: type=gha,mode=max flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest run: | diff --git a/app_python/CI.md b/app_python/CI.md new file mode 100644 index 0000000000..321db9f036 --- /dev/null +++ b/app_python/CI.md @@ -0,0 +1,14 @@ + +### Best Practices + +1. __Path-Based Triggers__ - The workflow is executed only when any file within app_python/ directory was changed + +2. __Password and Username Secrets__ - For security enhancement the `Secrets` variables allow hiding personal access tokens + +3. __Variables__ - Make script more maintainable by reducing duplicated code framgents + +4. __Explicit Version__ - The same as with `Dockerfile`, explicit tool versioning allows better code debugging and understanding its limits. + +5. __Docker Caching__ - `cache-from` and `cache-to` attributes of building and pushing Docker image accelerate the process + +6. __Snyk vulnerabilities check__ - Another Security enhancement diff --git a/app_python/PYTHON.md b/app_python/PYTHON.md index c837b75539..1439321d06 100644 --- a/app_python/PYTHON.md +++ b/app_python/PYTHON.md @@ -1,3 +1,4 @@ +## Moscow Time Application ### 1. Choice Justification Flask is easy and beginner-friendly framework that allows fast webapp coding. Hopefully, if the future labs will require us to extend this application, __flask lightweight nature__ will support highly covering the lack of __built-in admin interface__ that [Django](https://en.wikipedia.org/wiki/Django_(web_framework)) has. @@ -7,5 +8,12 @@ Flask is easy and beginner-friendly framework that allows fast webapp coding. Ho 3. Confident requirements.txt via automatic __'pip freeze > requirements.txt'__ command. 4. Coding conventions: [Pylint](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint) extension highlights problematic pieces of code to be changed. -### 3. Testing Implementation -So far the webapp is not so complex for requiring testing automatization ~~waiting for the next labs making to do this~~ . +## Unit-Testing +### 1. Unit-test list +* ```test_status_code(self)``` - makes sure that GET request successfully returns an html page with __200__ Status Code +* ```test_response_content(self)``` - Checks that the received webpage contains lines that we needed +* ```test_validate_time(self)``` - Time comparison between then one the program calculated and the one obtained from the response, with request-response delay considerations +### 2. Best Practices +* __Isolated environment__. Tests do not have any side effects hurting other tests' output +* __Setting test_app framework in class__. This avoids multiple application initialization for all the tests. +* __Assertion with considering possible environmental requests__. That is, when comparing the time values in assert, an error value must be introduced to allow some deviation between values to be approximately equal. \ No newline at end of file diff --git a/app_python/README.md b/app_python/README.md index fe69ce0d81..53cbb70432 100644 --- a/app_python/README.md +++ b/app_python/README.md @@ -1,5 +1,7 @@ ## Python Flask Moscow timezone Watch +![Python CI](https://github.com/IlsiyaNasibullina/S25-core-course-labs/actions/workflows/app_python.yml/badge.svg) + ## Repository preparation ```bash @@ -51,13 +53,31 @@ Result of running is as follows: ![](images/image_docker2.png) +## Running unittest -# ENDING +While being in the repository's directory, input the following commands: -To run distroless: +```bash +cd app_python +python -m unittest tests.py +``` +This runs a few commands to check that the webapp works properly. In case of successful passing, the result should be similar to this: ```bash -docker build -f distroless.dockerfile -t dless . -docker run dless +... +---------------------------------------------------------------------- +Ran 3 tests in 0.004s + +OK ``` +## CI Workflow + +The workflow is configured to automate the following stages: + +* Dependencies - using requerements.txt +* Lint - Checking coding conventions (line length, etc.) +* Snyk - Checks for security vulnerabilities. +* Test - tests.py unittest is called to verify functionality +* Docker - Builds and pushes the Docker image to Docker Hub. + diff --git a/app_python/tests.py b/app_python/tests.py new file mode 100644 index 0000000000..276299be0e --- /dev/null +++ b/app_python/tests.py @@ -0,0 +1,70 @@ +''' +COMMENT +''' +from datetime import datetime +import re +import unittest +from moscow_app import app, MOSCOW + + +class TestMoscowApp(unittest.TestCase): + ''' + DOCSTRING CLASS + ''' + test_app = app.test_client() + + def test_status_code(self): + ''' + Check that request to html page is granted + ''' + response = self.test_app.get('/') + self.assertEqual(response.status_code, 200) + + def test_response_content(self): + ''' + To check that the html content consists of required message + ''' + response = self.test_app.get('/') + self.assertIn(b'Moscow Time:', response.data) + + def test_validate_time(self): + ''' + comment + ''' + response = self.test_app.get('/') + msc = datetime.now(MOSCOW) + current = datetime.today() + # needed to measure time units to cast to a single integer + # meaning number of seconds + values = [3600, 60, 1] + # needed to prove approximate equality between + # 23:59:59 and 00:00:01 time dates, for instance + cycle = 3600 * 60 * 24 + # error means what difference between times is allowed to be true + error = 5 + + response_text = response.data.decode('utf-8') + + start = response_text.find("Moscow Time:") + msc_response = re.search(r'\d\d:\d\d:\d\d', response_text[start:]).group(0) + tmp = [int(x) for x in msc_response.split(":")] + msc_response = sum(a*b for a,b in zip(values,tmp)) + + start = response_text.find("Your Timezone:") + current_response = re.search(r'\d\d:\d\d:\d\d', response_text[start:]).group(0) + tmp = [int(x) for x in current_response.split(":")] + current_response = sum(a*b for a,b in zip(values, tmp)) + + # Gathering datetime the same way as in our app + tmp = [int(x) for x in msc.strftime("%H:%M:%S").split(":")] + msc = sum(a*b for a,b in zip(values, tmp)) + tmp = [int(x) for x in current.strftime("%H:%M:%S").split(":")] + current = sum(a*b for a,b in zip(values, tmp)) + + self.assertTrue(abs(msc_response-msc) <= error + or abs(abs(msc_response - msc) - cycle) <= error) + self.assertTrue(abs(current_response-current) <= error + or abs(abs(current_response - current) - cycle) <= error) + +if __name__ == '__main__': + unittest.main()