Skip to content

Commit

Permalink
Add Unit tests for the new views implementing PyPI's authorization model
Browse files Browse the repository at this point in the history
  • Loading branch information
jbernal0019 committed Sep 17, 2021
1 parent e0b2cfb commit a492e68
Show file tree
Hide file tree
Showing 10 changed files with 332 additions and 232 deletions.
79 changes: 13 additions & 66 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
![License][license-badge]
![Last Commit][last-commit-badge]

Back end for the ChRIS store. This is a Django-PostgreSQL project that houses descriptions of ChRIS plugin-apps and workflows for registering to a ChRIS CUBE instance.
Backend for the ChRIS store. This is a Django-PostgreSQL project that houses descriptions of ChRIS plugin-apps and workflows for registering to a ChRIS CUBE instance.

## ChRIS store development, testing and deployment

Expand All @@ -13,7 +13,9 @@ This page describes how to quickly get the set of services comprising the backen

### Preconditions

#### Install latest Docker and Docker Compose. Currently tested platforms
#### Install latest Docker and Docker Compose

Currently tested platforms:
* ``Ubuntu 18.04+ and MAC OS X 10.14+ and Fedora 31+`` ([Additional instructions for Fedora](https://github.com/mairin/ChRIS_store/wiki/Getting-the-ChRIS-Store-to-work-on-Fedora))
* ``Docker 18.06.0+``
* ``Docker Compose 1.27.0+``
Expand All @@ -35,7 +37,7 @@ cd ChRIS_store

The resulting instance uses the default Django development server and therefore is not suitable for production.

### Production deployment
### Production deployment on a single-machine Docker Swarm cluster

#### To get the production system up:

Expand Down Expand Up @@ -86,59 +88,6 @@ docker swarm leave --force

### Development

#### Optionally setup a virtual environment

#### Install virtualenv
```bash
pip install virtualenv virtualenvwrapper
```

#### Setup your virtual environments
Create a directory for your virtual environments e.g.:
```bash
mkdir ~/Python_Envs
```

You might want to add to your .bashrc file these two lines:
```bash
export WORKON_HOME=~/Python_Envs
source /usr/local/bin/virtualenvwrapper.sh
```

Then you can source your ``.bashrc`` and create a new Python3 virtual environment:

```bash
mkvirtualenv --python=python3 chris_store_env
```

To activate chris_store_env:
```bash
workon chris_store_env
```

To deactivate chris_store_env:
```bash
deactivate
```

#### Install useful python tools in your virtual environment
```bash
cd ChRIS_store
workon chris_store_env
pip install httpie
pip install python-swiftclient
pip install django-storage-swift
```

You can also install some python libraries (not all of them) specified in the ``requirements/base.txt`` and
``requirements/local.txt`` files in the source repo


To list installed dependencies in chris_store_env:
```
pip freeze --local
```

### Instantiate ChRIS Store dev environment

Start ChRIS Store services by running the make bash script from the repository source directory
Expand All @@ -153,36 +102,34 @@ After running this script all the automated tests should have successfully run a

#### Rerun automated tests after modifying source code

Open another terminal and find out the id of the container running the Django server in interactive mode:
```bash
chris_store=$(docker ps -f ancestor=fnndsc/chris_store:dev -f name=chris_store_dev -q)
```
and run the Unit and Integration tests within that container.
Open another terminal and run the Unit and Integration tests within the container running the Django server:

To run only the Unit tests:

```bash
docker exec -it $chris_store python manage.py test --exclude-tag integration
cd ChRIS_store
docker-compose -f docker-compose_dev.yml exec chris_store_dev python manage.py test --exclude-tag integration
```

To run only the Integration tests:

```bash
docker exec -it $chris_store python manage.py test --tag integration
docker-compose -f docker-compose_dev.yml exec chris_store_dev python manage.py test --tag integration
```

To run all the tests:

```bash
docker exec -it $chris_store python manage.py test
docker-compose -f docker-compose_dev.yml exec chris_store_dev python manage.py test
```


#### Check code coverage of the automated tests
Make sure the ``store_backend/`` dir is world writable. Then type:

```bash
docker exec -it $chris_store coverage run --source=plugins,users manage.py test
docker exec -it $chris_store coverage report
docker-compose -f docker-compose_dev.yml exec chris_store_dev coverage run --source=plugins,pipelines,users manage.py test
docker-compose -f docker-compose_dev.yml exec chris_store_dev coverage report
```

### Using [HTTPie](https://httpie.org/) to play with the REST API
Expand Down
10 changes: 0 additions & 10 deletions store_backend/plugins/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,16 +143,6 @@ def __str__(self):
return str(self.id)


class PluginMetaCollaboratorFilter(FilterSet):
username = django_filters.CharFilter(field_name='user__username', lookup_expr='exact')
plugin_name = django_filters.CharFilter(field_name='meta__name', lookup_expr='exact')
role = django_filters.CharFilter(field_name='role', lookup_expr='exact')

class Meta:
model = PluginMetaStar
fields = ['id', 'username', 'plugin_name', 'role']


def uploaded_file_path(instance, filename):
# file will be stored to Swift at:
# SWIFT_CONTAINER_NAME/plugins/<plugin_name>/<plugin_id>/<today_path>/<filename>
Expand Down
54 changes: 32 additions & 22 deletions store_backend/plugins/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ def has_object_permission(self, request, view, obj):
return True

# Write permissions are only allowed to the owners.
try:
collab = PluginMetaCollaborator.objects.get(meta=obj, user=request.user)
except PluginMetaCollaborator.DoesNotExist:
return False
return collab.role == 'O'
if request.user.is_authenticated:
try:
collab = PluginMetaCollaborator.objects.get(meta=obj, user=request.user)
except PluginMetaCollaborator.DoesNotExist:
return False
return collab.role == 'O'
return False


class IsStarOwner(permissions.BasePermission):
Expand All @@ -40,11 +42,13 @@ class IsMetaOwnerOrCollabReadOnly(permissions.BasePermission):

def has_object_permission(self, request, view, obj):
# Read permissions are only allowed to the collaborators.
try:
collab = PluginMetaCollaborator.objects.get(meta=obj, user=request.user)
except PluginMetaCollaborator.DoesNotExist:
return False
return request.method in permissions.SAFE_METHODS or collab.role == 'O'
if request.user.is_authenticated:
try:
collab = PluginMetaCollaborator.objects.get(meta=obj, user=request.user)
except PluginMetaCollaborator.DoesNotExist:
return False
return request.method in permissions.SAFE_METHODS or collab.role == 'O'
return False


class IsObjMetaOwnerAndNotUserOrCollabReadOnly(permissions.BasePermission):
Expand All @@ -56,13 +60,16 @@ class IsObjMetaOwnerAndNotUserOrCollabReadOnly(permissions.BasePermission):

def has_object_permission(self, request, view, obj):
# Read permissions are only allowed to the collaborators.
try:
collab = PluginMetaCollaborator.objects.get(meta=obj.meta, user=request.user)
except PluginMetaCollaborator.DoesNotExist:
return False
if request.method in permissions.SAFE_METHODS:
return True
return collab.role == 'O' and obj.user != request.user
if request.user.is_authenticated:
try:
collab = PluginMetaCollaborator.objects.get(meta=obj.meta,
user=request.user)
except PluginMetaCollaborator.DoesNotExist:
return False
if request.method in permissions.SAFE_METHODS:
return True
return collab.role == 'O' and obj.user != request.user
return False


class IsObjMetaOwnerOrReadOnly(permissions.BasePermission):
Expand All @@ -77,8 +84,11 @@ def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True

try:
collab = PluginMetaCollaborator.objects.get(meta=obj.meta, user=request.user)
except PluginMetaCollaborator.DoesNotExist:
return False
return collab.role == 'O'
if request.user.is_authenticated:
try:
collab = PluginMetaCollaborator.objects.get(meta=obj.meta,
user=request.user)
except PluginMetaCollaborator.DoesNotExist:
return False
return collab.role == 'O'
return False
40 changes: 25 additions & 15 deletions store_backend/plugins/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def validate(self, data):
Overriden to check if plugin meta and user are unique together.
"""
user = self.context['request'].user
meta = data['plugin_name']
meta = data['meta']['name']
try:
PluginMetaStar.objects.get(meta=meta, user=user)
except ObjectDoesNotExist:
Expand All @@ -82,7 +82,8 @@ class PluginMetaCollaboratorSerializer(serializers.HyperlinkedModelSerializer):
plugin_name = serializers.ReadOnlyField(source='meta.name')
meta_id = serializers.ReadOnlyField(source='meta.id')
user_id = serializers.ReadOnlyField(source='user.id')
username = serializers.CharField(min_length=4, max_length=32, source='user.username')
username = serializers.CharField(min_length=4, max_length=32, source='user.username',
required=False)
meta = serializers.HyperlinkedRelatedField(view_name='pluginmeta-detail',
read_only=True)
user = serializers.HyperlinkedRelatedField(view_name='user-detail', read_only=True)
Expand All @@ -95,25 +96,34 @@ class Meta:
def validate_username(self, username):
"""
Overriden to check whether the provided username exists in the DB and is not
the user making the request.
the user making the request when creating a new collaborator.
"""
req_user = self.context['request'].user
try:
# check whether username is a system-registered user
user = User.objects.get(username=username)
except ObjectDoesNotExist:
raise serializers.ValidationError("Could not find user %s." % username)
if user.username == req_user.username:
raise serializers.ValidationError("Can not be the user making the request.")
return user
if not self.instance:
req_user = self.context['request'].user
try:
# check whether username is a system-registered user
user = User.objects.get(username=username)
except ObjectDoesNotExist:
raise serializers.ValidationError("Could not find user %s." % username)
if user.username == req_user.username:
raise serializers.ValidationError(
"Can not be the user making the request.")
return user

def validate(self, data):
"""
Overriden to check if plugin meta and user are unique together.
Overriden to check if plugin meta and user are unique together when creating
a new collaborator.
"""
if not self.instance:
if self.instance:
data.pop('user', None) # make sure an update doesn't include 'user'
else:
meta = self.context.get('view').get_object()
user = data['user']['username']
try:
user = data['user']['username']
except KeyError:
msg = 'This field is required.'
raise serializers.ValidationError({'username': [msg]})
try:
PluginMetaCollaborator.objects.get(meta=meta, user=user)
except ObjectDoesNotExist:
Expand Down
10 changes: 4 additions & 6 deletions store_backend/plugins/tests/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from django.core.files.base import ContentFile
from django.test import TestCase, tag

from plugins.models import PluginMeta, Plugin, PluginParameter
from plugins.models import PluginMeta, PluginMetaCollaborator, Plugin, PluginParameter
from plugins.services import manager


Expand Down Expand Up @@ -51,7 +51,7 @@ def setUp(self):
(meta, tf) = PluginMeta.objects.get_or_create(name=self.plugin_name,
public_repo='http://gitgub.com',
**data)
meta.owner.set([user])
PluginMetaCollaborator.objects.create(meta=meta, user=user)
data = self.plg_data.copy()
(plugin, tf) = Plugin.objects.get_or_create(meta=meta,
dock_image='fnndsc/pl-testapp',
Expand Down Expand Up @@ -94,12 +94,10 @@ def test_mananger_can_modify_plugin(self):
password='anotherpassword')
plugin = Plugin.objects.get(meta__name=self.plugin_name)
initial_modification_date = plugin.meta.modification_date
self.pl_manager.run(['modify', self.plugin_name, 'http://github.com/repo',
'--newowner', user.username])
self.pl_manager.run(['modify', self.plugin_name, 'http://github.com/repo'])
plugin = Plugin.objects.get(meta__name=self.plugin_name)
self.assertTrue(plugin.meta.modification_date > initial_modification_date)
user1 = User.objects.get(username=self.username)
self.assertCountEqual(plugin.meta.owner.all(), [user1, user])
self.assertEqual(plugin.meta.public_repo, 'http://github.com/repo')

def test_mananger_can_remove_plugin(self):
"""
Expand Down
21 changes: 4 additions & 17 deletions store_backend/plugins/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from django.contrib.auth.models import User


from plugins.models import PluginMeta, Plugin, PluginFilter, PluginParameter
from plugins.models import (PluginMeta, PluginMetaCollaborator, Plugin, PluginFilter,
PluginParameter)


class ModelTests(TestCase):
Expand Down Expand Up @@ -44,7 +45,7 @@ def setUp(self):

# create a plugin
(meta, tf) = PluginMeta.objects.get_or_create(name=self.plugin_name)
meta.owner.set([user])
PluginMetaCollaborator.objects.create(meta=meta, user=user)
(plugin, tf) = Plugin.objects.get_or_create(meta=meta, version='0.1' )
plugin.descriptor_file.name = self.plugin_name + '.json'
plugin.save()
Expand All @@ -54,20 +55,6 @@ def tearDown(self):
logging.disable(logging.NOTSET)


class PluginMetaModelTests(ModelTests):

def test_add_owner(self):
"""
Test whether custom add_owner method adds a new owner to the plugin.
"""
pl_meta = PluginMeta.objects.get(name=self.plugin_name)
another_user = User.objects.create_user(username='another',
email='anotherdev@babymri.org',
password='anotherpassword')
pl_meta.add_owner(another_user)
self.assertIn(another_user, pl_meta.owner.all())


class PluginModelTests(ModelTests):

def test_get_plugin_parameter_names(self):
Expand Down Expand Up @@ -102,7 +89,7 @@ def setUp(self):

# create other plugin
(meta, tf) = PluginMeta.objects.get_or_create(name=self.other_plugin_name)
meta.owner.set([user])
PluginMetaCollaborator.objects.create(meta=meta, user=user)
Plugin.objects.get_or_create(meta=meta)

def test_search_name_title_category(self):
Expand Down
Loading

0 comments on commit a492e68

Please sign in to comment.