use super::*;
fn permissioned_function(name: &str, permission: &str) -> BoxedFunction {
struct Permissioned {
name: String,
permission: String,
}
impl Function for Permissioned {
fn manifest(&self) -> FunctionManifest {
FunctionManifest {
name: RString::from(self.name.as_str()),
version: RString::from("0.0.0"),
description: RString::from("permissioned test function"),
visibility: Visibility::Private,
role: RString::new(),
method: HttpMethod::Post,
permission: RString::from(self.permission.as_str()),
admin: RString::new(),
config_schema: RString::new(),
input_schema: RString::new(),
output_schema: RString::new(),
}
}
fn invoke(
&self,
_host: HostApi_TO<'_, RBox<()>>,
_input: RStr<'_>,
) -> RResult<RString, RString> {
RResult::ROk(RString::from(json!({ "ran": true }).to_string()))
}
}
Function_TO::from_value(
Permissioned {
name: name.to_string(),
permission: permission.to_string(),
},
TD_Opaque,
)
}
#[ntex::test]
async fn function_permissions_gate_endpoints_by_membership_and_role() {
let db = TempDatabase::create("fnperms").await;
let root = temp_dir("fnperms");
write_files(
&root,
&[(
"main.toml",
&format!(
"\n[server]\nbase_path = \"/api\"\n\n[database]\nurl = \"{}\"\n",
db.url
),
)],
);
let state = load_state_with(
&root,
vec![
permissioned_function("open", "public"),
permissioned_function("signed_in", "authenticated"),
permissioned_function("colleagues", "member"),
permissioned_function("bosses", "role:admin"),
permissioned_function("nobody", "private"),
],
)
.await;
let app = init_http_app!(state);
let call = |uri: &str, token: Option<&str>, org: Option<&str>| {
let mut request = test::TestRequest::post()
.uri(uri)
.header(CONTENT_TYPE, "application/json")
.set_payload("{}".to_string());
if let Some(token) = token {
request = request.header("authorization", format!("Bearer {token}"));
}
if let Some(org) = org {
request = request.header("x-organization", org.to_string());
}
request.to_request()
};
assert_eq!(
test::call_service(&app, call("/api/functions/open", None, None))
.await
.status()
.as_u16(),
200
);
assert_eq!(
test::call_service(&app, call("/api/functions/signed_in", None, None))
.await
.status()
.as_u16(),
401
);
assert_eq!(
test::call_service(&app, call("/api/functions/colleagues", None, None))
.await
.status()
.as_u16(),
401
);
assert_eq!(
test::call_service(&app, call("/api/functions/nobody", None, None))
.await
.status()
.as_u16(),
404
);
let registered = read_json(
test::call_service(
&app,
req_json(
"POST",
"/api/auth/register",
json!({"email":"ana@example.com","password":"pw"}),
),
)
.await,
)
.await;
let ana = registered["token"].as_str().unwrap().to_string();
assert_eq!(
test::call_service(&app, call("/api/functions/signed_in", Some(&ana), None))
.await
.status()
.as_u16(),
200
);
assert_eq!(
test::call_service(&app, call("/api/functions/colleagues", Some(&ana), None))
.await
.status()
.as_u16(),
403
);
let org = read_json(
test::call_service(
&app,
bearer(
test::TestRequest::post()
.uri("/api/organization")
.header(CONTENT_TYPE, "application/json")
.set_payload(json!({"name":"Alpha","slug":"alpha"}).to_string()),
&ana,
)
.to_request(),
)
.await,
)
.await;
let org_id = org["id"].as_str().unwrap().to_string();
for name in ["colleagues", "bosses"] {
assert_eq!(
test::call_service(
&app,
call(&format!("/api/functions/{name}"), Some(&ana), Some(&org_id))
)
.await
.status()
.as_u16(),
200,
"an admin should be able to call {name}"
);
}
let joined = read_json(
test::call_service(
&app,
req_json(
"POST",
"/api/auth/register",
json!({"email":"ben@example.com","password":"pw"}),
),
)
.await,
)
.await;
let ben = joined["token"].as_str().unwrap().to_string();
let ben_id = joined["user"]["id"].as_str().unwrap().to_string();
test::call_service(
&app,
bearer(
test::TestRequest::post()
.uri("/api/membership")
.header(CONTENT_TYPE, "application/json")
.header("x-organization", org_id.as_str())
.set_payload(json!({"user_id":ben_id,"role":"member"}).to_string()),
&ana,
)
.to_request(),
)
.await;
assert_eq!(
test::call_service(
&app,
call("/api/functions/colleagues", Some(&ben), Some(&org_id))
)
.await
.status()
.as_u16(),
200
);
assert_eq!(
test::call_service(
&app,
call("/api/functions/bosses", Some(&ben), Some(&org_id))
)
.await
.status()
.as_u16(),
403
);
fs::remove_dir_all(root).unwrap();
db.cleanup().await;
}
fn streaming_function(name: &str) -> BoxedFunction {
struct Streaming {
name: String,
}
impl Function for Streaming {
fn manifest(&self) -> FunctionManifest {
FunctionManifest {
name: RString::from(self.name.as_str()),
version: RString::from("0.0.0"),
description: RString::from("streaming test function"),
visibility: Visibility::Public,
role: RString::new(),
method: HttpMethod::Post,
permission: RString::from("public"),
admin: RString::new(),
config_schema: RString::new(),
input_schema: RString::new(),
output_schema: RString::new(),
}
}
fn invoke(
&self,
host: HostApi_TO<'_, RBox<()>>,
_input: RStr<'_>,
) -> RResult<RString, RString> {
let mut delivered = 0;
for chunk in ["one ", "two ", "three"] {
if host.emit(RStr::from_str(chunk)) {
delivered += 1;
}
}
RResult::ROk(RString::from(json!({ "chunks": delivered }).to_string()))
}
}
Function_TO::from_value(
Streaming {
name: name.to_string(),
},
TD_Opaque,
)
}
#[ntex::test]
async fn a_function_streams_what_it_emits_and_ends_with_what_it_returned() {
let db = TempDatabase::create("fnstream").await;
let root = temp_dir("fnstream");
write_files(
&root,
&[(
"main.toml",
&format!(
"\n[server]\nbase_path = \"/api\"\n\n[database]\nurl = \"{}\"\n",
db.url
),
)],
);
let state = load_state_with(&root, vec![streaming_function("narrate")]).await;
let app = init_http_app!(state);
let post = |uri: &str| {
test::TestRequest::post()
.uri(uri)
.header(CONTENT_TYPE, "application/json")
.set_payload("{}".to_string())
.to_request()
};
let response = test::call_service(&app, post("/api/functions/narrate/stream")).await;
assert_eq!(response.status().as_u16(), 200);
assert_eq!(
response
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok()),
Some("text/event-stream")
);
let body = String::from_utf8(test::read_body(response).await.to_vec()).unwrap();
assert_eq!(
body,
"event: delta\ndata: {\"text\":\"one \"}\n\n\
event: delta\ndata: {\"text\":\"two \"}\n\n\
event: delta\ndata: {\"text\":\"three\"}\n\n\
event: done\ndata: {\"result\":{\"chunks\":3}}\n\n"
);
let response = test::call_service(&app, post("/api/functions/narrate")).await;
assert_eq!(response.status().as_u16(), 200);
assert_eq!(read_json(response).await, json!({ "chunks": 3 }));
assert_eq!(
test::call_service(&app, post("/api/functions/nope/stream"))
.await
.status()
.as_u16(),
404
);
fs::remove_dir_all(root).unwrap();
db.cleanup().await;
}