diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a14c5fb..1ffe30d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -25,6 +25,6 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "monthly" + interval: "weekly" reviewers: - "GabrielPalmar" diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index a6866a7..2cbedd2 100644 --- a/.github/workflows/build_test.yml +++ b/.github/workflows/build_test.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Build the Docker image - run: docker build . --file Dockerfile --tag pommed3rre/hivebox:$(cat version.txt) + run: docker build . --file docker/Dockerfile --tag pommed3rre/hivebox:$(cat version.txt) - name: Run Docker container run: docker run -d -p 5000:5000 pommed3rre/hivebox:$(cat version.txt) @@ -36,7 +36,7 @@ jobs: - name: Run tests run: | - python test_main.py + python tests/test_main.py TEST_EXIT_CODE=$? if [ $TEST_EXIT_CODE -ne 0 ]; then echo "Tests failed!" diff --git a/.github/workflows/hadolint.yml b/.github/workflows/hadolint.yml index 67f3db6..3118bcb 100644 --- a/.github/workflows/hadolint.yml +++ b/.github/workflows/hadolint.yml @@ -19,4 +19,4 @@ jobs: - uses: actions/checkout@v4 - uses: hadolint/hadolint-action@v3.1.0 with: - dockerfile: Dockerfile + dockerfile: docker/Dockerfile diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 1a6e8e1..98ebc38 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -33,4 +33,8 @@ jobs: pip install prometheus_client - name: Analysing the code with pylint run: | - pylint $(git ls-files '*.py') + # Set PYTHONPATH so pylint can find the app module + export PYTHONPATH="${PYTHONPATH}:$(pwd)" + pylint app/ --ignore=__pycache__ + env: + PYTHONPATH: ${{ github.workspace }} \ No newline at end of file diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 9ae7570..505cbcd 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -1,11 +1,12 @@ name: SonarQube analysis -on: +on: push: - branches: [ "main", "test" ] + branches: + - main pull_request: - branches: [ "main" ] - workflow_dispatch: + branches: + - main permissions: pull-requests: read @@ -18,7 +19,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 with: - fetch-depth: 0 # Disable shallow clones for better analysis + fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v5 @@ -33,9 +34,12 @@ jobs: - name: Run tests with coverage run: | - coverage run test_main.py - coverage xml - + export PYTHONPATH="${GITHUB_WORKSPACE}:${PYTHONPATH}" + coverage run --source=app -m pytest tests/test_modules.py -v + coverage xml -o coverage.xml + coverage report --show-missing + working-directory: ${{ github.workspace }} + - name: Analyze with SonarQube uses: SonarSource/sonarqube-scan-action@master env: @@ -45,5 +49,7 @@ jobs: args: > -Dsonar.projectKey=GabrielPalmar_HiveBox-Project -Dsonar.organization=gabrielpalmar - -Dsonar.sources=. - -Dsonar.exclusions=**/*test*/**,**/fixtures/**,**/__pycache__/**,**/venv/**,**/.git/** \ No newline at end of file + -Dsonar.sources=app + -Dsonar.tests=tests + -Dsonar.python.coverage.reportPaths=coverage.xml + -Dsonar.exclusions=**/fixtures/**,**/__pycache__/**,**/venv/**,**/.git/**,docker/**,k8s/**,scripts/** \ No newline at end of file diff --git a/main.py b/app/main.py similarity index 64% rename from main.py rename to app/main.py index 136b393..df141fb 100644 --- a/main.py +++ b/app/main.py @@ -1,16 +1,17 @@ '''Module containing the main function of the app.''' +import os from flask import Flask, Response from prometheus_client import generate_latest, CONTENT_TYPE_LATEST -import opensense -#import test_main +from app import opensense app = Flask(__name__) @app.route('/version') def print_version(): '''Function printing the current version of the app.''' + version_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'version.txt') - with open('version.txt', 'r', encoding="utf-8") as f: + with open(version_file, 'r', encoding="utf-8") as f: version = f.read() return f"Current app version: {version}\n" @@ -25,15 +26,5 @@ def metrics(): '''Function to return Prometheus metrics.''' return Response(generate_latest(), mimetype=CONTENT_TYPE_LATEST) -### Test module ### -# @app.route('/test') -# def test(): -# '''Function to test the app.''' -# success = test_main.run_all_tests() -# if success: -# return "All tests passed!\n", 200 -# else: -# return "Some tests failed. Check the logs for details.\n", 500 - if __name__ == "__main__": app.run() diff --git a/opensense.py b/app/opensense.py similarity index 100% rename from opensense.py rename to app/opensense.py diff --git a/Dockerfile b/docker/Dockerfile similarity index 78% rename from Dockerfile rename to docker/Dockerfile index 6fc3401..eba5c29 100644 --- a/Dockerfile +++ b/docker/Dockerfile @@ -1,18 +1,19 @@ -FROM python:3.13.5-alpine@sha256:9b4929a72599b6c6389ece4ecbf415fd1355129f22bb92bb137eea098f05e975 - -RUN addgroup -S appgroup && adduser -S -G appgroup appuser - -WORKDIR /app - -COPY main.py opensense.py version.txt requirements.txt /app/ - -RUN pip install --no-cache-dir -r /app/requirements.txt --require-hashes && \ - chown -R appuser:appgroup /app - -ENV FLASK_APP=main.py \ - PYTHONUNBUFFERED=1 - -USER appuser - -ENTRYPOINT [ "flask" ] +FROM python:3.13.5-alpine@sha256:9b4929a72599b6c6389ece4ecbf415fd1355129f22bb92bb137eea098f05e975 + +RUN addgroup -S appgroup && adduser -S -G appgroup appuser + +WORKDIR /app + +COPY app/ /app/app/ +COPY version.txt requirements.txt /app/ + +RUN pip install --no-cache-dir -r /app/requirements.txt --require-hashes && \ + chown -R appuser:appgroup /app + +ENV FLASK_APP=app.main.py:app \ + PYTHONUNBUFFERED=1 + +USER appuser + +ENTRYPOINT [ "flask" ] CMD [ "run", "--host=0.0.0.0" ] \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..d810fe3 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,7 @@ +[tool:pytest] +testpaths = tests +pythonpath = . +python_files = test_*.py +python_classes = Test* +python_functions = test_* +addopts = --verbose --tb=short \ No newline at end of file diff --git a/fixtures/vcr_cassettes/metrics.yaml b/tests/fixtures/vcr_cassettes/metrics.yaml similarity index 100% rename from fixtures/vcr_cassettes/metrics.yaml rename to tests/fixtures/vcr_cassettes/metrics.yaml diff --git a/fixtures/vcr_cassettes/temperature.yaml b/tests/fixtures/vcr_cassettes/temperature.yaml similarity index 100% rename from fixtures/vcr_cassettes/temperature.yaml rename to tests/fixtures/vcr_cassettes/temperature.yaml diff --git a/fixtures/vcr_cassettes/version.yaml b/tests/fixtures/vcr_cassettes/version.yaml similarity index 100% rename from fixtures/vcr_cassettes/version.yaml rename to tests/fixtures/vcr_cassettes/version.yaml diff --git a/test_main.py b/tests/test_main.py similarity index 95% rename from test_main.py rename to tests/test_main.py index dab0066..7c9ec55 100644 --- a/test_main.py +++ b/tests/test_main.py @@ -1,4 +1,4 @@ -'''This module contains the test cases for the opensense module.''' +'''This module contains the test cases for the main module.''' import sys import re import os @@ -8,7 +8,7 @@ API_HOST = os.environ.get('API_HOST', 'http://127.0.0.1:5000') my_vcr = vcr.VCR( - cassette_library_dir='fixtures/vcr_cassettes', + cassette_library_dir='tests/fixtures/vcr_cassettes', record_mode='once', match_on=['uri', 'method'], ) diff --git a/tests/test_modules.py b/tests/test_modules.py new file mode 100644 index 0000000..8e68208 --- /dev/null +++ b/tests/test_modules.py @@ -0,0 +1,32 @@ +'''This module contains tests for the Flask and OpenSense modules.''' +from app.main import app +from app import opensense + +def test_app_exists(): + """Test that the Flask app exists""" + assert app is not None + +def test_version_endpoint(): + """Test version endpoint with test client""" + client = app.test_client() + response = client.get('/version') + assert response.status_code == 200 + +def test_temperature_endpoint(): + """Test temperature endpoint with test client""" + client = app.test_client() + response = client.get('/temperature') + assert response.status_code in [200, 500] + +def test_metrics_endpoint(): + """Test metrics endpoint""" + client = app.test_client() + response = client.get('/metrics') + assert response.status_code == 200 + +# Add a simple test for the opensense module +def test_opensense_get_temperature(): + """Test that opensense.get_temperature returns a string""" + result = opensense.get_temperature() + assert isinstance(result, str) + assert "Average temperature:" in result