示例#1
0
class TestSites(TestCase, FixturesMixin):
    """Contains tests for the UserPortal 'sites' file."""

    render_templates = False
    db_cfg = config.get_config_context()['database']
    s_db_cfg = config.get_config_context()['s_database']
    TESTING = True

    # Fixtures are usually in warno-vagrant/data_store/data/WarnoConfig/fixtures
    fixtures = ['sites.yml']

    def setUp(self):
        db.create_all()

    def tearDown(self):
        db.session.remove()
        db.drop_all()

    def create_app(self):
        views.app.config['TESTING'] = True
        views.app.config['FIXTURES_DIRS'] = [os.environ.get('FIXTURES_DIR')]
        views.app.config[
            'SQLALCHEMY_DATABASE_URI'] = 'postgresql://%s:%s@%s:%s/%s' % (
                self.db_cfg['DB_USER'], self.s_db_cfg['DB_PASS'],
                self.db_cfg['DB_HOST'], self.db_cfg['DB_PORT'],
                self.db_cfg['TEST_DB_NAME'])

        FixturesMixin.init_app(views.app, db)

        return views.app

    @mock.patch('UserPortal.sites.current_user')
    def test_method_get_on_edit_site_returns_200_ok_and_passes_fixture_site_as_context_variable_using_correct_template(
            self, current_user, logger):
        """A 'GET' request to /sites/<site_id>/edit returns a response of '200' OK and passes the database information
        of the site with an id matching 'site_id' as a context variable to the 'edit_site.html' template."""
        # Should get database fixture site entry with id 2
        # Mocks an authorized user
        current_user.is_anonymous = False
        current_user.authorizations = "engineer"

        get_request_return = self.client.get('/sites/2/edit')
        self.assert200(get_request_return)

        context_site = self.get_context_variable('site')

        self.assertEqual(
            context_site['name_short'], 'TESTSIT2',
            "'TESTSIT2' is not in the 'name_short' field for the context variable site."
        )
        self.assertEqual(
            context_site['location_name'], 'Test Location 2',
            "'Test Location 2' is not in the 'location_name' field for the context variable site."
        )

        self.assert_template_used('edit_site.html')
示例#2
0
class TestUsers(TestCase, FixturesMixin):
    """Contains tests for the UserPortal 'users' file."""

    render_templates = False
    db_cfg = config.get_config_context()['database']
    s_db_cfg = config.get_config_context()['s_database']
    TESTING = True

    # Fixtures are usually in warno-vagrant/data_store/data/WarnoConfig/fixtures
    fixtures = ['users.yml']

    def setUp(self):
        db.create_all()

    def tearDown(self):
        db.session.remove()
        db.drop_all()

    def create_app(self):
        views.app.config['TESTING'] = True
        views.app.config['FIXTURES_DIRS'] = [os.environ.get('FIXTURES_DIR')]
        views.app.config[
            'SQLALCHEMY_DATABASE_URI'] = 'postgresql://%s:%s@%s:%s/%s' % (
                self.db_cfg['DB_USER'], self.s_db_cfg['DB_PASS'],
                self.db_cfg['DB_HOST'], self.db_cfg['DB_PORT'],
                self.db_cfg['TEST_DB_NAME'])

        FixturesMixin.init_app(views.app, db)

        return views.app

    @mock.patch("UserPortal.users.current_user")
    def test_method_get_on_edit_user_returns_200_ok_and_passes_fixture_user_using_correct_template(
            self, current_user, logger):
        """Method 'GET' on /users/<user_id>/edit gets the edit page for the user matching 'user_id'.  The user's
        database information is passed as a context variable to the template.  The response for the 'GET' request
        is '200' OK."""
        # Mocks an authorized user
        current_user.is_anonymous = False
        current_user.authorizations = "engineer"

        # Should get database fixture user entry with id 2
        get_request_return = self.client.get('/users/2/edit')
        self.assert200(get_request_return)

        context_user = self.get_context_variable('user')

        self.assertEqual(
            context_user['name'], 'Test User 2',
            "'Test User 2' is not equal to the 'name' field for the context variable user."
        )

        self.assert_template_used('edit_user.html')
示例#3
0
class TestViews(TestCase, FixturesMixin):
    """Contains tests for the UserPortal main 'views' file."""

    render_templates = False
    db_cfg = config.get_config_context()['database']
    s_db_cfg = config.get_config_context()['s_database']
    TESTING = True

    # Fixtures are usually in warno-vagrant/data_store/data/WarnoConfig/fixtures
    fixtures = [
        'sites.yml', 'users.yml', 'instruments.yml', 'instrument_logs.yml'
    ]

    def setUp(self):
        db.create_all()

    def tearDown(self):
        db.session.remove()
        db.drop_all()

    def create_app(self):
        views.app.config['TESTING'] = True
        views.app.config['FIXTURES_DIRS'] = [os.environ.get('FIXTURES_DIR')]
        views.app.config[
            'SQLALCHEMY_DATABASE_URI'] = 'postgresql://%s:%s@%s:%s/%s' % (
                self.db_cfg['DB_USER'], self.s_db_cfg['DB_PASS'],
                self.db_cfg['DB_HOST'], self.db_cfg['DB_PORT'],
                self.db_cfg['TEST_DB_NAME'])

        FixturesMixin.init_app(views.app, db)

        return views.app

    def test_status_log_for_each_instrument_returns_expected_logs(
            self, logger):
        """Test that the status_log_for_each_instrument function returns the two expected logs, which are the most
        recent instrument logs for each instrument. There are two logs for instrument 1 and one for instrument 2.
        The most recent log for instrument 1 has the contents 'Log 2 Contents'.  The only log for instrument 2 has
        contents 'Log 3 Contents'"""
        function_return = views.status_log_for_each_instrument()
        # According to the database fixture instrument log entries:
        # Instrument id 1 has two logs, the most recent having contents 'Log 2 Contents'
        # Instrument id 2 has one log, having contents 'Log 3 Contents'

        self.assertEqual(
            function_return[1]['contents'], 'Log 2 Contents',
            "Instrument with id '1's most recent log did not have expected contents 'Log 2 Contents'"
        )
        self.assertEqual(
            function_return[2]['contents'], 'Log 3 Contents',
            "Instrument with id '2's most recent log did not have expected contents 'Log 3 Contents'"
        )
