1use std::sync::Arc;
2use std::time::Duration;
3
4use http::Method;
5use hyper_util::client::legacy::Client;
6use hyper_util::rt::TokioExecutor;
7use sofie_auth::SessionClaims;
8use sofie_core::{Error, Result, Timestamp, Uuid};
9use sofie_http::{App, CookieNames, SigningKeys};
10use tokio::net::TcpListener;
11use tokio::sync::oneshot;
12use tokio::task::JoinHandle;
13
14use crate::request::{HttpClient, TestRequest, TestSession};
15
16const STOP_TIMEOUT: Duration = Duration::from_secs(5);
17
18pub struct TestServer {
19 base: String,
20 keys: Arc<SigningKeys>,
21 cookies: CookieNames,
22 client: HttpClient,
23 stop: Option<oneshot::Sender<()>>,
24 served: Option<JoinHandle<Result<()>>>,
25}
26
27impl TestServer {
28 pub async fn start(app: App) -> Self {
29 let mut cookies = None;
30 let mut key = [0; 32];
31 sofie_core::random::fill(&mut key).expect("generating a session key");
32 let keys = || SigningKeys::new(&[key]).expect("a 32-byte session key");
33 let listener = TcpListener::bind("127.0.0.1:0")
34 .await
35 .expect("binding a local port");
36 let address = listener.local_addr().expect("reading the bound address");
37 let (stop, stopped) = oneshot::channel::<()>();
38 let app = app.serving(|serving| {
39 cookies = Some(CookieNames::of_serving(&serving));
40 serving.signing_keys(keys())
41 });
42 let cookies = cookies
43 .expect("`App::serving` configures at once")
44 .expect("an app name that names a cookie");
45 let served = tokio::spawn(app.serve(listener, async {
46 let _ = stopped.await;
47 }));
48 let served = started(served).await;
49 Self {
50 base: format!("http://{address}"),
51 keys: Arc::new(keys()),
52 cookies,
53 client: Client::builder(TokioExecutor::new()).build_http(),
54 stop: Some(stop),
55 served: Some(served),
56 }
57 }
58
59 #[must_use]
60 pub fn signing_keys(&self) -> Arc<SigningKeys> {
61 Arc::clone(&self.keys)
62 }
63
64 #[must_use]
65 pub fn origin(&self) -> &str {
66 &self.base
67 }
68
69 pub fn get(&self, path: &str) -> TestRequest {
70 self.request(Method::GET, path)
71 }
72
73 pub fn post(&self, path: &str) -> TestRequest {
74 self.request(Method::POST, path)
75 }
76
77 pub fn request(&self, method: Method, path: &str) -> TestRequest {
78 TestRequest::new(
79 self.client.clone(),
80 method,
81 format!("{}{path}", self.base),
82 self.cookies.clone(),
83 )
84 }
85
86 pub async fn stop(mut self) -> Result<()> {
87 self.signal_stop();
88 let served = self.served.take().expect("a server is stopped once");
89 tokio::time::timeout(STOP_TIMEOUT, served)
90 .await
91 .map_err(|error| Error::internal("waiting for the server to stop", error))?
92 .map_err(|error| Error::internal("joining the server task", error))?
93 }
94
95 pub(crate) fn seal(&self, area: &str, principal: Uuid, now: Timestamp) -> TestSession {
96 let session = Uuid::new_v4().expect("choosing a test session's id");
97 let claims = SessionClaims::new(session, area, principal, now);
98 let sealed = claims.seal(&self.keys).expect("sealing a test session");
99 TestSession::new(format!("{}={sealed}", self.cookies.session()))
100 }
101
102 pub(crate) fn open(&self, session: &TestSession, now: Timestamp) -> Option<SessionClaims> {
103 let sealed = session
104 .cookie()
105 .strip_prefix(self.cookies.session())?
106 .strip_prefix('=')?;
107 SessionClaims::open(&self.keys, sealed, now).ok()
108 }
109
110 fn signal_stop(&mut self) {
111 if let Some(stop) = self.stop.take() {
112 let _ = stop.send(());
113 }
114 }
115}
116
117async fn started(mut served: JoinHandle<Result<()>>) -> JoinHandle<Result<()>> {
120 tokio::task::yield_now().await;
121 if !served.is_finished() {
122 return served;
123 }
124 let reason = match (&mut served).await {
125 Ok(Ok(())) => "it stopped at once".to_owned(),
126 Ok(Err(error)) => error.to_string(),
127 Err(error) => error.to_string(),
128 };
129 panic!("the test app did not start: {reason}");
130}
131
132impl Drop for TestServer {
133 fn drop(&mut self) {
134 self.signal_stop();
135 }
136}