v0.3.2: add GET /api/artifacts/{id}/raw for content read-back
Metadata (GET /api/artifacts/{id}) stays metadata-only; the full content now
has its own endpoint so an agent can verify exactly what it posted.
- New ArtifactRawView at /api/artifacts/{id}/raw: bearer-or-session auth,
owner-scoped. Serves exact source text (markdown/html_full/json/csv), file
bytes (image/pdf/binary), or 404 for render-only kinds (html_fragment/deck).
Reuses raw_artifact_path/safe_artifact_path.
- serialize_artifact exposes api_raw_url; /api/capabilities advertises the
route. (The old /a/{id}?raw= viewer stays session-only for browsers.)
- Docs + tests (exact content, owner-scoping, 403 unauth, 404 render-only).
Suite: 136 passing; verified live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
2b901f7844
commit
f6f004c689
6 changed files with 102 additions and 15 deletions
|
|
@ -253,6 +253,46 @@ class ArtifactAPITestCase(APITestCase):
|
|||
response = self.client.get(reverse("artifact-detail-api", args=[artifact_id]))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
# -- raw content (GET {id}/raw) ---------------------------------------
|
||||
|
||||
def _raw_body(self, response):
|
||||
return b"".join(response.streaming_content).decode()
|
||||
|
||||
def test_raw_returns_exact_content_with_bearer(self):
|
||||
create = self.create_artifact(content="# Exact\n\nverify me")
|
||||
artifact_id = create.data["id"]
|
||||
# metadata detail advertises the raw endpoint
|
||||
self.assertTrue(create.data["api_raw_url"].endswith(f"/api/artifacts/{artifact_id}/raw"))
|
||||
|
||||
response = self.client.get(
|
||||
reverse("artifact-raw-api", args=[artifact_id]), **self.auth_headers(self.token)
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response["Content-Type"], "text/plain; charset=utf-8")
|
||||
# Content round-trips faithfully; normalize newlines since the stored
|
||||
# sidecar is written in the host's text mode (CRLF on Windows dev, LF on
|
||||
# the Linux deploy).
|
||||
self.assertEqual(self._raw_body(response).replace("\r\n", "\n"), "# Exact\n\nverify me")
|
||||
|
||||
def test_raw_requires_auth(self):
|
||||
artifact_id = self.create_artifact().data["id"]
|
||||
response = self.client.get(reverse("artifact-raw-api", args=[artifact_id]))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_raw_owner_scoped(self):
|
||||
artifact_id = self.create_artifact().data["id"]
|
||||
response = self.client.get(
|
||||
reverse("artifact-raw-api", args=[artifact_id]), **self.auth_headers(self.other_token)
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_raw_not_available_for_render_only_kind(self):
|
||||
artifact_id = self.create_artifact(kind="html_fragment", content="<p>hi</p>").data["id"]
|
||||
response = self.client.get(
|
||||
reverse("artifact-raw-api", args=[artifact_id]), **self.auth_headers(self.token)
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_patch_updates_title_and_tags(self):
|
||||
artifact_id = self.create_artifact().data["id"]
|
||||
response = self.client.patch(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue