Skip to content

Commit

Permalink
[DOJOT-75] [DOJOT-76] Feature - Unique Labels (#201)
Browse files Browse the repository at this point in the history
* Set device and template labels as unique

* Handle template integrity errors using utils func

* Update docs

* Fix automated tests

* Add tests for integrity errors

* Fix codefactor error
  • Loading branch information
LuanEdCosta committed Sep 2, 2022
1 parent 7fb82f3 commit 0a64333
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 14 deletions.
4 changes: 2 additions & 2 deletions DeviceManager/DatabaseModels.py 100644 → 100755
Expand Up @@ -68,7 +68,7 @@ class DeviceTemplate(db.Model):
__tablename__ = 'templates'

id = db.Column(db.Integer, db.Sequence('template_id'), primary_key=True)
label = db.Column(db.String(128), nullable=False)
label = db.Column(db.String(128), nullable=False, unique=True)
created = db.Column(db.DateTime, default=datetime.now)
updated = db.Column(db.DateTime, onupdate=datetime.now)

Expand Down Expand Up @@ -99,7 +99,7 @@ class Device(db.Model):
__tablename__ = 'devices'

id = db.Column(db.String(8), unique=True, nullable=False, primary_key=True)
label = db.Column(db.String(128), nullable=False)
label = db.Column(db.String(128), nullable=False, unique=True)
created = db.Column(db.DateTime, default=datetime.now)
updated = db.Column(db.DateTime, onupdate=datetime.now)

Expand Down
6 changes: 3 additions & 3 deletions DeviceManager/TemplateHandler.py
Expand Up @@ -197,9 +197,9 @@ def create_template(params, token):
try:
db.session.commit()
LOGGER.debug(f" Created template in database")
except IntegrityError as e:
LOGGER.error(f' {e}')
raise HTTPRequestError(400, 'Template attribute constraints are violated by the request')
except IntegrityError as error:
LOGGER.error(f' {error}')
handle_consistency_exception(error)

results = {
'template': template_schema.dump(loaded_template),
Expand Down
6 changes: 3 additions & 3 deletions README.md
Expand Up @@ -133,7 +133,7 @@ The information model used for both “real” and virtual devices is as followi
| **attrs** | Map of attributes | read-only | No | Map of device's attributes (check the attributes in the next table)
| **created** | DateTime (with timezone and µs precision) in ISO format | read-only | No | Device creation time.
| **id** | String (length of 8 bytes) | read-only | No | Unique identifier for the device.
| **label** | String (length of 128 bytes) | read-write | Yes | An user-defined label to facilitate the device's identification.
| **label** | String (length of 128 bytes) | read-write | Yes | An user-defined label to facilitate the device's identification. The label is unique for this device.
| **templates** | Strings list | read-only | No | List of template IDs used by the device.
| **updated** | DateTime (with timezone and µs precision) in ISO format | read-only | No | Device last update time.

Expand Down Expand Up @@ -214,7 +214,7 @@ An example of such structure would be:
### **Template**

All devices are based on a **template**, which can be thought as a blueprint: all devices built
using the same template will have the same characteristics. Templates in dojot have one label (any
using the same template will have the same characteristics. Templates in dojot have one unique label (any
alphanumeric sequence), a list of attributes which will hold all the device emitted information, and
optionally a few special attributes which will indicate how the device communicates, including
transmission methods (protocol, ports, etc.) and message formats.
Expand All @@ -239,7 +239,7 @@ The information model used for templates is:
| **created** | DateTime (with timezone and µs precision) in ISO format | read-only | No | Device creation time.
| **data_attrs** | Map of attributes | read-write | No | Stores attributes with the types `dynamic`, `static` and `actuator`.
| **id** | String (length of 8 bytes) | read-write | No | Unique identifier for the template.
| **label** | String (length of 128 bytes) | read-write | Yes | An user-defined label to facilitate the template's identification.
| **label** | String (length of 128 bytes) | read-write | Yes | An user-defined label to facilitate the template's identification. The label is unique for this template.
| **updated** | DateTime (with timezone and µs precision) in ISO format | read-only | No | Device last update time.

An example template structure:
Expand Down
48 changes: 47 additions & 1 deletion docs/apiary.apib
Expand Up @@ -100,7 +100,6 @@ Register a new template
}
}


+ Response 400 (application/json)

{
Expand All @@ -112,6 +111,19 @@ Register a new template
"message": "failed to parse input"
}

+ Response 400 (application/json)

{
"message": "duplicate key value violates unique constraint \"templates_label_key\"",
"status": 400
}

+ Response 400 (application/json)

{
"message": "duplicate key value violates unique constraint \"attrs_template_id_type_label_key\"",
"status": 400
}

### Get the current list of templates [GET /template{?page_size,page_num,attr_format,attr,attr_type,label,sortBy}]

Expand Down Expand Up @@ -498,6 +510,20 @@ updated.
"message": "No such template: 123",
"status": 404
}

