use std::path::Path;
use std::sync::Arc;
use devpage_core::designs::testing::*;
use crate::drive::http::{App, router};
use devpage_core::comments::Identity;
use devpage_core::designs::Designs;
use devpage_core::document::Document;
use devpage_core::folder::{Folder, ServedFolder};
fn opened(dir: &Path, entry: &str) -> Folder {
let served = ServedFolder::new(dir).unwrap();
Folder::new(Arc::new(Document::load(&dir.join(entry), &served).unwrap()))
}
async fn checked(dir: &Path, entry: &str) -> Vec<devpage_core::problem::Problem> {
let designs = Designs::new(&ServedFolder::new(dir).unwrap());
devpage_core::check::document(&opened(dir, entry), &designs, None).await
}
pub(crate) async fn start(dir: &Path, entry: &str) -> (String, Designs) {
let document =
Arc::new(Document::load(&dir.join(entry), &ServedFolder::new(dir).unwrap()).unwrap());
let designs = Designs::new(&document.served);
let changes = crate::adapters::changes::Changes::default();
let router = router(
App::testing(document, changes)
.with_designs(designs.clone())
.with_comments_on_disk(Identity::Local("T".into())),
);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = format!("http://{}", listener.local_addr().unwrap());
tokio::spawn(async move { axum::serve(listener, router).await.ok() });
(address, designs)
}
#[tokio::test]
async fn a_png_answers_304_from_its_scene_without_painting() {
let dir = served("png-etag");
let (address, designs) = start(&dir, "app/screen.design").await;
let url = format!("{address}/app/screen.design.png");
let client = reqwest::Client::new();
let first = client.get(&url).send().await.unwrap();
assert_eq!(first.status(), 200);
assert_eq!(first.headers()["cache-control"], "no-cache");
let tag = first.headers()["etag"].to_str().unwrap().to_string();
let again = client
.get(&url)
.header("if-none-match", &tag)
.send()
.await
.unwrap();
assert_eq!(again.status(), 304);
assert_eq!(again.headers()["etag"], tag.as_str());
let button = std::fs::read_to_string(dir.join("app/button.design")).unwrap();
save(
&designs,
&dir.join("app/button.design"),
&button.replace("#2563eb", "#16a34a"),
);
let changed = client
.get(&url)
.header("if-none-match", &tag)
.send()
.await
.unwrap();
assert_eq!(changed.status(), 200);
assert_ne!(changed.headers()["etag"], tag.as_str());
}
#[tokio::test]
async fn a_png_or_the_problems() {
let dir = served("png");
let (address, _) = start(&dir, "app/screen.design").await;
let response = reqwest::get(format!("{address}/app/screen.design.png"))
.await
.unwrap();
assert_eq!(response.status(), 200);
assert_eq!(response.headers()["content-type"], "image/png");
let png = response.bytes().await.unwrap();
assert_eq!(&png[1..4], b"PNG");
assert_eq!(u32::from_be_bytes(png[16..20].try_into().unwrap()), 200);
std::fs::write(
dir.join("app/broken.design"),
"import Button from \"./button.design\"\n<Frame>\n <Button lable=\"x\" />\n <Text size=\"big\">a</Text>\n</Frame>\n",
)
.unwrap();
let response = reqwest::get(format!("{address}/app/broken.design.png"))
.await
.unwrap();
assert_eq!(response.status(), 422);
let text = response.text().await.unwrap();
let lines: Vec<&str> = text.lines().collect();
assert_eq!(lines.len(), 2, "{text}");
assert!(lines[0].starts_with("app/broken.design:3:"), "{text}");
assert!(lines[1].starts_with("app/broken.design:4:"), "{text}");
}
#[tokio::test]
async fn a_png_in_either_theme() {
let dir = served("png-theme");
std::fs::write(dir.join("themes.yml"), THEMES).unwrap();
std::fs::write(
dir.join("app/themed.design"),
"<Frame width={10} height={10} fill={theme.bg} />",
)
.unwrap();
let (address, _) = start(&dir, "app/screen.design").await;
let get = |query: &str| reqwest::get(format!("{address}/app/themed.design.png{query}"));
let mut tags = Vec::new();
for (query, value) in [("", 255), ("?theme=light", 255), ("?theme=dark", 0)] {
let response = get(query).await.unwrap();
assert_eq!(response.status(), 200, "{query}");
tags.push(response.headers()["etag"].to_str().unwrap().to_string());
let png = response.bytes().await.unwrap();
let (pixels, _, _) = devpage_design::compare::pixels(&png).unwrap();
assert_eq!(pixels[..4], [value, value, value, 255], "{query}");
}
assert_eq!(tags[0], tags[1]);
assert_ne!(tags[0], tags[2], "each theme is its own PNG");
let wrong = get("?theme=blue").await.unwrap();
assert_eq!(wrong.status(), 400);
assert_eq!(
wrong.text().await.unwrap(),
"theme: expected light or dark, found \"blue\""
);
}
#[tokio::test]
async fn no_png_of_a_design_outside_the_served_folder() {
let dir = served("png-outside");
let outside = dir.join("../png-outside-secret.design");
std::fs::write(&outside, "<Frame width={10} height={10} />").unwrap();
std::os::unix::fs::symlink(&outside, dir.join("app/escape.design")).unwrap();
let (address, _) = start(&dir, "app/screen.design").await;
for path in [
"/../png-outside-secret.design.png",
"/%2E%2E/png-outside-secret.design.png",
"/app/escape.design.png",
"/app/missing.design.png",
] {
let response = reqwest::get(format!("{address}{path}")).await.unwrap();
assert_eq!(response.status(), 404, "{path}");
let body = response.text().await.unwrap();
assert!(
body.ends_with("is not part of the served folder"),
"{path}: {body}"
);
}
}
#[tokio::test]
async fn a_real_file_named_like_a_designs_png_wins() {
let dir = served("png-real");
std::fs::write(dir.join("app/screen.design.png"), b"not a drawing").unwrap();
let (address, _) = start(&dir, "app/screen.design").await;
let real = reqwest::get(format!("{address}/app/screen.design.png"))
.await
.unwrap();
assert_eq!(real.status(), 200);
assert_eq!(real.bytes().await.unwrap().as_ref(), b"not a drawing");
let drawn = reqwest::get(format!("{address}/app/other.design.png"))
.await
.unwrap();
assert_eq!(drawn.headers()["content-type"], "image/png");
}
#[tokio::test]
async fn a_designs_png_is_a_link_check_accepts() {
let dir = served("png-link");
std::fs::write(
dir.join("app/broken.design"),
SCREEN.replace("<Button label", "<Button lable"),
)
.unwrap();
std::fs::write(
dir.join("page.md"),
"# Page\n\n\n\n\n\n\n",
)
.unwrap();
let problems: Vec<String> = checked(&dir, "page.md")
.await
.iter()
.map(|p| p.to_string())
.collect();
assert_eq!(problems.len(), 2, "{problems:?}");
assert!(
problems[0]
.starts_with("page.md:5: broken design app/broken.design.png — app/broken.design:5:"),
"{problems:?}"
);
assert_eq!(
problems[1],
"page.md:7: broken link app/nothing.design.png — no such file"
);
}
#[tokio::test]
async fn a_design_is_an_entry_document() {
let dir = served("entry");
let (address, _) = start(&dir, "app/screen.design").await;
let client = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.unwrap();
let home = client.get(format!("{address}/")).send().await.unwrap();
assert_eq!(home.status(), 307);
assert_eq!(home.headers()["location"], "/app/screen.design");
let page = client
.get(format!("{address}/app/screen.design"))
.send()
.await
.unwrap();
assert_eq!(page.status(), 200);
assert!(page.text().await.unwrap().contains("<canvas"));
}
#[tokio::test]
async fn check_reports_a_broken_embedded_design_where_it_is_wrong() {
let dir = served("check");
std::fs::write(
dir.join("app/screen.design"),
SCREEN.replace("<Button label", "<Button lable"),
)
.unwrap();
std::fs::write(
dir.join("page.md"),
"# Page\n\n\n\nEmbed with ``:\n\n```markdown\n\n```\n",
)
.unwrap();
let problems = checked(&dir, "page.md").await;
assert_eq!(problems.len(), 1, "{problems:?}");
let shown = problems[0].to_string();
assert!(
shown.starts_with("page.md:3: broken design app/screen.design — app/screen.design:5:"),
"{shown}"
);
assert!(shown.contains("lable"), "{shown}");
let problems = checked(&dir, "app/screen.design").await;
assert_eq!(problems.len(), 1, "{problems:?}");
assert!(
problems[0].to_string().starts_with("app/screen.design:5:"),
"{problems:?}"
);
}