diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba0cd0c..983ada7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,37 +2,23 @@ name: CI on: push: - branches: [ main, master ] + branches: [main, master] pull_request: - branches: [ main, master ] + branches: [main, master] jobs: - test: + python: + name: pytest and flake8 (Python 3.11) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: '3.9' - - name: Install deps - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - - name: Run tests - run: pytest -q - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.9' - - name: Install flake8 - run: | - python -m pip install --upgrade pip - pip install flake8 - - name: Run flake8 - run: | - flake8 --max-line-length=120 || true + python-version: '3.11' + - name: Install test tools + run: python -m pip install pytest flake8 + - name: Test database model + run: python -m pytest -q tests + - name: Check Python syntax and critical lint errors + run: python -m flake8 . --select=E9,F63,F7,F82 --show-source diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..80bf2df --- /dev/null +++ b/tests/README.md @@ -0,0 +1,16 @@ +# Isolated Python model tests + +Run from the repository root with Python 3.11+: + + python -m pip install pytest flake8 + python -m pytest -q tests + python -m flake8 . --select=E9,F63,F7,F82 --show-source + +The tests use a temporary SQLite database. They do not need actual Raspberry Pi +hardware, a Docker daemon, FastAPI, network access, or production credentials. + +The CI intentionally runs a critical-error lint subset across the existing +Python files: syntax errors and high-impact Pyflakes checks. This is not +equivalent to full-project flake8 compliance. The previous lint step ignored +failures, and the previous dependency-install step targeted requirements.txt, +which does not exist in this repository. diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..e8f8554 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,74 @@ +"""Isolated SQLite model regression tests; no Raspberry Pi/network required.""" +import sqlite3 + +import pytest + +from app.models import Database + + +@pytest.fixture +def database(tmp_path): + db = Database(str(tmp_path / "devices.db")) + db.init_db() + return db + + +def test_initial_database_is_empty(database): + assert database.list_devices() == [] + + +def test_upsert_creates_device_with_boolean_liveness(database): + database.upsert_device("192.0.2.1", "Primary Pi", True) + devices = database.list_devices() + assert len(devices) == 1 + assert devices[0]["ip"] == "192.0.2.1" + assert devices[0]["name"] == "Primary Pi" + assert devices[0]["alive"] is True + + +def test_upsert_same_ip_updates_without_creating_duplicate(database): + database.upsert_device("192.0.2.2", "Old name", True) + existing = database.list_devices()[0]["id"] + database.upsert_device("192.0.2.2", "Updated name", False) + devices = database.list_devices() + assert len(devices) == 1 + assert devices[0]["id"] == existing + assert devices[0]["name"] == "Updated name" + assert devices[0]["alive"] is False + + +def test_get_device_returns_one_device_by_id(database): + database.upsert_device("192.0.2.3", "Living room", False) + id_ = database.list_devices()[0]["id"] + assert database.get_device(id_) == { + "id": id_, + "ip": "192.0.2.3", + "name": "Living room", + "alive": False, + } + + +def test_get_device_missing_returns_none(database): + assert database.get_device(999) is None + + +def test_unique_ip_constraint_and_other_rows_are_unchanged(database): + database.upsert_device("192.0.2.4", "Sensor", True) + with pytest.raises(sqlite3.IntegrityError): + with database.conn() as conn: + conn.execute( + "INSERT INTO devices(ip, name) VALUES (?, ?)", + ("192.0.2.4", "Duplicate"), + ) + assert len(database.list_devices()) == 1 + + +def test_upsert_allows_null_name(database): + database.upsert_device("192.0.2.5", None, True) + assert database.list_devices()[0]["name"] is None + + +def test_schema_init_is_repeatable(database): + database.upsert_device("192.0.2.6", "Retained", True) + database.init_db() + assert database.list_devices()[0]["name"] == "Retained"