Agora

Testing

Drive the media library in tests with the in-memory doubles from @adonis-agora/media/testing — InMemoryMediaStore, InMemoryDisk + inMemoryDiskResolver, InMemoryUploadSessionStore, FakeImageProcessor and FakeTransformer — no disk, no database, no sharp, no media engine.

Every seam the library depends on — the disk, the store, the image processor — is an interface, so testing is just a matter of supplying fast, deterministic doubles. The @adonis-agora/media/testing subpath ships one for each, so you can exercise the full library with no filesystem, no database, and no sharp.


Install

The helpers ship inside the core package as the @adonis-agora/media/testing subpath — nothing extra to install:

npm i @adonis-agora/media
import {
  InMemoryMediaStore,
  InMemoryDisk,
  inMemoryDiskResolver,
  FakeImageProcessor,
} from '@adonis-agora/media/testing'

The doubles

DoubleImplementsWhat it does
InMemoryMediaStoreMediaStoreMap-backed metadata store.
InMemoryDiskDiskMap-backed disk; exposes .files for assertions.
inMemoryDiskResolver(names)DiskResolver factoryBuilds a resolver over named in-memory disks; returns { resolve, disks }.
FakeImageProcessorImageProcessorDeterministic, sharp-free; records every convert call on .calls.
InMemoryUploadSessionStoreUploadSessionStoreMap-backed resumable-upload session store; drives TUS / multipart tests with no database. Non-durable (sessions vanish on restart) and returns defensive copies.
FakeTransformerTransformerDeterministic, engine-free transformer; writes exactly the artifacts it was configured with and records every context on .calls.
DiskVisibilitytype'public' | 'private' | 'unknown' — what an in-memory disk reports from getVisibility.

FakeTransformer

The transformer counterpart of FakeImageProcessor, and the reason you can test a transformer pipeline without a media engine anywhere near it:

import { FakeTransformer } from '@adonis-agora/media/testing'

const hls = new FakeTransformer({
  name: 'hls',
  eager: false,
  artifacts: { 'index.m3u8': '#EXTM3U', 'segment-0.ts': 'bytes' },
  meta: { durationSeconds: 12 },
})

// metadata-only, like a probe: no artifact at all
const probe = new FakeTransformer({ name: 'probe', entry: null, meta: { width: 1920 } })
OptionWhat
nameThe conversion name it produces.
eagerRun inside attach instead of on transform(). Default false.
artifactsPrefix-relative path → content, each written through context.write.
entryThe declared entry. Defaults to the first artifact; pass null for a metadata-only result.
metaThe metadata the result declares.
behaviorFull override — throw from here to assert rollback, or return a hand-built result.

.calls collects every TransformerContext it received, so you can assert when it ran, not only what it produced — which is how you pin eager-vs-deferred behaviour. And because it writes through the real context.write, the library's own artifact bookkeeping (the persisted files list, deletion of the whole package) is genuinely exercised rather than mocked away.

Apps benefit too: registering a FakeTransformer under the name your real one uses lets you test your transcode job's orchestration — dispatch, retry, idempotency — without ever touching a video codec.

Disk visibility

InMemoryDisk and inMemoryDiskResolver both take a DiskVisibility as their last argument, defaulting to 'private'. It exists to test the one branch that depends on it — delivery.mode: 'auto', which asks the disk whether an object is public:

const publicDisks = inMemoryDiskResolver(['cdn'], 'public')    // auto ⇒ public URL
const privateDisks = inMemoryDiskResolver(['s3'], 'private')   // auto ⇒ signed URL
const oldDisks = inMemoryDiskResolver(['legacy'], 'unknown')   // no getVisibility at all

'unknown' is the interesting one: it builds a disk with no getVisibility method, which is exactly the shape of a minimal disk that cannot answer the question. That is how you assert auto degrades to signed rather than assuming public — a fallback that is easy to get backwards and impossible to test with a disk that always answers.


Wiring a MediaLibrary by hand

The most direct setup constructs a MediaLibrary from the doubles — no container, no provider:

import { MediaLibrary, StorageManager } from '@adonis-agora/media'
import {
  InMemoryMediaStore,
  inMemoryDiskResolver,
  FakeImageProcessor,
} from '@adonis-agora/media/testing'

const { resolve, disks } = inMemoryDiskResolver(['fs'])

