Files
spectra/tests/test_api.py

354 lines
13 KiB
Python

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 "simulate" in data
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_not_accepted(self, client):
resp = client.put(
"/api/config/display/saturation",
data={"value": "0.5"},
)
assert resp.status_code == 400 or resp.status_code == 200
class TestGallery:
def test_gallery_escaping(self, client, sample_image_bytes):
malicious_title = '<script>alert("xss")</script>'
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