Implemented. Sign in, then open Experience or Skills to:
- Add, edit, and delete experience entries and project links. - Manage skill categories and individual skill labels. - Edit the skills introduction and “How I work” text. Changes save directly to Supabase—no code edits or Git push needed. Existing content is preserved.
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from backend import main, portfolio_content
|
||||
|
||||
|
||||
class PortfolioContentTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
directory = TemporaryDirectory()
|
||||
self.addCleanup(directory.cleanup)
|
||||
self.root = Path(directory.name)
|
||||
(self.root / "experience.json").write_text("[]", encoding="utf-8")
|
||||
(self.root / "skills.json").write_text(json.dumps({"intro": "Original intro", "principles": ["Keep it simple"], "categories": []}), encoding="utf-8")
|
||||
for mocked in (patch.object(main, "DATA_DIR", self.root), patch.object(main, "get_supabase", return_value=None),
|
||||
patch.dict(os.environ, {"JOURNAL_ADMIN_PASSWORD": "test-password", "JOURNAL_TOKEN_SECRET": "test-secret"})):
|
||||
mocked.start(); self.addCleanup(mocked.stop)
|
||||
self.client = TestClient(main.app)
|
||||
token = self.client.post("/api/auth/login", json={"password": "test-password"}).json()["access_token"]
|
||||
self.headers = {"Authorization": "Bearer " + token}
|
||||
self.experience = {"company": "Example", "role": "Engineer", "location": "Remote", "start": "2024-06", "end": None,
|
||||
"summary": "Build and maintain useful applications.", "highlights": ["Delivered a product"], "technologies": ["Python"],
|
||||
"projects": [{"name": "Project", "href": "https://example.com", "period": "2024"}]}
|
||||
self.category = {"name": "Engineering", "description": "Tools I use", "accent": "blue", "items": ["Python", "React"]}
|
||||
|
||||
def test_experience_crud_is_publicly_readable_and_persistent(self):
|
||||
created = self.client.post("/api/experience", json=self.experience, headers=self.headers)
|
||||
self.assertEqual(created.status_code, 201, created.text)
|
||||
item = created.json()
|
||||
older = self.client.post("/api/experience", json={**self.experience, "start": "2020-01", "end": "2023-12"}, headers=self.headers).json()
|
||||
self.assertEqual([entry["id"] for entry in self.client.get("/api/experience").json()], [item["id"], older["id"]])
|
||||
changed = {**self.experience, "role": "Lead engineer", "technologies": ["Go"], "projects": [], "highlights": []}
|
||||
response = self.client.put("/api/experience/" + item["id"], json=changed, headers=self.headers)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json()["id"], item["id"])
|
||||
saved = json.loads((self.root / "experience.json").read_text())
|
||||
self.assertEqual(saved[0]["role"], "Lead engineer")
|
||||
for entry in [item, older]:
|
||||
self.assertEqual(self.client.delete("/api/experience/" + entry["id"], headers=self.headers).status_code, 200)
|
||||
self.assertEqual(self.client.get("/api/experience").json(), [])
|
||||
|
||||
def test_skills_categories_labels_and_overview_crud(self):
|
||||
response = self.client.post("/api/skills/categories", json=self.category, headers=self.headers)
|
||||
self.assertEqual(response.status_code, 201, response.text)
|
||||
item = response.json()
|
||||
changed = {**self.category, "name": "Updated category", "items": ["TypeScript"]}
|
||||
response = self.client.put("/api/skills/categories/" + item["id"], json=changed, headers=self.headers)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
overview = {"intro": "A new introduction", "principles": ["One principle", "Another principle"]}
|
||||
self.assertEqual(self.client.patch("/api/skills", json=overview, headers=self.headers).status_code, 200)
|
||||
content = self.client.get("/api/skills").json()
|
||||
self.assertEqual(content["intro"], overview["intro"])
|
||||
self.assertEqual(content["categories"][0]["items"], ["TypeScript"])
|
||||
self.assertEqual(content["categories"][0]["id"], item["id"])
|
||||
self.assertEqual(self.client.delete("/api/skills/categories/" + item["id"], headers=self.headers).status_code, 200)
|
||||
self.client.patch("/api/skills", json={**overview, "principles": []}, headers=self.headers)
|
||||
content = self.client.get("/api/skills").json()
|
||||
self.assertEqual(content["categories"], [])
|
||||
self.assertEqual(content["principles"], [])
|
||||
|
||||
def test_all_mutations_require_admin(self):
|
||||
for method, path, body in [
|
||||
("post", "/api/experience", self.experience), ("put", "/api/experience/missing", self.experience),
|
||||
("delete", "/api/experience/missing", None), ("post", "/api/skills/categories", self.category),
|
||||
("put", "/api/skills/categories/missing", self.category), ("delete", "/api/skills/categories/missing", None),
|
||||
("patch", "/api/skills", {"intro": "Intro", "principles": []}),
|
||||
]:
|
||||
for headers in ({}, {"Authorization": "Bearer 1.invalid"}):
|
||||
response = self.client.request(method, path, json=body, headers=headers)
|
||||
self.assertEqual(response.status_code, 401, (method, path))
|
||||
self.assertEqual(self.client.get("/api/experience").status_code, 200)
|
||||
self.assertEqual(self.client.get("/api/skills").status_code, 200)
|
||||
|
||||
def test_invalid_dates_links_fields_and_missing_records(self):
|
||||
for changes in ({"start": "2024-13"}, {"end": "2020-01"}, {"role": " "},
|
||||
{"projects": [{"name": "Unsafe", "href": "javascript:alert(1)"}]}, {"id": "client-selected"}):
|
||||
self.assertEqual(self.client.post("/api/experience", json={**self.experience, **changes}, headers=self.headers).status_code, 422)
|
||||
self.assertEqual(self.client.post("/api/skills/categories", json={**self.category, "accent": "invalid"}, headers=self.headers).status_code, 422)
|
||||
for path, body in [("/api/experience/missing", self.experience), ("/api/skills/categories/missing", self.category)]:
|
||||
self.assertEqual(self.client.put(path, json=body, headers=self.headers).status_code, 404)
|
||||
self.assertEqual(self.client.delete(path, headers=self.headers).status_code, 404)
|
||||
|
||||
def test_supabase_writes_use_versions_and_fail_without_silent_fallback(self):
|
||||
client = MagicMock()
|
||||
table = client.table.return_value
|
||||
read = table.select.return_value.eq.return_value.limit.return_value.retry.return_value.execute
|
||||
read.return_value.data = [{"payload": [], "version": 7}]
|
||||
write = table.update.return_value.eq.return_value.eq.return_value.execute
|
||||
write.return_value.data = [{"key": "experience"}]
|
||||
with patch.object(main, "get_supabase", return_value=client):
|
||||
response = self.client.post("/api/experience", json=self.experience, headers=self.headers)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
written = table.update.call_args.args[0]
|
||||
self.assertEqual(written["version"], 8)
|
||||
self.assertEqual(written["payload"][0]["role"], "Engineer")
|
||||
table.update.return_value.eq.assert_called_with("key", "experience")
|
||||
table.update.return_value.eq.return_value.eq.assert_called_with("version", 7)
|
||||
write.return_value.data = []
|
||||
self.assertEqual(self.client.post("/api/experience", json=self.experience, headers=self.headers).status_code, 409)
|
||||
write.side_effect = RuntimeError("offline")
|
||||
self.assertEqual(self.client.post("/api/experience", json=self.experience, headers=self.headers).status_code, 502)
|
||||
read.return_value.data = []
|
||||
self.assertEqual(self.client.get("/api/experience").status_code, 503)
|
||||
self.assertEqual(json.loads((self.root / "experience.json").read_text()), [])
|
||||
|
||||
def test_local_category_ids_remain_stable_when_deleting_previous_category(self):
|
||||
content = {"intro": "Intro", "principles": [], "categories": [{**self.category, "name": name} for name in ["First", "Second"]]}
|
||||
(self.root / "skills.json").write_text(json.dumps(content))
|
||||
categories = self.client.get("/api/skills").json()["categories"]
|
||||
self.client.delete("/api/skills/categories/" + categories[0]["id"], headers=self.headers)
|
||||
self.assertEqual(self.client.get("/api/skills").json()["categories"][0]["id"], categories[1]["id"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user