All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/labhelper) (push) Successful in 20s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/labhelper-data-loader) (push) Successful in 5s
1313 lines
49 KiB
Python
1313 lines
49 KiB
Python
from django.contrib.admin.sites import AdminSite
|
|
from django.contrib.auth.models import User
|
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
from django.db import IntegrityError
|
|
from django.test import Client, TestCase
|
|
from django.urls import reverse
|
|
|
|
from .admin import BoxAdmin, BoxTypeAdmin, ThingAdmin, ThingTypeAdmin
|
|
from .models import Box, BoxType, Thing, ThingType
|
|
|
|
|
|
class AuthTestCase(TestCase):
|
|
"""Base test case that provides authenticated client."""
|
|
|
|
def setUp(self):
|
|
"""Set up test user and authenticated client."""
|
|
self.user = User.objects.create_user(
|
|
username='testuser',
|
|
password='testpass123'
|
|
)
|
|
self.client = Client()
|
|
self.client.login(username='testuser', password='testpass123')
|
|
|
|
|
|
class BoxTypeModelTests(TestCase):
|
|
"""Tests for the BoxType model."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures."""
|
|
self.box_type = BoxType.objects.create(
|
|
name='Small Box',
|
|
width=100,
|
|
height=50,
|
|
length=150
|
|
)
|
|
|
|
def test_box_type_str_returns_name(self):
|
|
"""BoxType __str__ should return the name."""
|
|
self.assertEqual(str(self.box_type), 'Small Box')
|
|
|
|
def test_box_type_creation(self):
|
|
"""BoxType should be created with correct attributes."""
|
|
self.assertEqual(self.box_type.name, 'Small Box')
|
|
self.assertEqual(self.box_type.width, 100)
|
|
self.assertEqual(self.box_type.height, 50)
|
|
self.assertEqual(self.box_type.length, 150)
|
|
|
|
def test_box_type_ordering(self):
|
|
"""BoxTypes should be ordered by name."""
|
|
BoxType.objects.create(name='Alpha Box', width=10, height=10, length=10)
|
|
BoxType.objects.create(name='Zeta Box', width=20, height=20, length=20)
|
|
box_types = list(BoxType.objects.values_list('name', flat=True))
|
|
self.assertEqual(box_types, ['Alpha Box', 'Small Box', 'Zeta Box'])
|
|
|
|
|
|
class BoxModelTests(TestCase):
|
|
"""Tests for the Box model."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures."""
|
|
self.box_type = BoxType.objects.create(
|
|
name='Standard Box',
|
|
width=200,
|
|
height=100,
|
|
length=300
|
|
)
|
|
self.box = Box.objects.create(
|
|
id='BOX001',
|
|
box_type=self.box_type
|
|
)
|
|
|
|
def test_box_str_returns_id(self):
|
|
"""Box __str__ should return the box ID."""
|
|
self.assertEqual(str(self.box), 'BOX001')
|
|
|
|
def test_box_creation(self):
|
|
"""Box should be created with correct attributes."""
|
|
self.assertEqual(self.box.id, 'BOX001')
|
|
self.assertEqual(self.box.box_type, self.box_type)
|
|
|
|
def test_box_type_relationship(self):
|
|
"""Box should be accessible from BoxType via related_name."""
|
|
self.assertIn(self.box, self.box_type.boxes.all())
|
|
|
|
def test_box_id_max_length(self):
|
|
"""Box ID should accept up to 10 characters."""
|
|
box = Box.objects.create(id='ABCD123456', box_type=self.box_type)
|
|
self.assertEqual(len(box.id), 10)
|
|
|
|
def test_box_type_protect_on_delete(self):
|
|
"""Deleting a BoxType with boxes should raise IntegrityError."""
|
|
with self.assertRaises(IntegrityError):
|
|
self.box_type.delete()
|
|
|
|
def test_box_type_delete_when_no_boxes(self):
|
|
"""Deleting a BoxType without boxes should succeed."""
|
|
empty_type = BoxType.objects.create(
|
|
name='Empty Type',
|
|
width=10,
|
|
height=10,
|
|
length=10
|
|
)
|
|
empty_type_id = empty_type.id
|
|
empty_type.delete()
|
|
self.assertFalse(BoxType.objects.filter(id=empty_type_id).exists())
|
|
|
|
|
|
class BoxTypeAdminTests(TestCase):
|
|
"""Tests for the BoxType admin configuration."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures."""
|
|
self.site = AdminSite()
|
|
self.admin = BoxTypeAdmin(BoxType, self.site)
|
|
|
|
def test_list_display(self):
|
|
"""BoxTypeAdmin should display correct fields."""
|
|
self.assertEqual(
|
|
self.admin.list_display,
|
|
('name', 'width', 'height', 'length')
|
|
)
|
|
|
|
def test_search_fields(self):
|
|
"""BoxTypeAdmin should search by name."""
|
|
self.assertEqual(self.admin.search_fields, ('name',))
|
|
|
|
|
|
class BoxAdminTests(TestCase):
|
|
"""Tests for the Box admin configuration."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures."""
|
|
self.site = AdminSite()
|
|
self.admin = BoxAdmin(Box, self.site)
|
|
|
|
def test_list_display(self):
|
|
"""BoxAdmin should display correct fields."""
|
|
self.assertEqual(self.admin.list_display, ('id', 'box_type'))
|
|
|
|
def test_list_filter(self):
|
|
"""BoxAdmin should filter by box_type."""
|
|
self.assertEqual(self.admin.list_filter, ('box_type',))
|
|
|
|
def test_search_fields(self):
|
|
"""BoxAdmin should search by id."""
|
|
self.assertEqual(self.admin.search_fields, ('id',))
|
|
|
|
|
|
class ThingTypeModelTests(TestCase):
|
|
"""Tests for the ThingType model."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures."""
|
|
self.thing_type = ThingType.objects.create(name='Electronics')
|
|
|
|
def test_thing_type_str_returns_name(self):
|
|
"""ThingType __str__ should return the name."""
|
|
self.assertEqual(str(self.thing_type), 'Electronics')
|
|
|
|
def test_thing_type_creation(self):
|
|
"""ThingType should be created with correct attributes."""
|
|
self.assertEqual(self.thing_type.name, 'Electronics')
|
|
|
|
def test_thing_type_hierarchy(self):
|
|
"""ThingType should support parent-child relationships."""
|
|
child = ThingType.objects.create(
|
|
name='Resistors',
|
|
parent=self.thing_type
|
|
)
|
|
self.assertEqual(child.parent, self.thing_type)
|
|
self.assertIn(child, self.thing_type.children.all())
|
|
|
|
def test_thing_type_is_leaf_node(self):
|
|
"""ThingType without children should be a leaf node."""
|
|
self.assertTrue(self.thing_type.is_leaf_node())
|
|
|
|
def test_thing_type_is_not_leaf_with_children(self):
|
|
"""ThingType with children should not be a leaf node."""
|
|
ThingType.objects.create(name='Capacitors', parent=self.thing_type)
|
|
self.assertFalse(self.thing_type.is_leaf_node())
|
|
|
|
def test_thing_type_ancestors(self):
|
|
"""ThingType should return correct ancestors."""
|
|
child = ThingType.objects.create(
|
|
name='Resistors',
|
|
parent=self.thing_type
|
|
)
|
|
grandchild = ThingType.objects.create(
|
|
name='10k Resistors',
|
|
parent=child
|
|
)
|
|
ancestors = list(grandchild.get_ancestors())
|
|
self.assertEqual(ancestors, [self.thing_type, child])
|
|
|
|
def test_thing_type_descendants(self):
|
|
"""ThingType should return correct descendants."""
|
|
child = ThingType.objects.create(
|
|
name='Resistors',
|
|
parent=self.thing_type
|
|
)
|
|
grandchild = ThingType.objects.create(
|
|
name='10k Resistors',
|
|
parent=child
|
|
)
|
|
descendants = list(self.thing_type.get_descendants())
|
|
self.assertEqual(descendants, [child, grandchild])
|
|
|
|
def test_thing_type_level(self):
|
|
"""ThingType should have correct level in hierarchy."""
|
|
self.assertEqual(self.thing_type.level, 0)
|
|
child = ThingType.objects.create(
|
|
name='Resistors',
|
|
parent=self.thing_type
|
|
)
|
|
self.assertEqual(child.level, 1)
|
|
|
|
|
|
class ThingModelTests(TestCase):
|
|
"""Tests for the Thing model."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures."""
|
|
self.box_type = BoxType.objects.create(
|
|
name='Standard Box',
|
|
width=200,
|
|
height=100,
|
|
length=300
|
|
)
|
|
self.box = Box.objects.create(
|
|
id='BOX001',
|
|
box_type=self.box_type
|
|
)
|
|
self.thing_type = ThingType.objects.create(name='Electronics')
|
|
self.thing = Thing.objects.create(
|
|
name='Arduino Uno',
|
|
thing_type=self.thing_type,
|
|
box=self.box
|
|
)
|
|
|
|
def test_thing_str_returns_name(self):
|
|
"""Thing __str__ should return the name."""
|
|
self.assertEqual(str(self.thing), 'Arduino Uno')
|
|
|
|
def test_thing_creation(self):
|
|
"""Thing should be created with correct attributes."""
|
|
self.assertEqual(self.thing.name, 'Arduino Uno')
|
|
self.assertEqual(self.thing.thing_type, self.thing_type)
|
|
self.assertEqual(self.thing.box, self.box)
|
|
|
|
def test_thing_optional_description(self):
|
|
"""Thing description should be optional."""
|
|
self.assertEqual(self.thing.description, '')
|
|
self.thing.description = 'A microcontroller board'
|
|
self.thing.save()
|
|
self.thing.refresh_from_db()
|
|
self.assertEqual(self.thing.description, 'A microcontroller board')
|
|
|
|
def test_thing_optional_picture(self):
|
|
"""Thing picture should be optional."""
|
|
self.assertEqual(self.thing.picture.name, '')
|
|
|
|
def test_thing_with_picture(self):
|
|
"""Thing should accept an image upload."""
|
|
# Create a simple 1x1 pixel PNG
|
|
image_data = (
|
|
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01'
|
|
b'\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00'
|
|
b'\x00\x00\x0cIDATx\x9cc\xf8\x0f\x00\x00\x01\x01\x00'
|
|
b'\x05\x18\xd8N\x00\x00\x00\x00IEND\xaeB`\x82'
|
|
)
|
|
image = SimpleUploadedFile(
|
|
name='test.png',
|
|
content=image_data,
|
|
content_type='image/png'
|
|
)
|
|
thing = Thing.objects.create(
|
|
name='Test Item',
|
|
thing_type=self.thing_type,
|
|
box=self.box,
|
|
picture=image
|
|
)
|
|
self.assertTrue(thing.picture.name.startswith('things/'))
|
|
# Clean up
|
|
thing.picture.delete()
|
|
|
|
def test_thing_ordering(self):
|
|
"""Things should be ordered by name."""
|
|
Thing.objects.create(
|
|
name='Zeta Item',
|
|
thing_type=self.thing_type,
|
|
box=self.box
|
|
)
|
|
Thing.objects.create(
|
|
name='Alpha Item',
|
|
thing_type=self.thing_type,
|
|
box=self.box
|
|
)
|
|
things = list(Thing.objects.values_list('name', flat=True))
|
|
self.assertEqual(things, ['Alpha Item', 'Arduino Uno', 'Zeta Item'])
|
|
|
|
def test_thing_type_relationship(self):
|
|
"""Thing should be accessible from ThingType via related_name."""
|
|
self.assertIn(self.thing, self.thing_type.things.all())
|
|
|
|
def test_thing_box_relationship(self):
|
|
"""Thing should be accessible from Box via related_name."""
|
|
self.assertIn(self.thing, self.box.things.all())
|
|
|
|
def test_thing_type_protect_on_delete(self):
|
|
"""Deleting a ThingType with things should raise IntegrityError."""
|
|
with self.assertRaises(IntegrityError):
|
|
self.thing_type.delete()
|
|
|
|
def test_box_protect_on_delete_with_things(self):
|
|
"""Deleting a Box with things should raise IntegrityError."""
|
|
with self.assertRaises(IntegrityError):
|
|
self.box.delete()
|
|
|
|
|
|
class ThingTypeAdminTests(TestCase):
|
|
"""Tests for the ThingType admin configuration."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures."""
|
|
self.site = AdminSite()
|
|
self.admin = ThingTypeAdmin(ThingType, self.site)
|
|
|
|
def test_search_fields(self):
|
|
"""ThingTypeAdmin should search by name."""
|
|
self.assertEqual(self.admin.search_fields, ('name',))
|
|
|
|
|
|
class ThingAdminTests(TestCase):
|
|
"""Tests for the Thing admin configuration."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures."""
|
|
self.site = AdminSite()
|
|
self.admin = ThingAdmin(Thing, self.site)
|
|
|
|
def test_list_display(self):
|
|
"""ThingAdmin should display correct fields."""
|
|
self.assertEqual(
|
|
self.admin.list_display,
|
|
('name', 'thing_type', 'box')
|
|
)
|
|
|
|
def test_list_filter(self):
|
|
"""ThingAdmin should filter by thing_type and box."""
|
|
self.assertEqual(self.admin.list_filter, ('thing_type', 'box'))
|
|
|
|
def test_search_fields(self):
|
|
"""ThingAdmin should search by name and description."""
|
|
self.assertEqual(self.admin.search_fields, ('name', 'description'))
|
|
|
|
|
|
class IndexViewTests(AuthTestCase):
|
|
"""Tests for the index view."""
|
|
|
|
def test_index_returns_200(self):
|
|
"""Index page should return 200 status."""
|
|
response = self.client.get('/')
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
def test_index_contains_labhelper(self):
|
|
"""Index page should contain LabHelper title."""
|
|
response = self.client.get('/')
|
|
self.assertContains(response, 'LabHelper')
|
|
|
|
def test_index_contains_admin_link(self):
|
|
"""Index page should contain link to admin."""
|
|
response = self.client.get('/')
|
|
self.assertContains(response, '/admin/')
|
|
|
|
def test_index_requires_login(self):
|
|
"""Index page should redirect to login if not authenticated."""
|
|
self.client.logout()
|
|
response = self.client.get('/')
|
|
self.assertRedirects(response, '/login/?next=/')
|
|
|
|
|
|
class BoxDetailViewTests(AuthTestCase):
|
|
"""Tests for the box detail view."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures."""
|
|
super().setUp()
|
|
self.box_type = BoxType.objects.create(
|
|
name='Standard Box',
|
|
width=200,
|
|
height=100,
|
|
length=300
|
|
)
|
|
self.box = Box.objects.create(
|
|
id='BOX001',
|
|
box_type=self.box_type
|
|
)
|
|
self.thing_type = ThingType.objects.create(name='Electronics')
|
|
|
|
def test_box_detail_returns_200(self):
|
|
"""Box detail page should return 200 status."""
|
|
response = self.client.get(f'/box/{self.box.id}/')
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
def test_box_detail_returns_404_for_invalid_box(self):
|
|
"""Box detail page should return 404 for non-existent box."""
|
|
response = self.client.get('/box/INVALID/')
|
|
self.assertEqual(response.status_code, 404)
|
|
|
|
def test_box_detail_shows_box_id(self):
|
|
"""Box detail page should show box ID."""
|
|
response = self.client.get(f'/box/{self.box.id}/')
|
|
self.assertContains(response, 'BOX001')
|
|
|
|
def test_box_detail_shows_box_type(self):
|
|
"""Box detail page should show box type name."""
|
|
response = self.client.get(f'/box/{self.box.id}/')
|
|
self.assertContains(response, 'Standard Box')
|
|
|
|
def test_box_detail_shows_dimensions(self):
|
|
"""Box detail page should show box dimensions."""
|
|
response = self.client.get(f'/box/{self.box.id}/')
|
|
self.assertContains(response, '200')
|
|
self.assertContains(response, '100')
|
|
self.assertContains(response, '300')
|
|
|
|
def test_box_detail_shows_empty_message(self):
|
|
"""Box detail page should show empty message when no things."""
|
|
response = self.client.get(f'/box/{self.box.id}/')
|
|
self.assertContains(response, 'This box is empty')
|
|
|
|
def test_box_detail_shows_thing(self):
|
|
"""Box detail page should show things in the box."""
|
|
Thing.objects.create(
|
|
name='Arduino Uno',
|
|
thing_type=self.thing_type,
|
|
box=self.box,
|
|
description='A microcontroller board'
|
|
)
|
|
response = self.client.get(f'/box/{self.box.id}/')
|
|
self.assertContains(response, 'Arduino Uno')
|
|
self.assertContains(response, 'Electronics')
|
|
self.assertContains(response, 'A microcontroller board')
|
|
|
|
def test_box_detail_shows_no_image_placeholder(self):
|
|
"""Box detail page should show placeholder for things without images."""
|
|
Thing.objects.create(
|
|
name='Test Item',
|
|
thing_type=self.thing_type,
|
|
box=self.box
|
|
)
|
|
response = self.client.get(f'/box/{self.box.id}/')
|
|
self.assertContains(response, 'No image')
|
|
|
|
def test_box_detail_shows_multiple_things(self):
|
|
"""Box detail page should show multiple things."""
|
|
Thing.objects.create(
|
|
name='Item One',
|
|
thing_type=self.thing_type,
|
|
box=self.box
|
|
)
|
|
Thing.objects.create(
|
|
name='Item Two',
|
|
thing_type=self.thing_type,
|
|
box=self.box
|
|
)
|
|
response = self.client.get(f'/box/{self.box.id}/')
|
|
self.assertContains(response, 'Item One')
|
|
self.assertContains(response, 'Item Two')
|
|
|
|
def test_box_detail_uses_correct_template(self):
|
|
"""Box detail page should use the correct template."""
|
|
response = self.client.get(f'/box/{self.box.id}/')
|
|
self.assertTemplateUsed(response, 'boxes/box_detail.html')
|
|
|
|
def test_box_detail_with_image(self):
|
|
"""Box detail page should show thumbnail for things with images."""
|
|
# Create a simple 1x1 pixel PNG
|
|
image_data = (
|
|
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01'
|
|
b'\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00'
|
|
b'\x00\x00\x0cIDATx\x9cc\xf8\x0f\x00\x00\x01\x01\x00'
|
|
b'\x05\x18\xd8N\x00\x00\x00\x00IEND\xaeB`\x82'
|
|
)
|
|
image = SimpleUploadedFile(
|
|
name='test.png',
|
|
content=image_data,
|
|
content_type='image/png'
|
|
)
|
|
thing = Thing.objects.create(
|
|
name='Item With Image',
|
|
thing_type=self.thing_type,
|
|
box=self.box,
|
|
picture=image
|
|
)
|
|
response = self.client.get(f'/box/{self.box.id}/')
|
|
self.assertContains(response, 'Item With Image')
|
|
self.assertContains(response, '<img')
|
|
# Clean up
|
|
thing.picture.delete()
|
|
|
|
def test_box_detail_url_name(self):
|
|
"""Box detail URL should be reversible by name."""
|
|
url = reverse('box_detail', kwargs={'box_id': 'BOX001'})
|
|
self.assertEqual(url, '/box/BOX001/')
|
|
|
|
def test_box_detail_requires_login(self):
|
|
"""Box detail page should redirect to login if not authenticated."""
|
|
self.client.logout()
|
|
response = self.client.get(f'/box/{self.box.id}/')
|
|
self.assertRedirects(response, f'/login/?next=/box/{self.box.id}/')
|
|
|
|
|
|
class ThingDetailViewTests(AuthTestCase):
|
|
"""Tests for thing detail view."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures."""
|
|
super().setUp()
|
|
self.box_type = BoxType.objects.create(
|
|
name='Standard Box',
|
|
width=200,
|
|
height=100,
|
|
length=300
|
|
)
|
|
self.box = Box.objects.create(
|
|
id='BOX001',
|
|
box_type=self.box_type
|
|
)
|
|
self.thing_type = ThingType.objects.create(name='Electronics')
|
|
self.thing = Thing.objects.create(
|
|
name='Arduino Uno',
|
|
thing_type=self.thing_type,
|
|
box=self.box,
|
|
description='A microcontroller board'
|
|
)
|
|
|
|
def test_thing_detail_returns_200(self):
|
|
"""Thing detail page should return 200 status."""
|
|
response = self.client.get(f'/thing/{self.thing.id}/')
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
def test_thing_detail_returns_404_for_invalid_thing(self):
|
|
"""Thing detail page should return 404 for non-existent thing."""
|
|
response = self.client.get('/thing/99999/')
|
|
self.assertEqual(response.status_code, 404)
|
|
|
|
def test_thing_detail_shows_thing_name(self):
|
|
"""Thing detail page should show thing name."""
|
|
response = self.client.get(f'/thing/{self.thing.id}/')
|
|
self.assertContains(response, 'Arduino Uno')
|
|
|
|
def test_thing_detail_shows_type(self):
|
|
"""Thing detail page should show thing type."""
|
|
response = self.client.get(f'/thing/{self.thing.id}/')
|
|
self.assertContains(response, 'Electronics')
|
|
|
|
def test_thing_detail_shows_description(self):
|
|
"""Thing detail page should show thing description."""
|
|
response = self.client.get(f'/thing/{self.thing.id}/')
|
|
self.assertContains(response, 'A microcontroller board')
|
|
|
|
def test_thing_detail_shows_box(self):
|
|
"""Thing detail page should show box info."""
|
|
response = self.client.get(f'/thing/{self.thing.id}/')
|
|
self.assertContains(response, 'BOX001')
|
|
self.assertContains(response, 'Standard Box')
|
|
|
|
def test_thing_detail_uses_correct_template(self):
|
|
"""Thing detail page should use correct template."""
|
|
response = self.client.get(f'/thing/{self.thing.id}/')
|
|
self.assertTemplateUsed(response, 'boxes/thing_detail.html')
|
|
|
|
def test_thing_detail_url_name(self):
|
|
"""Thing detail URL should be reversible by name."""
|
|
url = reverse('thing_detail', kwargs={'thing_id': 1})
|
|
self.assertEqual(url, '/thing/1/')
|
|
|
|
def test_thing_detail_requires_login(self):
|
|
"""Thing detail page should redirect to login if not authenticated."""
|
|
self.client.logout()
|
|
response = self.client.get(f'/thing/{self.thing.id}/')
|
|
self.assertRedirects(response, f'/login/?next=/thing/{self.thing.id}/')
|
|
|
|
def test_thing_detail_move_to_box(self):
|
|
"""Thing can be moved to another box via POST."""
|
|
new_box = Box.objects.create(id='BOX002', box_type=self.box_type)
|
|
response = self.client.post(
|
|
f'/thing/{self.thing.id}/',
|
|
{'action': 'move', 'new_box': 'BOX002'}
|
|
)
|
|
self.assertRedirects(response, f'/thing/{self.thing.id}/')
|
|
self.thing.refresh_from_db()
|
|
self.assertEqual(self.thing.box, new_box)
|
|
|
|
def test_thing_detail_move_shows_all_boxes(self):
|
|
"""Thing detail page should show all available boxes in dropdown."""
|
|
Box.objects.create(id='BOX002', box_type=self.box_type)
|
|
Box.objects.create(id='BOX003', box_type=self.box_type)
|
|
response = self.client.get(f'/thing/{self.thing.id}/')
|
|
self.assertContains(response, 'BOX001')
|
|
self.assertContains(response, 'BOX002')
|
|
self.assertContains(response, 'BOX003')
|
|
|
|
|
|
class SearchViewTests(AuthTestCase):
|
|
"""Tests for search view."""
|
|
|
|
def test_search_returns_200(self):
|
|
"""Search page should return 200 status."""
|
|
response = self.client.get('/search/')
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
def test_search_contains_search_input(self):
|
|
"""Search page should contain search input field."""
|
|
response = self.client.get('/search/')
|
|
self.assertContains(response, 'id="search-input"')
|
|
|
|
def test_search_contains_results_container(self):
|
|
"""Search page should contain results table."""
|
|
response = self.client.get('/search/')
|
|
self.assertContains(response, 'id="results-container"')
|
|
|
|
def test_search_requires_login(self):
|
|
"""Search page should redirect to login if not authenticated."""
|
|
self.client.logout()
|
|
response = self.client.get('/search/')
|
|
self.assertRedirects(response, '/login/?next=/search/')
|
|
|
|
|
|
class SearchApiTests(AuthTestCase):
|
|
"""Tests for search API."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures."""
|
|
super().setUp()
|
|
self.box_type = BoxType.objects.create(
|
|
name='Standard Box',
|
|
width=200,
|
|
height=100,
|
|
length=300
|
|
)
|
|
self.box = Box.objects.create(
|
|
id='BOX001',
|
|
box_type=self.box_type
|
|
)
|
|
self.thing_type = ThingType.objects.create(name='Electronics')
|
|
Thing.objects.create(
|
|
name='Arduino Uno',
|
|
thing_type=self.thing_type,
|
|
box=self.box,
|
|
description='A microcontroller board'
|
|
)
|
|
Thing.objects.create(
|
|
name='Raspberry Pi',
|
|
thing_type=self.thing_type,
|
|
box=self.box
|
|
)
|
|
|
|
def test_search_api_returns_empty_for_short_query(self):
|
|
"""Search API should return empty results for queries under 2 chars."""
|
|
response = self.client.get('/search/api/?q=a')
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(response.json()['results'], [])
|
|
|
|
def test_search_api_returns_results(self):
|
|
"""Search API should return matching results."""
|
|
response = self.client.get('/search/api/?q=ard')
|
|
self.assertEqual(response.status_code, 200)
|
|
results = response.json()['results']
|
|
self.assertEqual(len(results), 1)
|
|
self.assertEqual(results[0]['name'], 'Arduino Uno')
|
|
|
|
def test_search_api_is_case_insensitive(self):
|
|
"""Search API should be case-insensitive."""
|
|
response = self.client.get('/search/api/?q=ARDUINO')
|
|
self.assertEqual(response.status_code, 200)
|
|
results = response.json()['results']
|
|
self.assertEqual(len(results), 1)
|
|
self.assertEqual(results[0]['name'], 'Arduino Uno')
|
|
|
|
def test_search_api_truncates_description(self):
|
|
"""Search API should truncate long descriptions."""
|
|
Thing.objects.create(
|
|
name='Long Description Item',
|
|
thing_type=self.thing_type,
|
|
box=self.box,
|
|
description='A' * 200
|
|
)
|
|
response = self.client.get('/search/api/?q=long')
|
|
self.assertEqual(response.status_code, 200)
|
|
results = response.json()['results']
|
|
self.assertEqual(len(results), 1)
|
|
self.assertLessEqual(len(results[0]['description']), 100)
|
|
|
|
def test_search_api_limits_results(self):
|
|
"""Search API should limit results to 50."""
|
|
for i in range(60):
|
|
Thing.objects.create(
|
|
name=f'Item {i}',
|
|
thing_type=self.thing_type,
|
|
box=self.box
|
|
)
|
|
response = self.client.get('/search/api/?q=Item')
|
|
self.assertEqual(response.status_code, 200)
|
|
results = response.json()['results']
|
|
self.assertEqual(len(results), 50)
|
|
|
|
def test_search_api_includes_type_and_box(self):
|
|
"""Search API results should include type and box info."""
|
|
response = self.client.get('/search/api/?q=ard')
|
|
self.assertEqual(response.status_code, 200)
|
|
results = response.json()['results']
|
|
self.assertEqual(results[0]['type'], 'Electronics')
|
|
self.assertEqual(results[0]['box'], 'BOX001')
|
|
|
|
def test_search_api_requires_login(self):
|
|
"""Search API should redirect to login if not authenticated."""
|
|
self.client.logout()
|
|
response = self.client.get('/search/api/?q=test')
|
|
self.assertRedirects(response, '/login/?next=/search/api/%3Fq%3Dtest')
|
|
|
|
|
|
class AddThingsViewTests(AuthTestCase):
|
|
"""Tests for add things view."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures."""
|
|
super().setUp()
|
|
self.box_type = BoxType.objects.create(
|
|
name='Standard Box',
|
|
width=200,
|
|
height=100,
|
|
length=300
|
|
)
|
|
self.box = Box.objects.create(
|
|
id='BOX001',
|
|
box_type=self.box_type
|
|
)
|
|
self.thing_type = ThingType.objects.create(name='Electronics')
|
|
|
|
def test_add_things_get_request(self):
|
|
"""Add things page should return 200 for GET request."""
|
|
response = self.client.get(f'/box/{self.box.id}/add/')
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
def test_add_things_shows_box_id(self):
|
|
"""Add things page should show box ID."""
|
|
response = self.client.get(f'/box/{self.box.id}/add/')
|
|
self.assertContains(response, 'BOX001')
|
|
self.assertContains(response, 'Standard Box')
|
|
|
|
def test_add_things_shows_form(self):
|
|
"""Add things page should show form."""
|
|
response = self.client.get(f'/box/{self.box.id}/add/')
|
|
self.assertContains(response, 'Save Things')
|
|
self.assertContains(response, 'Name')
|
|
self.assertContains(response, 'Type')
|
|
self.assertContains(response, 'Description')
|
|
self.assertContains(response, 'Picture')
|
|
|
|
def test_add_things_post_valid(self):
|
|
"""Adding valid things should show success message and create things."""
|
|
response = self.client.post(f'/box/{self.box.id}/add/', {
|
|
'form-TOTAL_FORMS': '3',
|
|
'form-INITIAL_FORMS': '0',
|
|
'form-0-name': 'Arduino Uno',
|
|
'form-0-thing_type': self.thing_type.id,
|
|
'form-0-description': 'A microcontroller',
|
|
'form-1-name': 'LED Strip',
|
|
'form-1-thing_type': self.thing_type.id,
|
|
'form-1-description': 'Lighting component',
|
|
'form-2-name': '',
|
|
'form-2-thing_type': '',
|
|
'form-2-description': '',
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, 'Added 2 things successfully')
|
|
self.assertEqual(Thing.objects.count(), 2)
|
|
|
|
def test_add_things_post_required_name(self):
|
|
"""Adding things without name should show error."""
|
|
response = self.client.post(f'/box/{self.box.id}/add/', {
|
|
'form-TOTAL_FORMS': '2',
|
|
'form-0-thing_type': self.thing_type.id,
|
|
'form-1-thing_type': self.thing_type.id,
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, 'This field is required')
|
|
|
|
def test_add_things_post_partial_valid_invalid(self):
|
|
"""Partial submission: one valid, one missing name - nothing saved due to formset validation."""
|
|
response = self.client.post(f'/box/{self.box.id}/add/', {
|
|
'form-TOTAL_FORMS': '2',
|
|
'form-INITIAL_FORMS': '0',
|
|
'form-0-name': 'Arduino Uno',
|
|
'form-0-thing_type': self.thing_type.id,
|
|
'form-1-name': '',
|
|
'form-1-thing_type': self.thing_type.id,
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, 'This field is required')
|
|
# Formset validation fails, so nothing is saved
|
|
self.assertEqual(Thing.objects.count(), 0)
|
|
|
|
def test_add_things_creates_thing_types(self):
|
|
"""Can add things with different thing types."""
|
|
new_type = ThingType.objects.create(name='Components')
|
|
response = self.client.post(f'/box/{self.box.id}/add/', {
|
|
'form-TOTAL_FORMS': '2',
|
|
'form-INITIAL_FORMS': '0',
|
|
'form-0-name': 'Resistor',
|
|
'form-0-thing_type': new_type.id,
|
|
'form-1-name': 'Capacitor',
|
|
'form-1-thing_type': new_type.id,
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, 'Added 2 things successfully')
|
|
self.assertEqual(Thing.objects.count(), 2)
|
|
self.assertEqual(Thing.objects.filter(thing_type=new_type).count(), 2)
|
|
|
|
def test_add_things_empty_all(self):
|
|
"""Submitting empty forms should not create anything."""
|
|
response = self.client.post(f'/box/{self.box.id}/add/', {
|
|
'form-TOTAL_FORMS': '2',
|
|
'form-INITIAL_FORMS': '0',
|
|
'form-0-name': '',
|
|
'form-0-thing_type': '',
|
|
'form-1-name': '',
|
|
'form-1-thing_type': '',
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(Thing.objects.count(), 0)
|
|
|
|
def test_add_things_box_not_exists(self):
|
|
"""Adding things to non-existent box should return 404."""
|
|
response = self.client.get('/box/INVALID/add/')
|
|
self.assertEqual(response.status_code, 404)
|
|
|
|
def test_add_things_populates_box(self):
|
|
"""Created things should be assigned to the correct box."""
|
|
self.thing_type_2 = ThingType.objects.create(name='Mechanical')
|
|
response = self.client.post(f'/box/{self.box.id}/add/', {
|
|
'form-TOTAL_FORMS': '3',
|
|
'form-INITIAL_FORMS': '0',
|
|
'form-0-name': 'Bolt',
|
|
'form-0-thing_type': self.thing_type_2.id,
|
|
'form-1-name': 'Nut',
|
|
'form-1-thing_type': self.thing_type_2.id,
|
|
'form-2-name': 'Washer',
|
|
'form-2-thing_type': self.thing_type_2.id,
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, 'Added 3 things successfully')
|
|
things = Thing.objects.all()
|
|
self.assertEqual(things.count(), 3)
|
|
for thing in things:
|
|
self.assertEqual(thing.box, self.box)
|
|
|
|
def test_add_things_requires_login(self):
|
|
"""Add things page should redirect to login if not authenticated."""
|
|
self.client.logout()
|
|
response = self.client.get(f'/box/{self.box.id}/add/')
|
|
self.assertRedirects(response, f'/login/?next=/box/{self.box.id}/add/')
|
|
|
|
|
|
class ThingTypeDetailViewTests(AuthTestCase):
|
|
"""Tests for thing type detail view."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures."""
|
|
super().setUp()
|
|
self.box_type = BoxType.objects.create(
|
|
name='Standard Box',
|
|
width=200,
|
|
height=100,
|
|
length=300
|
|
)
|
|
self.box = Box.objects.create(
|
|
id='BOX001',
|
|
box_type=self.box_type
|
|
)
|
|
self.parent_type = ThingType.objects.create(name='Electronics')
|
|
self.child_type = ThingType.objects.create(
|
|
name='Microcontrollers',
|
|
parent=self.parent_type
|
|
)
|
|
self.thing = Thing.objects.create(
|
|
name='Arduino Uno',
|
|
thing_type=self.child_type,
|
|
box=self.box
|
|
)
|
|
|
|
def test_thing_type_detail_returns_200(self):
|
|
"""Thing type detail page should return 200 status."""
|
|
response = self.client.get(f'/thing-type/{self.parent_type.id}/')
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
def test_thing_type_detail_returns_404_for_invalid(self):
|
|
"""Thing type detail page should return 404 for non-existent type."""
|
|
response = self.client.get('/thing-type/99999/')
|
|
self.assertEqual(response.status_code, 404)
|
|
|
|
def test_thing_type_detail_shows_type_name(self):
|
|
"""Thing type detail page should show type name."""
|
|
response = self.client.get(f'/thing-type/{self.parent_type.id}/')
|
|
self.assertContains(response, 'Electronics')
|
|
|
|
def test_thing_type_detail_shows_descendants(self):
|
|
"""Thing type detail page should show descendant types."""
|
|
response = self.client.get(f'/thing-type/{self.parent_type.id}/')
|
|
self.assertContains(response, 'Microcontrollers')
|
|
|
|
def test_thing_type_detail_shows_things(self):
|
|
"""Thing type detail page should show things of this type."""
|
|
response = self.client.get(f'/thing-type/{self.parent_type.id}/')
|
|
self.assertContains(response, 'Arduino Uno')
|
|
|
|
def test_thing_type_detail_uses_correct_template(self):
|
|
"""Thing type detail page should use correct template."""
|
|
response = self.client.get(f'/thing-type/{self.parent_type.id}/')
|
|
self.assertTemplateUsed(response, 'boxes/thing_type_detail.html')
|
|
|
|
def test_thing_type_detail_url_name(self):
|
|
"""Thing type detail URL should be reversible by name."""
|
|
url = reverse('thing_type_detail', kwargs={'type_id': 1})
|
|
self.assertEqual(url, '/thing-type/1/')
|
|
|
|
def test_thing_type_detail_requires_login(self):
|
|
"""Thing type detail page should redirect to login if not authenticated."""
|
|
self.client.logout()
|
|
response = self.client.get(f'/thing-type/{self.parent_type.id}/')
|
|
self.assertRedirects(response, f'/login/?next=/thing-type/{self.parent_type.id}/')
|
|
|
|
|
|
class LoginViewTests(TestCase):
|
|
"""Tests for login view."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures."""
|
|
self.client = Client()
|
|
self.user = User.objects.create_user(
|
|
username='testuser',
|
|
password='testpass123'
|
|
)
|
|
|
|
def test_login_page_returns_200(self):
|
|
"""Login page should return 200 status."""
|
|
response = self.client.get('/login/')
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
def test_login_page_contains_form(self):
|
|
"""Login page should contain login form."""
|
|
response = self.client.get('/login/')
|
|
self.assertContains(response, 'Username')
|
|
self.assertContains(response, 'Password')
|
|
self.assertContains(response, 'Login')
|
|
|
|
def test_login_with_valid_credentials(self):
|
|
"""Login with valid credentials should redirect to home."""
|
|
response = self.client.post('/login/', {
|
|
'username': 'testuser',
|
|
'password': 'testpass123'
|
|
})
|
|
self.assertRedirects(response, '/')
|
|
|
|
def test_login_with_invalid_credentials(self):
|
|
"""Login with invalid credentials should show error."""
|
|
response = self.client.post('/login/', {
|
|
'username': 'testuser',
|
|
'password': 'wrongpassword'
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, 'username and password')
|
|
|
|
def test_login_redirects_to_next(self):
|
|
"""Login should redirect to 'next' parameter after success."""
|
|
response = self.client.post('/login/?next=/search/', {
|
|
'username': 'testuser',
|
|
'password': 'testpass123'
|
|
})
|
|
self.assertRedirects(response, '/search/')
|
|
|
|
def test_logout_redirects_to_login(self):
|
|
"""Logout should redirect to login page."""
|
|
self.client.login(username='testuser', password='testpass123')
|
|
response = self.client.post('/logout/')
|
|
self.assertRedirects(response, '/login/')
|
|
|
|
|
|
class BoxManagementViewTests(AuthTestCase):
|
|
"""Tests for box management view."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures."""
|
|
super().setUp()
|
|
self.box_type = BoxType.objects.create(
|
|
name='Standard Box',
|
|
width=200,
|
|
height=100,
|
|
length=300
|
|
)
|
|
self.box = Box.objects.create(
|
|
id='BOX001',
|
|
box_type=self.box_type
|
|
)
|
|
|
|
def test_box_management_returns_200(self):
|
|
"""Box management page should return 200 status."""
|
|
response = self.client.get('/box-management/')
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
def test_box_management_shows_box_types(self):
|
|
"""Box management page should show box types."""
|
|
response = self.client.get('/box-management/')
|
|
self.assertContains(response, 'Standard Box')
|
|
|
|
def test_box_management_shows_boxes(self):
|
|
"""Box management page should show boxes."""
|
|
response = self.client.get('/box-management/')
|
|
self.assertContains(response, 'BOX001')
|
|
|
|
def test_box_management_shows_add_box_type_form(self):
|
|
"""Box management page should show add box type form."""
|
|
response = self.client.get('/box-management/')
|
|
self.assertContains(response, 'Add New Box Type')
|
|
self.assertContains(response, 'name="name"')
|
|
self.assertContains(response, 'name="width"')
|
|
self.assertContains(response, 'name="height"')
|
|
self.assertContains(response, 'name="length"')
|
|
|
|
def test_box_management_shows_add_box_form(self):
|
|
"""Box management page should show add box form."""
|
|
response = self.client.get('/box-management/')
|
|
self.assertContains(response, 'Add New Box')
|
|
self.assertContains(response, 'name="id"')
|
|
self.assertContains(response, 'name="box_type"')
|
|
|
|
def test_box_management_requires_login(self):
|
|
"""Box management page should redirect to login if not authenticated."""
|
|
self.client.logout()
|
|
response = self.client.get('/box-management/')
|
|
self.assertRedirects(response, '/login/?next=/box-management/')
|
|
|
|
|
|
class BoxTypeCRUDTests(AuthTestCase):
|
|
"""Tests for box type CRUD operations."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures."""
|
|
super().setUp()
|
|
self.box_type = BoxType.objects.create(
|
|
name='Standard Box',
|
|
width=200,
|
|
height=100,
|
|
length=300
|
|
)
|
|
|
|
def test_add_box_type_post_creates_box_type(self):
|
|
"""Adding a box type should create it in the database."""
|
|
response = self.client.post('/box-type/add/', {
|
|
'name': 'Large Box',
|
|
'width': '400',
|
|
'height': '200',
|
|
'length': '600'
|
|
})
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertRedirects(response, '/box-management/')
|
|
self.assertTrue(BoxType.objects.filter(name='Large Box').exists())
|
|
|
|
def test_add_box_type_invalid_data(self):
|
|
"""Adding a box type with invalid data should not create it."""
|
|
response = self.client.post('/box-type/add/', {
|
|
'name': 'Invalid Box',
|
|
'width': 'invalid',
|
|
'height': '100',
|
|
'length': '200'
|
|
})
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertFalse(BoxType.objects.filter(name='Invalid Box').exists())
|
|
|
|
def test_edit_box_type_updates_box_type(self):
|
|
"""Editing a box type should update it in the database."""
|
|
response = self.client.post(f'/box-type/{self.box_type.id}/edit/', {
|
|
'name': 'Updated Box',
|
|
'width': '300',
|
|
'height': '150',
|
|
'length': '450'
|
|
})
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertRedirects(response, '/box-management/')
|
|
self.box_type.refresh_from_db()
|
|
self.assertEqual(self.box_type.name, 'Updated Box')
|
|
self.assertEqual(self.box_type.width, 300)
|
|
|
|
def test_edit_box_type_invalid_data(self):
|
|
"""Editing a box type with invalid data should not update it."""
|
|
old_name = self.box_type.name
|
|
response = self.client.post(f'/box-type/{self.box_type.id}/edit/', {
|
|
'name': 'Updated Box',
|
|
'width': 'invalid',
|
|
'height': '150',
|
|
'length': '450'
|
|
})
|
|
self.assertEqual(response.status_code, 302)
|
|
self.box_type.refresh_from_db()
|
|
self.assertEqual(self.box_type.name, old_name)
|
|
|
|
def test_delete_box_type_deletes_box_type(self):
|
|
"""Deleting a box type should remove it from the database."""
|
|
type_id = self.box_type.id
|
|
response = self.client.post(f'/box-type/{type_id}/delete/')
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertRedirects(response, '/box-management/')
|
|
self.assertFalse(BoxType.objects.filter(id=type_id).exists())
|
|
|
|
def test_delete_box_type_with_boxes_redirects(self):
|
|
"""Deleting a box type with boxes should redirect without deleting."""
|
|
Box.objects.create(id='BOX001', box_type=self.box_type)
|
|
type_id = self.box_type.id
|
|
response = self.client.post(f'/box-type/{type_id}/delete/')
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertRedirects(response, '/box-management/')
|
|
self.assertTrue(BoxType.objects.filter(id=type_id).exists())
|
|
|
|
|
|
class BoxCRUDTests(AuthTestCase):
|
|
"""Tests for box CRUD operations."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures."""
|
|
super().setUp()
|
|
self.box_type = BoxType.objects.create(
|
|
name='Standard Box',
|
|
width=200,
|
|
height=100,
|
|
length=300
|
|
)
|
|
self.box = Box.objects.create(
|
|
id='BOX001',
|
|
box_type=self.box_type
|
|
)
|
|
|
|
def test_add_box_post_creates_box(self):
|
|
"""Adding a box should create it in the database."""
|
|
response = self.client.post('/box/add/', {
|
|
'id': 'BOX002',
|
|
'box_type': str(self.box_type.id)
|
|
})
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertRedirects(response, '/box-management/')
|
|
self.assertTrue(Box.objects.filter(id='BOX002').exists())
|
|
|
|
def test_add_box_invalid_data(self):
|
|
"""Adding a box with invalid data should not create it."""
|
|
response = self.client.post('/box/add/', {
|
|
'id': '',
|
|
'box_type': str(self.box_type.id)
|
|
})
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertEqual(Box.objects.count(), 1)
|
|
|
|
def test_edit_box_updates_box_type(self):
|
|
"""Editing a box should update its box type in the database."""
|
|
new_type = BoxType.objects.create(
|
|
name='Large Box',
|
|
width=400,
|
|
height=200,
|
|
length=600
|
|
)
|
|
response = self.client.post(f'/box/{self.box.id}/edit/', {
|
|
'id': 'BOX001',
|
|
'box_type': str(new_type.id)
|
|
})
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertRedirects(response, '/box-management/')
|
|
self.box.refresh_from_db()
|
|
self.assertEqual(self.box.box_type, new_type)
|
|
|
|
def test_edit_box_invalid_data(self):
|
|
"""Editing a box with invalid data should not update it."""
|
|
old_id = self.box.id
|
|
response = self.client.post(f'/box/{self.box.id}/edit/', {
|
|
'id': '',
|
|
'box_type': str(self.box_type.id)
|
|
})
|
|
self.assertEqual(response.status_code, 302)
|
|
self.box.refresh_from_db()
|
|
self.assertEqual(self.box.id, old_id)
|
|
|
|
def test_delete_box_deletes_box(self):
|
|
"""Deleting a box should remove it from the database."""
|
|
box_id = self.box.id
|
|
response = self.client.post(f'/box/{box_id}/delete/')
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertRedirects(response, '/box-management/')
|
|
self.assertFalse(Box.objects.filter(id=box_id).exists())
|
|
|
|
def test_delete_box_with_things_redirects(self):
|
|
"""Deleting a box with things should redirect without deleting."""
|
|
thing_type = ThingType.objects.create(name='Test')
|
|
Thing.objects.create(
|
|
name='Test Item',
|
|
thing_type=thing_type,
|
|
box=self.box
|
|
)
|
|
box_id = self.box.id
|
|
response = self.client.post(f'/box/{box_id}/delete/')
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertRedirects(response, '/box-management/')
|
|
self.assertTrue(Box.objects.filter(id=box_id).exists())
|
|
|
|
|
|
class ThingPictureUploadTests(AuthTestCase):
|
|
"""Tests for thing picture deletion."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures."""
|
|
super().setUp()
|
|
self.box_type = BoxType.objects.create(
|
|
name='Standard Box',
|
|
width=200,
|
|
height=100,
|
|
length=300
|
|
)
|
|
self.box = Box.objects.create(
|
|
id='BOX001',
|
|
box_type=self.box_type
|
|
)
|
|
self.thing_type = ThingType.objects.create(name='Electronics')
|
|
self.thing = Thing.objects.create(
|
|
name='Arduino Uno',
|
|
thing_type=self.thing_type,
|
|
box=self.box
|
|
)
|
|
self.image_data = (
|
|
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01'
|
|
b'\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00'
|
|
b'\x00\x00\x0cIDATx\x9cc\xf8\x0f\x00\x00\x01\x01\x00'
|
|
b'\x05\x18\xd8N\x00\x00\x00\x00IEND\xaeB`\x82'
|
|
)
|
|
|
|
def test_thing_detail_shows_add_picture_button(self):
|
|
"""Thing detail page should show 'Add picture' button when thing has no picture."""
|
|
response = self.client.get(f'/thing/{self.thing.id}/')
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, 'Add picture')
|
|
|
|
def test_thing_detail_shows_change_picture_button(self):
|
|
"""Thing detail page should show 'Change picture' button when thing has a picture."""
|
|
image = SimpleUploadedFile(
|
|
name='test.png',
|
|
content=self.image_data,
|
|
content_type='image/png'
|
|
)
|
|
self.thing.picture = image
|
|
self.thing.save()
|
|
response = self.client.get(f'/thing/{self.thing.id}/')
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, 'Change picture')
|
|
# Clean up
|
|
self.thing.picture.delete(save=False)
|
|
|
|
def test_thing_detail_shows_remove_button(self):
|
|
"""Thing detail page should show 'Remove' button when thing has a picture."""
|
|
image = SimpleUploadedFile(
|
|
name='test.png',
|
|
content=self.image_data,
|
|
content_type='image/png'
|
|
)
|
|
self.thing.picture = image
|
|
self.thing.save()
|
|
response = self.client.get(f'/thing/{self.thing.id}/')
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, 'Remove')
|
|
# Clean up
|
|
self.thing.picture.delete(save=False)
|
|
|
|
def test_delete_picture_removes_picture(self):
|
|
"""Deleting a picture should remove it from the thing."""
|
|
image = SimpleUploadedFile(
|
|
name='test.png',
|
|
content=self.image_data,
|
|
content_type='image/png'
|
|
)
|
|
self.thing.picture = image
|
|
self.thing.save()
|
|
|
|
response = self.client.post(f'/thing/{self.thing.id}/', {
|
|
'action': 'delete_picture'
|
|
})
|
|
self.assertRedirects(response, f'/thing/{self.thing.id}/')
|
|
self.thing.refresh_from_db()
|
|
self.assertFalse(self.thing.picture.name)
|
|
|
|
def test_delete_picture_on_thing_without_picture(self):
|
|
"""Deleting a picture from a thing without a picture should succeed."""
|
|
response = self.client.post(f'/thing/{self.thing.id}/', {
|
|
'action': 'delete_picture'
|
|
})
|
|
self.assertRedirects(response, f'/thing/{self.thing.id}/')
|
|
self.thing.refresh_from_db()
|
|
self.assertFalse(self.thing.picture.name)
|
|
|
|
def test_delete_picture_on_thing_without_picture(self):
|
|
"""Deleting a picture from thing without picture should succeed."""
|
|
response = self.client.post(f'/thing/{self.thing.id}/', {
|
|
'action': 'delete_picture'
|
|
})
|
|
self.assertRedirects(response, f'/thing/{self.thing.id}/')
|
|
self.thing.refresh_from_db()
|
|
self.assertFalse(self.thing.picture.name)
|