Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Created Integration Tests #5

Merged
merged 12 commits into from
Feb 11, 2025
32 changes: 32 additions & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Integration tests

on:
workflow_call:
inputs:
python-version:
required: true
type: string

jobs:
test:
name: Integration tests
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install poetry
run: pipx install poetry

- name: Set up Python ${{ inputs.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
cache: "poetry"

- name: Install dependencies
run: poetry install

- name: Run unit tests
run: poetry run pytest tests/integration
7 changes: 3 additions & 4 deletions .github/workflows/linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ jobs:
lint:
name: Ruff
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4
Expand All @@ -25,9 +26,7 @@ jobs:
cache: "poetry"

- name: Install dependencies
run: |
poetry install
run: poetry install

- name: Run Ruff
run: |
poetry run ruff check .
run: poetry run ruff check .
9 changes: 7 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@ jobs:
with:
python-version: "3.13"

test:
unit-tests:
uses: ./.github/workflows/unit-tests.yml
with:
matrix: |
["3.9", "3.10", "3.11", "3.12", "3.13"]
["3.9", "3.10", "3.11", "3.12", "3.13"]

integration-tests:
uses: ./.github/workflows/integration-tests.yml
with:
python-version: "3.13"
4 changes: 2 additions & 2 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,5 @@ jobs:
- name: Install dependencies
run: poetry install

- name: Run tests
run: poetry run pytest
- name: Run unit tests
run: poetry run pytest tests/unit
6 changes: 6 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ FROM python:3.13-alpine
# Set the working directory
WORKDIR /app

# Install curl
RUN apk add --no-cache curl

# Copy the installed packages from the builder stage
COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
Expand All @@ -36,5 +39,8 @@ ENV FLASK_ENV=production
# Expose the port the app runs on
EXPOSE 4200

# Add healthcheck instruction
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD curl -f http://localhost:4200/healthcheck || exit 1

# Run the application
CMD ["gunicorn", "--bind", "0.0.0.0:4200", "wakeonlanservice.app:app"]
219 changes: 213 additions & 6 deletions poetry.lock

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ pytest = "^8.3.4"
ruff = "^0.9.5"
python-dotenv = "^1.0.1"
pytest-cov = "^6.0.0"
docker = "^7.1.0"
testcontainers = "^4.9.1"

[tool.poetry.group.production.dependencies]
gunicorn = "^23.0.0"
Expand Down Expand Up @@ -60,7 +62,7 @@ ignore = [
fixable = ["E", "F", "C", "N", "B", "Q", "S"]

# Ignore specific rules in test files
per-file-ignores = {"tests/test_*.py" = ["S101"]}
per-file-ignores = {"tests/**/test_*.py" = ["S101"]}

[tool.ruff.format]
# Set the quote style to double quotes
Expand Down
10 changes: 10 additions & 0 deletions src/wakeonlanservice/blueprints/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@ def debug_status():
"""
return f"App debug mode is {'on' if current_app.debug else 'off'}"

@status_bp.route("/healthcheck")
def healthcheck():
"""
Health check endpoint to verify that the application is running.

Returns:
Response: JSON response indicating the health status of the application.
"""
return jsonify({"status": "healthy"})

def validate_mac_address(mac_address: str) -> None:
"""
Validate the MAC address.
Expand Down
51 changes: 51 additions & 0 deletions tests/integration/test_intergration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import pytest
from testcontainers.core.container import DockerContainer
from testcontainers.core.waiting_utils import wait_for_logs
from time import sleep

@pytest.fixture(scope="session")
def docker_container():
# Build the Docker image from the Dockerfile
import docker
client = docker.from_env()
client.images.build(path=".", dockerfile="docker/Dockerfile", tag="wakeonlanservice:test")

# Run the container
container = DockerContainer("wakeonlanservice:test")
container.with_bind_ports(4200, 4200)

# Set the environment variables
container.with_env("MAC_ADDRESS", "18:C0:4D:07:5B:2D")
container.with_env("URL", "https://example.com")

container.start()

try:
duration = wait_for_logs(container, "wakeonlanservice: App created successfully", timeout=360)
print(f"Container started successfully after {duration} seconds")
except Exception as e:
print(f"Failed to find log message: {e}")
container.stop()
pytest.fail("Failed to find log message within the timeout period")

yield container

container.stop()

def test_container_health_check(docker_container):
# Wait for the container to start and become healthy
wrapped_container = docker_container.get_wrapped_container()
for _ in range(10):
wrapped_container.reload()
health_status = wrapped_container.attrs.get("State", {}).get("Health", {}).get("Status", {})
if health_status == "healthy":
break
else:
print(f"Container state: \n{wrapped_container.attrs.get("State", {})}")
sleep(10)
else:
# Log container output for debugging
logs = wrapped_container.logs().decode("utf-8")
print(f"Container logs: {logs}")
pytest.fail("Container did not become healthy in time")

File renamed without changes.
File renamed without changes.
File renamed without changes.
9 changes: 8 additions & 1 deletion tests/test_status.py → tests/unit/test_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,11 @@ class MockResponse:
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "error"
assert data["attempts"] == 11
assert data["attempts"] == 11

class TestHealthCheck:
def test_healthcheck(self, client):
response = client.get("/healthcheck")
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "healthy"