21b8aebb91
- Create 26 pytest tests covering all core borrowing functionality - Test status determination (planned → active → completed) - Test CRUD operations (create, read, update, delete) - Test conflict detection for overlapping borrowings - Test full lifecycle scenarios (create → activate → complete) - Test edge cases (same start/end time, no end date, etc.) - All tests passing with MongoDB integration - Add TEST_AUSLEIHUNG.md documentation with usage guide Test Coverage: - Status Determination: 5 tests - CRUD Operations: 8 tests - Conflict Detection: 3 tests - Query Operations: 3 tests - Lifecycle Tests: 3 tests - Edge Cases: 3 tests - Period Bookings: 1 test Run with: pytest test_ausleihung.py -v
589 lines
20 KiB
Python
589 lines
20 KiB
Python
"""
|
|
Test Suite for Ausleihung (Borrowing) System
|
|
Tests all core functionality of the borrowing/lending module
|
|
|
|
Run with: pytest test_ausleihung.py -v
|
|
"""
|
|
|
|
import pytest
|
|
import datetime
|
|
from bson.objectid import ObjectId
|
|
import sys
|
|
import os
|
|
|
|
# Add Web directory to path
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'Web'))
|
|
|
|
import ausleihung
|
|
import settings as cfg
|
|
from settings import MongoClient
|
|
|
|
|
|
@pytest.fixture(scope='session')
|
|
def db_client():
|
|
"""Create MongoDB connection for tests"""
|
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
|
yield client
|
|
client.close()
|
|
|
|
|
|
@pytest.fixture(scope='session')
|
|
def test_db(db_client):
|
|
"""Get test database"""
|
|
return db_client[cfg.MONGODB_DB]
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def cleanup_test_data(test_db):
|
|
"""Clean up test data before and after each test"""
|
|
yield
|
|
# Clean up after test
|
|
test_db['ausleihungen'].delete_many({})
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_ausleihung_data():
|
|
"""Fixture with sample borrowing data"""
|
|
now = datetime.datetime.now()
|
|
return {
|
|
'item_id': str(ObjectId()),
|
|
'user': 'test_user',
|
|
'start_date': now,
|
|
'end_date': now + datetime.timedelta(days=1),
|
|
'notes': 'Test borrowing',
|
|
'period': None
|
|
}
|
|
|
|
|
|
# ============================================================================
|
|
# Status Determination Tests
|
|
# ============================================================================
|
|
|
|
class TestGetCurrentStatus:
|
|
"""Test status determination based on dates"""
|
|
|
|
def test_planned_status_future_date(self):
|
|
"""Test that future borrowing is marked as 'planned'"""
|
|
future_time = datetime.datetime.now() + datetime.timedelta(days=1)
|
|
ausleihung_doc = {
|
|
'Status': 'planned',
|
|
'Start': future_time,
|
|
'End': future_time + datetime.timedelta(hours=1)
|
|
}
|
|
status = ausleihung.get_current_status(ausleihung_doc)
|
|
assert status == 'planned'
|
|
|
|
def test_active_status_during_borrowing(self):
|
|
"""Test that current borrowing is marked as 'active'"""
|
|
now = datetime.datetime.now()
|
|
start = now - datetime.timedelta(hours=1)
|
|
end = now + datetime.timedelta(hours=1)
|
|
ausleihung_doc = {
|
|
'Status': 'active',
|
|
'Start': start,
|
|
'End': end
|
|
}
|
|
status = ausleihung.get_current_status(ausleihung_doc)
|
|
assert status == 'active'
|
|
|
|
def test_completed_status_after_end_time(self):
|
|
"""Test that past borrowing is marked as 'completed'"""
|
|
now = datetime.datetime.now()
|
|
start = now - datetime.timedelta(days=2)
|
|
end = now - datetime.timedelta(hours=1)
|
|
ausleihung_doc = {
|
|
'Status': 'active',
|
|
'Start': start,
|
|
'End': end
|
|
}
|
|
status = ausleihung.get_current_status(ausleihung_doc)
|
|
assert status == 'completed'
|
|
|
|
def test_cancelled_status_remains_cancelled(self):
|
|
"""Test that cancelled status is never changed"""
|
|
future_time = datetime.datetime.now() + datetime.timedelta(days=1)
|
|
ausleihung_doc = {
|
|
'Status': 'cancelled',
|
|
'Start': future_time,
|
|
'End': future_time + datetime.timedelta(hours=1)
|
|
}
|
|
status = ausleihung.get_current_status(ausleihung_doc)
|
|
assert status == 'cancelled'
|
|
|
|
def test_active_with_no_end_time(self):
|
|
"""Test that borrowing without end time stays active if started"""
|
|
now = datetime.datetime.now()
|
|
start = now - datetime.timedelta(hours=1)
|
|
ausleihung_doc = {
|
|
'Status': 'active',
|
|
'Start': start,
|
|
'End': None
|
|
}
|
|
status = ausleihung.get_current_status(ausleihung_doc)
|
|
assert status == 'active'
|
|
|
|
|
|
# ============================================================================
|
|
# Create and Update Tests
|
|
# ============================================================================
|
|
|
|
class TestCreateAusleihung:
|
|
"""Test creating new borrowings"""
|
|
|
|
def test_create_active_ausleihung(self, test_db):
|
|
"""Test creating an immediately active borrowing"""
|
|
item_id = str(ObjectId())
|
|
user = 'test_user'
|
|
start = datetime.datetime.now()
|
|
end = start + datetime.timedelta(hours=2)
|
|
|
|
result = ausleihung.add_ausleihung(
|
|
item_id=item_id,
|
|
user=user,
|
|
start_date=start,
|
|
end_date=end,
|
|
notes='Test active',
|
|
status='active'
|
|
)
|
|
|
|
assert result is not None
|
|
|
|
# Verify in database
|
|
ausleihung_col = test_db['ausleihungen']
|
|
stored = ausleihung_col.find_one({'_id': result})
|
|
assert stored is not None
|
|
assert stored['Item'] == item_id # Correct field name
|
|
assert stored['User'] == user
|
|
assert stored['Status'] == 'active'
|
|
|
|
def test_create_planned_ausleihung(self, test_db):
|
|
"""Test creating a future/planned borrowing"""
|
|
item_id = str(ObjectId())
|
|
user = 'test_user'
|
|
future_start = datetime.datetime.now() + datetime.timedelta(days=1)
|
|
future_end = future_start + datetime.timedelta(hours=2)
|
|
|
|
result = ausleihung.add_ausleihung(
|
|
item_id=item_id,
|
|
user=user,
|
|
start_date=future_start,
|
|
end_date=future_end,
|
|
notes='Test planned',
|
|
status='planned'
|
|
)
|
|
|
|
assert result is not None
|
|
|
|
ausleihung_col = test_db['ausleihungen']
|
|
stored = ausleihung_col.find_one({'_id': result})
|
|
assert stored['Status'] == 'planned'
|
|
# Check approximately equal (within 1 second for datetime precision)
|
|
assert abs((stored['Start'] - future_start).total_seconds()) < 1
|
|
|
|
|
|
class TestUpdateAusleihung:
|
|
"""Test updating existing borrowings"""
|
|
|
|
def test_update_ausleihung_dates(self, test_db, sample_ausleihung_data):
|
|
"""Test updating borrowing dates"""
|
|
# Create initial borrowing
|
|
ausleihung_id = ausleihung.add_ausleihung(**sample_ausleihung_data)
|
|
assert ausleihung_id is not None
|
|
|
|
# Update dates
|
|
new_start = datetime.datetime.now() + datetime.timedelta(days=2)
|
|
new_end = new_start + datetime.timedelta(hours=1)
|
|
|
|
ausleihung.update_ausleihung(
|
|
id=ausleihung_id,
|
|
start=new_start,
|
|
end=new_end
|
|
)
|
|
|
|
# Verify update (within 1 second tolerance for datetime precision)
|
|
stored = test_db['ausleihungen'].find_one({'_id': ausleihung_id})
|
|
assert abs((stored['Start'] - new_start).total_seconds()) < 1
|
|
assert abs((stored['End'] - new_end).total_seconds()) < 1
|
|
|
|
def test_update_ausleihung_status(self, test_db, sample_ausleihung_data):
|
|
"""Test updating borrowing status"""
|
|
ausleihung_id = ausleihung.add_ausleihung(**sample_ausleihung_data)
|
|
|
|
ausleihung.update_ausleihung(id=ausleihung_id, status='completed')
|
|
|
|
stored = test_db['ausleihungen'].find_one({'_id': ausleihung_id})
|
|
assert stored['Status'] == 'completed'
|
|
|
|
def test_update_ausleihung_notes(self, test_db, sample_ausleihung_data):
|
|
"""Test updating borrowing notes"""
|
|
ausleihung_id = ausleihung.add_ausleihung(**sample_ausleihung_data)
|
|
new_notes = 'Updated notes'
|
|
|
|
ausleihung.update_ausleihung(id=ausleihung_id, notes=new_notes)
|
|
|
|
stored = test_db['ausleihungen'].find_one({'_id': ausleihung_id})
|
|
assert stored['Notes'] == new_notes
|
|
|
|
|
|
# ============================================================================
|
|
# Complete and Cancel Tests
|
|
# ============================================================================
|
|
|
|
class TestCompleteAusleihung:
|
|
"""Test completing borrowings"""
|
|
|
|
def test_complete_ausleihung(self, test_db, sample_ausleihung_data):
|
|
"""Test marking a borrowing as completed"""
|
|
ausleihung_id = ausleihung.add_ausleihung(**sample_ausleihung_data)
|
|
|
|
end_time = datetime.datetime.now()
|
|
ausleihung.complete_ausleihung(ausleihung_id, end_time=end_time)
|
|
|
|
stored = test_db['ausleihungen'].find_one({'_id': ausleihung_id})
|
|
assert stored['Status'] == 'completed'
|
|
assert stored['End'] == end_time or stored['End'] is not None
|
|
|
|
|
|
class TestCancelAusleihung:
|
|
"""Test canceling borrowings"""
|
|
|
|
def test_cancel_ausleihung(self, test_db, sample_ausleihung_data):
|
|
"""Test canceling a borrowing"""
|
|
ausleihung_id = ausleihung.add_ausleihung(**sample_ausleihung_data)
|
|
|
|
ausleihung.cancel_ausleihung(ausleihung_id)
|
|
|
|
stored = test_db['ausleihungen'].find_one({'_id': ausleihung_id})
|
|
assert stored['Status'] == 'cancelled'
|
|
|
|
|
|
# ============================================================================
|
|
# Query Tests
|
|
# ============================================================================
|
|
|
|
class TestGetAusleihung:
|
|
"""Test retrieving borrowings"""
|
|
|
|
def test_get_ausleihung_by_id(self, test_db, sample_ausleihung_data):
|
|
"""Test fetching a borrowing by ID"""
|
|
ausleihung_id = ausleihung.add_ausleihung(**sample_ausleihung_data)
|
|
|
|
retrieved = ausleihung.get_ausleihung(ausleihung_id)
|
|
assert retrieved is not None
|
|
assert retrieved['_id'] == ausleihung_id
|
|
assert retrieved['User'] == sample_ausleihung_data['user']
|
|
|
|
def test_get_ausleihung_by_user(self, test_db):
|
|
"""Test retrieving all borrowings for a user"""
|
|
user = 'test_user_xyz'
|
|
item1 = str(ObjectId())
|
|
item2 = str(ObjectId())
|
|
now = datetime.datetime.now()
|
|
|
|
# Create multiple borrowings for same user
|
|
ausleihung.add_ausleihung(
|
|
item_id=item1,
|
|
user=user,
|
|
start_date=now,
|
|
end_date=now + datetime.timedelta(hours=1),
|
|
status='active'
|
|
)
|
|
ausleihung.add_ausleihung(
|
|
item_id=item2,
|
|
user=user,
|
|
start_date=now + datetime.timedelta(days=1),
|
|
end_date=now + datetime.timedelta(days=1, hours=1),
|
|
status='planned'
|
|
)
|
|
|
|
# Retrieve all for user
|
|
borrowings = ausleihung.get_ausleihung_by_user(user)
|
|
assert len(borrowings) >= 2
|
|
assert all(b['User'] == user for b in borrowings)
|
|
|
|
def test_get_ausleihungen_by_status(self, test_db):
|
|
"""Test retrieving borrowings by status"""
|
|
item_id = str(ObjectId())
|
|
user = 'test_user'
|
|
now = datetime.datetime.now()
|
|
|
|
# Create active
|
|
active_id = ausleihung.add_ausleihung(
|
|
item_id=item_id,
|
|
user=user,
|
|
start_date=now - datetime.timedelta(hours=1),
|
|
end_date=now + datetime.timedelta(hours=1),
|
|
status='active'
|
|
)
|
|
|
|
# Get active borrowings
|
|
active_borrowings = ausleihung.get_active_ausleihungen()
|
|
assert any(b['_id'] == active_id for b in active_borrowings)
|
|
|
|
|
|
# ============================================================================
|
|
# Conflict Detection Tests
|
|
# ============================================================================
|
|
|
|
class TestConflictDetection:
|
|
"""Test detecting overlapping/conflicting borrowings"""
|
|
|
|
def test_no_conflict_different_items(self, test_db):
|
|
"""Test that different items don't conflict"""
|
|
item1 = str(ObjectId())
|
|
item2 = str(ObjectId())
|
|
now = datetime.datetime.now()
|
|
start = now
|
|
end = now + datetime.timedelta(hours=1)
|
|
|
|
# Create first borrowing
|
|
ausleihung.add_ausleihung(
|
|
item_id=item1,
|
|
user='user1',
|
|
start_date=start,
|
|
end_date=end,
|
|
status='active'
|
|
)
|
|
|
|
# Check conflict on different item (should be no conflict)
|
|
conflict = ausleihung.check_ausleihung_conflict(
|
|
item_id=item2,
|
|
start_date=start,
|
|
end_date=end
|
|
)
|
|
assert conflict is False
|
|
|
|
def test_conflict_same_item_overlapping(self, test_db):
|
|
"""Test that overlapping borrowings on same item are detected"""
|
|
item_id = str(ObjectId())
|
|
now = datetime.datetime.now()
|
|
|
|
# Create first borrowing
|
|
ausleihung.add_ausleihung(
|
|
item_id=item_id,
|
|
user='user1',
|
|
start_date=now,
|
|
end_date=now + datetime.timedelta(hours=2),
|
|
status='active'
|
|
)
|
|
|
|
# Try to create overlapping borrowing
|
|
conflict = ausleihung.check_ausleihung_conflict(
|
|
item_id=item_id,
|
|
start_date=now + datetime.timedelta(minutes=30),
|
|
end_date=now + datetime.timedelta(hours=3)
|
|
)
|
|
assert conflict is True or conflict == item_id # Depending on implementation
|
|
|
|
def test_no_conflict_different_times(self, test_db):
|
|
"""Test that non-overlapping borrowings don't conflict"""
|
|
item_id = str(ObjectId())
|
|
now = datetime.datetime.now()
|
|
|
|
# Create first borrowing
|
|
ausleihung.add_ausleihung(
|
|
item_id=item_id,
|
|
user='user1',
|
|
start_date=now,
|
|
end_date=now + datetime.timedelta(hours=1),
|
|
status='active'
|
|
)
|
|
|
|
# Check borrowing after first ends (should be no conflict)
|
|
conflict = ausleihung.check_ausleihung_conflict(
|
|
item_id=item_id,
|
|
start_date=now + datetime.timedelta(hours=2),
|
|
end_date=now + datetime.timedelta(hours=3)
|
|
)
|
|
assert conflict is False or conflict is None
|
|
|
|
|
|
# ============================================================================
|
|
# Period-based Borrowing Tests
|
|
# ============================================================================
|
|
|
|
class TestPeriodBookings:
|
|
"""Test period-based borrowings (school periods)"""
|
|
|
|
def test_create_period_booking(self, test_db):
|
|
"""Test creating a borrowing for a specific school period"""
|
|
item_id = str(ObjectId())
|
|
user = 'test_user'
|
|
today = datetime.datetime.now().date()
|
|
|
|
result = ausleihung.add_ausleihung(
|
|
item_id=item_id,
|
|
user=user,
|
|
start_date=datetime.datetime.combine(today, datetime.time(8, 0)),
|
|
end_date=datetime.datetime.combine(today, datetime.time(9, 0)),
|
|
period=1, # Assuming period 1 is first period
|
|
status='active'
|
|
)
|
|
|
|
assert result is not None
|
|
stored = test_db['ausleihungen'].find_one({'_id': result})
|
|
assert stored.get('Period') == 1
|
|
|
|
|
|
# ============================================================================
|
|
# Remove Tests
|
|
# ============================================================================
|
|
|
|
class TestRemoveAusleihung:
|
|
"""Test removing/deleting borrowings"""
|
|
|
|
def test_remove_ausleihung(self, test_db, sample_ausleihung_data):
|
|
"""Test deleting a borrowing record (soft delete)"""
|
|
ausleihung_id = ausleihung.add_ausleihung(**sample_ausleihung_data)
|
|
stored_before = test_db['ausleihungen'].find_one({'_id': ausleihung_id})
|
|
assert stored_before is not None
|
|
|
|
# Remove (soft delete - adds DeletedAt timestamp)
|
|
ausleihung.remove_ausleihung(ausleihung_id)
|
|
|
|
# Verify it's marked as deleted (soft delete)
|
|
stored_after = test_db['ausleihungen'].find_one({'_id': ausleihung_id})
|
|
assert stored_after is not None # Still exists
|
|
assert 'DeletedAt' in stored_after or stored_after.get('Status') == 'deleted'
|
|
|
|
|
|
# ============================================================================
|
|
# Integration Tests
|
|
# ============================================================================
|
|
|
|
class TestAusleihungLifecycle:
|
|
"""Test complete borrowing lifecycle"""
|
|
|
|
def test_full_lifecycle_active_to_complete(self, test_db):
|
|
"""Test a complete borrowing lifecycle: active → complete"""
|
|
item_id = str(ObjectId())
|
|
user = 'test_user'
|
|
now = datetime.datetime.now()
|
|
|
|
# 1. Create active borrowing
|
|
ausleihung_id = ausleihung.add_ausleihung(
|
|
item_id=item_id,
|
|
user=user,
|
|
start_date=now - datetime.timedelta(hours=1),
|
|
end_date=now + datetime.timedelta(hours=1),
|
|
status='active'
|
|
)
|
|
|
|
stored = test_db['ausleihungen'].find_one({'_id': ausleihung_id})
|
|
status = ausleihung.get_current_status(stored)
|
|
assert status == 'active'
|
|
|
|
# 2. Complete the borrowing
|
|
ausleihung.complete_ausleihung(ausleihung_id)
|
|
|
|
stored = test_db['ausleihungen'].find_one({'_id': ausleihung_id})
|
|
status = ausleihung.get_current_status(stored)
|
|
assert status == 'completed'
|
|
|
|
def test_full_lifecycle_planned_to_active_to_complete(self, test_db):
|
|
"""Test complete lifecycle: planned → active → complete"""
|
|
item_id = str(ObjectId())
|
|
user = 'test_user'
|
|
now = datetime.datetime.now()
|
|
future = now + datetime.timedelta(hours=1)
|
|
|
|
# 1. Create planned borrowing
|
|
ausleihung_id = ausleihung.add_ausleihung(
|
|
item_id=item_id,
|
|
user=user,
|
|
start_date=future,
|
|
end_date=future + datetime.timedelta(hours=1),
|
|
status='planned'
|
|
)
|
|
|
|
stored = test_db['ausleihungen'].find_one({'_id': ausleihung_id})
|
|
assert ausleihung.get_current_status(stored) == 'planned'
|
|
|
|
# 2. Update to active (simulate time passing or manual activation)
|
|
ausleihung.update_ausleihung(id=ausleihung_id, status='active')
|
|
stored = test_db['ausleihungen'].find_one({'_id': ausleihung_id})
|
|
assert stored['Status'] == 'active'
|
|
|
|
# 3. Complete
|
|
ausleihung.complete_ausleihung(ausleihung_id)
|
|
stored = test_db['ausleihungen'].find_one({'_id': ausleihung_id})
|
|
assert ausleihung.get_current_status(stored) == 'completed'
|
|
|
|
def test_cancel_planned_borrowing(self, test_db):
|
|
"""Test canceling a planned borrowing"""
|
|
item_id = str(ObjectId())
|
|
user = 'test_user'
|
|
future = datetime.datetime.now() + datetime.timedelta(days=1)
|
|
|
|
# Create planned
|
|
ausleihung_id = ausleihung.add_ausleihung(
|
|
item_id=item_id,
|
|
user=user,
|
|
start_date=future,
|
|
end_date=future + datetime.timedelta(hours=1),
|
|
status='planned'
|
|
)
|
|
|
|
# Cancel
|
|
ausleihung.cancel_ausleihung(ausleihung_id)
|
|
|
|
stored = test_db['ausleihungen'].find_one({'_id': ausleihung_id})
|
|
assert stored['Status'] == 'cancelled'
|
|
|
|
|
|
# ============================================================================
|
|
# Edge Cases
|
|
# ============================================================================
|
|
|
|
class TestEdgeCases:
|
|
"""Test edge cases and boundary conditions"""
|
|
|
|
def test_borrowing_with_same_start_and_end(self, test_db):
|
|
"""Test borrowing where start equals end"""
|
|
item_id = str(ObjectId())
|
|
user = 'test_user'
|
|
now = datetime.datetime.now()
|
|
|
|
result = ausleihung.add_ausleihung(
|
|
item_id=item_id,
|
|
user=user,
|
|
start_date=now,
|
|
end_date=now, # Same time
|
|
status='active'
|
|
)
|
|
|
|
assert result is not None
|
|
|
|
def test_borrowing_without_end_date(self, test_db):
|
|
"""Test creating borrowing without end date"""
|
|
item_id = str(ObjectId())
|
|
user = 'test_user'
|
|
now = datetime.datetime.now()
|
|
|
|
result = ausleihung.add_ausleihung(
|
|
item_id=item_id,
|
|
user=user,
|
|
start_date=now,
|
|
end_date=None,
|
|
status='active'
|
|
)
|
|
|
|
assert result is not None
|
|
stored = test_db['ausleihungen'].find_one({'_id': result})
|
|
# End field should not exist or be None if not provided
|
|
assert 'End' not in stored or stored.get('End') is None
|
|
|
|
def test_get_nonexistent_borrowing(self, test_db):
|
|
"""Test retrieving a nonexistent borrowing"""
|
|
fake_id = ObjectId()
|
|
result = ausleihung.get_ausleihung(fake_id)
|
|
assert result is None or result == {} or result == []
|
|
|
|
|
|
# ============================================================================
|
|
# Run Tests
|
|
# ============================================================================
|
|
|
|
if __name__ == '__main__':
|
|
pytest.main([__file__, '-v', '--tb=short'])
|