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("", 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()