Docs Test Case Attachments

Test Case Attachments

TCM attachments are files bound to a test case, a step, or a case run — typically
screenshots, traces, HARs, or PDFs. Uploads stream directly to the configured blob
backend without buffering the whole payload in memory, and a per-namespace quota keeps
storage use predictable.

About URLs in examples: all examples use localhost:5770 as the default Mockarty address. If your instance runs on a remote server, replace localhost:5770 with its actual address. See Tips & Useful Features for details.

Related pages: Test Case Management · Review Workflow

Storage backends

Pick one with MOCKARTY_BLOB_BACKEND:

  • fs (default) — local filesystem. Required: MOCKARTY_BLOB_FS_ROOT. Writes are atomic (.tmp + fsync + rename). Layout: <root>/<namespace>/<shard-a>/<shard-b>/<sha256>; a meta/<sha256>.json companion holds metadata.
  • s3 — any S3-compatible bucket (AWS S3, MinIO, Yandex Object Storage, VK Cloud, Cloud.ru). Required: MOCKARTY_BLOB_S3_ENDPOINT, MOCKARTY_BLOB_S3_BUCKET. Optional: MOCKARTY_BLOB_S3_REGION, MOCKARTY_BLOB_S3_ACCESS_KEY, MOCKARTY_BLOB_S3_SECRET_KEY, MOCKARTY_BLOB_S3_PREFIX, MOCKARTY_BLOB_S3_USE_SSL (default true).

Both backends satisfy the same interface — switching from fs to s3 needs only a
restart and env changes; previously uploaded attachments stay in their original backend.

Upload pipeline

  1. Client POSTs multipart/form-data with a file field to /api/v1/namespaces/:ns/tcm/attachments/upload?parentKind=<kind>&parentId=<id>.
  2. The server spools the body straight to disk under an io.LimitReader (default cap 25 MiB; +1 byte lets the server distinguish “exactly at cap” from “over cap”).
  3. Quota pre-check uses the actual spooled bytes (not the declared Content-Length).
  4. Image pipeline: PNGs ≥ 512 KiB are re-encoded to JPEG q=85; a 256×256 thumbnail is generated for any image.
  5. Backend writes the final blob and a metadata row.
  6. Response — the persisted Attachment with its UUID, MIME, size, sha256, and storage key.

Peak RAM per upload is bounded by the image decoder’s working set, not by the payload
size — 1 000 concurrent 25 MiB uploads do not imply 75 GiB resident.

Parent kinds

parentKind Attached to
tcm_case Case (shared across versions — rolling back to v3 still sees v5’s attachments).
tcm_case_step Single step (per-version; rollback brings the step’s original attachments).
tcm_case_run Run-level artefact (logs, final reports).
tcm_case_run_step Step evidence captured during resolution.
review_comment File attached to a review comment.

Endpoints

Method Path Purpose
POST /tcm/attachments/upload?parentKind=&parentId= Multipart upload.
GET /tcm/attachments?parentKind=&parentId= List with pagination.
GET /tcm/attachments/:id Metadata only.
GET /tcm/attachments/:id/raw Stream the blob body. RFC 6266 Content-Disposition with filename*=UTF-8''….
GET /tcm/attachments/:id/thumb 256×256 JPEG thumbnail (404 if the source isn’t an image).
GET /tcm/attachments/:id/view Inline display for the report viewer. On an S3/MinIO backend this 302-redirects to a short-lived presigned URL so the bytes (and video range/seek requests) come straight from object storage; on the filesystem backend it streams the body. Renders inline only for images, video, audio, PDF and plain text — anything else (incl. SVG/HTML) is sent as a download.
POST /tcm/attachments/presign Request a presigned direct-to-storage upload for a large artefact (video, full-page screenshot, trace bundle). Body: {parentKind, parentId, filename, contentType, sha256, size}. Returns {uploadUrl, uri, method, expiresAt, maxBytes}. On the filesystem backend (which can’t presign) returns 501 with {"fallback":"/upload"} — clients then use the multipart /upload.
POST /tcm/attachments/confirm Record the metadata row after the client PUT the bytes to the presigned URL. Body: {parentKind, parentId, filename, contentType, sha256}. The storage location is re-derived server-side from (namespace, sha256) — the client’s claimed URI is never trusted; 409 if the bytes weren’t uploaded yet.
PUT /tcm/attachments/:id/content Overwrite an attachment’s content in place (the request body IS the new content; same attachment id — existing links keep working). Text types (text/plain, Markdown, JSON, XML, CSV, YAML, HTML) are edited from the text viewer (5 MiB cap). Images are replaced by an annotated version (Content-Type: image/png, 15 MiB cap) — the row’s media type + dimensions update to the annotated PNG. Other types return 415.
PATCH /tcm/attachments/:id Rename (display-only). Body: {"originalName": "..."}. Storage key, MIME, dimensions and existing /raw / /thumb URLs stay valid.
DELETE /tcm/attachments/:id Soft-delete; async cleanup removes the blob body after retention expires.

