Skip to main content

vcon_server/
api.rs

1//! The HTTP API, served over TLS only.
2
3use std::collections::BTreeSet;
4use std::future::Future;
5use std::io;
6use std::path::Path;
7use std::sync::Arc;
8use std::time::{Duration, Instant};
9
10use axum::Router;
11use axum::body::{Body, Bytes};
12use axum::extract::{MatchedPath, Path as UriPath, Request, State};
13use axum::http::header::{
14    AUTHORIZATION, CACHE_CONTROL, CONTENT_LENGTH, CONTENT_TYPE, ETAG, IF_MATCH, LOCATION,
15    WWW_AUTHENTICATE,
16};
17use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode, Uri};
18use axum::middleware::Next;
19use axum::response::{IntoResponse, Response};
20use axum::routing::{get, patch, post, put};
21use http_body_util::LengthLimitError;
22use hyper_util::rt::{TokioExecutor, TokioIo};
23use hyper_util::server::conn::auto;
24use hyper_util::service::TowerToHyperService;
25use rustls::ServerConfig;
26use rustls::pki_types::pem::PemObject;
27use rustls::pki_types::{CertificateDer, PrivateKeyDer};
28use serde::de::{Deserialize, DeserializeOwned, Deserializer, Error as _};
29use tokio::net::TcpListener;
30use tokio_rustls::TlsAcceptor;
31
32use crate::access::{self, AuthError, Caller, Scope, Scopes};
33use crate::audit::{Action, Actor, AuditQuery, Entry};
34use crate::ids::{KeyId, TenantId, VersionId};
35use crate::retention::Retention;
36use crate::store::{Caller as StoreCaller, Created, ListQuery, Store, StoreError};
37use crate::tenants::{KeyInfo, Removal, TenantError, TenantInfo, Tenants};
38use time::format_description::well_known::Rfc3339;
39use uuid::Uuid;
40use vcon::read::{Inspection, Rejection};
41
42/// The limits from configuration that the API applies to every request.
43#[derive(Debug, Clone, Copy)]
44pub struct Limits {
45    /// The most bytes a request body may hold, as received.
46    pub body: usize,
47    /// The most bytes gzip inflation may produce while a body is inspected.
48    pub inflated: u64,
49    /// How long a request body may take to arrive.
50    pub body_timeout: Duration,
51}
52
53/// What the API serves from.
54pub struct App {
55    pub tenants: Arc<Tenants>,
56    pub limits: Limits,
57    /// The must_support extensions this server supports.
58    pub supported: BTreeSet<String>,
59    /// The retention period of a tenant that sets none.
60    pub retention_default: Retention,
61    /// The MCP server's limits.
62    pub mcp: crate::mcp::Limits,
63}
64
65/// The routes, served from `app`.
66pub fn router(app: Arc<App>) -> Router {
67    Router::new()
68        .route("/vcons", post(create).get(list))
69        .route("/vcons/{uuid}", get(read).put(replace).delete(delete))
70        .route("/health", get(health))
71        .route("/key", get(key_info))
72        .route("/audit", get(audit_read))
73        .route("/admin/audit", get(admin_audit_read))
74        .route("/openapi.json", get(openapi))
75        .route("/legal/vcons/{uuid}", get(legal_read))
76        .route("/legal/vcons/{uuid}/versions", get(legal_versions))
77        .route("/legal/vcons/{uuid}/versions/{version}", get(legal_version))
78        .route("/legal/vcons/{uuid}/restore", post(legal_restore))
79        .route("/legal/vcons/{uuid}/hold", put(hold_on).delete(hold_off))
80        .route("/admin/tenants", post(tenant_create).get(tenant_list))
81        .route(
82            "/admin/tenants/{id}",
83            patch(tenant_retention).delete(tenant_remove),
84        )
85        .route("/admin/tenants/{id}/keys", post(key_create).get(key_list))
86        .route("/admin/keys/{id}", axum::routing::delete(key_revoke))
87        .merge(crate::console::router())
88        .with_state(app.clone())
89        .merge(crate::mcp::router(app))
90        .layer(axum::middleware::from_fn(log_request))
91}
92
93/// Logs each request once it is answered: a request id, the method, the
94/// route template, never the path, which holds ids, the status and how
95/// long it took. Bodies, tokens, uuids and tenant ids are in the audit
96/// only.
97async fn log_request(matched: Option<MatchedPath>, request: Request, next: Next) -> Response {
98    let started = Instant::now();
99    let method = request.method().clone();
100    let route = matched
101        .as_ref()
102        .map_or("unmatched", MatchedPath::as_str)
103        .to_owned();
104    let answer = next.run(request).await;
105    // In the unit tests every answer must be one the description lists, so
106    // a route or status added without its description fails the test
107    // that produced it. A method a path does not take (405) and a path no
108    // route matches are the router's, not a route's.
109    #[cfg(test)]
110    if route != "unmatched" && answer.status() != StatusCode::METHOD_NOT_ALLOWED {
111        let status = answer.status().as_u16();
112        assert!(
113            description::lists(method.as_str(), &route, status),
114            "openapi.json does not list {status} for {method} {route}"
115        );
116    }
117    let mut id = [0; 8];
118    // AWS-LC's RAND_bytes aborts the process when it cannot get enough
119    // randomness, so `fill` never returns its error.
120    aws_lc_rs::rand::fill(&mut id).expect("AWS-LC aborts instead of failing");
121    tracing::info!(
122        request_id = %u64::from_be_bytes(id),
123        %method,
124        %route,
125        status = answer.status().as_u16(),
126        duration_ms = u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX),
127        "request"
128    );
129    answer
130}
131
132/// The OpenAPI description of every route, as `GET /openapi.json` serves
133/// it and the website publishes it.
134pub const OPENAPI: &str = include_str!("../openapi.json");
135
136/// `GET /openapi.json`: the description, to any valid key, recorded as
137/// openapi in the key's tenant log or the admin log.
138async fn openapi(State(app): State<Arc<App>>, headers: HeaderMap) -> Response {
139    let caller = match authenticated(&app, &headers).await {
140        Ok(caller) => caller,
141        Err(refused) => return refused.into_response(),
142    };
143    let tenants = app.tenants.clone();
144    let recorded = blocking(move || {
145        let entry = |status| Entry {
146            actor: Actor::Key(caller.key),
147            action: Action::Openapi,
148            target: None,
149            version: None,
150            status,
151        };
152        let logged = |done: bool, answer| {
153            if done {
154                answer
155            } else {
156                Err(Problem::AuditFailed)
157            }
158        };
159        match caller.tenant.map(|t| tenants.store(t)) {
160            Some(Ok(store)) => logged(store.log(&entry(200)).is_ok(), Ok(())),
161            // A tenant being removed, or whose log cannot be opened, answers
162            // 503 here too, recorded in the admin log.
163            Some(Err(_)) => logged(
164                tenants.record_alone(&entry(503)).is_ok(),
165                Err(Problem::Unavailable),
166            ),
167            None => logged(tenants.record_alone(&entry(200)).is_ok(), Ok(())),
168        }
169    })
170    .await;
171    match recorded {
172        Ok(()) => (
173            StatusCode::OK,
174            [(CONTENT_TYPE, "application/json")],
175            OPENAPI,
176        )
177            .into_response(),
178        Err(refused) => refused.into_response(),
179    }
180}
181
182/// `POST /vcons`: stores a new vCon, or answers 200 when the same bytes
183/// are already current.
184async fn create(State(app): State<Arc<App>>, headers: HeaderMap, body: Body) -> Response {
185    let admitted = match admit(&app, &headers, body, Scope::Write, Action::Create).await {
186        Ok(admitted) => admitted,
187        Err(refused) => return refused.into_response(),
188    };
189    let found = match inspect(&app, &admitted, &headers, Action::Create).await {
190        Ok(found) => found,
191        Err(refused) => return refused.into_response(),
192    };
193    let (store, body, key) = (
194        admitted.store.clone(),
195        admitted.body.clone(),
196        admitted.caller.key,
197    );
198    // Store records its own outcomes.
199    match blocking(move || store.create(&StoreCaller { key }, &found, &body)).await {
200        Ok(Created::New(version)) => (
201            StatusCode::CREATED,
202            [
203                (LOCATION, format!("/vcons/{}", found.uuid)),
204                (ETAG, entity_tag(version)),
205            ],
206        )
207            .into_response(),
208        Ok(Created::Same(version)) => {
209            (StatusCode::OK, [(ETAG, entity_tag(version))]).into_response()
210        }
211        Err(error) => failed(error).into_response(),
212    }
213}
214
215/// What Format makes of `body`, sent as `content_type`, within the
216/// configured inflation limit and supported extensions; the HTTP routes
217/// and the MCP tools read every vCon through this. Records nothing.
218pub(crate) async fn read_vcon(
219    app: &Arc<App>,
220    body: Bytes,
221    content_type: Option<String>,
222) -> Result<Inspection, Problem> {
223    let worker = app.clone();
224    blocking(move || {
225        let limits = vcon::read::Limits::new(worker.limits.inflated);
226        vcon::read::inspect(&body, content_type.as_deref(), &limits, &worker.supported)
227            .map_err(|r| rejected(&r))
228    })
229    .await
230}
231
232/// Reads an admitted body as a vCon: its media type must be one a vCon is
233/// sent as (415), and Format must take it (422, or 413 when it inflates
234/// too far). A refusal here is recorded here, since Store never sees it.
235async fn inspect(
236    app: &Arc<App>,
237    admitted: &Admitted,
238    headers: &HeaderMap,
239    action: Action,
240) -> Result<Inspection, Problem> {
241    let content_type = headers
242        .get(CONTENT_TYPE)
243        .map(|v| v.to_str().unwrap_or("\u{0}").to_owned());
244    let found = if content_type.as_deref().is_none_or(is_vcon_media_type) {
245        read_vcon(app, admitted.body.clone(), content_type).await
246    } else {
247        Err(Problem::UnsupportedMediaType)
248    };
249    match found {
250        Ok(found) => Ok(found),
251        Err(refused) => {
252            let entry = refusal(admitted.caller.key, action, refused);
253            Err(app.refuse(Some(admitted.tenant), entry, refused).await)
254        }
255    }
256}
257
258/// `GET /vcons`: a page of the tenant's live vCons, newest stored first,
259/// as JSON, with the cursor of the next page.
260async fn list(State(app): State<Arc<App>>, uri: Uri, headers: HeaderMap, body: Body) -> Response {
261    let admitted = match admit(&app, &headers, body, Scope::Read, Action::List).await {
262        Ok(admitted) => admitted,
263        Err(refused) => return refused.into_response(),
264    };
265    let Ok(query) = ListQuery::from_query(uri.query()) else {
266        let refused = Problem::BadQuery;
267        let entry = refusal(admitted.caller.key, Action::List, refused);
268        return app
269            .refuse(Some(admitted.tenant), entry, refused)
270            .await
271            .into_response();
272    };
273    let (store, key) = (admitted.store.clone(), admitted.caller.key);
274    // Store records its own outcomes.
275    match blocking(move || store.list(&StoreCaller { key }, &query)).await {
276        Ok(listing) => json_answer(StatusCode::OK, &listing.to_json()),
277        Err(error) => failed(error).into_response(),
278    }
279}
280
281/// The media type of an audit page: JSON lines, one record a line.
282const JSON_LINES: &str = "application/jsonl";
283
284/// `GET /audit`: a page of the key's tenant's log, the lines `audit export`
285/// writes for the same records.
286async fn audit_read(
287    State(app): State<Arc<App>>,
288    uri: Uri,
289    headers: HeaderMap,
290    body: Body,
291) -> Response {
292    let admitted = match admit(&app, &headers, body, Scope::Audit, Action::AuditRead).await {
293        Ok(admitted) => admitted,
294        Err(refused) => return refused.into_response(),
295    };
296    let Ok(query) = AuditQuery::from_query(uri.query()) else {
297        let refused = Problem::BadQuery;
298        let entry = refusal(admitted.caller.key, Action::AuditRead, refused);
299        return app
300            .refuse(Some(admitted.tenant), entry, refused)
301            .await
302            .into_response();
303    };
304    let (store, key) = (admitted.store.clone(), admitted.caller.key);
305    // Store records its own outcomes.
306    match blocking(move || store.audit_page(&StoreCaller { key }, &query)).await {
307        Ok(lines) => (StatusCode::OK, [(CONTENT_TYPE, JSON_LINES)], lines).into_response(),
308        Err(error) => failed(error).into_response(),
309    }
310}
311
312/// `GET /admin/audit`: a page of the admin log, the lines
313/// `audit export --admin` writes for the same records.
314async fn admin_audit_read(
315    State(app): State<Arc<App>>,
316    uri: Uri,
317    headers: HeaderMap,
318    body: Body,
319) -> Response {
320    let action = Action::AuditRead;
321    let read = async {
322        let (key, _) = admin_admit(&app, &headers, body, action).await?;
323        let Ok(query) = AuditQuery::from_query(uri.query()) else {
324            let refused = Problem::BadQuery;
325            return Err(app
326                .refuse(None, refusal(key, action, refused), refused)
327                .await);
328        };
329        let tenants = app.tenants.clone();
330        // Tenants records its own outcomes.
331        blocking(move || tenants.audit_page(Actor::Key(key), &query))
332            .await
333            .map_err(Problem::Tenant)
334    };
335    match read.await {
336        Ok(lines) => (StatusCode::OK, [(CONTENT_TYPE, JSON_LINES)], lines).into_response(),
337        Err(refused) => refused.into_response(),
338    }
339}
340
341/// `GET /vcons/{uuid}`: the current version, as stored.
342async fn read(
343    State(app): State<Arc<App>>,
344    UriPath(uuid): UriPath<String>,
345    headers: HeaderMap,
346    body: Body,
347) -> Response {
348    let admitted = match admit(&app, &headers, body, Scope::Read, Action::Read).await {
349        Ok(admitted) => admitted,
350        Err(refused) => return refused.into_response(),
351    };
352    let Some(uuid) = vcon::parse_uuid(&uuid) else {
353        let refused = Problem::BadPathUuid;
354        let entry = refusal(admitted.caller.key, Action::Read, refused);
355        return app
356            .refuse(Some(admitted.tenant), entry, refused)
357            .await
358            .into_response();
359    };
360    let (store, key) = (admitted.store.clone(), admitted.caller.key);
361    match blocking(move || store.get(&StoreCaller { key }, uuid)).await {
362        Ok(stored) => {
363            let content_type = if stored.gzip {
364                "application/vcon+gzip"
365            } else {
366                "application/vcon"
367            };
368            (
369                StatusCode::OK,
370                [
371                    (CONTENT_TYPE, content_type.to_owned()),
372                    (ETAG, entity_tag(stored.version)),
373                ],
374                stored.body,
375            )
376                .into_response()
377        }
378        // Store records its own outcomes.
379        Err(error) => failed(error).into_response(),
380    }
381}
382
383/// `PUT /vcons/{uuid}`: replaces the current version named by `If-Match`.
384async fn replace(
385    State(app): State<Arc<App>>,
386    UriPath(uuid): UriPath<String>,
387    headers: HeaderMap,
388    body: Body,
389) -> Response {
390    let admitted = match admit(&app, &headers, body, Scope::Write, Action::Replace).await {
391        Ok(admitted) => admitted,
392        Err(refused) => return refused.into_response(),
393    };
394    let (shared, key, tenant) = (&app, admitted.caller.key, admitted.tenant);
395    let refuse = |refused| async move {
396        let entry = refusal(key, Action::Replace, refused);
397        shared
398            .refuse(Some(tenant), entry, refused)
399            .await
400            .into_response()
401    };
402    let Some(uuid) = vcon::parse_uuid(&uuid) else {
403        return refuse(Problem::BadPathUuid).await;
404    };
405    let current = match if_match(&headers) {
406        None => return refuse(Problem::PreconditionRequired).await,
407        Some(None) => return refuse(Problem::Store(StoreError::PreconditionFailed)).await,
408        Some(Some(current)) => current,
409    };
410    let found = match inspect(&app, &admitted, &headers, Action::Replace).await {
411        Ok(found) => found,
412        Err(refused) => return refused.into_response(),
413    };
414    if found.uuid != uuid {
415        return refuse(Problem::PathUuidMismatch).await;
416    }
417    let (store, body, key) = (
418        admitted.store.clone(),
419        admitted.body.clone(),
420        admitted.caller.key,
421    );
422    // Store records its own outcomes.
423    match blocking(move || store.replace(&StoreCaller { key }, current, &found, &body)).await {
424        Ok(version) => (StatusCode::OK, [(ETAG, entity_tag(version))]).into_response(),
425        Err(error) => failed(error).into_response(),
426    }
427}
428
429/// The version an `If-Match` header names: None without the header, and
430/// Some(None) when it is anything but one strong tag holding a version id,
431/// such as `*`, a weak tag or a list, since that names no version this
432/// server could replace.
433fn if_match(headers: &HeaderMap) -> Option<Option<VersionId>> {
434    let value = headers.get(IF_MATCH)?;
435    Some(
436        value
437            .to_str()
438            .ok()
439            .and_then(|tag| tag.trim().strip_prefix('"')?.strip_suffix('"'))
440            .and_then(VersionId::parse),
441    )
442}
443
444/// `DELETE /vcons/{uuid}`: hides the vCon; also 204 when it is already
445/// deleted or purged.
446async fn delete(
447    State(app): State<Arc<App>>,
448    UriPath(uuid): UriPath<String>,
449    headers: HeaderMap,
450    body: Body,
451) -> Response {
452    let admitted = match admit(&app, &headers, body, Scope::Delete, Action::Delete).await {
453        Ok(admitted) => admitted,
454        Err(refused) => return refused.into_response(),
455    };
456    let Some(uuid) = vcon::parse_uuid(&uuid) else {
457        let refused = Problem::BadPathUuid;
458        let entry = refusal(admitted.caller.key, Action::Delete, refused);
459        return app
460            .refuse(Some(admitted.tenant), entry, refused)
461            .await
462            .into_response();
463    };
464    // The tenant's own period, else the server's, read now so a change
465    // counts from the next delete. Unread, nothing is deleted.
466    let (tenants, tenant) = (app.tenants.clone(), admitted.tenant);
467    let retention = match blocking(move || tenants.retention(tenant)).await {
468        Ok(own) => own.unwrap_or(app.retention_default),
469        Err(_) => {
470            let refused = Problem::Unavailable;
471            let entry = refusal(admitted.caller.key, Action::Delete, refused);
472            return app
473                .refuse(Some(tenant), entry, refused)
474                .await
475                .into_response();
476        }
477    };
478    let (store, key) = (admitted.store.clone(), admitted.caller.key);
479    // Store records its own outcomes.
480    match blocking(move || store.delete(&StoreCaller { key }, uuid, retention)).await {
481        Ok(()) => StatusCode::NO_CONTENT.into_response(),
482        Err(error) => failed(error).into_response(),
483    }
484}
485
486/// Whether a legal read is of a live or a deleted uuid.
487const VCON_STATE: HeaderName = HeaderName::from_static("vcon-state");
488
489/// The legal routes' shared start: a key with the legal scope, then the
490/// path's uuid, each refusal recorded as `action`.
491async fn legal_admit(
492    app: &Arc<App>,
493    headers: &HeaderMap,
494    body: Body,
495    action: Action,
496    uuid: &str,
497) -> Result<(Admitted, Uuid), Problem> {
498    let admitted = admit(app, headers, body, Scope::Legal, action).await?;
499    let Some(uuid) = vcon::parse_uuid(uuid) else {
500        let refused = Problem::BadPathUuid;
501        let entry = refusal(admitted.caller.key, action, refused);
502        return Err(app.refuse(Some(admitted.tenant), entry, refused).await);
503    };
504    Ok((admitted, uuid))
505}
506
507/// `GET /legal/vcons/{uuid}`: the current version, or the latest kept one
508/// of a deleted uuid, as stored, with `Vcon-State`.
509async fn legal_read(
510    State(app): State<Arc<App>>,
511    UriPath(uuid): UriPath<String>,
512    headers: HeaderMap,
513    body: Body,
514) -> Response {
515    match legal_admit(&app, &headers, body, Action::LegalRead, &uuid).await {
516        Ok((admitted, uuid)) => legal_body(&admitted, uuid, None).await,
517        Err(refused) => refused.into_response(),
518    }
519}
520
521/// `GET /legal/vcons/{uuid}/versions/{version}`: that kept version, as
522/// stored.
523async fn legal_version(
524    State(app): State<Arc<App>>,
525    UriPath((uuid, version)): UriPath<(String, String)>,
526    headers: HeaderMap,
527    body: Body,
528) -> Response {
529    let (admitted, uuid) = match legal_admit(&app, &headers, body, Action::LegalRead, &uuid).await {
530        Ok(admitted) => admitted,
531        Err(refused) => return refused.into_response(),
532    };
533    let Some(version) = VersionId::parse(&version) else {
534        let refused = Problem::BadPathVersion;
535        let entry = refusal(admitted.caller.key, Action::LegalRead, refused);
536        return app
537            .refuse(Some(admitted.tenant), entry, refused)
538            .await
539            .into_response();
540    };
541    legal_body(&admitted, uuid, Some(version)).await
542}
543
544/// A legal read's answer: the body as stored, its tag and the uuid's state.
545async fn legal_body(admitted: &Admitted, uuid: Uuid, version: Option<VersionId>) -> Response {
546    let (store, key) = (admitted.store.clone(), admitted.caller.key);
547    // Store records its own outcomes.
548    match blocking(move || store.legal_get(&StoreCaller { key }, uuid, version)).await {
549        Ok(read) => {
550            let content_type = if read.stored.gzip {
551                "application/vcon+gzip"
552            } else {
553                "application/vcon"
554            };
555            let state = if read.deleted { "deleted" } else { "live" };
556            (
557                StatusCode::OK,
558                [
559                    (CONTENT_TYPE, content_type.to_owned()),
560                    (ETAG, entity_tag(read.stored.version)),
561                    (VCON_STATE, state.to_owned()),
562                ],
563                read.stored.body,
564            )
565                .into_response()
566        }
567        Err(error) => failed(error).into_response(),
568    }
569}
570
571/// `GET /legal/vcons/{uuid}/versions`: every kept version, oldest first,
572/// as JSON: its id, when it was stored and hidden (RFC 3339, UTC), and
573/// whether it is current.
574async fn legal_versions(
575    State(app): State<Arc<App>>,
576    UriPath(uuid): UriPath<String>,
577    headers: HeaderMap,
578    body: Body,
579) -> Response {
580    let (admitted, uuid) =
581        match legal_admit(&app, &headers, body, Action::LegalVersions, &uuid).await {
582            Ok(admitted) => admitted,
583            Err(refused) => return refused.into_response(),
584        };
585    let (store, key) = (admitted.store.clone(), admitted.caller.key);
586    match blocking(move || store.legal_versions(&StoreCaller { key }, uuid)).await {
587        Ok(versions) => match versions_json(&versions) {
588            Some(listed) => (
589                StatusCode::OK,
590                [(CONTENT_TYPE, "application/json")],
591                serde_json::Value::Array(listed).to_string(),
592            )
593                .into_response(),
594            None => failed(StoreError::Damaged).into_response(),
595        },
596        Err(error) => failed(error).into_response(),
597    }
598}
599
600/// Kept versions as the legal routes and tools render them: each one's id,
601/// when it was stored and hidden (RFC 3339, UTC; null while current), and
602/// whether it is current. None when a time cannot be written.
603pub(crate) fn versions_json(
604    versions: &[crate::store::VersionInfo],
605) -> Option<Vec<serde_json::Value>> {
606    let time = |at: time::OffsetDateTime| at.format(&Rfc3339).ok();
607    versions
608        .iter()
609        .map(|v| {
610            let hidden_at = match v.hidden_at {
611                Some(at) => Some(time(at)?),
612                None => None,
613            };
614            Some(serde_json::json!({
615                "version": v.version.to_string(),
616                "stored_at": time(v.stored_at)?,
617                "hidden_at": hidden_at,
618                "current": v.current,
619            }))
620        })
621        .collect()
622}
623
624/// `POST /legal/vcons/{uuid}/restore`: makes a deleted vCon live again and
625/// answers 200 with the restored version's tag.
626async fn legal_restore(
627    State(app): State<Arc<App>>,
628    UriPath(uuid): UriPath<String>,
629    headers: HeaderMap,
630    body: Body,
631) -> Response {
632    let (admitted, uuid) = match legal_admit(&app, &headers, body, Action::Restore, &uuid).await {
633        Ok(admitted) => admitted,
634        Err(refused) => return refused.into_response(),
635    };
636    let (store, key) = (admitted.store.clone(), admitted.caller.key);
637    match blocking(move || store.restore(&StoreCaller { key }, uuid)).await {
638        Ok(version) => (StatusCode::OK, [(ETAG, entity_tag(version))]).into_response(),
639        Err(error) => failed(error).into_response(),
640    }
641}
642
643/// `PUT /legal/vcons/{uuid}/hold`: puts the uuid under a legal hold.
644async fn hold_on(
645    State(app): State<Arc<App>>,
646    UriPath(uuid): UriPath<String>,
647    headers: HeaderMap,
648    body: Body,
649) -> Response {
650    hold(app, uuid, headers, body, true).await
651}
652
653/// `DELETE /legal/vcons/{uuid}/hold`: releases the uuid's legal hold.
654async fn hold_off(
655    State(app): State<Arc<App>>,
656    UriPath(uuid): UriPath<String>,
657    headers: HeaderMap,
658    body: Body,
659) -> Response {
660    hold(app, uuid, headers, body, false).await
661}
662
663async fn hold(app: Arc<App>, uuid: String, headers: HeaderMap, body: Body, on: bool) -> Response {
664    // A release is its own action, so the log tells it from a hold.
665    let action = if on { Action::Hold } else { Action::Release };
666    let (admitted, uuid) = match legal_admit(&app, &headers, body, action, &uuid).await {
667        Ok(admitted) => admitted,
668        Err(refused) => return refused.into_response(),
669    };
670    let (store, key) = (admitted.store.clone(), admitted.caller.key);
671    match blocking(move || store.set_hold(&StoreCaller { key }, uuid, on)).await {
672        Ok(()) => StatusCode::NO_CONTENT.into_response(),
673        Err(error) => failed(error).into_response(),
674    }
675}
676
677/// The admin routes' shared start: an operator key holding the admin
678/// scope, then the body within the limits, each refusal recorded as
679/// `action`.
680async fn admin_admit(
681    app: &Arc<App>,
682    headers: &HeaderMap,
683    body: Body,
684    action: Action,
685) -> Result<(KeyId, Bytes), Problem> {
686    let caller = authenticated(app, headers).await?;
687    let entry = |refused| refusal(caller.key, action, refused);
688    if access::require(&caller, Scope::Admin).is_err() {
689        let refused = Problem::Forbidden;
690        return Err(app.refuse(caller.tenant, entry(refused), refused).await);
691    }
692    match read_body(body, headers, &app.limits).await {
693        Ok(body) => Ok((caller.key, body)),
694        Err(refused) => Err(app.refuse(None, entry(refused), refused).await),
695    }
696}
697
698/// An admin body as JSON, or the refusal recorded as `action`.
699async fn admin_body<T: DeserializeOwned>(
700    app: &Arc<App>,
701    key: KeyId,
702    action: Action,
703    body: &Bytes,
704) -> Result<T, Problem> {
705    match serde_json::from_slice(body) {
706        Ok(read) => Ok(read),
707        Err(_) => {
708            let refused = Problem::InvalidBody;
709            Err(app
710                .refuse(None, refusal(key, action, refused), refused)
711                .await)
712        }
713    }
714}
715
716/// A retention period in a body: a string `Retention` reads, or null for
717/// the server default.
718fn period<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Retention>, D::Error> {
719    Option::<String>::deserialize(d)?
720        .map(|text| Retention::parse(&text).ok_or_else(|| D::Error::custom("a bad period")))
721        .transpose()
722}
723
724/// The body of `POST /admin/tenants`.
725#[derive(serde::Deserialize)]
726#[serde(deny_unknown_fields)]
727struct NewTenant {
728    name: String,
729    #[serde(default, deserialize_with = "period")]
730    retention: Option<Retention>,
731}
732
733/// The body of `PATCH /admin/tenants/{id}`. The period must be there,
734/// null included, so an empty body changes nothing by mistake.
735#[derive(serde::Deserialize)]
736#[serde(deny_unknown_fields)]
737struct NewRetention {
738    #[serde(deserialize_with = "period")]
739    retention: Option<Retention>,
740}
741
742/// A JSON answer.
743fn json_answer(status: StatusCode, body: &serde_json::Value) -> Response {
744    (
745        status,
746        [(CONTENT_TYPE, "application/json")],
747        body.to_string(),
748    )
749        .into_response()
750}
751
752/// `POST /admin/tenants`: makes a tenant and answers 201 with its id.
753async fn tenant_create(State(app): State<Arc<App>>, headers: HeaderMap, body: Body) -> Response {
754    let action = Action::TenantCreate;
755    let made = async {
756        let (key, body) = admin_admit(&app, &headers, body, action).await?;
757        let new: NewTenant = admin_body(&app, key, action, &body).await?;
758        let tenants = app.tenants.clone();
759        // Tenants records its own outcomes.
760        blocking(move || tenants.create(Actor::Key(key), &new.name, new.retention))
761            .await
762            .map_err(Problem::Tenant)
763    };
764    match made.await {
765        Ok(id) => json_answer(
766            StatusCode::CREATED,
767            &serde_json::json!({ "id": id.to_string() }),
768        ),
769        Err(refused) => refused.into_response(),
770    }
771}
772
773/// `GET /admin/tenants`: every tenant by name, with its id and its own
774/// retention period, null for the server default.
775async fn tenant_list(State(app): State<Arc<App>>, headers: HeaderMap, body: Body) -> Response {
776    let listed = async {
777        let (key, _) = admin_admit(&app, &headers, body, Action::TenantList).await?;
778        let tenants = app.tenants.clone();
779        blocking(move || tenants.list(Actor::Key(key)))
780            .await
781            .map_err(Problem::Tenant)
782    };
783    match listed.await {
784        Ok(listed) => {
785            let listed: Vec<serde_json::Value> = listed.iter().map(TenantInfo::to_json).collect();
786            json_answer(StatusCode::OK, &serde_json::Value::Array(listed))
787        }
788        Err(refused) => refused.into_response(),
789    }
790}
791
792/// `PATCH /admin/tenants/{id}`: sets the tenant's retention period, or
793/// with null returns it to the server default, and answers 200 with it.
794async fn tenant_retention(
795    State(app): State<Arc<App>>,
796    UriPath(id): UriPath<String>,
797    headers: HeaderMap,
798    body: Body,
799) -> Response {
800    let action = Action::TenantRetention;
801    let changed = async {
802        let (key, body) = admin_admit(&app, &headers, body, action).await?;
803        let Some(id) = TenantId::parse(&id) else {
804            let refused = Problem::Tenant(TenantError::NotFound);
805            return Err(app
806                .refuse(None, refusal(key, action, refused), refused)
807                .await);
808        };
809        let new: NewRetention = admin_body(&app, key, action, &body).await?;
810        let tenants = app.tenants.clone();
811        blocking(move || tenants.set_retention(Actor::Key(key), id, new.retention))
812            .await
813            .map_err(Problem::Tenant)?;
814        Ok((id, new.retention))
815    };
816    match changed.await {
817        Ok((id, retention)) => json_answer(
818            StatusCode::OK,
819            &serde_json::json!({
820                "id": id.to_string(),
821                "retention": retention.map(|r| r.to_string()),
822            }),
823        ),
824        Err(refused) => refused.into_response(),
825    }
826}
827
828/// `DELETE /admin/tenants/{id}`: removes a tenant that keeps no version,
829/// or with `?force=true` one that does, unless a uuid of it is under a
830/// legal hold; answers 204.
831async fn tenant_remove(
832    State(app): State<Arc<App>>,
833    UriPath(id): UriPath<String>,
834    uri: Uri,
835    headers: HeaderMap,
836    body: Body,
837) -> Response {
838    let action = Action::TenantRemove;
839    let removed = async {
840        let (key, _) = admin_admit(&app, &headers, body, action).await?;
841        let refuse = |refused| app.refuse(None, refusal(key, action, refused), refused);
842        let Some(id) = TenantId::parse(&id) else {
843            return Err(refuse(Problem::Tenant(TenantError::NotFound)).await);
844        };
845        let Some(removal) = removal(uri.query()) else {
846            return Err(refuse(Problem::BadQuery).await);
847        };
848        let tenants = app.tenants.clone();
849        // Tenants records its own outcomes.
850        blocking(move || tenants.remove(Actor::Key(key), id, removal))
851            .await
852            .map_err(Problem::Tenant)
853    };
854    match removed.await {
855        Ok(()) => StatusCode::NO_CONTENT.into_response(),
856        Err(refused) => refused.into_response(),
857    }
858}
859
860/// How far a removal asked over HTTP may go: with no query or
861/// `force=false` only an empty tenant, with `force=true` its vCons too.
862/// No query overrides a legal hold; that is the command line's alone.
863fn removal(query: Option<&str>) -> Option<Removal> {
864    match query {
865        None | Some("force=false") => Some(Removal::Empty),
866        Some("force=true") => Some(Removal::Force),
867        Some(_) => None,
868    }
869}
870
871/// The body of `POST /admin/tenants/{id}/keys`.
872#[derive(serde::Deserialize)]
873#[serde(deny_unknown_fields)]
874struct NewKeyBody {
875    scopes: Vec<String>,
876    label: String,
877}
878
879/// `POST /admin/tenants/{id}/keys`: makes a key for the tenant and
880/// answers 201 with its id and token. The token is shown this once; only
881/// its hash is kept.
882async fn key_create(
883    State(app): State<Arc<App>>,
884    UriPath(id): UriPath<String>,
885    headers: HeaderMap,
886    body: Body,
887) -> Response {
888    let action = Action::KeyCreate;
889    let made = async {
890        let (key, body) = admin_admit(&app, &headers, body, action).await?;
891        let refuse = |refused| app.refuse(None, refusal(key, action, refused), refused);
892        let Some(id) = TenantId::parse(&id) else {
893            return Err(refuse(Problem::Tenant(TenantError::NotFound)).await);
894        };
895        let new: NewKeyBody = admin_body(&app, key, action, &body).await?;
896        let Some(scopes) = Scopes::named(&new.scopes) else {
897            return Err(refuse(Problem::Tenant(TenantError::BadScopes)).await);
898        };
899        let tenants = app.tenants.clone();
900        // Tenants records its own outcomes.
901        blocking(move || tenants.create_key(Actor::Key(key), Some(id), scopes, &new.label))
902            .await
903            .map_err(Problem::Tenant)
904    };
905    match made.await {
906        Ok(made) => {
907            let body = serde_json::json!({ "id": made.id.to_string(), "token": made.token });
908            let mut answer = json_answer(StatusCode::CREATED, &body);
909            answer
910                .headers_mut()
911                .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
912            answer
913        }
914        Err(refused) => refused.into_response(),
915    }
916}
917
918/// `GET /admin/tenants/{id}/keys`: the tenant's keys, oldest first, with
919/// their ids, scopes, labels and times (RFC 3339, UTC), never a token.
920async fn key_list(
921    State(app): State<Arc<App>>,
922    UriPath(id): UriPath<String>,
923    headers: HeaderMap,
924    body: Body,
925) -> Response {
926    let action = Action::KeyList;
927    let listed = async {
928        let (key, _) = admin_admit(&app, &headers, body, action).await?;
929        let Some(id) = TenantId::parse(&id) else {
930            let refused = Problem::Tenant(TenantError::NotFound);
931            return Err(app
932                .refuse(None, refusal(key, action, refused), refused)
933                .await);
934        };
935        let tenants = app.tenants.clone();
936        blocking(move || tenants.list_keys(Actor::Key(key), Some(id)))
937            .await
938            .map_err(Problem::Tenant)
939    };
940    let keys = match listed.await {
941        Ok(keys) => keys,
942        Err(refused) => return refused.into_response(),
943    };
944    let listed: Option<Vec<serde_json::Value>> = keys.iter().map(KeyInfo::to_json).collect();
945    match listed {
946        Some(listed) => json_answer(StatusCode::OK, &serde_json::Value::Array(listed)),
947        None => Problem::Tenant(TenantError::Io).into_response(),
948    }
949}
950
951/// `DELETE /admin/keys/{id}`: revokes the key, so its next request is
952/// refused; 204 again for a key already revoked.
953async fn key_revoke(
954    State(app): State<Arc<App>>,
955    UriPath(id): UriPath<String>,
956    headers: HeaderMap,
957    body: Body,
958) -> Response {
959    let action = Action::KeyRevoke;
960    let revoked = async {
961        let (key, _) = admin_admit(&app, &headers, body, action).await?;
962        let Some(id) = KeyId::parse(&id) else {
963            let refused = Problem::UnknownKey;
964            return Err(app
965                .refuse(None, refusal(key, action, refused), refused)
966                .await);
967        };
968        let tenants = app.tenants.clone();
969        // Tenants records its own outcomes.
970        blocking(move || tenants.revoke_key(Actor::Key(key), id))
971            .await
972            .map_err(|error| match error {
973                TenantError::NotFound => Problem::UnknownKey,
974                error => Problem::Tenant(error),
975            })
976    };
977    match revoked.await {
978        Ok(()) => StatusCode::NO_CONTENT.into_response(),
979        Err(refused) => refused.into_response(),
980    }
981}
982
983/// `GET /health`: any valid key may ask; 200 when server.sqlite answers,
984/// and 503 for a key whose tenant is being removed.
985///
986/// Health requests are counted, one record per key, status and minute, in
987/// the key's tenant log, or the admin log for an operator key or a tenant
988/// whose log cannot be opened; a health request that cannot be counted is
989/// not healthy.
990async fn health(State(app): State<Arc<App>>, headers: HeaderMap) -> Response {
991    let caller = match authenticated(&app, &headers).await {
992        Ok(caller) => caller,
993        Err(refused) => return refused.into_response(),
994    };
995    let tenants = app.tenants.clone();
996    let (healthy, counted) = blocking(move || {
997        // A tenant being removed answers 503 here too, as on every route.
998        let store = caller.tenant.map(|t| tenants.store(t));
999        let closing = matches!(store, Some(Err(TenantError::Closing)));
1000        let healthy = !closing && tenants.check().is_ok();
1001        let entry = Entry {
1002            actor: Actor::Key(caller.key),
1003            action: Action::Health,
1004            target: None,
1005            version: None,
1006            status: if healthy { 200 } else { 503 },
1007        };
1008        let counted = match store {
1009            Some(Ok(store)) => store.count(&entry).is_ok(),
1010            _ => tenants.count_admin(&entry).is_ok(),
1011        };
1012        (healthy, counted)
1013    })
1014    .await;
1015    match (healthy, counted) {
1016        (_, false) => Problem::AuditFailed.into_response(),
1017        (false, true) => Problem::Unavailable.into_response(),
1018        (true, true) => (
1019            StatusCode::OK,
1020            [(CONTENT_TYPE, "application/json")],
1021            r#"{"status":"ok"}"#,
1022        )
1023            .into_response(),
1024    }
1025}
1026
1027/// `GET /key`: any valid key reads what it may do: its own id, its tenant
1028/// (null for an operator key) and its scopes, so a client can tell which
1029/// requests it may make.
1030///
1031/// The read is recorded before the answer, in the key's tenant log, or the
1032/// admin log for an operator key. A key whose tenant is being removed, or
1033/// whose tenant's log cannot be opened, is refused with 503, recorded in
1034/// the admin log.
1035async fn key_info(State(app): State<Arc<App>>, headers: HeaderMap) -> Response {
1036    let caller = match authenticated(&app, &headers).await {
1037        Ok(caller) => caller,
1038        Err(refused) => return refused.into_response(),
1039    };
1040    let entry = |status| Entry {
1041        actor: Actor::Key(caller.key),
1042        action: Action::KeyInfo,
1043        target: Some(caller.key.to_string()),
1044        version: None,
1045        status,
1046    };
1047    let store = match caller.tenant {
1048        Some(t) => {
1049            let tenants = app.tenants.clone();
1050            match blocking(move || tenants.store(t)).await {
1051                Ok(store) => Some(store),
1052                Err(_) => {
1053                    let refused = Problem::Unavailable;
1054                    let entry = entry(refused.status().as_u16());
1055                    return app.refuse(None, entry, refused).await.into_response();
1056                }
1057            }
1058        }
1059        None => None,
1060    };
1061    let tenants = app.tenants.clone();
1062    let record = entry(200);
1063    let recorded = blocking(move || match store {
1064        Some(store) => store.log(&record).is_ok(),
1065        None => tenants.record_alone(&record).is_ok(),
1066    })
1067    .await;
1068    if !recorded {
1069        return Problem::AuditFailed.into_response();
1070    }
1071    let body = serde_json::json!({
1072        "id": caller.key.to_string(),
1073        "tenant": caller.tenant.map(|t| t.to_string()),
1074        "scopes": caller.scopes.names(),
1075    });
1076    let mut answer = json_answer(StatusCode::OK, &body);
1077    answer
1078        .headers_mut()
1079        .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
1080    answer
1081}
1082
1083/// The audit record of a request refused before Store saw it.
1084fn refusal(key: KeyId, action: Action, refused: Problem) -> Entry {
1085    Entry {
1086        actor: Actor::Key(key),
1087        action,
1088        target: None,
1089        version: None,
1090        status: refused.status().as_u16(),
1091    }
1092}
1093
1094/// A version id as a strong entity tag.
1095fn entity_tag(version: VersionId) -> String {
1096    format!("\"{version}\"")
1097}
1098
1099/// Whether a request's media type is one a vCon may be sent as:
1100/// `application/vcon`, `application/vcon+gzip` or `application/json`, in
1101/// any case and with any parameters.
1102fn is_vcon_media_type(content_type: &str) -> bool {
1103    let essence = content_type.split(';').next().unwrap_or("").trim();
1104    [
1105        "application/vcon",
1106        "application/vcon+gzip",
1107        "application/json",
1108    ]
1109    .iter()
1110    .any(|taken| essence.eq_ignore_ascii_case(taken))
1111}
1112
1113/// A request the pipeline let through, with its body read.
1114struct Admitted {
1115    caller: Caller,
1116    tenant: TenantId,
1117    store: Arc<Store>,
1118    body: Bytes,
1119}
1120
1121/// The steps every vCon request takes before its route's own work, in
1122/// order: authenticate, check the scope, open the caller's tenant, read
1123/// the body within the limits. A refusal at any step is recorded here,
1124/// since Store never sees the request.
1125async fn admit(
1126    app: &Arc<App>,
1127    headers: &HeaderMap,
1128    body: Body,
1129    scope: Scope,
1130    action: Action,
1131) -> Result<Admitted, Problem> {
1132    let caller = authenticated(app, headers).await?;
1133    let entry = |refused| refusal(caller.key, action, refused);
1134    let Ok(tenant) = access::tenant_for(&caller, scope) else {
1135        let refused = Problem::Forbidden;
1136        return Err(app.refuse(caller.tenant, entry(refused), refused).await);
1137    };
1138    let tenants = app.tenants.clone();
1139    let Ok(store) = blocking(move || tenants.store(tenant)).await else {
1140        let refused = Problem::Unavailable;
1141        return Err(app.refuse(None, entry(refused), refused).await);
1142    };
1143    match read_body(body, headers, &app.limits).await {
1144        Ok(body) => Ok(Admitted {
1145            caller,
1146            tenant,
1147            store,
1148            body,
1149        }),
1150        Err(refused) => Err(app.refuse(Some(tenant), entry(refused), refused).await),
1151    }
1152}
1153
1154/// The caller a request's bearer key names: 401 for a missing or bad
1155/// key, and 503 when the key table cannot be read.
1156pub(crate) async fn authenticated(app: &Arc<App>, headers: &HeaderMap) -> Result<Caller, Problem> {
1157    let authorization = headers
1158        .get(AUTHORIZATION)
1159        .map(|v| v.to_str().unwrap_or_default().to_owned());
1160    let tenants = app.tenants.clone();
1161    blocking(move || access::authenticate(&tenants, authorization.as_deref()))
1162        .await
1163        .map_err(|e| match e {
1164            AuthError::Io => Problem::Unavailable,
1165            _ => Problem::Unauthorized,
1166        })
1167}
1168
1169impl App {
1170    /// Records a refusal in the tenant's log, or the admin log when there
1171    /// is no tenant or its log cannot be opened, and answers it; a refusal
1172    /// that cannot be recorded answers that the audit failed.
1173    pub(crate) async fn refuse(
1174        &self,
1175        tenant: Option<TenantId>,
1176        entry: Entry,
1177        refused: Problem,
1178    ) -> Problem {
1179        let tenants = self.tenants.clone();
1180        let recorded = blocking(move || match tenant.and_then(|t| tenants.store(t).ok()) {
1181            Some(store) => store.log(&entry).is_ok(),
1182            None => tenants.record_alone(&entry).is_ok(),
1183        })
1184        .await;
1185        if recorded {
1186            refused
1187        } else {
1188            Problem::AuditFailed
1189        }
1190    }
1191}
1192
1193/// Reads a body within the size limit and the time limit. A declared
1194/// length over the limit is refused before anything is read, and a body
1195/// that passes it is refused as soon as it does.
1196pub(crate) async fn read_body(
1197    body: Body,
1198    headers: &HeaderMap,
1199    limits: &Limits,
1200) -> Result<Bytes, Problem> {
1201    let declared = headers
1202        .get(CONTENT_LENGTH)
1203        .and_then(|v| v.to_str().ok())
1204        .and_then(|v| v.parse::<u64>().ok());
1205    if declared.is_some_and(|length| length > limits.body as u64) {
1206        return Err(Problem::TooLarge);
1207    }
1208    match tokio::time::timeout(limits.body_timeout, axum::body::to_bytes(body, limits.body)).await {
1209        Err(_) => Err(Problem::Timeout),
1210        Ok(Ok(body)) => Ok(body),
1211        Ok(Err(e)) if std::error::Error::source(&e).is_some_and(|s| s.is::<LengthLimitError>()) => {
1212            Err(Problem::TooLarge)
1213        }
1214        Ok(Err(_)) => Err(Problem::BadBody),
1215    }
1216}
1217
1218/// Runs blocking work, SQLite and files, off the async workers.
1219pub(crate) async fn blocking<T: Send + 'static>(work: impl FnOnce() -> T + Send + 'static) -> T {
1220    tokio::task::spawn_blocking(work)
1221        .await
1222        .expect("blocking work does not panic")
1223}
1224
1225/// An error answer: an RFC 9457 problem document with one type per error.
1226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1227pub(crate) enum Problem {
1228    Unauthorized,
1229    Forbidden,
1230    TooLarge,
1231    Timeout,
1232    BadBody,
1233    Unavailable,
1234    AuditFailed,
1235    UnsupportedMediaType,
1236    /// A uuid in the path is not in RFC 9562 text form.
1237    BadPathUuid,
1238    /// A version in the path is not a version id.
1239    BadPathVersion,
1240    /// A replacement came without `If-Match`.
1241    PreconditionRequired,
1242    /// The body's uuid is not the one in the path.
1243    PathUuidMismatch,
1244    /// Format refused the body.
1245    Invalid(Invalid),
1246    /// An admin body is not one the route takes.
1247    InvalidBody,
1248    /// The query is not one the route takes.
1249    BadQuery,
1250    /// No key has the id.
1251    UnknownKey,
1252    /// Store refused or failed the request.
1253    Store(StoreError),
1254    /// Tenants refused or failed the request.
1255    Tenant(TenantError),
1256    /// An MCP key made more tool calls than its allowance for this second.
1257    RateLimited,
1258    /// As many MCP tool calls are running as the server allows.
1259    AtCapacity,
1260    /// No MCP tool has the name called.
1261    UnknownTool,
1262    /// An MCP tool call's arguments do not fit the tool.
1263    InvalidArguments,
1264}
1265
1266/// Why Format refused a body, one problem type each.
1267#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1268pub(crate) enum Invalid {
1269    BadGzip,
1270    NotJson,
1271    DuplicateMember,
1272    AmbiguousForm,
1273    NoForm,
1274    MissingUuid,
1275    BadUuid,
1276    MissingCreatedAt,
1277    BadPayload,
1278    UuidMismatch,
1279    BadCritical,
1280    UnsupportedExtension,
1281    TooDeep,
1282    /// A refusal from the vcon crate that this server does not name.
1283    Other,
1284}
1285
1286/// The problem for Format's refusal. Inflating past the limit is a body
1287/// too large; the others are unprocessable. No problem repeats the body,
1288/// so unsupported extensions are not named.
1289fn rejected(rejection: &Rejection) -> Problem {
1290    Problem::Invalid(match rejection {
1291        Rejection::TooLarge => return Problem::TooLarge,
1292        Rejection::BadGzip => Invalid::BadGzip,
1293        Rejection::NotJson => Invalid::NotJson,
1294        Rejection::DuplicateKey => Invalid::DuplicateMember,
1295        Rejection::AmbiguousForm => Invalid::AmbiguousForm,
1296        Rejection::NoForm => Invalid::NoForm,
1297        Rejection::MissingUuid => Invalid::MissingUuid,
1298        Rejection::BadUuid => Invalid::BadUuid,
1299        Rejection::MissingCreatedAt => Invalid::MissingCreatedAt,
1300        Rejection::BadPayload => Invalid::BadPayload,
1301        Rejection::UuidMismatch => Invalid::UuidMismatch,
1302        Rejection::BadCritical => Invalid::BadCritical,
1303        Rejection::UnsupportedCritical(_) => Invalid::UnsupportedExtension,
1304        Rejection::TooDeep => Invalid::TooDeep,
1305        // A refusal a later vcon release adds, before this server names it.
1306        // Every refusal there is today is named above, so no test reaches
1307        // this arm; the problem it answers with is tested on its own.
1308        _ => Invalid::Other,
1309    })
1310}
1311
1312/// The problem for Store's refusal or failure.
1313fn failed(error: StoreError) -> Problem {
1314    Problem::Store(error)
1315}
1316
1317impl Problem {
1318    pub(crate) fn status(self) -> StatusCode {
1319        match self {
1320            Problem::Unauthorized => StatusCode::UNAUTHORIZED,
1321            Problem::Forbidden => StatusCode::FORBIDDEN,
1322            Problem::UnknownKey => StatusCode::NOT_FOUND,
1323            Problem::TooLarge => StatusCode::PAYLOAD_TOO_LARGE,
1324            Problem::Timeout => StatusCode::REQUEST_TIMEOUT,
1325            Problem::BadBody
1326            | Problem::BadPathUuid
1327            | Problem::BadPathVersion
1328            | Problem::BadQuery => StatusCode::BAD_REQUEST,
1329            Problem::PreconditionRequired => StatusCode::PRECONDITION_REQUIRED,
1330            Problem::PathUuidMismatch => StatusCode::UNPROCESSABLE_ENTITY,
1331            Problem::Unavailable | Problem::AuditFailed | Problem::AtCapacity => {
1332                StatusCode::SERVICE_UNAVAILABLE
1333            }
1334            Problem::RateLimited => StatusCode::TOO_MANY_REQUESTS,
1335            Problem::UnknownTool => StatusCode::NOT_FOUND,
1336            Problem::InvalidArguments => StatusCode::UNPROCESSABLE_ENTITY,
1337            Problem::UnsupportedMediaType => StatusCode::UNSUPPORTED_MEDIA_TYPE,
1338            Problem::Invalid(_) | Problem::InvalidBody => StatusCode::UNPROCESSABLE_ENTITY,
1339            Problem::Store(error) => match error {
1340                StoreError::Conflict | StoreError::NotDeleted | StoreError::Immutable => {
1341                    StatusCode::CONFLICT
1342                }
1343                StoreError::Gone => StatusCode::GONE,
1344                StoreError::NotFound => StatusCode::NOT_FOUND,
1345                StoreError::PreconditionFailed => StatusCode::PRECONDITION_FAILED,
1346                StoreError::LimitReached | StoreError::AuditFailed => {
1347                    StatusCode::SERVICE_UNAVAILABLE
1348                }
1349                StoreError::Damaged | StoreError::Io => StatusCode::INTERNAL_SERVER_ERROR,
1350            },
1351            Problem::Tenant(error) => match error {
1352                TenantError::NotFound => StatusCode::NOT_FOUND,
1353                TenantError::NameTaken | TenantError::HoldsVcons | TenantError::UnderHold => {
1354                    StatusCode::CONFLICT
1355                }
1356                TenantError::BadName | TenantError::BadScopes => StatusCode::UNPROCESSABLE_ENTITY,
1357                TenantError::CliOnly => StatusCode::FORBIDDEN,
1358                TenantError::LimitReached | TenantError::AuditFailed | TenantError::Closing => {
1359                    StatusCode::SERVICE_UNAVAILABLE
1360                }
1361                TenantError::WrongKey | TenantError::Io => StatusCode::INTERNAL_SERVER_ERROR,
1362            },
1363        }
1364    }
1365
1366    /// The last part of its type URI, and its title.
1367    pub(crate) fn name(self) -> (&'static str, &'static str) {
1368        match self {
1369            Problem::Unauthorized => ("unauthorized", "A valid bearer key is needed"),
1370            Problem::Forbidden => ("forbidden", "The key does not allow this"),
1371            Problem::TooLarge => ("too-large", "The body is over the size limit"),
1372            Problem::Timeout => ("timeout", "The body did not arrive in time"),
1373            Problem::BadBody => ("bad-body", "The body could not be read"),
1374            Problem::BadPathUuid => (
1375                "bad-path-uuid",
1376                "The path's uuid is not in RFC 9562 text form",
1377            ),
1378            Problem::BadPathVersion => {
1379                ("bad-path-version", "The path's version is not a version id")
1380            }
1381            Problem::PreconditionRequired => (
1382                "precondition-required",
1383                "If-Match must name the current version",
1384            ),
1385            Problem::PathUuidMismatch => (
1386                "path-uuid-mismatch",
1387                "The body's uuid is not the one in the path",
1388            ),
1389            Problem::Unavailable => ("unavailable", "The service cannot answer now"),
1390            Problem::AuditFailed => ("audit-failed", "The audit record could not be written"),
1391            Problem::UnsupportedMediaType => {
1392                ("unsupported-media-type", "This media type is not taken")
1393            }
1394            Problem::Invalid(invalid) => match invalid {
1395                Invalid::BadGzip => ("bad-gzip", "The body is marked gzip but is not"),
1396                Invalid::NotJson => ("not-json", "The body is not one JSON object"),
1397                Invalid::DuplicateMember => ("duplicate-member", "An object repeats a member"),
1398                Invalid::AmbiguousForm => {
1399                    ("ambiguous-form", "The body is both signed and encrypted")
1400                }
1401                Invalid::NoForm => ("no-form", "The body is in no vCon form"),
1402                Invalid::MissingUuid => ("missing-uuid", "The vCon has no uuid"),
1403                Invalid::BadUuid => ("bad-uuid", "The uuid is not in RFC 9562 text form"),
1404                Invalid::MissingCreatedAt => ("missing-created-at", "The vCon has no created_at"),
1405                Invalid::BadPayload => ("bad-payload", "The signed payload cannot be read"),
1406                Invalid::UuidMismatch => ("uuid-mismatch", "A signature names another uuid"),
1407                Invalid::BadCritical => (
1408                    "bad-critical",
1409                    "critical or must_support is not a list of names",
1410                ),
1411                Invalid::UnsupportedExtension => (
1412                    "unsupported-extension",
1413                    "An extension the vCon needs is not supported",
1414                ),
1415                Invalid::TooDeep => ("too-deep", "The body nests too deeply"),
1416                Invalid::Other => ("not-a-vcon", "The body is not a vCon this server can read"),
1417            },
1418            Problem::Store(error) => match error {
1419                StoreError::Conflict => ("conflict", "Another vCon holds the uuid"),
1420                StoreError::Gone => ("gone", "The vCon was deleted or purged"),
1421                StoreError::NotFound => ("not-found", "No vCon has the uuid"),
1422                StoreError::NotDeleted => ("not-deleted", "The vCon is not deleted"),
1423                StoreError::PreconditionFailed => (
1424                    "precondition-failed",
1425                    "If-Match does not name the current version",
1426                ),
1427                StoreError::Immutable => {
1428                    ("immutable", "A signed or encrypted vCon is not replaced")
1429                }
1430                StoreError::LimitReached => ("limit-reached", "The tenant key must be rotated"),
1431                StoreError::Damaged => ("damaged", "A stored version is damaged"),
1432                StoreError::AuditFailed => {
1433                    ("audit-failed", "The audit record could not be written")
1434                }
1435                StoreError::Io => ("internal", "The server failed"),
1436            },
1437            Problem::BadQuery => ("bad-query", "The query is not one this route takes"),
1438            Problem::RateLimited => ("rate-limited", "Too many tool calls from this key"),
1439            Problem::AtCapacity => ("at-capacity", "Too many tool calls are running"),
1440            Problem::UnknownTool => ("unknown-tool", "No tool has the name"),
1441            Problem::InvalidArguments => ("invalid-arguments", "The arguments do not fit the tool"),
1442            Problem::UnknownKey => ("unknown-key", "No key has the id"),
1443            Problem::InvalidBody | Problem::Tenant(TenantError::BadName) => {
1444                ("invalid-body", "The body is not one this route takes")
1445            }
1446            Problem::Tenant(error) => match error {
1447                TenantError::NotFound => ("unknown-tenant", "No tenant has the id"),
1448                TenantError::NameTaken => ("name-taken", "Another tenant has the name"),
1449                TenantError::HoldsVcons => (
1450                    "holds-vcons",
1451                    "The tenant holds vCons; removing them needs force",
1452                ),
1453                TenantError::UnderHold => {
1454                    ("under-hold", "A vCon of the tenant is under a legal hold")
1455                }
1456                TenantError::BadScopes => ("bad-scopes", "A tenant key cannot hold these scopes"),
1457                TenantError::CliOnly => ("forbidden", "The key does not allow this"),
1458                TenantError::LimitReached => ("master-key-limit", "The master key must be rotated"),
1459                TenantError::AuditFailed => {
1460                    ("audit-failed", "The audit record could not be written")
1461                }
1462                TenantError::Closing => ("unavailable", "The service cannot answer now"),
1463                TenantError::BadName | TenantError::WrongKey | TenantError::Io => {
1464                    ("internal", "The server failed")
1465                }
1466            },
1467        }
1468    }
1469}
1470
1471impl Problem {
1472    /// The problem document (RFC 9457): its type, title and status. The
1473    /// HTTP API answers it, and an MCP refusal carries it as its data.
1474    pub(crate) fn document(self) -> serde_json::Value {
1475        let (name, title) = self.name();
1476        serde_json::json!({
1477            "type": format!("https://vcon-server.com/problems/{name}"),
1478            "title": title,
1479            "status": self.status().as_u16(),
1480        })
1481    }
1482}
1483
1484impl IntoResponse for Problem {
1485    fn into_response(self) -> Response {
1486        let (status, body) = (self.status(), self.document());
1487        let mut answer = (
1488            status,
1489            [(CONTENT_TYPE, "application/problem+json")],
1490            body.to_string(),
1491        )
1492            .into_response();
1493        if self == Problem::Unauthorized {
1494            answer
1495                .headers_mut()
1496                .insert(WWW_AUTHENTICATE, HeaderValue::from_static("Bearer"));
1497        }
1498        answer
1499    }
1500}
1501
1502/// Why the TLS configuration could not be made.
1503#[derive(Debug)]
1504pub enum TlsError {
1505    /// The certificate file could not be read, or holds no certificate.
1506    Certificate,
1507    /// The key file could not be read, or holds no key.
1508    Key,
1509    /// rustls refused the pair, for instance a key that is not the
1510    /// certificate's.
1511    Refused(rustls::Error),
1512}
1513
1514/// The server's TLS configuration from a PEM certificate chain and a PEM
1515/// private key.
1516pub fn tls_config(certificate: &Path, key: &Path) -> Result<Arc<ServerConfig>, TlsError> {
1517    let chain: Vec<CertificateDer<'static>> = CertificateDer::pem_file_iter(certificate)
1518        .map_err(|_| TlsError::Certificate)?
1519        .collect::<Result<_, _>>()
1520        .map_err(|_| TlsError::Certificate)?;
1521    if chain.is_empty() {
1522        return Err(TlsError::Certificate);
1523    }
1524    let key = PrivateKeyDer::from_pem_file(key).map_err(|_| TlsError::Key)?;
1525    let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
1526    let mut config = ServerConfig::builder_with_provider(provider)
1527        .with_safe_default_protocol_versions()
1528        .map_err(TlsError::Refused)?
1529        .with_no_client_auth()
1530        .with_single_cert(chain, key)
1531        .map_err(TlsError::Refused)?;
1532    config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
1533    Ok(Arc::new(config))
1534}
1535
1536/// Serves `app` on `listener` over TLS until `shutdown` completes. A
1537/// connection whose handshake fails, such as plain HTTP, is dropped; the
1538/// server goes on.
1539pub async fn serve(
1540    listener: TcpListener,
1541    tls: Arc<ServerConfig>,
1542    app: Router,
1543    shutdown: impl Future<Output = ()>,
1544) -> io::Result<()> {
1545    let acceptor = TlsAcceptor::from(tls);
1546    let mut shutdown = std::pin::pin!(shutdown);
1547    loop {
1548        let (tcp, _) = tokio::select! {
1549            accepted = listener.accept() => accepted?,
1550            () = &mut shutdown => return Ok(()),
1551        };
1552        let acceptor = acceptor.clone();
1553        let service = TowerToHyperService::new(app.clone());
1554        tokio::spawn(async move {
1555            let Ok(stream) = acceptor.accept(tcp).await else {
1556                return;
1557            };
1558            // The connection's own errors end only the connection.
1559            let _ = auto::Builder::new(TokioExecutor::new())
1560                .serve_connection(TokioIo::new(stream), service)
1561                .await;
1562        });
1563    }
1564}
1565
1566#[cfg(test)]
1567mod tests {
1568    use std::net::SocketAddr;
1569    use std::path::PathBuf;
1570
1571    use axum::routing::get;
1572
1573    use super::*;
1574
1575    /// A self-signed certificate for localhost and its key, as PEM files.
1576    struct Certificate {
1577        _dir: tempfile::TempDir,
1578        cert: PathBuf,
1579        key: PathBuf,
1580        pem: String,
1581    }
1582
1583    fn certificate() -> Certificate {
1584        let dir = tempfile::tempdir().expect("temp dir");
1585        let made = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()])
1586            .expect("a certificate");
1587        let pem = made.cert.pem();
1588        let (cert, key) = (dir.path().join("cert.pem"), dir.path().join("key.pem"));
1589        std::fs::write(&cert, &pem).expect("write");
1590        std::fs::write(&key, made.signing_key.serialize_pem()).expect("write");
1591        Certificate {
1592            _dir: dir,
1593            cert,
1594            key,
1595            pem,
1596        }
1597    }
1598
1599    fn client(c: &Certificate, addr: SocketAddr) -> reqwest::Client {
1600        reqwest::Client::builder()
1601            .tls_certs_only([reqwest::Certificate::from_pem(c.pem.as_bytes()).expect("pem")])
1602            .resolve("localhost", addr)
1603            .no_proxy()
1604            .build()
1605            .expect("a client")
1606    }
1607
1608    #[tokio::test]
1609    async fn the_listener_speaks_tls_only() {
1610        let c = certificate();
1611        let tls = tls_config(&c.cert, &c.key).expect("loads");
1612        let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
1613        let addr = listener.local_addr().expect("addr");
1614        let app = Router::new().route("/ping", get(|| async { "pong" }));
1615        let (stop, stopped) = tokio::sync::oneshot::channel::<()>();
1616        let server = tokio::spawn(serve(listener, tls, app, async {
1617            stopped.await.ok();
1618        }));
1619        let plain = reqwest::Client::builder()
1620            .no_proxy()
1621            .build()
1622            .expect("a client")
1623            .get(format!("http://{addr}/ping"))
1624            .send()
1625            .await;
1626        assert!(plain.is_err(), "plain HTTP is not served: {plain:?}");
1627        let answer = client(&c, addr)
1628            .get(format!("https://localhost:{}/ping", addr.port()))
1629            .send()
1630            .await
1631            .expect("TLS is served, after a plain-HTTP attempt too");
1632        assert_eq!(answer.status(), 200);
1633        assert_eq!(answer.version(), reqwest::Version::HTTP_2, "h2 by ALPN");
1634        assert_eq!(answer.text().await.expect("body"), "pong");
1635        stop.send(()).expect("stop");
1636        server.await.expect("joins").expect("serves");
1637    }
1638
1639    #[test]
1640    fn a_missing_or_unusable_certificate_or_key_is_refused() {
1641        let c = certificate();
1642        let other = certificate();
1643        let dir = tempfile::tempdir().expect("temp dir");
1644        let (missing, empty) = (dir.path().join("missing"), dir.path().join("empty"));
1645        std::fs::write(&empty, "").expect("write");
1646        assert!(matches!(
1647            tls_config(&missing, &c.key),
1648            Err(TlsError::Certificate)
1649        ));
1650        assert!(matches!(
1651            tls_config(&empty, &c.key),
1652            Err(TlsError::Certificate)
1653        ));
1654        assert!(matches!(tls_config(&c.cert, &missing), Err(TlsError::Key)));
1655        assert!(matches!(tls_config(&c.cert, &empty), Err(TlsError::Key)));
1656        assert!(matches!(
1657            tls_config(&c.cert, &other.key),
1658            Err(TlsError::Refused(_))
1659        ));
1660        assert!(tls_config(&c.cert, &c.key).is_ok());
1661    }
1662
1663    pub(crate) mod pipeline {
1664        use std::net::SocketAddr;
1665
1666        use serde_json::Value;
1667        use tokio::io::{AsyncReadExt, AsyncWriteExt};
1668
1669        use super::*;
1670        use crate::access::{Scope, Scopes};
1671        use crate::audit::Actor;
1672        use crate::clock::Clock;
1673        use crate::envelope::Key;
1674        use crate::fs::OsFs;
1675        use crate::ids::TenantId;
1676
1677        pub(crate) const LIMIT: usize = 64 * 1024;
1678
1679        /// The time every test server starts at.
1680        pub(crate) const START: time::OffsetDateTime =
1681            time::macros::datetime!(2026-10-23 12:00 UTC);
1682
1683        /// The test servers' clock. It starts at START, and each reading
1684        /// moves it on by a microsecond, so times read one after another are
1685        /// in order and differ, as on a real clock, but no answer depends on
1686        /// when a test runs. A test may set it.
1687        pub(crate) struct TestClock(std::sync::Mutex<time::OffsetDateTime>);
1688
1689        impl TestClock {
1690            pub(crate) fn set(&self, now: time::OffsetDateTime) {
1691                *self.0.lock().expect("clock") = now;
1692            }
1693        }
1694
1695        impl Clock for TestClock {
1696            fn now(&self) -> time::OffsetDateTime {
1697                let mut next = self.0.lock().expect("clock");
1698                let now = *next;
1699                *next += time::Duration::MICROSECOND;
1700                now
1701            }
1702        }
1703
1704        /// A server with one tenant, on a free port. Its registry and stores
1705        /// read the time from `clock`.
1706        pub(crate) struct Server {
1707            _dir: tempfile::TempDir,
1708            certificate: Certificate,
1709            addr: SocketAddr,
1710            pub(crate) clock: Arc<TestClock>,
1711            pub(crate) tenants: Arc<Tenants>,
1712            pub(crate) tenant: TenantId,
1713            stop: Option<tokio::sync::oneshot::Sender<()>>,
1714        }
1715
1716        impl Drop for Server {
1717            fn drop(&mut self) {
1718                if let Some(stop) = self.stop.take() {
1719                    let _ = stop.send(());
1720                }
1721            }
1722        }
1723
1724        pub(crate) async fn server() -> Server {
1725            server_with(&[]).await
1726        }
1727
1728        /// A server that supports the must_support extensions named.
1729        pub(crate) async fn server_with(supported: &[&str]) -> Server {
1730            server_from(supported, Retention::Years(7)).await
1731        }
1732
1733        /// A server whose tenants that set no retention period keep
1734        /// vCons for `default`.
1735        pub(crate) async fn server_keeping(default: Retention) -> Server {
1736            server_from(&[], default).await
1737        }
1738
1739        async fn server_from(supported: &[&str], retention_default: Retention) -> Server {
1740            let dir = tempfile::tempdir().expect("temp dir");
1741            let clock = Arc::new(TestClock(std::sync::Mutex::new(START)));
1742            let tenants = Arc::new(
1743                Tenants::open(
1744                    dir.path(),
1745                    Key::generate().expect("master key"),
1746                    Arc::new(OsFs),
1747                    clock.clone(),
1748                )
1749                .expect("opens"),
1750            );
1751            let tenant = tenants.create(Actor::Cli, "acme", None).expect("a tenant");
1752            let app = Arc::new(App {
1753                tenants: tenants.clone(),
1754                limits: Limits {
1755                    body: LIMIT,
1756                    inflated: 1 << 20,
1757                    body_timeout: Duration::from_millis(300),
1758                },
1759                supported: supported.iter().map(|s| (*s).to_owned()).collect(),
1760                retention_default,
1761                mcp: crate::mcp::Limits::DEFAULT,
1762            });
1763            let certificate = certificate();
1764            let tls = tls_config(&certificate.cert, &certificate.key).expect("tls");
1765            let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
1766            let addr = listener.local_addr().expect("addr");
1767            let (stop, stopped) = tokio::sync::oneshot::channel::<()>();
1768            tokio::spawn(serve(listener, tls, router(app), async {
1769                stopped.await.ok();
1770            }));
1771            Server {
1772                _dir: dir,
1773                certificate,
1774                addr,
1775                clock,
1776                tenants,
1777                tenant,
1778                stop: Some(stop),
1779            }
1780        }
1781
1782        impl Server {
1783            pub(crate) fn dir(&self) -> &std::path::Path {
1784                self._dir.path()
1785            }
1786
1787            pub(crate) fn url(&self, path: &str) -> String {
1788                format!("https://localhost:{}{path}", self.addr.port())
1789            }
1790
1791            pub(crate) fn client(&self) -> reqwest::Client {
1792                client(&self.certificate, self.addr)
1793            }
1794
1795            /// A client that shows a redirect instead of following it.
1796            pub(crate) fn client_not_following(&self) -> reqwest::Client {
1797                let pem = self.certificate.pem.as_bytes();
1798                reqwest::Client::builder()
1799                    .tls_certs_only([reqwest::Certificate::from_pem(pem).expect("pem")])
1800                    .resolve("localhost", self.addr)
1801                    .no_proxy()
1802                    .redirect(reqwest::redirect::Policy::none())
1803                    .build()
1804                    .expect("a client")
1805            }
1806
1807            pub(crate) fn key(&self, scopes: &[Scope]) -> String {
1808                self.tenants
1809                    .create_key(Actor::Cli, Some(self.tenant), Scopes::of(scopes), "test")
1810                    .expect("a key")
1811                    .token
1812            }
1813
1814            pub(crate) fn tenant_log(&self, action: &str) -> Vec<Value> {
1815                let store = self.tenants.store(self.tenant).expect("its store");
1816                store
1817                    .records()
1818                    .into_iter()
1819                    .filter(|r| r["action"] == action)
1820                    .collect()
1821            }
1822
1823            /// Sends a POST /vcons over HTTP/1.1 with a chunked body: the
1824            /// chunks given, and then, unless `finish`, nothing more while
1825            /// the connection stays open. Returns the status line.
1826            async fn chunked(&self, token: &str, chunks: &[usize], finish: bool) -> String {
1827                let mut body = String::new();
1828                for &size in chunks {
1829                    body.push_str(&format!("{size:x}\r\n{}\r\n", "x".repeat(size)));
1830                }
1831                if finish {
1832                    body.push_str("0\r\n\r\n");
1833                }
1834                self.raw(token, "transfer-encoding: chunked", body.as_bytes())
1835                    .await
1836            }
1837
1838            /// Sends a POST /vcons over HTTP/1.1 with one header of the
1839            /// caller's and the bytes given, keeping the connection open.
1840            /// Returns the status line.
1841            async fn raw(&self, token: &str, header: &str, body: &[u8]) -> String {
1842                let mut roots = rustls::RootCertStore::empty();
1843                for cert in CertificateDer::pem_slice_iter(self.certificate.pem.as_bytes()) {
1844                    roots.add(cert.expect("pem")).expect("a root");
1845                }
1846                let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
1847                let mut config = rustls::ClientConfig::builder_with_provider(provider)
1848                    .with_safe_default_protocol_versions()
1849                    .expect("versions")
1850                    .with_root_certificates(roots)
1851                    .with_no_client_auth();
1852                config.alpn_protocols = vec![b"http/1.1".to_vec()];
1853                let tcp = tokio::net::TcpStream::connect(self.addr)
1854                    .await
1855                    .expect("connect");
1856                let mut tls = tokio_rustls::TlsConnector::from(Arc::new(config))
1857                    .connect("localhost".try_into().expect("name"), tcp)
1858                    .await
1859                    .expect("handshake");
1860                let head = format!(
1861                    "POST /vcons HTTP/1.1\r\nhost: localhost\r\nauthorization: Bearer {token}\r\n\
1862                     content-type: application/vcon\r\n{header}\r\n\r\n"
1863                );
1864                tls.write_all(head.as_bytes()).await.expect("head");
1865                // The server may answer and stop reading before all is sent.
1866                let _ = tls.write_all(body).await;
1867                let mut answer = vec![0; 4096];
1868                let read = tokio::time::timeout(Duration::from_secs(10), tls.read(&mut answer))
1869                    .await
1870                    .expect("an answer while the body is still open")
1871                    .expect("read");
1872                let answer = String::from_utf8_lossy(&answer[..read]).into_owned();
1873                answer.lines().next().unwrap_or_default().to_owned()
1874            }
1875        }
1876
1877        async fn post(s: &Server, token: Option<&str>, body: Vec<u8>) -> reqwest::Response {
1878            let mut request = s
1879                .client()
1880                .post(s.url("/vcons"))
1881                .header("content-type", "application/vcon")
1882                .body(body);
1883            if let Some(token) = token {
1884                request = request.bearer_auth(token);
1885            }
1886            request.send().await.expect("an answer")
1887        }
1888
1889        /// Checks the answer is a problem document for its status, and
1890        /// returns its type's last part.
1891        pub(crate) async fn problem(answer: reqwest::Response) -> String {
1892            assert_eq!(
1893                answer.headers()["content-type"],
1894                "application/problem+json",
1895                "{answer:?}"
1896            );
1897            let status = answer.status().as_u16();
1898            let body: Value =
1899                serde_json::from_str(&answer.text().await.expect("text")).expect("JSON");
1900            assert_eq!(body["status"], status);
1901            assert!(body["title"].as_str().is_some_and(|t| !t.is_empty()));
1902            body["type"]
1903                .as_str()
1904                .and_then(|t| t.strip_prefix("https://vcon-server.com/problems/"))
1905                .expect("a type under the server's domain")
1906                .to_owned()
1907        }
1908
1909        #[tokio::test]
1910        async fn a_request_without_a_valid_key_is_unauthorized() {
1911            let s = server().await;
1912            for token in [None, Some("vcs_nonsense")] {
1913                let answer = post(&s, token, b"{}".to_vec()).await;
1914                assert_eq!(answer.status(), 401, "{token:?}");
1915                assert_eq!(answer.headers()["www-authenticate"], "Bearer");
1916                assert_eq!(problem(answer).await, "unauthorized");
1917            }
1918        }
1919
1920        #[tokio::test]
1921        async fn a_key_table_that_cannot_be_read_makes_the_service_unavailable() {
1922            let s = server().await;
1923            let writer = s.key(&[Scope::Write]);
1924            s.tenants
1925                .conn_for_test()
1926                .execute_batch("ALTER TABLE api_key RENAME TO gone")
1927                .expect("lose the table");
1928            let answer = post(&s, Some(&writer), b"{}".to_vec()).await;
1929            assert_eq!(answer.status(), 503);
1930            assert_eq!(problem(answer).await, "unavailable");
1931        }
1932
1933        #[tokio::test]
1934        async fn a_refusal_that_cannot_be_recorded_answers_that_the_audit_failed() {
1935            let s = server().await;
1936            let reader = s.key(&[Scope::Read]);
1937            s.tenants.store(s.tenant).expect("its store").break_audit();
1938            let answer = post(&s, Some(&reader), b"{}".to_vec()).await;
1939            assert_eq!(answer.status(), 503);
1940            assert_eq!(problem(answer).await, "audit-failed");
1941
1942            let operator = s
1943                .tenants
1944                .create_key(Actor::Cli, None, Scopes::of(&[Scope::Admin]), "op")
1945                .expect("made")
1946                .token;
1947            s.tenants.break_audit();
1948            let answer = post(&s, Some(&operator), b"{}".to_vec()).await;
1949            assert_eq!(answer.status(), 503);
1950            assert_eq!(problem(answer).await, "audit-failed");
1951        }
1952
1953        #[tokio::test]
1954        async fn a_key_without_the_scope_is_forbidden_and_recorded() {
1955            let s = server().await;
1956            let reader = s.key(&[Scope::Read]);
1957            let answer = post(&s, Some(&reader), b"{}".to_vec()).await;
1958            assert_eq!(answer.status(), 403);
1959            assert_eq!(problem(answer).await, "forbidden");
1960            let refused = s.tenant_log("create");
1961            assert_eq!(refused.len(), 1);
1962            assert_eq!(refused[0]["status"], 403);
1963
1964            let operator = s
1965                .tenants
1966                .create_key(Actor::Cli, None, Scopes::of(&[Scope::Admin]), "op")
1967                .expect("made")
1968                .token;
1969            let answer = post(&s, Some(&operator), b"{}".to_vec()).await;
1970            assert_eq!(answer.status(), 403);
1971            let admin: Vec<Value> = s
1972                .tenants
1973                .admin_records()
1974                .into_iter()
1975                .filter(|r| r["action"] == "create")
1976                .collect();
1977            assert_eq!(admin.len(), 1, "an operator's refusal is in the admin log");
1978            assert_eq!(admin[0]["status"], 403);
1979        }
1980
1981        #[tokio::test]
1982        async fn a_closing_tenant_is_unavailable() {
1983            let s = server().await;
1984            let writer = s.key(&[Scope::Write]);
1985            s.tenants.close(s.tenant);
1986            let answer = post(&s, Some(&writer), b"{}".to_vec()).await;
1987            assert_eq!(answer.status(), 503);
1988            assert_eq!(problem(answer).await, "unavailable");
1989        }
1990
1991        #[tokio::test]
1992        async fn a_declared_length_over_the_limit_is_refused_at_once() {
1993            let s = server().await;
1994            let writer = s.key(&[Scope::Write]);
1995            let answer = post(&s, Some(&writer), vec![b'x'; LIMIT + 1]).await;
1996            assert_eq!(answer.status(), 413);
1997            assert_eq!(problem(answer).await, "too-large");
1998            assert_eq!(s.tenant_log("create")[0]["status"], 413);
1999            let declared = format!("content-length: {}", LIMIT + 1);
2000            assert_eq!(
2001                s.raw(&writer, &declared, b"").await,
2002                "HTTP/1.1 413 Payload Too Large",
2003                "refused on the declared length, with no byte of the body sent"
2004            );
2005        }
2006
2007        #[tokio::test]
2008        async fn a_body_over_the_limit_is_refused_before_it_is_read_in_full() {
2009            let s = server().await;
2010            let writer = s.key(&[Scope::Write]);
2011            let status = s.chunked(&writer, &[LIMIT / 2, LIMIT / 2 + 1], false).await;
2012            assert_eq!(status, "HTTP/1.1 413 Payload Too Large");
2013        }
2014
2015        #[tokio::test]
2016        async fn a_body_that_stops_arriving_times_out() {
2017            let s = server().await;
2018            let writer = s.key(&[Scope::Write]);
2019            let status = s.chunked(&writer, &[10], false).await;
2020            assert_eq!(status, "HTTP/1.1 408 Request Timeout");
2021            assert_eq!(s.tenant_log("create")[0]["status"], 408);
2022        }
2023
2024        #[tokio::test]
2025        /// The body reaches Format whole: a body of x's at the limit is
2026        /// refused as not JSON, not as too large.
2027        async fn a_request_within_the_limits_goes_on_to_its_route() {
2028            let s = server().await;
2029            let writer = s.key(&[Scope::Write]);
2030            let status = s.chunked(&writer, &[LIMIT / 2, LIMIT / 2], true).await;
2031            assert_eq!(status, "HTTP/1.1 422 Unprocessable Entity");
2032            let answer = post(&s, Some(&writer), b"{}".to_vec()).await;
2033            assert_eq!(answer.status(), 422);
2034            assert_eq!(problem(answer).await, "no-form");
2035        }
2036    }
2037
2038    #[test]
2039    fn a_removal_query_is_none_or_force_true_or_false_and_nothing_else() {
2040        assert_eq!(removal(None), Some(Removal::Empty));
2041        assert_eq!(removal(Some("force=false")), Some(Removal::Empty));
2042        assert_eq!(removal(Some("force=true")), Some(Removal::Force));
2043        for query in [
2044            "",
2045            "force",
2046            "force=",
2047            "force=1",
2048            "Force=true",
2049            "force=true&",
2050        ] {
2051            assert_eq!(removal(Some(query)), None, "{query:?}");
2052        }
2053    }
2054
2055    /// A refusal the vcon crate adds in a later release, which this
2056    /// server does not name yet, still has a problem: not-a-vcon. No test
2057    /// can send one, since every refusal there is today is named above; this
2058    /// holds the problem the catch-all arm answers with.
2059    #[test]
2060    fn a_refusal_this_server_does_not_name_is_not_a_vcon() {
2061        let problem = Problem::Invalid(Invalid::Other);
2062        assert_eq!(
2063            (problem.status().as_u16(), problem.name().0),
2064            (422, "not-a-vcon")
2065        );
2066    }
2067
2068    #[tokio::test]
2069    async fn every_refusal_from_format_has_its_own_problem() {
2070        let cases = [
2071            (Rejection::TooLarge, 413, "too-large"),
2072            (Rejection::TooDeep, 422, "too-deep"),
2073            (Rejection::BadGzip, 422, "bad-gzip"),
2074            (Rejection::NotJson, 422, "not-json"),
2075            (Rejection::DuplicateKey, 422, "duplicate-member"),
2076            (Rejection::AmbiguousForm, 422, "ambiguous-form"),
2077            (Rejection::NoForm, 422, "no-form"),
2078            (Rejection::MissingUuid, 422, "missing-uuid"),
2079            (Rejection::BadUuid, 422, "bad-uuid"),
2080            (Rejection::MissingCreatedAt, 422, "missing-created-at"),
2081            (Rejection::BadPayload, 422, "bad-payload"),
2082            (Rejection::UuidMismatch, 422, "uuid-mismatch"),
2083            (Rejection::BadCritical, 422, "bad-critical"),
2084            (
2085                Rejection::UnsupportedCritical(vec!["secret-name".to_owned()]),
2086                422,
2087                "unsupported-extension",
2088            ),
2089        ];
2090        for (rejection, status, name) in &cases {
2091            let problem = rejected(rejection);
2092            assert_eq!(
2093                (problem.status().as_u16(), problem.name().0),
2094                (*status, *name),
2095                "{rejection:?}"
2096            );
2097            let body = axum::body::to_bytes(problem.into_response().into_body(), usize::MAX)
2098                .await
2099                .expect("the body");
2100            let answer = String::from_utf8(body.to_vec()).expect("UTF-8");
2101            assert!(answer.contains(&format!("\"status\":{status}")), "{answer}");
2102            assert!(
2103                !answer.contains("secret-name"),
2104                "the name is not repeated: {answer}"
2105            );
2106        }
2107    }
2108
2109    #[test]
2110    fn every_store_outcome_has_its_problem() {
2111        let cases = [
2112            (StoreError::Conflict, 409, "conflict"),
2113            (StoreError::Gone, 410, "gone"),
2114            (StoreError::NotFound, 404, "not-found"),
2115            (StoreError::NotDeleted, 409, "not-deleted"),
2116            (StoreError::PreconditionFailed, 412, "precondition-failed"),
2117            (StoreError::Immutable, 409, "immutable"),
2118            (StoreError::LimitReached, 503, "limit-reached"),
2119            (StoreError::Damaged, 500, "damaged"),
2120            (StoreError::AuditFailed, 503, "audit-failed"),
2121            (StoreError::Io, 500, "internal"),
2122        ];
2123        for (error, status, name) in cases {
2124            let problem = failed(error);
2125            assert_eq!(
2126                (problem.status().as_u16(), problem.name().0),
2127                (status, name),
2128                "{error:?}"
2129            );
2130        }
2131    }
2132
2133    #[test]
2134    fn every_tenants_outcome_has_its_problem() {
2135        let cases = [
2136            (TenantError::NotFound, 404, "unknown-tenant"),
2137            (TenantError::NameTaken, 409, "name-taken"),
2138            (TenantError::BadName, 422, "invalid-body"),
2139            (TenantError::HoldsVcons, 409, "holds-vcons"),
2140            (TenantError::UnderHold, 409, "under-hold"),
2141            (TenantError::LimitReached, 503, "master-key-limit"),
2142            (TenantError::AuditFailed, 503, "audit-failed"),
2143            (TenantError::Closing, 503, "unavailable"),
2144            (TenantError::CliOnly, 403, "forbidden"),
2145            (TenantError::BadScopes, 422, "bad-scopes"),
2146            (TenantError::WrongKey, 500, "internal"),
2147            (TenantError::Io, 500, "internal"),
2148        ];
2149        for (error, status, name) in cases {
2150            let problem = Problem::Tenant(error);
2151            assert_eq!(
2152                (problem.status().as_u16(), problem.name().0),
2153                (status, name),
2154                "{error:?}"
2155            );
2156        }
2157    }
2158
2159    mod vcons {
2160        use std::io::Write;
2161
2162        use super::pipeline::{Server, problem, server};
2163        use crate::access::Scope;
2164        use crate::store::Caller as StoreCaller;
2165        use crate::store::testing::vcon;
2166
2167        const UUID: &str = "01a07da8-c2bb-83e5-b9a2-279e0d16bc46";
2168
2169        fn uuid(n: u8) -> String {
2170            format!("01a07da8-c2bb-83e5-b9a2-279e0d16bc{n:02x}")
2171        }
2172
2173        async fn post(
2174            s: &Server,
2175            token: &str,
2176            content_type: Option<&str>,
2177            body: Vec<u8>,
2178        ) -> reqwest::Response {
2179            let mut request = s
2180                .client()
2181                .post(s.url("/vcons"))
2182                .bearer_auth(token)
2183                .body(body);
2184            if let Some(content_type) = content_type {
2185                request = request.header("content-type", content_type);
2186            }
2187            request.send().await.expect("an answer")
2188        }
2189
2190        fn gzip(body: &[u8]) -> Vec<u8> {
2191            let mut encoder =
2192                flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
2193            encoder.write_all(body).expect("gzip");
2194            encoder.finish().expect("gzip")
2195        }
2196
2197        fn store_caller() -> StoreCaller {
2198            StoreCaller {
2199                key: crate::ids::KeyId([9; 8]),
2200            }
2201        }
2202
2203        #[tokio::test]
2204        async fn a_new_vcon_is_created_with_its_location_and_tag() {
2205            let s = server().await;
2206            let writer = s.key(&[Scope::Write]);
2207            let (found, body) = vcon(UUID, "first");
2208            let answer = post(&s, &writer, Some("application/vcon"), body.clone()).await;
2209            assert_eq!(answer.status(), 201);
2210            assert_eq!(answer.headers()["location"], format!("/vcons/{UUID}"));
2211            let tag = answer.headers()["etag"].to_str().expect("ascii").to_owned();
2212            let stored = s
2213                .tenants
2214                .store(s.tenant)
2215                .expect("its store")
2216                .get(&store_caller(), found.uuid)
2217                .expect("stored");
2218            assert_eq!(tag, format!("\"{}\"", stored.version), "a strong tag");
2219            assert_eq!(stored.body, body);
2220
2221            let again = post(&s, &writer, Some("application/vcon"), body).await;
2222            assert_eq!(again.status(), 200, "the same bytes are already current");
2223            assert_eq!(again.headers()["etag"], tag.as_str());
2224        }
2225
2226        #[tokio::test]
2227        async fn another_vcon_with_the_uuid_conflicts() {
2228            let s = server().await;
2229            let writer = s.key(&[Scope::Write]);
2230            let (_, first) = vcon(UUID, "first");
2231            let (_, other) = vcon(UUID, "other");
2232            assert_eq!(post(&s, &writer, None, first).await.status(), 201);
2233            let answer = post(&s, &writer, None, other).await;
2234            assert_eq!(answer.status(), 409);
2235            assert_eq!(problem(answer).await, "conflict");
2236            let statuses: Vec<_> = s
2237                .tenant_log("create")
2238                .into_iter()
2239                .map(|r| r["status"].clone())
2240                .collect();
2241            assert_eq!(statuses, [201, 409], "Store records its own, once");
2242        }
2243
2244        #[tokio::test]
2245        async fn a_vcon_needing_an_unsupported_extension_is_unprocessable() {
2246            let s = super::pipeline::server_with(&["known"]).await;
2247            let writer = s.key(&[Scope::Write]);
2248            let needing = |n: u8, extension: &str| {
2249                format!(
2250                    r#"{{"uuid": "{}", "created_at": "2026-10-23T12:00:00Z", "must_support": ["{extension}"], "parties": []}}"#,
2251                    uuid(n)
2252                )
2253                .into_bytes()
2254            };
2255            assert_eq!(
2256                post(&s, &writer, None, needing(1, "known")).await.status(),
2257                201
2258            );
2259            let answer = post(&s, &writer, None, needing(2, "unknown")).await;
2260            assert_eq!(answer.status(), 422);
2261            assert_eq!(problem(answer).await, "unsupported-extension");
2262        }
2263
2264        #[tokio::test]
2265        async fn a_media_type_that_is_not_text_is_unsupported() {
2266            let s = server().await;
2267            let writer = s.key(&[Scope::Write]);
2268            let (_, body) = vcon(UUID, "first");
2269            let answer = s
2270                .client()
2271                .post(s.url("/vcons"))
2272                .bearer_auth(&writer)
2273                .header(
2274                    "content-type",
2275                    reqwest::header::HeaderValue::from_bytes(b"application/vcon\xff")
2276                        .expect("a header value"),
2277                )
2278                .body(body)
2279                .send()
2280                .await
2281                .expect("an answer");
2282            assert_eq!(answer.status(), 415);
2283        }
2284
2285        #[tokio::test]
2286        async fn a_deleted_vcon_is_gone() {
2287            let s = server().await;
2288            let writer = s.key(&[Scope::Write]);
2289            let (found, body) = vcon(UUID, "first");
2290            assert_eq!(post(&s, &writer, None, body.clone()).await.status(), 201);
2291            s.tenants
2292                .store(s.tenant)
2293                .expect("its store")
2294                .delete(
2295                    &store_caller(),
2296                    found.uuid,
2297                    crate::retention::Retention::Years(7),
2298                )
2299                .expect("deleted");
2300            let answer = post(&s, &writer, None, body).await;
2301            assert_eq!(answer.status(), 410);
2302            assert_eq!(problem(answer).await, "gone");
2303        }
2304
2305        #[tokio::test]
2306        async fn the_vcon_media_types_json_and_none_are_taken() {
2307            let s = server().await;
2308            let writer = s.key(&[Scope::Write]);
2309            let cases: [(Option<&str>, bool); 6] = [
2310                (None, false),
2311                (Some("application/vcon"), false),
2312                (Some("application/vcon+gzip"), true),
2313                (Some("application/json"), false),
2314                (Some("Application/VCON; charset=utf-8"), false),
2315                (Some("APPLICATION/VCON+GZIP ; q=1"), true),
2316            ];
2317            for (n, (content_type, gzipped)) in cases.into_iter().enumerate() {
2318                let (_, body) = vcon(&uuid(n as u8), "typed");
2319                let body = if gzipped { gzip(&body) } else { body };
2320                let answer = post(&s, &writer, content_type, body).await;
2321                assert_eq!(answer.status(), 201, "{content_type:?}");
2322            }
2323        }
2324
2325        #[tokio::test]
2326        async fn another_media_type_is_unsupported_and_recorded() {
2327            let s = server().await;
2328            let writer = s.key(&[Scope::Write]);
2329            let (_, body) = vcon(UUID, "first");
2330            for content_type in ["text/plain", "application/vcon-json", "application/xml"] {
2331                let answer = post(&s, &writer, Some(content_type), body.clone()).await;
2332                assert_eq!(answer.status(), 415, "{content_type}");
2333                assert_eq!(problem(answer).await, "unsupported-media-type");
2334            }
2335            let refused: Vec<_> = s
2336                .tenant_log("create")
2337                .iter()
2338                .map(|r| r["status"].clone())
2339                .collect();
2340            assert_eq!(refused, [415, 415, 415], "one record per refusal");
2341        }
2342
2343        #[tokio::test]
2344        async fn a_body_format_refuses_is_unprocessable_and_recorded() {
2345            let s = server().await;
2346            let writer = s.key(&[Scope::Write]);
2347            let answer = post(&s, &writer, None, b"not json".to_vec()).await;
2348            assert_eq!(answer.status(), 422);
2349            assert_eq!(problem(answer).await, "not-json");
2350            let (_, body) = vcon(UUID, "first");
2351            let answer = post(&s, &writer, Some("application/vcon+gzip"), body).await;
2352            assert_eq!(answer.status(), 422);
2353            assert_eq!(problem(answer).await, "bad-gzip");
2354            let refused = s.tenant_log("create");
2355            assert_eq!(refused.len(), 2);
2356            assert_eq!(refused[0]["status"], 422);
2357        }
2358
2359        #[tokio::test]
2360        async fn a_body_that_inflates_past_its_limit_is_too_large() {
2361            let s = server().await;
2362            let writer = s.key(&[Scope::Write]);
2363            let bomb = gzip(&vec![b' '; 2 << 20]);
2364            assert!(bomb.len() < 64 * 1024, "small on the wire");
2365            let answer = post(&s, &writer, Some("application/vcon+gzip"), bomb).await;
2366            assert_eq!(answer.status(), 413);
2367            assert_eq!(problem(answer).await, "too-large");
2368        }
2369
2370        #[tokio::test]
2371        async fn a_body_nested_too_deeply_is_unprocessable_and_recorded() {
2372            use base64::Engine;
2373            use base64::engine::general_purpose::URL_SAFE_NO_PAD;
2374            let s = server().await;
2375            let writer = s.key(&[Scope::Write]);
2376            let (_, mut signed) = vcon(UUID, "first");
2377            for _ in 0..=vcon::read::MAX_SIGNED_LAYERS {
2378                signed = serde_json::json!({
2379                    "payload": URL_SAFE_NO_PAD.encode(gzip(&signed)),
2380                    "signatures": [{}],
2381                })
2382                .to_string()
2383                .into_bytes();
2384            }
2385            let open = "[".repeat(vcon::read::MAX_JSON_DEPTH);
2386            let close = "]".repeat(vcon::read::MAX_JSON_DEPTH);
2387            let json = format!(r#"{{"uuid":"{UUID}","created_at":"x","a":{open}{close}}}"#);
2388            for body in [signed, json.into_bytes()] {
2389                let answer = post(&s, &writer, None, body).await;
2390                assert_eq!(answer.status(), 422);
2391                assert_eq!(problem(answer).await, "too-deep");
2392            }
2393            let refused = s.tenant_log("create");
2394            assert_eq!(refused.len(), 2);
2395            assert!(refused.iter().all(|r| r["status"] == 422));
2396        }
2397
2398        #[tokio::test]
2399        async fn a_create_whose_record_fails_is_unavailable() {
2400            let s = server().await;
2401            let writer = s.key(&[Scope::Write]);
2402            s.tenants.store(s.tenant).expect("its store").break_audit();
2403            let (_, body) = vcon(UUID, "first");
2404            let answer = post(&s, &writer, None, body).await;
2405            assert_eq!(answer.status(), 503);
2406            assert_eq!(problem(answer).await, "audit-failed");
2407        }
2408    }
2409
2410    mod legal {
2411        use reqwest::Method;
2412        use serde_json::Value;
2413
2414        use super::pipeline::{Server, problem, server};
2415        use crate::access::{Scope, Scopes};
2416        use crate::audit::Actor;
2417        use crate::ids::VersionId;
2418        use crate::retention::Retention;
2419
2420        const UUID: &str = "01a07da8-c2bb-83e5-b9a2-279e0d16bc46";
2421        const NEVER: &str = "0199e5c6-0d7a-7c11-8000-000000000001";
2422
2423        fn key(s: &Server, scopes: &[Scope]) -> String {
2424            s.tenants
2425                .create_key(Actor::Cli, Some(s.tenant), Scopes::of(scopes), "test")
2426                .expect("a key")
2427                .token
2428        }
2429
2430        fn a4() -> Vec<u8> {
2431            std::fs::read(format!(
2432                "{}/../vcon/tests/fixtures/ietf/ab_call_ext_rec.vcon",
2433                env!("CARGO_MANIFEST_DIR")
2434            ))
2435            .expect("A.4")
2436        }
2437
2438        async fn call(s: &Server, method: Method, path: &str, token: &str) -> reqwest::Response {
2439            s.client()
2440                .request(method, s.url(path))
2441                .bearer_auth(token)
2442                .send()
2443                .await
2444                .expect("an answer")
2445        }
2446
2447        /// Stores A.4 and returns its entity tag.
2448        async fn stored(s: &Server) -> String {
2449            let writer = key(s, &[Scope::Write]);
2450            let answer = s
2451                .client()
2452                .post(s.url("/vcons"))
2453                .bearer_auth(&writer)
2454                .header("content-type", "application/vcon")
2455                .body(a4())
2456                .send()
2457                .await
2458                .expect("an answer");
2459            assert_eq!(answer.status(), 201);
2460            answer.headers()["etag"].to_str().expect("tag").to_owned()
2461        }
2462
2463        async fn deleted(s: &Server) {
2464            let deleter = key(s, &[Scope::Delete]);
2465            let path = format!("/vcons/{UUID}");
2466            assert_eq!(call(s, Method::DELETE, &path, &deleter).await.status(), 204);
2467        }
2468
2469        /// Every legal route for `uuid`, with `version` for the one that
2470        /// names a version.
2471        fn routes(uuid: &str, version: &str) -> Vec<(Method, String)> {
2472            vec![
2473                (Method::GET, format!("/legal/vcons/{uuid}")),
2474                (Method::GET, format!("/legal/vcons/{uuid}/versions")),
2475                (
2476                    Method::GET,
2477                    format!("/legal/vcons/{uuid}/versions/{version}"),
2478                ),
2479                (Method::POST, format!("/legal/vcons/{uuid}/restore")),
2480                (Method::PUT, format!("/legal/vcons/{uuid}/hold")),
2481                (Method::DELETE, format!("/legal/vcons/{uuid}/hold")),
2482            ]
2483        }
2484
2485        fn bare(tag: &str) -> String {
2486            tag.trim_matches('"').to_owned()
2487        }
2488
2489        #[tokio::test]
2490        async fn every_legal_route_needs_the_legal_scope() {
2491            let s = server().await;
2492            let tag = stored(&s).await;
2493            let others = key(&s, &[Scope::Write, Scope::Read, Scope::Delete]);
2494            for (method, path) in routes(UUID, &bare(&tag)) {
2495                let answer = call(&s, method.clone(), &path, &others).await;
2496                assert_eq!(answer.status(), 403, "{method} {path}");
2497            }
2498        }
2499
2500        #[tokio::test]
2501        async fn a_deleted_vcon_reads_back_byte_for_byte_with_a_legal_key() {
2502            let s = server().await;
2503            let tag = stored(&s).await;
2504            let legal = key(&s, &[Scope::Legal]);
2505            let path = format!("/legal/vcons/{UUID}");
2506
2507            let live = call(&s, Method::GET, &path, &legal).await;
2508            assert_eq!(live.status(), 200);
2509            assert_eq!(live.headers()["vcon-state"], "live");
2510
2511            deleted(&s).await;
2512            let answer = call(&s, Method::GET, &path, &legal).await;
2513            assert_eq!(answer.status(), 200);
2514            assert_eq!(answer.headers()["vcon-state"], "deleted");
2515            assert_eq!(answer.headers()["etag"], tag.as_str());
2516            assert_eq!(answer.headers()["content-type"], "application/vcon");
2517            assert_eq!(
2518                answer.bytes().await.expect("body").as_ref(),
2519                a4().as_slice()
2520            );
2521            assert_eq!(
2522                s.tenant_log("legal_read").last().expect("recorded")["status"],
2523                200
2524            );
2525        }
2526
2527        #[tokio::test]
2528        async fn the_version_list_and_a_named_version() {
2529            let s = server().await;
2530            let tag = stored(&s).await;
2531            let legal = key(&s, &[Scope::Legal]);
2532            let live = call(
2533                &s,
2534                Method::GET,
2535                &format!("/legal/vcons/{UUID}/versions"),
2536                &legal,
2537            )
2538            .await;
2539            let live: Value =
2540                serde_json::from_slice(&live.bytes().await.expect("body")).expect("json");
2541            assert_eq!(live[0]["current"], true, "live, so current");
2542            assert!(live[0]["hidden_at"].is_null(), "not hidden while live");
2543            deleted(&s).await;
2544
2545            let list = call(
2546                &s,
2547                Method::GET,
2548                &format!("/legal/vcons/{UUID}/versions"),
2549                &legal,
2550            )
2551            .await;
2552            assert_eq!(list.status(), 200);
2553            assert_eq!(list.headers()["content-type"], "application/json");
2554            let list: Value =
2555                serde_json::from_slice(&list.bytes().await.expect("body")).expect("json");
2556            let versions = list.as_array().expect("a list");
2557            assert_eq!(versions.len(), 1);
2558            assert_eq!(versions[0]["version"], bare(&tag));
2559            assert_eq!(versions[0]["current"], false);
2560            assert!(
2561                versions[0]["stored_at"]
2562                    .as_str()
2563                    .expect("a time")
2564                    .ends_with('Z')
2565            );
2566            assert!(versions[0]["hidden_at"].is_string(), "hidden by the delete");
2567
2568            let one = format!("/legal/vcons/{UUID}/versions/{}", bare(&tag));
2569            let answer = call(&s, Method::GET, &one, &legal).await;
2570            assert_eq!(answer.status(), 200);
2571            assert_eq!(answer.headers()["etag"], tag.as_str());
2572            assert_eq!(
2573                answer.bytes().await.expect("body").as_ref(),
2574                a4().as_slice()
2575            );
2576
2577            let unknown = format!("/legal/vcons/{UUID}/versions/{}", VersionId::generate());
2578            let answer = call(&s, Method::GET, &unknown, &legal).await;
2579            assert_eq!(answer.status(), 404);
2580            assert_eq!(problem(answer).await, "not-found");
2581
2582            let bad = format!("/legal/vcons/{UUID}/versions/not-a-version");
2583            let answer = call(&s, Method::GET, &bad, &legal).await;
2584            assert_eq!(answer.status(), 400);
2585            assert_eq!(problem(answer).await, "bad-path-version");
2586            assert_eq!(
2587                s.tenant_log("legal_read").last().expect("recorded")["status"],
2588                400
2589            );
2590        }
2591
2592        #[tokio::test]
2593        async fn restore_answers_its_tag_then_409_once_live() {
2594            let s = server().await;
2595            let tag = stored(&s).await;
2596            deleted(&s).await;
2597            let token = key(&s, &[Scope::Legal, Scope::Read]);
2598            let path = format!("/legal/vcons/{UUID}/restore");
2599
2600            let answer = call(&s, Method::POST, &path, &token).await;
2601            assert_eq!(answer.status(), 200);
2602            assert_eq!(answer.headers()["etag"], tag.as_str());
2603            let read = call(&s, Method::GET, &format!("/vcons/{UUID}"), &token).await;
2604            assert_eq!(read.status(), 200, "live again");
2605
2606            let again = call(&s, Method::POST, &path, &token).await;
2607            assert_eq!(again.status(), 409);
2608            assert_eq!(problem(again).await, "not-deleted");
2609        }
2610
2611        #[tokio::test]
2612        async fn a_hold_is_set_and_released_with_204() {
2613            let s = server().await;
2614            stored(&s).await;
2615            let legal = key(&s, &[Scope::Legal]);
2616            let path = format!("/legal/vcons/{UUID}/hold");
2617            for method in [Method::PUT, Method::DELETE] {
2618                let answer = call(&s, method, &path, &legal).await;
2619                assert_eq!(answer.status(), 204);
2620                assert!(answer.bytes().await.expect("body").is_empty());
2621            }
2622            assert_eq!(s.tenant_log("hold").len(), 1, "the hold");
2623            assert_eq!(s.tenant_log("release").len(), 1, "the release, as itself");
2624        }
2625
2626        #[tokio::test]
2627        async fn a_held_vcon_survives_a_zero_period_delete_until_released() {
2628            let s = server().await;
2629            stored(&s).await;
2630            let legal = key(&s, &[Scope::Legal]);
2631            let hold = format!("/legal/vcons/{UUID}/hold");
2632            let read = format!("/legal/vcons/{UUID}");
2633            assert_eq!(call(&s, Method::PUT, &hold, &legal).await.status(), 204);
2634            s.tenants
2635                .set_retention(Actor::Cli, s.tenant, Some(Retention::Days(0)))
2636                .expect("set");
2637            deleted(&s).await;
2638            assert_eq!(
2639                call(&s, Method::GET, &read, &legal).await.status(),
2640                200,
2641                "held"
2642            );
2643
2644            assert_eq!(call(&s, Method::DELETE, &hold, &legal).await.status(), 204);
2645            s.tenants
2646                .store(s.tenant)
2647                .expect("its store")
2648                .purge_due(Retention::Days(0))
2649                .expect("swept");
2650            assert_eq!(
2651                call(&s, Method::GET, &read, &legal).await.status(),
2652                410,
2653                "released"
2654            );
2655        }
2656
2657        #[tokio::test]
2658        async fn a_gzipped_vcon_reads_back_gzipped() {
2659            use std::io::Write;
2660            let s = server().await;
2661            let mut gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
2662            gz.write_all(&a4()).expect("gzip");
2663            let gzipped = gz.finish().expect("gzip");
2664            let writer = key(&s, &[Scope::Write]);
2665            let answer = s
2666                .client()
2667                .post(s.url("/vcons"))
2668                .bearer_auth(&writer)
2669                .header("content-type", "application/vcon+gzip")
2670                .body(gzipped.clone())
2671                .send()
2672                .await
2673                .expect("an answer");
2674            assert_eq!(answer.status(), 201);
2675            deleted(&s).await;
2676            let legal = key(&s, &[Scope::Legal]);
2677            let answer = call(&s, Method::GET, &format!("/legal/vcons/{UUID}"), &legal).await;
2678            assert_eq!(answer.status(), 200);
2679            assert_eq!(answer.headers()["content-type"], "application/vcon+gzip");
2680            assert_eq!(
2681                answer.bytes().await.expect("body").as_ref(),
2682                gzipped.as_slice()
2683            );
2684        }
2685
2686        #[tokio::test]
2687        async fn a_purged_uuid_is_410_on_every_legal_route() {
2688            let s = server().await;
2689            let tag = stored(&s).await;
2690            s.tenants
2691                .set_retention(Actor::Cli, s.tenant, Some(Retention::Days(0)))
2692                .expect("set");
2693            deleted(&s).await;
2694            let legal = key(&s, &[Scope::Legal]);
2695            for (method, path) in routes(UUID, &bare(&tag)) {
2696                let answer = call(&s, method.clone(), &path, &legal).await;
2697                assert_eq!(answer.status(), 410, "{method} {path}");
2698                assert_eq!(problem(answer).await, "gone");
2699            }
2700        }
2701
2702        #[tokio::test]
2703        async fn a_uuid_never_stored_is_404_and_a_bad_one_400_on_every_legal_route() {
2704            let s = server().await;
2705            let legal = key(&s, &[Scope::Legal]);
2706            let version = VersionId::generate().to_string();
2707            for (method, path) in routes(NEVER, &version) {
2708                let answer = call(&s, method.clone(), &path, &legal).await;
2709                assert_eq!(answer.status(), 404, "{method} {path}");
2710                assert_eq!(problem(answer).await, "not-found");
2711            }
2712            for (method, path) in routes("not-a-uuid", &version) {
2713                let answer = call(&s, method.clone(), &path, &legal).await;
2714                assert_eq!(answer.status(), 400, "{method} {path}");
2715                assert_eq!(problem(answer).await, "bad-path-uuid");
2716            }
2717            for action in ["legal_read", "legal_versions", "restore", "hold", "release"] {
2718                let last = s.tenant_log(action).last().expect("recorded").clone();
2719                assert_eq!(last["status"], 400, "{action}'s refusal is recorded");
2720            }
2721        }
2722
2723        #[tokio::test]
2724        async fn damage_is_500_on_every_legal_route() {
2725            let s = server().await;
2726            let tag = stored(&s).await;
2727            deleted(&s).await;
2728            let legal = key(&s, &[Scope::Legal]);
2729            let store = s.tenants.store(s.tenant).expect("its store");
2730            store
2731                .conn_for_test()
2732                .execute_batch(
2733                    "UPDATE version SET wrapped_key = x'00', stored_at = 'x';
2734                     CREATE TEMP TRIGGER broken BEFORE UPDATE ON vcon
2735                     BEGIN SELECT RAISE(ABORT, 'the disk has failed'); END;",
2736                )
2737                .expect("damaged");
2738            for (method, path) in routes(UUID, &bare(&tag)) {
2739                let answer = call(&s, method.clone(), &path, &legal).await;
2740                assert_eq!(answer.status(), 500, "{method} {path}");
2741            }
2742        }
2743
2744        #[tokio::test]
2745        async fn an_answer_that_cannot_be_recorded_is_503_on_every_legal_route() {
2746            let s = server().await;
2747            let tag = stored(&s).await;
2748            let legal = key(&s, &[Scope::Legal]);
2749            s.tenants.store(s.tenant).expect("its store").break_audit();
2750            for (method, path) in routes(UUID, &bare(&tag)) {
2751                let answer = call(&s, method.clone(), &path, &legal).await;
2752                assert_eq!(answer.status(), 503, "{method} {path}");
2753                assert_eq!(problem(answer).await, "audit-failed");
2754            }
2755        }
2756    }
2757
2758    mod admin {
2759        use reqwest::Method;
2760        use serde_json::{Value, json};
2761
2762        use super::pipeline::{Server, problem, server};
2763        use crate::access::{Scope, Scopes};
2764        use crate::audit::Actor;
2765        use time::format_description::well_known::Rfc3339;
2766
2767        use crate::access;
2768        use crate::ids::{KeyId, TenantId};
2769        use crate::retention::Retention;
2770
2771        fn operator(s: &Server) -> String {
2772            s.tenants
2773                .create_key(Actor::Cli, None, Scopes::of(&[Scope::Admin]), "op")
2774                .expect("an operator key")
2775                .token
2776        }
2777
2778        async fn call(
2779            s: &Server,
2780            method: Method,
2781            path: &str,
2782            token: Option<&str>,
2783            body: Option<&str>,
2784        ) -> reqwest::Response {
2785            let mut request = s.client().request(method, s.url(path));
2786            if let Some(token) = token {
2787                request = request.bearer_auth(token);
2788            }
2789            if let Some(body) = body {
2790                request = request
2791                    .header("content-type", "application/json")
2792                    .body(body.to_owned());
2793            }
2794            request.send().await.expect("an answer")
2795        }
2796
2797        async fn json(answer: reqwest::Response) -> Value {
2798            assert_eq!(answer.headers()["content-type"], "application/json");
2799            serde_json::from_slice(&answer.bytes().await.expect("body")).expect("JSON")
2800        }
2801
2802        /// The admin log's records of `action` written for requests, not
2803        /// for the tests' own setup from the command line.
2804        fn admin_log(s: &Server, action: &str) -> Vec<Value> {
2805            s.tenants
2806                .admin_records()
2807                .into_iter()
2808                .filter(|r| r["action"] == action && r["actor"] != "cli")
2809                .collect()
2810        }
2811
2812        fn key_id(token: &str) -> KeyId {
2813            access::parse(token).expect("a token")
2814        }
2815
2816        /// Each admin route, with a body it takes, and its audit action.
2817        fn routes(
2818            id: TenantId,
2819            key: KeyId,
2820        ) -> Vec<(Method, String, Option<&'static str>, &'static str)> {
2821            vec![
2822                (
2823                    Method::POST,
2824                    "/admin/tenants".to_owned(),
2825                    Some(r#"{"name":"beta"}"#),
2826                    "tenant_create",
2827                ),
2828                (
2829                    Method::GET,
2830                    "/admin/tenants".to_owned(),
2831                    None,
2832                    "tenant_list",
2833                ),
2834                (
2835                    Method::PATCH,
2836                    format!("/admin/tenants/{id}"),
2837                    Some(r#"{"retention":"5y"}"#),
2838                    "tenant_retention",
2839                ),
2840                (
2841                    Method::DELETE,
2842                    format!("/admin/tenants/{id}"),
2843                    None,
2844                    "tenant_remove",
2845                ),
2846                (
2847                    Method::POST,
2848                    format!("/admin/tenants/{id}/keys"),
2849                    Some(r#"{"scopes":["read"],"label":"ingest"}"#),
2850                    "key_create",
2851                ),
2852                (
2853                    Method::GET,
2854                    format!("/admin/tenants/{id}/keys"),
2855                    None,
2856                    "key_list",
2857                ),
2858                (
2859                    Method::DELETE,
2860                    format!("/admin/keys/{key}"),
2861                    None,
2862                    "key_revoke",
2863                ),
2864            ]
2865        }
2866
2867        #[tokio::test]
2868        async fn a_tenant_key_is_forbidden_and_no_key_unauthorized_on_every_admin_route() {
2869            let s = server().await;
2870            let every = [Scope::Write, Scope::Read, Scope::Delete, Scope::Legal];
2871            let tenant_key = s.key(&every);
2872            for (method, path, body, action) in routes(s.tenant, key_id(&tenant_key)) {
2873                let answer = call(&s, method.clone(), &path, Some(&tenant_key), body).await;
2874                assert_eq!(answer.status(), 403, "{method} {path}");
2875                assert_eq!(problem(answer).await, "forbidden");
2876                let refused: Vec<Value> = s
2877                    .tenant_log(action)
2878                    .into_iter()
2879                    .filter(|r| r["actor"] != "cli")
2880                    .collect();
2881                assert_eq!(refused.len(), 1, "{action} refusal in the tenant's log");
2882                assert_eq!(refused[0]["status"], 403);
2883
2884                let answer = call(&s, method.clone(), &path, None, body).await;
2885                assert_eq!(answer.status(), 401, "{method} {path}");
2886            }
2887            assert_eq!(s.tenants.list(Actor::Cli).expect("listed").len(), 1);
2888            assert_eq!(s.tenants.retention(s.tenant), Ok(None), "nothing changed");
2889            let keys = s.tenants.list_keys(Actor::Cli, Some(s.tenant));
2890            let keys = keys.expect("listed");
2891            assert_eq!(keys.len(), 1, "no key made");
2892            assert_eq!(keys[0].revoked_at, None, "nor revoked");
2893        }
2894
2895        #[tokio::test]
2896        async fn a_tenant_is_created_with_201_and_its_id_and_listed() {
2897            let s = server().await;
2898            let op = operator(&s);
2899            let body = r#"{"name":"beta","retention":"5y"}"#;
2900            let answer = call(&s, Method::POST, "/admin/tenants", Some(&op), Some(body)).await;
2901            assert_eq!(answer.status(), 201);
2902            let made = json(answer).await;
2903            let id = TenantId::parse(made["id"].as_str().expect("an id")).expect("a tenant id");
2904            assert_eq!(made, json!({ "id": id.to_string() }));
2905            assert_eq!(s.tenants.retention(id), Ok(Some(Retention::Years(5))));
2906            let created = admin_log(&s, "tenant_create");
2907            assert_eq!(created.last().expect("recorded")["status"], 201);
2908
2909            let body = r#"{"name":"gamma"}"#;
2910            let answer = call(&s, Method::POST, "/admin/tenants", Some(&op), Some(body)).await;
2911            assert_eq!(answer.status(), 201);
2912
2913            let answer = call(&s, Method::GET, "/admin/tenants", Some(&op), None).await;
2914            assert_eq!(answer.status(), 200);
2915            let listed = json(answer).await;
2916            let gamma = s
2917                .tenants
2918                .list(Actor::Cli)
2919                .expect("listed")
2920                .into_iter()
2921                .find(|t| t.name == "gamma")
2922                .expect("gamma")
2923                .id;
2924            assert_eq!(
2925                listed,
2926                json!([
2927                    { "id": s.tenant.to_string(), "name": "acme", "retention": null },
2928                    { "id": id.to_string(), "name": "beta", "retention": "5y" },
2929                    { "id": gamma.to_string(), "name": "gamma", "retention": null },
2930                ])
2931            );
2932            assert_eq!(admin_log(&s, "tenant_list")[0]["status"], 200);
2933        }
2934
2935        #[tokio::test]
2936        async fn a_taken_name_is_409() {
2937            let s = server().await;
2938            let op = operator(&s);
2939            let body = r#"{"name":"acme"}"#;
2940            let answer = call(&s, Method::POST, "/admin/tenants", Some(&op), Some(body)).await;
2941            assert_eq!(answer.status(), 409);
2942            assert_eq!(problem(answer).await, "name-taken");
2943            assert_eq!(s.tenants.list(Actor::Cli).expect("listed").len(), 1);
2944        }
2945
2946        #[tokio::test]
2947        async fn a_create_body_it_does_not_take_is_422_and_recorded_once() {
2948            let s = server().await;
2949            let op = operator(&s);
2950            let bodies = [
2951                "",
2952                "not json",
2953                "[]",
2954                "{}",
2955                r#"{"name":7}"#,
2956                r#"{"name":null}"#,
2957                r#"{"name":"beta","colour":"red"}"#,
2958                r#"{"name":"beta","name":"gamma"}"#,
2959                r#"{"name":"beta","retention":"7x"}"#,
2960                r#"{"name":"beta","retention":7}"#,
2961                r#"{"name":" beta"}"#,
2962                r#"{"name":""}"#,
2963            ];
2964            for body in bodies {
2965                let answer = call(&s, Method::POST, "/admin/tenants", Some(&op), Some(body)).await;
2966                assert_eq!(answer.status(), 422, "{body}");
2967                assert_eq!(problem(answer).await, "invalid-body", "{body}");
2968            }
2969            let refused = admin_log(&s, "tenant_create");
2970            assert_eq!(refused.len(), bodies.len(), "one per refusal");
2971            assert!(refused.iter().all(|r| r["status"] == 422));
2972            assert_eq!(s.tenants.list(Actor::Cli).expect("listed").len(), 1);
2973        }
2974
2975        #[tokio::test]
2976        async fn a_create_past_the_master_keys_wrap_limit_is_503() {
2977            let s = server().await;
2978            let op = operator(&s);
2979            s.tenants
2980                .conn_for_test()
2981                .execute("UPDATE master_key_use SET wraps = ?1", [1_i64 << 32])
2982                .expect("at the limit");
2983            let body = r#"{"name":"beta"}"#;
2984            let answer = call(&s, Method::POST, "/admin/tenants", Some(&op), Some(body)).await;
2985            assert_eq!(answer.status(), 503);
2986            assert_eq!(problem(answer).await, "master-key-limit");
2987        }
2988
2989        #[tokio::test]
2990        async fn a_retention_change_answers_200_with_the_period() {
2991            let s = server().await;
2992            let op = operator(&s);
2993            let path = format!("/admin/tenants/{}", s.tenant);
2994            let id = s.tenant.to_string();
2995            for (body, period, kept) in [
2996                (
2997                    r#"{"retention":"30d"}"#,
2998                    json!("30d"),
2999                    Some(Retention::Days(30)),
3000                ),
3001                (r#"{"retention":null}"#, Value::Null, None),
3002            ] {
3003                let answer = call(&s, Method::PATCH, &path, Some(&op), Some(body)).await;
3004                assert_eq!(answer.status(), 200, "{body}");
3005                assert_eq!(json(answer).await, json!({ "id": id, "retention": period }));
3006                assert_eq!(s.tenants.retention(s.tenant), Ok(kept), "{body}");
3007            }
3008            let changed = admin_log(&s, "tenant_retention");
3009            assert_eq!(changed.len(), 2);
3010            assert!(changed.iter().all(|r| r["status"] == 200));
3011            assert_eq!(s.tenant_log("retention").len(), 2, "the tenant's log too");
3012        }
3013
3014        #[tokio::test]
3015        async fn a_retention_change_for_no_tenant_is_404_and_recorded() {
3016            let s = server().await;
3017            let op = operator(&s);
3018            let body = Some(r#"{"retention":"5y"}"#);
3019            let unknown = TenantId([7; 16]).to_string();
3020            for id in [
3021                unknown.as_str(),
3022                "acme",
3023                "0123",
3024                &s.tenant.to_string().to_uppercase(),
3025            ] {
3026                let path = format!("/admin/tenants/{id}");
3027                let answer = call(&s, Method::PATCH, &path, Some(&op), body).await;
3028                assert_eq!(answer.status(), 404, "{id}");
3029                assert_eq!(problem(answer).await, "unknown-tenant", "{id}");
3030            }
3031            let refused = admin_log(&s, "tenant_retention");
3032            assert_eq!(refused.len(), 4);
3033            assert!(refused.iter().all(|r| r["status"] == 404));
3034            assert_eq!(s.tenants.retention(s.tenant), Ok(None));
3035        }
3036
3037        #[tokio::test]
3038        async fn a_retention_body_it_does_not_take_is_422_and_recorded() {
3039            let s = server().await;
3040            let op = operator(&s);
3041            let path = format!("/admin/tenants/{}", s.tenant);
3042            let bodies = [
3043                "",
3044                "{}",
3045                r#"{"retention":"7"}"#,
3046                r#"{"retention":"07y"}"#,
3047                r#"{"retention":5}"#,
3048                r#"{"retention":"5y","name":"beta"}"#,
3049            ];
3050            for body in bodies {
3051                let answer = call(&s, Method::PATCH, &path, Some(&op), Some(body)).await;
3052                assert_eq!(answer.status(), 422, "{body}");
3053                assert_eq!(problem(answer).await, "invalid-body", "{body}");
3054            }
3055            let refused = admin_log(&s, "tenant_retention");
3056            assert_eq!(refused.len(), bodies.len());
3057            assert!(refused.iter().all(|r| r["status"] == 422));
3058            assert_eq!(s.tenants.retention(s.tenant), Ok(None));
3059        }
3060
3061        #[tokio::test]
3062        async fn a_retention_change_for_a_closing_tenant_is_503() {
3063            let s = server().await;
3064            let op = operator(&s);
3065            s.tenants.close(s.tenant);
3066            let path = format!("/admin/tenants/{}", s.tenant);
3067            let body = Some(r#"{"retention":"5y"}"#);
3068            let answer = call(&s, Method::PATCH, &path, Some(&op), body).await;
3069            assert_eq!(answer.status(), 503);
3070            assert_eq!(problem(answer).await, "unavailable");
3071        }
3072
3073        #[tokio::test]
3074        async fn damage_is_500_on_every_admin_route() {
3075            let s = server().await;
3076            let op = operator(&s);
3077            let kept = key_id(&s.key(&[Scope::Read]));
3078            s.tenants
3079                .conn_for_test()
3080                .execute_batch(
3081                    "UPDATE tenant SET retention = 'x';
3082                     UPDATE api_key SET created_at = 'x' WHERE tenant IS NOT NULL;
3083                     CREATE TEMP TRIGGER no_key BEFORE INSERT ON api_key
3084                     BEGIN SELECT RAISE(ABORT, 'the disk has failed'); END;
3085                     CREATE TEMP TRIGGER no_revoke BEFORE UPDATE ON api_key
3086                     BEGIN SELECT RAISE(ABORT, 'the disk has failed'); END;
3087                     CREATE TEMP TRIGGER broken BEFORE INSERT ON tenant
3088                     BEGIN SELECT RAISE(ABORT, 'the disk has failed'); END;
3089                     CREATE TEMP TRIGGER broken_too BEFORE UPDATE ON tenant
3090                     BEGIN SELECT RAISE(ABORT, 'the disk has failed'); END;
3091                     CREATE TEMP TRIGGER broken_three BEFORE DELETE ON tenant
3092                     BEGIN SELECT RAISE(ABORT, 'the disk has failed'); END;",
3093                )
3094                .expect("damaged");
3095            for (method, path, body, _) in routes(s.tenant, kept) {
3096                let answer = call(&s, method.clone(), &path, Some(&op), body).await;
3097                assert_eq!(answer.status(), 500, "{method} {path}");
3098                assert_eq!(problem(answer).await, "internal");
3099            }
3100        }
3101
3102        #[tokio::test]
3103        async fn an_answer_that_cannot_be_recorded_is_503_on_every_admin_route() {
3104            let s = server().await;
3105            let op = operator(&s);
3106            let kept = key_id(&s.key(&[Scope::Read]));
3107            s.tenants.break_audit();
3108            for (method, path, body, _) in routes(s.tenant, kept) {
3109                let answer = call(&s, method.clone(), &path, Some(&op), body).await;
3110                assert_eq!(answer.status(), 503, "{method} {path}");
3111                assert_eq!(problem(answer).await, "audit-failed");
3112            }
3113            for body in ["{}", r#"{"retention":"5y"}"#] {
3114                let path = if body == "{}" {
3115                    "/admin/tenants".to_owned()
3116                } else {
3117                    format!("/admin/tenants/{}", TenantId([7; 16]))
3118                };
3119                let method = if body == "{}" {
3120                    Method::POST
3121                } else {
3122                    Method::PATCH
3123                };
3124                let answer = call(&s, method, &path, Some(&op), Some(body)).await;
3125                assert_eq!(problem(answer).await, "audit-failed", "a refusal too");
3126            }
3127            assert_eq!(
3128                s.tenants.list(Actor::Cli).err(),
3129                Some(crate::tenants::TenantError::AuditFailed)
3130            );
3131        }
3132
3133        /// Stores A.4 in the server's tenant with a key of its own.
3134        async fn stored(s: &Server) {
3135            let body = std::fs::read(format!(
3136                "{}/../vcon/tests/fixtures/ietf/ab_call_ext_rec.vcon",
3137                env!("CARGO_MANIFEST_DIR")
3138            ))
3139            .expect("A.4");
3140            let answer = s
3141                .client()
3142                .post(s.url("/vcons"))
3143                .bearer_auth(s.key(&[Scope::Write]))
3144                .header("content-type", "application/vcon")
3145                .body(body)
3146                .send()
3147                .await
3148                .expect("an answer");
3149            assert_eq!(answer.status(), 201);
3150        }
3151
3152        fn listed(s: &Server) -> Vec<TenantId> {
3153            let tenants = s.tenants.list(Actor::Cli).expect("listed");
3154            tenants.into_iter().map(|t| t.id).collect()
3155        }
3156
3157        #[tokio::test]
3158        async fn an_empty_tenant_is_removed_with_204_then_unknown() {
3159            let s = server().await;
3160            let op = operator(&s);
3161            let beta = s.tenants.create(Actor::Cli, "beta", None).expect("made");
3162            let path = format!("/admin/tenants/{beta}");
3163            let answer = call(&s, Method::DELETE, &path, Some(&op), None).await;
3164            assert_eq!(answer.status(), 204);
3165            assert_eq!(listed(&s), vec![s.tenant]);
3166            let answer = call(&s, Method::DELETE, &path, Some(&op), None).await;
3167            assert_eq!(answer.status(), 404);
3168            assert_eq!(problem(answer).await, "unknown-tenant");
3169            let removed = admin_log(&s, "tenant_remove");
3170            let statuses: Vec<&Value> = removed.iter().map(|r| &r["status"]).collect();
3171            assert_eq!(statuses, [204, 404]);
3172        }
3173
3174        #[tokio::test]
3175        async fn a_tenant_holding_vcons_is_409_without_force_and_removed_with_it() {
3176            let s = server().await;
3177            let op = operator(&s);
3178            stored(&s).await;
3179            let reader = s.key(&[Scope::Read]);
3180            let path = format!("/admin/tenants/{}", s.tenant);
3181            for query in ["", "?force=false"] {
3182                let path = format!("{path}{query}");
3183                let answer = call(&s, Method::DELETE, &path, Some(&op), None).await;
3184                assert_eq!(answer.status(), 409, "{query}");
3185                assert_eq!(problem(answer).await, "holds-vcons", "{query}");
3186            }
3187            assert_eq!(listed(&s), vec![s.tenant], "still there");
3188
3189            let forced = format!("{path}?force=true");
3190            let answer = call(&s, Method::DELETE, &forced, Some(&op), None).await;
3191            assert_eq!(answer.status(), 204);
3192            assert_eq!(listed(&s), Vec::<TenantId>::new());
3193            let health = s
3194                .client()
3195                .get(s.url("/health"))
3196                .bearer_auth(&reader)
3197                .send()
3198                .await
3199                .expect("an answer");
3200            assert_eq!(health.status(), 401, "its keys went with it");
3201        }
3202
3203        #[tokio::test]
3204        async fn a_legal_hold_is_409_even_with_force() {
3205            let s = server().await;
3206            let op = operator(&s);
3207            stored(&s).await;
3208            let legal = s.key(&[Scope::Legal]);
3209            let hold = s
3210                .client()
3211                .put(s.url("/legal/vcons/01a07da8-c2bb-83e5-b9a2-279e0d16bc46/hold"))
3212                .bearer_auth(&legal)
3213                .send()
3214                .await
3215                .expect("an answer");
3216            assert_eq!(hold.status(), 204);
3217            let path = format!("/admin/tenants/{}?force=true", s.tenant);
3218            let answer = call(&s, Method::DELETE, &path, Some(&op), None).await;
3219            assert_eq!(answer.status(), 409);
3220            assert_eq!(problem(answer).await, "under-hold");
3221            assert_eq!(listed(&s), vec![s.tenant], "still there");
3222            let removed = admin_log(&s, "tenant_remove");
3223            assert_eq!(removed.len(), 1);
3224            assert_eq!(removed[0]["status"], 409);
3225            let read = s
3226                .client()
3227                .get(s.url("/legal/vcons/01a07da8-c2bb-83e5-b9a2-279e0d16bc46"))
3228                .bearer_auth(&legal)
3229                .send()
3230                .await
3231                .expect("an answer");
3232            assert_eq!(read.status(), 200, "the tenant is served again");
3233        }
3234
3235        #[tokio::test]
3236        async fn a_query_it_does_not_take_is_400_and_recorded() {
3237            let s = server().await;
3238            let op = operator(&s);
3239            let queries = [
3240                "force=yes",
3241                "force=TRUE",
3242                "force",
3243                "force=",
3244                "force=true&force=true",
3245                "force=true&other=1",
3246                "other=1",
3247            ];
3248            for query in queries {
3249                let path = format!("/admin/tenants/{}?{query}", s.tenant);
3250                let answer = call(&s, Method::DELETE, &path, Some(&op), None).await;
3251                assert_eq!(answer.status(), 400, "{query}");
3252                assert_eq!(problem(answer).await, "bad-query", "{query}");
3253            }
3254            let refused = admin_log(&s, "tenant_remove");
3255            assert_eq!(refused.len(), queries.len());
3256            assert!(refused.iter().all(|r| r["status"] == 400));
3257            assert_eq!(listed(&s), vec![s.tenant]);
3258        }
3259
3260        #[tokio::test]
3261        async fn a_removal_of_no_tenant_is_404_and_recorded() {
3262            let s = server().await;
3263            let op = operator(&s);
3264            let unknown = TenantId([7; 16]).to_string();
3265            for id in [unknown.as_str(), "acme"] {
3266                let path = format!("/admin/tenants/{id}?force=true");
3267                let answer = call(&s, Method::DELETE, &path, Some(&op), None).await;
3268                assert_eq!(answer.status(), 404, "{id}");
3269                assert_eq!(problem(answer).await, "unknown-tenant", "{id}");
3270            }
3271            let refused = admin_log(&s, "tenant_remove");
3272            assert_eq!(refused.len(), 2);
3273            assert!(refused.iter().all(|r| r["status"] == 404));
3274        }
3275
3276        #[tokio::test]
3277        async fn a_removal_of_a_closing_tenant_is_503() {
3278            let s = server().await;
3279            let op = operator(&s);
3280            s.tenants.close(s.tenant);
3281            let path = format!("/admin/tenants/{}", s.tenant);
3282            let answer = call(&s, Method::DELETE, &path, Some(&op), None).await;
3283            assert_eq!(answer.status(), 503);
3284            assert_eq!(problem(answer).await, "unavailable");
3285        }
3286
3287        async fn health(s: &Server, token: &str) -> u16 {
3288            let answer = s.client().get(s.url("/health")).bearer_auth(token).send();
3289            answer.await.expect("an answer").status().as_u16()
3290        }
3291
3292        /// Issues a key over HTTP and returns its id and token.
3293        async fn issued(s: &Server, op: &str, body: &str) -> (String, String) {
3294            let path = format!("/admin/tenants/{}/keys", s.tenant);
3295            let answer = call(s, Method::POST, &path, Some(op), Some(body)).await;
3296            assert_eq!(answer.status(), 201, "{body}");
3297            assert_eq!(answer.headers()["cache-control"], "no-store");
3298            let made = json(answer).await;
3299            let id = made["id"].as_str().expect("an id").to_owned();
3300            let token = made["token"].as_str().expect("a token").to_owned();
3301            assert_eq!(made, json!({ "id": id, "token": token }), "nothing else");
3302            assert_eq!(key_id(&token).to_string(), id, "the token names its key");
3303            (id, token)
3304        }
3305
3306        async fn key_list(s: &Server, op: &str) -> (String, Value) {
3307            let path = format!("/admin/tenants/{}/keys", s.tenant);
3308            let answer = call(s, Method::GET, &path, Some(op), None).await;
3309            assert_eq!(answer.status(), 200);
3310            assert_eq!(answer.headers()["content-type"], "application/json");
3311            let text = answer.text().await.expect("text");
3312            let listed = serde_json::from_str(&text).expect("JSON");
3313            (text, listed)
3314        }
3315
3316        #[tokio::test]
3317        async fn a_key_is_issued_with_201_and_its_token_shown_once() {
3318            let s = server().await;
3319            let op = operator(&s);
3320            let body = r#"{"scopes":["read","write"],"label":"ingest"}"#;
3321            let (id, token) = issued(&s, &op, body).await;
3322            assert_eq!(health(&s, &token).await, 200, "the token works");
3323            let (again, other) = issued(&s, &op, body).await;
3324            assert_ne!((&again, &other), (&id, &token), "each key is new");
3325
3326            let (text, listed) = key_list(&s, &op).await;
3327            for token in [&token, &other] {
3328                // vcs_<key id>_<secret>: the secret is base64url, whose
3329                // alphabet includes `_`, so it is all after the second `_`.
3330                let secret = token.splitn(3, '_').nth(2).expect("a secret");
3331                assert_eq!(secret.len(), 43, "the whole 32-byte secret");
3332                assert!(!text.contains(secret), "no token is listed");
3333            }
3334            let first = &listed[0];
3335            assert_eq!(first["id"], id.as_str());
3336            assert_eq!(
3337                first["scopes"],
3338                json!(["write", "read"]),
3339                "in the fixed order"
3340            );
3341            assert_eq!(first["label"], "ingest");
3342            assert_eq!(first["revoked_at"], Value::Null);
3343            let stored = s.tenants.list_keys(Actor::Cli, Some(s.tenant));
3344            let stored = stored.expect("listed")[0].created_at;
3345            let stored = stored.format(&Rfc3339).expect("RFC 3339");
3346            assert_eq!(first["created_at"], stored.as_str());
3347            let fields: Vec<&String> = first.as_object().expect("an object").keys().collect();
3348            assert_eq!(
3349                fields,
3350                [
3351                    "created_at",
3352                    "id",
3353                    "label",
3354                    "revoked_at",
3355                    "scopes",
3356                    "tenant"
3357                ]
3358            );
3359            assert_eq!(first["tenant"], s.tenant.to_string().as_str());
3360
3361            let made = admin_log(&s, "key_create");
3362            assert_eq!(made.len(), 2);
3363            assert_eq!(made[0]["status"], 201);
3364            assert_eq!(made[0]["target"], id.as_str());
3365            let copied = s.tenant_log("key_create");
3366            assert_eq!(copied.len(), 2, "the tenant's log shows it");
3367            assert_eq!(copied[0]["target"], id.as_str());
3368            assert_eq!(admin_log(&s, "key_list")[0]["status"], 200);
3369        }
3370
3371        #[tokio::test]
3372        async fn a_revoked_key_answers_401_and_the_tenants_log_shows_it() {
3373            let s = server().await;
3374            let op = operator(&s);
3375            let (id, token) = issued(&s, &op, r#"{"scopes":["read"],"label":"a"}"#).await;
3376            let (_, kept) = issued(&s, &op, r#"{"scopes":["read"],"label":"b"}"#).await;
3377            let path = format!("/admin/keys/{id}");
3378            for _ in 0..2 {
3379                let answer = call(&s, Method::DELETE, &path, Some(&op), None).await;
3380                assert_eq!(answer.status(), 204, "again too");
3381            }
3382            assert_eq!(health(&s, &token).await, 401);
3383            assert_eq!(health(&s, &kept).await, 200, "only that key");
3384            let (_, listed) = key_list(&s, &op).await;
3385            assert!(listed[0]["revoked_at"].is_string(), "{listed}");
3386            assert_eq!(listed[1]["revoked_at"], Value::Null);
3387            let revoked = s.tenant_log("key_revoke");
3388            assert_eq!(revoked.len(), 2);
3389            assert!(
3390                revoked
3391                    .iter()
3392                    .all(|r| r["status"] == 204 && r["target"] == id.as_str())
3393            );
3394            assert_eq!(admin_log(&s, "key_revoke").len(), 2);
3395        }
3396
3397        #[tokio::test]
3398        async fn scopes_a_tenant_key_cannot_hold_are_422_and_recorded() {
3399            let s = server().await;
3400            let op = operator(&s);
3401            let path = format!("/admin/tenants/{}/keys", s.tenant);
3402            let scopes = [
3403                "[]",
3404                r#"["admin"]"#,
3405                r#"["read","admin"]"#,
3406                r#"["superuser"]"#,
3407                r#"["read","superuser"]"#,
3408                r#"["Read"]"#,
3409                r#"["read","read"]"#,
3410            ];
3411            for scopes in scopes {
3412                let body = format!(r#"{{"scopes":{scopes},"label":"x"}}"#);
3413                let answer = call(&s, Method::POST, &path, Some(&op), Some(&body)).await;
3414                assert_eq!(answer.status(), 422, "{scopes}");
3415                assert_eq!(problem(answer).await, "bad-scopes", "{scopes}");
3416            }
3417            let refused = admin_log(&s, "key_create");
3418            assert_eq!(refused.len(), scopes.len(), "each recorded once");
3419            assert!(refused.iter().all(|r| r["status"] == 422));
3420            let keys = s.tenants.list_keys(Actor::Cli, Some(s.tenant));
3421            assert_eq!(keys.expect("listed").len(), 0, "no key made");
3422        }
3423
3424        #[tokio::test]
3425        async fn a_key_body_it_does_not_take_is_422_and_recorded() {
3426            let s = server().await;
3427            let op = operator(&s);
3428            let path = format!("/admin/tenants/{}/keys", s.tenant);
3429            let bodies = [
3430                "",
3431                "{}",
3432                r#"{"scopes":["read"]}"#,
3433                r#"{"label":"x"}"#,
3434                r#"{"scopes":"read","label":"x"}"#,
3435                r#"{"scopes":["read"],"label":7}"#,
3436                r#"{"scopes":["read"],"label":"x","tenant":"y"}"#,
3437            ];
3438            for body in bodies {
3439                let answer = call(&s, Method::POST, &path, Some(&op), Some(body)).await;
3440                assert_eq!(answer.status(), 422, "{body}");
3441                assert_eq!(problem(answer).await, "invalid-body", "{body}");
3442            }
3443            let refused = admin_log(&s, "key_create");
3444            assert_eq!(refused.len(), bodies.len());
3445            assert!(refused.iter().all(|r| r["status"] == 422));
3446        }
3447
3448        #[tokio::test]
3449        async fn key_routes_for_no_tenant_or_no_key_are_404_and_recorded() {
3450            let s = server().await;
3451            let op = operator(&s);
3452            let body = Some(r#"{"scopes":["read"],"label":"x"}"#);
3453            let unknown = TenantId([7; 16]).to_string();
3454            for id in [unknown.as_str(), "acme"] {
3455                let path = format!("/admin/tenants/{id}/keys");
3456                let answer = call(&s, Method::POST, &path, Some(&op), body).await;
3457                assert_eq!(answer.status(), 404, "POST {id}");
3458                assert_eq!(problem(answer).await, "unknown-tenant");
3459                let answer = call(&s, Method::GET, &path, Some(&op), None).await;
3460                assert_eq!(answer.status(), 404, "GET {id}");
3461                assert_eq!(problem(answer).await, "unknown-tenant");
3462            }
3463            let unknown_key = KeyId([7; 8]).to_string();
3464            for id in [unknown_key.as_str(), "acme", &unknown_key.to_uppercase()] {
3465                let path = format!("/admin/keys/{id}");
3466                let answer = call(&s, Method::DELETE, &path, Some(&op), None).await;
3467                assert_eq!(answer.status(), 404, "DELETE {id}");
3468                assert_eq!(problem(answer).await, "unknown-key");
3469            }
3470            for (action, count) in [("key_create", 2), ("key_list", 2), ("key_revoke", 3)] {
3471                let refused = admin_log(&s, action);
3472                assert_eq!(refused.len(), count, "{action}");
3473                assert!(refused.iter().all(|r| r["status"] == 404), "{action}");
3474            }
3475        }
3476
3477        #[tokio::test]
3478        async fn requests_during_a_removal_get_503_then_401() {
3479            let s = server().await;
3480            let op = operator(&s);
3481            stored(&s).await;
3482            let uuid = "01a07da8-c2bb-83e5-b9a2-279e0d16bc46";
3483            let writer = s.key(&[Scope::Write]);
3484            let reader = s.key(&[Scope::Read]);
3485            let legal = s.key(&[Scope::Legal]);
3486            let a4 = std::fs::read(format!(
3487                "{}/../vcon/tests/fixtures/ietf/ab_call_ext_rec.vcon",
3488                env!("CARGO_MANIFEST_DIR")
3489            ))
3490            .expect("A.4");
3491            let requests = || {
3492                let client = s.client();
3493                [
3494                    client
3495                        .post(s.url("/vcons"))
3496                        .bearer_auth(&writer)
3497                        .header("content-type", "application/vcon")
3498                        .body(a4.clone()),
3499                    client
3500                        .get(s.url(&format!("/vcons/{uuid}")))
3501                        .bearer_auth(&reader),
3502                    client
3503                        .get(s.url(&format!("/legal/vcons/{uuid}")))
3504                        .bearer_auth(&legal),
3505                    client.get(s.url("/health")).bearer_auth(&reader),
3506                ]
3507            };
3508
3509            // A request still holding the tenant keeps the removal draining.
3510            let in_flight = s.tenants.store(s.tenant).expect("its store");
3511            let removal = {
3512                let request = s
3513                    .client()
3514                    .delete(s.url(&format!("/admin/tenants/{}?force=true", s.tenant)))
3515                    .bearer_auth(&op);
3516                tokio::spawn(async move { request.send().await.expect("an answer").status() })
3517            };
3518            let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(10);
3519            while s.tenants.store(s.tenant).is_ok() {
3520                assert!(
3521                    tokio::time::Instant::now() < deadline,
3522                    "the removal never began"
3523                );
3524                tokio::time::sleep(std::time::Duration::from_millis(5)).await;
3525            }
3526            for request in requests() {
3527                let answer = request.send().await.expect("an answer");
3528                let url = answer.url().path().to_owned();
3529                assert_eq!(answer.status(), 503, "{url} while removing");
3530                assert_eq!(problem(answer).await, "unavailable", "{url}");
3531            }
3532            assert!(!removal.is_finished(), "still draining");
3533
3534            drop(in_flight);
3535            assert_eq!(removal.await.expect("joined"), 204);
3536            for request in requests() {
3537                let answer = request.send().await.expect("an answer");
3538                let url = answer.url().path().to_owned();
3539                assert_eq!(answer.status(), 401, "{url} once removed");
3540            }
3541        }
3542
3543        #[tokio::test]
3544        async fn a_key_for_a_closing_tenant_is_503() {
3545            let s = server().await;
3546            let op = operator(&s);
3547            s.tenants.close(s.tenant);
3548            let path = format!("/admin/tenants/{}/keys", s.tenant);
3549            let body = Some(r#"{"scopes":["read"],"label":"x"}"#);
3550            let answer = call(&s, Method::POST, &path, Some(&op), body).await;
3551            assert_eq!(answer.status(), 503);
3552            assert_eq!(problem(answer).await, "unavailable");
3553        }
3554
3555        #[tokio::test]
3556        async fn a_body_over_the_limit_is_413() {
3557            let s = server().await;
3558            let op = operator(&s);
3559            let body = format!(r#"{{"name":"{}"}}"#, "x".repeat(super::pipeline::LIMIT));
3560            let answer = call(&s, Method::POST, "/admin/tenants", Some(&op), Some(&body)).await;
3561            assert_eq!(answer.status(), 413);
3562            assert_eq!(problem(answer).await, "too-large");
3563        }
3564    }
3565
3566    mod by_uuid {
3567        use super::pipeline::{START, Server, problem, server, server_keeping};
3568        use crate::access::{Scope, Scopes};
3569        use crate::audit::Actor;
3570        use crate::ids::TenantId;
3571        use crate::retention::Retention;
3572
3573        const UUID: &str = "01a07da8-c2bb-83e5-b9a2-279e0d16bc46";
3574
3575        fn fixture(name: &str) -> Vec<u8> {
3576            let path = format!(
3577                "{}/../vcon/tests/fixtures/ietf/{name}",
3578                env!("CARGO_MANIFEST_DIR")
3579            );
3580            std::fs::read(&path).unwrap_or_else(|e| panic!("{path}: {e}"))
3581        }
3582
3583        fn gzip(body: &[u8]) -> Vec<u8> {
3584            use std::io::Write;
3585            let mut encoder =
3586                flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
3587            encoder.write_all(body).expect("gzip");
3588            encoder.finish().expect("gzip")
3589        }
3590
3591        /// A key of this tenant's with these scopes.
3592        fn key_in(s: &Server, tenant: TenantId, scopes: &[Scope]) -> String {
3593            s.tenants
3594                .create_key(Actor::Cli, Some(tenant), Scopes::of(scopes), "test")
3595                .expect("a key")
3596                .token
3597        }
3598
3599        async fn get(s: &Server, token: &str, uuid: &str) -> reqwest::Response {
3600            s.client()
3601                .get(s.url(&format!("/vcons/{uuid}")))
3602                .bearer_auth(token)
3603                .send()
3604                .await
3605                .expect("an answer")
3606        }
3607
3608        async fn post(s: &Server, token: &str, content_type: &str, body: Vec<u8>) -> String {
3609            let answer = s
3610                .client()
3611                .post(s.url("/vcons"))
3612                .bearer_auth(token)
3613                .header("content-type", content_type)
3614                .body(body)
3615                .send()
3616                .await
3617                .expect("an answer");
3618            assert_eq!(answer.status(), 201);
3619            answer.headers()["etag"].to_str().expect("ascii").to_owned()
3620        }
3621
3622        /// A.4, A.6 and A.7 share one uuid, so each runs in its own tenant.
3623        #[tokio::test]
3624        async fn the_core_examples_come_back_byte_for_byte_plain_and_gzipped() {
3625            let s = server().await;
3626            let files = [
3627                "ab_call_ext_rec.vcon",
3628                "ab_call_ext_rec_signed.vcon",
3629                "ab_call_ext_rec_encrypted.vcon",
3630            ];
3631            for (n, file) in files.into_iter().enumerate() {
3632                for gzipped in [false, true] {
3633                    let tenant = s
3634                        .tenants
3635                        .create(Actor::Cli, &format!("t{n}{gzipped}"), None)
3636                        .expect("a tenant");
3637                    let token = key_in(&s, tenant, &[Scope::Write, Scope::Read]);
3638                    let plain = fixture(file);
3639                    let (sent, content_type) = if gzipped {
3640                        (gzip(&plain), "application/vcon+gzip")
3641                    } else {
3642                        (plain, "application/vcon")
3643                    };
3644                    let tag = post(&s, &token, content_type, sent.clone()).await;
3645                    let answer = get(&s, &token, UUID).await;
3646                    assert_eq!(answer.status(), 200, "{file} {gzipped}");
3647                    assert_eq!(answer.headers()["content-type"], content_type);
3648                    assert_eq!(answer.headers()["etag"], tag.as_str());
3649                    assert!(answer.headers().get("content-encoding").is_none());
3650                    let got = answer.bytes().await.expect("body");
3651                    assert_eq!(got.as_ref(), sent.as_slice(), "{file} {gzipped}");
3652                }
3653            }
3654        }
3655
3656        #[tokio::test]
3657        async fn a_uuid_is_found_in_either_case() {
3658            let s = server().await;
3659            let token = key_in(&s, s.tenant, &[Scope::Write, Scope::Read]);
3660            post(
3661                &s,
3662                &token,
3663                "application/vcon",
3664                fixture("ab_call_ext_rec.vcon"),
3665            )
3666            .await;
3667            let answer = get(&s, &token, &UUID.to_uppercase()).await;
3668            assert_eq!(answer.status(), 200);
3669        }
3670
3671        #[tokio::test]
3672        async fn a_uuid_never_stored_is_not_found_and_a_deleted_one_is_gone() {
3673            let s = server().await;
3674            let token = key_in(&s, s.tenant, &[Scope::Write, Scope::Read]);
3675            let answer = get(&s, &token, UUID).await;
3676            assert_eq!(answer.status(), 404);
3677            assert_eq!(problem(answer).await, "not-found");
3678            post(
3679                &s,
3680                &token,
3681                "application/vcon",
3682                fixture("ab_call_ext_rec.vcon"),
3683            )
3684            .await;
3685            s.tenants
3686                .store(s.tenant)
3687                .expect("its store")
3688                .delete(
3689                    &crate::store::Caller {
3690                        key: crate::ids::KeyId([9; 8]),
3691                    },
3692                    UUID.parse().expect("uuid"),
3693                    crate::retention::Retention::Years(7),
3694                )
3695                .expect("deleted");
3696            let answer = get(&s, &token, UUID).await;
3697            assert_eq!(answer.status(), 410);
3698            assert_eq!(problem(answer).await, "gone");
3699        }
3700
3701        #[tokio::test]
3702        async fn a_path_uuid_not_in_text_form_is_a_bad_request_and_recorded() {
3703            let s = server().await;
3704            let token = key_in(&s, s.tenant, &[Scope::Read]);
3705            let simple = UUID.replace('-', "");
3706            for bad in [
3707                "not-a-uuid".to_owned(),
3708                simple.clone(),
3709                format!("%7B{UUID}%7D"),
3710                format!("urn:uuid:{UUID}"),
3711                format!("{UUID}0"),
3712            ] {
3713                let answer = get(&s, &token, &bad).await;
3714                assert_eq!(answer.status(), 400, "{bad}");
3715                assert_eq!(problem(answer).await, "bad-path-uuid");
3716            }
3717            let refused = s.tenant_log("read");
3718            assert_eq!(refused.len(), 5);
3719            assert_eq!(refused[0]["status"], 400);
3720        }
3721
3722        #[tokio::test]
3723        async fn a_key_without_read_is_forbidden() {
3724            let s = server().await;
3725            let token = key_in(&s, s.tenant, &[Scope::Write]);
3726            let answer = get(&s, &token, UUID).await;
3727            assert_eq!(answer.status(), 403);
3728        }
3729
3730        #[tokio::test]
3731        async fn a_read_whose_record_fails_sends_nothing() {
3732            let s = server().await;
3733            let token = key_in(&s, s.tenant, &[Scope::Write, Scope::Read]);
3734            post(
3735                &s,
3736                &token,
3737                "application/vcon",
3738                fixture("ab_call_ext_rec.vcon"),
3739            )
3740            .await;
3741            s.tenants.store(s.tenant).expect("its store").break_audit();
3742            let answer = get(&s, &token, UUID).await;
3743            assert_eq!(answer.status(), 503);
3744            assert_eq!(problem(answer).await, "audit-failed");
3745        }
3746
3747        #[tokio::test]
3748        async fn a_version_whose_file_is_missing_is_damaged() {
3749            let s = server().await;
3750            let token = key_in(&s, s.tenant, &[Scope::Write, Scope::Read]);
3751            let tag = post(
3752                &s,
3753                &token,
3754                "application/vcon",
3755                fixture("ab_call_ext_rec.vcon"),
3756            )
3757            .await;
3758            let object = s
3759                .dir()
3760                .join("tenants")
3761                .join(s.tenant.to_string())
3762                .join("objects")
3763                .join(tag.trim_matches('"'));
3764            std::fs::remove_file(&object).expect("remove the object file");
3765            let answer = get(&s, &token, UUID).await;
3766            assert_eq!(answer.status(), 500);
3767            assert_eq!(problem(answer).await, "damaged");
3768        }
3769
3770        async fn put(
3771            s: &Server,
3772            token: &str,
3773            uuid: &str,
3774            if_match: Option<&str>,
3775            body: Vec<u8>,
3776        ) -> reqwest::Response {
3777            let mut request = s
3778                .client()
3779                .put(s.url(&format!("/vcons/{uuid}")))
3780                .bearer_auth(token)
3781                .header("content-type", "application/vcon")
3782                .body(body);
3783            if let Some(tag) = if_match {
3784                request = request.header("if-match", tag);
3785            }
3786            request.send().await.expect("an answer")
3787        }
3788
3789        /// A writer and reader in the fixture's tenant, with A.4 stored.
3790        async fn stored_a4(s: &Server) -> (String, String) {
3791            let token = key_in(s, s.tenant, &[Scope::Write, Scope::Read]);
3792            let tag = post(
3793                s,
3794                &token,
3795                "application/vcon",
3796                fixture("ab_call_ext_rec.vcon"),
3797            )
3798            .await;
3799            (token, tag)
3800        }
3801
3802        /// An unsigned vCon may be replaced by its signed form, and a signed
3803        /// one is replaced by nothing.
3804        #[tokio::test]
3805        async fn a4_then_a6_by_put_and_a7_after_a6_conflicts() {
3806            let s = server().await;
3807            let (token, a4) = stored_a4(&s).await;
3808            let a6 = fixture("ab_call_ext_rec_signed.vcon");
3809            let answer = put(&s, &token, UUID, Some(&a4), a6.clone()).await;
3810            assert_eq!(answer.status(), 200);
3811            let new_tag = answer.headers()["etag"].to_str().expect("ascii").to_owned();
3812            assert_ne!(new_tag, a4);
3813            let read = get(&s, &token, UUID).await;
3814            assert_eq!(read.headers()["etag"], new_tag.as_str());
3815            assert_eq!(read.bytes().await.expect("body").as_ref(), a6.as_slice());
3816
3817            let a7 = fixture("ab_call_ext_rec_encrypted.vcon");
3818            let answer = put(&s, &token, UUID, Some(&new_tag), a7).await;
3819            assert_eq!(answer.status(), 409);
3820            assert_eq!(problem(answer).await, "immutable");
3821        }
3822
3823        #[tokio::test]
3824        async fn a_put_without_if_match_is_refused_and_recorded() {
3825            let s = server().await;
3826            let (token, _) = stored_a4(&s).await;
3827            let a6 = fixture("ab_call_ext_rec_signed.vcon");
3828            let answer = put(&s, &token, UUID, None, a6).await;
3829            assert_eq!(answer.status(), 428);
3830            assert_eq!(problem(answer).await, "precondition-required");
3831            assert_eq!(s.tenant_log("replace")[0]["status"], 428);
3832        }
3833
3834        #[tokio::test]
3835        async fn an_if_match_that_does_not_name_the_current_version_fails() {
3836            let s = server().await;
3837            let (token, a4) = stored_a4(&s).await;
3838            let a6 = fixture("ab_call_ext_rec_signed.vcon");
3839            let weak = format!("W/{a4}");
3840            let listed = format!("{a4}, \"00000000000000000000000000000000\"");
3841            let stale = format!("\"{}\"", crate::ids::VersionId([7; 16]));
3842            for tag in [
3843                "*",
3844                weak.as_str(),
3845                listed.as_str(),
3846                "garbage",
3847                stale.as_str(),
3848            ] {
3849                let answer = put(&s, &token, UUID, Some(tag), a6.clone()).await;
3850                assert_eq!(answer.status(), 412, "{tag}");
3851                assert_eq!(problem(answer).await, "precondition-failed");
3852            }
3853            let current = get(&s, &token, UUID).await;
3854            assert_eq!(current.headers()["etag"], a4.as_str(), "nothing replaced");
3855        }
3856
3857        #[tokio::test]
3858        async fn a_body_naming_another_uuid_is_unprocessable() {
3859            let s = server().await;
3860            let (token, a4) = stored_a4(&s).await;
3861            let (_, other) =
3862                crate::store::testing::vcon("01a07da8-c2bb-83e5-b9a2-279e0d16bc47", "x");
3863            let answer = put(&s, &token, UUID, Some(&a4), other).await;
3864            assert_eq!(answer.status(), 422);
3865            assert_eq!(problem(answer).await, "path-uuid-mismatch");
3866            assert_eq!(s.tenant_log("replace")[0]["status"], 422);
3867        }
3868
3869        #[tokio::test]
3870        async fn a_put_to_a_uuid_never_stored_or_deleted_or_badly_written() {
3871            let s = server().await;
3872            let token = key_in(&s, s.tenant, &[Scope::Write, Scope::Read]);
3873            let tag = format!("\"{}\"", crate::ids::VersionId([7; 16]));
3874            let a4 = fixture("ab_call_ext_rec.vcon");
3875            let answer = put(&s, &token, UUID, Some(&tag), a4.clone()).await;
3876            assert_eq!(answer.status(), 404);
3877            assert_eq!(problem(answer).await, "not-found");
3878
3879            let answer = put(&s, &token, "not-a-uuid", Some(&tag), a4.clone()).await;
3880            assert_eq!(answer.status(), 400);
3881            assert_eq!(problem(answer).await, "bad-path-uuid");
3882
3883            let a4_tag = post(&s, &token, "application/vcon", a4.clone()).await;
3884            s.tenants
3885                .store(s.tenant)
3886                .expect("its store")
3887                .delete(
3888                    &crate::store::Caller {
3889                        key: crate::ids::KeyId([9; 8]),
3890                    },
3891                    UUID.parse().expect("uuid"),
3892                    crate::retention::Retention::Years(7),
3893                )
3894                .expect("deleted");
3895            let answer = put(&s, &token, UUID, Some(&a4_tag), a4).await;
3896            assert_eq!(answer.status(), 410);
3897            assert_eq!(problem(answer).await, "gone");
3898        }
3899
3900        #[tokio::test]
3901        async fn a_put_needs_write_and_a_vcon_media_type() {
3902            let s = server().await;
3903            let (_, a4) = stored_a4(&s).await;
3904            let reader = key_in(&s, s.tenant, &[Scope::Read]);
3905            let a6 = fixture("ab_call_ext_rec_signed.vcon");
3906            let answer = put(&s, &reader, UUID, Some(&a4), a6.clone()).await;
3907            assert_eq!(answer.status(), 403);
3908            let writer = key_in(&s, s.tenant, &[Scope::Write]);
3909            let answer = s
3910                .client()
3911                .put(s.url(&format!("/vcons/{UUID}")))
3912                .bearer_auth(&writer)
3913                .header("content-type", "text/plain")
3914                .header("if-match", &a4)
3915                .body(a6)
3916                .send()
3917                .await
3918                .expect("an answer");
3919            assert_eq!(answer.status(), 415);
3920        }
3921
3922        async fn delete(s: &Server, token: &str, uuid: &str) -> reqwest::Response {
3923            s.client()
3924                .delete(s.url(&format!("/vcons/{uuid}")))
3925                .bearer_auth(token)
3926                .send()
3927                .await
3928                .expect("an answer")
3929        }
3930
3931        #[tokio::test]
3932        async fn a_delete_answers_204_again_and_again() {
3933            let s = server().await;
3934            let token = key_in(&s, s.tenant, &[Scope::Write, Scope::Read, Scope::Delete]);
3935            post(
3936                &s,
3937                &token,
3938                "application/vcon",
3939                fixture("ab_call_ext_rec.vcon"),
3940            )
3941            .await;
3942            for _ in 0..2 {
3943                let answer = delete(&s, &token, UUID).await;
3944                assert_eq!(answer.status(), 204);
3945                assert!(answer.bytes().await.expect("body").is_empty());
3946            }
3947            assert_eq!(get(&s, &token, UUID).await.status(), 410);
3948            let deletes = s.tenant_log("delete");
3949            assert_eq!(deletes.len(), 2, "Store records each");
3950        }
3951
3952        #[tokio::test]
3953        async fn a_delete_of_a_uuid_never_stored_or_badly_written() {
3954            let s = server().await;
3955            let token = key_in(&s, s.tenant, &[Scope::Delete]);
3956            let answer = delete(&s, &token, UUID).await;
3957            assert_eq!(answer.status(), 404);
3958            assert_eq!(problem(answer).await, "not-found");
3959            let answer = delete(&s, &token, "not-a-uuid").await;
3960            assert_eq!(answer.status(), 400);
3961            assert_eq!(problem(answer).await, "bad-path-uuid");
3962            assert_eq!(
3963                s.tenant_log("delete").last().expect("recorded")["status"],
3964                400
3965            );
3966        }
3967
3968        #[tokio::test]
3969        async fn a_delete_needs_the_delete_scope_and_its_record() {
3970            let s = server().await;
3971            let (writer, _) = stored_a4(&s).await;
3972            let answer = delete(&s, &writer, UUID).await;
3973            assert_eq!(answer.status(), 403);
3974            let deleter = key_in(&s, s.tenant, &[Scope::Delete]);
3975            s.tenants.store(s.tenant).expect("its store").break_audit();
3976            let answer = delete(&s, &deleter, UUID).await;
3977            assert_eq!(answer.status(), 503);
3978            assert_eq!(problem(answer).await, "audit-failed");
3979        }
3980
3981        /// The purge records of `s`'s tenant whose actor is `token`'s key.
3982        fn purged_by(s: &Server, token: &str) -> usize {
3983            s.tenant_log("purge")
3984                .iter()
3985                .filter(|r| r["actor"] == token[4..20])
3986                .count()
3987        }
3988
3989        #[tokio::test]
3990        async fn a_delete_erases_at_once_under_the_tenants_zero_period() {
3991            let s = server().await;
3992            s.tenants
3993                .set_retention(Actor::Cli, s.tenant, Some(Retention::Days(0)))
3994                .expect("set");
3995            let (_, _) = stored_a4(&s).await;
3996            let deleter = key_in(&s, s.tenant, &[Scope::Delete]);
3997            assert_eq!(delete(&s, &deleter, UUID).await.status(), 204);
3998            assert_eq!(purged_by(&s, &deleter), 1);
3999        }
4000
4001        #[tokio::test]
4002        async fn a_delete_uses_the_server_default_when_the_tenant_sets_none() {
4003            let s = server_keeping(Retention::Days(0)).await;
4004            let (_, _) = stored_a4(&s).await;
4005            let deleter = key_in(&s, s.tenant, &[Scope::Delete]);
4006            assert_eq!(delete(&s, &deleter, UUID).await.status(), 204);
4007            assert_eq!(purged_by(&s, &deleter), 1);
4008        }
4009
4010        #[tokio::test]
4011        async fn a_delete_keeps_a_young_vcon_for_legal_reads() {
4012            let s = server().await;
4013            let (_, _) = stored_a4(&s).await;
4014            let deleter = key_in(&s, s.tenant, &[Scope::Delete]);
4015            assert_eq!(delete(&s, &deleter, UUID).await.status(), 204);
4016            assert_eq!(purged_by(&s, &deleter), 0);
4017        }
4018
4019        #[tokio::test]
4020        async fn a_delete_whose_period_cannot_be_read_deletes_nothing() {
4021            let s = server().await;
4022            let (_, _) = stored_a4(&s).await;
4023            s.tenants
4024                .conn_for_test()
4025                .execute(
4026                    "UPDATE tenant SET retention = '7w' WHERE id = ?1",
4027                    [s.tenant.0],
4028                )
4029                .expect("damaged");
4030            let token = key_in(&s, s.tenant, &[Scope::Delete, Scope::Read]);
4031            let answer = delete(&s, &token, UUID).await;
4032            assert_eq!(answer.status(), 503);
4033            assert_eq!(problem(answer).await, "unavailable");
4034            assert_eq!(get(&s, &token, UUID).await.status(), 200);
4035            assert_eq!(
4036                s.tenant_log("delete").last().expect("recorded")["status"],
4037                503
4038            );
4039        }
4040
4041        async fn health(s: &Server, token: Option<&str>) -> reqwest::Response {
4042            let mut request = s.client().get(s.url("/health"));
4043            if let Some(token) = token {
4044                request = request.bearer_auth(token);
4045            }
4046            request.send().await.expect("an answer")
4047        }
4048
4049        #[tokio::test]
4050        async fn health_needs_a_key_of_any_scope_and_is_counted() {
4051            let s = server().await;
4052            assert_eq!(health(&s, None).await.status(), 401);
4053            let reader = key_in(&s, s.tenant, &[Scope::Read]);
4054            for _ in 0..2 {
4055                let answer = health(&s, Some(&reader)).await;
4056                assert_eq!(answer.status(), 200);
4057                assert_eq!(answer.headers()["content-type"], "application/json");
4058            }
4059            let counted = s.tenant_log("health");
4060            assert_eq!(counted.len(), 1, "one record a minute");
4061            assert_eq!(counted[0]["count"], 2);
4062            assert_eq!(counted[0]["status"], 200);
4063            // The last second of the minute still counts in it; the next
4064            // minute starts a record of its own.
4065            s.clock.set(START + time::Duration::seconds(59));
4066            health(&s, Some(&reader)).await;
4067            s.clock.set(START + time::Duration::minutes(1));
4068            health(&s, Some(&reader)).await;
4069            let counted = s.tenant_log("health");
4070            assert_eq!(
4071                counted
4072                    .iter()
4073                    .map(|r| (
4074                        r["at"].as_str().expect("at"),
4075                        r["count"].as_u64().expect("count")
4076                    ))
4077                    .collect::<Vec<_>>(),
4078                [("2026-10-23T12:00:00Z", 3), ("2026-10-23T12:01:00Z", 1)],
4079                "a record counts the requests of its minute only"
4080            );
4081
4082            let operator = s
4083                .tenants
4084                .create_key(Actor::Cli, None, Scopes::of(&[Scope::Admin]), "op")
4085                .expect("made")
4086                .token;
4087            assert_eq!(health(&s, Some(&operator)).await.status(), 200);
4088            let admin: Vec<_> = s
4089                .tenants
4090                .admin_records()
4091                .into_iter()
4092                .filter(|r| r["action"] == "health")
4093                .collect();
4094            assert_eq!(admin.len(), 1);
4095            assert_eq!(admin[0]["actor"], operator[4..20]);
4096        }
4097
4098        #[tokio::test]
4099        async fn health_is_unavailable_when_the_catalog_does_not_answer() {
4100            let s = server().await;
4101            let reader = key_in(&s, s.tenant, &[Scope::Read]);
4102            s.tenants
4103                .conn_for_test()
4104                .execute_batch("ALTER TABLE tenant RENAME TO gone")
4105                .expect("lose the table");
4106            let answer = health(&s, Some(&reader)).await;
4107            assert_eq!(answer.status(), 503);
4108            assert_eq!(problem(answer).await, "unavailable");
4109        }
4110
4111        #[tokio::test]
4112        async fn health_whose_count_fails_is_unavailable() {
4113            let s = server().await;
4114            let operator = s
4115                .tenants
4116                .create_key(Actor::Cli, None, Scopes::of(&[Scope::Admin]), "op")
4117                .expect("made")
4118                .token;
4119            s.tenants.break_audit();
4120            let answer = health(&s, Some(&operator)).await;
4121            assert_eq!(answer.status(), 503);
4122            assert_eq!(problem(answer).await, "audit-failed");
4123        }
4124    }
4125
4126    /// `GET /openapi.json`: the description, to any valid key.
4127    mod openapi_route {
4128        use super::pipeline::{Server, problem, server};
4129        use crate::access::{Scope, Scopes};
4130        use crate::audit::Actor;
4131
4132        async fn get(s: &Server, token: Option<&str>) -> reqwest::Response {
4133            let mut request = s.client().get(s.url("/openapi.json"));
4134            if let Some(token) = token {
4135                request = request.bearer_auth(token);
4136            }
4137            request.send().await.expect("an answer")
4138        }
4139
4140        fn admin_openapi_records(s: &Server) -> usize {
4141            let mut out = Vec::new();
4142            s.tenants.export_admin(None, &mut out).expect("export");
4143            String::from_utf8(out)
4144                .expect("UTF-8")
4145                .lines()
4146                .filter(|l| l.contains("\"action\":\"openapi\""))
4147                .count()
4148        }
4149
4150        #[tokio::test]
4151        async fn any_valid_key_gets_the_description_as_compiled_and_it_is_recorded() {
4152            let s = server().await;
4153            let operator = s
4154                .tenants
4155                .create_key(Actor::Cli, None, Scopes::of(&[Scope::Admin]), "op")
4156                .expect("an operator key")
4157                .token;
4158            for token in [s.key(&[Scope::Read]), s.key(&[Scope::Write]), operator] {
4159                let answer = get(&s, Some(&token)).await;
4160                assert_eq!(answer.status(), 200);
4161                assert_eq!(answer.headers()["content-type"], "application/json");
4162                assert_eq!(answer.text().await.expect("text"), super::super::OPENAPI);
4163            }
4164            let recorded = s.tenant_log("openapi");
4165            assert_eq!(recorded.len(), 2, "the two tenant keys");
4166            assert!(recorded.iter().all(|r| r["status"] == 200));
4167            assert_eq!(admin_openapi_records(&s), 1, "the operator key");
4168        }
4169
4170        #[tokio::test]
4171        async fn no_key_is_401_and_a_read_that_cannot_be_recorded_503() {
4172            let s = server().await;
4173            let answer = get(&s, None).await;
4174            assert_eq!(answer.status(), 401);
4175            assert_eq!(problem(answer).await, "unauthorized");
4176            let reader = s.key(&[Scope::Read]);
4177            s.tenants.store(s.tenant).expect("its store").break_audit();
4178            let answer = get(&s, Some(&reader)).await;
4179            assert_eq!(answer.status(), 503);
4180            assert_eq!(problem(answer).await, "audit-failed");
4181        }
4182
4183        #[tokio::test]
4184        async fn a_key_of_a_closing_tenant_is_503() {
4185            let s = server().await;
4186            let reader = s.key(&[Scope::Read]);
4187            s.tenants.close(s.tenant);
4188            let answer = get(&s, Some(&reader)).await;
4189            assert_eq!(answer.status(), 503);
4190            assert_eq!(problem(answer).await, "unavailable");
4191        }
4192    }
4193
4194    /// `GET /audit` and `GET /admin/audit`: a log a page at a time, as the
4195    /// command line exports it.
4196    mod audit_routes {
4197        use super::pipeline::{Server, problem, server};
4198        use crate::access::{Scope, Scopes};
4199        use crate::audit::Actor;
4200        use crate::store::testing::vcon;
4201
4202        fn operator(s: &Server) -> String {
4203            s.tenants
4204                .create_key(Actor::Cli, None, Scopes::of(&[Scope::Admin]), "op")
4205                .expect("an operator key")
4206                .token
4207        }
4208
4209        async fn get(s: &Server, path: &str, token: Option<&str>) -> reqwest::Response {
4210            let mut request = s.client().get(s.url(path));
4211            if let Some(token) = token {
4212                request = request.bearer_auth(token);
4213            }
4214            request.send().await.expect("an answer")
4215        }
4216
4217        async fn body(answer: reqwest::Response) -> Vec<u8> {
4218            assert_eq!(answer.status(), 200);
4219            assert_eq!(answer.headers()["content-type"], "application/jsonl");
4220            answer.bytes().await.expect("body").to_vec()
4221        }
4222
4223        /// The tenant's log as `audit export --tenant` writes it.
4224        fn tenant_export(s: &Server, since: Option<i64>) -> Vec<u8> {
4225            let mut out = Vec::new();
4226            s.tenants
4227                .store(s.tenant)
4228                .expect("its store")
4229                .export(since, &mut out)
4230                .expect("export");
4231            out
4232        }
4233
4234        fn admin_export(s: &Server) -> Vec<u8> {
4235            let mut out = Vec::new();
4236            s.tenants.export_admin(None, &mut out).expect("export");
4237            out
4238        }
4239
4240        async fn three_stored(s: &Server) {
4241            let writer = s.key(&[Scope::Write]);
4242            for n in 1..=3 {
4243                let answer = s
4244                    .client()
4245                    .post(s.url("/vcons"))
4246                    .bearer_auth(&writer)
4247                    .header("content-type", "application/vcon")
4248                    .body(vcon(&format!("01a07da8-c2bb-83e5-b9a2-{n:012x}"), "x").1)
4249                    .send()
4250                    .await
4251                    .expect("an answer");
4252                assert_eq!(answer.status(), 201);
4253            }
4254        }
4255
4256        #[tokio::test]
4257        async fn the_tenant_log_is_its_export_byte_for_byte_a_page_at_a_time() {
4258            let s = server().await;
4259            three_stored(&s).await;
4260            let auditor = s.key(&[Scope::Audit]);
4261            let before = tenant_export(&s, None);
4262            assert_eq!(body(get(&s, "/audit", Some(&auditor)).await).await, before);
4263            let lines: Vec<&[u8]> = before.split_inclusive(|b| *b == b'\n').collect();
4264            assert_eq!(
4265                body(get(&s, "/audit?after=1&limit=2", Some(&auditor)).await).await,
4266                lines[1..3].concat()
4267            );
4268            // The reads are recorded, each after its own page.
4269            let reads = s.tenant_log("audit_read");
4270            assert_eq!(reads.len(), 2);
4271            assert!(reads.iter().all(|r| r["status"] == 200));
4272            // The page is the log as it stood before the read, which then
4273            // records itself after it.
4274            let after = 3;
4275            let expected = tenant_export(&s, Some(after));
4276            assert!(
4277                !expected.is_empty(),
4278                "three stores and two reads leave records past seq 3"
4279            );
4280            let page = body(get(&s, &format!("/audit?after={after}"), Some(&auditor)).await).await;
4281            assert_eq!(
4282                page, expected,
4283                "the page is every record after seq 3, whole"
4284            );
4285        }
4286
4287        #[tokio::test]
4288        async fn the_admin_log_is_its_export_byte_for_byte() {
4289            let s = server().await;
4290            let op = operator(&s);
4291            let before = admin_export(&s);
4292            assert!(!before.is_empty(), "the tenant's creation, at least");
4293            assert_eq!(body(get(&s, "/admin/audit", Some(&op)).await).await, before);
4294            let lines: Vec<&[u8]> = before.split_inclusive(|b| *b == b'\n').collect();
4295            assert_eq!(
4296                body(get(&s, "/admin/audit?limit=1", Some(&op)).await).await,
4297                lines[0]
4298            );
4299            let mut recorded = Vec::new();
4300            s.tenants.export_admin(None, &mut recorded).expect("export");
4301            let reads = String::from_utf8(recorded)
4302                .expect("UTF-8")
4303                .lines()
4304                .filter(|l| l.contains("\"action\":\"audit_read\""))
4305                .count();
4306            assert_eq!(reads, 2);
4307        }
4308
4309        #[tokio::test]
4310        async fn the_tenant_log_needs_a_tenant_key_with_audit() {
4311            let s = server().await;
4312            assert_eq!(get(&s, "/audit", None).await.status(), 401);
4313            for scopes in [
4314                &[Scope::Read][..],
4315                &[Scope::Legal],
4316                &[Scope::Write, Scope::Delete],
4317            ] {
4318                let answer = get(&s, "/audit", Some(&s.key(scopes))).await;
4319                assert_eq!(answer.status(), 403, "{scopes:?}");
4320                assert_eq!(problem(answer).await, "forbidden");
4321            }
4322            let answer = get(&s, "/audit", Some(&operator(&s))).await;
4323            assert_eq!(answer.status(), 403, "an operator key");
4324            let refused = s.tenant_log("audit_read");
4325            assert_eq!(refused.len(), 3, "the tenant keys' refusals");
4326            assert!(refused.iter().all(|r| r["status"] == 403));
4327        }
4328
4329        #[tokio::test]
4330        async fn the_admin_log_needs_an_operator_key() {
4331            let s = server().await;
4332            assert_eq!(get(&s, "/admin/audit", None).await.status(), 401);
4333            let every = s.key(&[
4334                Scope::Write,
4335                Scope::Read,
4336                Scope::Delete,
4337                Scope::Legal,
4338                Scope::Audit,
4339            ]);
4340            let answer = get(&s, "/admin/audit", Some(&every)).await;
4341            assert_eq!(answer.status(), 403);
4342            assert_eq!(problem(answer).await, "forbidden");
4343            assert_eq!(s.tenant_log("audit_read").len(), 1, "recorded");
4344        }
4345
4346        #[tokio::test]
4347        async fn a_query_the_routes_do_not_take_is_a_bad_query_and_recorded() {
4348            let s = server().await;
4349            let auditor = s.key(&[Scope::Audit]);
4350            let op = operator(&s);
4351            let bad = [
4352                "?since=3",
4353                "?after=-1",
4354                "?limit=0",
4355                "?limit=10001",
4356                "?after=1&after=2",
4357            ];
4358            for query in bad {
4359                for (path, token) in [("/audit", &auditor), ("/admin/audit", &op)] {
4360                    let answer = get(&s, &format!("{path}{query}"), Some(token)).await;
4361                    assert_eq!(answer.status(), 400, "{path}{query}");
4362                    assert_eq!(problem(answer).await, "bad-query");
4363                }
4364            }
4365            let refused = s.tenant_log("audit_read");
4366            assert_eq!(refused.len(), bad.len());
4367            assert!(refused.iter().all(|r| r["status"] == 400));
4368            let mut admin = Vec::new();
4369            s.tenants.export_admin(None, &mut admin).expect("export");
4370            let admin_refused = String::from_utf8(admin)
4371                .expect("UTF-8")
4372                .lines()
4373                .filter(|l| l.contains("\"action\":\"audit_read\"") && l.contains("\"status\":400"))
4374                .count();
4375            assert_eq!(admin_refused, bad.len());
4376        }
4377
4378        #[tokio::test]
4379        async fn a_read_that_cannot_be_recorded_is_503() {
4380            let s = server().await;
4381            let auditor = s.key(&[Scope::Audit]);
4382            let op = operator(&s);
4383            s.tenants.store(s.tenant).expect("its store").break_audit();
4384            let answer = get(&s, "/audit", Some(&auditor)).await;
4385            assert_eq!(answer.status(), 503);
4386            assert_eq!(problem(answer).await, "audit-failed");
4387            s.tenants.break_audit();
4388            let answer = get(&s, "/admin/audit", Some(&op)).await;
4389            assert_eq!(answer.status(), 503);
4390            assert_eq!(problem(answer).await, "audit-failed");
4391        }
4392    }
4393
4394    /// `GET /vcons`: a tenant's live vCons, a page at a time.
4395    mod list {
4396        use serde_json::Value;
4397
4398        use super::pipeline::{START, Server, problem, server};
4399        use crate::access::{Scope, Scopes};
4400        use crate::audit::Actor;
4401        use crate::ids::TenantId;
4402        use crate::store::testing::vcon;
4403
4404        fn uuid(n: u32) -> String {
4405            format!("01a07da8-c2bb-83e5-b9a2-{n:012x}")
4406        }
4407
4408        fn key_of(s: &Server, tenant: TenantId, scopes: &[Scope]) -> String {
4409            s.tenants
4410                .create_key(Actor::Cli, Some(tenant), Scopes::of(scopes), "test")
4411                .expect("a key")
4412                .token
4413        }
4414
4415        /// Stores vCon `n` with `writer` and returns its tag, unquoted.
4416        async fn stored(s: &Server, writer: &str, n: u32) -> String {
4417            let answer = s
4418                .client()
4419                .post(s.url("/vcons"))
4420                .bearer_auth(writer)
4421                .header("content-type", "application/vcon")
4422                .body(vcon(&uuid(n), "listed").1)
4423                .send()
4424                .await
4425                .expect("an answer");
4426            assert_eq!(answer.status(), 201);
4427            answer.headers()["etag"]
4428                .to_str()
4429                .expect("text")
4430                .trim_matches('"')
4431                .to_owned()
4432        }
4433
4434        async fn list(s: &Server, token: Option<&str>, query: &str) -> reqwest::Response {
4435            let mut request = s.client().get(s.url(&format!("/vcons{query}")));
4436            if let Some(token) = token {
4437                request = request.bearer_auth(token);
4438            }
4439            request.send().await.expect("an answer")
4440        }
4441
4442        async fn page(s: &Server, reader: &str, query: &str) -> Value {
4443            let answer = list(s, Some(reader), query).await;
4444            assert_eq!(answer.status(), 200, "{query}");
4445            assert_eq!(answer.headers()["content-type"], "application/json");
4446            serde_json::from_str(&answer.text().await.expect("text")).expect("JSON")
4447        }
4448
4449        fn uuids(page: &Value) -> Vec<String> {
4450            page["vcons"]
4451                .as_array()
4452                .expect("a list")
4453                .iter()
4454                .map(|v| v["uuid"].as_str().expect("a uuid").to_owned())
4455                .collect()
4456        }
4457
4458        /// Every vCon the reader's pages list, following `next`.
4459        async fn every_page(s: &Server, reader: &str, limit: u32) -> Vec<String> {
4460            let mut seen = Vec::new();
4461            let mut query = format!("?limit={limit}");
4462            for _ in 0..100 {
4463                let page = page(s, reader, &query).await;
4464                seen.extend(uuids(&page));
4465                match page["next"].as_str() {
4466                    Some(cursor) => query = format!("?limit={limit}&cursor={cursor}"),
4467                    None => return seen,
4468                }
4469            }
4470            panic!("the cursors end")
4471        }
4472
4473        #[tokio::test]
4474        async fn the_list_is_newest_first_with_each_tag_and_time() {
4475            let s = server().await;
4476            let writer = s.key(&[Scope::Write]);
4477            let mut tags = Vec::new();
4478            for n in 1..=3 {
4479                tags.push(stored(&s, &writer, n).await);
4480            }
4481            let reader = s.key(&[Scope::Read]);
4482            let page = page(&s, &reader, "").await;
4483            assert_eq!(uuids(&page), [3, 2, 1].map(uuid));
4484            assert_eq!(page["next"], Value::Null);
4485            for (listed, tag) in page["vcons"]
4486                .as_array()
4487                .expect("a list")
4488                .iter()
4489                .zip(tags.iter().rev())
4490            {
4491                assert_eq!(listed["version"], tag.as_str());
4492                assert_eq!(listed["stored_at"], listed["updated_at"]);
4493                let at = listed["stored_at"].as_str().expect("a time");
4494                assert!(at.ends_with('Z'), "{at}");
4495            }
4496            let recorded = s.tenant_log("list");
4497            assert_eq!(recorded.len(), 1);
4498            assert_eq!(recorded[0]["status"], 200);
4499        }
4500
4501        #[tokio::test]
4502        async fn paging_returns_each_vcon_once_and_a_deleted_one_never() {
4503            let s = server().await;
4504            let writer = s.key(&[Scope::Write]);
4505            for n in 1..=7 {
4506                stored(&s, &writer, n).await;
4507            }
4508            let deleted = s
4509                .client()
4510                .delete(s.url(&format!("/vcons/{}", uuid(4))))
4511                .bearer_auth(s.key(&[Scope::Delete]))
4512                .send()
4513                .await
4514                .expect("an answer");
4515            assert_eq!(deleted.status(), 204);
4516            let reader = s.key(&[Scope::Read]);
4517            for limit in [1, 2, 3, 6, 7, 100] {
4518                assert_eq!(
4519                    every_page(&s, &reader, limit).await,
4520                    [7, 6, 5, 3, 2, 1].map(uuid),
4521                    "limit {limit}"
4522                );
4523            }
4524        }
4525
4526        #[tokio::test]
4527        async fn a_tenant_lists_its_own_vcons_never_another_tenants() {
4528            let s = server().await;
4529            let beta = s.tenants.create(Actor::Cli, "beta", None).expect("made");
4530            let acme_writer = s.key(&[Scope::Write]);
4531            let beta_writer = key_of(&s, beta, &[Scope::Write]);
4532            // Stored a second apart, acme's first: 1, 3, 5, then beta's
4533            // 2, 3, 4. vCon 3 is held by both tenants, each its own.
4534            let mut at = START;
4535            for (writer, n) in [(&acme_writer, 1), (&acme_writer, 3), (&acme_writer, 5)]
4536                .into_iter()
4537                .chain([(&beta_writer, 2), (&beta_writer, 3), (&beta_writer, 4)])
4538            {
4539                at += time::Duration::SECOND;
4540                s.clock.set(at);
4541                stored(&s, writer, n).await;
4542            }
4543            let acme = s.key(&[Scope::Read]);
4544            let beta_reader = key_of(&s, beta, &[Scope::Read]);
4545            assert_eq!(every_page(&s, &acme, 1).await, [5, 3, 1].map(uuid));
4546            assert_eq!(every_page(&s, &beta_reader, 1).await, [4, 3, 2].map(uuid));
4547            // A cursor from the other tenant's list only sets a place.
4548            let beta_page = page(&s, &beta_reader, "?limit=1").await;
4549            let cursor = beta_page["next"].as_str().expect("a cursor");
4550            // Beta's first, vCon 4, was stored after all of acme's, so the
4551            // place is before every acme vCon.
4552            let mixed = page(&s, &acme, &format!("?cursor={cursor}")).await;
4553            assert_eq!(
4554                uuids(&mixed),
4555                [5, 3, 1].map(uuid),
4556                "a foreign cursor sets a place in this tenant's list, and lists all of it after"
4557            );
4558        }
4559
4560        #[tokio::test]
4561        async fn since_and_until_bound_the_first_store_time() {
4562            let s = server().await;
4563            let writer = s.key(&[Scope::Write]);
4564            stored(&s, &writer, 1).await;
4565            let reader = s.key(&[Scope::Read]);
4566            let first = page(&s, &reader, "").await;
4567            let at = first["vcons"][0]["stored_at"]
4568                .as_str()
4569                .expect("a time")
4570                .to_owned();
4571            let at = at.replace(':', "%3A");
4572            assert_eq!(
4573                uuids(&page(&s, &reader, &format!("?since={at}")).await),
4574                vec![uuid(1)]
4575            );
4576            assert_eq!(
4577                uuids(&page(&s, &reader, &format!("?until={at}")).await),
4578                Vec::<String>::new()
4579            );
4580        }
4581
4582        #[tokio::test]
4583        async fn a_query_the_route_does_not_take_is_a_bad_query_and_recorded() {
4584            let s = server().await;
4585            let reader = s.key(&[Scope::Read]);
4586            let bad = [
4587                "?offset=2",
4588                "?limit=0",
4589                "?limit=1001",
4590                "?limit=ten",
4591                "?limit=1&limit=2",
4592                "?cursor=nope",
4593                "?since=yesterday",
4594                "?until=2026-10-25",
4595            ];
4596            for query in bad {
4597                let answer = list(&s, Some(&reader), query).await;
4598                assert_eq!(answer.status(), 400, "{query}");
4599                assert_eq!(problem(answer).await, "bad-query", "{query}");
4600            }
4601            let recorded = s.tenant_log("list");
4602            assert_eq!(recorded.len(), bad.len());
4603            assert!(recorded.iter().all(|r| r["status"] == 400));
4604        }
4605
4606        #[tokio::test]
4607        async fn the_list_needs_a_tenant_key_with_read() {
4608            let s = server().await;
4609            let answer = list(&s, None, "").await;
4610            assert_eq!(answer.status(), 401);
4611            for scopes in [&[Scope::Write][..], &[Scope::Delete], &[Scope::Legal]] {
4612                let answer = list(&s, Some(&s.key(scopes)), "").await;
4613                assert_eq!(answer.status(), 403, "{scopes:?}");
4614                assert_eq!(problem(answer).await, "forbidden");
4615            }
4616            let operator = s
4617                .tenants
4618                .create_key(Actor::Cli, None, Scopes::of(&[Scope::Admin]), "op")
4619                .expect("an operator key")
4620                .token;
4621            let answer = list(&s, Some(&operator), "").await;
4622            assert_eq!(answer.status(), 403);
4623            let recorded = s.tenant_log("list");
4624            assert_eq!(recorded.len(), 3, "the tenant keys' refusals");
4625            assert!(recorded.iter().all(|r| r["status"] == 403));
4626        }
4627
4628        #[tokio::test]
4629        async fn damage_is_500_and_a_list_that_cannot_be_recorded_503() {
4630            let s = server().await;
4631            let writer = s.key(&[Scope::Write]);
4632            stored(&s, &writer, 1).await;
4633            let reader = s.key(&[Scope::Read]);
4634            let store = s.tenants.store(s.tenant).expect("its store");
4635            store
4636                .conn_for_test()
4637                .execute("DELETE FROM version", [])
4638                .expect("damaged");
4639            let answer = list(&s, Some(&reader), "").await;
4640            assert_eq!(answer.status(), 500);
4641            assert_eq!(problem(answer).await, "damaged");
4642            store.break_audit();
4643            let answer = list(&s, Some(&reader), "").await;
4644            assert_eq!(answer.status(), 503);
4645            assert_eq!(problem(answer).await, "audit-failed");
4646        }
4647    }
4648
4649    /// `GET /key`: a key learns its own id, tenant and scopes, and the
4650    /// request is recorded before the answer goes out.
4651    mod key_info {
4652        use serde_json::Value;
4653
4654        use super::pipeline::{Server, problem, server};
4655        use crate::access::{self, Scope, Scopes};
4656        use crate::audit::Actor;
4657
4658        async fn key(s: &Server, token: Option<&str>) -> reqwest::Response {
4659            let mut request = s.client().get(s.url("/key"));
4660            if let Some(token) = token {
4661                request = request.bearer_auth(token);
4662            }
4663            request.send().await.expect("an answer")
4664        }
4665
4666        /// What the key may do, and nothing else: its id, tenant and scopes.
4667        fn own(token: &str, tenant: Option<String>, scopes: &[&str]) -> Value {
4668            let id = access::parse(token).expect("a token").to_string();
4669            serde_json::json!({ "id": id, "tenant": tenant, "scopes": scopes })
4670        }
4671
4672        #[tokio::test]
4673        async fn a_tenant_key_reads_its_own_record_and_the_read_is_recorded() {
4674            let s = server().await;
4675            let token = s.key(&[Scope::Read, Scope::Delete]);
4676            let answer = key(&s, Some(&token)).await;
4677            assert_eq!(answer.status(), 200);
4678            assert_eq!(answer.headers()["content-type"], "application/json");
4679            assert_eq!(answer.headers()["cache-control"], "no-store");
4680            let body: Value =
4681                serde_json::from_slice(&answer.bytes().await.expect("body")).expect("JSON");
4682            let tenant = Some(s.tenant.to_string());
4683            assert_eq!(body, own(&token, tenant, &["read", "delete"]));
4684            let records = s.tenant_log("key_info");
4685            assert_eq!(records.len(), 1, "one record per request");
4686            assert_eq!(records[0]["status"], 200);
4687            assert_eq!(records[0]["actor"], token[4..20]);
4688            assert_eq!(records[0]["target"], token[4..20], "the key read");
4689        }
4690
4691        #[tokio::test]
4692        async fn an_operator_key_reads_its_record_in_the_admin_log() {
4693            let s = server().await;
4694            let token = s
4695                .tenants
4696                .create_key(Actor::Cli, None, Scopes::of(&[Scope::Admin]), "op")
4697                .expect("made")
4698                .token;
4699            let answer = key(&s, Some(&token)).await;
4700            assert_eq!(answer.status(), 200);
4701            let body: Value =
4702                serde_json::from_slice(&answer.bytes().await.expect("body")).expect("JSON");
4703            assert_eq!(body, own(&token, None, &["admin"]));
4704            let records: Vec<_> = s
4705                .tenants
4706                .admin_records()
4707                .into_iter()
4708                .filter(|r| r["action"] == "key_info")
4709                .collect();
4710            assert_eq!(records.len(), 1);
4711            assert_eq!(records[0]["status"], 200);
4712            assert_eq!(records[0]["target"], token[4..20]);
4713            assert!(s.tenant_log("key_info").is_empty(), "no tenant's log");
4714        }
4715
4716        #[tokio::test]
4717        async fn no_key_or_a_bad_one_is_401() {
4718            let s = server().await;
4719            for token in [None, Some("vcs_nonsense")] {
4720                let answer = key(&s, token).await;
4721                assert_eq!(answer.status(), 401, "{token:?}");
4722                assert_eq!(problem(answer).await, "unauthorized");
4723            }
4724        }
4725
4726        #[tokio::test]
4727        async fn a_read_that_cannot_be_recorded_shows_nothing() {
4728            let s = server().await;
4729            let token = s.key(&[Scope::Read]);
4730            s.tenants.store(s.tenant).expect("its store").break_audit();
4731            let answer = key(&s, Some(&token)).await;
4732            assert_eq!(answer.status(), 503);
4733            assert_eq!(problem(answer).await, "audit-failed");
4734        }
4735
4736        #[tokio::test]
4737        async fn a_key_whose_tenant_is_closing_is_503() {
4738            let s = server().await;
4739            let token = s.key(&[Scope::Read]);
4740            s.tenants.close(s.tenant);
4741            let answer = key(&s, Some(&token)).await;
4742            assert_eq!(answer.status(), 503);
4743            assert_eq!(problem(answer).await, "unavailable");
4744            let refused: Vec<_> = s
4745                .tenants
4746                .admin_records()
4747                .into_iter()
4748                .filter(|r| r["action"] == "key_info")
4749                .collect();
4750            assert_eq!(refused.len(), 1, "the refusal is recorded");
4751            assert_eq!(refused[0]["status"], 503);
4752        }
4753    }
4754
4755    /// The console's files are served through the API's own router, with
4756    /// its headers, and the API itself never asks a browser to keep or
4757    /// share anything.
4758    mod console {
4759        use super::pipeline::server;
4760        use crate::access::Scope;
4761
4762        #[tokio::test]
4763        async fn the_router_serves_the_console_without_a_key() {
4764            let s = server().await;
4765            let files = [
4766                ("/console/", "index.html", "text/html; charset=utf-8"),
4767                (
4768                    "/console/console.js",
4769                    "console.js",
4770                    "text/javascript; charset=utf-8",
4771                ),
4772                (
4773                    "/console/console.css",
4774                    "console.css",
4775                    "text/css; charset=utf-8",
4776                ),
4777            ];
4778            for (path, file, media_type) in files {
4779                let answer = s.client().get(s.url(path)).send().await.expect("an answer");
4780                assert_eq!(answer.status(), 200, "{path}");
4781                assert_eq!(answer.headers()["content-type"], media_type, "{path}");
4782                for (name, value) in crate::console::HEADERS {
4783                    assert_eq!(answer.headers()[name], value, "{path} {name}");
4784                }
4785                let source = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
4786                    .join("console")
4787                    .join(file);
4788                let source = std::fs::read(source).expect("the source file");
4789                assert_eq!(answer.bytes().await.expect("body"), source, "{path}");
4790            }
4791        }
4792
4793        /// Every answer under /console carries the headers, a refusal too.
4794        #[tokio::test]
4795        async fn every_other_console_answer_carries_the_headers() {
4796            let s = server().await;
4797            let client = s.client_not_following();
4798            let answers = [
4799                (client.get(s.url("/console/other.js")), 404),
4800                (client.get(s.url("/console/a/b")), 404),
4801                (client.post(s.url("/console/")), 405),
4802                (client.get(s.url("/console")), 308),
4803            ];
4804            for (request, status) in answers {
4805                let answer = request.send().await.expect("an answer");
4806                assert_eq!(answer.status(), status, "{answer:?}");
4807                for (name, value) in crate::console::HEADERS {
4808                    assert_eq!(answer.headers()[name], value, "{status} {name}");
4809                }
4810                if status == 308 {
4811                    assert_eq!(answer.headers()["location"], "/console/");
4812                }
4813            }
4814            let elsewhere = client
4815                .get(s.url("/consoles"))
4816                .send()
4817                .await
4818                .expect("an answer");
4819            assert_eq!(elsewhere.status(), 404);
4820            assert!(
4821                !elsewhere.headers().contains_key("content-security-policy"),
4822                "only console answers carry its headers"
4823            );
4824        }
4825
4826        #[tokio::test]
4827        async fn no_answer_sets_a_cookie_or_allows_another_origin() {
4828            let s = server().await;
4829            let token = s.key(&[Scope::Read]);
4830            for path in ["/console/", "/health", "/key", "/vcons/nonsense"] {
4831                let answer = s
4832                    .client()
4833                    .get(s.url(path))
4834                    .bearer_auth(&token)
4835                    .header("origin", "https://elsewhere.example.com")
4836                    .send()
4837                    .await
4838                    .expect("an answer");
4839                let headers = answer.headers();
4840                assert!(!headers.contains_key("set-cookie"), "{path}");
4841                assert!(
4842                    !headers.contains_key("access-control-allow-origin"),
4843                    "{path}"
4844                );
4845            }
4846        }
4847    }
4848}
4849
4850/// The website documents every problem type at the URL the type names, so
4851/// a client following a `type` finds what it means. These tests hold the
4852/// pages to the code.
4853#[cfg(test)]
4854mod site_pages {
4855    use std::collections::BTreeSet;
4856    use std::path::PathBuf;
4857
4858    use super::*;
4859
4860    /// One of every problem the API can answer. Each list sits behind an
4861    /// exhaustive match, so a new kind of problem fails to compile here
4862    /// until it is listed, and then fails the test until it has its page.
4863    pub(super) fn every_problem() -> Vec<Problem> {
4864        let mut all = vec![
4865            Problem::Unauthorized,
4866            Problem::Forbidden,
4867            Problem::TooLarge,
4868            Problem::Timeout,
4869            Problem::BadBody,
4870            Problem::Unavailable,
4871            Problem::AuditFailed,
4872            Problem::UnsupportedMediaType,
4873            Problem::BadPathUuid,
4874            Problem::BadPathVersion,
4875            Problem::PreconditionRequired,
4876            Problem::PathUuidMismatch,
4877            Problem::InvalidBody,
4878            Problem::BadQuery,
4879            Problem::UnknownKey,
4880            Problem::RateLimited,
4881            Problem::AtCapacity,
4882            Problem::UnknownTool,
4883            Problem::InvalidArguments,
4884        ];
4885        for problem in &all {
4886            match problem {
4887                Problem::Unauthorized
4888                | Problem::Forbidden
4889                | Problem::TooLarge
4890                | Problem::Timeout
4891                | Problem::BadBody
4892                | Problem::Unavailable
4893                | Problem::AuditFailed
4894                | Problem::UnsupportedMediaType
4895                | Problem::BadPathUuid
4896                | Problem::BadPathVersion
4897                | Problem::PreconditionRequired
4898                | Problem::PathUuidMismatch
4899                | Problem::InvalidBody
4900                | Problem::BadQuery
4901                | Problem::UnknownKey
4902                | Problem::RateLimited
4903                | Problem::AtCapacity
4904                | Problem::UnknownTool
4905                | Problem::InvalidArguments
4906                | Problem::Invalid(_)
4907                | Problem::Store(_)
4908                | Problem::Tenant(_) => {}
4909            }
4910        }
4911        let invalid = [
4912            Invalid::BadGzip,
4913            Invalid::NotJson,
4914            Invalid::DuplicateMember,
4915            Invalid::AmbiguousForm,
4916            Invalid::NoForm,
4917            Invalid::MissingUuid,
4918            Invalid::BadUuid,
4919            Invalid::MissingCreatedAt,
4920            Invalid::BadPayload,
4921            Invalid::UuidMismatch,
4922            Invalid::BadCritical,
4923            Invalid::UnsupportedExtension,
4924            Invalid::TooDeep,
4925            Invalid::Other,
4926        ];
4927        for kind in invalid {
4928            match kind {
4929                Invalid::BadGzip
4930                | Invalid::NotJson
4931                | Invalid::DuplicateMember
4932                | Invalid::AmbiguousForm
4933                | Invalid::NoForm
4934                | Invalid::MissingUuid
4935                | Invalid::BadUuid
4936                | Invalid::MissingCreatedAt
4937                | Invalid::BadPayload
4938                | Invalid::UuidMismatch
4939                | Invalid::BadCritical
4940                | Invalid::UnsupportedExtension
4941                | Invalid::TooDeep
4942                | Invalid::Other => all.push(Problem::Invalid(kind)),
4943            }
4944        }
4945        let store = [
4946            StoreError::Conflict,
4947            StoreError::Gone,
4948            StoreError::NotFound,
4949            StoreError::NotDeleted,
4950            StoreError::PreconditionFailed,
4951            StoreError::Immutable,
4952            StoreError::LimitReached,
4953            StoreError::Damaged,
4954            StoreError::AuditFailed,
4955            StoreError::Io,
4956        ];
4957        for error in store {
4958            match error {
4959                StoreError::Conflict
4960                | StoreError::Gone
4961                | StoreError::NotFound
4962                | StoreError::NotDeleted
4963                | StoreError::PreconditionFailed
4964                | StoreError::Immutable
4965                | StoreError::LimitReached
4966                | StoreError::Damaged
4967                | StoreError::AuditFailed
4968                | StoreError::Io => all.push(Problem::Store(error)),
4969            }
4970        }
4971        let tenant = [
4972            TenantError::NotFound,
4973            TenantError::NameTaken,
4974            TenantError::BadName,
4975            TenantError::HoldsVcons,
4976            TenantError::UnderHold,
4977            TenantError::LimitReached,
4978            TenantError::AuditFailed,
4979            TenantError::Closing,
4980            TenantError::CliOnly,
4981            TenantError::BadScopes,
4982            TenantError::WrongKey,
4983            TenantError::Io,
4984        ];
4985        for error in tenant {
4986            match error {
4987                TenantError::NotFound
4988                | TenantError::NameTaken
4989                | TenantError::BadName
4990                | TenantError::HoldsVcons
4991                | TenantError::UnderHold
4992                | TenantError::LimitReached
4993                | TenantError::AuditFailed
4994                | TenantError::Closing
4995                | TenantError::CliOnly
4996                | TenantError::BadScopes
4997                | TenantError::WrongKey
4998                | TenantError::Io => all.push(Problem::Tenant(error)),
4999            }
5000        }
5001        all
5002    }
5003
5004    fn pages() -> PathBuf {
5005        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../website/content/problems")
5006    }
5007
5008    #[test]
5009    fn every_problem_type_has_its_page_with_its_status_and_title() {
5010        for problem in every_problem() {
5011            let (name, title) = problem.name();
5012            let page = pages().join(format!("{name}.md"));
5013            let text = std::fs::read_to_string(&page)
5014                .unwrap_or_else(|e| panic!("{problem:?}: {}: {e}", page.display()));
5015            let status = problem.status().as_u16();
5016            for line in [
5017                format!("title = \"{name}\""),
5018                format!("status = {status}"),
5019                format!("problem_title = \"{title}\""),
5020            ] {
5021                assert!(
5022                    text.lines().any(|l| l == line),
5023                    "{}: no line `{line}`",
5024                    page.display()
5025                );
5026            }
5027        }
5028    }
5029
5030    /// No page outlives its problem type.
5031    #[test]
5032    fn every_problem_page_is_a_problem_type() {
5033        let names: BTreeSet<String> = every_problem()
5034            .into_iter()
5035            .map(|p| p.name().0.to_owned())
5036            .collect();
5037        let pages: BTreeSet<String> = std::fs::read_dir(pages())
5038            .expect("the problem pages")
5039            .map(|e| e.expect("entry").file_name().to_string_lossy().into_owned())
5040            .filter(|f| f != "_index.md")
5041            .map(|f| f.strip_suffix(".md").unwrap_or(&f).to_owned())
5042            .collect();
5043        assert_eq!(pages, names);
5044    }
5045}
5046
5047/// Reading the description, for the tests that hold it to the code.
5048#[cfg(test)]
5049mod description {
5050    use std::sync::OnceLock;
5051
5052    use serde_json::Value;
5053
5054    fn parsed() -> &'static Value {
5055        static PARSED: OnceLock<Value> = OnceLock::new();
5056        PARSED.get_or_init(|| serde_json::from_str(super::OPENAPI).expect("openapi.json is JSON"))
5057    }
5058
5059    /// Whether the description lists `status` for `method` on `route`, a
5060    /// route template as the router writes it. The router's `{*name}`, the
5061    /// rest of a path, is OpenAPI's `{name}`.
5062    pub(super) fn lists(method: &str, route: &str, status: u16) -> bool {
5063        let path = route.replace("{*", "{");
5064        parsed()["paths"][path.as_str()][method.to_ascii_lowercase()]["responses"]
5065            .get(status.to_string())
5066            .is_some()
5067    }
5068
5069    /// Whether the operation needs a key: it does unless it says
5070    /// `security: []`.
5071    pub(super) fn keyed(method: &str, path: &str) -> bool {
5072        parsed()["paths"][path][method.to_ascii_lowercase()]["security"]
5073            .as_array()
5074            .is_none_or(|s| !s.is_empty())
5075    }
5076
5077    /// Every path and its methods, as the description names them.
5078    pub(super) fn operations() -> Vec<(String, String)> {
5079        let mut all = Vec::new();
5080        for (path, item) in parsed()["paths"].as_object().expect("paths") {
5081            for method in item.as_object().expect("a path item").keys() {
5082                if ["get", "put", "post", "delete", "patch"].contains(&method.as_str()) {
5083                    all.push((method.to_ascii_uppercase(), path.clone()));
5084                }
5085            }
5086        }
5087        all
5088    }
5089
5090    pub(super) fn document() -> &'static Value {
5091        parsed()
5092    }
5093}
5094
5095/// The description against the code: the published copy, the version, the
5096/// problem types and every route and method. Statuses are checked on every
5097/// answer the unit tests produce, in `log_request`.
5098#[cfg(test)]
5099mod openapi_description {
5100    use std::collections::BTreeSet;
5101
5102    use reqwest::Method;
5103
5104    use super::description::{document, keyed, lists, operations};
5105    use super::site_pages::every_problem;
5106    use super::tests::pipeline::server;
5107    use super::*;
5108
5109    #[test]
5110    fn the_website_publishes_the_compiled_description() {
5111        let published = std::fs::read_to_string(format!(
5112            "{}/../../website/static/openapi.json",
5113            env!("CARGO_MANIFEST_DIR")
5114        ))
5115        .expect("the website's copy");
5116        assert!(published == OPENAPI, "website/static/openapi.json differs");
5117    }
5118
5119    #[test]
5120    fn it_is_openapi_3_1_for_this_version() {
5121        let doc = document();
5122        assert!(
5123            doc["openapi"]
5124                .as_str()
5125                .is_some_and(|v| v.starts_with("3.1.")),
5126            "{}",
5127            doc["openapi"]
5128        );
5129        assert_eq!(doc["info"]["version"], env!("CARGO_PKG_VERSION"));
5130        assert!(doc["info"]["title"].as_str().is_some_and(|t| !t.is_empty()));
5131    }
5132
5133    #[test]
5134    fn its_problem_types_are_the_ones_the_api_answers() {
5135        let described: BTreeSet<String> =
5136            document()["components"]["schemas"]["Problem"]["properties"]["type"]["enum"]
5137                .as_array()
5138                .expect("the Problem type list")
5139                .iter()
5140                .map(|t| t.as_str().expect("a URL").to_owned())
5141                .collect();
5142        let answered: BTreeSet<String> = every_problem()
5143            .into_iter()
5144            .map(|p| format!("https://vcon-server.com/problems/{}", p.name().0))
5145            .collect();
5146        assert_eq!(described, answered);
5147    }
5148
5149    /// A path template with sample values, so it reaches its route.
5150    fn concrete(path: &str) -> String {
5151        path.replace("{uuid}", "01a07da8-c2bb-83e5-b9a2-279e0d16bc46")
5152            .replace("{version}", "00112233445566778899aabbccddeeff")
5153            .replace("{id}", "00112233445566778899aabbccddeeff")
5154            .replace("{other}", "no-such-file")
5155    }
5156
5157    /// Every operation the description names is routed (one that needs a
5158    /// key asks for it; one that needs none answers a status it lists), and
5159    /// every other method on its paths is not (405).
5160    #[tokio::test]
5161    async fn every_described_route_is_routed_and_no_other_method() {
5162        let s = server().await;
5163        let described: BTreeSet<(String, String)> = operations().into_iter().collect();
5164        assert!(!described.is_empty());
5165        let paths: BTreeSet<&String> = described.iter().map(|(_, p)| p).collect();
5166        for path in paths {
5167            for method in ["GET", "POST", "PUT", "PATCH", "DELETE"] {
5168                // Not following redirects, so each answer is the route's own.
5169                let answer = s
5170                    .client_not_following()
5171                    .request(
5172                        Method::from_bytes(method.as_bytes()).expect("a method"),
5173                        s.url(&concrete(path)),
5174                    )
5175                    .send()
5176                    .await
5177                    .expect("an answer");
5178                let status = answer.status().as_u16();
5179                if !described.contains(&(method.to_owned(), path.clone())) {
5180                    assert_eq!(status, 405, "{method} {path}");
5181                } else if keyed(method, path) {
5182                    assert_eq!(status, 401, "{method} {path}");
5183                } else {
5184                    assert!(lists(method, path, status), "{method} {path}: {status}");
5185                }
5186            }
5187        }
5188    }
5189}