示例#4
0
 def __init__(self):
     super(SystemStatusPlugin, self).__init__()
     self.plugin_name = 'System Status Plugin'
     self.plugin_description = 'test'
     self.add_event_code("cpu_usage")
     self.white_list = white_list
     self.config_ctxt = config.get_config_context()
     self.config_id = None
 def __init__(self):
     super(ProSensingPAFPlugin, self).__init__()
     self.instrument_name = None
     self.plugin_name = 'ProSensing PAF Plugin'
     self.plugin_description = 'Monitors Prosensing Instruments via PAF'
     self.add_event_code("prosensing_paf")
     self.add_event_code("non_paf_event")
     self.white_list = white_list
     self.config_ctxt = config.get_config_context()
     self.config_id = None
示例#6
0
 def __init__(self):
     super(IrisBitePlugin, self).__init__()
     self.instrument_name = None
     self.plugin_name = 'Iris BITE Plugin'
     self.plugin_description = 'Monitors Iris BITE statuses on instruments'
     self.add_event_code("iris_bite")
     self.add_event_code("non_iris_event")
     self.white_list = white_list
     self.config_ctxt = config.get_config_context()
     self.config_id = None
示例#7
0
    def test_get_config_context_construct_contains_expected_database_keys(self):
        """The configuration context should be properly set with the expected database keys."""

        config_construct = config.get_config_context()

        for value in self.required_config_keys:
            self.assertIn(value, config_construct['database'],
                          'config context does not contain "database" key:"%s"' % value)

        self.assertIn('DB_PASS', config_construct['s_database'],
                      'config context does not contain "s_database" key: "DB_PASS"')
示例#8
0
    def test_get_config_context_database_entries(self):
        """Test the configuration context"""

        cfg = config.get_config_context()

        for value in self.list_required_keys:
            self.assertIn(value, cfg['database'],
                          'config context does not contain key:"%s"' % value)

        self.assertIn('DB_PASS', cfg['s_database'],
                      'config context does not contain key: "DB_PASS"')
示例#9
0
class TestInstruments(TestCase, FixturesMixin):
    """Contains tests for the UserPortal 'instruments' file."""
    render_templates = False
    db_cfg = config.get_config_context()['database']
    s_db_cfg = config.get_config_context()['s_database']
    TESTING = True

    # Fixtures are usually in warno-vagrant/data_store/data/WarnoConfig/fixtures
    # Note that, especially with this many different tables, the fixtures should be loaded in the proper order.
    # For example, in this case, instruments have foreign keys to sites, so it errors if sites have not yet been defined
    fixtures = [
        'sites.yml', 'users.yml', 'instruments.yml',
        'instrument_data_references.yml', 'event_codes.yml',
        'instrument_logs.yml', 'valid_columns.yml', 'prosensing_paf.yml',
        'events_with_value.yml'
    ]

    def setUp(self):
        db.create_all()

    def tearDown(self):
        db.session.remove()
        db.drop_all()

    def create_app(self):
        views.app.config['TESTING'] = True
        views.app.config['FIXTURES_DIRS'] = [os.environ.get('FIXTURES_DIR')]
        views.app.config[
            'SQLALCHEMY_DATABASE_URI'] = 'postgresql://%s:%s@%s:%s/%s' % (
                self.db_cfg['DB_USER'], self.s_db_cfg['DB_PASS'],
                self.db_cfg['DB_HOST'], self.db_cfg['DB_PORT'],
                self.db_cfg['TEST_DB_NAME'])

        FixturesMixin.init_app(views.app, db)

        return views.app
示例#10
0
    def __init__(self):

        self.plugin_path = DEFAULT_PLUGIN_PATH
        self.config_ctxt = config.get_config_context()
        self.event_manager_url = self.config_ctxt['setup']['em_url']
        self.is_central = self.config_ctxt['type']['central_facility']
        self.site_id = None
        self.msg_queue = Queue()
        self.event_code_dict = {}
        self.instrument_ids = []
        self.continue_processing_events = True
        self.cert_verify = self.config_ctxt['setup']['cert_verify']
        self.info = {'site': self.config_ctxt['setup']['site']}
        self.plugin_managers = [PluginManager({
                                    'site': self.config_ctxt['setup']['site'],
                                    'config_id': instrument_name
                                    }, instrument)
                                for instrument_name, instrument
                                in self.config_ctxt['agent']['instrument_list'].items()]

        #Set up logging
        log_path = os.environ.get("LOG_PATH")
        if log_path is None:
            log_path = "/vagrant/logs/"

        # Logs to the main log
        logging.basicConfig( format='%(levelname)s:%(asctime)s:%(module)s:%(lineno)d:  %(message)s',
                             filename='%scombined.log' % log_path,
                             filemode='a', level=logging.DEBUG)

        # Logs to the agent log
        self.agent_logger = logging.getLogger(__name__)
        agent_handler = logging.FileHandler("%sagent_server.log" % log_path, mode="a")
        agent_handler.setFormatter(logging.Formatter('%(levelname)s:%(asctime)s:%(module)s:%(lineno)d:  %(message)s'))
        self.agent_logger.addHandler(agent_handler)
        # Add agent handler to the main werkzeug logger
        logging.getLogger("werkzeug").addHandler(agent_handler)

        self.agent_logger.info(self.info)
