Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -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:"));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user