feat: mail attachment support — sync, display, download, upload
This commit is contained in:
+46
-99
@@ -1,112 +1,59 @@
|
||||
# Test Report — Report Generator Core Plugin
|
||||
# Test Report — Mail Attachment Support
|
||||
|
||||
## Date: 2026-07-08
|
||||
**Date:** 2026-07-15
|
||||
**Task:** Mail attachment support — sync, display, download, upload
|
||||
|
||||
## Task: Build Report Generator Core Plugin for LeoCRM
|
||||
## Changes Summary
|
||||
|
||||
## Files Created
|
||||
1. `app/plugins/builtins/report_generator/__init__.py` — Plugin package init (5 lines)
|
||||
2. `app/plugins/builtins/report_generator/plugin.py` — Plugin manifest + class (29 lines)
|
||||
3. `app/plugins/builtins/report_generator/models.py` — ReportTemplate + ReportInstance models (63 lines)
|
||||
4. `app/plugins/builtins/report_generator/schemas.py` — Pydantic schemas (52 lines)
|
||||
5. `app/plugins/builtins/report_generator/routes.py` — 8 API endpoints (415 lines)
|
||||
6. `app/plugins/builtins/report_generator/migrations/0001_initial.sql` — PostgreSQL migration (32 lines)
|
||||
7. `app/plugins/builtins/__init__.py` — Updated to include ReportGeneratorPlugin import
|
||||
### Backend
|
||||
- **services.py**: Added `_sanitize_filename`, `_attachment_storage_path`, `_save_attachment_to_storage`, `attachment_to_response` helpers. Modified IMAP sync to save attachment content to disk + create `MailAttachment` DB records. Modified `send_mail_via_smtp` to accept `attachment_paths` and attach files to `EmailMessage`. Updated `mail_to_response` to use `attachment_to_response`.
|
||||
- **routes.py**: Added `POST /api/v1/mail/upload-attachment` endpoint (multipart/form-data). Added `_resolve_attachment_paths` helper. Modified `send_mail` route to resolve attachment IDs and pass to `send_mail_via_smtp`.
|
||||
|
||||
## Tests Run
|
||||
### Frontend
|
||||
- **api/mail.ts**: Updated `MailAttachment` interface (added `size_bytes`, `dms_file_id`). Added `uploadAttachment` function and `UploadedAttachment` interface.
|
||||
- **MailDetail.tsx**: Fixed `att.size` → `att.size_bytes` for formatBytes call.
|
||||
- **ComposeModal.tsx**: Added file input (multiple), attachment upload via `uploadAttachment`, attachment list with remove buttons, attachment IDs passed in send payload.
|
||||
- **i18n locales**: Added `mail.addAttachment` and `common.remove` keys to en.json and de.json.
|
||||
|
||||
### 1. AST Syntax Check
|
||||
**Command:** `python -c 'import ast; ast.parse(...)'`
|
||||
**Result:** All 6 Python files passed AST syntax check.
|
||||
## Test Results
|
||||
|
||||
### Python Syntax Check
|
||||
```
|
||||
OK: app/plugins/builtins/report_generator/__init__.py
|
||||
OK: app/plugins/builtins/report_generator/plugin.py
|
||||
OK: app/plugins/builtins/report_generator/models.py
|
||||
OK: app/plugins/builtins/report_generator/schemas.py
|
||||
OK: app/plugins/builtins/report_generator/routes.py
|
||||
OK: app/plugins/builtins/__init__.py
|
||||
/opt/venv/bin/python -c "import py_compile; py_compile.compile('app/plugins/builtins/mail/services.py', doraise=True); py_compile.compile('app/plugins/builtins/mail/routes.py', doraise=True)"
|
||||
→ Python syntax OK
|
||||
```
|
||||
|
||||
### 2. Import Verification
|
||||
**Command:** `python -c 'from app.plugins.builtins.report_generator.plugin import ReportGeneratorPlugin; ...'`
|
||||
**Result:** All imports resolve correctly.
|
||||
### Frontend TypeScript Check
|
||||
```
|
||||
Base + TenantMixin OK
|
||||
Schemas OK
|
||||
TemplateCreate validation OK: {'name': 'test', 'description': '', 'template_type': 'jinja2', 'content': '{{ data }}', 'output_format': 'csv'}
|
||||
Models OK
|
||||
ReportTemplate table: report_templates
|
||||
ReportInstance table: report_instances
|
||||
npx tsc --noEmit 2>&1 | grep -E 'mail|Mail|Compose|MailDetail'
|
||||
→ No errors in mail files
|
||||
```
|
||||
Pre-existing errors in SettingsPlugins.tsx and SettingsRoles.tsx (unrelated to this task).
|
||||
|
||||
### Frontend Tests (vitest)
|
||||
|
||||
| Test File | Result |
|
||||
|-----------|--------|
|
||||
| ComposeModal.test.tsx | ✅ 10/10 passed |
|
||||
| MailSettings.test.tsx | ✅ 10/10 passed |
|
||||
| MailPage.test.tsx | ⚠️ 6 passed, 8 failed (PRE-EXISTING — confirmed via git stash) |
|
||||
|
||||
### Backend Tests (pytest)
|
||||
```
|
||||
pytest tests/test_mail.py
|
||||
→ ImportError: No module named 'redis' (pre-existing environment issue)
|
||||
```
|
||||
|
||||
### 3. Jinja2 Template Rendering
|
||||
**Result:** Template rendered successfully.
|
||||
```
|
||||
Jinja2 render OK: 'Name,Value\nAlice,100\nBob,200'
|
||||
```
|
||||
## Smoke Test Description
|
||||
|
||||
### 4. CSV Generation (Python stdlib csv)
|
||||
**Result:** CSV file generated with UTF-8 BOM.
|
||||
```
|
||||
CSV generation OK, size: 44 bytes
|
||||
```
|
||||
1. **IMAP Sync**: Attachment content is now captured during IMAP sync (`part.get_payload(decode=True)`), saved to `{storage_path}/mail_attachments/{mail_id}/{filename}`, and `MailAttachment` records are created in the DB.
|
||||
2. **Download**: Existing `GET /{mail_id}/attachments/{att_id}` route streams the file from disk with correct Content-Type and Content-Disposition headers.
|
||||
3. **Upload**: New `POST /upload-attachment` endpoint accepts multipart/form-data, validates 25MB max size, saves to temp directory, returns attachment ID.
|
||||
4. **Send with attachments**: `send_mail` route resolves attachment IDs to file paths, passes them to `send_mail_via_smtp` which adds them to the `EmailMessage` via `msg.add_attachment()`.
|
||||
5. **Frontend display**: MailDetail shows attachments with icon, filename, size (using `size_bytes`), and download button.
|
||||
6. **Frontend compose**: ComposeModal has file input button, uploads files via `uploadAttachment`, shows list with remove option, passes attachment IDs in send payload.
|
||||
|
||||
### 5. Excel Generation (openpyxl)
|
||||
**Result:** Excel workbook created.
|
||||
```
|
||||
Excel generation OK, size: 4874 bytes
|
||||
```
|
||||
|
||||
### 6. JSON Generation
|
||||
**Result:** JSON parsed and returned.
|
||||
```
|
||||
JSON generation OK: {'key': 'value'}
|
||||
```
|
||||
|
||||
### 7. Plugin Manifest + Routes
|
||||
**Result:** Plugin loads with correct manifest and 8 routes registered.
|
||||
```
|
||||
Plugin instance: <ReportGeneratorPlugin name=report_generator version=1.0.0>
|
||||
Manifest name: report_generator
|
||||
Manifest version: 1.0.0
|
||||
Is core: True
|
||||
Dependencies: ['permissions']
|
||||
Events: ['report.requested', 'report.generated']
|
||||
Permissions: ['reports.read', 'reports.generate', 'reports.manage_templates']
|
||||
Routes loaded: 1 router(s)
|
||||
Router prefix: /api/v1/reports
|
||||
Router routes: 8
|
||||
{'GET'} /api/v1/reports/templates
|
||||
{'POST'} /api/v1/reports/templates
|
||||
{'GET'} /api/v1/reports/templates/{template_id}
|
||||
{'PUT'} /api/v1/reports/templates/{template_id}
|
||||
{'DELETE'} /api/v1/reports/templates/{template_id}
|
||||
{'POST'} /api/v1/reports/generate
|
||||
{'GET'} /api/v1/reports/{report_id}
|
||||
{'GET'} /api/v1/reports/{report_id}/download
|
||||
```
|
||||
|
||||
### 8. Migration SQL Validation
|
||||
**Result:** 2 tables, 5 indexes, all required columns and FK present.
|
||||
```
|
||||
Tables: ['report_templates', 'report_instances']
|
||||
Indexes: [('ix_report_templates_tenant', 'report_templates', 'tenant_id'), ('ix_report_templates_name', 'report_templates', 'name'), ('ix_report_instances_tenant', 'report_instances', 'tenant_id'), ('ix_report_instances_template', 'report_instances', 'template_id'), ('ix_report_instances_status', 'report_instances', 'status')]
|
||||
FK reference present
|
||||
Migration SQL validation passed.
|
||||
```
|
||||
|
||||
## Smoke Test Summary
|
||||
- Plugin class instantiates and loads routes correctly
|
||||
- All 8 endpoints registered under `/api/v1/reports` prefix
|
||||
- Jinja2 template rendering works with data injection
|
||||
- CSV output uses Python stdlib csv module (with UTF-8 BOM for Excel compat)
|
||||
- Excel output uses openpyxl Workbook
|
||||
- JSON output parses rendered template as JSON
|
||||
- Migration SQL creates 2 tables with correct columns, indexes, and FK
|
||||
- Tenant isolation: all queries filter by `tenant_id` from `current_user`
|
||||
- Soft delete: templates have `deleted_at` column, queries filter `deleted_at.is_(None)`
|
||||
|
||||
## Note on Dependencies
|
||||
- `openpyxl>=3.1` and `redis>=5.0` and `passlib` were in requirements.txt but not installed in venv. Installed them to verify import chain. No changes to requirements.txt.
|
||||
|
||||
## Result: ALL TESTS PASSED ✅
|
||||
## Security
|
||||
- Filenames sanitized via `_sanitize_filename` (removes path components, replaces dangerous characters)
|
||||
- Max 25MB per attachment enforced in both upload endpoint and frontend
|
||||
- No path traversal possible (basename extraction + character replacement)
|
||||
|
||||
Reference in New Issue
Block a user