示例#11
0
def new_log():
    """Submit a new log entry to WARNO.

    Rather than having the normal 'Get' 'Post' format, this is designed to be available to
        more than just web users.  If there are no optional arguments user_id, instrument_id,
        time, or status (or if one of those options is missing), the normal form to create a new
        log will render. If all of those options exist, the function will attempt a database insertion
        with the data.  If the insertion fails, the form to create a new log will render with an error
        message.  If the insertion is successful, the user will be redirected instead to the page of
        the instrument the log was submitted for.  Also, upon success, if it is not the central facility,
        the log's information will be sent to the central facility's Event Manager.

    Parameters
    ----------
    error: optional, integer
        Passed as an HTML parameter, an error message set if the latitude or longitude are not
        floating point numbers
    user-id: optional, integer
        Passed as an HTML parameter, the database id of the author of the new log

    instrument: optional, integer
        Passed as an HTML parameter, the database id of the instrument the log is for

    time: optional, string
        Passed as an HTML parameter, a string representing the date and time for the log

    status: optional, integer
        Passed as an HTML parameter, the status code of the instrument for the log

    contents: optional, string
        Passed as an HTML parameter, the message contents for the log

    create-another: optional, string
        Passed as an HTML parameter, 'on' indicates that the option to create a new log was selected, '' otherwise

    Returns
    -------
    new_log.html: HTML document
        If the new log insertion was attempted but failed, or if no insertion was attempted,
            returns an HTML form to create a new site, with an optional argument 'error' if it
            was a failed database insertion.
    instrument: Flask redirect location
        If a new log insertion was attempted and succeeded,  returns a Flask redirect location
            to the instrument function, redirecting the user to the page showing the
            instrument with the instrument_id matching the insertion.
    """
    if current_user.is_anonymous or current_user.authorizations not in [
            "engineer", "technician"
    ]:
        abort(403)

    # Default error message will not show on template when its the empty string
    error = ""

    if request.args.get('create-another') == "on":
        create_another = True
    else:
        create_another = False

    new_db_log = InstrumentLog()
    new_db_log.author_id = request.args.get('user-id')
    new_db_log.instrument_id = request.args.get('instrument')
    new_db_log.time = request.args.get('time')
    new_db_log.status = request.args.get('status')
    new_db_log.contents = request.args.get('contents')

    cfg = config.get_config_context()
    cert_verify = cfg['setup']['cert_verify']

    # If there is valid data entered with the get request, insert and redirect to the instrument
    # that the log was placed for
    if new_db_log.author_id and new_db_log.instrument_id and new_db_log.status and new_db_log.time:
        # Attempt to insert an item into the database. Try/Except is necessary because
        # the timedate datatype the database expects has to be formatted correctly.
        if new_db_log.time == 'Invalid Date':
            error = "Invalid Date/Time Format"
            up_logger.error(
                "Invalid Date/Time format for new log entry. "
                "'Invalid Date' passed from JavaScript parser in template")
        else:
            try:
                db.session.add(new_db_log)
                db.session.commit()
            except psycopg2.DataError:
                # If the timedate object expected by the database was incorrectly formatted, error is set
                # for when the page is rendered again
                error = "Invalid Date/Time Format"
                up_logger.error(
                    "Invalid Date/Time format for new log entry.  Value: %s",
                    new_db_log.time)
            else:
                # If it is not a central facility, pass the log to the central facility
                if not cfg['type']['central_facility']:
                    packet = dict(event_code=5,
                                  data=dict(
                                      instrument_id=new_db_log.instrument_id,
                                      author_id=new_db_log.author_id,
                                      time=str(new_db_log.time),
                                      status=new_db_log.status,
                                      contents=new_db_log.contents,
                                      supporting_images=None))
                    payload = json.dumps(packet)
                    requests.post(cfg['setup']['cf_url'],
                                  data=payload,
                                  headers={'Content-Type': 'application/json'},
                                  verify=cert_verify)

                # If planning to create another, redirect back to this page.  Prevents previous log information
                # from staying in the url bar, which would cause refreshes to submit new logs.  If not creating another,
                # redirect to the instrument page that the log was submitted for
                if create_another:
                    return redirect(url_for('logs.new_log'))
                else:
                    return redirect(
                        url_for('instruments.instrument',
                                instrument_id=new_db_log.instrument_id))

    # If there was no valid insert render normally but pass the indicative error

    # Format the instrument names to be more descriptive
    db_instruments = db.session.query(Instrument).all()
    instruments = [
        dict(id=instrument.id,
             name=instrument.site.name_short + ":" + instrument.name_short)
        for instrument in db_instruments
    ]
    for instrument in instruments:
        recent_log = db.session.query(InstrumentLog).filter(
            InstrumentLog.instrument_id == instrument["id"]).order_by(
                InstrumentLog.time.desc()).first()
        if recent_log:
            instrument["status"] = status_code_to_text(recent_log.status)
            recent_log.contents = Markup(recent_log.contents)
        instrument["log"] = recent_log

    sorted_instruments = sorted(instruments, key=lambda x: x["name"])

    return render_template('new_log.html',
                           instruments=sorted_instruments,
                           status=status_text,
                           error=error)
