use super::Project; use std::{collections::BTreeSet, path::PathBuf}; fn fixture(name: &str) -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../test/rust/fixtures") .join(name) .canonicalize() .unwrap() } fn charts(page: &str) -> impl Iterator { page.split("```mermaid\n") .skip(1) .map(|chart| chart.split("```").next().unwrap()) } fn node(chart: &str, text: &str) -> String { chart .lines() .find(|line| line.contains(text) && !line.contains(" -->") && !line.contains("click ")) .unwrap_or_else(|| panic!("missing {text}:\n{chart}")) .trim() .split(['[', '{', '(']) .next() .unwrap() .into() } // 按图的边检查可达性,不能把节点声明顺序误当执行顺序。 fn reaches(chart: &str, from: &str, to: &str) -> bool { let mut pending = vec![from.to_owned()]; let mut seen = BTreeSet::new(); while let Some(current) = pending.pop() { if current == to { return true; } if !seen.insert(current.clone()) { continue; } for line in chart.lines() { if line.trim().starts_with(&format!("{current} -->")) { pending.push(line.split_whitespace().last().unwrap().to_owned()); } } } false } #[test] fn http_flows_follow_authentication_permission_decode_call_and_transaction_order() { let root = fixture("auth"); let project = Project::load(&root).unwrap(); let index = project.inspect().unwrap(); let pages = index.links(&root).unwrap(); let page = &pages[&root.join("module/demo/account/link.md")]; let program = project.check().unwrap(); let rust = dever::compiler::emit::application::generate(&program); for (position, entry) in program.http.iter().enumerate() { if !entry.path.starts_with("/demo/account/") { continue; } let chart = charts(page) .find(|chart| chart.contains(&format!("路由 {} {},", entry.method, entry.path))) .unwrap(); let decode = node(chart, "解码具体参数"); let call = node( chart, &format!( "[[\"demo.account.{}\"", program.functions[entry.function].signature.name ), ); assert!(reaches(chart, &decode, &call)); assert!(!reaches(chart, &node(chart, "失败:保留 HTTP"), &call)); assert!(chart.contains("demo.account.verify")); let emitted = rust .split(&format!("async fn api{position}(")) .nth(1) .unwrap() .split("struct HttpArgs") .next() .unwrap(); assert!( emitted.find("authentication.prepare_at").unwrap() < emitted.find("let args: HttpArgs").unwrap() ); match entry.access { dever::compiler::model::auth::Access::Protected => { let permission = node(chart, "权限检查:"); assert!(reaches(chart, &permission, &decode)); assert!(!reaches(chart, &decode, &permission)); assert!( emitted.find("authorization::authorize_entry").unwrap() < emitted.find("let args: HttpArgs").unwrap() ); } dever::compiler::model::auth::Access::Public => { assert!(chart.contains("public 忽略自动 Cookie")); assert!(chart.contains("没有显式凭据:匿名")); assert!(!chart.contains("权限检查:")); } dever::compiler::model::auth::Access::Authenticated => { assert!(!chart.contains("没有显式凭据:匿名")); assert!(!chart.contains("权限检查:")); } } let write = program.functions[entry.function] .signature .effects .database_write && !entry.long; assert_eq!(chart.contains("进入写入口事务"), write); if write { assert!(reaches(chart, &call, &node(chart, "提交入口拥有的事务"))); assert!(reaches( chart, &node(chart, "失败:保留 HTTP"), &node(chart, "回滚入口拥有的事务") )); } } let anchors: Vec<_> = page .lines() .filter(|line| line.starts_with("