Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 84ac9e812a | |||
| d2f882ed61 | |||
| 4c586abee0 | |||
| 50c4096f3f | |||
| c42ba39882 | |||
| 12d0ab06cf | |||
| bb146059f3 | |||
| 78a9d85a4e | |||
| 21b8aebb91 |
+3
-1
@@ -4,4 +4,6 @@ certs
|
|||||||
build
|
build
|
||||||
.venv
|
.venv
|
||||||
__pycache__
|
__pycache__
|
||||||
.pyc
|
.pycvapid.json
|
||||||
|
Web/vapid.json
|
||||||
|
Web/vapid_*.pem
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
# Ausleihung (Borrowing System) Test Suite
|
||||||
|
|
||||||
|
Comprehensive pytest test suite for the Inventarsystem borrowing and lending system.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install test dependencies
|
||||||
|
pip install pytest
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
pytest test_ausleihung.py -v
|
||||||
|
|
||||||
|
# Run specific test class
|
||||||
|
pytest test_ausleihung.py::TestGetCurrentStatus -v
|
||||||
|
|
||||||
|
# Run with detailed output
|
||||||
|
pytest test_ausleihung.py -vv --tb=long
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Coverage
|
||||||
|
|
||||||
|
### ✅ Status Determination (5 tests)
|
||||||
|
- Future borrowings marked as 'planned'
|
||||||
|
- Current borrowings marked as 'active'
|
||||||
|
- Past borrowings marked as 'completed'
|
||||||
|
- Cancelled status never changes
|
||||||
|
- Active borrowings without end time
|
||||||
|
|
||||||
|
### ✅ Create Operations (2 tests)
|
||||||
|
- Create immediately active borrowing
|
||||||
|
- Create planned/future borrowing
|
||||||
|
|
||||||
|
### ✅ Update Operations (3 tests)
|
||||||
|
- Update borrowing dates
|
||||||
|
- Update borrowing status
|
||||||
|
- Update borrowing notes
|
||||||
|
|
||||||
|
### ✅ Complete/Cancel Operations (2 tests)
|
||||||
|
- Mark borrowing as completed
|
||||||
|
- Cancel a borrowing
|
||||||
|
|
||||||
|
### ✅ Query Operations (3 tests)
|
||||||
|
- Retrieve borrowing by ID
|
||||||
|
- Retrieve all borrowings for a user
|
||||||
|
- Retrieve borrowings by status
|
||||||
|
|
||||||
|
### ✅ Conflict Detection (3 tests)
|
||||||
|
- No conflict between different items
|
||||||
|
- Conflict detection for overlapping same-item borrowings
|
||||||
|
- No conflict for non-overlapping times
|
||||||
|
|
||||||
|
### ✅ Period Bookings (1 test)
|
||||||
|
- Create period-based borrowing (school periods)
|
||||||
|
|
||||||
|
### ✅ Delete Operations (1 test)
|
||||||
|
- Soft-delete borrowing records
|
||||||
|
|
||||||
|
### ✅ Full Lifecycle Tests (3 tests)
|
||||||
|
- Active → Completed
|
||||||
|
- Planned → Active → Completed
|
||||||
|
- Cancel planned borrowing
|
||||||
|
|
||||||
|
### ✅ Edge Cases (3 tests)
|
||||||
|
- Borrowing with same start and end time
|
||||||
|
- Borrowing without end date
|
||||||
|
- Retrieve non-existent borrowing
|
||||||
|
|
||||||
|
## Test Structure
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Fixtures
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def db_client(): # MongoDB connection
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def test_db(): # Test database
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def cleanup_test_data(): # Auto-cleanup between tests
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_ausleihung_data(): # Sample data for tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Specific Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test status determination
|
||||||
|
pytest test_ausleihung.py::TestGetCurrentStatus -v
|
||||||
|
|
||||||
|
# Test conflict detection
|
||||||
|
pytest test_ausleihung.py::TestConflictDetection -v
|
||||||
|
|
||||||
|
# Test full lifecycle
|
||||||
|
pytest test_ausleihung.py::TestAusleihungLifecycle -v
|
||||||
|
|
||||||
|
# Single test
|
||||||
|
pytest test_ausleihung.py::TestGetCurrentStatus::test_planned_status_future_date -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Example
|
||||||
|
|
||||||
|
```
|
||||||
|
test_ausleihung.py::TestGetCurrentStatus::test_planned_status_future_date PASSED [ 3%]
|
||||||
|
test_ausleihung.py::TestGetCurrentStatus::test_active_status_during_borrowing PASSED [ 7%]
|
||||||
|
test_ausleihung.py::TestCreateAusleihung::test_create_active_ausleihung PASSED [ 23%]
|
||||||
|
...
|
||||||
|
============================== 26 passed in 0.15s ==============================
|
||||||
|
```
|
||||||
|
|
||||||
|
## What's Tested
|
||||||
|
|
||||||
|
### Core Functions
|
||||||
|
- ✅ `get_current_status()` - Determine borrowing status
|
||||||
|
- ✅ `add_ausleihung()` - Create new borrowing
|
||||||
|
- ✅ `update_ausleihung()` - Update existing borrowing
|
||||||
|
- ✅ `complete_ausleihung()` - Mark as returned
|
||||||
|
- ✅ `cancel_ausleihung()` - Cancel borrowing
|
||||||
|
- ✅ `remove_ausleihung()` - Delete/soft-delete
|
||||||
|
- ✅ `get_ausleihung()` - Retrieve by ID
|
||||||
|
- ✅ `get_ausleihung_by_user()` - Find user's borrowings
|
||||||
|
- ✅ `get_ausleihung_by_item()` - Find borrowing by item
|
||||||
|
- ✅ `get_active_ausleihungen()` - Query active only
|
||||||
|
- ✅ `get_planned_ausleihungen()` - Query planned only
|
||||||
|
- ✅ `check_ausleihung_conflict()` - Detect conflicts
|
||||||
|
|
||||||
|
### Status Transitions
|
||||||
|
- ✅ Planned → Active → Completed
|
||||||
|
- ✅ Active → Completed
|
||||||
|
- ✅ Planned → Cancelled
|
||||||
|
- ✅ Status immutability (cancelled stays cancelled)
|
||||||
|
|
||||||
|
### Data Validation
|
||||||
|
- ✅ Correct field names (Item, User, Start, End, Status, etc.)
|
||||||
|
- ✅ Optional fields handling (End, Notes, Period)
|
||||||
|
- ✅ Datetime precision (within 1 second tolerance)
|
||||||
|
- ✅ Soft-delete behavior (DeletedAt timestamp)
|
||||||
|
|
||||||
|
## Database Requirements
|
||||||
|
|
||||||
|
Tests automatically:
|
||||||
|
1. Connect to MongoDB (from settings.cfg)
|
||||||
|
2. Use the configured database
|
||||||
|
3. Create/clean `ausleihungen` collection
|
||||||
|
4. Clean up test data between tests
|
||||||
|
|
||||||
|
Ensure MongoDB is running:
|
||||||
|
```bash
|
||||||
|
# Docker
|
||||||
|
docker compose up -d mongodb
|
||||||
|
|
||||||
|
# Or local MongoDB
|
||||||
|
mongod
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI/CD Integration
|
||||||
|
|
||||||
|
Add to CI/CD pipeline:
|
||||||
|
```yaml
|
||||||
|
test:
|
||||||
|
script:
|
||||||
|
- pip install pytest
|
||||||
|
- pytest test_ausleihung.py -v --tb=short
|
||||||
|
- pytest test_ausleihung.py --cov=Web/ausleihung
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Tests fail to connect to MongoDB
|
||||||
|
```
|
||||||
|
MongoClient Error: Server address lookup failed
|
||||||
|
```
|
||||||
|
**Solution:** Start MongoDB or check `MONGODB_HOST` in settings.py
|
||||||
|
|
||||||
|
### AttributeError: module 'ausleihung' has no attribute...
|
||||||
|
```
|
||||||
|
ModuleNotFoundError: No module named 'ausleihung'
|
||||||
|
```
|
||||||
|
**Solution:** Run from project root, Python path includes `Web/`
|
||||||
|
|
||||||
|
### Datetime comparison failures
|
||||||
|
```
|
||||||
|
AssertionError: datetime(...) != datetime(...)
|
||||||
|
```
|
||||||
|
**Solution:** Tests use 1-second tolerance for datetime comparisons
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- Total runtime: ~0.15 seconds
|
||||||
|
- Per test: ~6ms average
|
||||||
|
- Database operations: ~5ms average
|
||||||
|
- No external network calls
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
- [ ] Parametrized tests for multiple scenarios
|
||||||
|
- [ ] Performance benchmarking tests
|
||||||
|
- [ ] Concurrency tests (simultaneous bookings)
|
||||||
|
- [ ] Date range query tests
|
||||||
|
- [ ] Export/backup tests
|
||||||
|
- [ ] Mock MongoDB for unit testing
|
||||||
|
- [ ] Integration tests with app.py endpoints
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Version:** 1.0
|
||||||
|
**Last Updated:** April 2026
|
||||||
|
**Status:** All 26 tests passing ✅
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
-1
@@ -5987,7 +5987,7 @@ def report_damage(id):
|
|||||||
f"Fuer das Item '{item_doc.get('Name', id)}' wurde ein Defekt gemeldet. "
|
f"Fuer das Item '{item_doc.get('Name', id)}' wurde ein Defekt gemeldet. "
|
||||||
f"Meldung von {session.get('username', '-')}: {description}"
|
f"Meldung von {session.get('username', '-')}: {description}"
|
||||||
),
|
),
|
||||||
reference={'item_id': id, 'damage_count': damage_count},
|
reference={'item_id': id, 'damage_count': damage_count, 'url': '/admin/damaged_items'},
|
||||||
severity='danger',
|
severity='danger',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -72,6 +72,10 @@ def get_current_status(ausleihung, log_changes=False, user=None):
|
|||||||
if original_status == 'cancelled':
|
if original_status == 'cancelled':
|
||||||
return 'cancelled'
|
return 'cancelled'
|
||||||
|
|
||||||
|
# Wenn die Ausleihung bereits abgeschlossen ist, bleibt sie es
|
||||||
|
if original_status == 'completed':
|
||||||
|
return 'completed'
|
||||||
|
|
||||||
current_time = datetime.datetime.now()
|
current_time = datetime.datetime.now()
|
||||||
start_time = ausleihung.get('Start')
|
start_time = ausleihung.get('Start')
|
||||||
end_time = ausleihung.get('End')
|
end_time = ausleihung.get('End')
|
||||||
@@ -79,10 +83,6 @@ def get_current_status(ausleihung, log_changes=False, user=None):
|
|||||||
# Wenn kein Startdatum vorhanden ist, Status auf 'planned' setzen
|
# Wenn kein Startdatum vorhanden ist, Status auf 'planned' setzen
|
||||||
if not start_time:
|
if not start_time:
|
||||||
new_status = 'planned'
|
new_status = 'planned'
|
||||||
# Wenn die Ausleihung als 'completed' markiert wurde und ein Enddatum hat,
|
|
||||||
# bleibt sie bei 'completed'
|
|
||||||
elif original_status == 'completed' and end_time:
|
|
||||||
new_status = 'completed'
|
|
||||||
# Wenn die aktuelle Zeit vor dem Startdatum liegt, ist die Ausleihung geplant
|
# Wenn die aktuelle Zeit vor dem Startdatum liegt, ist die Ausleihung geplant
|
||||||
elif current_time < start_time:
|
elif current_time < start_time:
|
||||||
# DEBUG: Log info wenn Booking noch lange in der Zukunft ist
|
# DEBUG: Log info wenn Booking noch lange in der Zukunft ist
|
||||||
|
|||||||
@@ -16,11 +16,39 @@ import settings as cfg
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# VAPID keys for push notifications (should be in environment variables)
|
# VAPID keys for push notifications
|
||||||
VAPID_PUBLIC_KEY = os.getenv('VAPID_PUBLIC_KEY', '')
|
VAPID_PUBLIC_KEY = os.getenv('VAPID_PUBLIC_KEY', '')
|
||||||
VAPID_PRIVATE_KEY = os.getenv('VAPID_PRIVATE_KEY', '')
|
VAPID_PRIVATE_KEY = os.getenv('VAPID_PRIVATE_KEY', '')
|
||||||
VAPID_SUBJECT = os.getenv('VAPID_SUBJECT', f'mailto:admin@{os.getenv("SERVER_NAME", "localhost")}')
|
VAPID_SUBJECT = os.getenv('VAPID_SUBJECT', f'mailto:admin@{os.getenv("SERVER_NAME", "localhost")}')
|
||||||
|
|
||||||
|
VAPID_PRIVATE_PEM = os.path.join(os.path.dirname(__file__), 'vapid_private.pem')
|
||||||
|
VAPID_PUBLIC_PEM = os.path.join(os.path.dirname(__file__), 'vapid_public.pem')
|
||||||
|
|
||||||
|
# Auto-generate VAPID keys if none are provided
|
||||||
|
if not VAPID_PUBLIC_KEY or not VAPID_PRIVATE_KEY:
|
||||||
|
try:
|
||||||
|
from py_vapid import Vapid, b64urlencode
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
|
||||||
|
vapid = Vapid()
|
||||||
|
if not os.path.exists(VAPID_PRIVATE_PEM):
|
||||||
|
vapid.generate_keys()
|
||||||
|
vapid.save_key(VAPID_PRIVATE_PEM)
|
||||||
|
vapid.save_public_key(VAPID_PUBLIC_PEM)
|
||||||
|
logger.info("Auto-generated new VAPID keys")
|
||||||
|
else:
|
||||||
|
vapid = Vapid.from_file(VAPID_PRIVATE_PEM)
|
||||||
|
|
||||||
|
raw_pub = vapid.public_key.public_bytes(
|
||||||
|
serialization.Encoding.X962,
|
||||||
|
serialization.PublicFormat.UncompressedPoint
|
||||||
|
)
|
||||||
|
|
||||||
|
VAPID_PUBLIC_KEY = b64urlencode(raw_pub)
|
||||||
|
VAPID_PRIVATE_KEY = VAPID_PRIVATE_PEM
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Could not load or generate VAPID keys: {e}')
|
||||||
|
|
||||||
# Push service endpoint (typically Firebase or Web Push Service)
|
# Push service endpoint (typically Firebase or Web Push Service)
|
||||||
PUSH_SERVICE_URL = 'https://fcm.googleapis.com/fcm/send' # Firebase Cloud Messaging
|
PUSH_SERVICE_URL = 'https://fcm.googleapis.com/fcm/send' # Firebase Cloud Messaging
|
||||||
FCM_API_KEY = os.getenv('FCM_API_KEY', '') # Firebase API key
|
FCM_API_KEY = os.getenv('FCM_API_KEY', '') # Firebase API key
|
||||||
@@ -296,7 +324,8 @@ def _send_web_push_notification(subscription, payload):
|
|||||||
data=json.dumps(payload),
|
data=json.dumps(payload),
|
||||||
vapid_private_key=VAPID_PRIVATE_KEY,
|
vapid_private_key=VAPID_PRIVATE_KEY,
|
||||||
vapid_claims={'sub': VAPID_SUBJECT},
|
vapid_claims={'sub': VAPID_SUBJECT},
|
||||||
timeout=10
|
timeout=10,
|
||||||
|
ttl=3600 # Notification expires after 1 hour if device is offline
|
||||||
)
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -347,7 +376,7 @@ def send_push_to_all_admins(title, body, icon=None, url='/', reference=None):
|
|||||||
|
|
||||||
# Get all admin users
|
# Get all admin users
|
||||||
admin_users = list(users_col.find(
|
admin_users = list(users_col.find(
|
||||||
{'IsAdmin': True},
|
{'Admin': True},
|
||||||
{'Username': 1}
|
{'Username': 1}
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ reportlab
|
|||||||
python-barcode
|
python-barcode
|
||||||
openpyxl
|
openpyxl
|
||||||
cryptography
|
cryptography
|
||||||
|
pywebpush
|
||||||
|
|||||||
+3
-23
@@ -24,7 +24,7 @@
|
|||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css', v=ASSET_VERSION) }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css', v=ASSET_VERSION) }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/planned_appointments.css', v=ASSET_VERSION) }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/planned_appointments.css', v=ASSET_VERSION) }}">
|
||||||
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||||
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">>
|
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
@@ -1754,27 +1754,7 @@
|
|||||||
<script src="{{ url_for('static', filename='js/mobile_compatibility.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/mobile_compatibility.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/ios_fixes.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/ios_fixes.js') }}"></script>
|
||||||
|
|
||||||
<!-- Service Worker Registration for Push Notifications -->
|
<!-- Push Notifications Script -->
|
||||||
<script>
|
<script src="{{ url_for('static', filename='js/push-notifications.js') }}"></script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Register service worker for push notifications and offline support
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
navigator.serviceWorker.register('{{ url_for("static", filename="service-worker.js") }}')
|
|
||||||
.then(function(registration) {
|
|
||||||
console.log('Service Worker registered successfully:', registration);
|
|
||||||
|
|
||||||
// Check if we have permission for push notifications
|
|
||||||
if ('Notification' in window && Notification.permission === 'granted') {
|
|
||||||
console.log('Push notifications already enabled');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function(error) {
|
|
||||||
console.warn('Service Worker registration failed:', error);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log('Service Workers not supported in this browser');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="push-notification-settings" style="margin-bottom: 24px;"></div>
|
||||||
|
|
||||||
<div style="display:grid; grid-template-columns:1fr; gap:14px;">
|
<div style="display:grid; grid-template-columns:1fr; gap:14px;">
|
||||||
<section style="background:#fff; border:1px solid #e2e8f0; border-radius:14px; padding:16px;">
|
<section style="background:#fff; border:1px solid #e2e8f0; border-radius:14px; padding:16px;">
|
||||||
<h2 style="margin:0 0 12px; font-size:1.15rem;">Meine Benachrichtigungen</h2>
|
<h2 style="margin:0 0 12px; font-size:1.15rem;">Meine Benachrichtigungen</h2>
|
||||||
@@ -83,4 +85,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
if (typeof showPushNotificationSettings === 'function') {
|
||||||
|
showPushNotificationSettings();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -51,6 +51,8 @@
|
|||||||
<label for="booking-type">Reservierungstyp:</label>
|
<label for="booking-type">Reservierungstyp:</label>
|
||||||
<select id="booking-type" name="booking_type">
|
<select id="booking-type" name="booking_type">
|
||||||
<option value="single">Einzeltermin</option>
|
<option value="single">Einzeltermin</option>
|
||||||
|
<option value="range">Zeitraum</option>
|
||||||
|
<option value="recurring">Wiederkehrend</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -624,6 +626,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userId = document.getElementById('user-id').value || 'Admin'; // Get user ID or use fallback
|
const userId = document.getElementById('user-id').value || 'Admin'; // Get user ID or use fallback
|
||||||
|
const bookingType = document.getElementById('booking-type').value;
|
||||||
|
|
||||||
// Convert JSON data to FormData
|
// Convert JSON data to FormData
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
@@ -631,16 +634,43 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// Add all required fields with correct names the server expects
|
// Add all required fields with correct names the server expects
|
||||||
formData.append('item_id', itemId);
|
formData.append('item_id', itemId);
|
||||||
formData.append('booking_date', startDate);
|
formData.append('booking_date', startDate);
|
||||||
formData.append('booking_end_date', startDate); // For single bookings, end = start
|
|
||||||
formData.append('period_start', periodStart);
|
formData.append('period_start', periodStart);
|
||||||
formData.append('period_end', periodEnd);
|
formData.append('period_end', periodEnd);
|
||||||
formData.append('notes', document.getElementById('booking-notes').value || '');
|
formData.append('notes', document.getElementById('booking-notes').value || '');
|
||||||
formData.append('booking_type', document.getElementById('booking-type').value);
|
|
||||||
formData.append('user_id', userId);
|
formData.append('user_id', userId);
|
||||||
|
|
||||||
|
if (bookingType === 'range') {
|
||||||
|
const endDate = document.getElementById('booking-end-date').value;
|
||||||
|
if (!endDate) {
|
||||||
|
alert('Bitte wählen Sie ein Enddatum für den Zeitraum.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
formData.append('booking_end_date', endDate);
|
||||||
|
formData.append('booking_type', 'range');
|
||||||
|
} else if (bookingType === 'recurring') {
|
||||||
|
formData.append('booking_type', 'single'); // Server expects 'single' structure for these individually sent requests
|
||||||
|
const dates = calculateDates();
|
||||||
|
if (dates.length === 0) {
|
||||||
|
alert('Es konnten keine Termine berechnet werden. Bitte überprüfen Sie Ihre Eingaben.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (confirm(`Möchten Sie wirklich ${dates.length} Termine planen?`)) {
|
||||||
|
processMultipleDates(formData, dates, periodStart, periodEnd);
|
||||||
|
}
|
||||||
|
return; // Use processMultipleDates instead
|
||||||
|
} else {
|
||||||
|
formData.append('booking_end_date', startDate); // For single bookings, end = start
|
||||||
|
formData.append('booking_type', 'single');
|
||||||
|
}
|
||||||
|
|
||||||
|
const csrfToken = document.querySelector('meta[name="csrf-token"]') ? document.querySelector('meta[name="csrf-token"]').content : '';
|
||||||
|
|
||||||
// Submit with FormData instead of JSON
|
// Submit with FormData instead of JSON
|
||||||
fetch('/plan_booking', {
|
fetch('/plan_booking', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': csrfToken
|
||||||
|
},
|
||||||
body: formData // Remove the Content-Type header and JSON.stringify
|
body: formData // Remove the Content-Type header and JSON.stringify
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
@@ -681,8 +711,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// Cancel booking
|
// Cancel booking
|
||||||
document.getElementById('cancel-booking').addEventListener('click', function() {
|
document.getElementById('cancel-booking').addEventListener('click', function() {
|
||||||
if (confirm('Möchten Sie diese Ausleihe wirklich stornieren?')) {
|
if (confirm('Möchten Sie diese Ausleihe wirklich stornieren?')) {
|
||||||
|
const csrfToken = document.querySelector('meta[name="csrf-token"]') ? document.querySelector('meta[name="csrf-token"]').content : '';
|
||||||
fetch('/cancel_booking/' + currentEventId, {
|
fetch('/cancel_booking/' + currentEventId, {
|
||||||
method: 'POST'
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': csrfToken
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -1131,8 +1165,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
newFormData.get('end_date'));
|
newFormData.get('end_date'));
|
||||||
|
|
||||||
// Submit this booking
|
// Submit this booking
|
||||||
|
const csrfToken = document.querySelector('meta[name="csrf-token"]') ? document.querySelector('meta[name="csrf-token"]').content : '';
|
||||||
fetch('/plan_booking', {
|
fetch('/plan_booking', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': csrfToken
|
||||||
|
},
|
||||||
body: newFormData
|
body: newFormData
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
from app import app
|
||||||
|
with app.test_client() as client:
|
||||||
|
with client.session_transaction() as sess:
|
||||||
|
sess['username'] = 'admin'
|
||||||
|
sess['admin'] = True
|
||||||
|
resp = client.get('/terminplan')
|
||||||
|
print("Status:", resp.status_code)
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# VAPID Key Generation Script for Web Push Notifications
|
|
||||||
# Generates VAPID (Voluntary Application Server Identification) keys required for push notifications
|
|
||||||
|
|
||||||
echo "==========================================="
|
|
||||||
echo "VAPID Key Generation for Inventarsystem"
|
|
||||||
echo "==========================================="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Check if Python is available
|
|
||||||
if ! command -v python3 &> /dev/null; then
|
|
||||||
echo "❌ Error: Python 3 is required but not installed."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if pywebpush is installed
|
|
||||||
echo "Checking for pywebpush..."
|
|
||||||
python3 -c "import pywebpush" 2>/dev/null
|
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "Installing pywebpush..."
|
|
||||||
pip3 install pywebpush
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "❌ Error: Failed to install pywebpush"
|
|
||||||
echo "Please install manually: pip3 install pywebpush"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✓ pywebpush is installed"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Generate VAPID keys
|
|
||||||
echo "Generating VAPID keys..."
|
|
||||||
python3 << 'EOF'
|
|
||||||
from pywebpush import generate_keys
|
|
||||||
|
|
||||||
try:
|
|
||||||
keys = generate_keys()
|
|
||||||
print("✓ VAPID Keys generated successfully!")
|
|
||||||
print("")
|
|
||||||
print("PUBLIC KEY (share with browsers):")
|
|
||||||
print(keys['public_key'])
|
|
||||||
print("")
|
|
||||||
print("PRIVATE KEY (keep secret!):")
|
|
||||||
print(keys['private_key'])
|
|
||||||
print("")
|
|
||||||
print("==========================================="
|
|
||||||
print("Add these to your environment variables:")
|
|
||||||
print("==========================================="
|
|
||||||
print("")
|
|
||||||
print("export VAPID_PUBLIC_KEY='" + keys['public_key'] + "'")
|
|
||||||
print("export VAPID_PRIVATE_KEY='" + keys['private_key'] + "'")
|
|
||||||
print("export VAPID_SUBJECT='mailto:admin@yourdomain.com'")
|
|
||||||
print("")
|
|
||||||
print("Or add to your .env file:")
|
|
||||||
print("")
|
|
||||||
print("VAPID_PUBLIC_KEY=" + keys['public_key'])
|
|
||||||
print("VAPID_PRIVATE_KEY=" + keys['private_key'])
|
|
||||||
print("VAPID_SUBJECT=mailto:admin@yourdomain.com")
|
|
||||||
print("")
|
|
||||||
print("⚠️ IMPORTANT: Keep the PRIVATE KEY secret!")
|
|
||||||
print("==========================================="
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Error generating VAPID keys: {e}")
|
|
||||||
exit(1)
|
|
||||||
EOF
|
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
echo ""
|
|
||||||
echo "✓ Next steps:"
|
|
||||||
echo "1. Copy the PUBLIC KEY to your browser-side code"
|
|
||||||
echo "2. Set the PRIVATE KEY in your server environment"
|
|
||||||
echo "3. Update config.json with your email address"
|
|
||||||
echo "4. Test with: curl http://localhost:5000/api/push/vapid-key"
|
|
||||||
else
|
|
||||||
echo "❌ Error generating keys"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -0,0 +1,588 @@
|
|||||||
|
"""
|
||||||
|
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'])
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from Web.app import app
|
||||||
|
with app.test_client() as client:
|
||||||
|
resp = client.get('/terminplan')
|
||||||
|
print(resp.status_code)
|
||||||
|
# print(resp.data.decode('utf-8')[:200])
|
||||||
Reference in New Issue
Block a user