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
84 changes: 84 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# Testing
.pytest_cache/
.coverage
htmlcov/
coverage.xml
*.cover
*.py,cover
.hypothesis/
.tox/
.nox/

# Virtual environments
venv/
ENV/
env/
.venv/
.env

# IDEs
.idea/
.vscode/
*.swp
*.swo
*~
.project
.pydevproject
.settings/

# OS
.DS_Store
Thumbs.db

# Claude settings
.claude/*

# Logs
*.log

# Local environment
.env.local
.env.*.local

# Poetry - do not ignore lock file
# poetry.lock

# UV - do not ignore lock file
# uv.lock

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Token files
.mi.token
*.token
878 changes: 878 additions & 0 deletions poetry.lock

Large diffs are not rendered by default.

81 changes: 81 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
[tool.poetry]
name = "miservice"
version = "2.3.0"
description = "XiaoMi Cloud Service"
authors = ["Yonsm <[email protected]>"]
license = "MIT"
readme = "README.md"
homepage = "https://github.com/Yonsm/MiService"
repository = "https://github.com/Yonsm/MiService"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent"
]
packages = [{include = "miservice"}]

[tool.poetry.scripts]
micli = "micli:main"

[tool.poetry.dependencies]
python = "^3.7"
aiohttp = "*"
aiofiles = "*"

[tool.poetry.group.dev.dependencies]
pytest = "^7.4.0"
pytest-cov = "^4.1.0"
pytest-mock = "^3.11.1"

[tool.pytest.ini_options]
minversion = "7.0"
addopts = [
"-ra",
"--strict-markers",
"--cov=miservice",
"--cov-branch",
"--cov-report=term-missing:skip-covered",
"--cov-report=html",
"--cov-report=xml",
"--cov-fail-under=80",
]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
markers = [
"unit: Unit tests",
"integration: Integration tests",
"slow: Slow tests",
]

[tool.coverage.run]
source = ["miservice"]
omit = [
"*/tests/*",
"*/__pycache__/*",
"*/venv/*",
"*/.venv/*",
]

[tool.coverage.report]
precision = 2
show_missing = true
skip_covered = true
exclude_lines = [
"pragma: no cover",
"def __repr__",
"def __str__",
"raise AssertionError",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
"if typing.TYPE_CHECKING:",
]

[tool.coverage.html]
directory = "htmlcov"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Empty file added tests/__init__.py
Empty file.
133 changes: 133 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import pytest
import tempfile
import shutil
from pathlib import Path
from unittest.mock import MagicMock, AsyncMock
import json


@pytest.fixture
def temp_dir():
"""Create a temporary directory for test files."""
temp_path = tempfile.mkdtemp()
yield Path(temp_path)
shutil.rmtree(temp_path)


@pytest.fixture
def mock_config():
"""Provide a mock configuration dictionary."""
return {
"user": "[email protected]",
"password": "test_password",
"device_id": "test_device_123",
"server": "https://api.test.xiaomi.com",
}


@pytest.fixture
def mock_token_store(temp_dir):
"""Create a mock token store file."""
token_file = temp_dir / ".mi.token"
token_data = {
"userId": "12345",
"serviceToken": "test_service_token",
"ssecurity": "test_ssecurity",
}
token_file.write_text(json.dumps(token_data))
return token_file


@pytest.fixture
def mock_session():
"""Create a mock aiohttp ClientSession."""
session = AsyncMock()
session.get = AsyncMock()
session.post = AsyncMock()
session.close = AsyncMock()
return session


@pytest.fixture
def mock_response():
"""Create a mock HTTP response."""
response = AsyncMock()
response.status = 200
response.json = AsyncMock(return_value={"code": 0, "data": {}})
response.text = AsyncMock(return_value='{"code": 0, "data": {}}')
response.headers = {"Content-Type": "application/json"}
return response


@pytest.fixture
def mock_device_list():
"""Provide a mock device list response."""
return {
"code": 0,
"data": {
"list": [
{
"did": "test_device_123",
"name": "Test Device",
"model": "test.model.v1",
"isOnline": True,
},
{
"did": "test_device_456",
"name": "Another Device",
"model": "test.model.v2",
"isOnline": False,
},
]
}
}


@pytest.fixture
def mock_mi_account(mock_session):
"""Create a mock MiAccount instance."""
account = MagicMock()
account.session = mock_session
account.user = "[email protected]"
account.password = "test_password"
account.token_store = ".mi.token"
account.service_token = "test_service_token"
account.ssecurity = "test_ssecurity"
account.user_id = "12345"
return account


@pytest.fixture
def capture_logs():
"""Capture log output for testing."""
import logging
from io import StringIO

log_capture = StringIO()
handler = logging.StreamHandler(log_capture)
handler.setLevel(logging.DEBUG)

logger = logging.getLogger()
original_handlers = logger.handlers[:]
logger.handlers = [handler]
logger.setLevel(logging.DEBUG)

yield log_capture

logger.handlers = original_handlers


@pytest.fixture(autouse=True)
def reset_environment(monkeypatch):
"""Reset environment variables for each test."""
monkeypatch.delenv("MI_USER", raising=False)
monkeypatch.delenv("MI_PASS", raising=False)
monkeypatch.delenv("MI_DID", raising=False)


@pytest.fixture
def set_test_environment(monkeypatch):
"""Set test environment variables."""
monkeypatch.setenv("MI_USER", "[email protected]")
monkeypatch.setenv("MI_PASS", "test_password")
monkeypatch.setenv("MI_DID", "test_device_123")
Empty file added tests/integration/__init__.py
Empty file.
71 changes: 71 additions & 0 deletions tests/test_setup_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import pytest
import sys
from pathlib import Path


class TestSetupValidation:
"""Validation tests to ensure the testing infrastructure is properly configured."""

def test_python_version(self):
"""Verify Python version is 3.7 or higher."""
assert sys.version_info >= (3, 7), "Python 3.7 or higher is required"

def test_project_structure(self):
"""Verify the project structure is correct."""
root_dir = Path(__file__).parent.parent

# Check main package exists
assert (root_dir / "miservice").is_dir()
assert (root_dir / "miservice" / "__init__.py").is_file()

# Check test directories exist
assert (root_dir / "tests").is_dir()
assert (root_dir / "tests" / "unit").is_dir()
assert (root_dir / "tests" / "integration").is_dir()

# Check configuration files exist
assert (root_dir / "pyproject.toml").is_file()

def test_imports(self):
"""Verify main package can be imported."""
try:
import miservice
assert miservice is not None
except ImportError:
pytest.fail("Failed to import miservice package")

@pytest.mark.unit
def test_unit_marker(self):
"""Verify unit test marker works."""
assert True

@pytest.mark.integration
def test_integration_marker(self):
"""Verify integration test marker works."""
assert True

@pytest.mark.slow
def test_slow_marker(self):
"""Verify slow test marker works."""
assert True

def test_fixtures_available(self, temp_dir, mock_config, mock_session):
"""Verify pytest fixtures are available."""
assert temp_dir.is_dir()
assert isinstance(mock_config, dict)
assert mock_config["user"] == "[email protected]"
assert mock_session is not None

def test_environment_reset(self):
"""Verify environment variables are reset between tests."""
import os
assert "MI_USER" not in os.environ
assert "MI_PASS" not in os.environ
assert "MI_DID" not in os.environ

def test_environment_fixture(self, set_test_environment):
"""Verify environment fixture works correctly."""
import os
assert os.environ["MI_USER"] == "[email protected]"
assert os.environ["MI_PASS"] == "test_password"
assert os.environ["MI_DID"] == "test_device_123"
Empty file added tests/unit/__init__.py
Empty file.