示例#12
0
class TestEventManager(TestCase, FixturesMixin):
    render_templates = False
    db_cfg = config.get_config_context()['database']
    s_db_cfg = config.get_config_context()['s_database']
    TESTING = True

    # Fixtures are usually in warno-vagrant/data_store/data/WarnoConfig/fixtures
    fixtures = [
        'sites.yml', 'users.yml', 'instruments.yml',
        'instrument_data_references.yml', 'instrument_logs.yml',
        'event_codes.yml', 'prosensing_paf.yml', 'events_with_text.yml',
        'events_with_value.yml', 'pulse_captures.yml'
    ]

    def setUp(self):
        db.create_all()

    def tearDown(self):
        db.session.remove()
        db.drop_all()

    def create_app(self):
        warno_event_manager.app.config['TESTING'] = True
        warno_event_manager.app.config['FIXTURES_DIRS'] = [
            os.environ.get('FIXTURES_DIR')
        ]
        warno_event_manager.app.config[
            'SQLALCHEMY_DATABASE_URI'] = 'postgresql://%s:%s@%s:%s/%s' % (
                self.db_cfg['DB_USER'], self.s_db_cfg['DB_PASS'],
                self.db_cfg['DB_HOST'], self.db_cfg['DB_PORT'],
                self.db_cfg['TEST_DB_NAME'])

        FixturesMixin.init_app(warno_event_manager.app, db)

        return warno_event_manager.app

    def test_save_json_db_data_output_file_matches_expected_file(self, logger):
        # Have to manually clean up generated files if test fails

        warno_event_manager.save_json_db_data()

        parent_path = os.path.join(os.path.dirname(__file__), "../")
        possible_files = os.listdir(".")

        # Get the three most recently created files (which should have been created when the tested function was called)
        dated_files = [(os.path.getmtime(fn), os.path.basename(fn))
                       for fn in possible_files
                       if fn.lower().endswith('.json')]

        dated_files.sort()
        dated_files.reverse()
        newest = dated_files[:3]

        first_instrument = [
            item[1] for item in newest if "1_archived_" in item[1]
        ][0]
        second_instrument = [
            item[1] for item in newest if "2_archived_" in item[1]
        ][0]
        db_info = [item[1] for item in newest if "db_info_" in item[1]][0]

        contents = ""
        expected = ""

        # Test each file against the expected example files
        with open(first_instrument, 'r') as borky:
            contents = borky.read()
        with open(
                os.path.join(os.path.dirname(__file__),
                             "archive_testfile_1.json"), 'r') as borky:
            expected = borky.read()
        self.assertEqual(
            contents, expected,
            "Instrument 1's output does not match 'archive_testfile_1.json'")

        with open(second_instrument, 'r') as borky:
            contents = borky.read()
        with open(
                os.path.join(os.path.dirname(__file__),
                             "archive_testfile_2.json"), 'r') as borky:
            expected = borky.read()
        self.assertEqual(
            contents, expected,
            "Instrument 2's output does not match 'archive_testfile_2.json'")

        with open(db_info, 'r') as borky:
            contents = borky.read()
        with open(
                os.path.join(os.path.dirname(__file__),
                             "archive_testfile_info.json"), 'r') as borky:
            expected = borky.read()
        self.assertEqual(
            contents, expected,
            "Database info output does not match 'archive_testfile_info.json'")

        os.remove(first_instrument)
        os.remove(second_instrument)
        os.remove(db_info)
示例#13
0
from WarnoConfig import config, utility
from flask import Flask, render_template, redirect, url_for, request

from PluginManager import PluginManager

global agent
global remote_server

headers = {'Content-Type': 'application/json'}

DEFAULT_PLUGIN_PATH = 'Agent/plugins/'
MAX_CONN_ATTEMPTS = 99999
CONN_RETRY_TIME = 15
AGENT_DASHBOARD_PORT = 6309

ctx = config.get_config_context()
if ctx['agent']['local_debug']:
    AGENT_DASHBOARD_PORT = int(ctx['agent']['dev_port'])

app = Flask(__name__)

log_path = os.environ.get("LOG_PATH")
if log_path is None:
    log_path = "/vagrant/logs/"

logfile = log_path + "agent_exceptions.log"


