Files
Reklamator/tests/contract/test_submission_routes.py
T
gurixandClaude b301def134 Implement MVP: Anonymous feedback submission (User Story 1)
Complete implementation of Phase 1-3 (64 tasks):
- Phase 1: Project setup with Flask, pytest, configuration
- Phase 2: Core infrastructure (auth, models, services, testing)
- Phase 3: Anonymous feedback submission with file uploads

Features:
- Anonymous feedback submission (text and/or up to 3 file attachments)
- Multi-language support (any language accepted)
- File validation (type, size) and virus scanning (ClamAV)
- Product management with active/archived status
- File-based storage with YAML metadata
- User authentication system (Flask-Login)
- CSRF protection and rate limiting
- Test coverage: 10 passing tests (contract + integration)

Security:
- No IP address logging (FR-055 compliance)
- File type whitelist and size limits (10MB max)
- Virus scanning with graceful degradation
- Filename sanitization and secure storage

Test Results:
- 8 contract tests passed
- 2 integration tests passed
- End-to-end workflow verified

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 15:14:51 +02:00

210 lines
6.3 KiB
Python

"""Contract tests for submission routes"""
import pytest
import io
import os
import yaml
@pytest.fixture
def test_product(app):
"""Create a test product"""
with app.app_context():
# Create test product directory and config
product_dir = os.path.join(app.config['DATA_DIR'], 'products', 'test-product')
os.makedirs(product_dir, exist_ok=True)
# Create product config
config_file = os.path.join(product_dir, 'config.yaml')
config_data = {
'product_id': 'test-product',
'name': 'Test Product',
'submission_url_slug': 'test-product',
'owner_language': 'en',
'assigned_owner_ids': ['usr_0001'],
'status': 'active'
}
with open(config_file, 'w') as f:
yaml.dump(config_data, f)
yield 'test-product'
@pytest.mark.contract
def test_get_submission_form(client, test_product):
"""T030: Contract test for GET /submit/{product_slug}
Expected: 200 OK with HTML form containing textarea and file inputs
"""
response = client.get('/submit/test-product')
assert response.status_code == 200
assert b'<form' in response.data
assert b'textarea' in response.data or b'<textarea' in response.data
assert b'type="file"' in response.data
@pytest.mark.contract
def test_post_submission_text_only(client, test_product):
"""T031: Contract test for POST /submit/{product_slug} with text only
Expected: 200/302 success with confirmation message
"""
data = {
'feedback_text': 'This is my feedback about the product.'
}
response = client.post('/submit/test-product', data=data, follow_redirects=True)
assert response.status_code == 200
assert b'success' in response.data.lower() or b'thank' in response.data.lower()
@pytest.mark.contract
def test_post_submission_files_only(client, test_product):
"""T032: Contract test for POST /submit/{product_slug} with files only
Expected: 200/302 success with confirmation message
"""
data = {
'files': [
(io.BytesIO(b'test file content'), 'test.txt')
]
}
response = client.post('/submit/test-product',
data=data,
content_type='multipart/form-data',
follow_redirects=True)
assert response.status_code == 200
assert b'success' in response.data.lower() or b'thank' in response.data.lower()
@pytest.mark.contract
def test_post_submission_text_and_files(client, test_product):
"""T033: Contract test for POST /submit/{product_slug} with text and files
Expected: 200/302 success with confirmation message
"""
data = {
'feedback_text': 'Here is my feedback with attachments.',
'files': [
(io.BytesIO(b'screenshot content'), 'screenshot.png'),
(io.BytesIO(b'log file content'), 'error.log')
]
}
response = client.post('/submit/test-product',
data=data,
content_type='multipart/form-data',
follow_redirects=True)
assert response.status_code == 200
assert b'success' in response.data.lower() or b'thank' in response.data.lower()
@pytest.mark.contract
def test_empty_submission_rejected(client, test_product):
"""T034: Contract test for empty submission rejection (400)
Expected: 400 Bad Request - must provide either text or files
"""
data = {
'feedback_text': ''
}
response = client.post('/submit/test-product', data=data)
assert response.status_code == 400
@pytest.mark.contract
def test_too_many_files_rejected(client, test_product):
"""T035: Contract test for >3 files rejection (400)
Expected: 400 Bad Request - maximum 3 files allowed
"""
data = {
'files': [
(io.BytesIO(b'file1'), 'file1.txt'),
(io.BytesIO(b'file2'), 'file2.txt'),
(io.BytesIO(b'file3'), 'file3.txt'),
(io.BytesIO(b'file4'), 'file4.txt')
]
}
response = client.post('/submit/test-product',
data=data,
content_type='multipart/form-data')
assert response.status_code == 400
assert b'maximum' in response.data.lower() or b'3' in response.data
@pytest.mark.contract
def test_large_file_rejected(client, test_product):
"""T036: Contract test for >10MB file rejection (413)
Expected: 413 Request Entity Too Large
"""
# Create a file larger than 10MB
large_content = b'x' * (11 * 1024 * 1024) # 11MB
data = {
'files': [
(io.BytesIO(large_content), 'large.txt')
]
}
response = client.post('/submit/test-product',
data=data,
content_type='multipart/form-data')
# Flask will reject this with 413 due to MAX_CONTENT_LENGTH
assert response.status_code == 413
@pytest.mark.contract
def test_unsupported_file_type_rejected(client, test_product):
"""T037: Contract test for unsupported file type rejection (400)
Expected: 400 Bad Request - file type not allowed
"""
data = {
'files': [
(io.BytesIO(b'#!/bin/bash\necho malicious'), 'script.sh')
]
}
response = client.post('/submit/test-product',
data=data,
content_type='multipart/form-data')
assert response.status_code == 400
assert b'not allowed' in response.data.lower() or b'type' in response.data.lower()
@pytest.mark.contract
def test_rate_limiting(client, test_product, app):
"""T038: Contract test for rate limiting (429 after 10 submissions)
Expected: 429 Too Many Requests after exceeding rate limit
"""
# Skip if rate limiting is disabled
if not app.config.get('RATELIMIT_ENABLED'):
pytest.skip('Rate limiting disabled in test config')
# Make 10 successful submissions (the limit)
for i in range(10):
data = {'feedback_text': f'Feedback {i}'}
response = client.post('/submit/test-product', data=data)
# Should succeed (200 or 302)
assert response.status_code in [200, 302]
# 11th submission should be rate limited
data = {'feedback_text': 'This should be rate limited'}
response = client.post('/submit/test-product', data=data)
assert response.status_code == 429