johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
//! MCP over HTTP against a reference PDS: an assistant registers, its user
//! signs in with their account, and it works as them with the token it gets.
#[path = "../../knotless/tests/common/mod.rs"]
mod common;
use common::*;
use knotless_mcp::http::{Event, Limits, Mcp, router};
use reqwest::blocking::{Client, Response};
use serde_json::{Value, json};
use std::collections::HashMap;
use std::io::{BufRead, BufReader, Write};
use std::sync::{Arc, Mutex, mpsc};
struct Server {
/// Where tests reach it, and where PDSes and assistants do.
base: String,
url: String,
_rt: tokio::runtime::Runtime,
_proxy: Option<TlsProxy>,
/// What it told its operator's counts, as `name subject ok did`.
events: Arc<Mutex<Vec<String>>>,
}
/// The MCP server, turning away the accounts `refused` names and keeping
/// what it needs in `s`'s directories.
fn server(s: &Sandbox, refused: Vec<String>) -> Server {
serve(|url: &str| {
let mcp = Mcp::new(&ctx(s), url, Limits::default()).unwrap();
mcp.refuse(Arc::new(move |did| refused.iter().any(|r| r == did)));
mcp
})
}
/// The MCP server `mcp` makes for its public URL, served.
fn serve(mcp: impl FnOnce(&str) -> Arc<Mcp>) -> Server {
let rt = tokio::runtime::Runtime::new().unwrap();
let listener = rt
.block_on(tokio::net::TcpListener::bind("127.0.0.1:0"))
.unwrap();
let port = listener.local_addr().unwrap().port();
// PDSes reach their clients over https.
let proxy = tls_proxy(port);
let mcp = mcp(&format!("{}/", proxy.url));
let events = Arc::new(Mutex::new(Vec::new()));
let told = events.clone();
mcp.observe(Arc::new(move |e: Event| {
let did = e.did.unwrap_or("-");
let line = format!("{} {} {} {did}", e.name, e.subject, e.ok);
told.lock().unwrap().push(line);
}));
rt.spawn(async move { axum::serve(listener, router(mcp)).await.unwrap() });
Server {
base: format!("http://127.0.0.1:{port}"),
url: proxy.url.clone(),
_rt: rt,
_proxy: Some(proxy),
events,
}
}
fn client() -> Client {
Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.unwrap()
}
fn location(res: &Response) -> String {
res.headers()["location"].to_str().unwrap().to_string()
}
fn query(url: &str) -> HashMap<String, String> {
reqwest::Url::parse(url)
.unwrap()
.query_pairs()
.into_owned()
.collect()
}
/// An assistant's loopback redirect, and the query the browser brings it.
fn assistant() -> (String, mpsc::Receiver<HashMap<String, String>>) {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let uri = format!(
"http://127.0.0.1:{}/back",
listener.local_addr().unwrap().port()
);
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
for stream in listener.incoming() {
let mut stream = stream.unwrap();
let mut line = String::new();
BufReader::new(&stream).read_line(&mut line).unwrap();
let target = line.split(' ').nth(1).unwrap_or_default();
stream
.write_all(b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\nconnection: close\r\n\r\n")
.unwrap();
let _ = tx.send(query(&format!("http://127.0.0.1{target}")));
}
});
(uri, rx)
}
/// Approves (or rejects) the sign-in at `url` as `account`, in a browser.
fn approve(account: &Account, extra: &str, url: &str) {
let ok = std::process::Command::new("sh")
.arg("-c")
.arg(format!("{} '{url}'", browser(account, extra)))
.status()
.unwrap()
.success();
assert!(ok);
}
fn register(s: &Server, body: Value) -> (u16, Value) {
let res = client()
.post(format!("{}/mcp/register", s.base))
.json(&body)
.send()
.unwrap();
(res.status().as_u16(), res.json().unwrap())
}
fn token(s: &Server, form: &[(&str, &str)]) -> (u16, Value) {
let res = client()
.post(format!("{}/mcp/token", s.base))
.form(form)
.send()
.unwrap();
(res.status().as_u16(), res.json().unwrap())
}
/// The sign-in form's signed request, from its page.
fn ask(page: &str) -> String {
let at = page.find("name=ask value=\"").unwrap() + 16;
page[at..].split('"').next().unwrap().to_string()
}
/// The authorization page for `params`.
fn authorize(s: &Server, params: &[(&str, &str)]) -> Response {
client()
.get(format!("{}/mcp/authorize", s.base))
.query(params)
.send()
.unwrap()
}
fn sign_in(s: &Server, ask: &str, handle: &str) -> Response {
client()
.post(format!("{}/mcp/authorize", s.base))
.form(&[("ask", ask), ("handle", handle)])
.send()
.unwrap()
}
/// A JSON-RPC call to `/mcp` with `bearer`.
fn rpc(s: &Server, bearer: Option<&str>, method: &str, params: Value) -> (u16, Value) {
let mut req = client()
.post(format!("{}/mcp", s.base))
.header("accept", "application/json, text/event-stream")
.json(&json!({"jsonrpc": "2.0", "id": 1, "method": method, "params": params}));
if let Some(t) = bearer {
req = req.bearer_auth(t);
}
let res = req.send().unwrap();
let status = res.status().as_u16();
(status, res.json().unwrap_or(Value::Null))
}
#[test]
fn an_assistant_signs_in_and_works_as_its_user() {
let s = Sandbox::new();
let allowed = account();
let other = account();
let srv = server(&s, vec![other.did.clone()]);
// Discovery, from the 401 on.
let (status, _) = rpc(&srv, None, "tools/list", json!({}));
assert_eq!(status, 401);
let res = client().post(format!("{}/mcp", srv.base)).send().unwrap();
let challenge = res.headers()["www-authenticate"].to_str().unwrap();
assert!(
challenge.contains(&format!(
"{}/.well-known/oauth-protected-resource/mcp",
srv.url
)),
"{challenge}"
);
for path in ["oauth-protected-resource", "oauth-protected-resource/mcp"] {
let meta: Value = client()
.get(format!("{}/.well-known/{path}", srv.base))
.send()
.unwrap()
.json()
.unwrap();
assert_eq!(meta["resource"], format!("{}/mcp", srv.url));
assert_eq!(meta["authorization_servers"][0], srv.url);
}
let meta: Value = client()
.get(format!(
"{}/.well-known/oauth-authorization-server",
srv.base
))
.send()
.unwrap()
.json()
.unwrap();
assert_eq!(meta["token_endpoint"], format!("{}/mcp/token", srv.url));
// Registration: https, or http on the assistant's own machine.
let (redirect, back) = assistant();
for bad in [
json!({}),
json!({"redirect_uris": ["http://elsewhere.test/back"]}),
json!({"redirect_uris": ["https://ok.test/back", "https://ok.test/#frag"]}),
json!({"redirect_uris": ["not a url"]}),
] {
let (status, body) = register(&srv, bad);
assert_eq!(status, 400);
assert_eq!(body["error"], "invalid_redirect_uri");
}
let (status, reg) = register(
&srv,
json!({
"redirect_uris": [redirect],
"client_name": "Claude <b>",
"token_endpoint_auth_method": "client_secret_post",
}),
);
assert_eq!(status, 201, "{reg}");
assert_eq!(reg["token_endpoint_auth_method"], "client_secret_post");
let id = reg["client_id"].as_str().unwrap().to_string();
let secret = reg["client_secret"].as_str().unwrap().to_string();
// What the authorization page refuses: to the user when it cannot
// trust where to send them back, else to the assistant.
let verifier = "a-verifier-long-enough-for-pkce-0123456789";
let challenge = knotless::oauth::sha256(verifier);
let mut params = vec![
("client_id", id.as_str()),
("response_type", "code"),
("code_challenge", challenge.as_str()),
("code_challenge_method", "S256"),
("state", "st"),
];
assert_eq!(authorize(&srv, &[("client_id", "nope")]).status(), 400);
let res = authorize(
&srv,
&[("client_id", &id), ("redirect_uri", "https://x.test/")],
);
assert_eq!(res.status(), 400);
let refused = |extra: (&str, &str)| {
let mut p = params.clone();
p.retain(|(k, _)| *k != extra.0);
p.push(extra);
let res = authorize(&srv, &p);
assert_eq!(res.status(), 303);
let q = query(&location(&res));
assert_eq!(q["state"], "st");
assert_eq!(q["iss"], srv.url);
q["error"].clone()
};
assert_eq!(
refused(("response_type", "token")),
"unsupported_response_type"
);
assert_eq!(
refused(("code_challenge_method", "plain")),
"invalid_request"
);
assert_eq!(
refused(("resource", "https://elsewhere.test/mcp")),
"invalid_target"
);
let resource = format!("{}/mcp/", srv.url);
params.push(("resource", &resource));
// The user says who they are; one turned away goes no further.
let page = authorize(&srv, ¶ms).text().unwrap();
assert!(
page.contains("Connect Claude <b> to knotless"),
"{page}"
);
let signed = ask(&page);
assert_eq!(sign_in(&srv, "forged", &allowed.handle).status(), 400);
let res = sign_in(&srv, &signed, "nobody.invalid");
assert_eq!(res.status(), 400);
assert!(
res.text()
.unwrap()
.contains("No account answers to nobody.invalid")
);
let res = sign_in(&srv, &signed, &other.handle);
assert_eq!(res.status(), 403);
// A sign-in the user walks away from waits, among others.
assert_eq!(sign_in(&srv, &signed, &allowed.handle).status(), 303);
// A refusal at the PDS goes back to the assistant.
let res = sign_in(&srv, &signed, &format!("@{}", allowed.handle));
assert_eq!(res.status(), 303);
approve(&allowed, "--reject", &location(&res));
let q = back.recv().unwrap();
assert_eq!(q["error"], "access_denied");
assert_eq!(q["state"], "st");
// Approved: the assistant gets a code, and trades it for tokens.
let res = sign_in(&srv, &signed, &allowed.handle);
approve(&allowed, "", &location(&res));
let q = back.recv().unwrap();
assert_eq!(q["state"], "st");
assert_eq!(q["iss"], srv.url);
let code = q["code"].clone();
let form = |extra: &[(&'static str, &str)]| {
let mut f: Vec<(&str, String)> = vec![
("grant_type", "authorization_code".into()),
("client_id", id.clone()),
("client_secret", secret.clone()),
("code", code.clone()),
("code_verifier", verifier.into()),
("redirect_uri", redirect.clone()),
];
for (k, v) in extra {
f.retain(|(key, _)| key != k);
f.push((k, v.to_string()));
}
f
};
let send = |f: Vec<(&str, String)>| {
let f: Vec<(&str, &str)> = f.iter().map(|(k, v)| (*k, v.as_str())).collect();
token(&srv, &f)
};
let (status, body) = send(form(&[("client_secret", "bm9wZQ")]));
assert_eq!(
(status, body["error"].as_str()),
(401, Some("invalid_client"))
);
let (status, _) = send(form(&[("client_id", "nope")]));
assert_eq!(status, 401);
let (status, body) = send(form(&[("grant_type", "password")]));
assert_eq!(
(status, body["error"].as_str()),
(400, Some("unsupported_grant_type"))
);
let (status, tokens) = send(form(&[]));
assert_eq!(status, 200, "{tokens}");
assert_eq!(tokens["token_type"], "Bearer");
// A code is good once.
let (status, body) = send(form(&[]));
assert_eq!(
(status, body["error"].as_str()),
(400, Some("invalid_grant"))
);
let access = tokens["access_token"].as_str().unwrap().to_string();
let refresh = tokens["refresh_token"].as_str().unwrap().to_string();
// The tools, as the user.
let (status, init) = rpc(
&srv,
Some(&access),
"initialize",
json!({"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "t", "version": "1"}}),
);
assert_eq!(status, 200, "{init}");
assert!(
init["result"]["instructions"]
.as_str()
.unwrap()
.contains("handle/name")
);
assert!(init["result"]["capabilities"]["resources"].is_object());
let (_, list) = rpc(&srv, Some(&access), "tools/list", json!({}));
assert!(list.to_string().contains("repo_create"), "{list}");
let (_, made) = rpc(
&srv,
Some(&access),
"tools/call",
json!({"name": "repo_create", "arguments": {"name": "over-http"}}),
);
let text = made["result"]["content"][0]["text"].as_str().unwrap();
assert!(
text.contains(&format!("{}/over-http", allowed.handle)),
"{made}"
);
// A newer client names the tool in headers too, which are checked
// against its schema.
let res = client()
.post(format!("{}/mcp", srv.base))
.bearer_auth(&access)
.header("accept", "application/json, text/event-stream")
.header("mcp-protocol-version", "2026-07-28")
.header("mcp-method", "tools/call")
.header("mcp-name", "repo_list")
.json(
&json!({"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {
"name": "repo_list",
"arguments": {"handle": allowed.handle},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": {"name": "t", "version": "1"},
},
}}),
)
.send()
.unwrap();
let listed = res.text().unwrap();
assert!(listed.contains("over-http"), "{listed}");
// A tool that fails, as its result says.
let (_, failed) = rpc(
&srv,
Some(&access),
"tools/call",
json!({"name": "repo_read", "arguments": {"repo": "!", "path": "a"}}),
);
assert_eq!(failed["result"]["isError"], true, "{failed}");
// A call no tool takes, as an error.
let (_, none) = rpc(
&srv,
Some(&access),
"tools/call",
json!({"name": "no_such_tool", "arguments": {}}),
);
assert!(none["error"].is_object(), "{none}");
// Resources, read as the user too.
let (_, templates) = rpc(&srv, Some(&access), "resources/templates/list", json!({}));
let templates = templates["result"]["resourceTemplates"].as_array().unwrap();
assert_eq!(templates.len(), 6, "{templates:?}");
let repo = format!("{}/over-http", allowed.handle);
let commit = json!({"name": "repo_commit", "arguments": {"repo": repo, "branch": "main", "message": "m", "changes": [{"path": "a.md", "content": "over\n"}]}});
let (_, committed) = rpc(&srv, Some(&access), "tools/call", commit);
assert_ne!(committed["result"]["isError"], true, "{committed}");
let file = format!("knotless://{repo}/file/main/a.md");
let (_, read) = rpc(&srv, Some(&access), "resources/read", json!({"uri": file}));
assert_eq!(read["result"]["contents"][0]["text"], "over\n", "{read}");
let (_, unknown) = rpc(
&srv,
Some(&access),
"resources/read",
json!({"uri": "knotless://nowhere"}),
);
assert!(
unknown["error"]["message"]
.as_str()
.unwrap()
.contains("expected one of")
);
// The operator's counts were told of the sign-ins, the tool calls and
// the resources read, each read named for the tool that answers it.
let me = &allowed.did;
let told = srv.events.lock().unwrap().clone();
let tools: Vec<&String> = told.iter().filter(|e| e.starts_with("mcp.tool")).collect();
assert_eq!(
tools,
[
&format!("mcp.tool repo_create true {me}"),
&format!("mcp.tool repo_list true {me}"),
&format!("mcp.tool repo_read false {me}"),
&format!("mcp.tool no_such_tool false {me}"),
&format!("mcp.tool repo_commit true {me}"),
]
);
let reads: Vec<&String> = told
.iter()
.filter(|e| e.starts_with("mcp.resource"))
.collect();
assert_eq!(
reads,
[
&format!("mcp.resource repo_read true {me}"),
&format!("mcp.resource false {me}"),
]
);
assert!(
told.contains(&"mcp.signin false -".to_string()),
"{told:?}"
);
assert!(told.contains(&format!("mcp.signin true {me}")), "{told:?}");
// A refresh token is the client's alone, and makes new tokens.
let (status, _) = token(
&srv,
&[
("grant_type", "refresh_token"),
("client_id", &id),
("client_secret", &secret),
("refresh_token", &access),
],
);
assert_eq!(status, 400);
let (status, fresh) = token(
&srv,
&[
("grant_type", "refresh_token"),
("client_id", &id),
("client_secret", &secret),
("refresh_token", &refresh),
],
);
assert_eq!(status, 200, "{fresh}");
// A public client proves nothing but its PKCE; a code is for the
// client it was issued to, with the verifier it was asked with.
let (_, public) = register(&srv, json!({"redirect_uris": [redirect]}));
assert_eq!(public["token_endpoint_auth_method"], "none");
assert!(public.get("client_secret").is_none());
let pid = public["client_id"].as_str().unwrap().to_string();
let mut p = params.clone();
p[0] = ("client_id", &pid);
let page = authorize(&srv, &p).text().unwrap();
assert!(page.contains("Connect An assistant to knotless"));
// A code the assistant never trades waits, among others.
let res = sign_in(&srv, &ask(&page), &allowed.handle);
approve(&allowed, "", &location(&res));
back.recv().unwrap();
let res = sign_in(&srv, &ask(&page), &allowed.handle);
approve(&allowed, "", &location(&res));
let code = back.recv().unwrap()["code"].clone();
let (status, _) = token(
&srv,
&[
("grant_type", "authorization_code"),
("client_id", &pid),
("code", &code),
("code_verifier", "wrong"),
],
);
assert_eq!(status, 400);
// Nothing waits for a callback nobody started.
let res = client()
.get(format!("{}/mcp/oauth/callback?state=nope", srv.base))
.send()
.unwrap();
assert_eq!(res.status(), 400);
// Turned away, its tokens stop working.
let off = server(&s, vec![allowed.did.clone()]);
assert_eq!(rpc(&off, Some(&access), "tools/list", json!({})).0, 401);
let (status, body) = token(
&off,
&[
("grant_type", "refresh_token"),
("client_id", &id),
("client_secret", &secret),
("refresh_token", &refresh),
],
);
assert_eq!(status, 400);
assert!(
body["error_description"]
.as_str()
.unwrap()
.contains("no longer")
);
// The login it keeps with the PDS is refreshed as it runs out, and
// waits out a PDS that cannot be reached.
let dir = allowed.did.replace(':', "_");
let kept = ctx(&s)
.env
.config_dir
.join("mcp")
.join(&dir)
.join("sessions")
.join(format!("{dir}.json"));
let change = |what: &dyn Fn(&mut Value)| {
let mut session: Value = serde_json::from_slice(&std::fs::read(&kept).unwrap()).unwrap();
session["oauth"]["expiresAt"] = 0.into();
what(&mut session);
std::fs::write(&kept, session.to_string()).unwrap();
};
change(&|_| {});
assert_eq!(rpc(&srv, Some(&access), "tools/list", json!({})).0, 200);
let read: Value = serde_json::from_slice(&std::fs::read(&kept).unwrap()).unwrap();
assert_ne!(read["oauth"]["expiresAt"], 0, "{read}");
let endpoint = read["oauth"]["tokenEndpoint"].clone();
change(&|s| s["oauth"]["tokenEndpoint"] = "http://127.0.0.1:1/token".into());
assert_eq!(rpc(&srv, Some(&access), "tools/list", json!({})).0, 200);
assert!(kept.exists());
// Once the PDS turns it down the login is over: the assistant is sent
// to sign in again, rather than working as no one.
change(&|s| {
s["oauth"]["tokenEndpoint"] = endpoint.clone();
s["refreshJwt"] = format!("ref-{}", "0".repeat(64)).into();
});
let (status, _) = rpc(&srv, Some(&access), "tools/list", json!({}));
assert_eq!(status, 401);
assert!(!kept.exists());
let (status, body) = token(
&srv,
&[
("grant_type", "refresh_token"),
("client_id", &id),
("client_secret", &secret),
("refresh_token", &refresh),
],
);
assert_eq!(
(status, body["error"].as_str()),
(400, Some("invalid_grant"))
);
}
#[test]
fn a_pds_that_will_not_take_the_sign_in() {
// A server whose public URL no PDS can reach is no client of theirs.
let s = Sandbox::new();
let allowed = account();
let rt = tokio::runtime::Runtime::new().unwrap();
let listener = rt
.block_on(tokio::net::TcpListener::bind("127.0.0.1:0"))
.unwrap();
let base = format!("http://127.0.0.1:{}", listener.local_addr().unwrap().port());
let mcp = Mcp::new(&ctx(&s), "https://nowhere.invalid", Limits::default()).unwrap();
rt.spawn(async move { axum::serve(listener, router(mcp)).await.unwrap() });
let srv = Server {
base,
url: String::new(),
_rt: rt,
_proxy: None,
events: Arc::default(),
};
let meta: Value = client()
.get(format!("{}/mcp/oauth-client-metadata.json", srv.base))
.send()
.unwrap()
.json()
.unwrap();
assert_eq!(
meta["client_id"],
"https://nowhere.invalid/mcp/oauth-client-metadata.json"
);
let (_, reg) = register(&srv, json!({"redirect_uris": ["https://claude.test/back"]}));
let id = reg["client_id"].as_str().unwrap();
let page = authorize(
&srv,
&[
("client_id", id),
("response_type", "code"),
("code_challenge", "c"),
("code_challenge_method", "S256"),
],
)
.text()
.unwrap();
let res = sign_in(&srv, &ask(&page), &allowed.handle);
assert_eq!(res.status(), 502);
assert!(res.text().unwrap().contains("didn't take the sign-in"));
}
#[test]
fn open_to_anyone_but_those_turned_away() {
let s = Sandbox::new();
let anyone = account();
let away = account();
let refused = away.did.clone();
let srv = serve(|url| {
let limits = Limits {
signing_in: 1,
running: 8,
waiting: 2,
per_minute: 3,
};
let mcp = Mcp::new(&ctx(&s), url, limits).unwrap();
mcp.refuse(Arc::new(move |did| did == refused));
mcp
});
let (redirect, back) = assistant();
let (_, reg) = register(&srv, json!({"redirect_uris": [redirect]}));
let id = reg["client_id"].as_str().unwrap().to_string();
let verifier = "a-verifier-long-enough-for-pkce-0123456789";
let challenge = knotless::oauth::sha256(verifier);
let page = authorize(
&srv,
&[
("client_id", &id),
("response_type", "code"),
("code_challenge", &challenge),
("code_challenge_method", "S256"),
],
)
.text()
.unwrap();
let signed = ask(&page);
// Turned away.
let res = sign_in(&srv, &signed, &away.handle);
assert_eq!(res.status(), 403);
assert!(res.text().unwrap().contains("taken down here"));
// Any other account goes on, as many at once as the server lets.
let res = sign_in(&srv, &signed, &anyone.handle);
assert_eq!(res.status(), 303);
let crowded = sign_in(&srv, &signed, &anyone.handle);
assert_eq!(crowded.status(), 503);
assert!(crowded.text().unwrap().contains("Too many sign-ins"));
approve(&anyone, "", &location(&res));
let code = back.recv().unwrap()["code"].clone();
let (status, tokens) = token(
&srv,
&[
("grant_type", "authorization_code"),
("client_id", &id),
("code", &code),
("code_verifier", verifier),
],
);
assert_eq!(status, 200, "{tokens}");
let access = tokens["access_token"].as_str().unwrap();
// A public repository is read from the server's own mirror, not one
// of the account's.
let create = json!({"name": "repo_create", "arguments": {"name": "shared"}});
let (_, made) = rpc(&srv, Some(access), "tools/call", create);
assert_ne!(made["result"]["isError"], true, "{made}");
let repo = format!("{}/shared", anyone.handle);
let tree = json!({"name": "repo_tree", "arguments": {"repo": repo}});
rpc(&srv, Some(access), "tools/call", tree);
let cache = ctx(&s).env.cache_dir;
let own = anyone.did.replace(':', "_");
assert!(cache.join("mirrors").join(&own).exists());
assert!(!cache.join("mcp").join(&own).join("mirrors").exists());
// So many tool calls a minute, and then a result that says to wait.
let list = json!({"name": "repo_list", "arguments": {"handle": anyone.handle}});
let (_, listed) = rpc(&srv, Some(access), "tools/call", list.clone());
assert_ne!(listed["result"]["isError"], true, "{listed}");
let (_, waits) = rpc(&srv, Some(access), "tools/call", list);
assert_eq!(waits["result"]["isError"], true, "{waits}");
let said = waits["result"]["content"][0]["text"].as_str().unwrap();
assert!(said.contains("this minute"), "{said}");
// A resource read is a call as a tool's is, and waits the same.
let file = json!({"uri": format!("knotless://{repo}/file/main/a")});
let (_, refused) = rpc(&srv, Some(access), "resources/read", file);
let said = refused["error"]["message"].as_str().unwrap();
assert!(said.contains("this minute"), "{said}");
}