354 lines
13 KiB
Python
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
|