Files
picpeak/backend/__tests__
Paul NothaftandPaul Nothaft 849a5807b7 fix(admin): make "Storage used" report storage used (#1164) (#1170)
* fix(admin): make "Storage used" report storage used (#1164)

The tile summed photos.size_bytes — the catalogued size of the ORIGINALS,
which has no relationship to the disk PicPeak runs on. In reference mode those
files are never copied and sit on the NAS; duplicate rows counted the same
file twice (#1162); and it ignored everything PicPeak genuinely does write
locally: thumbnails, previews, hero renditions, watermarks and the per-event
download cache. The reporter's tile read ~80 GB against 21 GB of real usage.

Worse than the label: the same number drove the storage soft-limit warning bar
and, via /storage/info, the recommended soft limit — so a reference-mode
install got a disk-capacity recommendation computed from bytes that are not on
the disk.

- new localStorageUsage service walks the storage root and reports the total
  plus a breakdown. Walking rather than summing DB columns is the point:
  thumbnail/preview/hero rows record a key and never a byte count, and orphans
  from a deleted event or an interrupted import are real bytes. Symlinks are
  not followed, so a link into the media mount cannot put the NAS back in the
  total. Cached for 5 minutes, since the dashboard polls.
- the dashboard tile and /storage/info now report that, with the catalogued
  figure kept and labelled as such next to it. A failed measurement reads as
  "unavailable" rather than substituting a number that means something else.

On the local rig: 64.37 MB used against 15.75 MB catalogued, of which 27.9 MB
is watermarks and 6.8 MB is download cache — none of which the old figure
could see.

Not addressed here: `.download-cache/all.zip` still has no TTL or size cap. It
is now at least visible in the breakdown, which is what makes the case for
capping it.

* fix(admin): exclude the media share from local storage usage (#1164)

External review found the walk could reintroduce the exact over-count it
replaces.

EXTERNAL_MEDIA_ROOT's compose default is `<storage>/external-media`, where the
NAS is bind-mounted. That is a plain directory, not a symlink, so the symlink
guard did not cover it and the walk descended into the share — putting every
referenced original back into a figure whose whole purpose is to leave them
out, and comparing NAS bytes against statfs() of the local disk. On the
reference-mode installs this issue is about, that is the failure mode
reappearing inside its own fix.

The configured root is now skipped when it lies inside the storage root, and
the result reports which path was excluded. A directory that merely shares the
name is still counted, because those really are local bytes.

Also from the review:

- concurrent cold-cache callers now share one walk. /dashboard/stats,
  /storage/info and the sidebar are routinely requested together, and each was
  starting its own stat-per-file traversal of the whole library.
- storage_partial is surfaced in the StorageInfo type and the sidebar tile, not
  just the dashboard and analytics cards. An unreadable subtree makes the total
  a floor, and a floor silently compared against a soft limit reads as "safely
  under".

* fix(admin): do not report a disk walk on an S3 backend (#1164)

Second review round.

S3 installs were regressed. With STORAGE_BACKEND=s3 the originals, renditions,
archives and download caches are objects in the bucket and STORAGE_PATH holds
only incidental local files — so the walk reported near-zero and the soft-limit
recommendation was derived from it. Those installs now keep the catalogued
figure, which is the approximation they had before this PR, and the response
says which measurement it is (`storage_measurement: 'disk' | 'catalog'`) so the
UI labels it instead of implying a disk measurement that never happened.

The Settings → Status storage card ignored storage_partial, formatting a lower
bound as exact and deriving the limit percentage from it — so an unreadable
subtree could read as safely under the limit. It now carries the same `+`
marker as the sidebar and dashboard.

* fix(admin): stop rendering an absent measurement as zero usage (#1164)

Third review round, two findings.

The analytics storage bar coerced a null measurement to 0, drawing an empty
bar labelled "0% of limit" and suppressing the over-limit state — reading as
plenty of room at exactly the moment nothing is known. It now shows the
catalogued figure on S3, where that IS the available answer, and says "no
measurement available" rather than inventing a percentage when there is none.

/storage/info walked the filesystem before checking the backend and then threw
the result away on S3. The sidebar polls that endpoint, so a migrated install
still holding a large local tree paid a full stat-per-file traversal on every
cold cache for nothing. Gated before the walk, as the dashboard route already
was.

* fix(admin): tell "no disk to measure" apart from "the measurement failed" (#1164)

External review of the stable twin.

Both were reported as `storage_measurement: 'catalog'`, so a failed local walk
made the dashboard claim the objects live in S3. They are different things —
one is a fact about the install, the other is a fault — and there is now an
`unavailable` state for the second.

The analytics percentage could reach the billions. `safeSoftLimit` fell back to
`storageUsed || 1`, and on S3 that is null → 1, while the figure beside it came
from `catalogedBytes`. An editor or viewer holds `analytics.view` but not
`settings.view`, so `/storage/info` 403s for them and `storageInfo` is
undefined — which is exactly when that fallback fires. It now falls back to the
measured figure, and suppresses the percentage entirely when there is no real
limit rather than dividing usage by itself and always reading 100%.

Also lands the AnalyticsPage half of the previous round, which the commit
message claimed but the commit did not contain — only its backend counterpart
was staged. The stable twin has carried it since it was written, so this is the
parity gap in the unusual direction.

---------

Co-authored-by: Paul Nothaft <[email protected]>
2026-08-26 08:53:39 +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