Skip to content

Commit

Permalink
Implement lock route alias (#229)
Browse files Browse the repository at this point in the history
* Update lock alias

* Add unit tests

* Add integration tests

* Add end-to-end tests

* Update docs

* Fix typo
  • Loading branch information
Hoanh An authored Nov 27, 2018
1 parent 91d1947 commit 774d576
Show file tree
Hide file tree
Showing 7 changed files with 426 additions and 9 deletions.
106 changes: 104 additions & 2 deletions docs/api/index.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -504,9 +504,14 @@ response = requests.get('http://host:5000/synse/v2/scan')
"type": "temperature"
},
{
"id":" f838b2d6afceb01e7a2634893f6f935c",
"id": "f838b2d6afceb01e7a2634893f6f935c",
"info": "Synse Pressure Sensor 2",
"type": "pressure"
},
{
"id": "da7fbdfc8e962922685af9d0fac53379",
"info": "Synse Door Lock",
"type": "lock"
}
]
}
Expand Down Expand Up @@ -1469,4 +1474,101 @@ Invalid query parameters will result in a 400 Invalid Arguments error.

### Response Fields

See the responses for [read](#read) and [write](#write).
See the responses for [read](#read) and [write](#write).


## Lock

> If no *valid* query parameters are specified, this will **read** from the lock device.
```shell
curl "http://host:5000/synse/v2/lock/rack-1/vec/da7fbdfc8e962922685af9d0fac53379"
```

```python
import requests

response = requests.get('http://host:5000/synse/v2/lock/rack-1/vec/da7fbdfc8e962922685af9d0fac53379')
```

> The response JSON will be the same as read response:
```json
{
"kind": "lock",
"data": [
{
"value": "locked",
"timestamp": "2018-11-27T13:47:19.998713947Z",
"unit": null,
"type": "state",
"info": ""
}
]
}
```

> If any *valid* query parameters are specified, this will **write** to the lock device.
```shell
curl "http://host:5000/synse/v2/lock/rack-1/vec/da7fbdfc8e962922685af9d0fac53379?action=unlock"
```

```python
import requests

response = requests.get('http://host:5000/synse/v2/lock/rack-1/vec/da7fbdfc8e962922685af9d0fac53379?action=unlock')
```

> The response JSON will be the same as a write response:
```json
[
{
"context": {
"action": "unlock"
},
"transaction": "gbo5t0a8atig19jnhue1"
}
]
```

An alias to `read` from or `write` to a known lock device.

While a lock device can be read directly via the [read](#read) route or written to directly from the
[write](#write) route, this route provides some additional checks and validation before dispatching to
the appropriate plugin handler. In particular, it checks if the specified device is a lock device and
that the given query parameter value(s), if any, are permissible.

If no valid query parameters are specified, this endpoint will read the specified device. If any number
of valid query parameters are specified, the endpoint will write to the specified device.

Invalid query parameters will result in a 400 Invalid Arguments error.

### HTTP Request

`GET http://host:5000/synse/v2/lock/{rack}/{board}/{device}`

### URI Parameters

| Parameter | Required | Description |
| --------- | -------- | ----------- |
| *rack* | yes | The id of the rack containing the lock device to read from/write to. |
| *board* | yes | The id of the board containing the lock device to read from/write to. |
| *device* | yes | The id of the lock device to read from/write to. |

### Query Parameters

| Parameter | Description |
| --------- | ----------- |
| *action* | The state to set the fan to. *Valid values:* (`lock`, `unlock`, `pulseUnlock`) |

<aside class="notice">
While Synse Server supports the listed Query Parameters, not all devices will support the
corresponding actions. As a result, writing to some <i>lock</i> instances may result in error.
</aside>

### Response Fields

See the responses for [read](#read) and [write](#write).

18 changes: 18 additions & 0 deletions emulator/config/device/lock.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
version: 1.0
locations:
- name: r1vec
rack:
name: rack-1
board:
name: vec
devices:
- name: lock
metadata:
model: emul8-lock
outputs:
- type: lock.state
instances:
- info: Synse Door Lock
location: r1vec
data:
id: 1
4 changes: 2 additions & 2 deletions synse/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@
# Lock action definitions
LOCK_LOCK = 'lock'
LOCK_UNLOCK = 'unlock'
LOCK_MUNLOCK = 'munlock' # momentary unlock
LOCK_PULSEUNLOCK = 'pulseUnlock'

# All lock actions
lock_actions = (
LOCK_LOCK,
LOCK_UNLOCK,
LOCK_MUNLOCK
LOCK_PULSEUNLOCK,
)
34 changes: 32 additions & 2 deletions synse/routes/aliases.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,5 +301,35 @@ async def lock_route(request, rack, board, device): # pylint: disable=unused-arg
Returns:
sanic.response.HTTPResponse: The endpoint response.
"""
# FIXME (etd) - error out until a lock backend is actually implemented
return errors.SynseError('Endpoint not yet implemented')
await validate.validate_device_type(const.LOCK_TYPES, rack, board, device)

# Get the valid query parameters. If unsupported query parameters
# are specified, this will raise an error.
qparams = validate.validate_query_params(
request.raw_args,
'action'
)
param_action = qparams.get('action')

# If any of the parameters are specified, this will be a write request
# using those parameters.
if param_action is not None:
logger.debug(_('Lock alias route: writing (query parameters: {})').format(qparams))

if param_action not in const.lock_actions:
raise errors.InvalidArgumentsError(
_('Invalid boot target "{}". Must be one of: {}').format(
param_action, const.lock_actions)
)

data = {
'action': param_action,
}
transaction = await commands.write(rack, board, device, data)
return transaction.to_json()

# Otherwise, we just read from the device.
else:
logger.debug(_('Lock alias route: reading'))
reading = await commands.read(rack, board, device)
return reading.to_json()
93 changes: 90 additions & 3 deletions tests/end_to_end/test_synse.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ class EmulatorDevices:
'12835beffd3e6c603aa4dd92127707b5'
]

all = airflow + temperature + pressure + humidity + led + fan
lock = [
'da7fbdfc8e962922685af9d0fac53379'
]

all = airflow + temperature + pressure + humidity + led + fan + lock

@classmethod
def is_airflow(cls, device):
Expand Down Expand Up @@ -99,6 +103,12 @@ def is_fan(cls, device):
"""Check if the device ID corresponds to a fan type device."""
return device in cls.fan

@classmethod
def is_lock(cls, device):
"""Check if the device ID corresponds to a lock type device."""
return device in cls.lock



def url_unversioned(uri):
"""Create the unversioned URL for Synse Server with the given URI."""
Expand Down Expand Up @@ -203,7 +213,8 @@ def validate_read(data):
'fan': ['speed'],
'airflow': ['airflow'],
'pressure': ['pressure'],
'humidity': ['temperature', 'humidity']
'humidity': ['temperature', 'humidity'],
'lock': ['state']
}

keys = lookup.get(k)
Expand Down Expand Up @@ -299,6 +310,8 @@ def validate_scan_board(board):
assert device['type'] == 'airflow'
elif EmulatorDevices.is_fan(_id):
assert device['type'] == 'fan'
elif EmulatorDevices.is_lock(_id):
assert device['type'] == 'lock'
else:
pytest.fail('Unexpected device type: {}'.format(device))

Expand Down Expand Up @@ -587,7 +600,7 @@ def test_write_ok(self, device):
assert transaction['status'] == 'done'

@pytest.mark.parametrize(
'device', filter(lambda x: x not in EmulatorDevices.fan + EmulatorDevices.led, EmulatorDevices.all)
'device', filter(lambda x: x not in EmulatorDevices.fan + EmulatorDevices.led + EmulatorDevices.lock, EmulatorDevices.all)
)
def test_write_error(self, device):
"""Test Synse Server's 'write' route, specifying non-writable devices."""
Expand Down Expand Up @@ -925,3 +938,77 @@ def test_fan_write_bad_params(self, device, params):

data = response.json()
validate_error_response(data, 400, errors.INVALID_ARGUMENTS)


#
# Lock
#

class TestLock:
"""Tests for the 'lock' route."""

@pytest.mark.parametrize(
'device', EmulatorDevices.lock
)
def test_lock_read_ok(self, device):
"""Test Synse Server's 'lock' route, specifying valid lock devices to read from."""
response = requests.get(url('lock/rack-1/vec/{}'.format(device)))
assert response.status_code == 200

data = response.json()
validate_read(data)

@pytest.mark.parametrize(
'device', EmulatorDevices.lock
)
def test_lock_write_ok(self, device):
"""Test Synse Server's 'lock' route, specifying valid lock devices to write to."""
response = requests.get(url('lock/rack-1/vec/{}'.format(device)), params={'action': 'unlock'})
assert response.status_code == 200

data = response.json()
validate_write_ok(data, 1)

for t in data:
transaction_id = t['transaction']
transaction = wait_for_transaction(transaction_id)

assert transaction['state'] == 'ok'
assert transaction['status'] == 'done'

@pytest.mark.parametrize(
'device', filter(lambda x: x not in EmulatorDevices.lock, EmulatorDevices.all)
)
def test_lock_read_bad_device(self, device):
"""Test Synse Server's 'lock' route, specifying non-lock devices to read from."""
response = requests.get(url('lock/rack-1/vec/{}'.format(device)))
assert response.status_code == 400

data = response.json()
validate_error_response(data, 400, errors.INVALID_DEVICE_TYPE)

@pytest.mark.parametrize(
'device', filter(lambda x: x not in EmulatorDevices.lock, EmulatorDevices.all)
)
def test_lock_write_bad_device(self, device):
"""Test Synse Server's 'lock' route, specifying non-lock devices to write to."""
response = requests.get(url('lock/rack-1/vec/{}'.format(device)), params={'action': 'unlock'})
assert response.status_code == 400

data = response.json()
validate_error_response(data, 400, errors.INVALID_DEVICE_TYPE)

@pytest.mark.parametrize(
'device,params', [(device, params) for params in [
{'foo': 'bar'},
{'action': 'unlock', 'foo': 'bar'},
{'action': 'invalid-action'}
] for device in EmulatorDevices.lock]
)
def test_lock_write_bad_params(self, device, params):
"""Test Synse Server's 'lock' route, specifying invalid query parameters for write."""
response = requests.get(url('lock/rack-1/vec/{}'.format(device)), params=params)
assert response.status_code == 400

data = response.json()
validate_error_response(data, 400, errors.INVALID_ARGUMENTS)
50 changes: 50 additions & 0 deletions tests/integration/routes/aliases/test_lock_route.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Test the 'synse.routes.aliases' module's lock route."""
# pylint: disable=redefined-outer-name,unused-argument

from synse import errors
from synse.version import __api_version__
from tests import utils

invalid_lock_route_url = '/synse/{}/lock/invalid-rack/invalid-board/invalid-device'.format(__api_version__)


def test_lock_endpoint_invalid(app):
"""Get lock info for a nonexistent device."""
_, response = app.test_client.get(invalid_lock_route_url)
utils.test_error_json(response, errors.DEVICE_NOT_FOUND, 404)


def test_lock_endpoint_post_not_allowed(app):
"""Invalid request: POST"""
_, response = app.test_client.post(invalid_lock_route_url)
assert response.status == 405


def test_lock_endpoint_put_not_allowed(app):
"""Invalid request: PUT"""
_, response = app.test_client.put(invalid_lock_route_url)
assert response.status == 405


def test_lock_endpoint_delete_not_allowed(app):
"""Invalid request: DELETE"""
_, response = app.test_client.delete(invalid_lock_route_url)
assert response.status == 405


def test_lock_endpoint_patch_not_allowed(app):
"""Invalid request: PATCH"""
_, response = app.test_client.patch(invalid_lock_route_url)
assert response.status == 405


def test_lock_endpoint_head_not_allowed(app):
"""Invalid request: HEAD"""
_, response = app.test_client.head(invalid_lock_route_url)
assert response.status == 405


def test_lock_endpoint_options_not_allowed(app):
"""Invalid request: OPTIONS"""
_, response = app.test_client.options(invalid_lock_route_url)
assert response.status == 405
Loading

0 comments on commit 774d576

Please sign in to comment.