Skip to content

Commit

Permalink
Merge branch 'uilibs:main' into add-keyring
Browse files Browse the repository at this point in the history
  • Loading branch information
RaHehl authored Dec 6, 2024
2 parents 99163a8 + 432da70 commit c636bfc
Show file tree
Hide file tree
Showing 9 changed files with 352 additions and 244 deletions.
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ ci:

repos:
- repo: https://github.com/commitizen-tools/commitizen
rev: v3.31.0
rev: v4.0.0
hooks:
- id: commitizen
stages: [commit-msg]
Expand All @@ -36,7 +36,7 @@ repos:
- id: prettier
args: ["--tab-width", "2"]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.0
rev: v0.8.1
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## v6.6.5 (2024-12-02)

### Fix


- Add isthirdpartycamera field to camera model (#302) ([`828b510`](https://github.com/uilibs/uiprotect/commit/828b5109f225613f04066eafa1063e6ce715fe3a))


## v6.6.4 (2024-11-29)

### Fix


- Update permission logic for get_snapshot method (#298) ([`207959b`](https://github.com/uilibs/uiprotect/commit/207959bf1598acd4ad9e1da1146058b8a18de99c))


## v6.6.3 (2024-11-27)

### Fix
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
project = "uiprotect"
copyright = "2024, UI Protect Maintainers"
author = "UI Protect Maintainers"
release = "6.6.3"
release = "6.6.5"

# General configuration
extensions = [
Expand Down
367 changes: 182 additions & 185 deletions poetry.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "uiprotect"
version = "6.6.3"
version = "6.6.5"
description = "Python API for Unifi Protect (Unofficial)"
authors = ["UI Protect Maintainers <[email protected]>"]
readme = "README.md"
Expand Down Expand Up @@ -52,7 +52,7 @@ propcache = ">=0.0.0"
pytest = ">=7,<9"
pytest-cov = ">=3,<7"
aiosqlite = ">=0.20.0"
asttokens = "^2.4.1"
asttokens = ">=2.4.1,<4.0.0"
pytest-asyncio = ">=0.23.7,<0.25.0"
pytest-benchmark = ">=4,<6"
pytest-sugar = "^1.0.0"
Expand Down
45 changes: 24 additions & 21 deletions src/uiprotect/data/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -974,7 +974,8 @@ class Camera(ProtectMotionDeviceModel):
is_ptz: bool | None = None
# requires 2.11.13+
audio_settings: CameraAudioSettings | None = None

# requires 5.0.33+
is_third_party_camera: bool | None = None
# TODO: used for adopting
# apMac read only
# apRssi read only
Expand Down Expand Up @@ -2014,17 +2015,18 @@ async def get_snapshot(
Datetime of screenshot is approximate. It may be +/- a few seconds.
"""
# Use READ_LIVE if dt is None, otherwise READ_MEDIA
permission = (
PermissionNode.READ_LIVE if dt is None else PermissionNode.READ_MEDIA
)
if not self._api.bootstrap.auth_user.can(
ModelType.CAMERA,
permission,
self,
):
action = "read live" if dt is None else "read media"
auth_user = self._api.bootstrap.auth_user
if dt is None:
if not (
auth_user.can(ModelType.CAMERA, PermissionNode.READ_LIVE, self)
or auth_user.can(ModelType.CAMERA, PermissionNode.READ_MEDIA, self)
):
raise NotAuthorized(
f"Do not have permission to read live or media for camera: {self.id}"
)
elif not auth_user.can(ModelType.CAMERA, PermissionNode.READ_MEDIA, self):
raise NotAuthorized(
f"Do not have permission to {action} for camera: {self.id}"
f"Do not have permission to read media for camera: {self.id}"
)

if height is None and width is None and self.high_camera_channel is not None:
Expand All @@ -2046,18 +2048,19 @@ async def get_package_snapshot(
if not self.feature_flags.has_package_camera:
raise BadRequest("Device does not have package camera")

auth_user = self._api.bootstrap.auth_user
# Use READ_LIVE if dt is None, otherwise READ_MEDIA
permission = (
PermissionNode.READ_LIVE if dt is None else PermissionNode.READ_MEDIA
)
if not self._api.bootstrap.auth_user.can(
ModelType.CAMERA,
permission,
self,
):
action = "read live" if dt is None else "read media"
if dt is None:
if not (
auth_user.can(ModelType.CAMERA, PermissionNode.READ_LIVE, self)
or auth_user.can(ModelType.CAMERA, PermissionNode.READ_MEDIA, self)
):
raise NotAuthorized(
f"Do not have permission to read live or media for camera: {self.id}"
)
elif not auth_user.can(ModelType.CAMERA, PermissionNode.READ_MEDIA, self):
raise NotAuthorized(
f"Do not have permission to {action} for camera: {self.id}"
f"Do not have permission to read media for camera: {self.id}"
)

if height is None and width is None and self.package_camera_channel is not None:
Expand Down
2 changes: 2 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,8 @@ def tmp_binary_file():
"trackNo",
"hasHttpsClientOTA",
"isUCoreStacked",
# 5.0.33+
"isThirdPartyCamera",
}

NEW_CAMERA_FEATURE_FLAGS = {
Expand Down
155 changes: 122 additions & 33 deletions tests/data/test_camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
VideoMode,
)
from uiprotect.data.devices import CameraZone, Hotplug, HotplugExtender
from uiprotect.data.types import DEFAULT, SmartDetectObjectType
from uiprotect.data.types import DEFAULT, PermissionNode, SmartDetectObjectType
from uiprotect.data.websocket import WSAction, WSSubscriptionMessage
from uiprotect.exceptions import BadRequest, NotAuthorized
from uiprotect.utils import to_js_time
Expand Down Expand Up @@ -1336,36 +1336,50 @@ async def test_camera_set_ptz_home(ptz_camera: Camera | None):
)


@pytest.mark.skipif(not TEST_CAMERA_EXISTS, reason="Missing test data")
@pytest.mark.asyncio()
async def test_get_snapshot_read_live(camera_obj: Camera | None):
camera_obj.api.api_request.reset_mock()
@pytest.mark.asyncio
async def test_get_snapshot_read_live_granted(camera_obj: Camera | None):
camera_obj._api = MagicMock(spec=ProtectApiClient)
camera_obj._api.get_camera_snapshot = AsyncMock(return_value=b"snapshot_data")

auth_user = camera_obj._api.bootstrap.auth_user

def mock_can(model_type: str, permission: PermissionNode, camera: Camera) -> bool:
return permission == PermissionNode.READ_LIVE

with patch.object(auth_user, "can", side_effect=mock_can):
snapshot = await camera_obj.get_snapshot()
assert snapshot == b"snapshot_data"


@pytest.mark.asyncio
async def test_get_snapshot_read_media_granted(camera_obj: Camera | None):
camera_obj._api = MagicMock(spec=ProtectApiClient)
camera_obj._api.get_camera_snapshot = AsyncMock(return_value=b"snapshot_data")

snapshot = await camera_obj.get_snapshot()
auth_user = camera_obj._api.bootstrap.auth_user

assert snapshot == b"snapshot_data"
camera_obj._api.get_camera_snapshot.assert_called_once_with(
camera_obj.id, None, camera_obj.high_camera_channel.height, dt=None
)
def mock_can(model_type: str, permission: PermissionNode, camera: Camera) -> bool:
return permission == PermissionNode.READ_MEDIA

with patch.object(auth_user, "can", side_effect=mock_can):
snapshot = await camera_obj.get_snapshot()
assert snapshot == b"snapshot_data"

@pytest.mark.skipif(not TEST_CAMERA_EXISTS, reason="Missing test data")
@pytest.mark.asyncio()
async def test_get_snapshot_read_live_no_perm(camera_obj: Camera | None):

@pytest.mark.asyncio
async def test_get_snapshot_no_permissions(camera_obj: Camera | None):
camera_obj._api = MagicMock(spec=ProtectApiClient)
camera_obj._api.get_camera_snapshot = AsyncMock(return_value=b"snapshot_data")

auth_user = camera_obj._api.bootstrap.auth_user

with patch.object(auth_user, "can", return_value=False):
camera_obj.api.api_request.reset_mock()
def mock_can(model_type: str, permission: PermissionNode, camera: Camera) -> bool:
return False

with patch.object(auth_user, "can", side_effect=mock_can):
with pytest.raises(
NotAuthorized,
match=f"Do not have permission to read live for camera: {camera_obj.id}",
match=f"Do not have permission to read live or media for camera: {camera_obj.id}",
):
await camera_obj.get_snapshot()

Expand All @@ -1387,42 +1401,77 @@ async def test_get_snapshot_with_dt(camera_obj: Camera):
)


@pytest.mark.skipif(not TEST_CAMERA_EXISTS, reason="Missing test data")
@pytest.mark.asyncio()
async def test_get_package_snapshot_read_live(camera_obj: Camera | None):
camera_obj.api.api_request.reset_mock()
camera_obj.feature_flags.has_package_camera = True
@pytest.mark.asyncio
async def test_get_snapshot_with_dt_no_read_media(camera_obj: Camera | None):
camera_obj._api = MagicMock(spec=ProtectApiClient)
camera_obj._api.get_camera_snapshot = AsyncMock(return_value=b"snapshot_data")

auth_user = camera_obj._api.bootstrap.auth_user

def mock_can(model_type: str, permission: PermissionNode, camera: Camera) -> bool:
return permission != PermissionNode.READ_MEDIA

with patch.object(auth_user, "can", side_effect=mock_can):
with pytest.raises(
NotAuthorized,
match=f"Do not have permission to read media for camera: {camera_obj.id}",
):
await camera_obj.get_snapshot(dt=datetime.now())


@pytest.mark.asyncio
async def test_get_package_snapshot_read_live_granted(camera_obj: Camera | None):
camera_obj._api = MagicMock(spec=ProtectApiClient)
camera_obj._api.get_package_camera_snapshot = AsyncMock(
return_value=b"snapshot_data"
)
camera_obj.feature_flags.has_package_camera = True

snapshot = await camera_obj.get_package_snapshot()
auth_user = camera_obj._api.bootstrap.auth_user

assert snapshot == b"snapshot_data"
camera_obj._api.get_package_camera_snapshot.assert_called_once_with(
camera_obj.id, None, None, dt=None
)
def mock_can(model_type: str, permission: PermissionNode, camera: Camera) -> bool:
return permission == PermissionNode.READ_LIVE

with patch.object(auth_user, "can", side_effect=mock_can):
snapshot = await camera_obj.get_package_snapshot()
assert snapshot == b"snapshot_data"

@pytest.mark.skipif(not TEST_CAMERA_EXISTS, reason="Missing test data")
@pytest.mark.asyncio()
async def test_get_package_snapshot_read_live_no_perm(camera_obj: Camera | None):

@pytest.mark.asyncio
async def test_get_package_snapshot_read_media_granted(camera_obj: Camera | None):
camera_obj._api = MagicMock(spec=ProtectApiClient)
camera_obj._api.get_package_camera_snapshot = AsyncMock(
return_value=b"snapshot_data"
)
camera_obj.feature_flags.has_package_camera = True

auth_user = camera_obj._api.bootstrap.auth_user

def mock_can(model_type: str, permission: PermissionNode, camera: Camera) -> bool:
return permission == PermissionNode.READ_MEDIA

with patch.object(auth_user, "can", side_effect=mock_can):
snapshot = await camera_obj.get_package_snapshot()
assert snapshot == b"snapshot_data"


@pytest.mark.asyncio
async def test_get_package_snapshot_no_permissions(camera_obj: Camera | None):
camera_obj._api = MagicMock(spec=ProtectApiClient)
camera_obj._api.get_package_camera_snapshot = AsyncMock(
return_value=b"snapshot_data"
)
camera_obj.feature_flags.has_package_camera = True

auth_user = camera_obj._api.bootstrap.auth_user

with patch.object(auth_user, "can", return_value=False):
camera_obj.api.api_request.reset_mock()
def mock_can(model_type: str, permission: PermissionNode, camera: Camera) -> bool:
return False

with patch.object(auth_user, "can", side_effect=mock_can):
with pytest.raises(
NotAuthorized,
match=f"Do not have permission to read live for camera: {camera_obj.id}",
match=f"Do not have permission to read live or media for camera: {camera_obj.id}",
):
await camera_obj.get_package_snapshot()

Expand All @@ -1431,11 +1480,11 @@ async def test_get_package_snapshot_read_live_no_perm(camera_obj: Camera | None)
@pytest.mark.asyncio
async def test_get_package_snapshot_with_dt(camera_obj: Camera):
camera_obj.api.api_request.reset_mock()
camera_obj.feature_flags.has_package_camera = True
camera_obj._api = MagicMock(spec=ProtectApiClient)
camera_obj._api.get_package_camera_snapshot = AsyncMock(
return_value=b"snapshot_data"
)
camera_obj.feature_flags.has_package_camera = True

now = datetime.now(tz=timezone.utc)

Expand All @@ -1445,3 +1494,43 @@ async def test_get_package_snapshot_with_dt(camera_obj: Camera):
camera_obj._api.get_package_camera_snapshot.assert_called_once_with(
camera_obj.id, None, None, dt=now
)


@pytest.mark.asyncio
async def test_get_package_snapshot_no_package_camera(camera_obj: Camera | None):
camera_obj._api = MagicMock(spec=ProtectApiClient)
camera_obj._api.get_package_camera_snapshot = AsyncMock(
return_value=b"snapshot_data"
)

# Simulate a device without a package camera
camera_obj.feature_flags.has_package_camera = False

with pytest.raises(
BadRequest,
match="Device does not have package camera",
):
await camera_obj.get_package_snapshot()


@pytest.mark.asyncio
async def test_get_package_snapshot_dt_no_read_media(camera_obj: Camera | None):
camera_obj._api = MagicMock(spec=ProtectApiClient)
camera_obj._api.get_package_camera_snapshot = AsyncMock(
return_value=b"snapshot_data"
)
camera_obj.feature_flags.has_package_camera = True

auth_user = camera_obj._api.bootstrap.auth_user

def mock_can(model_type: str, permission: PermissionNode, camera: Camera) -> bool:
return (
permission != PermissionNode.READ_MEDIA
) # Simulate missing READ_MEDIA permission

with patch.object(auth_user, "can", side_effect=mock_can):
with pytest.raises(
NotAuthorized,
match=f"Do not have permission to read media for camera: {camera_obj.id}",
):
await camera_obj.get_package_snapshot(dt=datetime.now())
1 change: 1 addition & 0 deletions tests/sample_data/sample_camera.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
"videoReconfigurationInProgress": false,
"voltage": 27.3,
"isPoorNetwork": false,
"isThirdPartyCamera": false,
"wiredConnectionState": {
"phyRate": null
},
Expand Down

0 comments on commit c636bfc

Please sign in to comment.