Skip to content
211 changes: 210 additions & 1 deletion tests/test_project_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Tests for the project commands (create, delete, list, update)."""
"""Tests for the project commands (create, delete, list, update, export, import)."""

import json
from pathlib import Path
from unittest.mock import Mock, patch

Expand Down Expand Up @@ -519,3 +520,211 @@
assert "update" in result.output
assert "--id" in result.output
assert "--config" in result.output


@pytest.mark.unit
@pytest.mark.cli
class TestExportProjectCommand:
"""Test cases for the project export command."""

def test_export_project_success_default_filename(
self,
cli_runner: CliRunner,
mock_sync_apis: Mock,
sample_project: api_models.Project,
temp_dir: Path,
) -> None:
"""Test successful project export with auto-generated filename."""
# Arrange
project_create = api_models.ProjectCreate(
name="Test Project",
config=sample_project.config,
pics=api_models.PICS(clusters={}),
)
mock_sync_apis.projects_api.export_project_config_api_v1_projects__id__export_get.return_value = (
project_create
)

with patch("th_cli.commands.project.SyncApis", return_value=mock_sync_apis):
with cli_runner.isolated_filesystem(temp_dir=temp_dir):
# Act
result = cli_runner.invoke(project, ["export", "--id", "1"])

Check failure on line 552 in tests/test_project_commands.py

View workflow job for this annotation

GitHub Actions / Black

tests/test_project_commands.py#L539-L552

project_create = api_models.ProjectCreate( name="Test Project", config=sample_project.config, pics=api_models.PICS(clusters={}), ) - mock_sync_apis.projects_api.export_project_config_api_v1_projects__id__export_get.return_value = ( - project_create - ) + mock_sync_apis.projects_api.export_project_config_api_v1_projects__id__export_get.return_value = project_create with patch("th_cli.commands.project.SyncApis", return_value=mock_sync_apis): with cli_runner.isolated_filesystem(temp_dir=temp_dir): # Act result = cli_runner.invoke(project, ["export", "--id", "1"])
# Assert
assert result.exit_code == 0
assert "exported to" in result.output
mock_sync_apis.projects_api.export_project_config_api_v1_projects__id__export_get.assert_called_once_with(id=1)

def test_export_project_success_custom_filename(
self,
cli_runner: CliRunner,
mock_sync_apis: Mock,
sample_project: api_models.Project,
temp_dir: Path,
) -> None:
"""Test successful project export to a specified output file."""
# Arrange
project_create = api_models.ProjectCreate(
name="Test Project",
config=sample_project.config,
pics=api_models.PICS(clusters={}),
)
mock_sync_apis.projects_api.export_project_config_api_v1_projects__id__export_get.return_value = (
project_create
)
output_path = str(temp_dir / "my_export.json")

with patch("th_cli.commands.project.SyncApis", return_value=mock_sync_apis):
# Act
result = cli_runner.invoke(project, ["export", "--id", "1", "--output-file", output_path])

Check failure on line 580 in tests/test_project_commands.py

View workflow job for this annotation

GitHub Actions / Black

tests/test_project_commands.py#L567-L580

project_create = api_models.ProjectCreate( name="Test Project", config=sample_project.config, pics=api_models.PICS(clusters={}), ) - mock_sync_apis.projects_api.export_project_config_api_v1_projects__id__export_get.return_value = ( - project_create - ) + mock_sync_apis.projects_api.export_project_config_api_v1_projects__id__export_get.return_value = project_create output_path = str(temp_dir / "my_export.json") with patch("th_cli.commands.project.SyncApis", return_value=mock_sync_apis): # Act result = cli_runner.invoke(project, ["export", "--id", "1", "--output-file", output_path])
# Assert
assert result.exit_code == 0
assert f"exported to '{output_path}'" in result.output
assert Path(output_path).exists()
saved = json.loads(Path(output_path).read_text())
assert saved["name"] == "Test Project"

def test_export_project_file_content_is_valid_json(
self,
cli_runner: CliRunner,
mock_sync_apis: Mock,
sample_project: api_models.Project,
temp_dir: Path,
) -> None:
"""Test that the exported file contains valid JSON matching the project config."""
# Arrange
project_create = api_models.ProjectCreate(
name="My Device",
config=sample_project.config,
pics=api_models.PICS(clusters={}),
)
mock_sync_apis.projects_api.export_project_config_api_v1_projects__id__export_get.return_value = (
project_create
)
output_path = str(temp_dir / "export.json")

with patch("th_cli.commands.project.SyncApis", return_value=mock_sync_apis):
result = cli_runner.invoke(project, ["export", "--id", "42", "--output-file", output_path])

assert result.exit_code == 0

Check failure on line 610 in tests/test_project_commands.py

View workflow job for this annotation

GitHub Actions / Black

tests/test_project_commands.py#L597-L610

project_create = api_models.ProjectCreate( name="My Device", config=sample_project.config, pics=api_models.PICS(clusters={}), ) - mock_sync_apis.projects_api.export_project_config_api_v1_projects__id__export_get.return_value = ( - project_create - ) + mock_sync_apis.projects_api.export_project_config_api_v1_projects__id__export_get.return_value = project_create output_path = str(temp_dir / "export.json") with patch("th_cli.commands.project.SyncApis", return_value=mock_sync_apis): result = cli_runner.invoke(project, ["export", "--id", "42", "--output-file", output_path])
data = json.loads(Path(output_path).read_text())
assert data["name"] == "My Device"
assert "config" in data

def test_export_project_api_error(
self,
cli_runner: CliRunner,
mock_sync_apis: Mock,
) -> None:
"""Test project export with API error."""
# Arrange
mock_sync_apis.projects_api.export_project_config_api_v1_projects__id__export_get.side_effect = (
UnexpectedResponse(status_code=404, content=b"Not Found")
)

with patch("th_cli.commands.project.SyncApis", return_value=mock_sync_apis):
result = cli_runner.invoke(project, ["export", "--id", "99"])

assert result.exit_code == 1
assert "Error: Failed to export project ID '99' (Status: 404) - Not Found" in result.output

def test_export_project_help_message(self, cli_runner: CliRunner) -> None:
"""Test the help message for the export command."""
result = cli_runner.invoke(project, ["export", "--help"])

assert result.exit_code == 0
assert "--id" in result.output
assert "--output-file" in result.output


@pytest.mark.unit
@pytest.mark.cli
class TestImportProjectCommand:
"""Test cases for the project import command."""

def test_import_project_success(
self,
cli_runner: CliRunner,
mock_sync_apis: Mock,
sample_project: api_models.Project,
temp_dir: Path,
) -> None:
"""Test successful project import from a JSON file."""
# Arrange
import_file = temp_dir / "import.json"
import_file.write_text(
json.dumps({"name": "Imported Project", "config": sample_project.config, "pics": {"clusters": {}}})
)
mock_sync_apis.projects_api.importproject_config_api_v1_projects_import_post.return_value = sample_project

with patch("th_cli.commands.project.SyncApis", return_value=mock_sync_apis):
result = cli_runner.invoke(project, ["import", "--file", str(import_file)])

# Assert
assert result.exit_code == 0
assert f"Project '{sample_project.name}' imported with ID {sample_project.id}" in result.output
mock_sync_apis.projects_api.importproject_config_api_v1_projects_import_post.assert_called_once()

def test_import_project_file_not_found(
self,
cli_runner: CliRunner,
mock_sync_apis: Mock,
) -> None:
"""Test project import with a non-existent file (Click validates exists=True)."""
with patch("th_cli.commands.project.SyncApis", return_value=mock_sync_apis):
result = cli_runner.invoke(project, ["import", "--file", "nonexistent.json"])

assert result.exit_code == 2
assert "does not exist" in result.output

def test_import_project_api_error(
self,
cli_runner: CliRunner,
mock_sync_apis: Mock,
sample_project: api_models.Project,
temp_dir: Path,
) -> None:
"""Test project import with API error."""
# Arrange
import_file = temp_dir / "import.json"
import_file.write_text(
json.dumps({"name": "Imported Project", "config": sample_project.config, "pics": {"clusters": {}}})
)
mock_sync_apis.projects_api.importproject_config_api_v1_projects_import_post.side_effect = (
UnexpectedResponse(status_code=422, content=b"Unprocessable Entity")
)

with patch("th_cli.commands.project.SyncApis", return_value=mock_sync_apis):
result = cli_runner.invoke(project, ["import", "--file", str(import_file)])

assert result.exit_code == 1

Check failure on line 701 in tests/test_project_commands.py

View workflow job for this annotation

GitHub Actions / Black

tests/test_project_commands.py#L689-L701

# Arrange import_file = temp_dir / "import.json" import_file.write_text( json.dumps({"name": "Imported Project", "config": sample_project.config, "pics": {"clusters": {}}}) ) - mock_sync_apis.projects_api.importproject_config_api_v1_projects_import_post.side_effect = ( - UnexpectedResponse(status_code=422, content=b"Unprocessable Entity") + mock_sync_apis.projects_api.importproject_config_api_v1_projects_import_post.side_effect = UnexpectedResponse( + status_code=422, content=b"Unprocessable Entity" ) with patch("th_cli.commands.project.SyncApis", return_value=mock_sync_apis): result = cli_runner.invoke(project, ["import", "--file", str(import_file)])
assert "422" in result.output

def test_import_project_passes_file_bytes_to_api(
self,
cli_runner: CliRunner,
mock_sync_apis: Mock,
sample_project: api_models.Project,
temp_dir: Path,
) -> None:
"""Test that the import command sends the file bytes to the API correctly."""
# Arrange
payload = {"name": "Byte Check Project", "config": sample_project.config, "pics": {"clusters": {}}}
import_file = temp_dir / "import.json"
import_file.write_text(json.dumps(payload))
mock_sync_apis.projects_api.importproject_config_api_v1_projects_import_post.return_value = sample_project

with patch("th_cli.commands.project.SyncApis", return_value=mock_sync_apis):
cli_runner.invoke(project, ["import", "--file", str(import_file)])

call_args = mock_sync_apis.projects_api.importproject_config_api_v1_projects_import_post.call_args
body = call_args.kwargs["body"]
assert body.import_file == import_file.read_bytes()

def test_import_project_help_message(self, cli_runner: CliRunner) -> None:
"""Test the help message for the import command."""
result = cli_runner.invoke(project, ["import", "--help"])

assert result.exit_code == 0
assert "--file" in result.output
46 changes: 46 additions & 0 deletions tests/test_run/camera/test_camera_http_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,52 @@ def test_do_post_unknown_path_sends_404(self):
assert handler._error_code == 404


# ---------------------------------------------------------------------------
# VideoStreamingHandler.serve_player - Push AV Stream Verification template
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestServePlayerPushAVTemplate:
"""Regression tests for issue #1051: the Push AV Server returns each stream's
uploaded files under valid_uploads/error_uploads (list of {file_path, reasons?}),
not the legacy files/valid_files/invalid_files shape. The rendered template's
JS must read the current field names or the video player stays blank even
when the DUT has successfully uploaded content."""

def _render(self):
handler = _make_handler(
path="/",
server_attrs={
"prompt_options": {"PASS": 1, "FAIL": 2},
"prompt_text": "Verify the video stream",
"is_push_av_verification": True,
"push_av_server_url": "https://192.168.0.53:1234",
},
)
handler.serve_player()
return handler.wfile.getvalue().decode("utf-8")

def test_renders_without_template_error(self):
html_content = self._render()
assert "Template error" not in html_content
assert "<!DOCTYPE html>" in html_content or "<html>" in html_content

def test_reads_valid_and_error_uploads_fields(self):
html_content = self._render()
assert "stream.valid_uploads" in html_content
assert "stream.error_uploads" in html_content
assert "file_path" in html_content

def test_no_longer_relies_solely_on_legacy_file_fields(self):
"""The old field names may still appear as a fallback, but the current
server field names must be checked first."""
html_content = self._render()
valid_uploads_idx = html_content.index("stream.valid_uploads")
valid_files_idx = html_content.index("stream.valid_files")
assert valid_uploads_idx < valid_files_idx


# ---------------------------------------------------------------------------
# VideoStreamingHandler.handle_response
# ---------------------------------------------------------------------------
Expand Down
Loading
Loading