Testing

Prev Next

Testing

The Upload Engine test suite validates infrastructure correctness — lifecycle, concurrency, persistence, and retry behavior. All tests run with Jest. No test depends on UI presence.

Run Tests

# All Upload Engine tests
npm test -- src/__tests__/lib/internal/UploadEngine/UploadEngine.test.ts

# Run in band (for debugging)
npm test -- src/__tests__/lib/internal/UploadEngine/UploadEngine.test.ts --runInBand

# Individual test file
npm test -- src/__tests__/lib/internal/UploadEngine/UploadWorker.test.ts

Test Suite Inventory

File Tests What It Validates
UploadEngine.test.ts 10 Engine orchestration, pause/resume persistence, rehydration, parallel flag propagation
UploadSession.test.ts 9 State transitions, event emission, illegal transition rejection, internalFlags
UploadWorker.test.ts 12 Chunk execution, pause/abort control flow, retry+recovery, parallel vs sequential, PARALLEL_CHUNK_LIMIT
UploadQueue.test.ts 2 Concurrency enforcement, remove() behavior
UploadPersistence.test.ts 6 Save/load, tenant filtering, SSR safety, malformed JSON handling
RetryPolicy.test.ts 3 Retry bounds, linear backoff calculation, retryable error detection
Total 42

Key Test Scenarios

UploadEngine.test.ts (10 tests)

Start with defaults:

  • Creates session with correct uploadId format
  • Applies default chunk size (5 MB) when no override provided
  • Enqueues upload into queue immediately

Pause + persistence:

  • pause() calls worker.requestPause() + session.pause()
  • Persists state to sessionStorage via UploadPersistence.save()
  • Frees queue slot via queue.finish()

Resume:

  • Re-enqueues session into UploadQueue
  • Emits UPLOAD_RESUMED immediately
  • Worker's initialize() detects uploadServiceData → takes resume path

Abort + cleanup:

  • Calls worker.abort() + session.abort()
  • handleEvent(UPLOAD_ABORTED) removes persistence + deletes session
  • Session no longer appears in list()

Rehydration filtering:

  • Only paused and needs-attention states are rehydrated
  • uploading state in persistence is filtered out (safety)
  • Missing tenant cookies → no rehydration attempt

enableParallelChunkProcessing flag:

  • Propagated from StartUploadOptionsUploadSession.internalFlags
  • Falls back to Config.UploadEngine.enableParallelChunkUpload
  • Preserved through session lifecycle and passed to worker

UploadSession.test.ts (9 tests)

Lifecycle events:

  • startInitialization() emits UPLOAD_STARTED
  • pause() emits UPLOAD_PAUSED
  • complete(fileUrl) emits UPLOAD_COMPLETED with fileUrl
  • abort() emits UPLOAD_ABORTED

State machine enforcement:

  • fail() transitions to needs-attention, emits UPLOAD_NEEDS_ATTENTION
  • abort() from terminal state → idempotent (no-op, no event)
  • Illegal transition (e.g., idle → completed) → throws [UploadStateMachine] Illegal state transition

internalFlags:

  • Accepted in constructor → stored in snapshot
  • Available via getSnapshot().internalFlags
  • Preserved through rehydrate()

UploadWorker.test.ts (12 tests)

Happy path:

  • initialize()uploadAllWindows()completeMultipart()runCompletionHandler()
  • Progress emitted per chunk completion
  • completedParts sorted by PartNumber before completion

Pause without persisting:

  • requestPause() sets flag + aborts controller
  • Worker catches PauseError → returns silently
  • No persistence write (engine handles that)

Abort:

  • abort() triggers controller abort signal
  • Worker catches abort → calls session.abort()

Chunk retry + recovery:

  • Non-retryable presigned URL error → throws (no retry)
  • Chunk upload failure → retries up to 3x with 500ms linear backoff
  • markRetrying()startUploading() cycle during retries
  • If all retries exhausted → session.fail('upload', ...) + persist

Completion failure:

  • completeMultipartUpload() failure → 2 retries at 1000ms
  • If exhausted → session.fail('completion', ...) + persist (doesn't throw)

Part tracking (Set + Map):

  • completedPartNumbers: Set<number> — skip already-completed chunks on resume
  • completedPartMap: Map<number, string> — PartNumber → ETag mapping
  • Resume path: both populated from snapshot.completedParts

Parallel vs sequential:

  • Sequential (default): for-of loop over chunks
  • Parallel (enableParallelChunkProcessing): Promise.race() sliding window
  • PARALLEL_CHUNK_LIMIT = 5 enforced — validated that no more than 5 concurrent fetches

Sorted completedParts:

  • Array.from(completedPartMap.entries()).sort(([a], [b]) => a - b) before setCompletedParts()
  • Same sort applied before completeMultipartUpload() call

UploadQueue.test.ts (2 tests)

Concurrency enforcement:

  • With limit=5, 6th upload stays in pending until one finishes
  • finish() triggers maybeStartNext() → drains pending

remove() behavior:

  • Removes from pending if not yet started
  • If active: deletes from active + calls onFinish + starts next

UploadPersistence.test.ts (6 tests)

Save/load round-trip:

  • save() writes to sessionStorage keyed by Keys.UploadPersistenceKey
  • loadAll() returns all persisted states
  • loadForTenant(site, siteId) filters by tenant

Remove + clearAll:

  • remove(key) deletes single entry
  • clearAll() removes entire storage key

SSR safety:

  • All methods guard typeof window === 'undefined' — return [] or no-op
  • No throws in SSR context

Malformed JSON:

  • read() catches JSON.parse failures → returns {}
  • Corrupted storage doesn't crash the engine

RetryPolicy.test.ts (3 tests)

Retry bounds:

Category Max Retries Base Delay
chunk-upload 3 500ms
presigned-url 2 500ms
complete-upload 2 1000ms
completion-handler 1 0ms

Linear backoff:

  • getRetryDelayMs(category, attempt) = baseDelayMs × attempt
  • Attempt 1 → 500ms, Attempt 2 → 1000ms, Attempt 3 → 1500ms

Retryable error detection:

  • TypeError → retryable (network/fetch errors)
  • HTTP 500+ → retryable
  • HTTP 408, 429 → retryable
  • HTTP 400, 403, 404 → not retryable

Test Architecture Diagram

Image

  • Unit tests validate individual modules in isolation (mocked dependencies)
  • Integration tests validate cross-module behavior (engine→worker→session→retry)

What Is Intentionally Not Tested

Concern Why
UI rendering Infrastructure layer — no React dependency
Route changes Upload correctness is UI-independent
Backend cleanup Storage hygiene is a backend concern
Cross-browser Validated at integration/E2E level
UploadService network calls Mocked — real HTTP validated in E2E

Adding New Tests

  • New invariants must be accompanied by tests
  • Tests must target infrastructure behavior, not UI
  • Failure cases must be tested explicitly — no "assume success" patterns
  • Use the existing mock patterns (mock UploadService, CookieManager, UploadPersistence)