use std::io::Write as _;
use std::net::TcpListener;
use std::sync::{Arc, Mutex};
use std::thread;
use onetaskgraph_github_projects::accounting::Mode;
use onetaskgraph_github_projects::{graphql, worst_case_point_cost};
use serde_json::json;
mod board;
#[allow(dead_code)]
mod journey;
#[allow(dead_code)]
mod lane;
use board::{FIXTURE_BUDGET_LIMIT, Pricing};
const OVERSTATED_BY: u64 = 1;
fn serving(pricing: Pricing, asked: &Arc<Mutex<Vec<String>>>) -> journey::Endpoints {
let listener = TcpListener::bind("127.0.0.1:0").expect("a fixture listener");
let host = format!("http://{}", listener.local_addr().unwrap());
let recorded = Arc::clone(asked);
thread::spawn(move || {
for stream in listener.incoming() {
let mut stream = stream.expect("a fixture connection");
let (method, path, body) = board::read_http_request(&mut stream);
let (status, payload) = match (method.as_str(), path.as_str()) {
("GET", "/rate_limit") => ("200 OK", board::ample_allowance().to_string()),
("POST", "/graphql") => {
let request = body.expect("a GraphQL request carries a body");
let query = request["query"].as_str().expect("a GraphQL document");
graphql_parser::parse_query::<String>(query).expect("a valid GraphQL document");
recorded.lock().unwrap().push(query.to_owned());
let answered = board::answer_a_stateless_session_call(query, pricing);
let body = match answered {
Some(data) => json!({ "data": data }),
None => json!({"errors":[{"message":
format!("this board answers a session's own calls and nothing else: {query}")}]}),
};
("200 OK", body.to_string())
}
_ => (
"404 Not Found",
json!({"message":format!("this board does not answer {method} {path}")})
.to_string(),
),
};
let response = format!(
"HTTP/1.1 {status}\r\nContent-Type: application/json\r\n\
x-ratelimit-limit: {FIXTURE_BUDGET_LIMIT}\r\n\
x-ratelimit-used: 1\r\n\
x-ratelimit-remaining: {}\r\n\
x-ratelimit-resource: graphql\r\n\
Content-Length: {}\r\nConnection: close\r\n\r\n{payload}",
FIXTURE_BUDGET_LIMIT - 1,
payload.len()
);
stream.write_all(response.as_bytes()).expect("a response");
}
});
journey::Endpoints {
graphql: format!("{host}/graphql"),
rest_host: host,
source: None,
}
}
fn first_document_asked_about() -> (&'static str, &'static str) {
*graphql::DOCUMENTS
.iter()
.find(|(document, _)| Mode::of_document(document) == Mode::Read)
.expect("this source sends at least one query document")
}
#[test]
fn a_board_that_prices_a_document_differently_fails_the_run_naming_both_figures() {
let asked = Arc::new(Mutex::new(Vec::new()));
journey::against(serving(Pricing::Overstating { by: OVERSTATED_BY }, &asked));
let (document, doing) = first_document_asked_about();
let ours = worst_case_point_cost(document).expect("a priceable document");
let refusal = thread::spawn(|| {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("a runtime for the journey")
.block_on(journey::run(journey::Nomination {
token: "test-token".to_owned(),
owner: "octo-org".to_owned(),
project_number: 7,
repository: "acme/work".to_owned(),
}));
})
.join()
.expect_err("a run against a board that misprices a document does not finish");
let refusal = panicked_with(&refusal);
assert!(refusal.contains(doing), "{refusal}");
assert!(
refusal.contains(&format!("at {} points", ours + OVERSTATED_BY)),
"{refusal}"
);
assert!(refusal.contains(&format!("computes {ours}")), "{refusal}");
assert!(refusal.contains("GitHub is the authority"), "{refusal}");
let asked = asked.lock().unwrap();
assert!(
asked.iter().any(|sent| {
sent.contains("rateLimit(dryRun:true)")
&& board::strip_probe(sent).as_deref() == Some(document)
}),
"the board was never asked about {doing}; it answered {asked:?}"
);
}
fn panicked_with(payload: &Box<dyn std::any::Any + Send>) -> String {
payload
.downcast_ref::<String>()
.cloned()
.or_else(|| {
payload
.downcast_ref::<&str>()
.map(|held| (*held).to_owned())
})
.unwrap_or_else(|| "the journey panicked with a payload this test cannot read".to_owned())
}
#[test]
fn the_same_board_answers_that_document_as_this_workspace_prices_it() {
let (document, _) = first_document_asked_about();
let ours = worst_case_point_cost(document).expect("a priceable document");
let truthful = board::probe_answer(document, Pricing::AsComputed);
let overstated = board::probe_answer(document, Pricing::Overstating { by: OVERSTATED_BY });
assert_eq!(truthful.pointer("/rateLimit/cost"), Some(&json!(ours)));
assert_eq!(
overstated.pointer("/rateLimit/cost"),
Some(&json!(ours + OVERSTATED_BY))
);
assert_eq!(
truthful.pointer("/rateLimit/nodeCount"),
overstated.pointer("/rateLimit/nodeCount"),
"only the price is what a mispricing board changes"
);
}