forked from enviPath/enviPy
Add API key authentication to v1 API Also includes: - management command to create keys for users - Improvements to API tests Minor: - more robust way to start docker dev container. Reviewed-on: enviPath/enviPy#327 Co-authored-by: Tobias O <tobias.olenyi@envipath.com> Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
302 lines
10 KiB
Python
302 lines
10 KiB
Python
"""
|
|
Tests for Scenario Creation Endpoint Error Handling.
|
|
|
|
Tests comprehensive error handling for POST /api/v1/package/{uuid}/scenario/
|
|
including package not found, permission denied, validation errors, and database errors.
|
|
"""
|
|
|
|
from django.test import TestCase, tag
|
|
import json
|
|
from uuid import uuid4
|
|
|
|
from epdb.logic import PackageManager, UserManager
|
|
from epdb.models import Scenario
|
|
|
|
|
|
@tag("api", "scenario_creation")
|
|
class ScenarioCreationAPITests(TestCase):
|
|
"""Test scenario creation endpoint error handling."""
|
|
|
|
@classmethod
|
|
def setUpTestData(cls):
|
|
"""Set up test data: users and packages."""
|
|
cls.user = UserManager.create_user(
|
|
"scenario-test-user",
|
|
"scenario-test@envipath.com",
|
|
"SuperSafe",
|
|
set_setting=False,
|
|
add_to_group=False,
|
|
is_active=True,
|
|
)
|
|
cls.other_user = UserManager.create_user(
|
|
"other-user",
|
|
"other@envipath.com",
|
|
"SuperSafe",
|
|
set_setting=False,
|
|
add_to_group=False,
|
|
is_active=True,
|
|
)
|
|
cls.package = PackageManager.create_package(
|
|
cls.user, "Test Package", "Test package for scenario creation"
|
|
)
|
|
|
|
def test_create_scenario_package_not_found(self):
|
|
"""Test that non-existent package UUID returns 404."""
|
|
self.client.force_login(self.user)
|
|
|
|
fake_uuid = uuid4()
|
|
payload = {
|
|
"name": "Test Scenario",
|
|
"description": "Test description",
|
|
"scenario_date": "2024-01-01",
|
|
"scenario_type": "biodegradation",
|
|
"additional_information": [],
|
|
}
|
|
|
|
response = self.client.post(
|
|
f"/api/v1/package/{fake_uuid}/scenario/",
|
|
data=json.dumps(payload),
|
|
content_type="application/json",
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 404)
|
|
self.assertIn("Package not found", response.json()["detail"])
|
|
|
|
def test_create_scenario_insufficient_permissions(self):
|
|
"""Test that unauthorized access returns 403."""
|
|
self.client.force_login(self.other_user)
|
|
|
|
payload = {
|
|
"name": "Test Scenario",
|
|
"description": "Test description",
|
|
"scenario_date": "2024-01-01",
|
|
"scenario_type": "biodegradation",
|
|
"additional_information": [],
|
|
}
|
|
|
|
response = self.client.post(
|
|
f"/api/v1/package/{self.package.uuid}/scenario/",
|
|
data=json.dumps(payload),
|
|
content_type="application/json",
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 403)
|
|
self.assertIn("permission", response.json()["detail"].lower())
|
|
|
|
def test_create_scenario_invalid_ai_type(self):
|
|
"""Test that unknown additional information type returns 400."""
|
|
self.client.force_login(self.user)
|
|
|
|
payload = {
|
|
"name": "Test Scenario",
|
|
"description": "Test description",
|
|
"scenario_date": "2024-01-01",
|
|
"scenario_type": "biodegradation",
|
|
"additional_information": [
|
|
{"type": "invalid_type_that_does_not_exist", "data": {"some_field": "some_value"}}
|
|
],
|
|
}
|
|
|
|
response = self.client.post(
|
|
f"/api/v1/package/{self.package.uuid}/scenario/",
|
|
data=json.dumps(payload),
|
|
content_type="application/json",
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 400)
|
|
response_data = response.json()
|
|
self.assertIn("Validation errors", response_data["detail"])
|
|
|
|
def test_create_scenario_validation_error(self):
|
|
"""Test that invalid additional information data returns 400."""
|
|
self.client.force_login(self.user)
|
|
|
|
# Use malformed data structure for an actual AI type
|
|
payload = {
|
|
"name": "Test Scenario",
|
|
"description": "Test description",
|
|
"scenario_date": "2024-01-01",
|
|
"scenario_type": "biodegradation",
|
|
"additional_information": [
|
|
{
|
|
"type": "invalid_type_name",
|
|
"data": None, # This should cause a validation error
|
|
}
|
|
],
|
|
}
|
|
|
|
response = self.client.post(
|
|
f"/api/v1/package/{self.package.uuid}/scenario/",
|
|
data=json.dumps(payload),
|
|
content_type="application/json",
|
|
)
|
|
|
|
# Should return 422 for validation errors
|
|
self.assertEqual(response.status_code, 422)
|
|
|
|
def test_create_scenario_success(self):
|
|
"""Test that valid scenario creation returns 200."""
|
|
self.client.force_login(self.user)
|
|
|
|
payload = {
|
|
"name": "Test Scenario",
|
|
"description": "Test description",
|
|
"scenario_date": "2024-01-01",
|
|
"scenario_type": "biodegradation",
|
|
"additional_information": [],
|
|
}
|
|
|
|
response = self.client.post(
|
|
f"/api/v1/package/{self.package.uuid}/scenario/",
|
|
data=json.dumps(payload),
|
|
content_type="application/json",
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.json()
|
|
self.assertEqual(data["name"], "Test Scenario")
|
|
self.assertEqual(data["description"], "Test description")
|
|
|
|
# Verify scenario was actually created
|
|
scenario = Scenario.objects.get(name="Test Scenario")
|
|
self.assertEqual(scenario.package, self.package)
|
|
self.assertEqual(scenario.scenario_type, "biodegradation")
|
|
|
|
def test_create_scenario_auto_name(self):
|
|
"""Test that empty name triggers auto-generation."""
|
|
self.client.force_login(self.user)
|
|
|
|
payload = {
|
|
"name": "", # Empty name should be auto-generated
|
|
"description": "Test description",
|
|
"scenario_date": "2024-01-01",
|
|
"scenario_type": "biodegradation",
|
|
"additional_information": [],
|
|
}
|
|
|
|
response = self.client.post(
|
|
f"/api/v1/package/{self.package.uuid}/scenario/",
|
|
data=json.dumps(payload),
|
|
content_type="application/json",
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.json()
|
|
# Auto-generated name should follow pattern "Scenario N"
|
|
self.assertTrue(data["name"].startswith("Scenario "))
|
|
|
|
def test_create_scenario_xss_protection(self):
|
|
"""Test that XSS attempts are sanitized."""
|
|
self.client.force_login(self.user)
|
|
|
|
payload = {
|
|
"name": "<script>alert('xss')</script>Clean Name",
|
|
"description": "<img src=x onerror=alert('xss')>Description",
|
|
"scenario_date": "2024-01-01",
|
|
"scenario_type": "biodegradation",
|
|
"additional_information": [],
|
|
}
|
|
|
|
response = self.client.post(
|
|
f"/api/v1/package/{self.package.uuid}/scenario/",
|
|
data=json.dumps(payload),
|
|
content_type="application/json",
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.json()
|
|
# XSS should be cleaned out
|
|
self.assertNotIn("<script>", data["name"])
|
|
self.assertNotIn("onerror", data["description"])
|
|
|
|
def test_create_scenario_missing_required_field(self):
|
|
"""Test that missing required fields returns validation error."""
|
|
self.client.force_login(self.user)
|
|
|
|
# Missing 'name' field entirely
|
|
payload = {
|
|
"description": "Test description",
|
|
"scenario_date": "2024-01-01",
|
|
"scenario_type": "biodegradation",
|
|
"additional_information": [],
|
|
}
|
|
|
|
response = self.client.post(
|
|
f"/api/v1/package/{self.package.uuid}/scenario/",
|
|
data=json.dumps(payload),
|
|
content_type="application/json",
|
|
)
|
|
|
|
# Should return 422 for schema validation errors
|
|
self.assertEqual(response.status_code, 422)
|
|
|
|
def test_create_scenario_type_error_in_ai(self):
|
|
"""Test that TypeError in AI instantiation returns 400."""
|
|
self.client.force_login(self.user)
|
|
|
|
payload = {
|
|
"name": "Test Scenario",
|
|
"description": "Test description",
|
|
"scenario_date": "2024-01-01",
|
|
"scenario_type": "biodegradation",
|
|
"additional_information": [
|
|
{
|
|
"type": "invalid_type_name",
|
|
"data": "string instead of dict", # Wrong type
|
|
}
|
|
],
|
|
}
|
|
|
|
response = self.client.post(
|
|
f"/api/v1/package/{self.package.uuid}/scenario/",
|
|
data=json.dumps(payload),
|
|
content_type="application/json",
|
|
)
|
|
|
|
# Should return 422 for validation errors
|
|
self.assertEqual(response.status_code, 422)
|
|
|
|
def test_create_scenario_default_values(self):
|
|
"""Test that default values are applied correctly."""
|
|
self.client.force_login(self.user)
|
|
|
|
# Minimal payload with only name
|
|
payload = {"name": "Minimal Scenario"}
|
|
|
|
response = self.client.post(
|
|
f"/api/v1/package/{self.package.uuid}/scenario/",
|
|
data=json.dumps(payload),
|
|
content_type="application/json",
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.json()
|
|
self.assertEqual(data["name"], "Minimal Scenario")
|
|
# Check defaults are applied
|
|
scenario = Scenario.objects.get(name="Minimal Scenario")
|
|
# Default description from model is "no description"
|
|
self.assertIn(scenario.description.lower(), ["", "no description"])
|
|
|
|
def test_create_scenario_unicode_characters(self):
|
|
"""Test that unicode characters are handled properly."""
|
|
self.client.force_login(self.user)
|
|
|
|
payload = {
|
|
"name": "Test Scenario 测试 🧪",
|
|
"description": "Description with émojis and spëcial çhars",
|
|
"scenario_date": "2024-01-01",
|
|
"scenario_type": "biodegradation",
|
|
"additional_information": [],
|
|
}
|
|
|
|
response = self.client.post(
|
|
f"/api/v1/package/{self.package.uuid}/scenario/",
|
|
data=json.dumps(payload),
|
|
content_type="application/json",
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.json()
|
|
self.assertIn("测试", data["name"])
|
|
self.assertIn("émojis", data["description"])
|