mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +00:00
Allow unregistering a push subscription (#2921)
* Allow unregistering a push subscription
* Update frontend
* ps - HTML5 tests DRY 🍾
This commit is contained in:
parent
d2f7b3c7db
commit
7598de90cb
@ -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.
@ -1 +1 @@
|
|||||||
Subproject commit 59627864944a93538858ed0930de92891b4bfe66
|
Subproject commit c0c0b2c2f3ccfa6b04c6073c4e826ccae9baf8b6
|
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])
|
||||||
|
Loading…
x
Reference in New Issue
Block a user