s3nd

Testing

Test the code around s3nd without credentials, without network.

A bucket in memory

The highest-value stand-in is a fake S3Client that actually stores what it is given, because then a snapshot genuinely round-trips — including the conditional headers, which is where the interesting bugs live:

import type { S3Client } from '@aws-sdk/client-s3'
import { createBucket } from 's3nd'

export function memoryBucket() {
  const objects = new Map<string, { body: Uint8Array; etag: string }>()
  let counter = 0

  const client = {
    send: async (command: any) => {
      const { Key, Body, IfMatch, IfNoneMatch } = command.input
      const stored = objects.get(Key)

      switch (command.constructor.name) {
        case 'PutObjectCommand': {
          if (IfNoneMatch === '*' && stored) throw precondition()
          if (IfMatch != null && (!stored || IfMatch !== `"${stored.etag}"`)) throw precondition()

          counter += 1
          objects.set(Key, { body: Buffer.from(Body), etag: `etag-${counter}` })

          return { ETag: `"etag-${counter}"` }
        }

        case 'GetObjectCommand': {
          if (!stored) throw notFound()

          return {
            Body: {
              transformToByteArray: async () => stored.body,
              transformToString: async () => Buffer.from(stored.body).toString('utf8'),
            },
            ETag: `"${stored.etag}"`,
            ContentLength: stored.body.byteLength,
          }
        }

        case 'DeleteObjectCommand':
          objects.delete(Key)
          return {}

        default:
          throw new Error(`Unexpected command: ${command.constructor.name}`)
      }
    },
  } as unknown as S3Client

  return createBucket({ bucket: 'test', client })
}

const precondition = () =>
  Object.assign(new Error('precondition failed'), {
    name: 'PreconditionFailed',
    $metadata: { httpStatusCode: 412 },
  })

const notFound = () =>
  Object.assign(new Error('no such key'), { name: 'NoSuchKey', $metadata: { httpStatusCode: 404 } })

With that, the tests read like the feature:

it('carries the database to the other device', async () => {
  const bucket = memoryBucket()

  const code = store.codes.create()
  await store.putSnapshot(code, { notes: [{ id: 1, title: 'Roof' }] }, { version: 3 })

  const restored = await store.getSnapshot(store.codes.normalize(code.toLowerCase()), { maxVersion: 3 })

  expect(restored?.data).toEqual({ notes: [{ id: 1, title: 'Roof' }] })
})

it('refuses a write when another device got there first', async () => {
  const bucket = memoryBucket()

  const first = await store.putSnapshot('user-42', { notes: [] })
  await store.putSnapshot('user-42', { notes: ['from the phone'] })

  await expect(
    store.putSnapshot('user-42', { notes: ['from the laptop'] }, { ifMatch: first.etag }),
  ).rejects.toMatchObject({ code: 'PRECONDITION_FAILED' })
})

Testing expiry

Expiry is decided from the envelope's expiresAt against the current time, so fake timers are enough — no waiting, no sleeping:

it('stops answering once the code expires', async () => {
  vi.useFakeTimers()
  vi.setSystemTime(new Date('2026-08-27T12:00:00Z'))

  const bucket = memoryBucket()
  await store.putSnapshot('K7QP2M4X', { notes: [] }, { expiresIn: 3600 })

  vi.setSystemTime(new Date('2026-08-27T13:00:01Z'))

  expect(await store.getSnapshot('K7QP2M4X')).toBeNull()
})

A recording stub is enough for simpler cases

When you only want to assert what was sent, a stub that records commands does the job:

const calls: any[] = []

const client = {
  send: async (command: any) => {
    calls.push(command)
    return { ETag: '"stub"' }
  },
} as unknown as S3Client

It covers putSnapshot, upload, put and delete. It does not cover getUrl(), which signs with the real SDK.

Testing signed URLs

Signing is a local computation: it needs credentials, but no network. Pass static ones:

const store = createBucket({
  bucket: 'test',
  region: 'eu-west-3',
  credentials: {
    accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
    secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
  },
})

const url = new URL(await store.getUrl('a.txt', { expiresIn: 60 }))

expect(url.searchParams.get('X-Amz-Expires')).toBe('60')
expect(url.searchParams.get('X-Amz-Signature')).toBeTruthy()

Against a real bucket

An in-memory double cannot catch what only the real protocol does: checksum headers, path-style addressing, and whether your provider actually honours If-Match on PutObject — support for that arrived late, and not every S3-compatible service has it. Run MinIO and point an example at it; see S3-compatible providers for the container command.

On this page