Compare commits

...

1 commit

Author SHA1 Message Date
bd59c40ac6 stash
Some checks failed
continuous-integration/drone/push Build is failing
2024-03-01 00:50:26 +01:00
20 changed files with 431 additions and 25 deletions

View file

@ -13,5 +13,4 @@ steps:
- apk add --no-cache gcc musl-dev python3-dev
- pip install --upgrade pip && pip install -r requirements.txt
- python3 configure.py
- coverage run manage.py test
- coverage report
- coverage run manage.py test && coverage report

3
.gitignore vendored
View file

@ -130,4 +130,5 @@ dmypy.json
staticfiles/
userfiles/
testdata.py
backend/templates/
backend/testdata.py

View file

@ -5,7 +5,9 @@
``` bash
git clone https://github.com/gr4yj3d1/toolshed.git
```
or
``` bash
git clone https://git.neulandlabor.de/j3d1/toolshed.git
```
@ -20,7 +22,9 @@ pip install -r requirements.txt
python configure.py
python manage.py runserver 0.0.0.0:8000 --insecure
```
to run this in properly in production, you need to configure a webserver to serve the static files and proxy the requests to the backend, then run the backend with just `python manage.py runserver` without the `--insecure` flag.
to run this in properly in production, you need to configure a webserver to serve the static files and proxy the
requests to the backend, then run the backend with just `python manage.py runserver` without the `--insecure` flag.
### Frontend
@ -37,8 +41,6 @@ cd toolshed/docs
mkdocs serve
```
## CLI Client
### Requirements

View file

@ -22,7 +22,8 @@ class KnownIdentity(models.Model):
def __str__(self):
return f"{self.username}@{self.domain}"
def is_authenticated(self):
@staticmethod
def is_authenticated():
return True
def friends_or_self(self):
@ -30,9 +31,9 @@ class KnownIdentity(models.Model):
public_identity=self)
def verify(self, message, signature):
if len(signature) != 128 or type(signature) != str:
if len(signature) != 128 or not isinstance(signature, str):
raise TypeError('Signature must be 128 characters long and a string')
if type(message) != str:
if not isinstance(message, str):
raise TypeError('Message must be a string')
try:
VerifyKey(bytes.fromhex(self.public_key)).verify(message.encode('utf-8'), bytes.fromhex(signature))
@ -44,14 +45,17 @@ class KnownIdentity(models.Model):
class ToolshedUserManager(auth.models.BaseUserManager):
def create_user(self, username, email, password, **extra_fields):
domain = extra_fields.pop('domain', 'localhost')
private_key_hex = extra_fields.pop('private_key', None)
if private_key_hex and type(private_key_hex) != str:
raise TypeError('Private key must be a string or no private key must be provided')
if private_key_hex and len(private_key_hex) != 64:
raise ValueError('Private key must be 64 characters long or no private key must be provided')
if private_key_hex and not all(c in '0123456789abcdef' for c in private_key_hex):
raise ValueError('Private key must be a hexadecimal string or no private key must be provided')
private_key = SigningKey(bytes.fromhex(private_key_hex)) if private_key_hex else SigningKey.generate()
private_key_hex: str | None = extra_fields.pop('private_key', None)
if private_key_hex is not None:
if not isinstance(private_key_hex, str):
raise TypeError('Private key must be a string or no private key must be provided')
if len(private_key_hex) != 64:
raise ValueError('Private key must be 64 characters long or no private key must be provided')
if not all(c in '0123456789abcdef' for c in private_key_hex):
raise ValueError('Private key must be a hexadecimal string or no private key must be provided')
private_key = SigningKey(bytes.fromhex(private_key_hex))
else:
private_key = SigningKey.generate()
public_key = SigningKey(private_key.encode()).verify_key
extra_fields['private_key'] = private_key.encode(encoder=HexEncoder).decode('utf-8')
try:

View file

