feat: publish Dever core with project scaffolding and versioned AI skills
This commit is contained in:
1 parent
70fa73cdc2
commit
ac458ffa7f
960 files changed
+221319
-1163
No files matched your search
@@ -0,0 +1,252 @@
|
||||
//! HTTP enqueues trusted references; the later worker must recheck current grants.
|
||||
use super::{
|
||||
native::{Server, application_ir},
|
||||
process, security, sqlite_inspect,
|
||||
};
|
||||
use dever_backend_bridge::{Target, emit_object};
|
||||
use dever_runtime::{config::Settings, orm::Value as SqlValue};
|
||||
use serde_json::{Value, json};
|
||||
use std::fs;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
fn files() -> Vec<(String, String)> {
|
||||
let mut files = security::files()
|
||||
.into_iter()
|
||||
.map(|(path, text)| (path.to_owned(), text.to_owned()))
|
||||
.collect::<Vec<_>>();
|
||||
files.push((
|
||||
"news/article/model/delivery.dever".into(),
|
||||
"type Delivery { name: Text }".into(),
|
||||
));
|
||||
files
|
||||
.iter_mut()
|
||||
.find(|(path, _)| path == "news/article/app.dever")
|
||||
.unwrap()
|
||||
.1
|
||||
.push_str(
|
||||
r#"
|
||||
type Delivery { name: Text }
|
||||
submit(name: Text) (id: Id) {
|
||||
id = dever.job.enqueue(job.publish, Delivery { name = name }, name)
|
||||
}
|
||||
deliver(input: Delivery) () {
|
||||
saved = model.delivery.create({ name = input.name })
|
||||
}
|
||||
"#,
|
||||
);
|
||||
files
|
||||
.iter_mut()
|
||||
.find(|(path, _)| path == "news/article/api/admin/manage.dever")
|
||||
.unwrap()
|
||||
.1
|
||||
.push_str("\npost submit = app.submit");
|
||||
files.push(("news/article/job.dever".into(), "database default\njob publish(input: app.Delivery) () retry(2) timeout(1000) { app.deliver(input) }".into()));
|
||||
files
|
||||
}
|
||||
|
||||
fn configuration() -> Value {
|
||||
let mut value = security::configuration();
|
||||
value["database"]["default"]["max_connections"] = 3.into();
|
||||
value["job"] = json!({"workers": 2, "poll_ms": 10, "lease_ms": 3000, "retry_base_ms": 20, "retry_max_ms": 100});
|
||||
value
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn job_application_auth_and_tenant_roots_emit_all_six_targets() {
|
||||
let files = files();
|
||||
let files = files
|
||||
.iter()
|
||||
.map(|(path, source)| (path.as_str(), source.as_str()))
|
||||
.collect::<Vec<_>>();
|
||||
let ir = application_ir(&files, &configuration().to_string(), false, None);
|
||||
for target in Target::ALL {
|
||||
assert!(!emit_object(&ir, target).unwrap().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
fn queue_settled(server: &Server, tenant: i64) {
|
||||
let path = server
|
||||
.directory
|
||||
.path()
|
||||
.join(format!("data/tenants/tenant_{tenant}.db"));
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
loop {
|
||||
let rows = sqlite_inspect::query(
|
||||
&path,
|
||||
"SELECT COUNT(*) FROM _dever_jobs WHERE state IN ('pending','running')",
|
||||
);
|
||||
if rows == [vec![SqlValue::Int(0)]] {
|
||||
return;
|
||||
}
|
||||
assert!(
|
||||
Instant::now() < deadline,
|
||||
"tenant {tenant} jobs did not settle: {rows:?}\n{}",
|
||||
server.stderr()
|
||||
);
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires explicit runtime archive; owned loopback API/worker and two tenant SQLite databases"]
|
||||
fn queued_users_recheck_roles_sessions_catalog_and_tenant_components() {
|
||||
let owned_files = files();
|
||||
let files = owned_files
|
||||
.iter()
|
||||
.map(|(path, source)| (path.as_str(), source.as_str()))
|
||||
.collect::<Vec<_>>();
|
||||
let mut server = Server::start_with(&files, 0, configuration(), &security::tenant_bootstrap());
|
||||
server.ready_at("/sample/echo/test/probe/increment?q=1");
|
||||
let settings = Settings::load_project(server.directory.path()).unwrap();
|
||||
let owner_one = security::credential(&settings, "admin", "owner", "active", "one");
|
||||
let owner_two = security::credential(&settings, "admin", "owner", "active", "two");
|
||||
let reader = security::credential(&settings, "admin", "reader", "active", "one");
|
||||
let manage = "/news/article/admin/manage";
|
||||
security::request(
|
||||
&server,
|
||||
"POST",
|
||||
&format!("{manage}/role"),
|
||||
&owner_one,
|
||||
json!({"id": "publisher", "keys": ["news.article.admin.submit"]}),
|
||||
200,
|
||||
);
|
||||
security::request(
|
||||
&server,
|
||||
"POST",
|
||||
&format!("{manage}/grant"),
|
||||
&owner_one,
|
||||
json!({"user_id": 2, "role_id": "publisher"}),
|
||||
200,
|
||||
);
|
||||
for (name, cookie) in [
|
||||
("allowed-one", &owner_one),
|
||||
("allowed-two", &owner_two),
|
||||
("revoked", &reader),
|
||||
("stale-session", &owner_one),
|
||||
("removed-permission", &owner_one),
|
||||
("missing-subject", &owner_one),
|
||||
("wrong-tenant", &owner_one),
|
||||
] {
|
||||
security::request(
|
||||
&server,
|
||||
"POST",
|
||||
&format!("{manage}/submit"),
|
||||
cookie,
|
||||
json!({"name": name}),
|
||||
200,
|
||||
);
|
||||
}
|
||||
security::request(
|
||||
&server,
|
||||
"POST",
|
||||
&format!("{manage}/revoke"),
|
||||
&owner_one,
|
||||
json!({"user_id": 2, "role_id": "publisher"}),
|
||||
200,
|
||||
);
|
||||
server.stop_cycle(1);
|
||||
server.finish();
|
||||
|
||||
let first = server.directory.path().join("data/tenants/tenant_1.db");
|
||||
let second = server.directory.path().join("data/tenants/tenant_2.db");
|
||||
assert_eq!(
|
||||
sqlite_inspect::query(
|
||||
&first,
|
||||
"SELECT DISTINCT auth_permission,execution_kind,state FROM _dever_jobs"
|
||||
),
|
||||
[vec![
|
||||
SqlValue::Text("news.article.admin.submit".into()),
|
||||
SqlValue::Text("user".into()),
|
||||
SqlValue::Text("pending".into())
|
||||
]]
|
||||
);
|
||||
// Keep stored references structurally valid so dispatch must reverify them.
|
||||
// Corrupt storage ownership is rejected earlier by the shared queue store.
|
||||
for sql in [
|
||||
"UPDATE _dever_jobs SET auth_session='stale' WHERE dedupe_key='business:stale-session'",
|
||||
"UPDATE _dever_jobs SET auth_permission='news.article.admin.removed' WHERE dedupe_key='business:removed-permission'",
|
||||
"UPDATE _dever_jobs SET auth_subject='deleted-account' WHERE dedupe_key='business:missing-subject'",
|
||||
"UPDATE _dever_jobs SET auth_tenant='two' WHERE dedupe_key='business:wrong-tenant'",
|
||||
] {
|
||||
sqlite_inspect::execute(&first, sql);
|
||||
}
|
||||
|
||||
server.restart("all");
|
||||
server.ready_at("/sample/echo/test/probe/increment?q=1");
|
||||
queue_settled(&server, 1);
|
||||
queue_settled(&server, 2);
|
||||
assert_eq!(
|
||||
sqlite_inspect::query(&first, "SELECT name FROM delivery"),
|
||||
[vec![SqlValue::Text("allowed-one".into())]]
|
||||
);
|
||||
assert_eq!(
|
||||
sqlite_inspect::query(&second, "SELECT name FROM delivery"),
|
||||
[vec![SqlValue::Text("allowed-two".into())]]
|
||||
);
|
||||
let blocked = sqlite_inspect::query(
|
||||
&first,
|
||||
"SELECT dedupe_key,state,attempt,error FROM _dever_jobs WHERE dedupe_key!='business:allowed-one' ORDER BY dedupe_key",
|
||||
);
|
||||
assert_eq!(blocked.len(), 5);
|
||||
assert!(
|
||||
blocked.iter().all(|row| row[1..]
|
||||
== [
|
||||
SqlValue::Text("blocked".into()),
|
||||
SqlValue::Int(1),
|
||||
SqlValue::Text("identity_rejected".into())
|
||||
]),
|
||||
"{blocked:?}"
|
||||
);
|
||||
server.stop_cycle(1);
|
||||
server.finish();
|
||||
|
||||
// A fresh API-only root enqueues while enabled. The same stored user must
|
||||
// be rejected after the platform disables that tenant's component.
|
||||
server.restart("api");
|
||||
server.ready_at("/sample/echo/test/probe/increment?q=1");
|
||||
security::request(
|
||||
&server,
|
||||
"POST",
|
||||
&format!("{manage}/submit"),
|
||||
&owner_one,
|
||||
json!({"name": "disabled"}),
|
||||
200,
|
||||
);
|
||||
server.stop_cycle(1);
|
||||
server.finish();
|
||||
let status = process::status(
|
||||
Command::new(server.directory.path().join("managed-kernel"))
|
||||
.env_clear()
|
||||
.args(["--dever-tenant-component", "1", "disable", "news"])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::inherit()),
|
||||
Duration::from_secs(10),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(status.success());
|
||||
server.restart("worker");
|
||||
queue_settled(&server, 1);
|
||||
server.stop_cycle(1);
|
||||
server.finish();
|
||||
assert_eq!(
|
||||
sqlite_inspect::query(
|
||||
&first,
|
||||
"SELECT state,error FROM _dever_jobs WHERE dedupe_key='business:disabled'"
|
||||
),
|
||||
[vec![
|
||||
SqlValue::Text("blocked".into()),
|
||||
SqlValue::Text("component_disabled".into())
|
||||
]]
|
||||
);
|
||||
assert_eq!(
|
||||
sqlite_inspect::query(&first, "SELECT COUNT(*) FROM delivery"),
|
||||
[vec![SqlValue::Int(1)]]
|
||||
);
|
||||
assert!(
|
||||
!fs::read_to_string(server.directory.path().join("stderr"))
|
||||
.unwrap()
|
||||
.contains("panicked")
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user