mirror of
https://github.com/home-assistant/core.git
synced 2025-08-05 21:55:10 +02:00
Allow unregistering a push subscription (#2921)
* Allow unregistering a push subscription
* Update frontend
* ps - HTML5 tests DRY 🍾
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
|
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
|
||||||
|
|
||||||
FINGERPRINTS = {
|
FINGERPRINTS = {
|
||||||
"core.js": "b4ee3a700ef5549a36b436611e27d3a9",
|
"core.js": "7901b14f238956024a19139d6c479d68",
|
||||||
"frontend.html": "203371247fdba69b4d4d92fd707a459a",
|
"frontend.html": "b33df7a012ea6d2aaf353c4466d6554a",
|
||||||
"mdi.html": "710b84acc99b32514f52291aba9cd8e8",
|
"mdi.html": "710b84acc99b32514f52291aba9cd8e8",
|
||||||
"panels/ha-panel-dev-event.html": "3cc881ae8026c0fba5aa67d334a3ab2b",
|
"panels/ha-panel-dev-event.html": "3cc881ae8026c0fba5aa67d334a3ab2b",
|
||||||
"panels/ha-panel-dev-info.html": "34e2df1af32e60fffcafe7e008a92169",
|
"panels/ha-panel-dev-info.html": "34e2df1af32e60fffcafe7e008a92169",
|
||||||
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -182,6 +182,30 @@ class HTML5PushRegistrationView(HomeAssistantView):
|
|||||||
|
|
||||||
return self.json_message('Push notification subscriber registered.')
|
return self.json_message('Push notification subscriber registered.')
|
||||||
|
|
||||||
|
def delete(self, request):
|
||||||
|
"""Delete a registration."""
|
||||||
|
subscription = request.json.get(ATTR_SUBSCRIPTION)
|
||||||
|
|
||||||
|
found = None
|
||||||
|
|
||||||
|
for key, registration in self.registrations.items():
|
||||||
|
if registration.get(ATTR_SUBSCRIPTION) == subscription:
|
||||||
|
found = key
|
||||||
|
break
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
# If not found, unregistering was already done. Return 200
|
||||||
|
return self.json_message('Registration not found.')
|
||||||
|
|
||||||
|
reg = self.registrations.pop(found)
|
||||||
|
|
||||||
|
if not _save_config(self.json_path, self.registrations):
|
||||||
|
self.registrations[found] = reg
|
||||||
|
return self.json_message('Error saving registration.',
|
||||||
|
HTTP_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
return self.json_message('Push notification subscriber unregistered.')
|
||||||
|
|
||||||
|
|
||||||
class HTML5PushCallbackView(HomeAssistantView):
|
class HTML5PushCallbackView(HomeAssistantView):
|
||||||
"""Accepts push registrations from a browser."""
|
"""Accepts push registrations from a browser."""
|
||||||
|
@@ -8,6 +8,34 @@ from werkzeug.test import EnvironBuilder
|
|||||||
from homeassistant.components.http import request_class
|
from homeassistant.components.http import request_class
|
||||||
from homeassistant.components.notify import html5
|
from homeassistant.components.notify import html5
|
||||||
|
|
||||||
|
SUBSCRIPTION_1 = {
|
||||||
|
'browser': 'chrome',
|
||||||
|
'subscription': {
|
||||||
|
'endpoint': 'https://google.com',
|
||||||
|
'keys': {'auth': 'auth', 'p256dh': 'p256dh'}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
SUBSCRIPTION_2 = {
|
||||||
|
'browser': 'firefox',
|
||||||
|
'subscription': {
|
||||||
|
'endpoint': 'https://example.com',
|
||||||
|
'keys': {
|
||||||
|
'auth': 'bla',
|
||||||
|
'p256dh': 'bla',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
SUBSCRIPTION_3 = {
|
||||||
|
'browser': 'chrome',
|
||||||
|
'subscription': {
|
||||||
|
'endpoint': 'https://example.com/not_exist',
|
||||||
|
'keys': {
|
||||||
|
'auth': 'bla',
|
||||||
|
'p256dh': 'bla',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class TestHtml5Notify(object):
|
class TestHtml5Notify(object):
|
||||||
"""Tests for HTML5 notify platform."""
|
"""Tests for HTML5 notify platform."""
|
||||||
@@ -40,13 +68,7 @@ class TestHtml5Notify(object):
|
|||||||
hass = MagicMock()
|
hass = MagicMock()
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'device': {
|
'device': SUBSCRIPTION_1
|
||||||
'browser': 'chrome',
|
|
||||||
'subscription': {
|
|
||||||
'endpoint': 'https://google.com',
|
|
||||||
'keys': {'auth': 'auth', 'p256dh': 'p256dh'}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile() as fp:
|
with tempfile.NamedTemporaryFile() as fp:
|
||||||
@@ -63,10 +85,7 @@ class TestHtml5Notify(object):
|
|||||||
assert len(mock_wp.mock_calls) == 2
|
assert len(mock_wp.mock_calls) == 2
|
||||||
|
|
||||||
# WebPusher constructor
|
# WebPusher constructor
|
||||||
assert mock_wp.mock_calls[0][1][0] == {'endpoint':
|
assert mock_wp.mock_calls[0][1][0] == SUBSCRIPTION_1['subscription']
|
||||||
'https://google.com',
|
|
||||||
'keys': {'auth': 'auth',
|
|
||||||
'p256dh': 'p256dh'}}
|
|
||||||
|
|
||||||
# Call to send
|
# Call to send
|
||||||
payload = json.loads(mock_wp.mock_calls[1][1][0])
|
payload = json.loads(mock_wp.mock_calls[1][1][0])
|
||||||
@@ -92,22 +111,13 @@ class TestHtml5Notify(object):
|
|||||||
assert view.json_path == fp.name
|
assert view.json_path == fp.name
|
||||||
assert view.registrations == {}
|
assert view.registrations == {}
|
||||||
|
|
||||||
builder = EnvironBuilder(method='POST', data=json.dumps({
|
builder = EnvironBuilder(method='POST',
|
||||||
'browser': 'chrome',
|
data=json.dumps(SUBSCRIPTION_1))
|
||||||
'subscription': {'endpoint': 'https://google.com',
|
|
||||||
'keys': {'auth': 'auth',
|
|
||||||
'p256dh': 'p256dh'}},
|
|
||||||
}))
|
|
||||||
Request = request_class()
|
Request = request_class()
|
||||||
resp = view.post(Request(builder.get_environ()))
|
resp = view.post(Request(builder.get_environ()))
|
||||||
|
|
||||||
expected = {
|
expected = {
|
||||||
'unnamed device': {
|
'unnamed device': SUBSCRIPTION_1,
|
||||||
'browser': 'chrome',
|
|
||||||
'subscription': {'endpoint': 'https://google.com',
|
|
||||||
'keys': {'auth': 'auth',
|
|
||||||
'p256dh': 'p256dh'}},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert resp.status_code == 200, resp.response
|
assert resp.status_code == 200, resp.response
|
||||||
@@ -154,6 +164,116 @@ class TestHtml5Notify(object):
|
|||||||
resp = view.post(Request(builder.get_environ()))
|
resp = view.post(Request(builder.get_environ()))
|
||||||
assert resp.status_code == 400, resp.response
|
assert resp.status_code == 400, resp.response
|
||||||
|
|
||||||
|
def test_unregistering_device_view(self):
|
||||||
|
"""Test that the HTML unregister view works."""
|
||||||
|
hass = MagicMock()
|
||||||
|
|
||||||
|
config = {
|
||||||
|
'some device': SUBSCRIPTION_1,
|
||||||
|
'other device': SUBSCRIPTION_2,
|
||||||
|
}
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile() as fp:
|
||||||
|
hass.config.path.return_value = fp.name
|
||||||
|
fp.write(json.dumps(config).encode('utf-8'))
|
||||||
|
fp.flush()
|
||||||
|
service = html5.get_service(hass, {})
|
||||||
|
|
||||||
|
assert service is not None
|
||||||
|
|
||||||
|
# assert hass.called
|
||||||
|
assert len(hass.mock_calls) == 3
|
||||||
|
|
||||||
|
view = hass.mock_calls[1][1][0]
|
||||||
|
assert view.json_path == fp.name
|
||||||
|
assert view.registrations == config
|
||||||
|
|
||||||
|
builder = EnvironBuilder(method='DELETE', data=json.dumps({
|
||||||
|
'subscription': SUBSCRIPTION_1['subscription'],
|
||||||
|
}))
|
||||||
|
Request = request_class()
|
||||||
|
resp = view.delete(Request(builder.get_environ()))
|
||||||
|
|
||||||
|
config.pop('some device')
|
||||||
|
|
||||||
|
assert resp.status_code == 200, resp.response
|
||||||
|
assert view.registrations == config
|
||||||
|
with open(fp.name) as fpp:
|
||||||
|
assert json.load(fpp) == config
|
||||||
|
|
||||||
|
def test_unregistering_device_view_handles_unknown_subscription(self):
|
||||||
|
"""Test that the HTML unregister view handles unknown subscriptions."""
|
||||||
|
hass = MagicMock()
|
||||||
|
|
||||||
|
config = {
|
||||||
|
'some device': SUBSCRIPTION_1,
|
||||||
|
'other device': SUBSCRIPTION_2,
|
||||||
|
}
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile() as fp:
|
||||||
|
hass.config.path.return_value = fp.name
|
||||||
|
fp.write(json.dumps(config).encode('utf-8'))
|
||||||
|
fp.flush()
|
||||||
|
service = html5.get_service(hass, {})
|
||||||
|
|
||||||
|
assert service is not None
|
||||||
|
|
||||||
|
# assert hass.called
|
||||||
|
assert len(hass.mock_calls) == 3
|
||||||
|
|
||||||
|
view = hass.mock_calls[1][1][0]
|
||||||
|
assert view.json_path == fp.name
|
||||||
|
assert view.registrations == config
|
||||||
|
|
||||||
|
builder = EnvironBuilder(method='DELETE', data=json.dumps({
|
||||||
|
'subscription': SUBSCRIPTION_3['subscription']
|
||||||
|
}))
|
||||||
|
Request = request_class()
|
||||||
|
resp = view.delete(Request(builder.get_environ()))
|
||||||
|
|
||||||
|
assert resp.status_code == 200, resp.response
|
||||||
|
assert view.registrations == config
|
||||||
|
with open(fp.name) as fpp:
|
||||||
|
assert json.load(fpp) == config
|
||||||
|
|
||||||
|
def test_unregistering_device_view_handles_json_safe_error(self):
|
||||||
|
"""Test that the HTML unregister view handles JSON write errors."""
|
||||||
|
hass = MagicMock()
|
||||||
|
|
||||||
|
config = {
|
||||||
|
'some device': SUBSCRIPTION_1,
|
||||||
|
'other device': SUBSCRIPTION_2,
|
||||||
|
}
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile() as fp:
|
||||||
|
hass.config.path.return_value = fp.name
|
||||||
|
fp.write(json.dumps(config).encode('utf-8'))
|
||||||
|
fp.flush()
|
||||||
|
service = html5.get_service(hass, {})
|
||||||
|
|
||||||
|
assert service is not None
|
||||||
|
|
||||||
|
# assert hass.called
|
||||||
|
assert len(hass.mock_calls) == 3
|
||||||
|
|
||||||
|
view = hass.mock_calls[1][1][0]
|
||||||
|
assert view.json_path == fp.name
|
||||||
|
assert view.registrations == config
|
||||||
|
|
||||||
|
builder = EnvironBuilder(method='DELETE', data=json.dumps({
|
||||||
|
'subscription': SUBSCRIPTION_1['subscription'],
|
||||||
|
}))
|
||||||
|
Request = request_class()
|
||||||
|
|
||||||
|
with patch('homeassistant.components.notify.html5._save_config',
|
||||||
|
return_value=False):
|
||||||
|
resp = view.delete(Request(builder.get_environ()))
|
||||||
|
|
||||||
|
assert resp.status_code == 500, resp.response
|
||||||
|
assert view.registrations == config
|
||||||
|
with open(fp.name) as fpp:
|
||||||
|
assert json.load(fpp) == config
|
||||||
|
|
||||||
def test_callback_view_no_jwt(self):
|
def test_callback_view_no_jwt(self):
|
||||||
"""Test that the notification callback view works without JWT."""
|
"""Test that the notification callback view works without JWT."""
|
||||||
hass = MagicMock()
|
hass = MagicMock()
|
||||||
@@ -185,13 +305,7 @@ class TestHtml5Notify(object):
|
|||||||
hass = MagicMock()
|
hass = MagicMock()
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'device': {
|
'device': SUBSCRIPTION_1,
|
||||||
'browser': 'chrome',
|
|
||||||
'subscription': {
|
|
||||||
'endpoint': 'https://google.com',
|
|
||||||
'keys': {'auth': 'auth', 'p256dh': 'p256dh'}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile() as fp:
|
with tempfile.NamedTemporaryFile() as fp:
|
||||||
@@ -211,11 +325,8 @@ class TestHtml5Notify(object):
|
|||||||
assert len(mock_wp.mock_calls) == 2
|
assert len(mock_wp.mock_calls) == 2
|
||||||
|
|
||||||
# WebPusher constructor
|
# WebPusher constructor
|
||||||
assert mock_wp.mock_calls[0][1][0] == {'endpoint':
|
assert mock_wp.mock_calls[0][1][0] == \
|
||||||
'https://google.com',
|
SUBSCRIPTION_1['subscription']
|
||||||
'keys': {'auth': 'auth',
|
|
||||||
'p256dh':
|
|
||||||
'p256dh'}}
|
|
||||||
|
|
||||||
# Call to send
|
# Call to send
|
||||||
push_payload = json.loads(mock_wp.mock_calls[1][1][0])
|
push_payload = json.loads(mock_wp.mock_calls[1][1][0])
|
||||||
|
Reference in New Issue
Block a user