Skip to content

Commit 60bdbdd

Browse files
author
Gabriel Monroy
committed
improve code coverage on api views/models
1 parent 8abeea0 commit 60bdbdd

8 files changed

Lines changed: 73 additions & 119 deletions

File tree

api/models.py

Lines changed: 32 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
# pylint: disable=R0903,W0232
88

99
from __future__ import unicode_literals
10+
import importlib
1011
import os
1112
import subprocess
1213

@@ -19,18 +20,16 @@
1920
from django.dispatch.dispatcher import Signal
2021
from django.utils.encoding import python_2_unicode_compatible
2122

22-
from api import fields
23+
from api import fields, tasks
2324
from provider import import_provider_module
2425

26+
# import user-defined configuration management module
27+
CM = importlib.import_module(settings.CM_MODULE)
28+
2529

2630
# define custom signals
2731
release_signal = Signal(providing_args=['user', 'app'])
2832

29-
# define custom exceptions
30-
31-
32-
class ScalingError(Exception):
33-
pass
3433

3534
# base models
3635

@@ -90,7 +89,7 @@ def seed(self, user, **kwargs):
9089
"""
9190
Seeds the database with Providers for clouds supported by Deis.
9291
"""
93-
providers = (('ec2', 'ec2'), ('mock', 'mock'))
92+
providers = [(p, p) for p in settings.PROVIDER_MODULES]
9493
for p_id, p_type in providers:
9594
self.create(owner=user, id=p_id, type=p_type, creds='{}')
9695

@@ -120,19 +119,14 @@ class Meta:
120119
def __str__(self):
121120
return "{}-{}".format(self.id, self.get_type_display())
122121

123-
def flat(self):
124-
return {'id': self.id,
125-
'type': self.type,
126-
'creds': dict(self.creds)}
127-
128122

129123
@python_2_unicode_compatible
130124
class FlavorManager(models.Manager):
131125
"""Manage database interactions for :class:`Flavor`."""
132126

133127
def seed(self, user, **kwargs):
134128
"""Seed the database with default Flavors for each cloud region."""
135-
for provider_type in ('mock', 'ec2'):
129+
for provider_type in settings.PROVIDER_MODULES:
136130
provider = import_provider_module(provider_type)
137131
flavors = provider.seed_flavors()
138132
p = Provider.objects.get(owner=user, id=provider_type)
@@ -162,48 +156,13 @@ class Meta:
162156
def __str__(self):
163157
return self.id
164158

165-
def flat(self):
166-
return {'id': self.id,
167-
'creds': dict(self.provider.creds),
168-
'provider': self.provider.id,
169-
'params': self.params}
170-
171-
172-
@python_2_unicode_compatible
173-
class FormationManager(models.Manager):
174-
"""Manage database interactions for :class:`Formation`."""
175-
176-
def next_container_node(self, formation, container_type, reverse=False):
177-
count = []
178-
layers = formation.layer_set.filter(runtime=True)
179-
runtime_nodes = []
180-
for l in layers:
181-
runtime_nodes.extend(Node.objects.filter(
182-
formation=formation, layer=l).order_by('created'))
183-
container_map = {n: [] for n in runtime_nodes}
184-
containers = list(Container.objects.filter(
185-
formation=formation, type=container_type).order_by('created'))
186-
for c in containers:
187-
container_map[c.node].append(c)
188-
for n in container_map.keys():
189-
# (2, node3), (2, node2), (3, node1)
190-
count.append((len(container_map[n]), n))
191-
if not count:
192-
raise ScalingError('No nodes available for containers')
193-
count.sort()
194-
# reverse means order by greatest # of containers, otherwise fewest
195-
if reverse:
196-
count.reverse()
197-
return count[0][1]
198-
199159

200160
@python_2_unicode_compatible
201161
class Formation(UuidAuditedModel):
202162

203163
"""
204164
Formation of nodes used to host applications
205165
"""
206-
objects = FormationManager()
207166

208167
owner = models.ForeignKey(settings.AUTH_USER_MODEL)
209168
id = models.SlugField(max_length=64, unique=True)
@@ -381,6 +340,29 @@ def scale(self, formation, structure, **kwargs):
381340
return formation.converge()
382341
return formation.calculate()
383342

