import io from PIL import Image as PILImage class TestConfigAPI: def test_get_config(self, client): resp = client.get("/api/config") assert resp.status_code == 200 data = resp.get_json() assert "display" in data assert "unsplash" in data assert "schedule" in data def test_patch_config(self, client): resp = client.patch("/api/config", json={"display": {"saturation": 0.7}}) assert resp.status_code == 200 data = resp.get_json() assert data["display"]["saturation"] == 0.7 def test_get_config_section(self, client): resp = client.get("/api/config/display") assert resp.status_code == 200 data = resp.get_json() assert "saturation" in data assert "orientation" in data def test_get_config_section_not_found(self, client): resp = client.get("/api/config/nonexistent") assert resp.status_code == 404 def test_set_nested_config_key(self, client): resp = client.put( "/api/config/display/saturation", json={"value": 0.9}, ) assert resp.status_code == 200 assert resp.get_json() == {"saturation": 0.9} resp = client.get("/api/config/display") assert resp.get_json()["saturation"] == 0.9 def test_set_config_orientation(self, client): resp = client.put("/api/config/display/orientation", json={"value": 90}) assert resp.status_code == 200 resp = client.get("/api/config/display") assert resp.get_json()["orientation"] == 90 def test_set_config_missing_value_returns_400(self, client): resp = client.put("/api/config/display/saturation", json={}) assert resp.status_code == 400 class TestImagesAPI: def test_list_images_empty(self, client): resp = client.get("/api/images") assert resp.status_code == 200 data = resp.get_json() assert data["images"] == [] assert data["total"] == 0 def test_upload_image(self, client, sample_image_bytes): resp = client.post( "/api/images", data={"file": (sample_image_bytes, "test.jpg")}, content_type="multipart/form-data", ) assert resp.status_code == 201 data = resp.get_json() assert data["source"] == "upload" assert data["title"] == "test" assert data["width"] == 800 assert data["height"] == 600 def test_upload_image_with_title_and_author(self, client, sample_image_bytes): resp = client.post( "/api/images", data={"file": (sample_image_bytes, "photo.jpg"), "title": "My Photo", "author": "Test User"}, content_type="multipart/form-data", ) assert resp.status_code == 201 data = resp.get_json() assert data["title"] == "My Photo" assert data["author"] == "Test User" def test_upload_image_no_file_returns_400(self, client): resp = client.post("/api/images", content_type="multipart/form-data") assert resp.status_code == 400 def test_upload_invalid_extension_returns_400(self, client): buf = io.BytesIO(b"not an image") resp = client.post( "/api/images", data={"file": (buf, "test.txt")}, content_type="multipart/form-data", ) assert resp.status_code == 400 def test_upload_corrupted_image_returns_400(self, client): buf = io.BytesIO(b"not an image at all") resp = client.post( "/api/images", data={"file": (buf, "test.jpg")}, content_type="multipart/form-data", ) assert resp.status_code == 400 def test_get_image_detail(self, client, uploaded_image): image_id = uploaded_image["id"] resp = client.get(f"/api/images/{image_id}") assert resp.status_code == 200 data = resp.get_json() assert data["id"] == image_id assert data["source"] == "upload" def test_get_image_detail_not_found(self, client): resp = client.get("/api/images/99999") assert resp.status_code == 404 def test_delete_image(self, client, uploaded_image): image_id = uploaded_image["id"] resp = client.delete(f"/api/images/{image_id}") assert resp.status_code == 200 assert resp.get_json() == {"status": "deleted"} resp = client.get(f"/api/images/{image_id}") assert resp.status_code == 404 def test_delete_image_not_found(self, client): resp = client.delete("/api/images/99999") assert resp.status_code == 404 def test_list_images_after_upload(self, client, uploaded_image): resp = client.get("/api/images") data = resp.get_json() assert data["total"] >= 1 ids = [i["id"] for i in data["images"]] assert uploaded_image["id"] in ids def test_upload_security_filename_traversal(self, client): img = PILImage.new("RGB", (100, 100)) buf = io.BytesIO() img.save(buf, format="PNG") buf.seek(0) resp = client.post( "/api/images", data={"file": (buf, "../../etc/passwd.png")}, content_type="multipart/form-data", ) assert resp.status_code == 201 data = resp.get_json() assert "/" not in data["filename"] assert ".." not in data["filename"] def test_image_download(self, client, uploaded_image): image_id = uploaded_image["id"] resp = client.get(f"/api/images/{image_id}/file") assert resp.status_code == 200 assert resp.content_type.startswith("image/") def test_image_download_not_found(self, client): resp = client.get("/api/images/99999/file") assert resp.status_code == 404 class TestImageShow: def test_show_image(self, client, uploaded_image): image_id = uploaded_image["id"] resp = client.post(f"/api/images/{image_id}/show") assert resp.status_code == 200 data = resp.get_json() assert data["status"] == "triggered" def test_show_image_not_found(self, client): resp = client.post("/api/images/99999/show") assert resp.status_code == 404 class TestImageThumbnail: def test_thumbnail(self, client, uploaded_image): image_id = uploaded_image["id"] resp = client.get(f"/api/images/{image_id}/thumbnail") assert resp.status_code == 200 assert resp.content_type == "image/png" def test_thumbnail_not_found(self, client): resp = client.get("/api/images/99999/thumbnail") assert resp.status_code == 404 class TestPreviewAPI: def test_preview(self, client, uploaded_image): image_id = uploaded_image["id"] resp = client.get(f"/api/preview/{image_id}") assert resp.status_code == 200 assert resp.content_type == "image/png" def test_preview_not_found(self, client): resp = client.get("/api/preview/99999") assert resp.status_code == 404 class TestDisplayAPI: def test_status(self, client): resp = client.get("/api/display/status") assert resp.status_code == 200 data = resp.get_json() assert "resolution" in data assert "pending_trigger" in data def test_refresh(self, client): resp = client.post("/api/display/refresh") assert resp.status_code == 200 assert resp.get_json()["status"] == "triggered" def test_clear(self, client): resp = client.post("/api/display/clear") assert resp.status_code == 200 assert resp.get_json()["status"] == "triggered" class TestRotationAPI: def test_list_rotation_empty(self, client): resp = client.get("/api/rotation") assert resp.status_code == 200 assert resp.get_json()["entries"] == [] def test_add_to_rotation(self, client, uploaded_image): image_id = uploaded_image["id"] resp = client.post("/api/rotation", json={"image_ids": [image_id]}) assert resp.status_code == 201 assert resp.get_json() == {"added": [image_id]} def test_add_nonexistent_image_to_rotation(self, client): resp = client.post("/api/rotation", json={"image_ids": [99999]}) assert resp.status_code == 201 assert resp.get_json() == {"added": []} def test_add_duplicate_to_rotation(self, client, uploaded_image): image_id = uploaded_image["id"] client.post("/api/rotation", json={"image_ids": [image_id]}) resp = client.post("/api/rotation", json={"image_ids": [image_id]}) assert resp.status_code == 201 assert resp.get_json() == {"added": []} def test_rotation_detail(self, client, uploaded_image): image_id = uploaded_image["id"] client.post("/api/rotation", json={"image_ids": [image_id]}) resp = client.get("/api/rotation") rotation_id = resp.get_json()["entries"][0]["id"] resp = client.delete(f"/api/rotation/{rotation_id}") assert resp.status_code == 200 def test_patch_rotation_weight(self, client, uploaded_image): image_id = uploaded_image["id"] client.post("/api/rotation", json={"image_ids": [image_id]}) resp = client.get("/api/rotation") entry = resp.get_json()["entries"][0] entry_id = entry["id"] resp = client.patch(f"/api/rotation/{entry_id}", json={"weight": 5}) assert resp.status_code == 200 assert resp.get_json()["weight"] == 5 def test_patch_rotation_active(self, client, uploaded_image): image_id = uploaded_image["id"] client.post("/api/rotation", json={"image_ids": [image_id]}) resp = client.get("/api/rotation") entry_id = resp.get_json()["entries"][0]["id"] resp = client.patch(f"/api/rotation/{entry_id}", json={"active": False}) assert resp.status_code == 200 assert resp.get_json()["active"] is False def test_rotation_not_found(self, client): resp = client.delete("/api/rotation/99999") assert resp.status_code == 404 def test_rotation_patch_not_found(self, client): resp = client.patch("/api/rotation/99999", json={"weight": 3}) assert resp.status_code == 404 class TestPages: def test_index_page(self, client): resp = client.get("/") assert resp.status_code == 200 assert b"Dashboard" in resp.data def test_gallery_page(self, client): resp = client.get("/gallery") assert resp.status_code == 200 assert b"Gallery" in resp.data def test_upload_page(self, client): resp = client.get("/upload") assert resp.status_code == 200 assert b"Upload" in resp.data def test_settings_page(self, client): resp = client.get("/settings") assert resp.status_code == 200 assert b"Settings" in resp.data assert b"orientation" in resp.data def test_preview_page(self, client): resp = client.get("/preview") assert resp.status_code == 200 assert b"Preview" in resp.data class TestCORSAndErrors: def test_404_json(self, client): resp = client.get("/api/nonexistent") assert resp.status_code == 404 assert resp.is_json def test_400_has_error(self, client): resp = client.post("/api/images", content_type="multipart/form-data") assert resp.status_code == 400 data = resp.get_json() assert "error" in data def test_url_encoded_form_accepted(self, client): resp = client.put( "/api/config/display/saturation", data={"value": "0.5"}, ) assert resp.status_code == 200 resp = client.get("/api/config/display") assert resp.get_json()["saturation"] == 0.5 class TestGallery: def test_gallery_escaping(self, client, sample_image_bytes): malicious_title = '' resp = client.post( "/api/images", data={"file": (sample_image_bytes, "test.jpg"), "title": malicious_title}, content_type="multipart/form-data", ) assert resp.status_code == 201 img_id = resp.get_json()["id"] resp = client.get("/api/images") img = next(i for i in resp.get_json()["images"] if i["id"] == img_id) assert img["title"] == malicious_title def test_exact_json_equality_cautious(self, client, uploaded_image): resp = client.get(f"/api/images/{uploaded_image['id']}") data = resp.get_json() assert data["id"] == uploaded_image["id"] assert "created_at" in data def test_result_not_affected_by_previous_delete(self, client, uploaded_image): image_id = uploaded_image["id"] client.delete(f"/api/images/{image_id}") resp = client.get(f"/api/images/{image_id}") assert resp.status_code == 404