- gitleaks (via docker cp, dockerignore-agnostic) and hadolint scan every push/PR - new ruff lint stage (ruff.toml pins known-first-party for host/container consistency; F841/B007 in webui.py left un-fixed via per-file-ignores — app-logic changes, flagged for review rather than auto-fixed) - pytest --cov-fail-under=90 gate on the test stage (already had pytest-cov, never wired into CI until now) - scheduled Trivy critical failures now attempt an apk upgrade rebuild and open a PR if it clears the finding, instead of just failing red - ruff --fix/--format applied to existing code to start the gate clean
317 lines
11 KiB
Python
317 lines
11 KiB
Python
import io
|
|
import zipfile
|
|
|
|
import yaml
|
|
|
|
|
|
def test_index_page(client):
|
|
resp = client.get("/")
|
|
assert resp.status_code == 200
|
|
|
|
|
|
def test_get_gallery_and_hero_start_empty(client):
|
|
assert client.get("/api/gallery").get_json() == []
|
|
assert client.get("/api/hero").get_json() == []
|
|
|
|
|
|
def test_update_gallery_and_hero(client, app_env):
|
|
images = [{"src": "gallery/a.jpg", "tags": ["x"]}]
|
|
resp = client.post("/api/gallery/update", json=images)
|
|
assert resp.get_json() == {"status": "ok"}
|
|
assert client.get("/api/gallery").get_json() == images
|
|
|
|
hero_images = [{"src": "hero/a.jpg"}]
|
|
resp = client.post("/api/hero/update", json=hero_images)
|
|
assert resp.get_json() == {"status": "ok"}
|
|
assert client.get("/api/hero").get_json() == hero_images
|
|
|
|
|
|
def test_refresh_gallery_and_hero_pick_up_new_files(client, app_env, make_image):
|
|
make_image(app_env / "config" / "photos" / "gallery" / "new.jpg")
|
|
make_image(app_env / "config" / "photos" / "hero" / "new.jpg")
|
|
|
|
client.post("/api/gallery/refresh")
|
|
client.post("/api/hero/refresh")
|
|
|
|
gallery = client.get("/api/gallery").get_json()
|
|
hero = client.get("/api/hero").get_json()
|
|
assert gallery[0]["src"] == "gallery/new.jpg"
|
|
assert hero[0]["src"] == "hero/new.jpg"
|
|
|
|
|
|
def test_delete_gallery_photo(client, app_env, make_image):
|
|
make_image(app_env / "config" / "photos" / "gallery" / "a.jpg")
|
|
|
|
resp = client.post("/api/gallery/delete", json={"src": "a.jpg"})
|
|
assert resp.get_json() == {"status": "ok"}
|
|
assert not (app_env / "config" / "photos" / "gallery" / "a.jpg").exists()
|
|
|
|
|
|
def test_delete_gallery_photo_not_found(client):
|
|
resp = client.post("/api/gallery/delete", json={"src": "missing.jpg"})
|
|
assert resp.status_code == 404
|
|
|
|
|
|
def test_delete_hero_photo(client, app_env, make_image):
|
|
make_image(app_env / "config" / "photos" / "hero" / "a.jpg")
|
|
|
|
resp = client.post("/api/hero/delete", json={"src": "a.jpg"})
|
|
assert resp.get_json() == {"status": "ok"}
|
|
assert not (app_env / "config" / "photos" / "hero" / "a.jpg").exists()
|
|
|
|
|
|
def test_delete_all_gallery_photos(client, app_env, make_image):
|
|
make_image(app_env / "config" / "photos" / "gallery" / "a.jpg")
|
|
make_image(app_env / "config" / "photos" / "gallery" / "b.jpg")
|
|
client.post("/api/gallery/update", json=[{"src": "gallery/a.jpg"}, {"src": "gallery/b.jpg"}])
|
|
|
|
resp = client.post("/api/gallery/delete_all")
|
|
|
|
body = resp.get_json()
|
|
assert body["status"] == "ok"
|
|
assert body["deleted"] == 2
|
|
assert client.get("/api/gallery").get_json() == []
|
|
assert list((app_env / "config" / "photos" / "gallery").iterdir()) == []
|
|
|
|
|
|
def test_delete_all_hero_photos(client, app_env, make_image):
|
|
make_image(app_env / "config" / "photos" / "hero" / "a.jpg")
|
|
client.post("/api/hero/update", json=[{"src": "hero/a.jpg"}])
|
|
|
|
resp = client.post("/api/hero/delete_all")
|
|
|
|
body = resp.get_json()
|
|
assert body["deleted"] == 1
|
|
assert client.get("/api/hero").get_json() == []
|
|
|
|
|
|
def test_serve_photo_by_section(client, app_env, make_image):
|
|
make_image(app_env / "config" / "photos" / "gallery" / "a.jpg")
|
|
resp = client.get("/photos/gallery/a.jpg")
|
|
assert resp.status_code == 200
|
|
|
|
|
|
def test_serve_photo_at_root(client, app_env, make_image):
|
|
make_image(app_env / "config" / "photos" / "thumbnail.png", fmt="PNG")
|
|
resp = client.get("/photos/thumbnail.png")
|
|
assert resp.status_code == 200
|
|
|
|
|
|
def test_site_info_get_and_post(client, app_env):
|
|
resp = client.get("/api/site-info")
|
|
data = resp.get_json()
|
|
assert data["info"]["title"] == "Test"
|
|
|
|
client.post(
|
|
"/api/site-info",
|
|
json={
|
|
"info": {"subtitle": "New subtitle"},
|
|
"social": {"instagram_url": "https://insta.example"},
|
|
},
|
|
)
|
|
|
|
updated = client.get("/api/site-info").get_json()
|
|
assert updated["info"]["title"] == "Test"
|
|
assert updated["info"]["subtitle"] == "New subtitle"
|
|
assert updated["social"]["instagram_url"] == "https://insta.example"
|
|
|
|
|
|
def test_list_themes(client):
|
|
assert client.get("/api/themes").get_json() == ["modern"]
|
|
|
|
|
|
def test_thumbnail_upload_and_remove(client, app_env):
|
|
data = {"file": (io.BytesIO(b"fake-png-bytes"), "thumb.png")}
|
|
resp = client.post("/api/thumbnail/upload", data=data, content_type="multipart/form-data")
|
|
body = resp.get_json()
|
|
assert body["status"] == "ok"
|
|
assert (app_env / "config" / "photos" / "thumbnail.png").exists()
|
|
|
|
site_info = client.get("/api/site-info").get_json()
|
|
assert site_info["social"]["thumbnail"] == "thumbnail.png"
|
|
|
|
resp = client.post("/api/thumbnail/remove")
|
|
assert resp.get_json() == {"status": "ok"}
|
|
assert not (app_env / "config" / "photos" / "thumbnail.png").exists()
|
|
site_info = client.get("/api/site-info").get_json()
|
|
assert site_info["social"]["thumbnail"] == ""
|
|
|
|
|
|
def test_thumbnail_upload_missing_file(client):
|
|
resp = client.post("/api/thumbnail/upload", data={}, content_type="multipart/form-data")
|
|
assert resp.status_code == 400
|
|
|
|
|
|
def test_theme_upload_and_remove(client, app_env):
|
|
data = {
|
|
"files": [
|
|
(io.BytesIO(b"colors: {}"), "custom/theme.yaml"),
|
|
(io.BytesIO(b"body{}"), "custom/theme.css"),
|
|
]
|
|
}
|
|
resp = client.post("/api/theme/upload", data=data, content_type="multipart/form-data")
|
|
body = resp.get_json()
|
|
assert body["status"] == "ok"
|
|
assert body["theme"] == "custom"
|
|
assert (app_env / "config" / "themes" / "custom" / "theme.yaml").exists()
|
|
assert (app_env / "config" / "themes" / "custom" / "theme.css").exists()
|
|
|
|
resp = client.post("/api/theme/remove", json={"theme": "custom"})
|
|
assert resp.get_json() == {"status": "ok"}
|
|
assert not (app_env / "config" / "themes" / "custom").exists()
|
|
|
|
|
|
def test_theme_remove_protected_theme(client):
|
|
resp = client.post("/api/theme/remove", json={"theme": "modern"})
|
|
assert resp.status_code == 400
|
|
|
|
|
|
def test_theme_remove_missing_theme(client):
|
|
resp = client.post("/api/theme/remove", json={"theme": "does-not-exist"})
|
|
assert resp.status_code == 404
|
|
|
|
|
|
def test_theme_info_get_and_post(client, app_env):
|
|
resp = client.get("/api/theme-info")
|
|
body = resp.get_json()
|
|
assert body["theme_name"] == "modern"
|
|
assert body["theme_yaml"]["favicon"]["path"] == "favicon.png"
|
|
|
|
new_theme_yaml = dict(body["theme_yaml"])
|
|
new_theme_yaml["colors"] = {"primary": "#123456"}
|
|
resp = client.post(
|
|
"/api/theme-info",
|
|
json={"theme_name": "modern", "theme_yaml": new_theme_yaml},
|
|
)
|
|
assert resp.get_json() == {"status": "ok"}
|
|
|
|
saved = yaml.safe_load((app_env / "config" / "themes" / "modern" / "theme.yaml").read_text())
|
|
assert saved["colors"]["primary"] == "#123456"
|
|
|
|
|
|
def test_update_theme_google_fonts(client, app_env):
|
|
resp = client.post(
|
|
"/api/theme-google-fonts",
|
|
json={"theme_name": "modern", "google_fonts": [{"family": "Lato"}]},
|
|
)
|
|
assert resp.get_json() == {"status": "ok"}
|
|
|
|
saved = yaml.safe_load((app_env / "config" / "themes" / "modern" / "theme.yaml").read_text())
|
|
assert saved["google_fonts"] == [{"family": "Lato"}]
|
|
|
|
|
|
def test_local_fonts_empty_then_populated(client, app_env):
|
|
assert client.get("/api/local-fonts?theme=modern").get_json() == []
|
|
|
|
fonts_dir = app_env / "config" / "themes" / "modern" / "fonts"
|
|
fonts_dir.mkdir(parents=True)
|
|
(fonts_dir / "trixie.woff2").write_bytes(b"fake")
|
|
|
|
assert client.get("/api/local-fonts?theme=modern").get_json() == ["trixie.woff2"]
|
|
|
|
|
|
def test_favicon_upload_invalid_extension(client):
|
|
data = {"theme": "modern", "file": (io.BytesIO(b"not-an-image"), "favicon.txt")}
|
|
resp = client.post("/api/favicon/upload", data=data, content_type="multipart/form-data")
|
|
assert resp.status_code == 400
|
|
|
|
|
|
def test_favicon_upload_and_remove(client, app_env):
|
|
data = {"theme": "modern", "file": (io.BytesIO(b"fake-png"), "favicon.png")}
|
|
resp = client.post("/api/favicon/upload", data=data, content_type="multipart/form-data")
|
|
assert resp.get_json()["status"] == "ok"
|
|
assert (app_env / "config" / "themes" / "modern" / "favicon.png").exists()
|
|
|
|
resp = client.post("/api/favicon/remove", json={"theme": "modern"})
|
|
assert resp.get_json() == {"status": "ok"}
|
|
assert not (app_env / "config" / "themes" / "modern" / "favicon.png").exists()
|
|
|
|
|
|
def test_serve_theme_asset(client, app_env):
|
|
(app_env / "config" / "themes" / "modern" / "theme.css").write_text("body{}", encoding="utf-8")
|
|
resp = client.get("/themes/modern/theme.css")
|
|
assert resp.status_code == 200
|
|
|
|
|
|
def test_font_upload_invalid_extension(client):
|
|
data = {"theme": "modern", "file": (io.BytesIO(b"data"), "font.ttf")}
|
|
resp = client.post("/api/font/upload", data=data, content_type="multipart/form-data")
|
|
assert resp.status_code == 400
|
|
|
|
|
|
def test_font_upload_and_remove(client, app_env):
|
|
data = {"theme": "modern", "file": (io.BytesIO(b"data"), "trixie.woff2")}
|
|
resp = client.post("/api/font/upload", data=data, content_type="multipart/form-data")
|
|
body = resp.get_json()
|
|
assert body["status"] == "ok"
|
|
assert body["filename"] == "trixie"
|
|
|
|
resp = client.post("/api/font/remove", json={"theme": "modern", "font": "trixie.woff2"})
|
|
assert resp.get_json() == {"status": "ok"}
|
|
|
|
|
|
def test_font_remove_missing(client):
|
|
resp = client.post("/api/font/remove", json={"theme": "modern", "font": "missing.woff2"})
|
|
assert resp.status_code == 404
|
|
|
|
|
|
def test_trigger_build_missing_site_yaml(client, app_env):
|
|
(app_env / "config" / "site.yaml").unlink()
|
|
resp = client.post("/api/build")
|
|
assert resp.status_code == 400
|
|
assert "site.yaml not found" in resp.get_json()["message"]
|
|
|
|
|
|
def test_trigger_build_missing_required_field(client, app_env):
|
|
resp = client.post("/api/build")
|
|
assert resp.status_code == 400
|
|
assert "Site info are not set" in resp.get_json()["message"]
|
|
|
|
|
|
def test_trigger_build_success_invokes_subprocess(client, app_env, monkeypatch):
|
|
import src.py.webui.webui as webui
|
|
|
|
(app_env / "config" / "site.yaml").write_text(
|
|
"info:\n title: Test\n canonical: https://example.com\nsocial:\n thumbnail: t.png\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
calls = []
|
|
monkeypatch.setattr(webui.subprocess, "run", lambda *a, **k: calls.append((a, k)))
|
|
|
|
resp = client.post("/api/build")
|
|
assert resp.get_json() == {"status": "ok"}
|
|
assert calls
|
|
|
|
|
|
def test_trigger_build_subprocess_failure_returns_500(client, app_env, monkeypatch):
|
|
import src.py.webui.webui as webui
|
|
|
|
(app_env / "config" / "site.yaml").write_text(
|
|
"info:\n title: Test\n canonical: https://example.com\nsocial:\n thumbnail: t.png\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
def _raise(*a, **k):
|
|
raise RuntimeError("boom")
|
|
|
|
monkeypatch.setattr(webui.subprocess, "run", _raise)
|
|
|
|
resp = client.post("/api/build")
|
|
assert resp.status_code == 500
|
|
assert "boom" in resp.get_json()["message"]
|
|
|
|
|
|
def test_download_output_zip(client, app_env):
|
|
output_dir = app_env / "output"
|
|
output_dir.mkdir()
|
|
(output_dir / "index.html").write_text("<html></html>", encoding="utf-8")
|
|
|
|
resp = client.post("/download-output-zip")
|
|
|
|
assert resp.status_code == 200
|
|
zip_bytes = io.BytesIO(resp.data)
|
|
with zipfile.ZipFile(zip_bytes) as zf:
|
|
assert "index.html" in zf.namelist()
|
|
assert not (app_env / "site_output.zip").exists()
|