343+
def next_runtime_node(self, formation, container_type, reverse=False):
344+
count = []
345+
layers = formation.layer_set.filter(runtime=True)
346+
runtime_nodes = []
347+
for l in layers:
348+
runtime_nodes.extend(Node.objects.filter(
349+
formation=formation, layer=l).order_by('created'))
350+
container_map = {n: [] for n in runtime_nodes}
351+
containers = list(Container.objects.filter(
352+
formation=formation, type=container_type).order_by('created'))
353+
for c in containers:
354+
container_map[c.node].append(c)
355+
for n in container_map.keys():
356+
# (2, node3), (2, node2), (3, node1)
357+
count.append((len(container_map[n]), n))
358+
if not count:
359+
raise EnvironmentError('No nodes available for containers')
360+
count.sort()
361+
# reverse means order by greatest # of containers, otherwise fewest
362+
if reverse:
363+
count.reverse()
364+
return count[0][1]
365+
384366

385367
@python_2_unicode_compatible
386368
class Node(UuidAuditedModel):
@@ -555,7 +537,7 @@ def scale(self, app, structure, **kwargs):
555537
changed = True
556538
while diff < 0:
557539
# get the next node with the most containers
558-
node = Formation.objects.next_container_node(
540+
node = Node.objects.next_runtime_node(
559541
formation, container_type, reverse=True)
560542
# delete a container attached to that node
561543
for c in containers:
@@ -566,7 +548,7 @@ def scale(self, app, structure, **kwargs):
566548
break
567549
while diff > 0:
568550
# get the next node with the fewest containers
569-
node = Formation.objects.next_container_node(formation, container_type)
551+
node = Node.objects.next_runtime_node(formation, container_type)
570552
c = Container.objects.create(owner=app.owner,
571553
formation=formation,
572554
node=node,
@@ -836,11 +818,3 @@ def _publish_user_to_cm(**kwargs):
836818
post_save.connect(_publish_to_cm, sender=App, dispatch_uid='api.models')
837819
post_save.connect(_publish_to_cm, sender=Formation, dispatch_uid='api.models')
838820
post_save.connect(_publish_user_to_cm, sender=User, dispatch_uid='api.models')
839-
840-
841-
# now that we've defined models that may be imported by celery tasks
842-
# import tasks and user-defined config management module
843-
from api import tasks
844-
845-
import importlib
846-
CM = importlib.import_module(settings.CM_MODULE)

api/tests/auth.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
from __future__ import unicode_literals
88

99
import json
10+
import urllib
1011

1112
from django.test import TestCase
12-
import urllib
1313

1414

1515
class AuthTest(TestCase):
@@ -56,11 +56,11 @@ def test_auth(self):
5656
url = '/api/providers'
5757
response = self.client.get(url)
5858
self.assertEqual(response.status_code, 200)
59-
self.assertEqual(response.data['count'], 2)
59+
self.assertEqual(response.data['count'], 1)
6060
url = '/api/flavors'
6161
response = self.client.get(url)
6262
self.assertEqual(response.status_code, 200)
63-
self.assertEqual(response.data['count'], 10)
63+
self.assertEqual(response.data['count'], 2)
6464
# test logout and login
6565
url = '/api/auth/logout/'
6666
response = self.client.post(url, content_type='application/json')

api/tests/container.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,17 @@ def test_container_scale(self):
115115
self.assertEqual(response.status_code, 200)
116116
self.assertEqual(response.data['containers'], json.dumps(body))
117117

118+
def test_container_scale_errors(self):
119+
url = '/api/apps'
120+
body = {'formation': 'autotest'}
121+
response = self.client.post(url, json.dumps(body), content_type='application/json')
122+
self.assertEqual(response.status_code, 201)
123+
app_id = response.data['id']
124+
url = "/api/apps/{app_id}/scale".format(**locals())
125+
body = {'web': 'not_an_int'}
126+
response = self.client.post(url, json.dumps(body), content_type='application/json')
127+
self.assertContains(response, 'Invalid scaling format', status_code=400)
128+
118129
def test_container_scale_single_layer(self):
119130
# create & scale a single layer formation
120131
response = self.client.post('/api/formations', json.dumps(

api/tests/node.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -140,22 +140,28 @@ def test_node_scale(self):
140140
self.assertEqual(response.status_code, 200)
141141
self.assertEqual(response.data['nodes'], json.dumps(body))
142142

143-
def test_node_no_creds(self):
144-
url = '/api/providers/autotest'
145-
body = {'creds': json.dumps({})}
146-
response = self.client.patch(url, json.dumps(body), content_type='application/json')
147-
self.assertEqual(response.status_code, 200)
148-
# try to scale and a formation
143+
def test_node_scale_errors(self):
149144
url = '/api/formations'
150-
body = {'id': 'autotest', 'domain': 'localhost.localdomain'}
145+
body = {'id': 'autotest'}
151146
response = self.client.post(url, json.dumps(body), content_type='application/json')
152147
self.assertEqual(response.status_code, 201)
153148
formation_id = response.data['id'] # noqa
149+
url = '/api/formations/{formation_id}/scale'.format(**locals())
150+
body = {'runtime': 'not_an_int'}
151+
response = self.client.post(url, json.dumps(body), content_type='application/json')
152+
self.assertContains(response, 'Invalid scaling format', status_code=400)
153+
body = {'runtime': '1'}
154+
response = self.client.post(url, json.dumps(body), content_type='application/json')
155+
self.assertContains(response, 'Layer matching query does not exist', status_code=400)
156+
url = '/api/providers/autotest'
157+
body = {'creds': json.dumps({})}
158+
response = self.client.patch(url, json.dumps(body), content_type='application/json')
159+
self.assertEqual(response.status_code, 200)
154160
url = '/api/formations/{formation_id}/layers'.format(**locals())
155161
body = {'id': 'runtime', 'flavor': 'autotest', 'run_list': 'recipe[deis::runtime]'}
156162
response = self.client.post(url, json.dumps(body), content_type='application/json')
157163
self.assertEqual(response.status_code, 201)
158164
url = '/api/formations/{formation_id}/scale'.format(**locals())
159165
body = {'runtime': 1}
160166
response = self.client.post(url, json.dumps(body), content_type='application/json')
161-
self.assertEqual(response.status_code, 400)
167+
self.assertContains(response, 'No provider credentials available', status_code=400)

api/tests/provider.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ def test_provider(self):
2727
environment variables
2828
"""
2929
url = '/api/providers'
30-
creds = {'secret_key': 'x'*64, 'access_key': 1*20}
31-
body = {'id': 'autotest', 'type': 'ec2', 'creds': json.dumps(creds)}
30+
creds = {'secret_key': 'x' * 64, 'access_key': 1 * 20}
31+
body = {'id': 'autotest', 'type': 'mock', 'creds': json.dumps(creds)}
3232
response = self.client.post(url, json.dumps(body), content_type='application/json')
3333
self.assertEqual(response.status_code, 201)
3434
provider_id = response.data['id']
@@ -39,10 +39,10 @@ def test_provider(self):
3939
response = self.client.get(url)
4040
self.assertEqual(response.status_code, 200)
4141
new_creds = {'access_key': 'new', 'secret_key': 'new'}
42-
body = {'type': 'ec2', 'creds': json.dumps(new_creds)}
42+
body = {'type': 'mock', 'creds': json.dumps(new_creds)}
4343
response = self.client.patch(url, json.dumps(body), content_type='application/json')
4444
self.assertEqual(response.status_code, 200)
4545
self.assertEqual(response.data['creds'], json.dumps(new_creds))
46-
self.assertEqual(response.data['type'], 'ec2')
46+
self.assertEqual(response.data['type'], 'mock')
4747
response = self.client.delete(url)
4848
self.assertEqual(response.status_code, 204)

api/views.py

Lines changed: 5 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from Crypto.PublicKey import RSA
1010
from celery.canvas import group
1111
from django.contrib.auth.models import AnonymousUser, User
12-
from django.db.utils import IntegrityError
1312
from django.utils import timezone
1413
from rest_framework import permissions, status, viewsets
1514
from rest_framework.authentication import BaseAuthentication
@@ -128,18 +127,6 @@ class FormationViewSet(OwnerViewSet):
128127
serializer_class = serializers.FormationSerializer
129128
lookup_field = 'id'
130129

131-
def create(self, request, **kwargs):
132-
request._data = request.DATA.copy()
133-
try:
134-
return OwnerViewSet.create(self, request, **kwargs)
135-
except EnvironmentError as e:
136-
return Response(str(e), status=HTTP_400_BAD_REQUEST)
137-
except IntegrityError as e:
138-
if 'violates unique constraint' in str(e).lower():
139-
return Response('Formation with this Id already exists.',
140-
status=HTTP_400_BAD_REQUEST)
141-
raise e
142-
143130
def post_save(self, formation, created=False, **kwargs):
144131
if created:
145132
formation.build()
@@ -160,8 +147,6 @@ def scale(self, request, **kwargs):
160147
formation = self.get_object()
161148
try:
162149
databag = models.Node.objects.scale(formation, new_structure)
163-
except models.ScalingError as e:
164-
return Response(str(e), status=status.HTTP_400_BAD_REQUEST)
165150
except models.Layer.DoesNotExist as e:
166151
return Response(str(e), status=status.HTTP_400_BAD_REQUEST)
167152
return Response(databag, status=status.HTTP_200_OK,
@@ -187,10 +172,7 @@ def converge(self, request, **kwargs):
187172

188173
def destroy(self, request, **kwargs):
189174
formation = self.get_object()
190-
try:
191-
formation.destroy()
192-
except EnvironmentError as e:
193-
return Response(str(e), status=HTTP_400_BAD_REQUEST)
175+
formation.destroy()
194176
return Response(status=status.HTTP_204_NO_CONTENT)
195177

196178

@@ -225,11 +207,7 @@ def create(self, request, **kwargs):
225207
key = RSA.generate(2048)
226208
request.DATA['ssh_private_key'] = key.exportKey('PEM')
227209
request.DATA['ssh_public_key'] = key.exportKey('OpenSSH')
228-
try:
229-
return OwnerViewSet.create(self, request, **kwargs)
230-
except IntegrityError:
231-
return Response("Layer with this Id already exists",
232-
status=HTTP_400_BAD_REQUEST)
210+
return OwnerViewSet.create(self, request, **kwargs)
233211

234212
def post_save(self, layer, created=False, **kwargs):
235213
if created:
@@ -254,10 +232,7 @@ def get_object(self, *args, **kwargs):
254232

255233
def destroy(self, request, **kwargs):
256234
node = self.get_object()
257-
try:
258-
node.destroy()
259-
except EnvironmentError as e:
260-
return Response(str(e), status=HTTP_400_BAD_REQUEST)
235+
node.destroy()
261236
return Response(status=status.HTTP_204_NO_CONTENT)
262237

263238

@@ -275,16 +250,6 @@ class AppViewSet(OwnerViewSet):
275250
serializer_class = serializers.AppSerializer
276251
lookup_field = 'id'
277252

278-
def create(self, request, **kwargs):
279-
request._data = request.DATA.copy()
280-
try:
281-
return OwnerViewSet.create(self, request, **kwargs)
282-
except IntegrityError:
283-
return Response('App with this Id already exists.',
284-
status=HTTP_400_BAD_REQUEST)
285-
except EnvironmentError as e:
286-
return Response(str(e), status=HTTP_400_BAD_REQUEST)
287-
288253
def post_save(self, app, created=False, **kwargs):
289254
if created:
290255
app.build()
@@ -300,11 +265,9 @@ def scale(self, request, **kwargs):
300265
return Response('Invalid scaling format', status=HTTP_400_BAD_REQUEST)
301266
app = self.get_object()
302267
try:
303-
changed = models.Container.objects.scale(app, new_structure)
304-
except models.ScalingError as e:
268+
models.Container.objects.scale(app, new_structure)
269+
except EnvironmentError as e:
305270
return Response(str(e), status=status.HTTP_400_BAD_REQUEST)
306-
if not changed:
307-
return Response(status=status.HTTP_204_NO_CONTENT)
308271
# save new structure now that scaling was successful
309272
app.containers.update(new_structure)
310273
app.save()

deis/settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,9 @@
275275
CM_MODULE = 'cm.mock'
276276
TEMPDIR = tempfile.mkdtemp(prefix='deis')
277277

278+
# default providers, typically overriden in local_settings to include ec2, etc.
279+
PROVIDER_MODULES = ('mock',)
280+
278281
# Create a file named "local_settings.py" to contain sensitive settings data
279282
# such as database configuration, admin email, or passwords and keys. It
280283
# should also be used for any settings which differ between development

provider/__init__.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,5 @@ def import_provider_module(provider_type):
55
"""
66
Return the module for a provider.
77
"""
8-
try:
9-
tasks = importlib.import_module('provider.' + provider_type)
10-
except ImportError as e:
11-
raise e
8+
tasks = importlib.import_module('provider.' + provider_type)
129
return tasks

0 commit comments

Comments
 (0)