from django.test import TestCase from datetime import datetime, timedelta import random import json import zmq from django.utils.timezone import make_aware, utc, now from pytz import AmbiguousTimeError import re import time fake_zmq_socket = None class FakeZMQContext(object): def socket(self, socket_type): global fake_zmq_socket fake_zmq_socket = FakeZMQSocket(socket_type) return fake_zmq_socket class FakeZMQSocket(object): def __init__(self, socket_type): self._type = socket_type self.sent_msgs = {} def bind(self, *args, **kwargs): pass def connect(self, *args, **kwargs): pass def send(self, msg): topic, _, msg = msg.partition(' ') if topic not in self.sent_msgs: self.sent_msgs[topic] = [] self.sent_msgs[topic].append(json.loads(msg)) def send_string(self, msg): self.send(msg) def clear(self): self.sent_msgs = {} # monkey patch so we can check what messages the API is sending # anything that imports chain.api should come after this zmq.Context = FakeZMQContext from chain.core.models import Unit, Metric, Device, ScalarSensor, Site, \ PresenceSensor, Person, Metadata from chain.core.models import GeoLocation from chain.core.resources import DeviceResource from chain.core.api import HTTP_STATUS_SUCCESS, HTTP_STATUS_CREATED from chain.core.hal import HALDoc from chain.core import resources from chain.localsettings import INFLUX_HOST, INFLUX_PORT, INFLUX_MEASUREMENT from chain.influx_client import InfluxClient from chain.core.chaintestcase import ChainTestCase, BASE_API_URL, ACCEPT_TAIL resources.influx_client = InfluxClient(INFLUX_HOST, INFLUX_PORT, 'test', INFLUX_MEASUREMENT) HTTP_STATUS_NOT_ACCEPTABLE = 406 HTTP_STATUS_NOT_FOUND = 404 HTTP_STATUS_BAD_REQUEST = 400 SCALAR_DATA_URL = BASE_API_URL + 'scalar_data/' SITES_URL = BASE_API_URL + 'sites/' def obj_from_filled_schema(schema): '''Creates an object corresponding to the default values provided with a form schema''' obj = {} for k, v in schema['properties'].iteritems(): if v['type'] == 'object': subobj = obj_from_filled_schema(v) if subobj: obj[k] = subobj elif 'default' in v: obj[k] = v['default'] return obj class HalTests(TestCase): def setUp(self): self.test_doc = { '_links': {'self': {'href': 'http://example.com'}}, 'attr1': 1, 'attr2': 2 } def test_basic_attrs_are_available(self): haldoc = HALDoc(self.test_doc) self.assertEqual(haldoc.attr1, self.test_doc['attr1']) self.assertEqual(haldoc.attr2, self.test_doc['attr2']) def test_links_is_an_attr_without_underscore(self): haldoc = HALDoc(self.test_doc) self.assertIn('self', haldoc.links) def test_link_href_is_available_as_attr(self): haldoc = HALDoc(self.test_doc) self.assertEqual(haldoc.links.self.href, self.test_doc['_links']['self']['href']) def test_exception_raised_if_no_href_in_link(self): no_href = {'_links': {'nohref': {'title': 'A Title'}}} with self.assertRaises(ValueError): HALDoc(no_href) def test_links_should_allow_lists(self): doc = { '_links': { 'self': {'href': 'http://example.com'}, 'children': [ {'href': 'http://example.com/children/1'}, {'href': 'http://example.com/children/2'} ] } } haldoc = HALDoc(doc) self.assertEquals(haldoc.links.children[0].href, doc['_links']['children'][0]['href']) self.assertEquals(haldoc.links.children[1].href, doc['_links']['children'][1]['href']) class ScalarSensorDataTest(ChainTestCase): def test_data_can_be_added(self): data = { 'sensor': self.sensors[0], 'value': 25, 'timestamp': now() } resources.influx_client.post_data(data['sensor'].device.site.id, data['sensor'].device.id, data['sensor'].id, data['sensor'].metric, data['value'], data['timestamp']) self.assertEqual(data['value'], 25) class BasicHALJSONTests(ChainTestCase): def test_response_with_accept_hal_json_should_return_hal_json(self): response = self.client.get(BASE_API_URL, HTTP_ACCEPT='application/hal+json') self.assertEqual(response.status_code, HTTP_STATUS_SUCCESS) self.assertEqual(response['Content-Type'], 'application/hal+json') class CacheTests(ChainTestCase): def test_site_is_not_cached(self): site = self.get_a_site(should_cache=False) def test_device_is_not_cached(self): device = self.get_a_device(should_cache=False) def test_site_summary_is_cached(self): site = self.get_a_site() summary = self.get_resource(site.links['ch:siteSummary'].href, should_cache=True) class DefaultMIMETests(ChainTestCase): def test_root_should_supply_json_if_no_accept_header(self): data = self.get_resource(BASE_API_URL) sites_coll = data.links['ch:sites'] response = self.client.get(sites_coll.href, HTTP_HOST='localhost') self.assertEqual(response.status_code, HTTP_STATUS_SUCCESS) self.assertEqual(response['Content-Type'], "application/json") class SafePostTests(ChainTestCase): def test_lack_of_json_data_in_edit_should_not_crash_server(self): site = self.get_a_site() edit_href = site.links.editForm.href edit_form = self.get_resource(edit_href) new_site = obj_from_filled_schema(edit_form) new_site['name'] = 'Some New Name' new_site['rawZMQStream'] = 'tcp://newexample.com:7162' mime_type = 'application/hal+json' accept_header = mime_type + ',' + ACCEPT_TAIL response = None try: response = self.client.post(edit_href, "", content_type=mime_type, HTTP_ACCEPT=accept_header, HTTP_HOST='localhost') except ValueError: self.assertTrue(False) # lack of JSON crashed the server self.assertEqual(response.status_code, HTTP_STATUS_BAD_REQUEST) self.assertEqual(response['Content-Type'], "application/json") def test_lack_of_json_data_in_create_should_not_crash_server(self): sites = self.get_sites() mime_type = 'application/hal+json' accept_header = mime_type + ',' + ACCEPT_TAIL response = None try: response = self.client.post(sites.links.createForm.href, "bad json", content_type=mime_type, HTTP_ACCEPT=accept_header, HTTP_HOST='localhost') except ValueError: self.assertTrue(False) # lack of JSON crashed the server self.assertEqual(response.status_code, HTTP_STATUS_BAD_REQUEST) self.assertEqual(response['Content-Type'], "application/json") def test_ambiguous_timestamps_should_not_crash_server(self): sensor = self.get_a_sensor() sensor_data = self.get_resource( sensor.links['ch:dataHistory'].href) data_url = sensor_data.links.createForm.href data = { 'value': 20, 'timestamp': datetime(2015, 11, 1, 1, 0, 0).isoformat() } mime_type = 'application/hal+json' accept_header = mime_type + ',' + ACCEPT_TAIL try: response = self.client.post(data_url, json.dumps(data), content_type=mime_type, HTTP_ACCEPT=accept_header, HTTP_HOST='localhost') except AmbiguousTimeError: self.assertTrue(False) self.assertEqual(response.status_code, HTTP_STATUS_BAD_REQUEST) self.assertEqual(response['Content-Type'], "application/json") class ApiRootTests(ChainTestCase): def test_root_should_have_self_rel(self): root = self.get_resource(BASE_API_URL, mime_type='application/hal+json') self.assertIn('self', root.links) self.assertIn('href', root.links.self) def test_root_should_have_curies_link(self): data = self.get_resource(BASE_API_URL) curies = data.links.curies self.assertEqual(curies[0].name, 'ch') self.assertRegexpMatches(curies[0].href, 'http://.*') def test_root_should_have_sites_link(self): data = self.get_resource(BASE_API_URL) sites_coll = data.links['ch:sites'] self.assertRegexpMatches(sites_coll.href, 'http://.*' + SITES_URL) class ApiSitesTests(ChainTestCase): def test_nonexistant_site_should_return_404(self): sites = self.get_sites() non_existant_site_url = sites.links.items[0].href site_urls = set((item.href for item in sites.links.items)) while non_existant_site_url in site_urls: non_existant_site_url += str(random.randint(100, 999)) self.get_resource(non_existant_site_url, expect_status_code=HTTP_STATUS_NOT_FOUND, \ check_mime_type=False, check_vary_header=False) def test_sites_coll_should_have_self_rel(self): sites = self.get_sites() self.assertIn('href', sites.links.self) def test_site_should_have_curies_link(self): site = self.get_a_site() curies = site.links.curies self.assertEqual(curies[0].name, 'ch') self.assertRegexpMatches(curies[0].href, 'http://.*') def test_sites_coll_should_have_curies_link(self): sites = self.get_sites() curies = sites.links.curies self.assertEqual(curies[0].name, 'ch') self.assertRegexpMatches(curies[0].href, 'http://.*') def test_sites_should_have_createform_link(self): sites = self.get_sites() self.assertIn('createForm', sites.links) self.assertIn('href', sites.links.createForm) self.assertEqual(sites.links.createForm.title, 'Create Site') def test_sites_should_have_items_link(self): sites = self.get_sites() self.assertIn('items', sites.links) def test_sites_links_should_have_title(self): sites = self.get_sites() self.assertIn(sites.links.items[0].title, [s.name for s in self.sites]) def test_sites_collection_should_have_total_count(self): sites = self.get_sites() self.assertEqual(sites.totalCount, len(self.sites)) def test_site_should_have_self_link(self): site = self.get_a_site() self.assertIn('href', site.links.self) def test_site_should_have_stream_href(self): site = self.get_a_site() stream_href = site.links['ch:websocketStream'].href self.assertIn('ws://', stream_href) def test_site_should_have_name(self): site = self.get_a_site() self.assertIn(site.name, [s.name for s in self.sites]) def test_site_should_have_devices_link(self): site = self.get_a_site() self.assertIn('ch:devices', site.links) self.assertIn('href', site.links['ch:devices']) devices = self.get_resource(site.links['ch:devices'].href) db_site = Site.objects.get(name=site.name) self.assertEqual(devices.totalCount, db_site.devices.count()) def test_site_should_have_geolocation(self): site = self.get_a_site() self.assertIn('geoLocation', site) self.assertIn('elevation', site.geoLocation) self.assertIn(site.geoLocation['elevation'], [l.elevation for l in self.geo_locations]) self.assertIn('latitude', site.geoLocation) self.assertIn(site.geoLocation['latitude'], [l.latitude for l in self.geo_locations]) self.assertIn('longitude', site.geoLocation) self.assertIn(site.geoLocation['longitude'], [l.longitude for l in self.geo_locations]) def test_site_should_have_tidmarsh_zmq_link(self): site = self.get_a_site() self.assertIn('rawZMQStream', site.links) self.assertIn('href', site.links.rawZMQStream) def test_sites_should_be_postable(self): new_site = { 'geoLocation': { 'latitude': 42.360461, 'longitude': -71.087347, 'elevation': 12 }, 'name': 'MIT Media Lab', 'rawZMQStream': 'tcp://example.com:8372' } sites = self.get_sites() response = self.create_resource(sites.links.createForm.href, new_site) db_obj = Site.objects.get(name='MIT Media Lab') self.assertEqual(new_site['name'], response.name) self.assertEqual(new_site['name'], db_obj.name) self.assertEqual(new_site['name'], response.links['self'].title) self.assertEqual(new_site['rawZMQStream'], response.links.rawZMQStream.href) self.assertEqual(new_site['rawZMQStream'], db_obj.raw_zmq_stream) for field in ['latitude', 'longitude', 'elevation']: self.assertEqual(new_site['geoLocation'][field], response.geoLocation[field]) self.assertEqual(new_site['geoLocation'][field], getattr(db_obj.geo_location, field)) def test_site_create_form_should_return_schema(self): sites = self.get_sites() site_schema = self.get_resource(sites.links.createForm.href) self.assertIn('type', site_schema) self.assertEquals(site_schema['type'], 'object') self.assertIn('properties', site_schema) self.assertIn('name', site_schema['properties']) self.assertEquals(site_schema['properties']['name'], {'type': 'string', 'title': 'name', 'minLength': 1}) self.assertIn('rawZMQStream', site_schema['properties']) self.assertEquals(site_schema['properties']['rawZMQStream'], {'type': 'string', 'format': 'uri', 'title': 'rawZMQStream'}) self.assertIn('required', site_schema) self.assertEquals(site_schema['required'], ['name']) def test_site_schema_should_include_geolocation(self): sites = self.get_sites() site_schema = self.get_resource(sites.links.createForm.href) self.assertIn('properties', site_schema) self.assertIn('geoLocation', site_schema['properties']) self.assertEquals(site_schema['properties']['geoLocation'], { 'type': 'object', 'title': 'geoLocation', 'properties': { 'latitude': {'type': 'number', 'title': 'latitude'}, 'longitude': {'type': 'number', 'title': 'longitude'}, 'elevation': {'type': 'number', 'title': 'elevation'} }, 'required': ['latitude', 'longitude'] }) def test_site_should_have_edit_link(self): site = self.get_a_site() self.assertIn('editForm', site.links) def test_site_edit_view_should_have_schema_with_defaults(self): site = self.get_a_site() edit_form = self.get_resource(site.links.editForm.href) self.assertEquals(edit_form['type'], 'object') self.assertEquals(edit_form['properties']['name']['default'], site.name) self.assertEquals(edit_form['properties']['rawZMQStream']['default'], site.links.rawZMQStream.href) edit_loc = edit_form['properties']['geoLocation'] self.assertEquals(edit_loc['properties']['latitude']['default'], site.geoLocation['latitude']) self.assertEquals(edit_loc['properties']['longitude']['default'], site.geoLocation['longitude']) def test_sites_should_be_editable(self): site = self.get_a_site() edit_href = site.links.editForm.href edit_form = self.get_resource(edit_href) new_site = obj_from_filled_schema(edit_form) new_site['name'] = 'Some New Name' new_site['rawZMQStream'] = 'tcp://newexample.com:7162' response = self.update_resource(edit_href, new_site) reget = self.get_resource(site.links.self.href) self.assertEqual(response.name, new_site['name']) self.assertEqual(response.geoLocation['latitude'], site.geoLocation['latitude']) self.assertEqual(response.links.rawZMQStream.href, new_site['rawZMQStream']) self.assertEqual(response, reget) def test_geolocation_should_be_addable_to_site(self): site = { 'name': 'Geolocate Add Test', } sites = self.get_sites() site_response = self.create_resource(sites.links.createForm.href, site) edit_form = self.get_resource(site_response.links.editForm.href) new_site = obj_from_filled_schema(edit_form) new_site['geoLocation'] = { 'latitude': 42.360461, 'longitude': -71.087347 } self.update_resource(site_response.links.editForm.href, new_site) new_site_response = self.get_resource(site_response.links.self.href) self.assertEqual(new_site_response.geoLocation['latitude'], new_site['geoLocation']['latitude']) self.assertEqual(new_site_response.geoLocation['longitude'], new_site['geoLocation']['longitude']) def test_site_should_have_summary_link(self): site = self.get_a_site() self.assertIn('ch:siteSummary', site.links) def test_site_summary_should_have_devices(self): site = self.get_a_site() device = self.get_a_device() summary = self.get_resource(site.links['ch:siteSummary'].href) self.assertIn(device.name, [dev['name'] for dev in summary.devices]) def test_site_summary_devices_should_not_have_rels(self): site = self.get_a_site() summary = self.get_resource(site.links['ch:siteSummary'].href) summary_dev = summary.devices[0] self.assertNotIn('_links', summary_dev) self.assertNotIn('_embedded', summary_dev) def test_site_summary_should_have_sensors(self): site = self.get_a_site() summary = self.get_resource(site.links['ch:siteSummary'].href) summary_dev = summary.devices[0] self.assertIn('metric', summary_dev['sensors'][0]) def test_site_summary_should_have_empty_data(self): site = self.get_a_site() summary = self.get_resource(site.links['ch:siteSummary'].href) summary_dev = summary.devices[0] self.assertEqual(0, len(summary_dev['sensors'][0]['data'])) def test_site_summary_resources_should_have_href(self): site = self.get_a_site() summary = self.get_resource(site.links['ch:siteSummary'].href) summary_dev = summary.devices[0] self.assertIn('href', summary_dev) self.assertIn('href', summary_dev['sensors'][0]) class ApiDeviceTests(ChainTestCase): def test_nonexistant_device_should_return_404(self): devices = self.get_devices() self.get_resource(devices.links.items[0].href \ + "NONEXISTANT_RESOURCE", expect_status_code=HTTP_STATUS_NOT_FOUND, \ check_mime_type=False, check_vary_header=False) def test_device_should_have_sensors_link(self): device = self.get_a_device() self.assertIn('ch:sensors', device.links) self.assertEqual('Sensors', device.links['ch:sensors'].title) def test_device_should_have_site_link(self): device = self.get_a_device() self.assertIn('ch:site', device.links) def test_device_should_have_curies_link(self): device = self.get_a_device() curies = device.links.curies self.assertEqual(curies[0].name, 'ch') self.assertRegexpMatches(curies[0].href, 'http://.*') def test_devices_coll_should_have_curies_link(self): devices = self.get_devices() curies = devices.links.curies self.assertEqual(curies[0].name, 'ch') self.assertRegexpMatches(curies[0].href, 'http://.*') def test_device_should_be_postable_to_a_site(self): site = self.get_a_site() devices = self.get_resource(site.links['ch:devices'].href) dev_url = devices.links.createForm.href new_device = { "building": "E14", "description": "A great device", "floor": "5", "name": "Unit Test Thermostat 42", "room": "E14-548R" } response = self.create_resource(dev_url, new_device) self.assertEqual(new_device['name'], response.links['self'].title) # make sure that a device now exists with the right name db_device = Device.objects.get(name=new_device['name']) # make sure that the device is set up in the right site db_site = Site.objects.get(name=site['name']) self.assertEqual(db_device.site, db_site) def test_device_create_form_should_return_schema(self): devices = self.get_devices() device_schema = self.get_resource(devices.links.createForm.href) self.assertIn('type', device_schema) self.assertEquals(device_schema['type'], 'object') self.assertIn('properties', device_schema) self.assertIn('name', device_schema['properties']) self.assertEquals(device_schema['properties']['name'], {'type': 'string', 'title': 'name', 'minLength': 1}) for field_name in ['description', 'building', 'floor', 'room']: self.assertIn(field_name, device_schema['properties']) self.assertEquals(device_schema['properties'][field_name], {'type': 'string', 'title': field_name}) self.assertIn('required', device_schema) self.assertEquals(device_schema['required'], ['name']) def test_device_schema_should_include_geolocation(self): devices = self.get_devices() device_schema = self.get_resource(devices.links.createForm.href) self.assertIn('properties', device_schema) self.assertIn('geoLocation', device_schema['properties']) self.assertEquals(device_schema['properties']['geoLocation'], { 'type': 'object', 'title': 'geoLocation', 'properties': { 'latitude': {'type': 'number', 'title': 'latitude'}, 'longitude': {'type': 'number', 'title': 'longitude'}, 'elevation': {'type': 'number', 'title': 'elevation'} }, 'required': ['latitude', 'longitude'] }) def test_posting_device_should_send_zmq_msgs(self): fake_zmq_socket.clear() site = self.get_a_site() devices = self.get_resource(site.links['ch:devices'].href) dev_url = devices.links.createForm.href new_device = {"name": "Unit Test Thermostat 42"} self.create_resource(dev_url, new_device) db_device = Device.objects.get(name=new_device['name']) # make sure that a message got sent to all the appropriate tags stream_tags = [ 'site-%d' % db_device.site_id, 'device-%d' % db_device.id ] for tag in stream_tags: self.assertEqual(1, len(fake_zmq_socket.sent_msgs[tag])) self.assertEqual(new_device['name'], fake_zmq_socket.sent_msgs[tag][0]['name']) def test_device_should_be_deactivatable(self): device = self.get_a_device() edit_href = device.links.editForm.href device['active'] = False response = self.update_resource(edit_href, device) self.assertEqual(response.active, device['active']) class ApiScalarSensorTests(ChainTestCase): def test_sensors_should_be_postable_to_existing_device(self): device = self.get_a_device() sensors = self.get_resource(device.links['ch:sensors'].href) sensor_url = sensors.links['createForm'].href new_sensor = { 'metric': 'Bridge Length', 'unit': 'Smoots', } self.create_resource(sensor_url, new_sensor) db_sensor = ScalarSensor.objects.get(metric__name='Bridge Length', device__name=device.name) self.assertEqual('Smoots', db_sensor.unit.name) def test_sensors_should_be_postable_to_newly_posted_device(self): site = self.get_a_site() devices = self.get_resource(site.links['ch:devices'].href) new_device = { "building": "E14", "description": "A great device", "floor": "5", "name": "Unit Test Thermostat 49382", "room": "E14-548R" } device = self.create_resource(devices.links['createForm'].href, new_device) sensors = self.get_resource(device.links['ch:sensors'].href) new_sensor = { 'metric': 'Beauty', 'unit': 'millihelen', } response = self.create_resource(sensors.links['createForm'].href, new_sensor) db_sensor = ScalarSensor.objects.get(metric__name='Beauty', device__name=device.name) self.assertEqual('millihelen', db_sensor.unit.name) self.assertEqual('Beauty', response.links['self'].title) def test_sensor_should_have_data_url(self): sensor = self.get_a_sensor() self.assertIn('ch:dataHistory', sensor.links) def test_sensor_should_have_parent_link(self): sensor = self.get_a_sensor() self.assertIn('ch:device', sensor.links) def test_sensor_should_have_value_and_timestamp(self): sensor = self.get_a_sensor() self.assertIn('value', sensor) self.assertIn('updated', sensor) def test_sensor_should_have_float_datatype(self): sensor = self.get_a_sensor() self.assertIn('dataType', sensor) self.assertEquals(sensor.dataType, 'float') def test_sensor_should_be_editable(self): sensor = self.get_a_sensor() edit_href = sensor.links.editForm.href edit_form = self.get_resource(edit_href) new_sensor = obj_from_filled_schema(edit_form) new_sensor['metric'] = 'fuzziness' response = self.update_resource(edit_href, new_sensor) self.assertEqual(response.metric, new_sensor['metric']) self.assertEqual(response.unit, new_sensor['unit']) def test_sensor_should_be_deactivatable(self): sensor = self.get_a_sensor() edit_href = sensor.links.editForm.href sensor['active'] = False response = self.update_resource(edit_href, sensor) self.assertEqual(response.active, sensor['active']) # class ApiPresenceSensorTests(ChainTestCase): # def test_presence_sensors_should_be_postable_to_existing_device(self): # device = self.get_a_device() # sensors = self.get_resource(device.links['ch:sensors'].href) # sensor_url = sensors.links['createForm'].href # # new_sensor = { # 'sensor-type': 'presence', # 'metric': 'rfid', # 'unit': 'N/A', # } # new_sensor_res = self.create_resource(sensor_url, new_sensor) # new_sensor_link = new_sensor_res['_links']['self']['href'] # db_sensor = PresenceSensor.objects.get(metric__name='rfid', # device__name=device.name) # self.assertTrue(db_sensor is not None) # self.assertEqual(new_sensor_res.links['self'].title, new_sensor['metric']) # # # Reload the list of sensors: # sensors = self.get_resource(device.links['ch:sensors'].href) # # # Check to see if new sensor included in the list: # found_self = False # for link in sensors.links['items']: # if link['href'] == new_sensor_link: # found_self = True # break # self.assertTrue(found_self) # # def test_presence_sensors_should_be_postable_to_newly_posted_device(self): # site = self.get_a_site() # devices = self.get_resource(site.links['ch:devices'].href) # # new_device = { # "building": "E14", # "description": "Another great device", # "floor": "5", # "name": "Unit Test Presence Sensor 49382", # "room": "E14-548R" # } # device = self.create_resource(devices.links['createForm'].href, # new_device) # # sensors = self.get_resource(device.links['ch:sensors'].href) # sensor_url = sensors.links['createForm'].href # new_sensor = { # 'sensor-type': 'presence', # 'metric': 'rfid', # 'unit': 'N/A', # } # new_sensor_res = self.create_resource(sensor_url, new_sensor) # new_sensor_link = new_sensor_res['_links']['self']['href'] # db_sensor = PresenceSensor.objects.get(metric__name='rfid', # device__name=device.name) # self.assertTrue(db_sensor is not None) # # # Reload the list of sensors: # sensors = self.get_resource(device.links['ch:sensors'].href) # # # Check to see if new sensor included in the list: # found_self = False # for link in sensors.links['items']: # if link['href'] == new_sensor_link: # found_self = True # break # self.assertTrue(found_self) # # def test_presence_sensor_should_have_data_url(self): # sensor = self.get_a_sensor_of_type('presence') # self.assertTrue(sensor is not None) # self.assertIn('ch:dataHistory', sensor.links) # # def test_presence_sensor_should_have_parent_link(self): # sensor = self.get_a_sensor_of_type('presence') # self.assertTrue(sensor is not None) # self.assertIn('ch:device', sensor.links) # # '''def test_sensor_should_have_value_and_timestamp(self): # sensor = self.get_a_sensor_of_type('presence') # self.assertTrue(sensor is not None) # self.assertIn('value', sensor) # self.assertIn('updated', sensor)''' # # def test_presence_sensor_should_have_presence_datatype(self): # sensor = self.get_a_sensor_of_type('presence') # self.assertTrue(sensor is not None) # self.assertIn('dataType', sensor) # self.assertEquals(sensor.dataType, 'presence') # # def test_presence_sensor_should_be_editable(self): # sensor = self.get_a_sensor_of_type('presence') # self.assertTrue(sensor is not None) # edit_href = sensor.links.editForm.href # edit_form = self.get_resource(edit_href) # new_sensor = obj_from_filled_schema(edit_form) # new_sensor['unit'] = 'rfid2' # self.update_resource(edit_href, new_sensor) # # # class ApiPresenceSensorDataTests(ChainTestCase): # def setUp(self): # super(ApiPresenceSensorDataTests, self).setUp() # # make sure there's data in the first presence sensor # sensor = self.get_a_sensor_of_type('presence') # data = self.get_resource(sensor.links['ch:dataHistory'].href) # create_url = data.links['createForm'].href # new_data = { # 'present': True, # 'person': self.get_a_person().links['self'].href # } # self.create_resource(create_url, new_data) # # def test_presence_data_should_have_edit_form(self): # sensor = self.get_a_sensor_of_type('presence') # all_data = self.get_resource(sensor.links['ch:dataHistory'].href) # data = self.get_resource(all_data.links['items'][0].href) # edit_href = data.links['editForm'].href # self.get_resource(edit_href) # # def test_presence_data_should_be_editable(self): # sensor = self.get_a_sensor_of_type('presence') # all_data = self.get_resource(sensor.links['ch:dataHistory'].href) # data = self.get_resource(all_data.links['items'][0].href) # edit_href = data.links['editForm'].href # schema = self.get_resource(edit_href) # new_data = obj_from_filled_schema(schema) # new_data['present'] = not new_data['present'] # self.update_resource(edit_href, new_data) class ApiScalarSensorDataTests(ChainTestCase): def test_sensor_data_should_have_timestamp_and_value(self): sensor = self.get_a_sensor() sensor_data = self.get_resource( sensor.links['ch:dataHistory'].href) self.assertIn('timestamp', sensor_data.data[0]) self.assertIn('value', sensor_data.data[0]) def test_sensor_data_should_have_data_type(self): sensor = self.get_a_sensor() sensor_data = self.get_resource( sensor.links['ch:dataHistory'].href) self.assertIn('dataType', sensor_data) self.assertEqual('float', sensor_data.dataType) def test_sensor_data_should_be_postable(self): device = self.get_a_device() sensor = self.get_a_sensor() sensor_data = self.get_resource( sensor.links['ch:dataHistory'].href) data_url = sensor_data.links.createForm.href timestamp = make_aware(datetime(2013, 1, 1, 0, 0, 0), utc) data = { 'value': 23, 'timestamp': timestamp.isoformat() } sensor_id = re.search(r'[^=]*$', data_url).group(0) self.create_resource(data_url, data) filters = { 'sensor_id': sensor_id, 'timestamp__gte': timestamp, 'timestamp__lt': timestamp + timedelta(seconds=0.1) } db_data = resources.influx_client.get_sensor_data(filters)[0] self.assertEqual(db_data['value'], data['value']) self.assertEqual(db_data['metric'], ScalarSensor.objects.get(id=sensor_id).metric.name) def test_lists_of_sensor_data_should_be_postable(self): device = self.get_a_device() sensor = self.get_a_sensor() sensor_data = self.get_resource( sensor.links['ch:dataHistory'].href) data_url = sensor_data.links.createForm.href sensor_id = re.search(r'[^=]*$', data_url).group(0) basetime = make_aware(datetime(2013, 1, 1, 0, 0, 0), utc) timestamps = [basetime + timedelta(seconds=i) for i in range(0, 3)] values = range(0, 3) data = [{ 'value': value, 'timestamp': timestamp.isoformat() } for value, timestamp in zip(values, timestamps)] self.create_resource(data_url, data) filters = { 'sensor_id': sensor_id } for i in range(0, 3): filters['timestamp__gte'] = timestamps[i] filters['timestamp__lt'] = timestamps[i] + timedelta(seconds=0.1) db_data = resources.influx_client.get_sensor_data(filters)[0] self.assertEqual(db_data['value'], values[i]) self.assertEqual(db_data['metric'], ScalarSensor.objects.get(id=sensor_id).metric.name) def test_posting_data_should_send_zmq_msgs(self): fake_zmq_socket.clear() sensor = self.get_a_sensor() device = self.get_resource( sensor.links['ch:device'].href) db_sensor = ScalarSensor.objects.get( metric__name=sensor.metric, device__name=device.name) sensor_data = self.get_resource( sensor.links['ch:dataHistory'].href) data_url = sensor_data.links.createForm.href data = {'value': 23} self.create_resource(data_url, data) # make sure that a message got sent to all the appropriate tags stream_tags = [ 'site-%d' % db_sensor.device.site_id, 'device-%d' % db_sensor.device_id, 'sensor-%d' % db_sensor.id ] for tag in stream_tags: self.assertEqual(1, len(fake_zmq_socket.sent_msgs[tag])) self.assertEqual(data['value'], fake_zmq_socket.sent_msgs[tag][0]['value']) self.assertEqual(sensor.links['self'].href, fake_zmq_socket.sent_msgs[tag][0]['_links']['ch:sensor']['href']) def test_posting_data_should_sanitize_args_for_response(self): fake_zmq_socket.clear() sensor = self.get_a_sensor() sensor_data = self.get_resource( sensor.links['ch:dataHistory'].href) data_url = sensor_data.links.createForm.href data = {'value': "23"} response = self.create_resource(data_url, data) self.assertEqual(response.value, 23.0) self.assertEqual(type(response.value), float) def test_collection_links_should_not_have_page_info(self): # we want to allow the server to just give the default pagination when # the client is just following links around sensor = self.get_a_sensor() self.assertNotIn('offset', sensor.links['ch:dataHistory'].href) self.assertNotIn('limit', sensor.links['ch:dataHistory'].href) def test_paginated_data_can_be_requested_with_only_limit(self): site = self.get_a_site() db_site = Site.objects.get(name=site.name) Device.objects.bulk_create( [Device(name="Test ScalarSensor %d" % i, site=db_site) for i in range(1500)]) datapage = self.get_resource( site.links["ch:devices"].href + "&limit=20") self.assertEqual(20, len(datapage.links['items'])) datapage = self.get_resource( site.links["ch:devices"].href + "&limit=1000") self.assertEqual(1000, len(datapage.links['items'])) def test_sensor_data_timestamp_edge_cases(self): sensor = self.get_a_sensor() self.get_resource( sensor.links['ch:dataHistory'].href + "×tamp__gte=NaN×tamp__lt=NaN", expect_status_code=HTTP_STATUS_BAD_REQUEST, check_mime_type=False, check_vary_header=False) self.get_resource( sensor.links['ch:dataHistory'].href + "×tamp__gte=NaN", expect_status_code=HTTP_STATUS_BAD_REQUEST, check_mime_type=False, check_vary_header=False) self.get_resource( sensor.links['ch:dataHistory'].href + "×tamp__lt=NaN", expect_status_code=HTTP_STATUS_BAD_REQUEST, check_mime_type=False, check_vary_header=False) self.get_resource( sensor.links['ch:dataHistory'].href + "×tamp__lt=TestingBadInput", expect_status_code=HTTP_STATUS_BAD_REQUEST, check_mime_type=False, check_vary_header=False) class ApiAggregateScalarSensorDataTests(ChainTestCase): def setUp(self): super(ApiAggregateScalarSensorDataTests, self).setUp() time_end = now() + timedelta(days=1) time_begin = time_end - timedelta(days=7) time_end = time_end.strftime("%Y-%m-%d") time_begin = time_begin.strftime("%Y-%m-%d") resources.influx_client.post('query', ''' SELECT max("value"), min("value"), mean("value"), count("value"), sum("value") INTO "{0}" FROM "{1}" WHERE "time" < '{2}' AND "time" >= '{3}' GROUP BY "sensor_id", time(1h), *'''.format(INFLUX_MEASUREMENT + '_1h', INFLUX_MEASUREMENT, time_end, time_begin), True) resources.influx_client.post('query', ''' SELECT max("max"), min("min"), sum("sum")/sum("count") as "mean", sum("count") as "count", sum("sum") INTO "{0}" FROM "{1}" WHERE "time" < '{2}' AND "time" >= '{3}' GROUP BY "sensor_id", time(1d), *'''.format(INFLUX_MEASUREMENT + '_1d', INFLUX_MEASUREMENT + '_1h', time_end, time_begin), True) resources.influx_client.post('query', ''' SELECT max("max"), min("min"), sum("sum")/sum("count") as "mean", sum("count") as "count", sum("sum") INTO "{0}" FROM "{1}" WHERE "time" < '{2}' AND "time" >= '{3}' GROUP BY "sensor_id", time(1w), *'''.format(INFLUX_MEASUREMENT + '_1w', INFLUX_MEASUREMENT + '_1d', time_end, time_begin), True) def test_aggregate_sensor_data_query_should_include_argument(self): sensor = self.get_a_sensor() self.get_resource( sensor.links['ch:aggregateData'].href, expect_status_code=HTTP_STATUS_BAD_REQUEST, check_mime_type=False, check_vary_header=False) def test_aggregate_sensor_data_should_have_data_type(self): sensor = self.get_a_sensor() sensor_data = self.get_resource( sensor.links['ch:aggregateData'].href.replace('{&aggtime}', '&aggtime=1h')) self.assertIn('dataType', sensor_data) self.assertEqual('float', sensor_data.dataType) def test_aggregate_sensor_data_should_have_timestamp_and_statistics(self): sensor = self.get_a_sensor() href = sensor.links['ch:aggregateData'].href params = ['1h', '1d', '1w'] time_end = now() + timedelta(days=1) time_begin = time_end - timedelta(days=9) time_end = time.mktime(time_end.timetuple()) time_begin = time.mktime(time_begin.timetuple()) for param in params: # make sure there is data sensor_data = self.get_resource( href.replace('{&aggtime}', '&aggtime=' + param) + '×tamp__gte={0}×tamp__lt={1}'.format(time_begin, time_end)) self.assertIn('timestamp', sensor_data.data[0]) self.assertIn('min', sensor_data.data[0]) self.assertIn('max', sensor_data.data[0]) self.assertIn('mean', sensor_data.data[0]) self.assertIn('count', sensor_data.data[0]) def test_aggregate_sensor_data_invalid_arguments(self): sensor = self.get_a_sensor() href = sensor.links['ch:aggregateData'].href self.get_resource( href.replace('{&aggtime}', '&aggtime=1s'), expect_status_code=HTTP_STATUS_BAD_REQUEST, check_mime_type=False, check_vary_header=False) class ApiMetadataTests(ChainTestCase): def test_site_device_sensor_should_have_metadata_link(self): resources = self.get_site_device_sensor() for resource in resources: self.assertIn('ch:metadata', resource.links) self.assertEqual('Metadata', resource.links['ch:metadata'].title) def test_metadata_should_be_postable_to_site_device_resource(self): resources = self.get_site_device_sensor() for resource in resources: metadata = self.get_resource(resource.links['ch:metadata'].href) metadata_url = metadata.links.createForm.href new_metadata = { "key": "Test", "value": "Unit Test Metadata", "timestamp": now().isoformat() } self.create_resource(metadata_url, new_metadata) # ids contain object_id and type_id ids = re.findall('\w+=(\d+)', metadata_url) db_metadata = Metadata.objects.get(content_type__pk=ids[0], object_id=ids[1], value=new_metadata['value']) self.assertEqual(db_metadata.value, new_metadata['value']) self.assertEqual(db_metadata.key, new_metadata['key']) def test_posting_metadata_should_sanitize_args_for_response(self): resources = self.get_site_device_sensor() for resource in resources: metadata = self.get_resource(resource.links['ch:metadata'].href) metadata_url = metadata.links.createForm.href new_metadata = { "key": "Test", "value": 123, "timestamp": now().isoformat() } response = self.create_resource(metadata_url, new_metadata) self.assertEqual(type(response.value), unicode) self.assertEqual(response.value, '123') def test_metadata_query_should_return_most_recent_key_value(self): device = self.get_a_device() metadata = self.get_resource(device.links['ch:metadata'].href) metadata_url = metadata.links.createForm.href new_time = now() old_time = new_time - timedelta(minutes=2) new_metadata = [ { "key": "Test", "value": "Old Metadata", "timestamp": old_time.isoformat() }, { "key": "Test", "value": "New Metadata", "timestamp": new_time.isoformat() } ] self.create_resource(metadata_url, new_metadata) # query again metadata = self.get_resource(device.links['ch:metadata'].href) self.assertIn('data', metadata) self.assertEqual(type(metadata.data), list) self.assertGreater(len(metadata.data), 0) data_found = False for data in metadata.data: if data['key'] == 'Test': self.assertEqual(data['value'], 'New Metadata') data_found = True self.assertTrue(data_found) def test_metadata_should_be_immutable(self): device = self.get_a_device() metadata = self.get_resource(device.links['ch:metadata'].href) metadata_url = metadata.links.createForm.href new_metadata = { "key": "Test edit", "value": 123, "timestamp": now().isoformat() } response = self.create_resource(metadata_url, new_metadata) metadata_id = re.search(r'(\d+)$', response.links.self.href).group(0) edit_url = BASE_API_URL + 'metadata/' + metadata_id + '/edit' mime_type = 'application/hal+json' accept_header = mime_type + ',' + ACCEPT_TAIL response = None try: response = self.client.post(edit_url, new_metadata, content_type=mime_type, HTTP_ACCEPT=accept_header, HTTP_HOST='localhost') except: self.assertTrue(False) self.assertEqual(response.status_code, HTTP_STATUS_BAD_REQUEST) self.assertEqual(response['Content-Type'], "application/json") # these tests are testing specific URL conventions within this application class CollectionFilteringTests(ChainTestCase): def test_devices_can_be_filtered_by_site(self): full_devices_coll = self.get_resource(BASE_API_URL + 'devices/') filtered_devices_coll = self.get_resource( BASE_API_URL + 'devices/?site=%d' % self.sites[0].id) self.assertEqual(len(full_devices_coll.links.items), len(self.devices)) self.assertEqual(len(filtered_devices_coll.links.items), len([d for d in self.devices if d.site==self.sites[0]])) def test_filtered_collection_has_filtered_url(self): site_id = self.sites[0].id coll = self.get_resource( BASE_API_URL + 'devices/?site=%d' % site_id) self.assertTrue(('site=%d' % site_id) in coll.links.self.href) def test_device_collections_should_limit_to_default_page_size(self): site = self.get_a_site() devices = self.get_resource(site.links['ch:devices'].href) create_url = devices.links['createForm'].href # make sure we create more devices than will fit on a page for i in range(0, DeviceResource.page_size + 1): dev = {'name': 'test dev %d' % i} self.create_resource(create_url, dev) devs = self.get_resource(BASE_API_URL + 'devices/') self.assertEqual(len(devs.links.items), DeviceResource.page_size) def test_pages_should_have_next_and_prev_links(self): site = self.get_a_site() devices = self.get_resource(site.links['ch:devices'].href) create_url = devices.links['createForm'].href # make sure we create more devices than will fit on a page for i in range(0, DeviceResource.page_size + 1): dev = {'name': 'test dev %d' % i} self.create_resource(create_url, dev) devs = self.get_resource(site.links['ch:devices'].href) self.assertIn('next', devs.links) self.assertNotIn('previous', devs.links) next_devs = self.get_resource(devs.links.next.href) self.assertIn('previous', next_devs.links) self.assertNotIn('next', next_devs.links) class HTMLTests(ChainTestCase): def test_root_request_accepting_html_gets_it(self): res = self.get_resource(BASE_API_URL, mime_type='text/html').strip() # check that it startswith a doctype self.assertTrue(res.startswith("<!DOCTYPE html")) self.assertTrue(res.endswith("</html>")) class ErrorTests(TestCase): def test_unsupported_mime_types_should_return_406_status(self): response = self.client.get(BASE_API_URL, HTTP_ACCEPT='foobar') self.assertEqual(response.status_code, HTTP_STATUS_NOT_ACCEPTABLE) self.assertEqual(response['Content-Type'], 'application/hal+json') self.assertIn('message', json.loads(response.content)) def test_if_client_accepts_wildcard_send_hal_json(self): response = self.client.get(BASE_API_URL, HTTP_ACCEPT='foobar, */*') self.assertEqual(response.status_code, HTTP_STATUS_SUCCESS) self.assertEqual(response['Content-Type'], 'application/hal+json') def test_bad_url_returns_404(self): response = self.client.get('/foobar/', HTTP_ACCEPT='application/json') self.assertEqual(response.status_code, HTTP_STATUS_NOT_FOUND) self.assertEqual(response['Content-Type'], 'application/json') self.assertIn('message', json.loads(response.content))