@@ -1,7 +1,8 @@ | |||
# -*- coding: utf-8 -*- | |||
# Generated by Django 1.9.1 on 2016-01-07 19:39 | |||
# Generated by Django 1.9.1 on 2016-01-07 19:49 | |||
from __future__ import unicode_literals | |||
from django.conf import settings | |||
from django.db import migrations, models | |||
import django.db.models.deletion | |||
import uuid | |||
@@ -12,6 +13,7 @@ class Migration(migrations.Migration): | |||
initial = True | |||
dependencies = [ | |||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), | |||
] | |||
operations = [ | |||
@@ -22,14 +24,14 @@ class Migration(migrations.Migration): | |||
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')), | |||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), | |||
('site', models.CharField(max_length=255)), | |||
('title', models.CharField(default=True, max_length=255, null=True)), | |||
('username', models.CharField(default=True, max_length=255, null=True)), | |||
('email', models.EmailField(default=True, max_length=254, null=True)), | |||
('description', models.TextField(default=True, null=True)), | |||
('url', models.URLField(default=True, null=True)), | |||
('title', models.CharField(blank=True, max_length=255, null=True)), | |||
('username', models.CharField(blank=True, max_length=255, null=True)), | |||
('email', models.EmailField(blank=True, max_length=254, null=True)), | |||
('description', models.TextField(blank=True, null=True)), | |||
('url', models.URLField(blank=True, null=True)), | |||
], | |||
options={ | |||
'abstract': False, | |||
'verbose_name_plural': 'Entries', | |||
}, | |||
), | |||
migrations.CreateModel( | |||
@@ -40,10 +42,18 @@ class Migration(migrations.Migration): | |||
('settings', models.TextField()), | |||
('length', models.IntegerField(default=12)), | |||
], | |||
options={ | |||
'verbose_name_plural': 'Password info', | |||
}, | |||
), | |||
migrations.AddField( | |||
model_name='entry', | |||
name='password', | |||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.PasswordInfo'), | |||
), | |||
migrations.AddField( | |||
model_name='entry', | |||
name='user', | |||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='entries', to=settings.AUTH_USER_MODEL), | |||
), | |||
] |
@@ -1,6 +1,7 @@ | |||
import uuid | |||
from django.db import models | |||
from django.contrib.auth.models import User | |||
class DateMixin(models.Model): | |||
@@ -26,6 +27,7 @@ class PasswordInfo(models.Model): | |||
class Entry(DateMixin): | |||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) | |||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='entries') | |||
site = models.CharField(max_length=255) | |||
password = models.ForeignKey(PasswordInfo) | |||
@@ -0,0 +1,6 @@ | |||
from rest_framework import permissions | |||
class IsOwner(permissions.BasePermission): | |||
def has_object_permission(self, request, view, obj): | |||
return obj.user == request.user |
@@ -1,8 +1,29 @@ | |||
import json | |||
from api import models | |||
from rest_framework import serializers | |||
from django.utils.translation import ugettext_lazy as _ | |||
class JsonSettingsField(serializers.Field): | |||
default_error_messages = { | |||
'invalid': _('Value must be valid JSON.') | |||
} | |||
def to_representation(self, value): | |||
return json.loads(value) | |||
def to_internal_value(self, data): | |||
try: | |||
return json.dumps(data) | |||
except (TypeError, ValueError): | |||
self.fail('invalid') | |||
class PasswordInfoSerializer(serializers.ModelSerializer): | |||
settings = JsonSettingsField() | |||
class Meta: | |||
model = models.PasswordInfo | |||
fields = ('counter', 'settings', 'length') | |||
@@ -15,3 +36,21 @@ class EntrySerializer(serializers.ModelSerializer): | |||
model = models.Entry | |||
fields = ('id', 'site', 'password', 'title', 'username', 'email', 'description', 'url', 'created', 'modified') | |||
read_only_fields = ('created', 'modified') | |||
def create(self, validated_data): | |||
password_data = validated_data.pop('password') | |||
user = self.context['request'].user | |||
password_info = models.PasswordInfo.objects.create(**password_data) | |||
return models.Entry.objects.create(user=user, password=password_info, **validated_data) | |||
def update(self, instance, validated_data): | |||
password_data = validated_data.pop('password') | |||
passwordInfo = instance.password | |||
for attr, value in password_data.items(): | |||
setattr(passwordInfo, attr, value) | |||
passwordInfo.save() | |||
for attr, value in validated_data.items(): | |||
setattr(instance, attr, value) | |||
instance.save() | |||
return instance |
@@ -0,0 +1,39 @@ | |||
import factory | |||
from api import models | |||
class UserFactory(factory.DjangoModelFactory): | |||
class Meta: | |||
model = models.User | |||
username = factory.Sequence(lambda n: 'username{0}'.format(n)) | |||
first_name = factory.Faker('first_name') | |||
last_name = factory.Faker('last_name') | |||
email = factory.LazyAttribute(lambda a: '{0}.{1}@oslab.fr'.format(a.first_name, a.last_name).lower()) | |||
password = factory.PostGenerationMethodCall('set_password', 'password') | |||
is_staff = False | |||
class AdminFactory(UserFactory): | |||
is_staff = True | |||
class PasswordInfoFactory(factory.DjangoModelFactory): | |||
class Meta: | |||
model = models.PasswordInfo | |||
settings = '["lowercase", "uppercase", "numbers", "symbols"]' | |||
class EntryFactory(factory.DjangoModelFactory): | |||
class Meta: | |||
model = models.Entry | |||
user = factory.SubFactory(UserFactory) | |||
password = factory.SubFactory(PasswordInfoFactory) | |||
title = 'twitter' | |||
site = 'twitter' | |||
username = 'guillaume20100' | |||
url = 'https://twitter.com/' |
@@ -0,0 +1,109 @@ | |||
import json | |||
from rest_framework.test import APITestCase, APIClient | |||
from api import models | |||
from api.tests import factories | |||
class LogoutApiTestCase(APITestCase): | |||
def test_get_entries_403(self): | |||
response = self.client.get('/api/entries/') | |||
self.assertEqual(403, response.status_code) | |||
class LoginApiTestCase(APITestCase): | |||
def setUp(self): | |||
self.user = factories.UserFactory() | |||
self.client = APIClient() | |||
self.client.force_authenticate(user=self.user) | |||
def test_get_empty_entries(self): | |||
request = self.client.get('/api/entries/') | |||
self.assertEqual(0, len(request.data['results'])) | |||
def test_retrieve_its_own_entries(self): | |||
entry = factories.EntryFactory(user=self.user) | |||
request = self.client.get('/api/entries/') | |||
self.assertEqual(1, len(request.data['results'])) | |||
self.assertEqual(entry.site, request.data['results'][0]['site']) | |||
def test_cant_retrieve_other_entries(self): | |||
not_my_entry = factories.EntryFactory(user=factories.UserFactory()) | |||
request = self.client.get('/api/entries/%s/' % not_my_entry.id) | |||
self.assertEqual(404, request.status_code) | |||
def test_delete_its_own_entries(self): | |||
entry = factories.EntryFactory(user=self.user) | |||
self.assertEqual(1, models.Entry.objects.all().count()) | |||
request = self.client.delete('/api/entries/%s/' % entry.id) | |||
self.assertEqual(204, request.status_code) | |||
self.assertEqual(0, models.Entry.objects.all().count()) | |||
def test_cant_delete_other_entry(self): | |||
not_my_entry = factories.EntryFactory(user=factories.UserFactory()) | |||
self.assertEqual(1, models.Entry.objects.all().count()) | |||
request = self.client.delete('/api/entries/%s/' % not_my_entry.id) | |||
self.assertEqual(404, request.status_code) | |||
self.assertEqual(1, models.Entry.objects.all().count()) | |||
def test_create_entry(self): | |||
entry = { | |||
"site": "twitter", | |||
"password": { | |||
"counter": 1, | |||
"settings": [ | |||
"lowercase", | |||
"uppercase", | |||
"numbers", | |||
"symbols" | |||
], | |||
"length": 12 | |||
}, | |||
"title": "twitter", | |||
"username": "guillaume20100", | |||
"email": "guillaume@oslab.fr", | |||
"description": "", | |||
"url": "https://twitter.com/" | |||
} | |||
self.assertEqual(0, models.Entry.objects.count()) | |||
self.assertEqual(0, models.PasswordInfo.objects.count()) | |||
self.client.post('/api/entries/', entry) | |||
self.assertEqual(1, models.Entry.objects.count()) | |||
self.assertEqual(1, models.PasswordInfo.objects.count()) | |||
def test_update_entry(self): | |||
entry = factories.EntryFactory(user=self.user) | |||
self.assertNotEqual('facebook', entry.site) | |||
new_entry = { | |||
"site": "facebook", | |||
"password": { | |||
"counter": 1, | |||
"settings": [ | |||
"lowercase", | |||
"uppercase", | |||
"numbers" | |||
], | |||
"length": 12 | |||
}, | |||
"title": "facebook", | |||
"username": "", | |||
"email": "", | |||
"description": "", | |||
"url": "https://facebook.com/" | |||
} | |||
self.client.put('/api/entries/%s/' % entry.id, new_entry) | |||
entry_updated = models.Entry.objects.get(id=entry.id) | |||
self.assertEqual('facebook', entry_updated.site) | |||
self.assertEqual(3, len(json.loads(entry_updated.password.settings))) | |||
def test_cant_update_other_entry(self): | |||
not_my_entry = factories.EntryFactory(user=factories.UserFactory()) | |||
self.assertEqual('twitter', not_my_entry.site) | |||
new_entry = { | |||
"site": "facebook", | |||
"password": {"settings": []} | |||
} | |||
request = self.client.put('/api/entries/%s/' % not_my_entry.id, new_entry) | |||
self.assertEqual(404, request.status_code) | |||
self.assertEqual(1, models.Entry.objects.all().count()) |
@@ -5,6 +5,7 @@ from api import views | |||
router = DefaultRouter() | |||
router.register(r'auth', views.AuthViewSet, base_name="auth") | |||
router.register(r'entries', views.EntryViewSet, base_name='entries') | |||
urlpatterns = [ | |||
url(r'^', include(router.urls)), | |||
@@ -1,4 +1,5 @@ | |||
import json | |||
from api import models, serializers | |||
from api.permissions import IsOwner | |||
from django.contrib.auth import login, authenticate | |||
from rest_framework import status, permissions, viewsets | |||
@@ -36,3 +37,11 @@ class AuthViewSet(viewsets.ViewSet): | |||
login(request, user) | |||
return Response(status=status.HTTP_201_CREATED) | |||
return Response(status=status.HTTP_401_UNAUTHORIZED) | |||
class EntryViewSet(viewsets.ModelViewSet): | |||
serializer_class = serializers.EntrySerializer | |||
permission_classes = (permissions.IsAuthenticated, IsOwner,) | |||
def get_queryset(self): | |||
return models.Entry.objects.filter(user=self.request.user) |
@@ -125,7 +125,8 @@ REST_FRAMEWORK = { | |||
), | |||
'DEFAULT_PARSER_CLASSES': ( | |||
'rest_framework.parsers.JSONParser', | |||
) | |||
), | |||
'TEST_REQUEST_DEFAULT_FORMAT': 'json' | |||
} | |||
AUTHENTICATION_BACKENDS = ( | |||
@@ -0,0 +1,2 @@ | |||
-r requirements.txt | |||
factory-boy==2.6.0 |
@@ -1,3 +1,4 @@ | |||
Django==1.9.1 | |||
djangorestframework==3.3.2 | |||
smartconfigparser==0.1.1 | |||
smartconfigparser==0.1.1 | |||
django-allauth==0.24.1 |