Files
picpeak/backend/__tests__
Paul NothaftandPaul Nothaft 011f6ae7ec feat(gallery): sized preview tiers so phones stop pulling 1920px (#1095) (#1099)
* feat(gallery): sized preview tiers so phones stop pulling 1920px (#1095)

A phone can display ~1170px at most, but the preview tier is a single
1920px JPEG with no size parameter — so every lightbox swipe ships
roughly twice the bytes it can use, and the slide track preloads
neighbours, which multiplies it. On the reporter's all-external install
a null preview_url falls back to the untouched NAS original, which makes
it worse again.

Backend: ?w= on the gallery preview route, whitelisted to 640/1280/1920.
A whitelist rather than a free-form width because every distinct value
is a permanent rendition on disk — an open parameter is an invitation to
fill the volume. Unrecognised or absent values fall through to the
canonical 1920 preview, so old clients and hand-typed URLs behave
exactly as today.

Extra tiers are cache, not state: ensurePreviewImageAtWidth keys them by
width, looks them up in storage and generates on miss, and never writes
photos.preview_path. That column owns the canonical rendition, and
threading a width through it would mean the last size anyone requested
silently becomes "the" preview. Requesting 1920 resolves to the existing
preview rather than a w1920 duplicate, so no install grows a second copy
of every preview it already has.

The tier is part of the ETag. Without it a client holding the 1920
rendition gets a 304 for its 640 request and renders the wrong size,
which is this feature inverted.

Frontend: the lightbox picks a tier from innerWidth x devicePixelRatio,
capped at DPR 3 — uncapped, a DPR-10 device asks for 3900px and lands
straight back on the desktop rendition. At the top tier the URL is left
byte-identical so existing caches and ETags stay valid and desktop sees
no change at all. saveData and a 2g/3g effectiveType drop one tier;
both are Chromium-only, so they are a bonus rather than the mechanism.

Grid thumbnails are NOT tiered here, deliberately. generateThumbnail
resolves its width from admin settings rather than an argument, so
tiering it is a separate change — and shipping a srcset whose candidates
the server ignores would be worse than shipping none: the browser would
take the "600w" candidate, receive the 300px image and upscale it, which
is the reported softness made slightly worse. That half of #1095 lands
separately.

* fix(gallery): scope tier keys per photo, size by long edge, clean up tiers

External review. Three findings against the tier work, one a
cross-gallery leak.

The tier cache key was the photo's BASENAME. Managed uploads keep camera
basenames, so two events can each hold an IMG_0001.jpg — and a tier is
served straight from a cache hit without re-reading the source, so the
second gallery gets the first gallery's photo. Keys are now scoped by
photo id for every source type. The RAW branch passed proc.outputBasename,
which would have dropped that scoping again; it now passes the scoped name.

Tier selection used viewport WIDTH, but ?w= bounds the LONG edge
(fit:'inside'). On a 390x844 phone at DPR 3 a 2:3 portrait is bound by
height and renders ~1755 device px, so width-only picked 1280 and made
portraits softer than today; landscape on the same phone needs ~1170. It
now computes the rendered long edge from the photo's own dimensions and
falls back to the top tier — today's behaviour — when they are unknown.

Tiers live outside photos.preview_path, so nothing else knew they
existed: delete, bulk-delete and archive left them orphaned in previews/
forever, and regenerate-previews refreshed only the canonical rendition
while phones kept the stale copy. previewTierKeys derives them from the
same deterministic scheme and all four paths clean up. Deliberately
outside the preview_path guard — a tier can exist when the canonical
rendition never did, so keying cleanup off preview_path would strand
precisely the photos only ever viewed on a phone.

The existing tier tests encoded the old width-only semantics and were
updated rather than kept; that is a behaviour change, not a test fix.

---------

Co-authored-by: Paul Nothaft <[email protected]>
2026-08-20 14:08:07 +02:00
..

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

  1. For Unit Tests: No special setup required, all dependencies are mocked.

  2. 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"
    
  3. 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:

  1. Add unit tests to backupService.enhanced.test.js
  2. Add integration tests to backup-s3.test.js if S3-specific
  3. Update manual test script for comprehensive testing
  4. Ensure mocks are properly configured
  5. Document any new environment requirements