Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 12 additions & 26 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
16 changes: 16 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -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.
74 changes: 74 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
@@ -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"