@app.route('/agent')
def serve_dashboard():
    """Anchor point to serve the agent dashboard.
示例#14
0
    def test_get_config_context_construct_contains_expected_top_level_dicts_setup_and_type(self):
        """Configuration construct should have top level dictionaries 'setup' and 'type'"""
        config_construct = config.get_config_context()

        self.assertIn('setup', config_construct, 'Configuration should have "setup" entry')
        self.assertIn('type', config_construct, 'Configuration should have "type" entry')
示例#15
0
    def test_get_config_context_top_level_dicts(self):
        cfg = config.get_config_context()

        self.assertIn('setup', cfg, 'Configuration should have "setup" entry')
        self.assertIn('type', cfg, 'Configuration should have "type" entry')
示例#16
0
is_central = 0

status_text = {
    1: "OPERATIONAL",
    2: "NOT WORKING",
    3: "TESTING",
    4: "IN-UPGRADE",
    5: "TRANSIT"
}

# Set Up app
app.config.from_object(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app)

# Database Setup
db_cfg = config.get_config_context()['database']
s_db_cfg = config.get_config_context()['s_database']
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://%s:%s@%s:%s/%s' % (
    db_cfg['DB_USER'], s_db_cfg['DB_PASS'], db_cfg['DB_HOST'],
    db_cfg['DB_PORT'], db_cfg['DB_NAME'])
app.config['SECRET_KEY'] = "THIS IS AN INSECURE SECRET"
app.config['CSRF_ENABLED'] = True

# Flask User email customization https://github.com/lingthio/Flask-User/blob/master/docs/source/customization.rst#emails

app.config['MAIL_USERNAME'] = os.getenv('MAIL_USERNAME', '')
app.config['MAIL_PASSWORD'] = os.getenv('MAIL_PASSWORD', '')
app.config['MAIL_DEFAULT_SENDER'] = os.getenv(
    'MAIL_DEFAULT_SENDER', '"WARNO" <*****@*****.**>')
app.config['MAIL_SERVER'] = os.getenv('MAIL_SERVER', 'localhost')
app.config['MAIL_PORT'] = int(os.getenv('MAIL_PORT', '25'))
示例#17
0
from UserPortal import app
from WarnoConfig import config

if __name__ == '__main__':
    cfg = config.get_config_context()
    app.run(debug=True, host='0.0.0.0', port=cfg['setup']['user_portal_port'])
示例#18
0
class TestInstruments(TestCase, FixturesMixin):
    """Contains tests for the UserPortal 'instruments' file."""
    render_templates = False
    db_cfg = config.get_config_context()['database']
    s_db_cfg = config.get_config_context()['s_database']
    TESTING = True

    # Fixtures are usually in warno-vagrant/data_store/data/WarnoConfig/fixtures
    # Note that, especially with this many different tables, the fixtures should be loaded in the proper order.
    # For example, in this case, instruments have foreign keys to sites, so it errors if sites have not yet been defined
    fixtures = [
        'sites.yml', 'users.yml', 'instruments.yml',
        'instrument_data_references.yml', 'event_codes.yml',
        'instrument_logs.yml', 'valid_columns.yml', 'prosensing_paf.yml',
        'events_with_value.yml'
    ]

    def setUp(self):
        db.create_all()

    def tearDown(self):
        db.session.remove()
        db.drop_all()

    def create_app(self):
        views.app.config['TESTING'] = True
        views.app.config['FIXTURES_DIRS'] = [os.environ.get('FIXTURES_DIR')]
        views.app.config[
            'SQLALCHEMY_DATABASE_URI'] = 'postgresql://%s:%s@%s:%s/%s' % (
                self.db_cfg['DB_USER'], self.s_db_cfg['DB_PASS'],
                self.db_cfg['DB_HOST'], self.db_cfg['DB_PORT'],
                self.db_cfg['TEST_DB_NAME'])

        FixturesMixin.init_app(views.app, db)

        return views.app

    def test_list_instruments_passes_fixture_instruments_using_correct_template(
            self, logger):
        """A 'GET' request to '/instruments' returns a response of '200 OK' and then  renders 'instrument_list.html',
        passing in the list of database instruments as a context variable 'instruments'."""
        get_request_return = self.client.get('/instruments')
        self.assert200(get_request_return, "GET return is not '200 OK'")

        # Accessing context variable by name feels brittle, but it seems to be the only way
        # Check that test fixture data is passed by context as expected
        context_instruments = self.get_context_variable('instruments')

        self.assertTrue(
            'Test Vendor 1' in context_instruments[0]['vendor'],
            "'Test Vendor 1' is not in the 'vendor' field for the first instrument."
        )
        self.assertTrue(
            'TESTSIT1' in context_instruments[0]['location'],
            "'TESTSIT1' is not in the 'location' field for the first instrument."
        )
        self.assertTrue(
            'Test Vendor 2' in context_instruments[1]['vendor'],
            "'Test Vendor 2' is not in the 'vendor' field for the second instrument."
        )
        self.assertTrue(
            'TESTSIT2' in context_instruments[1]['location'],
            "'TESTSIT2' is not in the 'location' field for the second instrument."
        )
        self.assert_template_used('instrument_list.html')

    @mock.patch("UserPortal.instruments.current_user")
    def test_method_get_on_new_instrument_passes_fixture_sites_using_correct_template(
            self, current_user, logger):
        """A 'GET' request to '/instruments/new' returns a response of '200 OK' and then  renders 'new_instrument.html',
        passing in the list of database sites as a context variable 'sites'."""
        # Mocks an authorized user
        current_user.is_anonymous = False
        current_user.authorizations = "engineer"

        get_request_return = self.client.get('/instruments/new')
        self.assert200(get_request_return)

        # Accessing context variable by name feels brittle, but it seems to be the only way
        # Fixture sites should be passed as expected
        context_sites = self.get_context_variable('sites')
        self.assertTrue(
            'TESTSIT1' in context_sites[0]['name'],
            "'TESTSIT1' is not in the 'name' field for the first context variable site."
        )
        self.assertEqual(
            1, context_sites[0]['id'],
            "First context variable site does not have the correct id of '1'")
        self.assertTrue(
            'TESTSIT2' in context_sites[1]['name'],
            "'TESTSIT2' is not in the 'name' field for the second context variable site."
        )
        self.assert_template_used('new_instrument.html')

    @mock.patch("UserPortal.instruments.current_user")
    def test_method_get_on_edit_instrument_with_id_2_passes_fixture_sites_and_correct_instrument_as_context_variables_using_correct_template(
            self, current_user, logger):
        """A 'GET' request to '/instruments/<instrument_id>/edit' returns a response of '200 OK' and then  renders
        'edit_instrument.html', passing in the list of database sites and the instrument with an id matching
        'instrument_id' as context variables 'sites' and 'instrument' respectively."""
        # Mocks an authorized user
        current_user.is_anonymous = False
        current_user.authorizations = "engineer"

        get_request_return = self.client.get('/instruments/2/edit')
        self.assert200(get_request_return)

        # Accessing context variable by name feels brittle, but it seems to be the only way
        # Fixture sites should be passed as expected
        context_sites = self.get_context_variable('sites')
        self.assertTrue(
            "TESTSIT1" in context_sites[0]['name'],
            "'TESTSIT1' is not in the first context variable site.")
        self.assertTrue(
            "TESTSIT2" in context_sites[1]['name'],
            "'TESTSIT1' is not in the second context variable site.")

        # Second fixture instrument should be passed
        context_instrument = self.get_context_variable('instrument')
        self.assertTrue(
            'TESTINS2' in context_instrument['name_short'],
            "'TESTINS2' is not in the 'name_short' field for the context variable instrument."
        )
        self.assertTrue(
            'Test Description 2' in context_instrument['description'],
            "'Test Description 2' is not in the 'description' field for the context variable instrument."
        )

        self.assert_template_used('edit_instrument.html')

    # TODO method post new instrument, post edit instrument
    # TODO method get generate_instrument_dygraph arguments?
    @mock.patch('UserPortal.instruments.db_recent_logs_by_instrument')
    @mock.patch('UserPortal.instruments.valid_columns_for_instrument')
    def test_method_get_on_instrument_when_id_is_2_calls_db_with_correct_arguments_and_returns_200(
            self, valid_columns, recent_logs, logger):
        """A 'GET' request on '/instruments/<instrument_id>' returns a response of '200 OK' and passes various context
        variables to the template 'show_instrument.html'.  Context variables: 'instrument' as the database information
        for the instrument with id matching 'instrument_id', 'recent_logs' as he list of the most recent instrument logs
        for the instrument, 'status' being the status of the instrument indicated by the most recent log, and 'columns'
        being the list of valid columns to graph."""
        recent_logs.return_value = [
            dict(time="01/01/2001 01:01:01",
                 contents="contents",
                 status="1",
                 supporting_images="supporting_images",
                 author="author")
        ]
        valid_columns_return = ["column"]
        valid_columns.return_value = valid_columns_return
        instrument_id = 2

        test_url = "/instruments/%s" % instrument_id
        result = self.client.get(test_url)

        context_instrument = self.get_context_variable('instrument')
        context_recent_logs = self.get_context_variable('recent_logs')
        context_status = self.get_context_variable('status')
        context_columns = self.get_context_variable('columns')

        # Second fixture instrument should be acquired and passed back as a context variable
        self.assertTrue(
            'TESTINS2' in context_instrument['abbv'],
            "'TESTINS2' is not in the 'abbv' field for the context variable instrument."
        )
        self.assertTrue(
            'Test Description 2' in context_instrument['description'],
            "'Test Description 2' is not in the 'description' field for the context variable instrument."
        )

        # Confirm that the status for the returned log has been converted from code to text
        self.assertEqual(
            'OPERATIONAL', context_recent_logs[0]['status'],
            "'status' field for the most recent log was not properly changed from 1 to 'OPERATIONAL'."
        )
        # Status context variable should also be set to 'OPERATIONAL'
        self.assertEqual('OPERATIONAL', context_status,
                         "'status' context variable was not 'OPERATIONAL'")

        self.assertListEqual(
            valid_columns_return, context_columns,
            "Valid columns were not passed properly as a context variable")

        self.assert200(result, "GET return is not '200 OK'")
        self.assert_template_used('show_instrument.html')

    # /instruments/instrument_id
    def test_method_delete_on_instrument_when_id_is_2_returns_200_and_reduces_count_by_1(
            self, logger):
        """A 'DELETE' request on '/instruments/<instrument_id>' returns a response of '200 OK' and reduces the total
        count of instruments in the database by 1."""
        instrument_id = 2
        test_url = "/instruments/%s" % instrument_id

        # Instrument count after delete should be reduced by one
        count_before = db.session.query(Instrument).count()
        delete_request_return = self.client.delete(test_url)
        self.assert200(delete_request_return, "GET return is not '200 OK'")
        count_after = db.session.query(Instrument).count()

        self.assertEqual(
            count_before - 1, count_after,
            "The count of instruments was not decremented by exactly one.")

    def test_db_delete_instrument_when_id_is_2_removes_the_entry_with_id_of_2(
            self, logger):
        """A 'DELETE' request on '/instruments/<instrument_id>' returns a response of '200 OK' and removes the
        instrument entry from the database with an id matching 'instrument_id'."""
        instrument_id = 2
        instruments.db_delete_instrument(instrument_id)

        after_delete_count = db.session.query(Instrument).filter(
            Instrument.id == instrument_id).count()
        self.assertEqual(
            after_delete_count, 0,
            "The Instrument with id %s was not deleted." % instrument_id)

    @mock.patch('redis_interface.RedisInterface')
    def test_recent_values_for_instrument_id_1_with_valid_key_antenna_humidity_returns_expected_object(
            self, interface, logger):
        instrument_id = 1
        key = "antenna_humidity"
        test_url = "/recent_values?instrument_id=%s&keys=%s" % (instrument_id,
                                                                key)
        result = self.client.get(test_url)
        result_object = result.json

        # Expected values are defined in the fixtures
        expected_value = 75.0
        expected_time = "2001-01-01T01:01:01"

        # There should only be one key returned, so every access is index [0]
        self.assertEqual(
            1, len(result_object),
            "Expected returned object to have a length of 1. Length is '%s'." %
            len(result_object))
        self.assertEqual(
            key, result_object[0]["key"],
            "The returned object's key did not match the requested key '%s'." %
            key)
        self.assertEqual(
            expected_value, result_object[0]["data"]["value"],
            "The returned objects first entry's data did not have a value of '%s'."
            % expected_value)
        self.assertEqual(
            expected_time, result_object[0]["data"]["time"],
            "The returned objects first entry's data did not have a time of '%s'."
            % expected_time)

    @mock.patch('redis_interface.RedisInterface')
    def test_recent_values_for_instrument_id_1_with_invalid_key_antenna_temp_returns_empty_object(
            self, interface, logger):
        instrument_id = 1
        key = "antenna_temp"
        test_url = "/recent_values?instrument_id=%s&keys=%s" % (instrument_id,
                                                                key)
        result = self.client.get(test_url)
        result_object = result.json

        # There should only be no keys returned, because the key is not in 'valid_columns' in the fixtures
        self.assertEqual(
            0, len(result_object),
            "Expected returned object to have a length of 0. Length is '%s'." %
            len(result_object))

    # Database Helpers
    def test_db_get_instrument_references_returns_the_instrument_data_references_for_the_correct_instrument(
            self, logger):
        """Calling the 'db_get_instrument_references' function returns all the instrument data references for the
        instrument with an id matching 'instrument_id'."""
        instrument_id = 1
        function_result = instruments.db_get_instrument_references(
            instrument_id)
        db_result = db.session.query(InstrumentDataReference).filter(
            InstrumentDataReference.instrument_id == instrument_id).all()

        self.assertListEqual(
            function_result, db_result,
            "Returned InstrumentDataReferences do not match database query.")
        # 'matches' will only have a 'True' in it if  the expected description matches one of the returned descriptions
        matches = [
            True if res.description == 'prosensing_paf' else False
            for res in function_result
        ]
        self.assertIn(
            True, matches,
            "No reference's 'description' does not matches the fixture's 'prosensing_paf'."
        )

    def test_db_select_instrument_when_id_is_2_returns_the_correct_instrument_dictionary(
            self, logger):
        """Calling the 'db_select_instrument function returns a dictionary containing the database information for the
        instrument with an id matching 'instrument_id'."""
        instrument_id = 2
        function_return = instruments.db_select_instrument(instrument_id)
        db_result = db.session.query(Instrument).filter(
            Instrument.id == instrument_id).first()

        self.assertEqual(
            db_result.name_short, function_return['abbv'],
            "The function result's 'abbv' field does not match 'name_short' of the instrument with id 2."
        )
        self.assertEqual(
            function_return['id'], instrument_id,
            "The function result's 'id' field does not match the id it was called with."
        )

    def test_db_recent_logs_by_instrument_when_id_is_1_returns_logs_in_the_correct_order(
            self, logger):
        """Calling the 'db_recent_logs_by_instrument' function returns the most recent instrument logs for the
        instrument with an id matching 'instrument_id'.  These logs are ordered by the 'time' field, with the greatest
        time being the most recently created log, which is at the head of the list."""
        # The parameters for the instrument id integer seem to be passed in a strange way for the 'filter' part
        # of a query, and I cannot find a way to access it from the tests.
        instrument_id = 1
        function_return = instruments.db_recent_logs_by_instrument(
            instrument_id)

        # Assert the logs are in the correct order (later time first)
        self.assertTrue(
            function_return[0]['time'] > function_return[1]['time'],
            "First log's time is not more recent than the second.")
        self.assertEqual(
            function_return[0]['contents'], "Log 2 Contents",
            "Most recent log's 'contents' are not the expected 'Log 2 Contents'"
        )

    def test_db_recent_logs_by_instrument_when_id_is_1_and_maximum_number_is_1_limits_count_of_returned_logs_to_1(
            self, logger):
        """Calling the 'db_recent_logs_by_instrument' function with a specified maximum number of logs as 1 reduces the
        number of returned instrument logs to a maximum count of 1."""
        instrument_id = 1
        maximum_number = 1
        result = instruments.db_recent_logs_by_instrument(
            instrument_id, maximum_number)

        self.assertEqual(
            len(result), maximum_number,
            "Number of logs returned does not match given 'maximum_number' parameter."
        )

    # Helper Functions
    def test_valid_columns_for_instrument_returns_expected_column_list_for_both_special_and_non_special_references(
            self, logger):
        """Calling the 'valid_columns_for_instrument' function returns a list of the columns available to graph for the
        instrument with the id matching 'instrument_id'.  This test checks that some expected columns from  the fixtures
        are available as expected."""
        instrument_id = 1
        returned_column_list = instruments.valid_columns_for_instrument(
            instrument_id)

        # Check that column list contains the non_special entry as well as some prosensing_paf specific entries
        self.assertIn(
            "not_special", returned_column_list,
            "'not_special' reference is not in returned column list.")
        self.assertIn(
            "packet_id", returned_column_list,
            "prosensing_paf 'packet_id' not in returned column list.")
        self.assertIn(
            "antenna_humidity", returned_column_list,
            "prosensing_paf 'antenna_humidity' not in returned column list.")

    def test_update_valid_columns_for_instrument_successfully_adds_expected_entries(
            self, logger):
        """Calling the 'update_valid_columns_for_instrument' function updates the list of valid columns for the
        instrument with an id matching 'instrument_id'.  The count of valid columns after the function is called should
        be greater than the count before the function was called, and there should be some expected values in the set
        of valid columns."""
        instrument_id = 1
        pre_function_column_count = db.session.query(ValidColumn).count()
        _ = instruments.update_valid_columns_for_instrument(instrument_id)
        post_function_column_count = db.session.query(ValidColumn).count()

        db_valid_columns = db.session.query(ValidColumn).filter(
            ValidColumn.instrument_id == instrument_id).all()
        result_column_list = [
            column.column_name for column in db_valid_columns
        ]

        self.assertTrue(pre_function_column_count < post_function_column_count,
                        "The count of valid columns did not increase")
        self.assertIn(
            "packet_id", result_column_list,
            "prosensing_paf 'packet_id' not in returned valid column list.")
        self.assertIn(
            "antenna_temp", result_column_list,
            "prosensing_paf 'antenna_temp' not in returned valid column list.")
        self.assertNotIn(
            "ad_skip_count", result_column_list,
            "prosensing_paf 'ad_skip_count' in returned valid column list, even though it shouldn't."
        )

    def test_synchronize_sort_correctly_sorts_3_simple_data_sets_into_expected_output_format(
            self, logger):
        """Test that three simple data sets are correctly sorted and used to construct a return value.  The explanation
        of the sorting algorithm is a little long, so please see the function's docstring for the details. The test
        compares the expected output to the actual output."""
        input_data_0 = dict(data=[
            (datetime.datetime.strptime("2015-05-11 02:00", "%Y-%m-%d %H:%M"),
             03),
            (datetime.datetime.strptime("2015-05-11 01:30", "%Y-%m-%d %H:%M"),
             02),
            (datetime.datetime.strptime("2015-05-11 01:00", "%Y-%m-%d %H:%M"),
             01)
        ])
        input_data_1 = dict(data=[
            (datetime.datetime.strptime("2015-05-11 03:00", "%Y-%m-%d %H:%M"),
             13),
            (datetime.datetime.strptime("2015-05-11 02:30", "%Y-%m-%d %H:%M"),
             12),
            (datetime.datetime.strptime("2015-05-11 02:00", "%Y-%m-%d %H:%M"),
             11)
        ])
        input_data_2 = dict(data=[
            (datetime.datetime.strptime("2015-05-11 02:30", "%Y-%m-%d %H:%M"),
             23),
            (datetime.datetime.strptime("2015-05-11 02:00", "%Y-%m-%d %H:%M"),
             22),
            (datetime.datetime.strptime("2015-05-11 01:00", "%Y-%m-%d %H:%M"),
             21)
        ])

        expected_return = [
            [
                datetime.datetime.strptime("2015-05-11 01:00",
                                           "%Y-%m-%d %H:%M"), 01, None, 21
            ],
            [
                datetime.datetime.strptime("2015-05-11 01:30",
                                           "%Y-%m-%d %H:%M"), 02, None, None
            ],
            [
                datetime.datetime.strptime("2015-05-11 02:00",
                                           "%Y-%m-%d %H:%M"), 03, 11, 22
            ],
            [
                datetime.datetime.strptime("2015-05-11 02:30",
                                           "%Y-%m-%d %H:%M"), None, 12, 23
            ],
            [
                datetime.datetime.strptime("2015-05-11 03:00",
                                           "%Y-%m-%d %H:%M"), None, 13, None
            ]
        ]

        input_dictionary = {0: input_data_0, 1: input_data_1, 2: input_data_2}

        returned_list = instruments.synchronize_sort(input_dictionary)

        self.assertListEqual(
            returned_list, expected_return,
            "The expected result list of synchronize_sort and the "
            "actual list returned do not match.")

    def test_iso_first_elements_changes_the_datetime_object_first_element_of_a_list_to_iso_format_in_place(
            self, logger):
        """Tests that the first element of a list is properly converted from a python datetime object into an ISO 8601
        formatted string in place."""
        input_list = [
            datetime.datetime.strptime("2015-01-01 01:30:30",
                                       "%Y-%m-%d %H:%M:%S"), 0, 1, 2
        ]
        expected_list = ['2015-01-01T01:30:30', 0, 1, 2]

        # Should update input list in place
        instruments.iso_first_element(input_list)

        self.assertListEqual(
            input_list, expected_list,
            "The updated input list and the expected list do not match.")