use std::collections::BTreeMap;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use notedthat_core::{AccessPolicy, Verb, Who};
use tower::ServiceExt;
use super::fixture::{
TOKEN, app, grant, grant_under, json, listed_keys, policy, signed_in_everything,
};
fn notes(
rules: impl IntoIterator<Item = notedthat_core::AccessRule>,
) -> BTreeMap<String, AccessPolicy> {
BTreeMap::from([("notes".to_string(), policy(rules))])
}
async fn keys_for(app: axum::Router, uri: &str, token: Option<&str>) -> Vec<String> {
let mut builder = Request::builder().uri(uri);
if let Some(token) = token {
builder = builder.header("authorization", format!("Bearer {token}"));
}
let response = app
.oneshot(builder.body(Body::empty()).expect("request"))
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
listed_keys(response).await
}
#[tokio::test]
async fn a_listing_shows_every_key_the_credential_holder_may_see() {
let app = app(notes([signed_in_everything()])).await;
let keys = keys_for(app, "/api/v1/knowledgebases/notes", Some(TOKEN)).await;
assert!(keys.contains(&".notedthat/manifest.json".to_string()));
assert!(keys.contains(&"internal/secret.md".to_string()));
}
#[tokio::test]
async fn a_prefix_scoped_list_grant_shows_only_keys_under_that_prefix() {
let app = app(notes([grant_under(
Who::Anyone,
[Verb::List],
&["public/**"],
)]))
.await;
let keys = keys_for(app, "/api/v1/knowledgebases/notes", None).await;
assert_eq!(
keys,
vec![
"public/deep/note.md".to_string(),
"public/index.md".to_string()
],
"only the granted subtree, and `public.md` is not under `public/`"
);
}
#[tokio::test]
async fn a_listing_drops_the_internal_namespace_for_an_anonymous_caller() {
let app = app(notes([grant(Who::Anyone, [Verb::List])])).await;
let keys = keys_for(app, "/api/v1/knowledgebases/notes", None).await;
assert!(!keys.is_empty());
assert!(
!keys.iter().any(|key| key.starts_with(".notedthat")),
"{keys:?}"
);
}
#[tokio::test]
async fn a_caller_prefix_outside_the_granted_scope_lists_nothing() {
let app = app(notes([grant_under(
Who::Anyone,
[Verb::List],
&["public/**"],
)]))
.await;
let response = app
.oneshot(
Request::builder()
.uri("/api/v1/knowledgebases/notes?prefix=internal/")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
let body = json(response).await;
assert_eq!(body["objects"], serde_json::json!([]));
assert_eq!(body["truncated"], serde_json::json!(false));
assert_eq!(body["next_cursor"], serde_json::Value::Null);
}
#[tokio::test]
async fn a_caller_prefix_inside_the_granted_scope_narrows_further() {
let app = app(notes([grant_under(
Who::Anyone,
[Verb::List],
&["public/**"],
)]))
.await;
let keys = keys_for(
app,
"/api/v1/knowledgebases/notes?prefix=public/deep/",
None,
)
.await;
assert_eq!(keys, vec!["public/deep/note.md".to_string()]);
}
#[tokio::test]
async fn listing_is_refused_without_a_list_grant() {
let app = app(notes([grant(Who::Anyone, [Verb::Read])])).await;
let response = app
.oneshot(
Request::builder()
.uri("/api/v1/knowledgebases/notes")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn an_anonymous_denial_is_indistinguishable_from_an_undeclared_slug() {
let app = app(notes([signed_in_everything()])).await;
let hidden = app
.clone()
.oneshot(
Request::builder()
.uri("/api/v1/knowledgebases/notes")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
let undeclared = app
.oneshot(
Request::builder()
.uri("/api/v1/knowledgebases/nope")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(hidden.status(), StatusCode::NOT_FOUND);
assert_eq!(undeclared.status(), StatusCode::NOT_FOUND);
let hidden = super::fixture::json(hidden).await;
let undeclared = super::fixture::json(undeclared).await;
assert_eq!(hidden["error"], undeclared["error"]);
assert_eq!(
hidden["message"]
.as_str()
.expect("message")
.replace("notes", "<slug>"),
undeclared["message"]
.as_str()
.expect("message")
.replace("nope", "<slug>"),
);
}
#[tokio::test]
async fn listing_an_undeclared_knowledge_base_is_not_found_rather_than_unauthorized() {
let app = app(notes([signed_in_everything()])).await;
let response = app
.oneshot(
Request::builder()
.uri("/api/v1/knowledgebases/nope")
.header("authorization", format!("Bearer {TOKEN}"))
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn a_deny_under_a_prefix_hides_those_keys_from_a_whole_kb_listing() {
let app = app(notes([
signed_in_everything(),
notedthat_core::AccessRule::deny(Who::SignedIn, [Verb::List]).under(
notedthat_core::KeyPattern::parse("internal/**")
.map(|p| vec![p])
.expect("pattern"),
),
]))
.await;
let keys = keys_for(app, "/api/v1/knowledgebases/notes", Some(TOKEN)).await;
assert!(!keys.contains(&"internal/secret.md".to_string()));
assert!(keys.contains(&"public/index.md".to_string()));
assert!(keys.contains(&".notedthat/manifest.json".to_string()));
}
#[tokio::test]
async fn a_user_identity_never_sees_the_internal_namespace_in_a_listing() {
let app = app(notes([signed_in_everything()])).await;
let keys = keys_for(
app,
"/api/v1/knowledgebases/notes",
Some(super::fixture::ALICE_TOKEN),
)
.await;
assert!(
!keys.iter().any(|key| key.starts_with(".notedthat")),
"{keys:?}"
);
assert!(keys.contains(&"internal/secret.md".to_string()));
}