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:5770as the default Mockarty address. If your instance runs on a remote server, replacelocalhost:5770with 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>; ameta/<sha256>.jsoncompanion 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(defaulttrue).
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
- Client POSTs
multipart/form-datawith afilefield to/api/v1/namespaces/:ns/tcm/attachments/upload?parentKind=<kind>&parentId=<id>. - 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”). - Quota pre-check uses the actual spooled bytes (not the declared
Content-Length). - Image pipeline: PNGs ≥ 512 KiB are re-encoded to JPEG q=85; a 256×256 thumbnail is generated for any image.
- Backend writes the final blob and a metadata row.
- Response — the persisted
Attachmentwith 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:
POST /tcm/attachments/presignwith the file’ssha256+size→ a
presignedPUTURL.PUTthe bytes to that URL (the client streams from disk; supports retries).POST /tcm/attachments/confirmto 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_failedevents 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 return415 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.