From 1a29d9407e377b8772ee042d2d00aadcdd646794 Mon Sep 17 00:00:00 2001 From: Ekaterina Chernova Date: Wed, 18 Apr 2018 15:04:55 +0300 Subject: [PATCH] Move 'required'check to the Field validator * Use one type of return value for validate method * Fix some typos --- kqueen/blueprints/api/test_cluster.py | 75 ++++--- kqueen/blueprints/api/test_crud.py | 93 ++++---- kqueen/blueprints/api/test_helpers.py | 20 +- kqueen/blueprints/api/test_organization.py | 4 +- kqueen/blueprints/api/test_provisioner.py | 4 +- kqueen/blueprints/api/test_user.py | 7 +- kqueen/conftest.py | 244 +++++++++++++++------ kqueen/engines/test_manual.py | 13 +- kqueen/models.py | 4 +- kqueen/storages/etcd.py | 44 ++-- kqueen/storages/test_model_fields.py | 21 +- kqueen/tests/test_manual_cluster.py | 7 +- kqueen/tests/test_models.py | 6 +- 13 files changed, 350 insertions(+), 192 deletions(-) diff --git a/kqueen/blueprints/api/test_cluster.py b/kqueen/blueprints/api/test_cluster.py index 8f894027..c3c70d4c 100644 --- a/kqueen/blueprints/api/test_cluster.py +++ b/kqueen/blueprints/api/test_cluster.py @@ -1,19 +1,29 @@ from .test_crud import BaseTestCRUD from flask import url_for from kqueen.config import current_config -from kqueen.conftest import cluster -from uuid import uuid4 +from kqueen.conftest import TestCluster, TestProvisioner import json import pytest +from uuid import uuid4 config = current_config() class TestClusterCRUD(BaseTestCRUD): - def get_object(self): - obj = cluster() + def setup(self): + super().setup() + self.user = self.test_user.obj + self.test_provisioner = TestProvisioner(self.test_user) + self.provisioner = self.test_provisioner.obj + + def teardown(self): + super().teardown() + self.test_provisioner.destroy() + + def get_object(self): + obj = TestCluster() return obj def get_edit_data(self): @@ -74,7 +84,7 @@ def test_crud_list(self): ) assert obj.get_dict(expand=True) in data - @pytest.mark.parametrize('cluster_id,status_code', [ + @pytest.mark.parametrize('cluster_id, status_code', [ (uuid4(), 404), ('wrong-uuid', 404), ]) @@ -148,14 +158,13 @@ def test_progress_format(self): assert 'progress' in response.json assert 'result' in response.json - def test_create(self, provisioner, user): - provisioner.save() - user.save() + def test_create(self): + self.provisioner.save() post_data = { 'name': 'Testing cluster', - 'provisioner': 'Provisioner:{}'.format(provisioner.id), - 'owner': 'User:{}'.format(user.id) + 'provisioner': 'Provisioner:{}'.format(self.provisioner.id), + 'owner': 'User:{}'.format(self.user.id) } response = self.client.post( @@ -169,11 +178,11 @@ def test_create(self, provisioner, user): assert 'id' in response.json assert response.json['name'] == post_data['name'] - assert response.json['provisioner'] == provisioner.get_dict(expand=True) + assert response.json['provisioner'] == self.provisioner.get_dict(expand=True) - def test_provision_after_create(self, provisioner, user, monkeypatch): - provisioner.save() - user.save() + def test_provision_after_create(self, monkeypatch): + self.provisioner.save() + self.user.save() def fake_provision(self, *args, **kwargs): self.cluster.name = 'Provisioned' @@ -181,12 +190,12 @@ def fake_provision(self, *args, **kwargs): return True, None - monkeypatch.setattr(provisioner.get_engine_cls(), 'provision', fake_provision) + monkeypatch.setattr(self.provisioner.get_engine_cls(), 'provision', fake_provision) post_data = { 'name': 'Testing cluster', - 'provisioner': 'Provisioner:{}'.format(provisioner.id), - 'owner': 'User:{}'.format(user.id) + 'provisioner': 'Provisioner:{}'.format(self.provisioner.id), + 'owner': 'User:{}'.format(self.user.id) } response = self.client.post( @@ -202,19 +211,19 @@ def fake_provision(self, *args, **kwargs): assert response.status_code == 200 assert obj.name == 'Provisioned' - def test_provision_failed(self, provisioner, user, monkeypatch): - provisioner.save() - user.save() + def test_provision_failed(self, monkeypatch): + self.provisioner.save() + self.user.save() def fake_provision(self, *args, **kwargs): return False, 'Testing msg' - monkeypatch.setattr(provisioner.get_engine_cls(), 'provision', fake_provision) + monkeypatch.setattr(self.provisioner.get_engine_cls(), 'provision', fake_provision) post_data = { 'name': 'Testing cluster', - 'provisioner': 'Provisioner:{}'.format(provisioner.id), - 'owner': 'User:{}'.format(user.id) + 'provisioner': 'Provisioner:{}'.format(self.provisioner.id), + 'owner': 'User:{}'.format(self.user.id) } response = self.client.post( @@ -230,15 +239,15 @@ def fake_provision(self, *args, **kwargs): config.get('PROVISIONER_UNKNOWN_STATE'), config.get('PROVISIONER_ERROR_STATE') ]) - def test_provision_failed_with_unhealthy_provisioner(self, provisioner, user, provisioner_state): - provisioner.state = provisioner_state - provisioner.save(check_status=False) - user.save() + def test_provision_failed_with_unhealthy_provisioner(self, provisioner_state): + self.provisioner.state = provisioner_state + self.provisioner.save(check_status=False) + self.user.save() post_data = { 'name': 'Testing cluster', - 'provisioner': 'Provisioner:{}'.format(provisioner.id), - 'owner': 'User:{}'.format(user.id) + 'provisioner': 'Provisioner:{}'.format(self.provisioner.id), + 'owner': 'User:{}'.format(self.user.id) } response = self.client.post( @@ -274,8 +283,11 @@ def test_error_codes(self, data, code, content_type): assert response.status_code == code def test_cluster_list_run_get_state(self, monkeypatch): + clusters_to_remove = [] for _ in range(10): - c = cluster() + test_cluster = TestCluster() + clusters_to_remove.append(test_cluster) + c = test_cluster.obj c.save() def fake_get_state(self): @@ -298,3 +310,6 @@ def fake_get_state(self): assert obj.metadata, 'get_state wasn\'t executed for cluster {}'.format(obj) assert obj.metadata['executed'], 'get_state wasn\'t executed' + + for c in clusters_to_remove: + c.destroy() diff --git a/kqueen/blueprints/api/test_crud.py b/kqueen/blueprints/api/test_crud.py index b95366c5..08e2d667 100644 --- a/kqueen/blueprints/api/test_crud.py +++ b/kqueen/blueprints/api/test_crud.py @@ -1,14 +1,15 @@ from flask import url_for -from kqueen.conftest import auth_header, user_with_namespace, get_auth_token +from kqueen.conftest import AuthHeader, TestUserWithNamespace, TestUser, etcd_setup from kqueen.config import current_config +import faker import json import pytest config = current_config() -@pytest.mark.usefixtures('client_class') +@pytest.mark.usefixtures('client_class', 'etcd_setup') class BaseTestCRUD: def get_object(self): raise NotImplementedError @@ -52,13 +53,25 @@ def get_urls(self, pk=None): } def setup(self): - self.obj = self.get_object() + etcd_setup() + self.test_object = self.get_object() + self.obj = self.test_object.obj self.obj.save() - - self.auth_header = auth_header(self.client) + self.test_user = TestUser() + self.test_auth_header = AuthHeader(self.test_user) + self.auth_header = self.test_auth_header.get(self.client) self.namespace = self.auth_header['X-Test-Namespace'] + # self.user = self.test_user.obj + # self.test_provisioner = TestProvisioner(self.test_user) + # self.provisioner = self.test_provisioner.obj self.urls = self.get_urls() + def teardown(self): + self.test_auth_header.destroy() + print('!!!!!!!!!!!deleting ' + self.test_object.obj.id) + self.test_object.destroy() + # self.test_provisioner.destroy() + def test_crud_create(self): data = self.get_create_data() @@ -98,7 +111,6 @@ def fake_save(self, *args, **kwargs): assert response.status_code == 500 def test_crud_get(self): - self.obj.save() response = self.client.get( self.urls['get'], @@ -117,7 +129,7 @@ def test_crud_get(self): def test_crud_list(self): response = self.client.get( self.urls['list'], - headers=self.auth_header, + headers=self.auth_header ) data = response.json @@ -186,7 +198,7 @@ def fake_save(self, *args, **kwargs): def test_crud_delete(self): response = self.client.delete( self.urls['delete'], - headers=self.auth_header, + headers=self.auth_header ) assert response.status_code == 200 @@ -197,48 +209,40 @@ def test_crud_delete(self): ) def test_crud_delete_failed(self, monkeypatch): + original_delete = getattr(self.obj.__class__, 'delete') + def fake_delete(self, *args, **kwargs): raise Exception('Testing') monkeypatch.setattr(self.obj.__class__, 'delete', fake_delete) - response = self.client.delete( - self.urls['delete'], - headers=self.auth_header, - ) + response = self.client.delete(self.urls['delete'], headers=self.auth_header) assert response.status_code == 500 + monkeypatch.setattr(self.obj.__class__, 'delete', original_delete) # # namespacing tests # - @pytest.fixture - def setup_namespace(self): - self.user1 = user_with_namespace() - self.user2 = user_with_namespace() - @pytest.mark.usefixtures('setup_namespace') - def test_namespacing(self, client): - obj = self.get_object() + def test_namespacing(self): + user1 = TestUserWithNamespace() + user1.auth_header = AuthHeader(user1).get(self.client) + user2 = TestUserWithNamespace() + user2.auth_header = AuthHeader(user2).get(self.client) + + # skip if object class isn't namespaced - if not obj.__class__.is_namespaced(): - pytest.skip('Class {} isn\'t namespaced'.format(obj.__class__.__name__)) + if not self.obj.__class__.is_namespaced(): + pytest.skip('Class {} isn\'t namespaced'.format(self.obj.__class__.__name__)) objs = {} # create objects for both users - for u in [self.user1, self.user2]: + for u in [user1, user2]: data = self.get_create_data() - auth_header = get_auth_token(self.client, u) - headers = { - 'Authorization': '{} {}'.format( - config.get('JWT_AUTH_HEADER_PREFIX'), - auth_header - ) - } - # TODO: fix this # Dirty hack to make testing data namespaced. organization_data = { @@ -248,13 +252,14 @@ def test_namespacing(self, client): response = self.client.post( url_for('api.organization_create'), data=json.dumps(organization_data), - headers=headers, + headers=u.auth_header, content_type='application/json', ) organization_ref = 'Organization:{}'.format(response.json['id']) + profile = faker.Faker().simple_profile() owner_data = { - 'username': 'Test owner', - 'email': 'owner@pytest.org', + 'username': profile['username'], + 'email': profile['mail'], 'password': 'pytest', 'organization': organization_ref, 'role': 'admin', @@ -263,7 +268,7 @@ def test_namespacing(self, client): response = self.client.post( url_for('api.user_create'), data=json.dumps(owner_data), - headers=headers, + headers=u.auth_header, content_type='application/json', ) if 'owner' in data: @@ -277,7 +282,7 @@ def test_namespacing(self, client): response = self.client.post( url_for('api.provisioner_create'), data=json.dumps(provisioner_data), - headers=headers, + headers=u.auth_header, content_type='application/json', ) data['provisioner'] = 'Provisioner:{}'.format(response.json['id']) @@ -285,36 +290,30 @@ def test_namespacing(self, client): response = self.client.post( self.urls['create'], data=json.dumps(data), - headers=headers, + headers=u.auth_header, content_type='application/json', ) print(response.data.decode(response.charset)) - objs[u.namespace] = response.json['id'] + objs[u.obj.namespace] = response.json['id'] print(response.json) + # test use can't read other's object - for u in [self.user1, self.user2]: - auth_header = get_auth_token(self.client, u) - headers = { - 'Authorization': '{} {}'.format( - config.get('JWT_AUTH_HEADER_PREFIX'), - auth_header - ) - } + for u in [user1, user2]: for ns, pk in objs.items(): url = self.get_urls(pk)['get'] - if ns == u.namespace: + if ns == u.obj.namespace: req_code = 200 else: req_code = 404 response = self.client.get( url, - headers=headers, + headers=u.auth_header, content_type='application/json', ) diff --git a/kqueen/blueprints/api/test_helpers.py b/kqueen/blueprints/api/test_helpers.py index d6f54d68..55e9f0d3 100644 --- a/kqueen/blueprints/api/test_helpers.py +++ b/kqueen/blueprints/api/test_helpers.py @@ -1,21 +1,19 @@ from .helpers import get_object -from kqueen.conftest import cluster -from kqueen.conftest import user +# from kqueen.conftest import cluster +# from kqueen.conftest import user from kqueen.storages.exceptions import BackendError import pytest +@pytest.mark.usefixtures('cluster', 'user') class TestGetObject: - def setup(self): - self.obj = cluster() - self.obj.save() - self.user = user() - def test_get_objects(self): - obj = get_object(self.obj.__class__, self.obj.id, self.user) + def test_get_objects(self, cluster, user): + cluster.save() + obj = get_object(cluster.__class__, cluster.id, user) - assert obj.get_dict(True) == self.obj.get_dict(True) + assert obj.get_dict(True) == obj.get_dict(True) @pytest.mark.parametrize('bad_user', [ 'None', @@ -23,6 +21,6 @@ def test_get_objects(self): None, {}, ]) - def test_get_object_malformed_user(self, bad_user): + def test_get_object_malformed_user(self, cluster, bad_user): with pytest.raises(BackendError, match='Missing namespace for class'): - get_object(self.obj.__class__, self.obj.id, bad_user) + get_object(cluster.__class__, cluster.id, bad_user) diff --git a/kqueen/blueprints/api/test_organization.py b/kqueen/blueprints/api/test_organization.py index 5ac6969c..742fa906 100644 --- a/kqueen/blueprints/api/test_organization.py +++ b/kqueen/blueprints/api/test_organization.py @@ -1,7 +1,7 @@ from .test_crud import BaseTestCRUD from flask import url_for from kqueen.config import current_config -from kqueen.conftest import organization +from kqueen.conftest import TestOrganization import pytest @@ -10,7 +10,7 @@ class TestOrganizationCRUD(BaseTestCRUD): def get_object(self): - return organization() + return TestOrganization() def get_edit_data(self): return { diff --git a/kqueen/blueprints/api/test_provisioner.py b/kqueen/blueprints/api/test_provisioner.py index d7f4e9d2..73769f7d 100644 --- a/kqueen/blueprints/api/test_provisioner.py +++ b/kqueen/blueprints/api/test_provisioner.py @@ -1,13 +1,13 @@ from .test_crud import BaseTestCRUD from flask import url_for -from kqueen.conftest import provisioner +from kqueen.conftest import TestProvisioner from kqueen.engines.__init__ import __all__ as all_engines from pprint import pprint as print class TestProvisionerCRUD(BaseTestCRUD): def get_object(self): - return provisioner() + return TestProvisioner() def get_edit_data(self): return { diff --git a/kqueen/blueprints/api/test_user.py b/kqueen/blueprints/api/test_user.py index a24c5cea..2bbf244c 100644 --- a/kqueen/blueprints/api/test_user.py +++ b/kqueen/blueprints/api/test_user.py @@ -1,6 +1,6 @@ from .test_crud import BaseTestCRUD from flask import url_for -from kqueen.conftest import user +from kqueen.conftest import TestUser from kqueen.config import current_config import bcrypt @@ -12,7 +12,7 @@ class TestUserCRUD(BaseTestCRUD): def get_object(self): - return user() + return TestUser() def get_edit_data(self): return { @@ -24,6 +24,7 @@ def get_create_data(self): data = self.obj.get_dict() data['id'] = None data['organization'] = 'Organization:{}'.format(self.obj.organization.id) + data['username'] = 'newusername' return data @@ -58,7 +59,7 @@ def test_whoami(self): assert response.json == self.obj.get_dict(expand=True) def test_namespace(self): - user = self.get_object() + user = self.obj assert user.namespace == user.organization.namespace diff --git a/kqueen/conftest.py b/kqueen/conftest.py index 0560f404..3a8a7392 100644 --- a/kqueen/conftest.py +++ b/kqueen/conftest.py @@ -18,6 +18,156 @@ fake = Faker() +class TestCluster: + def __init__(self, test_provisioner=None, test_user=None): + test_user = test_user if test_user is not None else TestUser() + + if not test_provisioner: + test_provisioner = TestProvisioner(test_user=test_user) + self.test_provisioner = test_provisioner + + owner = test_user.obj + provisioner = self.test_provisioner.obj + + provisioner.state = config.get('PROVISIONER_OK_STATE') + provisioner.save(check_status=False) + + _uuid = uuid.uuid4() + create_kwargs = { + 'id': _uuid, + 'name': 'Name for cluster {}'.format(_uuid), + 'provisioner': provisioner, + 'state': config.get('CLUSTER_UNKNOWN_STATE'), + 'kubeconfig': yaml.load(open('kubeconfig_localhost', 'r').read()), + 'created_at': datetime.datetime.utcnow().replace(microsecond=0), + 'owner': owner + } + self.obj = Cluster.create(owner.namespace, **create_kwargs) + + def destroy(self): + try: + self.obj.delete() + self.test_provisioner.destroy() + except Exception: + # Provisioner may be already deleted + pass + + +class TestProvisioner: + def __init__(self, test_user=None): + if test_user is None: + test_user = TestUser() + self.test_user = test_user + + owner = test_user.obj + create_kwargs = { + 'name': 'Fixtured provisioner', + 'engine': 'kqueen.engines.ManualEngine', + 'owner': owner + } + self.obj = Provisioner.create(owner.namespace, **create_kwargs) + + def destroy(self): + try: + self.obj.delete() + self.test_user.destroy() + except Exception: + # Provisioner may be already deleted + pass + + +class TestUser: + def __init__(self): + profile = fake.simple_profile() + user = User.create( + None, + username=profile['username'], + password=profile['username'] + 'password', + organization=TestOrganization().obj, + role='superadmin', + active=True + ) + user.save() + self.obj = user + + def destroy(self): + try: + self.obj.delete() + except: + # User be may already deleted + pass + + +class TestOrganization: + def __init__(self): + """Prepare organization object.""" + organization = Organization( + None, + name='DemoOrg', + namespace='demoorg', + ) + organization.save() + + self.obj = organization + + def destroy(self): + try: + self.obj.delete() + except Exception: + # Organization be may already deleted + pass + + +class AuthHeader: + def __init__(self, test_user=None): + self.user = test_user if test_user is not None else TestUser() + + def get(self, client): + """ + Get JWT access token and convert it to HTTP header. + + Args: + client: Flask client + + Returns: + dict: {'Authorization': 'JWT access_token'} + + """ + _user = self.user.obj + token = get_auth_token(client, _user) + + return { + 'Authorization': '{token_prefix} {token}'.format( + token_prefix=config.get('JWT_AUTH_HEADER_PREFIX'), + token=token, + ), + 'X-Test-Namespace': _user.namespace, + 'X-User': str(_user.id), + } + + def destroy(self): + self.user.destroy() + + +class TestUserWithNamespace: + def __init__(self): + self.test_org = TestOrganization() + org = self.test_org.obj + + namespace = fake.user_name() + org.namespace = namespace + org.save() + + self.test_user = TestUser() + _user = self.test_user.obj + _user.organization = org + _user.save() + self.obj = _user + + def destroy(self): + self.test_org.destroy() + self.test_user.destroy() + @pytest.fixture(autouse=True, scope='session') def app(): """Prepare app.""" @@ -40,43 +190,18 @@ def etcd_setup(): @pytest.fixture def cluster(): """Create cluster with manual provisioner.""" - _uuid = uuid.uuid4() - _user = user() - - prov = Provisioner( - _user.namespace, - name='Fixtured provisioner', - engine='kqueen.engines.ManualEngine', - owner=_user - ) - prov.state = config.get('PROVISIONER_OK_STATE') - prov.save(check_status=False) - - create_kwargs = { - 'id': _uuid, - 'name': 'Name for cluster {}'.format(_uuid), - 'provisioner': prov, - 'state': config.get('CLUSTER_UNKNOWN_STATE'), - 'kubeconfig': yaml.load(open('kubeconfig_localhost', 'r').read()), - 'created_at': datetime.datetime.utcnow().replace(microsecond=0), - 'owner': _user - } - - return Cluster.create(_user.namespace, **create_kwargs) + test_cluster = TestCluster() + yield test_cluster.obj + test_cluster.destroy() @pytest.fixture def provisioner(): """Create dummy manual provisioner.""" - _user = user() - - create_kwargs = { - 'name': 'Fixtured provisioner', - 'engine': 'kqueen.engines.ManualEngine', - 'owner': _user - } - - return Provisioner.create(_user.namespace, **create_kwargs) + test_provisioner = TestProvisioner() + test_provisioner.obj.save() + yield test_provisioner.obj + test_provisioner.destroy() def get_auth_token(_client, _user): @@ -109,7 +234,7 @@ def get_auth_token(_client, _user): return token -@pytest.fixture +@pytest.fixture() def auth_header(client): """ Get JWT access token and convert it to HTTP header. @@ -121,10 +246,10 @@ def auth_header(client): dict: {'Authorization': 'JWT access_token'} """ - _user = user() + _user = TestUser().obj token = get_auth_token(client, _user) - return { + yield { 'Authorization': '{token_prefix} {token}'.format( token_prefix=config.get('JWT_AUTH_HEADER_PREFIX'), token=token, @@ -133,46 +258,35 @@ def auth_header(client): 'X-User': str(_user.id), } - -@pytest.fixture -def organization(): - """Prepare organization object.""" - organization = Organization( - None, - name='DemoOrg', - namespace='demoorg', - ) - organization.save() - - return organization - - +# +# @pytest.fixture +# def organization(): +# """Prepare organization object.""" +# organization = Organization( +# None, +# name='DemoOrg', +# namespace='demoorg', +# ) +# organization.save() +# +# return organization + +# @pytest.fixture(scope='class') def user(): """Prepare user object.""" + profile = fake.simple_profile() user = User.create( None, username=profile['username'], password=profile['username'] + 'password', - organization=organization(), + organization=TestOrganization().obj, role='superadmin', active=True ) user.save() - return user - - -@pytest.fixture -def user_with_namespace(): - - org = organization() - org.namespace = fake.user_name() - org.save() - - _user = user() - _user.organization = org - _user.save() - - return _user + yield user + with app().app_context(): + user.delete() diff --git a/kqueen/engines/test_manual.py b/kqueen/engines/test_manual.py index 42cbe610..da36be05 100644 --- a/kqueen/engines/test_manual.py +++ b/kqueen/engines/test_manual.py @@ -1,8 +1,8 @@ from .manual import ManualEngine from flask import url_for from kqueen.config import current_config -from kqueen.conftest import auth_header -from kqueen.conftest import user +from kqueen.conftest import AuthHeader +from kqueen.conftest import TestUser from kqueen.models import Cluster from kqueen.models import Provisioner @@ -26,7 +26,8 @@ @pytest.mark.usefixtures('client_class') class ManualEngineBase: def setup(self): - _user = user() + self.test_user = TestUser() + _user = self.test_user.obj create_kwargs_provisioner = { 'name': 'Testing manual', 'engine': 'kqueen.engines.ManualEngine', @@ -51,9 +52,13 @@ def setup(self): self.engine = ManualEngine(cluster=self.cluster) # client setup - self.auth_header = auth_header(self.client) + self.auth_header = AuthHeader(self.test_user).get(self.client) self.namespace = self.auth_header['X-Test-Namespace'] + def teardown(self): + self.auth_header.destroy() + self.cluster.delete() + class TestClusterAction(ManualEngineBase): def test_initialization(self): diff --git a/kqueen/models.py b/kqueen/models.py index f23c1541..44017b76 100644 --- a/kqueen/models.py +++ b/kqueen/models.py @@ -440,8 +440,8 @@ class User(Model, metaclass=ModelMeta): global_namespace = True id = IdField(required=True) - username = StringField(required=True) - email = StringField(required=False) + username = StringField(required=True, unique=True) + email = StringField(required=False, unique=True) password = PasswordField(required=True) organization = RelationField(required=True, remote_class_name='Organization') created_at = DatetimeField(default=datetime.utcnow) diff --git a/kqueen/storages/etcd.py b/kqueen/storages/etcd.py index 72d9672b..e11a3183 100644 --- a/kqueen/storages/etcd.py +++ b/kqueen/storages/etcd.py @@ -40,6 +40,7 @@ def __init__(self, *args, **kwargs): Attributes: required (bool): Set field to be required before saving the model. Defaults to False. + unique (bool): Set field to be unique within the Model before saving the model. Defaults to False. value: Set field value. """ @@ -48,6 +49,7 @@ def __init__(self, *args, **kwargs): self.required = kwargs.get('required', False) self.encrypted = kwargs.get('encrypted', False) self.default = kwargs.get('default', None) + self.unique = kwargs.get('unique', False) # value can be passed as args[0] or kwargs['value'] if len(args) >= 1: @@ -110,12 +112,16 @@ def empty(self): def validate(self): """ This method is called before saving model and can be used to validate format or - consitence of fields + consistence of fields Returns: - Result of validation. True for success, False otherwise. + tuple: Result of validation (bool:True for success, False otherwise), error message (str). """ - return True + + if self.required and self.value is None: + return False, 'Field is required' + + return True, None def _get_encryption_key(self): """ @@ -341,9 +347,11 @@ def validate(self): class_name = self.value.__class__.__name__ selfid = self.value.id except Exception: - return False + return False, 'Class name and id should be specified for a RelationField' - return class_name and selfid + if not (class_name and selfid): + return False, 'Class name and id should not be None' + return True, None def set_value(self, value, **kwargs): """Detect serialized format and deserialized according to format.""" @@ -503,6 +511,7 @@ def load(cls, namespace, object_id): key = '{}{}'.format(cls.get_db_prefix(namespace), str(object_id)) try: + print('********************' + key) response = current_app.db.client.read(key) value = response.value except etcd.EtcdKeyNotFound: @@ -584,7 +593,7 @@ def save(self, validate=True, assign_id=True): Attributes: validate (bool): Validate model before saving. Defaults to `True`. - assign_id (bool): Assing id (if missing) before saving model. Defaults to `True` + assign_id (bool): Assigning id (if missing) before saving model. Defaults to `True` Return: bool: `True` if model was saved without errors, `False` otherwise. @@ -621,7 +630,7 @@ def validate(self): * Required fields Returns: - Validation result. `True` for passed, `False` for failed. + (tuple): (bool) validation result: (`True` for passed, `False` for failed), """ fields = self.__class__.get_field_names() @@ -629,14 +638,17 @@ def validate(self): hidden_field = '_{}'.format(field) field_object = getattr(self, hidden_field) - # validation - # TODO: move to validate method of Field - if field_object.required and field_object.value is None: - return False, 'Required field {} is None'.format(field) - - if field_object.value and not field_object.validate(): - return False, 'Field {} validation failed'.format(field) - + result, err_msg = field_object.validate() + if not result: + return False, 'Field {name} validation failed: {reason}'.format(name=field, reason=err_msg) + + if field_object.unique and field_object.value: + for k, v in self.list(self.namespace).items(): + # Skip checking for uniqueness while on object update + if getattr(v, 'id') == self.id: + continue + if getattr(v, field) == field_object.value: + return False, 'Field {name} should be unique'.format(name=field) return True, None def _expand(self, obj): @@ -692,8 +704,6 @@ def __eq__(self, other): return False # TODO: implement autogenerated fields (generete them if missing) -# TODO: implement unique field # TODO: implement predefined values for fields -# TODO: use validation # TODO: add is_saved method # TODO: add load raw data method diff --git a/kqueen/storages/test_model_fields.py b/kqueen/storages/test_model_fields.py index 4ae44788..3cf11a44 100644 --- a/kqueen/storages/test_model_fields.py +++ b/kqueen/storages/test_model_fields.py @@ -16,7 +16,7 @@ import pytest -def create_model(required=False, global_ns=False, encrypted=False): +def create_model(required=False, global_ns=False, encrypted=False, unique=False): class TestModel(Model, metaclass=ModelMeta): if global_ns: global_namespace = global_ns @@ -25,23 +25,25 @@ class TestModel(Model, metaclass=ModelMeta): string = StringField(required=required, encrypted=encrypted) json = JSONField(required=required, encrypted=encrypted) password = PasswordField(required=required, encrypted=encrypted) - relation = RelationField(required=required, encrypted=encrypted) + relation = RelationField(required=required, encrypted=encrypted, remote_class_name='TestModel') datetime = DatetimeField(required=required, encrypted=encrypted) boolean = BoolField(required=required, encrypted=encrypted) _required = required _global_ns = global_ns _encrypted = encrypted + _unique = unique if _global_ns: _namespace = None else: _namespace = namespace - print('Creating model with: required: {}, global_ns: {}, encrypted: {}'.format( + print('Creating model with: required: {}, global_ns: {}, encrypted: {}, unique: {}'.format( required, global_ns, - encrypted + encrypted, + unique )) return TestModel @@ -160,6 +162,16 @@ def test_required(self, required): assert validation != required +class TestUniqueFields: + @pytest.mark.parametrize('unique', [True, False]) + def test_unique(self, unique): + model = create_model(unique=unique) + obj = model(namespace, **model_kwargs) + + validation, _ = obj.validate() + assert validation != unique + + class TestGetFieldNames: def test_get_field_names(self, get_object): field_names = get_object.__class__.get_field_names() @@ -535,6 +547,7 @@ def fake(self, class_name): return get_object.__class__ monkeypatch.setattr(RelationField, '_get_related_class', fake) + monkeypatch.setattr(RelationField, 'validate', (True, None)) # load information about test setup namespace = get_object.__class__._namespace diff --git a/kqueen/tests/test_manual_cluster.py b/kqueen/tests/test_manual_cluster.py index 9d590837..0d3deff4 100644 --- a/kqueen/tests/test_manual_cluster.py +++ b/kqueen/tests/test_manual_cluster.py @@ -1,5 +1,5 @@ from flask import url_for -from kqueen.conftest import auth_header +from kqueen.conftest import AuthHeader from kqueen.models import User from datetime import datetime @@ -11,12 +11,15 @@ @pytest.mark.usefixtures('client_class') class TestInsertManualCluster: def setup(self): - self.auth_header = auth_header(self.client) + self.auth_header = AuthHeader().get(self.client) self.namespace = self.auth_header['X-Test-Namespace'] self.user = User.load(None, self.auth_header['X-User']) self.provisioner_id = None + def teardown(self): + self.auth_header.destroy() + def test_run(self): self.create_provisioner() self.get_provisioners() diff --git a/kqueen/tests/test_models.py b/kqueen/tests/test_models.py index cd79c5ed..ac7ed081 100644 --- a/kqueen/tests/test_models.py +++ b/kqueen/tests/test_models.py @@ -26,6 +26,7 @@ def test_get_model_name(self, model_class, req): assert model_name == req +@pytest.mark.usefixtures('app') class TestClusterModel: def test_create(self, cluster): validation, _ = cluster.validate() @@ -49,12 +50,11 @@ def test_id_generation(self, provisioner, user): empty = Cluster(provisioner._object_namespace, name='test', provisioner=provisioner, owner=user) empty.save() - def test_added_key(self, cluster): + def test_added_key(self, cluster, app): """Test _key is added after saving""" - cluster.id = None - assert not hasattr(cluster, '_key') + cluster.id = None cluster.save() assert hasattr(cluster, '_key'), 'Saved object is missing _key'