Initial public commit
Daybook: a self-hosted, multi-user Django + DRF app where agents, scripts, or people publish artifacts and each logged-in user browses their own time-ordered, label-filterable feed. This squashes prior local development history (kept privately on a separate branch) into a single clean starting point for the public repository. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
commit
940a703cd7
62 changed files with 5755 additions and 0 deletions
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
0
tests/api/__init__.py
Normal file
0
tests/api/__init__.py
Normal file
276
tests/api/test_artifacts.py
Normal file
276
tests/api/test_artifacts.py
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from rest_framework.authtoken.models import Token
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from artifacts.models import Artifact
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class ArtifactAPITestCase(APITestCase):
|
||||
"""Exercises the ingest/list/detail API against a real (temp) filesystem
|
||||
and a real (test) database, the way an agent posting over HTTP would."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.artifacts_root = Path(tempfile.mkdtemp(prefix="daybook-test-artifacts-"))
|
||||
cls.settings_override = override_settings(ARTIFACTS_ROOT=cls.artifacts_root)
|
||||
cls.settings_override.enable()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.settings_override.disable()
|
||||
shutil.rmtree(cls.artifacts_root, ignore_errors=True)
|
||||
super().tearDownClass()
|
||||
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(username="alice", password="pw")
|
||||
self.token = Token.objects.create(user=self.user)
|
||||
self.other_user = User.objects.create_user(username="bob", password="pw")
|
||||
self.other_token = Token.objects.create(user=self.other_user)
|
||||
|
||||
def auth_headers(self, token):
|
||||
return {"HTTP_AUTHORIZATION": f"Bearer {token.key}"}
|
||||
|
||||
def create_artifact(self, token=None, **overrides):
|
||||
token = token or self.token
|
||||
payload = {
|
||||
"source": "test-agent",
|
||||
"title": "Hello World",
|
||||
"kind": "markdown",
|
||||
"content": "# Hello\n\nBody text.",
|
||||
}
|
||||
payload.update(overrides)
|
||||
return self.client.post(
|
||||
reverse("artifact-api"), payload, format="json", **self.auth_headers(token)
|
||||
)
|
||||
|
||||
# -- capabilities -------------------------------------------------
|
||||
|
||||
def test_capabilities_requires_auth(self):
|
||||
response = self.client.get(reverse("capabilities-api"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_capabilities_with_token(self):
|
||||
response = self.client.get(reverse("capabilities-api"), **self.auth_headers(self.token))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("markdown", response.data["artifact_kinds"])
|
||||
|
||||
def test_capabilities_lists_supported_methods(self):
|
||||
response = self.client.get(reverse("capabilities-api"), **self.auth_headers(self.token))
|
||||
methods = response.data["methods"]
|
||||
self.assertEqual(methods["/api/artifacts"], ["GET", "POST"])
|
||||
self.assertEqual(methods["/api/artifacts/{id}"], ["GET", "PATCH", "DELETE"])
|
||||
|
||||
def test_capabilities_with_session(self):
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("capabilities-api"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# -- create (POST) --------------------------------------------------
|
||||
|
||||
def test_create_markdown_artifact(self):
|
||||
response = self.create_artifact(tags=["demo", "weekly"])
|
||||
self.assertEqual(response.status_code, 201)
|
||||
body = response.data
|
||||
self.assertEqual(body["source"], "test-agent")
|
||||
self.assertEqual(body["kind"], "markdown")
|
||||
self.assertEqual(body["title"], "Hello World")
|
||||
self.assertEqual([tag["name"] for tag in body["tags"]], ["demo", "weekly"])
|
||||
|
||||
artifact = Artifact.objects.get(pk=body["id"])
|
||||
self.assertEqual(artifact.owner, self.user)
|
||||
|
||||
def test_create_artifact_writes_files_to_disk(self):
|
||||
response = self.create_artifact()
|
||||
artifact = Artifact.objects.get(pk=response.data["id"])
|
||||
stored_path = self.artifacts_root / artifact.path
|
||||
self.assertTrue(stored_path.exists())
|
||||
# Markdown artifacts also preserve the original .md alongside the rendered .html
|
||||
self.assertTrue(stored_path.with_suffix(".md").exists())
|
||||
|
||||
def test_create_artifact_requires_token(self):
|
||||
response = self.client.post(
|
||||
reverse("artifact-api"),
|
||||
{"source": "test-agent", "title": "x", "kind": "markdown", "content": "hi"},
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_create_artifact_rejects_invalid_kind(self):
|
||||
response = self.create_artifact(kind="pdf")
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_create_artifact_rejects_missing_fields(self):
|
||||
response = self.client.post(
|
||||
reverse("artifact-api"), {"kind": "markdown"}, format="json", **self.auth_headers(self.token)
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
# -- list (GET) -------------------------------------------------------
|
||||
|
||||
def test_list_requires_session_login(self):
|
||||
response = self.client.get(reverse("artifact-api"))
|
||||
self.assertEqual(response.status_code, 302) # redirected to login
|
||||
|
||||
def test_list_scoped_to_owner(self):
|
||||
self.create_artifact()
|
||||
self.create_artifact(token=self.other_token, title="Not mine")
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("artifact-api"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual([a["title"] for a in response.data], ["Hello World"])
|
||||
|
||||
def test_list_filter_by_label(self):
|
||||
self.create_artifact(tags=["urgent"])
|
||||
self.create_artifact(title="Second", tags=["later"])
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("artifact-api"), {"label": "urgent"})
|
||||
self.assertEqual(len(response.data), 1)
|
||||
self.assertEqual(response.data[0]["title"], "Hello World")
|
||||
|
||||
def test_list_filter_by_kind(self):
|
||||
self.create_artifact(kind="markdown")
|
||||
self.create_artifact(title="Frag", kind="html_fragment", content="<p>hi</p>")
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("artifact-api"), {"kind": "html_fragment"})
|
||||
self.assertEqual(len(response.data), 1)
|
||||
self.assertEqual(response.data[0]["title"], "Frag")
|
||||
|
||||
def test_list_filter_by_query(self):
|
||||
self.create_artifact(title="Weekly Report")
|
||||
self.create_artifact(title="Random Notes")
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("artifact-api"), {"q": "weekly"})
|
||||
self.assertEqual(len(response.data), 1)
|
||||
self.assertEqual(response.data[0]["title"], "Weekly Report")
|
||||
|
||||
def test_list_filter_by_day(self):
|
||||
self.create_artifact()
|
||||
today = timezone.localdate().isoformat()
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("artifact-api"), {"day": today})
|
||||
self.assertEqual(len(response.data), 1)
|
||||
|
||||
response = self.client.get(reverse("artifact-api"), {"day": "2000-01-01"})
|
||||
self.assertEqual(len(response.data), 0)
|
||||
|
||||
# -- detail (GET/PATCH) -----------------------------------------------
|
||||
|
||||
def test_detail_get_scoped_to_owner(self):
|
||||
artifact_id = self.create_artifact().data["id"]
|
||||
|
||||
self.client.force_login(self.other_user)
|
||||
response = self.client.get(reverse("artifact-detail-api", args=[artifact_id]))
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("artifact-detail-api", args=[artifact_id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_patch_updates_title_and_tags(self):
|
||||
artifact_id = self.create_artifact().data["id"]
|
||||
response = self.client.patch(
|
||||
reverse("artifact-detail-api", args=[artifact_id]),
|
||||
{"title": "Updated title", "tags": ["a", "b"]},
|
||||
format="json",
|
||||
**self.auth_headers(self.token),
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data["title"], "Updated title")
|
||||
self.assertEqual([t["name"] for t in response.data["tags"]], ["a", "b"])
|
||||
|
||||
def test_patch_rejects_blank_title(self):
|
||||
artifact_id = self.create_artifact().data["id"]
|
||||
response = self.client.patch(
|
||||
reverse("artifact-detail-api", args=[artifact_id]),
|
||||
{"title": ""},
|
||||
format="json",
|
||||
**self.auth_headers(self.token),
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_patch_requires_token(self):
|
||||
artifact_id = self.create_artifact().data["id"]
|
||||
response = self.client.patch(
|
||||
reverse("artifact-detail-api", args=[artifact_id]), {"title": "No token"}, format="json"
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_patch_cannot_touch_other_users_artifact(self):
|
||||
artifact_id = self.create_artifact().data["id"]
|
||||
response = self.client.patch(
|
||||
reverse("artifact-detail-api", args=[artifact_id]),
|
||||
{"title": "Hijacked"},
|
||||
format="json",
|
||||
**self.auth_headers(self.other_token),
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
# -- delete (DELETE) --------------------------------------------------
|
||||
|
||||
def test_delete_removes_artifact_and_files(self):
|
||||
artifact_id = self.create_artifact().data["id"]
|
||||
artifact = Artifact.objects.get(pk=artifact_id)
|
||||
stored_path = self.artifacts_root / artifact.path
|
||||
self.assertTrue(stored_path.exists())
|
||||
|
||||
response = self.client.delete(
|
||||
reverse("artifact-detail-api", args=[artifact_id]), **self.auth_headers(self.token)
|
||||
)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.assertFalse(Artifact.objects.filter(pk=artifact_id).exists())
|
||||
self.assertFalse(stored_path.exists())
|
||||
self.assertFalse(stored_path.with_suffix(".md").exists())
|
||||
|
||||
def test_delete_requires_token(self):
|
||||
artifact_id = self.create_artifact().data["id"]
|
||||
response = self.client.delete(reverse("artifact-detail-api", args=[artifact_id]))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertTrue(Artifact.objects.filter(pk=artifact_id).exists())
|
||||
|
||||
def test_delete_cannot_touch_other_users_artifact(self):
|
||||
artifact_id = self.create_artifact().data["id"]
|
||||
response = self.client.delete(
|
||||
reverse("artifact-detail-api", args=[artifact_id]), **self.auth_headers(self.other_token)
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
self.assertTrue(Artifact.objects.filter(pk=artifact_id).exists())
|
||||
|
||||
def test_delete_twice_second_call_404s(self):
|
||||
artifact_id = self.create_artifact().data["id"]
|
||||
first = self.client.delete(
|
||||
reverse("artifact-detail-api", args=[artifact_id]), **self.auth_headers(self.token)
|
||||
)
|
||||
self.assertEqual(first.status_code, 204)
|
||||
|
||||
second = self.client.delete(
|
||||
reverse("artifact-detail-api", args=[artifact_id]), **self.auth_headers(self.token)
|
||||
)
|
||||
self.assertEqual(second.status_code, 404)
|
||||
|
||||
def test_deleted_artifact_is_gone_from_list_and_detail(self):
|
||||
artifact_id = self.create_artifact().data["id"]
|
||||
self.client.delete(
|
||||
reverse("artifact-detail-api", args=[artifact_id]), **self.auth_headers(self.token)
|
||||
)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
list_response = self.client.get(reverse("artifact-api"))
|
||||
self.assertEqual(list_response.data, [])
|
||||
|
||||
detail_response = self.client.get(reverse("artifact-detail-api", args=[artifact_id]))
|
||||
self.assertEqual(detail_response.status_code, 404)
|
||||
Loading…
Add table
Add a link
Reference in a new issue