@ -1,5 +1,6 @@
import json
from django.core.exceptions import ValidationError
from django.test import Client, RequestFactory
from nacl.encoding import HexEncoder
from nacl.signing import SigningKey

View file

@ -13,11 +13,14 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from backend import settings
openapi_info = openapi.Info(
title="Toolshed API",
default_version='v1',
@ -35,9 +38,10 @@ urlpatterns = [
path('auth/', include('authentication.api')),
path('admin/', include('hostadmin.api')),
path('api/', include('toolshed.api.friend')),
path('api/', include('toolshed.api.social')),
path('api/', include('toolshed.api.inventory')),
path('api/', include('toolshed.api.info')),
path('api/', include('toolshed.api.files')),
path('media/', include('files.media_urls')),
path('docs/', schema_view.with_ui('swagger', cache_timeout=0), name='api-docs'),
]
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View file

@ -74,10 +74,18 @@ def configure():
from django.core.management import call_command
call_command('migrate')
# if yesno("Do you want to create initial domains?"):
# domains = input("Enter a comma-separated list of allowed hosts: ")
# from hostadmin import Domain
#
# Domain.objects.
# TODO check if superuser exists
if yesno("Do you want to create a superuser?"):
from django.core.management import call_command
call_command('createsuperuser')
# TODO ask for which static directory to use and save it in .env
call_command('collectstatic', '--no-input')
if yesno("Do you want to import all categories, properties and tags contained in this repository?", default=True):

View file

@ -0,0 +1,59 @@
{
"categories": [
{ "name": "hardware"},
{ "name": "material"},
{ "name": "tools"}
],
"properties": [
{ "name": "angle", "unit_symbol": "°", "unit_name": "degree", "unit_name_plural": "degrees" },
{ "name": "area", "unit_symbol": "m²", "unit_name": "square meter", "unit_name_plural": "square meters" },
{ "name": "current", "unit_symbol": "A", "unit_name": "ampere", "unit_name_plural": "amperes" },
{ "name": "diameter", "unit_symbol": "m", "unit_name": "meter", "unit_name_plural": "meters" },
{ "name": "energy", "unit_symbol": "J", "unit_name": "joule", "unit_name_plural": "joules" },
{ "name": "frequency", "unit_symbol": "Hz", "unit_name": "hertz", "unit_name_plural": "hertz" },
{ "name": "height", "unit_symbol": "m", "unit_name": "meter", "unit_name_plural": "meters" },
{ "name": "length", "unit_symbol": "m", "unit_name": "meter", "unit_name_plural": "meters" },
{ "name": "memory", "unit_symbol": "B", "unit_name": "byte", "unit_name_plural": "bytes", "base2_prefix": true },
{ "name": "power", "unit_symbol": "W", "unit_name": "watt", "unit_name_plural": "watts" },
{ "name": "price", "unit_symbol": "€", "unit_name": "euro", "unit_name_plural": "euros" },
{ "name": "speed", "unit_symbol": "m/s", "unit_name": "meter per second", "unit_name_plural": "meters per second" },
{ "name": "temperature", "unit_symbol": "°C", "unit_name": "degree Celsius", "unit_name_plural": "degrees Celsius" },
{ "name": "time", "unit_symbol": "s", "unit_name": "second", "unit_name_plural": "seconds" },
{ "name": "voltage", "unit_symbol": "V", "unit_name": "volt", "unit_name_plural": "volts" },
{ "name": "volume", "unit_symbol": "l", "unit_name": "liter", "unit_name_plural": "liters" },
{ "name": "weight", "unit_symbol": "g", "unit_name": "gram", "unit_name_plural": "grams" },
{ "name": "width", "unit_symbol": "m", "unit_name": "meter", "unit_name_plural": "meters" }
],
"tags": [
{"name": "bolt", "category": "hardware"},
{"name": "chisel", "category": "tools"},
{"name": "clamp", "category": "tools"},
{"name": "drill", "category": "tools"},
{"name": "ear plugs", "category": "tools"},
{"name": "extension cord", "category": "tools"},
{"name": "flashlight", "category": "tools"},
{"name": "gloves", "category": "tools"},
{"name": "goggles", "category": "tools"},
{"name": "hammer", "category": "tools"},
{"name": "level", "category": "tools"},
{"name": "mask", "category": "tools"},
{"name": "nail", "category": "hardware"},
{"name": "nut", "category": "hardware"},
{"name": "paint brush", "category": "tools"},
{"name": "paint roller", "category": "tools"},
{"name": "paint tray", "category": "tools"},
{"name": "pliers", "category": "tools"},
{"name": "power strip", "category": "tools"},
{"name": "sander", "category": "tools"},
{"name": "saw", "category": "tools"},
{"name": "screw", "category": "hardware"},
{"name": "screwdriver", "category": "tools"},
{"name": "soldering iron", "category": "tools"},
{"name": "stapler", "category": "tools"},
{"name": "tape measure", "category": "tools"},
{"name": "tool"},
{"name": "vise", "category": "tools"},
{"name": "washer", "category": "hardware"},
{"name": "wrench", "category": "tools"}
]
}

View file

@ -0,0 +1,30 @@
{
"depends": [ "git:base" ],
"categories": [
{ "name": "screws", "parent": "hardware"}
],
"tags": [
{"name": "m1", "category": "screws"},
{"name": "m2", "category": "screws"},
{"name": "m2.5", "category": "screws"},
{"name": "m3", "category": "screws"},
{"name": "m4", "category": "screws"},
{"name": "m5", "category": "screws"},
{"name": "m6", "category": "screws"},
{"name": "m8", "category": "screws"},
{"name": "m10", "category": "screws"},
{"name": "m12", "category": "screws"},
{"name": "m16", "category": "screws"},
{"name": "torx", "category": "screws"},
{"name": "hex", "category": "screws"},
{"name": "phillips", "category": "screws"},
{"name": "pozidriv", "category": "screws"},
{"name": "slotted", "category": "screws"},
{"name": "socket", "category": "screws"},
{"name": "flat", "category": "screws"},
{"name": "pan", "category": "screws"},
{"name": "button", "category": "screws"},
{"name": "countersunk", "category": "screws"},
{"name": "round", "category": "screws"}
]
}

View file

@ -1,6 +1,14 @@
from django.contrib import admin
from toolshed.models import InventoryItem, Property, Tag, ItemProperty, ItemTag
from toolshed.models import Profile, InventoryItem, Property, Tag, ItemProperty, ItemTag
class ProfileAdmin(admin.ModelAdmin):
list_display = ('user', 'bio', 'location')
search_fields = ('user', 'bio', 'location')
admin.site.register(Profile, ProfileAdmin)
class InventoryItemAdmin(admin.ModelAdmin):
@ -25,3 +33,26 @@ class TagAdmin(admin.ModelAdmin):
admin.site.register(Tag, TagAdmin)
# class ItemPropertyAdmin(admin.ModelAdmin):
# list_display = ('item', 'property', 'value')
# search_fields = ('item', 'property', 'value')
#
#
# admin.site.register(ItemProperty, ItemPropertyAdmin)
#
#
# class ItemTagAdmin(admin.ModelAdmin):
# list_display = ('item', 'tag')
# search_fields = ('item', 'tag')
#
#
# admin.site.register(ItemTag, ItemTagAdmin)
# class LendingPeriodAdmin(admin.ModelAdmin):
# list_display = ('item', 'start_date', 'end_date')
# search_fields = ('item', 'start_date', 'end_date')
#
#
# admin.site.register(LendingPeriod, LendingPeriodAdmin)

View file

@ -0,0 +1,16 @@
from toolshed.models import Event, Message
def timeline_notifications(user):
"""Return a list of notifications that the user is interested in."""
for evt in Event.objects.all():
if evt.user == user:
yield evt
for tool in user.inventory.all():
if evt.tool == tool:
yield evt
def unread_messages(user):
"""Return a list of unread messages."""
return Message.objects.filter(recipient=user, read=False)

View file

@ -61,9 +61,11 @@ def combined_info(request, format=None): # /info/
tags = [tag.name for tag in Tag.objects.all()]
properties = PropertySerializer(Property.objects.all(), many=True).data
categories = [str(category) for category in Category.objects.all()]
policies = ['private', 'friends', 'internal', 'public']
policies = InventoryItem.AVAILABILITY_POLICY_CHOICES
domains = [domain.name for domain in Domain.objects.filter(open_registration=True)]
return Response({'tags': tags, 'properties': properties, 'availability_policies': policies, 'categories': categories, 'domains': domains})
return Response(
{'tags': tags, 'properties': properties, 'availability_policies': policies, 'categories': categories,
'domains': domains})
urlpatterns = [

View file

@ -1,12 +1,13 @@
from django.db import transaction
from django.urls import path
from rest_framework import routers, viewsets
from rest_framework import routers, viewsets, serializers
from rest_framework.decorators import authentication_classes, api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from authentication.models import ToolshedUser, KnownIdentity
from authentication.signature_auth import SignatureAuthentication
from files.models import File
from toolshed.models import InventoryItem, StorageLocation
from toolshed.serializers import InventoryItemSerializer, StorageLocationSerializer

View file

@ -0,0 +1,40 @@
from django.urls import path
from rest_framework.decorators import api_view, authentication_classes, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from authentication.signature_auth import SignatureAuthenticationLocal
from toolshed.models import Message, Profile
from toolshed.serializers import MessageSerializer, ProfileSerializer
from toolshed.aggregators import unread_messages, timeline_notifications
@api_view(['GET'])
@authentication_classes([SignatureAuthenticationLocal])
@permission_classes([IsAuthenticated])
def get_messages(request):
messages = Message.objects.filter(recipient=request.user)
return Response(MessageSerializer(messages, many=True).data)
@api_view(['GET'])
@authentication_classes([SignatureAuthenticationLocal])
@permission_classes([IsAuthenticated])
def get_profile(request):
profile = Profile.objects.get(user=request.user)
return Response(ProfileSerializer(profile).data)
@api_view(['GET'])
@authentication_classes([SignatureAuthenticationLocal])
@permission_classes([IsAuthenticated])
def get_notifications(request):
notifications = timeline_notifications(request.user)
return Response(notifications)
urlpatterns = [
path('messages/', get_messages),
path('profile/', get_profile),
path('notifications/', get_notifications),
]

View file

@ -0,0 +1,67 @@
# Generated by Django 4.2.2 on 2024-02-23 15:30
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('files', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('toolshed', '0005_alter_inventoryitem_availability_policy'),
]
operations = [
migrations.CreateModel(
name='Event',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('description', models.TextField()),
('location', models.CharField(max_length=255)),
('date', models.DateField()),
('time', models.TimeField()),
('host_username', models.CharField(max_length=255)),
('host_domain', models.CharField(max_length=255)),
],
),
migrations.CreateModel(
name='Transaction',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('status', models.CharField(choices=[('pending', 'Pending'), ('accepted', 'Accepted'), ('rejected', 'Rejected')], default='pending', max_length=20)),
('message', models.TextField(blank=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('item_offered', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='offered_transactions', to='toolshed.inventoryitem')),
('item_requested', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='requested_transactions', to='toolshed.inventoryitem')),
('offerer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='offered_transactions', to=settings.AUTH_USER_MODEL)),
('requester', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='requested_transactions', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Profile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('bio', models.TextField(blank=True)),
('location', models.CharField(blank=True, max_length=255)),
('profile_picture', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='files.file')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Message',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('subject', models.CharField(max_length=255)),
('body', models.TextField()),
('read', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_messages', to=settings.AUTH_USER_MODEL)),
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to=settings.AUTH_USER_MODEL)),
],
),
]

View file

@ -1,4 +1,6 @@
from django.db import models
from django.db.models.signals import post_save, pre_save
from django.dispatch import receiver
from django.core.validators import MinValueValidator
from django_softdelete.models import SoftDeleteModel
from rest_framework.exceptions import ValidationError
@ -7,6 +9,52 @@ from authentication.models import ToolshedUser, KnownIdentity
from files.models import File
# @receiver(pre_save)
# def pre_save_handler(sender, instance, *args, **kwargs):
# instance.full_clean()
class Event(models.Model):
name = models.CharField(max_length=255)
description = models.TextField()
location = models.CharField(max_length=255)
date = models.DateField()
time = models.TimeField()
# host = models.ForeignKey(User, on_delete=models.CASCADE, related_name='events')
host_username = models.CharField(max_length=255)
host_domain = models.CharField(max_length=255)
# def __str__(self):
# return self.name
class Message(models.Model):
sender = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='sent_messages')
recipient = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='received_messages')
subject = models.CharField(max_length=255)
body = models.TextField()
read = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Profile(models.Model):
user = models.OneToOneField(ToolshedUser, on_delete=models.CASCADE)
profile_picture = models.ForeignKey(File, on_delete=models.SET_NULL, null=True, blank=True)
bio = models.TextField(blank=True)
location = models.CharField(max_length=255, blank=True)
@receiver(post_save, sender=ToolshedUser)
def create_user_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
@receiver(post_save, sender=ToolshedUser)
def save_user_profile(sender, instance, **kwargs):
instance.profile.save()
class Category(SoftDeleteModel):
name = models.CharField(max_length=255, unique=True)
description = models.TextField(null=True, blank=True)
@ -29,6 +77,7 @@ class Property(models.Model):
unit_name = models.CharField(max_length=255, null=True, blank=True)
unit_name_plural = models.CharField(max_length=255, null=True, blank=True)
base2_prefix = models.BooleanField(default=False)
# sort_lexicographically = models.BooleanField(default=False)
dimensions = models.IntegerField(null=False, blank=False, default=1, validators=[MinValueValidator(1)])
origin = models.CharField(max_length=255, null=False, blank=False)
@ -89,6 +138,23 @@ class ItemTag(models.Model):
inventory_item = models.ForeignKey(InventoryItem, on_delete=models.CASCADE)
class Transaction(models.Model):
STATUS_CHOICES = (
('pending', 'Pending'),
('accepted', 'Accepted'),
('rejected', 'Rejected'),
)
item_requested = models.ForeignKey(InventoryItem, on_delete=models.CASCADE, related_name='requested_transactions')
item_offered = models.ForeignKey(InventoryItem, on_delete=models.CASCADE, related_name='offered_transactions')
requester = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='requested_transactions')
offerer = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='offered_transactions')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
message = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class StorageLocation(models.Model):
name = models.CharField(max_length=255)
description = models.TextField(null=True, blank=True)

