d68d84e5c8
* fix(admin): serve videos with their real MIME type in the admin photo view (#908) (stable) The admin view route built Content-Type from the filename extension — image/<ext> — which is invalid for videos (image/mp4). The admin player fetches this URL into a blob that inherits the type, and browsers refuse to play a <video> blob labeled image/*: blank/grey preview, while download (which already uses photo.mime_type) worked fine. Stored mime_type now wins; videos without one fall back to video/mp4, images to the extension, and extensionless files to image/jpeg instead of the equally invalid bare 'image/'. Also unrefs chunkedUploadService's module-level hourly cleanup interval: it kept Jest from exiting for any suite requiring adminPhotos (it's why adminPhotos.reference sits on the CI ignore list). Production behavior unchanged — the HTTP listener keeps the process alive. New adminPhotoContentType suite pins all four MIME cases. * fix(admin): harden admin photo Content-Type resolution (#908 review round) External review findings, all verified: - The header is now ALWAYS image/* or video/*. photos.mime_type is never echoed verbatim unless it is a video/ type — the chunked-upload path stores the client-sent MIME unvalidated, so a stored text/html served inline under the app origin was a same-origin XSS hazard. - MIME-less videos map from the extension via the shared EXTENSION_TO_MIME (.mov → video/quicktime, .webm → video/webm) instead of a blanket video/mp4 that would mislabel them. - Images ignore the stored MIME entirely: migration 039 backfilled image/jpeg onto every legacy row (PNGs included), so trusting it would regress previously-correct extension-derived types. Extension wins, normalized (jpg → image/jpeg). Suite extended to 8 MIME cases including the XSS guard and the 039-backfill immunity. * fix(admin): validate stored video MIME as a full header-safe token (#908 review round 2) A prefix check let malformed client-stored values through: 'video/mp4\r\nX: y' makes res.setHeader throw ERR_INVALID_CHAR — a permanent 500 for that photo — and a bare 'video/' is an invalid type. Strict /^video\/[\w.+-]+$/ now gates the stored value; anything else falls back to the extension map. Two new tests pin both shapes. * fix(admin): map-only image Content-Type — no raw extension interpolation (#908 review round 3) image/${ext} could synthesize image/svg+xml (scriptable when served inline) or header-invalid values from client-controlled chunked-upload filenames. The shared EXTENSION_TO_MIME map is now the allowlist on the image side too; unmapped extensions serve as image/jpeg — browsers sniff image bytes in img/blob contexts, so a mislabel is harmless where an injected type is not. * fix(admin): own-property lookup in the extension MIME map (#908 review round) A client-controlled filename ending in .constructor / .__proto__ / .toString made EXTENSION_TO_MIME[ext] return an inherited Object.prototype member (truthy), and the downstream extMime.startsWith threw — a permanent 500 on the admin view for that photo instead of the JPEG / mp4 fallback. hasOwnProperty-gated now; test pins both a .constructor image and a .__proto__ video. * fix(admin): honor safe stored image MIME for auto-imported formats (#908 review round 2) My previous round made the image side map-only to dodge the migration 039 image/jpeg backfill and image/svg+xml — but that regressed the S3 auto-importer (STORAGE_AUTO_IMPORT), which stores correct types for avif/bmp/tiff/heic whose extensions aren't in EXTENSION_TO_MIME. Those now served as image/jpeg (JPEG-labelled non-JPEG bytes). Precedence is now mapped-extension (still corrects the 039 backfill on PNGs) -> stored MIME IF in a safe raster allowlist (avif/bmp/tiff/heic + the mapped ones) -> image/jpeg. Allowlist, not a regex: image/svg+xml stays excluded (scriptable inline). Tests pin avif preserved and svg degraded to jpeg. * fix(admin): allow any header-safe raster MIME, deny svg/xml (#908 review round 3) The round-2 hand-listed Set kept missing formats the S3 auto-importer stores (apng/ico/jxl beyond avif/bmp/tiff). Replace it with a regex: honor image/<token> EXCEPT the scriptable svg / *+xml family. Covers every current and future raster type in one rule while still blocking inline-scriptable svg and header injection. Tests pin apng + x-icon preserved, svg still degraded to jpeg. --------- Co-authored-by: Paul Nothaft <paul@MacStudio-von-Paul.local>
Enhanced Backup System Test Suite
This directory contains comprehensive tests for the enhanced backup system with S3 support.
Test Structure
Unit Tests
services/backupService.enhanced.test.js- Unit tests for the enhanced backup service- Configuration management
- S3 backup functionality
- Manifest generation
- Error handling and recovery
- Backward compatibility (local and rsync)
- Service lifecycle management
Integration Tests
integration/backup-s3.test.js- Integration tests for S3 backups- Real S3/MinIO connection tests
- Full backup process with actual files
- Incremental backup verification
- Manifest storage and retrieval
- Error recovery scenarios
Manual Integration Test Script
../scripts/test-backup-integration.js- Comprehensive manual testing script- Can test against MinIO, AWS S3, or any S3-compatible service
- Tests all backup types (S3, local, rsync)
- Performance testing with large files
- Detailed progress reporting
Running Tests
Prerequisites
-
For Unit Tests: No special setup required, all dependencies are mocked.
-
For Integration Tests: Requires a running S3-compatible service (MinIO recommended)
# Start MinIO using Docker docker run -d \ -p 9000:9000 \ -p 9001:9001 \ --name minio-test \ -e MINIO_ROOT_USER=minioadmin \ -e MINIO_ROOT_PASSWORD=minioadmin \ minio/minio server /data --console-address ":9001" -
Environment Variables (for integration tests):
# Optional - defaults work with local MinIO export TEST_S3_ENDPOINT=http://localhost:9000 export TEST_S3_ACCESS_KEY=minioadmin export TEST_S3_SECRET_KEY=minioadmin # Skip S3 tests if no S3 service available export SKIP_S3_TESTS=true
Running Unit Tests
# Run all backup service tests
npm test -- __tests__/services/backupService.enhanced.test.js
# Run specific test suite
npm test -- __tests__/services/backupService.enhanced.test.js -t "S3 Backup Functionality"
# Run with coverage
npm test -- --coverage __tests__/services/backupService.enhanced.test.js
Running Integration Tests
# Ensure MinIO is running first!
# Run S3 integration tests
npm test -- __tests__/integration/backup-s3.test.js
# Run with verbose output
npm test -- __tests__/integration/backup-s3.test.js --verbose
# Skip S3 tests if needed
SKIP_S3_TESTS=true npm test -- __tests__/integration/backup-s3.test.js
Running Manual Integration Tests
# Test with local MinIO (default)
node scripts/test-backup-integration.js
# Test with AWS S3
node scripts/test-backup-integration.js \
--endpoint https://s3.amazonaws.com \
--access-key YOUR_ACCESS_KEY \
--secret-key YOUR_SECRET_KEY \
--bucket your-test-bucket
# Test local backup
node scripts/test-backup-integration.js --type local
# Test with cleanup after completion
node scripts/test-backup-integration.js --cleanup
# Verbose output
node scripts/test-backup-integration.js --verbose
Test Coverage
The test suite covers:
Configuration
- ✅ Database configuration retrieval
- ✅ JSON parsing and error handling
- ✅ Configuration validation
- ✅ Required field validation
S3 Functionality
- ✅ S3 client initialization
- ✅ Connection testing
- ✅ File upload with progress tracking
- ✅ Large file handling (multipart upload)
- ✅ Metadata and custom headers
- ✅ Error handling and retries
Backup Process
- ✅ Full backup execution
- ✅ Incremental backup (changed files only)
- ✅ File checksum calculation and comparison
- ✅ Database backup inclusion
- ✅ Archive inclusion toggle
- ✅ File size limits
Manifest Generation
- ✅ Full manifest generation
- ✅ Incremental manifest with parent reference
- ✅ JSON and YAML format support
- ✅ Manifest validation
- ✅ S3 manifest storage and retrieval
- ✅ Checksum verification
Error Handling
- ✅ S3 connection failures
- ✅ File read errors
- ✅ Individual file failure recovery
- ✅ Retry logic with exponential backoff
- ✅ Email notifications on failure
- ✅ Concurrent backup prevention
Backward Compatibility
- ✅ Local directory backup
- ✅ Rsync backup
- ✅ Existing manifest format support
Service Management
- ✅ Cron job scheduling
- ✅ Service start/stop
- ✅ Manual backup triggering
- ✅ Backup history and status
Mock Setup
The unit tests use comprehensive mocking:
// Database mocking
jest.mock('../../src/database/db');
// S3 client mocking
jest.mock('../../src/services/storage/s3Storage');
// File system mocking
const mockFs = require('mock-fs');
// Cron job mocking
jest.mock('node-cron');
CI/CD Integration
To run tests in CI/CD pipeline:
# Example GitHub Actions
- name: Run Unit Tests
run: npm test -- __tests__/services/backupService.enhanced.test.js
- name: Start MinIO
run: |
docker run -d \
-p 9000:9000 \
--name minio-test \
-e MINIO_ROOT_USER=minioadmin \
-e MINIO_ROOT_PASSWORD=minioadmin \
minio/minio server /data
- name: Run Integration Tests
run: npm test -- __tests__/integration/backup-s3.test.js
Debugging Tests
# Run tests in debug mode
node --inspect-brk ./node_modules/.bin/jest __tests__/services/backupService.enhanced.test.js
# Run single test with console output
npm test -- __tests__/services/backupService.enhanced.test.js -t "should perform S3 backup" --verbose
Performance Considerations
- Integration tests create real files and S3 objects
- Each test run creates a unique S3 bucket to avoid conflicts
- Cleanup is automatic but can be disabled for debugging
- Large file tests (10MB+) are included but can be slow
Adding New Tests
When adding new backup features:
- Add unit tests to
backupService.enhanced.test.js - Add integration tests to
backup-s3.test.jsif S3-specific - Update manual test script for comprehensive testing
- Ensure mocks are properly configured
- Document any new environment requirements