const library = new MediaLibrary({
  storage: new StorageManager({ default: 'fs', resolve }),
  store: new InMemoryMediaStore(),
  imageProcessor: new FakeImageProcessor(),
  collections: [
    { name: 'gallery', conversions: [{ name: 'thumb', width: 200, eager: false }] },
  ],
})

inMemoryDiskResolver(['fs']) makes one disk named fs (the first name is the default; a single unnamed disk is named default) and hands you both the resolver and the disks map so you can assert on what was written.

Deterministic ids and clock

MediaLibrary accepts an idGenerator and a clock for fully deterministic tests: new MediaLibrary({ ..., idGenerator: () => 'fixed-id', clock: () => new Date(0) }). The same options exist on AttachmentManager (idGenerator).


Asserting on storage

InMemoryDisk keeps written files on its .files map (keyed by storage path), so you can assert that bytes landed where you expect:

import { test } from '@japa/runner'

test('attach writes the original to the disk', async () => {
  const m = await library.attach({
    ownerType: 'Post', ownerId: '1', collection: 'gallery',
    fileName: 'photo.jpg', mimeType: 'image/jpeg', contents: Buffer.from('bytes'),
  })

  assert.isTrue(disks.fs.files.has(m.path))
  assert.equal(m.path, `Post/1/gallery/${m.id}/photo.jpg`)
})

Asserting eager vs lazy conversions

FakeImageProcessor never decodes an image — it returns a tiny synthetic buffer tagged with the preset and pushes a { inputSize, preset } entry onto .calls. That makes the eager/lazy behaviour easy to assert:

test('lazy conversions run on first url(), then cache', async () => {
  const processor = new FakeImageProcessor()
  const library = new MediaLibrary({
    storage: new StorageManager({ default: 'fs', resolve }),
    store: new InMemoryMediaStore(),
    imageProcessor: processor,
    collections: [{ name: 'gallery', conversions: [{ name: 'thumb', width: 100 }] }],
  })

  const m = await library.attach({
    ownerType: 'Post', ownerId: '1', collection: 'gallery',
    fileName: 'p.jpg', mimeType: 'image/jpeg', contents: Buffer.from('x'),
  })

  assert.lengthOf(processor.calls, 0) // lazy: nothing converted on attach

  await library.url(m.id, 'thumb')
  assert.lengthOf(processor.calls, 1) // generated on first url()
  assert.equal(processor.calls[0].preset.name, 'thumb')

  await library.url(m.id, 'thumb')
  assert.lengthOf(processor.calls, 1) // cached: not converted again
})

Flip the preset to eager: true and assert processor.calls has length 1 immediately after attach.


Testing column attachments

The same doubles drive the AttachmentManager:

import { AttachmentManager, StorageManager } from '@adonis-agora/media'
import { inMemoryDiskResolver, FakeImageProcessor } from '@adonis-agora/media/testing'

const { resolve, disks } = inMemoryDiskResolver(['fs'])
const attachments = new AttachmentManager({
  storage: new StorageManager({ default: 'fs', resolve }),
  imageProcessor: new FakeImageProcessor(),
})

const att = await attachments.createFromFile(
  { fileName: 'a.png', mimeType: 'image/png', contents: Buffer.from('x') },
  { variants: [{ name: 'thumb', width: 50 }] },
)

assert.deepEqual(Object.keys(att.variants), ['thumb'])
assert.isTrue(disks.fs.files.has(att.path))

Inside a real app: drive.fake()

If you're testing through the actual container and provider (an HTTP test, say), you usually don't need the doubles at all — let the real wiring stand and swap only the disk. @adonisjs/drive ships drive.fake(), which backs your disks with an in-memory fake while keeping the rest of the media stack intact:

import drive from '@adonisjs/drive/services/main'

test.group('uploads', (group) => {
  group.each.setup(() => {
    const fake = drive.fake()
    return () => drive.restore(fake)
  })

  test('stores an avatar', async ({ client }) => {
    await client.post('/avatar').file('image', pngBuffer)
    // assert via the MediaManager resolved from the container
  })
})

This keeps your config-driven store (in-memory or Lucid) and the real sharp processor in play, faking only storage — the closest test to production.

For pure unit tests prefer the hand-wired doubles (faster, no app boot); for integration tests prefer drive.fake() over the real container. Pick the layer that matches what you're verifying.


Next steps

On this page