dánwòdánwò

Storage

S3 and Tigris

S3 object storage for apps on AWS S3 or Tigris, with the AWS SDK and the Tigris SDK, presigned URLs, multipart uploads and signature checks.

danwo's S3 emulator answers Tigris's and AWS's S3 endpoints, path-style and virtual-hosted. Both @aws-sdk/client-s3 and @tigrisdata/storage work against it unchanged.

Set up

Add s3Service with your bucket, and hand the app the variables env() gives:

danwo.config.ts
import { appEnvironment, defineConfig, s3Service } from "@danwo/sdk"

export default defineConfig({
  testDir: "tests",
  environment: () => {
    const s3 = s3Service({ seed: { buckets: [{ name: "avatars" }] } })

    return appEnvironment({
      app: { command: "npm run dev", url: "http://localhost:3000", env: s3.env() },
      services: [s3],
    })
  },
})

env() points both SDKs at the emulator with a key it accepts:

VariableFor
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGIONThe AWS SDK's credentials, region auto.
AWS_ENDPOINT_URL_S3, AWS_ENDPOINT_URLWhere the AWS SDK sends S3 calls.
TIGRIS_STORAGE_ACCESS_KEY_ID, TIGRIS_STORAGE_SECRET_ACCESS_KEY, TIGRIS_STORAGE_ENDPOINTThe same, for the Tigris SDK.
TIGRIS_STORAGE_BUCKETThe first seeded bucket.

Pass env({ endpoint }) to point them at another endpoint the emulator answers, such as https://s3.amazonaws.com.

Seed it

danwo.config.ts · the seed
s3Service({
  seed: {
    buckets: [
      {
        name: "avatars",
        acl: "public-read",
        objects: [{ key: "default.png", body: "…", contentType: "image/png" }],
      },
    ],
    credentials: [{ accessKeyId: "tid_danwo", secretAccessKey: "tsec_danwo" }],
  },
})
FieldWhat it seeds
bucketsBuckets by name, with a region, an ACL and objects already in them.
credentialsAccess keys whose signatures are checked. Without any, any well-formed key is accepted.

The S3 seed is a plain object, not a function.

Use it in a test

Upload an avatar, then read what landed in the bucket:

tests/avatar.danwo.ts
import { type S3Fixtures, s3Objects, test as base } from "@danwo/sdk"

const test = base.extend<S3Fixtures>()

test("stores the uploaded avatar", async ({ app, screen, expect, s3 }) => {
  await app.open("/settings")
  await screen.getByLabel("Avatar").setInputFiles("tests/fixtures/ada.png")
  await screen.getByRole("button", { name: "Save" }).click()
  await expect(screen.getByText("Avatar saved")).toBeVisible()

  const [avatar] = s3Objects(s3).list("avatars", "users/")

  expect(avatar?.contentType).toBe("image/png")
  expect(avatar?.size).toBeGreaterThan(0)
})
s3Objects(s3)What it's for
buckets()The buckets.
list(bucket, prefix)The objects in a bucket, under a prefix.
get(bucket, key)One object, with its type, size, metadata and text().

What's covered

AreaCovered
ObjectsPut, read with ranges, copy and delete objects, with ACLs and metadata; browser form uploads.
ListingListings with prefixes, delimiters and pages.
MultipartMultipart uploads, from start to complete or abort.
Presigned URLsPresigned reads and writes, with expiry.
Conditional requestsReads and writes on an ETag or a date.
BucketsCreate, list and delete buckets.
SignaturesSigV4 signatures checked against the seeded keys.
TigrisThe Tigris SDK's presign call, and renames.

Faults

CallFaults the matrix tries
A write, such as PUT /avatars/users/ada.png500, 503, connection reset, answer lost after the object was stored
A read, such as GET /avatars/users/ada.png500, 503, connection reset

Errors are S3's XML, so both SDKs raise the error they would for real.

Fidelity

Live. danwo fidelity s3 runs 7 scenarios against real Tigris storage: objects, listings, multipart, presigned URLs, conditional requests, errors, and Tigris's presign call and renames. All 7 match.

Not covered yet

  • Versioning, lifecycle rules and replication
  • Bucket policies, CORS and website hosting
  • Object tagging and Object Lock
  • Other AWS services: use MiniStack

Next

On this page

Run it on every pull request

A GitHub check and one comment per pull request, with the flight records attached.

Set up CI