View file

@ -1,9 +1,10 @@
from rest_framework import serializers
from authentication.models import KnownIdentity, ToolshedUser, FriendRequestIncoming
from authentication.serializers import OwnerSerializer
from files.models import File
from files.serializers import FileSerializer
from toolshed.models import Category, Property, ItemProperty, InventoryItem, Tag, StorageLocation
from toolshed.models import Category, Property, ItemProperty, InventoryItem, Tag, Profile, Message, StorageLocation
class FriendSerializer(serializers.ModelSerializer):
@ -28,6 +29,18 @@ class FriendRequestSerializer(serializers.ModelSerializer):
return obj.befriender_username + '@' + obj.befriender_domain
class ProfileSerializer(serializers.Serializer):
class Meta:
model = Profile
fields = '__all__'
class MessageSerializer(serializers.Serializer):
class Meta:
model = Message
fields = '__all__'
class PropertySerializer(serializers.ModelSerializer):
category = serializers.SlugRelatedField(queryset=Category.objects.all(), slug_field='name')
@ -103,6 +116,8 @@ class InventoryItemSerializer(serializers.ModelSerializer):
tags = validated_data.pop('tags', [])
props = validated_data.pop('itemproperty_set', [])
files = validated_data.pop('files', [])
# if 'category' in validated_data and validated_data['category'] == '':
# validated_data.pop('category')
item = InventoryItem.objects.create(**validated_data)
for tag in tags:
item.tags.add(tag, through_defaults={})
@ -127,6 +142,8 @@ class InventoryItemSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
tags = validated_data.pop('tags', [])
props = validated_data.pop('itemproperty_set', [])
# if 'category' in validated_data and validated_data['category'] == '':
# validated_data.pop('category')
item = super().update(instance, validated_data)
item.tags.clear()
item.properties.clear()

View file

@ -53,7 +53,8 @@ class CombinedApiTestCase(UserTestMixin, CategoryTestMixin, TagTestMixin, Proper
def test_combined_api(self):
response = client.get('/api/info/', self.f['local_user1'])
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['availability_policies'], ['private', 'friends', 'internal', 'public'])
self.assertEqual(response.json()['availability_policies'], [['sell', 'Sell'], ['rent', 'Rent'], ['lend', 'Lend'],
['share', 'Share'], ['private', 'Private']])
self.assertEqual(response.json()['categories'],
['cat1', 'cat2', 'cat3', 'cat1/subcat1', 'cat1/subcat2', 'cat1/subcat1/subcat3'])
self.assertEqual(response.json()['tags'], ['tag1', 'tag2', 'tag3'])

View file

@ -369,13 +369,29 @@ class FriendRequestOutgoingTestCase(UserTestMixin, ToolshedTestCase):
self.assertEqual(befriendee.friends.first().username, befriender.username)
self.assertEqual(befriendee.friends.first().domain, befriender.domain)
# TODO:
# - test that the friend request is deleted after a certain amount of time
# - decline friend request Endpoint ('reject'?)
# - cancel friend request Endpoint ('retract'?)
# - drop friend Endpoint
# Szenarios: (all also with broken signature, wrong key, wrong secret, wrong author and without authorisation header)
# (plus: friend request exists already, already friends)
# - local1 requests local2, local2 accepts
# - local1 requests local2, local2 declines
# - local1 requests local2, local1 cancels
# - ext requests local, local accepts
# - ext requests local, local declines
# - ext requests local, ext cancels
# - local requests ext, ext accepts
# - local requests ext, ext declines
# - local requests ext, local cancels
class FriendRequestCombinedTestCase(UserTestMixin, ToolshedTestCase):
def setUp(self):
super().setUp()
self.prepare_users()
print(self.f)
def test_friend_request_combined(self):
befriender = self.f['local_user1']

View file

@ -0,0 +1,41 @@
from django.test import Client
from authentication.tests import SignatureAuthClient, UserTestMixin, ToolshedTestCase
anonymous_client = Client()
client = SignatureAuthClient()
class MessageApiTestCase(UserTestMixin, ToolshedTestCase):
def setUp(self):
super().setUp()
self.prepare_users()
def test_get_messages(self):
response = client.get('/api/messages/', self.f['local_user1'])
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [])
class ProfileApiTestCase(UserTestMixin, ToolshedTestCase):
def setUp(self):
super().setUp()
self.prepare_users()
def test_get_profile(self):
response = client.get('/api/profile/', self.f['local_user1'])
self.assertEqual(response.status_code, 200)
class NotificationApiTestCase(UserTestMixin, ToolshedTestCase):
def setUp(self):
super().setUp()
self.prepare_users()
def test_get_notifications(self):
response = client.get('/api/notifications/', self.f['local_user1'])
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [])