-
Notifications
You must be signed in to change notification settings - Fork 0
Seeding database with testing data #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0c48fbd
a530afb
da27e69
be0022c
f1b0d29
e0fc072
92bcc3e
4fd242e
804c1a5
a166187
19b435b
c635f44
b76388a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import pytest | ||
|
|
||
| def pytest_collection_modifyitems(items): | ||
| # Automatically adds django_db marker to all test functions | ||
| for item in items: | ||
| item.add_marker(pytest.mark.django_db) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| [pytest] | ||
| DJANGO_SETTINGS_MODULE = testbed.settings | ||
| python_files = tests.py test_*.py *_tests.py | ||
| pythonpath = . | ||
| python_files = test_*.py |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import factory | ||
| from factory.django import DjangoModelFactory | ||
| from django.contrib.auth.models import User | ||
| from testbed.core.models import Actor, Note, Activity, PortabilityOutbox | ||
|
|
||
|
|
||
| class UserFactory(DjangoModelFactory): | ||
| class Meta: | ||
| model = User | ||
|
|
||
| username = factory.Sequence(lambda n: f'user_{n}') | ||
| email = factory.LazyAttribute(lambda o: f'{o.username}@example.com') | ||
| password = factory.PostGenerationMethodCall('set_password', 'testpass123') | ||
| is_staff = False | ||
| is_active = True | ||
|
|
||
| @classmethod | ||
| def _after_postgeneration(cls, instance, create, results=None): | ||
| if create: | ||
| instance.save() | ||
|
|
||
| class ActorFactory(DjangoModelFactory): | ||
| class Meta: | ||
| model = Actor | ||
|
|
||
| user = factory.SubFactory(UserFactory) | ||
| username = factory.SelfAttribute('user.username') | ||
| full_name = factory.Faker('name') | ||
| previously = factory.Dict({}) | ||
|
|
||
| class NoteFactory(DjangoModelFactory): | ||
| class Meta: | ||
| model = Note | ||
|
|
||
| actor = factory.SubFactory(ActorFactory) | ||
| content = factory.Faker('text', max_nb_chars=200) | ||
| visibility = factory.Iterator(['public', 'private', 'followers-only']) | ||
|
|
||
| class ActivityFactory(DjangoModelFactory): | ||
| class Meta: | ||
| model = Activity | ||
|
|
||
| actor = factory.SubFactory(ActorFactory) | ||
| type = factory.Iterator(['Create', 'Like', 'Update', 'Follow', 'Announce', 'Delete', 'Undo', 'Flag']) | ||
| note = factory.SubFactory(NoteFactory) | ||
| visibility = factory.Iterator(['public', 'private', 'followers-only']) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| from django.core.management.base import BaseCommand | ||
| from django.contrib.auth.models import User | ||
| from testbed.core.factories import ActorFactory, ActivityFactory | ||
|
|
||
|
|
||
| class Command(BaseCommand): | ||
| help = 'Seed the database with sample data' | ||
|
|
||
| def handle(self, *args, **kwargs): | ||
| try: | ||
| # Check for admin user | ||
| if not User.objects.filter(is_staff=True, is_active=True).exists(): | ||
| self.stdout.write(self.style.WARNING('No admin user found.')) | ||
| answer = input('Would you like to create one? (Y/N): ').strip().lower() | ||
| if answer == 'y': | ||
| self.stdout.write(self.style.WARNING('Creating admin user...')) | ||
| User.objects.create_superuser( | ||
| username='admin', | ||
| email='[email protected]', | ||
| password='admin123' | ||
| ) | ||
| self.stdout.write(self.style.SUCCESS('Admin user created successfully.')) | ||
| else: | ||
| self.stdout.write(self.style.WARNING('Skipping admin user creation.')) | ||
| else: | ||
| self.stdout.write(self.style.SUCCESS('Admin user already exists.')) | ||
|
|
||
| # Create multiple actors (Outbox will be created automatically) | ||
| self.stdout.write('Creating actors...') | ||
| actors = ActorFactory.create_batch(10) | ||
|
|
||
| # Create activities (notes will be created automatically) | ||
| self.stdout.write('Creating activities with their notes...') | ||
| for actor in actors: | ||
| ActivityFactory.create_batch( | ||
| 5, | ||
| actor=actor, | ||
| ) | ||
| self.stdout.write( | ||
| self.style.SUCCESS( | ||
| f'Successfully created:\n' | ||
| f'- {len(actors)} actors and their outboxes\n' | ||
| f'- {len(actors) * 5} activities with their notes' | ||
| ) | ||
| ) | ||
|
|
||
| except Exception as e: | ||
| self.stdout.write(self.style.ERROR(f'Error seeding database: {str(e)}')) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,7 +19,25 @@ class Actor(models.Model): | |
|
|
||
| def __str__(self): | ||
| return self.username | ||
|
|
||
| def save(self, *args, **kwargs): | ||
| is_new = self._state.adding | ||
| super().save(*args, **kwargs) | ||
|
|
||
| if is_new: | ||
| # Create a PortabilityOutbox for new actor | ||
| outbox = PortabilityOutbox.objects.create(actor=self) | ||
|
|
||
| # Create an initial Activity for the Actor | ||
| activity = Activity.objects.create( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this supposed to be the activity that announces the creation of the Actor themselves? Is it correct for an Activity of type "Create" to have no internal object?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Create",
"actor": "https://example.com/users/someuser",
"object": {
"type": "Person",
"id": "https://example.com/users/someuser",
"preferredUsername": "someuser",
"name": "Some User",
}
} |
||
| actor=self, | ||
| type='Create', | ||
| visibility='public' | ||
| ) | ||
|
|
||
| outbox.activities.add(activity) | ||
|
|
||
|
|
||
| def get_json_ld(self): | ||
| # Return a LOLA-compliant JSON-LD representation of the account | ||
| return { | ||
|
|
||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,21 +1,23 @@ | ||
| import pytest | ||
| from django.contrib.auth.models import User | ||
| from testbed.core.models import Actor, PortabilityOutbox | ||
| from testbed.core.factories import UserFactory, ActorFactory, NoteFactory, ActivityFactory | ||
|
|
||
|
|
||
| # Create and return a user | ||
| @pytest.fixture | ||
| def user(): | ||
| return User.objects.create_user(username='testuser', password='testpass') | ||
| return UserFactory() | ||
|
|
||
| # Create and return an actor linked to the user | ||
| @pytest.fixture | ||
| def actor(user): | ||
| return Actor.objects.create(user=user, username='testactor', full_name='Test Actor') | ||
| def actor(): | ||
| return ActorFactory() | ||
|
|
||
| # Create and return a portability outbox linked to the actor | ||
| @pytest.fixture | ||
| def outbox(actor): | ||
| return PortabilityOutbox.objects.create(actor=actor) | ||
| def note(actor): | ||
| return NoteFactory(actor=actor) | ||
|
|
||
| @pytest.fixture | ||
| def activity(actor, note): | ||
| return ActivityFactory(actor=actor, note=note) | ||
|
|
||
| @pytest.fixture | ||
| def outbox(actor): | ||
| return actor.portability_outbox.first() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import pytest | ||
|
|
||
|
|
||
| def test_activity_creation(activity): | ||
| assert activity.actor is not None | ||
| assert activity.note is not None | ||
| assert activity.type in ['Create', 'Like', 'Update', 'Follow', 'Announce', 'Delete', 'Undo', 'Flag'] | ||
| assert activity.visibility in ['public', 'private', 'followers-only'] | ||
|
|
||
| def test_activity_str_representation(activity): | ||
| assert activity.type in str(activity) | ||
| assert activity.actor.username in str(activity) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import pytest | ||
|
|
||
|
|
||
| def test_actor_json_ld(actor): | ||
| json_ld = actor.get_json_ld() | ||
| assert json_ld['type'] == 'Person' | ||
| assert json_ld['preferredUsername'] == actor.username | ||
| assert '@context' in json_ld | ||
|
|
||
| def test_note_json_ld(note): | ||
| json_ld = note.get_json_ld() | ||
| assert json_ld['type'] == 'Note' | ||
| assert json_ld['content'] == note.content | ||
| assert '@context' in json_ld | ||
|
|
||
| def test_activity_json_ld(activity): | ||
| json_ld = activity.get_json_ld() | ||
| assert json_ld['type'] == activity.type | ||
| assert 'object' in json_ld | ||
| assert '@context' in json_ld | ||
|
|
||
| def test_outbox_json_ld(outbox): | ||
| json_ld = outbox.get_json_ld() | ||
| assert json_ld['type'] == 'OrderedCollection' | ||
| assert '@context' in json_ld | ||
| assert json_ld['totalItems'] == outbox.activities.count() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,34 +1,28 @@ | ||
| import pytest | ||
| from django.core.exceptions import ValidationError | ||
| from testbed.core.factories import ActorFactory | ||
|
|
||
|
|
||
| # Test that an Actor is created correctly | ||
| @pytest.mark.django_db | ||
| def test_actor_creation(actor): | ||
| assert actor.username == 'testactor' | ||
| assert actor.full_name == 'Test Actor' | ||
| assert actor.user.username == 'testuser' | ||
| assert actor.user is not None | ||
| assert actor.username == actor.user.username | ||
| assert actor.full_name is not None | ||
|
|
||
| # Test that the Actor's JSON-LD representation is correct | ||
| @pytest.mark.django_db | ||
| def test_actor_json_ld(actor): | ||
| expected = { | ||
| "@context": [ | ||
| "https://www.w3.org/ns/activitystreams", | ||
| "https://swicg.github.io/activitypub-data-portability/lola.jsonld", | ||
| ], | ||
| "type": "Person", | ||
| "id": "https://example.com/users/testactor", | ||
| "preferredUsername": "testactor", | ||
| "name": "testactor", | ||
| "previously": {}, | ||
| } | ||
| assert actor.get_json_ld() == expected | ||
| def test_actor_str_representation(actor): | ||
| assert str(actor) == actor.username | ||
|
|
||
| # Test that a Portability Outbox is linked to an Actor | ||
| @pytest.mark.django_db | ||
| def test_outbox_creation(outbox): | ||
| assert outbox.actor.username == 'testactor' | ||
| assert outbox.actor.full_name == 'Test Actor' | ||
| assert outbox.actor.user.username == 'testuser' | ||
| assert outbox.actor.user.actor == outbox.actor | ||
| assert outbox.actor.actor_activities.count() == 0 | ||
| @pytest.mark.parametrize('invalid_username', [ | ||
| 'user space', # Contains space | ||
| 'ab', # Too short | ||
| 'user@name' # Non-alphanumeric | ||
| ]) | ||
| def test_username_validation(invalid_username): | ||
| with pytest.raises(ValidationError): | ||
| actor =ActorFactory(username=invalid_username) | ||
| actor.full_clean() # This triggers the validation | ||
|
|
||
| def test_portability_outbox_creation(actor): | ||
| assert actor.portability_outbox.count() == 1 | ||
| outbox = actor.portability_outbox.first() | ||
| assert outbox.activities.count() == 1 # Initial Create activity | ||
| assert outbox.activities.first().type == 'Create' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import pytest | ||
|
|
||
|
|
||
| def test_note_creation(note): | ||
| assert note.actor is not None | ||
| assert note.content is not None | ||
| assert note.published is not None | ||
| assert note.visibility in ['public', 'private', 'followers-only'] | ||
|
|
||
| def test_note_str_representation(note): | ||
| assert str(note).startswith('Note by') | ||
| assert note.content[:30] in str(note) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is going to create conformant ActivityPub objects. Some of these types can have a Note attached but some shouldn't. This is fine for this PR but we'll need to iterate. Possibly some of the Activity types should be subclasses of the Activity model class that enforce things like whether the type has a note or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this was something I was going to deal with in the next PR.