+ Response 400 (application/json)

{
"message": "duplicate key value violates unique constraint \"templates_label_key\"",
"status": 400
}

+ Response 400 (application/json)

{
"message": "duplicate key value violates unique constraint \"attrs_template_id_type_label_key\"",
"status": 400
}

### Delete template [DELETE /template/{id}]

Expand Down Expand Up @@ -648,6 +674,13 @@ attributes to be applied to this device.
"status": 400
}

+ Response 400 (application/json)

{
"message": "duplicate key value violates unique constraint \"devices_label_key\"",
"status": 400
}

+ Response 404 (application/json)

{
Expand Down Expand Up @@ -750,6 +783,13 @@ attributes to be applied to this device.
"status": 400
}

+ Response 400 (application/json)

{
"message": "duplicate key value violates unique constraint \"devices_label_key\"",
"status": 400
}

+ Response 404 (application/json)

{
Expand Down Expand Up @@ -1160,6 +1200,12 @@ Updates a device's configuration
"status": 400
}

+ Response 400 (application/json)

{
"message": "duplicate key value violates unique constraint \"devices_label_key\"",
"status": 400
}

### Configure device [PUT /device/{id}/actuate]

Expand Down
30 changes: 30 additions & 0 deletions migrations/versions/944f3122f51e_.py
@@ -0,0 +1,30 @@
"""empty message
Revision ID: 944f3122f51e
Revises: fabf2ca39860
Create Date: 2022-08-12 13:44:41.398334
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '944f3122f51e'
down_revision = 'fabf2ca39860'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_unique_constraint(None, 'devices', ['label'])
op.create_unique_constraint(None, 'templates', ['label'])
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'templates', type_='unique')
op.drop_constraint(None, 'devices', type_='unique')
# ### end Alembic commands ###
11 changes: 7 additions & 4 deletions tests/dredd-hooks/operation_hook.py
@@ -1,5 +1,6 @@
from __future__ import absolute_import
import dredd_hooks as hooks
from uuid import uuid4
import json
import re

Expand All @@ -17,13 +18,15 @@ def __init__(self, data):
self.args = data['args']
self.data = data['body']

def generate_unique_label(prefix):
return f"{prefix}_{uuid4()}"

def sort_attributes(device, attribute):
device[attribute] = sorted(device[attribute], key=lambda k: k['label'])

def create_sample_template():
template = {
"label": "SensorModel",
"label": generate_unique_label("SensorModel"),
"attrs": [
{
"label": "temperature",
Expand Down Expand Up @@ -68,7 +71,7 @@ def create_sample_template():

def create_actuator_template():
template = {
"label": "SensorModel",
"label": generate_unique_label("SensorModel"),
"attrs": [
{
"label": "temperature",
Expand Down Expand Up @@ -168,7 +171,7 @@ def create_single_device(transaction):
transaction['proprietary'] = {}
transaction['proprietary']['template_id'] = template_id
device = {
"label": "test_device",
"label": generate_unique_label("test_device"),
"templates": [template_id]
}
req = {
Expand Down Expand Up @@ -202,7 +205,7 @@ def create_actuator_device(transaction):
transaction['proprietary'] = {}
transaction['proprietary']['template_id'] = template_id
device = {
"label": "test_device",
"label": generate_unique_label("test_device"),
"templates": [template_id]
}
req = {
Expand Down
28 changes: 28 additions & 0 deletions tests/test_device_handler.py
Expand Up @@ -3,6 +3,7 @@
import unittest
from unittest.mock import Mock, MagicMock, patch, call
from flask import Flask
from sqlalchemy.exc import IntegrityError

from DeviceManager.DeviceHandler import DeviceHandler, flask_delete_all_device, flask_get_device, flask_remove_device, flask_add_template_to_device, flask_remove_template_from_device, flask_gen_psk,flask_internal_get_device
from DeviceManager.utils import HTTPRequestError
Expand Down Expand Up @@ -305,6 +306,33 @@ def test_create_device(self, db_mock_session, query_property_getter_mock):
with self.assertRaises(HTTPRequestError):
result = DeviceHandler.create_device(params, token)

@patch('flask_sqlalchemy._QueryProperty.__get__')
def test_create_device_integrity_errors(self, query_get_mock):
with patch('DeviceManager.DeviceHandler.init_tenant_context') as mock_init_tenant_context:
mock_init_tenant_context.return_value = 'admin'
with patch('DeviceManager.DeviceHandler.DeviceHandler.generate_device_id') as mock_device_id:
mock_device_id.return_value = 'test_device_id'
with patch('DeviceManager.DeviceHandler.DeviceHandler.validate_device_id') as mock_validate_device_id:
mock_validate_device_id.return_value = True
with patch.object(KafkaInstanceHandler, "getInstance", return_value=MagicMock()):
with patch('DeviceManager.DeviceHandler.db') as db_mock:
diag = Exception()
diag.message_primary = 'test'
orig = Exception()
orig.diag = diag

db_mock.session = AlchemyMagicMock()
db_mock.session.commit = Mock()
db_mock.session.commit.side_effect = IntegrityError(statement='test', params='test', orig=orig)

token = generate_token()
data = '{"label":"test_device","templates":[1]}'
params = {'count': '1', 'verbose': 'false',
'content_type': 'application/json', 'data': data}

with self.assertRaises(HTTPRequestError):
DeviceHandler.create_device(params, token)

@patch('DeviceManager.DeviceHandler.db')
@patch('flask_sqlalchemy._QueryProperty.__get__')
def test_update_device(self, db_mock_session, query_property_getter_mock):
Expand Down
39 changes: 38 additions & 1 deletion tests/test_template_handler.py
Expand Up @@ -10,7 +10,7 @@
from DeviceManager.utils import HTTPRequestError
from DeviceManager.BackendHandler import KafkaInstanceHandler
from datetime import datetime

from sqlalchemy.exc import IntegrityError

from .token_test_generator import generate_token

Expand Down Expand Up @@ -77,6 +77,43 @@ def test_create_tempĺate(self, db_mock):
self.assertEqual(result['result'], 'ok')
self.assertIsNotNone(result['template'])

def test_create_template_integrity_errors(self):
with patch('DeviceManager.TemplateHandler.init_tenant_context') as mock_init_tenant_context:
mock_init_tenant_context.return_value = 'admin'
with patch('DeviceManager.TemplateHandler.db') as db_mock:
diag = Exception()
diag.message_primary = 'test'
orig = Exception()
orig.diag = diag

db_mock.session = AlchemyMagicMock()
db_mock.session.commit = Mock()
db_mock.session.commit.side_effect = IntegrityError(statement='test', params='test', orig=orig)

token = generate_token()

data = """{
"label": "SensorModel",
"attrs": [
{
"label": "temperature",
"type": "dynamic",
"value_type": "float"
},
{
"label": "model-id",
"type": "static",
"value_type": "string",
"static_value": "model-001"
}
]
}"""

params_query = {'content_type': 'application/json', 'data': data}

with self.assertRaises(HTTPRequestError):
TemplateHandler.create_template(params_query, token)

@patch('DeviceManager.TemplateHandler.db')
def test_get_template(self, db_mock):
db_mock.session = AlchemyMagicMock()
Expand Down

0 comments on commit 0a64333

Please sign in to comment.