Viewing & editing in the UI

Click any attachment in a case or run report to open it in a preview modal — no
download round-trip, no leaving the page:

  • Images, video, audio, PDF render inline. With an S3/MinIO backend the
    player streams straight from object storage (video seeking works), so the
    admin node never proxies the bytes.
  • Markdown is rendered formatted; text, logs, JSON, CSV, XML, YAML show
    in a monospace viewer.
  • Word (.docx) and Excel (.xlsx) display read-only (Word as formatted
    HTML, Excel as per-sheet tables). These are view-only — to change them,
    re-upload an edited file.
  • Text artefacts can be edited in place. Open a text/Markdown/JSON/CSV/…
    attachment, press Edit, change the content, and Save — the same
    attachment is updated without a re-upload, so links to it keep working.
  • Screenshots can be annotated. Open an image, press Annotate, and draw
    arrows, rectangles, ellipses, highlights, freehand or text on it. Save as
    copy
    keeps the original and adds the marked-up version; Replace original
    overwrites the same attachment. Annotated bug screenshots then appear in the
    run report you ship to developers. (On an S3/MinIO backend the bucket needs
    CORS configured so the editor can read the image bytes; the filesystem
    backend is same-origin and needs no extra setup.)
  • Large files (over 15 MiB) show a download button instead of an inline preview
    to keep the browser responsive.

Direct-to-storage upload (large artefacts)

For large files (recordings, videos) on an S3/MinIO backend, clients upload the
bytes directly to object storage so they never pass through the admin node:

  1. POST /tcm/attachments/presign with the file’s sha256 + size → a
    presigned PUT URL.
  2. PUT the bytes to that URL (the client streams from disk; supports retries).
  3. POST /tcm/attachments/confirm to record the attachment.

mockarty-cli attachments upload <file> --parent-kind … --parent-id … and the
runners use this path automatically, falling back to the multipart /upload
when the backend is the filesystem.

Quotas & limits

  • Per-namespace quota: a storage cap per namespace, admin-settable via Settings → Storage → Attachments. When hard enforcement is enabled, uploads past the cap return 409 Conflict.
  • Scan failures: surface as tcm.attachment.scan_failed events and are recorded in the audit log; the blob is not persisted.
  • MIME whitelist: default includes PNG, JPEG, WebP, GIF, PDF, JSON, XML, plain text, CSV, ZIP, HAR, and application/octet-stream. Extend via adapter configuration if needed. Uploads of non-whitelisted MIME types return 415 Unsupported Media Type.

Deletion & retention

  • Soft-delete records who deleted the attachment, when, and why. The metadata is kept for the retention window so restores are possible.
  • The blob body itself is deleted asynchronously; in-flight body deletions are allowed to finish during graceful shutdown before the process exits.
  • Legal-hold blocks hard deletion until the hold is released, even past retention.

Air-gapped notes

The image pipeline is pure Go — no external codecs. The S3 backend uses minio-go which
has no C dependencies, so the same container image runs air-gapped against MinIO /
Yandex Object Storage. The optional virus scanner is the only component that reaches
attachments fully offline.

External-run attachment offload

The CI ingest endpoint /api/v1/namespaces/:ns/tcm/external-runs accepts inline
base64-encoded attachments alongside each run. Small payloads (logs, short JSON
reports) stay inline so case context stays self-contained; large screenshots or
HARs can grow case_context_json past the JSON depth budget on Postgres and
slow case-run listings.

Toggle inline → blob offload with MOCKARTY_EXTERNAL_RUN_BLOB:

Value Behaviour
(unset) or inline Keep attachments inline (default). No blob backend dependency. Right for single-node and small-team deployments.
auto / fs / s3 Stream every inline attachment into the configured blob backend (MOCKARTY_BLOB_BACKEND) and replace the body with a blobUri reference. The hint (fs / s3) is descriptive; the actual backend is picked by MOCKARTY_BLOB_BACKEND.

When offload is enabled the run row carries only the URI; the UI fetches the
artefact on demand via /tcm/attachments/:id/raw. Inline storage stays as a
fallback — if the backend is temporarily unreachable the upload still records
the run with the body inline rather than dropping the result entirely.