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>
This commit is contained in:
2025-10-16 15:14:51 +02:00
co-authored by Claude
parent 07e51d7468
commit b301def134
37 changed files with 2416 additions and 39 deletions
+1
View File
@@ -0,0 +1 @@
"""Integration tests package"""
@@ -0,0 +1,166 @@
"""Integration test for complete feedback submission flow"""
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.integration
def test_complete_feedback_submission_flow(client, app, test_product):
"""T039: Integration test for complete feedback submission flow
Test the entire user journey:
1. User visits submission form
2. User fills in feedback text
3. User attaches files
4. User submits form
5. System validates input
6. System saves feedback to filesystem
7. System displays confirmation
8. Feedback is retrievable from storage
"""
# Step 1: Visit submission form
response = client.get('/submit/test-product')
assert response.status_code == 200
assert b'<form' in response.data
# Step 2-4: Submit feedback with text and files
feedback_text = 'I found a bug in the login page. When I enter my password, it does not accept special characters.'
data = {
'feedback_text': feedback_text,
'files': [
(io.BytesIO(b'PNG fake image data'), 'screenshot.png'),
(io.BytesIO(b'Error log contents\nLine 2\nLine 3'), 'error.log')
]
}
response = client.post('/submit/test-product',
data=data,
content_type='multipart/form-data',
follow_redirects=True)
# Step 7: Verify success confirmation
assert response.status_code == 200
assert b'success' in response.data.lower() or b'thank' in response.data.lower()
# Step 8: Verify feedback was saved to filesystem
with app.app_context():
data_dir = app.config['DATA_DIR']
products_dir = os.path.join(data_dir, 'products', 'test-product', 'feedback')
# Check that feedback directory was created
assert os.path.exists(products_dir)
# Find the created feedback directory (should be UUID-named)
feedback_dirs = [d for d in os.listdir(products_dir)
if os.path.isdir(os.path.join(products_dir, d))]
assert len(feedback_dirs) > 0, "No feedback directory was created"
feedback_dir = os.path.join(products_dir, feedback_dirs[0])
# Verify metadata.yaml exists
metadata_file = os.path.join(feedback_dir, 'metadata.yaml')
assert os.path.exists(metadata_file)
# Verify metadata content
with open(metadata_file, 'r') as f:
metadata = yaml.safe_load(f)
assert metadata['feedback_id'] == feedback_dirs[0]
assert metadata['product_id'] == 'test-product'
assert metadata['status'] == 'new'
assert 'submitted_at' in metadata
assert metadata.get('has_attachments') == True
assert metadata.get('attachment_count') == 2
# Verify content.txt exists and contains the feedback
content_file = os.path.join(feedback_dir, 'content.txt')
assert os.path.exists(content_file)
with open(content_file, 'r') as f:
saved_content = f.read()
assert feedback_text in saved_content
# Verify attachments directory and files exist
attachments_dir = os.path.join(feedback_dir, 'attachments')
assert os.path.exists(attachments_dir)
attachments = os.listdir(attachments_dir)
assert len(attachments) == 2
# Verify specific attachment files
attachment_names = [a for a in attachments]
assert 'screenshot.png' in attachment_names
assert 'error.log' in attachment_names
# Verify no IP address is stored (FR-055 compliance)
assert 'ip_address' not in metadata
assert 'submitter_ip' not in metadata
@pytest.mark.integration
def test_feedback_submission_without_attachments(client, app, test_product):
"""Integration test for feedback submission with text only (no files)"""
feedback_text = 'Simple text feedback without attachments.'
data = {
'feedback_text': feedback_text
}
response = client.post('/submit/test-product',
data=data,
follow_redirects=True)
assert response.status_code == 200
# Verify feedback was saved
with app.app_context():
data_dir = app.config['DATA_DIR']
products_dir = os.path.join(data_dir, 'products', 'test-product', 'feedback')
feedback_dirs = [d for d in os.listdir(products_dir)
if os.path.isdir(os.path.join(products_dir, d))]
# Find the most recent feedback
feedback_dir = os.path.join(products_dir, feedback_dirs[-1])
# Verify metadata shows no attachments
metadata_file = os.path.join(feedback_dir, 'metadata.yaml')
with open(metadata_file, 'r') as f:
metadata = yaml.safe_load(f)
assert metadata.get('has_attachments') == False
assert metadata.get('attachment_count') == 0
# Verify attachments directory doesn't exist or is empty
attachments_dir = os.path.join(feedback_dir, 'attachments')
if os.path.exists(attachments_dir):
assert len(os.listdir(attachments_dir)) == 0