Commit 78a2b35b authored by Vitali Stupin's avatar Vitali Stupin
Browse files

Adding tests, coverage and lint

parent baf60177
......@@ -63,3 +63,43 @@ curl --cert client.crt --key client.key --cacert csapi.crt -i -d '{"member_code"
```
Add nginx configuration from this repository: `nginx/csapi` to nginx server: `/etc/nginx/sites-enabled/csapi`
## Testing
Note that `server.py` is a configuration file for logging and flask and therefore not covered by tests.
Running the tests:
```bash
cd <project_directory>
python -m unittest
```
Or alternatively run the test file directly:
```bash
python test_member.py
```
In order to measure code coverage install `coverage` module:
```bash
pip install coverage
```
Then coverage analyse:
```bash
coverage run test_member.py
coverage report member.py
```
Alternatively you can generate html report with:
```bash
coverage html member.py
```
In order to lint the code install `pylint` module:
```bash
pip install pylint
```
Then run the analyse:
```bash
pylint member.py
```
#!/usr/bin/env python3
"""This is a module for X-Road Central Server member API.
This module allows adding new members to the X-Road Central Server.
"""
import logging
import psycopg2
import re
import psycopg2
from flask import request, jsonify
from flask_restful import Resource
logger = logging.getLogger('member')
DB_CONF_FILE = '/etc/xroad/db.properties'
LOGGER = logging.getLogger('member')
def get_db_conf():
"""Get Central Server database configuration parameters"""
conf = {
'database': '',
'username': '',
......@@ -18,19 +25,19 @@ def get_db_conf():
# Getting database credentials from X-Road configuration
try:
with open('/etc/xroad/db.properties', 'r') as dbConf:
for line in dbConf:
m = re.match('^database\\s*=\\s*(.+)$', line)
if m:
conf['database'] = m.group(1)
m = re.match('^username\\s*=\\s*(.+)$', line)
if m:
conf['username'] = m.group(1)
m = re.match('^password\\s*=\\s*(.+)$', line)
if m:
conf['password'] = m.group(1)
with open(DB_CONF_FILE, 'r') as db_conf:
for line in db_conf:
match_res = re.match('^database\\s*=\\s*(.+)$', line)
if match_res:
conf['database'] = match_res.group(1)
match_res = re.match('^username\\s*=\\s*(.+)$', line)
if match_res:
conf['username'] = match_res.group(1)
match_res = re.match('^password\\s*=\\s*(.+)$', line)
if match_res:
conf['password'] = match_res.group(1)
except IOError:
pass
......@@ -38,21 +45,23 @@ def get_db_conf():
def get_db_connection(conf):
"""Get connection object for Central Server database"""
return psycopg2.connect(
'host={} port={} dbname={} user={} password={}'.format(
'localhost', '5432', conf['database'], conf['username'], conf['password']))
def get_member_class_id(cur, member_class):
"""Get ID of member class from Central Server"""
cur.execute("""select id from member_classes where code=%(str)s""", {'str': member_class})
rec = cur.fetchone()
if rec and len(rec) > 0:
if rec:
return rec[0]
else:
return
return None
def member_exists(cur, member_code):
"""Check if member exists in Central Server"""
cur.execute(
"""
select exists(
......@@ -65,11 +74,17 @@ def member_exists(cur, member_code):
def get_utc_time(cur):
"""Get current time in UTC timezone from Central Server database"""
cur.execute("""select current_timestamp at time zone 'UTC'""")
return cur.fetchone()[0]
def add_identifier(cur, member_class, member_code, utc_time):
def add_identifier(cur, **kwargs):
"""Add new X-Road identifier to Central Server
Required keyword arguments:
member_class, member_code, utc_time
"""
cur.execute(
"""
insert into identifiers (
......@@ -79,12 +94,19 @@ def add_identifier(cur, member_class, member_code, utc_time):
'MEMBER', (select value from system_parameters where key='instanceIdentifier'),
%(class)s, %(code)s, 'ClientId', %(time)s, %(time)s
) returning id
""", {'class': member_class, 'code': member_code, 'time': utc_time}
""", {
'class': kwargs['member_class'], 'code': kwargs['member_code'],
'time': kwargs['utc_time']}
)
return cur.fetchone()[0]
def add_client(cur, member_code, member_name, class_id, identifier_id, utc_time):
def add_client(cur, **kwargs):
"""Add new X-Road client to Central Server
Required keyword arguments:
member_code, member_name, class_id, identifier_id, utc_time
"""
cur.execute(
"""
insert into security_server_clients (
......@@ -94,13 +116,19 @@ def add_client(cur, member_code, member_name, class_id, identifier_id, utc_time)
%(time)s
)
""", {
'code': member_code, 'name': member_name, 'class_id': class_id,
'identifier_id': identifier_id, 'time': utc_time
'code': kwargs['member_code'], 'name': kwargs['member_name'],
'class_id': kwargs['class_id'], 'identifier_id': kwargs['identifier_id'],
'time': kwargs['utc_time']
}
)
def add_client_name(cur, member_name, identifier_id, utc_time):
def add_client_name(cur, **kwargs):
"""Add new X-Road client name to Central Server
Required keyword arguments:
member_name, identifier_id, utc_time
"""
cur.execute(
"""
insert into security_server_client_names (
......@@ -108,15 +136,17 @@ def add_client_name(cur, member_name, identifier_id, utc_time):
) values (
%(name)s, %(identifier_id)s, %(time)s, %(time)s
)
""", {'name': member_name, 'identifier_id': identifier_id, 'time': utc_time}
""", {
'name': kwargs['member_name'], 'identifier_id': kwargs['identifier_id'],
'time': kwargs['utc_time']}
)
def add_member(member_code, member_name, member_class, json_data):
# Getting database configuration
"""Add new X-Road member to Central Server"""
conf = get_db_conf()
if not conf['username'] or not conf['password'] or not conf['database']:
logger.error('DB_CONF_ERROR: Cannot access database configuration')
LOGGER.error('DB_CONF_ERROR: Cannot access database configuration')
return {
'http_status': 500, 'code': 'DB_CONF_ERROR',
'msg': 'Cannot access database configuration'}
......@@ -125,17 +155,17 @@ def add_member(member_code, member_name, member_class, json_data):
with conn.cursor() as cur:
class_id = get_member_class_id(cur, member_class)
if class_id is None:
logger.warning(
LOGGER.warning(
'INVALID_MEMBER_CLASS: Provided Member Class does not exist '
'(Request: {})'.format(json_data))
'(Request: %s)', json_data)
return {
'http_status': 400, 'code': 'INVALID_MEMBER_CLASS',
'msg': 'Provided Member Class does not exist'}
if member_exists(cur, member_code):
logger.warning(
LOGGER.warning(
'MEMBER_EXISTS: Provided Member already exists '
'(Request: {})'.format(json_data))
'(Request: %s)', json_data)
return {
'http_status': 409, 'code': 'MEMBER_EXISTS',
'msg': 'Provided Member already exists'}
......@@ -143,34 +173,47 @@ def add_member(member_code, member_name, member_class, json_data):
# Timestamps must be in UTC timezone
utc_time = get_utc_time(cur)
identifier_id = add_identifier(cur, member_class, member_code, utc_time)
identifier_id = add_identifier(
cur, member_class=member_class, member_code=member_code, utc_time=utc_time)
add_client(cur, member_code, member_name, class_id, identifier_id, utc_time)
add_client(
cur, member_code=member_code, member_name=member_name, class_id=class_id,
identifier_id=identifier_id, utc_time=utc_time)
add_client_name(cur, member_name, identifier_id, utc_time)
add_client_name(
cur, member_name=member_name, identifier_id=identifier_id, utc_time=utc_time)
conn.commit()
logger.info('Added new member: member_code={}, member_name={}, member_class={}'.format(
member_code, member_name, member_class))
LOGGER.info(
'Added new member: member_code=%s, member_name=%s, member_class=%s',
member_code, member_name, member_class)
return {'http_status': 201, 'code': 'CREATED', 'msg': 'New member added'}
def make_response(data):
"""Create JSON response object"""
response = jsonify({'code': data['code'], 'msg': data['msg']})
response.status_code = data['http_status']
logger.info('Response: {}'.format(data))
LOGGER.info('Response: %s', data)
return response
def get_input(json_data, param_name):
"""Get parameter from request parameters
Returns two items:
* parameter value
* error response (if parameter not found).
If one parameter is set then other is always None.
"""
try:
param = json_data[param_name]
except KeyError:
logger.warning(
'MISSING_PARAMETER: Request parameter {} is missing '
'(Request: {})'.format(param_name, json_data))
LOGGER.warning(
'MISSING_PARAMETER: Request parameter %s is missing '
'(Request: %s)', param_name, json_data)
return None, {
'http_status': 400, 'code': 'MISSING_PARAMETER',
'msg': 'Request parameter {} is missing'.format(param_name)}
......@@ -179,11 +222,13 @@ def get_input(json_data, param_name):
class MemberApi(Resource):
"""Member API class for Flask"""
@staticmethod
def post():
"""POST method"""
json_data = request.get_json(force=True)
logger.info('Incoming request: {}'.format(json_data))
LOGGER.info('Incoming request: %s', json_data)
(member_code, fault_response) = get_input(json_data, 'member_code')
if member_code is None:
......@@ -200,7 +245,7 @@ class MemberApi(Resource):
try:
response = add_member(member_code, member_name, member_class, json_data)
except psycopg2.Error:
logger.error('DB_ERROR: Unclassified database error')
LOGGER.error('DB_ERROR: Unclassified database error')
response = {
'http_status': 500, 'code': 'DB_ERROR',
'msg': 'Unclassified database error'}
......
psycopg2-binary==2.8.2
Flask==1.0.3
Flask-RESTful==0.3.7
gunicorn==19.9.0
coverage
pylint
......@@ -5,7 +5,7 @@ import member
import psycopg2
from flask import Flask, jsonify
from flask_restful import Api
from unittest.mock import patch, mock_open, MagicMock
from unittest.mock import patch, MagicMock
class MemberTestCase(unittest.TestCase):
......@@ -90,7 +90,8 @@ reconnect=true
cur = MagicMock()
cur.execute = MagicMock()
cur.fetchone = MagicMock(return_value=[12345])
self.assertEqual(12345, member.add_identifier(cur, 'MEMBER_CLASS', 'MEMBER_CODE', 'TIME'))
self.assertEqual(12345, member.add_identifier(
cur, member_class='MEMBER_CLASS', member_code='MEMBER_CODE', utc_time='TIME'))
cur.execute.assert_called_with(
"\n insert into identifiers (\n object_type, "
"xroad_instance, member_class, member_code, type, created_at,\n "
......@@ -105,7 +106,8 @@ reconnect=true
cur = MagicMock()
cur.execute = MagicMock()
self.assertEqual(None, member.add_client(
cur, 'MEMBER_CODE', 'MEMBER_NAME', 'CLASS_ID', 'IDENT_ID', 'TIME'))
cur, member_code='MEMBER_CODE', member_name='MEMBER_NAME', class_id='CLASS_ID',
identifier_id='IDENT_ID', utc_time='TIME'))
cur.execute.assert_called_with(
"\n insert into security_server_clients (\n member_code, "
"name, member_class_id, server_client_id, type, created_at, updated_at\n ) "
......@@ -118,7 +120,7 @@ reconnect=true
cur = MagicMock()
cur.execute = MagicMock()
self.assertEqual(None, member.add_client_name(
cur, 'MEMBER_NAME', 'IDENT_ID', 'TIME'))
cur, member_name='MEMBER_NAME', identifier_id='IDENT_ID', utc_time='TIME'))
cur.execute.assert_called_with(
'\n insert into security_server_client_names (\n name, '
'client_identifier_id, created_at, updated_at\n ) values (\n'
......@@ -131,7 +133,7 @@ reconnect=true
'password': 'centerui_pass',
'username': 'centerui_user'})
def test_add_member_no_database(self, mock_get_db_conf, mock_get_db_connection):
with self.assertLogs(member.logger, level='INFO') as cm:
with self.assertLogs(member.LOGGER, level='INFO') as cm:
self.assertEqual(
{
'code': 'DB_CONF_ERROR', 'http_status': 500,
......@@ -148,7 +150,7 @@ reconnect=true
'password': '',
'username': 'centerui_user'})
def test_add_member_no_password(self, mock_get_db_conf, mock_get_db_connection):
with self.assertLogs(member.logger, level='INFO') as cm:
with self.assertLogs(member.LOGGER, level='INFO') as cm:
self.assertEqual(
{
'code': 'DB_CONF_ERROR', 'http_status': 500,
......@@ -165,7 +167,7 @@ reconnect=true
'password': 'centerui_pass',
'username': ''})
def test_add_member_no_username(self, mock_get_db_conf, mock_get_db_connection):
with self.assertLogs(member.logger, level='INFO') as cm:
with self.assertLogs(member.LOGGER, level='INFO') as cm:
self.assertEqual(
{
'code': 'DB_CONF_ERROR', 'http_status': 500,
......@@ -184,7 +186,7 @@ reconnect=true
'username': 'centerui_user'})
def test_add_member_no_class(
self, mock_get_db_conf, mock_get_db_connection, mock_get_member_class_id):
with self.assertLogs(member.logger, level='INFO') as cm:
with self.assertLogs(member.LOGGER, level='INFO') as cm:
self.assertEqual(
{
'code': 'INVALID_MEMBER_CLASS', 'http_status': 400,
......@@ -200,6 +202,7 @@ reconnect=true
mock_get_member_class_id.assert_called_with(
mock_get_db_connection().__enter__().cursor().__enter__(), 'MEMBER_CLASS')
@patch('member.member_exists', return_value=True)
@patch('member.get_member_class_id', return_value=12345)
@patch('member.get_db_connection')
@patch('member.get_db_conf', return_value={
......@@ -207,8 +210,9 @@ reconnect=true
'password': 'centerui_pass',
'username': 'centerui_user'})
def test_add_member_member_exists(
self, mock_get_db_conf, mock_get_db_connection, mock_get_member_class_id):
with self.assertLogs(member.logger, level='INFO') as cm:
self, mock_get_db_conf, mock_get_db_connection, mock_get_member_class_id,
mock_member_exists):
with self.assertLogs(member.LOGGER, level='INFO') as cm:
self.assertEqual(
{
'code': 'MEMBER_EXISTS', 'http_status': 409,
......@@ -223,10 +227,57 @@ reconnect=true
'username': 'centerui_user'})
mock_get_member_class_id.assert_called_with(
mock_get_db_connection().__enter__().cursor().__enter__(), 'MEMBER_CLASS')
mock_member_exists.assert_called_with(
mock_get_db_connection().__enter__().cursor().__enter__(), 'MEMBER_CODE')
@patch('member.add_client_name')
@patch('member.add_client')
@patch('member.add_identifier', return_value=123456)
@patch('member.get_utc_time', return_value='TIME')
@patch('member.member_exists', return_value=False)
@patch('member.get_member_class_id', return_value=12345)
@patch('member.get_db_connection')
@patch('member.get_db_conf', return_value={
'database': 'centerui_production',
'password': 'centerui_pass',
'username': 'centerui_user'})
def test_add_member_ok(
self, mock_get_db_conf, mock_get_db_connection, mock_get_member_class_id,
mock_member_exists, mock_get_utc_time, mock_add_identifier, mock_add_client,
mock_add_client_name):
with self.assertLogs(member.LOGGER, level='INFO') as cm:
self.assertEqual(
{
'code': 'CREATED', 'http_status': 201,
'msg': 'New member added'},
member.add_member('MEMBER_CODE', 'MEMBER_NAME', 'MEMBER_CLASS', 'JSON_DATA'))
self.assertEqual([
'INFO:member:Added new member: member_code=MEMBER_CODE, '
'member_name=MEMBER_NAME, member_class=MEMBER_CLASS'], cm.output)
mock_get_db_conf.assert_called_with()
mock_get_db_connection.assert_called_with({
'database': 'centerui_production', 'password': 'centerui_pass',
'username': 'centerui_user'})
mock_get_member_class_id.assert_called_with(
mock_get_db_connection().__enter__().cursor().__enter__(), 'MEMBER_CLASS')
mock_member_exists.assert_called_with(
mock_get_db_connection().__enter__().cursor().__enter__(), 'MEMBER_CODE')
mock_get_utc_time.assert_called_with(
mock_get_db_connection().__enter__().cursor().__enter__())
mock_add_identifier.assert_called_with(
mock_get_db_connection().__enter__().cursor().__enter__(),
member_class='MEMBER_CLASS', member_code='MEMBER_CODE', utc_time='TIME')
mock_add_client.assert_called_with(
mock_get_db_connection().__enter__().cursor().__enter__(),
member_code='MEMBER_CODE', member_name='MEMBER_NAME', class_id=12345,
identifier_id=123456, utc_time='TIME')
mock_add_client_name.assert_called_with(
mock_get_db_connection().__enter__().cursor().__enter__(),
member_name='MEMBER_NAME', identifier_id=123456, utc_time='TIME')
def test_make_response(self):
with self.app.app_context():
with self.assertLogs(member.logger, level='INFO') as cm:
with self.assertLogs(member.LOGGER, level='INFO') as cm:
response = member.make_response(
{'http_status': 200, 'code': 'OK', 'msg': 'All Correct'})
self.assertEqual(200, response.status_code)
......@@ -246,7 +297,7 @@ reconnect=true
self.assertEqual(None, err)
def test_get_input_err(self):
with self.assertLogs(member.logger, level='INFO') as cm:
with self.assertLogs(member.LOGGER, level='INFO') as cm:
(value, err) = member.get_input(
{'member_name': 'MEMBER_NAME', 'member_class': 'MEMBER_CLASS'},
'member_code')
......@@ -260,7 +311,7 @@ reconnect=true
cm.output)
def test_empty_query(self):
with self.assertLogs(member.logger, level='INFO') as cm:
with self.assertLogs(member.LOGGER, level='INFO') as cm:
response = self.client.post('/member', data=json.dumps({}))
self.assertEqual(400, response.status_code)
# Not testing response content, it does not come from application
......@@ -273,7 +324,7 @@ reconnect=true
def test_empty_member_code_query(self):
with self.app.app_context():
with self.assertLogs(member.logger, level='INFO') as cm:
with self.assertLogs(member.LOGGER, level='INFO') as cm:
response = self.client.post('/member', data=json.dumps(
{'member_name': 'MEMBER_NAME', 'member_class': 'MEMBER_CLASS'}))
self.assertEqual(response.status_code, 400)
......@@ -293,7 +344,7 @@ reconnect=true
def test_empty_member_name_query(self):
with self.app.app_context():
with self.assertLogs(member.logger, level='INFO') as cm:
with self.assertLogs(member.LOGGER, level='INFO') as cm:
response = self.client.post('/member', data=json.dumps(
{'member_code': 'MEMBER_CODE', 'member_class': 'MEMBER_CLASS'}))
self.assertEqual(response.status_code, 400)
......@@ -313,7 +364,7 @@ reconnect=true
def test_empty_member_class_query(self):
with self.app.app_context():
with self.assertLogs(member.logger, level='INFO') as cm:
with self.assertLogs(member.LOGGER, level='INFO') as cm:
response = self.client.post('/member', data=json.dumps(
{'member_code': 'MEMBER_CODE', 'member_name': 'MEMBER_NAME'}))
self.assertEqual(response.status_code, 400)
......@@ -334,7 +385,7 @@ reconnect=true
@patch('member.add_member', side_effect=psycopg2.Error)
def test_db_error_handled(self, mock_add_member):
with self.app.app_context():
with self.assertLogs(member.logger, level='INFO') as cm:
with self.assertLogs(member.LOGGER, level='INFO') as cm:
response = self.client.post('/member', data=json.dumps({
'member_code': 'MEMBER_CODE', 'member_name': 'MEMBER_NAME',
'member_class': 'MEMBER_CLASS'}))
......@@ -359,7 +410,7 @@ reconnect=true
'http_status': 200, 'code': 'OK', 'msg': 'All Correct'})
def test_ok_query(self, mock_add_member):
with self.app.app_context():
with self.assertLogs(member.logger, level='INFO') as cm:
with self.assertLogs(member.LOGGER, level='INFO') as cm:
response = self.client.post('/member', data=json.dumps({
'member_code': 'MEMBER_CODE', 'member_name': 'MEMBER_NAME',
'member_class': 'MEMBER_CLASS'}))
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment