Initial Dever 0.0.1

This commit is contained in:
Codex committed 2026-11-03 03:32:07 +00:00
commit 81233d098c
1479 files changed
+163782

No files matched your search

+84
View File
@@ -0,0 +1,84 @@
use super::*;
fn count(binary: &Path) -> i64 {
let result = execute(binary, "demo.account.job_count", "{}");
assert!(
result.status.success(),
"{}",
String::from_utf8_lossy(&result.stderr)
);
serde_json::from_slice::<serde_json::Value>(&result.stdout).unwrap()["data"]
.as_i64()
.unwrap()
}
fn submit(port: u16, bearer: &str, subject: &str) {
let (status, body) = request(
port,
"POST",
"/demo/account/admin/jobs/job_submit",
"fixture.test",
&format!("{bearer}Content-Type: application/json\r\n"),
&serde_json::json!({"subject":subject,"key":subject}).to_string(),
);
assert_eq!(status, 200, "{body}");
}
fn worker(binary: &Path, settings: &serde_json::Value) -> Server {
let path = binary.parent().unwrap().join("worker.json");
let mut settings = settings.clone();
settings["runtime"] = serde_json::json!({"mode":"worker","shutdown_ms":1000});
settings["job"] = serde_json::json!({"workers":1,"poll_ms":10,"lease_ms":2000});
std::fs::write(&path, settings.to_string()).unwrap();
Server(
Command::new(binary)
.args(["--config"])
.arg(path)
.spawn()
.unwrap(),
)
}
fn until(binary: &Path, server: &mut Server, expected: i64) {
let deadline = Instant::now() + Duration::from_secs(10);
while count(binary) != expected {
assert!(server.0.try_wait().unwrap().is_none(), "worker exited");
assert!(Instant::now() < deadline, "Job result not committed");
std::thread::sleep(Duration::from_millis(10));
}
}
pub(super) fn before_revocation(
binary: &Path,
port: u16,
bearer: &str,
settings: &serde_json::Value,
) -> i64 {
let before = count(binary);
submit(port, bearer, "job-valid");
let mut server = worker(binary, settings);
until(binary, &mut server, before + 1);
assert!(
Command::new("sh")
.args(["-c", "kill -TERM \"$1\"", "--", &server.0.id().to_string()])
.status()
.unwrap()
.success()
);
assert!(server.0.wait().unwrap().success());
submit(port, bearer, "job-revoked");
before + 1
}
pub(super) fn after_revocation(binary: &Path, settings: &serde_json::Value, before: i64) {
let submitted = execute(
binary,
"demo.account.job_submit",
r#"{"subject":"job-system","key":"system"}"#,
);
assert!(submitted.status.success());
let mut server = worker(binary, settings);
until(binary, &mut server, before + 1);
std::thread::sleep(Duration::from_millis(60));
assert_eq!(count(binary), before + 1, "revoked User job executed");
}
@@ -0,0 +1,35 @@
use super::*;
pub(super) fn check(port: u16) {
let headers = "Origin: https://fixture.test\r\nContent-Type: application/json\r\nX-Test: request-value\r\nCookie: theme=light\r\nX-Forwarded-For: 203.0.113.9\r\n";
let path = "/demo/account/admin/metadata/metadata";
let mut previous = None;
for _ in 0..2 {
let (status, head, body) = request_head(port, "POST", path, "fixture.test", headers, "{}");
assert_eq!(status, 200, "{body}");
let value = serde_json::from_str::<serde_json::Value>(&body).unwrap()["data"].clone();
assert_eq!(value["method"], "POST");
assert_eq!(value["path"], path);
assert_eq!(value["address"], "127.0.0.1");
assert_eq!(value["header"], "request-value");
assert_eq!(value["cookie"], "light");
let id = value["request_id"].as_str().unwrap().to_owned();
assert!(head.contains(&format!("x-request-id: {id}")));
assert!(head.contains("set-cookie: theme=dark; Path=/; SameSite=Lax; Secure; HttpOnly"));
assert!(head.contains(
"set-cookie: strict=yes; Path=/; SameSite=Strict; Max-Age=60; Secure; HttpOnly"
));
assert_ne!(previous.as_ref(), Some(&id));
previous = Some(id);
}
let (status, head, _) = request_head(
port,
"POST",
"/demo/account/admin/metadata/metadata_fail",
"fixture.test",
headers,
"{}",
);
assert_eq!(status, 409);
assert!(!head.contains("x-request-id:") && !head.contains("set-cookie:"));
}
+184
View File
@@ -0,0 +1,184 @@
use super::*;
pub(super) fn check(binary: &std::path::Path, port: u16, bearer: &str) {
let path = "/demo/account/admin/profile";
let call = |method: &str, path: &str, body: &str| {
let (status, body) = request(
port,
method,
path,
"fixture.test",
&format!("{bearer}Content-Type: application/json\r\n"),
body,
);
(
status,
serde_json::from_str::<serde_json::Value>(&body).unwrap()["data"].clone(),
)
};
assert_eq!(request(port, "GET", path, "fixture.test", "", "").0, 401);
for (method, path, allowed) in [
("PUT", path.to_owned(), "GET, POST"),
("POST", format!("{path}/1"), "GET, PUT, DELETE"),
] {
let (status, head, _) = request_head(port, method, &path, "fixture.test", bearer, "");
assert_eq!(status, 405);
assert!(
head.to_ascii_lowercase()
.contains(&format!("\r\nallow: {}\r\n", allowed.to_ascii_lowercase()))
);
}
assert_eq!(call("POST", &format!("{path}/invalid"), "").0, 404);
assert_eq!(call("GET", &format!("{path}?size=101"), "").0, 400);
assert_eq!(call("POST", path, r#"{"name":"","bio":null}"#).0, 400);
for body in [
r#"{"name":"first","name":"second"}"#,
r#"{"name":"first","na\u006de":"second"}"#,
r#"{"name":"bad","rank":9223372036854775808}"#,
r#"{"name":"bad","status":{"Active":null}}"#,
r#"{"name":"bad","bio":"\ud800"}"#,
r#"["bad",null,7,null,"Active"]"#,
] {
assert_eq!(call("POST", path, body).0, 400, "{body}");
}
assert_eq!(
call("POST", path, r#"{"name":"bad","note":"private"}"#).0,
400
);
assert_eq!(call("POST", path, r#"{"name":"bad","user_id":1}"#).0, 400);
assert_eq!(
call("POST", path, r#"{"name":"bad","creation_key":"forged"}"#).0,
400
);
let (status, created) = call("POST", path, r#"{"name":" Dever 中文 "}"#);
assert_eq!(status, 200, "{created}");
assert_eq!(created["name"], "Dever 中文");
assert_eq!(created["rank"], 7);
assert_eq!(created["status"], "Active");
assert_eq!(created["tag"], "automatic");
assert_eq!(created["uid"].as_str().unwrap().len(), 36);
assert!(created.get("note").is_none());
assert!(created.get("user_id").is_none());
assert!(created.get("creation_key").is_none());
assert_eq!(
call("POST", path, r#"{"name":"invalid-null","rank":null}"#).0,
400
);
assert_eq!(
call(
"POST",
path,
r#"{"name":"invalid-status","status":"Unknown"}"#
)
.0,
400
);
assert_eq!(
call(
"POST",
path,
&serde_json::json!({"name":"invalid-generated","uid":created["uid"]}).to_string()
)
.0,
400
);
assert!(created["created_at"].as_str().unwrap().ends_with('Z'));
let id = created["id"].as_i64().unwrap();
let detail = format!("{path}/{id}");
assert_eq!(call("GET", &detail, "").1, created);
let bootstrap = execute(
binary,
"dever.auth.owner",
r#"{"site":"admin","user_id":2}"#,
);
assert!(
bootstrap.status.success(),
"{}",
String::from_utf8_lossy(&bootstrap.stderr)
);
let other = format!(
"Authorization: Bearer {}\r\nContent-Type: application/json\r\n",
token("reader", "reader-session")
);
for (method, body) in [("GET", ""), ("PUT", r#"{"name":"stolen"}"#), ("DELETE", "")] {
assert_eq!(
request(port, method, &detail, "fixture.test", &other, body).0,
404
);
}
let (status, body) = request(port, "GET", path, "fixture.test", &other, "");
assert_eq!(status, 200);
assert_eq!(
serde_json::from_str::<serde_json::Value>(&body).unwrap()["data"]["total"],
0
);
// 不同身份的创建键不同,名称重复仍须由名称自身的唯一约束拒绝。
assert_eq!(
request(
port,
"POST",
path,
"fixture.test",
&other,
r#"{"name":"Dever 中文"}"#
)
.0,
409
);
assert_eq!(call("POST", path, r#"{"name":"Dever 中文"}"#).0, 409);
let (status, page) = call("GET", path, "");
assert_eq!(status, 200);
assert_eq!(page["total"], 1);
assert_eq!(page["items"][0], created);
assert_eq!(
call("GET", &format!("{path}?name=absent"), "").1["total"],
0
);
assert_eq!(
call(
"GET",
&format!("{path}?name=%20Dever%20%E4%B8%AD%E6%96%87%20"),
""
)
.1["total"],
1
);
assert_eq!(
call("GET", &format!("{path}?page=2&size=1"), "").1["items"],
serde_json::json!([])
);
assert_eq!(call("GET", &format!("{detail}?extra=1"), "").0, 400);
assert_eq!(
call("PUT", &detail, r#"{"name":"bad","creation_key":"forged"}"#).0,
400
);
assert_eq!(
call(
"PUT",
&detail,
r#"{"name":"new","bio":"updated","tag":null}"#
)
.1["bio"],
"updated"
);
assert_eq!(call("GET", &detail, "").1["tag"], serde_json::Value::Null);
assert_eq!(
call("PUT", &detail, r#"{"name":"new","status":"Disabled"}"#).1["status"],
"Disabled"
);
let (status, replaced) = call("PUT", &detail, r#"{"name":"REPLACED"}"#);
assert_eq!(status, 200);
assert_eq!(replaced["bio"], serde_json::Value::Null);
assert_eq!(replaced["name"], "replaced");
assert!(replaced.get("creation_key").is_none());
// POST:user 仍在数据库中;若 PUT 重算为 PUT:user 或清空,新名称的创建会错误成功。
assert_eq!(call("POST", path, r#"{"name":"another"}"#).0, 409);
assert_eq!(call("GET", &detail, "").1, replaced);
let (_, page) = call("GET", path, "");
assert_eq!(page["total"], 1);
assert_eq!(page["items"][0], replaced);
assert_eq!(call("DELETE", &detail, "").0, 200);
assert_eq!(call("DELETE", &detail, "").0, 404);
assert_eq!(call("GET", &detail, "").0, 404);
assert_eq!(call("GET", path, "").1["total"], 0);
}
@@ -0,0 +1,78 @@
use super::*;
pub(super) fn check(port: u16, bearer: &str) {
let path = "/demo/document/admin/content";
let call = |method: &str, path: &str, body: &str| {
request(
port,
method,
path,
"fixture.test",
&format!("{bearer}Content-Type: application/json\r\n"),
body,
)
};
let (status, body) = call("POST", path, r#"{"value": {"count":1.00,"text":"\u96ea"}}"#);
assert_eq!(status, 200, "{body}");
assert!(body.contains("1.00"));
assert!(body.contains(r"\u96ea"));
let created: serde_json::Value = serde_json::from_str(&body).unwrap();
assert!(created["data"]["extra"].is_null());
let detail = format!("{path}/{}", created["data"]["id"].as_i64().unwrap());
for target in [&detail, path] {
let (status, body) = call("GET", target, "");
assert_eq!(status, 200, "{body}");
assert!(body.contains("1.00"));
assert!(body.contains(r"\u96ea"));
let result: serde_json::Value = serde_json::from_str(&body).unwrap();
let row = if target == path {
assert_eq!(result["data"]["total"], 1);
&result["data"]["items"][0]
} else {
&result["data"]
};
assert_eq!(row, &created["data"]);
}
let (status, body) = call("PUT", &detail, r#"{"value":null,"extra":{"ready":true}}"#);
assert_eq!(status, 200, "{body}");
let updated: serde_json::Value = serde_json::from_str(&body).unwrap();
assert!(updated["data"]["value"].is_null());
assert_eq!(updated["data"]["extra"]["ready"], true);
let (status, body) = call("PUT", &detail, r#"{"value":1e9999,"extra":-0}"#);
assert_eq!(status, 200, "{body}");
for target in [&detail, path] {
let (status, body) = call("GET", target, "");
assert_eq!(status, 200, "{body}");
assert!(body.contains("\"value\":1e9999"));
assert!(body.contains("\"extra\":-0"));
}
assert_eq!(call("PUT", &detail, r#"{"value":[],"extra":null}"#).0, 200);
for body in [
r#"{}"#.to_owned(),
r#"[{},null]"#.to_owned(),
r#"{"value":0} null"#.to_owned(),
r#"{"value":{"a":1,"\u0061":2}}"#.to_owned(),
r#"{"value":0,"extra":{"a":1,"a":2}}"#.to_owned(),
r#"{"value":0,"extra":"\ud800"}"#.to_owned(),
r#"{"value":0,"extra":null,"extra":null}"#.to_owned(),
r#"{"value":0,"unknown":null}"#.to_owned(),
format!("{{\"value\":{}{}}}", "[".repeat(64), "]".repeat(64)),
format!(
"{{\"value\":[{}],\"extra\":[0]}}",
vec!["0"; 65534].join(",")
),
] {
assert_eq!(call("POST", path, &body).0, 400);
assert_eq!(call("PUT", &detail, &body).0, 400);
}
assert_eq!(call("DELETE", &detail, "").0, 200);
let body = format!("{{\"value\":{}{}}}", "[".repeat(61), "]".repeat(61));
let (status, body) = call("POST", path, &body);
assert_eq!(status, 200, "{body}");
let created: serde_json::Value = serde_json::from_str(&body).unwrap();
let detail = format!("{path}/{}", created["data"]["id"].as_i64().unwrap());
assert_eq!(call("GET", &detail, "").0, 200);
// 分页比详情多两层容器,必须把响应自身计入深度预算。
assert_eq!(call("GET", path, "").0, 500);
assert_eq!(call("DELETE", &detail, "").0, 200);
}
+159
View File
@@ -0,0 +1,159 @@
use super::{execute, request, token};
use serde_json::{Value, json};
use std::path::Path;
fn get(port: u16, action: &str, credentials: &str) -> (u16, Value) {
let (status, body) = request(
port,
"GET",
&format!("/demo/account/admin/roles/{action}"),
"fixture.test",
credentials,
"",
);
(status, serde_json::from_str(&body).unwrap())
}
fn post(port: u16, action: &str, credentials: &str, input: Value) -> (u16, Value) {
let (status, body) = request(
port,
"POST",
&format!("/demo/account/admin/roles/{action}"),
"fixture.test",
&format!("{credentials}Content-Type: application/json\r\n"),
&input.to_string(),
);
(status, serde_json::from_str(&body).unwrap())
}
pub(super) fn check(binary: &Path, port: u16, owner: &str) {
assert_eq!(get(port, "protected_me?unexpected=1", owner).0, 403);
assert_eq!(
post(port, "save_role", owner, json!({"unexpected":1})).0,
403
);
assert_eq!(get(port, "current_permissions", owner).1["data"], json!([]));
let bootstrap = execute(
binary,
"dever.auth.owner",
r#"{"site":"admin","user_id":1}"#,
);
assert!(
bootstrap.status.success(),
"{}",
String::from_utf8_lossy(&bootstrap.stderr)
);
assert_eq!(get(port, "protected_me", owner).0, 200);
assert_eq!(get(port, "protected_me?unexpected=1", owner).0, 400);
assert_eq!(get(port, "is_owner?user_id=1", owner).1["data"], true);
let directory = get(port, "permissions", owner).1["data"]
.as_array()
.unwrap()
.clone();
assert!(
directory
.iter()
.any(|entry| entry["key"] == "demo.account.admin.protected_me")
);
assert!(
!directory
.iter()
.any(|entry| entry["key"] == "demo.account.admin.me")
);
assert_eq!(
get(port, "current_permissions", owner).1["data"],
json!(directory)
);
for (action, input) in [
("delete_role", json!({"role_id":"owner"})),
("disable_role", json!({"role_id":"owner"})),
("revoke_role", json!({"role_id":"owner","user_id":1})),
("grant_role", json!({"role_id":"owner","user_id":2})),
(
"save_role",
json!({"id":"owner","name":"changed","all_permissions":false,"permission_keys":[]}),
),
] {
assert_eq!(post(port, action, owner, input).0, 403, "{action}");
}
let seeded = execute(
binary,
"demo.account.seed",
r#"{"subject":"reader","session":"reader-session"}"#,
);
assert!(seeded.status.success());
let reader = format!(
"Authorization: Bearer {}\r\n",
token("reader", "reader-session")
);
assert_eq!(get(port, "protected_me", &reader).0, 403);
let role = json!({"id":"reader","name":"Reader","all_permissions":false,"permission_keys":["demo.account.admin.protected_me"]});
assert_eq!(post(port, "save_role", owner, role.clone()).0, 200);
assert_eq!(
post(
port,
"grant_role",
owner,
json!({"user_id":2,"role_id":"reader"})
)
.0,
200
);
assert_eq!(get(port, "protected_me", &reader).0, 200);
assert_eq!(
get(port, "user_roles?user_id=2", &reader).1["data"][0]["permission_keys"],
json!(["demo.account.admin.protected_me"])
);
assert_eq!(get(port, "is_owner?user_id=2", &reader).1["data"], false);
assert_eq!(post(port, "save_role", &reader, role.clone()).0, 403);
let mut invalid = role.clone();
invalid["permission_keys"] = json!(["demo.account.admin.unknown"]);
assert_eq!(post(port, "save_role", owner, invalid).0, 400);
assert_eq!(get(port, "protected_me", &reader).0, 200);
assert_eq!(
post(port, "role_fail", owner, json!({"id":"rolled_back"})).0,
409
);
assert!(get(port, "role?id=rolled_back", owner).1["data"].is_null());
assert_eq!(
post(port, "disable_role", owner, json!({"role_id":"reader"})).0,
200
);
assert_eq!(get(port, "protected_me", &reader).0, 403);
assert_eq!(
get(port, "role?id=reader", owner).1["data"]["enabled"],
false
);
assert_eq!(post(port, "save_role", owner, role).0, 200);
assert_eq!(get(port, "protected_me", &reader).0, 200);
assert_eq!(
post(
port,
"revoke_role",
owner,
json!({"user_id":2,"role_id":"reader"})
)
.0,
200
);
assert_eq!(get(port, "protected_me", &reader).0, 403);
assert_eq!(
post(
port,
"grant_role",
owner,
json!({"user_id":2,"role_id":"reader"})
)
.0,
200
);
assert_eq!(
post(port, "delete_role", owner, json!({"role_id":"reader"})).0,
200
);
assert_eq!(
get(port, "user_roles?user_id=2", owner).1["data"],
json!([])
);
assert_eq!(get(port, "protected_me", &reader).0, 403);
}
+148
View File
@@ -0,0 +1,148 @@
use super::*;
#[test]
fn concurrent_enqueues_preserve_system_and_revalidated_user_identity() {
let directory = Directory::new();
let mut project = Project::load(&fixture().with_file_name("auth")).unwrap();
for source in &mut project.sources {
let text = if source.path.ends_with("app/jobs.dever") {
include_str!("../../../fixtures/job-tasks.dever").into()
} else if source.path.ends_with("api/admin/jobs.dever") {
source
.text
.replace("post job_submit =", "long post job_submit =")
.replace("cmd job_submit", "long cmd job_submit")
} else {
continue;
};
*source = dever::syntax::source::Source::new(source.id, source.path.clone(), text);
}
let program = project.check().unwrap();
let binary = build::build(
&program,
&project.sources,
"main",
&directory.0.join("tasks"),
)
.unwrap();
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
drop(listener);
std::fs::create_dir(directory.0.join("config")).unwrap();
let mut settings = serde_json::json!({
"http":{"host":"127.0.0.1","port":port},
"sites":{"admin":{"path":"admin","auth":"session","hosts":["fixture.test"],"origin":"https://fixture.test"}},
"database":{"default":{"type":"sqlite","path":"data/account.db","max_connections":8}},
"runtime":{"mode":"api","shutdown_ms":1000},
"auth":{"providers":{"session":{"verify":"demo.account.verify","jwtSecret":KEY,"ttlSeconds":60,"cookie":"session"}}},
"job":{"workers":1,"poll_ms":10,"lease_ms":2000}
});
std::fs::write(
directory.0.join("config/setting.json"),
settings.to_string(),
)
.unwrap();
let run = |command: &str, input: &str| {
let result = execute(&binary, command, input);
assert!(
result.status.success(),
"{}",
String::from_utf8_lossy(&result.stderr)
);
serde_json::from_slice::<serde_json::Value>(&result.stdout).unwrap()["data"].clone()
};
run(
"demo.account.seed",
r#"{"subject":"user","session":"valid-session"}"#,
);
run("dever.auth.owner", r#"{"site":"admin","user_id":1}"#);
let mut server = Server(Command::new(&binary).spawn().unwrap());
let deadline = Instant::now() + Duration::from_secs(5);
while TcpStream::connect(("127.0.0.1", port)).is_err() {
assert!(server.0.try_wait().unwrap().is_none());
assert!(Instant::now() < deadline);
std::thread::sleep(Duration::from_millis(10));
}
let bearer = format!(
"Authorization: Bearer {}\r\nContent-Type: application/json\r\n",
token("user", "valid-session")
);
let forged = request(
port,
"POST",
"/demo/account/admin/jobs/job_submit",
"fixture.test",
&bearer,
r#"{"subject":"forged","key":"forged","execution":{"kind":"System"}}"#,
);
assert_eq!(forged.0, 400, "{}", forged.1);
let submit = |subject: &str| {
let (status, body) = request(
port,
"POST",
"/demo/account/admin/jobs/job_submit",
"fixture.test",
&bearer,
&serde_json::json!({"subject":subject,"key":subject}).to_string(),
);
assert_eq!(status, 200, "{body}");
};
// payload 的 subject 与登录用户不同,不能用它替换请求建立的可信执行者。
submit("other-person");
run(
"demo.account.job_submit",
r#"{"subject":"system","key":"system"}"#,
);
settings["runtime"]["mode"] = "worker".into();
let worker_config = directory.0.join("worker.json");
std::fs::write(&worker_config, settings.to_string()).unwrap();
let start = || {
Server(
Command::new(&binary)
.arg("--config")
.arg(&worker_config)
.spawn()
.unwrap(),
)
};
let until = |worker: &mut Server, expected: i64| {
let deadline = Instant::now() + Duration::from_secs(10);
loop {
let actual = run("demo.account.job_count", "{}");
if actual == expected {
break;
}
assert!(worker.0.try_wait().unwrap().is_none());
assert!(
Instant::now() < deadline,
"concurrent Job did not complete: expected {expected}, got {actual}"
);
std::thread::sleep(Duration::from_millis(10));
}
};
let mut worker = start();
until(&mut worker, 13);
assert!(
Command::new("sh")
.args(["-c", "kill -TERM \"$1\"", "--", &worker.0.id().to_string()])
.status()
.unwrap()
.success()
);
assert!(worker.0.wait().unwrap().success());
submit("revoked");
run("demo.account.revoke", r#"{"id":1}"#);
run(
"demo.account.job_submit",
r#"{"subject":"system-next","key":"system-next"}"#,
);
let mut worker = start();
until(&mut worker, 19);
std::thread::sleep(Duration::from_millis(100));
assert_eq!(
run("demo.account.job_count", "{}"),
19,
"revoked User was upgraded to System"
);
}